diff --git a/EC2_MONITORING.md b/EC2_MONITORING.md new file mode 100644 index 000000000..a0c5e612e --- /dev/null +++ b/EC2_MONITORING.md @@ -0,0 +1,89 @@ +# EC2 Provisioning Monitoring + +This feature provides real-time monitoring for AWS EC2 node provisioning issues by integrating with Snowhouse database using JDBC. + +## Overview + +The monitoring system captures detailed information about EC2 provisioning attempts and sends them to a Snowhouse database for real-time analysis and alerting. This helps identify provisioning issues faster than relying on application logs and CloudTrail. + +## Data Captured + +For each provisioning attempt, the following information is recorded: + +- **region**: AWS region where provisioning was attempted +- **availability_zone**: Specific AZ within the region +- **request_id**: Unique identifier for the provisioning request +- **requested_instance_type**: EC2 instance type requested (e.g., m5.large) +- **requested_max_count**: Maximum number of instances requested +- **requested_min_count**: Minimum number of instances requested +- **provisioned_instances_count**: Actual number of instances provisioned +- **controller_name**: Name of the Jenkins controller +- **timestamp**: When the event occurred +- **phase**: Event phase (REQUEST, SUCCESS, FAILURE, REQUEST_FALLBACK, SUCCESS_FALLBACK) +- **error_message**: Error details if provisioning failed +- **jenkins_url**: URL of the Jenkins instance + +## Database Schema + +The monitoring system creates the following table in Snowhouse: + +```sql +CREATE TABLE IF NOT EXISTS EC2_PROVISIONING_EVENTS ( + ID NUMBER AUTOINCREMENT, + CREATE_TIME TIMESTAMP_NTZ, + REGION VARCHAR(50), + AVAILABILITY_ZONE VARCHAR(50), + REQUEST_ID VARCHAR(100), + REQUESTED_INSTANCE_TYPE VARCHAR(50), + REQUESTED_MAX_COUNT NUMBER, + REQUESTED_MIN_COUNT NUMBER, + PROVISIONED_INSTANCES_COUNT NUMBER, + CONTROLLER_NAME VARCHAR(200), + PHASE VARCHAR(50), + ERROR_MESSAGE VARCHAR(2000), + JENKINS_URL VARCHAR(500), + EVENT_DATA VARIANT, + PRIMARY KEY (ID) +); +``` + +## Configuration + +1. **Install Database Plugin**: The monitoring requires the Jenkins Database plugin to be installed. + +2. **Configure Snowhouse Database**: In Jenkins system configuration, add a new Snowflake database connection with: + - Account Name: Your Snowflake account + - Database: Target database name + - Warehouse: Snowflake warehouse to use + - Credentials: Username/password credentials for Snowflake + - Timeouts: Network, query, and login timeouts + +3. **Set as Global Database**: Configure the Snowflake connection as the global database in Jenkins. + +## Event Flow + +1. **Provisioning Request**: When Jenkins attempts to provision EC2 instances, a "REQUEST" event is recorded +2. **Success/Failure**: Based on the AWS API response, either "SUCCESS" or "FAILURE" events are recorded +3. **Fallback Scenarios**: For spot instances that fall back to on-demand, additional "REQUEST_FALLBACK" and "SUCCESS_FALLBACK" events are recorded +4. **Batch Processing**: Events are queued and sent to Snowhouse in batches every 30 seconds + +## Monitoring Points + +The system monitors both: + +- **On-demand instances**: Direct EC2 instance provisioning +- **Spot instances**: Spot instance requests and their fallback scenarios + +## Benefits + +- **Real-time alerting**: Immediate visibility into provisioning issues +- **Trend analysis**: Historical data for capacity planning +- **Failure investigation**: Detailed error information for troubleshooting +- **Performance monitoring**: Track provisioning success rates and response times + +## Implementation Details + +- **Non-blocking**: Event recording doesn't impact provisioning performance +- **Fault-tolerant**: Monitoring failures don't affect EC2 provisioning +- **Scalable**: Batch processing handles high-volume environments +- **Configurable**: Database connection is configurable through Jenkins UI \ No newline at end of file diff --git a/README.md b/README.md index c8149ec0e..003af1a92 100644 --- a/README.md +++ b/README.md @@ -364,7 +364,7 @@ def slaveTemplateUsEast1Parameters = [ ] def EC2CloudParameters = [ - name: 'MyCompany', + cloudName: 'MyCompany', credentialsId: 'jenkins-aws-key', instanceCapStr: '2', privateKey: '''-----BEGIN RSA PRIVATE KEY----- @@ -465,7 +465,7 @@ SlaveTemplate slaveTemplateUsEast1 = new SlaveTemplate( // https://javadoc.jenkins.io/plugin/ec2/hudson/plugins/ec2/EC2Cloud.html EC2Cloud ec2Cloud = new EC2Cloud( - EC2CloudParameters.name, + EC2CloudParameters.cloudName, EC2CloudParameters.useInstanceProfileForCredentials, EC2CloudParameters.credentialsId, EC2CloudParameters.region, diff --git a/jenkins_build.sh b/jenkins_build.sh new file mode 100755 index 000000000..a706da48b --- /dev/null +++ b/jenkins_build.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# auth setup +eval $(sf artifact maven auth) + +ENV="sandbox" + +if [ -n "${JENKINS_ENVIRONMENT}" ]; then + ENV="${JENKINS_ENVIRONMENT}" +fi + +# Generate version depending on the env +generate_version() { + local git_sha=$(git rev-parse --short=7 HEAD) + + local timestamp=$(date +"%Y%m%d-%H%M%S") + + local version="${timestamp}.${git_sha}" + + # build num not set during sandbox dev generally + if [ -n "${BUILD_NUMBER}" ]; then + version="${version}.${BUILD_NUMBER}" + fi + + echo "${version}" +} + +DYNAMIC_VERSION=$(generate_version) +echo "Generated version: ${DYNAMIC_VERSION}" + +# build & deploy +mvn clean deploy -P "${ENV}" -Dchangelist="${DYNAMIC_VERSION}" diff --git a/pom.xml b/pom.xml index 8aada6c44..73994ea88 100644 --- a/pom.xml +++ b/pom.xml @@ -72,13 +72,26 @@ THE SOFTWARE. https://github.com/${gitHubRepo} + + + snowflake-jenkins-plugins + Snowflake Jenkins Plugins Repository + ${deploy.repo.url} + + + snowflake-jenkins-plugins + Snowflake Jenkins Plugins Repository + ${deploy.repo.url} + + + - 999999-SNAPSHOT + + 20250819-nthirumoorthy-aws-monitoring-info 2.479 ${jenkins.baseline}.3 jenkinsci/${project.artifactId}-plugin - 1885 false @@ -158,6 +171,20 @@ THE SOFTWARE. org.jenkins-ci.plugins.workflow workflow-step-api + + net.snowflake + snowflake-jdbc + 3.10.1 + + + org.jenkins-ci.plugins + database + 274.vea_2e859b_2661 + + + io.jenkins.plugins + json-api + io.jenkins configuration-as-code @@ -208,16 +235,56 @@ THE SOFTWARE. + + + + true + + + false + + central + Maven Central + https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-maven-virtual + + + + true + + + true + repo.jenkins-ci.org - https://repo.jenkins-ci.org/public/ + Jenkins CI + https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-jenkins-ci-virtual + + + true + + + false + + central + Maven Central Plugins + https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-maven-virtual + + + + + true + + + true + repo.jenkins-ci.org - https://repo.jenkins-ci.org/public/ + Jenkins CI Plugins + https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-jenkins-ci-virtual @@ -233,4 +300,25 @@ THE SOFTWARE. + + + + prod + + https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/internal-production-maven-jenkins_plugins-local/ + + + + dev + + https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/internal-development-maven-jenkins_plugins-local/ + + + + sandbox + + https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/internal-sandbox-maven-jenkins_plugins-local/ + + + diff --git a/pom.xml.bak b/pom.xml.bak new file mode 100644 index 000000000..ad1c3161f --- /dev/null +++ b/pom.xml.bak @@ -0,0 +1,324 @@ + + + + 4.0.0 + + org.jenkins-ci.plugins + plugin + 5.18 + + + + ec2 + ${changelist} + hpi + Amazon EC2 plugin + This is a Jenkins plugin to support ephemeral Jenkins agents on Amazon EC2 or other EC2-compatible clouds + https://github.com/jenkinsci/${project.artifactId}-plugin + + + + The MIT License + https://opensource.org/licenses/MIT + repo + + + + + + + thoulen + F Manfred Furuholen + fabrizio.manfredi@gmail.com + + + julienduchesne + Julien Duchesne + julienduchesne@live.com + + + raihaan + Raihaan Shouhell + raihaanhimself@gmail.com + + + + + scm:git:https://github.com/${gitHubRepo}.git + scm:git:git@github.com:${gitHubRepo}.git + ${scmTag} + https://github.com/${gitHubRepo} + + + + + snowflake-jenkins-plugins + Snowflake Jenkins Plugins Repository + ${deploy.repo.url} + + + snowflake-jenkins-plugins + Snowflake Jenkins Plugins Repository + ${deploy.repo.url} + + + + + + 999999-SNAPSHOT + + 2.479 + ${jenkins.baseline}.3 + jenkinsci/${project.artifactId}-plugin + false + + + + + + io.jenkins.tools.bom + bom-${jenkins.baseline}.x + 5054.v620b_5d2b_d5e6 + pom + import + + + + + + + com.hierynomus + smbj + 0.14.0 + + + org.bouncycastle + bcprov-jdk18on + + + org.slf4j + slf4j-api + + + + + io.jenkins.plugins.aws-java-sdk2 + aws-java-sdk2-core + + + io.jenkins.plugins.aws-java-sdk2 + aws-java-sdk2-ec2 + + + io.jenkins.plugins.mina-sshd-api + mina-sshd-api-core + + + io.jenkins.plugins.mina-sshd-api + mina-sshd-api-scp + + + org.jenkins-ci.plugins + apache-httpcomponents-client-4-api + + + org.jenkins-ci.plugins + aws-credentials + + + org.jenkins-ci.plugins + bouncycastle-api + + + org.jenkins-ci.plugins + command-launcher + + + org.jenkins-ci.plugins + credentials + + + org.jenkins-ci.plugins + node-iterator-api + + + org.jenkins-ci.plugins + ssh-credentials + + + org.jenkins-ci.plugins.workflow + workflow-step-api + + + net.snowflake + snowflake-jdbc + 3.10.1 + + + org.jenkins-ci.plugins + database + 274.vea_2e859b_2661 + + + io.jenkins.plugins + json-api + + + io.jenkins + configuration-as-code + test + + + io.jenkins.configuration-as-code + test-harness + test + + + org.jenkins-ci.plugins.workflow + workflow-api + test + + + org.jenkins-ci.plugins.workflow + workflow-cps + test + + + org.jenkins-ci.plugins.workflow + workflow-durable-task-step + test + + + org.jenkins-ci.plugins.workflow + workflow-job + test + + + org.junit-pioneer + junit-pioneer + 2.3.0 + test + + + org.mockito + mockito-junit-jupiter + test + + + org.testcontainers + junit-jupiter + 1.21.3 + test + + + + + + + + true + + + false + + central + Maven Central + https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-maven-virtual + + + + + true + + + true + + repo.jenkins-ci.org + Jenkins CI + https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-jenkins-ci-virtual + + + + + + + + true + + + false + + central + Maven Central Plugins + https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-maven-virtual + + + + + true + + + true + + repo.jenkins-ci.org + Jenkins CI Plugins + https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-jenkins-ci-virtual + + + + + + + org.jenkins-ci.tools + maven-hpi-plugin + true + + 1.45 + + + + + + + + prod + + https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/internal-production-maven-jenkins_plugins-local/ + + + + dev + + https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/internal-development-maven-jenkins_plugins-local/ + + + + sandbox + + https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/internal-sandbox-maven-jenkins_plugins-local/ + + + + diff --git a/set-version.sh b/set-version.sh new file mode 100755 index 000000000..e48c87105 --- /dev/null +++ b/set-version.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Get current branch name +BRANCH=$(git branch --show-current) + +# Get current date in YYYYMMDD format +DATE=$(date +%Y%m%d) + +# Set version based on branch +if [[ "$BRANCH" == "main" || "$BRANCH" == "master" ]]; then + VERSION="$DATE" +else + # Replace any special characters in branch name with hyphens + CLEAN_BRANCH=$(echo "$BRANCH" | sed 's/[^a-zA-Z0-9]/-/g') + VERSION="$DATE-$CLEAN_BRANCH" +fi + +echo "Setting version to: $VERSION" + +# Update the changelist property in pom.xml +sed -i.bak "s/.*<\/changelist>/$VERSION<\/changelist>/" pom.xml + +echo "Version updated in pom.xml" \ No newline at end of file diff --git a/settings.xml b/settings.xml new file mode 100644 index 000000000..3e0766ea1 --- /dev/null +++ b/settings.xml @@ -0,0 +1,30 @@ + + + + + + + artifactory-central + Artifactory Central Mirror + https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-maven-virtual + central + + + artifactory-jenkins + Artifactory Jenkins Mirror + https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-jenkins-ci-virtual + repo.jenkins-ci.org + + + + artifactory-everything + Block External Repositories + https://artifactory.ci1.us-west-2.aws-dev.app.snowflake.com/artifactory/development-maven-virtual + external:*,!repo.jenkins-ci.org,!central + + + + diff --git a/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java b/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java index 31083accd..8872b2d0d 100644 --- a/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java @@ -31,7 +31,7 @@ @Deprecated public class AmazonEC2Cloud extends EC2Cloud { public AmazonEC2Cloud( - String name, + String cloudName, boolean useInstanceProfileForCredentials, String credentialsId, String region, @@ -42,7 +42,7 @@ public AmazonEC2Cloud( String roleArn, String roleSessionName) { super( - name, + cloudName, // now matches the constructor parameter name useInstanceProfileForCredentials, credentialsId, region, @@ -55,7 +55,7 @@ public AmazonEC2Cloud( } public AmazonEC2Cloud( - String name, + String cloudName, boolean useInstanceProfileForCredentials, String credentialsId, String region, @@ -65,7 +65,7 @@ public AmazonEC2Cloud( String roleArn, String roleSessionName) { super( - name, + cloudName, // now matches the constructor parameter name useInstanceProfileForCredentials, credentialsId, region, diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index f7d179c6f..fb83f71ad 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -217,7 +217,7 @@ public class EC2Cloud extends Cloud { @DataBoundConstructor public EC2Cloud( - String name, + String cloudName, boolean useInstanceProfileForCredentials, String credentialsId, String region, @@ -227,7 +227,7 @@ public EC2Cloud( List templates, String roleArn, String roleSessionName) { - super(name); + super(cloudName != null && !cloudName.trim().isEmpty() ? cloudName : "ec2-cloud-" + System.currentTimeMillis()); this.useInstanceProfileForCredentials = useInstanceProfileForCredentials; this.roleArn = roleArn; this.roleSessionName = roleSessionName; @@ -250,6 +250,8 @@ public EC2Cloud( readResolve(); // set parents } + + @Deprecated public EC2Cloud( String name, @@ -262,7 +264,7 @@ public EC2Cloud( String roleArn, String roleSessionName) { this( - name, + name, // pass name as cloudName for backward compatibility useInstanceProfileForCredentials, credentialsId, region, @@ -285,11 +287,11 @@ protected EC2Cloud( String roleArn, String roleSessionName) { this( - id, + id, // pass id as cloudName for backward compatibility useInstanceProfileForCredentials, credentialsId, - privateKey, null, + privateKey, null, instanceCapStr, templates, @@ -315,13 +317,22 @@ public EC2PrivateKey resolvePrivateKey() { } /** - * @deprecated Use public field "name" instead. + * Getter for cloudName to support Configuration as Code (CasC). + * This is the preferred field name for EC2 cloud configurations. */ - @Deprecated public String getCloudName() { return name; } + /** + * Setter for cloudName to support Configuration as Code (CasC). + * This is the preferred field name for EC2 cloud configurations. + */ + @DataBoundSetter + public void setCloudName(String cloudName) { + this.name = cloudName; + } + public String getRegion() { if (region == null) { region = DEFAULT_EC2_HOST; // Backward compatibility diff --git a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java index dbd962f65..4925616b6 100644 --- a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java +++ b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java @@ -42,6 +42,8 @@ import hudson.plugins.ec2.util.KeyPair; import hudson.plugins.ec2.util.MinimumInstanceChecker; import hudson.plugins.ec2.util.MinimumNumberOfInstancesTimeRangeConfig; +import hudson.plugins.ec2.monitoring.EC2ProvisioningMonitor; +import hudson.plugins.ec2.monitoring.ProvisioningEvent; import hudson.security.Permission; import hudson.slaves.NodeProperty; import hudson.slaves.NodePropertyDescriptor; @@ -128,6 +130,7 @@ import software.amazon.awssdk.services.ec2.model.ResourceType; import software.amazon.awssdk.services.ec2.model.RunInstancesMonitoringEnabled; import software.amazon.awssdk.services.ec2.model.RunInstancesRequest; +import software.amazon.awssdk.services.ec2.model.RunInstancesResponse; import software.amazon.awssdk.services.ec2.model.SecurityGroup; import software.amazon.awssdk.services.ec2.model.ShutdownBehavior; import software.amazon.awssdk.services.ec2.model.SpotInstanceRequest; @@ -1940,7 +1943,7 @@ HashMap> makeRunInstancesRequestAndFilters( .imageId(image.imageId()) .minCount(1) .maxCount(number) - .instanceType(type) + .instanceType(InstanceType.fromValue(type)) .ebsOptimized(ebsOptimized) .monitoring(RunInstancesMonitoringEnabled.builder() .enabled(monitoring) @@ -2188,27 +2191,55 @@ private List provisionOndemand( instanceMarketOptionsRequestBuilder.spotOptions(spotOptions); } riRequestBuilder.instanceMarketOptions(instanceMarketOptionsRequestBuilder.build()); + RunInstancesRequest request = riRequestBuilder.build(); try { - newInstances = new ArrayList<>( - ec2.runInstances(riRequestBuilder.build()).instances()); + // Record provisioning attempt + recordProvisioningEvent(request, "REQUEST", null, 0); + + RunInstancesResponse response = ec2.runInstances(request); + newInstances = new ArrayList<>(response.instances()); + + // Record successful provisioning + recordProvisioningEvent(request, "SUCCESS", null, newInstances.size()); } catch (Ec2Exception e) { + // Record failed provisioning + recordProvisioningEvent(request, "FAILURE", e.getMessage(), 0); + if (fallbackSpotToOndemand && "InsufficientInstanceCapacity" .equals(e.awsErrorDetails().errorCode())) { logProvisionInfo( "There is no spot capacity available matching your request, falling back to on-demand instance."); riRequestBuilder.instanceMarketOptions(instanceMarketOptionsRequestBuilder.build()); - newInstances = new ArrayList<>( - ec2.runInstances(riRequestBuilder.build()).instances()); + + RunInstancesRequest fallbackRequest = riRequestBuilder.build(); + // Record fallback attempt + recordProvisioningEvent(fallbackRequest, "REQUEST_FALLBACK", null, 0); + + RunInstancesResponse fallbackResponse = ec2.runInstances(fallbackRequest); + newInstances = new ArrayList<>(fallbackResponse.instances()); + + // Record successful fallback provisioning + recordProvisioningEvent(fallbackRequest, "SUCCESS_FALLBACK", null, newInstances.size()); } else { throw e; } } } else { + RunInstancesRequest request = riRequestBuilder.build(); try { - newInstances = new ArrayList<>( - ec2.runInstances(riRequestBuilder.build()).instances()); + // Record provisioning attempt + recordProvisioningEvent(request, "REQUEST", null, 0); + + RunInstancesResponse response = ec2.runInstances(request); + newInstances = new ArrayList<>(response.instances()); + + // Record successful provisioning + recordProvisioningEvent(request, "SUCCESS", null, newInstances.size()); } catch (Ec2Exception e) { + // Record failed provisioning + recordProvisioningEvent(request, "FAILURE", e.getMessage(), 0); + logProvisionInfo("Jenkins attempted to reserve " + riRequest.maxCount() + " instances and received this EC2 exception: " + e.getMessage()); @@ -2463,7 +2494,7 @@ private List provisionSpot(Image image, int number, EnumSet provisionSpot(Image image, int number, EnumSet provisionSpot(Image image, int number, EnumSet envVars = hudson.EnvVars.masterEnvVars; + if (envVars != null) { + controllerName = envVars.get("JENKINS_BASE_HOSTNAME_SHORT"); + if (controllerName != null && !controllerName.trim().isEmpty()) { + return controllerName.trim(); + } + } + } catch (Exception e) { + // Ignore if EnvVars not available + } + + // Try other environment variables + controllerName = System.getenv("HOSTNAME"); + if (controllerName != null && !controllerName.trim().isEmpty()) { + return controllerName.trim(); + } + + controllerName = System.getenv("COMPUTERNAME"); + if (controllerName != null && !controllerName.trim().isEmpty()) { + return controllerName.trim(); + } + + // Try system properties + try { + controllerName = System.getProperty("jenkins.hostname"); + if (controllerName != null && !controllerName.trim().isEmpty()) { + return controllerName.trim(); + } + } catch (Exception e) { + // Ignore security exceptions + } + + // Try to extract from Jenkins URL + try { + String jenkinsUrl = Jenkins.get().getRootUrl(); + if (jenkinsUrl != null) { + java.net.URL url = new java.net.URL(jenkinsUrl); + String host = url.getHost(); + if (host != null && !host.trim().isEmpty()) { + // Remove domain suffix if present + int dotIndex = host.indexOf('.'); + if (dotIndex > 0) { + host = host.substring(0, dotIndex); + } + return host.trim(); + } + } + } catch (Exception e) { + // Ignore URL parsing exceptions + } + + // Try Java system hostname + try { + controllerName = java.net.InetAddress.getLocalHost().getHostName(); + if (controllerName != null && !controllerName.trim().isEmpty()) { + // Remove domain suffix if present + int dotIndex = controllerName.indexOf('.'); + if (dotIndex > 0) { + controllerName = controllerName.substring(0, dotIndex); + } + return controllerName.trim(); + } + } catch (Exception e) { + // Ignore network exceptions + } + + // Final fallback - also log what env vars we do have for debugging + LOGGER.log(Level.WARNING, "Could not determine controller name, using fallback: jenkins-controller"); + LOGGER.log(Level.FINE, "Available env vars: HOSTNAME=" + System.getenv("HOSTNAME") + + ", COMPUTERNAME=" + System.getenv("COMPUTERNAME")); + return "jenkins-controller"; + } } diff --git a/src/main/java/hudson/plugins/ec2/monitoring/EC2ProvisioningMonitor.java b/src/main/java/hudson/plugins/ec2/monitoring/EC2ProvisioningMonitor.java new file mode 100644 index 000000000..49f4c01fb --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/monitoring/EC2ProvisioningMonitor.java @@ -0,0 +1,161 @@ +package hudson.plugins.ec2.monitoring; + +import hudson.Extension; +import hudson.triggers.SafeTimerTask; +import jenkins.model.Jenkins; +import jenkins.util.Timer; +import org.jenkinsci.plugins.database.Database; +import org.jenkinsci.plugins.database.GlobalDatabaseConfiguration; +import org.json.JSONObject; +import net.snowflake.client.jdbc.SnowflakeConnection; + +import java.io.ByteArrayInputStream; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.time.Instant; +import java.util.HashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Monitor for EC2 provisioning events that sends data to Snowhouse database. + * Implements event queuing and batch processing similar to the Snowflake Jenkins connector. + */ +@Extension +public class EC2ProvisioningMonitor { + private static final Logger LOG = Logger.getLogger(EC2ProvisioningMonitor.class.getName()); + + private static final Level LOG_LEVEL = Level.FINE; + private static final ConcurrentLinkedQueue eventQueue = new ConcurrentLinkedQueue<>(); + + static { + // Schedule periodic sending of events to Snowhouse every 30 seconds + Timer.get().scheduleAtFixedRate(new SafeTimerTask() { + @Override + protected void doRun() throws Exception { + if (eventQueue.size() > 0) { + EC2ProvisioningMonitor.sendQueue(); + } + } + }, TimeUnit.MINUTES.toMillis(1), TimeUnit.SECONDS.toMillis(30), TimeUnit.MILLISECONDS); + } + + /** + * Add a provisioning event to the queue for batch processing. + */ + public static void recordProvisioningEvent(ProvisioningEvent event) { + HashMap eventMap = new HashMap<>(); + eventMap.put("timestamp", event.getTimestamp()); + eventMap.put("region", event.getRegion()); + eventMap.put("availability_zone", event.getAvailabilityZone()); + eventMap.put("request_id", event.getRequestId()); + eventMap.put("requested_instance_type", event.getRequestedInstanceType()); + eventMap.put("requested_max_count", event.getRequestedMaxCount()); + eventMap.put("requested_min_count", event.getRequestedMinCount()); + eventMap.put("provisioned_instances_count", event.getProvisionedInstancesCount()); + eventMap.put("controller_name", event.getControllerName()); + eventMap.put("cloud_name", event.getCloudName()); + eventMap.put("phase", event.getPhase()); + eventMap.put("error_message", event.getErrorMessage()); + eventMap.put("jenkins_url", event.getJenkinsUrl()); + + JSONObject jsonMap = new JSONObject(eventMap); + enQueue(jsonMap.toString()); + } + + /** + * Add an event to the queue. + */ + private static boolean enQueue(String queueItem) { + boolean retVal = eventQueue.add(queueItem); + LOG.log(LOG_LEVEL, "EC2 Provisioning event queue size: " + eventQueue.size()); + return retVal; + } + + /** + * Send all queued events to Snowhouse database. + */ + static synchronized void sendQueue() throws Exception { + if (eventQueue.size() == 0) { + return; + } + + Database db = GlobalDatabaseConfiguration.get().getDatabase(); + if (db == null) { + LOG.log(Level.WARNING, "EC2ProvisioningMonitor failed - no database configured. " + + "Discarding " + eventQueue.size() + " events"); + // Drop the existing queue to prevent it from growing forever + eventQueue.clear(); + return; + } + + Connection con = null; + PreparedStatement copyStatement = null; + try { + long startTime = System.currentTimeMillis(); + ConcurrentLinkedQueue pushQueue = new ConcurrentLinkedQueue<>(eventQueue); + eventQueue.clear(); + + LOG.log(Level.INFO, pushQueue.size() + " EC2 provisioning events found in queue"); + LOG.log(LOG_LEVEL, "Fetching database connection"); + + con = db.getDataSource().getConnection(); + LOG.log(LOG_LEVEL, "Database connection fetched"); + con.createStatement().execute("USE SCHEMA PUBLIC;"); + + String fileName = Jenkins.get().getRootUrl().replaceAll( + "https?://", "").replaceAll("/.*", "").replaceAll(":.*", "") + + "_ec2_provisioning.json"; + + String eventsString = String.join("\n", pushQueue.toArray(new String[0])); + LOG.log(Level.FINER, "Events being sent: " + eventsString); + + // Upload events to Snowflake automatic table stage + con.unwrap(SnowflakeConnection.class).uploadStream("@%EC2_PROVISIONING_EVENTS", + "ec2_provisioning", + new ByteArrayInputStream(eventsString.getBytes()), + fileName, true); + + // Copy data from automatic table stage to table (using existing schema) + String copySql = "COPY INTO EC2_PROVISIONING_EVENTS " + + "(CREATE_TIME, REGION, AVAILABILITY_ZONE, REQUEST_ID, REQUESTED_INSTANCE_TYPE, " + + "REQUESTED_MAX_COUNT, REQUESTED_MIN_COUNT, PROVISIONED_INSTANCES_COUNT, " + + "CONTROLLER_NAME, PHASE, ERROR_MESSAGE, JENKINS_URL, EVENT_DATA) " + + "from (select $1:timestamp, $1:region, $1:availability_zone, $1:request_id, " + + "$1:requested_instance_type, $1:requested_max_count, $1:requested_min_count, " + + "$1:provisioned_instances_count, $1:controller_name, $1:phase, $1:error_message, " + + "$1:jenkins_url, $1 from @%EC2_PROVISIONING_EVENTS/ec2_provisioning/" + fileName + ".gz) " + + "file_format=(type='json' strip_outer_array=true) " + + "on_error='continue' FORCE=TRUE purge=true;"; + + LOG.log(LOG_LEVEL, "Executing SQL: " + copySql); + + copyStatement = con.prepareStatement(copySql); + copyStatement.execute(); + long endTime = System.currentTimeMillis(); + LOG.log(Level.INFO, copyStatement.getUpdateCount() + " EC2 provisioning events inserted in " + + (endTime - startTime) + " ms"); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Failed to send EC2 provisioning events to Snowhouse", e); + } finally { + if (copyStatement != null) { + try { + copyStatement.close(); + LOG.log(LOG_LEVEL, "Statement closed"); + } catch (Exception e) { + LOG.log(Level.WARNING, "Failed to close statement", e); + } + } + if (con != null) { + try { + con.close(); + LOG.log(LOG_LEVEL, "Connection closed"); + } catch (Exception e) { + LOG.log(Level.WARNING, "Failed to close connection", e); + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/hudson/plugins/ec2/monitoring/ProvisioningEvent.java b/src/main/java/hudson/plugins/ec2/monitoring/ProvisioningEvent.java new file mode 100644 index 000000000..f4f108598 --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/monitoring/ProvisioningEvent.java @@ -0,0 +1,57 @@ +package hudson.plugins.ec2.monitoring; + +import java.time.Instant; + +/** + * Data model for AWS EC2 provisioning events to be sent to Snowhouse database. + * Contains all the required information for monitoring provisioning issues. + */ +public class ProvisioningEvent { + private final String region; + private final String availabilityZone; + private final String requestId; + private final String requestedInstanceType; + private final int requestedMaxCount; + private final int requestedMinCount; + private final int provisionedInstancesCount; + private final String controllerName; + private final String cloudName; + private final Instant timestamp; + private final String phase; // "REQUEST", "SUCCESS", "FAILURE" + private final String errorMessage; // null if successful + private final String jenkinsUrl; + + public ProvisioningEvent(String region, String availabilityZone, String requestId, + String requestedInstanceType, int requestedMaxCount, int requestedMinCount, + int provisionedInstancesCount, String controllerName, String cloudName, + String phase, String errorMessage, String jenkinsUrl) { + this.region = region; + this.availabilityZone = availabilityZone; + this.requestId = requestId; + this.requestedInstanceType = requestedInstanceType; + this.requestedMaxCount = requestedMaxCount; + this.requestedMinCount = requestedMinCount; + this.provisionedInstancesCount = provisionedInstancesCount; + this.controllerName = controllerName; + this.cloudName = cloudName; + this.phase = phase; + this.errorMessage = errorMessage; + this.jenkinsUrl = jenkinsUrl; + this.timestamp = Instant.now(); + } + + // Getters + public String getRegion() { return region; } + public String getAvailabilityZone() { return availabilityZone; } + public String getRequestId() { return requestId; } + public String getRequestedInstanceType() { return requestedInstanceType; } + public int getRequestedMaxCount() { return requestedMaxCount; } + public int getRequestedMinCount() { return requestedMinCount; } + public int getProvisionedInstancesCount() { return provisionedInstancesCount; } + public String getControllerName() { return controllerName; } + public String getCloudName() { return cloudName; } + public Instant getTimestamp() { return timestamp; } + public String getPhase() { return phase; } + public String getErrorMessage() { return errorMessage; } + public String getJenkinsUrl() { return jenkinsUrl; } +} \ No newline at end of file diff --git a/src/main/java/hudson/plugins/ec2/monitoring/SnowflakeDatabase.java b/src/main/java/hudson/plugins/ec2/monitoring/SnowflakeDatabase.java new file mode 100644 index 000000000..6d6494b87 --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/monitoring/SnowflakeDatabase.java @@ -0,0 +1,172 @@ +package hudson.plugins.ec2.monitoring; + +import hudson.Extension; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; +import hudson.util.Secret; +import org.jenkinsci.plugins.database.BasicDataSource2; +import org.jenkinsci.plugins.database.DatabaseDescriptor; +import org.jenkinsci.plugins.database.Database; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.interceptor.RequirePOST; +import com.snowflake.client.jdbc.SnowflakeDriver; + +import javax.sql.DataSource; +import java.lang.reflect.InvocationTargetException; +import java.sql.*; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +import jenkins.model.Jenkins; + +import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.domains.DomainRequirement; +import hudson.security.ACL; + +/** + * Snowflake database configuration for EC2 provisioning monitoring. + * Based on the snowflake-jenkins-connector implementation. + */ +public class SnowflakeDatabase extends Database { + + private static final Logger LOG = Logger.getLogger(SnowflakeDatabase.class.getName()); + private transient DataSource source; + + public final String accountname; + public final String database; + public final String networktimeout; + public final String querytimeout; + public final String logintimeout; + public final String warehouse; + public final String credentialsId; + + @DataBoundConstructor + public SnowflakeDatabase(String accountname, + String database, + String warehouse, + String credentialsId, + String networktimeout, + String querytimeout, + String logintimeout) { + + this.accountname = accountname; + this.database = database; + this.warehouse = warehouse; + this.credentialsId = credentialsId; + this.networktimeout = networktimeout; + this.querytimeout = querytimeout; + this.logintimeout = logintimeout; + } + + protected Class getDriverClass() { + return SnowflakeDriver.class; + } + + @Override + public synchronized DataSource getDataSource() throws SQLException { + List credentialList = CredentialsProvider.lookupCredentials( + StandardUsernamePasswordCredentials.class, Jenkins.getInstanceOrNull(), ACL.SYSTEM, + Collections.emptyList()); + + StandardUsernamePasswordCredentials credentials = (StandardUsernamePasswordCredentials)CredentialsMatchers.firstOrNull(credentialList, + CredentialsMatchers.allOf(CredentialsMatchers.withId(credentialsId))); + + if (source==null) { + BasicDataSource2 fac = new BasicDataSource2(); + fac.setDriverClass(getDriverClass()); + fac.setUrl(getJdbcUrl()); + fac.setUsername(credentials.getUsername()); + fac.setPassword(Secret.toString(credentials.getPassword())); + fac.setValidationQuery("SELECT 1"); + + source = fac.createDataSource(); + } + return source; + } + + protected String getJdbcUrl() { + String url = "jdbc:snowflake://"+this.accountname+ + ".snowflakecomputing.com/?db="+this.database+ + "&networkTimeout="+this.networktimeout+ + "&queryTimeout="+this.querytimeout+ + "&warehouse="+this.warehouse+ + "&loginTimeout="+this.logintimeout; + LOG.log(Level.FINE, "JDBC URL {0}", url); + return url; + } + + public String fetchJdbcUrl() { + return getJdbcUrl(); + } + + @Extension + public static class DescriptorImpl extends DatabaseDescriptor { + @Override + public String getDisplayName() { + return "Snowflake EC2 Monitoring"; + } + + public FormValidation doValidateSnowflake( + @QueryParameter String accountname, + @QueryParameter String database, + @QueryParameter String warehouse, + @QueryParameter String credentialsId, + @QueryParameter String networktimeout, + @QueryParameter String querytimeout, + @QueryParameter String logintimeout) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { + + DataSource ds; + Connection con = null; + Statement s = null; + try { + Database db = clazz.getConstructor(String.class, + String.class, + String.class, + String.class, + String.class, + String.class, + String.class).newInstance(accountname, + database, + warehouse, + credentialsId, + networktimeout, + querytimeout, + logintimeout); + ds = db.getDataSource(); + con = ds.getConnection(); + s = con.createStatement(); + s.execute("SELECT 1"); + return FormValidation.ok("OK"); + } catch (SQLException e) { + return FormValidation.error(e, "Failed to connect to " + getDisplayName()); + } finally { + try { + if (s != null) + s.close(); + if (con != null) + con.close(); + } catch (Exception e) { + } + } + } + + @RequirePOST + public ListBoxModel doFillCredentialsIdItems() { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + return new StandardListBoxModel() + .withEmptySelection() + .withMatching( + CredentialsMatchers.always(), + CredentialsProvider.lookupCredentials(StandardUsernamePasswordCredentials.class, + Jenkins.get(), + ACL.SYSTEM, + Collections.emptyList())); + } + } +} \ No newline at end of file diff --git a/src/main/java/hudson/plugins/ec2/util/InstanceTypeCompat.java b/src/main/java/hudson/plugins/ec2/util/InstanceTypeCompat.java index 15fbc4da2..de587508c 100644 --- a/src/main/java/hudson/plugins/ec2/util/InstanceTypeCompat.java +++ b/src/main/java/hudson/plugins/ec2/util/InstanceTypeCompat.java @@ -834,6 +834,79 @@ public final class InstanceTypeCompat { map.put("R8gMetal24xl", "r8g.metal-24xl"); map.put("R8gMetal48xl", "r8g.metal-48xl"); map.put("Mac2M1ultraMetal", "mac2-m1ultra.metal"); + map.put("M8gMedium", "m8g.medium"); + map.put("M8gLarge", "m8g.large"); + map.put("M8gXlarge", "m8g.xlarge"); + map.put("M8g2xlarge", "m8g.2xlarge"); + map.put("M8g4xlarge", "m8g.4xlarge"); + map.put("M8g8xlarge", "m8g.8xlarge"); + map.put("M8g12xlarge", "m8g.12xlarge"); + map.put("M8g16xlarge", "m8g.16xlarge"); + map.put("M8g24xlarge", "m8g.24xlarge"); + map.put("M8g48xlarge", "m8g.48xlarge"); + map.put("M8gMetal24xl", "m8g.metal-24xl"); + map.put("M8gMetal48xl", "m8g.metal-48xl"); + map.put("M8gdMedium", "m8gd.medium"); + map.put("M8gdLarge", "m8gd.large"); + map.put("M8gdXlarge", "m8gd.xlarge"); + map.put("M8gd2xlarge", "m8gd.2xlarge"); + map.put("M8gd4xlarge", "m8gd.4xlarge"); + map.put("M8gd8xlarge", "m8gd.8xlarge"); + map.put("M8gd12xlarge", "m8gd.12xlarge"); + map.put("M8gd16xlarge", "m8gd.16xlarge"); + map.put("M8gd24xlarge", "m8gd.24xlarge"); + map.put("M8gd48xlarge", "m8gd.48xlarge"); + map.put("M8gdMetal24xl", "m8gd.metal-24xl"); + map.put("M8gdMetal48xl", "m8gd.metal-48xl"); + // R8gd instances (Graviton 4 memory-optimized with local storage) + map.put("R8gdMedium", "r8gd.medium"); + map.put("R8gdLarge", "r8gd.large"); + map.put("R8gdXlarge", "r8gd.xlarge"); + map.put("R8gd2xlarge", "r8gd.2xlarge"); + map.put("R8gd4xlarge", "r8gd.4xlarge"); + map.put("R8gd8xlarge", "r8gd.8xlarge"); + map.put("R8gd12xlarge", "r8gd.12xlarge"); + map.put("R8gd16xlarge", "r8gd.16xlarge"); + map.put("R8gd24xlarge", "r8gd.24xlarge"); + map.put("R8gd48xlarge", "r8gd.48xlarge"); + map.put("R8gdMetal24xl", "r8gd.metal-24xl"); + map.put("R8gdMetal48xl", "r8gd.metal-48xl"); + map.put("C8gMedium", "c8g.medium"); + map.put("C8gLarge", "c8g.large"); + map.put("C8gXlarge", "c8g.xlarge"); + map.put("C8g2xlarge", "c8g.2xlarge"); + map.put("C8g4xlarge", "c8g.4xlarge"); + map.put("C8g8xlarge", "c8g.8xlarge"); + map.put("C8g12xlarge", "c8g.12xlarge"); + map.put("C8g16xlarge", "c8g.16xlarge"); + map.put("C8g24xlarge", "c8g.24xlarge"); + map.put("C8g48xlarge", "c8g.48xlarge"); + map.put("C8gMetal24xl", "c8g.metal-24xl"); + map.put("C8gMetal48xl", "c8g.metal-48xl"); + map.put("C8gdMedium", "c8gd.medium"); + map.put("C8gdLarge", "c8gd.large"); + map.put("C8gdXlarge", "c8gd.xlarge"); + map.put("C8gd2xlarge", "c8gd.2xlarge"); + map.put("C8gd4xlarge", "c8gd.4xlarge"); + map.put("C8gd8xlarge", "c8gd.8xlarge"); + map.put("C8gd12xlarge", "c8gd.12xlarge"); + map.put("C8gd16xlarge", "c8gd.16xlarge"); + map.put("C8gd24xlarge", "c8gd.24xlarge"); + map.put("C8gd48xlarge", "c8gd.48xlarge"); + map.put("C8gdMetal24xl", "c8gd.metal-24xl"); + map.put("C8gdMetal48xl", "c8gd.metal-48xl"); + map.put("C8gnMedium", "c8gn.medium"); + map.put("C8gnLarge", "c8gn.large"); + map.put("C8gnXlarge", "c8gn.xlarge"); + map.put("C8gn2xlarge", "c8gn.2xlarge"); + map.put("C8gn4xlarge", "c8gn.4xlarge"); + map.put("C8gn8xlarge", "c8gn.8xlarge"); + map.put("C8gn12xlarge", "c8gn.12xlarge"); + map.put("C8gn16xlarge", "c8gn.16xlarge"); + map.put("C8gn24xlarge", "c8gn.24xlarge"); + map.put("C8gn48xlarge", "c8gn.48xlarge"); + map.put("C8gnMetal24xl", "c8gn.metal-24xl"); + map.put("C8gnMetal48xl", "c8gn.metal-48xl"); AWS_SDK_JAVA_V1 = Collections.unmodifiableMap(map); } diff --git a/src/main/resources/hudson/plugins/ec2/monitoring/SnowflakeDatabase/config.jelly b/src/main/resources/hudson/plugins/ec2/monitoring/SnowflakeDatabase/config.jelly new file mode 100644 index 000000000..3a74999be --- /dev/null +++ b/src/main/resources/hudson/plugins/ec2/monitoring/SnowflakeDatabase/config.jelly @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java index 685e8a2b0..15f7092b3 100644 --- a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java @@ -86,7 +86,7 @@ void testConfigRoundtrip() throws Exception { r.assertEqualBeans( cloud, r.jenkins.clouds.get(EC2Cloud.class), - "name,region,useInstanceProfileForCredentials,privateKey,instanceCap,roleArn,roleSessionName"); + "cloudName,region,useInstanceProfileForCredentials,privateKey,instanceCap,roleArn,roleSessionName"); } @Test @@ -125,7 +125,9 @@ void testSshKeysCredentialsIdRemainsUnchangedAfterUpdatingOtherFields() throws E EC2Cloud actual = r.jenkins.clouds.get(EC2Cloud.class); assertEquals("updatedSessionName", actual.getRoleSessionName()); r.assertEqualBeans( - cloud, actual, "name,region,useInstanceProfileForCredentials,sshKeysCredentialsId,instanceCap,roleArn"); + cloud, + actual, + "cloudName,region,useInstanceProfileForCredentials,sshKeysCredentialsId,instanceCap,roleArn"); } @Test @@ -206,6 +208,34 @@ void testCustomSshCredentialTypes() throws IOException { assertThat(actual.resolvePrivateKey(), notNullValue()); } + @Test + public void testCloudNameForCasC() { + EC2Cloud cloud = new EC2Cloud("test-cloud", false, null, "us-east-1", null, null, null, Collections.emptyList(), null, null); + + // Test that getCloudName returns the name + assertEquals("test-cloud", cloud.getCloudName()); + assertEquals("test-cloud", cloud.name); + + // Test that setCloudName updates the name field + cloud.setCloudName("my-ec2-cloud"); + assertEquals("my-ec2-cloud", cloud.name); + assertEquals("my-ec2-cloud", cloud.getCloudName()); + + // Test that cloudName is the primary field for CasC configurations + cloud.setCloudName("production-ec2"); + assertEquals("production-ec2", cloud.getCloudName()); + } + + @Test + public void testCloudNameConstructorParameter() { + // Test that cloudName constructor parameter works directly + EC2Cloud cloud = new EC2Cloud("my-casc-cloud", false, null, "us-east-1", null, null, null, Collections.emptyList(), null, null); + + // Constructor parameter sets the name directly + assertEquals("my-casc-cloud", cloud.name); + assertEquals("my-casc-cloud", cloud.getCloudName()); + } + private HtmlForm getConfigForm() throws IOException, SAXException { return r.createWebClient().goTo(cloud.getUrl() + "configure").getFormByName("config"); } diff --git a/src/test/java/hudson/plugins/ec2/monitoring/EC2ProvisioningMonitorTest.java b/src/test/java/hudson/plugins/ec2/monitoring/EC2ProvisioningMonitorTest.java new file mode 100644 index 000000000..7934f40f3 --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/monitoring/EC2ProvisioningMonitorTest.java @@ -0,0 +1,74 @@ +package hudson.plugins.ec2.monitoring; + +import org.junit.Test; +import org.junit.Rule; +import org.jvnet.hudson.test.JenkinsRule; +import jenkins.model.Jenkins; + +import static org.junit.Assert.*; + +/** + * Test for EC2ProvisioningMonitor functionality. + */ +public class EC2ProvisioningMonitorTest { + + @Rule + public JenkinsRule jenkins = new JenkinsRule(); + + @Test + public void testProvisioningEventCreation() { + String region = "us-west-2"; + String az = "us-west-2a"; + String requestId = "test-request-123"; + String instanceType = "m5.large"; + int maxCount = 5; + int minCount = 1; + int provisionedCount = 3; + String controllerName = "test-controller"; + String phase = "SUCCESS"; + String errorMessage = null; + String jenkinsUrl = "https://jenkins.example.com/"; + + ProvisioningEvent event = new ProvisioningEvent( + region, az, requestId, instanceType, maxCount, minCount, + provisionedCount, controllerName, "test-cloud", phase, errorMessage, jenkinsUrl + ); + + assertEquals(region, event.getRegion()); + assertEquals(az, event.getAvailabilityZone()); + assertEquals(requestId, event.getRequestId()); + assertEquals(instanceType, event.getRequestedInstanceType()); + assertEquals(maxCount, event.getRequestedMaxCount()); + assertEquals(minCount, event.getRequestedMinCount()); + assertEquals(provisionedCount, event.getProvisionedInstancesCount()); + assertEquals(controllerName, event.getControllerName()); + assertEquals(phase, event.getPhase()); + assertEquals(errorMessage, event.getErrorMessage()); + assertEquals(jenkinsUrl, event.getJenkinsUrl()); + assertNotNull(event.getTimestamp()); + } + + @Test + public void testProvisioningEventRecording() { + // Test that recording an event doesn't throw exceptions + // This test will work even without a database configured + ProvisioningEvent event = new ProvisioningEvent( + "us-west-2", "us-west-2a", "test-request-123", "m5.large", + 5, 1, 3, "test-controller", "test-cloud", "SUCCESS", null, + "https://jenkins.example.com/" + ); + + // This should not throw an exception even without database configuration + assertDoesNotThrow(() -> { + EC2ProvisioningMonitor.recordProvisioningEvent(event); + }); + } + + private void assertDoesNotThrow(Runnable runnable) { + try { + runnable.run(); + } catch (Exception e) { + fail("Expected no exception, but got: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/test/resources/hudson/plugins/ec2/Ami.yml b/src/test/resources/hudson/plugins/ec2/Ami.yml index 4f31e9fd5..165d8b1a6 100644 --- a/src/test/resources/hudson/plugins/ec2/Ami.yml +++ b/src/test/resources/hudson/plugins/ec2/Ami.yml @@ -4,7 +4,7 @@ configuration-as-code: jenkins: clouds: - amazonEC2: - name: "test" + cloudName: "test" privateKey: "${PRIVATE_KEY}" templates: - ami: ami-0123456789abcdefg diff --git a/src/test/resources/hudson/plugins/ec2/BackwardsCompatibleConnectionStrategy.yml b/src/test/resources/hudson/plugins/ec2/BackwardsCompatibleConnectionStrategy.yml index cefc4c3b1..0abb97154 100644 --- a/src/test/resources/hudson/plugins/ec2/BackwardsCompatibleConnectionStrategy.yml +++ b/src/test/resources/hudson/plugins/ec2/BackwardsCompatibleConnectionStrategy.yml @@ -4,7 +4,7 @@ configuration-as-code: jenkins: clouds: - amazonEC2: - name: "us-east-1" + cloudName: "us-east-1" privateKey: "${PRIVATE_KEY}" templates: - associatePublicIp: false diff --git a/src/test/resources/hudson/plugins/ec2/EC2CloudEmpty.yml b/src/test/resources/hudson/plugins/ec2/EC2CloudEmpty.yml index dad5e0bea..114db2f8c 100644 --- a/src/test/resources/hudson/plugins/ec2/EC2CloudEmpty.yml +++ b/src/test/resources/hudson/plugins/ec2/EC2CloudEmpty.yml @@ -2,5 +2,5 @@ jenkins: clouds: - amazonEC2: - name: "empty" + cloudName: "empty" privateKey: "${PRIVATE_KEY}" diff --git a/src/test/resources/hudson/plugins/ec2/Mac.yml b/src/test/resources/hudson/plugins/ec2/Mac.yml index 10d31122b..839a86046 100644 --- a/src/test/resources/hudson/plugins/ec2/Mac.yml +++ b/src/test/resources/hudson/plugins/ec2/Mac.yml @@ -2,7 +2,7 @@ jenkins: clouds: - amazonEC2: - name: "staging" + cloudName: "staging" useInstanceProfileForCredentials: true privateKey: "${PRIVATE_KEY}" templates: diff --git a/src/test/resources/hudson/plugins/ec2/MacData.yml b/src/test/resources/hudson/plugins/ec2/MacData.yml index dc96e0419..811e708ff 100644 --- a/src/test/resources/hudson/plugins/ec2/MacData.yml +++ b/src/test/resources/hudson/plugins/ec2/MacData.yml @@ -2,7 +2,7 @@ jenkins: clouds: - amazonEC2: - name: "production" + cloudName: "production" useInstanceProfileForCredentials: true sshKeysCredentialsId: "random credentials id" templates: diff --git a/src/test/resources/hudson/plugins/ec2/MacDataExport.yml b/src/test/resources/hudson/plugins/ec2/MacDataExport.yml index e499c0b72..26d28e4ed 100644 --- a/src/test/resources/hudson/plugins/ec2/MacDataExport.yml +++ b/src/test/resources/hudson/plugins/ec2/MacDataExport.yml @@ -1,5 +1,5 @@ - amazonEC2: - name: "production" + cloudName: "production" region: "us-east-1" sshKeysCredentialsId: "random credentials id" templates: diff --git a/src/test/resources/hudson/plugins/ec2/Unix-withAvoidUsingOrphanedNodes.yml b/src/test/resources/hudson/plugins/ec2/Unix-withAvoidUsingOrphanedNodes.yml index aa56da176..db60b7f50 100644 --- a/src/test/resources/hudson/plugins/ec2/Unix-withAvoidUsingOrphanedNodes.yml +++ b/src/test/resources/hudson/plugins/ec2/Unix-withAvoidUsingOrphanedNodes.yml @@ -2,7 +2,7 @@ jenkins: clouds: - amazonEC2: - name: "avoidUsingOrphanedNodesTest" + cloudName: "avoidUsingOrphanedNodesTest" useInstanceProfileForCredentials: true sshKeysCredentialsId: "random credentials id" templates: diff --git a/src/test/resources/hudson/plugins/ec2/Unix-withEnclaveEnabled.yml b/src/test/resources/hudson/plugins/ec2/Unix-withEnclaveEnabled.yml index 7fb3effa8..3e11923eb 100644 --- a/src/test/resources/hudson/plugins/ec2/Unix-withEnclaveEnabled.yml +++ b/src/test/resources/hudson/plugins/ec2/Unix-withEnclaveEnabled.yml @@ -2,7 +2,7 @@ jenkins: clouds: - amazonEC2: - name: "production" + cloudName: "production" useInstanceProfileForCredentials: true sshKeysCredentialsId: "random credentials id" templates: diff --git a/src/test/resources/hudson/plugins/ec2/Unix-withMinimumInstancesTimeRange.yml b/src/test/resources/hudson/plugins/ec2/Unix-withMinimumInstancesTimeRange.yml index 690500c76..4f07db386 100644 --- a/src/test/resources/hudson/plugins/ec2/Unix-withMinimumInstancesTimeRange.yml +++ b/src/test/resources/hudson/plugins/ec2/Unix-withMinimumInstancesTimeRange.yml @@ -2,7 +2,7 @@ jenkins: clouds: - amazonEC2: - name: "timed" + cloudName: "timed" useInstanceProfileForCredentials: true sshKeysCredentialsId: "random credentials id" templates: diff --git a/src/test/resources/hudson/plugins/ec2/Unix.yml b/src/test/resources/hudson/plugins/ec2/Unix.yml index 0681b4063..643bb8ef1 100644 --- a/src/test/resources/hudson/plugins/ec2/Unix.yml +++ b/src/test/resources/hudson/plugins/ec2/Unix.yml @@ -2,7 +2,7 @@ jenkins: clouds: - amazonEC2: - name: "staging" + cloudName: "staging" useInstanceProfileForCredentials: true privateKey: "${PRIVATE_KEY}" templates: diff --git a/src/test/resources/hudson/plugins/ec2/UnixData-withAltEndpointAndJavaPath.yml b/src/test/resources/hudson/plugins/ec2/UnixData-withAltEndpointAndJavaPath.yml index fb53b85d4..3f11d4288 100644 --- a/src/test/resources/hudson/plugins/ec2/UnixData-withAltEndpointAndJavaPath.yml +++ b/src/test/resources/hudson/plugins/ec2/UnixData-withAltEndpointAndJavaPath.yml @@ -3,7 +3,7 @@ jenkins: clouds: - amazonEC2: altEC2Endpoint: "https.//ec2.us-east-1.amazonaws.com" - name: "production" + cloudName: "production" region: "eu-central-1" useInstanceProfileForCredentials: true sshKeysCredentialsId: "random credentials id" diff --git a/src/test/resources/hudson/plugins/ec2/UnixData.yml b/src/test/resources/hudson/plugins/ec2/UnixData.yml index 4a4bfb34b..4e04ce205 100644 --- a/src/test/resources/hudson/plugins/ec2/UnixData.yml +++ b/src/test/resources/hudson/plugins/ec2/UnixData.yml @@ -2,7 +2,7 @@ jenkins: clouds: - amazonEC2: - name: "production" + cloudName: "production" useInstanceProfileForCredentials: true sshKeysCredentialsId: "random credentials id" templates: diff --git a/src/test/resources/hudson/plugins/ec2/UnixDataExport-withAltEndpointAndJavaPath.yml b/src/test/resources/hudson/plugins/ec2/UnixDataExport-withAltEndpointAndJavaPath.yml index d2f493244..2a6507a0a 100644 --- a/src/test/resources/hudson/plugins/ec2/UnixDataExport-withAltEndpointAndJavaPath.yml +++ b/src/test/resources/hudson/plugins/ec2/UnixDataExport-withAltEndpointAndJavaPath.yml @@ -1,6 +1,6 @@ - amazonEC2: altEC2Endpoint: "https.//ec2.us-east-1.amazonaws.com" - name: "production" + cloudName: "production" region: "eu-central-1" sshKeysCredentialsId: "random credentials id" templates: diff --git a/src/test/resources/hudson/plugins/ec2/UnixDataExport.yml b/src/test/resources/hudson/plugins/ec2/UnixDataExport.yml index 0fd486701..062d3c7d5 100644 --- a/src/test/resources/hudson/plugins/ec2/UnixDataExport.yml +++ b/src/test/resources/hudson/plugins/ec2/UnixDataExport.yml @@ -1,5 +1,5 @@ - amazonEC2: - name: "production" + cloudName: "production" region: "us-east-1" sshKeysCredentialsId: "random credentials id" templates: diff --git a/src/test/resources/hudson/plugins/ec2/WindowsData.yml b/src/test/resources/hudson/plugins/ec2/WindowsData.yml index fff116a61..f271efb06 100644 --- a/src/test/resources/hudson/plugins/ec2/WindowsData.yml +++ b/src/test/resources/hudson/plugins/ec2/WindowsData.yml @@ -2,7 +2,7 @@ jenkins: clouds: - amazonEC2: - name: "development" + cloudName: "development" useInstanceProfileForCredentials: true privateKey: "${PRIVATE_KEY}" templates: diff --git a/src/test/resources/hudson/plugins/ec2/WindowsSSHData-withAltEndpointAndJavaPath.yml b/src/test/resources/hudson/plugins/ec2/WindowsSSHData-withAltEndpointAndJavaPath.yml index 8f3aef987..19750bd9e 100644 --- a/src/test/resources/hudson/plugins/ec2/WindowsSSHData-withAltEndpointAndJavaPath.yml +++ b/src/test/resources/hudson/plugins/ec2/WindowsSSHData-withAltEndpointAndJavaPath.yml @@ -3,7 +3,7 @@ jenkins: clouds: - amazonEC2: altEC2Endpoint: "https.//ec2.us-east-1.amazonaws.com" - name: "production" + cloudName: "production" region: "eu-central-1" useInstanceProfileForCredentials: true sshKeysCredentialsId: "random credentials id" diff --git a/src/test/resources/hudson/plugins/ec2/WindowsSSHData.yml b/src/test/resources/hudson/plugins/ec2/WindowsSSHData.yml index 442361dcb..ee806eae3 100644 --- a/src/test/resources/hudson/plugins/ec2/WindowsSSHData.yml +++ b/src/test/resources/hudson/plugins/ec2/WindowsSSHData.yml @@ -2,7 +2,7 @@ jenkins: clouds: - amazonEC2: - name: "production" + cloudName: "production" useInstanceProfileForCredentials: true sshKeysCredentialsId: "random credentials id" templates: diff --git a/src/test/resources/hudson/plugins/ec2/WindowsSSHDataExport-withAltEndpointAndJavaPath.yml b/src/test/resources/hudson/plugins/ec2/WindowsSSHDataExport-withAltEndpointAndJavaPath.yml index 32528d4b7..b8e97ac87 100644 --- a/src/test/resources/hudson/plugins/ec2/WindowsSSHDataExport-withAltEndpointAndJavaPath.yml +++ b/src/test/resources/hudson/plugins/ec2/WindowsSSHDataExport-withAltEndpointAndJavaPath.yml @@ -1,6 +1,6 @@ - amazonEC2: altEC2Endpoint: "https.//ec2.us-east-1.amazonaws.com" - name: "production" + cloudName: "production" region: "eu-central-1" sshKeysCredentialsId: "random credentials id" templates: diff --git a/src/test/resources/hudson/plugins/ec2/WindowsSSHDataExport.yml b/src/test/resources/hudson/plugins/ec2/WindowsSSHDataExport.yml index ca31f3633..9c23d0537 100644 --- a/src/test/resources/hudson/plugins/ec2/WindowsSSHDataExport.yml +++ b/src/test/resources/hudson/plugins/ec2/WindowsSSHDataExport.yml @@ -1,5 +1,5 @@ - amazonEC2: - name: "production" + cloudName: "production" region: "us-east-1" sshKeysCredentialsId: "random credentials id" templates: