From 81c69c9959bd149ae6795aace07c0b09557cbd25 Mon Sep 17 00:00:00 2001 From: Antonio Busuladzich Date: Thu, 17 Oct 2024 21:13:48 +0300 Subject: [PATCH 001/267] Add spec to tag Node Network interface on provision --- src/main/java/hudson/plugins/ec2/SlaveTemplate.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java index 337b2b78d..c2e2767f6 100644 --- a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java +++ b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java @@ -1105,6 +1105,7 @@ HashMap> makeRunInstancesRequestAndFilters(Ima tagSpecification.setTags(instTags); tagList.add(tagSpecification.clone().withResourceType(ResourceType.Instance)); tagList.add(tagSpecification.clone().withResourceType(ResourceType.Volume)); + tagList.add(tagSpecification.clone().withResourceType(ResourceType.NetworkInterface)); riRequest.setTagSpecifications(tagList); if (metadataSupported) { From e6493d58717136c04dabf7b0f35f62d2cde3140b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 07:07:44 +0000 Subject: [PATCH 002/267] build(deps-dev): bump org.testcontainers:testcontainers Bumps [org.testcontainers:testcontainers](https://github.com/testcontainers/testcontainers-java) from 1.20.2 to 1.20.3. - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.20.2...1.20.3) --- updated-dependencies: - dependency-name: org.testcontainers:testcontainers dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2e02b6b9e..c6c3d5465 100644 --- a/pom.xml +++ b/pom.xml @@ -188,7 +188,7 @@ THE SOFTWARE. org.testcontainers testcontainers - 1.20.2 + 1.20.3 test From cc6b299701455e0a8abdfae6ac1cb77593c9ea7e Mon Sep 17 00:00:00 2001 From: Zbynek Konecny Date: Fri, 25 Oct 2024 23:13:32 +0200 Subject: [PATCH 003/267] Replace dropdown button to remove dependency on YUI --- pom.xml | 6 ++-- .../plugins/ec2/EC2Cloud/computerSet.jelly | 34 ++++++++++--------- .../hudson/plugins/ec2/EC2Cloud/provision.js | 8 +++++ 3 files changed, 29 insertions(+), 19 deletions(-) create mode 100644 src/main/resources/hudson/plugins/ec2/EC2Cloud/provision.js diff --git a/pom.xml b/pom.xml index 2e02b6b9e..3b6068c08 100644 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,7 @@ THE SOFTWARE. 999999-SNAPSHOT - 2.414.3 + 2.452.3 jenkinsci/${project.artifactId}-plugin 1626 @@ -201,8 +201,8 @@ THE SOFTWARE. io.jenkins.tools.bom - bom-2.414.x - 2982.vdce2153031a_0 + bom-2.452.x + 3208.vb_21177d4b_cd9 import pom diff --git a/src/main/resources/hudson/plugins/ec2/EC2Cloud/computerSet.jelly b/src/main/resources/hudson/plugins/ec2/EC2Cloud/computerSet.jelly index b8f540ab2..1f9a89863 100644 --- a/src/main/resources/hudson/plugins/ec2/EC2Cloud/computerSet.jelly +++ b/src/main/resources/hudson/plugins/ec2/EC2Cloud/computerSet.jelly @@ -22,28 +22,30 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --> - + + + + + + + + - - - - - + + diff --git a/src/main/resources/hudson/plugins/ec2/EC2Cloud/provision.js b/src/main/resources/hudson/plugins/ec2/EC2Cloud/provision.js new file mode 100644 index 000000000..0c60f02ea --- /dev/null +++ b/src/main/resources/hudson/plugins/ec2/EC2Cloud/provision.js @@ -0,0 +1,8 @@ +Behaviour.specify("[data-type='ec2-provision']", 'ec2-provision', -99, function(e) { + e.addEventListener("click", function (event) { + const form = document.querySelector("form[name='provision']"); + form.querySelector("[name='template']").value = e.dataset.url; + buildFormTree(form); + form.submit(); + }); +}); \ No newline at end of file From 6faa35ae6658b8d256a5cf6440a7c975b39cabe4 Mon Sep 17 00:00:00 2001 From: Zbynek Konecny Date: Sat, 26 Oct 2024 15:19:52 +0200 Subject: [PATCH 004/267] Fix cell width for provision row --- .../resources/hudson/plugins/ec2/EC2Cloud/computerSet.jelly | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/hudson/plugins/ec2/EC2Cloud/computerSet.jelly b/src/main/resources/hudson/plugins/ec2/EC2Cloud/computerSet.jelly index 1f9a89863..b5664389c 100644 --- a/src/main/resources/hudson/plugins/ec2/EC2Cloud/computerSet.jelly +++ b/src/main/resources/hudson/plugins/ec2/EC2Cloud/computerSet.jelly @@ -27,7 +27,7 @@ THE SOFTWARE. - + Date: Sun, 27 Oct 2024 13:22:20 +0100 Subject: [PATCH 005/267] Use unique ID for forms --- .../resources/hudson/plugins/ec2/EC2Cloud/computerSet.jelly | 4 +++- src/main/resources/hudson/plugins/ec2/EC2Cloud/provision.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/resources/hudson/plugins/ec2/EC2Cloud/computerSet.jelly b/src/main/resources/hudson/plugins/ec2/EC2Cloud/computerSet.jelly index b5664389c..54c968f11 100644 --- a/src/main/resources/hudson/plugins/ec2/EC2Cloud/computerSet.jelly +++ b/src/main/resources/hudson/plugins/ec2/EC2Cloud/computerSet.jelly @@ -25,6 +25,7 @@ THE SOFTWARE. + @@ -36,13 +37,14 @@ THE SOFTWARE. - + diff --git a/src/main/resources/hudson/plugins/ec2/EC2Cloud/provision.js b/src/main/resources/hudson/plugins/ec2/EC2Cloud/provision.js index 0c60f02ea..82e1459e0 100644 --- a/src/main/resources/hudson/plugins/ec2/EC2Cloud/provision.js +++ b/src/main/resources/hudson/plugins/ec2/EC2Cloud/provision.js @@ -1,6 +1,6 @@ Behaviour.specify("[data-type='ec2-provision']", 'ec2-provision', -99, function(e) { e.addEventListener("click", function (event) { - const form = document.querySelector("form[name='provision']"); + const form = document.getElementById(e.dataset.form); form.querySelector("[name='template']").value = e.dataset.url; buildFormTree(form); form.submit(); From b9ad411496e3d49a11f93c5c0cca3ce9f689237c Mon Sep 17 00:00:00 2001 From: Allan Burdajewicz Date: Tue, 29 Oct 2024 16:27:46 +1000 Subject: [PATCH 006/267] Adding FINE logging in SG lookups for easier troubleshooting --- src/main/java/hudson/plugins/ec2/SlaveTemplate.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java index 337b2b78d..58a15a9c0 100644 --- a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java +++ b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java @@ -1042,6 +1042,7 @@ HashMap> makeRunInstancesRequestAndFilters(Ima } String subnetId = chooseSubnetId(rotateSubnet); + LOGGER.log(Level.FINE, String.format("Chose subnetId %s", subnetId)); InstanceNetworkInterfaceSpecification net = new InstanceNetworkInterfaceSpecification(); if (StringUtils.isNotBlank(subnetId)) { @@ -1435,6 +1436,7 @@ private List provisionSpot(Image image, int number, EnumSet instTags, String ca * Get a list of security group ids for the agent */ private List getEc2SecurityGroups(AmazonEC2 ec2) throws AmazonClientException { + LOGGER.log(Level.FINE, () -> String.format("Get security group %s for EC2Cloud %s with currentSubnetId %s", + securityGroupSet, this.getParent().name, getCurrentSubnetId())); List groupIds = new ArrayList<>(); - DescribeSecurityGroupsResult groupResult = getSecurityGroupsBy("group-name", securityGroupSet, ec2); if (groupResult.getSecurityGroups().size() == 0) { groupResult = getSecurityGroupsBy("group-id", securityGroupSet, ec2); } for (SecurityGroup group : groupResult.getSecurityGroups()) { + LOGGER.log(Level.FINE, () -> String.format("Checking security group %s (vpc-id = %s, subnet-id = %s)", + group.getGroupId(), group.getVpcId(), getCurrentSubnetId())); if (group.getVpcId() != null && !group.getVpcId().isEmpty()) { List filters = new ArrayList<>(); filters.add(new Filter("vpc-id").withValues(group.getVpcId())); @@ -1710,6 +1715,7 @@ private List getEc2SecurityGroups(AmazonEC2 ec2) throws AmazonClientExce List subnets = subnetResult.getSubnets(); if (subnets != null && !subnets.isEmpty()) { + LOGGER.log(Level.FINE, () -> "Adding security group"); groupIds.add(group.getGroupId()); } } From 4a7c0ca6e431bd9896f03312b9fb7bc8ae685db7 Mon Sep 17 00:00:00 2001 From: Raihaan Shouhell Date: Mon, 4 Nov 2024 20:55:13 +0800 Subject: [PATCH 007/267] Update jenkins baseline to 2.452.4 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 805399b20..ab8003b87 100644 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,7 @@ THE SOFTWARE. 999999-SNAPSHOT - 2.452.3 + 2.452.4 jenkinsci/${project.artifactId}-plugin 1626 From 215d30e97a2b6f809df5485841fd68f36e92239c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 14:18:30 +0000 Subject: [PATCH 008/267] build(deps): bump io.jenkins.tools.bom:bom-2.452.x Bumps [io.jenkins.tools.bom:bom-2.452.x](https://github.com/jenkinsci/bom) from 3208.vb_21177d4b_cd9 to 3613.v584fca_12cf5c. - [Release notes](https://github.com/jenkinsci/bom/releases) - [Commits](https://github.com/jenkinsci/bom/commits) --- updated-dependencies: - dependency-name: io.jenkins.tools.bom:bom-2.452.x dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ab8003b87..ad2ed62cc 100644 --- a/pom.xml +++ b/pom.xml @@ -202,7 +202,7 @@ THE SOFTWARE. io.jenkins.tools.bom bom-2.452.x - 3208.vb_21177d4b_cd9 + 3613.v584fca_12cf5c import pom From 0c01c4238364fee54061bceb09edb96c63dcc50b Mon Sep 17 00:00:00 2001 From: mike cirioli Date: Thu, 7 Nov 2024 14:52:23 -0500 Subject: [PATCH 009/267] allow using a file based ssh credential via system property --- README.md | 21 +++++-- .../java/hudson/plugins/ec2/EC2Cloud.java | 59 +++++++++++++++---- .../hudson/plugins/ec2/SlaveTemplate.java | 1 + 3 files changed, 64 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 3aeaff214..b25986cb5 100644 --- a/README.md +++ b/README.md @@ -52,14 +52,25 @@ the main "Manage Jenkins" \> "Configure System" page, and scroll down near the bottom to the "Cloud" section. There, you click the "Add a new cloud" button, and select the "Amazon EC2" option. This will display the UI for configuring the EC2 plugin.  Then enter the Access Key and Secret -Access Key which act like a username/password (see IAM section). Because -of the way EC2 works, you also need to have an RSA private key that the +Access Key which act like a username/password (see IAM section). + +Because of the way EC2 works, you also need to have an RSA private key that the cloud has the other half for, to permit sshing into the instances that are started. Please use the AWS console or any other tool of your choice -to generate the private key to interactively log in to EC2 instances. +to generate the private key to interactively log in to EC2 instances. + +Once you have generated the needed private key you must either store it as +a Jenkins `SSH Private Key` credential (and select that credential in your cloud +config). + +If you do not want to create a new Jenkins credential you may alterantively store it +in plain text on disk, indicating its file path via the Jenkins system property +`SSH_KEY_PAIR_PRIVATE_KEY_FILE`. If this system property has a non-empty value then +it will override the ssh credential specified in the cloud configuration page. This +approach works well for `k8s` secrets that are mounted in a jenkins container for example. -Once you have put in your Access Key and Secret Access Key, select a -region for the cloud (not shown in screenshot). You may define only one +Once you have put in your Access Key, Secret Access Key, and configured an ssh private key +select a region for the cloud (not shown in screenshot). You may define only one cloud for each region, and the regions offered in the UI will show only the regions that you don't already have clouds defined for them. diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 76f8d112c..d1b6382ca 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -87,6 +87,9 @@ import java.net.MalformedURLException; import java.net.Proxy; import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; @@ -127,6 +130,10 @@ public abstract class EC2Cloud extends Cloud { private static final SimpleFormatter sf = new SimpleFormatter(); + // if this system property is defined and its value points to a valid ssh private key on disk + // then this will be used instead of any configured ssh credential + private static final String SSH_KEY_PAIR_PRIVATE_KEY_FILE = "SSH_KEY_PAIR_PRIVATE_KEY_FILE"; + private transient ReentrantLock slaveCountingLock = new ReentrantLock(); private final boolean useInstanceProfileForCredentials; @@ -195,7 +202,11 @@ protected EC2Cloud(String id, boolean useInstanceProfileForCredentials, String c @CheckForNull public EC2PrivateKey resolvePrivateKey(){ - if (sshKeysCredentialsId != null) { + if (!System.getProperty(SSH_KEY_PAIR_PRIVATE_KEY_FILE, "").isEmpty()) { + LOGGER.fine(() -> "(resolvePrivateKey) secret key file configured, will load from disk"); + return fetchPrivateKeyFromDisk(); + } else if (sshKeysCredentialsId != null) { + LOGGER.fine(() -> "(resolvePrivateKey) Using jenkins ssh credential"); SSHUserPrivateKey privateKeyCredential = getSshCredential(sshKeysCredentialsId, Jenkins.get()); if (privateKeyCredential != null) { return new EC2PrivateKey(privateKeyCredential.getPrivateKey()); @@ -204,6 +215,19 @@ public EC2PrivateKey resolvePrivateKey(){ return null; } + private static EC2PrivateKey fetchPrivateKeyFromDisk() { + String filename = System.getProperty(SSH_KEY_PAIR_PRIVATE_KEY_FILE, ""); + if (!filename.isEmpty()) { + try { + return new EC2PrivateKey(new String(Files.readAllBytes(Paths.get(filename)), StandardCharsets.UTF_8)); + } catch (IOException e) { + LOGGER.warning(() -> "unable to read private key from file " + filename); + return null; + } + } + return null; + } + public abstract URL getEc2EndpointUrl() throws IOException; public abstract URL getS3EndpointUrl() throws IOException; @@ -1122,6 +1146,7 @@ public ListBoxModel doFillSshKeysCredentialsIdItems(@AncestorInPath ItemGroup co AbstractIdCredentialsListBoxModel result = new StandardListBoxModel(); if (Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { result = result + .includeEmptyValue() .includeMatchingAs(Jenkins.getAuthentication(), context, SSHUserPrivateKey.class, Collections.emptyList(), CredentialsMatchers.always()) .includeMatchingAs(ACL.SYSTEM, context, SSHUserPrivateKey.class, Collections.emptyList(), CredentialsMatchers.always()) .includeCurrentValue(sshKeysCredentialsId); @@ -1135,16 +1160,22 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont // Don't do anything if the user is only reading the configuration return FormValidation.ok(); } - if (value == null || value.isEmpty()){ - return FormValidation.error("No ssh credentials selected"); - } - SSHUserPrivateKey sshCredential = getSshCredential(value, context); String privateKey = ""; - if (sshCredential != null) { - privateKey = sshCredential.getPrivateKey(); + + if (System.getProperty(SSH_KEY_PAIR_PRIVATE_KEY_FILE, "").isEmpty()) { + if (value == null || value.isEmpty()) { + return FormValidation.error("No ssh credentials selected"); + } + + SSHUserPrivateKey sshCredential = getSshCredential(value, context); + if (sshCredential != null) { + privateKey = sshCredential.getPrivateKey(); + } else { + return FormValidation.error("Failed to find credential \"" + value + "\" in store."); + } } else { - return FormValidation.error("Failed to find credential \"" + value + "\" in store."); + privateKey = fetchPrivateKeyFromDisk().getPrivateKey(); } boolean hasStart = false, hasEnd = false; @@ -1188,12 +1219,16 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL return FormValidation.ok(); } try { - SSHUserPrivateKey sshCredential = getSshCredential(sshKeysCredentialsId, context); String privateKey = ""; - if (sshCredential != null) { - privateKey = sshCredential.getPrivateKey(); + if (System.getProperty(SSH_KEY_PAIR_PRIVATE_KEY_FILE, "").isEmpty()) { + SSHUserPrivateKey sshCredential = getSshCredential(sshKeysCredentialsId, context); + if (sshCredential != null) { + privateKey = sshCredential.getPrivateKey(); + } else { + return FormValidation.error("Failed to find credential \"" + sshKeysCredentialsId + "\" in store."); + } } else { - return FormValidation.error("Failed to find credential \"" + sshKeysCredentialsId + "\" in store."); + privateKey = fetchPrivateKeyFromDisk().getPrivateKey() ; } AWSCredentialsProvider credentialsProvider = createCredentialsProvider(useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); diff --git a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java index 337b2b78d..ce741d0cc 100644 --- a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java +++ b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java @@ -1655,6 +1655,7 @@ private KeyPair getKeyPair(AmazonEC2 ec2) throws IOException, AmazonClientExcept if (keyPair == null) { throw new AmazonClientException("No matching keypair found on EC2. Is the EC2 private key a valid one?"); } + LOGGER.fine("found matching keypair"); return keyPair; } From c51223bef3e7cc01952019e2acc00eb4fea7d421 Mon Sep 17 00:00:00 2001 From: mike cirioli Date: Thu, 7 Nov 2024 17:01:13 -0500 Subject: [PATCH 010/267] follow convention --- src/main/java/hudson/plugins/ec2/EC2Cloud.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index d1b6382ca..c1a216323 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -132,7 +132,7 @@ public abstract class EC2Cloud extends Cloud { // if this system property is defined and its value points to a valid ssh private key on disk // then this will be used instead of any configured ssh credential - private static final String SSH_KEY_PAIR_PRIVATE_KEY_FILE = "SSH_KEY_PAIR_PRIVATE_KEY_FILE"; + private static final String sshPrivateKeyFilePath =EC2Cloud.class.getName() + "sshPrivateKeyFilePath"; private transient ReentrantLock slaveCountingLock = new ReentrantLock(); @@ -202,7 +202,7 @@ protected EC2Cloud(String id, boolean useInstanceProfileForCredentials, String c @CheckForNull public EC2PrivateKey resolvePrivateKey(){ - if (!System.getProperty(SSH_KEY_PAIR_PRIVATE_KEY_FILE, "").isEmpty()) { + if (!System.getProperty(sshPrivateKeyFilePath, "").isEmpty()) { LOGGER.fine(() -> "(resolvePrivateKey) secret key file configured, will load from disk"); return fetchPrivateKeyFromDisk(); } else if (sshKeysCredentialsId != null) { @@ -216,7 +216,7 @@ public EC2PrivateKey resolvePrivateKey(){ } private static EC2PrivateKey fetchPrivateKeyFromDisk() { - String filename = System.getProperty(SSH_KEY_PAIR_PRIVATE_KEY_FILE, ""); + String filename = System.getProperty(sshPrivateKeyFilePath, ""); if (!filename.isEmpty()) { try { return new EC2PrivateKey(new String(Files.readAllBytes(Paths.get(filename)), StandardCharsets.UTF_8)); @@ -1163,7 +1163,7 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont String privateKey = ""; - if (System.getProperty(SSH_KEY_PAIR_PRIVATE_KEY_FILE, "").isEmpty()) { + if (System.getProperty(sshPrivateKeyFilePath, "").isEmpty()) { if (value == null || value.isEmpty()) { return FormValidation.error("No ssh credentials selected"); } @@ -1220,7 +1220,7 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL } try { String privateKey = ""; - if (System.getProperty(SSH_KEY_PAIR_PRIVATE_KEY_FILE, "").isEmpty()) { + if (System.getProperty(sshPrivateKeyFilePath, "").isEmpty()) { SSHUserPrivateKey sshCredential = getSshCredential(sshKeysCredentialsId, context); if (sshCredential != null) { privateKey = sshCredential.getPrivateKey(); From 9d321ab4cd63626dac667d75fc8893c458623d70 Mon Sep 17 00:00:00 2001 From: mike cirioli Date: Thu, 7 Nov 2024 17:01:38 -0500 Subject: [PATCH 011/267] simplify --- src/main/java/hudson/plugins/ec2/EC2Cloud.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index c1a216323..114ec485c 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -219,9 +219,9 @@ private static EC2PrivateKey fetchPrivateKeyFromDisk() { String filename = System.getProperty(sshPrivateKeyFilePath, ""); if (!filename.isEmpty()) { try { - return new EC2PrivateKey(new String(Files.readAllBytes(Paths.get(filename)), StandardCharsets.UTF_8)); + return new EC2PrivateKey(Files.readString(Paths.get(filename), StandardCharsets.UTF_8)); } catch (IOException e) { - LOGGER.warning(() -> "unable to read private key from file " + filename); + LOGGER.warning(() -> "unable to read private key from file " + filename + ": " + e); return null; } } @@ -1161,7 +1161,7 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont return FormValidation.ok(); } - String privateKey = ""; + String privateKey; if (System.getProperty(sshPrivateKeyFilePath, "").isEmpty()) { if (value == null || value.isEmpty()) { From e5c8ac8860cf51c6f3000af857c070c1af487a74 Mon Sep 17 00:00:00 2001 From: michael cirioli Date: Thu, 7 Nov 2024 17:55:01 -0500 Subject: [PATCH 012/267] better logging Co-authored-by: Jesse Glick --- src/main/java/hudson/plugins/ec2/EC2Cloud.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 114ec485c..2ee776c95 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -221,7 +221,7 @@ private static EC2PrivateKey fetchPrivateKeyFromDisk() { try { return new EC2PrivateKey(Files.readString(Paths.get(filename), StandardCharsets.UTF_8)); } catch (IOException e) { - LOGGER.warning(() -> "unable to read private key from file " + filename + ": " + e); + LOGGER.log(Level.WARNING, "unable to read private key from file " + filename, e); return null; } } From 33d198fdec8ddadf4bf626d2620323f56dd796fd Mon Sep 17 00:00:00 2001 From: michael cirioli Date: Fri, 8 Nov 2024 03:54:25 -0500 Subject: [PATCH 013/267] Apply suggestions from code review pr feedback Co-authored-by: Antonio Muniz --- src/main/java/hudson/plugins/ec2/EC2Cloud.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 2ee776c95..964b356c2 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -132,7 +132,7 @@ public abstract class EC2Cloud extends Cloud { // if this system property is defined and its value points to a valid ssh private key on disk // then this will be used instead of any configured ssh credential - private static final String sshPrivateKeyFilePath =EC2Cloud.class.getName() + "sshPrivateKeyFilePath"; + public static String SSH_PRIVATE_KEY_FILEPATH = EC2Cloud.class.getName() + ".sshPrivateKeyFilePath"; private transient ReentrantLock slaveCountingLock = new ReentrantLock(); @@ -202,7 +202,7 @@ protected EC2Cloud(String id, boolean useInstanceProfileForCredentials, String c @CheckForNull public EC2PrivateKey resolvePrivateKey(){ - if (!System.getProperty(sshPrivateKeyFilePath, "").isEmpty()) { + if (!System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { LOGGER.fine(() -> "(resolvePrivateKey) secret key file configured, will load from disk"); return fetchPrivateKeyFromDisk(); } else if (sshKeysCredentialsId != null) { @@ -215,8 +215,9 @@ public EC2PrivateKey resolvePrivateKey(){ return null; } + @CheckForNull private static EC2PrivateKey fetchPrivateKeyFromDisk() { - String filename = System.getProperty(sshPrivateKeyFilePath, ""); + String filename = System.getProperty(SSH_PRIVATE_KEY_FILEPATH, ""); if (!filename.isEmpty()) { try { return new EC2PrivateKey(Files.readString(Paths.get(filename), StandardCharsets.UTF_8)); @@ -1163,7 +1164,7 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont String privateKey; - if (System.getProperty(sshPrivateKeyFilePath, "").isEmpty()) { + if (System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { if (value == null || value.isEmpty()) { return FormValidation.error("No ssh credentials selected"); } @@ -1220,7 +1221,7 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL } try { String privateKey = ""; - if (System.getProperty(sshPrivateKeyFilePath, "").isEmpty()) { + if (System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { SSHUserPrivateKey sshCredential = getSshCredential(sshKeysCredentialsId, context); if (sshCredential != null) { privateKey = sshCredential.getPrivateKey(); From 8c68a511f0f846b6c1bda61d9cc1844bf2c7ead7 Mon Sep 17 00:00:00 2001 From: mike cirioli Date: Fri, 8 Nov 2024 04:35:25 -0500 Subject: [PATCH 014/267] add simple test for new functionality --- src/main/java/hudson/plugins/ec2/EC2Cloud.java | 3 ++- src/test/java/hudson/plugins/ec2/EC2CloudTest.java | 7 +++++++ src/test/resources/hudson/plugins/ec2/test.pem | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/hudson/plugins/ec2/test.pem diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 964b356c2..afecc2f8d 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -215,8 +215,9 @@ public EC2PrivateKey resolvePrivateKey(){ return null; } + /* visible for testing */ @CheckForNull - private static EC2PrivateKey fetchPrivateKeyFromDisk() { + public static EC2PrivateKey fetchPrivateKeyFromDisk() { String filename = System.getProperty(SSH_PRIVATE_KEY_FILEPATH, ""); if (!filename.isEmpty()) { try { diff --git a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java index eb0461a21..05616a1a2 100644 --- a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java @@ -27,6 +27,13 @@ @RunWith(MockitoJUnitRunner.class) public class EC2CloudTest { + @Test + public void testFileBasedSShKey() { + System.setProperty("hudson.plugins.ec2.EC2Cloud.sshPrivateKeyFilePath", getClass().getClassLoader().getResource("hudson/plugins/ec2/test.pem").getPath()); + assertTrue("file content should not have been empty", EC2Cloud.fetchPrivateKeyFromDisk() != null); + System.setProperty("hudson.plugins.ec2.EC2Cloud.sshPrivateKeyFilePath",null); + } + @Test public void testSlaveTemplateAddition() throws Exception { AmazonEC2Cloud cloud = new AmazonEC2Cloud("us-east-1", true, diff --git a/src/test/resources/hudson/plugins/ec2/test.pem b/src/test/resources/hudson/plugins/ec2/test.pem new file mode 100644 index 000000000..30f51a3fb --- /dev/null +++ b/src/test/resources/hudson/plugins/ec2/test.pem @@ -0,0 +1 @@ +hello, world! \ No newline at end of file From 6fbefc7e9732ed0eea438633e8a9ef764907a15e Mon Sep 17 00:00:00 2001 From: mike cirioli Date: Fri, 8 Nov 2024 05:07:43 -0500 Subject: [PATCH 015/267] thank you spotbugs --- .../java/hudson/plugins/ec2/EC2Cloud.java | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index afecc2f8d..49ef02db0 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -132,7 +132,7 @@ public abstract class EC2Cloud extends Cloud { // if this system property is defined and its value points to a valid ssh private key on disk // then this will be used instead of any configured ssh credential - public static String SSH_PRIVATE_KEY_FILEPATH = EC2Cloud.class.getName() + ".sshPrivateKeyFilePath"; + public static final String SSH_PRIVATE_KEY_FILEPATH = EC2Cloud.class.getName() + ".sshPrivateKeyFilePath"; private transient ReentrantLock slaveCountingLock = new ReentrantLock(); @@ -217,13 +217,17 @@ public EC2PrivateKey resolvePrivateKey(){ /* visible for testing */ @CheckForNull - public static EC2PrivateKey fetchPrivateKeyFromDisk() { - String filename = System.getProperty(SSH_PRIVATE_KEY_FILEPATH, ""); - if (!filename.isEmpty()) { + public static EC2PrivateKey fetchPrivateKeyFromDisk() { + return fetchPrivateKeyFromDisk(System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "")); + } + + @CheckForNull + public static EC2PrivateKey fetchPrivateKeyFromDisk(String filepath) { + if (!(filepath == null) && !(filepath.isEmpty())) { try { - return new EC2PrivateKey(Files.readString(Paths.get(filename), StandardCharsets.UTF_8)); + return new EC2PrivateKey(Files.readString(Paths.get(filepath), StandardCharsets.UTF_8)); } catch (IOException e) { - LOGGER.log(Level.WARNING, "unable to read private key from file " + filename, e); + LOGGER.log(Level.WARNING, "unable to read private key from file " + filepath, e); return null; } } @@ -1177,7 +1181,11 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont return FormValidation.error("Failed to find credential \"" + value + "\" in store."); } } else { - privateKey = fetchPrivateKeyFromDisk().getPrivateKey(); + EC2PrivateKey k = fetchPrivateKeyFromDisk(); + if (k == null) { + return FormValidation.error("Failed to find private key file " + System.getProperty(SSH_PRIVATE_KEY_FILEPATH)); + } + privateKey = k.getPrivateKey(); } boolean hasStart = false, hasEnd = false; @@ -1230,7 +1238,11 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL return FormValidation.error("Failed to find credential \"" + sshKeysCredentialsId + "\" in store."); } } else { - privateKey = fetchPrivateKeyFromDisk().getPrivateKey() ; + EC2PrivateKey k = fetchPrivateKeyFromDisk(); + if (k == null) { + return FormValidation.error("Failed to find private key file " + System.getProperty(SSH_PRIVATE_KEY_FILEPATH)); + } + privateKey = k.getPrivateKey(); } AWSCredentialsProvider credentialsProvider = createCredentialsProvider(useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); From 536de5ffae538ce3d0a133e894a436b50c0c215e Mon Sep 17 00:00:00 2001 From: mike cirioli Date: Fri, 8 Nov 2024 05:07:59 -0500 Subject: [PATCH 016/267] fixup test --- src/test/java/hudson/plugins/ec2/EC2CloudTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java index 05616a1a2..34317b716 100644 --- a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java @@ -29,9 +29,7 @@ public class EC2CloudTest { @Test public void testFileBasedSShKey() { - System.setProperty("hudson.plugins.ec2.EC2Cloud.sshPrivateKeyFilePath", getClass().getClassLoader().getResource("hudson/plugins/ec2/test.pem").getPath()); - assertTrue("file content should not have been empty", EC2Cloud.fetchPrivateKeyFromDisk() != null); - System.setProperty("hudson.plugins.ec2.EC2Cloud.sshPrivateKeyFilePath",null); + assertTrue("file content should not have been empty", EC2Cloud.fetchPrivateKeyFromDisk(getClass().getClassLoader().getResource("hudson/plugins/ec2/test.pem").getPath()) != null); } @Test From 8687121cee46b6cc7c5b8aa064199eafacc1b4a5 Mon Sep 17 00:00:00 2001 From: michael cirioli Date: Fri, 8 Nov 2024 08:42:27 -0500 Subject: [PATCH 017/267] cleaner code Co-authored-by: Francisco Javier Fernandez <31063239+fcojfernandez@users.noreply.github.com> --- README.md | 2 +- src/main/java/hudson/plugins/ec2/EC2Cloud.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b25986cb5..a9492c135 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ config). If you do not want to create a new Jenkins credential you may alterantively store it in plain text on disk, indicating its file path via the Jenkins system property -`SSH_KEY_PAIR_PRIVATE_KEY_FILE`. If this system property has a non-empty value then +`hudson.plugins.ec2.EC2Cloud.sshPrivateKeyFilePath`. If this system property has a non-empty value then it will override the ssh credential specified in the cloud configuration page. This approach works well for `k8s` secrets that are mounted in a jenkins container for example. diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 49ef02db0..ca7995cb8 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -223,7 +223,7 @@ public static EC2PrivateKey fetchPrivateKeyFromDisk() { @CheckForNull public static EC2PrivateKey fetchPrivateKeyFromDisk(String filepath) { - if (!(filepath == null) && !(filepath.isEmpty())) { + if (StringUtils.isNotEmpty(filepath)) { try { return new EC2PrivateKey(Files.readString(Paths.get(filepath), StandardCharsets.UTF_8)); } catch (IOException e) { From 74c74f5b448ea6c2bd3f08eddb0ad36f12113ef6 Mon Sep 17 00:00:00 2001 From: mike cirioli Date: Fri, 8 Nov 2024 08:58:24 -0500 Subject: [PATCH 018/267] refactor --- .../java/hudson/plugins/ec2/EC2Cloud.java | 28 ++---------------- .../hudson/plugins/ec2/EC2PrivateKey.java | 29 +++++++++++++++++++ .../java/hudson/plugins/ec2/EC2CloudTest.java | 2 +- 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index ca7995cb8..a8a79059f 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -87,9 +87,6 @@ import java.net.MalformedURLException; import java.net.Proxy; import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; @@ -204,7 +201,7 @@ protected EC2Cloud(String id, boolean useInstanceProfileForCredentials, String c public EC2PrivateKey resolvePrivateKey(){ if (!System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { LOGGER.fine(() -> "(resolvePrivateKey) secret key file configured, will load from disk"); - return fetchPrivateKeyFromDisk(); + return EC2PrivateKey.fetchFromDisk(); } else if (sshKeysCredentialsId != null) { LOGGER.fine(() -> "(resolvePrivateKey) Using jenkins ssh credential"); SSHUserPrivateKey privateKeyCredential = getSshCredential(sshKeysCredentialsId, Jenkins.get()); @@ -215,25 +212,6 @@ public EC2PrivateKey resolvePrivateKey(){ return null; } - /* visible for testing */ - @CheckForNull - public static EC2PrivateKey fetchPrivateKeyFromDisk() { - return fetchPrivateKeyFromDisk(System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "")); - } - - @CheckForNull - public static EC2PrivateKey fetchPrivateKeyFromDisk(String filepath) { - if (StringUtils.isNotEmpty(filepath)) { - try { - return new EC2PrivateKey(Files.readString(Paths.get(filepath), StandardCharsets.UTF_8)); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "unable to read private key from file " + filepath, e); - return null; - } - } - return null; - } - public abstract URL getEc2EndpointUrl() throws IOException; public abstract URL getS3EndpointUrl() throws IOException; @@ -1181,7 +1159,7 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont return FormValidation.error("Failed to find credential \"" + value + "\" in store."); } } else { - EC2PrivateKey k = fetchPrivateKeyFromDisk(); + EC2PrivateKey k = EC2PrivateKey.fetchFromDisk(); if (k == null) { return FormValidation.error("Failed to find private key file " + System.getProperty(SSH_PRIVATE_KEY_FILEPATH)); } @@ -1238,7 +1216,7 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL return FormValidation.error("Failed to find credential \"" + sshKeysCredentialsId + "\" in store."); } } else { - EC2PrivateKey k = fetchPrivateKeyFromDisk(); + EC2PrivateKey k = EC2PrivateKey.fetchFromDisk(); if (k == null) { return FormValidation.error("Failed to find private key file " + System.getProperty(SSH_PRIVATE_KEY_FILEPATH)); } diff --git a/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java b/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java index a15728cac..bc5bba2ea 100644 --- a/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java +++ b/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java @@ -26,6 +26,9 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; import java.security.UnrecoverableKeyException; import java.util.Base64; @@ -33,15 +36,20 @@ import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.model.KeyPairInfo; +import edu.umd.cs.findbugs.annotations.CheckForNull; import hudson.util.Secret; import jenkins.bouncycastle.api.PEMEncodable; import javax.crypto.Cipher; import java.nio.charset.Charset; +import java.util.logging.Level; +import java.util.logging.Logger; import org.apache.commons.lang.StringUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; +import static hudson.plugins.ec2.EC2Cloud.SSH_PRIVATE_KEY_FILEPATH; + /** * RSA private key (the one that you generate with ec2-add-keypair.) * @@ -51,6 +59,8 @@ */ public class EC2PrivateKey { + private static final Logger LOGGER = Logger.getLogger(EC2PrivateKey.class.getName()); + private final Secret privateKey; EC2PrivateKey(String privateKey) { @@ -143,6 +153,25 @@ public String decryptWindowsPassword(String encodedPassword) throws AmazonClient } } + /* visible for testing */ + @CheckForNull + public static EC2PrivateKey fetchFromDisk() { + return fetchFromDisk(System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "")); + } + + @CheckForNull + public static EC2PrivateKey fetchFromDisk(String filepath) { + if (StringUtils.isNotEmpty(filepath)) { + try { + return new EC2PrivateKey(Files.readString(Paths.get(filepath), StandardCharsets.UTF_8)); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "unable to read private key from file " + filepath, e); + return null; + } + } + return null; + } + @Override public int hashCode() { return privateKey.hashCode(); diff --git a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java index 34317b716..2f6cb71ee 100644 --- a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java @@ -29,7 +29,7 @@ public class EC2CloudTest { @Test public void testFileBasedSShKey() { - assertTrue("file content should not have been empty", EC2Cloud.fetchPrivateKeyFromDisk(getClass().getClassLoader().getResource("hudson/plugins/ec2/test.pem").getPath()) != null); + assertNotNull("file content should not have been empty", EC2PrivateKey.fetchFromDisk(getClass().getClassLoader().getResource("hudson/plugins/ec2/test.pem").getPath())); } @Test From 83f632b5f94bed88eb7390f68f44222852fff0c6 Mon Sep 17 00:00:00 2001 From: mike cirioli Date: Fri, 8 Nov 2024 08:59:01 -0500 Subject: [PATCH 019/267] more user friendly --- src/main/java/hudson/plugins/ec2/EC2Cloud.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index a8a79059f..2bd4ede27 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -1148,6 +1148,7 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont String privateKey; if (System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { + // not using a static ssh key file if (value == null || value.isEmpty()) { return FormValidation.error("No ssh credentials selected"); } @@ -1182,6 +1183,14 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont if (!hasEnd) return FormValidation .error("The private key is missing the trailing 'END RSA PRIVATE KEY' marker. Copy&paste error?"); + + if (System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { + if (!StringUtils.isEmpty(value)) { + return FormValidation.ok("Using private key file instead of selected credential"); + } else { + return FormValidation.ok("Using private key file"); + } + } return FormValidation.ok(); } @@ -1236,6 +1245,14 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL + pk.getFingerprint() + ")"); } + if (System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { + if (!StringUtils.isEmpty(sshKeysCredentialsId)) { + return FormValidation.ok("Using private key file instead of selected credential"); + } else { + return FormValidation.ok("Using private key file"); + } + } + return FormValidation.ok(Messages.EC2Cloud_Success()); } catch (AmazonClientException e) { LOGGER.log(Level.WARNING, "Failed to check EC2 credential", e); From d9094f65ba8bb91e857a57543ee61422b1f8da70 Mon Sep 17 00:00:00 2001 From: mike cirioli Date: Fri, 8 Nov 2024 09:58:44 -0500 Subject: [PATCH 020/267] somewhat better test --- .../java/hudson/plugins/ec2/EC2CloudTest.java | 6 ----- .../plugins/ec2/FileBasedSSHKeyTest.java | 26 +++++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 src/test/java/hudson/plugins/ec2/FileBasedSSHKeyTest.java diff --git a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java index 2f6cb71ee..5ec6a49ae 100644 --- a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java @@ -26,12 +26,6 @@ @RunWith(MockitoJUnitRunner.class) public class EC2CloudTest { - - @Test - public void testFileBasedSShKey() { - assertNotNull("file content should not have been empty", EC2PrivateKey.fetchFromDisk(getClass().getClassLoader().getResource("hudson/plugins/ec2/test.pem").getPath())); - } - @Test public void testSlaveTemplateAddition() throws Exception { AmazonEC2Cloud cloud = new AmazonEC2Cloud("us-east-1", true, diff --git a/src/test/java/hudson/plugins/ec2/FileBasedSSHKeyTest.java b/src/test/java/hudson/plugins/ec2/FileBasedSSHKeyTest.java new file mode 100644 index 000000000..25ec0d1c6 --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/FileBasedSSHKeyTest.java @@ -0,0 +1,26 @@ +package hudson.plugins.ec2; + +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.RealJenkinsRule; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class FileBasedSSHKeyTest { + @Rule + public RealJenkinsRule r = new RealJenkinsRule().javaOptions("-D" + EC2Cloud.class.getName() + ".sshPrivateKeyFilePath=" + + getClass().getClassLoader().getResource("hudson/plugins/ec2/test.pem").getPath()); + + @Test + public void testFileBasedSShKey() throws Throwable { + r.startJenkins(); + r.runRemotely(FileBasedSSHKeyTest::verifyKeyFile); + } + + private static void verifyKeyFile(JenkinsRule r) throws Throwable { + assertNotNull("file content should not have been empty", EC2PrivateKey.fetchFromDisk()); + assertEquals("file content did not match", EC2PrivateKey.fetchFromDisk().getPrivateKey(),"hello, world!"); + } +} From d25dc754619f9d1d0cfec2102b49e99247c62547 Mon Sep 17 00:00:00 2001 From: mike cirioli Date: Fri, 8 Nov 2024 10:41:28 -0500 Subject: [PATCH 021/267] doh! --- src/main/java/hudson/plugins/ec2/EC2Cloud.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 2bd4ede27..e94e15895 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -1184,7 +1184,7 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont return FormValidation .error("The private key is missing the trailing 'END RSA PRIVATE KEY' marker. Copy&paste error?"); - if (System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { + if (!System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { if (!StringUtils.isEmpty(value)) { return FormValidation.ok("Using private key file instead of selected credential"); } else { @@ -1245,7 +1245,7 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL + pk.getFingerprint() + ")"); } - if (System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { + if (!System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { if (!StringUtils.isEmpty(sshKeysCredentialsId)) { return FormValidation.ok("Using private key file instead of selected credential"); } else { From dffd59972fbdd60cfcafd1ffa337aa40d3046fde Mon Sep 17 00:00:00 2001 From: mike cirioli Date: Fri, 8 Nov 2024 10:41:50 -0500 Subject: [PATCH 022/267] better logs and validations --- src/main/java/hudson/plugins/ec2/EC2Cloud.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index e94e15895..a5230332a 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -1186,7 +1186,7 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont if (!System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { if (!StringUtils.isEmpty(value)) { - return FormValidation.ok("Using private key file instead of selected credential"); + return FormValidation.warning("Using private key file instead of selected credential"); } else { return FormValidation.ok("Using private key file"); } @@ -1216,8 +1216,10 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL return FormValidation.ok(); } try { + LOGGER.fine(() -> "begin doTestConnection()"); String privateKey = ""; if (System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { + LOGGER.fine(() -> "static credential is in use"); SSHUserPrivateKey sshCredential = getSshCredential(sshKeysCredentialsId, context); if (sshCredential != null) { privateKey = sshCredential.getPrivateKey(); @@ -1225,12 +1227,14 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL return FormValidation.error("Failed to find credential \"" + sshKeysCredentialsId + "\" in store."); } } else { + LOGGER.fine(() -> "using static ssh keyfile"); EC2PrivateKey k = EC2PrivateKey.fetchFromDisk(); if (k == null) { return FormValidation.error("Failed to find private key file " + System.getProperty(SSH_PRIVATE_KEY_FILEPATH)); } privateKey = k.getPrivateKey(); } + LOGGER.fine(() -> "private key found ok"); AWSCredentialsProvider credentialsProvider = createCredentialsProvider(useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); AmazonEC2 ec2 = AmazonEC2Factory.getInstance().connect(credentialsProvider, ec2endpoint); @@ -1247,7 +1251,7 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL if (!System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { if (!StringUtils.isEmpty(sshKeysCredentialsId)) { - return FormValidation.ok("Using private key file instead of selected credential"); + return FormValidation.warning("Using private key file instead of selected credential"); } else { return FormValidation.ok("Using private key file"); } From 5cbf9500f078ae1176548d7124479c0dca660b06 Mon Sep 17 00:00:00 2001 From: mike cirioli Date: Fri, 8 Nov 2024 10:55:12 -0500 Subject: [PATCH 023/267] better validation code --- .../java/hudson/plugins/ec2/EC2Cloud.java | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index a5230332a..68eca639c 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -1150,7 +1150,7 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont if (System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { // not using a static ssh key file if (value == null || value.isEmpty()) { - return FormValidation.error("No ssh credentials selected"); + return FormValidation.error("No ssh credentials selected and no static key file defined"); } SSHUserPrivateKey sshCredential = getSshCredential(value, context); @@ -1167,6 +1167,8 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont privateKey = k.getPrivateKey(); } + List validations = new ArrayList<>(); + boolean hasStart = false, hasEnd = false; BufferedReader br = new BufferedReader(new StringReader(privateKey)); String line; @@ -1179,19 +1181,18 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont hasEnd = true; } if (!hasStart) - return FormValidation.error("This doesn't look like a private key at all"); + validations.add(FormValidation.error("This doesn't look like a private key at all")); if (!hasEnd) - return FormValidation - .error("The private key is missing the trailing 'END RSA PRIVATE KEY' marker. Copy&paste error?"); + validations.add(FormValidation.error("The private key is missing the trailing 'END RSA PRIVATE KEY' marker. Copy&paste error?")); if (!System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { if (!StringUtils.isEmpty(value)) { - return FormValidation.warning("Using private key file instead of selected credential"); + validations.add(FormValidation.warning("Using private key file instead of selected credential")); } else { - return FormValidation.ok("Using private key file"); + validations.add(FormValidation.ok("Using private key file")); } } - return FormValidation.ok(); + return validations.isEmpty() ? FormValidation.ok() : FormValidation.aggregate(validations); } /** @@ -1240,24 +1241,26 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL AmazonEC2 ec2 = AmazonEC2Factory.getInstance().connect(credentialsProvider, ec2endpoint); ec2.describeInstances(); + List validations = new ArrayList<>(); + if (privateKey.trim().length() > 0) { // check if this key exists EC2PrivateKey pk = new EC2PrivateKey(privateKey); if (pk.find(ec2) == null) - return FormValidation + validations.add(FormValidation .error("The EC2 key pair private key isn't registered to this EC2 region (fingerprint is " - + pk.getFingerprint() + ")"); + + pk.getFingerprint() + ")")); } if (!System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { if (!StringUtils.isEmpty(sshKeysCredentialsId)) { - return FormValidation.warning("Using private key file instead of selected credential"); + validations.add(FormValidation.warning("Using private key file instead of selected credential")); } else { - return FormValidation.ok("Using private key file"); + validations.add(FormValidation.ok("Using private key file")); } } - return FormValidation.ok(Messages.EC2Cloud_Success()); + return validations.isEmpty() ? FormValidation.ok(Messages.EC2Cloud_Success()) : FormValidation.aggregate(validations); } catch (AmazonClientException e) { LOGGER.log(Level.WARNING, "Failed to check EC2 credential", e); return FormValidation.error(e.getMessage()); From 4c493cc04e1a34b2366da46c3018624b767f7aaf Mon Sep 17 00:00:00 2001 From: mike cirioli Date: Fri, 8 Nov 2024 11:03:13 -0500 Subject: [PATCH 024/267] aggregate sensibly --- src/main/java/hudson/plugins/ec2/EC2Cloud.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 68eca639c..2b6889239 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -1192,7 +1192,9 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont validations.add(FormValidation.ok("Using private key file")); } } - return validations.isEmpty() ? FormValidation.ok() : FormValidation.aggregate(validations); + + validations.add(FormValidation.ok("SSH key validation successful")); + return FormValidation.aggregate(validations); } /** @@ -1259,8 +1261,8 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL validations.add(FormValidation.ok("Using private key file")); } } - - return validations.isEmpty() ? FormValidation.ok(Messages.EC2Cloud_Success()) : FormValidation.aggregate(validations); + validations.add(FormValidation.ok(Messages.EC2Cloud_Success())); + return FormValidation.aggregate(validations); } catch (AmazonClientException e) { LOGGER.log(Level.WARNING, "Failed to check EC2 credential", e); return FormValidation.error(e.getMessage()); From 7e4f0fa2b8f3ca8ceb079a3a28052b0171760c25 Mon Sep 17 00:00:00 2001 From: mike cirioli Date: Fri, 8 Nov 2024 12:15:57 -0500 Subject: [PATCH 025/267] better validation --- .../java/hudson/plugins/ec2/EC2Cloud.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 2b6889239..138a94179 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -1146,11 +1146,12 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont } String privateKey; + List validations = new ArrayList<>(); if (System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { // not using a static ssh key file if (value == null || value.isEmpty()) { - return FormValidation.error("No ssh credentials selected and no static key file defined"); + return FormValidation.error("No ssh credentials selected and no private key file defined"); } SSHUserPrivateKey sshCredential = getSshCredential(value, context); @@ -1162,12 +1163,15 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont } else { EC2PrivateKey k = EC2PrivateKey.fetchFromDisk(); if (k == null) { - return FormValidation.error("Failed to find private key file " + System.getProperty(SSH_PRIVATE_KEY_FILEPATH)); + validations.add(FormValidation.error("Failed to find private key file " + System.getProperty(SSH_PRIVATE_KEY_FILEPATH))); + if (!StringUtils.isEmpty(value)) { + validations.add(FormValidation.warning("Private key file path defined, selected credential will be ignored")); + } + return FormValidation.aggregate(validations); } privateKey = k.getPrivateKey(); } - List validations = new ArrayList<>(); boolean hasStart = false, hasEnd = false; BufferedReader br = new BufferedReader(new StringReader(privateKey)); @@ -1219,6 +1223,8 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL return FormValidation.ok(); } try { + List validations = new ArrayList<>(); + LOGGER.fine(() -> "begin doTestConnection()"); String privateKey = ""; if (System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { @@ -1230,10 +1236,13 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL return FormValidation.error("Failed to find credential \"" + sshKeysCredentialsId + "\" in store."); } } else { - LOGGER.fine(() -> "using static ssh keyfile"); EC2PrivateKey k = EC2PrivateKey.fetchFromDisk(); if (k == null) { - return FormValidation.error("Failed to find private key file " + System.getProperty(SSH_PRIVATE_KEY_FILEPATH)); + validations.add(FormValidation.error("Failed to find private key file " + System.getProperty(SSH_PRIVATE_KEY_FILEPATH))); + if (!StringUtils.isEmpty(sshKeysCredentialsId)) { + validations.add(FormValidation.warning("Private key file path defined, selected credential will be ignored")); + } + return FormValidation.aggregate(validations); } privateKey = k.getPrivateKey(); } @@ -1243,7 +1252,6 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL AmazonEC2 ec2 = AmazonEC2Factory.getInstance().connect(credentialsProvider, ec2endpoint); ec2.describeInstances(); - List validations = new ArrayList<>(); if (privateKey.trim().length() > 0) { // check if this key exists From 1f54c8d8fe3ec3d55fd49d84c4a959515ed34d66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 07:39:20 +0000 Subject: [PATCH 026/267] build(deps): bump io.jenkins.tools.bom:bom-2.452.x Bumps [io.jenkins.tools.bom:bom-2.452.x](https://github.com/jenkinsci/bom) from 3613.v584fca_12cf5c to 3654.v237e4a_f2d8da_. - [Release notes](https://github.com/jenkinsci/bom/releases) - [Commits](https://github.com/jenkinsci/bom/commits) --- updated-dependencies: - dependency-name: io.jenkins.tools.bom:bom-2.452.x dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ad2ed62cc..161392eae 100644 --- a/pom.xml +++ b/pom.xml @@ -202,7 +202,7 @@ THE SOFTWARE. io.jenkins.tools.bom bom-2.452.x - 3613.v584fca_12cf5c + 3654.v237e4a_f2d8da_ import pom From 5ec3dde0729c12ca2abfa36fe37f2c63f7ec0016 Mon Sep 17 00:00:00 2001 From: mike cirioli Date: Mon, 11 Nov 2024 09:25:13 -0500 Subject: [PATCH 027/267] even better test --- .../java/hudson/plugins/ec2/FileBasedSSHKeyTest.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/test/java/hudson/plugins/ec2/FileBasedSSHKeyTest.java b/src/test/java/hudson/plugins/ec2/FileBasedSSHKeyTest.java index 25ec0d1c6..37ac265ae 100644 --- a/src/test/java/hudson/plugins/ec2/FileBasedSSHKeyTest.java +++ b/src/test/java/hudson/plugins/ec2/FileBasedSSHKeyTest.java @@ -5,6 +5,8 @@ import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.RealJenkinsRule; +import java.util.Collections; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -17,10 +19,18 @@ public class FileBasedSSHKeyTest { public void testFileBasedSShKey() throws Throwable { r.startJenkins(); r.runRemotely(FileBasedSSHKeyTest::verifyKeyFile); + r.runRemotely(FileBasedSSHKeyTest::verifyCorrectKeyIsResolved); } private static void verifyKeyFile(JenkinsRule r) throws Throwable { assertNotNull("file content should not have been empty", EC2PrivateKey.fetchFromDisk()); assertEquals("file content did not match", EC2PrivateKey.fetchFromDisk().getPrivateKey(),"hello, world!"); } + + private static void verifyCorrectKeyIsResolved(JenkinsRule r) throws Throwable { + AmazonEC2Cloud cloud = new AmazonEC2Cloud("us-east-1", true, "abc", "us-east-1", null, "ghi", "3", Collections.emptyList(), "roleArn", "roleSessionName"); + r.jenkins.clouds.add(cloud); + AmazonEC2Cloud c = r.jenkins.clouds.get(AmazonEC2Cloud.class); + assertEquals("An unexpected key was returned!", c.resolvePrivateKey().getPrivateKey(),"hello, world!"); + } } From cc9dfd3d6e4feaffae83243a73fde6e468b63bd3 Mon Sep 17 00:00:00 2001 From: Allan Burdajewicz Date: Thu, 14 Nov 2024 15:39:09 +1000 Subject: [PATCH 028/267] Use lambda to create log record Co-authored-by: Raihaan Shouhell --- src/main/java/hudson/plugins/ec2/SlaveTemplate.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java index 58a15a9c0..15e32ea04 100644 --- a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java +++ b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java @@ -1042,7 +1042,7 @@ HashMap> makeRunInstancesRequestAndFilters(Ima } String subnetId = chooseSubnetId(rotateSubnet); - LOGGER.log(Level.FINE, String.format("Chose subnetId %s", subnetId)); + LOGGER.log(Level.FINE, () -> String.format("Chose subnetId %s", subnetId)); InstanceNetworkInterfaceSpecification net = new InstanceNetworkInterfaceSpecification(); if (StringUtils.isNotBlank(subnetId)) { @@ -1436,7 +1436,7 @@ private List provisionSpot(Image image, int number, EnumSet String.format("Chose subnetId %s", subnetId)); if (StringUtils.isNotBlank(subnetId)) { net.setSubnetId(subnetId); From 30051438acc690eb297fe742503cb6f46d7141da Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Thu, 21 Nov 2024 15:48:25 +0100 Subject: [PATCH 029/267] Bump jenkins version Required to have access to jenkins.security.FIPS140 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2e02b6b9e..c0637ed87 100644 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,7 @@ THE SOFTWARE. 999999-SNAPSHOT - 2.414.3 + 2.462.2 jenkinsci/${project.artifactId}-plugin 1626 From 66389700cd77fa10aeced42a652cd6715a5b5669 Mon Sep 17 00:00:00 2001 From: Salvo Marino Date: Fri, 22 Nov 2024 22:11:17 +0000 Subject: [PATCH 030/267] Reconnect to EC2 also when error code is ExpiredToken --- src/main/java/hudson/plugins/ec2/EC2Cloud.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 138a94179..8b9de5da0 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -778,9 +778,9 @@ public Collection provision(final Label label, int excessWorkload) if (excessWorkload == 0) break; } catch (AmazonServiceException e) { LOGGER.log(Level.WARNING, t + ". Exception during provisioning", e); - if (e.getErrorCode().equals("RequestExpired")) { - // JENKINS-71554: A RequestExpired error can indicate that credentials have expired so reconnect - LOGGER.log(Level.INFO, "[JENKINS-71554] Reconnecting to EC2 due to RequestExpired error"); + if (e.getErrorCode().equals("RequestExpired") || e.getErrorCode().equals("ExpiredToken")) { + // A RequestExpired or ExpiredToken error can indicate that credentials have expired so reconnect + LOGGER.log(Level.INFO, "Reconnecting to EC2 due to RequestExpired or ExpiredToken error"); try { reconnectToEc2(); } catch (IOException e2) { From ab2914f853d8e02c46ff95ae0269e95327929db6 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Fri, 22 Nov 2024 15:14:41 +0100 Subject: [PATCH 031/267] Add FIPS utility methods and messages --- .../hudson/plugins/ec2/util/FIPS140Utils.java | 108 +++++++++++ .../hudson/plugins/ec2/Messages.properties | 7 + .../ec2/util/FIPS140UtilsWithFIPSTest.java | 178 ++++++++++++++++++ .../ec2/util/FIPS140UtilsWithoutFIPSTest.java | 154 +++++++++++++++ 4 files changed, 447 insertions(+) create mode 100644 src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java create mode 100644 src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java create mode 100644 src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithoutFIPSTest.java diff --git a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java new file mode 100644 index 000000000..4f97b4938 --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java @@ -0,0 +1,108 @@ +package hudson.plugins.ec2.util; + +import hudson.plugins.ec2.Messages; +import jenkins.security.FIPS140; + +import java.net.URL; +import java.security.Key; +import java.security.interfaces.DSAKey; +import java.security.interfaces.ECKey; +import java.security.interfaces.RSAKey; + +/** + * FIPS related utility methods (check Private and Public keys, ...) + */ +public class FIPS140Utils { + + /** + * Checks if the key is allowed when FIPS mode is requested. + * Allowed key with the following algorithms and sizes: + *
    + *
  • DSA with key size >= 2048
  • + *
  • RSA with key size >= 2048
  • + *
  • Elliptic curve (ED25519) with field size >= 224
  • + *
+ * If the key is valid and allowed or not in FIPS mode method will just exit. + * If not it will throw an {@link IllegalArgumentException}. + * @param key The key to check. + */ + public static void ensureKeyInFipsMode(Key key) { + if (!FIPS140.useCompliantAlgorithms()) { + return; + } + try { + if (key instanceof RSAKey) { + if (((RSAKey) key).getModulus().bitLength() < 2048) { + throw new IllegalArgumentException(Messages.AmazonEC2Cloud_invalidKeySize()); + } + } else if (key instanceof DSAKey) { + if (((DSAKey) key).getParams().getP().bitLength() < 2048) { + throw new IllegalArgumentException(Messages.AmazonEC2Cloud_invalidKeySize()); + } + } else if (key instanceof ECKey) { + if (((ECKey) key).getParams().getCurve().getField().getFieldSize() < 224) { + throw new IllegalArgumentException(Messages.AmazonEC2Cloud_invalidKeySizeEC()); + } + } else { + throw new IllegalArgumentException(Messages.AmazonEC2Cloud_keyIsNotApprovedInFIPSMode(key.getAlgorithm())); + } + } catch (RuntimeException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + + private static boolean isNotEmpty(String password) { + return password != null && !password.isEmpty(); + } + + /** + * Password leak prevention when FIPS mode is requested. If FIPS mode is not requested, this method does nothing. + * Otherwise, ensure that no password can be leaked + * @param url the requested URL + * @param password the password used + * @throws IllegalArgumentException if there is a risk that the password will leak + */ + public static void ensureNoPasswordLeak(URL url, String password) { + ensureNoPasswordLeak("https".equals(url.getProtocol()), password); + } + + /** + * Password leak prevention when FIPS mode is requested. If FIPS mode is not requested, this method does nothing. + * Otherwise, ensure that no password can be leaked. + * @param useHTTPS is TLS used or not + * @param password the password used + * @throws IllegalArgumentException if there is a risk that the password will leak + */ + public static void ensureNoPasswordLeak(boolean useHTTPS, String password) { + ensureNoPasswordLeak(useHTTPS, isNotEmpty(password)); + } + + /** + * Password leak prevention when FIPS mode is requested. If FIPS mode is not requested, this method does nothing. + * Otherwise, ensure that no password can be leaked. + * @param useHTTPS is TLS used or not + * @param usePassword is a password used + * @throws IllegalArgumentException if there is a risk that the password will leak + */ + public static void ensureNoPasswordLeak(boolean useHTTPS, boolean usePassword) { + if (FIPS140.useCompliantAlgorithms()) { + if (!useHTTPS && usePassword) { + throw new IllegalArgumentException(Messages.AmazonEC2Cloud_tlsIsRequiredInFIPSMode()); + } + } + } + + /** + * Password leak prevention when FIPS mode is requested. If FIPS mode is not requested, this method does nothing. + * Otherwise, ensure that no password can be leaked. + * @param allowSelfSignedCertificate is self-signed certificate allowed + * @throws IllegalArgumentException if FIPS mode is requested and a self-signed certificate is allowed + */ + public static void ensureNoSelfSignedCertificate(boolean allowSelfSignedCertificate) { + if (FIPS140.useCompliantAlgorithms()) { + if (allowSelfSignedCertificate) { + throw new IllegalArgumentException(Messages.AmazonEC2Cloud_selfSignedCertificateNotAllowedInFIPSMode()); + } + } + } +} diff --git a/src/main/resources/hudson/plugins/ec2/Messages.properties b/src/main/resources/hudson/plugins/ec2/Messages.properties index 0152a90c3..ccb596a08 100644 --- a/src/main/resources/hudson/plugins/ec2/Messages.properties +++ b/src/main/resources/hudson/plugins/ec2/Messages.properties @@ -10,4 +10,11 @@ EC2SpotSlave.Spot2= max bid price AmazonEC2Cloud.NonUniqName=Cloud name must be unique across EC2 clouds AmazonEC2Cloud.MalformedUrl=The URL is malformed. The default endpoint will be used +AmazonEC2Cloud.tlsIsRequiredInFIPSMode=TLS is required in FIPS mode to avoid password leak +AmazonEC2Cloud.selfSignedCertificateNotAllowedInFIPSMode=Self-signed certificate is not allowed in FIPS mode +AmazonEC2Cloud.skipTlsVerifyNotAllowedInFIPSMode=Skipping TLS verification is not allowed in FIPS mode +AmazonEC2Cloud.keyIsNotApprovedInFIPSMode=Key is not valid: {0} +AmazonEC2Cloud.invalidKeySize=Invalid key size, at least 2048 is needed in FIPS mode. +AmazonEC2Cloud.invalidKeySizeEC=Invalid curve size, at least 224 is needed in FIPS mode. +AmazonEC2Cloud.keyIsMandatory=The key is mandatory in FIPS mode. General.MissingPermission=You do not have the Overall/Administer right to modify this field diff --git a/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java new file mode 100644 index 000000000..463f4d2ac --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java @@ -0,0 +1,178 @@ +package hudson.plugins.ec2.util; + +import hudson.plugins.ec2.Messages; +import io.vavr.CheckedRunnable; +import jenkins.security.FIPS140; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.FlagRule; +import org.mockito.Mockito; + +import java.net.URL; +import java.security.Key; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.ECKey; +import java.security.interfaces.RSAPublicKey; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class FIPS140UtilsWithFIPSTest { + @ClassRule + public static FlagRule fipsSystemPropertyRule = + FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); + + private void assertInvalidKey(Key key, String expectedMessage) { + try { + FIPS140Utils.ensureKeyInFipsMode(key); + fail("Should be invalid with message: " + expectedMessage); + } catch (IllegalArgumentException e) { + assertEquals(expectedMessage, e.getMessage()); + } + } + + private void assertValidKey(Key key) { + try { + FIPS140Utils.ensureKeyInFipsMode(key); + } catch (IllegalArgumentException e) { + fail("Should be valid key but failed with message: " + e.getMessage()); + } + } + + private void assertTLSIsNonCompliant(CheckedRunnable block) { + try { + block.run(); + fail("Should be invalid with message: " + Messages.AmazonEC2Cloud_tlsIsRequiredInFIPSMode()); + } catch (IllegalArgumentException e) { + assertEquals(Messages.AmazonEC2Cloud_tlsIsRequiredInFIPSMode(), e.getMessage()); + } catch (Throwable e) { + fail("Unexpected error: " + e.getMessage()); + } + } + + private void assertTLSIsCompliant(CheckedRunnable block) { + try { + block.run(); + } catch (IllegalArgumentException e) { + fail("TLS should not be required: " + e.getMessage()); + } catch (Throwable e) { + fail("Unexpected error: " + e.getMessage()); + } + } + + @Test + public void testDSAInvalidKeyMessage() { + DSAPublicKey key = Mockito.mock(DSAPublicKey.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(key.getParams().getP().bitLength()).thenReturn(2047); + + assertInvalidKey(key, Messages.AmazonEC2Cloud_invalidKeySize()); + } + + @Test + public void testDSAValidKey() { + DSAPublicKey key = Mockito.mock(DSAPublicKey.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(key.getParams().getP().bitLength()).thenReturn(2048); + + assertValidKey(key); + } + + @Test + public void testRSAInvalidKeyMessage() { + RSAPublicKey key = Mockito.mock(RSAPublicKey.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(key.getModulus().bitLength()).thenReturn(2047); + + assertInvalidKey(key, Messages.AmazonEC2Cloud_invalidKeySize()); + } + + @Test + public void testRSAValidKey() { + RSAPublicKey key = Mockito.mock(RSAPublicKey.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(key.getModulus().bitLength()).thenReturn(2048); + + assertValidKey(key); + } + + @Test + public void testECDSAInvalidKeyMessage() { + ECKey key = Mockito.mock(ECKey.class, Mockito.withSettings().extraInterfaces(Key.class).defaultAnswer(Mockito.RETURNS_DEEP_STUBS)); + Mockito.when(key.getParams().getCurve().getField().getFieldSize()).thenReturn(223); + + assertInvalidKey((Key) key, Messages.AmazonEC2Cloud_invalidKeySizeEC()); + } + + @Test + public void testECDSAValidKey() { + ECKey key = Mockito.mock(ECKey.class, Mockito.withSettings().extraInterfaces(Key.class).defaultAnswer(Mockito.RETURNS_DEEP_STUBS)); + Mockito.when(key.getParams().getCurve().getField().getFieldSize()).thenReturn(224); + + assertValidKey((Key) key); + } + + @Test + public void testUnknownInstance() { + String message = "My mock algorithm"; + Key key = Mockito.mock(Key.class); + Mockito.when(key.getAlgorithm()).thenReturn(message); + + assertInvalidKey(key, Messages.AmazonEC2Cloud_keyIsNotApprovedInFIPSMode(message)); + } + + @Test + public void testRuntimeException() { + String message = "The test message"; + Key key = Mockito.mock(Key.class); + Mockito.when(key.getAlgorithm()).thenThrow(new RuntimeException(message)); + + assertInvalidKey(key, message); + } + + @Test + public void testTLSCheckWithHTTPAndPassword() { + assertTLSIsNonCompliant(() -> FIPS140Utils.ensureNoPasswordLeak(new URL("http://localhost"), "non-empty")); + } + + @Test + public void testTLSCheckWithHTTPSAndPassword() { + assertTLSIsCompliant(() -> FIPS140Utils.ensureNoPasswordLeak(new URL("https://localhost"), "non-empty")); + } + + @Test + public void testTLSCheckWithHTTPAndNullPassword() { + assertTLSIsCompliant(() -> FIPS140Utils.ensureNoPasswordLeak(new URL("http://localhost"), null)); + } + + @Test + public void testTLSCheckWithHTTPSAndNullPassword() { + assertTLSIsCompliant(() -> FIPS140Utils.ensureNoPasswordLeak(new URL("https://localhost"), null)); + } + + @Test + public void testTLSCheckWithHTTPAndNoPassword() { + assertTLSIsCompliant(() -> FIPS140Utils.ensureNoPasswordLeak(new URL("http://localhost"), "")); + } + + @Test + public void testTLSCheckWithHTTPSAndNoPassword() { + assertTLSIsCompliant(() -> FIPS140Utils.ensureNoPasswordLeak(new URL("https://localhost"), "")); + } + + @Test + public void testNotAllowSelfSignedCertificate() { + try{ + FIPS140Utils.ensureNoSelfSignedCertificate(false); + } catch (IllegalArgumentException e) { + fail("Not allowing self-signed certificate should be valid, but got : " + e.getMessage()); + } + } + + @Test + public void testAllowSelfSignedCertificate() { + String expectedMessage = Messages.AmazonEC2Cloud_selfSignedCertificateNotAllowedInFIPSMode(); + try { + FIPS140Utils.ensureNoSelfSignedCertificate(true); + fail("Should be invalid with message: " + expectedMessage); + } catch (IllegalArgumentException e) { + assertEquals(expectedMessage, e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithoutFIPSTest.java b/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithoutFIPSTest.java new file mode 100644 index 000000000..4650bfe06 --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithoutFIPSTest.java @@ -0,0 +1,154 @@ +package hudson.plugins.ec2.util; + +import io.vavr.CheckedRunnable; +import jenkins.security.FIPS140; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.FlagRule; +import org.mockito.Mockito; + +import java.net.URL; +import java.security.Key; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.ECKey; +import java.security.interfaces.RSAPublicKey; + +import static org.junit.Assert.fail; + +public class FIPS140UtilsWithoutFIPSTest { + @ClassRule + public static FlagRule fipsSystemPropertyRule = + FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); + + private void assertValidKey(Key key) { + try { + FIPS140Utils.ensureKeyInFipsMode(key); + } catch (IllegalArgumentException e) { + fail("Should be valid key but failed with message: " + e.getMessage()); + } + } + + private void assertTLSIsCompliant(CheckedRunnable block) { + try { + block.run(); + } catch (IllegalArgumentException e) { + fail("TLS should not be required: " + e.getMessage()); + } catch (Throwable e) { + fail("Unexpected error: " + e.getMessage()); + } + } + + @Test + public void testDSAInvalidKeyMessage() { + DSAPublicKey key = Mockito.mock(DSAPublicKey.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(key.getParams().getP().bitLength()).thenReturn(2047); + + assertValidKey(key); + } + + @Test + public void testDSAValidKey() { + DSAPublicKey key = Mockito.mock(DSAPublicKey.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(key.getParams().getP().bitLength()).thenReturn(2048); + + assertValidKey(key); + } + + @Test + public void testRSAInvalidKeyMessage() { + RSAPublicKey key = Mockito.mock(RSAPublicKey.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(key.getModulus().bitLength()).thenReturn(2047); + + assertValidKey(key); + } + + @Test + public void testRSAValidKey() { + RSAPublicKey key = Mockito.mock(RSAPublicKey.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(key.getModulus().bitLength()).thenReturn(2048); + + assertValidKey(key); + } + + @Test + public void testECDSAInvalidKeyMessage() { + ECKey key = Mockito.mock(ECKey.class, Mockito.withSettings().extraInterfaces(Key.class).defaultAnswer(Mockito.RETURNS_DEEP_STUBS)); + Mockito.when(key.getParams().getCurve().getField().getFieldSize()).thenReturn(223); + + assertValidKey((Key) key); + } + + @Test + public void testECDSAValidKey() { + ECKey key = Mockito.mock(ECKey.class, Mockito.withSettings().extraInterfaces(Key.class).defaultAnswer(Mockito.RETURNS_DEEP_STUBS)); + Mockito.when(key.getParams().getCurve().getField().getFieldSize()).thenReturn(224); + + assertValidKey((Key) key); + } + + @Test + public void testUnknownInstance() { + String message = "My mock algorithm"; + Key key = Mockito.mock(Key.class); + Mockito.when(key.getAlgorithm()).thenReturn(message); + + assertValidKey(key); + } + + @Test + public void testRuntimeException() { + String message = "The test message"; + Key key = Mockito.mock(Key.class); + Mockito.when(key.getAlgorithm()).thenThrow(new RuntimeException(message)); + + assertValidKey(key); + } + + @Test + public void testTLSCheckWithHTTPAndPassword() { + assertTLSIsCompliant(() -> FIPS140Utils.ensureNoPasswordLeak(new URL("http://localhost"), "non-empty")); + } + + @Test + public void testTLSCheckWithHTTPSAndPassword() { + assertTLSIsCompliant(() -> FIPS140Utils.ensureNoPasswordLeak(new URL("https://localhost"), "non-empty")); + } + + @Test + public void testTLSCheckWithHTTPAndNullPassword() { + assertTLSIsCompliant(() -> FIPS140Utils.ensureNoPasswordLeak(new URL("http://localhost"), null)); + } + + @Test + public void testTLSCheckWithHTTPSAndNullPassword() { + assertTLSIsCompliant(() -> FIPS140Utils.ensureNoPasswordLeak(new URL("https://localhost"), null)); + } + + @Test + public void testTLSCheckWithHTTPAndNoPassword() { + assertTLSIsCompliant(() -> FIPS140Utils.ensureNoPasswordLeak(new URL("http://localhost"), "")); + } + + @Test + public void testTLSCheckWithHTTPSAndNoPassword() { + assertTLSIsCompliant(() -> FIPS140Utils.ensureNoPasswordLeak(new URL("https://localhost"), "")); + } + + @Test + public void testNotAllowSelfSignedCertificate() { + try{ + FIPS140Utils.ensureNoSelfSignedCertificate(false); + } catch (IllegalArgumentException e) { + fail("Not allowing self-signed certificate should be valid, but got : " + e.getMessage()); + } + } + + @Test + public void testAllowSelfSignedCertificate() { + try { + FIPS140Utils.ensureNoSelfSignedCertificate(true); + } catch (IllegalArgumentException e) { + fail("Not allowing self-signed certificate should be valid, but got : " + e.getMessage()); + } + } +} \ No newline at end of file From 986fec8e7a0002b3c2fbfe451e6f586760f689b3 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Fri, 22 Nov 2024 15:15:28 +0100 Subject: [PATCH 032/267] Add FIPS compliance check for WinRM --- .../hudson/plugins/ec2/win/winrm/WinRM.java | 9 ++++ .../ec2/win/winrm/WinRMWithFIPSTest.java | 50 +++++++++++++++++++ .../ec2/win/winrm/WinRMWithoutFIPSTest.java | 50 +++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithFIPSTest.java create mode 100644 src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithoutFIPSTest.java diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/WinRM.java b/src/main/java/hudson/plugins/ec2/win/winrm/WinRM.java index ed34b5063..18123aa4c 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/WinRM.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/WinRM.java @@ -1,5 +1,9 @@ package hudson.plugins.ec2.win.winrm; +import hudson.plugins.ec2.Messages; +import hudson.plugins.ec2.util.FIPS140Utils; +import jenkins.security.FIPS140; + import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; @@ -22,6 +26,8 @@ public WinRM(String host, String username, String password) { } public WinRM(String host, String username, String password, boolean allowSelfSignedCertificate) { + FIPS140Utils.ensureNoSelfSignedCertificate(allowSelfSignedCertificate); + this.host = host; this.username = username; this.password = password; @@ -57,6 +63,8 @@ public WindowsProcess execute(String commandLine) { } public URL buildURL() { + FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); + String scheme = useHTTPS ? "https" : "http"; int port = useHTTPS ? 5986 : 5985; @@ -79,6 +87,7 @@ public boolean isUseHTTPS() { * the useHTTPS to set */ public void setUseHTTPS(boolean useHTTPS) { + FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); this.useHTTPS = useHTTPS; } diff --git a/src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithFIPSTest.java new file mode 100644 index 000000000..ae2f8bdd5 --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithFIPSTest.java @@ -0,0 +1,50 @@ +package hudson.plugins.ec2.win.winrm; + +import jenkins.security.FIPS140; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.FlagRule; + +public class WinRMWithFIPSTest { + + @ClassRule + public static FlagRule + fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); + + /** + * Self-signed certificate should not be allowed in FIPS mode, an {@link IllegalArgumentException} is expected + */ + @Test(expected = IllegalArgumentException.class) + public void testSelfSignedCertificateNotAllowed() { + new WinRM("host", "username", "password", true); + } + + /** + * Build valid URL, an {@link IllegalArgumentException} is not expected + */ + @Test + public void testBuildCompliantURL() { + WinRM winRM = new WinRM("host", "username", "password", false); + winRM.setUseHTTPS(true); + winRM.buildURL(); + } + + /** + * Self-signed certificate should not be allowed in FIPS mode, an {@link IllegalArgumentException} is expected + */ + @Test(expected = IllegalArgumentException.class) + public void testSetUseHTTPSWithPasswordLeak() { + WinRM winRM = new WinRM("host", "username", "password", false); + winRM.setUseHTTPS(false); + } + + /** + * Using HTTP without a password should be allowed, an {@link IllegalArgumentException} is not expected + */ + @Test + public void testSetUseHTTPSWithoutPasswordLeak() { + WinRM winRM = new WinRM("host", "username", null, false); + winRM.setUseHTTPS(false); + winRM.buildURL(); + } +} \ No newline at end of file diff --git a/src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithoutFIPSTest.java b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithoutFIPSTest.java new file mode 100644 index 000000000..96b05f335 --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithoutFIPSTest.java @@ -0,0 +1,50 @@ +package hudson.plugins.ec2.win.winrm; + +import jenkins.security.FIPS140; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.FlagRule; + +public class WinRMWithoutFIPSTest { + + @ClassRule + public static FlagRule + fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); + + /** + * When FIPS mode is not activated, no FIPS check should be performed, an {@link IllegalArgumentException} is not expected + */ + @Test + public void testSelfSignedCertificateNotAllowed() { + new WinRM("host", "username", "password", true); + } + + /** + * When FIPS mode is not activated, no FIPS check should be performed, an {@link IllegalArgumentException} is not expected + */ + @Test + public void testBuildCompliantURL() { + WinRM winRM = new WinRM("host", "username", "password", false); + winRM.setUseHTTPS(true); + winRM.buildURL(); + } + + /** + * When FIPS mode is not activated, no FIPS check should be performed, an {@link IllegalArgumentException} is not expected + */ + @Test + public void testSetuseHTTPSWithPasswordLeak() { + WinRM winRM = new WinRM("host", "username", "password", false); + winRM.setUseHTTPS(false); + } + + /** + * When FIPS mode is not activated, no FIPS check should be performed, an {@link IllegalArgumentException} is not expected + */ + @Test + public void testSetUseHTTPSWithoutPasswordLeak() { + WinRM winRM = new WinRM("host", "username", null, false); + winRM.setUseHTTPS(false); + winRM.buildURL(); + } +} \ No newline at end of file From 986005e1fea05f64f06354535a3b7b20def9a0cf Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Fri, 22 Nov 2024 17:07:26 +0100 Subject: [PATCH 033/267] Add FIPS compliance check for WinRMClient --- .../plugins/ec2/win/winrm/WinRMClient.java | 14 +++ .../win/winrm/WinRMClientWithFIPSTest.java | 99 +++++++++++++++++++ .../win/winrm/WinRMClientWithoutFIPSTest.java | 50 ++++++++++ 3 files changed, 163 insertions(+) create mode 100644 src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithFIPSTest.java create mode 100644 src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithoutFIPSTest.java diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java index 2e76ebbf1..c52c920b2 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java @@ -1,5 +1,7 @@ package hudson.plugins.ec2.win.winrm; +import hudson.plugins.ec2.Messages; +import hudson.plugins.ec2.util.FIPS140Utils; import hudson.plugins.ec2.win.winrm.request.RequestFactory; import hudson.plugins.ec2.win.winrm.soap.Namespaces; import hudson.remoting.FastPipedOutputStream; @@ -15,6 +17,7 @@ import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.security.FIPS140; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.ParseException; @@ -73,6 +76,9 @@ public WinRMClient(URL url, String username, String password) { } public WinRMClient(URL url, String username, String password, boolean allowSelfSignedCertificate) { + FIPS140Utils.ensureNoPasswordLeak(url, password); + FIPS140Utils.ensureNoSelfSignedCertificate(allowSelfSignedCertificate); + this.url = url; this.username = username; this.password = password; @@ -182,6 +188,13 @@ private void setupHTTPClient() { } private HttpClient buildHTTPClient() { + // This can occur if setUseHTTPS is not called + FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); + // This is a double check and should be caught by the check in the constructor + FIPS140Utils.ensureNoPasswordLeak(url, password); + // This is a double check and should be caught by the check in the constructor + FIPS140Utils.ensureNoSelfSignedCertificate(allowSelfSignedCertificate); + HttpClientBuilder builder = HttpClientBuilder.create().setDefaultCredentialsProvider(credsProvider); if(! (username.contains("\\")|| username.contains("/"))) { //user is not a domain user @@ -337,6 +350,7 @@ public void setTimeout(String timeout) { } public void setUseHTTPS(boolean useHTTPS) { + FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); this.useHTTPS = useHTTPS; } } diff --git a/src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithFIPSTest.java new file mode 100644 index 000000000..95e0fc79e --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithFIPSTest.java @@ -0,0 +1,99 @@ +package hudson.plugins.ec2.win.winrm; + +import jenkins.security.FIPS140; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.FlagRule; + +import java.net.MalformedURLException; +import java.net.URL; + +import static org.junit.Assert.fail; + +public class WinRMClientWithFIPSTest { + + @ClassRule + public static FlagRule + fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); + + /** + * Self-signed certificate should not be allowed in FIPS mode, an {@link IllegalArgumentException} is expected + */ + @Test(expected = IllegalArgumentException.class) + public void testSelfSignedCertificateNotAllowed() throws MalformedURLException { + new WinRMClient(new URL("https://localhost"), "username", "password", true); + } + + /** + * Using a password without using TLS, an {@link IllegalArgumentException} is expected + */ + @Test(expected = IllegalArgumentException.class) + public void testCreateClientWithPasswordWithoutTLS() throws MalformedURLException { + new WinRMClient(new URL("http://localhost"), "username", "password", false); + } + + /** + * Using a password with TLS, an {@link IllegalArgumentException} is not expected + */ + @Test + public void testCreateClientWithPasswordWithTLS() throws MalformedURLException { + new WinRMClient(new URL("https://localhost"), "username", "password", false); + } + + /** + * If no password is used TLS can have any value, an {@link IllegalArgumentException} is not expected + */ + @Test + public void testCreateClientWithoutPassword() throws MalformedURLException { + new WinRMClient(new URL("http://localhost"), "username", null, false); + new WinRMClient(new URL("https://localhost"), "username", null, false); + } + + /** + * It should be allowed to enable useHTTPS on any valid {@link WinRMClient}, an {@link IllegalArgumentException} is not expected + */ + @Test + public void testSetUseHTTPSTrue() throws MalformedURLException { + new WinRMClient(new URL("https://localhost"), "username", "password", false) + .setUseHTTPS(true); + new WinRMClient(new URL("http://localhost"), "username", null, false) + .setUseHTTPS(true); + new WinRMClient(new URL("https://localhost"), "username", null, false) + .setUseHTTPS(true); + } + + /** + * It should be allowed to disable useHTTPS only when no password is used, an {@link IllegalArgumentException} is not expected + */ + @Test + public void testSetUseHTTPSFalseWithoutPassword() throws MalformedURLException { + new WinRMClient(new URL("http://localhost"), "username", null, false) + .setUseHTTPS(false); + new WinRMClient(new URL("https://localhost"), "username", null, false) + .setUseHTTPS(false); + } + + /** + * It should not be allowed to disable useHTTPS only when a password is used, an {@link IllegalArgumentException} is expected + */ + @Test(expected = IllegalArgumentException.class) + public void testSetUseHTTPSFalseWithPassword() throws MalformedURLException { + new WinRMClient(new URL("https://localhost"), "username", "password", false) + .setUseHTTPS(false); + } + + /** + * Password leak prevention when setUseHTTPS is not called + */ + @Test(expected = IllegalArgumentException.class) + public void testBuildWinRMClientWithoutTLS() throws MalformedURLException { + try { + WinRMClient winRM = new WinRMClient(new URL("https://localhost"), "username", "password", false); + // do not call winRM.setUseHTTPS(false); to avoid trigger the check + winRM.openShell(); + } catch (WinRMConnectException e) { + fail("The client should not attempt to connect"); + } + } + +} \ No newline at end of file diff --git a/src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithoutFIPSTest.java b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithoutFIPSTest.java new file mode 100644 index 000000000..6d51901ad --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithoutFIPSTest.java @@ -0,0 +1,50 @@ +package hudson.plugins.ec2.win.winrm; + +import jenkins.security.FIPS140; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.FlagRule; + +import java.net.MalformedURLException; +import java.net.URL; + +import static org.junit.Assert.fail; + +public class WinRMClientWithoutFIPSTest { + + @ClassRule + public static FlagRule + fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); + + /** + * When FIPS is not enabled, it should always be allowed to create the {@link WinRMClient}, an {@link IllegalArgumentException} is not expected + */ + @Test + public void testClientCreation() throws MalformedURLException { + new WinRMClient(new URL("http://localhost"), "username", "password", true) + .setUseHTTPS(true); + new WinRMClient(new URL("https://localhost"), "username", "password", true) + .setUseHTTPS(true); + new WinRMClient(new URL("http://localhost"), "username", "password", false) + .setUseHTTPS(true); + new WinRMClient(new URL("https://localhost"), "username", "password", false) + .setUseHTTPS(true); + new WinRMClient(new URL("http://localhost"), "username", null, false) + .setUseHTTPS(true); + new WinRMClient(new URL("https://localhost"), "username", null, false) + .setUseHTTPS(true); + + new WinRMClient(new URL("http://localhost"), "username", "password", true) + .setUseHTTPS(false); + new WinRMClient(new URL("https://localhost"), "username", "password", true) + .setUseHTTPS(false); + new WinRMClient(new URL("http://localhost"), "username", "password", false) + .setUseHTTPS(false); + new WinRMClient(new URL("https://localhost"), "username", "password", false) + .setUseHTTPS(false); + new WinRMClient(new URL("http://localhost"), "username", null, false) + .setUseHTTPS(false); + new WinRMClient(new URL("https://localhost"), "username", null, false) + .setUseHTTPS(false); + } +} \ No newline at end of file From 066106e038400806d7eece8b81e0c1911b802ae7 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Fri, 22 Nov 2024 17:08:18 +0100 Subject: [PATCH 034/267] Add FIPS compliance check for HostKey --- .../plugins/ec2/ssh/verifiers/HostKey.java | 30 +++++++ .../plugins/ec2/HostKeyWithFIPSTest.java | 83 +++++++++++++++++++ .../plugins/ec2/HostKeyWithoutFIPSTest.java | 74 +++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 src/test/java/hudson/plugins/ec2/HostKeyWithFIPSTest.java create mode 100644 src/test/java/hudson/plugins/ec2/HostKeyWithoutFIPSTest.java diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java index 1f01734fe..8ccf7c037 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java @@ -27,9 +27,18 @@ package hudson.plugins.ec2.ssh.verifiers; import com.trilead.ssh2.KnownHosts; +import com.trilead.ssh2.signature.KeyAlgorithm; +import com.trilead.ssh2.signature.KeyAlgorithmManager; import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.plugins.ec2.Messages; +import hudson.plugins.ec2.util.FIPS140Utils; +import jenkins.security.FIPS140; +import java.io.IOException; import java.io.Serializable; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; import java.util.Arrays; /** @@ -45,8 +54,29 @@ public final class HostKey implements Serializable { private final String algorithm; private final byte[] key; + public static void ensurePublicKeyInFipsMode(@NonNull String algorithm, @NonNull byte[] key) { + if (!FIPS140.useCompliantAlgorithms()) { + return; + } + + KeyAlgorithm publicKeyPrivateKeyKeyAlgorithm = KeyAlgorithmManager + .getSupportedAlgorithms() + .stream() + .filter((keyAlgorithm) -> keyAlgorithm.getKeyFormat().equals(algorithm)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(Messages.AmazonEC2Cloud_keyIsNotApprovedInFIPSMode(algorithm))); + try { + Key publicKey = publicKeyPrivateKeyKeyAlgorithm.decodePublicKey(key); + FIPS140Utils.ensureKeyInFipsMode(publicKey); + } catch (RuntimeException | IOException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + public HostKey(@NonNull String algorithm, @NonNull byte[] key) { super(); + ensurePublicKeyInFipsMode(algorithm, key); + this.algorithm = algorithm; this.key = key.clone(); } diff --git a/src/test/java/hudson/plugins/ec2/HostKeyWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/HostKeyWithFIPSTest.java new file mode 100644 index 000000000..8b142f29e --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/HostKeyWithFIPSTest.java @@ -0,0 +1,83 @@ +package hudson.plugins.ec2; + +import hudson.plugins.ec2.ssh.verifiers.HostKey; +import jenkins.security.FIPS140; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.jvnet.hudson.test.FlagRule; + +import java.security.Security; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; + +import static org.junit.Assert.fail; + +@RunWith(Parameterized.class) +public class HostKeyWithFIPSTest { + + @ClassRule + public static FlagRule fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); + + public static String PUBLIC_KEY_SSH_DSS_1024 = "AAAAB3NzaC1kc3MAAACBAMsQrriFgun2KVgmsGd8drsplZLXyU8uU6r90aIZ+evRpxvoLCJf317Wnu5qVBCzGgEZ8iygYB0bDB/JFch+UVgtyXGH358ClJCDDgNWdOSogTl2gCF+W+8KoRSF+i3ObnEPOTa2akByP5FDzOO+mruVPl8kg8NHYcadCtJizRjhAAAAFQCV9uGT1Mchfbm6uFxEmZf09DwjSQAAAIAyyLw64QIHel17rzdyMyvepkvW4q64WYb7xCVLffaYJA8x1pxHtH4Mmmm0fGG7GFgdnCeD95524CYZR7TDhzKFGcEX607qKg0v5sXs6z8U8lGOeARq/IXQphb7YPZ9PdKUIuJImQEXriI0p5G7aGMmSYjnyEpKhUsM12xpDb2qBAAAAIBbIZPuZzBbbeesmzmGoG63w0tFc+tpPV3lNkAeYYcWpVhpSdHGFatr1lU+8LNT6OXekV2CFyF5kuuYw/B3OFkmHasURnT1+yC49OEpSzA3KOtQzqO2BZqIxDG/IEajKtSPGOWWPaVrHdgDXo3EZ6yCJtCiOMxW5Xz3fiUufp1sdQ=="; + public static String PUBLIC_KEY_SSH_RSA_1024 = "AAAAB3NzaC1yc2EAAAADAQABAAAAgQC8nIRjuQr9gGfGTdq7BL4l3s4n4qEe3w7imhv1cT5cy2HT7DXvnA2gGVmFn4izbWlFlQG1lrtMgIiiwXH/shRx+2FnqayNsOmRJ37TiA0ICjkOrdR4JaYWRafQ0TEC0+EdHl+3iJYOhw9scFpJ2M9kB6W5HJsf4gmXoGGz8SsfsQ=="; + public static String PUBLIC_KEY_SSH_RSA_2048 = "AAAAB3NzaC1yc2EAAAADAQABAAABAQCjw8Wgl1usvj9LCzF1c8PufEIG11V2PHCDNlYc66ccIiojQX79st1Lbp0BJXsa2bvZLYjfqyYP5gqkX7jLslmXPN+Vew91sRTmXJTlANlm/fChHg+Fq+lQK0IKGBIn9RlPDFH+NNoUIw4LbZ4etRJuOfMwiVKsOVOYuuLjiIJTkda9eS9zrhTRUXhUuMIxBLdeJEAYve6oBpcnTKpUbTV+DYlru3Yh6lSIevhA361s65oJauNHFQLQ7Ysi9apiF5hmqt1sThv/NPM/xLwlPrSGqKZWnclJbBKaWFlCijuM7W3Q5zbcdmtvKhxEJMobu+KMbt/LVhV7kD3BBLhADKnZ"; + public static String PUBLIC_KEY_SSH_RSA_3072 = "AAAAB3NzaC1yc2EAAAADAQABAAABgQDzvqmjwxE3UgKOVZDoji9npAu7Uee47sdS60cTN0yx3Aj+c5IznoBLDYt7HwUcjKoj6soRJALFMGvrKe6n4H1+9jF5vrstMB40Ga8858wweehIAEzw/ONAORZdHA2y0WG8K3+bNOVSeXZwASsjbKrYcdotsZarhQtGVks6xQwd7qXUD44DDdFuWsuj5//hSSYSIgjJE3gAfeI2qVoe6Cl6gTGoK9zd+hNrYDehpN7bDgX45ulcaMw7N2kLf+Sg5QqOYL3Xdav/SeNEefNUyE058uRK8Br3WhZh5BJ2qFjzUYe20cFKHJ3gqKiY+8aor6YrDAS5AOEAEdCw1GWHJutGeApouTSqpNZf1uHspKEgLCUu6gb+i14k3YGSqUW/3fRdqmtN5qBYGvOoqgUEG1wlsxjf6lvJxSh6551MEiM3dpXBq3wniFjK56pj9nVjW0erJsOXPIqh9KeQL1dB+fzNX0r3oAog3EEl+x+V/YLE12b8MR9qnaZwyxWPGKoRE70="; + public static String PUBLIC_KEY_SSH_RSA_4096 = "AAAAB3NzaC1yc2EAAAADAQABAAACAQCvoE/zGFhSYP/aXLGYl27P+Bq5KJ0E53Er163GJRZ119kgPTB17JOKEG1k25tmspoNYVVaSIM81zBi4RUIrP7ft+1wj2FlsMchrEHlrqR31HCCsPmf/YGgzaBBgL2KDNHEsnxIzyZTsY4ZGPS4LZMP8McUXfwvOFkVs12AUNH5hrB0SgMv6sor1VyW43p6u1o1w9MX5omANopOv+Rqm7In0UXNmOocOhOFYqDJVKt05+fI+fduHIwO4Wi3e0K1jK1EmC9YlIJJIz0Ce1+CyGK0Cm7lHj+W2Ea5tERO0DsK/etGbn1w8NcW9XmPVzO4vSvsMm7XrL0hIdNQZKSxas4NNwxr0TZN70T+H3WKRK9VAxCEp5IdahsSevKyrcsRnKX3mcemqJZZ+ODAarPdHemNacywzoaEt2AOSOl1PcW/sA49R4yMYHj8RS6xDv9jeA4Vogj58ynqzaB2F4fCkaV4bmgb2vL0Fkw96Tvq0+Gs902zvtDmnneicCWhNnj+3jRKZjqiQRvA3/BgYrokFGcDra4j9C1vrDVajMitcY+dr0XeA+n9ot29GSx36Fwg3j3QUhamS6/nsKTeIdmEHeym7FT6LKweKL/XcUCs+tkaJFxsJ+S1E+vF2M7SmqkNuB0S17EijZtw01v1zbzocscnfpLXo3UEfBdIe7pjT/IGtw=="; + public static String PUBLIC_KEY_ECDSA_SHA2_NISTP256_256 = "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB1ZWbuchcMR2NMc4wu4sB2sFvnxZ45tAyijmSx9pUkQ51InNU7t1qzf2p29VhwdJ7kQSX3HdUcwBP1NfUSEoFw="; + public static String PUBLIC_KEY_ECDSA_SHA2_NISTP384_384 = "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEfdfV0luXJ0NJGMpeAMDZvfDjfwpC9U+YDSlgM0Gh2eCCtYCGv41G4tZd5+L1gjPEiS4Y8r+jb3JoAX6JdQfHecK6+NHpZsF0uwrn8zTfA9PT+I9nTtEyBgNWM/v/A5wQ=="; + public static String PUBLIC_KEY_ECDSA_SHA2_NISTP521_521 = "AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFrY2jC8sarrQqI13e9fDhzeUvTFt5j2krHfFfqDrP/M7L5RJzbg4jOSOly7FdOi7JhFkYaEguddhRh2DIUWKHR9ADR9/m4n9WxHR9QaVLUYUyZdQzgdtlY6KfLYJyO5PBSulMhpfDKGoycNKmr6Av1gyESAIBq+bINsgpUby+h9jkC7Q=="; + + private final String description; + + private final String algorithm; + + private final String publicKey; + + private final boolean isValid; + + public HostKeyWithFIPSTest(String description, String algorithm, String publicKey, boolean isValid) { + this.description = description; + this.algorithm = algorithm; + this.publicKey = publicKey; + this.isValid = isValid; + } + + @Before + public void before() { + // Add provider manually to avoid requiring jenkinsrule + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + } + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][]{ + { "SSH-DSS with key size 1024", "ssh-dss", PUBLIC_KEY_SSH_DSS_1024, false }, + { "SSH-RSA with key size 1024", "ssh-rsa", PUBLIC_KEY_SSH_RSA_1024, false }, + { "SSH-RSA with key size 2048", "ssh-rsa", PUBLIC_KEY_SSH_RSA_2048, true }, + { "SSH-RSA with key size 3072", "ssh-rsa", PUBLIC_KEY_SSH_RSA_3072, true }, + { "SSH-RSA with key size 4096", "ssh-rsa", PUBLIC_KEY_SSH_RSA_4096, true }, + { "ECDSA-SHA2-NISTP256 with key size 256", "ecdsa-sha2-nistp256", PUBLIC_KEY_ECDSA_SHA2_NISTP256_256, true }, + { "ECDSA-SHA2-NISTP384 with key size 384", "ecdsa-sha2-nistp384", PUBLIC_KEY_ECDSA_SHA2_NISTP384_384, true }, + { "ECDSA-SHA2-NISTP521 with key size 521", "ecdsa-sha2-nistp521", PUBLIC_KEY_ECDSA_SHA2_NISTP521_521, true } + }); + } + + + @Test + public void testPublicKeyValidation() { + try { + new HostKey(algorithm, Base64.getDecoder().decode(publicKey)); + if (!isValid) { + fail(description + " should not be valid"); + } + } catch (IllegalArgumentException e) { + if (isValid) { + fail(description + " should be valid"); + } + } + } +} diff --git a/src/test/java/hudson/plugins/ec2/HostKeyWithoutFIPSTest.java b/src/test/java/hudson/plugins/ec2/HostKeyWithoutFIPSTest.java new file mode 100644 index 000000000..c6533284a --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/HostKeyWithoutFIPSTest.java @@ -0,0 +1,74 @@ +package hudson.plugins.ec2; + +import hudson.plugins.ec2.ssh.verifiers.HostKey; +import jenkins.security.FIPS140; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.jvnet.hudson.test.FlagRule; + +import java.security.Security; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; + +import static hudson.plugins.ec2.HostKeyWithFIPSTest.PUBLIC_KEY_ECDSA_SHA2_NISTP256_256; +import static hudson.plugins.ec2.HostKeyWithFIPSTest.PUBLIC_KEY_ECDSA_SHA2_NISTP384_384; +import static hudson.plugins.ec2.HostKeyWithFIPSTest.PUBLIC_KEY_ECDSA_SHA2_NISTP521_521; +import static hudson.plugins.ec2.HostKeyWithFIPSTest.PUBLIC_KEY_SSH_DSS_1024; +import static hudson.plugins.ec2.HostKeyWithFIPSTest.PUBLIC_KEY_SSH_RSA_1024; +import static hudson.plugins.ec2.HostKeyWithFIPSTest.PUBLIC_KEY_SSH_RSA_2048; +import static hudson.plugins.ec2.HostKeyWithFIPSTest.PUBLIC_KEY_SSH_RSA_3072; +import static hudson.plugins.ec2.HostKeyWithFIPSTest.PUBLIC_KEY_SSH_RSA_4096; +import static org.junit.Assert.fail; + +@RunWith(Parameterized.class) +public class HostKeyWithoutFIPSTest { + + @ClassRule + public static FlagRule fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); + + private final String description; + + private final String algorithm; + + private final String publicKey; + + public HostKeyWithoutFIPSTest(String description, String algorithm, String publicKey) { + this.description = description; + this.algorithm = algorithm; + this.publicKey = publicKey; + } + + @Before + public void before() { + // Add provider manually to avoid requiring jenkinsrule + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + } + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][]{ + { "SSH-DSS with key size 1024", "ssh-dss", PUBLIC_KEY_SSH_DSS_1024 }, + { "SSH-RSA with key size 1024", "ssh-rsa", PUBLIC_KEY_SSH_RSA_1024 }, + { "SSH-RSA with key size 2048", "ssh-rsa", PUBLIC_KEY_SSH_RSA_2048 }, + { "SSH-RSA with key size 3072", "ssh-rsa", PUBLIC_KEY_SSH_RSA_3072 }, + { "SSH-RSA with key size 4096", "ssh-rsa", PUBLIC_KEY_SSH_RSA_4096 }, + { "ECDSA-SHA2-NISTP256 with key size 256", "ecdsa-sha2-nistp256", PUBLIC_KEY_ECDSA_SHA2_NISTP256_256 }, + { "ECDSA-SHA2-NISTP384 with key size 384", "ecdsa-sha2-nistp384", PUBLIC_KEY_ECDSA_SHA2_NISTP384_384 }, + { "ECDSA-SHA2-NISTP521 with key size 521", "ecdsa-sha2-nistp521", PUBLIC_KEY_ECDSA_SHA2_NISTP521_521 } + }); + } + + + @Test + public void testPublicKeyValidation() { + try { + new HostKey(algorithm, Base64.getDecoder().decode(publicKey)); + } catch (IllegalArgumentException e) { + fail(description + " should be valid"); + } + } +} From 89b9443b1780a14746f974547ebe01c85e7c0764 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Mon, 25 Nov 2024 10:02:02 +0100 Subject: [PATCH 035/267] Add FIPS compliance check for EC2Cloud --- .../java/hudson/plugins/ec2/EC2Cloud.java | 53 +++- .../ec2/EC2CloudPrivateKeyWithFIPSTest.java | 239 ++++++++++++++++++ .../EC2CloudPrivateKeyWithoutFIPSTest.java | 59 +++++ 3 files changed, 349 insertions(+), 2 deletions(-) create mode 100644 src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithFIPSTest.java create mode 100644 src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithoutFIPSTest.java diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 76f8d112c..6aecdb95d 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -59,6 +59,7 @@ import hudson.model.PeriodicWork; import hudson.model.TaskListener; import hudson.plugins.ec2.util.AmazonEC2Factory; +import hudson.plugins.ec2.util.FIPS140Utils; import hudson.security.ACL; import hudson.slaves.Cloud; import hudson.slaves.NodeProvisioner.PlannedNode; @@ -67,8 +68,10 @@ import hudson.util.ListBoxModel; import hudson.util.Secret; import hudson.util.StreamTaskListener; +import jenkins.bouncycastle.api.PEMEncodable; import jenkins.model.Jenkins; import jenkins.model.JenkinsLocationConfiguration; +import jenkins.security.FIPS140; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.HttpResponse; @@ -87,6 +90,8 @@ import java.net.MalformedURLException; import java.net.Proxy; import java.net.URL; +import java.security.Key; +import java.security.UnrecoverableKeyException; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; @@ -198,7 +203,9 @@ public EC2PrivateKey resolvePrivateKey(){ if (sshKeysCredentialsId != null) { SSHUserPrivateKey privateKeyCredential = getSshCredential(sshKeysCredentialsId, Jenkins.get()); if (privateKeyCredential != null) { - return new EC2PrivateKey(privateKeyCredential.getPrivateKey()); + String privateKey = privateKeyCredential.getPrivateKey(); + ensurePrivateKeyInFipsMode(privateKey); + return new EC2PrivateKey(privateKey); } } return null; @@ -266,7 +273,9 @@ protected Object readResolve() { t.parent = this; if (this.sshKeysCredentialsId == null && this.privateKey != null ){ - migratePrivateSshKeyToCredential(this.privateKey.getPrivateKey()); + String privateKey = this.privateKey.getPrivateKey(); + ensurePrivateKeyInFipsMode(privateKey); + migratePrivateSshKeyToCredential(privateKey); } this.privateKey = null; // This enforces it not to be persisted and that CasC will never output privateKey on export @@ -1098,6 +1107,33 @@ private static SSHUserPrivateKey getSshCredential(String id, ItemGroup context){ return credential; } + /** + * Checks if the private key is allowed when FIPS mode is requested. + * Allowed private key with the following algorithms and sizes: + *
    + *
  • DSA with key size >= 2048
  • + *
  • RSA with key size >= 2048
  • + *
  • Elliptic curve (ED25519) with field size >= 224
  • + *
+ * If the private key is valid and allowed or not in FIPS mode method will just exit. + * If not it will throw an {@link IllegalArgumentException}. + * @param privateKeyString String containing the private key PEM. + */ + public static void ensurePrivateKeyInFipsMode(String privateKeyString) { + if (!FIPS140.useCompliantAlgorithms()) { + return; + } + if (StringUtils.isBlank(privateKeyString)) { + throw new IllegalArgumentException(Messages.AmazonEC2Cloud_keyIsMandatory()); + } + try { + Key privateKey = PEMEncodable.decode(privateKeyString).toPrivateKey(); + FIPS140Utils.ensureKeyInFipsMode(privateKey); + } catch (RuntimeException | UnrecoverableKeyException | IOException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + public static abstract class DescriptorImpl extends Descriptor { public InstanceType[] getInstanceTypes() { @@ -1163,6 +1199,13 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont if (!hasEnd) return FormValidation .error("The private key is missing the trailing 'END RSA PRIVATE KEY' marker. Copy&paste error?"); + + try { + ensurePrivateKeyInFipsMode(privateKey); + } catch (IllegalArgumentException ex) { + return FormValidation.error(ex, ex.getLocalizedMessage()); + } + return FormValidation.ok(); } @@ -1209,6 +1252,12 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL + pk.getFingerprint() + ")"); } + try { + ensurePrivateKeyInFipsMode(privateKey); + } catch (IllegalArgumentException ex) { + return FormValidation.error(ex, ex.getLocalizedMessage()); + } + return FormValidation.ok(Messages.EC2Cloud_Success()); } catch (AmazonClientException e) { LOGGER.log(Level.WARNING, "Failed to check EC2 credential", e); diff --git a/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithFIPSTest.java new file mode 100644 index 000000000..c7c369148 --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithFIPSTest.java @@ -0,0 +1,239 @@ +package hudson.plugins.ec2; + +import jenkins.security.FIPS140; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.jvnet.hudson.test.FlagRule; + +import java.security.Security; +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.Assert.fail; + +@RunWith(Parameterized.class) +public class EC2CloudPrivateKeyWithFIPSTest { + + @ClassRule + public static FlagRule fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); + + public static String PRIVATE_KEY_DSA_1024 = "-----BEGIN DSA PRIVATE KEY-----\n" + + "MIIBuwIBAAKBgQDdxPbkTmoDD5cK4W9bE5OVKO1Qu7vw32ZOAFHEdOCjK/JxLEyu\n" + + "yV/nlywOmG3fjMKjQdaFDggg9TAZx6Kjor/lXSnpxbWvS5/Blv6QXWMsJZCmVSen\n" + + "lm4Qk3bKv1Y9P6SguJQXi+RmSQW5otoF5PikPK3Kd72LoWLioFfOUyQkuQIVAIfp\n" + + "1tgKiJUxzGooCovDKjNeiXMTAoGAR/BjnA3F2QYUlPVuQy9K2v5epJ020xHzJp/q\n" + + "h71qhTOTMgqT+hvsijfuK4yGwa0S+Lw1Li7sr0uQN+Bk6ayPyRxx+Lo91tz74DY3\n" + + "hkDwL8ocWjD7LIAo+Qdzc7Bme398XDHY7GVBrwcK6/YMCtQUevhxkVpd4apjsJtd\n" + + "wOK2SvMCgYEAsZ5gj+BrmqEGVOD8PfKbJH5H3BOLptvhdwJ1jNVnkH8CVpwWs6+R\n" + + "1ZzW3VVGSEqChuL1YvchfBSRRtxlVvUbFeSvxJPz2f+KmA8AXnJoStIY2WL1MFhs\n" + + "3ag3NN0XYO9X0pmwT109VUh3vFRtwzyG+YYUTx5pgv/yuEr7qMirt2kCFD7Av2KN\n" + + "uaL7v9mj+BwjsANb3l9A\n" + + "-----END DSA PRIVATE KEY-----\n"; + + public static String PRIVATE_KEY_RSA_1024 = "-----BEGIN RSA PRIVATE KEY-----\n" + + "MIICXQIBAAKBgQC/+fn2CosvVV26//CaY4tLHWxoK1XjSp2HYF6lxDgMWw4Rq+Rx\n" + + "ds62LC2oHjsO/YHtKmwh+C61jk0UNAENer5V9SiHCStzB/l8qLG4jHZb4JggpRsn\n" + + "ArTy1M0//0rhHbm6Adfr/Lr5aO3PM+TvusTpY3ZJmh7EUFSNyMEUCamfywIDAQAB\n" + + "AoGAWG3hQgBhVJBR+I1kWvl3dEY9ZU5w5Z29KlqtvlXAK5DVzjYLqGg9l5SKA2LJ\n" + + "eYI0kvZzkMItYdwGjUPXKEpd2ZRv8HZlyPWYhMilguT2Wd4cW0xNE+r9Rjgww9g/\n" + + "XaPycCKlIQlfWosWZaLK7hgmVDfPvIjVLBDpQrkdISy6o2ECQQD80i/oKkv4rkQ7\n" + + "TId6aA8PYroAcfSOOJJg1RyVYjEo2e1KD7HOkbzHtn7mnJ4SAbLnsHIZZQRLtORl\n" + + "wMvZDW53AkEAwmPvWHIl6WRNNfuztluDflrwdSst+hbxhQpAcPidNEV+UV1Ye/jV\n" + + "JpWkJNgaAMiJB0oP39Mu2nubZCfZHhdKTQJBAKuPFeM9kIAYAUUcEXMG2fFe1Uko\n" + + "CwPXb7014Eeeci1+dH8lV0sNqkT7mfFzpfAiJv0BxutkmR2mirZhtfJ8ItECQBB8\n" + + "VTIVDC4M+ZdYb1dJz48Ju1bUgKOzCmyT//8UtpBWTG4uEnEBG2KYUkFlql7iouxh\n" + + "VZNP36tbzEPkNT+eDgkCQQDlTonqWmXPaCij3LVbmBwUb6asaxmmf7fh+T+Vaizo\n" + + "mSwthk8w562jnJgQ5bsOA+PpgPckJJjqp5KbmAIw3xCb\n" + + "-----END RSA PRIVATE KEY-----\n"; + + public static String PRIVATE_KEY_RSA_2048 = "-----BEGIN RSA PRIVATE KEY-----\n" + + "MIIEogIBAAKCAQEAuKaL32E+cTIvWlfX6U4Q+ky+9/PREwVw/HA0nNWdf1bV8qxU\n" + + "8XQh/JFHuGr8Jr5i64VE70rlvE/q/jM3m6G+s5INYjG5x0VXXtNEQoab12/WELFJ\n" + + "S/G8RETtOZmkQJ4bBopNCtkEEw5x5JaFAzgdkIhXUc6ebjmNmJdaQ0BQWq6Sqa1x\n" + + "wv5KfE5I7zZnXJtVV29SXDdnK0YdKz/f0oiXTceNNOCkP/FePfS95/1j2YTt1TQg\n" + + "YaU3qXoZxs/eeH1LTyQVVmdeoUxRoGQB2U5wFXC2XAzZ1cFcWw+pZ6HDOiRnxV4c\n" + + "IAnwvqoGF999Mzsa2QK3ZG4yH/GO6YYBxVgcQwIDAQABAoIBADy0tIOKCudYIm1H\n" + + "N/rB6Z8AoEAQboocXdsAYKu3JwJ4X/paYcrH7WyFrtiYg7GRIiBgPhuVuhXBCHbu\n" + + "C7gk4vdSawf/ZR54U5MfTe+5JX8ci3oNbxWCseyX5I3tTyzXTfdGfLG2Szqgox0N\n" + + "x0kZp86epGaG0VtXnI+wIsK9YC2Pa42bakBZnFr8+zzHNhujwc9B2CAuFyE8KUda\n" + + "BONWQpPe+jiPgKOixb6KXzNk+QGRdi4kPIY4yToz+aWjMKPtEZDJuEc0zjv+uSaA\n" + + "cdxBu2hc1OLflQtbfamoEM7KXDYtVrWlGCsGpVFoPzdD55okFAgrtdIFBajfjTHZ\n" + + "BlOJNHECgYEA7+OdWHO80XJDXVz23JPpJBCi2kxL0WTqqApQ3jVphWA3Bb5fDb5Q\n" + + "J7Ek5je92BipPJkkN0THoChVmF7hLTI19YSo6tcQ62POPXu0mPENeKelWvI7YW8O\n" + + "+CpIvxl9RRwjkw2jLjWMInVVoCwU+//UBSUpW9cIMa7Joy0mDOS2swcCgYEAxQ04\n" + + "zoelsn3G4jw4bSo+yBbHDu8TuqnHVgE33HS13ysdxqeOeh6jvUVzK2KKcUZakQpS\n" + + "LZGfOF51OwY1YmpOlsLQalZ/8ntuefo5rtuFhu7lgWPrX4KQimNTCnJR5F0NV/Md\n" + + "1WovSdSKBilZErOn7qgjDBFkfBUtpZ43gnmlkeUCgYAV06T+ZlF40Se19/5yJXci\n" + + "E+1tZWHEpKUBMycWgM+gFhgLir3FV1qdse2EkO/SGLRVUi3MZZKwTNs06PUeEqJ8\n" + + "O1zPOVBNyp/6UiYlgFFUeBSAiOfEPsGi7N3/nUcboarO93+wdajRfdGTqE8kequE\n" + + "6FOyCoexVZD9Kt96btj8wwKBgCSJpxbkoBzQpagdcnkLdEi1sINcYVQjVwrjfvAp\n" + + "0+9ll0fWmdybAdF+pzRMOU93tCNgvowkjFlval1fcVamT5w002BkWaUkrf+AHmIF\n" + + "4mR6t6OeW26CTzrZ3/P37qdhea/tLIL+BXazKkSqNhH5rhHaq2T5dKBtbOFgzPos\n" + + "hD7hAoGAcOsgmYOU+AztBOic1mnRpOeTxR/A0QGrB/9wfzfwhp6lVNnkrnDFonHx\n" + + "264wgC6h9O2mLZk/85CAaod0YOv2wUziRsEoGDxnSR+QlS1VjDc8uau7R60l7flf\n" + + "7VOBXk8FUPgW87bydSqXfwgGqoG/WAmLsqqeXDe5DYg9wV9vPnk=\n" + + "-----END RSA PRIVATE KEY-----\n"; + + public static String PRIVATE_KEY_RSA_3072 = "-----BEGIN RSA PRIVATE KEY-----\n" + + "MIIG4wIBAAKCAYEAxiHReDpOX+whDtLOFJ1lcisLs3qgnlrN2EocdQ6Ir64MkrE6\n" + + "9xnmFW2ePzyxpamWKGAkj5Bh97/4ShCVYr7lfzTy8KrrOOyy6Dxv7FL+RjljGNjE\n" + + "PAJVBai4cbUgJwIugoxXZimDRaeTpQTmDLxpX0mbALK5A0lKK8RWfiz+JQ9C0e8R\n" + + "+dn1emimlu4JuetJshTKCkpELc1t3jgFK4rhWZMQTqCG3ahIffrN9LoBjH3h958t\n" + + "urE1LIDwNGRbGLT4KdW/fvLpgOrCWxGql+bkbYYskAqnVOV0+QItjUjjgPp21SaF\n" + + "2l8qliF76q7VhVHSK2VQvWr4drT3RzbFdNusrSETYR8CKxk+ls+Fd3lwmnUGAIzH\n" + + "GoOpMyfB1x+/NFSAAeDh8HN1D/C6tdmTqV2hgbQGdiaaeCao69v0CUoeAqj9YMKi\n" + + "hjOU+3VpAZOGng9ZhFcr1S8wG2jWqPf4yt6WnJBBRL8hONquAz/8Jgq5dVWkXo/y\n" + + "a/7RDI14LH+JnXTdAgMBAAECggGACPFy1otEiWDhAchzI+FnjavdZy20UqGE+WvS\n" + + "9VesGh3wPwN/8V0PvSI0X+SvR2vXCsiC2KDAQ0y8z38IdytoXtcMK4p+JpTMjd4D\n" + + "K/bNDRdps9qTn6Uiov2WzGR2veauecG4RN9qfPo6QwXBv+EV8oah1/oOjtQ/5FIu\n" + + "7fAS9/zvMQb/rThGM88bIeSUYiTO2ppvSjVITDb3FsOO5tm/H+oa9NRFx3s2lnUm\n" + + "gb2O9Kv5R1I94AJABQZm5pPXEXpC8DJu/1/n9wsKY0BEg38zc3Zq/yfHNmv9Ysz0\n" + + "rychuehMpKxTTU7+fUJMqpshm+Pdmo4jc+Mpfx+fc5+hKHI/9fP9sfONyyUjSKwL\n" + + "YTH3RWVuVT+9vzhPs6JYkc/ZXgzvgcNBTqTENTkewHqf+SeDfYt+YasBDizupmMc\n" + + "aGLIIyxnxUmms30hwmcxtBTIDhACLhw/FS/1lnlKQhwOCeMwtSyFUqMfnCPer+rO\n" + + "8jU1g5Lfs7sB1hlOr2B4hSIrpafTAoHBAP7biHCa3fRI/Jy/Cf/1laEWEoBG8HBH\n" + + "VOzW9bQUrc4bmyohqPXTL1jQA42/095hSG+3XmJEHiXycAGWXn5ilh234ijlrEzl\n" + + "KqcLDMP2iEGtzRgwiG50j6/xN5JKBJIw/M4eDEGyY9psfCgp8na1ztuJ8sHL0e5N\n" + + "fsB7KKVg4TU16X5kGuNNMUgMxFYXm9vskADCQ/AlyFdUpFglPkwU8GuAwlfLExDs\n" + + "kWsxryt8Xe2V+4qF4KbYafuZ/eJwEF7dnwKBwQDHBTBWQK3COoWMegX+4L1pWK6L\n" + + "KUuJhEffxeU8jbAxXHASfquRV3vF3jDYh8Y9kJgtgIMxbfSdBSwUpvvVglsgKvUF\n" + + "hM5X6eA7HY9eT8Ps43UyM5JCPofIpJZIx4y/OAR9hzs7VQjm8Eq0Ke3rm3bEbQlZ\n" + + "Vrm5fVxdKMq90wJVi2Z4VefXpWttv33N0BvXDwVGKLMYtkKAdVi4J13FxiaHJeyl\n" + + "Dr72ogO/HVsnzZlhyB3V+5jV6XQs7u3ITlIZpAMCgcEAv0m7oPk8euyFXoktUkbc\n" + + "ZioQ/ONB+KQxpAq8JMwYoEisL/VPwiMeuIR5Bl3jAlj2a5OwbgU+s7DCTQ62IhqR\n" + + "HgE06QlqR9UCLJrom/Vg1BtFg1B6Np2ac66TzWNtBuVp+rMm8/CXbgxbLDI/4MYZ\n" + + "W0KxSLBZA4p7BrHqEicjIjMy7EDqxYzc3n1mqE/UFj/63fbx00AonRPUvqxFlAlr\n" + + "YuUj+Y1c5CkMBO8n0XXpcjhOsuxFcDWjZstwehMu1mV1AoHAexbNd3sXPHpfYKuT\n" + + "i7jJzR7pDO6kZk/m+BJ4HgRvxYerVPT8/a5CwfUS9si6phcI15OVEHw1/utVAQzp\n" + + "0nqGC5Yl5pzl1d+zLDyzEBx7S8a+FCdrPQdZiZGp1Sd9+EIYHN8HlkGYeOSC/3yz\n" + + "RrXnNcNONe/6fCt5dbCl+9NGrUvDO4e+FVSc5cq6bxFYNqF2nJbNdeo7pSFulq7a\n" + + "Q1izOYEOJGPDXdyEPq5UU4DIbX6MXWz3cM2raaL4c5tlEbCrAoHAPa5Es7NUrA9p\n" + + "1UhLIihKJtUz1jpQTLm/yfEW3lwKosCmJVFph/2HkdPR2d6iielrMdpN3Cklpf72\n" + + "QDT6IROxw4kQ2r4cJV2mdzfxiw1sDlgoeKwEpKeaKq3zHMWH4GinFSdCrxm8DABK\n" + + "C1A1t+Y6MjfssXmE2UibzpOaeEEgBlfehkFrS/RKlpb14I6Pnt1gNsEhenvQS3Q2\n" + + "LfA3a4Vx+HNkoeqTfGhjbtZnae2C3RWMrn6FMPA2fXeVVwpQVMTF\n" + + "-----END RSA PRIVATE KEY-----\n"; + + public static String PRIVATE_KEY_RSA_4096 = "-----BEGIN RSA PRIVATE KEY-----\n" + + "MIIJKQIBAAKCAgEA34JSqlJeo9Kipy82+zprwJs9wLyDZJ5S4Obskz0kbNB3JQgQ\n" + + "J4lUkSCzOuLOOChGCxpuKgd2MmL1P4YmsHU77FMFisZlJ301Ts4EuYpn0uS8106E\n" + + "muBcsvR5cjzP+XB5Tsf3jJst4M+BngiP5Brk1Pi/WtQJceTX26YECvKPobiku1jd\n" + + "SOHrrIYxaPvsJPTAoRjyzT6NfNp5lKOk4cX32cwJsmUNrw0E2l8/w82H88alRofV\n" + + "E+0SYCIljFdve43em1AzIA9V8TYzU+/8NIptw3pvoqi3nG4WEfS9agX8cz4HZA5Q\n" + + "PiKtbP4Ncnvmlb+7b6u9GxIqPdXn/miiRFz2Feqr2daPFVHzm+rigNGtZspXP8qV\n" + + "PwX6SfB1vyP8NVmo6Fn9b+svsve/AFXb8M74QHU42DWH7831+bY9XTxewDkX3eNU\n" + + "B3Vrrbn1jkwmbjoTMLogae/V7Hv2R3Y6hJkfirDBgzysE/xXDpyqw2d3VsA40klz\n" + + "8ZsaoDNvIOWA0cuIov1WxwKoIEWwS0iAnbRn1hVl5eaEuCVkR/3osKVKyHRCsjPA\n" + + "CIWR1UK2q3atbaQTZWsd9Vd/WVkGU38/a47tfeyd4T/I2kckfzoe5b0YJfCKo8t+\n" + + "KtffL26CKiQPVAwIlxj9qIi6iKrONamW/h4mkirXUFQEoD0gQFAe7OmDo3UCAwEA\n" + + "AQKCAgAWGQzyQP2RZeBl9iGR+icwHkkfNqQo/QxFpx8puYhR59R3yVHLjuTZCmod\n" + + "/tGTtMukCmNs7VruxWDo/Grz1Ett5JFuNIpIurTcCztlWr1EGRBgmyc6JseTe99L\n" + + "/54yU7/ynYuoj6kcCngOt1r+mvgX5FYK9V7Pr2f2E/ZfXLb9rsM+sJ0EOS3zWzsk\n" + + "XY/t4XLwGoba6v3TI8iUfQ9usQN9uZIV3K7bKUbbDkLAKaBw5iluDTzwYOaJsaaT\n" + + "twtTJGYnZekAGPQOyqSNfnMpgKw0gPTrmJG6uhmdgdx+UIQyoqXZax7c3dHWrlGH\n" + + "CZ/1G0U9V7X1KLkbbvwmZ5Lvfl9oAPRiSmzyHiedOTmJ2E8U4y8fY2mZ87TVvr0g\n" + + "Z0/ivNTdajypB/xjx0zK46+nhSSMjBXHKV+IgwOFA19AVtxLBKgxqsGg0C/lzMRg\n" + + "GgTKYFDy4m/tEsAYuSYpTB+tLAgz0JQhbk0eMNPm+jpIYxx/jTDquxfjvRX54A0N\n" + + "syZZxrnKeAppcgEvVTkex+hf1e2kKH4QCGRYFFG/lMwgQ9cFWy7OIvMymN4OfIer\n" + + "Sdfpg7aG8XTtvEqFkDi1GIkOBafJ8zsozQSw0nINFA8ax3EohKQWLp0uGqk97yO6\n" + + "4A/rlqSwTFcDKk2qr+W+6EQoZe9Nw3VlKpasB1fUPZ19BnzqmwKCAQEA4zVulNFS\n" + + "Z56twfk45e9NZ6DjUqRuzP/kcW44jy+8ChpUpPvbIqx9iU1RKBzvpDfzQkzUsCq0\n" + + "zrtMXS47VTsi0Pi7h39yiV26ZmNkPV2BO+dnYjN6jMnEg/YoeiGAg0kWvBGEgLM/\n" + + "lwUvBXRxGHN4rqJ+RPhqIMbt/ucNVlYakLAN0iiVPQKzvkihXzziYhpvhuZ+Hd1v\n" + + "/6qboTvZYCM+gE+f6gV/5DSsrGt0EoTNKEbQAv45Riwq8+j0FlpJkW8Neu0eyydn\n" + + "GVgvDFVT656hYsB+Q07N/5E1vMIILBuO8WUdCwdFWk7Wx5iWgp1zlUp+6KJNdMKN\n" + + "C+e0xjoGp4nkAwKCAQEA+9TgNizrOwUqfytWbUG3ZGktVSc5EWDnsijOfY5N3J7d\n" + + "Y0I1Kkf+Yzh8uexQQLIhR3QxOiaS8VIa5DhK7cV9K078oR4vRaw+pO7q2OD4pNPu\n" + + "CEM+GiBiycyLOpAVxvZm90cJ1sv8rXXBZobMGZppMWS1K5eHM8ZVaq6HWk0wwdis\n" + + "fFtaoL1LPvyDuXPI+s9h7cPz8c6iVyh7F5rXjEycTAJZUMSRM4WuV5YnHOpnzXua\n" + + "cd44g1af0PvlVh9Jv3jOFIX9Vkm0zF+VN4TosntvgN2w34F+ZQDFRTFESmBlA97N\n" + + "W82YDUyAsi5hJF1VvyMqC4owK4l7tpwQxBUJL09NJwKCAQEAywfdJ+ig7W1TMcmF\n" + + "uZqMnbScTiYXyOJFfcMTkYgDTTfYOZHBcQuYJlBL3D93OVSx8KX9TOrspOujwoRk\n" + + "irYMV5Zc5SjS7cMupP3d/iQHLsOKk6sSsKpAC/e0leZIE4kFYst4jxUeFtKQARzb\n" + + "TxEoX01e7jzZgS6iT6yiM2s/09kukISpT1qRydDXOuaKGUYsMOzY99D/mwQWjA6S\n" + + "IaF84WXFrXZ6oS8cufpPP5kiRwJ4MKSCA53GSCz7qNnHcck9z4ICiWFNdM1jRW8e\n" + + "Tadz6W2/pl/OHrjgvyrX6Ko7oqRLPqahp6BZtwQ4QsF2HoryOumFs3eCWIgV9yi0\n" + + "95N1hwKCAQBnS+BUGIS8htfxpdMjqasR8tp3bUlJSZiASaC5e5+QeVGSH1wzZaiB\n" + + "BnCSys34W5iu+IggtCXd+rGxHy4M7c7z7shNRlZZm9duS9nk8BLNeWjP1tUoXlRn\n" + + "NhF+ChAEtplxoJ/2jWGtvPmBlpUtg1rWudpecR8yK45p3gEDF1qCiN/neoloGX09\n" + + "7tIRRd8QkfQ3VQNBEmMgoSgsfIUhtWL/Ao+kQ5zTp2fl4V9VywidDrBBOMexh9yy\n" + + "GkDt3JOhiGnvnS5XMJCKrEJGravNWjhYgZbFdxZjU7eXNCgw4e1NcxyFJYXTHqhD\n" + + "bibGhcpgRoo+hYZQtWobc1SlOYO09jBNAoIBAQDZJO9+MsiaOP2v/9A5h2sQNjgH\n" + + "LjrstO90BjhjQlxDBUNQGgPCm11d3KzSlmHcencCN1jU+e96x5cTHFSUGRwu9I0n\n" + + "Z1WTWmOBy/jlHfNqnNU1mZcPaEiPg7XvIHLijpcAIFDU2JVDNAUvwjV2qI58G+Nn\n" + + "zB8AAVXmKbRq/ZItSXo1lpEim2b43oG27ncmnMH7wwi5nl7d1K//Igk2BLA8CMNI\n" + + "JgGiOVyzUhk/BmuRSQbCUUGKdHF+MXjVRd4HdwlV3WXQPU0p3NcHE+k789Isqitf\n" + + "Hhf/srx3by2hLPDGDvi9IiKsJQFuw5eaAQ3v4XIp2kWK9r2Gq4onOPlwQE9y\n" + + "-----END RSA PRIVATE KEY-----\n"; + + public static String PRIVATE_KEY_ECDSA_256 = "-----BEGIN EC PRIVATE KEY-----\n" + + "MHcCAQEEICK/NJcJmy18Co6iL023g9/4K+6CNqLJBAHE8/sGYarLoAoGCCqGSM49\n" + + "AwEHoUQDQgAE+P+p3NmQK4QnrFNgeiNOwjGkikQ3Gf3yuf8O5WbfRbUFunP/dtnp\n" + + "Ow89uIagAaVuR44ao60eUST/YeHB2bgnnA==\n" + + "-----END EC PRIVATE KEY-----\n"; + + public static String PRIVATE_KEY_ECDSA_384 = "-----BEGIN EC PRIVATE KEY-----\n" + + "MIGkAgEBBDDPRHa8rL2Gx9vmI70pjQJbnyDxU0TOkwqj7ILfsv90fSsO19qQARhB\n" + + "w7gH1qOkclmgBwYFK4EEACKhZANiAAR2v/iys6np24cCT+/5Hi/pmjsCpj9OQaUP\n" + + "1IcnuLQYNZfIuqSbIdO9Y5zH1rCl5TAIvJXS7v9qcfLr3gSyl9hJmVfsovV1iuWj\n" + + "yMo7EYhtYhUAjIrtCV9mhL5Nd6zwnbg=\n" + + "-----END EC PRIVATE KEY-----\n"; + + public static String PRIVATE_KEY_ECDSA_521 = "-----BEGIN EC PRIVATE KEY-----\n" + + "MIHcAgEBBEIBVoBkpQalEMfeJGPQubTF3EyEqc9uveqRGmHtzjsK1ZyXZPqGfTdA\n" + + "nyS22O2PDwyUh/tmQBZ98XY5f6zmf8+NIimgBwYFK4EEACOhgYkDgYYABACVlF0S\n" + + "3l9PotmDxWM1KK0aob/HMBzZzFry0dygTa0UBvHHHpCYparbvWybIDv8rcf/nNnG\n" + + "1cWThWuMadULhkKZQQAgu6NEyx8LPpE72GG709VVThwD78f7cK0qG1nghPTvD9Mj\n" + + "382fy5Hz288ZSFx3dk3CwJQ+81opuN+tMYWgCDh+gQ==\n" + + "-----END EC PRIVATE KEY-----\n"; + + private final String description; + + private final String privateKey; + + private final boolean isValid; + + public EC2CloudPrivateKeyWithFIPSTest(String description, String privateKey, boolean isValid) { + this.description = description; + this.privateKey = privateKey; + this.isValid = isValid; + } + + @Before + public void before() { + // Add provider manually to avoid requiring jenkinsrule + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + } + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][] { + { "DSA with key size 1024", PRIVATE_KEY_DSA_1024, false }, + { "RSA with key size 1024", PRIVATE_KEY_RSA_1024, false }, + { "RSA with key size 2048", PRIVATE_KEY_RSA_2048, true }, + { "RSA with key size 3072", PRIVATE_KEY_RSA_3072, true }, + { "RSA with key size 4096", PRIVATE_KEY_RSA_4096, true }, + { "ECDSA with key size 256", PRIVATE_KEY_ECDSA_256, true }, + { "ECDSA with key size 384", PRIVATE_KEY_ECDSA_384, true }, + { "ECDSA with key size 521", PRIVATE_KEY_ECDSA_521, true } + }); + } + + @Test + public void testPrivateKeyValidation() { + try { + EC2Cloud.ensurePrivateKeyInFipsMode(privateKey); + if (!isValid) { + fail(description + " should not be valid"); + } + } catch (IllegalArgumentException e) { + if (isValid) { + fail(description + " should be valid"); + } + } + } + +} diff --git a/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithoutFIPSTest.java b/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithoutFIPSTest.java new file mode 100644 index 000000000..d14d7150c --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithoutFIPSTest.java @@ -0,0 +1,59 @@ +package hudson.plugins.ec2; + +import jenkins.security.FIPS140; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.jvnet.hudson.test.FlagRule; + +import java.util.Arrays; +import java.util.Collection; + +import static hudson.plugins.ec2.EC2CloudPrivateKeyWithFIPSTest.PRIVATE_KEY_DSA_1024; +import static hudson.plugins.ec2.EC2CloudPrivateKeyWithFIPSTest.PRIVATE_KEY_ECDSA_256; +import static hudson.plugins.ec2.EC2CloudPrivateKeyWithFIPSTest.PRIVATE_KEY_ECDSA_384; +import static hudson.plugins.ec2.EC2CloudPrivateKeyWithFIPSTest.PRIVATE_KEY_ECDSA_521; +import static hudson.plugins.ec2.EC2CloudPrivateKeyWithFIPSTest.PRIVATE_KEY_RSA_1024; +import static hudson.plugins.ec2.EC2CloudPrivateKeyWithFIPSTest.PRIVATE_KEY_RSA_2048; +import static hudson.plugins.ec2.EC2CloudPrivateKeyWithFIPSTest.PRIVATE_KEY_RSA_3072; +import static hudson.plugins.ec2.EC2CloudPrivateKeyWithFIPSTest.PRIVATE_KEY_RSA_4096; +import static org.junit.Assert.fail; + +@RunWith(Parameterized.class) +public class EC2CloudPrivateKeyWithoutFIPSTest { + @ClassRule + public static FlagRule fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); + + private final String description; + + private final String privateKey; + + public EC2CloudPrivateKeyWithoutFIPSTest(String description, String privateKey) { + this.description = description; + this.privateKey = privateKey; + } + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][] { + { "DSA with key size 1024", PRIVATE_KEY_DSA_1024 }, + { "RSA with key size 1024", PRIVATE_KEY_RSA_1024 }, + { "RSA with key size 2048", PRIVATE_KEY_RSA_2048 }, + { "RSA with key size 3072", PRIVATE_KEY_RSA_3072 }, + { "RSA with key size 4096", PRIVATE_KEY_RSA_4096 }, + { "ECDSA with key size 256", PRIVATE_KEY_ECDSA_256 }, + { "ECDSA with key size 384", PRIVATE_KEY_ECDSA_384 }, + { "ECDSA with key size 521", PRIVATE_KEY_ECDSA_521 }, + }); + } + + @Test + public void testPrivateKeyValidation() { + try { + EC2Cloud.ensurePrivateKeyInFipsMode(privateKey); + } catch (IllegalArgumentException e) { + fail(description + " should be valid"); + } + } +} From dff8af1058687453edf8c6414af4fc1bdc1110d8 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Mon, 25 Nov 2024 10:27:28 +0100 Subject: [PATCH 036/267] Add FIPS compliance check for WinConnection --- .../hudson/plugins/ec2/win/WinConnection.java | 11 +++- .../ec2/win/WinConnectionWithFIPSTest.java | 66 +++++++++++++++++++ .../ec2/win/WinConnectionWithoutFIPSTest.java | 49 ++++++++++++++ 3 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java create mode 100644 src/test/java/hudson/plugins/ec2/win/WinConnectionWithoutFIPSTest.java diff --git a/src/main/java/hudson/plugins/ec2/win/WinConnection.java b/src/main/java/hudson/plugins/ec2/win/WinConnection.java index c807dd68a..c207745aa 100644 --- a/src/main/java/hudson/plugins/ec2/win/WinConnection.java +++ b/src/main/java/hudson/plugins/ec2/win/WinConnection.java @@ -1,8 +1,8 @@ package hudson.plugins.ec2.win; import com.hierynomus.protocol.transport.TransportException; -import com.hierynomus.security.bc.BCSecurityProvider; -import com.hierynomus.smbj.SmbConfig; +import hudson.plugins.ec2.Messages; +import hudson.plugins.ec2.util.FIPS140Utils; import hudson.plugins.ec2.win.winrm.WinRM; import hudson.plugins.ec2.win.winrm.WindowsProcess; @@ -21,6 +21,7 @@ import com.hierynomus.msdtyp.AccessMask; import com.hierynomus.mssmb2.SMB2ShareAccess; import com.hierynomus.mssmb2.SMB2CreateDisposition; +import jenkins.security.FIPS140; import javax.net.ssl.SSLException; import java.util.logging.Level; @@ -49,6 +50,8 @@ public WinConnection(String host, String username, String password) { } public WinConnection(String host, String username, String password, boolean allowSelfSignedCertificate) { + FIPS140Utils.ensureNoSelfSignedCertificate(allowSelfSignedCertificate); + this.host = host; this.username = username; this.password = password; @@ -58,6 +61,9 @@ public WinConnection(String host, String username, String password, boolean allo } public WinRM winrm() { + FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); + FIPS140Utils.ensureNoSelfSignedCertificate(allowSelfSignedCertificate); + WinRM winrm = new WinRM(host, username, password, allowSelfSignedCertificate); winrm.setUseHTTPS(useHTTPS); return winrm; @@ -178,6 +184,7 @@ public void close() { } public void setUseHTTPS(boolean useHTTPS) { + FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); this.useHTTPS = useHTTPS; } } diff --git a/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java new file mode 100644 index 000000000..3512f1356 --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java @@ -0,0 +1,66 @@ +package hudson.plugins.ec2.win; + +import hudson.plugins.ec2.win.winrm.WinRMConnectException; +import hudson.plugins.ec2.win.winrm.WindowsProcess; +import jenkins.security.FIPS140; +import org.apache.commons.io.IOUtils; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.FlagRule; + +import static junit.framework.TestCase.assertTrue; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeThat; + +public class WinConnectionWithFIPSTest { + + @ClassRule + public static FlagRule + fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); + + /** + * Self-signed certificate should not be allowed in FIPS mode, an {@link IllegalArgumentException} is expected + */ + @Test(expected = IllegalArgumentException.class) + public void testSelfSignedCertificateNotAllowed() throws Exception { + new WinConnection("", "" , "", true); + } + + /** + * Using a password with TLS, an {@link IllegalArgumentException} is not expected + */ + @Test + public void testValidCreation() { + new WinConnection("", "" , "", false); + } + + /** + * It should be allowed to disable useHTTPS only when no password is used, an {@link IllegalArgumentException} is not expected + */ + @Test + public void testSetUseHTTPSFalseWithoutPassword() { + new WinConnection("", "" , "", false) + .setUseHTTPS(false); + } + + /** + * It should not be allowed to disable useHTTPS only when a password is used, an {@link IllegalArgumentException} is expected + */ + @Test(expected = IllegalArgumentException.class) + public void testSetUseHTTPSFalseWithPassword() { + new WinConnection("", "alice" , "yes", false) + .setUseHTTPS(false); + } + + /** + * Password leak prevention when setUseHTTPS is not called + */ + @Test(expected = IllegalArgumentException.class) + public void testBuildWinRMClientWithoutTLS() { + WinConnection winConnection = new WinConnection("", "alice", "yes", false); + winConnection.winrm(); + fail("The creation of the WinRMClient should fail"); + } +} diff --git a/src/test/java/hudson/plugins/ec2/win/WinConnectionWithoutFIPSTest.java b/src/test/java/hudson/plugins/ec2/win/WinConnectionWithoutFIPSTest.java new file mode 100644 index 000000000..ba5a2d125 --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/win/WinConnectionWithoutFIPSTest.java @@ -0,0 +1,49 @@ +package hudson.plugins.ec2.win; + +import hudson.plugins.ec2.win.winrm.WinRMClient; +import jenkins.security.FIPS140; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.FlagRule; + +import static org.junit.Assert.fail; + +public class WinConnectionWithoutFIPSTest { + + @ClassRule + public static FlagRule + fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); + + /** + * When FIPS is not enabled, it should always be allowed to create the {@link WinConnection}, an {@link IllegalArgumentException} is not expected + */ + @Test + public void testWinConnectionCreation() { + new WinConnection("", "" , "", true) + .setUseHTTPS(false); + new WinConnection("", "" , "", true) + .setUseHTTPS(true); + new WinConnection("", "alice" , "yes", true) + .setUseHTTPS(false); + new WinConnection("", "alice" , "yes", true) + .setUseHTTPS(true); + + new WinConnection("", "" , "", false) + .setUseHTTPS(false); + new WinConnection("", "" , "", false) + .setUseHTTPS(true); + new WinConnection("", "alice" , "yes", false) + .setUseHTTPS(false); + new WinConnection("", "alice" , "yes", false) + .setUseHTTPS(true); + } + + /** + * When FIPS is not enabled, an {@link IllegalArgumentException} is not expected + */ + @Test + public void testBuildWinRMClientWithoutTLS() { + WinConnection winConnection = new WinConnection("", "alice", "yes", false); + winConnection.winrm(); + } +} From 1729122ea85f2ab3fefe6c714aa5c5da15872cb1 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Mon, 25 Nov 2024 11:14:08 +0100 Subject: [PATCH 037/267] Add FIPS compliance check for WindowsData --- .../java/hudson/plugins/ec2/WindowsData.java | 26 ++++++ .../plugins/ec2/WindowsDataWithFIPSTest.java | 93 +++++++++++++++++++ .../ec2/WindowsDataWithoutFIPSTest.java | 82 ++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java create mode 100644 src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java diff --git a/src/main/java/hudson/plugins/ec2/WindowsData.java b/src/main/java/hudson/plugins/ec2/WindowsData.java index 4eba6b25f..47fc2b08f 100644 --- a/src/main/java/hudson/plugins/ec2/WindowsData.java +++ b/src/main/java/hudson/plugins/ec2/WindowsData.java @@ -6,8 +6,13 @@ import hudson.Extension; import hudson.model.Descriptor; +import hudson.plugins.ec2.util.FIPS140Utils; +import hudson.util.FormValidation; import hudson.util.Secret; +import jenkins.security.FIPS140; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; + public class WindowsData extends AMITypeData { @@ -19,6 +24,9 @@ public class WindowsData extends AMITypeData { @DataBoundConstructor public WindowsData(String password, boolean useHTTPS, String bootDelay, boolean specifyPassword, boolean allowSelfSignedCertificate) { + FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); + FIPS140Utils.ensureNoSelfSignedCertificate(allowSelfSignedCertificate); + this.password = Secret.fromString(password); this.useHTTPS = useHTTPS; this.bootDelay = bootDelay; @@ -89,6 +97,24 @@ public static class DescriptorImpl extends Descriptor { public String getDisplayName() { return "windows"; } + + @SuppressWarnings("unused") + public FormValidation doCheckUseHTTPS(@QueryParameter boolean useHTTPS, @QueryParameter String password) { + try { + FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); + } catch (IllegalArgumentException ex) { + return FormValidation.error(ex, ex.getLocalizedMessage()); + } + return FormValidation.ok(); + } + + @SuppressWarnings("unused") + public FormValidation doCheckAllowSelfSignedCertificate(@QueryParameter boolean allowSelfSignedCertificate) { + if (FIPS140.useCompliantAlgorithms() && allowSelfSignedCertificate) { + return FormValidation.error(Messages.AmazonEC2Cloud_selfSignedCertificateNotAllowedInFIPSMode()); + } + return FormValidation.ok(); + } } @Override diff --git a/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java new file mode 100644 index 000000000..ed422341e --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java @@ -0,0 +1,93 @@ +package hudson.plugins.ec2; + +import hudson.util.FormValidation; +import jenkins.security.FIPS140; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.FlagRule; + +import java.net.MalformedURLException; + +import static org.junit.Assert.*; + +public class WindowsDataWithFIPSTest { + @ClassRule + public static FlagRule + fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); + + /** + * Self-signed certificate should not be allowed in FIPS mode, an {@link IllegalArgumentException} is expected + */ + @Test(expected = IllegalArgumentException.class) + public void testSelfSignedCertificateNotAllowed() { + new WindowsData("", true, "", true, true); + } + + /** + * Using a password without using TLS, an {@link IllegalArgumentException} is expected + */ + @Test(expected = IllegalArgumentException.class) + public void testCreateWindowsDataWithPasswordWithoutTLS() throws MalformedURLException { + new WindowsData("yes", false, "", true, false); + } + + /** + * Using a password with TLS, an {@link IllegalArgumentException} is not expected + */ + @Test + public void testCreateWindowsDataWithPasswordWithTLS() throws MalformedURLException { + new WindowsData("yes", true, "", true, false); + // specifyPassword is set to true in the constructor + new WindowsData("yes", true, "", false, false); + } + + /** + * If no password is used TLS can have any value, an {@link IllegalArgumentException} is not expected + */ + @Test + public void testCreateWindowsDataWithoutPassword() throws MalformedURLException { + new WindowsData("", false, "", true, false); + // specifyPassword is set to true in the constructor + new WindowsData("", false, "", false, false); + + new WindowsData("", true, "", true, false); + // specifyPassword is set to true in the constructor + new WindowsData("", true, "", false, false); + } + + @Test + public void testDoCheckUseHTTPSWithPassword() { + FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckUseHTTPS(true, "yes"); + assertEquals(FormValidation.Kind.OK, formValidation.kind); + } + + @Test + public void testDoCheckUseHTTPSWithoutPassword() { + FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckUseHTTPS(true, ""); + assertEquals(FormValidation.Kind.OK, formValidation.kind); + } + + @Test + public void testDoCheckUseHTTPWithPassword() { + FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckUseHTTPS(false, "yes"); + assertEquals(FormValidation.Kind.ERROR, formValidation.kind); + } + + @Test + public void testDoCheckUseHTTPWithoutPassword() { + FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckUseHTTPS(false, ""); + assertEquals(FormValidation.Kind.OK, formValidation.kind); + } + + @Test + public void testDoCheckAllowSelfSignedCertificateChecked() { + FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckAllowSelfSignedCertificate(true); + assertEquals(FormValidation.Kind.ERROR, formValidation.kind); + } + + @Test + public void testDoCheckAllowSelfSignedCertificateNotChecked() { + FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckAllowSelfSignedCertificate(false); + assertEquals(FormValidation.Kind.OK, formValidation.kind); + } +} \ No newline at end of file diff --git a/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java b/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java new file mode 100644 index 000000000..f84531779 --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java @@ -0,0 +1,82 @@ +package hudson.plugins.ec2; + +import hudson.plugins.ec2.win.WinConnection; +import hudson.util.FormValidation; +import jenkins.security.FIPS140; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.FlagRule; + +import java.net.MalformedURLException; + +import static org.junit.Assert.assertEquals; + +public class WindowsDataWithoutFIPSTest { + @ClassRule + public static FlagRule + fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); + + /** + * When FIPS is not enabled, it should always be allowed to create the {@link WindowsData}, an {@link IllegalArgumentException} is not expected + */ + @Test + public void testWinConnectionCreation() { + new WindowsData("", true, "", true, true); + new WindowsData("yes", true, "", true, true); + new WindowsData("", false, "", true, true); + new WindowsData("yes", false, "", true, true); + + new WindowsData("", true, "", false, true); + new WindowsData("yes", true, "", false, true); + new WindowsData("", false, "", false, true); + new WindowsData("yes", false, "", false, true); + + + new WindowsData("", true, "", true, false); + new WindowsData("yes", true, "", true, false); + new WindowsData("", false, "", true, false); + new WindowsData("yes", false, "", true, false); + + new WindowsData("", true, "", false, false); + new WindowsData("yes", true, "", false, false); + new WindowsData("", false, "", false, false); + new WindowsData("yes", false, "", false, false); + } + + + @Test + public void testDoCheckUseHTTPSWithPassword() { + FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckUseHTTPS(true, "yes"); + assertEquals(FormValidation.Kind.OK, formValidation.kind); + } + + @Test + public void testDoCheckUseHTTPSWithoutPassword() { + FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckUseHTTPS(true, ""); + assertEquals(FormValidation.Kind.OK, formValidation.kind); + } + + @Test + public void testDoCheckUseHTTPWithPassword() { + FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckUseHTTPS(false, "yes"); + assertEquals(FormValidation.Kind.OK, formValidation.kind); + } + + @Test + public void testDoCheckUseHTTPWithoutPassword() { + FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckUseHTTPS(false, ""); + assertEquals(FormValidation.Kind.OK, formValidation.kind); + } + + @Test + public void testDoCheckAllowSelfSignedCertificateChecked() { + FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckAllowSelfSignedCertificate(true); + assertEquals(FormValidation.Kind.OK, formValidation.kind); + } + + @Test + public void testDoCheckAllowSelfSignedCertificateNotChecked() { + FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckAllowSelfSignedCertificate(false); + assertEquals(FormValidation.Kind.OK, formValidation.kind); + } +} \ No newline at end of file From 41419ccb89b38caac30267e5f2a0e3865dc1abc2 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Mon, 25 Nov 2024 18:39:55 +0100 Subject: [PATCH 038/267] Fix Jenkins security scan --- src/main/java/hudson/plugins/ec2/WindowsData.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/hudson/plugins/ec2/WindowsData.java b/src/main/java/hudson/plugins/ec2/WindowsData.java index 47fc2b08f..f06a534c6 100644 --- a/src/main/java/hudson/plugins/ec2/WindowsData.java +++ b/src/main/java/hudson/plugins/ec2/WindowsData.java @@ -9,9 +9,11 @@ import hudson.plugins.ec2.util.FIPS140Utils; import hudson.util.FormValidation; import hudson.util.Secret; +import jenkins.model.Jenkins; import jenkins.security.FIPS140; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.verb.POST; public class WindowsData extends AMITypeData { @@ -98,8 +100,13 @@ public String getDisplayName() { return "windows"; } + @POST @SuppressWarnings("unused") public FormValidation doCheckUseHTTPS(@QueryParameter boolean useHTTPS, @QueryParameter String password) { + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + // for security reasons, do not perform any check if the user is not an admin + return FormValidation.ok(); + } try { FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); } catch (IllegalArgumentException ex) { @@ -108,8 +115,13 @@ public FormValidation doCheckUseHTTPS(@QueryParameter boolean useHTTPS, @QueryPa return FormValidation.ok(); } + @POST @SuppressWarnings("unused") public FormValidation doCheckAllowSelfSignedCertificate(@QueryParameter boolean allowSelfSignedCertificate) { + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + // for security reasons, do not perform any check if the user is not an admin + return FormValidation.ok(); + } if (FIPS140.useCompliantAlgorithms() && allowSelfSignedCertificate) { return FormValidation.error(Messages.AmazonEC2Cloud_selfSignedCertificateNotAllowedInFIPSMode()); } From 2f073ac840be7379884f376b5b87dabba395d596 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Tue, 26 Nov 2024 10:18:00 +0100 Subject: [PATCH 039/267] Align BOM version with Jenkins version --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 21c90451b..cdf05f991 100644 --- a/pom.xml +++ b/pom.xml @@ -201,8 +201,8 @@ THE SOFTWARE. io.jenkins.tools.bom - bom-2.452.x - 3654.v237e4a_f2d8da_ + bom-2.462.x + 3722.vcc62e7311580 import pom From 5877f74bc19e7bef85caf23c797b92f53c323c2e Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Tue, 26 Nov 2024 16:24:12 +0100 Subject: [PATCH 040/267] Bump Jenkins --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cdf05f991..3daf4c7de 100644 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,7 @@ THE SOFTWARE. 999999-SNAPSHOT - 2.462.2 + 2.462.3 jenkinsci/${project.artifactId}-plugin 1626 From 1bb4a3185a790b84279880b7f7fea2995808568f Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Tue, 26 Nov 2024 16:24:25 +0100 Subject: [PATCH 041/267] Fix tests --- .../plugins/ec2/WindowsDataWithFIPSTest.java | 41 +++++++++++++------ .../ec2/WindowsDataWithoutFIPSTest.java | 23 +++++++---- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java index ed422341e..61161fbfc 100644 --- a/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java @@ -1,24 +1,30 @@ package hudson.plugins.ec2; +import hudson.ExtensionList; import hudson.util.FormValidation; import jenkins.security.FIPS140; import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.FlagRule; - -import java.net.MalformedURLException; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.WithoutJenkins; import static org.junit.Assert.*; public class WindowsDataWithFIPSTest { @ClassRule - public static FlagRule - fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); + public static FlagRule fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); + + @Rule + public JenkinsRule j = new JenkinsRule(); /** * Self-signed certificate should not be allowed in FIPS mode, an {@link IllegalArgumentException} is expected */ @Test(expected = IllegalArgumentException.class) + @WithoutJenkins public void testSelfSignedCertificateNotAllowed() { new WindowsData("", true, "", true, true); } @@ -27,7 +33,8 @@ public void testSelfSignedCertificateNotAllowed() { * Using a password without using TLS, an {@link IllegalArgumentException} is expected */ @Test(expected = IllegalArgumentException.class) - public void testCreateWindowsDataWithPasswordWithoutTLS() throws MalformedURLException { + @WithoutJenkins + public void testCreateWindowsDataWithPasswordWithoutTLS() { new WindowsData("yes", false, "", true, false); } @@ -35,7 +42,8 @@ public void testCreateWindowsDataWithPasswordWithoutTLS() throws MalformedURLExc * Using a password with TLS, an {@link IllegalArgumentException} is not expected */ @Test - public void testCreateWindowsDataWithPasswordWithTLS() throws MalformedURLException { + @WithoutJenkins + public void testCreateWindowsDataWithPasswordWithTLS() { new WindowsData("yes", true, "", true, false); // specifyPassword is set to true in the constructor new WindowsData("yes", true, "", false, false); @@ -45,7 +53,8 @@ public void testCreateWindowsDataWithPasswordWithTLS() throws MalformedURLExcept * If no password is used TLS can have any value, an {@link IllegalArgumentException} is not expected */ @Test - public void testCreateWindowsDataWithoutPassword() throws MalformedURLException { + @WithoutJenkins + public void testCreateWindowsDataWithoutPassword() { new WindowsData("", false, "", true, false); // specifyPassword is set to true in the constructor new WindowsData("", false, "", false, false); @@ -56,38 +65,44 @@ public void testCreateWindowsDataWithoutPassword() throws MalformedURLException } @Test + @Ignore("Disabled until the plugins dependencies are FIPS compliant") public void testDoCheckUseHTTPSWithPassword() { - FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckUseHTTPS(true, "yes"); + FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(true, "yes"); assertEquals(FormValidation.Kind.OK, formValidation.kind); } @Test + @Ignore("Disabled until the plugins dependencies are FIPS compliant") public void testDoCheckUseHTTPSWithoutPassword() { - FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckUseHTTPS(true, ""); + FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(true, ""); assertEquals(FormValidation.Kind.OK, formValidation.kind); } @Test + @Ignore("Disabled until the plugins dependencies are FIPS compliant") public void testDoCheckUseHTTPWithPassword() { - FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckUseHTTPS(false, "yes"); + FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(false, "yes"); assertEquals(FormValidation.Kind.ERROR, formValidation.kind); } @Test + @Ignore("Disabled until the plugins dependencies are FIPS compliant") public void testDoCheckUseHTTPWithoutPassword() { - FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckUseHTTPS(false, ""); + FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(false, ""); assertEquals(FormValidation.Kind.OK, formValidation.kind); } @Test + @Ignore("Disabled until the plugins dependencies are FIPS compliant") public void testDoCheckAllowSelfSignedCertificateChecked() { - FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckAllowSelfSignedCertificate(true); + FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckAllowSelfSignedCertificate(true); assertEquals(FormValidation.Kind.ERROR, formValidation.kind); } @Test + @Ignore("Disabled until the plugins dependencies are FIPS compliant") public void testDoCheckAllowSelfSignedCertificateNotChecked() { - FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckAllowSelfSignedCertificate(false); + FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckAllowSelfSignedCertificate(false); assertEquals(FormValidation.Kind.OK, formValidation.kind); } } \ No newline at end of file diff --git a/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java b/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java index f84531779..aa496acad 100644 --- a/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java @@ -1,13 +1,14 @@ package hudson.plugins.ec2; -import hudson.plugins.ec2.win.WinConnection; +import hudson.ExtensionList; import hudson.util.FormValidation; import jenkins.security.FIPS140; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.FlagRule; - -import java.net.MalformedURLException; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.WithoutJenkins; import static org.junit.Assert.assertEquals; @@ -16,10 +17,14 @@ public class WindowsDataWithoutFIPSTest { public static FlagRule fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); + @Rule + public JenkinsRule j = new JenkinsRule(); + /** * When FIPS is not enabled, it should always be allowed to create the {@link WindowsData}, an {@link IllegalArgumentException} is not expected */ @Test + @WithoutJenkins public void testWinConnectionCreation() { new WindowsData("", true, "", true, true); new WindowsData("yes", true, "", true, true); @@ -46,37 +51,37 @@ public void testWinConnectionCreation() { @Test public void testDoCheckUseHTTPSWithPassword() { - FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckUseHTTPS(true, "yes"); + FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(true, "yes"); assertEquals(FormValidation.Kind.OK, formValidation.kind); } @Test public void testDoCheckUseHTTPSWithoutPassword() { - FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckUseHTTPS(true, ""); + FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(true, ""); assertEquals(FormValidation.Kind.OK, formValidation.kind); } @Test public void testDoCheckUseHTTPWithPassword() { - FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckUseHTTPS(false, "yes"); + FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(false, "yes"); assertEquals(FormValidation.Kind.OK, formValidation.kind); } @Test public void testDoCheckUseHTTPWithoutPassword() { - FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckUseHTTPS(false, ""); + FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(false, ""); assertEquals(FormValidation.Kind.OK, formValidation.kind); } @Test public void testDoCheckAllowSelfSignedCertificateChecked() { - FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckAllowSelfSignedCertificate(true); + FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckAllowSelfSignedCertificate(true); assertEquals(FormValidation.Kind.OK, formValidation.kind); } @Test public void testDoCheckAllowSelfSignedCertificateNotChecked() { - FormValidation formValidation = new WindowsData.DescriptorImpl().doCheckAllowSelfSignedCertificate(false); + FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckAllowSelfSignedCertificate(false); assertEquals(FormValidation.Kind.OK, formValidation.kind); } } \ No newline at end of file From 78a96b7b05d0a9a96c51916ddd0b49a7447c080b Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Tue, 26 Nov 2024 16:24:39 +0100 Subject: [PATCH 042/267] Make use of Common Lang instead of a private method --- src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java index 4f97b4938..421f7840c 100644 --- a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java +++ b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java @@ -2,6 +2,7 @@ import hudson.plugins.ec2.Messages; import jenkins.security.FIPS140; +import org.apache.commons.lang.StringUtils; import java.net.URL; import java.security.Key; @@ -51,9 +52,6 @@ public static void ensureKeyInFipsMode(Key key) { } } - private static boolean isNotEmpty(String password) { - return password != null && !password.isEmpty(); - } /** * Password leak prevention when FIPS mode is requested. If FIPS mode is not requested, this method does nothing. @@ -74,7 +72,7 @@ public static void ensureNoPasswordLeak(URL url, String password) { * @throws IllegalArgumentException if there is a risk that the password will leak */ public static void ensureNoPasswordLeak(boolean useHTTPS, String password) { - ensureNoPasswordLeak(useHTTPS, isNotEmpty(password)); + ensureNoPasswordLeak(useHTTPS, !StringUtils.isEmpty(password)); } /** From c16aeafe26e6bc09d8880cc6b96f7aa5cbfc1dfb Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Tue, 26 Nov 2024 16:24:48 +0100 Subject: [PATCH 043/267] Throw FormException instead of IllegalArgumentException --- .../java/hudson/plugins/ec2/WindowsData.java | 20 +++++++++++++----- .../plugins/ec2/WindowsDataWithFIPSTest.java | 21 ++++++++++--------- .../ec2/WindowsDataWithoutFIPSTest.java | 3 ++- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/WindowsData.java b/src/main/java/hudson/plugins/ec2/WindowsData.java index f06a534c6..fb591109a 100644 --- a/src/main/java/hudson/plugins/ec2/WindowsData.java +++ b/src/main/java/hudson/plugins/ec2/WindowsData.java @@ -25,9 +25,18 @@ public class WindowsData extends AMITypeData { private final Boolean allowSelfSignedCertificate; //Boolean to allow nulls when the saved template doesn't have the field @DataBoundConstructor - public WindowsData(String password, boolean useHTTPS, String bootDelay, boolean specifyPassword, boolean allowSelfSignedCertificate) { - FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); - FIPS140Utils.ensureNoSelfSignedCertificate(allowSelfSignedCertificate); + public WindowsData(String password, boolean useHTTPS, String bootDelay, boolean specifyPassword, boolean allowSelfSignedCertificate) + throws Descriptor.FormException { + try { + FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); + } catch (IllegalArgumentException e) { + throw new Descriptor.FormException(e, "password"); + } + try { + FIPS140Utils.ensureNoSelfSignedCertificate(allowSelfSignedCertificate); + } catch (IllegalArgumentException e) { + throw new Descriptor.FormException(e, "allowSelfSignedCertificate"); + } this.password = Secret.fromString(password); this.useHTTPS = useHTTPS; @@ -42,11 +51,12 @@ public WindowsData(String password, boolean useHTTPS, String bootDelay, boolean } @Deprecated - public WindowsData(String password, boolean useHTTPS, String bootDelay, boolean specifyPassword) { + public WindowsData(String password, boolean useHTTPS, String bootDelay, boolean specifyPassword) + throws Descriptor.FormException { this(password, useHTTPS, bootDelay, specifyPassword, true); } - public WindowsData(String password, boolean useHTTPS, String bootDelay) { + public WindowsData(String password, boolean useHTTPS, String bootDelay) throws Descriptor.FormException { this(password, useHTTPS, bootDelay, false); } diff --git a/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java index 61161fbfc..913c34d22 100644 --- a/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java @@ -1,5 +1,6 @@ package hudson.plugins.ec2; +import hudson.model.Descriptor; import hudson.ExtensionList; import hudson.util.FormValidation; import jenkins.security.FIPS140; @@ -21,40 +22,40 @@ public class WindowsDataWithFIPSTest { public JenkinsRule j = new JenkinsRule(); /** - * Self-signed certificate should not be allowed in FIPS mode, an {@link IllegalArgumentException} is expected + * Self-signed certificate should not be allowed in FIPS mode, an {@link Descriptor.FormException} is expected */ - @Test(expected = IllegalArgumentException.class) + @Test(expected = Descriptor.FormException.class) @WithoutJenkins - public void testSelfSignedCertificateNotAllowed() { + public void testSelfSignedCertificateNotAllowed() throws Descriptor.FormException { new WindowsData("", true, "", true, true); } /** - * Using a password without using TLS, an {@link IllegalArgumentException} is expected + * Using a password without using TLS, an {@link Descriptor.FormException} is expected */ - @Test(expected = IllegalArgumentException.class) + @Test(expected = Descriptor.FormException.class) @WithoutJenkins - public void testCreateWindowsDataWithPasswordWithoutTLS() { + public void testCreateWindowsDataWithPasswordWithoutTLS() throws Descriptor.FormException { new WindowsData("yes", false, "", true, false); } /** - * Using a password with TLS, an {@link IllegalArgumentException} is not expected + * Using a password with TLS, an {@link Descriptor.FormException} is not expected */ @Test @WithoutJenkins - public void testCreateWindowsDataWithPasswordWithTLS() { + public void testCreateWindowsDataWithPasswordWithTLS() throws Descriptor.FormException { new WindowsData("yes", true, "", true, false); // specifyPassword is set to true in the constructor new WindowsData("yes", true, "", false, false); } /** - * If no password is used TLS can have any value, an {@link IllegalArgumentException} is not expected + * If no password is used TLS can have any value, an {@link Descriptor.FormException} is not expected */ @Test @WithoutJenkins - public void testCreateWindowsDataWithoutPassword() { + public void testCreateWindowsDataWithoutPassword() throws Descriptor.FormException { new WindowsData("", false, "", true, false); // specifyPassword is set to true in the constructor new WindowsData("", false, "", false, false); diff --git a/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java b/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java index aa496acad..e74e25e79 100644 --- a/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java @@ -1,5 +1,6 @@ package hudson.plugins.ec2; +import hudson.model.Descriptor; import hudson.ExtensionList; import hudson.util.FormValidation; import jenkins.security.FIPS140; @@ -25,7 +26,7 @@ public class WindowsDataWithoutFIPSTest { */ @Test @WithoutJenkins - public void testWinConnectionCreation() { + public void testWinConnectionCreation() throws Descriptor.FormException { new WindowsData("", true, "", true, true); new WindowsData("yes", true, "", true, true); new WindowsData("", false, "", true, true); From 1fbb8262ebade5b978e9c91f5d6e70e17abe667f Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Tue, 26 Nov 2024 16:24:58 +0100 Subject: [PATCH 044/267] Make use of FIPS140Utils --- src/main/java/hudson/plugins/ec2/WindowsData.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/WindowsData.java b/src/main/java/hudson/plugins/ec2/WindowsData.java index fb591109a..978c1ab3d 100644 --- a/src/main/java/hudson/plugins/ec2/WindowsData.java +++ b/src/main/java/hudson/plugins/ec2/WindowsData.java @@ -132,8 +132,10 @@ public FormValidation doCheckAllowSelfSignedCertificate(@QueryParameter boolean // for security reasons, do not perform any check if the user is not an admin return FormValidation.ok(); } - if (FIPS140.useCompliantAlgorithms() && allowSelfSignedCertificate) { - return FormValidation.error(Messages.AmazonEC2Cloud_selfSignedCertificateNotAllowedInFIPSMode()); + try { + FIPS140Utils.ensureNoSelfSignedCertificate(allowSelfSignedCertificate); + } catch (IllegalArgumentException ex) { + return FormValidation.error(ex, ex.getLocalizedMessage()); } return FormValidation.ok(); } From e04e639863eb32ae8f623bcd33cfb66b991fcd62 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Tue, 26 Nov 2024 16:25:08 +0100 Subject: [PATCH 045/267] Move utility methods into FIPS140Utils --- .../java/hudson/plugins/ec2/EC2Cloud.java | 39 ++----------- .../plugins/ec2/ssh/verifiers/HostKey.java | 29 +--------- .../hudson/plugins/ec2/util/FIPS140Utils.java | 55 +++++++++++++++++++ .../ec2/EC2CloudPrivateKeyWithFIPSTest.java | 3 +- .../EC2CloudPrivateKeyWithoutFIPSTest.java | 3 +- 5 files changed, 64 insertions(+), 65 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 0fa641404..8cd1d5a41 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -68,10 +68,8 @@ import hudson.util.ListBoxModel; import hudson.util.Secret; import hudson.util.StreamTaskListener; -import jenkins.bouncycastle.api.PEMEncodable; import jenkins.model.Jenkins; import jenkins.model.JenkinsLocationConfiguration; -import jenkins.security.FIPS140; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.HttpResponse; @@ -90,8 +88,6 @@ import java.net.MalformedURLException; import java.net.Proxy; import java.net.URL; -import java.security.Key; -import java.security.UnrecoverableKeyException; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; @@ -212,7 +208,7 @@ public EC2PrivateKey resolvePrivateKey(){ SSHUserPrivateKey privateKeyCredential = getSshCredential(sshKeysCredentialsId, Jenkins.get()); if (privateKeyCredential != null) { String privateKey = privateKeyCredential.getPrivateKey(); - ensurePrivateKeyInFipsMode(privateKey); + FIPS140Utils.ensurePrivateKeyInFipsMode(privateKey); return new EC2PrivateKey(privateKey); } } @@ -282,7 +278,7 @@ protected Object readResolve() { if (this.sshKeysCredentialsId == null && this.privateKey != null ){ String privateKey = this.privateKey.getPrivateKey(); - ensurePrivateKeyInFipsMode(privateKey); + FIPS140Utils.ensurePrivateKeyInFipsMode(privateKey); migratePrivateSshKeyToCredential(privateKey); } this.privateKey = null; // This enforces it not to be persisted and that CasC will never output privateKey on export @@ -1115,33 +1111,6 @@ private static SSHUserPrivateKey getSshCredential(String id, ItemGroup context){ return credential; } - /** - * Checks if the private key is allowed when FIPS mode is requested. - * Allowed private key with the following algorithms and sizes: - *
    - *
  • DSA with key size >= 2048
  • - *
  • RSA with key size >= 2048
  • - *
  • Elliptic curve (ED25519) with field size >= 224
  • - *
- * If the private key is valid and allowed or not in FIPS mode method will just exit. - * If not it will throw an {@link IllegalArgumentException}. - * @param privateKeyString String containing the private key PEM. - */ - public static void ensurePrivateKeyInFipsMode(String privateKeyString) { - if (!FIPS140.useCompliantAlgorithms()) { - return; - } - if (StringUtils.isBlank(privateKeyString)) { - throw new IllegalArgumentException(Messages.AmazonEC2Cloud_keyIsMandatory()); - } - try { - Key privateKey = PEMEncodable.decode(privateKeyString).toPrivateKey(); - FIPS140Utils.ensureKeyInFipsMode(privateKey); - } catch (RuntimeException | UnrecoverableKeyException | IOException e) { - throw new IllegalArgumentException(e.getMessage(), e); - } - } - public static abstract class DescriptorImpl extends Descriptor { public InstanceType[] getInstanceTypes() { @@ -1234,7 +1203,7 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont } try { - ensurePrivateKeyInFipsMode(privateKey); + FIPS140Utils.ensurePrivateKeyInFipsMode(privateKey); } catch (IllegalArgumentException ex) { validations.add(FormValidation.error(ex, ex.getLocalizedMessage())); } @@ -1313,7 +1282,7 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL } try { - ensurePrivateKeyInFipsMode(privateKey); + FIPS140Utils.ensurePrivateKeyInFipsMode(privateKey); } catch (IllegalArgumentException ex) { validations.add(FormValidation.error(ex, ex.getLocalizedMessage())); } diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java index 8ccf7c037..56e288c71 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java @@ -27,18 +27,10 @@ package hudson.plugins.ec2.ssh.verifiers; import com.trilead.ssh2.KnownHosts; -import com.trilead.ssh2.signature.KeyAlgorithm; -import com.trilead.ssh2.signature.KeyAlgorithmManager; import edu.umd.cs.findbugs.annotations.NonNull; -import hudson.plugins.ec2.Messages; import hudson.plugins.ec2.util.FIPS140Utils; -import jenkins.security.FIPS140; -import java.io.IOException; import java.io.Serializable; -import java.security.Key; -import java.security.PrivateKey; -import java.security.PublicKey; import java.util.Arrays; /** @@ -54,28 +46,9 @@ public final class HostKey implements Serializable { private final String algorithm; private final byte[] key; - public static void ensurePublicKeyInFipsMode(@NonNull String algorithm, @NonNull byte[] key) { - if (!FIPS140.useCompliantAlgorithms()) { - return; - } - - KeyAlgorithm publicKeyPrivateKeyKeyAlgorithm = KeyAlgorithmManager - .getSupportedAlgorithms() - .stream() - .filter((keyAlgorithm) -> keyAlgorithm.getKeyFormat().equals(algorithm)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException(Messages.AmazonEC2Cloud_keyIsNotApprovedInFIPSMode(algorithm))); - try { - Key publicKey = publicKeyPrivateKeyKeyAlgorithm.decodePublicKey(key); - FIPS140Utils.ensureKeyInFipsMode(publicKey); - } catch (RuntimeException | IOException e) { - throw new IllegalArgumentException(e.getMessage(), e); - } - } - public HostKey(@NonNull String algorithm, @NonNull byte[] key) { super(); - ensurePublicKeyInFipsMode(algorithm, key); + FIPS140Utils.ensurePublicKeyInFipsMode(algorithm, key); this.algorithm = algorithm; this.key = key.clone(); diff --git a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java index 421f7840c..2bcc632a4 100644 --- a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java +++ b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java @@ -1,11 +1,19 @@ package hudson.plugins.ec2.util; +import com.trilead.ssh2.signature.KeyAlgorithm; +import com.trilead.ssh2.signature.KeyAlgorithmManager; +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.plugins.ec2.Messages; +import jenkins.bouncycastle.api.PEMEncodable; import jenkins.security.FIPS140; import org.apache.commons.lang.StringUtils; +import java.io.IOException; import java.net.URL; import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.UnrecoverableKeyException; import java.security.interfaces.DSAKey; import java.security.interfaces.ECKey; import java.security.interfaces.RSAKey; @@ -103,4 +111,51 @@ public static void ensureNoSelfSignedCertificate(boolean allowSelfSignedCertific } } } + + /** + * Checks if the private key is allowed when FIPS mode is requested. + * Allowed private key with the following algorithms and sizes: + *
    + *
  • DSA with key size >= 2048
  • + *
  • RSA with key size >= 2048
  • + *
  • Elliptic curve (ED25519) with field size >= 224
  • + *
+ * If the private key is valid and allowed or not in FIPS mode method will just exit. + * If not it will throw an {@link IllegalArgumentException}. + * @param privateKeyString String containing the private key PEM. + */ + public static void ensurePrivateKeyInFipsMode(String privateKeyString) { + if (!FIPS140.useCompliantAlgorithms()) { + return; + } + if (StringUtils.isBlank(privateKeyString)) { + throw new IllegalArgumentException(Messages.AmazonEC2Cloud_keyIsMandatory()); + } + try { + Key privateKey = PEMEncodable.decode(privateKeyString).toPrivateKey(); + ensureKeyInFipsMode(privateKey); + } catch (RuntimeException | UnrecoverableKeyException | IOException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + + public static void ensurePublicKeyInFipsMode(@NonNull + String algorithm, @NonNull byte[] key) { + if (!FIPS140.useCompliantAlgorithms()) { + return; + } + + KeyAlgorithm publicKeyPrivateKeyKeyAlgorithm = KeyAlgorithmManager + .getSupportedAlgorithms() + .stream() + .filter((keyAlgorithm) -> keyAlgorithm.getKeyFormat().equals(algorithm)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(Messages.AmazonEC2Cloud_keyIsNotApprovedInFIPSMode(algorithm))); + try { + Key publicKey = publicKeyPrivateKeyKeyAlgorithm.decodePublicKey(key); + ensureKeyInFipsMode(publicKey); + } catch (RuntimeException | IOException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } } diff --git a/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithFIPSTest.java index c7c369148..7db5d93e9 100644 --- a/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithFIPSTest.java @@ -1,5 +1,6 @@ package hudson.plugins.ec2; +import hudson.plugins.ec2.util.FIPS140Utils; import jenkins.security.FIPS140; import org.junit.Before; import org.junit.ClassRule; @@ -225,7 +226,7 @@ public static Collection data() { @Test public void testPrivateKeyValidation() { try { - EC2Cloud.ensurePrivateKeyInFipsMode(privateKey); + FIPS140Utils.ensurePrivateKeyInFipsMode(privateKey); if (!isValid) { fail(description + " should not be valid"); } diff --git a/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithoutFIPSTest.java b/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithoutFIPSTest.java index d14d7150c..2d698b7ef 100644 --- a/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithoutFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithoutFIPSTest.java @@ -1,5 +1,6 @@ package hudson.plugins.ec2; +import hudson.plugins.ec2.util.FIPS140Utils; import jenkins.security.FIPS140; import org.junit.ClassRule; import org.junit.Test; @@ -51,7 +52,7 @@ public static Collection data() { @Test public void testPrivateKeyValidation() { try { - EC2Cloud.ensurePrivateKeyInFipsMode(privateKey); + FIPS140Utils.ensurePrivateKeyInFipsMode(privateKey); } catch (IllegalArgumentException e) { fail(description + " should be valid"); } From fcd7cd2f527f06a7340a1d7aebc54c05090c3567 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Tue, 26 Nov 2024 16:47:58 +0100 Subject: [PATCH 046/267] Add FIPS mention to messages keys --- src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java | 8 ++++---- src/main/resources/hudson/plugins/ec2/Messages.properties | 6 +++--- .../hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java index 2bcc632a4..8178aa776 100644 --- a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java +++ b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java @@ -42,15 +42,15 @@ public static void ensureKeyInFipsMode(Key key) { try { if (key instanceof RSAKey) { if (((RSAKey) key).getModulus().bitLength() < 2048) { - throw new IllegalArgumentException(Messages.AmazonEC2Cloud_invalidKeySize()); + throw new IllegalArgumentException(Messages.AmazonEC2Cloud_invalidKeySizeInFIPSMode()); } } else if (key instanceof DSAKey) { if (((DSAKey) key).getParams().getP().bitLength() < 2048) { - throw new IllegalArgumentException(Messages.AmazonEC2Cloud_invalidKeySize()); + throw new IllegalArgumentException(Messages.AmazonEC2Cloud_invalidKeySizeInFIPSMode()); } } else if (key instanceof ECKey) { if (((ECKey) key).getParams().getCurve().getField().getFieldSize() < 224) { - throw new IllegalArgumentException(Messages.AmazonEC2Cloud_invalidKeySizeEC()); + throw new IllegalArgumentException(Messages.AmazonEC2Cloud_invalidKeySizeECInFIPSMode()); } } else { throw new IllegalArgumentException(Messages.AmazonEC2Cloud_keyIsNotApprovedInFIPSMode(key.getAlgorithm())); @@ -129,7 +129,7 @@ public static void ensurePrivateKeyInFipsMode(String privateKeyString) { return; } if (StringUtils.isBlank(privateKeyString)) { - throw new IllegalArgumentException(Messages.AmazonEC2Cloud_keyIsMandatory()); + throw new IllegalArgumentException(Messages.AmazonEC2Cloud_keyIsMandatoryInFIPSMode()); } try { Key privateKey = PEMEncodable.decode(privateKeyString).toPrivateKey(); diff --git a/src/main/resources/hudson/plugins/ec2/Messages.properties b/src/main/resources/hudson/plugins/ec2/Messages.properties index ccb596a08..fd86b371f 100644 --- a/src/main/resources/hudson/plugins/ec2/Messages.properties +++ b/src/main/resources/hudson/plugins/ec2/Messages.properties @@ -14,7 +14,7 @@ AmazonEC2Cloud.tlsIsRequiredInFIPSMode=TLS is required in FIPS mode to avoid pas AmazonEC2Cloud.selfSignedCertificateNotAllowedInFIPSMode=Self-signed certificate is not allowed in FIPS mode AmazonEC2Cloud.skipTlsVerifyNotAllowedInFIPSMode=Skipping TLS verification is not allowed in FIPS mode AmazonEC2Cloud.keyIsNotApprovedInFIPSMode=Key is not valid: {0} -AmazonEC2Cloud.invalidKeySize=Invalid key size, at least 2048 is needed in FIPS mode. -AmazonEC2Cloud.invalidKeySizeEC=Invalid curve size, at least 224 is needed in FIPS mode. -AmazonEC2Cloud.keyIsMandatory=The key is mandatory in FIPS mode. +AmazonEC2Cloud.invalidKeySizeInFIPSMode=Invalid key size, at least 2048 is needed in FIPS mode. +AmazonEC2Cloud.invalidKeySizeECInFIPSMode=Invalid curve size, at least 224 is needed in FIPS mode. +AmazonEC2Cloud.keyIsMandatoryInFIPSMode=The key is mandatory in FIPS mode. General.MissingPermission=You do not have the Overall/Administer right to modify this field diff --git a/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java index 463f4d2ac..97af54890 100644 --- a/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java @@ -65,7 +65,7 @@ public void testDSAInvalidKeyMessage() { DSAPublicKey key = Mockito.mock(DSAPublicKey.class, Mockito.RETURNS_DEEP_STUBS); Mockito.when(key.getParams().getP().bitLength()).thenReturn(2047); - assertInvalidKey(key, Messages.AmazonEC2Cloud_invalidKeySize()); + assertInvalidKey(key, Messages.AmazonEC2Cloud_invalidKeySizeInFIPSMode()); } @Test @@ -81,7 +81,7 @@ public void testRSAInvalidKeyMessage() { RSAPublicKey key = Mockito.mock(RSAPublicKey.class, Mockito.RETURNS_DEEP_STUBS); Mockito.when(key.getModulus().bitLength()).thenReturn(2047); - assertInvalidKey(key, Messages.AmazonEC2Cloud_invalidKeySize()); + assertInvalidKey(key, Messages.AmazonEC2Cloud_invalidKeySizeInFIPSMode()); } @Test @@ -97,7 +97,7 @@ public void testECDSAInvalidKeyMessage() { ECKey key = Mockito.mock(ECKey.class, Mockito.withSettings().extraInterfaces(Key.class).defaultAnswer(Mockito.RETURNS_DEEP_STUBS)); Mockito.when(key.getParams().getCurve().getField().getFieldSize()).thenReturn(223); - assertInvalidKey((Key) key, Messages.AmazonEC2Cloud_invalidKeySizeEC()); + assertInvalidKey((Key) key, Messages.AmazonEC2Cloud_invalidKeySizeECInFIPSMode()); } @Test From 98fc9a96b65375210d2dd3f9285d924d91582f3f Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Tue, 26 Nov 2024 13:18:16 -0800 Subject: [PATCH 047/267] Format repository with Spotless --- pom.xml | 402 ++-- .../java/hudson/plugins/ec2/AMITypeData.java | 5 +- .../hudson/plugins/ec2/AmazonEC2Cloud.java | 105 +- .../java/hudson/plugins/ec2/CloudHelper.java | 47 +- .../plugins/ec2/ConnectionStrategy.java | 3 +- .../hudson/plugins/ec2/EC2AbstractSlave.java | 708 ++++--- .../java/hudson/plugins/ec2/EC2Cloud.java | 510 +++-- .../java/hudson/plugins/ec2/EC2Computer.java | 22 +- .../plugins/ec2/EC2ComputerLauncher.java | 24 +- .../plugins/ec2/EC2ComputerListener.java | 2 +- .../java/hudson/plugins/ec2/EC2Filter.java | 28 +- .../plugins/ec2/EC2HostAddressProvider.java | 7 +- .../hudson/plugins/ec2/EC2OndemandSlave.java | 415 +++- .../hudson/plugins/ec2/EC2PrivateKey.java | 33 +- .../java/hudson/plugins/ec2/EC2Readiness.java | 1 + .../plugins/ec2/EC2RetentionStrategy.java | 102 +- .../hudson/plugins/ec2/EC2SlaveMonitor.java | 23 +- .../java/hudson/plugins/ec2/EC2SpotSlave.java | 188 +- src/main/java/hudson/plugins/ec2/EC2Step.java | 27 +- src/main/java/hudson/plugins/ec2/EC2Tag.java | 26 +- .../plugins/ec2/EbsEncryptRootVolume.java | 1 - .../java/hudson/plugins/ec2/Eucalyptus.java | 96 +- .../ec2/HostKeyVerificationStrategyEnum.java | 11 +- .../hudson/plugins/ec2/InstanceState.java | 7 +- .../plugins/ec2/InstanceTypeConverter.java | 9 +- src/main/java/hudson/plugins/ec2/MacData.java | 41 +- .../ec2/NoDelayProvisionerStrategy.java | 40 +- .../java/hudson/plugins/ec2/PluginImpl.java | 17 +- .../hudson/plugins/ec2/SlaveTemplate.java | 1760 +++++++++++++---- .../hudson/plugins/ec2/SpotConfiguration.java | 45 +- src/main/java/hudson/plugins/ec2/Tenancy.java | 4 +- .../java/hudson/plugins/ec2/UnixData.java | 47 +- .../java/hudson/plugins/ec2/WindowsData.java | 57 +- .../plugins/ec2/ebs/ZPoolExpandNotice.java | 4 +- .../hudson/plugins/ec2/ebs/ZPoolMonitor.java | 14 +- .../hudson/plugins/ec2/ebs/package-info.java | 2 +- .../plugins/ec2/ssh/EC2MacLauncher.java | 183 +- .../plugins/ec2/ssh/EC2UnixLauncher.java | 207 +- .../plugins/ec2/ssh/HostKeyVerifierImpl.java | 11 +- .../ec2/ssh/verifiers/AcceptNewStrategy.java | 25 +- .../ssh/verifiers/CheckNewHardStrategy.java | 59 +- .../ssh/verifiers/CheckNewSoftStrategy.java | 62 +- .../plugins/ec2/ssh/verifiers/HostKey.java | 67 +- .../ec2/ssh/verifiers/HostKeyHelper.java | 63 +- .../NonVerifyingKeyVerificationStrategy.java | 13 +- ...tKeyVerificationAdministrativeMonitor.java | 51 +- .../SshHostKeyVerificationStrategy.java | 64 +- .../plugins/ec2/util/AmazonEC2Factory.java | 5 +- .../ec2/util/AmazonEC2FactoryImpl.java | 7 +- .../plugins/ec2/util/DeviceMappingParser.java | 6 +- .../plugins/ec2/util/EC2AgentConfig.java | 20 +- .../plugins/ec2/util/EC2AgentFactory.java | 4 +- .../plugins/ec2/util/EC2AgentFactoryImpl.java | 55 +- .../ec2/util/MinimumInstanceChecker.java | 136 +- ...nimumNumberOfInstancesTimeRangeConfig.java | 35 +- .../ec2/util/ResettableCountDownLatch.java | 5 +- .../plugins/ec2/win/EC2WindowsLauncher.java | 91 +- .../SelfSignedCertificateAllowedMonitor.java | 31 +- .../hudson/plugins/ec2/win/WinConnection.java | 73 +- .../win/winrm/NegotiateNTLMSchemaFactory.java | 6 +- .../hudson/plugins/ec2/win/winrm/WinRM.java | 4 +- .../plugins/ec2/win/winrm/WinRMClient.java | 90 +- .../ec2/win/winrm/WinRMConnectException.java | 1 - .../winrm/WinRMConnectionManagerFactory.java | 21 +- .../plugins/ec2/win/winrm/WindowsProcess.java | 10 +- .../winrm/request/AbstractWinRMRequest.java | 15 +- .../win/winrm/request/DeleteShellRequest.java | 6 +- .../winrm/request/ExecuteCommandRequest.java | 9 +- .../win/winrm/request/GetOutputRequest.java | 12 +- .../win/winrm/request/OpenShellRequest.java | 11 +- .../ec2/win/winrm/request/RequestFactory.java | 1 - .../win/winrm/request/SendInputRequest.java | 14 +- .../ec2/win/winrm/request/SignalRequest.java | 14 +- .../plugins/ec2/win/winrm/soap/Header.java | 68 +- .../ec2/win/winrm/soap/HeaderBuilder.java | 6 +- .../ec2/win/winrm/soap/MessageBuilder.java | 3 +- .../ec2/win/winrm/soap/Namespaces.java | 22 +- .../plugins/ec2/AmazonEC2CloudTest.java | 93 +- .../plugins/ec2/AmazonEC2CloudUnitTest.java | 131 +- .../hudson/plugins/ec2/CloudHelperTest.java | 31 +- .../plugins/ec2/ConfigurationAsCodeTest.java | 49 +- .../plugins/ec2/ConnectionStrategyTest.java | 24 +- .../plugins/ec2/EC2AbstractSlaveTest.java | 153 +- .../plugins/ec2/EC2CloudMigrationTest.java | 23 +- .../java/hudson/plugins/ec2/EC2CloudTest.java | 282 ++- .../hudson/plugins/ec2/EC2FilterTest.java | 12 +- .../ec2/EC2HostAddressProviderTest.java | 1 - .../plugins/ec2/EC2OndemandSlaveTest.java | 57 +- .../hudson/plugins/ec2/EC2PrivateKeyTest.java | 15 +- .../plugins/ec2/EC2RetentionStrategyTest.java | 792 ++++++-- .../plugins/ec2/EC2SlaveMonitorTest.java | 207 +- .../java/hudson/plugins/ec2/EC2StepTest.java | 56 +- .../hudson/plugins/ec2/EucalyptusTest.java | 20 +- .../plugins/ec2/FileBasedSSHKeyTest.java | 35 +- .../hudson/plugins/ec2/SlaveTemplateTest.java | 872 +++++++- .../plugins/ec2/SlaveTemplateUnitTest.java | 839 ++++++-- .../plugins/ec2/TemplateLabelsTest.java | 41 +- .../hudson/plugins/ec2/WinRMMessageTest.java | 9 +- .../ec2/ssh/HostKeyVerifierImplTest.java | 13 +- .../SshHostKeyVerificationStrategyTest.java | 308 +-- .../ec2/util/AmazonEC2FactoryMockImpl.java | 211 +- .../plugins/ec2/util/ConnectionRule.java | 103 +- .../ec2/util/DeviceMappingParserTest.java | 45 +- .../ec2/util/EC2AgentFactoryMockImpl.java | 221 ++- .../plugins/ec2/util/PrivateKeyHelper.java | 3 +- .../plugins/ec2/util/SSHCredentialHelper.java | 31 +- .../ec2/util/TestSSHUserPrivateKey.java | 16 +- .../plugins/ec2/win/WinConnectionTest.java | 14 +- 108 files changed, 7984 insertions(+), 3142 deletions(-) diff --git a/pom.xml b/pom.xml index 161392eae..e400bde7a 100644 --- a/pom.xml +++ b/pom.xml @@ -22,216 +22,216 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --> - - - 4.0.0 - - - org.jenkins-ci.plugins - plugin - 4.88 - - + 4.0.0 + + org.jenkins-ci.plugins + plugin + 4.88 + + - ec2 - ${changelist} - hpi + 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 - 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 - - + + + 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 - - + + + + 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 - https://github.com/${gitHubRepo} - ${scmTag} - + + scm:git:https://github.com/${gitHubRepo}.git + scm:git:git@github.com:${gitHubRepo}.git + ${scmTag} + https://github.com/${gitHubRepo} + - - 999999-SNAPSHOT - 2.452.4 - jenkinsci/${project.artifactId}-plugin - 1626 - + + 999999-SNAPSHOT + 2.452.4 + jenkinsci/${project.artifactId}-plugin + 1626 + false + + - - com.hierynomus - smbj - 0.13.0 - - - org.bouncycastle - bcprov-jdk18on - - - org.slf4j - slf4j-api - - - - - org.jenkins-ci.plugins - credentials - - - org.jenkins-ci.plugins - aws-credentials - - - org.jenkins-ci.plugins - ssh-credentials - - - org.jenkins-ci.plugins - bouncycastle-api - - - org.jenkins-ci.plugins.aws-java-sdk - aws-java-sdk-ec2 - 1.12.696-451.v0651a_da_9ca_ec - - - org.jenkins-ci.plugins.aws-java-sdk - aws-java-sdk-minimal - 1.12.767-467.vb_e93f0c614b_6 - - - org.jenkins-ci.plugins - node-iterator-api - 55.v3b_77d4032326 - - - org.jenkins-ci.plugins - apache-httpcomponents-client-4-api - - - org.jenkins-ci.plugins - command-launcher - - - org.jenkins-ci.plugins - trilead-api - - - org.jenkins-ci.plugins.workflow - workflow-step-api - - - org.mockito - mockito-core - test - - - junit - junit - 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.jenkins-ci.plugins.workflow - workflow-api - test - - - io.jenkins - configuration-as-code - test - - - io.jenkins.configuration-as-code - test-harness - test - - - org.testcontainers - testcontainers - 1.20.3 - test - - - org.kohsuke - libzfs - 0.8 - + + io.jenkins.tools.bom + bom-2.452.x + 3654.v237e4a_f2d8da_ + pom + import + - - - - io.jenkins.tools.bom - bom-2.452.x - 3654.v237e4a_f2d8da_ - import - pom - - - + + + + + com.hierynomus + smbj + 0.13.0 + + + org.bouncycastle + bcprov-jdk18on + + + org.slf4j + slf4j-api + + + + + 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 + 55.v3b_77d4032326 + + + org.jenkins-ci.plugins + ssh-credentials + + + org.jenkins-ci.plugins + trilead-api + + + org.jenkins-ci.plugins.aws-java-sdk + aws-java-sdk-ec2 + 1.12.696-451.v0651a_da_9ca_ec + + + org.jenkins-ci.plugins.aws-java-sdk + aws-java-sdk-minimal + 1.12.767-467.vb_e93f0c614b_6 + + + org.jenkins-ci.plugins.workflow + workflow-step-api + + + org.kohsuke + libzfs + 0.8 + + + io.jenkins + configuration-as-code + test + + + io.jenkins.configuration-as-code + test-harness + test + + + junit + junit + 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.mockito + mockito-core + test + + + org.testcontainers + testcontainers + 1.20.3 + test + + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + - - - repo.jenkins-ci.org - https://repo.jenkins-ci.org/public/ - - - - - repo.jenkins-ci.org - https://repo.jenkins-ci.org/public/ - - + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + - - - - org.jenkins-ci.tools - maven-hpi-plugin - true - - 1.45 - - - - + + + + org.jenkins-ci.tools + maven-hpi-plugin + true + + 1.45 + + + + diff --git a/src/main/java/hudson/plugins/ec2/AMITypeData.java b/src/main/java/hudson/plugins/ec2/AMITypeData.java index 6d27b95c8..997e43e10 100644 --- a/src/main/java/hudson/plugins/ec2/AMITypeData.java +++ b/src/main/java/hudson/plugins/ec2/AMITypeData.java @@ -1,7 +1,6 @@ package hudson.plugins.ec2; import hudson.model.AbstractDescribableImpl; - import java.util.concurrent.TimeUnit; public abstract class AMITypeData extends AbstractDescribableImpl { @@ -14,13 +13,13 @@ public abstract class AMITypeData extends AbstractDescribableImpl { public abstract String getBootDelay(); public int getBootDelayInMillis() { - if (getBootDelay() == null) + if (getBootDelay() == null) { return 0; + } try { return (int) TimeUnit.SECONDS.toMillis(Integer.parseInt(getBootDelay())); } catch (NumberFormatException nfe) { return 0; } } - } diff --git a/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java b/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java index 0441987a4..950fc104b 100644 --- a/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java @@ -24,6 +24,11 @@ package hudson.plugins.ec2; import com.amazonaws.SdkClientException; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.services.ec2.AmazonEC2; +import com.amazonaws.services.ec2.model.DescribeRegionsResult; +import com.amazonaws.services.ec2.model.Region; +import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; import hudson.Util; import hudson.model.Failure; @@ -31,7 +36,6 @@ import hudson.plugins.ec2.util.AmazonEC2Factory; import hudson.util.FormValidation; import hudson.util.ListBoxModel; - import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; @@ -39,21 +43,13 @@ import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; -import edu.umd.cs.findbugs.annotations.Nullable; import javax.servlet.ServletException; - import jenkins.model.Jenkins; - import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.interceptor.RequirePOST; - -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.model.DescribeRegionsResult; -import com.amazonaws.services.ec2.model.Region; import org.kohsuke.stapler.verb.POST; /** @@ -62,8 +58,8 @@ * @author Kohsuke Kawaguchi */ public class AmazonEC2Cloud extends EC2Cloud { - private final static Logger LOGGER = Logger.getLogger(AmazonEC2Cloud.class.getName()); - + private static final Logger LOGGER = Logger.getLogger(AmazonEC2Cloud.class.getName()); + /** * Represents the region. Can be null for backward compatibility reasons. */ @@ -74,14 +70,50 @@ public class AmazonEC2Cloud extends EC2Cloud { private boolean noDelayProvisioning; @DataBoundConstructor - public AmazonEC2Cloud(String name, boolean useInstanceProfileForCredentials, String credentialsId, String region, String privateKey, String sshKeysCredentialsId, String instanceCapStr, List templates, String roleArn, String roleSessionName) { - super(name, useInstanceProfileForCredentials, credentialsId, privateKey, sshKeysCredentialsId, instanceCapStr, templates, roleArn, roleSessionName); + public AmazonEC2Cloud( + String name, + boolean useInstanceProfileForCredentials, + String credentialsId, + String region, + String privateKey, + String sshKeysCredentialsId, + String instanceCapStr, + List templates, + String roleArn, + String roleSessionName) { + super( + name, + useInstanceProfileForCredentials, + credentialsId, + privateKey, + sshKeysCredentialsId, + instanceCapStr, + templates, + roleArn, + roleSessionName); this.region = region; } @Deprecated - public AmazonEC2Cloud(String name, boolean useInstanceProfileForCredentials, String credentialsId, String region, String privateKey, String instanceCapStr, List templates, String roleArn, String roleSessionName) { - super(name, useInstanceProfileForCredentials, credentialsId, privateKey, instanceCapStr, templates, roleArn, roleSessionName); + public AmazonEC2Cloud( + String name, + boolean useInstanceProfileForCredentials, + String credentialsId, + String region, + String privateKey, + String instanceCapStr, + List templates, + String roleArn, + String roleSessionName) { + super( + name, + useInstanceProfileForCredentials, + credentialsId, + privateKey, + instanceCapStr, + templates, + roleArn, + roleSessionName); this.region = region; } @@ -94,12 +126,14 @@ public String getCloudName() { } public String getRegion() { - if (region == null) + if (region == null) { region = DEFAULT_EC2_HOST; // Backward compatibility + } // Handles pre 1.14 region names that used the old AwsRegion enum, note we don't change // the region here to keep the meta-data compatible in the case of a downgrade (is that right?) - if (region.indexOf('_') > 0) + if (region.indexOf('_') > 0) { return region.replace('_', '-').toLowerCase(Locale.ENGLISH); + } return region; } @@ -145,7 +179,12 @@ public void setAltEC2Endpoint(String altEC2Endpoint) { @Override protected AWSCredentialsProvider createCredentialsProvider() { - return createCredentialsProvider(isUseInstanceProfileForCredentials(), getCredentialsId(), getRoleArn(), getRoleSessionName(), getRegion()); + return createCredentialsProvider( + isUseInstanceProfileForCredentials(), + getCredentialsId(), + getRoleArn(), + getRoleSessionName(), + getRegion()); } @Extension @@ -180,20 +219,20 @@ public FormValidation doCheckAltEC2Endpoint(@QueryParameter String value) { } return FormValidation.ok(); } - + @RequirePOST public ListBoxModel doFillRegionItems( @QueryParameter String altEC2Endpoint, @QueryParameter boolean useInstanceProfileForCredentials, @QueryParameter String credentialsId) - throws IOException, ServletException { ListBoxModel model = new ListBoxModel(); if (Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { try { - AWSCredentialsProvider credentialsProvider = createCredentialsProvider(useInstanceProfileForCredentials, - credentialsId); - AmazonEC2 client = AmazonEC2Factory.getInstance().connect(credentialsProvider, determineEC2EndpointURL(altEC2Endpoint)); + AWSCredentialsProvider credentialsProvider = + createCredentialsProvider(useInstanceProfileForCredentials, credentialsId); + AmazonEC2 client = AmazonEC2Factory.getInstance() + .connect(credentialsProvider, determineEC2EndpointURL(altEC2Endpoint)); DescribeRegionsResult regions = client.describeRegions(); List regionList = regions.getRegions(); for (Region r : regionList) { @@ -209,15 +248,18 @@ public ListBoxModel doFillRegionItems( // Will use the alternate EC2 endpoint if provided by the UI (via a @QueryParameter field), or use the default // value if not specified. - //VisibleForTesting + // VisibleForTesting URL determineEC2EndpointURL(@Nullable String altEC2Endpoint) throws MalformedURLException { if (Util.fixEmpty(altEC2Endpoint) == null) { return new URL(DEFAULT_EC2_ENDPOINT); } try { - return new URL(altEC2Endpoint); + return new URL(altEC2Endpoint); } catch (MalformedURLException e) { - LOGGER.log(Level.WARNING, "The alternate EC2 endpoint is malformed ({0}). Using the default endpoint ({1})", new Object[]{altEC2Endpoint, DEFAULT_EC2_ENDPOINT}); + LOGGER.log( + Level.WARNING, + "The alternate EC2 endpoint is malformed ({0}). Using the default endpoint ({1})", + new Object[] {altEC2Endpoint, DEFAULT_EC2_ENDPOINT}); return new URL(DEFAULT_EC2_ENDPOINT); } } @@ -231,14 +273,21 @@ public FormValidation doTestConnection( @QueryParameter String sshKeysCredentialsId, @QueryParameter String roleArn, @QueryParameter String roleSessionName) - throws IOException, ServletException { if (Util.fixEmpty(region) == null) { region = DEFAULT_EC2_HOST; } - return super.doTestConnection(context, getEc2EndpointUrl(region), useInstanceProfileForCredentials, credentialsId, sshKeysCredentialsId, roleArn, roleSessionName, region); + return super.doTestConnection( + context, + getEc2EndpointUrl(region), + useInstanceProfileForCredentials, + credentialsId, + sshKeysCredentialsId, + roleArn, + roleSessionName, + region); } } } diff --git a/src/main/java/hudson/plugins/ec2/CloudHelper.java b/src/main/java/hudson/plugins/ec2/CloudHelper.java index 82344f194..7a453d0ac 100644 --- a/src/main/java/hudson/plugins/ec2/CloudHelper.java +++ b/src/main/java/hudson/plugins/ec2/CloudHelper.java @@ -1,5 +1,7 @@ package hudson.plugins.ec2; +import static hudson.plugins.ec2.EC2Cloud.EC2_REQUEST_EXPIRED_ERROR_CODE; + import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.services.ec2.AmazonEC2; @@ -10,20 +12,18 @@ import com.amazonaws.services.ec2.model.Image; import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.ec2.model.Reservation; -import java.util.ArrayList; -import org.apache.commons.lang.StringUtils; - import edu.umd.cs.findbugs.annotations.CheckForNull; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.logging.Logger; - -import static hudson.plugins.ec2.EC2Cloud.EC2_REQUEST_EXPIRED_ERROR_CODE; +import org.apache.commons.lang.StringUtils; final class CloudHelper { private static final Logger LOGGER = Logger.getLogger(CloudHelper.class.getName()); - static Instance getInstanceWithRetry(String instanceId, EC2Cloud cloud) throws AmazonClientException, InterruptedException { + static Instance getInstanceWithRetry(String instanceId, EC2Cloud cloud) + throws AmazonClientException, InterruptedException { // Sometimes even after a successful RunInstances, DescribeInstances // returns an error for a few seconds. We do a few retries instead of // failing instantly. See [JENKINS-15319]. @@ -31,7 +31,8 @@ static Instance getInstanceWithRetry(String instanceId, EC2Cloud cloud) throws A try { return getInstance(instanceId, cloud); } catch (AmazonServiceException e) { - if (e.getErrorCode().equals("InvalidInstanceID.NotFound") || EC2_REQUEST_EXPIRED_ERROR_CODE.equals(e.getErrorCode())) { + if (e.getErrorCode().equals("InvalidInstanceID.NotFound") + || EC2_REQUEST_EXPIRED_ERROR_CODE.equals(e.getErrorCode())) { // retry in 5 seconds. Thread.sleep(5000); continue; @@ -45,31 +46,35 @@ static Instance getInstanceWithRetry(String instanceId, EC2Cloud cloud) throws A @CheckForNull static Instance getInstance(String instanceId, EC2Cloud cloud) throws AmazonClientException { - if (StringUtils.isEmpty(instanceId) || cloud == null) + if (StringUtils.isEmpty(instanceId) || cloud == null) { return null; + } DescribeInstancesRequest request = new DescribeInstancesRequest(); request.setInstanceIds(Collections.singletonList(instanceId)); - List reservations = cloud.connect().describeInstances(request).getReservations(); + List reservations = + cloud.connect().describeInstances(request).getReservations(); if (reservations.size() != 1) { - String message = "Unexpected number of reservations reported by EC2 for instance id '" + instanceId + "', expected 1 result, found " + reservations + "."; - if (reservations.size() == 0) { - message += " Instance seems to be dead."; - } - LOGGER.info(message); - throw new AmazonClientException(message); + String message = "Unexpected number of reservations reported by EC2 for instance id '" + instanceId + + "', expected 1 result, found " + reservations + "."; + if (reservations.size() == 0) { + message += " Instance seems to be dead."; + } + LOGGER.info(message); + throw new AmazonClientException(message); } Reservation reservation = reservations.get(0); List instances = reservation.getInstances(); if (instances.size() != 1) { - String message = "Unexpected number of instances reported by EC2 for instance id '" + instanceId + "', expected 1 result, found " + instances + "."; - if (instances.size() == 0) { - message += " Instance seems to be dead."; - } - LOGGER.info(message); - throw new AmazonClientException(message); + String message = "Unexpected number of instances reported by EC2 for instance id '" + instanceId + + "', expected 1 result, found " + instances + "."; + if (instances.size() == 0) { + message += " Instance seems to be dead."; + } + LOGGER.info(message); + throw new AmazonClientException(message); } return instances.get(0); } diff --git a/src/main/java/hudson/plugins/ec2/ConnectionStrategy.java b/src/main/java/hudson/plugins/ec2/ConnectionStrategy.java index f182b712d..bb49716a2 100644 --- a/src/main/java/hudson/plugins/ec2/ConnectionStrategy.java +++ b/src/main/java/hudson/plugins/ec2/ConnectionStrategy.java @@ -23,7 +23,8 @@ public String getDisplayText() { * @param associatePublicIp whether or not to associate to a public ip. * @return an {@link ConnectionStrategy} based on provided parameters. */ - public static ConnectionStrategy backwardsCompatible(boolean usePrivateDnsName, boolean connectUsingPublicIp, boolean associatePublicIp) { + public static ConnectionStrategy backwardsCompatible( + boolean usePrivateDnsName, boolean connectUsingPublicIp, boolean associatePublicIp) { if (usePrivateDnsName && !connectUsingPublicIp) { return PRIVATE_DNS; } else if (connectUsingPublicIp || associatePublicIp) { diff --git a/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java b/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java index 949124021..e70319f68 100644 --- a/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java +++ b/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java @@ -23,6 +23,20 @@ */ package hudson.plugins.ec2; +import com.amazonaws.AmazonClientException; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.services.ec2.AmazonEC2; +import com.amazonaws.services.ec2.model.AvailabilityZone; +import com.amazonaws.services.ec2.model.CreateTagsRequest; +import com.amazonaws.services.ec2.model.DeleteTagsRequest; +import com.amazonaws.services.ec2.model.DescribeAvailabilityZonesResult; +import com.amazonaws.services.ec2.model.Instance; +import com.amazonaws.services.ec2.model.InstanceBlockDeviceMapping; +import com.amazonaws.services.ec2.model.InstanceStateName; +import com.amazonaws.services.ec2.model.InstanceType; +import com.amazonaws.services.ec2.model.StopInstancesRequest; +import com.amazonaws.services.ec2.model.Tag; +import com.amazonaws.services.ec2.model.TerminateInstancesRequest; import hudson.Util; import hudson.model.Computer; import hudson.model.Descriptor; @@ -31,11 +45,11 @@ import hudson.model.Slave; import hudson.plugins.ec2.util.AmazonEC2Factory; import hudson.plugins.ec2.util.ResettableCountDownLatch; -import hudson.slaves.NodeProperty; import hudson.slaves.ComputerLauncher; +import hudson.slaves.NodeProperty; import hudson.slaves.RetentionStrategy; import hudson.util.ListBoxModel; - +import hudson.util.Secret; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -45,29 +59,11 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; - -import hudson.util.Secret; import jenkins.model.Jenkins; import net.sf.json.JSONObject; - import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.model.AvailabilityZone; -import com.amazonaws.services.ec2.model.CreateTagsRequest; -import com.amazonaws.services.ec2.model.DeleteTagsRequest; -import com.amazonaws.services.ec2.model.DescribeAvailabilityZonesResult; -import com.amazonaws.services.ec2.model.Instance; -import com.amazonaws.services.ec2.model.InstanceBlockDeviceMapping; -import com.amazonaws.services.ec2.model.InstanceStateName; -import com.amazonaws.services.ec2.model.InstanceType; -import com.amazonaws.services.ec2.model.StopInstancesRequest; -import com.amazonaws.services.ec2.model.Tag; -import com.amazonaws.services.ec2.model.TerminateInstancesRequest; import org.kohsuke.stapler.verb.POST; /** @@ -91,6 +87,7 @@ public abstract class EC2AbstractSlave extends Slave { * Comes from {@link SlaveTemplate#initScript}. */ public final String initScript; + public final String tmpDir; public final String remoteAdmin; // e.g. 'ubuntu' @@ -134,8 +131,8 @@ public abstract class EC2AbstractSlave extends Slave { * The time (in milliseconds) after which we will always re-fetch externally changeable EC2 data when we are asked * for it */ - protected static final long MIN_FETCH_TIME = Long.getLong("hudson.plugins.ec2.EC2AbstractSlave.MIN_FETCH_TIME", - TimeUnit.SECONDS.toMillis(20)); + protected static final long MIN_FETCH_TIME = + Long.getLong("hudson.plugins.ec2.EC2AbstractSlave.MIN_FETCH_TIME", TimeUnit.SECONDS.toMillis(20)); protected final int launchTimeout; @@ -157,7 +154,35 @@ public abstract class EC2AbstractSlave extends Slave { public static final String TEST_ZONE = "testZone"; - public EC2AbstractSlave(String name, String instanceId, String templateDescription, String remoteFS, int numExecutors, Mode mode, String labelString, ComputerLauncher launcher, RetentionStrategy retentionStrategy, String initScript, String tmpDir, List> nodeProperties, String remoteAdmin, String javaPath, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, List tags, String cloudName, int launchTimeout, AMITypeData amiType, ConnectionStrategy connectionStrategy, int maxTotalUses, Tenancy tenancy, Boolean metadataEndpointEnabled, Boolean metadataTokensRequired, Integer metadataHopsLimit, Boolean metadataSupported) + public EC2AbstractSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + Mode mode, + String labelString, + ComputerLauncher launcher, + RetentionStrategy retentionStrategy, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String javaPath, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + Tenancy tenancy, + Boolean metadataEndpointEnabled, + Boolean metadataTokensRequired, + Integer metadataHopsLimit, + Boolean metadataSupported) throws FormException, IOException { super(name, remoteFS, launcher); setNumExecutors(numExecutors); @@ -191,28 +216,224 @@ public EC2AbstractSlave(String name, String instanceId, String templateDescripti } @Deprecated - public EC2AbstractSlave(String name, String instanceId, String templateDescription, String remoteFS, int numExecutors, Mode mode, String labelString, ComputerLauncher launcher, RetentionStrategy retentionStrategy, String initScript, String tmpDir, List> nodeProperties, String remoteAdmin, String javaPath, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, List tags, String cloudName, int launchTimeout, AMITypeData amiType, ConnectionStrategy connectionStrategy, int maxTotalUses, Tenancy tenancy, Boolean metadataEndpointEnabled, Boolean metadataTokensRequired, Integer metadataHopsLimit) + public EC2AbstractSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + Mode mode, + String labelString, + ComputerLauncher launcher, + RetentionStrategy retentionStrategy, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String javaPath, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + Tenancy tenancy, + Boolean metadataEndpointEnabled, + Boolean metadataTokensRequired, + Integer metadataHopsLimit) throws FormException, IOException { - this(name, instanceId, templateDescription, remoteFS, numExecutors, mode, labelString, launcher, retentionStrategy, initScript, tmpDir, nodeProperties, remoteAdmin, DEFAULT_JAVA_PATH, jvmopts, stopOnTerminate, idleTerminationMinutes, tags, cloudName, launchTimeout, amiType, connectionStrategy, maxTotalUses, tenancy, metadataEndpointEnabled, metadataTokensRequired, metadataHopsLimit, DEFAULT_METADATA_SUPPORTED); + this( + name, + instanceId, + templateDescription, + remoteFS, + numExecutors, + mode, + labelString, + launcher, + retentionStrategy, + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + DEFAULT_JAVA_PATH, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + tags, + cloudName, + launchTimeout, + amiType, + connectionStrategy, + maxTotalUses, + tenancy, + metadataEndpointEnabled, + metadataTokensRequired, + metadataHopsLimit, + DEFAULT_METADATA_SUPPORTED); } @Deprecated - public EC2AbstractSlave(String name, String instanceId, String templateDescription, String remoteFS, int numExecutors, Mode mode, String labelString, ComputerLauncher launcher, RetentionStrategy retentionStrategy, String initScript, String tmpDir, List> nodeProperties, String remoteAdmin, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, List tags, String cloudName, int launchTimeout, AMITypeData amiType, ConnectionStrategy connectionStrategy, int maxTotalUses, Tenancy tenancy) + public EC2AbstractSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + Mode mode, + String labelString, + ComputerLauncher launcher, + RetentionStrategy retentionStrategy, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + Tenancy tenancy) throws FormException, IOException { - this(name, instanceId, templateDescription, remoteFS, numExecutors, mode, labelString, launcher, retentionStrategy, initScript, tmpDir, nodeProperties, remoteAdmin, DEFAULT_JAVA_PATH, jvmopts, stopOnTerminate, idleTerminationMinutes, tags, cloudName, launchTimeout, amiType, connectionStrategy, maxTotalUses, tenancy, DEFAULT_METADATA_ENDPOINT_ENABLED, DEFAULT_METADATA_TOKENS_REQUIRED, DEFAULT_METADATA_HOPS_LIMIT); + this( + name, + instanceId, + templateDescription, + remoteFS, + numExecutors, + mode, + labelString, + launcher, + retentionStrategy, + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + DEFAULT_JAVA_PATH, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + tags, + cloudName, + launchTimeout, + amiType, + connectionStrategy, + maxTotalUses, + tenancy, + DEFAULT_METADATA_ENDPOINT_ENABLED, + DEFAULT_METADATA_TOKENS_REQUIRED, + DEFAULT_METADATA_HOPS_LIMIT); } @Deprecated - public EC2AbstractSlave(String name, String instanceId, String templateDescription, String remoteFS, int numExecutors, Mode mode, String labelString, ComputerLauncher launcher, RetentionStrategy retentionStrategy, String initScript, String tmpDir, List> nodeProperties, String remoteAdmin, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, List tags, String cloudName, boolean useDedicatedTenancy, int launchTimeout, AMITypeData amiType, ConnectionStrategy connectionStrategy, int maxTotalUses) + public EC2AbstractSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + Mode mode, + String labelString, + ComputerLauncher launcher, + RetentionStrategy retentionStrategy, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + List tags, + String cloudName, + boolean useDedicatedTenancy, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses) throws FormException, IOException { - this(name, instanceId, templateDescription, remoteFS, numExecutors, mode, labelString, launcher, retentionStrategy, initScript, tmpDir, nodeProperties, remoteAdmin, jvmopts, stopOnTerminate, idleTerminationMinutes, tags, cloudName, launchTimeout, amiType, connectionStrategy, maxTotalUses, Tenancy.backwardsCompatible(useDedicatedTenancy)); + this( + name, + instanceId, + templateDescription, + remoteFS, + numExecutors, + mode, + labelString, + launcher, + retentionStrategy, + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + tags, + cloudName, + launchTimeout, + amiType, + connectionStrategy, + maxTotalUses, + Tenancy.backwardsCompatible(useDedicatedTenancy)); } @Deprecated - public EC2AbstractSlave(String name, String instanceId, String templateDescription, String remoteFS, int numExecutors, Mode mode, String labelString, ComputerLauncher launcher, RetentionStrategy retentionStrategy, String initScript, String tmpDir, List> nodeProperties, String remoteAdmin, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, List tags, String cloudName, boolean usePrivateDnsName, boolean useDedicatedTenancy, int launchTimeout, AMITypeData amiType) + public EC2AbstractSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + Mode mode, + String labelString, + ComputerLauncher launcher, + RetentionStrategy retentionStrategy, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + List tags, + String cloudName, + boolean usePrivateDnsName, + boolean useDedicatedTenancy, + int launchTimeout, + AMITypeData amiType) throws FormException, IOException { - this(name, instanceId, templateDescription, remoteFS, numExecutors, mode, labelString, launcher, retentionStrategy, initScript, tmpDir, nodeProperties, remoteAdmin, jvmopts, stopOnTerminate, idleTerminationMinutes, tags, cloudName, useDedicatedTenancy, launchTimeout, amiType, ConnectionStrategy.backwardsCompatible(usePrivateDnsName, false, false), -1); + this( + name, + instanceId, + templateDescription, + remoteFS, + numExecutors, + mode, + labelString, + launcher, + retentionStrategy, + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + tags, + cloudName, + useDedicatedTenancy, + launchTimeout, + amiType, + ConnectionStrategy.backwardsCompatible(usePrivateDnsName, false, false), + -1); } @Override @@ -228,7 +449,8 @@ protected Object readResolve() { } if (o.amiType == null) { - o.amiType = new UnixData(o.rootCommandPrefix, o.slaveCommandPrefix, o.slaveCommandSuffix, Integer.toString(o.sshPort), null); + o.amiType = new UnixData( + o.rootCommandPrefix, o.slaveCommandPrefix, o.slaveCommandSuffix, Integer.toString(o.sshPort), null); } if (o.maxTotalUses == 0) { @@ -263,174 +485,174 @@ public EC2Cloud getCloud() { /** * See http://aws.amazon.com/ec2/instance-types/ */ - /* package */static int toNumExecutors(InstanceType it) { + /* package */ static int toNumExecutors(InstanceType it) { switch (it) { - case T1Micro: - return 1; - case M1Small: - return 1; - case M1Medium: - return 2; - case M3Medium: - return 2; - case T3Nano: - return 2; - case T3aNano: - return 2; - case T3Micro: - return 2; - case T3aMicro: - return 2; - case T3Small: - return 2; - case T3aSmall: - return 2; - case T3Medium: - return 2; - case T3aMedium: - return 2; - case A1Large: - return 2; - case T3Large: - return 3; - case T3aLarge: - return 3; - case M1Large: - return 4; - case M3Large: - return 4; - case M4Large: - return 4; - case M5Large: - return 4; - case M5aLarge: - return 4; - case T3Xlarge: - return 5; - case T3aXlarge: - return 5; - case A1Xlarge: - return 5; - case C1Medium: - return 5; - case M2Xlarge: - return 6; - case C3Large: - return 7; - case C4Large: - return 7; - case C5Large: - return 7; - case C5dLarge: - return 7; - case M1Xlarge: - return 8; - case T32xlarge: - return 10; - case T3a2xlarge: - return 10; - case A12xlarge: - return 10; - case M22xlarge: - return 13; - case M3Xlarge: - return 13; - case M4Xlarge: - return 13; - case M5Xlarge: - return 13; - case M5aXlarge: - return 13; - case A14xlarge: - return 14; - case C3Xlarge: - return 14; - case C4Xlarge: - return 14; - case C5Xlarge: - return 14; - case C5dXlarge: - return 14; - case C1Xlarge: - return 20; - case M24xlarge: - return 26; - case M32xlarge: - return 26; - case M42xlarge: - return 26; - case M52xlarge: - return 26; - case M5a2xlarge: - return 26; - case G22xlarge: - return 26; - case C32xlarge: - return 28; - case C42xlarge: - return 28; - case C52xlarge: - return 28; - case C5d2xlarge: - return 28; - case Cc14xlarge: - return 33; - case Cg14xlarge: - return 33; - case Hi14xlarge: - return 35; - case Hs18xlarge: - return 35; - case C34xlarge: - return 55; - case C44xlarge: - return 55; - case C54xlarge: - return 55; - case C5d4xlarge: - return 55; - case M44xlarge: - return 55; - case M54xlarge: - return 55; - case M5a4xlarge: - return 55; - case Cc28xlarge: - return 88; - case Cr18xlarge: - return 88; - case C38xlarge: - return 108; - case C48xlarge: - return 108; - case C59xlarge: - return 108; - case C5d9xlarge: - return 108; - case M410xlarge: - return 120; - case M512xlarge: - return 120; - case M5a12xlarge: - return 120; - case M416xlarge: - return 160; - case C518xlarge: - return 216; - case C5d18xlarge: - return 216; - case M524xlarge: - return 240; - case M5a24xlarge: - return 240; - case Dl124xlarge: - return 250; - case Mac1Metal: - return 1; - // We don't have a suggestion, but we don't want to fail completely - // surely? - default: - return 1; + case T1Micro: + return 1; + case M1Small: + return 1; + case M1Medium: + return 2; + case M3Medium: + return 2; + case T3Nano: + return 2; + case T3aNano: + return 2; + case T3Micro: + return 2; + case T3aMicro: + return 2; + case T3Small: + return 2; + case T3aSmall: + return 2; + case T3Medium: + return 2; + case T3aMedium: + return 2; + case A1Large: + return 2; + case T3Large: + return 3; + case T3aLarge: + return 3; + case M1Large: + return 4; + case M3Large: + return 4; + case M4Large: + return 4; + case M5Large: + return 4; + case M5aLarge: + return 4; + case T3Xlarge: + return 5; + case T3aXlarge: + return 5; + case A1Xlarge: + return 5; + case C1Medium: + return 5; + case M2Xlarge: + return 6; + case C3Large: + return 7; + case C4Large: + return 7; + case C5Large: + return 7; + case C5dLarge: + return 7; + case M1Xlarge: + return 8; + case T32xlarge: + return 10; + case T3a2xlarge: + return 10; + case A12xlarge: + return 10; + case M22xlarge: + return 13; + case M3Xlarge: + return 13; + case M4Xlarge: + return 13; + case M5Xlarge: + return 13; + case M5aXlarge: + return 13; + case A14xlarge: + return 14; + case C3Xlarge: + return 14; + case C4Xlarge: + return 14; + case C5Xlarge: + return 14; + case C5dXlarge: + return 14; + case C1Xlarge: + return 20; + case M24xlarge: + return 26; + case M32xlarge: + return 26; + case M42xlarge: + return 26; + case M52xlarge: + return 26; + case M5a2xlarge: + return 26; + case G22xlarge: + return 26; + case C32xlarge: + return 28; + case C42xlarge: + return 28; + case C52xlarge: + return 28; + case C5d2xlarge: + return 28; + case Cc14xlarge: + return 33; + case Cg14xlarge: + return 33; + case Hi14xlarge: + return 35; + case Hs18xlarge: + return 35; + case C34xlarge: + return 55; + case C44xlarge: + return 55; + case C54xlarge: + return 55; + case C5d4xlarge: + return 55; + case M44xlarge: + return 55; + case M54xlarge: + return 55; + case M5a4xlarge: + return 55; + case Cc28xlarge: + return 88; + case Cr18xlarge: + return 88; + case C38xlarge: + return 108; + case C48xlarge: + return 108; + case C59xlarge: + return 108; + case C5d9xlarge: + return 108; + case M410xlarge: + return 120; + case M512xlarge: + return 120; + case M5a12xlarge: + return 120; + case M416xlarge: + return 160; + case C518xlarge: + return 216; + case C5d18xlarge: + return 216; + case M524xlarge: + return 240; + case M5a24xlarge: + return 240; + case Dl124xlarge: + return 250; + case Mac1Metal: + return 1; + // We don't have a suggestion, but we don't want to fail completely + // surely? + default: + return 1; } } @@ -482,13 +704,13 @@ void stop() { } catch (AmazonClientException e) { LOGGER.log(Level.WARNING, "Failed to stop EC2 instance: " + getInstanceId(), e); } - } boolean terminateInstance() { try { AmazonEC2 ec2 = getCloud().connect(); - TerminateInstancesRequest request = new TerminateInstancesRequest(Collections.singletonList(getInstanceId())); + TerminateInstancesRequest request = + new TerminateInstancesRequest(Collections.singletonList(getInstanceId())); LOGGER.fine("Sending terminate request for " + getInstanceId()); ec2.terminateInstances(request); LOGGER.info("EC2 instance terminate request sent for " + getInstanceId()); @@ -532,7 +754,7 @@ void idleTimeout() { } } - void launchTimeout(){ + void launchTimeout() { LOGGER.info("EC2 instance failed to launch: " + getInstanceId()); terminate(); } @@ -543,29 +765,39 @@ public long getLaunchTimeoutInMillis() { } public String getRemoteAdmin() { - if (remoteAdmin == null || remoteAdmin.length() == 0) + if (remoteAdmin == null || remoteAdmin.length() == 0) { return amiType.isWindows() ? "Administrator" : "root"; + } return remoteAdmin; } String getRootCommandPrefix() { - String commandPrefix = (amiType.isUnix() ? ((UnixData) amiType).getRootCommandPrefix() : (amiType.isMac() ? ((MacData) amiType).getRootCommandPrefix() : "")); - if (commandPrefix == null || commandPrefix.length() == 0) + String commandPrefix = (amiType.isUnix() + ? ((UnixData) amiType).getRootCommandPrefix() + : (amiType.isMac() ? ((MacData) amiType).getRootCommandPrefix() : "")); + if (commandPrefix == null || commandPrefix.length() == 0) { return ""; + } return commandPrefix + " "; } String getSlaveCommandPrefix() { - String commandPrefix = (amiType.isUnix() ? ((UnixData) amiType).getSlaveCommandPrefix() :(amiType.isMac() ? ((MacData) amiType).getSlaveCommandPrefix() : "")); - if (commandPrefix == null || commandPrefix.length() == 0) + String commandPrefix = (amiType.isUnix() + ? ((UnixData) amiType).getSlaveCommandPrefix() + : (amiType.isMac() ? ((MacData) amiType).getSlaveCommandPrefix() : "")); + if (commandPrefix == null || commandPrefix.length() == 0) { return ""; + } return commandPrefix + " "; } String getSlaveCommandSuffix() { - String commandSuffix = (amiType.isUnix() ? ((UnixData) amiType).getSlaveCommandSuffix() :(amiType.isMac() ? ((MacData) amiType).getSlaveCommandSuffix() : "")); - if (commandSuffix == null || commandSuffix.length() == 0) + String commandSuffix = (amiType.isUnix() + ? ((UnixData) amiType).getSlaveCommandSuffix() + : (amiType.isMac() ? ((MacData) amiType).getSlaveCommandSuffix() : "")); + if (commandSuffix == null || commandSuffix.length() == 0) { return ""; + } return " " + commandSuffix; } @@ -578,9 +810,12 @@ String getJvmopts() { } public int getSshPort() { - String sshPort = (amiType.isUnix() ? ((UnixData) amiType).getSshPort() :(amiType.isMac() ? ((MacData) amiType).getSshPort() : "22")); - if (sshPort == null || sshPort.length() == 0) + String sshPort = (amiType.isUnix() + ? ((UnixData) amiType).getSshPort() + : (amiType.isMac() ? ((MacData) amiType).getSshPort() : "22")); + if (sshPort == null || sshPort.length() == 0) { return 22; + } int port = 0; try { @@ -603,10 +838,12 @@ public void onConnected() { protected boolean isAlive(boolean force) { fetchLiveInstanceData(force); - if (lastFetchInstance == null) + if (lastFetchInstance == null) { return false; - if (lastFetchInstance.getState().getName().equals(InstanceStateName.Terminated.toString())) + } + if (lastFetchInstance.getState().getName().equals(InstanceStateName.Terminated.toString())) { return false; + } return true; } @@ -639,16 +876,15 @@ private void fetchLiveInstanceData(boolean force) throws AmazonClientException { i = CloudHelper.getInstanceWithRetry(getInstanceId(), getCloud()); } catch (InterruptedException e) { // We'll just retry next time we test for idleness. - LOGGER.fine("InterruptedException while get " + getInstanceId() - + " Exception: " + e); + LOGGER.fine("InterruptedException while get " + getInstanceId() + " Exception: " + e); return; } - lastFetchTime = now; lastFetchInstance = i; - if (i == null) + if (i == null) { return; + } publicDNS = i.getPublicDnsName(); privateDNS = i.getPrivateIpAddress(); @@ -659,7 +895,7 @@ private void fetchLiveInstanceData(boolean force) throws AmazonClientException { * Only fetch tags from live instance if tags are set. This check is required to mitigate a race condition * when fetchLiveInstanceData() is called before pushLiveInstancedata(). */ - if(!i.getTags().isEmpty()) { + if (!i.getTags().isEmpty()) { tags = new LinkedList(); for (Tag t : i.getTags()) { tags.add(new EC2Tag(t.getKey(), t.getValue())); @@ -676,12 +912,10 @@ protected void clearLiveInstancedata() throws AmazonClientException { inst = CloudHelper.getInstanceWithRetry(getInstanceId(), getCloud()); } catch (InterruptedException e) { // We'll just retry next time we test for idleness. - LOGGER.fine("InterruptedException while get " + getInstanceId() - + " Exception: " + e); + LOGGER.fine("InterruptedException while get " + getInstanceId() + " Exception: " + e); return; } - /* Now that we have our instance, we can clear the tags on it */ if (!tags.isEmpty()) { HashSet instTags = new HashSet(); @@ -707,11 +941,9 @@ protected void pushLiveInstancedata() throws AmazonClientException { inst = CloudHelper.getInstanceWithRetry(getInstanceId(), getCloud()); } catch (InterruptedException e) { // We'll just retry next time we test for idleness. - LOGGER.fine("InterruptedException while get " + getInstanceId() - + " Exception: " + e); + LOGGER.fine("InterruptedException while get " + getInstanceId() + " Exception: " + e); } - /* Now that we have our instance, we can set tags on it */ if (inst != null && tags != null && !tags.isEmpty()) { HashSet instTags = new HashSet(); @@ -733,7 +965,7 @@ protected void pushLiveInstancedata() throws AmazonClientException { private List getResourcesToTag(Instance inst) { List resources = new ArrayList<>(); resources.add(inst.getInstanceId()); - for(InstanceBlockDeviceMapping blockDeviceMapping : inst.getBlockDeviceMappings()) { + for (InstanceBlockDeviceMapping blockDeviceMapping : inst.getBlockDeviceMappings()) { resources.add(blockDeviceMapping.getEbs().getVolumeId()); } return resources; @@ -782,11 +1014,11 @@ public int getBootDelay() { } public Boolean getMetadataSupported() { - return metadataSupported; + return metadataSupported; } public Boolean getMetadataEndpointEnabled() { - return metadataEndpointEnabled; + return metadataEndpointEnabled; } public Boolean getMetadataTokensRequired() { @@ -809,7 +1041,8 @@ public static ListBoxModel fillZoneItems(AWSCredentialsProvider credentialsProvi ListBoxModel model = new ListBoxModel(); if (!StringUtils.isEmpty(region)) { - AmazonEC2 client = AmazonEC2Factory.getInstance().connect(credentialsProvider, AmazonEC2Cloud.getEc2EndpointUrl(region)); + AmazonEC2 client = AmazonEC2Factory.getInstance() + .connect(credentialsProvider, AmazonEC2Cloud.getEc2EndpointUrl(region)); DescribeAvailabilityZonesResult zones = client.describeAvailabilityZones(); List zoneList = zones.getAvailabilityZones(); model.add("", ""); @@ -823,9 +1056,9 @@ public static ListBoxModel fillZoneItems(AWSCredentialsProvider credentialsProvi /* * Used to determine if the agent is On Demand or Spot */ - abstract public String getEc2Type(); + public abstract String getEc2Type(); - public static abstract class DescriptorImpl extends SlaveDescriptor { + public abstract static class DescriptorImpl extends SlaveDescriptor { @Override public abstract String getDisplayName(); @@ -836,15 +1069,17 @@ public boolean isInstantiable() { } @POST - public ListBoxModel doFillZoneItems(@QueryParameter boolean useInstanceProfileForCredentials, - @QueryParameter String credentialsId, - @QueryParameter String region, - @QueryParameter String roleArn, - @QueryParameter String roleSessionName) { + public ListBoxModel doFillZoneItems( + @QueryParameter boolean useInstanceProfileForCredentials, + @QueryParameter String credentialsId, + @QueryParameter String region, + @QueryParameter String roleArn, + @QueryParameter String roleSessionName) { if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { return new ListBoxModel(); } - AWSCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider(useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); + AWSCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider( + useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); return fillZoneItems(credentialsProvider, region); } @@ -852,5 +1087,4 @@ public List> getAMITypeDescriptors() { return Jenkins.get().getDescriptorList(AMITypeData.class); } } - } diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 138a94179..59d06d0c6 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -18,6 +18,9 @@ */ package hudson.plugins.ec2; +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.ClientConfiguration; @@ -47,6 +50,7 @@ import com.cloudbees.plugins.credentials.common.StandardListBoxModel; import com.cloudbees.plugins.credentials.domains.Domain; import com.cloudbees.plugins.credentials.domains.DomainRequirement; +import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.ProxyConfiguration; @@ -67,17 +71,6 @@ import hudson.util.ListBoxModel; import hudson.util.Secret; import hudson.util.StreamTaskListener; -import jenkins.model.Jenkins; -import jenkins.model.JenkinsLocationConfiguration; -import org.apache.commons.lang.StringUtils; -import org.kohsuke.stapler.AncestorInPath; -import org.kohsuke.stapler.HttpResponse; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; - -import edu.umd.cs.findbugs.annotations.CheckForNull; -import javax.servlet.ServletException; import java.io.BufferedReader; import java.io.IOException; import java.io.PrintStream; @@ -95,10 +88,15 @@ import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; - -import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; -import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; - +import javax.servlet.ServletException; +import jenkins.model.Jenkins; +import jenkins.model.JenkinsLocationConfiguration; +import org.apache.commons.lang.StringUtils; +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.interceptor.RequirePOST; import org.kohsuke.stapler.verb.POST; @@ -144,15 +142,19 @@ public abstract class EC2Cloud extends Cloud { */ @CheckForNull private String credentialsId; + @CheckForNull @Deprecated private transient String accessId; + @CheckForNull @Deprecated private transient Secret secretKey; + @CheckForNull @Deprecated private transient EC2PrivateKey privateKey; + @CheckForNull private String sshKeysCredentialsId; @@ -167,8 +169,16 @@ public abstract class EC2Cloud extends Cloud { private transient volatile AmazonEC2 connection; - protected EC2Cloud(String name, boolean useInstanceProfileForCredentials, String credentialsId, String privateKey, String sshKeysCredentialsId, - String instanceCapStr, List templates, String roleArn, String roleSessionName) { + protected EC2Cloud( + String name, + boolean useInstanceProfileForCredentials, + String credentialsId, + String privateKey, + String sshKeysCredentialsId, + String instanceCapStr, + List templates, + String roleArn, + String roleSessionName) { super(name); this.useInstanceProfileForCredentials = useInstanceProfileForCredentials; this.roleArn = roleArn; @@ -192,13 +202,29 @@ protected EC2Cloud(String name, boolean useInstanceProfileForCredentials, String } @Deprecated - protected EC2Cloud(String id, boolean useInstanceProfileForCredentials, String credentialsId, String privateKey, - String instanceCapStr, List templates, String roleArn, String roleSessionName) { - this(id, useInstanceProfileForCredentials, credentialsId, privateKey, null, instanceCapStr, templates, roleArn, roleSessionName); + protected EC2Cloud( + String id, + boolean useInstanceProfileForCredentials, + String credentialsId, + String privateKey, + String instanceCapStr, + List templates, + String roleArn, + String roleSessionName) { + this( + id, + useInstanceProfileForCredentials, + credentialsId, + privateKey, + null, + instanceCapStr, + templates, + roleArn, + roleSessionName); } @CheckForNull - public EC2PrivateKey resolvePrivateKey(){ + public EC2PrivateKey resolvePrivateKey() { if (!System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { LOGGER.fine(() -> "(resolvePrivateKey) secret key file configured, will load from disk"); return EC2PrivateKey.fetchFromDisk(); @@ -218,18 +244,23 @@ public EC2PrivateKey resolvePrivateKey(){ public void addTemplate(SlaveTemplate newTemplate) throws Exception { String newTemplateDescription = newTemplate.description; - if (getTemplate(newTemplateDescription) != null) throw new Exception( - String.format("A SlaveTemplate with description %s already exists", newTemplateDescription)); + if (getTemplate(newTemplateDescription) != null) { + throw new Exception( + String.format("A SlaveTemplate with description %s already exists", newTemplateDescription)); + } List templatesHolder = new ArrayList<>(templates); templatesHolder.add(newTemplate); templates = templatesHolder; } - public void updateTemplate(SlaveTemplate newTemplate, String oldTemplateDescription) throws Exception{ - Optional optionalOldTemplate = templates.stream().filter(template -> - Objects.equals(template.description, oldTemplateDescription)).findFirst(); - if (!optionalOldTemplate.isPresent()) - throw new Exception(String.format("A SlaveTemplate with description %s does not exist", oldTemplateDescription)); + public void updateTemplate(SlaveTemplate newTemplate, String oldTemplateDescription) throws Exception { + Optional optionalOldTemplate = templates.stream() + .filter(template -> Objects.equals(template.description, oldTemplateDescription)) + .findFirst(); + if (!optionalOldTemplate.isPresent()) { + throw new Exception( + String.format("A SlaveTemplate with description %s does not exist", oldTemplateDescription)); + } int oldTemplateIndex = templates.indexOf(optionalOldTemplate.get()); List templatesHolder = new ArrayList<>(templates); templatesHolder.set(oldTemplateIndex, newTemplate); @@ -238,28 +269,33 @@ public void updateTemplate(SlaveTemplate newTemplate, String oldTemplateDescript private void migratePrivateSshKeyToCredential(String privateKey) { // GET matching private key credential from Credential API if exists - Optional keyCredential = SystemCredentialsProvider.getInstance().getCredentials() - .stream() - .filter((cred) -> cred instanceof SSHUserPrivateKey) - .filter((cred) -> ((SSHUserPrivateKey)cred).getPrivateKey().trim().equals(privateKey.trim())) - .map(cred -> (SSHUserPrivateKey)cred) + Optional keyCredential = SystemCredentialsProvider.getInstance().getCredentials().stream() + .filter(SSHUserPrivateKey.class::isInstance) + .filter(cred -> + ((SSHUserPrivateKey) cred).getPrivateKey().trim().equals(privateKey.trim())) + .map(cred -> (SSHUserPrivateKey) cred) .findFirst(); - if (keyCredential.isPresent()){ + if (keyCredential.isPresent()) { // SET this.sshKeysCredentialsId with the found credential sshKeysCredentialsId = keyCredential.get().getId(); } else { // CREATE new credential String credsId = UUID.randomUUID().toString(); - SSHUserPrivateKey sshKeyCredentials = new BasicSSHUserPrivateKey(CredentialsScope.SYSTEM, credsId, "key", + SSHUserPrivateKey sshKeyCredentials = new BasicSSHUserPrivateKey( + CredentialsScope.SYSTEM, + credsId, + "key", new BasicSSHUserPrivateKey.PrivateKeySource() { @NonNull @Override public List getPrivateKeys() { return Collections.singletonList(privateKey.trim()); } - }, "", "EC2 Cloud Private Key - " + getDisplayName()); + }, + "", + "EC2 Cloud Private Key - " + getDisplayName()); addNewGlobalCredential(sshKeyCredentials); @@ -270,13 +306,15 @@ public List getPrivateKeys() { protected Object readResolve() { this.slaveCountingLock = new ReentrantLock(); - for (SlaveTemplate t : templates) + for (SlaveTemplate t : templates) { t.parent = this; + } - if (this.sshKeysCredentialsId == null && this.privateKey != null ){ + if (this.sshKeysCredentialsId == null && this.privateKey != null) { migratePrivateSshKeyToCredential(this.privateKey.getPrivateKey()); } - this.privateKey = null; // This enforces it not to be persisted and that CasC will never output privateKey on export + this.privateKey = + null; // This enforces it not to be persisted and that CasC will never output privateKey on export if (this.accessId != null && this.secretKey != null && credentialsId == null) { String secretKeyEncryptedValue = this.secretKey.getEncryptedValue(); @@ -285,12 +323,12 @@ protected Object readResolve() { SystemCredentialsProvider systemCredentialsProvider = SystemCredentialsProvider.getInstance(); // ITERATE ON EXISTING CREDS AND DON'T CREATE IF EXIST - for (Credentials credentials: systemCredentialsProvider.getCredentials()) { + for (Credentials credentials : systemCredentialsProvider.getCredentials()) { if (credentials instanceof AmazonWebServicesCredentials) { AmazonWebServicesCredentials awsCreds = (AmazonWebServicesCredentials) credentials; AWSCredentials awsCredentials = awsCreds.getCredentials(); - if (accessId.equals(awsCredentials.getAWSAccessKeyId()) && - Secret.toString(this.secretKey).equals(awsCredentials.getAWSSecretKey())) { + if (accessId.equals(awsCredentials.getAWSAccessKeyId()) + && Secret.toString(this.secretKey).equals(awsCredentials.getAWSSecretKey())) { this.credentialsId = awsCreds.getId(); this.accessId = null; @@ -303,34 +341,39 @@ protected Object readResolve() { // CREATE String credsId = UUID.randomUUID().toString(); addNewGlobalCredential(new AWSCredentialsImpl( - CredentialsScope.SYSTEM, credsId, this.accessId, secretKeyEncryptedValue, + CredentialsScope.SYSTEM, + credsId, + this.accessId, + secretKeyEncryptedValue, "EC2 Cloud - " + getDisplayName())); this.credentialsId = credsId; this.accessId = null; this.secretKey = null; - // PROBLEM, GLOBAL STORE NOT FOUND - LOGGER.log(Level.WARNING, "EC2 Plugin could not migrate credentials to the Jenkins Global Credentials Store, EC2 Plugin for cloud {0} must be manually reconfigured", getDisplayName()); + LOGGER.log( + Level.WARNING, + "EC2 Plugin could not migrate credentials to the Jenkins Global Credentials Store, EC2 Plugin for cloud {0} must be manually reconfigured", + getDisplayName()); } return this; } - private void addNewGlobalCredential(Credentials credentials){ - for (CredentialsStore credentialsStore: CredentialsProvider.lookupStores(Jenkins.get())) { + private void addNewGlobalCredential(Credentials credentials) { + for (CredentialsStore credentialsStore : CredentialsProvider.lookupStores(Jenkins.get())) { - if (credentialsStore instanceof SystemCredentialsProvider.StoreImpl) { + if (credentialsStore instanceof SystemCredentialsProvider.StoreImpl) { try { credentialsStore.addCredentials(Domain.global(), credentials); } catch (IOException e) { this.credentialsId = null; - LOGGER.log(Level.WARNING, "Exception converting legacy configuration to the new credentials API", e); + LOGGER.log( + Level.WARNING, "Exception converting legacy configuration to the new credentials API", e); } } - } } @@ -361,10 +404,11 @@ public EC2PrivateKey getPrivateKey() { } public String getInstanceCapStr() { - if (instanceCap == Integer.MAX_VALUE) + if (instanceCap == Integer.MAX_VALUE) { return ""; - else + } else { return String.valueOf(instanceCap); + } } public int getInstanceCap() { @@ -474,17 +518,20 @@ public HttpResponse doProvision(@QueryParameter String template) throws ServletE } try { List nodes = getNewOrExistingAvailableSlave(t, 1, true); - if (nodes == null || nodes.isEmpty()) - throw HttpResponses.error(SC_BAD_REQUEST, "Cloud or AMI instance cap would be exceeded for: " + template); + if (nodes == null || nodes.isEmpty()) { + throw HttpResponses.error( + SC_BAD_REQUEST, "Cloud or AMI instance cap would be exceeded for: " + template); + } - //Reconnect a stopped instance, the ADD is invoking the connect only for the node creation + // Reconnect a stopped instance, the ADD is invoking the connect only for the node creation Computer c = nodes.get(0).toComputer(); - if (nodes.get(0).getStopOnTerminate() && c != null) { + if (nodes.get(0).getStopOnTerminate() && c != null) { c.connect(false); } jenkinsInstance.addNode(nodes.get(0)); - return HttpResponses.redirectViaContextPath("/computer/" + nodes.get(0).getNodeName()); + return HttpResponses.redirectViaContextPath( + "/computer/" + nodes.get(0).getNodeName()); } catch (AmazonClientException e) { throw HttpResponses.error(SC_INTERNAL_SERVER_ERROR, e); } @@ -500,13 +547,19 @@ private int countCurrentEC2Slaves(SlaveTemplate template) throws AmazonClientExc String jenkinsServerUrl = JenkinsLocationConfiguration.get().getUrl(); if (jenkinsServerUrl == null) { - LOGGER.log(Level.WARNING, "No Jenkins server URL specified, it is strongly recommended to open /configure and set the server URL. " + - "Not having has disabled the per-controller instance cap counting (cf. https://github.com/jenkinsci/ec2-plugin/pull/310)"); + LOGGER.log( + Level.WARNING, + "No Jenkins server URL specified, it is strongly recommended to open /configure and set the server URL. " + + "Not having has disabled the per-controller instance cap counting (cf. https://github.com/jenkinsci/ec2-plugin/pull/310)"); } - LOGGER.log(Level.FINE, "Counting current agents: " - + (template != null ? (" AMI: " + template.getAmi() + " TemplateDesc: " + template.description) : " All AMIS") - + " Jenkins Server: " + jenkinsServerUrl); + LOGGER.log( + Level.FINE, + "Counting current agents: " + + (template != null + ? (" AMI: " + template.getAmi() + " TemplateDesc: " + template.description) + : " All AMIS") + + " Jenkins Server: " + jenkinsServerUrl); int n = 0; Set instanceIds = new HashSet<>(); String description = template != null ? template.description : null; @@ -521,14 +574,17 @@ private int countCurrentEC2Slaves(SlaveTemplate template) throws AmazonClientExc for (Reservation r : result.getReservations()) { for (Instance i : r.getInstances()) { if (isEc2ProvisionedAmiSlave(i.getTags(), description)) { - LOGGER.log(Level.FINE, "Existing instance found: " + i.getInstanceId() + " AMI: " + i.getImageId() - + (template != null ? (" Template: " + description) : "") + " Jenkins Server: " + jenkinsServerUrl); + LOGGER.log( + Level.FINE, + "Existing instance found: " + i.getInstanceId() + " AMI: " + i.getImageId() + + (template != null ? (" Template: " + description) : "") + " Jenkins Server: " + + jenkinsServerUrl); n++; instanceIds.add(i.getInstanceId()); } } } - } while(result.getNextToken() != null); + } while (result.getNextToken() != null); n += countCurrentEC2SpotSlaves(template, jenkinsServerUrl, instanceIds); @@ -541,7 +597,8 @@ private int countCurrentEC2Slaves(SlaveTemplate template) throws AmazonClientExc * * @param template If left null, then all spot instances are counted. */ - private int countCurrentEC2SpotSlaves(SlaveTemplate template, String jenkinsServerUrl, Set instanceIds) throws AmazonClientException { + private int countCurrentEC2SpotSlaves(SlaveTemplate template, String jenkinsServerUrl, Set instanceIds) + throws AmazonClientException { int n = 0; String description = template != null ? template.description : null; List sirs = null; @@ -550,7 +607,8 @@ private int countCurrentEC2SpotSlaves(SlaveTemplate template, String jenkinsServ filters.add(new Filter("launch.image-id").withValues(template.getAmi())); } - DescribeSpotInstanceRequestsRequest dsir = new DescribeSpotInstanceRequestsRequest().withFilters(filters).withMaxResults(100); + DescribeSpotInstanceRequestsRequest dsir = + new DescribeSpotInstanceRequestsRequest().withFilters(filters).withMaxResults(100); Set sirSet = new HashSet<>(); DescribeSpotInstanceRequestsResult sirResp = null; @@ -569,51 +627,66 @@ private int countCurrentEC2SpotSlaves(SlaveTemplate template, String jenkinsServ for (SpotInstanceRequest sir : sirs) { sirSet.add(sir); if (sir.getState().equals("open") || sir.getState().equals("active")) { - if (sir.getInstanceId() != null && instanceIds.contains(sir.getInstanceId())) + if (sir.getInstanceId() != null && instanceIds.contains(sir.getInstanceId())) { continue; + } if (isEc2ProvisionedAmiSlave(sir.getTags(), description)) { - LOGGER.log(Level.FINE, "Spot instance request found: " + sir.getSpotInstanceRequestId() + " AMI: " - + sir.getInstanceId() + " state: " + sir.getState() + " status: " + sir.getStatus()); + LOGGER.log( + Level.FINE, + "Spot instance request found: " + sir.getSpotInstanceRequestId() + " AMI: " + + sir.getInstanceId() + " state: " + sir.getState() + " status: " + + sir.getStatus()); n++; - if (sir.getInstanceId() != null) + if (sir.getInstanceId() != null) { instanceIds.add(sir.getInstanceId()); + } } } else { // Cancelled or otherwise dead for (Node node : Jenkins.get().getNodes()) { try { - if (!(node instanceof EC2SpotSlave)) + if (!(node instanceof EC2SpotSlave)) { continue; + } EC2SpotSlave ec2Slave = (EC2SpotSlave) node; if (ec2Slave.getSpotInstanceRequestId().equals(sir.getSpotInstanceRequestId())) { - LOGGER.log(Level.INFO, "Removing dead request: " + sir.getSpotInstanceRequestId() + " AMI: " - + sir.getInstanceId() + " state: " + sir.getState() + " status: " + sir.getStatus()); + LOGGER.log( + Level.INFO, + "Removing dead request: " + sir.getSpotInstanceRequestId() + " AMI: " + + sir.getInstanceId() + " state: " + sir.getState() + " status: " + + sir.getStatus()); Jenkins.get().removeNode(node); break; } } catch (IOException e) { - LOGGER.log(Level.WARNING, "Failed to remove node for dead request: " + sir.getSpotInstanceRequestId() - + " AMI: " + sir.getInstanceId() + " state: " + sir.getState() + " status: " + sir.getStatus(), + LOGGER.log( + Level.WARNING, + "Failed to remove node for dead request: " + sir.getSpotInstanceRequestId() + + " AMI: " + sir.getInstanceId() + " state: " + sir.getState() + + " status: " + sir.getStatus(), e); } } } } } - } while(sirResp.getNextToken() != null); + } while (sirResp.getNextToken() != null); n += countJenkinsNodeSpotInstancesWithoutRequests(template, sirSet, instanceIds); return n; } // Count nodes where the spot request does not yet exist (sometimes it takes time for the request to appear // in the EC2 API) - private int countJenkinsNodeSpotInstancesWithoutRequests(SlaveTemplate template, Set sirSet, Set instanceIds) throws AmazonClientException { + private int countJenkinsNodeSpotInstancesWithoutRequests( + SlaveTemplate template, Set sirSet, Set instanceIds) + throws AmazonClientException { int n = 0; for (Node node : Jenkins.get().getNodes()) { - if (!(node instanceof EC2SpotSlave)) + if (!(node instanceof EC2SpotSlave)) { continue; + } EC2SpotSlave ec2Slave = (EC2SpotSlave) node; SpotInstanceRequest sir = ec2Slave.getSpotRequest(); @@ -623,8 +696,9 @@ private int countJenkinsNodeSpotInstancesWithoutRequests(SlaveTemplate template, continue; } - if (sirSet.contains(sir)) + if (sirSet.contains(sir)) { continue; + } sirSet.add(sir); @@ -632,17 +706,25 @@ private int countJenkinsNodeSpotInstancesWithoutRequests(SlaveTemplate template, if (template != null) { List instanceTags = sir.getTags(); for (Tag tag : instanceTags) { - if (StringUtils.equals(tag.getKey(), EC2Tag.TAG_NAME_JENKINS_SLAVE_TYPE) && StringUtils.equals(tag.getValue(), getSlaveTypeTagValue(EC2_SLAVE_TYPE_SPOT, template.description)) && sir.getLaunchSpecification().getImageId().equals(template.getAmi())) { + if (StringUtils.equals(tag.getKey(), EC2Tag.TAG_NAME_JENKINS_SLAVE_TYPE) + && StringUtils.equals( + tag.getValue(), getSlaveTypeTagValue(EC2_SLAVE_TYPE_SPOT, template.description)) + && sir.getLaunchSpecification().getImageId().equals(template.getAmi())) { - if (sir.getInstanceId() != null && instanceIds.contains(sir.getInstanceId())) + if (sir.getInstanceId() != null && instanceIds.contains(sir.getInstanceId())) { continue; + } - LOGGER.log(Level.FINE, "Spot instance request found (from node): " + sir.getSpotInstanceRequestId() + " AMI: " - + sir.getInstanceId() + " state: " + sir.getState() + " status: " + sir.getStatus()); + LOGGER.log( + Level.FINE, + "Spot instance request found (from node): " + sir.getSpotInstanceRequestId() + + " AMI: " + sir.getInstanceId() + " state: " + sir.getState() + " status: " + + sir.getStatus()); n++; - if (sir.getInstanceId() != null) + if (sir.getInstanceId() != null) { instanceIds.add(sir.getInstanceId()); + } } } } @@ -651,7 +733,6 @@ private int countJenkinsNodeSpotInstancesWithoutRequests(SlaveTemplate template, return n; } - private List getGenericFilters(String jenkinsServerUrl, SlaveTemplate template) { List filters = new ArrayList<>(); filters.add(new Filter("tag-key").withValues(EC2Tag.TAG_NAME_JENKINS_SLAVE_TYPE)); @@ -684,8 +765,10 @@ private boolean isEc2ProvisionedAmiSlave(List tags, String description) { || StringUtils.equals(tag.getValue(), EC2Cloud.EC2_SLAVE_TYPE_SPOT)) { // To handle cases where description is null and also upgrade cases for existing agent nodes. return true; - } else if (StringUtils.equals(tag.getValue(), getSlaveTypeTagValue(EC2Cloud.EC2_SLAVE_TYPE_DEMAND, description)) - || StringUtils.equals(tag.getValue(), getSlaveTypeTagValue(EC2Cloud.EC2_SLAVE_TYPE_SPOT, description))) { + } else if (StringUtils.equals( + tag.getValue(), getSlaveTypeTagValue(EC2Cloud.EC2_SLAVE_TYPE_DEMAND, description)) + || StringUtils.equals( + tag.getValue(), getSlaveTypeTagValue(EC2Cloud.EC2_SLAVE_TYPE_SPOT, description))) { return true; } else { return false; @@ -704,8 +787,10 @@ private int getPossibleNewSlavesCount(SlaveTemplate template) throws AmazonClien int availableTotalSlaves = instanceCap - estimatedTotalSlaves; int availableAmiSlaves = template.getInstanceCap() - estimatedAmiSlaves; - LOGGER.log(Level.FINE, "Available Total Agents: " + availableTotalSlaves + " Available AMI agents: " + availableAmiSlaves - + " AMI: " + template.getAmi() + " TemplateDesc: " + template.description); + LOGGER.log( + Level.FINE, + "Available Total Agents: " + availableTotalSlaves + " Available AMI agents: " + availableAmiSlaves + + " AMI: " + template.getAmi() + " TemplateDesc: " + template.description); return Math.min(availableAmiSlaves, availableTotalSlaves); } @@ -714,7 +799,8 @@ private int getPossibleNewSlavesCount(SlaveTemplate template) throws AmazonClien * Obtains a agent whose AMI matches the AMI of the given template, and that also has requiredLabel (if requiredLabel is non-null) * forceCreateNew specifies that the creation of a new agent is required. Otherwise, an existing matching agent may be re-used */ - private List getNewOrExistingAvailableSlave(SlaveTemplate t, int number, boolean forceCreateNew) throws IOException { + private List getNewOrExistingAvailableSlave(SlaveTemplate t, int number, boolean forceCreateNew) + throws IOException { try { slaveCountingLock.lock(); int possibleSlavesCount = getPossibleNewSlavesCount(t); @@ -724,19 +810,26 @@ private List getNewOrExistingAvailableSlave(SlaveTemplate t, i } EnumSet provisionOptions; - if (forceCreateNew) + if (forceCreateNew) { provisionOptions = EnumSet.of(SlaveTemplate.ProvisionOptions.FORCE_CREATE); - else + } else { provisionOptions = EnumSet.of(SlaveTemplate.ProvisionOptions.ALLOW_CREATE); + } if (number > possibleSlavesCount) { - LOGGER.log(Level.INFO, String.format("%d nodes were requested for the template %s, " + - "but because of instance cap only %d can be provisioned", number, t, possibleSlavesCount)); + LOGGER.log( + Level.INFO, + String.format( + "%d nodes were requested for the template %s, " + + "but because of instance cap only %d can be provisioned", + number, t, possibleSlavesCount)); number = possibleSlavesCount; } return t.provision(number, provisionOptions); - } finally { slaveCountingLock.unlock(); } + } finally { + slaveCountingLock.unlock(); + } } @Override @@ -755,7 +848,10 @@ public Collection provision(final Label label, int excessWorkload) for (SlaveTemplate t : matchingTemplates) { try { - LOGGER.log(Level.INFO, "{0}. Attempting to provision agent needed by excess workload of " + excessWorkload + " units", t); + LOGGER.log( + Level.INFO, + "{0}. Attempting to provision agent needed by excess workload of " + excessWorkload + " units", + t); int number = Math.max(excessWorkload / t.getNumExecutors(), 1); final List slaves = getNewOrExistingAvailableSlave(t, number, false); @@ -775,7 +871,9 @@ public Collection provision(final Label label, int excessWorkload) } LOGGER.log(Level.INFO, "{0}. Attempting provision finished, excess workload: " + excessWorkload, t); - if (excessWorkload == 0) break; + if (excessWorkload == 0) { + break; + } } catch (AmazonServiceException e) { LOGGER.log(Level.WARNING, t + ". Exception during provisioning", e); if (e.getErrorCode().equals("RequestExpired")) { @@ -791,12 +889,14 @@ public Collection provision(final Label label, int excessWorkload) LOGGER.log(Level.WARNING, t + ". Exception during provisioning", e); } } - LOGGER.log(Level.INFO, "We have now {0} computers, waiting for {1} more", - new Object[]{jenkinsInstance.getComputers().length, plannedNodes.size()}); + LOGGER.log(Level.INFO, "We have now {0} computers, waiting for {1} more", new Object[] { + jenkinsInstance.getComputers().length, plannedNodes.size() + }); return plannedNodes; } - private static void attachSlavesToJenkins(Jenkins jenkins, List slaves, SlaveTemplate t) throws IOException { + private static void attachSlavesToJenkins(Jenkins jenkins, List slaves, SlaveTemplate t) + throws IOException { for (final EC2AbstractSlave slave : slaves) { if (slave == null) { LOGGER.warning("Can't raise node for " + t); @@ -823,7 +923,7 @@ public void provision(SlaveTemplate t, int number) { } try { - LOGGER.log(Level.INFO, "{0}. Attempting to provision {1} agent(s)", new Object[]{t, number}); + LOGGER.log(Level.INFO, "{0}. Attempting to provision {1} agent(s)", new Object[] {t, number}); final List slaves = getNewOrExistingAvailableSlave(t, number, false); if (slaves == null || slaves.isEmpty()) { @@ -834,8 +934,9 @@ public void provision(SlaveTemplate t, int number) { attachSlavesToJenkins(jenkinsInstance, slaves, t); LOGGER.log(Level.INFO, "{0}. Attempting provision finished", t); - LOGGER.log(Level.INFO, "We have now {0} computers, waiting for {1} more", - new Object[]{Jenkins.get().getComputers().length, number}); + LOGGER.log(Level.INFO, "We have now {0} computers, waiting for {1} more", new Object[] { + Jenkins.get().getComputers().length, number + }); } catch (AmazonClientException | IOException e) { LOGGER.log(Level.WARNING, t + ". Exception during provisioning", e); } @@ -848,10 +949,11 @@ public void provision(SlaveTemplate t, int number) { * @param template The corresponding SlaveTemplate of the nodes that are to be re-attached * @param requestedNum The requested number of nodes to re-attach. We don't go above this in the case its value corresponds to an instance cap. */ - void attemptReattachOrphanOrStoppedNodes(Jenkins jenkinsInstance, SlaveTemplate template, int requestedNum) throws IOException { + void attemptReattachOrphanOrStoppedNodes(Jenkins jenkinsInstance, SlaveTemplate template, int requestedNum) + throws IOException { LOGGER.info("Attempting to wake & re-attach orphan/stopped nodes"); AmazonEC2 ec2 = this.connect(); - DescribeInstancesResult diResult = template.getDescribeInstanceResult(ec2,true); + DescribeInstancesResult diResult = template.getDescribeInstanceResult(ec2, true); List orphansOrStopped = template.findOrphansOrStopped(diResult, requestedNum); template.wakeOrphansOrStoppedUp(ec2, orphansOrStopped); /* If the number of possible nodes to re-attach is greater than the number of nodes requested, will only attempt to re-attach up to the number requested */ @@ -865,16 +967,22 @@ void attemptReattachOrphanOrStoppedNodes(Jenkins jenkinsInstance, SlaveTemplate } private PlannedNode createPlannedNode(final SlaveTemplate t, final EC2AbstractSlave slave) { - return new PlannedNode(t.getDisplayName(), + return new PlannedNode( + t.getDisplayName(), Computer.threadPoolForRemoting.submit(new Callable() { - int retryCount = 0; + int retryCount = 0; private static final int DESCRIBE_LIMIT = 2; + + @Override public Node call() throws Exception { while (true) { String instanceId = slave.getInstanceId(); if (slave instanceof EC2SpotSlave) { if (((EC2SpotSlave) slave).isSpotRequestDead()) { - LOGGER.log(Level.WARNING, "{0} Spot request died, can't do anything. Terminate provisioning", t); + LOGGER.log( + Level.WARNING, + "{0} Spot request died, can't do anything. Terminate provisioning", + t); return null; } @@ -887,47 +995,56 @@ public Node call() throws Exception { Instance instance = CloudHelper.getInstanceWithRetry(instanceId, slave.getCloud()); if (instance == null) { - LOGGER.log(Level.WARNING, "{0} Can't find instance with instance id `{1}` in cloud {2}. Terminate provisioning ", - new Object[]{t, instanceId, slave.cloudName}); + LOGGER.log( + Level.WARNING, + "{0} Can't find instance with instance id `{1}` in cloud {2}. Terminate provisioning ", + new Object[] {t, instanceId, slave.cloudName}); return null; } - InstanceStateName state = InstanceStateName.fromValue(instance.getState().getName()); - if (state.equals(InstanceStateName.Running)) { - //Spot instance are not reconnected automatically, + InstanceStateName state = InstanceStateName.fromValue( + instance.getState().getName()); + if (state.equals(InstanceStateName.Running)) { + // Spot instance are not reconnected automatically, // but could be new orphans that has the option enable Computer c = slave.toComputer(); - if (slave.getStopOnTerminate() && (c != null )) { + if (slave.getStopOnTerminate() && (c != null)) { c.connect(false); } - long startTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - instance.getLaunchTime().getTime()); - LOGGER.log(Level.INFO, "{0} Node {1} moved to RUNNING state in {2} seconds and is ready to be connected by Jenkins", - new Object[]{t, slave.getNodeName(), startTime}); + long startTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() + - instance.getLaunchTime().getTime()); + LOGGER.log( + Level.INFO, + "{0} Node {1} moved to RUNNING state in {2} seconds and is ready to be connected by Jenkins", + new Object[] {t, slave.getNodeName(), startTime}); return slave; } if (!state.equals(InstanceStateName.Pending)) { - if (retryCount >= DESCRIBE_LIMIT){ - LOGGER.log(Level.WARNING,"Instance {0} did not move to running after {1} attempts, terminating provisioning", - new Object[]{instanceId, retryCount}); + if (retryCount >= DESCRIBE_LIMIT) { + LOGGER.log( + Level.WARNING, + "Instance {0} did not move to running after {1} attempts, terminating provisioning", + new Object[] {instanceId, retryCount}); return null; } - LOGGER.log(Level.INFO, "Attempt {0}: {1}. Node {2} is neither pending, neither running, it''s {3}. Will try again after 5s", - new Object[]{retryCount, t, slave.getNodeName(), state}); + LOGGER.log( + Level.INFO, + "Attempt {0}: {1}. Node {2} is neither pending, neither running, it''s {3}. Will try again after 5s", + new Object[] {retryCount, t, slave.getNodeName(), state}); retryCount++; } Thread.sleep(5000); } } - }) - , t.getNumExecutors()); + }), + t.getNumExecutors()); } - @Override public boolean canProvision(Label label) { return !getTemplates(label).isEmpty(); @@ -941,15 +1058,17 @@ public static String getSlaveTypeTagValue(String slaveType, String templateDescr return templateDescription != null ? slaveType + "_" + templateDescription : slaveType; } - public static AWSCredentialsProvider createCredentialsProvider(final boolean useInstanceProfileForCredentials, final String credentialsId) { + public static AWSCredentialsProvider createCredentialsProvider( + final boolean useInstanceProfileForCredentials, final String credentialsId) { if (useInstanceProfileForCredentials) { return new InstanceProfileCredentialsProvider(false); } else if (StringUtils.isBlank(credentialsId)) { return new DefaultAWSCredentialsProviderChain(); } else { AmazonWebServicesCredentials credentials = getCredentials(credentialsId); - if (credentials != null) + if (credentials != null) { return new AWSStaticCredentialsProvider(credentials.getCredentials()); + } } return new DefaultAWSCredentialsProviderChain(); } @@ -982,13 +1101,13 @@ private static AmazonWebServicesCredentials getCredentials(@CheckForNull String return null; } return (AmazonWebServicesCredentials) CredentialsMatchers.firstOrNull( - CredentialsProvider.lookupCredentials(AmazonWebServicesCredentials.class, Jenkins.get(), - ACL.SYSTEM, Collections.emptyList()), + CredentialsProvider.lookupCredentials( + AmazonWebServicesCredentials.class, Jenkins.get(), ACL.SYSTEM, Collections.emptyList()), CredentialsMatchers.withId(credentialsId)); } private AmazonEC2 reconnectToEc2() throws IOException { - synchronized(this) { + synchronized (this) { connection = AmazonEC2Factory.getInstance().connect(createCredentialsProvider(), getEc2EndpointUrl()); return connection; } @@ -1001,8 +1120,7 @@ public AmazonEC2 connect() throws AmazonClientException { try { if (connection != null) { return connection; - } - else { + } else { return reconnectToEc2(); } } catch (IOException e) { @@ -1047,8 +1165,9 @@ public static String getAwsPartitionHostForService(String region, String service * Convert a configured hostname like 'us-east-1' to a FQDN or ip address */ public static String convertHostName(String ec2HostName) { - if (ec2HostName == null || ec2HostName.length() == 0) + if (ec2HostName == null || ec2HostName.length() == 0) { ec2HostName = DEFAULT_EC2_HOST; + } if (!ec2HostName.contains(".")) { ec2HostName = getAwsPartitionHostForService(ec2HostName, "ec2"); } @@ -1059,8 +1178,9 @@ public static String convertHostName(String ec2HostName) { * Convert a user entered string into a port number "" -> -1 to indicate default based on SSL setting */ public static Integer convertPort(String ec2Port) { - if (ec2Port == null || ec2Port.length() == 0) + if (ec2Port == null || ec2Port.length() == 0) { return -1; + } return Integer.parseInt(ec2Port); } @@ -1075,7 +1195,7 @@ public URL buildPresignedURL(String path) throws AmazonClientException { long expires = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(60); GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(path, credentials.getAWSSecretKey()); request.setExpiration(new Date(expires)); - AmazonS3 s3 = AmazonS3ClientBuilder.standard().withCredentials(provider).build(); + AmazonS3 s3 = AmazonS3ClientBuilder.standard().withCredentials(provider).build(); return s3.generatePresignedUrl(request); } @@ -1089,24 +1209,27 @@ public static URL checkEndPoint(String url) throws FormValidation { } @CheckForNull - private static SSHUserPrivateKey getSshCredential(String id, ItemGroup context){ + private static SSHUserPrivateKey getSshCredential(String id, ItemGroup context) { SSHUserPrivateKey credential = CredentialsMatchers.firstOrNull( - CredentialsProvider.lookupCredentials( - SSHUserPrivateKey.class, // (1) - context, - null, - Collections.emptyList()), - CredentialsMatchers.withId(id)); - - if (credential == null){ - LOGGER.log(Level.WARNING, "EC2 Plugin could not find the specified credentials ({0}) in the Jenkins Global Credentials Store, EC2 Plugin for cloud must be manually reconfigured", new String[]{id}); + CredentialsProvider.lookupCredentials( + SSHUserPrivateKey.class, // (1) + context, + null, + Collections.emptyList()), + CredentialsMatchers.withId(id)); + + if (credential == null) { + LOGGER.log( + Level.WARNING, + "EC2 Plugin could not find the specified credentials ({0}) in the Jenkins Global Credentials Store, EC2 Plugin for cloud must be manually reconfigured", + new String[] {id}); } return credential; } - public static abstract class DescriptorImpl extends Descriptor { + public abstract static class DescriptorImpl extends Descriptor { public InstanceType[] getInstanceTypes() { return InstanceType.values(); @@ -1126,20 +1249,31 @@ public FormValidation doCheckUseInstanceProfileForCredentials(@QueryParameter bo } @POST - public ListBoxModel doFillSshKeysCredentialsIdItems(@AncestorInPath ItemGroup context, @QueryParameter String sshKeysCredentialsId) { + public ListBoxModel doFillSshKeysCredentialsIdItems( + @AncestorInPath ItemGroup context, @QueryParameter String sshKeysCredentialsId) { AbstractIdCredentialsListBoxModel result = new StandardListBoxModel(); if (Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { - result = result - .includeEmptyValue() - .includeMatchingAs(Jenkins.getAuthentication(), context, SSHUserPrivateKey.class, Collections.emptyList(), CredentialsMatchers.always()) - .includeMatchingAs(ACL.SYSTEM, context, SSHUserPrivateKey.class, Collections.emptyList(), CredentialsMatchers.always()) + result = result.includeEmptyValue() + .includeMatchingAs( + Jenkins.getAuthentication(), + context, + SSHUserPrivateKey.class, + Collections.emptyList(), + CredentialsMatchers.always()) + .includeMatchingAs( + ACL.SYSTEM, + context, + SSHUserPrivateKey.class, + Collections.emptyList(), + CredentialsMatchers.always()) .includeCurrentValue(sshKeysCredentialsId); } return result; } @RequirePOST - public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup context, @QueryParameter String value) throws IOException, ServletException { + public FormValidation doCheckSshKeysCredentialsId( + @AncestorInPath ItemGroup context, @QueryParameter String value) throws IOException, ServletException { if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { // Don't do anything if the user is only reading the configuration return FormValidation.ok(); @@ -1163,31 +1297,36 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont } else { EC2PrivateKey k = EC2PrivateKey.fetchFromDisk(); if (k == null) { - validations.add(FormValidation.error("Failed to find private key file " + System.getProperty(SSH_PRIVATE_KEY_FILEPATH))); + validations.add(FormValidation.error( + "Failed to find private key file " + System.getProperty(SSH_PRIVATE_KEY_FILEPATH))); if (!StringUtils.isEmpty(value)) { - validations.add(FormValidation.warning("Private key file path defined, selected credential will be ignored")); + validations.add(FormValidation.warning( + "Private key file path defined, selected credential will be ignored")); } return FormValidation.aggregate(validations); } privateKey = k.getPrivateKey(); } - boolean hasStart = false, hasEnd = false; BufferedReader br = new BufferedReader(new StringReader(privateKey)); String line; while ((line = br.readLine()) != null) { - if (line.equals("-----BEGIN RSA PRIVATE KEY-----") || - line.equals("-----BEGIN OPENSSH PRIVATE KEY-----")) + if (line.equals("-----BEGIN RSA PRIVATE KEY-----") + || line.equals("-----BEGIN OPENSSH PRIVATE KEY-----")) { hasStart = true; - if (line.equals("-----END RSA PRIVATE KEY-----") || - line.equals("-----END OPENSSH PRIVATE KEY-----")) + } + if (line.equals("-----END RSA PRIVATE KEY-----") || line.equals("-----END OPENSSH PRIVATE KEY-----")) { hasEnd = true; + } } - if (!hasStart) + if (!hasStart) { validations.add(FormValidation.error("This doesn't look like a private key at all")); - if (!hasEnd) - validations.add(FormValidation.error("The private key is missing the trailing 'END RSA PRIVATE KEY' marker. Copy&paste error?")); + } + if (!hasEnd) { + validations.add(FormValidation.error( + "The private key is missing the trailing 'END RSA PRIVATE KEY' marker. Copy&paste error?")); + } if (!System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { if (!StringUtils.isEmpty(value)) { @@ -1217,7 +1356,15 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont * @throws ServletException */ @POST - protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL ec2endpoint, boolean useInstanceProfileForCredentials, String credentialsId, String sshKeysCredentialsId, String roleArn, String roleSessionName, String region) + protected FormValidation doTestConnection( + @AncestorInPath ItemGroup context, + URL ec2endpoint, + boolean useInstanceProfileForCredentials, + String credentialsId, + String sshKeysCredentialsId, + String roleArn, + String roleSessionName, + String region) throws IOException, ServletException { if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { return FormValidation.ok(); @@ -1233,14 +1380,17 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL if (sshCredential != null) { privateKey = sshCredential.getPrivateKey(); } else { - return FormValidation.error("Failed to find credential \"" + sshKeysCredentialsId + "\" in store."); + return FormValidation.error( + "Failed to find credential \"" + sshKeysCredentialsId + "\" in store."); } } else { EC2PrivateKey k = EC2PrivateKey.fetchFromDisk(); if (k == null) { - validations.add(FormValidation.error("Failed to find private key file " + System.getProperty(SSH_PRIVATE_KEY_FILEPATH))); + validations.add(FormValidation.error( + "Failed to find private key file " + System.getProperty(SSH_PRIVATE_KEY_FILEPATH))); if (!StringUtils.isEmpty(sshKeysCredentialsId)) { - validations.add(FormValidation.warning("Private key file path defined, selected credential will be ignored")); + validations.add(FormValidation.warning( + "Private key file path defined, selected credential will be ignored")); } return FormValidation.aggregate(validations); } @@ -1248,23 +1398,25 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL } LOGGER.fine(() -> "private key found ok"); - AWSCredentialsProvider credentialsProvider = createCredentialsProvider(useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); + AWSCredentialsProvider credentialsProvider = createCredentialsProvider( + useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); AmazonEC2 ec2 = AmazonEC2Factory.getInstance().connect(credentialsProvider, ec2endpoint); ec2.describeInstances(); - if (privateKey.trim().length() > 0) { // check if this key exists EC2PrivateKey pk = new EC2PrivateKey(privateKey); - if (pk.find(ec2) == null) - validations.add(FormValidation - .error("The EC2 key pair private key isn't registered to this EC2 region (fingerprint is " + if (pk.find(ec2) == null) { + validations.add(FormValidation.error( + "The EC2 key pair private key isn't registered to this EC2 region (fingerprint is " + pk.getFingerprint() + ")")); + } } if (!System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { if (!StringUtils.isEmpty(sshKeysCredentialsId)) { - validations.add(FormValidation.warning("Using private key file instead of selected credential")); + validations.add( + FormValidation.warning("Using private key file instead of selected credential")); } else { validations.add(FormValidation.ok("Using private key file")); } @@ -1284,7 +1436,12 @@ public ListBoxModel doFillCredentialsIdItems(@AncestorInPath ItemGroup context) } return new StandardListBoxModel() .includeEmptyValue() - .includeMatchingAs(ACL.SYSTEM, context, AmazonWebServicesCredentials.class, Collections.emptyList(), CredentialsMatchers.always()); + .includeMatchingAs( + ACL.SYSTEM, + context, + AmazonWebServicesCredentials.class, + Collections.emptyList(), + CredentialsMatchers.always()); } } @@ -1295,8 +1452,9 @@ public static void log(Logger logger, Level level, TaskListener listener, String public static void log(Logger logger, Level level, TaskListener listener, String message, Throwable exception) { logger.log(level, message, exception); if (listener != null) { - if (exception != null) + if (exception != null) { message += " Exception: " + exception; + } LogRecord lr = new LogRecord(level, message); lr.setLoggerName(LOGGER.getName()); PrintStream printStream = listener.getLogger(); @@ -1320,7 +1478,7 @@ protected void doRun() throws IOException { EC2Cloud ec2_cloud = (EC2Cloud) cloud; LOGGER.finer(() -> "Checking EC2 Connection on: " + ec2_cloud.getDisplayName()); try { - if(ec2_cloud.connection != null) { + if (ec2_cloud.connection != null) { List filters = new ArrayList<>(); filters.add(new Filter("tag-key").withValues("bogus-EC2ConnectionKeepalive")); DescribeInstancesRequest dir = new DescribeInstancesRequest().withFilters(filters); diff --git a/src/main/java/hudson/plugins/ec2/EC2Computer.java b/src/main/java/hudson/plugins/ec2/EC2Computer.java index 489b2f938..03e7b2494 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Computer.java +++ b/src/main/java/hudson/plugins/ec2/EC2Computer.java @@ -23,6 +23,8 @@ */ package hudson.plugins.ec2; +import com.amazonaws.AmazonClientException; +import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.model.*; import edu.umd.cs.findbugs.annotations.CheckForNull; import hudson.Util; @@ -32,12 +34,8 @@ import java.util.Collections; import java.util.logging.Level; import java.util.logging.Logger; - - import org.kohsuke.stapler.HttpRedirect; import org.kohsuke.stapler.HttpResponse; -import com.amazonaws.AmazonClientException; -import com.amazonaws.services.ec2.AmazonEC2; import org.kohsuke.stapler.verb.POST; /** @@ -119,11 +117,12 @@ public String getDecodedConsoleOutput() throws AmazonClientException { } } - private GetConsoleOutputResult getDecodedConsoleOutputResponse() throws AmazonClientException, InterruptedException { + private GetConsoleOutputResult getDecodedConsoleOutputResponse() + throws AmazonClientException, InterruptedException { AmazonEC2 ec2 = getCloud().connect(); GetConsoleOutputRequest request = new GetConsoleOutputRequest(getInstanceId()); if (checkIfNitro()) { - //Can only be used if instance has hypervisor Nitro + // Can only be used if instance has hypervisor Nitro request.setLatest(true); } return ec2.getConsoleOutput(request); @@ -136,7 +135,8 @@ private boolean checkIfNitro() throws AmazonClientException, InterruptedExceptio try { if (isNitro == null) { DescribeInstanceTypesRequest request = new DescribeInstanceTypesRequest(); - request.setInstanceTypes(Collections.singletonList(describeInstance().getInstanceType())); + request.setInstanceTypes( + Collections.singletonList(describeInstance().getInstanceType())); AmazonEC2 ec2 = getCloud().connect(); DescribeInstanceTypesResult result = ec2.describeInstanceTypes(request); if (result.getInstanceTypes().size() == 1) { @@ -145,7 +145,6 @@ private boolean checkIfNitro() throws AmazonClientException, InterruptedExceptio } else { isNitro = false; } - } return isNitro; } catch (AmazonClientException e) { @@ -165,8 +164,9 @@ private boolean checkIfNitro() throws AmazonClientException, InterruptedExceptio * The cache can be flushed using {@link #updateInstanceDescription()} */ public Instance describeInstance() throws AmazonClientException, InterruptedException { - if (ec2InstanceDescription == null) + if (ec2InstanceDescription == null) { ec2InstanceDescription = CloudHelper.getInstanceWithRetry(getInstanceId(), getCloud()); + } return ec2InstanceDescription; } @@ -219,8 +219,9 @@ public long getLaunchTime() throws InterruptedException { public HttpResponse doDoDelete() throws IOException { checkPermission(DELETE); EC2AbstractSlave node = getNode(); - if (node != null) + if (node != null) { node.terminate(); + } return new HttpRedirect(".."); } @@ -261,5 +262,4 @@ public void onConnected() { node.onConnected(); } } - } diff --git a/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java b/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java index 4d95a7b01..b268ed941 100644 --- a/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java +++ b/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java @@ -23,16 +23,14 @@ */ package hudson.plugins.ec2; +import com.amazonaws.AmazonClientException; import hudson.model.TaskListener; import hudson.slaves.ComputerLauncher; import hudson.slaves.SlaveComputer; - import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; -import com.amazonaws.AmazonClientException; - /** * {@link ComputerLauncher} for EC2 that wraps the real user-specified {@link ComputerLauncher}. * @@ -48,8 +46,13 @@ public void launch(SlaveComputer slaveComputer, TaskListener listener) { launchScript(computer, listener); } catch (AmazonClientException | IOException e) { e.printStackTrace(listener.error(e.getMessage())); - if (slaveComputer.getNode() instanceof EC2AbstractSlave) { - LOGGER.log(Level.FINE, String.format("Terminating the ec2 agent %s due a problem launching or connecting to it", slaveComputer.getName()), e); + if (slaveComputer.getNode() instanceof EC2AbstractSlave) { + LOGGER.log( + Level.FINE, + String.format( + "Terminating the ec2 agent %s due a problem launching or connecting to it", + slaveComputer.getName()), + e); EC2AbstractSlave ec2AbstractSlave = (EC2AbstractSlave) slaveComputer.getNode(); if (ec2AbstractSlave != null) { ec2AbstractSlave.terminate(); @@ -58,15 +61,19 @@ public void launch(SlaveComputer slaveComputer, TaskListener listener) { } catch (InterruptedException e) { Thread.currentThread().interrupt(); e.printStackTrace(listener.error(e.getMessage())); - if (slaveComputer.getNode() instanceof EC2AbstractSlave) { - LOGGER.log(Level.FINE, String.format("Terminating the ec2 agent %s due a problem launching or connecting to it", slaveComputer.getName()), e); + if (slaveComputer.getNode() instanceof EC2AbstractSlave) { + LOGGER.log( + Level.FINE, + String.format( + "Terminating the ec2 agent %s due a problem launching or connecting to it", + slaveComputer.getName()), + e); EC2AbstractSlave ec2AbstractSlave = (EC2AbstractSlave) slaveComputer.getNode(); if (ec2AbstractSlave != null) { ec2AbstractSlave.terminate(); } } } - } /** @@ -74,5 +81,4 @@ public void launch(SlaveComputer slaveComputer, TaskListener listener) { */ protected abstract void launchScript(EC2Computer computer, TaskListener listener) throws AmazonClientException, IOException, InterruptedException; - } diff --git a/src/main/java/hudson/plugins/ec2/EC2ComputerListener.java b/src/main/java/hudson/plugins/ec2/EC2ComputerListener.java index db79e2380..d199bd994 100644 --- a/src/main/java/hudson/plugins/ec2/EC2ComputerListener.java +++ b/src/main/java/hudson/plugins/ec2/EC2ComputerListener.java @@ -1,8 +1,8 @@ package hudson.plugins.ec2; import hudson.Extension; -import hudson.model.TaskListener; import hudson.model.Computer; +import hudson.model.TaskListener; import hudson.slaves.ComputerListener; @Extension diff --git a/src/main/java/hudson/plugins/ec2/EC2Filter.java b/src/main/java/hudson/plugins/ec2/EC2Filter.java index b432d27b0..fd7d707cb 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Filter.java +++ b/src/main/java/hudson/plugins/ec2/EC2Filter.java @@ -23,21 +23,18 @@ */ package hudson.plugins.ec2; -import hudson.model.Descriptor; -import hudson.model.AbstractDescribableImpl; +import com.amazonaws.services.ec2.model.Filter; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.Util; -import org.kohsuke.stapler.DataBoundConstructor; - +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; - -import edu.umd.cs.findbugs.annotations.CheckForNull; -import edu.umd.cs.findbugs.annotations.NonNull; - -import com.amazonaws.services.ec2.model.Filter; +import org.kohsuke.stapler.DataBoundConstructor; public class EC2Filter extends AbstractDescribableImpl { @NonNull @@ -65,10 +62,12 @@ public String toString() { @Override public boolean equals(Object o) { - if (o == null) + if (o == null) { return false; - if (this.getClass() != o.getClass()) + } + if (this.getClass() != o.getClass()) { return false; + } EC2Filter other = (EC2Filter) o; return name.equals(other.name) && getValuesList().equals(other.getValuesList()); @@ -91,8 +90,7 @@ public String getValues() { @NonNull private List getValuesList() { - return Stream.of(Util.tokenize(values)) - .collect(Collectors.toList()); + return Stream.of(Util.tokenize(values)).collect(Collectors.toList()); } /* Helper method to convert EC2Filter to Filter */ @@ -104,9 +102,7 @@ public Filter toFilter() { /* Helper method to convert list of EC2Filter to list of Filter */ @NonNull public static List toFilterList(@CheckForNull List filters) { - return Util.fixNull(filters).stream() - .map(EC2Filter::toFilter) - .collect(Collectors.toList()); + return Util.fixNull(filters).stream().map(EC2Filter::toFilter).collect(Collectors.toList()); } @Extension diff --git a/src/main/java/hudson/plugins/ec2/EC2HostAddressProvider.java b/src/main/java/hudson/plugins/ec2/EC2HostAddressProvider.java index 0be9d394c..596a4eefc 100644 --- a/src/main/java/hudson/plugins/ec2/EC2HostAddressProvider.java +++ b/src/main/java/hudson/plugins/ec2/EC2HostAddressProvider.java @@ -1,11 +1,10 @@ package hudson.plugins.ec2; -import com.amazonaws.services.ec2.model.Instance; -import org.apache.commons.lang.StringUtils; +import static hudson.plugins.ec2.ConnectionStrategy.*; +import com.amazonaws.services.ec2.model.Instance; import java.util.Optional; - -import static hudson.plugins.ec2.ConnectionStrategy.*; +import org.apache.commons.lang.StringUtils; public class EC2HostAddressProvider { public static String unix(Instance instance, ConnectionStrategy strategy) { diff --git a/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java b/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java index d648d8d10..fe759f14a 100644 --- a/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java +++ b/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java @@ -1,31 +1,26 @@ package hudson.plugins.ec2; +import com.amazonaws.AmazonClientException; +import com.amazonaws.services.ec2.AmazonEC2; +import com.amazonaws.services.ec2.model.*; import hudson.Extension; -import hudson.model.Descriptor.FormException; import hudson.model.Computer; +import hudson.model.Descriptor.FormException; import hudson.model.Node; +import hudson.plugins.ec2.ssh.EC2MacLauncher; import hudson.plugins.ec2.ssh.EC2UnixLauncher; import hudson.plugins.ec2.win.EC2WindowsLauncher; -import hudson.plugins.ec2.ssh.EC2MacLauncher; import hudson.slaves.NodeProperty; - import java.io.IOException; import java.util.Collections; import java.util.List; -import java.util.concurrent.CountDownLatch; import java.util.logging.Level; import java.util.logging.Logger; - import jenkins.model.Jenkins; import net.sf.json.JSONObject; - import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; -import com.amazonaws.AmazonClientException; -import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.model.*; - /** * Agent running on EC2. * @@ -35,46 +30,373 @@ public class EC2OndemandSlave extends EC2AbstractSlave { private static final Logger LOGGER = Logger.getLogger(EC2OndemandSlave.class.getName()); @Deprecated - public EC2OndemandSlave(String instanceId, String templateDescription, String remoteFS, int numExecutors, String labelString, Mode mode, String initScript, String tmpDir, String remoteAdmin, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, String publicDNS, String privateDNS, List tags, String cloudName, int launchTimeout, AMITypeData amiType) + public EC2OndemandSlave( + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + String labelString, + Mode mode, + String initScript, + String tmpDir, + String remoteAdmin, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + String publicDNS, + String privateDNS, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType) throws FormException, IOException { - this(instanceId, templateDescription, remoteFS, numExecutors, labelString, mode, initScript, tmpDir, remoteAdmin, jvmopts, stopOnTerminate, idleTerminationMinutes, publicDNS, privateDNS, tags, cloudName, false, launchTimeout, amiType); + this( + instanceId, + templateDescription, + remoteFS, + numExecutors, + labelString, + mode, + initScript, + tmpDir, + remoteAdmin, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + publicDNS, + privateDNS, + tags, + cloudName, + false, + launchTimeout, + amiType); } @Deprecated - public EC2OndemandSlave(String instanceId, String templateDescription, String remoteFS, int numExecutors, String labelString, Mode mode, String initScript, String tmpDir, String remoteAdmin, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, String publicDNS, String privateDNS, List tags, String cloudName, boolean useDedicatedTenancy, int launchTimeout, AMITypeData amiType) + public EC2OndemandSlave( + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + String labelString, + Mode mode, + String initScript, + String tmpDir, + String remoteAdmin, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + String publicDNS, + String privateDNS, + List tags, + String cloudName, + boolean useDedicatedTenancy, + int launchTimeout, + AMITypeData amiType) throws FormException, IOException { - this(instanceId, templateDescription, remoteFS, numExecutors, labelString, mode, initScript, tmpDir, remoteAdmin, jvmopts, stopOnTerminate, idleTerminationMinutes, publicDNS, privateDNS, tags, cloudName, false, useDedicatedTenancy, launchTimeout, amiType); + this( + instanceId, + templateDescription, + remoteFS, + numExecutors, + labelString, + mode, + initScript, + tmpDir, + remoteAdmin, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + publicDNS, + privateDNS, + tags, + cloudName, + false, + useDedicatedTenancy, + launchTimeout, + amiType); } @Deprecated - public EC2OndemandSlave(String instanceId, String templateDescription, String remoteFS, int numExecutors, String labelString, Mode mode, String initScript, String tmpDir, String remoteAdmin, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, String publicDNS, String privateDNS, List tags, String cloudName, boolean usePrivateDnsName, boolean useDedicatedTenancy, int launchTimeout, AMITypeData amiType) + public EC2OndemandSlave( + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + String labelString, + Mode mode, + String initScript, + String tmpDir, + String remoteAdmin, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + String publicDNS, + String privateDNS, + List tags, + String cloudName, + boolean usePrivateDnsName, + boolean useDedicatedTenancy, + int launchTimeout, + AMITypeData amiType) throws FormException, IOException { - this(templateDescription + " (" + instanceId + ")", instanceId, templateDescription, remoteFS, numExecutors, labelString, mode, initScript, tmpDir, Collections.emptyList(), remoteAdmin, jvmopts, stopOnTerminate, idleTerminationMinutes, publicDNS, privateDNS, tags, cloudName, useDedicatedTenancy, launchTimeout, amiType, ConnectionStrategy.backwardsCompatible(usePrivateDnsName, false, false), -1); + this( + templateDescription + " (" + instanceId + ")", + instanceId, + templateDescription, + remoteFS, + numExecutors, + labelString, + mode, + initScript, + tmpDir, + Collections.emptyList(), + remoteAdmin, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + publicDNS, + privateDNS, + tags, + cloudName, + useDedicatedTenancy, + launchTimeout, + amiType, + ConnectionStrategy.backwardsCompatible(usePrivateDnsName, false, false), + -1); } @Deprecated - public EC2OndemandSlave(String name, String instanceId, String templateDescription, String remoteFS, int numExecutors, String labelString, Mode mode, String initScript, String tmpDir, List> nodeProperties, String remoteAdmin, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, String publicDNS, String privateDNS, List tags, String cloudName, boolean useDedicatedTenancy, int launchTimeout, AMITypeData amiType, ConnectionStrategy connectionStrategy, int maxTotalUses) + public EC2OndemandSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + String labelString, + Mode mode, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + String publicDNS, + String privateDNS, + List tags, + String cloudName, + boolean useDedicatedTenancy, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses) throws FormException, IOException { - this(name, instanceId, templateDescription, remoteFS, numExecutors, labelString, mode, initScript, tmpDir, nodeProperties, remoteAdmin, jvmopts, stopOnTerminate, idleTerminationMinutes, publicDNS, privateDNS, tags, cloudName, launchTimeout, amiType, connectionStrategy, maxTotalUses, Tenancy.backwardsCompatible(useDedicatedTenancy)); + this( + name, + instanceId, + templateDescription, + remoteFS, + numExecutors, + labelString, + mode, + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + publicDNS, + privateDNS, + tags, + cloudName, + launchTimeout, + amiType, + connectionStrategy, + maxTotalUses, + Tenancy.backwardsCompatible(useDedicatedTenancy)); } @Deprecated - public EC2OndemandSlave(String name, String instanceId, String templateDescription, String remoteFS, int numExecutors, String labelString, Mode mode, String initScript, String tmpDir, List> nodeProperties, String remoteAdmin, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, String publicDNS, String privateDNS, List tags, String cloudName, int launchTimeout, AMITypeData amiType, ConnectionStrategy connectionStrategy, int maxTotalUses, Tenancy tenancy) + public EC2OndemandSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + String labelString, + Mode mode, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + String publicDNS, + String privateDNS, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + Tenancy tenancy) throws FormException, IOException { - this(name, instanceId, templateDescription, remoteFS, numExecutors, labelString, mode, initScript, tmpDir, nodeProperties, remoteAdmin, DEFAULT_JAVA_PATH, jvmopts, stopOnTerminate, idleTerminationMinutes, publicDNS, privateDNS, tags, cloudName, launchTimeout, amiType, connectionStrategy, maxTotalUses, tenancy, DEFAULT_METADATA_ENDPOINT_ENABLED, DEFAULT_METADATA_TOKENS_REQUIRED, DEFAULT_METADATA_HOPS_LIMIT); + this( + name, + instanceId, + templateDescription, + remoteFS, + numExecutors, + labelString, + mode, + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + DEFAULT_JAVA_PATH, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + publicDNS, + privateDNS, + tags, + cloudName, + launchTimeout, + amiType, + connectionStrategy, + maxTotalUses, + tenancy, + DEFAULT_METADATA_ENDPOINT_ENABLED, + DEFAULT_METADATA_TOKENS_REQUIRED, + DEFAULT_METADATA_HOPS_LIMIT); } @Deprecated - public EC2OndemandSlave(String name, String instanceId, String templateDescription, String remoteFS, int numExecutors, String labelString, Mode mode, String initScript, String tmpDir, List> nodeProperties, String remoteAdmin, String javaPath, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, String publicDNS, String privateDNS, List tags, String cloudName, int launchTimeout, AMITypeData amiType, ConnectionStrategy connectionStrategy, int maxTotalUses, Tenancy tenancy, Boolean metadataEndpointEnabled, Boolean metadataTokensRequired, Integer metadataHopsLimit) + public EC2OndemandSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + String labelString, + Mode mode, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String javaPath, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + String publicDNS, + String privateDNS, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + Tenancy tenancy, + Boolean metadataEndpointEnabled, + Boolean metadataTokensRequired, + Integer metadataHopsLimit) throws FormException, IOException { - this(name, instanceId, templateDescription, remoteFS, numExecutors, labelString, mode, initScript, tmpDir, nodeProperties, remoteAdmin, DEFAULT_JAVA_PATH, jvmopts, stopOnTerminate, idleTerminationMinutes, publicDNS, privateDNS, tags, cloudName, launchTimeout, amiType, connectionStrategy, maxTotalUses, tenancy, metadataEndpointEnabled, metadataTokensRequired, metadataHopsLimit, DEFAULT_METADATA_SUPPORTED); + this( + name, + instanceId, + templateDescription, + remoteFS, + numExecutors, + labelString, + mode, + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + DEFAULT_JAVA_PATH, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + publicDNS, + privateDNS, + tags, + cloudName, + launchTimeout, + amiType, + connectionStrategy, + maxTotalUses, + tenancy, + metadataEndpointEnabled, + metadataTokensRequired, + metadataHopsLimit, + DEFAULT_METADATA_SUPPORTED); } @DataBoundConstructor - public EC2OndemandSlave(String name, String instanceId, String templateDescription, String remoteFS, int numExecutors, String labelString, Mode mode, String initScript, String tmpDir, List> nodeProperties, String remoteAdmin, String javaPath, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, String publicDNS, String privateDNS, List tags, String cloudName, int launchTimeout, AMITypeData amiType, ConnectionStrategy connectionStrategy, int maxTotalUses, Tenancy tenancy, Boolean metadataEndpointEnabled, Boolean metadataTokensRequired, Integer metadataHopsLimit, Boolean metadataSupported) + public EC2OndemandSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + String labelString, + Mode mode, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String javaPath, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + String publicDNS, + String privateDNS, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + Tenancy tenancy, + Boolean metadataEndpointEnabled, + Boolean metadataTokensRequired, + Integer metadataHopsLimit, + Boolean metadataSupported) throws FormException, IOException { - super(name, instanceId, templateDescription, remoteFS, numExecutors, mode, labelString, (amiType.isWindows() ? new EC2WindowsLauncher() : (amiType.isMac() ? new EC2MacLauncher(): - new EC2UnixLauncher())), new EC2RetentionStrategy(idleTerminationMinutes), initScript, tmpDir, nodeProperties, remoteAdmin, javaPath, jvmopts, stopOnTerminate, idleTerminationMinutes, tags, cloudName, launchTimeout, amiType, connectionStrategy, maxTotalUses, tenancy, metadataEndpointEnabled, metadataTokensRequired, metadataHopsLimit, metadataSupported); + super( + name, + instanceId, + templateDescription, + remoteFS, + numExecutors, + mode, + labelString, + (amiType.isWindows() + ? new EC2WindowsLauncher() + : (amiType.isMac() ? new EC2MacLauncher() : new EC2UnixLauncher())), + new EC2RetentionStrategy(idleTerminationMinutes), + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + javaPath, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + tags, + cloudName, + launchTimeout, + amiType, + connectionStrategy, + maxTotalUses, + tenancy, + metadataEndpointEnabled, + metadataTokensRequired, + metadataHopsLimit, + metadataSupported); this.publicDNS = publicDNS; this.privateDNS = privateDNS; } @@ -83,26 +405,51 @@ public EC2OndemandSlave(String name, String instanceId, String templateDescripti * Constructor for debugging. */ public EC2OndemandSlave(String instanceId) throws FormException, IOException { - this(instanceId, instanceId, "debug", "/tmp/hudson", 1, "debug", Mode.NORMAL, "", "/tmp", Collections.emptyList(), null, null, false, null, "Fake public", "Fake private", null, null, false, 0, new UnixData(null, null, null, null, null), ConnectionStrategy.PRIVATE_IP, -1); + this( + instanceId, + instanceId, + "debug", + "/tmp/hudson", + 1, + "debug", + Mode.NORMAL, + "", + "/tmp", + Collections.emptyList(), + null, + null, + false, + null, + "Fake public", + "Fake private", + null, + null, + false, + 0, + new UnixData(null, null, null, null, null), + ConnectionStrategy.PRIVATE_IP, + -1); } /** * Terminates the instance in EC2. */ + @Override public void terminate() { if (terminateScheduled.getCount() == 0) { - synchronized(terminateScheduled) { + synchronized (terminateScheduled) { if (terminateScheduled.getCount() == 0) { Computer.threadPoolForRemoting.submit(() -> { try { if (!isAlive(true)) { /* - * The node has been killed externally, so we've nothing to do here - */ + * The node has been killed externally, so we've nothing to do here + */ LOGGER.info("EC2 instance already terminated: " + getInstanceId()); } else { AmazonEC2 ec2 = getCloud().connect(); - TerminateInstancesRequest request = new TerminateInstancesRequest(Collections.singletonList(getInstanceId())); + TerminateInstancesRequest request = + new TerminateInstancesRequest(Collections.singletonList(getInstanceId())); ec2.terminateInstances(request); LOGGER.info("Terminated EC2 instance (terminated): " + getInstanceId()); } @@ -111,7 +458,7 @@ public void terminate() { } catch (AmazonClientException | IOException e) { LOGGER.log(Level.WARNING, "Failed to terminate EC2 instance: " + getInstanceId(), e); } finally { - synchronized(terminateScheduled) { + synchronized (terminateScheduled) { terminateScheduled.countDown(); } } @@ -133,8 +480,10 @@ public Node reconfigure(final StaplerRequest req, JSONObject form) throws FormEx try { Jenkins.get().removeNode(this); } catch (IOException ioe) { - LOGGER.log(Level.WARNING, "Attempt to reconfigure EC2 instance which has been externally terminated: " - + getInstanceId(), ioe); + LOGGER.log( + Level.WARNING, + "Attempt to reconfigure EC2 instance which has been externally terminated: " + getInstanceId(), + ioe); } return null; diff --git a/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java b/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java index bc5bba2ea..496414905 100644 --- a/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java +++ b/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java @@ -23,33 +23,30 @@ */ package hudson.plugins.ec2; +import static hudson.plugins.ec2.EC2Cloud.SSH_PRIVATE_KEY_FILEPATH; + +import com.amazonaws.AmazonClientException; +import com.amazonaws.services.ec2.AmazonEC2; +import com.amazonaws.services.ec2.model.KeyPairInfo; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.util.Secret; import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.security.UnrecoverableKeyException; import java.util.Base64; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.model.KeyPairInfo; - -import edu.umd.cs.findbugs.annotations.CheckForNull; -import hudson.util.Secret; -import jenkins.bouncycastle.api.PEMEncodable; -import javax.crypto.Cipher; -import java.nio.charset.Charset; import java.util.logging.Level; import java.util.logging.Logger; - +import javax.crypto.Cipher; +import jenkins.bouncycastle.api.PEMEncodable; import org.apache.commons.lang.StringUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; -import static hudson.plugins.ec2.EC2Cloud.SSH_PRIVATE_KEY_FILEPATH; - /** * RSA private key (the one that you generate with ec2-add-keypair.) * @@ -110,8 +107,9 @@ public boolean isPrivateKey() throws IOException { BufferedReader br = new BufferedReader(new StringReader(privateKey.getPlainText())); String line; while ((line = br.readLine()) != null) { - if (line.equals("-----BEGIN RSA PRIVATE KEY-----")) + if (line.equals("-----BEGIN RSA PRIVATE KEY-----")) { return true; + } } return false; } @@ -144,7 +142,9 @@ public com.amazonaws.services.ec2.model.KeyPair find(AmazonEC2 ec2) throws IOExc public String decryptWindowsPassword(String encodedPassword) throws AmazonClientException { try { Cipher cipher = Cipher.getInstance("RSA/NONE/PKCS1Padding"); - cipher.init(Cipher.DECRYPT_MODE, PEMEncodable.decode(privateKey.getPlainText()).toPrivateKey()); + cipher.init( + Cipher.DECRYPT_MODE, + PEMEncodable.decode(privateKey.getPlainText()).toPrivateKey()); byte[] cipherText = Base64.getDecoder().decode(StringUtils.deleteWhitespace(encodedPassword)); byte[] plainText = cipher.doFinal(cipherText); return new String(plainText, Charset.forName("ASCII")); @@ -179,8 +179,9 @@ public int hashCode() { @Override public boolean equals(Object that) { - if (that != null && this.getClass() == that.getClass()) + if (that != null && this.getClass() == that.getClass()) { return this.privateKey.equals(((EC2PrivateKey) that).privateKey); + } return false; } diff --git a/src/main/java/hudson/plugins/ec2/EC2Readiness.java b/src/main/java/hudson/plugins/ec2/EC2Readiness.java index 3f3f112e3..ef21dfc4f 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Readiness.java +++ b/src/main/java/hudson/plugins/ec2/EC2Readiness.java @@ -4,5 +4,6 @@ public interface EC2Readiness { public boolean isReady(); + public String getEc2ReadinessStatus() throws AmazonClientException; } diff --git a/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java b/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java index 356e7bb47..21b4df61d 100644 --- a/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java +++ b/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java @@ -24,25 +24,21 @@ package hudson.plugins.ec2; import com.amazonaws.AmazonClientException; - - import hudson.init.InitMilestone; import hudson.model.Descriptor; import hudson.model.Executor; import hudson.model.ExecutorListener; +import hudson.model.Label; import hudson.model.Queue; import hudson.plugins.ec2.util.MinimumInstanceChecker; -import hudson.model.Label; import hudson.slaves.RetentionStrategy; -import jenkins.model.Jenkins; - import java.time.Clock; import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; - +import jenkins.model.Jenkins; import org.kohsuke.stapler.DataBoundConstructor; /** @@ -87,7 +83,6 @@ public EC2RetentionStrategy(String idleTerminationMinutes) { } } - EC2RetentionStrategy(String idleTerminationMinutes, Clock clock, long nextCheckAfter) { this(idleTerminationMinutes); this.clock = clock; @@ -121,21 +116,23 @@ public long check(EC2Computer c) { private long internalCheck(EC2Computer computer) { /* - * If we've been told never to terminate, or node is null(deleted), no checks to perform - */ + * If we've been told never to terminate, or node is null(deleted), no checks to perform + */ if (idleTerminationMinutes == 0 || computer.getNode() == null) { return CHECK_INTERVAL_MINUTES; } /* - * If we have equal or less number of agents than the template's minimum instance count, don't perform check. - */ + * If we have equal or less number of agents than the template's minimum instance count, don't perform check. + */ SlaveTemplate slaveTemplate = computer.getSlaveTemplate(); if (slaveTemplate != null) { long numberOfCurrentInstancesForTemplate = MinimumInstanceChecker.countCurrentNumberOfAgents(slaveTemplate); - if (numberOfCurrentInstancesForTemplate > 0 && numberOfCurrentInstancesForTemplate <= slaveTemplate.getMinimumNumberOfInstances()) { - //Check if we're in an active time-range for keeping minimum number of instances - if (MinimumInstanceChecker.minimumInstancesActive(slaveTemplate.getMinimumNumberOfInstancesTimeRangeConfig())) { + if (numberOfCurrentInstancesForTemplate > 0 + && numberOfCurrentInstancesForTemplate <= slaveTemplate.getMinimumNumberOfInstances()) { + // Check if we're in an active time-range for keeping minimum number of instances + if (MinimumInstanceChecker.minimumInstancesActive( + slaveTemplate.getMinimumNumberOfInstancesTimeRangeConfig())) { return CHECK_INTERVAL_MINUTES; } } @@ -147,7 +144,8 @@ private long internalCheck(EC2Computer computer) { InstanceState state; try { - state = computer.getState(); //Get State before Uptime because getState will refresh the cached EC2 info + state = computer.getState(); // Get State before Uptime because getState will refresh the cached EC2 + // info uptime = computer.getUptime(); launchedAtMs = computer.getLaunchTime(); } catch (AmazonClientException | InterruptedException e) { @@ -157,25 +155,31 @@ private long internalCheck(EC2Computer computer) { return CHECK_INTERVAL_MINUTES; } - //Don't bother checking anything else if the instance is already in the desired state: + // Don't bother checking anything else if the instance is already in the desired state: // * Already Terminated // * We use stop-on-terminate and the instance is currently stopped or stopping if (InstanceState.TERMINATED.equals(state) - || (slaveTemplate != null && slaveTemplate.stopOnTerminate) && (InstanceState.STOPPED.equals(state) || InstanceState.STOPPING.equals(state))) { + || (slaveTemplate != null && slaveTemplate.stopOnTerminate) + && (InstanceState.STOPPED.equals(state) || InstanceState.STOPPING.equals(state))) { if (computer.isOnline()) { - LOGGER.info("External Stop of " + computer.getName() + " detected - disconnecting. instance status" + state.toString()); + LOGGER.info("External Stop of " + computer.getName() + " detected - disconnecting. instance status" + + state.toString()); computer.disconnect(null); } return CHECK_INTERVAL_MINUTES; } - //on rare occasions, AWS may return fault instance which shows running in AWS console but can not be connected. - //need terminate such fault instance. + // on rare occasions, AWS may return fault instance which shows running in AWS console but can not be + // connected. + // need terminate such fault instance. // An instance may also fail running user data scripts and // need to be cleaned up. - if (computer.isOffline()){ + if (computer.isOffline()) { if (computer.isConnecting()) { - LOGGER.log(Level.FINE, "Computer {0} connecting and still offline, will check if the launch timeout has expired", computer.getInstanceId()); + LOGGER.log( + Level.FINE, + "Computer {0} connecting and still offline, will check if the launch timeout has expired", + computer.getInstanceId()); EC2AbstractSlave node = computer.getNode(); if (Objects.isNull(node)) { @@ -185,29 +189,32 @@ private long internalCheck(EC2Computer computer) { if (launchTimeout > 0 && uptime > launchTimeout) { // Computer is offline and startup time has expired LOGGER.info("Startup timeout of " + computer.getName() + " after " - + uptime + - " milliseconds (timeout: " + launchTimeout + " milliseconds), instance status: " + state.toString()); + + uptime + " milliseconds (timeout: " + + launchTimeout + " milliseconds), instance status: " + state.toString()); node.launchTimeout(); } return CHECK_INTERVAL_MINUTES; } else { - LOGGER.log(Level.FINE, "Computer {0} offline but not connecting, will check if it should be terminated because of the idle time configured", computer.getInstanceId()); + LOGGER.log( + Level.FINE, + "Computer {0} offline but not connecting, will check if it should be terminated because of the idle time configured", + computer.getInstanceId()); } } - final long idleMilliseconds = this.clock.millis() - Math.max(computer.getIdleStartMilliseconds(), launchedAtMs); - + final long idleMilliseconds = + this.clock.millis() - Math.max(computer.getIdleStartMilliseconds(), launchedAtMs); if (idleTerminationMinutes > 0) { // TODO: really think about the right strategy here, see // JENKINS-23792 - if (idleMilliseconds > TimeUnit.MINUTES.toMillis(idleTerminationMinutes) && - !itemsInQueueForThisSlave(computer)){ + if (idleMilliseconds > TimeUnit.MINUTES.toMillis(idleTerminationMinutes) + && !itemsInQueueForThisSlave(computer)) { LOGGER.info("Idle timeout of " + computer.getName() + " after " - + TimeUnit.MILLISECONDS.toMinutes(idleMilliseconds) + - " idle minutes, instance status"+state.toString()); + + TimeUnit.MILLISECONDS.toMinutes(idleMilliseconds) + " idle minutes, instance status" + + state.toString()); EC2AbstractSlave slaveNode = computer.getNode(); if (slaveNode != null) { slaveNode.idleTimeout(); @@ -215,14 +222,17 @@ private long internalCheck(EC2Computer computer) { } } else { final int oneHourSeconds = (int) TimeUnit.SECONDS.convert(1, TimeUnit.HOURS); - // AWS bills by the hour for EC2 Instances, so calculate the remaining seconds left in the "billing hour" - // Note: Since October 2017, this isn't true for Linux instances, but the logic hasn't yet been updated for this + // AWS bills by the hour for EC2 Instances, so calculate the remaining seconds left in the "billing + // hour" + // Note: Since October 2017, this isn't true for Linux instances, but the logic hasn't yet been updated + // for this final int freeSecondsLeft = oneHourSeconds - (int) (TimeUnit.SECONDS.convert(uptime, TimeUnit.MILLISECONDS) % oneHourSeconds); // if we have less "free" (aka already paid for) time left than // our idle time, stop/terminate the instance // See JENKINS-23821 - if (freeSecondsLeft <= TimeUnit.MINUTES.toSeconds(Math.abs(idleTerminationMinutes)) && !itemsInQueueForThisSlave(computer)) { + if (freeSecondsLeft <= TimeUnit.MINUTES.toSeconds(Math.abs(idleTerminationMinutes)) + && !itemsInQueueForThisSlave(computer)) { LOGGER.info("Idle timeout of " + computer.getName() + " after " + TimeUnit.MILLISECONDS.toMinutes(idleMilliseconds) + " idle minutes, with " + TimeUnit.SECONDS.toMinutes(freeSecondsLeft) @@ -250,7 +260,9 @@ private boolean itemsInQueueForThisSlave(EC2Computer c) { * doesn't have a node it will return null. In this case we want to * return false because there's no slave to prevent a timeout of. */ - if (selfNode == null) return false; + if (selfNode == null) { + return false; + } final Label selfLabel = selfNode.getSelfLabel(); Queue.Item[] items = Jenkins.getInstance().getQueue().getItems(); for (int i = 0; i < items.length; i++) { @@ -274,7 +286,7 @@ private boolean itemsInQueueForThisSlave(EC2Computer c) { */ @Override public void start(EC2Computer c) { - //Jenkins is in the process of starting up + // Jenkins is in the process of starting up if (Jenkins.get().getInitLevel() != InitMilestone.COMPLETED) { InstanceState state = null; try { @@ -286,10 +298,9 @@ public void start(EC2Computer c) { LOGGER.info("Ignoring start request for " + c.getName() + " during Jenkins startup due to EC2 instance state of " + state); return; - } + } } - LOGGER.info("Start requested for " + c.getName()); c.connect(false); } @@ -310,6 +321,7 @@ protected Object readResolve() { return this; } + @Override public void taskAccepted(Executor executor, Queue.Task task) { EC2Computer computer = (EC2Computer) executor.getOwner(); if (computer != null) { @@ -317,7 +329,8 @@ public void taskAccepted(Executor executor, Queue.Task task) { if (slaveNode != null) { int maxTotalUses = slaveNode.maxTotalUses; if (maxTotalUses <= -1) { - LOGGER.fine("maxTotalUses set to unlimited (" + slaveNode.maxTotalUses + ") for agent " + slaveNode.instanceId); + LOGGER.fine("maxTotalUses set to unlimited (" + slaveNode.maxTotalUses + ") for agent " + + slaveNode.instanceId); return; } else if (maxTotalUses <= 1) { LOGGER.info("maxTotalUses drained - suspending agent " + slaveNode.instanceId); @@ -330,10 +343,12 @@ public void taskAccepted(Executor executor, Queue.Task task) { } } + @Override public void taskCompleted(Executor executor, Queue.Task task, long durationMS) { postJobAction(executor); } + @Override public void taskCompletedWithProblems(Executor executor, Queue.Task task, long durationMS, Throwable problems) { postJobAction(executor); } @@ -343,13 +358,16 @@ private void postJobAction(Executor executor) { if (computer != null) { EC2AbstractSlave slaveNode = computer.getNode(); if (slaveNode != null) { - // At this point, if agent is in suspended state and has 1 last executer running, it is safe to terminate. + // At this point, if agent is in suspended state and has 1 last executer running, it is safe to + // terminate. if (computer.countBusy() <= 1 && !computer.isAcceptingTasks()) { - LOGGER.info("Agent " + slaveNode.instanceId + " is terminated due to maxTotalUses (" + slaveNode.maxTotalUses + ")"); + LOGGER.info("Agent " + slaveNode.instanceId + " is terminated due to maxTotalUses (" + + slaveNode.maxTotalUses + ")"); slaveNode.terminate(); } else { if (slaveNode.maxTotalUses == 1) { - LOGGER.info("Agent " + slaveNode.instanceId + " is still in use by more than one (" + computer.countBusy() + ") executers."); + LOGGER.info("Agent " + slaveNode.instanceId + " is still in use by more than one (" + + computer.countBusy() + ") executers."); } } } diff --git a/src/main/java/hudson/plugins/ec2/EC2SlaveMonitor.java b/src/main/java/hudson/plugins/ec2/EC2SlaveMonitor.java index daa622a39..5209c89b6 100644 --- a/src/main/java/hudson/plugins/ec2/EC2SlaveMonitor.java +++ b/src/main/java/hudson/plugins/ec2/EC2SlaveMonitor.java @@ -1,23 +1,20 @@ package hudson.plugins.ec2; +import static hudson.plugins.ec2.EC2Cloud.EC2_REQUEST_EXPIRED_ERROR_CODE; + +import com.amazonaws.AmazonClientException; +import com.amazonaws.services.ec2.model.AmazonEC2Exception; import hudson.Extension; import hudson.model.AsyncPeriodicWork; -import hudson.model.TaskListener; import hudson.model.Node; - +import hudson.model.TaskListener; +import hudson.plugins.ec2.util.MinimumInstanceChecker; import java.io.IOException; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; - -import hudson.plugins.ec2.util.MinimumInstanceChecker; import jenkins.model.Jenkins; -import com.amazonaws.AmazonClientException; -import com.amazonaws.services.ec2.model.AmazonEC2Exception; - -import static hudson.plugins.ec2.EC2Cloud.EC2_REQUEST_EXPIRED_ERROR_CODE; - /** * @author Bruno Meneguello */ @@ -54,9 +51,10 @@ private void removeDeadNodes() { ec2Slave.terminate(); } } catch (AmazonClientException e) { - if (e instanceof AmazonEC2Exception && - EC2_REQUEST_EXPIRED_ERROR_CODE.equals(((AmazonEC2Exception) e).getErrorCode())) { - LOGGER.info("EC2 request expired, skipping consideration of " + ec2Slave.getInstanceId() + " due to unknown state."); + if (e instanceof AmazonEC2Exception + && EC2_REQUEST_EXPIRED_ERROR_CODE.equals(((AmazonEC2Exception) e).getErrorCode())) { + LOGGER.info("EC2 request expired, skipping consideration of " + ec2Slave.getInstanceId() + + " due to unknown state."); } else { LOGGER.info("EC2 instance is dead and failed to terminate: " + ec2Slave.getInstanceId()); removeNode(ec2Slave); @@ -73,5 +71,4 @@ private void removeNode(EC2AbstractSlave ec2Slave) { LOGGER.log(Level.WARNING, "Failed to remove node: " + ec2Slave.getInstanceId()); } } - } diff --git a/src/main/java/hudson/plugins/ec2/EC2SpotSlave.java b/src/main/java/hudson/plugins/ec2/EC2SpotSlave.java index 04c4b861b..2f1372b1f 100644 --- a/src/main/java/hudson/plugins/ec2/EC2SpotSlave.java +++ b/src/main/java/hudson/plugins/ec2/EC2SpotSlave.java @@ -1,16 +1,5 @@ package hudson.plugins.ec2; -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.logging.Level; -import java.util.logging.Logger; - -import jenkins.model.Jenkins; -import org.apache.commons.lang.StringUtils; -import org.kohsuke.stapler.DataBoundConstructor; - import com.amazonaws.AmazonClientException; import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.model.CancelSpotInstanceRequestsRequest; @@ -19,15 +8,21 @@ import com.amazonaws.services.ec2.model.SpotInstanceRequest; import com.amazonaws.services.ec2.model.SpotInstanceState; import com.amazonaws.services.ec2.model.TerminateInstancesRequest; - +import edu.umd.cs.findbugs.annotations.CheckForNull; import hudson.Extension; import hudson.model.Computer; import hudson.model.Descriptor.FormException; import hudson.plugins.ec2.ssh.EC2UnixLauncher; import hudson.plugins.ec2.win.EC2WindowsLauncher; import hudson.slaves.NodeProperty; - -import edu.umd.cs.findbugs.annotations.CheckForNull; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.model.Jenkins; +import org.apache.commons.lang.StringUtils; +import org.kohsuke.stapler.DataBoundConstructor; public class EC2SpotSlave extends EC2AbstractSlave implements EC2Readiness { private static final Logger LOGGER = Logger.getLogger(EC2SpotSlave.class.getName()); @@ -35,23 +30,140 @@ public class EC2SpotSlave extends EC2AbstractSlave implements EC2Readiness { private final String spotInstanceRequestId; @Deprecated - public EC2SpotSlave(String name, String spotInstanceRequestId, String templateDescription, String remoteFS, int numExecutors, Mode mode, String initScript, String tmpDir, String labelString, String remoteAdmin, String jvmopts, String idleTerminationMinutes, List tags, String cloudName, int launchTimeout, AMITypeData amiType) + public EC2SpotSlave( + String name, + String spotInstanceRequestId, + String templateDescription, + String remoteFS, + int numExecutors, + Mode mode, + String initScript, + String tmpDir, + String labelString, + String remoteAdmin, + String jvmopts, + String idleTerminationMinutes, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType) throws FormException, IOException { - this(name, spotInstanceRequestId, templateDescription, remoteFS, numExecutors, mode, initScript, tmpDir, labelString, remoteAdmin, jvmopts, idleTerminationMinutes, tags, cloudName, false, launchTimeout, amiType); + this( + name, + spotInstanceRequestId, + templateDescription, + remoteFS, + numExecutors, + mode, + initScript, + tmpDir, + labelString, + remoteAdmin, + jvmopts, + idleTerminationMinutes, + tags, + cloudName, + false, + launchTimeout, + amiType); } @Deprecated - public EC2SpotSlave(String name, String spotInstanceRequestId, String templateDescription, String remoteFS, int numExecutors, Mode mode, String initScript, String tmpDir, String labelString, String remoteAdmin, String jvmopts, String idleTerminationMinutes, List tags, String cloudName, boolean usePrivateDnsName, int launchTimeout, AMITypeData amiType) + public EC2SpotSlave( + String name, + String spotInstanceRequestId, + String templateDescription, + String remoteFS, + int numExecutors, + Mode mode, + String initScript, + String tmpDir, + String labelString, + String remoteAdmin, + String jvmopts, + String idleTerminationMinutes, + List tags, + String cloudName, + boolean usePrivateDnsName, + int launchTimeout, + AMITypeData amiType) throws FormException, IOException { - this(templateDescription + " (" + name + ")", spotInstanceRequestId, templateDescription, remoteFS, numExecutors, mode, initScript, tmpDir, labelString, Collections.emptyList(), remoteAdmin, DEFAULT_JAVA_PATH, jvmopts, idleTerminationMinutes, tags, cloudName, launchTimeout, amiType, ConnectionStrategy.backwardsCompatible(usePrivateDnsName, false, false), -1); + this( + templateDescription + " (" + name + ")", + spotInstanceRequestId, + templateDescription, + remoteFS, + numExecutors, + mode, + initScript, + tmpDir, + labelString, + Collections.emptyList(), + remoteAdmin, + DEFAULT_JAVA_PATH, + jvmopts, + idleTerminationMinutes, + tags, + cloudName, + launchTimeout, + amiType, + ConnectionStrategy.backwardsCompatible(usePrivateDnsName, false, false), + -1); } @DataBoundConstructor - public EC2SpotSlave(String name, String spotInstanceRequestId, String templateDescription, String remoteFS, int numExecutors, Mode mode, String initScript, String tmpDir, String labelString, List> nodeProperties, String remoteAdmin, String javaPath, String jvmopts, String idleTerminationMinutes, List tags, String cloudName, int launchTimeout, AMITypeData amiType, ConnectionStrategy connectionStrategy, int maxTotalUses) + public EC2SpotSlave( + String name, + String spotInstanceRequestId, + String templateDescription, + String remoteFS, + int numExecutors, + Mode mode, + String initScript, + String tmpDir, + String labelString, + List> nodeProperties, + String remoteAdmin, + String javaPath, + String jvmopts, + String idleTerminationMinutes, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses) throws FormException, IOException { - super(name, "", templateDescription, remoteFS, numExecutors, mode, labelString, amiType.isWindows() ? new EC2WindowsLauncher() : - new EC2UnixLauncher(), new EC2RetentionStrategy(idleTerminationMinutes), initScript, tmpDir, nodeProperties, remoteAdmin, javaPath, jvmopts, false, idleTerminationMinutes, tags, cloudName, launchTimeout, amiType, connectionStrategy, maxTotalUses,null, DEFAULT_METADATA_ENDPOINT_ENABLED, DEFAULT_METADATA_TOKENS_REQUIRED, DEFAULT_METADATA_HOPS_LIMIT, DEFAULT_METADATA_SUPPORTED); + super( + name, + "", + templateDescription, + remoteFS, + numExecutors, + mode, + labelString, + amiType.isWindows() ? new EC2WindowsLauncher() : new EC2UnixLauncher(), + new EC2RetentionStrategy(idleTerminationMinutes), + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + javaPath, + jvmopts, + false, + idleTerminationMinutes, + tags, + cloudName, + launchTimeout, + amiType, + connectionStrategy, + maxTotalUses, + null, + DEFAULT_METADATA_ENDPOINT_ENABLED, + DEFAULT_METADATA_TOKENS_REQUIRED, + DEFAULT_METADATA_HOPS_LIMIT, + DEFAULT_METADATA_SUPPORTED); this.name = name; this.spotInstanceRequestId = spotInstanceRequestId; @@ -68,7 +180,7 @@ protected boolean isAlive(boolean force) { @Override public void terminate() { if (terminateScheduled.getCount() == 0) { - synchronized(terminateScheduled) { + synchronized (terminateScheduled) { if (terminateScheduled.getCount() == 0) { Computer.threadPoolForRemoting.submit(() -> { try { @@ -77,7 +189,8 @@ public void terminate() { String instanceId = getInstanceId(); List requestIds = Collections.singletonList(spotInstanceRequestId); - CancelSpotInstanceRequestsRequest cancelRequest = new CancelSpotInstanceRequestsRequest(requestIds); + CancelSpotInstanceRequestsRequest cancelRequest = + new CancelSpotInstanceRequestsRequest(requestIds); try { ec2.cancelSpotInstanceRequests(cancelRequest); LOGGER.info("Cancelled Spot request: " + spotInstanceRequestId); @@ -90,22 +203,26 @@ public void terminate() { if (instanceId != null && !instanceId.equals("")) { if (!super.isAlive(true)) { /* - * The node has been killed externally, so we've nothing to do here - */ + * The node has been killed externally, so we've nothing to do here + */ LOGGER.info("EC2 instance already terminated: " + instanceId); } else { - TerminateInstancesRequest request = new TerminateInstancesRequest(Collections.singletonList(instanceId)); + TerminateInstancesRequest request = + new TerminateInstancesRequest(Collections.singletonList(instanceId)); try { ec2.terminateInstances(request); LOGGER.info("Terminated EC2 instance (terminated): " + instanceId); } catch (AmazonClientException e) { // Spot request is no longer valid - LOGGER.log(Level.WARNING, "Failed to terminate the Spot instance: " + instanceId, e); + LOGGER.log( + Level.WARNING, + "Failed to terminate the Spot instance: " + instanceId, + e); } } } } catch (Exception e) { - LOGGER.log(Level.WARNING,"Failed to remove agent: ", e); + LOGGER.log(Level.WARNING, "Failed to remove agent: ", e); } finally { // Remove the instance even if deletion failed, otherwise it will hang around forever in // the nodes page. One way for this to occur is that an instance was terminated @@ -116,7 +233,7 @@ public void terminate() { } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to remove agent: " + name, e); } - synchronized(terminateScheduled) { + synchronized (terminateScheduled) { terminateScheduled.countDown(); } } @@ -140,7 +257,8 @@ SpotInstanceRequest getSpotRequest() { return null; } - DescribeSpotInstanceRequestsRequest dsirRequest = new DescribeSpotInstanceRequestsRequest().withSpotInstanceRequestIds(this.spotInstanceRequestId); + DescribeSpotInstanceRequestsRequest dsirRequest = + new DescribeSpotInstanceRequestsRequest().withSpotInstanceRequestIds(this.spotInstanceRequestId); try { DescribeSpotInstanceRequestsResult dsirResult = ec2.describeSpotInstanceRequests(dsirRequest); List siRequests = dsirResult.getSpotInstanceRequests(); @@ -148,7 +266,9 @@ SpotInstanceRequest getSpotRequest() { return siRequests.get(0); } catch (AmazonClientException e) { // Spot request is no longer valid - LOGGER.log(Level.WARNING, "Failed to fetch spot instance request for requestId: " + this.spotInstanceRequestId); + LOGGER.log( + Level.WARNING, + "Failed to fetch spot instance request for requestId: " + this.spotInstanceRequestId); } return null; @@ -177,8 +297,9 @@ public String getSpotInstanceRequestId() { public String getInstanceId() { if (StringUtils.isEmpty(instanceId)) { SpotInstanceRequest sr = getSpotRequest(); - if (sr != null) + if (sr != null) { instanceId = sr.getInstanceId(); + } } return instanceId; } @@ -204,7 +325,8 @@ public String getEc2Type() { SpotInstanceRequest spotRequest = getSpotRequest(); if (spotRequest != null) { String spotMaxBidPrice = spotRequest.getSpotPrice(); - return Messages.EC2SpotSlave_Spot1() + spotMaxBidPrice.substring(0, spotMaxBidPrice.length() - 3) + return Messages.EC2SpotSlave_Spot1() + + spotMaxBidPrice.substring(0, spotMaxBidPrice.length() - 3) + Messages.EC2SpotSlave_Spot2(); } return null; diff --git a/src/main/java/hudson/plugins/ec2/EC2Step.java b/src/main/java/hudson/plugins/ec2/EC2Step.java index bd8967760..9407462cf 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Step.java +++ b/src/main/java/hudson/plugins/ec2/EC2Step.java @@ -29,14 +29,13 @@ import hudson.model.TaskListener; import hudson.slaves.Cloud; import hudson.util.ListBoxModel; +import java.util.*; import jenkins.model.Jenkins; import org.jenkinsci.plugins.workflow.steps.*; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.verb.POST; -import java.util.*; - /** * Returns the instance provisioned. * @@ -60,6 +59,7 @@ public EC2Step(String cloud, String template) { this.cloud = cloud; this.template = template; } + public String getCloud() { return cloud; } @@ -70,7 +70,7 @@ public String getTemplate() { @Override public StepExecution start(StepContext context) throws Exception { - return new EC2Step.Execution( this, context); + return new EC2Step.Execution(this, context); } @Extension @@ -91,7 +91,8 @@ public ListBoxModel doFillCloudItems() { Jenkins.get().checkPermission(Jenkins.SYSTEM_READ); ListBoxModel r = new ListBoxModel(); r.add("", ""); - Jenkins.get().clouds + Jenkins.get() + .clouds .getAll(AmazonEC2Cloud.class) .forEach(c -> r.add(c.getDisplayName(), c.getDisplayName())); return r; @@ -106,26 +107,26 @@ public ListBoxModel doFillTemplateItems(@QueryParameter String cloudName) { AmazonEC2Cloud ec2Cloud = (AmazonEC2Cloud) cloud; for (SlaveTemplate template : ec2Cloud.getTemplates()) { for (String labelList : template.labels.split(" ")) { - r.add(labelList + " (AMI: " + template.getAmi() + ", REGION: " + ec2Cloud.getRegion() + ", TYPE: " + template.type.name() + ")", labelList); + r.add( + labelList + " (AMI: " + template.getAmi() + ", REGION: " + ec2Cloud.getRegion() + + ", TYPE: " + template.type.name() + ")", + labelList); } } } return r; } - @Override public Set> getRequiredContext() { return Collections.singleton(TaskListener.class); } - } public static class Execution extends SynchronousNonBlockingStepExecution { private final String cloud; private final String template; - Execution(EC2Step step, StepContext context) { super(context); this.cloud = step.cloud; @@ -145,16 +146,19 @@ protected Instance run() throws Exception { List instances = t.provision(1, opt); if (instances == null) { - throw new IllegalArgumentException("Error in AWS Cloud. Please review AWS template defined in Jenkins configuration."); + throw new IllegalArgumentException( + "Error in AWS Cloud. Please review AWS template defined in Jenkins configuration."); } EC2AbstractSlave slave = instances.get(0); return CloudHelper.getInstanceWithRetry(slave.getInstanceId(), (AmazonEC2Cloud) cl); } else { - throw new IllegalArgumentException("Error in AWS Cloud. Please review AWS template defined in Jenkins configuration."); + throw new IllegalArgumentException( + "Error in AWS Cloud. Please review AWS template defined in Jenkins configuration."); } } else { - throw new IllegalArgumentException("Error in AWS Cloud. Please review EC2 settings in Jenkins configuration."); + throw new IllegalArgumentException( + "Error in AWS Cloud. Please review EC2 settings in Jenkins configuration."); } } @@ -172,5 +176,4 @@ public Cloud getByDisplayName(Jenkins.CloudList clouds, String name) { return c; } } - } diff --git a/src/main/java/hudson/plugins/ec2/EC2Tag.java b/src/main/java/hudson/plugins/ec2/EC2Tag.java index ccdaef430..cfab3bdaa 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Tag.java +++ b/src/main/java/hudson/plugins/ec2/EC2Tag.java @@ -23,17 +23,14 @@ */ package hudson.plugins.ec2; -import hudson.model.Descriptor; -import hudson.model.AbstractDescribableImpl; +import com.amazonaws.services.ec2.model.Tag; import hudson.Extension; -import org.kohsuke.stapler.DataBoundConstructor; - -import java.util.List; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; import java.util.LinkedList; +import java.util.List; import java.util.Objects; - - -import com.amazonaws.services.ec2.model.Tag; +import org.kohsuke.stapler.DataBoundConstructor; public class EC2Tag extends AbstractDescribableImpl { private final String name; @@ -43,6 +40,7 @@ public class EC2Tag extends AbstractDescribableImpl { * Tag name for the specific jenkins agent type tag, used to identify the EC2 instances provisioned by this plugin. */ public static final String TAG_NAME_JENKINS_SLAVE_TYPE = "jenkins_slave_type"; + public static final String TAG_NAME_JENKINS_SERVER_URL = "jenkins_server_url"; @DataBoundConstructor @@ -72,16 +70,20 @@ public String toString() { @Override public boolean equals(Object o) { - if (o == null) + if (o == null) { return false; - if (this.getClass() != o.getClass()) + } + if (this.getClass() != o.getClass()) { return false; + } EC2Tag other = (EC2Tag) o; - if ((name == null && other.name != null) || (name != null && !name.equals(other.name))) + if ((name == null && other.name != null) || (name != null && !name.equals(other.name))) { return false; - if ((value == null && other.value != null) || (value != null && !value.equals(other.value))) + } + if ((value == null && other.value != null) || (value != null && !value.equals(other.value))) { return false; + } return true; } diff --git a/src/main/java/hudson/plugins/ec2/EbsEncryptRootVolume.java b/src/main/java/hudson/plugins/ec2/EbsEncryptRootVolume.java index 47ca5d15a..79d169b49 100644 --- a/src/main/java/hudson/plugins/ec2/EbsEncryptRootVolume.java +++ b/src/main/java/hudson/plugins/ec2/EbsEncryptRootVolume.java @@ -23,5 +23,4 @@ public String getDisplayText() { public Boolean getValue() { return value; } - } diff --git a/src/main/java/hudson/plugins/ec2/Eucalyptus.java b/src/main/java/hudson/plugins/ec2/Eucalyptus.java index 68c6fa612..788f4e5f0 100644 --- a/src/main/java/hudson/plugins/ec2/Eucalyptus.java +++ b/src/main/java/hudson/plugins/ec2/Eucalyptus.java @@ -26,13 +26,10 @@ import hudson.Extension; import hudson.model.ItemGroup; import hudson.util.FormValidation; - import java.io.IOException; import java.net.URL; import java.util.List; - import javax.servlet.ServletException; - import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; @@ -48,22 +45,83 @@ public class Eucalyptus extends EC2Cloud { private final URL s3endpoint; @DataBoundConstructor - public Eucalyptus(String name, URL ec2EndpointUrl, URL s3EndpointUrl, boolean useInstanceProfileForCredentials, String credentialsId, String privateKey, String sshKeysCredentialsId, String instanceCapStr, List templates, String roleArn, String roleSessionName) { - super(name, useInstanceProfileForCredentials, credentialsId, privateKey, sshKeysCredentialsId, instanceCapStr, templates, roleArn, roleSessionName); + public Eucalyptus( + String name, + URL ec2EndpointUrl, + URL s3EndpointUrl, + boolean useInstanceProfileForCredentials, + String credentialsId, + String privateKey, + String sshKeysCredentialsId, + String instanceCapStr, + List templates, + String roleArn, + String roleSessionName) { + super( + name, + useInstanceProfileForCredentials, + credentialsId, + privateKey, + sshKeysCredentialsId, + instanceCapStr, + templates, + roleArn, + roleSessionName); this.ec2endpoint = ec2EndpointUrl; this.s3endpoint = s3EndpointUrl; } @Deprecated - public Eucalyptus(URL ec2EndpointUrl, URL s3EndpointUrl, boolean useInstanceProfileForCredentials, String credentialsId, String privateKey, String sshKeysCredentialsId, String instanceCapStr, List templates, String roleArn, String roleSessionName) + public Eucalyptus( + URL ec2EndpointUrl, + URL s3EndpointUrl, + boolean useInstanceProfileForCredentials, + String credentialsId, + String privateKey, + String sshKeysCredentialsId, + String instanceCapStr, + List templates, + String roleArn, + String roleSessionName) throws IOException { - this("eucalyptus", ec2EndpointUrl, s3EndpointUrl, useInstanceProfileForCredentials, credentialsId, privateKey, sshKeysCredentialsId, instanceCapStr, templates, roleArn, roleSessionName); + this( + "eucalyptus", + ec2EndpointUrl, + s3EndpointUrl, + useInstanceProfileForCredentials, + credentialsId, + privateKey, + sshKeysCredentialsId, + instanceCapStr, + templates, + roleArn, + roleSessionName); } @Deprecated - public Eucalyptus(URL ec2EndpointUrl, URL s3EndpointUrl, boolean useInstanceProfileForCredentials, String credentialsId, String privateKey, String instanceCapStr, List templates, String roleArn, String roleSessionName) + public Eucalyptus( + URL ec2EndpointUrl, + URL s3EndpointUrl, + boolean useInstanceProfileForCredentials, + String credentialsId, + String privateKey, + String instanceCapStr, + List templates, + String roleArn, + String roleSessionName) throws IOException { - this("eucalyptus", ec2EndpointUrl, s3EndpointUrl, useInstanceProfileForCredentials, credentialsId, privateKey, null, instanceCapStr, templates, roleArn, roleSessionName); + this( + "eucalyptus", + ec2EndpointUrl, + s3EndpointUrl, + useInstanceProfileForCredentials, + credentialsId, + privateKey, + null, + instanceCapStr, + templates, + roleArn, + roleSessionName); } @Override @@ -85,9 +143,25 @@ public String getDisplayName() { @Override @RequirePOST - public FormValidation doTestConnection(@AncestorInPath ItemGroup context, @QueryParameter URL ec2endpoint, @QueryParameter boolean useInstanceProfileForCredentials, @QueryParameter String credentialsId, @QueryParameter String sshKeysCredentialsId, @QueryParameter String roleArn, @QueryParameter String roleSessionName, @QueryParameter String region) + public FormValidation doTestConnection( + @AncestorInPath ItemGroup context, + @QueryParameter URL ec2endpoint, + @QueryParameter boolean useInstanceProfileForCredentials, + @QueryParameter String credentialsId, + @QueryParameter String sshKeysCredentialsId, + @QueryParameter String roleArn, + @QueryParameter String roleSessionName, + @QueryParameter String region) throws IOException, ServletException { - return super.doTestConnection(context, ec2endpoint, useInstanceProfileForCredentials, credentialsId, sshKeysCredentialsId, roleArn, roleSessionName, region); + return super.doTestConnection( + context, + ec2endpoint, + useInstanceProfileForCredentials, + credentialsId, + sshKeysCredentialsId, + roleArn, + roleSessionName, + region); } } } diff --git a/src/main/java/hudson/plugins/ec2/HostKeyVerificationStrategyEnum.java b/src/main/java/hudson/plugins/ec2/HostKeyVerificationStrategyEnum.java index 69b8af71a..df4df25dd 100644 --- a/src/main/java/hudson/plugins/ec2/HostKeyVerificationStrategyEnum.java +++ b/src/main/java/hudson/plugins/ec2/HostKeyVerificationStrategyEnum.java @@ -35,12 +35,15 @@ public enum HostKeyVerificationStrategyEnum { CHECK_NEW_SOFT("check-new-soft", "accept-new", new CheckNewSoftStrategy()), ACCEPT_NEW("accept-new", "accept-new", new AcceptNewStrategy()), OFF("off", "no", new NonVerifyingKeyVerificationStrategy()); - + private final String displayText; private final SshHostKeyVerificationStrategy strategy; private final String sshCommandEquivalentFlag; - - HostKeyVerificationStrategyEnum(@NonNull String displayText, @NonNull String sshCommandEquivalentFlag, @NonNull SshHostKeyVerificationStrategy strategy) { + + HostKeyVerificationStrategyEnum( + @NonNull String displayText, + @NonNull String sshCommandEquivalentFlag, + @NonNull SshHostKeyVerificationStrategy strategy) { this.displayText = displayText; this.sshCommandEquivalentFlag = sshCommandEquivalentFlag; this.strategy = strategy; @@ -50,7 +53,7 @@ public enum HostKeyVerificationStrategyEnum { public SshHostKeyVerificationStrategy getStrategy() { return strategy; } - + public boolean equalsDisplayText(String other) { return this.displayText.equals(other); } diff --git a/src/main/java/hudson/plugins/ec2/InstanceState.java b/src/main/java/hudson/plugins/ec2/InstanceState.java index 4384bdca2..afaf150e4 100644 --- a/src/main/java/hudson/plugins/ec2/InstanceState.java +++ b/src/main/java/hudson/plugins/ec2/InstanceState.java @@ -29,7 +29,12 @@ * @author Kohsuke Kawaguchi */ public enum InstanceState { - PENDING, RUNNING, SHUTTING_DOWN, TERMINATED, STOPPING, STOPPED; + PENDING, + RUNNING, + SHUTTING_DOWN, + TERMINATED, + STOPPING, + STOPPED; public String getCode() { return name().toLowerCase().replace('_', '-'); diff --git a/src/main/java/hudson/plugins/ec2/InstanceTypeConverter.java b/src/main/java/hudson/plugins/ec2/InstanceTypeConverter.java index c659aa263..12b13b172 100644 --- a/src/main/java/hudson/plugins/ec2/InstanceTypeConverter.java +++ b/src/main/java/hudson/plugins/ec2/InstanceTypeConverter.java @@ -23,15 +23,14 @@ */ package hudson.plugins.ec2; -import java.util.HashMap; -import java.util.Map; - import com.amazonaws.services.ec2.model.InstanceType; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; +import java.util.HashMap; +import java.util.Map; /* * Note this is used only to handle the metadata for older versions of the ec2-plugin. The current @@ -55,15 +54,18 @@ public class InstanceTypeConverter implements Converter { TYPICAL_INSTANCE_TYPES.put("XLARGE_CLUSTER_COMPUTE", InstanceType.Cc14xlarge); } + @Override public boolean canConvert(Class type) { return InstanceType.class == type; } + @Override public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { InstanceType instanceType = (InstanceType) source; writer.setValue(instanceType.name()); } + @Override public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { InstanceType instanceType = null; @@ -77,5 +79,4 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co return instanceType; } - } diff --git a/src/main/java/hudson/plugins/ec2/MacData.java b/src/main/java/hudson/plugins/ec2/MacData.java index 7fc77a5d9..e8c5bdefe 100644 --- a/src/main/java/hudson/plugins/ec2/MacData.java +++ b/src/main/java/hudson/plugins/ec2/MacData.java @@ -14,7 +14,12 @@ public class MacData extends AMITypeData { private final String bootDelay; @DataBoundConstructor - public MacData(String rootCommandPrefix, String slaveCommandPrefix, String slaveCommandSuffix, String sshPort, String bootDelay) { + public MacData( + String rootCommandPrefix, + String slaveCommandPrefix, + String slaveCommandSuffix, + String sshPort, + String bootDelay) { this.rootCommandPrefix = rootCommandPrefix; this.slaveCommandPrefix = slaveCommandPrefix; this.slaveCommandSuffix = slaveCommandSuffix; @@ -44,6 +49,7 @@ public boolean isMac() { return true; } + @Override public String getBootDelay() { return bootDelay; } @@ -85,33 +91,44 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) + if (this == obj) { return true; - if (obj == null) + } + if (obj == null) { return false; - if (this.getClass() != obj.getClass()) + } + if (this.getClass() != obj.getClass()) { return false; + } final MacData other = (MacData) obj; if (StringUtils.isEmpty(rootCommandPrefix)) { - if (!StringUtils.isEmpty(other.rootCommandPrefix)) + if (!StringUtils.isEmpty(other.rootCommandPrefix)) { return false; - } else if (!rootCommandPrefix.equals(other.rootCommandPrefix)) + } + } else if (!rootCommandPrefix.equals(other.rootCommandPrefix)) { return false; + } if (StringUtils.isEmpty(slaveCommandPrefix)) { - if (!StringUtils.isEmpty(other.slaveCommandPrefix)) + if (!StringUtils.isEmpty(other.slaveCommandPrefix)) { return false; - } else if (!slaveCommandPrefix.equals(other.slaveCommandPrefix)) + } + } else if (!slaveCommandPrefix.equals(other.slaveCommandPrefix)) { return false; + } if (StringUtils.isEmpty(slaveCommandSuffix)) { - if (!StringUtils.isEmpty(other.slaveCommandSuffix)) + if (!StringUtils.isEmpty(other.slaveCommandSuffix)) { return false; - } else if (!slaveCommandSuffix.equals(other.slaveCommandSuffix)) + } + } else if (!slaveCommandSuffix.equals(other.slaveCommandSuffix)) { return false; + } if (StringUtils.isEmpty(sshPort)) { - if (!StringUtils.isEmpty(other.sshPort)) + if (!StringUtils.isEmpty(other.sshPort)) { return false; - } else if (!sshPort.equals(other.sshPort)) + } + } else if (!sshPort.equals(other.sshPort)) { return false; + } return true; } } diff --git a/src/main/java/hudson/plugins/ec2/NoDelayProvisionerStrategy.java b/src/main/java/hudson/plugins/ec2/NoDelayProvisionerStrategy.java index a4af7a43a..056ba5ab3 100644 --- a/src/main/java/hudson/plugins/ec2/NoDelayProvisionerStrategy.java +++ b/src/main/java/hudson/plugins/ec2/NoDelayProvisionerStrategy.java @@ -5,11 +5,10 @@ import hudson.model.LoadStatistics; import hudson.slaves.Cloud; import hudson.slaves.NodeProvisioner; -import jenkins.model.Jenkins; - import java.util.Collection; import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.model.Jenkins; /** * Implementation of {@link NodeProvisioner.Strategy} which will provision a new node immediately as @@ -28,27 +27,37 @@ public NodeProvisioner.StrategyDecision apply(NodeProvisioner.StrategyState stra final Label label = strategyState.getLabel(); LoadStatistics.LoadStatisticsSnapshot snapshot = strategyState.getSnapshot(); - int availableCapacity = - snapshot.getAvailableExecutors() // live executors - + snapshot.getConnectingExecutors() // executors present but not yet connected - + strategyState.getPlannedCapacitySnapshot() // capacity added by previous strategies from previous rounds - + strategyState.getAdditionalPlannedCapacity(); // capacity added by previous strategies _this round_ + int availableCapacity = snapshot.getAvailableExecutors() // live executors + + snapshot.getConnectingExecutors() // executors present but not yet connected + + strategyState + .getPlannedCapacitySnapshot() // capacity added by previous strategies from previous rounds + + strategyState.getAdditionalPlannedCapacity(); // capacity added by previous strategies _this round_ int currentDemand = snapshot.getQueueLength(); - LOGGER.log(Level.FINE, "Available capacity={0}, currentDemand={1}", - new Object[]{availableCapacity, currentDemand}); + LOGGER.log( + Level.FINE, "Available capacity={0}, currentDemand={1}", new Object[] {availableCapacity, currentDemand + }); if (availableCapacity < currentDemand) { Jenkins jenkinsInstance = Jenkins.get(); for (Cloud cloud : jenkinsInstance.clouds) { - if (!(cloud instanceof AmazonEC2Cloud)) continue; - if (!cloud.canProvision(label)) continue; + if (!(cloud instanceof AmazonEC2Cloud)) { + continue; + } + if (!cloud.canProvision(label)) { + continue; + } AmazonEC2Cloud ec2 = (AmazonEC2Cloud) cloud; - if (!ec2.isNoDelayProvisioning()) continue; + if (!ec2.isNoDelayProvisioning()) { + continue; + } - Collection plannedNodes = cloud.provision(label, currentDemand - availableCapacity); + Collection plannedNodes = + cloud.provision(label, currentDemand - availableCapacity); LOGGER.log(Level.FINE, "Planned {0} new nodes", plannedNodes.size()); strategyState.recordPendingLaunches(plannedNodes); availableCapacity += plannedNodes.size(); - LOGGER.log(Level.FINE, "After provisioning, available capacity={0}, currentDemand={1}", new Object[]{availableCapacity, currentDemand}); + LOGGER.log(Level.FINE, "After provisioning, available capacity={0}, currentDemand={1}", new Object[] { + availableCapacity, currentDemand + }); break; } } @@ -60,5 +69,4 @@ public NodeProvisioner.StrategyDecision apply(NodeProvisioner.StrategyState stra return NodeProvisioner.StrategyDecision.CONSULT_REMAINING_STRATEGIES; } } - -} \ No newline at end of file +} diff --git a/src/main/java/hudson/plugins/ec2/PluginImpl.java b/src/main/java/hudson/plugins/ec2/PluginImpl.java index 656cfff58..6f4dc0928 100644 --- a/src/main/java/hudson/plugins/ec2/PluginImpl.java +++ b/src/main/java/hudson/plugins/ec2/PluginImpl.java @@ -28,12 +28,9 @@ import hudson.model.Describable; import hudson.model.Descriptor; import hudson.plugins.ec2.util.MinimumInstanceChecker; -import jenkins.model.Jenkins; - import java.io.IOException; import java.util.logging.Logger; - -import java.io.IOException; +import jenkins.model.Jenkins; /** * Added to handle backwards compatibility of xstream class name mapping. @@ -41,17 +38,18 @@ @Extension public class PluginImpl extends Plugin implements Describable { private static final Logger LOGGER = Logger.getLogger(PluginImpl.class.getName()); - + // Whether the SshHostKeyVerificationAdministrativeMonitor should show messages when we have templates using // accept-new or check-new-soft strategies - private long dismissInsecureMessages; + private long dismissInsecureMessages; public void saveDismissInsecureMessages(long dismissInsecureMessages) { this.dismissInsecureMessages = dismissInsecureMessages; try { save(); - } catch(IOException io) { - LOGGER.warning("There was a problem saving that you want to dismiss all messages related to insecure EC2 templates"); + } catch (IOException io) { + LOGGER.warning( + "There was a problem saving that you want to dismiss all messages related to insecure EC2 templates"); } } @@ -59,6 +57,7 @@ public long getDismissInsecureMessages() { return dismissInsecureMessages; } + @Override public DescriptorImpl getDescriptor() { return (DescriptorImpl) Jenkins.get().getDescriptorOrDie(getClass()); } @@ -84,7 +83,7 @@ public void postInitialize() throws IOException { Jenkins.XSTREAM.registerConverter(new InstanceTypeConverter()); load(); - + MinimumInstanceChecker.checkForMinimumInstances(); } } diff --git a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java index 1c9de51d4..4d51368f5 100644 --- a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java +++ b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java @@ -17,11 +17,12 @@ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package hudson.plugins.ec2; -import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED; + +import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_JAVA_PATH; import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED; -import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED; import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT; -import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_JAVA_PATH; +import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED; +import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED; import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; @@ -70,6 +71,7 @@ import com.amazonaws.services.ec2.model.Subnet; import com.amazonaws.services.ec2.model.Tag; import com.amazonaws.services.ec2.model.TagSpecification; +import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.Util; @@ -98,22 +100,6 @@ import hudson.util.FormValidation; import hudson.util.ListBoxModel; import hudson.util.Secret; -import jenkins.model.Jenkins; -import jenkins.model.JenkinsLocationConfiguration; -import jenkins.slaves.iterators.api.NodeIterator; -import org.apache.commons.lang.StringUtils; -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.DataBoundSetter; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.Stapler; -import org.kohsuke.stapler.interceptor.RequirePOST; - -import edu.umd.cs.findbugs.annotations.CheckForNull; -import org.kohsuke.stapler.verb.POST; - -import javax.servlet.ServletException; import java.io.IOException; import java.io.PrintStream; import java.net.URL; @@ -136,6 +122,19 @@ import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.servlet.ServletException; +import jenkins.model.Jenkins; +import jenkins.model.JenkinsLocationConfiguration; +import jenkins.slaves.iterators.api.NodeIterator; +import org.apache.commons.lang.StringUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.Stapler; +import org.kohsuke.stapler.interceptor.RequirePOST; +import org.kohsuke.stapler.verb.POST; /** * Template of {@link EC2AbstractSlave} to launch. @@ -243,9 +242,9 @@ public class SlaveTemplate implements Describable { private Integer metadataHopsLimit; - private transient/* almost final */ Set labelSet; + private transient /* almost final */ Set labelSet; - private transient/* almost final */Set securityGroupSet; + private transient /* almost final */ Set securityGroupSet; /* FIXME: Ideally these would be List, but Jenkins currently * doesn't offer a usable way to represent those in forms. Instead @@ -287,23 +286,61 @@ public class SlaveTemplate implements Describable { public transient boolean useDedicatedTenancy; @DataBoundConstructor - public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS, - InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript, - String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String javaPath, String jvmopts, - boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, int minimumNumberOfInstances, - int minimumNumberOfSpareInstances, String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination, - boolean useEphemeralDevices, String launchTimeoutStr, boolean associatePublicIp, - String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring, - boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses, - List> nodeProperties, HostKeyVerificationStrategyEnum hostKeyVerificationStrategy, Tenancy tenancy, EbsEncryptRootVolume ebsEncryptRootVolume, - Boolean metadataEndpointEnabled, Boolean metadataTokensRequired, Integer metadataHopsLimit, Boolean metadataSupported) { - - if(StringUtils.isNotBlank(remoteAdmin) || StringUtils.isNotBlank(jvmopts) || StringUtils.isNotBlank(tmpDir)){ - LOGGER.log(Level.FINE, "As remoteAdmin, jvmopts or tmpDir is not blank, we must ensure the user has ADMINISTER rights."); + public SlaveTemplate( + String ami, + String zone, + SpotConfiguration spotConfig, + String securityGroups, + String remoteFS, + InstanceType type, + boolean ebsOptimized, + String labelString, + Node.Mode mode, + String description, + String initScript, + String tmpDir, + String userData, + String numExecutors, + String remoteAdmin, + AMITypeData amiType, + String javaPath, + String jvmopts, + boolean stopOnTerminate, + String subnetId, + List tags, + String idleTerminationMinutes, + int minimumNumberOfInstances, + int minimumNumberOfSpareInstances, + String instanceCapStr, + String iamInstanceProfile, + boolean deleteRootOnTermination, + boolean useEphemeralDevices, + String launchTimeoutStr, + boolean associatePublicIp, + String customDeviceMapping, + boolean connectBySSHProcess, + boolean monitoring, + boolean t2Unlimited, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + List> nodeProperties, + HostKeyVerificationStrategyEnum hostKeyVerificationStrategy, + Tenancy tenancy, + EbsEncryptRootVolume ebsEncryptRootVolume, + Boolean metadataEndpointEnabled, + Boolean metadataTokensRequired, + Integer metadataHopsLimit, + Boolean metadataSupported) { + + if (StringUtils.isNotBlank(remoteAdmin) || StringUtils.isNotBlank(jvmopts) || StringUtils.isNotBlank(tmpDir)) { + LOGGER.log( + Level.FINE, + "As remoteAdmin, jvmopts or tmpDir is not blank, we must ensure the user has ADMINISTER rights."); // Can be null during tests Jenkins j = Jenkins.getInstanceOrNull(); - if (j != null) + if (j != null) { j.checkPermission(Jenkins.ADMINISTER); + } } this.ami = ami; @@ -367,243 +404,1010 @@ public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, Stri this.customDeviceMapping = customDeviceMapping; this.t2Unlimited = t2Unlimited; - this.hostKeyVerificationStrategy = hostKeyVerificationStrategy != null ? hostKeyVerificationStrategy : HostKeyVerificationStrategyEnum.CHECK_NEW_SOFT; + this.hostKeyVerificationStrategy = hostKeyVerificationStrategy != null + ? hostKeyVerificationStrategy + : HostKeyVerificationStrategyEnum.CHECK_NEW_SOFT; this.tenancy = tenancy != null ? tenancy : Tenancy.Default; this.ebsEncryptRootVolume = ebsEncryptRootVolume != null ? ebsEncryptRootVolume : EbsEncryptRootVolume.DEFAULT; this.metadataSupported = metadataSupported != null ? metadataSupported : DEFAULT_METADATA_SUPPORTED; - this.metadataEndpointEnabled = metadataEndpointEnabled != null ? metadataEndpointEnabled : DEFAULT_METADATA_ENDPOINT_ENABLED; - this.metadataTokensRequired = metadataTokensRequired != null ? metadataTokensRequired : DEFAULT_METADATA_TOKENS_REQUIRED; + this.metadataEndpointEnabled = + metadataEndpointEnabled != null ? metadataEndpointEnabled : DEFAULT_METADATA_ENDPOINT_ENABLED; + this.metadataTokensRequired = + metadataTokensRequired != null ? metadataTokensRequired : DEFAULT_METADATA_TOKENS_REQUIRED; this.metadataHopsLimit = metadataHopsLimit != null ? metadataHopsLimit : DEFAULT_METADATA_HOPS_LIMIT; readResolve(); // initialize } @Deprecated - public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS, - InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript, - String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String javaPath, String jvmopts, - boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, int minimumNumberOfInstances, - int minimumNumberOfSpareInstances, String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination, - boolean useEphemeralDevices, String launchTimeoutStr, boolean associatePublicIp, - String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring, - boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses, - List> nodeProperties, HostKeyVerificationStrategyEnum hostKeyVerificationStrategy, Tenancy tenancy, EbsEncryptRootVolume ebsEncryptRootVolume, - Boolean metadataSupported, Boolean metadataEndpointEnabled, Boolean metadataTokensRequired, Integer metadataHopsLimit) { - this(ami, zone, spotConfig, securityGroups, remoteFS, - type, ebsOptimized, labelString, mode, description, initScript, - tmpDir, userData, numExecutors, remoteAdmin, amiType, DEFAULT_JAVA_PATH, jvmopts, - stopOnTerminate, subnetId, tags, idleTerminationMinutes, minimumNumberOfInstances, - minimumNumberOfSpareInstances, instanceCapStr, iamInstanceProfile, deleteRootOnTermination, - useEphemeralDevices, launchTimeoutStr, associatePublicIp, - customDeviceMapping, connectBySSHProcess, monitoring, - t2Unlimited, connectionStrategy, maxTotalUses, - nodeProperties, hostKeyVerificationStrategy, tenancy, null, metadataEndpointEnabled, - metadataTokensRequired, metadataHopsLimit, DEFAULT_METADATA_SUPPORTED); + public SlaveTemplate( + String ami, + String zone, + SpotConfiguration spotConfig, + String securityGroups, + String remoteFS, + InstanceType type, + boolean ebsOptimized, + String labelString, + Node.Mode mode, + String description, + String initScript, + String tmpDir, + String userData, + String numExecutors, + String remoteAdmin, + AMITypeData amiType, + String javaPath, + String jvmopts, + boolean stopOnTerminate, + String subnetId, + List tags, + String idleTerminationMinutes, + int minimumNumberOfInstances, + int minimumNumberOfSpareInstances, + String instanceCapStr, + String iamInstanceProfile, + boolean deleteRootOnTermination, + boolean useEphemeralDevices, + String launchTimeoutStr, + boolean associatePublicIp, + String customDeviceMapping, + boolean connectBySSHProcess, + boolean monitoring, + boolean t2Unlimited, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + List> nodeProperties, + HostKeyVerificationStrategyEnum hostKeyVerificationStrategy, + Tenancy tenancy, + EbsEncryptRootVolume ebsEncryptRootVolume, + Boolean metadataSupported, + Boolean metadataEndpointEnabled, + Boolean metadataTokensRequired, + Integer metadataHopsLimit) { + this( + ami, + zone, + spotConfig, + securityGroups, + remoteFS, + type, + ebsOptimized, + labelString, + mode, + description, + initScript, + tmpDir, + userData, + numExecutors, + remoteAdmin, + amiType, + DEFAULT_JAVA_PATH, + jvmopts, + stopOnTerminate, + subnetId, + tags, + idleTerminationMinutes, + minimumNumberOfInstances, + minimumNumberOfSpareInstances, + instanceCapStr, + iamInstanceProfile, + deleteRootOnTermination, + useEphemeralDevices, + launchTimeoutStr, + associatePublicIp, + customDeviceMapping, + connectBySSHProcess, + monitoring, + t2Unlimited, + connectionStrategy, + maxTotalUses, + nodeProperties, + hostKeyVerificationStrategy, + tenancy, + null, + metadataEndpointEnabled, + metadataTokensRequired, + metadataHopsLimit, + DEFAULT_METADATA_SUPPORTED); } @Deprecated - public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS, - InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript, - String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts, - boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, int minimumNumberOfInstances, - int minimumNumberOfSpareInstances, String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination, - boolean useEphemeralDevices, String launchTimeoutStr, boolean associatePublicIp, - String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring, - boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses, - List> nodeProperties, HostKeyVerificationStrategyEnum hostKeyVerificationStrategy, Tenancy tenancy, EbsEncryptRootVolume ebsEncryptRootVolume) { - this(ami, zone, spotConfig, securityGroups, remoteFS, - type, ebsOptimized, labelString, mode, description, initScript, - tmpDir, userData, numExecutors, remoteAdmin, amiType, DEFAULT_JAVA_PATH, jvmopts, - stopOnTerminate, subnetId, tags, idleTerminationMinutes, minimumNumberOfInstances, - minimumNumberOfSpareInstances, instanceCapStr, iamInstanceProfile, deleteRootOnTermination, - useEphemeralDevices, launchTimeoutStr, associatePublicIp, - customDeviceMapping, connectBySSHProcess, monitoring, - t2Unlimited, connectionStrategy, maxTotalUses, - nodeProperties, hostKeyVerificationStrategy, tenancy, null, DEFAULT_METADATA_ENDPOINT_ENABLED, - DEFAULT_METADATA_TOKENS_REQUIRED, DEFAULT_METADATA_HOPS_LIMIT, DEFAULT_METADATA_SUPPORTED); + public SlaveTemplate( + String ami, + String zone, + SpotConfiguration spotConfig, + String securityGroups, + String remoteFS, + InstanceType type, + boolean ebsOptimized, + String labelString, + Node.Mode mode, + String description, + String initScript, + String tmpDir, + String userData, + String numExecutors, + String remoteAdmin, + AMITypeData amiType, + String jvmopts, + boolean stopOnTerminate, + String subnetId, + List tags, + String idleTerminationMinutes, + int minimumNumberOfInstances, + int minimumNumberOfSpareInstances, + String instanceCapStr, + String iamInstanceProfile, + boolean deleteRootOnTermination, + boolean useEphemeralDevices, + String launchTimeoutStr, + boolean associatePublicIp, + String customDeviceMapping, + boolean connectBySSHProcess, + boolean monitoring, + boolean t2Unlimited, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + List> nodeProperties, + HostKeyVerificationStrategyEnum hostKeyVerificationStrategy, + Tenancy tenancy, + EbsEncryptRootVolume ebsEncryptRootVolume) { + this( + ami, + zone, + spotConfig, + securityGroups, + remoteFS, + type, + ebsOptimized, + labelString, + mode, + description, + initScript, + tmpDir, + userData, + numExecutors, + remoteAdmin, + amiType, + DEFAULT_JAVA_PATH, + jvmopts, + stopOnTerminate, + subnetId, + tags, + idleTerminationMinutes, + minimumNumberOfInstances, + minimumNumberOfSpareInstances, + instanceCapStr, + iamInstanceProfile, + deleteRootOnTermination, + useEphemeralDevices, + launchTimeoutStr, + associatePublicIp, + customDeviceMapping, + connectBySSHProcess, + monitoring, + t2Unlimited, + connectionStrategy, + maxTotalUses, + nodeProperties, + hostKeyVerificationStrategy, + tenancy, + null, + DEFAULT_METADATA_ENDPOINT_ENABLED, + DEFAULT_METADATA_TOKENS_REQUIRED, + DEFAULT_METADATA_HOPS_LIMIT, + DEFAULT_METADATA_SUPPORTED); } @Deprecated - public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS, - InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript, - String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts, - boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, int minimumNumberOfInstances, - int minimumNumberOfSpareInstances, String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination, - boolean useEphemeralDevices, String launchTimeoutStr, boolean associatePublicIp, - String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring, - boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses, - List> nodeProperties, HostKeyVerificationStrategyEnum hostKeyVerificationStrategy, Tenancy tenancy) { - this(ami, zone, spotConfig, securityGroups, remoteFS, - type, ebsOptimized, labelString, mode, description, initScript, - tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts, - stopOnTerminate, subnetId, tags, idleTerminationMinutes, minimumNumberOfInstances, - minimumNumberOfSpareInstances, instanceCapStr, iamInstanceProfile, deleteRootOnTermination, - useEphemeralDevices, launchTimeoutStr, associatePublicIp, - customDeviceMapping, connectBySSHProcess, monitoring, - t2Unlimited, connectionStrategy, maxTotalUses, - nodeProperties, hostKeyVerificationStrategy, tenancy, null); + public SlaveTemplate( + String ami, + String zone, + SpotConfiguration spotConfig, + String securityGroups, + String remoteFS, + InstanceType type, + boolean ebsOptimized, + String labelString, + Node.Mode mode, + String description, + String initScript, + String tmpDir, + String userData, + String numExecutors, + String remoteAdmin, + AMITypeData amiType, + String jvmopts, + boolean stopOnTerminate, + String subnetId, + List tags, + String idleTerminationMinutes, + int minimumNumberOfInstances, + int minimumNumberOfSpareInstances, + String instanceCapStr, + String iamInstanceProfile, + boolean deleteRootOnTermination, + boolean useEphemeralDevices, + String launchTimeoutStr, + boolean associatePublicIp, + String customDeviceMapping, + boolean connectBySSHProcess, + boolean monitoring, + boolean t2Unlimited, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + List> nodeProperties, + HostKeyVerificationStrategyEnum hostKeyVerificationStrategy, + Tenancy tenancy) { + this( + ami, + zone, + spotConfig, + securityGroups, + remoteFS, + type, + ebsOptimized, + labelString, + mode, + description, + initScript, + tmpDir, + userData, + numExecutors, + remoteAdmin, + amiType, + jvmopts, + stopOnTerminate, + subnetId, + tags, + idleTerminationMinutes, + minimumNumberOfInstances, + minimumNumberOfSpareInstances, + instanceCapStr, + iamInstanceProfile, + deleteRootOnTermination, + useEphemeralDevices, + launchTimeoutStr, + associatePublicIp, + customDeviceMapping, + connectBySSHProcess, + monitoring, + t2Unlimited, + connectionStrategy, + maxTotalUses, + nodeProperties, + hostKeyVerificationStrategy, + tenancy, + null); } @Deprecated - public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS, - InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript, - String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts, - boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, int minimumNumberOfInstances, - int minimumNumberOfSpareInstances, String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination, - boolean useEphemeralDevices, boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp, - String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring, - boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses, - List> nodeProperties, HostKeyVerificationStrategyEnum hostKeyVerificationStrategy) { - this(ami, zone, spotConfig, securityGroups, remoteFS, - type, ebsOptimized, labelString, mode, description, initScript, - tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts, - stopOnTerminate, subnetId, tags, idleTerminationMinutes, minimumNumberOfInstances, - minimumNumberOfSpareInstances, instanceCapStr, iamInstanceProfile, deleteRootOnTermination, - useEphemeralDevices, launchTimeoutStr, associatePublicIp, - customDeviceMapping, connectBySSHProcess, monitoring, - t2Unlimited, connectionStrategy, maxTotalUses, - nodeProperties, hostKeyVerificationStrategy, Tenancy.backwardsCompatible(useDedicatedTenancy)); + public SlaveTemplate( + String ami, + String zone, + SpotConfiguration spotConfig, + String securityGroups, + String remoteFS, + InstanceType type, + boolean ebsOptimized, + String labelString, + Node.Mode mode, + String description, + String initScript, + String tmpDir, + String userData, + String numExecutors, + String remoteAdmin, + AMITypeData amiType, + String jvmopts, + boolean stopOnTerminate, + String subnetId, + List tags, + String idleTerminationMinutes, + int minimumNumberOfInstances, + int minimumNumberOfSpareInstances, + String instanceCapStr, + String iamInstanceProfile, + boolean deleteRootOnTermination, + boolean useEphemeralDevices, + boolean useDedicatedTenancy, + String launchTimeoutStr, + boolean associatePublicIp, + String customDeviceMapping, + boolean connectBySSHProcess, + boolean monitoring, + boolean t2Unlimited, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + List> nodeProperties, + HostKeyVerificationStrategyEnum hostKeyVerificationStrategy) { + this( + ami, + zone, + spotConfig, + securityGroups, + remoteFS, + type, + ebsOptimized, + labelString, + mode, + description, + initScript, + tmpDir, + userData, + numExecutors, + remoteAdmin, + amiType, + jvmopts, + stopOnTerminate, + subnetId, + tags, + idleTerminationMinutes, + minimumNumberOfInstances, + minimumNumberOfSpareInstances, + instanceCapStr, + iamInstanceProfile, + deleteRootOnTermination, + useEphemeralDevices, + launchTimeoutStr, + associatePublicIp, + customDeviceMapping, + connectBySSHProcess, + monitoring, + t2Unlimited, + connectionStrategy, + maxTotalUses, + nodeProperties, + hostKeyVerificationStrategy, + Tenancy.backwardsCompatible(useDedicatedTenancy)); } @Deprecated - public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS, - InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript, - String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts, - boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, int minimumNumberOfInstances, - int minimumNumberOfSpareInstances, String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination, - boolean useEphemeralDevices, boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp, - String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring, - boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses, + public SlaveTemplate( + String ami, + String zone, + SpotConfiguration spotConfig, + String securityGroups, + String remoteFS, + InstanceType type, + boolean ebsOptimized, + String labelString, + Node.Mode mode, + String description, + String initScript, + String tmpDir, + String userData, + String numExecutors, + String remoteAdmin, + AMITypeData amiType, + String jvmopts, + boolean stopOnTerminate, + String subnetId, + List tags, + String idleTerminationMinutes, + int minimumNumberOfInstances, + int minimumNumberOfSpareInstances, + String instanceCapStr, + String iamInstanceProfile, + boolean deleteRootOnTermination, + boolean useEphemeralDevices, + boolean useDedicatedTenancy, + String launchTimeoutStr, + boolean associatePublicIp, + String customDeviceMapping, + boolean connectBySSHProcess, + boolean monitoring, + boolean t2Unlimited, + ConnectionStrategy connectionStrategy, + int maxTotalUses, List> nodeProperties) { - this(ami, zone, spotConfig, securityGroups, remoteFS, - type, ebsOptimized, labelString, mode, description, initScript, - tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts, - stopOnTerminate, subnetId, tags, idleTerminationMinutes, minimumNumberOfInstances, - minimumNumberOfSpareInstances, instanceCapStr, iamInstanceProfile, deleteRootOnTermination, - useEphemeralDevices, useDedicatedTenancy, launchTimeoutStr, associatePublicIp, - customDeviceMapping, connectBySSHProcess, monitoring, - t2Unlimited, connectionStrategy, maxTotalUses, - nodeProperties, null); + this( + ami, + zone, + spotConfig, + securityGroups, + remoteFS, + type, + ebsOptimized, + labelString, + mode, + description, + initScript, + tmpDir, + userData, + numExecutors, + remoteAdmin, + amiType, + jvmopts, + stopOnTerminate, + subnetId, + tags, + idleTerminationMinutes, + minimumNumberOfInstances, + minimumNumberOfSpareInstances, + instanceCapStr, + iamInstanceProfile, + deleteRootOnTermination, + useEphemeralDevices, + useDedicatedTenancy, + launchTimeoutStr, + associatePublicIp, + customDeviceMapping, + connectBySSHProcess, + monitoring, + t2Unlimited, + connectionStrategy, + maxTotalUses, + nodeProperties, + null); } @Deprecated - public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS, - InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript, - String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts, - boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, int minimumNumberOfInstances, - String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination, - boolean useEphemeralDevices, boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp, - String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring, - boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses,List> nodeProperties ) { - this(ami, zone, spotConfig, securityGroups, remoteFS, type, ebsOptimized, labelString, mode, description, initScript, - tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts, stopOnTerminate, subnetId, tags, - idleTerminationMinutes, minimumNumberOfInstances, 0, instanceCapStr, iamInstanceProfile, deleteRootOnTermination, - useEphemeralDevices, useDedicatedTenancy, launchTimeoutStr, associatePublicIp, customDeviceMapping, - connectBySSHProcess, monitoring, t2Unlimited, connectionStrategy, maxTotalUses, nodeProperties); + public SlaveTemplate( + String ami, + String zone, + SpotConfiguration spotConfig, + String securityGroups, + String remoteFS, + InstanceType type, + boolean ebsOptimized, + String labelString, + Node.Mode mode, + String description, + String initScript, + String tmpDir, + String userData, + String numExecutors, + String remoteAdmin, + AMITypeData amiType, + String jvmopts, + boolean stopOnTerminate, + String subnetId, + List tags, + String idleTerminationMinutes, + int minimumNumberOfInstances, + String instanceCapStr, + String iamInstanceProfile, + boolean deleteRootOnTermination, + boolean useEphemeralDevices, + boolean useDedicatedTenancy, + String launchTimeoutStr, + boolean associatePublicIp, + String customDeviceMapping, + boolean connectBySSHProcess, + boolean monitoring, + boolean t2Unlimited, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + List> nodeProperties) { + this( + ami, + zone, + spotConfig, + securityGroups, + remoteFS, + type, + ebsOptimized, + labelString, + mode, + description, + initScript, + tmpDir, + userData, + numExecutors, + remoteAdmin, + amiType, + jvmopts, + stopOnTerminate, + subnetId, + tags, + idleTerminationMinutes, + minimumNumberOfInstances, + 0, + instanceCapStr, + iamInstanceProfile, + deleteRootOnTermination, + useEphemeralDevices, + useDedicatedTenancy, + launchTimeoutStr, + associatePublicIp, + customDeviceMapping, + connectBySSHProcess, + monitoring, + t2Unlimited, + connectionStrategy, + maxTotalUses, + nodeProperties); } @Deprecated - public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS, - InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript, - String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts, - boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, int minimumNumberOfInstances, - String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination, - boolean useEphemeralDevices, boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp, - String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring, - boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses) { - this(ami, zone, spotConfig, securityGroups, remoteFS, type, ebsOptimized, labelString, mode, description, initScript, - tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts, stopOnTerminate, subnetId, tags, - idleTerminationMinutes, minimumNumberOfInstances, instanceCapStr, iamInstanceProfile, deleteRootOnTermination, - useEphemeralDevices, useDedicatedTenancy, launchTimeoutStr, associatePublicIp, customDeviceMapping, - connectBySSHProcess, monitoring, t2Unlimited, connectionStrategy, maxTotalUses, Collections.emptyList()); + public SlaveTemplate( + String ami, + String zone, + SpotConfiguration spotConfig, + String securityGroups, + String remoteFS, + InstanceType type, + boolean ebsOptimized, + String labelString, + Node.Mode mode, + String description, + String initScript, + String tmpDir, + String userData, + String numExecutors, + String remoteAdmin, + AMITypeData amiType, + String jvmopts, + boolean stopOnTerminate, + String subnetId, + List tags, + String idleTerminationMinutes, + int minimumNumberOfInstances, + String instanceCapStr, + String iamInstanceProfile, + boolean deleteRootOnTermination, + boolean useEphemeralDevices, + boolean useDedicatedTenancy, + String launchTimeoutStr, + boolean associatePublicIp, + String customDeviceMapping, + boolean connectBySSHProcess, + boolean monitoring, + boolean t2Unlimited, + ConnectionStrategy connectionStrategy, + int maxTotalUses) { + this( + ami, + zone, + spotConfig, + securityGroups, + remoteFS, + type, + ebsOptimized, + labelString, + mode, + description, + initScript, + tmpDir, + userData, + numExecutors, + remoteAdmin, + amiType, + jvmopts, + stopOnTerminate, + subnetId, + tags, + idleTerminationMinutes, + minimumNumberOfInstances, + instanceCapStr, + iamInstanceProfile, + deleteRootOnTermination, + useEphemeralDevices, + useDedicatedTenancy, + launchTimeoutStr, + associatePublicIp, + customDeviceMapping, + connectBySSHProcess, + monitoring, + t2Unlimited, + connectionStrategy, + maxTotalUses, + Collections.emptyList()); } @Deprecated - public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS, - InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript, - String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts, - boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, - String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination, - boolean useEphemeralDevices, boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp, - String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring, - boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses) { - this(ami, zone, spotConfig, securityGroups, remoteFS, type, ebsOptimized, labelString, mode, description, initScript, - tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts, stopOnTerminate, subnetId, tags, - idleTerminationMinutes, 0, instanceCapStr, iamInstanceProfile, deleteRootOnTermination, useEphemeralDevices, - useDedicatedTenancy, launchTimeoutStr, associatePublicIp, customDeviceMapping, connectBySSHProcess, - monitoring, t2Unlimited, connectionStrategy, maxTotalUses); + public SlaveTemplate( + String ami, + String zone, + SpotConfiguration spotConfig, + String securityGroups, + String remoteFS, + InstanceType type, + boolean ebsOptimized, + String labelString, + Node.Mode mode, + String description, + String initScript, + String tmpDir, + String userData, + String numExecutors, + String remoteAdmin, + AMITypeData amiType, + String jvmopts, + boolean stopOnTerminate, + String subnetId, + List tags, + String idleTerminationMinutes, + String instanceCapStr, + String iamInstanceProfile, + boolean deleteRootOnTermination, + boolean useEphemeralDevices, + boolean useDedicatedTenancy, + String launchTimeoutStr, + boolean associatePublicIp, + String customDeviceMapping, + boolean connectBySSHProcess, + boolean monitoring, + boolean t2Unlimited, + ConnectionStrategy connectionStrategy, + int maxTotalUses) { + this( + ami, + zone, + spotConfig, + securityGroups, + remoteFS, + type, + ebsOptimized, + labelString, + mode, + description, + initScript, + tmpDir, + userData, + numExecutors, + remoteAdmin, + amiType, + jvmopts, + stopOnTerminate, + subnetId, + tags, + idleTerminationMinutes, + 0, + instanceCapStr, + iamInstanceProfile, + deleteRootOnTermination, + useEphemeralDevices, + useDedicatedTenancy, + launchTimeoutStr, + associatePublicIp, + customDeviceMapping, + connectBySSHProcess, + monitoring, + t2Unlimited, + connectionStrategy, + maxTotalUses); } @Deprecated - public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS, - InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript, - String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts, - boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, - boolean usePrivateDnsName, String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination, - boolean useEphemeralDevices, boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp, - String customDeviceMapping, boolean connectBySSHProcess, boolean connectUsingPublicIp, boolean monitoring, + public SlaveTemplate( + String ami, + String zone, + SpotConfiguration spotConfig, + String securityGroups, + String remoteFS, + InstanceType type, + boolean ebsOptimized, + String labelString, + Node.Mode mode, + String description, + String initScript, + String tmpDir, + String userData, + String numExecutors, + String remoteAdmin, + AMITypeData amiType, + String jvmopts, + boolean stopOnTerminate, + String subnetId, + List tags, + String idleTerminationMinutes, + boolean usePrivateDnsName, + String instanceCapStr, + String iamInstanceProfile, + boolean deleteRootOnTermination, + boolean useEphemeralDevices, + boolean useDedicatedTenancy, + String launchTimeoutStr, + boolean associatePublicIp, + String customDeviceMapping, + boolean connectBySSHProcess, + boolean connectUsingPublicIp, + boolean monitoring, boolean t2Unlimited) { - this(ami, zone, spotConfig, securityGroups, remoteFS, type, ebsOptimized, labelString, mode, description, initScript, - tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts, stopOnTerminate, subnetId, tags, - idleTerminationMinutes, instanceCapStr, iamInstanceProfile, deleteRootOnTermination, useEphemeralDevices, - useDedicatedTenancy, launchTimeoutStr, associatePublicIp, customDeviceMapping, connectBySSHProcess, - monitoring, t2Unlimited, ConnectionStrategy.backwardsCompatible(usePrivateDnsName, connectUsingPublicIp, associatePublicIp), -1); + this( + ami, + zone, + spotConfig, + securityGroups, + remoteFS, + type, + ebsOptimized, + labelString, + mode, + description, + initScript, + tmpDir, + userData, + numExecutors, + remoteAdmin, + amiType, + jvmopts, + stopOnTerminate, + subnetId, + tags, + idleTerminationMinutes, + instanceCapStr, + iamInstanceProfile, + deleteRootOnTermination, + useEphemeralDevices, + useDedicatedTenancy, + launchTimeoutStr, + associatePublicIp, + customDeviceMapping, + connectBySSHProcess, + monitoring, + t2Unlimited, + ConnectionStrategy.backwardsCompatible(usePrivateDnsName, connectUsingPublicIp, associatePublicIp), + -1); } @Deprecated - public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS, - InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript, - String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts, - boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, - boolean usePrivateDnsName, String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination, - boolean useEphemeralDevices, boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp, - String customDeviceMapping, boolean connectBySSHProcess, boolean connectUsingPublicIp) { - this(ami, zone, spotConfig, securityGroups, remoteFS, type, ebsOptimized, labelString, mode, description, initScript, - tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts, stopOnTerminate, subnetId, tags, - idleTerminationMinutes, usePrivateDnsName, instanceCapStr, iamInstanceProfile, deleteRootOnTermination, useEphemeralDevices, - useDedicatedTenancy, launchTimeoutStr, associatePublicIp, customDeviceMapping, connectBySSHProcess, - connectUsingPublicIp, false, false); + public SlaveTemplate( + String ami, + String zone, + SpotConfiguration spotConfig, + String securityGroups, + String remoteFS, + InstanceType type, + boolean ebsOptimized, + String labelString, + Node.Mode mode, + String description, + String initScript, + String tmpDir, + String userData, + String numExecutors, + String remoteAdmin, + AMITypeData amiType, + String jvmopts, + boolean stopOnTerminate, + String subnetId, + List tags, + String idleTerminationMinutes, + boolean usePrivateDnsName, + String instanceCapStr, + String iamInstanceProfile, + boolean deleteRootOnTermination, + boolean useEphemeralDevices, + boolean useDedicatedTenancy, + String launchTimeoutStr, + boolean associatePublicIp, + String customDeviceMapping, + boolean connectBySSHProcess, + boolean connectUsingPublicIp) { + this( + ami, + zone, + spotConfig, + securityGroups, + remoteFS, + type, + ebsOptimized, + labelString, + mode, + description, + initScript, + tmpDir, + userData, + numExecutors, + remoteAdmin, + amiType, + jvmopts, + stopOnTerminate, + subnetId, + tags, + idleTerminationMinutes, + usePrivateDnsName, + instanceCapStr, + iamInstanceProfile, + deleteRootOnTermination, + useEphemeralDevices, + useDedicatedTenancy, + launchTimeoutStr, + associatePublicIp, + customDeviceMapping, + connectBySSHProcess, + connectUsingPublicIp, + false, + false); } @Deprecated - public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS, - InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript, - String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts, - boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, - boolean usePrivateDnsName, String instanceCapStr, String iamInstanceProfile, boolean useEphemeralDevices, - boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp, String customDeviceMapping, + public SlaveTemplate( + String ami, + String zone, + SpotConfiguration spotConfig, + String securityGroups, + String remoteFS, + InstanceType type, + boolean ebsOptimized, + String labelString, + Node.Mode mode, + String description, + String initScript, + String tmpDir, + String userData, + String numExecutors, + String remoteAdmin, + AMITypeData amiType, + String jvmopts, + boolean stopOnTerminate, + String subnetId, + List tags, + String idleTerminationMinutes, + boolean usePrivateDnsName, + String instanceCapStr, + String iamInstanceProfile, + boolean useEphemeralDevices, + boolean useDedicatedTenancy, + String launchTimeoutStr, + boolean associatePublicIp, + String customDeviceMapping, boolean connectBySSHProcess) { - this(ami, zone, spotConfig, securityGroups, remoteFS, type, ebsOptimized, labelString, mode, description, initScript, - tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts, stopOnTerminate, subnetId, tags, - idleTerminationMinutes, usePrivateDnsName, instanceCapStr, iamInstanceProfile, false, useEphemeralDevices, - useDedicatedTenancy, launchTimeoutStr, associatePublicIp, customDeviceMapping, connectBySSHProcess, false); + this( + ami, + zone, + spotConfig, + securityGroups, + remoteFS, + type, + ebsOptimized, + labelString, + mode, + description, + initScript, + tmpDir, + userData, + numExecutors, + remoteAdmin, + amiType, + jvmopts, + stopOnTerminate, + subnetId, + tags, + idleTerminationMinutes, + usePrivateDnsName, + instanceCapStr, + iamInstanceProfile, + false, + useEphemeralDevices, + useDedicatedTenancy, + launchTimeoutStr, + associatePublicIp, + customDeviceMapping, + connectBySSHProcess, + false); } @Deprecated - public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS, - InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript, - String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts, - boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, - boolean usePrivateDnsName, String instanceCapStr, String iamInstanceProfile, boolean useEphemeralDevices, - boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp, String customDeviceMapping) { - this(ami, zone, spotConfig, securityGroups, remoteFS, type, ebsOptimized, labelString, mode, description, initScript, - tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts, stopOnTerminate, subnetId, tags, - idleTerminationMinutes, usePrivateDnsName, instanceCapStr, iamInstanceProfile, useEphemeralDevices, - useDedicatedTenancy, launchTimeoutStr, associatePublicIp, customDeviceMapping, false); + public SlaveTemplate( + String ami, + String zone, + SpotConfiguration spotConfig, + String securityGroups, + String remoteFS, + InstanceType type, + boolean ebsOptimized, + String labelString, + Node.Mode mode, + String description, + String initScript, + String tmpDir, + String userData, + String numExecutors, + String remoteAdmin, + AMITypeData amiType, + String jvmopts, + boolean stopOnTerminate, + String subnetId, + List tags, + String idleTerminationMinutes, + boolean usePrivateDnsName, + String instanceCapStr, + String iamInstanceProfile, + boolean useEphemeralDevices, + boolean useDedicatedTenancy, + String launchTimeoutStr, + boolean associatePublicIp, + String customDeviceMapping) { + this( + ami, + zone, + spotConfig, + securityGroups, + remoteFS, + type, + ebsOptimized, + labelString, + mode, + description, + initScript, + tmpDir, + userData, + numExecutors, + remoteAdmin, + amiType, + jvmopts, + stopOnTerminate, + subnetId, + tags, + idleTerminationMinutes, + usePrivateDnsName, + instanceCapStr, + iamInstanceProfile, + useEphemeralDevices, + useDedicatedTenancy, + launchTimeoutStr, + associatePublicIp, + customDeviceMapping, + false); } /** * Backward compatible constructor for reloading previous version data */ - public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS, - String sshPort, InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, - String initScript, String tmpDir, String userData, String numExecutors, String remoteAdmin, String rootCommandPrefix, - String slaveCommandPrefix, String slaveCommandSuffix, String jvmopts, boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, - boolean usePrivateDnsName, String instanceCapStr, String iamInstanceProfile, boolean useEphemeralDevices, + public SlaveTemplate( + String ami, + String zone, + SpotConfiguration spotConfig, + String securityGroups, + String remoteFS, + String sshPort, + InstanceType type, + boolean ebsOptimized, + String labelString, + Node.Mode mode, + String description, + String initScript, + String tmpDir, + String userData, + String numExecutors, + String remoteAdmin, + String rootCommandPrefix, + String slaveCommandPrefix, + String slaveCommandSuffix, + String jvmopts, + boolean stopOnTerminate, + String subnetId, + List tags, + String idleTerminationMinutes, + boolean usePrivateDnsName, + String instanceCapStr, + String iamInstanceProfile, + boolean useEphemeralDevices, String launchTimeoutStr) { - this(ami, zone, spotConfig, securityGroups, remoteFS, type, ebsOptimized, labelString, mode, description, initScript, - tmpDir, userData, numExecutors, remoteAdmin, new UnixData(rootCommandPrefix, slaveCommandPrefix, slaveCommandSuffix, sshPort, null), - jvmopts, stopOnTerminate, subnetId, tags, idleTerminationMinutes, usePrivateDnsName, instanceCapStr, iamInstanceProfile, - useEphemeralDevices, false, launchTimeoutStr, false, null); + this( + ami, + zone, + spotConfig, + securityGroups, + remoteFS, + type, + ebsOptimized, + labelString, + mode, + description, + initScript, + tmpDir, + userData, + numExecutors, + remoteAdmin, + new UnixData(rootCommandPrefix, slaveCommandPrefix, slaveCommandSuffix, sshPort, null), + jvmopts, + stopOnTerminate, + subnetId, + tags, + idleTerminationMinutes, + usePrivateDnsName, + instanceCapStr, + iamInstanceProfile, + useEphemeralDevices, + false, + launchTimeoutStr, + false, + null); } public boolean isConnectBySSHProcess() { @@ -686,22 +1490,28 @@ public String getRemoteAdmin() { } public String getRootCommandPrefix() { - return (amiType.isUnix() ? ((UnixData) amiType).getRootCommandPrefix() : (amiType.isMac() ? ((MacData) amiType).getRootCommandPrefix():"")); + return (amiType.isUnix() + ? ((UnixData) amiType).getRootCommandPrefix() + : (amiType.isMac() ? ((MacData) amiType).getRootCommandPrefix() : "")); } public String getSlaveCommandPrefix() { - return (amiType.isUnix() ? ((UnixData) amiType).getSlaveCommandPrefix() : (amiType.isMac() ? ((MacData) amiType).getSlaveCommandPrefix() : "")); + return (amiType.isUnix() + ? ((UnixData) amiType).getSlaveCommandPrefix() + : (amiType.isMac() ? ((MacData) amiType).getSlaveCommandPrefix() : "")); } public String getSlaveCommandSuffix() { - return (amiType.isUnix() ? ((UnixData) amiType).getSlaveCommandSuffix() : (amiType.isMac() ? ((MacData) amiType).getSlaveCommandSuffix() : "")); + return (amiType.isUnix() + ? ((UnixData) amiType).getSlaveCommandSuffix() + : (amiType.isMac() ? ((MacData) amiType).getSlaveCommandSuffix() : "")); } public String chooseSubnetId() { if (StringUtils.isBlank(subnetId)) { return null; } else { - String[] subnetIdList= getSubnetId().split(EC2_RESOURCE_ID_DELIMETERS); + String[] subnetIdList = getSubnetId().split(EC2_RESOURCE_ID_DELIMETERS); // Round-robin subnet selection. currentSubnetId = subnetIdList[nextSubnet]; @@ -735,14 +1545,16 @@ public boolean getAssociatePublicIp() { @DataBoundSetter public void setConnectUsingPublicIp(boolean connectUsingPublicIp) { this.connectUsingPublicIp = connectUsingPublicIp; - this.connectionStrategy = ConnectionStrategy.backwardsCompatible(this.usePrivateDnsName, this.connectUsingPublicIp, this.associatePublicIp); + this.connectionStrategy = ConnectionStrategy.backwardsCompatible( + this.usePrivateDnsName, this.connectUsingPublicIp, this.associatePublicIp); } @Deprecated @DataBoundSetter public void setUsePrivateDnsName(boolean usePrivateDnsName) { this.usePrivateDnsName = usePrivateDnsName; - this.connectionStrategy = ConnectionStrategy.backwardsCompatible(this.usePrivateDnsName, this.connectUsingPublicIp, this.associatePublicIp); + this.connectionStrategy = ConnectionStrategy.backwardsCompatible( + this.usePrivateDnsName, this.connectUsingPublicIp, this.associatePublicIp); } @Deprecated @@ -756,8 +1568,9 @@ public boolean isConnectUsingPublicIp() { } public List getTags() { - if (null == tags) + if (null == tags) { return null; + } return Collections.unmodifiableList(tags); } @@ -801,7 +1614,8 @@ public MinimumNumberOfInstancesTimeRangeConfig getMinimumNumberOfInstancesTimeRa } @DataBoundSetter - public void setMinimumNumberOfInstancesTimeRangeConfig(MinimumNumberOfInstancesTimeRangeConfig minimumNumberOfInstancesTimeRangeConfig) { + public void setMinimumNumberOfInstancesTimeRangeConfig( + MinimumNumberOfInstancesTimeRangeConfig minimumNumberOfInstancesTimeRangeConfig) { this.minimumNumberOfInstancesTimeRangeConfig = minimumNumberOfInstancesTimeRangeConfig; } @@ -810,8 +1624,9 @@ public int getInstanceCap() { } public int getSpotBlockReservationDuration() { - if (spotConfig == null) + if (spotConfig == null) { return 0; + } return spotConfig.getSpotBlockReservationDuration(); } @@ -820,8 +1635,9 @@ public String getSpotBlockReservationDurationStr() { return ""; } else { int dur = getSpotBlockReservationDuration(); - if (dur == 0) + if (dur == 0) { return ""; + } return String.valueOf(getSpotBlockReservationDuration()); } } @@ -835,8 +1651,9 @@ public String getInstanceCapStr() { } public String getSpotMaxBidPrice() { - if (spotConfig == null) + if (spotConfig == null) { return null; + } return SpotConfiguration.normalizeBid(spotConfig.getSpotMaxBidPrice()); } @@ -846,12 +1663,16 @@ public String getIamInstanceProfile() { @DataBoundSetter public void setHostKeyVerificationStrategy(HostKeyVerificationStrategyEnum hostKeyVerificationStrategy) { - this.hostKeyVerificationStrategy = (hostKeyVerificationStrategy != null) ? hostKeyVerificationStrategy : HostKeyVerificationStrategyEnum.CHECK_NEW_SOFT; + this.hostKeyVerificationStrategy = (hostKeyVerificationStrategy != null) + ? hostKeyVerificationStrategy + : HostKeyVerificationStrategyEnum.CHECK_NEW_SOFT; } @NonNull public HostKeyVerificationStrategyEnum getHostKeyVerificationStrategy() { - return hostKeyVerificationStrategy != null ? hostKeyVerificationStrategy : HostKeyVerificationStrategyEnum.CHECK_NEW_SOFT; + return hostKeyVerificationStrategy != null + ? hostKeyVerificationStrategy + : HostKeyVerificationStrategyEnum.CHECK_NEW_SOFT; } @CheckForNull @@ -886,10 +1707,7 @@ public void setAmiFilters(List amiFilters) { @Override public String toString() { - return "SlaveTemplate{" + - "description='" + description + '\'' + - ", labels='" + labels + '\'' + - '}'; + return "SlaveTemplate{" + "description='" + description + '\'' + ", labels='" + labels + '\'' + '}'; } public int getMaxTotalUses() { @@ -897,7 +1715,7 @@ public int getMaxTotalUses() { } public Boolean getMetadataSupported() { - return metadataSupported; + return metadataSupported; } public Boolean getMetadataEndpointEnabled() { @@ -920,7 +1738,10 @@ public DescribableList, NodePropertyDescriptor> getNodePropertie return Objects.requireNonNull(nodeProperties); } - public enum ProvisionOptions { ALLOW_CREATE, FORCE_CREATE } + public enum ProvisionOptions { + ALLOW_CREATE, + FORCE_CREATE + } /** * Provisions a new EC2 agent or starts a previously stopped on-demand instance. @@ -928,11 +1749,14 @@ public enum ProvisionOptions { ALLOW_CREATE, FORCE_CREATE } * @return always non-null. This needs to be then added to {@link Hudson#addNode(Node)}. */ @NonNull - public List provision(int number, EnumSet provisionOptions) throws AmazonClientException, IOException { + public List provision(int number, EnumSet provisionOptions) + throws AmazonClientException, IOException { final Image image = getImage(); if (this.spotConfig != null) { - if (provisionOptions.contains(ProvisionOptions.ALLOW_CREATE) || provisionOptions.contains(ProvisionOptions.FORCE_CREATE)) + if (provisionOptions.contains(ProvisionOptions.ALLOW_CREATE) + || provisionOptions.contains(ProvisionOptions.FORCE_CREATE)) { return provisionSpot(image, number, provisionOptions); + } return Collections.emptyList(); } return provisionOndemand(image, number, provisionOptions); @@ -943,11 +1767,10 @@ public List provision(int number, EnumSet pr */ private boolean checkInstance(Instance instance) { for (EC2AbstractSlave node : NodeIterator.nodes(EC2AbstractSlave.class)) { - if ( (node.getInstanceId().equals(instance.getInstanceId())) && - (! (instance.getState().getName().equalsIgnoreCase(InstanceStateName.Stopped.toString()) - )) - ){ - logInstanceCheck(instance, ". false - found existing corresponding Jenkins agent: " + node.getInstanceId()); + if ((node.getInstanceId().equals(instance.getInstanceId())) + && (!(instance.getState().getName().equalsIgnoreCase(InstanceStateName.Stopped.toString())))) { + logInstanceCheck( + instance, ". false - found existing corresponding Jenkins agent: " + node.getInstanceId()); return false; } } @@ -960,10 +1783,9 @@ private void logInstanceCheck(Instance instance, String message) { } private boolean isSameIamInstanceProfile(Instance instance) { - return StringUtils.isBlank(getIamInstanceProfile()) || - (instance.getIamInstanceProfile() != null && - instance.getIamInstanceProfile().getArn().equals(getIamInstanceProfile())); - + return StringUtils.isBlank(getIamInstanceProfile()) + || (instance.getIamInstanceProfile() != null + && instance.getIamInstanceProfile().getArn().equals(getIamInstanceProfile())); } private boolean isTerminatingOrShuttindDown(String instanceStateName) { @@ -975,22 +1797,25 @@ private void logProvisionInfo(String message) { LOGGER.info(this + ". " + message); } - HashMap> makeRunInstancesRequestAndFilters(Image image, int number, AmazonEC2 ec2) throws IOException { + HashMap> makeRunInstancesRequestAndFilters(Image image, int number, AmazonEC2 ec2) + throws IOException { return makeRunInstancesRequestAndFilters(image, number, ec2, true); } @Deprecated - HashMap> makeRunInstancesRequestAndFilters(int number, AmazonEC2 ec2) throws IOException { + HashMap> makeRunInstancesRequestAndFilters(int number, AmazonEC2 ec2) + throws IOException { return makeRunInstancesRequestAndFilters(getImage(), number, ec2); } - HashMap> makeRunInstancesRequestAndFilters(Image image, int number, AmazonEC2 ec2, boolean rotateSubnet) throws IOException { + HashMap> makeRunInstancesRequestAndFilters( + Image image, int number, AmazonEC2 ec2, boolean rotateSubnet) throws IOException { String imageId = image.getImageId(); RunInstancesRequest riRequest = new RunInstancesRequest(imageId, 1, number).withInstanceType(type); riRequest.setEbsOptimized(ebsOptimized); riRequest.setMonitoring(monitoring); - if (t2Unlimited){ + if (t2Unlimited) { CreditSpecificationRequest creditRequest = new CreditSpecificationRequest(); creditRequest.setCpuCredits("unlimited"); riRequest.setCreditSpecification(creditRequest); @@ -998,10 +1823,10 @@ HashMap> makeRunInstancesRequestAndFilters(Ima setupBlockDeviceMappings(image, riRequest.getBlockDeviceMappings()); - if(stopOnTerminate){ + if (stopOnTerminate) { riRequest.setInstanceInitiatedShutdownBehavior(ShutdownBehavior.Stop); logProvisionInfo("Setting Instance Initiated Shutdown Behavior : ShutdownBehavior.Stop"); - }else{ + } else { riRequest.setInstanceInitiatedShutdownBehavior(ShutdownBehavior.Terminate); logProvisionInfo("Setting Instance Initiated Shutdown Behavior : ShutdownBehavior.Terminate"); } @@ -1011,7 +1836,7 @@ HashMap> makeRunInstancesRequestAndFilters(Ima diFilters.add(new Filter("instance-type").withValues(type.toString())); KeyPair keyPair = getKeyPair(ec2); - if (keyPair == null){ + if (keyPair == null) { logProvisionInfo("Could not retrieve a valid key pair."); return null; } @@ -1019,7 +1844,6 @@ HashMap> makeRunInstancesRequestAndFilters(Ima riRequest.setKeyName(keyPair.getKeyName()); diFilters.add(new Filter("key-name").withValues(keyPair.getKeyName())); - if (StringUtils.isNotBlank(getZone())) { Placement placement = new Placement(getZone()); if (getTenancyAttribute().equals(Tenancy.Dedicated)) { @@ -1029,12 +1853,12 @@ HashMap> makeRunInstancesRequestAndFilters(Ima diFilters.add(new Filter("availability-zone").withValues(getZone())); } - if(getTenancyAttribute().equals(Tenancy.Host)){ + if (getTenancyAttribute().equals(Tenancy.Host)) { Placement placement = new Placement(); placement.setTenancy("host"); riRequest.setPlacement(placement); diFilters.add(new Filter("tenancy").withValues(placement.getTenancy())); - }else if(getTenancyAttribute().equals(Tenancy.Default)){ + } else if (getTenancyAttribute().equals(Tenancy.Default)) { Placement placement = new Placement(); placement.setTenancy("default"); riRequest.setPlacement(placement); @@ -1071,10 +1895,10 @@ HashMap> makeRunInstancesRequestAndFilters(Ima } } } else { - List groupIds = getSecurityGroupsBy("group-name", securityGroupSet, ec2) - .getSecurityGroups() - .stream().map(SecurityGroup::getGroupId) - .collect(Collectors.toList()); + List groupIds = + getSecurityGroupsBy("group-name", securityGroupSet, ec2).getSecurityGroups().stream() + .map(SecurityGroup::getGroupId) + .collect(Collectors.toList()); if (getAssociatePublicIp()) { net.setGroups(groupIds); } else { @@ -1111,10 +1935,14 @@ HashMap> makeRunInstancesRequestAndFilters(Ima if (metadataSupported) { InstanceMetadataOptionsRequest instanceMetadataOptionsRequest = new InstanceMetadataOptionsRequest(); - instanceMetadataOptionsRequest.setHttpEndpoint(metadataEndpointEnabled ? InstanceMetadataEndpointState.Enabled.toString() : InstanceMetadataEndpointState.Disabled.toString()); - instanceMetadataOptionsRequest.setHttpPutResponseHopLimit(metadataHopsLimit == null ? EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT : metadataHopsLimit); + instanceMetadataOptionsRequest.setHttpEndpoint( + metadataEndpointEnabled + ? InstanceMetadataEndpointState.Enabled.toString() + : InstanceMetadataEndpointState.Disabled.toString()); + instanceMetadataOptionsRequest.setHttpPutResponseHopLimit( + metadataHopsLimit == null ? EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT : metadataHopsLimit); instanceMetadataOptionsRequest.setHttpTokens( - metadataTokensRequired ? HttpTokensState.Required.toString() : HttpTokensState.Optional.toString()); + metadataTokensRequired ? HttpTokensState.Required.toString() : HttpTokensState.Optional.toString()); riRequest.setMetadataOptions(instanceMetadataOptionsRequest); } @@ -1124,29 +1952,37 @@ HashMap> makeRunInstancesRequestAndFilters(Ima } @Deprecated - HashMap> makeRunInstancesRequestAndFilters(int number, AmazonEC2 ec2, boolean rotateSubnet) throws IOException { + HashMap> makeRunInstancesRequestAndFilters( + int number, AmazonEC2 ec2, boolean rotateSubnet) throws IOException { return makeRunInstancesRequestAndFilters(getImage(), number, ec2, rotateSubnet); } /** * Provisions an On-demand EC2 agent by launching a new instance or starting a previously-stopped instance. */ - private List provisionOndemand(Image image, int number, EnumSet provisionOptions) - throws IOException { + private List provisionOndemand( + Image image, int number, EnumSet provisionOptions) throws IOException { return provisionOndemand(image, number, provisionOptions, false, false); } /** * Provisions an On-demand EC2 agent by launching a new instance or starting a previously-stopped instance. */ - private List provisionOndemand(Image image, int number, EnumSet provisionOptions, boolean spotWithoutBidPrice, boolean fallbackSpotToOndemand) + private List provisionOndemand( + Image image, + int number, + EnumSet provisionOptions, + boolean spotWithoutBidPrice, + boolean fallbackSpotToOndemand) throws IOException { AmazonEC2 ec2 = getParent().connect(); logProvisionInfo("Considering launching"); - HashMap> runInstancesRequestFilterMap = makeRunInstancesRequestAndFilters(image, number, ec2); - Map.Entry> entry = runInstancesRequestFilterMap.entrySet().iterator().next(); + HashMap> runInstancesRequestFilterMap = + makeRunInstancesRequestAndFilters(image, number, ec2); + Map.Entry> entry = + runInstancesRequestFilterMap.entrySet().iterator().next(); RunInstancesRequest riRequest = entry.getKey(); List diFilters = entry.getValue(); @@ -1157,8 +1993,9 @@ private List provisionOndemand(Image image, int number, EnumSe DescribeInstancesResult diResult = ec2.describeInstances(diRequest); List orphansOrStopped = findOrphansOrStopped(diResult, number); - if (orphansOrStopped.isEmpty() && !provisionOptions.contains(ProvisionOptions.FORCE_CREATE) && - !provisionOptions.contains(ProvisionOptions.ALLOW_CREATE)) { + if (orphansOrStopped.isEmpty() + && !provisionOptions.contains(ProvisionOptions.FORCE_CREATE) + && !provisionOptions.contains(ProvisionOptions.ALLOW_CREATE)) { logProvisionInfo("No existing instance found - but cannot create new instance"); return null; } @@ -1173,9 +2010,11 @@ private List provisionOndemand(Image image, int number, EnumSe List newInstances; if (spotWithoutBidPrice) { - InstanceMarketOptionsRequest instanceMarketOptionsRequest = new InstanceMarketOptionsRequest().withMarketType(MarketType.Spot); + InstanceMarketOptionsRequest instanceMarketOptionsRequest = + new InstanceMarketOptionsRequest().withMarketType(MarketType.Spot); if (getSpotBlockReservationDuration() != 0) { - SpotMarketOptions spotOptions = new SpotMarketOptions().withBlockDurationMinutes(getSpotBlockReservationDuration() * 60); + SpotMarketOptions spotOptions = + new SpotMarketOptions().withBlockDurationMinutes(getSpotBlockReservationDuration() * 60); instanceMarketOptionsRequest.setSpotOptions(spotOptions); } riRequest.setInstanceMarketOptions(instanceMarketOptionsRequest); @@ -1183,7 +2022,8 @@ private List provisionOndemand(Image image, int number, EnumSe newInstances = ec2.runInstances(riRequest).getReservation().getInstances(); } catch (AmazonEC2Exception e) { if (fallbackSpotToOndemand && e.getErrorCode().equals("InsufficientInstanceCapacity")) { - logProvisionInfo("There is no spot capacity available matching your request, falling back to on-demand instance."); + logProvisionInfo( + "There is no spot capacity available matching your request, falling back to on-demand instance."); riRequest.setInstanceMarketOptions(new InstanceMarketOptionsRequest()); newInstances = ec2.runInstances(riRequest).getReservation().getInstances(); } else { @@ -1206,14 +2046,15 @@ private List provisionOndemand(Image image, int number, EnumSe void wakeOrphansOrStoppedUp(AmazonEC2 ec2, List orphansOrStopped) { List instances = new ArrayList<>(); - for(Instance instance : orphansOrStopped) { + for (Instance instance : orphansOrStopped) { if (instance.getState().getName().equalsIgnoreCase(InstanceStateName.Stopping.toString()) || instance.getState().getName().equalsIgnoreCase(InstanceStateName.Stopped.toString())) { logProvisionInfo("Found stopped instances - will start it: " + instance); instances.add(instance.getInstanceId()); } else { // Should be pending or running at this point, just let it come up - logProvisionInfo("Found existing pending or running: " + instance.getState().getName() + " instance: " + instance); + logProvisionInfo("Found existing pending or running: " + + instance.getState().getName() + " instance: " + instance); } } @@ -1222,7 +2063,6 @@ void wakeOrphansOrStoppedUp(AmazonEC2 ec2, List orphansOrStopped) { StartInstancesResult siResult = ec2.startInstances(siRequest); logProvisionInfo("Result of starting stopped instances:" + siResult); } - } List toSlaves(List newInstances) throws IOException { @@ -1245,7 +2085,9 @@ List findOrphansOrStopped(DescribeInstancesResult diResult, int number for (Reservation reservation : diResult.getReservations()) { for (Instance instance : reservation.getInstances()) { if (!isSameIamInstanceProfile(instance)) { - logInstanceCheck(instance, ". false - IAM Instance profile does not match: " + instance.getIamInstanceProfile()); + logInstanceCheck( + instance, + ". false - IAM Instance profile does not match: " + instance.getIamInstanceProfile()); continue; } @@ -1301,10 +2143,11 @@ private void setupRootDevice(Image image, List deviceMapping } newMapping.getEbs().setEncrypted(ebsEncryptRootVolume.getValue()); - String message = String.format("EBS default encryption value set to: %s (%s)", ebsEncryptRootVolume.getDisplayText(), ebsEncryptRootVolume.getValue()); + String message = String.format( + "EBS default encryption value set to: %s (%s)", + ebsEncryptRootVolume.getDisplayText(), ebsEncryptRootVolume.getValue()); logProvisionInfo(message); deviceMappings.add(0, newMapping); - } private List getNewEphemeralDeviceMapping(Image image) { @@ -1317,19 +2160,20 @@ private List getNewEphemeralDeviceMapping(Image image) { occupiedDevices.add(mapping.getDeviceName()); } - final List available = new ArrayList<>( - Arrays.asList("ephemeral0", "ephemeral1", "ephemeral2", "ephemeral3")); + final List available = + new ArrayList<>(Arrays.asList("ephemeral0", "ephemeral1", "ephemeral2", "ephemeral3")); final List newDeviceMapping = new ArrayList<>(4); for (char suffix = 'b'; suffix <= 'z' && !available.isEmpty(); suffix++) { final String deviceName = String.format("/dev/xvd%s", suffix); - if (occupiedDevices.contains(deviceName)) + if (occupiedDevices.contains(deviceName)) { continue; + } - final BlockDeviceMapping newMapping = new BlockDeviceMapping().withDeviceName(deviceName).withVirtualName( - available.get(0)); + final BlockDeviceMapping newMapping = + new BlockDeviceMapping().withDeviceName(deviceName).withVirtualName(available.get(0)); newDeviceMapping.add(newMapping); available.remove(0); @@ -1345,15 +2189,13 @@ private void setupEphemeralDeviceMapping(Image image, List d @NonNull private static List makeImageAttributeList(@CheckForNull String attr) { - return Stream.of(Util.tokenize(Util.fixNull(attr))) - .collect(Collectors.toList()); + return Stream.of(Util.tokenize(Util.fixNull(attr))).collect(Collectors.toList()); } @NonNull private DescribeImagesRequest makeDescribeImagesRequest() throws AmazonClientException { - List imageIds = Util.fixEmptyAndTrim(ami) == null ? - Collections.emptyList() : - Collections.singletonList(ami); + List imageIds = + Util.fixEmptyAndTrim(ami) == null ? Collections.emptyList() : Collections.singletonList(ami); List owners = makeImageAttributeList(amiOwners); List users = makeImageAttributeList(amiUsers); List filters = EC2Filter.toFilterList(amiFilters); @@ -1361,17 +2203,16 @@ private DescribeImagesRequest makeDescribeImagesRequest() throws AmazonClientExc // Raise an exception if there were no search attributes. // This is legal but not what anyone wants - it will // launch random recently created public AMIs. - int numAttrs = Stream.of(imageIds, owners, users, filters) - .collect(Collectors.summingInt(List::size)); + int numAttrs = Stream.of(imageIds, owners, users, filters).collect(Collectors.summingInt(List::size)); if (numAttrs == 0) { throw new AmazonClientException("Neither AMI ID nor AMI search attributes provided"); } return new DescribeImagesRequest() - .withImageIds(imageIds) - .withOwners(owners) - .withExecutableUsers(users) - .withFilters(filters); + .withImageIds(imageIds) + .withOwners(owners) + .withExecutableUsers(users) + .withFilters(filters); } @NonNull @@ -1389,7 +2230,6 @@ private Image getImage() throws AmazonClientException { return images.get(0); } - private void setupCustomDeviceMapping(List deviceMappings) { if (StringUtils.isNotBlank(customDeviceMapping)) { deviceMappings.addAll(DeviceMappingParser.parse(customDeviceMapping)); @@ -1452,10 +2292,10 @@ private List provisionSpot(Image image, int number, EnumSet groupIds = getSecurityGroupsBy("group-name", securityGroupSet, ec2) - .getSecurityGroups() - .stream().map(SecurityGroup::getGroupId) - .collect(Collectors.toList()); + List groupIds = + getSecurityGroupsBy("group-name", securityGroupSet, ec2).getSecurityGroups().stream() + .map(SecurityGroup::getGroupId) + .collect(Collectors.toList()); net.setGroups(groupIds); } } @@ -1473,7 +2313,8 @@ private List provisionSpot(Image image, int number, EnumSet instTags = buildTags(EC2Cloud.EC2_SLAVE_TYPE_SPOT); if (StringUtils.isNotBlank(getIamInstanceProfile())) { - launchSpecification.setIamInstanceProfile(new IamInstanceProfileSpecification().withArn(getIamInstanceProfile())); + launchSpecification.setIamInstanceProfile( + new IamInstanceProfileSpecification().withArn(getIamInstanceProfile())); } setupBlockDeviceMappings(image, launchSpecification.getBlockDeviceMappings()); @@ -1490,7 +2331,8 @@ private List provisionSpot(Image image, int number, EnumSet provisionSpot(Image image, int number, EnumSet slaves = new ArrayList<>(reqInstances.size()); - for(SpotInstanceRequest spotInstReq : reqInstances) { + for (SpotInstanceRequest spotInstReq : reqInstances) { if (spotInstReq == null) { throw new AmazonClientException("Spot instance request is null"); } @@ -1514,22 +2356,31 @@ private List provisionSpot(Image image, int number, EnumSet spotRequestBadCodes = Arrays.asList("capacity-not-available", "capacity-oversubscribed", "price-too-low"); + List spotRequestBadCodes = + Arrays.asList("capacity-not-available", "capacity-oversubscribed", "price-too-low"); if (spotRequestBadCodes.contains(spotInstReq.getStatus().getCode())) { - LOGGER.info("There is no spot capacity available matching your request, falling back to on-demand instance."); - List requestsToCancel = reqInstances.stream().map(SpotInstanceRequest::getSpotInstanceRequestId).collect(Collectors.toList()); - CancelSpotInstanceRequestsRequest cancelRequest = new CancelSpotInstanceRequestsRequest(requestsToCancel); + LOGGER.info( + "There is no spot capacity available matching your request, falling back to on-demand instance."); + List requestsToCancel = reqInstances.stream() + .map(SpotInstanceRequest::getSpotInstanceRequestId) + .collect(Collectors.toList()); + CancelSpotInstanceRequestsRequest cancelRequest = + new CancelSpotInstanceRequestsRequest(requestsToCancel); ec2.cancelSpotInstanceRequests(cancelRequest); return provisionOndemand(image, number, provisionOptions); } } // Now that we have our Spot request, we can set tags on it - updateRemoteTags(ec2, instTags, "InvalidSpotInstanceRequestID.NotFound", spotInstReq.getSpotInstanceRequestId()); + updateRemoteTags( + ec2, instTags, "InvalidSpotInstanceRequestID.NotFound", spotInstReq.getSpotInstanceRequestId()); // That was a remote request - we should also update our local instance data spotInstReq.setTags(instTags); @@ -1543,7 +2394,7 @@ private List provisionSpot(Image image, int number, EnumSet buildTags(String slaveType) { } } if (!hasCustomTypeTag) { - instTags.add(new Tag(EC2Tag.TAG_NAME_JENKINS_SLAVE_TYPE, EC2Cloud.getSlaveTypeTagValue( - slaveType, description))); + instTags.add( + new Tag(EC2Tag.TAG_NAME_JENKINS_SLAVE_TYPE, EC2Cloud.getSlaveTypeTagValue(slaveType, description))); } JenkinsLocationConfiguration jenkinsLocation = JenkinsLocationConfiguration.get(); if (!hasJenkinsServerUrlTag && jenkinsLocation.getUrl() != null) { @@ -1587,61 +2438,61 @@ private HashSet buildTags(String slaveType) { protected EC2OndemandSlave newOndemandSlave(Instance inst) throws FormException, IOException { EC2AgentConfig.OnDemand config = new EC2AgentConfig.OnDemandBuilder() - .withName(getSlaveName(inst.getInstanceId())) - .withInstanceId(inst.getInstanceId()) - .withDescription(description) - .withRemoteFS(remoteFS) - .withNumExecutors(getNumExecutors()) - .withLabelString(labels) - .withMode(mode) - .withInitScript(initScript) - .withTmpDir(tmpDir) - .withNodeProperties(nodeProperties.toList()) - .withRemoteAdmin(remoteAdmin) - .withJavaPath(javaPath) - .withJvmopts(jvmopts) - .withStopOnTerminate(stopOnTerminate) - .withIdleTerminationMinutes(idleTerminationMinutes) - .withPublicDNS(inst.getPublicDnsName()) - .withPrivateDNS(inst.getPrivateDnsName()) - .withTags(EC2Tag.fromAmazonTags(inst.getTags())) - .withCloudName(parent.name) - .withLaunchTimeout(getLaunchTimeout()) - .withAmiType(amiType) - .withConnectionStrategy(connectionStrategy) - .withMaxTotalUses(maxTotalUses) - .withTenancyAttribute(tenancy) - .withMetadataSupported(metadataSupported) - .withMetadataEndpointEnabled(metadataEndpointEnabled) - .withMetadataTokensRequired(metadataTokensRequired) - .withMetadataHopsLimit(metadataHopsLimit) - .build(); + .withName(getSlaveName(inst.getInstanceId())) + .withInstanceId(inst.getInstanceId()) + .withDescription(description) + .withRemoteFS(remoteFS) + .withNumExecutors(getNumExecutors()) + .withLabelString(labels) + .withMode(mode) + .withInitScript(initScript) + .withTmpDir(tmpDir) + .withNodeProperties(nodeProperties.toList()) + .withRemoteAdmin(remoteAdmin) + .withJavaPath(javaPath) + .withJvmopts(jvmopts) + .withStopOnTerminate(stopOnTerminate) + .withIdleTerminationMinutes(idleTerminationMinutes) + .withPublicDNS(inst.getPublicDnsName()) + .withPrivateDNS(inst.getPrivateDnsName()) + .withTags(EC2Tag.fromAmazonTags(inst.getTags())) + .withCloudName(parent.name) + .withLaunchTimeout(getLaunchTimeout()) + .withAmiType(amiType) + .withConnectionStrategy(connectionStrategy) + .withMaxTotalUses(maxTotalUses) + .withTenancyAttribute(tenancy) + .withMetadataSupported(metadataSupported) + .withMetadataEndpointEnabled(metadataEndpointEnabled) + .withMetadataTokensRequired(metadataTokensRequired) + .withMetadataHopsLimit(metadataHopsLimit) + .build(); return EC2AgentFactory.getInstance().createOnDemandAgent(config); } protected EC2SpotSlave newSpotSlave(SpotInstanceRequest sir) throws FormException, IOException { EC2AgentConfig.Spot config = new EC2AgentConfig.SpotBuilder() - .withName(getSlaveName(sir.getSpotInstanceRequestId())) - .withSpotInstanceRequestId(sir.getSpotInstanceRequestId()) - .withDescription(description) - .withRemoteFS(remoteFS) - .withNumExecutors(getNumExecutors()) - .withMode(mode) - .withInitScript(initScript) - .withTmpDir(tmpDir) - .withLabelString(labels) - .withNodeProperties(nodeProperties.toList()) - .withRemoteAdmin(remoteAdmin) - .withJavaPath(javaPath) - .withJvmopts(jvmopts) - .withIdleTerminationMinutes(idleTerminationMinutes) - .withTags(EC2Tag.fromAmazonTags(sir.getTags())) - .withCloudName(parent.name) - .withLaunchTimeout(getLaunchTimeout()) - .withAmiType(amiType) - .withConnectionStrategy(connectionStrategy) - .withMaxTotalUses(maxTotalUses) - .build(); + .withName(getSlaveName(sir.getSpotInstanceRequestId())) + .withSpotInstanceRequestId(sir.getSpotInstanceRequestId()) + .withDescription(description) + .withRemoteFS(remoteFS) + .withNumExecutors(getNumExecutors()) + .withMode(mode) + .withInitScript(initScript) + .withTmpDir(tmpDir) + .withLabelString(labels) + .withNodeProperties(nodeProperties.toList()) + .withRemoteAdmin(remoteAdmin) + .withJavaPath(javaPath) + .withJvmopts(jvmopts) + .withIdleTerminationMinutes(idleTerminationMinutes) + .withTags(EC2Tag.fromAmazonTags(sir.getTags())) + .withCloudName(parent.name) + .withLaunchTimeout(getLaunchTimeout()) + .withAmiType(amiType) + .withConnectionStrategy(connectionStrategy) + .withMaxTotalUses(maxTotalUses) + .build(); return EC2AgentFactory.getInstance().createSpotAgent(config); } @@ -1652,7 +2503,8 @@ protected EC2SpotSlave newSpotSlave(SpotInstanceRequest sir) throws FormExceptio private KeyPair getKeyPair(AmazonEC2 ec2) throws IOException, AmazonClientException { EC2PrivateKey ec2PrivateKey = getParent().resolvePrivateKey(); if (ec2PrivateKey == null) { - throw new AmazonClientException("No keypair credential found. Please configure a credential in the Jenkins configuration."); + throw new AmazonClientException( + "No keypair credential found. Please configure a credential in the Jenkins configuration."); } KeyPair keyPair = ec2PrivateKey.find(ec2); if (keyPair == null) { @@ -1694,8 +2546,11 @@ private void updateRemoteTags(AmazonEC2 ec2, Collection instTags, String ca * Get a list of security group ids for the agent */ private List getEc2SecurityGroups(AmazonEC2 ec2) throws AmazonClientException { - LOGGER.log(Level.FINE, () -> String.format("Get security group %s for EC2Cloud %s with currentSubnetId %s", - securityGroupSet, this.getParent().name, getCurrentSubnetId())); + LOGGER.log( + Level.FINE, + () -> String.format( + "Get security group %s for EC2Cloud %s with currentSubnetId %s", + securityGroupSet, this.getParent().name, getCurrentSubnetId())); List groupIds = new ArrayList<>(); DescribeSecurityGroupsResult groupResult = getSecurityGroupsBy("group-name", securityGroupSet, ec2); if (groupResult.getSecurityGroups().size() == 0) { @@ -1703,8 +2558,11 @@ private List getEc2SecurityGroups(AmazonEC2 ec2) throws AmazonClientExce } for (SecurityGroup group : groupResult.getSecurityGroups()) { - LOGGER.log(Level.FINE, () -> String.format("Checking security group %s (vpc-id = %s, subnet-id = %s)", - group.getGroupId(), group.getVpcId(), getCurrentSubnetId())); + LOGGER.log( + Level.FINE, + () -> String.format( + "Checking security group %s (vpc-id = %s, subnet-id = %s)", + group.getGroupId(), group.getVpcId(), getCurrentSubnetId())); if (group.getVpcId() != null && !group.getVpcId().isEmpty()) { List filters = new ArrayList<>(); filters.add(new Filter("vpc-id").withValues(group.getVpcId())); @@ -1730,7 +2588,8 @@ private List getEc2SecurityGroups(AmazonEC2 ec2) throws AmazonClientExce return groupIds; } - private DescribeSecurityGroupsResult getSecurityGroupsBy(String filterName, Set filterValues, AmazonEC2 ec2) { + private DescribeSecurityGroupsResult getSecurityGroupsBy( + String filterName, Set filterValues, AmazonEC2 ec2) { DescribeSecurityGroupsRequest groupReq = new DescribeSecurityGroupsRequest(); groupReq.withFilters(new Filter(filterName).withValues(filterValues)); return ec2.describeSecurityGroups(groupReq); @@ -1748,11 +2607,15 @@ public EC2AbstractSlave attach(String instanceId, TaskListener listener) throws LOGGER.info("Attaching to " + instanceId); DescribeInstancesRequest request = new DescribeInstancesRequest(); request.setInstanceIds(Collections.singletonList(instanceId)); - Instance inst = ec2.describeInstances(request).getReservations().get(0).getInstances().get(0); + Instance inst = ec2.describeInstances(request) + .getReservations() + .get(0) + .getInstances() + .get(0); return newOndemandSlave(inst); } catch (FormException e) { throw new AssertionError(); // we should have discovered all - // configuration issues upfront + // configuration issues upfront } } @@ -1761,9 +2624,9 @@ public EC2AbstractSlave attach(String instanceId, TaskListener listener) throws */ protected Object readResolve() { Jenkins j = Jenkins.getInstanceOrNull(); - if (j != null) { - j.checkPermission(Jenkins.ADMINISTER); - } + if (j != null) { + j.checkPermission(Jenkins.ADMINISTER); + } securityGroupSet = parseSecurityGroups(); @@ -1783,9 +2646,10 @@ protected Object readResolve() { amiType = new UnixData(rootCommandPrefix, slaveCommandPrefix, slaveCommandSuffix, sshPort, null); } - // 1.43 new parameters - if (connectionStrategy == null ) { - connectionStrategy = ConnectionStrategy.backwardsCompatible(usePrivateDnsName, connectUsingPublicIp, associatePublicIp); + // 1.43 new parameters + if (connectionStrategy == null) { + connectionStrategy = + ConnectionStrategy.backwardsCompatible(usePrivateDnsName, connectUsingPublicIp, associatePublicIp); } if (maxTotalUses == 0) { @@ -1828,6 +2692,7 @@ protected Object readResolve() { return this; } + @Override public Descriptor getDescriptor() { return Jenkins.get().getDescriptor(getClass()); } @@ -1871,8 +2736,10 @@ public boolean isUseHTTPS() { * @return DescribeInstancesResult of DescribeInstanceRequst constructed from this SlaveTemplate's configs */ DescribeInstancesResult getDescribeInstanceResult(AmazonEC2 ec2, boolean allSubnets) throws IOException { - HashMap> runInstancesRequestFilterMap = makeRunInstancesRequestAndFilters(getImage(), 1, ec2, false); - Map.Entry> entry = runInstancesRequestFilterMap.entrySet().iterator().next(); + HashMap> runInstancesRequestFilterMap = + makeRunInstancesRequestAndFilters(getImage(), 1, ec2, false); + Map.Entry> entry = + runInstancesRequestFilterMap.entrySet().iterator().next(); List diFilters = entry.getValue(); if (allSubnets) { @@ -1929,17 +2796,20 @@ public List> getAMITypeDescriptors() { @Override public String getHelpFile(String fieldName) { String p = super.getHelpFile(fieldName); - if (p != null) + if (p != null) { return p; + } Descriptor slaveDescriptor = Jenkins.get().getDescriptor(EC2OndemandSlave.class); if (slaveDescriptor != null) { p = slaveDescriptor.getHelpFile(fieldName); - if (p != null) + if (p != null) { return p; + } } slaveDescriptor = Jenkins.get().getDescriptor(EC2SpotSlave.class); - if (slaveDescriptor != null) + if (slaveDescriptor != null) { return slaveDescriptor.getHelpFile(fieldName); + } return null; } @@ -1958,15 +2828,22 @@ public FormValidation doCheckDescription(@QueryParameter String value) { * Check that the AMI requested is available in the cloud and can be used. */ @RequirePOST - public FormValidation doValidateAmi(@QueryParameter boolean useInstanceProfileForCredentials, - @QueryParameter String credentialsId, @QueryParameter String ec2endpoint, - @QueryParameter String region, final @QueryParameter String ami, @QueryParameter String roleArn, - @QueryParameter String roleSessionName) throws IOException { + public FormValidation doValidateAmi( + @QueryParameter boolean useInstanceProfileForCredentials, + @QueryParameter String credentialsId, + @QueryParameter String ec2endpoint, + @QueryParameter String region, + final @QueryParameter String ami, + @QueryParameter String roleArn, + @QueryParameter String roleSessionName) + throws IOException { checkPermission(EC2Cloud.PROVISION); - AWSCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider(useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); + AWSCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider( + useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); AmazonEC2 ec2; if (region != null) { - ec2 = AmazonEC2Factory.getInstance().connect(credentialsProvider, AmazonEC2Cloud.getEc2EndpointUrl(region)); + ec2 = AmazonEC2Factory.getInstance() + .connect(credentialsProvider, AmazonEC2Cloud.getEc2EndpointUrl(region)); } else { ec2 = AmazonEC2Factory.getInstance().connect(credentialsProvider, new URL(ec2endpoint)); } @@ -2003,12 +2880,14 @@ public FormValidation doCheckLabelString(@QueryParameter String value, @QueryPar @POST public FormValidation doCheckIdleTerminationMinutes(@QueryParameter String value) { - if (value == null || value.trim().isEmpty()) + if (value == null || value.trim().isEmpty()) { return FormValidation.ok(); + } try { int val = Integer.parseInt(value); - if (val >= -59) + if (val >= -59) { return FormValidation.ok(); + } } catch (NumberFormatException nfe) { } return FormValidation.error("Idle Termination time must be a greater than -59 (or null)"); @@ -2018,17 +2897,20 @@ public FormValidation doCheckIdleTerminationMinutes(@QueryParameter String value public FormValidation doCheckMaxTotalUses(@QueryParameter String value) { try { int val = Integer.parseInt(value); - if (val >= -1) + if (val >= -1) { return FormValidation.ok(); + } } catch (NumberFormatException nfe) { } return FormValidation.error("Maximum Total Uses must be greater or equal to -1"); } @POST - public FormValidation doCheckMinimumNumberOfInstances(@QueryParameter String value, @QueryParameter String instanceCapStr) { - if (value == null || value.trim().isEmpty()) + public FormValidation doCheckMinimumNumberOfInstances( + @QueryParameter String value, @QueryParameter String instanceCapStr) { + if (value == null || value.trim().isEmpty()) { return FormValidation.ok(); + } try { int val = Integer.parseInt(value); if (val >= 0) { @@ -2039,9 +2921,8 @@ public FormValidation doCheckMinimumNumberOfInstances(@QueryParameter String val instanceCap = Integer.MAX_VALUE; } if (val > instanceCap) { - return FormValidation - .error("Minimum number of instances must not be larger than AMI Instance Cap %d", - instanceCap); + return FormValidation.error( + "Minimum number of instances must not be larger than AMI Instance Cap %d", instanceCap); } return FormValidation.ok(); } @@ -2072,23 +2953,27 @@ public FormValidation doCheckMinimumNoInstancesActiveTimeRangeTo(@QueryParameter // For some reason, all days will validate against this method so no need to repeat for each day. @POST - public FormValidation doCheckMonday(@QueryParameter boolean monday, - @QueryParameter boolean tuesday, - @QueryParameter boolean wednesday, - @QueryParameter boolean thursday, - @QueryParameter boolean friday, - @QueryParameter boolean saturday, - @QueryParameter boolean sunday) { + public FormValidation doCheckMonday( + @QueryParameter boolean monday, + @QueryParameter boolean tuesday, + @QueryParameter boolean wednesday, + @QueryParameter boolean thursday, + @QueryParameter boolean friday, + @QueryParameter boolean saturday, + @QueryParameter boolean sunday) { if (!(monday || tuesday || wednesday || thursday || friday || saturday || sunday)) { - return FormValidation.warning("At least one day should be checked or minimum number of instances won't be active"); + return FormValidation.warning( + "At least one day should be checked or minimum number of instances won't be active"); } return FormValidation.ok(); } @POST - public FormValidation doCheckMinimumNumberOfSpareInstances(@QueryParameter String value, @QueryParameter String instanceCapStr) { - if (value == null || value.trim().isEmpty()) + public FormValidation doCheckMinimumNumberOfSpareInstances( + @QueryParameter String value, @QueryParameter String instanceCapStr) { + if (value == null || value.trim().isEmpty()) { return FormValidation.ok(); + } try { int val = Integer.parseInt(value); if (val >= 0) { @@ -2099,9 +2984,9 @@ public FormValidation doCheckMinimumNumberOfSpareInstances(@QueryParameter Strin instanceCap = Integer.MAX_VALUE; } if (val > instanceCap) { - return FormValidation - .error("Minimum number of spare instances must not be larger than AMI Instance Cap %d", - instanceCap); + return FormValidation.error( + "Minimum number of spare instances must not be larger than AMI Instance Cap %d", + instanceCap); } return FormValidation.ok(); } @@ -2112,12 +2997,14 @@ public FormValidation doCheckMinimumNumberOfSpareInstances(@QueryParameter Strin @POST public FormValidation doCheckInstanceCapStr(@QueryParameter String value) { - if (value == null || value.trim().isEmpty()) + if (value == null || value.trim().isEmpty()) { return FormValidation.ok(); + } try { int val = Integer.parseInt(value); - if (val > 0) + if (val > 0) { return FormValidation.ok(); + } } catch (NumberFormatException nfe) { } return FormValidation.error("InstanceCap must be a non-negative integer (or null)"); @@ -2128,12 +3015,14 @@ public FormValidation doCheckInstanceCapStr(@QueryParameter String value) { */ @POST public FormValidation doCheckSpotBlockReservationDurationStr(@QueryParameter String value) { - if (value == null || value.trim().isEmpty()) + if (value == null || value.trim().isEmpty()) { return FormValidation.ok(); + } try { int val = Integer.parseInt(value); - if (val >= 0 && val <= 6) + if (val >= 0 && val <= 6) { return FormValidation.ok(); + } } catch (NumberFormatException nfe) { } return FormValidation.error("Spot Block Reservation Duration must be an integer between 0 & 6"); @@ -2141,24 +3030,30 @@ public FormValidation doCheckSpotBlockReservationDurationStr(@QueryParameter Str @POST public FormValidation doCheckLaunchTimeoutStr(@QueryParameter String value) { - if (value == null || value.trim().isEmpty()) + if (value == null || value.trim().isEmpty()) { return FormValidation.ok(); + } try { int val = Integer.parseInt(value); - if (val >= 0) + if (val >= 0) { return FormValidation.ok(); + } } catch (NumberFormatException nfe) { } return FormValidation.error("Launch Timeout must be a non-negative integer (or null)"); } @RequirePOST - public ListBoxModel doFillZoneItems(@QueryParameter boolean useInstanceProfileForCredentials, - @QueryParameter String credentialsId, @QueryParameter String region, @QueryParameter String roleArn, + public ListBoxModel doFillZoneItems( + @QueryParameter boolean useInstanceProfileForCredentials, + @QueryParameter String credentialsId, + @QueryParameter String region, + @QueryParameter String roleArn, @QueryParameter String roleSessionName) throws IOException, ServletException { checkPermission(EC2Cloud.PROVISION); - AWSCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider(useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); + AWSCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider( + useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); return EC2AbstractSlave.fillZoneItems(credentialsProvider, region); } @@ -2229,10 +3124,12 @@ public ListBoxModel doFillHostKeyVerificationStrategyItems(@QueryParameter Strin @POST public FormValidation doCheckHostKeyVerificationStrategy(@QueryParameter String hostKeyVerificationStrategy) { Stream stream = Stream.of(HostKeyVerificationStrategyEnum.values()); - Stream filteredStream = stream.filter(v -> v.name().equals(hostKeyVerificationStrategy)); + Stream filteredStream = + stream.filter(v -> v.name().equals(hostKeyVerificationStrategy)); Optional matched = filteredStream.findFirst(); Optional okResult = matched.map(s -> FormValidation.ok()); - return okResult.orElse(FormValidation.error(String.format("Could not find selected host key verification (%s)", hostKeyVerificationStrategy))); + return okResult.orElse(FormValidation.error( + String.format("Could not find selected host key verification (%s)", hostKeyVerificationStrategy))); } @POST @@ -2247,12 +3144,13 @@ public ListBoxModel doFillTenancyItems(@QueryParameter String tenancy) { }) .collect(Collectors.toCollection(ListBoxModel::new)); } + public String getDefaultEbsEncryptRootVolume() { return EbsEncryptRootVolume.DEFAULT.getDisplayText(); } @POST - public ListBoxModel doFillEbsEncryptRootVolumeItems(@QueryParameter String ebsEncryptRootVolume ) { + public ListBoxModel doFillEbsEncryptRootVolumeItems(@QueryParameter String ebsEncryptRootVolume) { return Stream.of(EbsEncryptRootVolume.values()) .map(v -> { if (v.name().equals(ebsEncryptRootVolume)) { @@ -2267,10 +3165,12 @@ public ListBoxModel doFillEbsEncryptRootVolumeItems(@QueryParameter String ebsEn @POST public FormValidation doEbsEncryptRootVolume(@QueryParameter String ebsEncryptRootVolume) { Stream stream = Stream.of(EbsEncryptRootVolume.values()); - Stream filteredStream = stream.filter(v -> v.name().equals(ebsEncryptRootVolume)); + Stream filteredStream = + stream.filter(v -> v.name().equals(ebsEncryptRootVolume)); Optional matched = filteredStream.findFirst(); Optional okResult = matched.map(s -> FormValidation.ok()); - return okResult.orElse(FormValidation.error(String.format("Could not find selected option (%s)", ebsEncryptRootVolume))); + return okResult.orElse( + FormValidation.error(String.format("Could not find selected option (%s)", ebsEncryptRootVolume))); } } } diff --git a/src/main/java/hudson/plugins/ec2/SpotConfiguration.java b/src/main/java/hudson/plugins/ec2/SpotConfiguration.java index 5b278d45f..5a1a22ba0 100644 --- a/src/main/java/hudson/plugins/ec2/SpotConfiguration.java +++ b/src/main/java/hudson/plugins/ec2/SpotConfiguration.java @@ -1,5 +1,7 @@ package hudson.plugins.ec2; +import static hudson.Functions.checkPermission; + import com.amazonaws.AmazonServiceException; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.services.ec2.AmazonEC2; @@ -24,16 +26,18 @@ import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.interceptor.RequirePOST; -import static hudson.Functions.checkPermission; - -public final class SpotConfiguration extends AbstractDescribableImpl { +public final class SpotConfiguration extends AbstractDescribableImpl { public final boolean useBidPrice; private String spotMaxBidPrice; private boolean fallbackToOndemand; private int spotBlockReservationDuration; @Deprecated - public SpotConfiguration(boolean useBidPrice, String spotMaxBidPrice, boolean fallbackToOndemand, String spotBlockReservationDurationStr) { + public SpotConfiguration( + boolean useBidPrice, + String spotMaxBidPrice, + boolean fallbackToOndemand, + String spotBlockReservationDurationStr) { this.useBidPrice = useBidPrice; this.spotMaxBidPrice = spotMaxBidPrice; this.fallbackToOndemand = fallbackToOndemand; @@ -77,7 +81,8 @@ public void setSpotBlockReservationDuration(int spotBlockReservationDuration) { this.spotBlockReservationDuration = spotBlockReservationDuration; } - @Override public boolean equals(Object obj) { + @Override + public boolean equals(Object obj) { if (obj == null || (this.getClass() != obj.getClass())) { return false; } @@ -92,11 +97,14 @@ public void setSpotBlockReservationDuration(int spotBlockReservationDuration) { blockReservationIsEqual = false; } - return this.useBidPrice == config.useBidPrice && this.fallbackToOndemand == config.fallbackToOndemand - && normalizedBidsAreEqual && blockReservationIsEqual; + return this.useBidPrice == config.useBidPrice + && this.fallbackToOndemand == config.fallbackToOndemand + && normalizedBidsAreEqual + && blockReservationIsEqual; } - @Override public int hashCode() { + @Override + public int hashCode() { return Objects.hash(useBidPrice, spotMaxBidPrice, fallbackToOndemand); } @@ -119,7 +127,6 @@ public static String normalizeBid(String bid) { } catch (NumberFormatException ex) { return null; } - } @Extension @@ -133,10 +140,16 @@ public String getDisplayName() { * Check the current Spot price of the selected instance type for the selected region */ @RequirePOST - public FormValidation doCurrentSpotPrice(@QueryParameter boolean useInstanceProfileForCredentials, - @QueryParameter String credentialsId, @QueryParameter String region, - @QueryParameter String type, @QueryParameter String zone, @QueryParameter String roleArn, - @QueryParameter String roleSessionName, @QueryParameter String ami) throws IOException, ServletException { + public FormValidation doCurrentSpotPrice( + @QueryParameter boolean useInstanceProfileForCredentials, + @QueryParameter String credentialsId, + @QueryParameter String region, + @QueryParameter String type, + @QueryParameter String zone, + @QueryParameter String roleArn, + @QueryParameter String roleSessionName, + @QueryParameter String ami) + throws IOException, ServletException { checkPermission(EC2Cloud.PROVISION); @@ -145,8 +158,10 @@ public FormValidation doCurrentSpotPrice(@QueryParameter boolean useInstanceProf // Connect to the EC2 cloud with the access id, secret key, and // region queried from the created cloud - AWSCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider(useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); - AmazonEC2 ec2 = AmazonEC2Factory.getInstance().connect(credentialsProvider, AmazonEC2Cloud.getEc2EndpointUrl(region)); + AWSCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider( + useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); + AmazonEC2 ec2 = AmazonEC2Factory.getInstance() + .connect(credentialsProvider, AmazonEC2Cloud.getEc2EndpointUrl(region)); if (ec2 != null) { diff --git a/src/main/java/hudson/plugins/ec2/Tenancy.java b/src/main/java/hudson/plugins/ec2/Tenancy.java index 030d2b7dc..93b995f5e 100644 --- a/src/main/java/hudson/plugins/ec2/Tenancy.java +++ b/src/main/java/hudson/plugins/ec2/Tenancy.java @@ -11,6 +11,7 @@ private Tenancy(String value) { this.value = value; } + @Override public String toString() { return this.value; } @@ -20,7 +21,7 @@ public static Tenancy fromValue(String value) { Tenancy[] var1 = values(); int var2 = var1.length; - for(int var3 = 0; var3 < var2; ++var3) { + for (int var3 = 0; var3 < var2; ++var3) { Tenancy enumEntry = var1[var3]; if (enumEntry.toString().equals(value)) { return enumEntry; @@ -46,4 +47,3 @@ public static Tenancy backwardsCompatible(boolean useDedicatedTenancy) { } } } - diff --git a/src/main/java/hudson/plugins/ec2/UnixData.java b/src/main/java/hudson/plugins/ec2/UnixData.java index dba888018..36bd8575b 100644 --- a/src/main/java/hudson/plugins/ec2/UnixData.java +++ b/src/main/java/hudson/plugins/ec2/UnixData.java @@ -14,7 +14,12 @@ public class UnixData extends AMITypeData { private final String bootDelay; @DataBoundConstructor - public UnixData(String rootCommandPrefix, String slaveCommandPrefix, String slaveCommandSuffix, String sshPort, String bootDelay) { + public UnixData( + String rootCommandPrefix, + String slaveCommandPrefix, + String slaveCommandSuffix, + String sshPort, + String bootDelay) { this.rootCommandPrefix = rootCommandPrefix; this.slaveCommandPrefix = slaveCommandPrefix; this.slaveCommandSuffix = slaveCommandSuffix; @@ -71,6 +76,7 @@ public String getSshPort() { return sshPort == null || sshPort.isEmpty() ? "22" : sshPort; } + @Override public String getBootDelay() { return bootDelay; } @@ -89,38 +95,51 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) + if (this == obj) { return true; - if (obj == null) + } + if (obj == null) { return false; - if (this.getClass() != obj.getClass()) + } + if (this.getClass() != obj.getClass()) { return false; + } final UnixData other = (UnixData) obj; if (StringUtils.isEmpty(rootCommandPrefix)) { - if (!StringUtils.isEmpty(other.rootCommandPrefix)) + if (!StringUtils.isEmpty(other.rootCommandPrefix)) { return false; - } else if (!rootCommandPrefix.equals(other.rootCommandPrefix)) + } + } else if (!rootCommandPrefix.equals(other.rootCommandPrefix)) { return false; + } if (StringUtils.isEmpty(slaveCommandPrefix)) { - if (!StringUtils.isEmpty(other.slaveCommandPrefix)) + if (!StringUtils.isEmpty(other.slaveCommandPrefix)) { return false; - } else if (!slaveCommandPrefix.equals(other.slaveCommandPrefix)) + } + } else if (!slaveCommandPrefix.equals(other.slaveCommandPrefix)) { return false; + } if (StringUtils.isEmpty(slaveCommandSuffix)) { - if (!StringUtils.isEmpty(other.slaveCommandSuffix)) + if (!StringUtils.isEmpty(other.slaveCommandSuffix)) { return false; - } else if (!slaveCommandSuffix.equals(other.slaveCommandSuffix)) + } + } else if (!slaveCommandSuffix.equals(other.slaveCommandSuffix)) { return false; + } if (StringUtils.isEmpty(sshPort)) { - if (!StringUtils.isEmpty(other.sshPort)) + if (!StringUtils.isEmpty(other.sshPort)) { return false; - } else if (!sshPort.equals(other.sshPort)) + } + } else if (!sshPort.equals(other.sshPort)) { return false; + } if (bootDelay == null) { - if (other.bootDelay != null) + if (other.bootDelay != null) { return false; - } else if (!bootDelay.equals(other.bootDelay)) + } + } else if (!bootDelay.equals(other.bootDelay)) { return false; + } return true; } } diff --git a/src/main/java/hudson/plugins/ec2/WindowsData.java b/src/main/java/hudson/plugins/ec2/WindowsData.java index 4eba6b25f..fbea948e6 100644 --- a/src/main/java/hudson/plugins/ec2/WindowsData.java +++ b/src/main/java/hudson/plugins/ec2/WindowsData.java @@ -1,12 +1,10 @@ package hudson.plugins.ec2; -import java.util.concurrent.TimeUnit; -import java.util.Objects; - import hudson.Extension; import hudson.model.Descriptor; - import hudson.util.Secret; +import java.util.Objects; +import java.util.concurrent.TimeUnit; import org.kohsuke.stapler.DataBoundConstructor; public class WindowsData extends AMITypeData { @@ -15,14 +13,20 @@ public class WindowsData extends AMITypeData { private final boolean useHTTPS; private final String bootDelay; private final boolean specifyPassword; - private final Boolean allowSelfSignedCertificate; //Boolean to allow nulls when the saved template doesn't have the field + private final Boolean + allowSelfSignedCertificate; // Boolean to allow nulls when the saved template doesn't have the field @DataBoundConstructor - public WindowsData(String password, boolean useHTTPS, String bootDelay, boolean specifyPassword, boolean allowSelfSignedCertificate) { + public WindowsData( + String password, + boolean useHTTPS, + String bootDelay, + boolean specifyPassword, + boolean allowSelfSignedCertificate) { this.password = Secret.fromString(password); this.useHTTPS = useHTTPS; this.bootDelay = bootDelay; - //Backwards compatibility + // Backwards compatibility if (!specifyPassword && !this.password.getPlainText().isEmpty()) { specifyPassword = true; } @@ -30,9 +34,9 @@ public WindowsData(String password, boolean useHTTPS, String bootDelay, boolean this.allowSelfSignedCertificate = allowSelfSignedCertificate; } - + @Deprecated - public WindowsData(String password, boolean useHTTPS, String bootDelay, boolean specifyPassword) { + public WindowsData(String password, boolean useHTTPS, String bootDelay, boolean specifyPassword) { this(password, useHTTPS, bootDelay, specifyPassword, true); } @@ -63,6 +67,7 @@ public boolean isUseHTTPS() { return useHTTPS; } + @Override public String getBootDelay() { return bootDelay; } @@ -71,6 +76,7 @@ public boolean isSpecifyPassword() { return specifyPassword; } + @Override public int getBootDelayInMillis() { try { return (int) TimeUnit.SECONDS.toMillis(Integer.parseInt(bootDelay)); @@ -79,10 +85,10 @@ public int getBootDelayInMillis() { } } - public boolean isAllowSelfSignedCertificate(){ + public boolean isAllowSelfSignedCertificate() { return allowSelfSignedCertificate == null || allowSelfSignedCertificate; } - + @Extension public static class DescriptorImpl extends Descriptor { @Override @@ -93,33 +99,42 @@ public String getDisplayName() { @Override public int hashCode() { - return Objects.hash(password,useHTTPS, bootDelay, specifyPassword); + return Objects.hash(password, useHTTPS, bootDelay, specifyPassword); } @Override public boolean equals(Object obj) { - if (this == obj) + if (this == obj) { return true; - if (obj == null) + } + if (obj == null) { return false; - if (this.getClass() != obj.getClass()) + } + if (this.getClass() != obj.getClass()) { return false; + } final WindowsData other = (WindowsData) obj; if (bootDelay == null) { - if (other.bootDelay != null) + if (other.bootDelay != null) { return false; - } else if (!bootDelay.equals(other.bootDelay)) + } + } else if (!bootDelay.equals(other.bootDelay)) { return false; + } if (password == null) { - if (other.password != null) + if (other.password != null) { return false; - } else if (!password.equals(other.password)) + } + } else if (!password.equals(other.password)) { return false; + } if (allowSelfSignedCertificate == null) { - if (other.allowSelfSignedCertificate != null) + if (other.allowSelfSignedCertificate != null) { return false; - } else if (!allowSelfSignedCertificate.equals(other.allowSelfSignedCertificate)) + } + } else if (!allowSelfSignedCertificate.equals(other.allowSelfSignedCertificate)) { return false; + } return useHTTPS == other.useHTTPS && specifyPassword == other.specifyPassword; } } diff --git a/src/main/java/hudson/plugins/ec2/ebs/ZPoolExpandNotice.java b/src/main/java/hudson/plugins/ec2/ebs/ZPoolExpandNotice.java index e5ab4a08a..a11eb5a9e 100644 --- a/src/main/java/hudson/plugins/ec2/ebs/ZPoolExpandNotice.java +++ b/src/main/java/hudson/plugins/ec2/ebs/ZPoolExpandNotice.java @@ -23,8 +23,8 @@ */ package hudson.plugins.ec2.ebs; -import hudson.model.AdministrativeMonitor; import hudson.Extension; +import hudson.model.AdministrativeMonitor; /** * {@link AdministrativeMonitor} that tells the user that ZFS pool is filling up and they need to add more storage. @@ -36,7 +36,7 @@ public class ZPoolExpandNotice extends AdministrativeMonitor { /** * Set by {@link ZPoolMonitor}. */ - /* package */boolean activated = false; + /* package */ boolean activated = false; public ZPoolExpandNotice() { super("zpool.ebs"); diff --git a/src/main/java/hudson/plugins/ec2/ebs/ZPoolMonitor.java b/src/main/java/hudson/plugins/ec2/ebs/ZPoolMonitor.java index 61e6abd68..68dbb79e5 100644 --- a/src/main/java/hudson/plugins/ec2/ebs/ZPoolMonitor.java +++ b/src/main/java/hudson/plugins/ec2/ebs/ZPoolMonitor.java @@ -23,18 +23,17 @@ */ package hudson.plugins.ec2.ebs; -import hudson.model.PeriodicWork; -import hudson.model.AdministrativeMonitor; import hudson.Extension; +import hudson.model.AdministrativeMonitor; +import hudson.model.PeriodicWork; +import java.io.IOException; +import java.net.URL; +import java.util.concurrent.TimeUnit; import jenkins.model.Jenkins; import org.jvnet.solaris.libzfs.LibZFS; import org.jvnet.solaris.libzfs.ZFSFileSystem; import org.jvnet.solaris.libzfs.ZFSPool; -import java.net.URL; -import java.io.IOException; -import java.util.concurrent.TimeUnit; - /** * Once an hour, check if the main zpool is that hosts $HUDSON_HOME has still enough free space. * @@ -55,8 +54,9 @@ protected void doRun() { Jenkins jenkinsInstance = Jenkins.getInstanceOrNull(); ZFSFileSystem fs = null; try { - if (isInsideEC2() && jenkinsInstance != null) + if (isInsideEC2() && jenkinsInstance != null) { fs = new LibZFS().getFileSystemByMountPoint(jenkinsInstance.getRootDir()); + } } catch (LinkageError e) { // probably not running on OpenSolaris } diff --git a/src/main/java/hudson/plugins/ec2/ebs/package-info.java b/src/main/java/hudson/plugins/ec2/ebs/package-info.java index ccd1f7a4a..326cfe3a1 100644 --- a/src/main/java/hudson/plugins/ec2/ebs/package-info.java +++ b/src/main/java/hudson/plugins/ec2/ebs/package-info.java @@ -29,4 +29,4 @@ * This should eventually move to its own plugin, but for * now I'm putting this here. */ -package hudson.plugins.ec2.ebs; \ No newline at end of file +package hudson.plugins.ec2.ebs; diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index f45b54309..996245d62 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -39,10 +39,6 @@ import hudson.remoting.Channel.Listener; import hudson.slaves.CommandLauncher; import hudson.slaves.ComputerLauncher; -import jenkins.model.Jenkins; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.StringUtils; - import java.io.*; import java.net.InetSocketAddress; import java.net.Proxy; @@ -50,6 +46,9 @@ import java.nio.file.Files; import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.model.Jenkins; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; /** * {@link ComputerLauncher} that connects to a Unix agent on EC2 by using SSH. @@ -61,9 +60,9 @@ public class EC2MacLauncher extends EC2ComputerLauncher { private static final Logger LOGGER = Logger.getLogger(EC2MacLauncher.class.getName()); private static final String BOOTSTRAP_AUTH_SLEEP_MS = "jenkins.ec2.bootstrapAuthSleepMs"; - private static final String BOOTSTRAP_AUTH_TRIES= "jenkins.ec2.bootstrapAuthTries"; + private static final String BOOTSTRAP_AUTH_TRIES = "jenkins.ec2.bootstrapAuthTries"; private static final String READINESS_SLEEP_MS = "jenkins.ec2.readinessSleepMs"; - private static final String READINESS_TRIES= "jenkins.ec2.readinessTries"; + private static final String READINESS_TRIES = "jenkins.ec2.readinessTries"; private static int bootstrapAuthSleepMs = 30000; private static int bootstrapAuthTries = 30; @@ -71,19 +70,23 @@ public class EC2MacLauncher extends EC2ComputerLauncher { private static int readinessSleepMs = 1000; private static int readinessTries = 120; - static { + static { String prop = System.getProperty(BOOTSTRAP_AUTH_SLEEP_MS); - if (prop != null) + if (prop != null) { bootstrapAuthSleepMs = Integer.parseInt(prop); + } prop = System.getProperty(BOOTSTRAP_AUTH_TRIES); - if (prop != null) + if (prop != null) { bootstrapAuthTries = Integer.parseInt(prop); + } prop = System.getProperty(READINESS_TRIES); - if (prop != null) + if (prop != null) { readinessTries = Integer.parseInt(prop); + } prop = System.getProperty(READINESS_SLEEP_MS); - if (prop != null) + if (prop != null) { readinessSleepMs = Integer.parseInt(prop); + } } protected void log(Level level, EC2Computer computer, TaskListener listener, String message) { @@ -111,17 +114,17 @@ protected String buildUpCommand(EC2Computer computer, String command) { } @Override - protected void launchScript(EC2Computer computer, TaskListener listener) throws IOException, - AmazonClientException, InterruptedException { + protected void launchScript(EC2Computer computer, TaskListener listener) + throws IOException, AmazonClientException, InterruptedException { final Connection conn; Connection cleanupConn = null; // java's code path analysis for final - // doesn't work that well. + // doesn't work that well. boolean successful = false; PrintStream logger = listener.getLogger(); EC2AbstractSlave node = computer.getNode(); SlaveTemplate template = computer.getSlaveTemplate(); - if(node == null) { + if (node == null) { throw new IllegalStateException(); } @@ -138,12 +141,17 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws break; } - logInfo(computer, listener, "Node still not ready. Current status: " + readinessNode.getEc2ReadinessStatus()); + logInfo( + computer, + listener, + "Node still not ready. Current status: " + readinessNode.getEc2ReadinessStatus()); Thread.sleep(readinessSleepMs); } if (!readinessNode.isReady()) { - throw new AmazonClientException("Node still not ready, timed out after " + (readinessTries * readinessSleepMs / 1000) + "s with status " + readinessNode.getEc2ReadinessStatus()); + throw new AmazonClientException( + "Node still not ready, timed out after " + (readinessTries * readinessSleepMs / 1000) + + "s with status " + readinessNode.getEc2ReadinessStatus()); } } @@ -154,7 +162,10 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws if (isBootstrapped) { int bootDelay = node.getBootDelay(); if (bootDelay > 0) { - logInfo(computer, listener, "SSH service responded. Waiting " + bootDelay + "ms for service to stabilize"); + logInfo( + computer, + listener, + "SSH service responded. Waiting " + bootDelay + "ms for service to stabilize"); Thread.sleep(bootDelay); logInfo(computer, listener, "SSH service should have stabilized"); } @@ -162,7 +173,9 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws logInfo(computer, listener, "connect fresh as root"); cleanupConn = connectToSsh(computer, listener, template); KeyPair key = computer.getCloud().getKeyPair(); - if (key == null || !cleanupConn.authenticateWithPublicKey(computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), "")) { + if (key == null + || !cleanupConn.authenticateWithPublicKey( + computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), "")) { logWarning(computer, listener, "Authentication failed"); return; // failed to connect as root. } @@ -179,18 +192,19 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); conn.exec("mkdir -p " + tmpDir, logger); - if (initScript != null && initScript.trim().length() > 0 + if (initScript != null + && initScript.trim().length() > 0 && conn.exec("test -e ~/.hudson-run-init", logger) != 0) { logInfo(computer, listener, "Executing init script"); scp.put(initScript.getBytes("UTF-8"), "init.sh", tmpDir, "0700"); Session sess = conn.openSession(); sess.requestDumbPTY(); // so that the remote side bundles stdout - // and stderr + // and stderr sess.execCommand(buildUpCommand(computer, tmpDir + "/init.sh")); sess.getStdin().close(); // nothing to write here sess.getStderr().close(); // we are not supposed to get anything - // from stderr + // from stderr IOUtils.copy(sess.getStdout(), logger); int exitStatus = waitCompletion(sess); @@ -205,12 +219,12 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws // Needs a tty to run sudo. sess = conn.openSession(); sess.requestDumbPTY(); // so that the remote side bundles stdout - // and stderr + // and stderr sess.execCommand(buildUpCommand(computer, "touch ~/.hudson-run-init")); sess.getStdin().close(); // nothing to write here sess.getStderr().close(); // we are not supposed to get anything - // from stderr + // from stderr IOUtils.copy(sess.getStdout(), logger); exitStatus = waitCompletion(sess); @@ -227,10 +241,21 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws Instance nodeInstance = computer.describeInstance(); if (nodeInstance.getInstanceType().equals("mac2.metal")) { LOGGER.info("Running Command for mac2.metal"); - executeRemote(computer, conn, javaPath + " -fullversion", "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-aarch64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-aarch64-macos-jdk.pkg -target /", logger, listener); - } - else{ - executeRemote(computer, conn, javaPath + " -fullversion", "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-x64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-x64-macos-jdk.pkg -target /", logger, listener); + executeRemote( + computer, + conn, + javaPath + " -fullversion", + "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-aarch64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-aarch64-macos-jdk.pkg -target /", + logger, + listener); + } else { + executeRemote( + computer, + conn, + javaPath + " -fullversion", + "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-x64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-x64-macos-jdk.pkg -target /", + logger, + listener); } } catch (InterruptedException ex) { LOGGER.warning(ex.getMessage()); @@ -245,8 +270,9 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws final String suffix = computer.getSlaveCommandSuffix(); final String remoteFS = node.getRemoteFS(); final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; - String launchString = prefix + " " + javaPath + " " + (jvmopts != null ? jvmopts : "") + " -jar " + tmpDir + "/remoting.jar -workDir " + workDir + suffix; - // launchString = launchString.trim(); + String launchString = prefix + " " + javaPath + " " + (jvmopts != null ? jvmopts : "") + " -jar " + tmpDir + + "/remoting.jar -workDir " + workDir + suffix; + // launchString = launchString.trim(); SlaveTemplate slaveTemplate = computer.getSlaveTemplate(); @@ -256,13 +282,23 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws try { // Obviously the controller must have an installed ssh client. // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag - String sshClientLaunchString = String.format("ssh -o StrictHostKeyChecking=%s -i %s %s@%s -p %d %s", slaveTemplate.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), identityKeyFile.getAbsolutePath(), node.remoteAdmin, getEC2HostAddress(computer, template), node.getSshPort(), launchString); - - logInfo(computer, listener, "Launching remoting agent (via SSH client process): " + sshClientLaunchString); + String sshClientLaunchString = String.format( + "ssh -o StrictHostKeyChecking=%s -i %s %s@%s -p %d %s", + slaveTemplate.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), + identityKeyFile.getAbsolutePath(), + node.remoteAdmin, + getEC2HostAddress(computer, template), + node.getSshPort(), + launchString); + + logInfo( + computer, + listener, + "Launching remoting agent (via SSH client process): " + sshClientLaunchString); CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); commandLauncher.launch(computer, listener); } finally { - if(!identityKeyFile.delete()) { + if (!identityKeyFile.delete()) { LOGGER.log(Level.WARNING, "Failed to delete identity key file"); } } @@ -281,14 +317,21 @@ public void onClosed(Channel channel, IOException cause) { successful = true; } finally { - if (cleanupConn != null && !successful) + if (cleanupConn != null && !successful) { cleanupConn.close(); + } } } - private boolean executeRemote(EC2Computer computer, Connection conn, String checkCommand, String command, PrintStream logger, TaskListener listener) + private boolean executeRemote( + EC2Computer computer, + Connection conn, + String checkCommand, + String command, + PrintStream logger, + TaskListener listener) throws IOException, InterruptedException { - logInfo(computer, listener,"Verifying: " + checkCommand); + logInfo(computer, listener, "Verifying: " + checkCommand); if (conn.exec(checkCommand, logger) != 0) { logInfo(computer, listener, "Installing: " + command); if (conn.exec(command, logger) != 0) { @@ -302,7 +345,7 @@ private boolean executeRemote(EC2Computer computer, Connection conn, String chec private File createIdentityKeyFile(EC2Computer computer) throws IOException { EC2PrivateKey ec2PrivateKey = computer.getCloud().resolvePrivateKey(); String privateKey = ""; - if (ec2PrivateKey != null){ + if (ec2PrivateKey != null) { privateKey = ec2PrivateKey.getPrivateKey(); } @@ -329,8 +372,8 @@ private File createIdentityKeyFile(EC2Computer computer) throws IOException { } } - private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemplate template) throws IOException, - InterruptedException, AmazonClientException { + private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemplate template) + throws IOException, InterruptedException, AmazonClientException { logInfo(computer, listener, "bootstrap()"); Connection bootstrapConn = null; try { @@ -338,18 +381,22 @@ private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemp boolean isAuthenticated = false; logInfo(computer, listener, "Getting keypair..."); KeyPair key = computer.getCloud().getKeyPair(); - if (key == null){ + if (key == null) { logWarning(computer, listener, "Could not retrieve a valid key pair."); return false; } - logInfo(computer, listener, - String.format("Using private key %s (SHA-1 fingerprint %s)", key.getKeyName(), key.getKeyFingerprint())); + logInfo( + computer, + listener, + String.format( + "Using private key %s (SHA-1 fingerprint %s)", key.getKeyName(), key.getKeyFingerprint())); while (tries-- > 0) { logInfo(computer, listener, "Authenticating as " + computer.getRemoteAdmin()); try { bootstrapConn = connectToSsh(computer, listener, template); - isAuthenticated = bootstrapConn.authenticateWithPublicKey(computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), ""); - } catch(IOException e) { + isAuthenticated = bootstrapConn.authenticateWithPublicKey( + computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), ""); + } catch (IOException e) { logException(computer, listener, "Exception trying to authenticate", e); bootstrapConn.close(); } @@ -371,8 +418,8 @@ private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemp return true; } - private Connection connectToSsh(EC2Computer computer, TaskListener listener, SlaveTemplate template) throws AmazonClientException, - InterruptedException { + private Connection connectToSsh(EC2Computer computer, TaskListener listener, SlaveTemplate template) + throws AmazonClientException, InterruptedException { final EC2AbstractSlave node = computer.getNode(); final long timeout = node == null ? 0L : node.getLaunchTimeoutInMillis(); final long startTime = System.currentTimeMillis(); @@ -387,8 +434,8 @@ private Connection connectToSsh(EC2Computer computer, TaskListener listener, Sla String host = getEC2HostAddress(computer, template); if ((node instanceof EC2SpotSlave) && computer.getInstanceId() == null) { - // getInstanceId() on EC2SpotSlave can return null if the spot request doesn't yet know - // the instance id that it is starting. Continue to wait until the instanceId is set. + // getInstanceId() on EC2SpotSlave can return null if the spot request doesn't yet know + // the instance id that it is starting. Continue to wait until the instanceId is set. logInfo(computer, listener, "empty instanceId for Spot Slave."); throw new IOException("goto sleep"); } @@ -399,14 +446,19 @@ private Connection connectToSsh(EC2Computer computer, TaskListener listener, Sla } if ("0.0.0.0".equals(host)) { - logWarning(computer, listener, "Invalid host 0.0.0.0, your host is most likely waiting for an ip address."); + logWarning( + computer, + listener, + "Invalid host 0.0.0.0, your host is most likely waiting for an ip address."); throw new IOException("goto sleep"); } int port = computer.getSshPort(); Integer slaveConnectTimeout = Integer.getInteger("jenkins.ec2.slaveConnectTimeout", 10000); - logInfo(computer, listener, "Connecting to " + host + " on port " + port + ", with timeout " + slaveConnectTimeout - + "."); + logInfo( + computer, + listener, + "Connecting to " + host + " on port " + port + ", with timeout " + slaveConnectTimeout + "."); Connection conn = new Connection(host, port); ProxyConfiguration proxyConfig = Jenkins.get().proxy; Proxy proxy = proxyConfig == null ? Proxy.NO_PROXY : proxyConfig.createProxy(host); @@ -414,7 +466,11 @@ private Connection connectToSsh(EC2Computer computer, TaskListener listener, Sla InetSocketAddress address = (InetSocketAddress) proxy.address(); HTTPProxyData proxyData = null; if (null != proxyConfig.getUserName()) { - proxyData = new HTTPProxyData(address.getHostName(), address.getPort(), proxyConfig.getUserName(), proxyConfig.getPassword()); + proxyData = new HTTPProxyData( + address.getHostName(), + address.getPort(), + proxyConfig.getUserName(), + proxyConfig.getPassword()); } else { proxyData = new HTTPProxyData(address.getHostName(), address.getPort()); } @@ -422,7 +478,8 @@ private Connection connectToSsh(EC2Computer computer, TaskListener listener, Sla logInfo(computer, listener, "Using HTTP Proxy Configuration"); } - conn.connect(new ServerHostKeyVerifierImpl(computer, listener), slaveConnectTimeout, slaveConnectTimeout); + conn.connect( + new ServerHostKeyVerifierImpl(computer, listener), slaveConnectTimeout, slaveConnectTimeout); logInfo(computer, listener, "Connected via SSH."); return conn; // successfully connected } catch (IOException e) { @@ -431,8 +488,11 @@ private Connection connectToSsh(EC2Computer computer, TaskListener listener, Sla // If the computer was set offline because it's not trusted, we avoid persisting in connecting to it. // The computer is offline for a long period - if (computer.isOffline() && StringUtils.isNotBlank(computer.getOfflineCauseReason()) && computer.getOfflineCauseReason().equals(Messages.OfflineCause_SSHKeyCheckFailed())) { - throw new AmazonClientException("The connection couldn't be established and the computer is now offline", e); + if (computer.isOffline() + && StringUtils.isNotBlank(computer.getOfflineCauseReason()) + && computer.getOfflineCauseReason().equals(Messages.OfflineCause_SSHKeyCheckFailed())) { + throw new AmazonClientException( + "The connection couldn't be established and the computer is now offline", e); } else { logInfo(computer, listener, "Waiting for SSH to come up. Sleeping 5."); Thread.sleep(5000); @@ -455,9 +515,13 @@ public ServerHostKeyVerifierImpl(final EC2Computer computer, final TaskListener } @Override - public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception { + public boolean verifyServerHostKey( + String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception { SlaveTemplate template = computer.getSlaveTemplate(); - return template != null && template.getHostKeyVerificationStrategy().getStrategy().verify(computer, new HostKey(serverHostKeyAlgorithm, serverHostKey), listener); + return template != null + && template.getHostKeyVerificationStrategy() + .getStrategy() + .verify(computer, new HostKey(serverHostKeyAlgorithm, serverHostKey), listener); } } @@ -472,8 +536,9 @@ private int waitCompletion(Session session) throws InterruptedException { // to 1 sec. for (int i = 0; i < 10; i++) { Integer r = session.getExitStatus(); - if (r != null) + if (r != null) { return r; + } Thread.sleep(100); } return -1; diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index 663a356d6..04acbd606 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -23,9 +23,17 @@ */ package hudson.plugins.ec2.ssh; +import com.amazonaws.AmazonClientException; +import com.amazonaws.services.ec2.model.Instance; +import com.amazonaws.services.ec2.model.KeyPair; +import com.trilead.ssh2.Connection; +import com.trilead.ssh2.HTTPProxyData; +import com.trilead.ssh2.SCPClient; +import com.trilead.ssh2.ServerHostKeyVerifier; +import com.trilead.ssh2.Session; import hudson.FilePath; -import hudson.Util; import hudson.ProxyConfiguration; +import hudson.Util; import hudson.model.Descriptor; import hudson.model.TaskListener; import hudson.plugins.ec2.*; @@ -36,7 +44,6 @@ import hudson.remoting.Channel.Listener; import hudson.slaves.CommandLauncher; import hudson.slaves.ComputerLauncher; - import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -49,19 +56,8 @@ import java.util.Base64; import java.util.logging.Level; import java.util.logging.Logger; - import jenkins.model.Jenkins; - import org.apache.commons.io.IOUtils; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.services.ec2.model.Instance; -import com.amazonaws.services.ec2.model.KeyPair; -import com.trilead.ssh2.Connection; -import com.trilead.ssh2.HTTPProxyData; -import com.trilead.ssh2.SCPClient; -import com.trilead.ssh2.ServerHostKeyVerifier; -import com.trilead.ssh2.Session; import org.apache.commons.lang.StringUtils; /** @@ -74,9 +70,9 @@ public class EC2UnixLauncher extends EC2ComputerLauncher { private static final Logger LOGGER = Logger.getLogger(EC2UnixLauncher.class.getName()); private static final String BOOTSTRAP_AUTH_SLEEP_MS = "jenkins.ec2.bootstrapAuthSleepMs"; - private static final String BOOTSTRAP_AUTH_TRIES= "jenkins.ec2.bootstrapAuthTries"; + private static final String BOOTSTRAP_AUTH_TRIES = "jenkins.ec2.bootstrapAuthTries"; private static final String READINESS_SLEEP_MS = "jenkins.ec2.readinessSleepMs"; - private static final String READINESS_TRIES= "jenkins.ec2.readinessTries"; + private static final String READINESS_TRIES = "jenkins.ec2.readinessTries"; private static int bootstrapAuthSleepMs = 30000; private static int bootstrapAuthTries = 30; @@ -84,19 +80,23 @@ public class EC2UnixLauncher extends EC2ComputerLauncher { private static int readinessSleepMs = 1000; private static int readinessTries = 120; - static { + static { String prop = System.getProperty(BOOTSTRAP_AUTH_SLEEP_MS); - if (prop != null) + if (prop != null) { bootstrapAuthSleepMs = Integer.parseInt(prop); + } prop = System.getProperty(BOOTSTRAP_AUTH_TRIES); - if (prop != null) + if (prop != null) { bootstrapAuthTries = Integer.parseInt(prop); + } prop = System.getProperty(READINESS_TRIES); - if (prop != null) + if (prop != null) { readinessTries = Integer.parseInt(prop); + } prop = System.getProperty(READINESS_SLEEP_MS); - if (prop != null) + if (prop != null) { readinessSleepMs = Integer.parseInt(prop); + } } protected void log(Level level, EC2Computer computer, TaskListener listener, String message) { @@ -124,17 +124,17 @@ protected String buildUpCommand(EC2Computer computer, String command) { } @Override - protected void launchScript(EC2Computer computer, TaskListener listener) throws IOException, - AmazonClientException, InterruptedException { + protected void launchScript(EC2Computer computer, TaskListener listener) + throws IOException, AmazonClientException, InterruptedException { final Connection conn; Connection cleanupConn = null; // java's code path analysis for final - // doesn't work that well. + // doesn't work that well. boolean successful = false; PrintStream logger = listener.getLogger(); EC2AbstractSlave node = computer.getNode(); SlaveTemplate template = computer.getSlaveTemplate(); - if(node == null) { + if (node == null) { throw new IllegalStateException(); } @@ -151,12 +151,17 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws break; } - logInfo(computer, listener, "Node still not ready. Current status: " + readinessNode.getEc2ReadinessStatus()); + logInfo( + computer, + listener, + "Node still not ready. Current status: " + readinessNode.getEc2ReadinessStatus()); Thread.sleep(readinessSleepMs); } if (!readinessNode.isReady()) { - throw new AmazonClientException("Node still not ready, timed out after " + (readinessTries * readinessSleepMs / 1000) + "s with status " + readinessNode.getEc2ReadinessStatus()); + throw new AmazonClientException( + "Node still not ready, timed out after " + (readinessTries * readinessSleepMs / 1000) + + "s with status " + readinessNode.getEc2ReadinessStatus()); } } @@ -167,7 +172,10 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws if (isBootstrapped) { int bootDelay = node.getBootDelay(); if (bootDelay > 0) { - logInfo(computer, listener, "SSH service responded. Waiting " + bootDelay + "ms for service to stabilize"); + logInfo( + computer, + listener, + "SSH service responded. Waiting " + bootDelay + "ms for service to stabilize"); Thread.sleep(bootDelay); logInfo(computer, listener, "SSH service should have stabilized"); } @@ -176,7 +184,9 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws logInfo(computer, listener, "connect fresh as root"); cleanupConn = connectToSsh(computer, listener, template); KeyPair key = computer.getCloud().getKeyPair(); - if (key == null || !cleanupConn.authenticateWithPublicKey(computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), "")) { + if (key == null + || !cleanupConn.authenticateWithPublicKey( + computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), "")) { logWarning(computer, listener, "Authentication failed"); return; // failed to connect as root. } @@ -193,18 +203,19 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); conn.exec("mkdir -p " + tmpDir, logger); - if (initScript != null && initScript.trim().length() > 0 + if (initScript != null + && initScript.trim().length() > 0 && conn.exec("test -e ~/.hudson-run-init", logger) != 0) { logInfo(computer, listener, "Executing init script"); scp.put(initScript.getBytes("UTF-8"), "init.sh", tmpDir, "0700"); Session sess = conn.openSession(); sess.requestDumbPTY(); // so that the remote side bundles stdout - // and stderr + // and stderr sess.execCommand(buildUpCommand(computer, tmpDir + "/init.sh")); sess.getStdin().close(); // nothing to write here sess.getStderr().close(); // we are not supposed to get anything - // from stderr + // from stderr IOUtils.copy(sess.getStdout(), logger); int exitStatus = waitCompletion(sess); @@ -219,12 +230,12 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws // Needs a tty to run sudo. sess = conn.openSession(); sess.requestDumbPTY(); // so that the remote side bundles stdout - // and stderr + // and stderr sess.execCommand(buildUpCommand(computer, "touch ~/.hudson-run-init")); sess.getStdin().close(); // nothing to write here sess.getStderr().close(); // we are not supposed to get anything - // from stderr + // from stderr IOUtils.copy(sess.getStdout(), logger); exitStatus = waitCompletion(sess); @@ -237,7 +248,13 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws // TODO: parse the version number. maven-enforcer-plugin might help final String javaPath = node.javaPath; - executeRemote(computer, conn, javaPath + " -fullversion", "sudo amazon-linux-extras install java-openjdk11 -y; sudo yum install -y fontconfig java-11-openjdk", logger, listener); + executeRemote( + computer, + conn, + javaPath + " -fullversion", + "sudo amazon-linux-extras install java-openjdk11 -y; sudo yum install -y fontconfig java-11-openjdk", + logger, + listener); executeRemote(computer, conn, "which scp", "sudo yum install -y openssh-clients", logger, listener); // Always copy so we get the most recent remoting.jar @@ -249,8 +266,9 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws final String suffix = computer.getSlaveCommandSuffix(); final String remoteFS = node.getRemoteFS(); final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; - String launchString = prefix + " " + javaPath + " " + (jvmopts != null ? jvmopts : "") + " -jar " + tmpDir + "/remoting.jar -workDir " + workDir + suffix; - // launchString = launchString.trim(); + String launchString = prefix + " " + javaPath + " " + (jvmopts != null ? jvmopts : "") + " -jar " + tmpDir + + "/remoting.jar -workDir " + workDir + suffix; + // launchString = launchString.trim(); if (template.isConnectBySSHProcess()) { File identityKeyFile = createIdentityKeyFile(computer); @@ -258,22 +276,35 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws File hostKeyFile = createHostKeyFile(computer, ec2HostAddress, listener); String userKnownHostsFileFlag = ""; if (hostKeyFile != null) { - userKnownHostsFileFlag = String.format(" -o \"UserKnownHostsFile=%s\"", hostKeyFile.getAbsolutePath()); + userKnownHostsFileFlag = + String.format(" -o \"UserKnownHostsFile=%s\"", hostKeyFile.getAbsolutePath()); } try { // Obviously the controller must have an installed ssh client. // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag - String sshClientLaunchString = String.format("ssh -o StrictHostKeyChecking=%s%s%s -i %s %s@%s -p %d %s", template.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), userKnownHostsFileFlag, getEC2HostKeyAlgorithmFlag(computer), identityKeyFile.getAbsolutePath(), node.remoteAdmin, ec2HostAddress, node.getSshPort(), launchString); - - logInfo(computer, listener, "Launching remoting agent (via SSH client process): " + sshClientLaunchString); + String sshClientLaunchString = String.format( + "ssh -o StrictHostKeyChecking=%s%s%s -i %s %s@%s -p %d %s", + template.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), + userKnownHostsFileFlag, + getEC2HostKeyAlgorithmFlag(computer), + identityKeyFile.getAbsolutePath(), + node.remoteAdmin, + ec2HostAddress, + node.getSshPort(), + launchString); + + logInfo( + computer, + listener, + "Launching remoting agent (via SSH client process): " + sshClientLaunchString); CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); commandLauncher.launch(computer, listener); } finally { - if(!identityKeyFile.delete()) { + if (!identityKeyFile.delete()) { LOGGER.log(Level.WARNING, "Failed to delete identity key file"); } - if(hostKeyFile != null && !hostKeyFile.delete()) { + if (hostKeyFile != null && !hostKeyFile.delete()) { LOGGER.log(Level.WARNING, "Failed to delete host key file"); } } @@ -292,14 +323,21 @@ public void onClosed(Channel channel, IOException cause) { successful = true; } finally { - if (cleanupConn != null && (!successful || template.isConnectBySSHProcess())) + if (cleanupConn != null && (!successful || template.isConnectBySSHProcess())) { cleanupConn.close(); + } } } - private boolean executeRemote(EC2Computer computer, Connection conn, String checkCommand, String command, PrintStream logger, TaskListener listener) + private boolean executeRemote( + EC2Computer computer, + Connection conn, + String checkCommand, + String command, + PrintStream logger, + TaskListener listener) throws IOException, InterruptedException { - logInfo(computer, listener,"Verifying: " + checkCommand); + logInfo(computer, listener, "Verifying: " + checkCommand); if (conn.exec(checkCommand, logger) != 0) { logInfo(computer, listener, "Installing: " + command); if (conn.exec(command, logger) != 0) { @@ -313,7 +351,7 @@ private boolean executeRemote(EC2Computer computer, Connection conn, String chec private File createIdentityKeyFile(EC2Computer computer) throws IOException { EC2PrivateKey ec2PrivateKey = computer.getCloud().resolvePrivateKey(); String privateKey = ""; - if (ec2PrivateKey != null){ + if (ec2PrivateKey != null) { privateKey = ec2PrivateKey.getPrivateKey(); } @@ -340,17 +378,20 @@ private File createIdentityKeyFile(EC2Computer computer) throws IOException { } } - private File createHostKeyFile(EC2Computer computer, String ec2HostAddress, TaskListener listener) throws IOException { + private File createHostKeyFile(EC2Computer computer, String ec2HostAddress, TaskListener listener) + throws IOException { HostKey ec2HostKey = HostKeyHelper.getInstance().getHostKey(computer); - if (ec2HostKey == null){ + if (ec2HostKey == null) { return null; } File tempFile = Files.createTempFile("ec2_", "_known_hosts").toFile(); String knownHost = ""; - knownHost = String.format("%s %s %s", ec2HostAddress, ec2HostKey.getAlgorithm(), Base64.getEncoder().encodeToString(ec2HostKey.getKey())); + knownHost = String.format( + "%s %s %s", + ec2HostAddress, ec2HostKey.getAlgorithm(), Base64.getEncoder().encodeToString(ec2HostKey.getKey())); try (FileOutputStream fileOutputStream = new FileOutputStream(tempFile); - OutputStreamWriter writer = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8)) { + OutputStreamWriter writer = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8)) { writer.write(knownHost); writer.flush(); FilePath filePath = new FilePath(tempFile); @@ -364,8 +405,8 @@ private File createHostKeyFile(EC2Computer computer, String ec2HostAddress, Task } } - private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemplate template) throws IOException, - InterruptedException, AmazonClientException { + private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemplate template) + throws IOException, InterruptedException, AmazonClientException { logInfo(computer, listener, "bootstrap()"); Connection bootstrapConn = null; try { @@ -373,18 +414,22 @@ private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemp boolean isAuthenticated = false; logInfo(computer, listener, "Getting keypair..."); KeyPair key = computer.getCloud().getKeyPair(); - if (key == null){ + if (key == null) { logWarning(computer, listener, "Could not retrieve a valid key pair."); return false; } - logInfo(computer, listener, - String.format("Using private key %s (SHA-1 fingerprint %s)", key.getKeyName(), key.getKeyFingerprint())); + logInfo( + computer, + listener, + String.format( + "Using private key %s (SHA-1 fingerprint %s)", key.getKeyName(), key.getKeyFingerprint())); while (tries-- > 0) { logInfo(computer, listener, "Authenticating as " + computer.getRemoteAdmin()); try { bootstrapConn = connectToSsh(computer, listener, template); - isAuthenticated = bootstrapConn.authenticateWithPublicKey(computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), ""); - } catch(IOException e) { + isAuthenticated = bootstrapConn.authenticateWithPublicKey( + computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), ""); + } catch (IOException e) { logException(computer, listener, "Exception trying to authenticate", e); bootstrapConn.close(); } @@ -406,8 +451,8 @@ private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemp return true; } - private Connection connectToSsh(EC2Computer computer, TaskListener listener, SlaveTemplate template) throws AmazonClientException, - InterruptedException { + private Connection connectToSsh(EC2Computer computer, TaskListener listener, SlaveTemplate template) + throws AmazonClientException, InterruptedException { final EC2AbstractSlave node = computer.getNode(); final long timeout = node == null ? 0L : node.getLaunchTimeoutInMillis(); final long startTime = System.currentTimeMillis(); @@ -422,8 +467,8 @@ private Connection connectToSsh(EC2Computer computer, TaskListener listener, Sla String host = getEC2HostAddress(computer, template); if ((node instanceof EC2SpotSlave) && computer.getInstanceId() == null) { - // getInstanceId() on EC2SpotSlave can return null if the spot request doesn't yet know - // the instance id that it is starting. Continue to wait until the instanceId is set. + // getInstanceId() on EC2SpotSlave can return null if the spot request doesn't yet know + // the instance id that it is starting. Continue to wait until the instanceId is set. logInfo(computer, listener, "empty instanceId for Spot Slave."); throw new IOException("goto sleep"); } @@ -434,14 +479,19 @@ private Connection connectToSsh(EC2Computer computer, TaskListener listener, Sla } if ("0.0.0.0".equals(host)) { - logWarning(computer, listener, "Invalid host 0.0.0.0, your host is most likely waiting for an ip address."); + logWarning( + computer, + listener, + "Invalid host 0.0.0.0, your host is most likely waiting for an ip address."); throw new IOException("goto sleep"); } int port = computer.getSshPort(); Integer slaveConnectTimeout = Integer.getInteger("jenkins.ec2.slaveConnectTimeout", 10000); - logInfo(computer, listener, "Connecting to " + host + " on port " + port + ", with timeout " + slaveConnectTimeout - + "."); + logInfo( + computer, + listener, + "Connecting to " + host + " on port " + port + ", with timeout " + slaveConnectTimeout + "."); Connection conn = new Connection(host, port); ProxyConfiguration proxyConfig = Jenkins.get().proxy; Proxy proxy = proxyConfig == null ? Proxy.NO_PROXY : proxyConfig.createProxy(host); @@ -449,7 +499,11 @@ private Connection connectToSsh(EC2Computer computer, TaskListener listener, Sla InetSocketAddress address = (InetSocketAddress) proxy.address(); HTTPProxyData proxyData = null; if (null != proxyConfig.getUserName()) { - proxyData = new HTTPProxyData(address.getHostName(), address.getPort(), proxyConfig.getUserName(), proxyConfig.getPassword()); + proxyData = new HTTPProxyData( + address.getHostName(), + address.getPort(), + proxyConfig.getUserName(), + proxyConfig.getPassword()); } else { proxyData = new HTTPProxyData(address.getHostName(), address.getPort()); } @@ -457,7 +511,8 @@ private Connection connectToSsh(EC2Computer computer, TaskListener listener, Sla logInfo(computer, listener, "Using HTTP Proxy Configuration"); } - conn.connect(new ServerHostKeyVerifierImpl(computer, listener), slaveConnectTimeout, slaveConnectTimeout); + conn.connect( + new ServerHostKeyVerifierImpl(computer, listener), slaveConnectTimeout, slaveConnectTimeout); logInfo(computer, listener, "Connected via SSH."); return conn; // successfully connected } catch (IOException e) { @@ -466,8 +521,11 @@ private Connection connectToSsh(EC2Computer computer, TaskListener listener, Sla // If the computer was set offline because it's not trusted, we avoid persisting in connecting to it. // The computer is offline for a long period - if (computer.isOffline() && StringUtils.isNotBlank(computer.getOfflineCauseReason()) && computer.getOfflineCauseReason().equals(Messages.OfflineCause_SSHKeyCheckFailed())) { - throw new AmazonClientException("The connection couldn't be established and the computer is now offline", e); + if (computer.isOffline() + && StringUtils.isNotBlank(computer.getOfflineCauseReason()) + && computer.getOfflineCauseReason().equals(Messages.OfflineCause_SSHKeyCheckFailed())) { + throw new AmazonClientException( + "The connection couldn't be established and the computer is now offline", e); } else { logInfo(computer, listener, "Waiting for SSH to come up. Sleeping 5."); Thread.sleep(5000); @@ -490,9 +548,13 @@ public ServerHostKeyVerifierImpl(final EC2Computer computer, final TaskListener } @Override - public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception { + public boolean verifyServerHostKey( + String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception { SlaveTemplate template = computer.getSlaveTemplate(); - return template != null && template.getHostKeyVerificationStrategy().getStrategy().verify(computer, new HostKey(serverHostKeyAlgorithm, serverHostKey), listener); + return template != null + && template.getHostKeyVerificationStrategy() + .getStrategy() + .verify(computer, new HostKey(serverHostKeyAlgorithm, serverHostKey), listener); } } @@ -504,7 +566,7 @@ private static String getEC2HostAddress(EC2Computer computer, SlaveTemplate temp private static String getEC2HostKeyAlgorithmFlag(EC2Computer computer) throws IOException { HostKey ec2HostKey = HostKeyHelper.getInstance().getHostKey(computer); - if (ec2HostKey != null){ + if (ec2HostKey != null) { return String.format(" -o \"HostKeyAlgorithms=%s\"", ec2HostKey.getAlgorithm()); } return ""; @@ -515,8 +577,9 @@ private int waitCompletion(Session session) throws InterruptedException { // to 1 sec. for (int i = 0; i < 10; i++) { Integer r = session.getExitStatus(); - if (r != null) + if (r != null) { return r; + } Thread.sleep(100); } return -1; diff --git a/src/main/java/hudson/plugins/ec2/ssh/HostKeyVerifierImpl.java b/src/main/java/hudson/plugins/ec2/ssh/HostKeyVerifierImpl.java index 884a2420b..ac44fceb1 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/HostKeyVerifierImpl.java +++ b/src/main/java/hudson/plugins/ec2/ssh/HostKeyVerifierImpl.java @@ -23,10 +23,9 @@ */ package hudson.plugins.ec2.ssh; -import java.util.logging.Logger; - import com.trilead.ssh2.ServerHostKeyVerifier; import java.security.MessageDigest; +import java.util.logging.Logger; public class HostKeyVerifierImpl implements ServerHostKeyVerifier { private static final Logger LOGGER = Logger.getLogger(HostKeyVerifierImpl.class.getName()); @@ -44,13 +43,15 @@ private String getFingerprint(byte[] serverHostKey) throws Exception { StringBuilder buf = new StringBuilder(); for (byte b : fingerprint) { - if (buf.length() > 0) + if (buf.length() > 0) { buf.append(':'); + } buf.append(String.format("%02x", b)); } return buf.toString(); } + @Override public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception { String fingerprint = getFingerprint(serverHostKey); @@ -59,10 +60,10 @@ public boolean verifyServerHostKey(String hostname, int port, String serverHostK boolean matches = console.contains(fingerprint); - if (!matches) + if (!matches) { LOGGER.severe("No matching fingerprint found in the console output: " + console); + } return matches; } - } diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/AcceptNewStrategy.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/AcceptNewStrategy.java index 76bf4a015..cb056c3be 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/AcceptNewStrategy.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/AcceptNewStrategy.java @@ -27,7 +27,6 @@ import hudson.plugins.ec2.EC2Cloud; import hudson.plugins.ec2.EC2Computer; import hudson.slaves.OfflineCause; - import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; @@ -41,19 +40,35 @@ */ public class AcceptNewStrategy extends SshHostKeyVerificationStrategy { private static final Logger LOGGER = Logger.getLogger(AcceptNewStrategy.class.getName()); - + @Override public boolean verify(EC2Computer computer, HostKey hostKey, TaskListener listener) throws IOException { HostKey existingHostKey = HostKeyHelper.getInstance().getHostKey(computer); if (null == existingHostKey) { HostKeyHelper.getInstance().saveHostKey(computer, hostKey); - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("The SSH key %s %s has been automatically trusted for connections to %s", hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format( + "The SSH key %s %s has been automatically trusted for connections to %s", + hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); return true; } else if (existingHostKey.equals(hostKey)) { - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("Connection allowed after the host key has been verified")); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format("Connection allowed after the host key has been verified")); return true; } else { - EC2Cloud.log(LOGGER, Level.WARNING, computer.getListener(), String.format("The SSH key (%s) presented by the instance has changed since first saved (%s). The connection to %s is closed to prevent a possible man-in-the-middle attack", hostKey.getFingerprint(), existingHostKey.getFingerprint(), computer.getName())); + EC2Cloud.log( + LOGGER, + Level.WARNING, + computer.getListener(), + String.format( + "The SSH key (%s) presented by the instance has changed since first saved (%s). The connection to %s is closed to prevent a possible man-in-the-middle attack", + hostKey.getFingerprint(), existingHostKey.getFingerprint(), computer.getName())); // To avoid reconnecting continuously computer.setTemporarilyOffline(true, OfflineCause.create(Messages._OfflineCause_SSHKeyCheckFailed())); return false; diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewHardStrategy.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewHardStrategy.java index 4f637f7e5..b1c9f198d 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewHardStrategy.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewHardStrategy.java @@ -27,7 +27,6 @@ import hudson.plugins.ec2.EC2Cloud; import hudson.plugins.ec2.EC2Computer; import hudson.slaves.OfflineCause; - import java.io.IOException; import java.util.concurrent.TimeUnit; import java.util.logging.Level; @@ -43,22 +42,40 @@ */ public class CheckNewHardStrategy extends SshHostKeyVerificationStrategy { private static final Logger LOGGER = Logger.getLogger(CheckNewHardStrategy.class.getName()); - + @Override public boolean verify(EC2Computer computer, HostKey hostKey, TaskListener listener) throws IOException { HostKey existingHostKey = HostKeyHelper.getInstance().getHostKey(computer); if (null == existingHostKey) { HostKey consoleHostKey = getHostKeyFromConsole(LOGGER, computer, hostKey.getAlgorithm()); - + if (hostKey.equals(consoleHostKey)) { HostKeyHelper.getInstance().saveHostKey(computer, hostKey); - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("The SSH key %s %s has been successfully checked against the instance console for connections to %s", hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format( + "The SSH key %s %s has been successfully checked against the instance console for connections to %s", + hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); return true; } else if (consoleHostKey == null) { - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("The instance console is blank. Cannot check the key. The connection to %s is not allowed", computer.getName())); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format( + "The instance console is blank. Cannot check the key. The connection to %s is not allowed", + computer.getName())); return false; // waiting for next retry to have the console filled up } else if (consoleHostKey.getKey().length == 0) { - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("The SSH key (%s %s) presented by the instance has not been found on the instance console. Cannot check the key. The connection to %s is not allowed", hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format( + "The SSH key (%s %s) presented by the instance has not been found on the instance console. Cannot check the key. The connection to %s is not allowed", + hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); // it is the difference with the soft strategy, the key is not accepted boolean stop = false; try { @@ -68,21 +85,41 @@ public boolean verify(EC2Computer computer, HostKey hostKey, TaskListener listen } if (stop) { - computer.setTemporarilyOffline(true, OfflineCause.create(Messages._OfflineCause_SSHKeyCheckFailed())); // avoid next try + computer.setTemporarilyOffline( + true, OfflineCause.create(Messages._OfflineCause_SSHKeyCheckFailed())); // avoid next try } return false; } else { - EC2Cloud.log(LOGGER, Level.WARNING, computer.getListener(), String.format("The SSH key (%s %s) presented by the instance is different from the one printed out on the instance console (%s %s). The connection to %s is closed to prevent a possible man-in-the-middle attack", - hostKey.getAlgorithm(), hostKey.getFingerprint(), consoleHostKey.getAlgorithm(), consoleHostKey.getFingerprint(), computer.getName())); + EC2Cloud.log( + LOGGER, + Level.WARNING, + computer.getListener(), + String.format( + "The SSH key (%s %s) presented by the instance is different from the one printed out on the instance console (%s %s). The connection to %s is closed to prevent a possible man-in-the-middle attack", + hostKey.getAlgorithm(), + hostKey.getFingerprint(), + consoleHostKey.getAlgorithm(), + consoleHostKey.getFingerprint(), + computer.getName())); // To avoid reconnecting continuously computer.setTemporarilyOffline(true, OfflineCause.create(Messages._OfflineCause_SSHKeyCheckFailed())); return false; } } else if (existingHostKey.equals(hostKey)) { - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("Connection allowed after the host key has been verified")); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format("Connection allowed after the host key has been verified")); return true; } else { - EC2Cloud.log(LOGGER, Level.WARNING, computer.getListener(), String.format("The SSH key (%s) presented by the instance has changed since first saved (%s). The connection to %s is closed to prevent a possible man-in-the-middle attack", hostKey.getFingerprint(), existingHostKey.getFingerprint(), computer.getName())); + EC2Cloud.log( + LOGGER, + Level.WARNING, + computer.getListener(), + String.format( + "The SSH key (%s) presented by the instance has changed since first saved (%s). The connection to %s is closed to prevent a possible man-in-the-middle attack", + hostKey.getFingerprint(), existingHostKey.getFingerprint(), computer.getName())); // To avoid reconnecting continuously computer.setTemporarilyOffline(true, OfflineCause.create(Messages._OfflineCause_SSHKeyCheckFailed())); return false; diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewSoftStrategy.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewSoftStrategy.java index c884659b1..cad6090ad 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewSoftStrategy.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewSoftStrategy.java @@ -27,7 +27,6 @@ import hudson.plugins.ec2.EC2Cloud; import hudson.plugins.ec2.EC2Computer; import hudson.slaves.OfflineCause; - import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; @@ -45,38 +44,75 @@ */ public class CheckNewSoftStrategy extends SshHostKeyVerificationStrategy { private static final Logger LOGGER = Logger.getLogger(CheckNewSoftStrategy.class.getName()); - + @Override public boolean verify(EC2Computer computer, HostKey hostKey, TaskListener listener) throws IOException { HostKey existingHostKey = HostKeyHelper.getInstance().getHostKey(computer); if (null == existingHostKey) { HostKey consoleHostKey = getHostKeyFromConsole(LOGGER, computer, hostKey.getAlgorithm()); - + if (hostKey.equals(consoleHostKey)) { HostKeyHelper.getInstance().saveHostKey(computer, hostKey); - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("The SSH key %s %s has been successfully checked against the instance console for connections to %s", hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format( + "The SSH key %s %s has been successfully checked against the instance console for connections to %s", + hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); return true; } else if (consoleHostKey == null) { - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("The instance console is blank. Cannot check the key. The connection to %s is not allowed", computer.getName())); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format( + "The instance console is blank. Cannot check the key. The connection to %s is not allowed", + computer.getName())); return false; // waiting for next retry to have the console filled up } else if (consoleHostKey.getKey().length == 0) { - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("The SSH key (%s %s) presented by the instance has not been found on the instance console. Cannot check the key but the connection to %s is allowed", hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); - // it is the difference with the the hard strategy, the key is accepted + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format( + "The SSH key (%s %s) presented by the instance has not been found on the instance console. Cannot check the key but the connection to %s is allowed", + hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); + // it is the difference with the the hard strategy, the key is accepted HostKeyHelper.getInstance().saveHostKey(computer, hostKey); return true; } else { - EC2Cloud.log(LOGGER, Level.WARNING, computer.getListener(), String.format("The SSH key (%s %s) presented by the instance is different from the one printed out on the instance console (%s %s). The connection to %s is closed to prevent a possible man-in-the-middle attack", - hostKey.getAlgorithm(), hostKey.getFingerprint(), consoleHostKey.getAlgorithm(), consoleHostKey.getFingerprint(), computer.getName())); - // To avoid reconnecting continuously + EC2Cloud.log( + LOGGER, + Level.WARNING, + computer.getListener(), + String.format( + "The SSH key (%s %s) presented by the instance is different from the one printed out on the instance console (%s %s). The connection to %s is closed to prevent a possible man-in-the-middle attack", + hostKey.getAlgorithm(), + hostKey.getFingerprint(), + consoleHostKey.getAlgorithm(), + consoleHostKey.getFingerprint(), + computer.getName())); + // To avoid reconnecting continuously computer.setTemporarilyOffline(true, OfflineCause.create(Messages._OfflineCause_SSHKeyCheckFailed())); return false; } } else if (existingHostKey.equals(hostKey)) { - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("Connection allowed after the host key has been verified")); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format("Connection allowed after the host key has been verified")); return true; } else { - EC2Cloud.log(LOGGER, Level.WARNING, computer.getListener(), String.format("The SSH key (%s) presented by the instance has changed since first saved (%s). The connection to %s is closed to prevent a possible man-in-the-middle attack", hostKey.getFingerprint(), existingHostKey.getFingerprint(), computer.getName())); - // To avoid reconnecting continuously + EC2Cloud.log( + LOGGER, + Level.WARNING, + computer.getListener(), + String.format( + "The SSH key (%s) presented by the instance has changed since first saved (%s). The connection to %s is closed to prevent a possible man-in-the-middle attack", + hostKey.getFingerprint(), existingHostKey.getFingerprint(), computer.getName())); + // To avoid reconnecting continuously computer.setTemporarilyOffline(true, OfflineCause.create(Messages._OfflineCause_SSHKeyCheckFailed())); return false; } diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java index 1f01734fe..d88e04cb4 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java @@ -1,34 +1,33 @@ /* - * The MIT License - * - * Original work from ssh-slaves-plugin Copyright (c) 2016, Michael Clarke - * Modified work Copyright (c) 2020-, M Ramon Leon, CloudBees, Inc. - * Modified work: - * - Just the since annotation +* The MIT License +* +* Original work from ssh-slaves-plugin Copyright (c) 2016, Michael Clarke +* Modified work Copyright (c) 2020-, M Ramon Leon, CloudBees, Inc. +* Modified work: +* - Just the since annotation - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ package hudson.plugins.ec2.ssh.verifiers; import com.trilead.ssh2.KnownHosts; import edu.umd.cs.findbugs.annotations.NonNull; - import java.io.Serializable; import java.util.Arrays; @@ -84,20 +83,26 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) + if (this == obj) { return true; - if (obj == null) + } + if (obj == null) { return false; - if (getClass() != obj.getClass()) + } + if (getClass() != obj.getClass()) { return false; + } HostKey other = (HostKey) obj; if (algorithm == null) { - if (other.algorithm != null) + if (other.algorithm != null) { return false; - } else if (!algorithm.equals(other.algorithm)) + } + } else if (!algorithm.equals(other.algorithm)) { return false; - if (!Arrays.equals(key, other.key)) + } + if (!Arrays.equals(key, other.key)) { return false; + } return true; } } diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKeyHelper.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKeyHelper.java index f0eed8543..dfad6f888 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKeyHelper.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKeyHelper.java @@ -1,53 +1,52 @@ /* - * The MIT License - * - * Original work from ssh-slaves-plugin Copyright (c) 2016, Michael Clarke - * Modified work Copyright (c) 2020-, M Ramon Leon, CloudBees, Inc. - * Modified work: - * - Just the since annotation +* The MIT License +* +* Original work from ssh-slaves-plugin Copyright (c) 2016, Michael Clarke +* Modified work Copyright (c) 2020-, M Ramon Leon, CloudBees, Inc. +* Modified work: +* - Just the since annotation - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ package hudson.plugins.ec2.ssh.verifiers; import hudson.XmlFile; import hudson.model.Computer; import hudson.model.Node; -import jenkins.model.Jenkins; - import java.io.File; import java.io.IOException; import java.util.Map; import java.util.WeakHashMap; +import jenkins.model.Jenkins; /** * Helper methods to allow loading and saving of host keys for a computer. Verifiers * don't have a reference to the Node or Computer that they're running for at the point * they're created, so can only load the existing key to run comparisons against at the - * point the verifier is invoked during the connection attempt. + * point the verifier is invoked during the connection attempt. * @author Michael Clarke, M Ramon Leon * @since TODO */ public final class HostKeyHelper { private static final HostKeyHelper INSTANCE = new HostKeyHelper(); - + private final Map cache = new WeakHashMap<>(); private HostKeyHelper() { @@ -58,7 +57,6 @@ public static HostKeyHelper getInstance() { return INSTANCE; } - /** * Retrieve the currently trusted host key for the requested computer, or null if * no key is currently trusted. @@ -81,7 +79,6 @@ public HostKey getHostKey(Computer host) throws IOException { return key; } - /** * Persists an SSH key to disk for the requested host. This effectively marks * the requested key as trusted for all future connections to the host, until @@ -95,18 +92,18 @@ public void saveHostKey(Computer host, HostKey hostKey) throws IOException { xmlHostKeyFile.write(hostKey); cache.put(host, hostKey); } - + private File getSshHostKeyFile(Node node) throws IOException { return new File(getNodeDirectory(node), "ssh-host-key.xml"); } - + private File getNodeDirectory(Node node) throws IOException { if (null == node) { throw new IOException("Could not load key for the requested node"); } return new File(getNodesDirectory(), node.getNodeName()); } - + private File getNodesDirectory() throws IOException { // jenkins.model.Nodes#getNodesDirectory() is private, so we have to duplicate it here. File nodesDir = new File(Jenkins.get().getRootDir(), "nodes"); diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/NonVerifyingKeyVerificationStrategy.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/NonVerifyingKeyVerificationStrategy.java index 609e33940..a5141390f 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/NonVerifyingKeyVerificationStrategy.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/NonVerifyingKeyVerificationStrategy.java @@ -2,7 +2,7 @@ * The MIT License * * Copyright (c) 2020-, M Ramon Leon, CloudBees, Inc. - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -26,7 +26,6 @@ import hudson.model.TaskListener; import hudson.plugins.ec2.EC2Cloud; import hudson.plugins.ec2.EC2Computer; - import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; @@ -38,10 +37,16 @@ */ public class NonVerifyingKeyVerificationStrategy extends SshHostKeyVerificationStrategy { private static final Logger LOGGER = Logger.getLogger(NonVerifyingKeyVerificationStrategy.class.getName()); - + @Override public boolean verify(EC2Computer computer, HostKey hostKey, TaskListener listener) throws IOException { - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("No SSH key verification (%s %s) for connections to %s", hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format( + "No SSH key verification (%s %s) for connections to %s", + hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); return true; } } diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationAdministrativeMonitor.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationAdministrativeMonitor.java index 57e49f11f..cb0fe6d3b 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationAdministrativeMonitor.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationAdministrativeMonitor.java @@ -30,23 +30,22 @@ import hudson.plugins.ec2.PluginImpl; import hudson.plugins.ec2.SlaveTemplate; import hudson.slaves.Cloud; -import jenkins.model.Jenkins; -import org.kohsuke.stapler.HttpResponse; -import org.kohsuke.stapler.HttpResponses; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.interceptor.RequirePOST; - import java.io.IOException; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; import java.util.stream.Collectors; +import jenkins.model.Jenkins; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.HttpResponses; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.interceptor.RequirePOST; @Extension public class SshHostKeyVerificationAdministrativeMonitor extends AdministrativeMonitor { - private final static int MAX_TEMPLATES_FOUND = 5; - + private static final int MAX_TEMPLATES_FOUND = 5; + List veryInsecureTemplates = new ArrayList<>(MAX_TEMPLATES_FOUND); List insecureTemplates = new ArrayList<>(MAX_TEMPLATES_FOUND); @@ -73,9 +72,11 @@ public boolean showInsecureTemplates() { if (plugin == null) { return true; } - - Instant whenDismissed = Instant.ofEpochMilli(plugin.getDismissInsecureMessages()); // if not dismissed, it is EPOCH - return (whenDismissed.equals(Instant.EPOCH) || Instant.now().isBefore(whenDismissed)) && !insecureTemplates.isEmpty(); + + Instant whenDismissed = + Instant.ofEpochMilli(plugin.getDismissInsecureMessages()); // if not dismissed, it is EPOCH + return (whenDismissed.equals(Instant.EPOCH) || Instant.now().isBefore(whenDismissed)) + && !insecureTemplates.isEmpty(); } /** @@ -85,16 +86,16 @@ public boolean showInsecureTemplates() { @Override public boolean isActivated() { boolean maxTemplatesReached = false; - + ListIterator cloudIterator = Jenkins.get().clouds.listIterator(); - + // Let's clear the previously calculated wrong templates to populate the lists with them again veryInsecureTemplates.clear(); insecureTemplates.clear(); - + while (cloudIterator.hasNext() && !maxTemplatesReached) { Cloud cloud = cloudIterator.next(); - if (cloud instanceof EC2Cloud) { + if (cloud instanceof EC2Cloud) { maxTemplatesReached = gatherInsecureTemplate((EC2Cloud) cloud); } } @@ -115,29 +116,33 @@ private boolean gatherInsecureTemplate(EC2Cloud cloud) { } HostKeyVerificationStrategyEnum strategy = template.getHostKeyVerificationStrategy(); - if (veryInsecureTemplates.size() < MAX_TEMPLATES_FOUND && strategy.equals(HostKeyVerificationStrategyEnum.OFF)) { + if (veryInsecureTemplates.size() < MAX_TEMPLATES_FOUND + && strategy.equals(HostKeyVerificationStrategyEnum.OFF)) { veryInsecureTemplates.add(template.getDisplayName()); - } else if (insecureTemplates.size() < MAX_TEMPLATES_FOUND && (!strategy.equals(HostKeyVerificationStrategyEnum.CHECK_NEW_HARD))) { + } else if (insecureTemplates.size() < MAX_TEMPLATES_FOUND + && (!strategy.equals(HostKeyVerificationStrategyEnum.CHECK_NEW_HARD))) { // it is check-new-soft or accept-new insecureTemplates.add(template.getDisplayName()); } // stop collecting the status of the computers, we already have 5 each type - if (veryInsecureTemplates.size() >= MAX_TEMPLATES_FOUND || insecureTemplates.size() >= MAX_TEMPLATES_FOUND) { + if (veryInsecureTemplates.size() >= MAX_TEMPLATES_FOUND + || insecureTemplates.size() >= MAX_TEMPLATES_FOUND) { return true; } } - + return false; } - + @RequirePOST - public HttpResponse doAct(@QueryParameter String dismiss, @QueryParameter String dismissAllMessages) throws IOException { + public HttpResponse doAct(@QueryParameter String dismiss, @QueryParameter String dismissAllMessages) + throws IOException { Jenkins.get().checkPermission(Jenkins.ADMINISTER); if (dismiss != null) { PluginImpl.get().saveDismissInsecureMessages(System.currentTimeMillis()); - } - + } + if (dismissAllMessages != null) { disable(true); } diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategy.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategy.java index 12155fd60..8ba7b9552 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategy.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategy.java @@ -5,7 +5,7 @@ * Modified work Copyright (c) 2020-, M Ramon Leon, CloudBees, Inc. * Modified work: * - getHostKeyFromConsole method and called methods - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -35,16 +35,15 @@ import hudson.plugins.ec2.EC2Cloud; import hudson.plugins.ec2.EC2Computer; import hudson.plugins.ec2.InstanceState; -import jenkins.model.Jenkins; - import java.util.Base64; import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.model.Jenkins; /** * A method for verifying the host key provided by the remote host during the * initiation of each connection. - * + * * @author Michael Clarke * @since TODO */ @@ -66,8 +65,8 @@ public SshHostKeyVerificationStrategyDescriptor getDescriptor() { */ public abstract boolean verify(EC2Computer computer, HostKey hostKey, TaskListener listener) throws Exception; - public static abstract class SshHostKeyVerificationStrategyDescriptor extends Descriptor { - } + public abstract static class SshHostKeyVerificationStrategyDescriptor + extends Descriptor {} /** * Get the host key printed out in the console. @@ -77,13 +76,21 @@ public static abstract class SshHostKeyVerificationStrategyDescriptor extends De * but an empty array as the key if the console is not blank and the key for such an algorithm couldn't be found. */ @Nullable - HostKey getHostKeyFromConsole(@NonNull final Logger logger, @NonNull final EC2Computer computer, @NonNull final String serverHostKeyAlgorithm) { + HostKey getHostKeyFromConsole( + @NonNull final Logger logger, + @NonNull final EC2Computer computer, + @NonNull final String serverHostKeyAlgorithm) { HostKey key; TaskListener listener = computer.getListener(); try { - if(!computer.getState().equals(InstanceState.RUNNING)) { - EC2Cloud.log(logger, Level.INFO, listener, "The instance " + computer.getName() + " is not running, waiting to validate the key against the console"); + if (!computer.getState().equals(InstanceState.RUNNING)) { + EC2Cloud.log( + logger, + Level.INFO, + listener, + "The instance " + computer.getName() + + " is not running, waiting to validate the key against the console"); } } catch (InterruptedException e) { return null; @@ -93,7 +100,7 @@ HostKey getHostKeyFromConsole(@NonNull final Logger logger, @NonNull final EC2Co if (line != null && line.length() > 0) { key = getKeyFromLine(logger, line, listener); } else if (line != null) { - key = new HostKey(serverHostKeyAlgorithm, new byte[]{}); + key = new HostKey(serverHostKeyAlgorithm, new byte[] {}); } else { key = null; } @@ -102,20 +109,28 @@ HostKey getHostKeyFromConsole(@NonNull final Logger logger, @NonNull final EC2Co } /** - * Get the line with the key for such an algorithm - * @param logger the logger to print the messages - * @param computer the computer + * Get the line with the key for such an algorithm + * @param logger the logger to print the messages + * @param computer the computer * @param serverHostKeyAlgorithm the algorithm to search for * @return the line where the key for the algorithm is on, null if the console is blank, "" if the console is not * blank and the line is not found. */ @CheckForNull - String getLineWithKey(@NonNull final Logger logger, @NonNull final EC2Computer computer, @NonNull final String serverHostKeyAlgorithm) { + String getLineWithKey( + @NonNull final Logger logger, + @NonNull final EC2Computer computer, + @NonNull final String serverHostKeyAlgorithm) { String line = null; String console = computer.getDecodedConsoleOutput(); if (console == null) { // The instance is running and the console is blank - EC2Cloud.log(logger, Level.INFO, computer.getListener(), "The instance " + computer.getName() + " has a blank console. Maybe the console is yet not available. If enough time has passed, consider changing the key verification strategy or the AMI used by one printing out the host key in the instance console"); + EC2Cloud.log( + logger, + Level.INFO, + computer.getListener(), + "The instance " + computer.getName() + + " has a blank console. Maybe the console is yet not available. If enough time has passed, consider changing the key verification strategy or the AMI used by one printing out the host key in the instance console"); return null; } @@ -126,7 +141,13 @@ String getLineWithKey(@NonNull final Logger logger, @NonNull final EC2Computer c line = console.substring(start, end); } else { // The instance printed on the console but the key was not printed with the expected format - EC2Cloud.log(logger, Level.INFO, computer.getListener(), String.format("The instance %s didn't print the host key. Expected a line starting with: \"%s\"", computer.getName(), serverHostKeyAlgorithm)); + EC2Cloud.log( + logger, + Level.INFO, + computer.getListener(), + String.format( + "The instance %s didn't print the host key. Expected a line starting with: \"%s\"", + computer.getName(), serverHostKeyAlgorithm)); return ""; } } catch (IllegalArgumentException ignored) { @@ -135,13 +156,20 @@ String getLineWithKey(@NonNull final Logger logger, @NonNull final EC2Computer c } @CheckForNull - HostKey getKeyFromLine(@NonNull final Logger logger, @NonNull final String line, @Nullable final TaskListener listener) { + HostKey getKeyFromLine( + @NonNull final Logger logger, @NonNull final String line, @Nullable final TaskListener listener) { String[] parts = line.split(" "); if (parts.length >= 2) { // The public SSH key in the console is Base64 encoded return new HostKey(parts[0], Base64.getDecoder().decode(parts[1])); } else { - EC2Cloud.log(logger, Level.INFO, listener, String.format("The line with the key doesn't have the required format. Found: \"%s\". Expected a line with this text: \"ALGORITHM THEHOSTKEY\", example: \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJbvbEIoY3tqKwkeRW/L1FnbCLLp8a1TwSOyZHKJqFFR \"", line)); + EC2Cloud.log( + logger, + Level.INFO, + listener, + String.format( + "The line with the key doesn't have the required format. Found: \"%s\". Expected a line with this text: \"ALGORITHM THEHOSTKEY\", example: \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJbvbEIoY3tqKwkeRW/L1FnbCLLp8a1TwSOyZHKJqFFR \"", + line)); return null; } } diff --git a/src/main/java/hudson/plugins/ec2/util/AmazonEC2Factory.java b/src/main/java/hudson/plugins/ec2/util/AmazonEC2Factory.java index 2961a1827..780470b39 100644 --- a/src/main/java/hudson/plugins/ec2/util/AmazonEC2Factory.java +++ b/src/main/java/hudson/plugins/ec2/util/AmazonEC2Factory.java @@ -1,11 +1,9 @@ package hudson.plugins.ec2.util; -import java.net.URL; - import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.services.ec2.AmazonEC2; - import hudson.ExtensionPoint; +import java.net.URL; import jenkins.model.Jenkins; public interface AmazonEC2Factory extends ExtensionPoint { @@ -23,5 +21,4 @@ static AmazonEC2Factory getInstance() { } AmazonEC2 connect(AWSCredentialsProvider credentialsProvider, URL ec2Endpoint); - } diff --git a/src/main/java/hudson/plugins/ec2/util/AmazonEC2FactoryImpl.java b/src/main/java/hudson/plugins/ec2/util/AmazonEC2FactoryImpl.java index e685c4fbb..190b1f921 100644 --- a/src/main/java/hudson/plugins/ec2/util/AmazonEC2FactoryImpl.java +++ b/src/main/java/hudson/plugins/ec2/util/AmazonEC2FactoryImpl.java @@ -1,20 +1,19 @@ package hudson.plugins.ec2.util; -import java.net.URL; - import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.AmazonEC2Client; - import hudson.Extension; import hudson.plugins.ec2.EC2Cloud; +import java.net.URL; @Extension public class AmazonEC2FactoryImpl implements AmazonEC2Factory { @Override public AmazonEC2 connect(AWSCredentialsProvider credentialsProvider, URL ec2Endpoint) { - AmazonEC2 client = new AmazonEC2Client(credentialsProvider, EC2Cloud.createClientConfiguration(ec2Endpoint.getHost())); + AmazonEC2 client = + new AmazonEC2Client(credentialsProvider, EC2Cloud.createClientConfiguration(ec2Endpoint.getHost())); client.setEndpoint(ec2Endpoint.toString()); return client; } diff --git a/src/main/java/hudson/plugins/ec2/util/DeviceMappingParser.java b/src/main/java/hudson/plugins/ec2/util/DeviceMappingParser.java index 3ea5ac7b2..0b67eadb2 100644 --- a/src/main/java/hudson/plugins/ec2/util/DeviceMappingParser.java +++ b/src/main/java/hudson/plugins/ec2/util/DeviceMappingParser.java @@ -25,15 +25,13 @@ import com.amazonaws.services.ec2.model.BlockDeviceMapping; import com.amazonaws.services.ec2.model.EbsBlockDevice; -import org.apache.commons.lang.StringUtils; - import java.util.ArrayList; import java.util.List; +import org.apache.commons.lang.StringUtils; public class DeviceMappingParser { - private DeviceMappingParser() { - } + private DeviceMappingParser() {} public static List parse(String customDeviceMapping) { diff --git a/src/main/java/hudson/plugins/ec2/util/EC2AgentConfig.java b/src/main/java/hudson/plugins/ec2/util/EC2AgentConfig.java index 444c5b88a..527345dec 100644 --- a/src/main/java/hudson/plugins/ec2/util/EC2AgentConfig.java +++ b/src/main/java/hudson/plugins/ec2/util/EC2AgentConfig.java @@ -1,12 +1,11 @@ package hudson.plugins.ec2.util; -import hudson.plugins.ec2.Tenancy; import hudson.model.Node; import hudson.plugins.ec2.AMITypeData; import hudson.plugins.ec2.ConnectionStrategy; import hudson.plugins.ec2.EC2Tag; +import hudson.plugins.ec2.Tenancy; import hudson.slaves.NodeProperty; - import java.util.List; public abstract class EC2AgentConfig { @@ -60,8 +59,10 @@ public static class OnDemand extends EC2AgentConfig { final String publicDNS; final String privateDNS; final Tenancy tenancy; + @Deprecated final boolean useDedicatedTenancy; + final Boolean metadataSupported; final Boolean metadataEndpointEnabled; final Boolean metadataTokensRequired; @@ -92,7 +93,7 @@ private Spot(SpotBuilder builder) { } } - private static abstract class Builder, C extends EC2AgentConfig> { + private abstract static class Builder, C extends EC2AgentConfig> { private String name; private String description; @@ -225,8 +226,10 @@ public static class OnDemandBuilder extends Builder { private String publicDNS; private String privateDNS; private Tenancy tenancy; + @Deprecated private boolean useDedicatedTenancy; + private Boolean metadataSupported; private Boolean metadataEndpointEnabled; private Boolean metadataTokensRequired; @@ -279,16 +282,18 @@ public boolean isUseDedicatedTenancy() { return useDedicatedTenancy; } - public OnDemandBuilder withTenancyAttribute( Tenancy tenancy){ + public OnDemandBuilder withTenancyAttribute(Tenancy tenancy) { this.tenancy = tenancy; return this; } - public Tenancy getTenancyAttribute(){ return tenancy;} + public Tenancy getTenancyAttribute() { + return tenancy; + } public OnDemandBuilder withMetadataSupported(Boolean metadataSupported) { - this.metadataSupported = metadataSupported; - return this; + this.metadataSupported = metadataSupported; + return this; } public OnDemandBuilder withMetadataEndpointEnabled(Boolean metadataEndpointEnabled) { @@ -336,5 +341,4 @@ public Spot build() { return new Spot(this); } } - } diff --git a/src/main/java/hudson/plugins/ec2/util/EC2AgentFactory.java b/src/main/java/hudson/plugins/ec2/util/EC2AgentFactory.java index 59c79945c..aca9d136d 100644 --- a/src/main/java/hudson/plugins/ec2/util/EC2AgentFactory.java +++ b/src/main/java/hudson/plugins/ec2/util/EC2AgentFactory.java @@ -1,9 +1,8 @@ package hudson.plugins.ec2.util; -import java.io.IOException; - import hudson.model.Descriptor; import hudson.plugins.ec2.*; +import java.io.IOException; import jenkins.model.Jenkins; public interface EC2AgentFactory { @@ -23,5 +22,4 @@ static EC2AgentFactory getInstance() { EC2OndemandSlave createOnDemandAgent(EC2AgentConfig.OnDemand config) throws Descriptor.FormException, IOException; EC2SpotSlave createSpotAgent(EC2AgentConfig.Spot config) throws Descriptor.FormException, IOException; - } diff --git a/src/main/java/hudson/plugins/ec2/util/EC2AgentFactoryImpl.java b/src/main/java/hudson/plugins/ec2/util/EC2AgentFactoryImpl.java index d4d4ba372..3e1dd6d95 100644 --- a/src/main/java/hudson/plugins/ec2/util/EC2AgentFactoryImpl.java +++ b/src/main/java/hudson/plugins/ec2/util/EC2AgentFactoryImpl.java @@ -1,10 +1,9 @@ package hudson.plugins.ec2.util; -import java.io.IOException; - import hudson.Extension; import hudson.model.Descriptor; import hudson.plugins.ec2.*; +import java.io.IOException; @Extension public class EC2AgentFactoryImpl implements EC2AgentFactory { @@ -12,11 +11,59 @@ public class EC2AgentFactoryImpl implements EC2AgentFactory { @Override public EC2OndemandSlave createOnDemandAgent(EC2AgentConfig.OnDemand config) throws Descriptor.FormException, IOException { - return new EC2OndemandSlave(config.name, config.instanceId, config.description, config.remoteFS, config.numExecutors, config.labelString, config.mode, config.initScript, config.tmpDir, config.nodeProperties, config.remoteAdmin, config.javaPath, config.jvmopts, config.stopOnTerminate, config.idleTerminationMinutes, config.publicDNS, config.privateDNS, config.tags, config.cloudName, config.launchTimeout, config.amiType, config.connectionStrategy, config.maxTotalUses, config.tenancy, config.metadataEndpointEnabled, config.metadataTokensRequired, config.metadataHopsLimit, config.metadataSupported); + return new EC2OndemandSlave( + config.name, + config.instanceId, + config.description, + config.remoteFS, + config.numExecutors, + config.labelString, + config.mode, + config.initScript, + config.tmpDir, + config.nodeProperties, + config.remoteAdmin, + config.javaPath, + config.jvmopts, + config.stopOnTerminate, + config.idleTerminationMinutes, + config.publicDNS, + config.privateDNS, + config.tags, + config.cloudName, + config.launchTimeout, + config.amiType, + config.connectionStrategy, + config.maxTotalUses, + config.tenancy, + config.metadataEndpointEnabled, + config.metadataTokensRequired, + config.metadataHopsLimit, + config.metadataSupported); } @Override public EC2SpotSlave createSpotAgent(EC2AgentConfig.Spot config) throws Descriptor.FormException, IOException { - return new EC2SpotSlave(config.name, config.spotInstanceRequestId, config.description, config.remoteFS, config.numExecutors, config.mode, config.initScript, config.tmpDir, config.labelString, config.nodeProperties, config.remoteAdmin, config.javaPath, config.jvmopts, config.idleTerminationMinutes, config.tags, config.cloudName, config.launchTimeout, config.amiType, config.connectionStrategy, config.maxTotalUses); + return new EC2SpotSlave( + config.name, + config.spotInstanceRequestId, + config.description, + config.remoteFS, + config.numExecutors, + config.mode, + config.initScript, + config.tmpDir, + config.labelString, + config.nodeProperties, + config.remoteAdmin, + config.javaPath, + config.jvmopts, + config.idleTerminationMinutes, + config.tags, + config.cloudName, + config.launchTimeout, + config.amiType, + config.connectionStrategy, + config.maxTotalUses); } } diff --git a/src/main/java/hudson/plugins/ec2/util/MinimumInstanceChecker.java b/src/main/java/hudson/plugins/ec2/util/MinimumInstanceChecker.java index b573a1dfe..5841d3e5c 100644 --- a/src/main/java/hudson/plugins/ec2/util/MinimumInstanceChecker.java +++ b/src/main/java/hudson/plugins/ec2/util/MinimumInstanceChecker.java @@ -1,23 +1,22 @@ package hudson.plugins.ec2.util; +import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import hudson.plugins.ec2.EC2Cloud; -import hudson.plugins.ec2.EC2Computer; -import hudson.plugins.ec2.SlaveTemplate; import hudson.model.Computer; import hudson.model.Label; import hudson.model.Queue; -import jenkins.model.Jenkins; -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; - -import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.plugins.ec2.EC2Cloud; +import hudson.plugins.ec2.EC2Computer; +import hudson.plugins.ec2.SlaveTemplate; import java.time.Clock; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.Arrays; import java.util.Objects; import java.util.stream.Stream; +import jenkins.model.Jenkins; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; @Restricted(NoExternalUse.class) public class MinimumInstanceChecker { @@ -27,7 +26,7 @@ public class MinimumInstanceChecker { private static Stream agentsForTemplate(@NonNull SlaveTemplate agentTemplate) { return (Stream) Arrays.stream(Jenkins.get().getComputers()) - .filter(computer -> computer instanceof EC2Computer) + .filter(EC2Computer.class::isInstance) .filter(computer -> { SlaveTemplate computerTemplate = ((EC2Computer) computer).getSlaveTemplate(); return computerTemplate != null @@ -41,84 +40,79 @@ public static int countCurrentNumberOfAgents(@NonNull SlaveTemplate agentTemplat public static int countCurrentNumberOfSpareAgents(@NonNull SlaveTemplate agentTemplate) { return (int) agentsForTemplate(agentTemplate) - .filter(computer -> computer.countBusy() == 0) - .filter(computer -> computer.isOnline()) - .count(); + .filter(computer -> computer.countBusy() == 0) + .filter(Computer::isOnline) + .count(); } public static int countCurrentNumberOfProvisioningAgents(@NonNull SlaveTemplate agentTemplate) { return (int) agentsForTemplate(agentTemplate) - .filter(computer -> computer.countBusy() == 0) - .filter(computer -> computer.isOffline()) - .filter(computer -> computer.isConnecting()) - .count(); + .filter(computer -> computer.countBusy() == 0) + .filter(Computer::isOffline) + .filter(Computer::isConnecting) + .count(); } /* Get the number of queued builds that match an AMI (agentTemplate) */ public static int countQueueItemsForAgentTemplate(@NonNull SlaveTemplate agentTemplate) { - return (int) - Queue - .getInstance() - .getBuildableItems() - .stream() - .map((Queue.Item item) -> item.getAssignedLabel()) - .filter(Objects::nonNull) - .filter((Label label) -> label.matches(agentTemplate.getLabelSet())) - .count(); + return (int) Queue.getInstance().getBuildableItems().stream() + .map((Queue.Item item) -> item.getAssignedLabel()) + .filter(Objects::nonNull) + .filter((Label label) -> label.matches(agentTemplate.getLabelSet())) + .count(); } public static void checkForMinimumInstances() { Jenkins.get().clouds.stream() - .filter(cloud -> cloud instanceof EC2Cloud) - .map(cloud -> (EC2Cloud) cloud) - .forEach(cloud -> { - cloud.getTemplates().forEach(agentTemplate -> { - // Minimum instances now have a time range, check to see - // if we are within that time range and return early if not. - if (! minimumInstancesActive(agentTemplate.getMinimumNumberOfInstancesTimeRangeConfig())) { - return; - } - int requiredMinAgents = agentTemplate.getMinimumNumberOfInstances(); - int requiredMinSpareAgents = agentTemplate.getMinimumNumberOfSpareInstances(); - int currentNumberOfAgentsForTemplate = countCurrentNumberOfAgents(agentTemplate); - int currentNumberOfSpareAgentsForTemplate = countCurrentNumberOfSpareAgents(agentTemplate); - int currentNumberOfProvisioningAgentsForTemplate = countCurrentNumberOfProvisioningAgents(agentTemplate); - int currentBuildsWaitingForTemplate = countQueueItemsForAgentTemplate(agentTemplate); - int provisionForMinAgents = 0; - int provisionForMinSpareAgents = 0; - - // Check if we need to provision any agents because we - // don't have the minimum number of agents - provisionForMinAgents = requiredMinAgents - currentNumberOfAgentsForTemplate; - if (provisionForMinAgents < 0){ - provisionForMinAgents = 0; - } - - // Check if we need to provision any agents because we - // don't have the minimum number of spare agents. - // Don't double provision if minAgents and minSpareAgents are set. - provisionForMinSpareAgents = (requiredMinSpareAgents + currentBuildsWaitingForTemplate) - - ( - currentNumberOfSpareAgentsForTemplate + - provisionForMinAgents + - currentNumberOfProvisioningAgentsForTemplate - ); - if (provisionForMinSpareAgents < 0){ - provisionForMinSpareAgents = 0; - } - - int numberToProvision = provisionForMinAgents + provisionForMinSpareAgents; - if (numberToProvision > 0) { - cloud.provision(agentTemplate, numberToProvision); - } + .filter(EC2Cloud.class::isInstance) + .map(cloud -> (EC2Cloud) cloud) + .forEach(cloud -> { + cloud.getTemplates().forEach(agentTemplate -> { + // Minimum instances now have a time range, check to see + // if we are within that time range and return early if not. + if (!minimumInstancesActive(agentTemplate.getMinimumNumberOfInstancesTimeRangeConfig())) { + return; + } + int requiredMinAgents = agentTemplate.getMinimumNumberOfInstances(); + int requiredMinSpareAgents = agentTemplate.getMinimumNumberOfSpareInstances(); + int currentNumberOfAgentsForTemplate = countCurrentNumberOfAgents(agentTemplate); + int currentNumberOfSpareAgentsForTemplate = countCurrentNumberOfSpareAgents(agentTemplate); + int currentNumberOfProvisioningAgentsForTemplate = + countCurrentNumberOfProvisioningAgents(agentTemplate); + int currentBuildsWaitingForTemplate = countQueueItemsForAgentTemplate(agentTemplate); + int provisionForMinAgents = 0; + int provisionForMinSpareAgents = 0; + + // Check if we need to provision any agents because we + // don't have the minimum number of agents + provisionForMinAgents = requiredMinAgents - currentNumberOfAgentsForTemplate; + if (provisionForMinAgents < 0) { + provisionForMinAgents = 0; + } + + // Check if we need to provision any agents because we + // don't have the minimum number of spare agents. + // Don't double provision if minAgents and minSpareAgents are set. + provisionForMinSpareAgents = (requiredMinSpareAgents + currentBuildsWaitingForTemplate) + - (currentNumberOfSpareAgentsForTemplate + + provisionForMinAgents + + currentNumberOfProvisioningAgentsForTemplate); + if (provisionForMinSpareAgents < 0) { + provisionForMinSpareAgents = 0; + } + + int numberToProvision = provisionForMinAgents + provisionForMinSpareAgents; + if (numberToProvision > 0) { + cloud.provision(agentTemplate, numberToProvision); + } + }); }); - }); } public static boolean minimumInstancesActive( - MinimumNumberOfInstancesTimeRangeConfig minimumNumberOfInstancesTimeRangeConfig) { + MinimumNumberOfInstancesTimeRangeConfig minimumNumberOfInstancesTimeRangeConfig) { if (minimumNumberOfInstancesTimeRangeConfig == null) { return true; } @@ -126,7 +120,7 @@ public static boolean minimumInstancesActive( LocalTime toTime = minimumNumberOfInstancesTimeRangeConfig.getMinimumNoInstancesActiveTimeRangeToAsTime(); LocalDateTime now = LocalDateTime.now(clock); - LocalTime nowTime = LocalTime.from(now); //No date. Easier for comparison on time only. + LocalTime nowTime = LocalTime.from(now); // No date. Easier for comparison on time only. boolean passingMidnight = false; if (toTime.isBefore(fromTime)) { @@ -138,7 +132,7 @@ public static boolean minimumInstancesActive( String today = now.getDayOfWeek().name().toLowerCase(); return minimumNumberOfInstancesTimeRangeConfig.getDay(today); } else if (nowTime.isBefore(toTime)) { - //We've gone past midnight and want to check yesterday's setting. + // We've gone past midnight and want to check yesterday's setting. String yesterday = now.minusDays(1).getDayOfWeek().name().toLowerCase(); return minimumNumberOfInstancesTimeRangeConfig.getDay(yesterday); } diff --git a/src/main/java/hudson/plugins/ec2/util/MinimumNumberOfInstancesTimeRangeConfig.java b/src/main/java/hudson/plugins/ec2/util/MinimumNumberOfInstancesTimeRangeConfig.java index 5562245b7..9a4a58a66 100644 --- a/src/main/java/hudson/plugins/ec2/util/MinimumNumberOfInstancesTimeRangeConfig.java +++ b/src/main/java/hudson/plugins/ec2/util/MinimumNumberOfInstancesTimeRangeConfig.java @@ -1,15 +1,12 @@ package hudson.plugins.ec2.util; -import net.sf.json.JSONObject; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.DataBoundSetter; - import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; -import java.util.HashMap; import java.util.Locale; import java.util.Map; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; public class MinimumNumberOfInstancesTimeRangeConfig { @@ -28,10 +25,8 @@ public class MinimumNumberOfInstancesTimeRangeConfig { private Boolean saturday; private Boolean sunday; - @DataBoundConstructor - public MinimumNumberOfInstancesTimeRangeConfig() { - } + public MinimumNumberOfInstancesTimeRangeConfig() {} protected Object readResolve() { if (minimumNoInstancesActiveTimeRangeDays != null && !minimumNoInstancesActiveTimeRangeDays.isEmpty()) { @@ -157,14 +152,22 @@ public void setSunday(Boolean sunday) { public boolean getDay(String day) { switch (day.toLowerCase()) { - case "monday": return Boolean.TRUE.equals(this.monday); - case "tuesday": return Boolean.TRUE.equals(this.tuesday); - case "wednesday": return Boolean.TRUE.equals(this.wednesday); - case "thursday": return Boolean.TRUE.equals(this.thursday); - case "friday": return Boolean.TRUE.equals(this.friday); - case "saturday": return Boolean.TRUE.equals(this.saturday); - case "sunday": return Boolean.TRUE.equals(this.sunday); - default: throw new IllegalArgumentException("Can only get days"); + case "monday": + return Boolean.TRUE.equals(this.monday); + case "tuesday": + return Boolean.TRUE.equals(this.tuesday); + case "wednesday": + return Boolean.TRUE.equals(this.wednesday); + case "thursday": + return Boolean.TRUE.equals(this.thursday); + case "friday": + return Boolean.TRUE.equals(this.friday); + case "saturday": + return Boolean.TRUE.equals(this.saturday); + case "sunday": + return Boolean.TRUE.equals(this.sunday); + default: + throw new IllegalArgumentException("Can only get days"); } } } diff --git a/src/main/java/hudson/plugins/ec2/util/ResettableCountDownLatch.java b/src/main/java/hudson/plugins/ec2/util/ResettableCountDownLatch.java index e39c3adb6..aa97c36b4 100644 --- a/src/main/java/hudson/plugins/ec2/util/ResettableCountDownLatch.java +++ b/src/main/java/hudson/plugins/ec2/util/ResettableCountDownLatch.java @@ -3,9 +3,8 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; - -import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; @Restricted(NoExternalUse.class) public class ResettableCountDownLatch { @@ -44,4 +43,4 @@ public boolean await(long timeout, TimeUnit unit) throws InterruptedException { public long getCount() { return latchHolder.get().getCount(); } -} \ No newline at end of file +} diff --git a/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java b/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java index 3795be6fd..85809c9e6 100644 --- a/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java +++ b/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java @@ -1,43 +1,38 @@ package hudson.plugins.ec2.win; +import com.amazonaws.AmazonClientException; +import com.amazonaws.services.ec2.model.GetPasswordDataRequest; +import com.amazonaws.services.ec2.model.GetPasswordDataResult; +import com.amazonaws.services.ec2.model.Instance; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Util; import hudson.model.Descriptor; import hudson.model.TaskListener; +import hudson.os.WindowsUtil; import hudson.plugins.ec2.*; import hudson.plugins.ec2.win.winrm.WindowsProcess; import hudson.remoting.Channel; import hudson.remoting.Channel.Listener; import hudson.slaves.ComputerLauncher; -import hudson.Util; -import hudson.os.WindowsUtil; - +import hudson.slaves.OfflineCause; import java.io.EOFException; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; - -import hudson.slaves.OfflineCause; -import edu.umd.cs.findbugs.annotations.NonNull; - +import javax.net.ssl.SSLException; import jenkins.model.Jenkins; import org.apache.commons.io.IOUtils; -import com.amazonaws.AmazonClientException; -import com.amazonaws.services.ec2.model.Instance; -import com.amazonaws.services.ec2.model.GetPasswordDataRequest; -import com.amazonaws.services.ec2.model.GetPasswordDataResult; - -import javax.net.ssl.SSLException; - public class EC2WindowsLauncher extends EC2ComputerLauncher { private static final String AGENT_JAR = "remoting.jar"; final long sleepBetweenAttempts = TimeUnit.SECONDS.toMillis(10); @Override - protected void launchScript(EC2Computer computer, TaskListener listener) throws IOException, - AmazonClientException, InterruptedException { + protected void launchScript(EC2Computer computer, TaskListener listener) + throws IOException, AmazonClientException, InterruptedException { final PrintStream logger = listener.getLogger(); EC2AbstractSlave node = computer.getNode(); if (node == null) { @@ -50,10 +45,11 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws } final WinConnection connection = connectToWinRM(computer, node, template, logger); - + try { String initScript = node.initScript; - String tmpDir = (node.tmpDir != null && !node.tmpDir.equals("") ? WindowsUtil.quoteArgument(Util.ensureEndsWith(node.tmpDir,"\\")) + String tmpDir = (node.tmpDir != null && !node.tmpDir.equals("") + ? WindowsUtil.quoteArgument(Util.ensureEndsWith(node.tmpDir, "\\")) : "C:\\Windows\\Temp\\"); logger.println("Creating tmp directory if it does not exist"); @@ -66,7 +62,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws if (initScript != null && initScript.trim().length() > 0 && !connection.exists(tmpDir + ".jenkins-init")) { logger.println("Executing init script"); - try(OutputStream init = connection.putFile(tmpDir + "init.bat")) { + try (OutputStream init = connection.putFile(tmpDir + "init.bat")) { init.write(initScript.getBytes("utf-8")); } @@ -79,13 +75,13 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws return; } - try(OutputStream initGuard = connection.putFile(tmpDir + ".jenkins-init")) { + try (OutputStream initGuard = connection.putFile(tmpDir + ".jenkins-init")) { initGuard.write("init ran".getBytes(StandardCharsets.UTF_8)); } logger.println("init script ran successfully"); } - try(OutputStream agentJar = connection.putFile(tmpDir + AGENT_JAR)) { + try (OutputStream agentJar = connection.putFile(tmpDir + AGENT_JAR)) { agentJar.write(Jenkins.get().getJnlpJars(AGENT_JAR).readFully()); } @@ -95,7 +91,8 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws final String jvmopts = node.jvmopts; final String remoteFS = WindowsUtil.quoteArgument(node.getRemoteFS()); final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; - final String launchString = javaPath + " " + (jvmopts != null ? jvmopts : "") + " -jar " + tmpDir + AGENT_JAR + " -workDir " + workDir; + final String launchString = javaPath + " " + (jvmopts != null ? jvmopts : "") + " -jar " + tmpDir + + AGENT_JAR + " -workDir " + workDir; logger.println("Launching via WinRM:" + launchString); final WindowsProcess process = connection.execute(launchString, 86400); computer.setChannel(process.getStdout(), process.getStdin(), logger, new Listener() { @@ -106,11 +103,14 @@ public void onClosed(Channel channel, IOException cause) { } }); } catch (EOFException eof) { - // When we launch java with connection.execute(launchString... it keeps running, but if java is not installed - //the computer.setChannel fails with EOFException because the stream is already closed. It fails on - // setChannel - build - negotiate - is.read() == -1. Let's print a clear message to help diagnose the problem + // When we launch java with connection.execute(launchString... it keeps running, but if java is not + // installed + // the computer.setChannel fails with EOFException because the stream is already closed. It fails on + // setChannel - build - negotiate - is.read() == -1. Let's print a clear message to help diagnose the + // problem // In other case you see a EOFException which gives you few clues about the problem. - logger.println("The stream with the java process on the instance was closed. Maybe java is not installed there."); + logger.println( + "The stream with the java process on the instance was closed. Maybe java is not installed there."); eof.printStackTrace(logger); } catch (Throwable ioe) { logger.println("Ouch:"); @@ -121,8 +121,9 @@ public void onClosed(Channel channel, IOException cause) { } @NonNull - private WinConnection connectToWinRM(EC2Computer computer, EC2AbstractSlave node, SlaveTemplate template, PrintStream logger) throws AmazonClientException, - InterruptedException { + private WinConnection connectToWinRM( + EC2Computer computer, EC2AbstractSlave node, SlaveTemplate template, PrintStream logger) + throws AmazonClientException, InterruptedException { final long minTimeout = 3000; long timeout = node.getLaunchTimeoutInMillis(); // timeout is less than 0 when jenkins is booting up. if (timeout < minTimeout) { @@ -139,8 +140,8 @@ private WinConnection connectToWinRM(EC2Computer computer, EC2AbstractSlave node try { long waitTime = System.currentTimeMillis() - startTime; if (waitTime > timeout) { - throw new AmazonClientException("Timed out after " + (waitTime / 1000) - + " seconds of waiting for winrm to be connected"); + throw new AmazonClientException( + "Timed out after " + (waitTime / 1000) + " seconds of waiting for winrm to be connected"); } if (connection == null) { @@ -149,14 +150,17 @@ private WinConnection connectToWinRM(EC2Computer computer, EC2AbstractSlave node // Check when host is null or we will keep trying and receiving a hostname cannot be null forever. if (host == null || "0.0.0.0".equals(host)) { - logger.println("Invalid host (null or 0.0.0.0). Your host is most likely waiting for an IP address."); + logger.println( + "Invalid host (null or 0.0.0.0). Your host is most likely waiting for an IP address."); throw new IOException("goto sleep"); } if (!node.isSpecifyPassword()) { GetPasswordDataResult result; try { - result = node.getCloud().connect().getPasswordData(new GetPasswordDataRequest(instance.getInstanceId())); + result = node.getCloud() + .connect() + .getPasswordData(new GetPasswordDataRequest(instance.getInstanceId())); } catch (Exception e) { logger.println("Unexpected Exception: " + e.toString()); Thread.sleep(sleepBetweenAttempts); @@ -169,20 +173,26 @@ private WinConnection connectToWinRM(EC2Computer computer, EC2AbstractSlave node continue; } EC2PrivateKey ec2PrivateKey = node.getCloud().resolvePrivateKey(); - if (ec2PrivateKey == null){ - logger.println("Waiting for privateKey to be available. Consider checking the credentials in the cloud configuration. Sleeping 10s."); + if (ec2PrivateKey == null) { + logger.println( + "Waiting for privateKey to be available. Consider checking the credentials in the cloud configuration. Sleeping 10s."); Thread.sleep(sleepBetweenAttempts); continue; } String password = ec2PrivateKey.decryptWindowsPassword(passwordData); if (!node.getRemoteAdmin().equals("Administrator")) { - logger.println("WARNING: For password retrieval remote admin must be Administrator, ignoring user provided value"); + logger.println( + "WARNING: For password retrieval remote admin must be Administrator, ignoring user provided value"); } logger.println("Connecting to " + "(" + host + ") with WinRM as Administrator"); connection = new WinConnection(host, "Administrator", password, allowSelfSignedCertificate); - } else { //password Specified + } else { // password Specified logger.println("Connecting to " + "(" + host + ") with WinRM as " + node.getRemoteAdmin()); - connection = new WinConnection(host, node.getRemoteAdmin(), node.getAdminPassword().getPlainText(), allowSelfSignedCertificate); + connection = new WinConnection( + host, + node.getRemoteAdmin(), + node.getAdminPassword().getPlainText(), + allowSelfSignedCertificate); } connection.setUseHTTPS(node.isUseHTTPS()); } @@ -196,8 +206,8 @@ private WinConnection connectToWinRM(EC2Computer computer, EC2AbstractSlave node if (!alreadyBooted || node.stopOnTerminate) { int bootDelay = node.getBootDelay(); if (bootDelay > 0) { - logger.println("WinRM service responded. Waiting " + bootDelay + "ms for WinRM service to stabilize on " - + node.getDisplayName()); + logger.println("WinRM service responded. Waiting " + bootDelay + + "ms for WinRM service to stabilize on " + node.getDisplayName()); Thread.sleep(bootDelay); logger.println("WinRM should now be ok on " + node.getDisplayName()); } @@ -215,7 +225,8 @@ private WinConnection connectToWinRM(EC2Computer computer, EC2AbstractSlave node if (e instanceof SSLException) { // To avoid reconnecting continuously computer.setTemporarilyOffline(true, OfflineCause.create(Messages._OfflineCause_SSLException())); - // avoid waiting and trying again, this connection needs human intervention to change the certificate + // avoid waiting and trying again, this connection needs human intervention to change the + // certificate throw new AmazonClientException("The SSL connection failed while negotiating SSL", e); } logger.println("Waiting for WinRM to come up. Sleeping 10s."); diff --git a/src/main/java/hudson/plugins/ec2/win/SelfSignedCertificateAllowedMonitor.java b/src/main/java/hudson/plugins/ec2/win/SelfSignedCertificateAllowedMonitor.java index 9b6d16547..3ecebe0de 100644 --- a/src/main/java/hudson/plugins/ec2/win/SelfSignedCertificateAllowedMonitor.java +++ b/src/main/java/hudson/plugins/ec2/win/SelfSignedCertificateAllowedMonitor.java @@ -30,21 +30,20 @@ import hudson.plugins.ec2.SlaveTemplate; import hudson.plugins.ec2.WindowsData; import hudson.slaves.Cloud; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; import jenkins.model.Jenkins; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.interceptor.RequirePOST; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.ListIterator; - @Extension public class SelfSignedCertificateAllowedMonitor extends AdministrativeMonitor { - private final static int MAX_TEMPLATES_FOUND = 5; - + private static final int MAX_TEMPLATES_FOUND = 5; + List insecureTemplates = new ArrayList<>(MAX_TEMPLATES_FOUND); @Override @@ -65,15 +64,15 @@ public String getSelfSignedCertAllowedTemplates() { @Override public boolean isActivated() { boolean maxTemplatesReached = false; - + ListIterator cloudIterator = Jenkins.get().clouds.listIterator(); - + // Let's clear the previously calculated wrong templates to populate the lists with them again insecureTemplates.clear(); - + while (cloudIterator.hasNext() && !maxTemplatesReached) { Cloud cloud = cloudIterator.next(); - if (cloud instanceof EC2Cloud) { + if (cloud instanceof EC2Cloud) { maxTemplatesReached = gatherInsecureTemplate((EC2Cloud) cloud); } } @@ -90,7 +89,9 @@ private boolean gatherInsecureTemplate(EC2Cloud cloud) { } AMITypeData amiTypeData = template.getAmiType(); - if (insecureTemplates.size() < MAX_TEMPLATES_FOUND && amiTypeData.isWindows() && ((WindowsData)amiTypeData).isAllowSelfSignedCertificate()) { + if (insecureTemplates.size() < MAX_TEMPLATES_FOUND + && amiTypeData.isWindows() + && ((WindowsData) amiTypeData).isAllowSelfSignedCertificate()) { // it is insecure insecureTemplates.add(template.getDisplayName()); } @@ -100,17 +101,17 @@ private boolean gatherInsecureTemplate(EC2Cloud cloud) { return true; } } - + return false; } - + @RequirePOST @SuppressWarnings("unused") // used by message.jelly public HttpResponse doAct(@QueryParameter String dismiss) throws IOException { Jenkins.get().checkPermission(Jenkins.ADMINISTER); if (dismiss != null) { disable(true); - } + } return HttpResponses.forwardToPreviousPage(); } } diff --git a/src/main/java/hudson/plugins/ec2/win/WinConnection.java b/src/main/java/hudson/plugins/ec2/win/WinConnection.java index c807dd68a..b82684a59 100644 --- a/src/main/java/hudson/plugins/ec2/win/WinConnection.java +++ b/src/main/java/hudson/plugins/ec2/win/WinConnection.java @@ -1,30 +1,25 @@ package hudson.plugins.ec2.win; +import com.hierynomus.msdtyp.AccessMask; +import com.hierynomus.mssmb2.SMB2CreateDisposition; +import com.hierynomus.mssmb2.SMB2ShareAccess; import com.hierynomus.protocol.transport.TransportException; -import com.hierynomus.security.bc.BCSecurityProvider; -import com.hierynomus.smbj.SmbConfig; +import com.hierynomus.smbj.SMBClient; +import com.hierynomus.smbj.auth.AuthenticationContext; +import com.hierynomus.smbj.connection.Connection; +import com.hierynomus.smbj.session.Session; +import com.hierynomus.smbj.share.DiskShare; import hudson.plugins.ec2.win.winrm.WinRM; import hudson.plugins.ec2.win.winrm.WindowsProcess; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.util.EnumSet; - -import com.hierynomus.smbj.auth.AuthenticationContext; -import com.hierynomus.smbj.SMBClient; -import com.hierynomus.smbj.share.DiskShare; -import com.hierynomus.smbj.connection.Connection; -import com.hierynomus.smbj.session.Session; -import com.hierynomus.msdtyp.AccessMask; -import com.hierynomus.mssmb2.SMB2ShareAccess; -import com.hierynomus.mssmb2.SMB2CreateDisposition; - -import javax.net.ssl.SSLException; import java.util.logging.Level; import java.util.logging.Logger; +import javax.net.ssl.SSLException; public class WinConnection { private static final Logger LOGGER = Logger.getLogger(WinConnection.class.getName()); @@ -40,14 +35,14 @@ public class WinConnection { private Session session; private boolean useHTTPS; - private static final int TIMEOUT=8000; //8 seconds + private static final int TIMEOUT = 8000; // 8 seconds private boolean allowSelfSignedCertificate; @Deprecated public WinConnection(String host, String username, String password) { this(host, username, password, true); } - + public WinConnection(String host, String username, String password, boolean allowSelfSignedCertificate) { this.host = host; this.username = username; @@ -78,31 +73,31 @@ public WindowsProcess execute(String commandLine, int timeout) { } private DiskShare getSmbShare(String path) throws IOException { - if(this.connection == null) { + if (this.connection == null) { this.connection = smbclient.connect(host); } - if(this.session == null) { + if (this.session == null) { this.session = connection.authenticate(this.authentication); } return (DiskShare) session.connectShare(toAdministrativeShare(path)); } public OutputStream putFile(String path) throws IOException { - return getSmbShare(path).openFile(toFilePath(path), - EnumSet.of(AccessMask.GENERIC_READ, - AccessMask.GENERIC_WRITE), - null, - SMB2ShareAccess.ALL, - SMB2CreateDisposition.FILE_OVERWRITE_IF, - null).getOutputStream(); + return getSmbShare(path) + .openFile( + toFilePath(path), + EnumSet.of(AccessMask.GENERIC_READ, AccessMask.GENERIC_WRITE), + null, + SMB2ShareAccess.ALL, + SMB2CreateDisposition.FILE_OVERWRITE_IF, + null) + .getOutputStream(); } public InputStream getFile(String path) throws IOException { - return getSmbShare(path).openFile(toFilePath(path), - EnumSet.of(AccessMask.GENERIC_READ), - null, SMB2ShareAccess.ALL, - null, - null).getInputStream(); + return getSmbShare(path) + .openFile(toFilePath(path), EnumSet.of(AccessMask.GENERIC_READ), null, SMB2ShareAccess.ALL, null, null) + .getInputStream(); } public boolean exists(String path) throws IOException { @@ -115,7 +110,7 @@ private static String toAdministrativeShare(String path) { } private static String toFilePath(String path) { - //Strip drive and leading forward slash + // Strip drive and leading forward slash return path.substring(3); } @@ -127,14 +122,12 @@ public boolean ping() { return false; } } - + public boolean pingFailingIfSSHHandShakeError() throws IOException { LOGGER.log(Level.FINE, () -> "checking SMB connection to " + host); - try ( - Socket socket = new Socket(); - Connection connection = smbclient.connect(host); - Session session = connection.authenticate(authentication); - ) { + try (Socket socket = new Socket(); + Connection connection = smbclient.connect(host); + Session session = connection.authenticate(authentication); ) { socket.connect(new InetSocketAddress(host, 445), TIMEOUT); winrm().ping(); session.connectShare("IPC$"); @@ -154,21 +147,21 @@ public boolean pingFailingIfSSHHandShakeError() throws IOException { } public void close() { - if(this.session != null) { + if (this.session != null) { try { this.session.close(); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Failed to close session", e); } } - if(this.connection != null) { + if (this.connection != null) { try { this.connection.close(); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Failed to close connection", e); } } - if(this.smbclient != null) { + if (this.smbclient != null) { try { this.smbclient.close(); } catch (Exception e) { diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/NegotiateNTLMSchemaFactory.java b/src/main/java/hudson/plugins/ec2/win/winrm/NegotiateNTLMSchemaFactory.java index 9583553ab..749f331d7 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/NegotiateNTLMSchemaFactory.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/NegotiateNTLMSchemaFactory.java @@ -11,6 +11,7 @@ public class NegotiateNTLMSchemaFactory implements AuthSchemeProvider { + @Override public AuthScheme create(HttpContext context) { return new NegotiateNTLM(); } @@ -25,10 +26,11 @@ public String getSchemeName() { public Header authenticate(Credentials credentials, HttpRequest request) throws AuthenticationException { Credentials ntCredentials = credentials; if (!(credentials instanceof NTCredentials)) { - ntCredentials = new NTCredentials(credentials.getUserPrincipal().getName(), credentials.getPassword(), null, null); + ntCredentials = new NTCredentials( + credentials.getUserPrincipal().getName(), credentials.getPassword(), null, null); } Header header = super.authenticate(ntCredentials, request); - //need replace NTLM with Negotiate + // need replace NTLM with Negotiate CharArrayBuffer buffer = new CharArrayBuffer(512); buffer.append(header.getName()); buffer.append(": "); diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/WinRM.java b/src/main/java/hudson/plugins/ec2/win/winrm/WinRM.java index ed34b5063..c820fa8fe 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/WinRM.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/WinRM.java @@ -20,7 +20,7 @@ public class WinRM { public WinRM(String host, String username, String password) { this(host, username, password, true); } - + public WinRM(String host, String username, String password, boolean allowSelfSignedCertificate) { this.host = host; this.username = username; @@ -101,7 +101,7 @@ public void setTimeout(int timeout) { * # Convert the number of seconds to an ISO8601 duration format # @see * http://tools.ietf.org/html/rfc2445#section-4.3.6 # @param [Fixnum] seconds The amount of seconds for this * duration - * + * * @param seconds * @return */ diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java index 2e76ebbf1..f9d1beae2 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java @@ -14,7 +14,6 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; - import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.ParseException; @@ -22,10 +21,10 @@ import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.HttpClient; import org.apache.http.client.config.AuthSchemes; import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.config.Lookup; import org.apache.http.config.RegistryBuilder; @@ -66,7 +65,7 @@ public class WinRMClient { private boolean useHTTPS; private BasicCredentialsProvider credsProvider; private final boolean allowSelfSignedCertificate; - + @Deprecated public WinRMClient(URL url, String username, String password) { this(url, username, password, false); @@ -78,7 +77,7 @@ public WinRMClient(URL url, String username, String password, boolean allowSelfS this.password = password; this.factory = new RequestFactory(url); this.allowSelfSignedCertificate = allowSelfSignedCertificate; - + setupHTTPClient(); } @@ -93,7 +92,7 @@ public void executeCommand(String command) { LOGGER.log(Level.FINE, () -> "winrm execute on " + shellId + " command: " + command); Document request = factory.newExecuteCommandRequest(shellId, command).build(); commandId = first(sendRequest(request), "//" + Namespaces.NS_WIN_SHELL.getPrefix() + ":CommandId"); - LOGGER.log(Level.FINER, ()-> "winrm started execution on " + shellId + " commandId: " + commandId); + LOGGER.log(Level.FINER, () -> "winrm started execution on " + shellId + " commandId: " + commandId); } public void deleteShell() { @@ -105,7 +104,6 @@ public void deleteShell() { Document request = factory.newDeleteShellRequest(shellId).build(); sendRequest(request); - } public void signal() { @@ -122,13 +120,16 @@ public void signal() { public void sendInput(byte[] input) { LOGGER.log(Level.FINE, () -> "--> sending " + input.length); - Document request = factory.newSendInputRequest(input, shellId, commandId).build(); + Document request = + factory.newSendInputRequest(input, shellId, commandId).build(); sendRequest(request); } public boolean slurpOutput(FastPipedOutputStream stdout, FastPipedOutputStream stderr) throws IOException { LOGGER.log(Level.FINE, () -> "--> SlurpOutput"); - Map streams = new HashMap<>(); streams.put("stdout", stdout); streams.put("stderr", stderr); + Map streams = new HashMap<>(); + streams.put("stdout", stdout); + streams.put("stderr", stderr); Document request = factory.newGetOutputRequest(shellId, commandId).build(); Document response = sendRequest(request); @@ -141,16 +142,20 @@ public boolean slurpOutput(FastPipedOutputStream stdout, FastPipedOutputStream s for (Node node : xpath.selectNodes(response)) { if (node instanceof Element) { Element e = (Element) node; - FastPipedOutputStream stream = streams.get(e.attribute("Name").getText().toLowerCase()); + FastPipedOutputStream stream = + streams.get(e.attribute("Name").getText().toLowerCase()); final byte[] decode = Base64.getDecoder().decode(e.getText()); - LOGGER.log(Level.FINE, () -> "piping " + decode.length + " bytes from " - + e.attribute("Name").getText().toLowerCase()); + LOGGER.log( + Level.FINE, + () -> "piping " + decode.length + " bytes from " + + e.attribute("Name").getText().toLowerCase()); stream.write(decode); } } - XPath done = DocumentHelper.createXPath("//*[@State='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done']"); + XPath done = DocumentHelper.createXPath( + "//*[@State='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done']"); done.setNamespaceContext(namespaceContext); final List nodes = done.selectNodes(response); if (nodes != null && nodes.isEmpty()) { @@ -178,46 +183,44 @@ private static String first(Document doc, String selector) { private void setupHTTPClient() { credsProvider = new BasicCredentialsProvider(); - credsProvider.setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT), new UsernamePasswordCredentials(username, password)); + credsProvider.setCredentials( + new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT), + new UsernamePasswordCredentials(username, password)); } private HttpClient buildHTTPClient() { HttpClientBuilder builder = HttpClientBuilder.create().setDefaultCredentialsProvider(credsProvider); - if(! (username.contains("\\")|| username.contains("/"))) { - //user is not a domain user + if (!(username.contains("\\") || username.contains("/"))) { + // user is not a domain user Lookup authSchemeRegistry = RegistryBuilder.create() - .register(AuthSchemes.BASIC, new BasicSchemeFactory()) - .register(AuthSchemes.SPNEGO,new NegotiateNTLMSchemaFactory()) - .build(); + .register(AuthSchemes.BASIC, new BasicSchemeFactory()) + .register(AuthSchemes.SPNEGO, new NegotiateNTLMSchemaFactory()) + .build(); builder.setDefaultAuthSchemeRegistry(authSchemeRegistry); } if (useHTTPS) { - WinRMConnectionManagerFactory.WinRMHttpConnectionManager connectionManager = - allowSelfSignedCertificate ? WinRMConnectionManagerFactory.SSL_ALLOW_SELF_SIGNED - : WinRMConnectionManagerFactory.SSL; + WinRMConnectionManagerFactory.WinRMHttpConnectionManager connectionManager = allowSelfSignedCertificate + ? WinRMConnectionManagerFactory.SSL_ALLOW_SELF_SIGNED + : WinRMConnectionManagerFactory.SSL; builder.setSSLSocketFactory(connectionManager.getSocketFactory()); builder.setConnectionManager(connectionManager.getConnectionManager()); } else { builder.setConnectionManager(WinRMConnectionManagerFactory.DEFAULT.getConnectionManager()); } RequestConfig requestConfig = RequestConfig.custom() - .setConnectTimeout(5000) - .setSocketTimeout(0) - .build(); + .setConnectTimeout(5000) + .setSocketTimeout(0) + .build(); builder.setDefaultRequestConfig(requestConfig); - SocketConfig socketConfig = SocketConfig.custom() - .setTcpNoDelay(true) - .build(); + SocketConfig socketConfig = SocketConfig.custom().setTcpNoDelay(true).build(); builder.setDefaultSocketConfig(socketConfig); // Add sleep between re-tries, by-default the call gets retried immediately. builder.setRetryHandler(new DefaultHttpRequestRetryHandler() { @Override public boolean retryRequest( - final IOException exception, - final int executionCount, - final HttpContext context) { + final IOException exception, final int executionCount, final HttpContext context) { boolean retryRequest = super.retryRequest(exception, executionCount, context); - if ( retryRequest ) { + if (retryRequest) { // sleep before retrying, increase the sleep time on each re-try int sleepTime = executionCount * 5; try { @@ -266,7 +269,10 @@ private Document sendRequest(Document request, int retry) { // check for possible timeout if (response.getStatusLine().getStatusCode() == 500 - && (responseEntity.getContentType() != null && entity.getContentType().getValue().startsWith(ContentType.APPLICATION_SOAP_XML.getMimeType()))) { + && (responseEntity.getContentType() != null + && entity.getContentType() + .getValue() + .startsWith(ContentType.APPLICATION_SOAP_XML.getMimeType()))) { String respStr = EntityUtils.toString(responseEntity); if (respStr.contains("TimedOut")) { return DocumentHelper.parseText(respStr); @@ -279,7 +285,8 @@ private Document sendRequest(Document request, int retry) { if (response.getStatusLine().getStatusCode() == 401) { // we need to force using new connections here // throw away our auth cache - LOGGER.log(Level.WARNING, "winrm returned 401 - shouldn't happen though - retrying in 2 minutes"); + LOGGER.log( + Level.WARNING, "winrm returned 401 - shouldn't happen though - retrying in 2 minutes"); try { Thread.sleep(TimeUnit.MINUTES.toMillis(2)); } catch (InterruptedException e) { @@ -289,12 +296,15 @@ private Document sendRequest(Document request, int retry) { LOGGER.log(Level.WARNING, "winrm returned 401 - retrying now"); return sendRequest(request, ++retry); } - LOGGER.log(Level.WARNING, "winrm service " + shellId + " unexpected HTTP Response (" - + response.getStatusLine().getReasonPhrase() + "): " - + EntityUtils.toString(response.getEntity())); - - throw new RuntimeException("Unexpected HTTP response " + response.getStatusLine().getStatusCode() - + " on " + url + ": " + response.getStatusLine().getReasonPhrase()); + LOGGER.log( + Level.WARNING, + "winrm service " + shellId + " unexpected HTTP Response (" + + response.getStatusLine().getReasonPhrase() + "): " + + EntityUtils.toString(response.getEntity())); + + throw new RuntimeException("Unexpected HTTP response " + + response.getStatusLine().getStatusCode() + " on " + url + ": " + + response.getStatusLine().getReasonPhrase()); } } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectException.java b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectException.java index 85db112f3..368f036e8 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectException.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectException.java @@ -5,5 +5,4 @@ public class WinRMConnectException extends RuntimeIOException { public WinRMConnectException(String message, Throwable cause) { super(message, cause); } - } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectionManagerFactory.java b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectionManagerFactory.java index 0721343c5..c25c0e7a5 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectionManagerFactory.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectionManagerFactory.java @@ -1,5 +1,10 @@ package hudson.plugins.ec2.win.winrm; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.util.logging.Level; +import java.util.logging.Logger; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.socket.ConnectionSocketFactory; @@ -9,12 +14,6 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.ssl.SSLContextBuilder; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.util.logging.Level; -import java.util.logging.Logger; - public class WinRMConnectionManagerFactory { private static final Logger log = Logger.getLogger(WinRMClient.class.getName()); @@ -26,8 +25,8 @@ static class WinRMHttpConnectionManager { private final PoolingHttpClientConnectionManager connectionManager; private SSLConnectionSocketFactory socketFactory; - final static int DEFAULT_MAX_PER_ROUTE = 50; - final static int MAX_TOTAL = 2500; + static final int DEFAULT_MAX_PER_ROUTE = 50; + static final int MAX_TOTAL = 2500; WinRMHttpConnectionManager() { connectionManager = new PoolingHttpClientConnectionManager(); @@ -54,7 +53,9 @@ private Registry getSslSocketFactory(boolean allowSelfS try { if (allowSelfSignedCertificate) { this.socketFactory = new SSLConnectionSocketFactory( - new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build(), + new SSLContextBuilder() + .loadTrustMaterial(null, new TrustSelfSignedStrategy()) + .build(), NoopHostnameVerifier.INSTANCE); log.log(Level.FINE, "Allowing self-signed certificates"); } else { @@ -70,4 +71,4 @@ private Registry getSslSocketFactory(boolean allowSelfS .build(); } } -} \ No newline at end of file +} diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/WindowsProcess.java b/src/main/java/hudson/plugins/ec2/win/winrm/WindowsProcess.java index d255b700c..bbea1ef3f 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/WindowsProcess.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/WindowsProcess.java @@ -102,7 +102,7 @@ private void startStdoutCopyThread() { @Override public void run() { try { - for (;;) { + for (; ; ) { if (!client.slurpOutput(toCallersStdout, toCallersStderr)) { LOGGER.log(Level.FINE, () -> "no more output for " + command); break; @@ -127,13 +127,15 @@ private void startStdinCopyThread() { public void run() { try { byte[] buf = new byte[INPUT_BUFFER]; - for (;;) { + for (; ; ) { int n = toCallersStdin.read(buf); - if (n == -1) + if (n == -1) { break; - if (n == 0) + } + if (n == 0) { continue; + } byte[] bufToSend = new byte[n]; System.arraycopy(buf, 0, bufToSend, 0, n); diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/request/AbstractWinRMRequest.java b/src/main/java/hudson/plugins/ec2/win/winrm/request/AbstractWinRMRequest.java index 14bd5d1e4..0d9a1c6e1 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/request/AbstractWinRMRequest.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/request/AbstractWinRMRequest.java @@ -1,13 +1,11 @@ package hudson.plugins.ec2.win.winrm.request; +import hudson.plugins.ec2.win.winrm.soap.HeaderBuilder; +import hudson.plugins.ec2.win.winrm.soap.MessageBuilder; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.UUID; - -import hudson.plugins.ec2.win.winrm.soap.HeaderBuilder; -import hudson.plugins.ec2.win.winrm.soap.MessageBuilder; - import org.dom4j.Document; import org.dom4j.Element; @@ -28,13 +26,19 @@ public AbstractWinRMRequest(URL url) { protected abstract void construct(); + @Override public Document build() { construct(); return message.build(); } protected HeaderBuilder defaultHeader() throws URISyntaxException { - return header.to(url.toURI()).replyTo(new URI("http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous")).maxEnvelopeSize(envelopSize).id(generateUUID()).locale(locale).timeout(timeout); + return header.to(url.toURI()) + .replyTo(new URI("http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous")) + .maxEnvelopeSize(envelopSize) + .id(generateUUID()) + .locale(locale) + .timeout(timeout); } protected void setBody(Element body) { @@ -69,5 +73,4 @@ public String getLocale() { public void setLocale(String locale) { this.locale = locale; } - } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/request/DeleteShellRequest.java b/src/main/java/hudson/plugins/ec2/win/winrm/request/DeleteShellRequest.java index 5bdb7f673..3646355c6 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/request/DeleteShellRequest.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/request/DeleteShellRequest.java @@ -16,12 +16,14 @@ public DeleteShellRequest(URL url, String shellId) { @Override protected void construct() { try { - defaultHeader().action(new URI("http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete")).shellId(shellId).resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")); + defaultHeader() + .action(new URI("http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete")) + .shellId(shellId) + .resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")); setBody(null); } catch (URISyntaxException e) { throw new RuntimeException("Error while building request content", e); } } - } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/request/ExecuteCommandRequest.java b/src/main/java/hudson/plugins/ec2/win/winrm/request/ExecuteCommandRequest.java index df936b63d..9e3db9d0e 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/request/ExecuteCommandRequest.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/request/ExecuteCommandRequest.java @@ -2,12 +2,10 @@ import hudson.plugins.ec2.win.winrm.soap.Namespaces; import hudson.plugins.ec2.win.winrm.soap.Option; - import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Collections; - import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.QName; @@ -26,7 +24,11 @@ public ExecuteCommandRequest(URL url, String shellId, String command) { @Override protected void construct() { try { - defaultHeader().action(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command")).resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")).shellId(shellId).options(Collections.singletonList(new Option("WINRS_CONSOLEMODE_STDIN", "FALSE"))); + defaultHeader() + .action(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command")) + .resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")) + .shellId(shellId) + .options(Collections.singletonList(new Option("WINRS_CONSOLEMODE_STDIN", "FALSE"))); Element body = DocumentHelper.createElement(QName.get("CommandLine", Namespaces.NS_WIN_SHELL)); body.addElement(QName.get("Command", Namespaces.NS_WIN_SHELL)).addText("\"" + command + "\""); @@ -35,5 +37,4 @@ protected void construct() { throw new RuntimeException("Error while building request content", e); } } - } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/request/GetOutputRequest.java b/src/main/java/hudson/plugins/ec2/win/winrm/request/GetOutputRequest.java index 2b8e15a80..c4b5834a0 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/request/GetOutputRequest.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/request/GetOutputRequest.java @@ -1,11 +1,9 @@ package hudson.plugins.ec2.win.winrm.request; import hudson.plugins.ec2.win.winrm.soap.Namespaces; - import java.net.URI; import java.net.URISyntaxException; import java.net.URL; - import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.QName; @@ -23,14 +21,18 @@ public GetOutputRequest(URL url, String shellId, String commandId) { @Override protected void construct() { try { - defaultHeader().action(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive")).resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")).shellId(shellId); + defaultHeader() + .action(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive")) + .resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")) + .shellId(shellId); Element body = DocumentHelper.createElement(QName.get("Receive", Namespaces.NS_WIN_SHELL)); - body.addElement(QName.get("DesiredStream", Namespaces.NS_WIN_SHELL)).addAttribute("CommandId", commandId).addText("stdout stderr"); + body.addElement(QName.get("DesiredStream", Namespaces.NS_WIN_SHELL)) + .addAttribute("CommandId", commandId) + .addText("stdout stderr"); setBody(body); } catch (URISyntaxException e) { throw new RuntimeException("Error while building request content", e); } } - } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/request/OpenShellRequest.java b/src/main/java/hudson/plugins/ec2/win/winrm/request/OpenShellRequest.java index cdacfb908..92ae5fe05 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/request/OpenShellRequest.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/request/OpenShellRequest.java @@ -2,12 +2,10 @@ import hudson.plugins.ec2.win.winrm.soap.Namespaces; import hudson.plugins.ec2.win.winrm.soap.Option; - import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Arrays; - import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.QName; @@ -18,9 +16,14 @@ public OpenShellRequest(URL url) { super(url); } + @Override protected void construct() { try { - defaultHeader().action(new URI("http://schemas.xmlsoap.org/ws/2004/09/transfer/Create")).resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")).options(Arrays.asList(new Option("WINRS_NOPROFILE", "FALSE"), new Option("WINRS_CODEPAGE", "437"))); + defaultHeader() + .action(new URI("http://schemas.xmlsoap.org/ws/2004/09/transfer/Create")) + .resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")) + .options( + Arrays.asList(new Option("WINRS_NOPROFILE", "FALSE"), new Option("WINRS_CODEPAGE", "437"))); Element body = DocumentHelper.createElement(QName.get("Shell", Namespaces.NS_WIN_SHELL)); body.addElement(QName.get("InputStreams", Namespaces.NS_WIN_SHELL)).addText("stdin"); @@ -29,7 +32,5 @@ protected void construct() { } catch (URISyntaxException e) { throw new RuntimeException("Error while building request content", e); } - } - } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/request/RequestFactory.java b/src/main/java/hudson/plugins/ec2/win/winrm/request/RequestFactory.java index 0d01e5ad7..d77a5d5c9 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/request/RequestFactory.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/request/RequestFactory.java @@ -77,5 +77,4 @@ public String getLocale() { public void setLocale(String locale) { this.locale = locale; } - } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/request/SendInputRequest.java b/src/main/java/hudson/plugins/ec2/win/winrm/request/SendInputRequest.java index d483ae2f4..6a63dc869 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/request/SendInputRequest.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/request/SendInputRequest.java @@ -1,12 +1,10 @@ package hudson.plugins.ec2.win.winrm.request; import hudson.plugins.ec2.win.winrm.soap.Namespaces; - import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Base64; - import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.QName; @@ -26,17 +24,19 @@ public SendInputRequest(URL url, byte[] input, String shellId, String commandId) @Override protected void construct() { try { - defaultHeader().action(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Send")).resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")).shellId(shellId); + defaultHeader() + .action(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Send")) + .resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")) + .shellId(shellId); Element body = DocumentHelper.createElement(QName.get("Send", Namespaces.NS_WIN_SHELL)); body.addElement(QName.get("Stream", Namespaces.NS_WIN_SHELL)) - .addAttribute("Name", "stdin") - .addAttribute("CommandId", commandId) - .addText(Base64.getEncoder().encodeToString(input)); + .addAttribute("Name", "stdin") + .addAttribute("CommandId", commandId) + .addText(Base64.getEncoder().encodeToString(input)); setBody(body); } catch (URISyntaxException e) { throw new RuntimeException("Error while building request content", e); } } - } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/request/SignalRequest.java b/src/main/java/hudson/plugins/ec2/win/winrm/request/SignalRequest.java index 7e7c21975..4267fd3c3 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/request/SignalRequest.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/request/SignalRequest.java @@ -1,11 +1,9 @@ package hudson.plugins.ec2.win.winrm.request; import hudson.plugins.ec2.win.winrm.soap.Namespaces; - import java.net.URI; import java.net.URISyntaxException; import java.net.URL; - import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.QName; @@ -23,15 +21,19 @@ public SignalRequest(URL url, String shellId, String commandId) { @Override protected void construct() { try { - defaultHeader().action(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command")).resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")).shellId(shellId); + defaultHeader() + .action(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command")) + .resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")) + .shellId(shellId); - Element body = DocumentHelper.createElement(QName.get("Signal", Namespaces.NS_WIN_SHELL)).addAttribute("CommandId", commandId); + Element body = DocumentHelper.createElement(QName.get("Signal", Namespaces.NS_WIN_SHELL)) + .addAttribute("CommandId", commandId); - body.addElement(QName.get("Code", Namespaces.NS_WIN_SHELL)).addText("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate"); + body.addElement(QName.get("Code", Namespaces.NS_WIN_SHELL)) + .addText("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate"); setBody(body); } catch (URISyntaxException e) { throw new RuntimeException("Error while building request content", e); } } - } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/soap/Header.java b/src/main/java/hudson/plugins/ec2/win/winrm/soap/Header.java index d4292f862..c0ecdfa56 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/soap/Header.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/soap/Header.java @@ -1,25 +1,34 @@ package hudson.plugins.ec2.win.winrm.soap; -import org.dom4j.Element; -import org.dom4j.QName; - import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.dom4j.Element; +import org.dom4j.QName; public class Header { private final String to; - private final String replyTo; - private final String maxEnvelopeSize; - private final String timeout; - private final String locale; - private final String id; - private final String action; - private final String shellId; - private final String resourceURI; + private final String replyTo; + private final String maxEnvelopeSize; + private final String timeout; + private final String locale; + private final String id; + private final String action; + private final String shellId; + private final String resourceURI; private final List
From 2eac736069f72cc2408d409412725c3bb4c84be4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Nov 2024 07:26:34 +0000 Subject: [PATCH 049/267] build(deps): bump io.jenkins.tools.bom:bom-2.452.x Bumps [io.jenkins.tools.bom:bom-2.452.x](https://github.com/jenkinsci/bom) from 3654.v237e4a_f2d8da_ to 3722.vcc62e7311580. - [Release notes](https://github.com/jenkinsci/bom/releases) - [Commits](https://github.com/jenkinsci/bom/commits) --- updated-dependencies: - dependency-name: io.jenkins.tools.bom:bom-2.452.x dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e400bde7a..0bf6ecbec 100644 --- a/pom.xml +++ b/pom.xml @@ -85,7 +85,7 @@ THE SOFTWARE. io.jenkins.tools.bom bom-2.452.x - 3654.v237e4a_f2d8da_ + 3722.vcc62e7311580 pom import From cc93a2ec6efe99c820fe046e6d4cfb39d00558a4 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Thu, 28 Nov 2024 13:37:34 -0800 Subject: [PATCH 050/267] Fix Spotless (#1013) --- src/main/java/hudson/plugins/ec2/EC2Cloud.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 650f6f63a..53452ebe2 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -876,7 +876,8 @@ public Collection provision(final Label label, int excessWorkload) } } catch (AmazonServiceException e) { LOGGER.log(Level.WARNING, t + ". Exception during provisioning", e); - if (e.getErrorCode().equals("RequestExpired") || e.getErrorCode().equals("ExpiredToken")) { + if (e.getErrorCode().equals("RequestExpired") + || e.getErrorCode().equals("ExpiredToken")) { // A RequestExpired or ExpiredToken error can indicate that credentials have expired so reconnect LOGGER.log(Level.INFO, "Reconnecting to EC2 due to RequestExpired or ExpiredToken error"); try { From 40a5d0e677cf61045a28583ba0c4a74a5bdcc22b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 07:14:54 +0000 Subject: [PATCH 051/267] build(deps): bump io.jenkins.tools.bom:bom-2.452.x Bumps [io.jenkins.tools.bom:bom-2.452.x](https://github.com/jenkinsci/bom) from 3722.vcc62e7311580 to 3761.vd922730f0fd2. - [Release notes](https://github.com/jenkinsci/bom/releases) - [Commits](https://github.com/jenkinsci/bom/commits) --- updated-dependencies: - dependency-name: io.jenkins.tools.bom:bom-2.452.x dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index dd33be89c..9b7ab734f 100644 --- a/pom.xml +++ b/pom.xml @@ -85,7 +85,7 @@ THE SOFTWARE. io.jenkins.tools.bom bom-2.452.x - 3722.vcc62e7311580 + 3761.vd922730f0fd2 pom import From 6f2fb05b16c321b5ac87ebdd2e483f10f51345a7 Mon Sep 17 00:00:00 2001 From: Allan Burdajewicz Date: Wed, 4 Dec 2024 20:00:24 +1000 Subject: [PATCH 052/267] [JENKINS-74945] Use Role ARN when defined --- .../java/hudson/plugins/ec2/EC2Cloud.java | 24 +++++++++++++++++-- .../ec2/AmazonEC2Cloud/config-entries.jelly | 2 +- .../plugins/ec2/EC2Cloud/help-roleArn.html | 3 +++ .../ec2/EC2Cloud/help-roleSessionName.html | 3 +++ .../plugins/ec2/AmazonEC2CloudTest.java | 19 +++++++++++++++ 5 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 src/main/resources/hudson/plugins/ec2/EC2Cloud/help-roleArn.html create mode 100644 src/main/resources/hudson/plugins/ec2/EC2Cloud/help-roleSessionName.html diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 53452ebe2..df6726132 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -50,6 +50,7 @@ import com.cloudbees.plugins.credentials.common.StandardListBoxModel; import com.cloudbees.plugins.credentials.domains.Domain; import com.cloudbees.plugins.credentials.domains.DomainRequirement; +import com.google.common.annotations.VisibleForTesting; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; @@ -92,6 +93,8 @@ import jenkins.model.Jenkins; import jenkins.model.JenkinsLocationConfiguration; import org.apache.commons.lang.StringUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.QueryParameter; @@ -1083,8 +1086,9 @@ public static AWSCredentialsProvider createCredentialsProvider( AWSCredentialsProvider provider = createCredentialsProvider(useInstanceProfileForCredentials, credentialsId); - if (StringUtils.isNotEmpty(roleArn) && StringUtils.isNotEmpty(roleSessionName)) { - return new STSAssumeRoleSessionCredentialsProvider.Builder(roleArn, roleSessionName) + if (StringUtils.isNotEmpty(roleArn)) { + return new STSAssumeRoleSessionCredentialsProvider.Builder( + roleArn, StringUtils.defaultIfBlank(roleSessionName, "Jenkins")) .withStsClient(AWSSecurityTokenServiceClientBuilder.standard() .withCredentials(provider) .withRegion(region) @@ -1272,6 +1276,22 @@ public ListBoxModel doFillSshKeysCredentialsIdItems( return result; } + @NonNull + @RequirePOST + @Restricted(NoExternalUse.class) + @VisibleForTesting + public FormValidation doCheckRoleSessionName( + @QueryParameter String roleArn, @QueryParameter String roleSessionName) { + // Don't do anything if the user is only reading the configuration + if (Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + if (StringUtils.isNotEmpty(roleArn) && StringUtils.isBlank(roleSessionName)) { + return FormValidation.warning( + "Session Name is recommended when specifying an Arn Role. If empty, 'Jenkins' will be used."); + } + } + return FormValidation.ok(); + } + @RequirePOST public FormValidation doCheckSshKeysCredentialsId( @AncestorInPath ItemGroup context, @QueryParameter String value) throws IOException, ServletException { diff --git a/src/main/resources/hudson/plugins/ec2/AmazonEC2Cloud/config-entries.jelly b/src/main/resources/hudson/plugins/ec2/AmazonEC2Cloud/config-entries.jelly index c0631206d..66f495528 100644 --- a/src/main/resources/hudson/plugins/ec2/AmazonEC2Cloud/config-entries.jelly +++ b/src/main/resources/hudson/plugins/ec2/AmazonEC2Cloud/config-entries.jelly @@ -50,7 +50,7 @@ THE SOFTWARE. - + diff --git a/src/main/resources/hudson/plugins/ec2/EC2Cloud/help-roleArn.html b/src/main/resources/hudson/plugins/ec2/EC2Cloud/help-roleArn.html new file mode 100644 index 000000000..ec518e775 --- /dev/null +++ b/src/main/resources/hudson/plugins/ec2/EC2Cloud/help-roleArn.html @@ -0,0 +1,3 @@ +
+ The ARN of an IAM Role to be assumed. +
\ No newline at end of file diff --git a/src/main/resources/hudson/plugins/ec2/EC2Cloud/help-roleSessionName.html b/src/main/resources/hudson/plugins/ec2/EC2Cloud/help-roleSessionName.html new file mode 100644 index 000000000..0fc25bd4b --- /dev/null +++ b/src/main/resources/hudson/plugins/ec2/EC2Cloud/help-roleSessionName.html @@ -0,0 +1,3 @@ +
+ A name for the session of the assumed IAM role. If empty, this defaults to 'Jenkins'. +
\ No newline at end of file diff --git a/src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java b/src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java index 2c8b462eb..716b3f929 100644 --- a/src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java +++ b/src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java @@ -38,6 +38,7 @@ import com.cloudbees.plugins.credentials.SystemCredentialsProvider; import com.cloudbees.plugins.credentials.domains.Domain; import hudson.plugins.ec2.util.TestSSHUserPrivateKey; +import hudson.util.FormValidation; import hudson.util.ListBoxModel; import java.io.IOException; import java.util.Collections; @@ -96,6 +97,24 @@ public void testAmazonEC2FactoryGetInstance() throws Exception { Assert.assertTrue(Mockito.mockingDetails(connection).isMock()); } + @Test + public void testAmazonEC2FactoryWorksIfSessionNameMissing() throws Exception { + r.jenkins.clouds.replace(new AmazonEC2Cloud( + "us-east-1", true, "abc", "us-east-1", null, "ghi", "3", Collections.emptyList(), "roleArn", null)); + AmazonEC2Cloud cloud = r.jenkins.clouds.get(AmazonEC2Cloud.class); + AmazonEC2 connection = cloud.connect(); + Assert.assertNotNull(connection); + Assert.assertTrue(Mockito.mockingDetails(connection).isMock()); + } + + @Test + public void testSessionNameMissingWarning() { + AmazonEC2Cloud actual = r.jenkins.clouds.get(AmazonEC2Cloud.class); + AmazonEC2Cloud.DescriptorImpl descriptor = (AmazonEC2Cloud.DescriptorImpl) actual.getDescriptor(); + assertThat(descriptor.doCheckRoleSessionName("roleArn", "").kind, is(FormValidation.Kind.WARNING)); + assertThat(descriptor.doCheckRoleSessionName("roleArn", "roleSessionName").kind, is(FormValidation.Kind.OK)); + } + @Test public void testSshKeysCredentialsIdRemainsUnchangedAfterUpdatingOtherFields() throws Exception { HtmlForm form = getConfigForm(); From 0798e8b3319363919681b070a6e204825fceed16 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 16:57:47 -0800 Subject: [PATCH 053/267] Use enhanced for loop --- src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java | 3 +-- src/main/java/hudson/plugins/ec2/Tenancy.java | 6 +----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java b/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java index 21b4df61d..3e04b5410 100644 --- a/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java +++ b/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java @@ -265,8 +265,7 @@ private boolean itemsInQueueForThisSlave(EC2Computer c) { } final Label selfLabel = selfNode.getSelfLabel(); Queue.Item[] items = Jenkins.getInstance().getQueue().getItems(); - for (int i = 0; i < items.length; i++) { - Queue.Item item = items[i]; + for (Queue.Item item : items) { final Label assignedLabel = item.getAssignedLabel(); if (assignedLabel == selfLabel) { LOGGER.fine("Preventing idle timeout of " + c.getName() diff --git a/src/main/java/hudson/plugins/ec2/Tenancy.java b/src/main/java/hudson/plugins/ec2/Tenancy.java index 93b995f5e..bed6372cb 100644 --- a/src/main/java/hudson/plugins/ec2/Tenancy.java +++ b/src/main/java/hudson/plugins/ec2/Tenancy.java @@ -18,11 +18,7 @@ public String toString() { public static Tenancy fromValue(String value) { if (value != null && !"".equals(value)) { - Tenancy[] var1 = values(); - int var2 = var1.length; - - for (int var3 = 0; var3 < var2; ++var3) { - Tenancy enumEntry = var1[var3]; + for (Tenancy enumEntry : values()) { if (enumEntry.toString().equals(value)) { return enumEntry; } From f22af57ebd89a374402f775e50c667fd4e205017 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 16:58:04 -0800 Subject: [PATCH 054/267] Use Objects.equals where possible --- src/main/java/hudson/plugins/ec2/SpotConfiguration.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/SpotConfiguration.java b/src/main/java/hudson/plugins/ec2/SpotConfiguration.java index 5a1a22ba0..fa1e9b3b3 100644 --- a/src/main/java/hudson/plugins/ec2/SpotConfiguration.java +++ b/src/main/java/hudson/plugins/ec2/SpotConfiguration.java @@ -90,8 +90,7 @@ public boolean equals(Object obj) { String normalizedBid = normalizeBid(this.spotMaxBidPrice); String otherNormalizedBid = normalizeBid(config.spotMaxBidPrice); - boolean normalizedBidsAreEqual = - normalizedBid == null ? (otherNormalizedBid == null) : normalizedBid.equals(otherNormalizedBid); + boolean normalizedBidsAreEqual = Objects.equals(normalizedBid, otherNormalizedBid); boolean blockReservationIsEqual = true; if (this.spotBlockReservationDuration != config.spotBlockReservationDuration) { blockReservationIsEqual = false; From b7ed6a3eb9e7c49790d6be6b8f8bedd288e911ca Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 16:58:21 -0800 Subject: [PATCH 055/267] Use try-with-resources where possible --- src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java | 8 ++------ src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index 996245d62..18ffc57f4 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -352,14 +352,10 @@ private File createIdentityKeyFile(EC2Computer computer) throws IOException { File tempFile = Files.createTempFile("ec2_", ".pem").toFile(); try { - FileOutputStream fileOutputStream = new FileOutputStream(tempFile); - OutputStreamWriter writer = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8); - try { + try (FileOutputStream fileOutputStream = new FileOutputStream(tempFile); + OutputStreamWriter writer = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8)) { writer.write(privateKey); writer.flush(); - } finally { - writer.close(); - fileOutputStream.close(); } FilePath filePath = new FilePath(tempFile); filePath.chmod(0400); // octal file mask - readonly by owner diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index 04acbd606..ac38cf377 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -358,14 +358,10 @@ private File createIdentityKeyFile(EC2Computer computer) throws IOException { File tempFile = Files.createTempFile("ec2_", ".pem").toFile(); try { - FileOutputStream fileOutputStream = new FileOutputStream(tempFile); - OutputStreamWriter writer = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8); - try { + try (FileOutputStream fileOutputStream = new FileOutputStream(tempFile); + OutputStreamWriter writer = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8)) { writer.write(privateKey); writer.flush(); - } finally { - writer.close(); - fileOutputStream.close(); } FilePath filePath = new FilePath(tempFile); filePath.chmod(0400); // octal file mask - readonly by owner From 1888868624901c2934b3a073b1c0525b932ded6f Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 16:59:17 -0800 Subject: [PATCH 056/267] Use statement lambda where possible --- .../plugins/ec2/util/AmazonEC2FactoryMockImpl.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/test/java/hudson/plugins/ec2/util/AmazonEC2FactoryMockImpl.java b/src/test/java/hudson/plugins/ec2/util/AmazonEC2FactoryMockImpl.java index ab8e715fd..6b1b7f7e1 100644 --- a/src/test/java/hudson/plugins/ec2/util/AmazonEC2FactoryMockImpl.java +++ b/src/test/java/hudson/plugins/ec2/util/AmazonEC2FactoryMockImpl.java @@ -214,12 +214,10 @@ private static void mockTerminateInstances(AmazonEC2Client mock) { Mockito.doAnswer(invocationOnMock -> { TerminateInstancesRequest request = invocationOnMock.getArgument(0); List instancesToRemove = new ArrayList<>(); - request.getInstanceIds().forEach(instanceId -> { - instances.stream() - .filter(instance -> instance.getInstanceId().equals(instanceId)) - .findFirst() - .ifPresent(instancesToRemove::add); - }); + request.getInstanceIds().forEach(instanceId -> instances.stream() + .filter(instance -> instance.getInstanceId().equals(instanceId)) + .findFirst() + .ifPresent(instancesToRemove::add)); instances.removeAll(instancesToRemove); return new TerminateInstancesResult() .withTerminatingInstances(instancesToRemove.stream() From ed1eec49bee0f5251e4efa004cb8e991c37318e2 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 16:59:38 -0800 Subject: [PATCH 057/267] Use diamond operator where possible --- src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java | 6 +++--- src/main/java/hudson/plugins/ec2/EC2Cloud.java | 2 +- src/main/java/hudson/plugins/ec2/EC2Tag.java | 2 +- .../java/hudson/plugins/ec2/InstanceTypeConverter.java | 2 +- .../hudson/plugins/ec2/util/DeviceMappingParser.java | 2 +- .../hudson/plugins/ec2/AmazonEC2CloudUnitTest.java | 2 +- src/test/java/hudson/plugins/ec2/CloudHelperTest.java | 4 ++-- .../java/hudson/plugins/ec2/EC2AbstractSlaveTest.java | 5 ++--- .../hudson/plugins/ec2/EC2RetentionStrategyTest.java | 7 +++---- .../java/hudson/plugins/ec2/SlaveTemplateUnitTest.java | 10 +++++----- .../verifiers/SshHostKeyVerificationStrategyTest.java | 3 +-- 11 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java b/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java index e70319f68..a68d54836 100644 --- a/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java +++ b/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java @@ -896,7 +896,7 @@ private void fetchLiveInstanceData(boolean force) throws AmazonClientException { * when fetchLiveInstanceData() is called before pushLiveInstancedata(). */ if (!i.getTags().isEmpty()) { - tags = new LinkedList(); + tags = new LinkedList<>(); for (Tag t : i.getTags()) { tags.add(new EC2Tag(t.getKey(), t.getValue())); } @@ -918,7 +918,7 @@ protected void clearLiveInstancedata() throws AmazonClientException { /* Now that we have our instance, we can clear the tags on it */ if (!tags.isEmpty()) { - HashSet instTags = new HashSet(); + HashSet instTags = new HashSet<>(); for (EC2Tag t : tags) { instTags.add(new Tag(t.getName(), t.getValue())); @@ -946,7 +946,7 @@ protected void pushLiveInstancedata() throws AmazonClientException { /* Now that we have our instance, we can set tags on it */ if (inst != null && tags != null && !tags.isEmpty()) { - HashSet instTags = new HashSet(); + HashSet instTags = new HashSet<>(); for (EC2Tag t : tags) { instTags.add(new Tag(t.getName(), t.getValue())); diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index df6726132..bbd37aaac 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -973,7 +973,7 @@ void attemptReattachOrphanOrStoppedNodes(Jenkins jenkinsInstance, SlaveTemplate private PlannedNode createPlannedNode(final SlaveTemplate t, final EC2AbstractSlave slave) { return new PlannedNode( t.getDisplayName(), - Computer.threadPoolForRemoting.submit(new Callable() { + Computer.threadPoolForRemoting.submit(new Callable<>() { int retryCount = 0; private static final int DESCRIBE_LIMIT = 2; diff --git a/src/main/java/hudson/plugins/ec2/EC2Tag.java b/src/main/java/hudson/plugins/ec2/EC2Tag.java index cfab3bdaa..49c0aed41 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Tag.java +++ b/src/main/java/hudson/plugins/ec2/EC2Tag.java @@ -107,7 +107,7 @@ public static List fromAmazonTags(List amazonTags) { return null; } - LinkedList result = new LinkedList(); + LinkedList result = new LinkedList<>(); for (Tag t : amazonTags) { result.add(new EC2Tag(t)); } diff --git a/src/main/java/hudson/plugins/ec2/InstanceTypeConverter.java b/src/main/java/hudson/plugins/ec2/InstanceTypeConverter.java index 12b13b172..63b81e4e0 100644 --- a/src/main/java/hudson/plugins/ec2/InstanceTypeConverter.java +++ b/src/main/java/hudson/plugins/ec2/InstanceTypeConverter.java @@ -38,7 +38,7 @@ */ public class InstanceTypeConverter implements Converter { - private static final Map TYPICAL_INSTANCE_TYPES = new HashMap(); + private static final Map TYPICAL_INSTANCE_TYPES = new HashMap<>(); static { TYPICAL_INSTANCE_TYPES.put("DEFAULT", InstanceType.M1Small); diff --git a/src/main/java/hudson/plugins/ec2/util/DeviceMappingParser.java b/src/main/java/hudson/plugins/ec2/util/DeviceMappingParser.java index 0b67eadb2..5716b1ef4 100644 --- a/src/main/java/hudson/plugins/ec2/util/DeviceMappingParser.java +++ b/src/main/java/hudson/plugins/ec2/util/DeviceMappingParser.java @@ -35,7 +35,7 @@ private DeviceMappingParser() {} public static List parse(String customDeviceMapping) { - List deviceMappings = new ArrayList(); + List deviceMappings = new ArrayList<>(); for (String mapping : customDeviceMapping.split(",")) { String[] mappingPair = mapping.split("="); diff --git a/src/test/java/hudson/plugins/ec2/AmazonEC2CloudUnitTest.java b/src/test/java/hudson/plugins/ec2/AmazonEC2CloudUnitTest.java index 40c543e25..08a2bbdee 100644 --- a/src/test/java/hudson/plugins/ec2/AmazonEC2CloudUnitTest.java +++ b/src/test/java/hudson/plugins/ec2/AmazonEC2CloudUnitTest.java @@ -116,7 +116,7 @@ public void testSpotInstanceCount() throws Exception { when(spotSlaveMock.getSpotRequest()).thenReturn(null); when(spotSlaveMock.getSpotInstanceRequestId()).thenReturn("sir-id"); - List instances = new ArrayList(); + List instances = new ArrayList<>(); for (int i = 0; i <= numberOfSpotInstanceRequests; i++) { instances.add(new Instance() .withInstanceId("id" + i) diff --git a/src/test/java/hudson/plugins/ec2/CloudHelperTest.java b/src/test/java/hudson/plugins/ec2/CloudHelperTest.java index 24f3d0894..50008c4bb 100644 --- a/src/test/java/hudson/plugins/ec2/CloudHelperTest.java +++ b/src/test/java/hudson/plugins/ec2/CloudHelperTest.java @@ -74,7 +74,7 @@ public void testGetInstanceWithRetryInstanceNotFound() throws Exception { AmazonServiceException amazonServiceException = new AmazonServiceException("test exception"); amazonServiceException.setErrorCode("InvalidInstanceID.NotFound"); - Answer answerWithRetry = new Answer() { + Answer answerWithRetry = new Answer<>() { private boolean first = true; @Override @@ -110,7 +110,7 @@ public void testGetInstanceWithRetryRequestExpired() throws Exception { AmazonServiceException amazonServiceException = new AmazonServiceException("test exception"); amazonServiceException.setErrorCode("RequestExpired"); - Answer answerWithRetry = new Answer() { + Answer answerWithRetry = new Answer<>() { private boolean first = true; @Override diff --git a/src/test/java/hudson/plugins/ec2/EC2AbstractSlaveTest.java b/src/test/java/hudson/plugins/ec2/EC2AbstractSlaveTest.java index a52d997e2..1cd1374ac 100644 --- a/src/test/java/hudson/plugins/ec2/EC2AbstractSlaveTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2AbstractSlaveTest.java @@ -8,7 +8,6 @@ import com.amazonaws.services.ec2.model.InstanceType; import hudson.model.Node; -import hudson.slaves.NodeProperty; import java.util.ArrayList; import java.util.List; import org.junit.Rule; @@ -37,7 +36,7 @@ public void testGetLaunchTimeoutInMillisShouldNotOverflow() throws Exception { null, "init", "tmpDir", - new ArrayList>(), + new ArrayList<>(), "root", "java", "jvm", @@ -137,7 +136,7 @@ public void testMaxUsesBackwardCompat() throws Exception { null, "init", "tmpDir", - new ArrayList>(), + new ArrayList<>(), "root", "jvm", false, diff --git a/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java b/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java index 4ce202d0a..2812ff288 100644 --- a/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java @@ -27,7 +27,6 @@ import hudson.security.ACL; import hudson.security.AccessControlled; import hudson.security.Permission; -import hudson.slaves.NodeProperty; import hudson.slaves.OfflineCause; import java.time.Clock; import java.time.Duration; @@ -231,7 +230,7 @@ private EC2Computer computerWithIdleTime( null, "init", "tmpDir", - new ArrayList>(), + new ArrayList<>(), "remote", "jvm", false, @@ -356,7 +355,7 @@ private EC2Computer computerWithUpTime( null, "init", "tmpDir", - new ArrayList>(), + new ArrayList<>(), "remote", "jvm", false, @@ -502,7 +501,7 @@ private EC2Computer computerWithUsageLimit(final int usageLimit) throws Exceptio null, "init", "tmpDir", - new ArrayList>(), + new ArrayList<>(), "remote", "jvm", false, diff --git a/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java b/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java index 1dc486051..f31601e63 100644 --- a/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java +++ b/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java @@ -56,7 +56,7 @@ public CreateTagsResult createTags(com.amazonaws.services.ec2.model.CreateTagsRe EC2Tag tag1 = new EC2Tag("name1", "value1"); EC2Tag tag2 = new EC2Tag("name2", "value2"); - List tags = new ArrayList(); + List tags = new ArrayList<>(); tags.add(tag1); tags.add(tag2); String instanceId = "123"; @@ -98,7 +98,7 @@ protected Object readResolve() { } }; - ArrayList awsTags = new ArrayList(); + ArrayList awsTags = new ArrayList<>(); awsTags.add(new Tag(EC2Tag.TAG_NAME_JENKINS_SLAVE_TYPE, "value1")); awsTags.add(new Tag(EC2Tag.TAG_NAME_JENKINS_SLAVE_TYPE, "value2")); @@ -127,7 +127,7 @@ public CreateTagsResult createTags(com.amazonaws.services.ec2.model.CreateTagsRe EC2Tag tag1 = new EC2Tag("name1", "value1"); EC2Tag tag2 = new EC2Tag("name2", "value2"); - List tags = new ArrayList(); + List tags = new ArrayList<>(); tags.add(tag1); tags.add(tag2); String instanceId = "123"; @@ -169,7 +169,7 @@ protected Object readResolve() { } }; - ArrayList awsTags = new ArrayList(); + ArrayList awsTags = new ArrayList<>(); awsTags.add(new Tag(EC2Tag.TAG_NAME_JENKINS_SLAVE_TYPE, "value1")); awsTags.add(new Tag(EC2Tag.TAG_NAME_JENKINS_SLAVE_TYPE, "value2")); @@ -985,7 +985,7 @@ public void testConnectionStrategyDeprecatedFieldsAreExported() { } class TestHandler extends Handler { - private final List records = new LinkedList(); + private final List records = new LinkedList<>(); @Override public void close() throws SecurityException {} diff --git a/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java b/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java index e9974f54f..96c4b09cf 100644 --- a/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java +++ b/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java @@ -15,7 +15,6 @@ import hudson.plugins.ec2.InstanceState; import hudson.plugins.ec2.SlaveTemplate; import hudson.plugins.ec2.util.ConnectionRule; -import hudson.slaves.NodeProperty; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -365,7 +364,7 @@ private static MockEC2Computer createComputer(String suffix) throws Exception { null, "init", "tmpDir", - new ArrayList>(), + new ArrayList<>(), "remote", "jvm", false, From c9b09ed99e3195f82d2a4cee997585ba80369c76 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:00:57 -0800 Subject: [PATCH 058/267] Add final to enum field --- src/main/java/hudson/plugins/ec2/Tenancy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/hudson/plugins/ec2/Tenancy.java b/src/main/java/hudson/plugins/ec2/Tenancy.java index bed6372cb..15e7ddf53 100644 --- a/src/main/java/hudson/plugins/ec2/Tenancy.java +++ b/src/main/java/hudson/plugins/ec2/Tenancy.java @@ -5,7 +5,7 @@ public enum Tenancy { Dedicated("dedicated"), Host("host"); - private String value; + private final String value; private Tenancy(String value) { this.value = value; From 898e98dd4ef53888eaa45596fe110bfaf81af58d Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:01:18 -0800 Subject: [PATCH 059/267] Replace C-style array declaration with Java-style array declaration --- src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java b/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java index f31601e63..fb21249c9 100644 --- a/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java +++ b/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java @@ -105,7 +105,7 @@ protected Object readResolve() { Method updateRemoteTags = SlaveTemplate.class.getDeclaredMethod( "updateRemoteTags", AmazonEC2.class, Collection.class, String.class, String[].class); updateRemoteTags.setAccessible(true); - final Object params[] = {ec2, awsTags, "InvalidInstanceRequestID.NotFound", new String[] {instanceId}}; + final Object[] params = {ec2, awsTags, "InvalidInstanceRequestID.NotFound", new String[] {instanceId}}; updateRemoteTags.invoke(orig, params); assertEquals(0, handler.getRecords().size()); } @@ -176,7 +176,7 @@ protected Object readResolve() { Method updateRemoteTags = SlaveTemplate.class.getDeclaredMethod( "updateRemoteTags", AmazonEC2.class, Collection.class, String.class, String[].class); updateRemoteTags.setAccessible(true); - final Object params[] = {ec2, awsTags, "InvalidSpotInstanceRequestID.NotFound", new String[] {instanceId}}; + final Object[] params = {ec2, awsTags, "InvalidSpotInstanceRequestID.NotFound", new String[] {instanceId}}; updateRemoteTags.invoke(orig, params); assertEquals(5, handler.getRecords().size()); From ca0562ef474518dffbcbc1a3440c60da430d91f8 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:01:42 -0800 Subject: [PATCH 060/267] Use isEmpty() where possible --- src/main/java/hudson/plugins/ec2/CloudHelper.java | 4 ++-- src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java | 10 +++++----- src/main/java/hudson/plugins/ec2/EC2Cloud.java | 8 ++++---- src/main/java/hudson/plugins/ec2/SlaveTemplate.java | 4 ++-- .../java/hudson/plugins/ec2/ssh/EC2MacLauncher.java | 2 +- .../java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java | 2 +- .../ssh/verifiers/SshHostKeyVerificationStrategy.java | 2 +- .../hudson/plugins/ec2/win/EC2WindowsLauncher.java | 2 +- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/CloudHelper.java b/src/main/java/hudson/plugins/ec2/CloudHelper.java index 7a453d0ac..158078051 100644 --- a/src/main/java/hudson/plugins/ec2/CloudHelper.java +++ b/src/main/java/hudson/plugins/ec2/CloudHelper.java @@ -58,7 +58,7 @@ static Instance getInstance(String instanceId, EC2Cloud cloud) throws AmazonClie if (reservations.size() != 1) { String message = "Unexpected number of reservations reported by EC2 for instance id '" + instanceId + "', expected 1 result, found " + reservations + "."; - if (reservations.size() == 0) { + if (reservations.isEmpty()) { message += " Instance seems to be dead."; } LOGGER.info(message); @@ -70,7 +70,7 @@ static Instance getInstance(String instanceId, EC2Cloud cloud) throws AmazonClie if (instances.size() != 1) { String message = "Unexpected number of instances reported by EC2 for instance id '" + instanceId + "', expected 1 result, found " + instances + "."; - if (instances.size() == 0) { + if (instances.isEmpty()) { message += " Instance seems to be dead."; } LOGGER.info(message); diff --git a/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java b/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java index a68d54836..618f6d65b 100644 --- a/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java +++ b/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java @@ -765,7 +765,7 @@ public long getLaunchTimeoutInMillis() { } public String getRemoteAdmin() { - if (remoteAdmin == null || remoteAdmin.length() == 0) { + if (remoteAdmin == null || remoteAdmin.isEmpty()) { return amiType.isWindows() ? "Administrator" : "root"; } return remoteAdmin; @@ -775,7 +775,7 @@ String getRootCommandPrefix() { String commandPrefix = (amiType.isUnix() ? ((UnixData) amiType).getRootCommandPrefix() : (amiType.isMac() ? ((MacData) amiType).getRootCommandPrefix() : "")); - if (commandPrefix == null || commandPrefix.length() == 0) { + if (commandPrefix == null || commandPrefix.isEmpty()) { return ""; } return commandPrefix + " "; @@ -785,7 +785,7 @@ String getSlaveCommandPrefix() { String commandPrefix = (amiType.isUnix() ? ((UnixData) amiType).getSlaveCommandPrefix() : (amiType.isMac() ? ((MacData) amiType).getSlaveCommandPrefix() : "")); - if (commandPrefix == null || commandPrefix.length() == 0) { + if (commandPrefix == null || commandPrefix.isEmpty()) { return ""; } return commandPrefix + " "; @@ -795,7 +795,7 @@ String getSlaveCommandSuffix() { String commandSuffix = (amiType.isUnix() ? ((UnixData) amiType).getSlaveCommandSuffix() : (amiType.isMac() ? ((MacData) amiType).getSlaveCommandSuffix() : "")); - if (commandSuffix == null || commandSuffix.length() == 0) { + if (commandSuffix == null || commandSuffix.isEmpty()) { return ""; } return " " + commandSuffix; @@ -813,7 +813,7 @@ public int getSshPort() { String sshPort = (amiType.isUnix() ? ((UnixData) amiType).getSshPort() : (amiType.isMac() ? ((MacData) amiType).getSshPort() : "22")); - if (sshPort == null || sshPort.length() == 0) { + if (sshPort == null || sshPort.isEmpty()) { return 22; } diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index bbd37aaac..f5553a14e 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -965,7 +965,7 @@ void attemptReattachOrphanOrStoppedNodes(Jenkins jenkinsInstance, SlaveTemplate orphansOrStopped.remove(0); } attachSlavesToJenkins(jenkinsInstance, template.toSlaves(orphansOrStopped), template); - if (orphansOrStopped.size() > 0) { + if (!orphansOrStopped.isEmpty()) { LOGGER.info("Found and re-attached " + orphansOrStopped.size() + " orphan/stopped nodes"); } } @@ -1170,7 +1170,7 @@ public static String getAwsPartitionHostForService(String region, String service * Convert a configured hostname like 'us-east-1' to a FQDN or ip address */ public static String convertHostName(String ec2HostName) { - if (ec2HostName == null || ec2HostName.length() == 0) { + if (ec2HostName == null || ec2HostName.isEmpty()) { ec2HostName = DEFAULT_EC2_HOST; } if (!ec2HostName.contains(".")) { @@ -1183,7 +1183,7 @@ public static String convertHostName(String ec2HostName) { * Convert a user entered string into a port number "" -> -1 to indicate default based on SSL setting */ public static Integer convertPort(String ec2Port) { - if (ec2Port == null || ec2Port.length() == 0) { + if (ec2Port == null || ec2Port.isEmpty()) { return -1; } return Integer.parseInt(ec2Port); @@ -1424,7 +1424,7 @@ protected FormValidation doTestConnection( AmazonEC2 ec2 = AmazonEC2Factory.getInstance().connect(credentialsProvider, ec2endpoint); ec2.describeInstances(); - if (privateKey.trim().length() > 0) { + if (!privateKey.trim().isEmpty()) { // check if this key exists EC2PrivateKey pk = new EC2PrivateKey(privateKey); if (pk.find(ec2) == null) { diff --git a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java index 4d51368f5..df1ac07a3 100644 --- a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java +++ b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java @@ -2117,7 +2117,7 @@ private void setupRootDevice(Image image, List deviceMapping // get the root device (only one expected in the blockmappings) final List rootDeviceMappings = image.getBlockDeviceMappings(); - if (rootDeviceMappings.size() == 0) { + if (rootDeviceMappings.isEmpty()) { LOGGER.warning("AMI missing block devices"); return; } @@ -2553,7 +2553,7 @@ private List getEc2SecurityGroups(AmazonEC2 ec2) throws AmazonClientExce securityGroupSet, this.getParent().name, getCurrentSubnetId())); List groupIds = new ArrayList<>(); DescribeSecurityGroupsResult groupResult = getSecurityGroupsBy("group-name", securityGroupSet, ec2); - if (groupResult.getSecurityGroups().size() == 0) { + if (groupResult.getSecurityGroups().isEmpty()) { groupResult = getSecurityGroupsBy("group-id", securityGroupSet, ec2); } diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index 18ffc57f4..d45c7d9b9 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -193,7 +193,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) conn.exec("mkdir -p " + tmpDir, logger); if (initScript != null - && initScript.trim().length() > 0 + && !initScript.trim().isEmpty() && conn.exec("test -e ~/.hudson-run-init", logger) != 0) { logInfo(computer, listener, "Executing init script"); scp.put(initScript.getBytes("UTF-8"), "init.sh", tmpDir, "0700"); diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index ac38cf377..792be2975 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -204,7 +204,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) conn.exec("mkdir -p " + tmpDir, logger); if (initScript != null - && initScript.trim().length() > 0 + && !initScript.trim().isEmpty() && conn.exec("test -e ~/.hudson-run-init", logger) != 0) { logInfo(computer, listener, "Executing init script"); scp.put(initScript.getBytes("UTF-8"), "init.sh", tmpDir, "0700"); diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategy.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategy.java index 8ba7b9552..7ea2bd59a 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategy.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategy.java @@ -97,7 +97,7 @@ HostKey getHostKeyFromConsole( } String line = getLineWithKey(logger, computer, serverHostKeyAlgorithm); - if (line != null && line.length() > 0) { + if (line != null && !line.isEmpty()) { key = getKeyFromLine(logger, line, listener); } else if (line != null) { key = new HostKey(serverHostKeyAlgorithm, new byte[] {}); diff --git a/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java b/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java index 85809c9e6..640719cac 100644 --- a/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java +++ b/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java @@ -60,7 +60,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) return; } - if (initScript != null && initScript.trim().length() > 0 && !connection.exists(tmpDir + ".jenkins-init")) { + if (initScript != null && !initScript.trim().isEmpty() && !connection.exists(tmpDir + ".jenkins-init")) { logger.println("Executing init script"); try (OutputStream init = connection.putFile(tmpDir + "init.bat")) { init.write(initScript.getBytes("utf-8")); From 7da06384d617e8e8438420c22266b9f253e91d22 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:01:57 -0800 Subject: [PATCH 061/267] Use Charset constant where possible --- src/main/java/hudson/plugins/ec2/EC2PrivateKey.java | 3 +-- src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java | 2 +- src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java | 2 +- src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java | 2 +- .../java/hudson/plugins/ec2/ssh/HostKeyVerifierImplTest.java | 5 +++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java b/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java index 496414905..30f708bb9 100644 --- a/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java +++ b/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java @@ -33,7 +33,6 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; @@ -147,7 +146,7 @@ public String decryptWindowsPassword(String encodedPassword) throws AmazonClient PEMEncodable.decode(privateKey.getPlainText()).toPrivateKey()); byte[] cipherText = Base64.getDecoder().decode(StringUtils.deleteWhitespace(encodedPassword)); byte[] plainText = cipher.doFinal(cipherText); - return new String(plainText, Charset.forName("ASCII")); + return new String(plainText, StandardCharsets.US_ASCII); } catch (Exception e) { throw new AmazonClientException("Unable to decode password:\n" + e.toString()); } diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index d45c7d9b9..c26805e23 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -196,7 +196,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) && !initScript.trim().isEmpty() && conn.exec("test -e ~/.hudson-run-init", logger) != 0) { logInfo(computer, listener, "Executing init script"); - scp.put(initScript.getBytes("UTF-8"), "init.sh", tmpDir, "0700"); + scp.put(initScript.getBytes(StandardCharsets.UTF_8), "init.sh", tmpDir, "0700"); Session sess = conn.openSession(); sess.requestDumbPTY(); // so that the remote side bundles stdout // and stderr diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index 792be2975..c81803fa5 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -207,7 +207,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) && !initScript.trim().isEmpty() && conn.exec("test -e ~/.hudson-run-init", logger) != 0) { logInfo(computer, listener, "Executing init script"); - scp.put(initScript.getBytes("UTF-8"), "init.sh", tmpDir, "0700"); + scp.put(initScript.getBytes(StandardCharsets.UTF_8), "init.sh", tmpDir, "0700"); Session sess = conn.openSession(); sess.requestDumbPTY(); // so that the remote side bundles stdout // and stderr diff --git a/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java b/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java index 640719cac..508ddfa5b 100644 --- a/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java +++ b/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java @@ -63,7 +63,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) if (initScript != null && !initScript.trim().isEmpty() && !connection.exists(tmpDir + ".jenkins-init")) { logger.println("Executing init script"); try (OutputStream init = connection.putFile(tmpDir + "init.bat")) { - init.write(initScript.getBytes("utf-8")); + init.write(initScript.getBytes(StandardCharsets.UTF_8)); } WindowsProcess initProcess = connection.execute("cmd /c " + tmpDir + "init.bat"); diff --git a/src/test/java/hudson/plugins/ec2/ssh/HostKeyVerifierImplTest.java b/src/test/java/hudson/plugins/ec2/ssh/HostKeyVerifierImplTest.java index 97d935d69..bc8c4bf9d 100644 --- a/src/test/java/hudson/plugins/ec2/ssh/HostKeyVerifierImplTest.java +++ b/src/test/java/hudson/plugins/ec2/ssh/HostKeyVerifierImplTest.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.nio.charset.StandardCharsets; import org.junit.Test; public class HostKeyVerifierImplTest { @@ -13,12 +14,12 @@ public class HostKeyVerifierImplTest { @Test public void testVerifyFail() throws Exception { HostKeyVerifierImpl impl = new HostKeyVerifierImpl(""); - assertFalse(impl.verifyServerHostKey("", 0, null, key.getBytes("UTF-8"))); + assertFalse(impl.verifyServerHostKey("", 0, null, key.getBytes(StandardCharsets.UTF_8))); } @Test public void testVerifyTrue() throws Exception { HostKeyVerifierImpl impl = new HostKeyVerifierImpl(fp); - assertTrue(impl.verifyServerHostKey("", 0, null, key.getBytes("UTF-8"))); + assertTrue(impl.verifyServerHostKey("", 0, null, key.getBytes(StandardCharsets.UTF_8))); } } From 882bf27ff2686339c7e5c6f4f584a23642193b03 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:02:46 -0800 Subject: [PATCH 062/267] Remove redundant modifiers --- src/main/java/hudson/plugins/ec2/EC2Readiness.java | 4 ++-- src/main/java/hudson/plugins/ec2/Tenancy.java | 2 +- .../java/hudson/plugins/ec2/win/winrm/soap/Namespaces.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2Readiness.java b/src/main/java/hudson/plugins/ec2/EC2Readiness.java index ef21dfc4f..86892723e 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Readiness.java +++ b/src/main/java/hudson/plugins/ec2/EC2Readiness.java @@ -3,7 +3,7 @@ import com.amazonaws.AmazonClientException; public interface EC2Readiness { - public boolean isReady(); + boolean isReady(); - public String getEc2ReadinessStatus() throws AmazonClientException; + String getEc2ReadinessStatus() throws AmazonClientException; } diff --git a/src/main/java/hudson/plugins/ec2/Tenancy.java b/src/main/java/hudson/plugins/ec2/Tenancy.java index 15e7ddf53..3778350be 100644 --- a/src/main/java/hudson/plugins/ec2/Tenancy.java +++ b/src/main/java/hudson/plugins/ec2/Tenancy.java @@ -7,7 +7,7 @@ public enum Tenancy { private final String value; - private Tenancy(String value) { + Tenancy(String value) { this.value = value; } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/soap/Namespaces.java b/src/main/java/hudson/plugins/ec2/win/winrm/soap/Namespaces.java index 0bef823ef..ec58f7fab 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/soap/Namespaces.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/soap/Namespaces.java @@ -24,7 +24,7 @@ public class Namespaces { private Namespaces() {} - public static final List mostUsed() { + public static List mostUsed() { return Collections.unmodifiableList( Arrays.asList(NS_SOAP_ENV, NS_ADDRESSING, NS_WIN_SHELL, NS_WSMAN_DMTF, NS_WSMAN_MSFT)); } From c7971a8dc82a76886dc89575e692676fa78ebf1e Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:03:11 -0800 Subject: [PATCH 063/267] Remove unnecessary semicolon --- src/main/java/hudson/plugins/ec2/win/WinConnection.java | 2 +- src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/win/WinConnection.java b/src/main/java/hudson/plugins/ec2/win/WinConnection.java index b82684a59..687deb8c3 100644 --- a/src/main/java/hudson/plugins/ec2/win/WinConnection.java +++ b/src/main/java/hudson/plugins/ec2/win/WinConnection.java @@ -127,7 +127,7 @@ public boolean pingFailingIfSSHHandShakeError() throws IOException { LOGGER.log(Level.FINE, () -> "checking SMB connection to " + host); try (Socket socket = new Socket(); Connection connection = smbclient.connect(host); - Session session = connection.authenticate(authentication); ) { + Session session = connection.authenticate(authentication)) { socket.connect(new InetSocketAddress(host, 445), TIMEOUT); winrm().ping(); session.connectShare("IPC$"); diff --git a/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java b/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java index fb21249c9..69ae18086 100644 --- a/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java +++ b/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java @@ -463,7 +463,6 @@ protected Object readResolve() { if (rootVolumeEnum instanceof EbsEncryptRootVolume) { template.ebsEncryptRootVolume = rootVolumeEnum; } - ; Method setupRootDevice = SlaveTemplate.class.getDeclaredMethod("setupRootDevice", Image.class, List.class); setupRootDevice.setAccessible(true); setupRootDevice.invoke(template, image, deviceMappings); From 997553b72de42436f65e01b0239d36a05c381caa Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:04:36 -0800 Subject: [PATCH 064/267] Remove unnecessary toString calls --- src/main/java/hudson/plugins/ec2/EC2HostAddressProvider.java | 4 ++-- src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2HostAddressProvider.java b/src/main/java/hudson/plugins/ec2/EC2HostAddressProvider.java index 596a4eefc..d36c04283 100644 --- a/src/main/java/hudson/plugins/ec2/EC2HostAddressProvider.java +++ b/src/main/java/hudson/plugins/ec2/EC2HostAddressProvider.java @@ -33,7 +33,7 @@ public static String mac(Instance instance, ConnectionStrategy strategy) { case PRIVATE_IP: return getPrivateIpAddress(instance); default: - throw new IllegalArgumentException("Could not mac host address for strategy = " + strategy.toString()); + throw new IllegalArgumentException("Could not mac host address for strategy = " + strategy); } } @@ -43,7 +43,7 @@ public static String windows(Instance instance, ConnectionStrategy strategy) { } else if (strategy.equals(PUBLIC_DNS) || strategy.equals(PUBLIC_IP)) { return getPublicIpAddress(instance); } else { - throw new IllegalArgumentException("Could not windows host address for strategy = " + strategy.toString()); + throw new IllegalArgumentException("Could not windows host address for strategy = " + strategy); } } diff --git a/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java b/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java index 3e04b5410..1c007fcd6 100644 --- a/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java +++ b/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java @@ -163,7 +163,7 @@ private long internalCheck(EC2Computer computer) { && (InstanceState.STOPPED.equals(state) || InstanceState.STOPPING.equals(state))) { if (computer.isOnline()) { LOGGER.info("External Stop of " + computer.getName() + " detected - disconnecting. instance status" - + state.toString()); + + state); computer.disconnect(null); } return CHECK_INTERVAL_MINUTES; From f956e298188bafa7368dfb77a22ebc0a88e7480a Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:06:42 -0800 Subject: [PATCH 065/267] Use correct class for logger --- .../plugins/ec2/win/winrm/WinRMConnectionManagerFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectionManagerFactory.java b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectionManagerFactory.java index c25c0e7a5..690fc12c0 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectionManagerFactory.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectionManagerFactory.java @@ -15,7 +15,7 @@ import org.apache.http.ssl.SSLContextBuilder; public class WinRMConnectionManagerFactory { - private static final Logger log = Logger.getLogger(WinRMClient.class.getName()); + private static final Logger log = Logger.getLogger(WinRMConnectionManagerFactory.class.getName()); static final WinRMHttpConnectionManager DEFAULT = new WinRMHttpConnectionManager(); static final WinRMHttpConnectionManager SSL = new WinRMHttpConnectionManager(false); From f793086935e46ee74d0ff690ed49c30463131ea7 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:06:54 -0800 Subject: [PATCH 066/267] Use List.of where possible --- src/test/java/hudson/plugins/ec2/EC2FilterTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/hudson/plugins/ec2/EC2FilterTest.java b/src/test/java/hudson/plugins/ec2/EC2FilterTest.java index a034c57ef..c61180e10 100644 --- a/src/test/java/hudson/plugins/ec2/EC2FilterTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2FilterTest.java @@ -31,6 +31,7 @@ import com.amazonaws.services.ec2.model.Filter; import java.util.Arrays; import java.util.Collections; +import java.util.List; import org.junit.Test; public class EC2FilterTest { @@ -43,7 +44,7 @@ public void testSingleValue() throws Exception { Filter filter = ec2Filter.toFilter(); assertNotNull(filter); assertEquals("name", filter.getName()); - assertEquals(Arrays.asList("value"), filter.getValues()); + assertEquals(List.of("value"), filter.getValues()); } @Test From f2721f002ef35c09ed2a578184ca585c91778eed Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:07:09 -0800 Subject: [PATCH 067/267] Remove redundant call to String#format --- .../hudson/plugins/ec2/ssh/verifiers/AcceptNewStrategy.java | 2 +- .../hudson/plugins/ec2/ssh/verifiers/CheckNewHardStrategy.java | 2 +- .../hudson/plugins/ec2/ssh/verifiers/CheckNewSoftStrategy.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/AcceptNewStrategy.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/AcceptNewStrategy.java index cb056c3be..5251a2dfa 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/AcceptNewStrategy.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/AcceptNewStrategy.java @@ -59,7 +59,7 @@ public boolean verify(EC2Computer computer, HostKey hostKey, TaskListener listen LOGGER, Level.INFO, computer.getListener(), - String.format("Connection allowed after the host key has been verified")); + "Connection allowed after the host key has been verified"); return true; } else { EC2Cloud.log( diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewHardStrategy.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewHardStrategy.java index b1c9f198d..cb67393a6 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewHardStrategy.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewHardStrategy.java @@ -110,7 +110,7 @@ public boolean verify(EC2Computer computer, HostKey hostKey, TaskListener listen LOGGER, Level.INFO, computer.getListener(), - String.format("Connection allowed after the host key has been verified")); + "Connection allowed after the host key has been verified"); return true; } else { EC2Cloud.log( diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewSoftStrategy.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewSoftStrategy.java index cad6090ad..698789b20 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewSoftStrategy.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewSoftStrategy.java @@ -102,7 +102,7 @@ public boolean verify(EC2Computer computer, HostKey hostKey, TaskListener listen LOGGER, Level.INFO, computer.getListener(), - String.format("Connection allowed after the host key has been verified")); + "Connection allowed after the host key has been verified"); return true; } else { EC2Cloud.log( From c55c3d458bb0e1193afa6c0d5ef6e191042030a9 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:07:34 -0800 Subject: [PATCH 068/267] Use String#isEmpty where possible --- src/main/java/hudson/plugins/ec2/EC2SpotSlave.java | 2 +- src/main/java/hudson/plugins/ec2/SlaveTemplate.java | 2 +- src/main/java/hudson/plugins/ec2/Tenancy.java | 2 +- src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2SpotSlave.java b/src/main/java/hudson/plugins/ec2/EC2SpotSlave.java index 2f1372b1f..66d8db405 100644 --- a/src/main/java/hudson/plugins/ec2/EC2SpotSlave.java +++ b/src/main/java/hudson/plugins/ec2/EC2SpotSlave.java @@ -200,7 +200,7 @@ public void terminate() { } // Terminate the agent if it is running - if (instanceId != null && !instanceId.equals("")) { + if (instanceId != null && !instanceId.isEmpty()) { if (!super.isAlive(true)) { /* * The node has been killed externally, so we've nothing to do here diff --git a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java index df1ac07a3..fcd624143 100644 --- a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java +++ b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java @@ -1455,7 +1455,7 @@ public Set getSecurityGroupSet() { } public Set parseSecurityGroups() { - if (securityGroups == null || "".equals(securityGroups.trim())) { + if (securityGroups == null || securityGroups.trim().isEmpty()) { return Collections.emptySet(); } else { return new HashSet<>(Arrays.asList(securityGroups.split("\\s*,\\s*"))); diff --git a/src/main/java/hudson/plugins/ec2/Tenancy.java b/src/main/java/hudson/plugins/ec2/Tenancy.java index 3778350be..9912ab933 100644 --- a/src/main/java/hudson/plugins/ec2/Tenancy.java +++ b/src/main/java/hudson/plugins/ec2/Tenancy.java @@ -17,7 +17,7 @@ public String toString() { } public static Tenancy fromValue(String value) { - if (value != null && !"".equals(value)) { + if (value != null && !value.isEmpty()) { for (Tenancy enumEntry : values()) { if (enumEntry.toString().equals(value)) { return enumEntry; diff --git a/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java b/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java index 508ddfa5b..4f6951535 100644 --- a/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java +++ b/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java @@ -48,7 +48,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) try { String initScript = node.initScript; - String tmpDir = (node.tmpDir != null && !node.tmpDir.equals("") + String tmpDir = (node.tmpDir != null && !node.tmpDir.isEmpty() ? WindowsUtil.quoteArgument(Util.ensureEndsWith(node.tmpDir, "\\")) : "C:\\Windows\\Temp\\"); From 4c39c63edbb00c77b1665d1c839bf66974b475b9 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:09:41 -0800 Subject: [PATCH 069/267] Fix string comparison --- src/main/java/hudson/plugins/ec2/SpotConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/hudson/plugins/ec2/SpotConfiguration.java b/src/main/java/hudson/plugins/ec2/SpotConfiguration.java index fa1e9b3b3..ee5a1bb52 100644 --- a/src/main/java/hudson/plugins/ec2/SpotConfiguration.java +++ b/src/main/java/hudson/plugins/ec2/SpotConfiguration.java @@ -205,7 +205,7 @@ public FormValidation doCurrentSpotPrice( Image img = CloudHelper.getAmiImage(ec2, ami); if (img != null) { Collection productDescriptions = new ArrayList<>(); - productDescriptions.add(img.getPlatform() == "Windows" ? "Windows" : "Linux/UNIX"); + productDescriptions.add("Windows".equals(img.getPlatform()) ? "Windows" : "Linux/UNIX"); request.setProductDescriptions(productDescriptions); } } From 5883dcc729bdfa4e21f9352c760c49e8cc8fcc62 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:09:44 -0800 Subject: [PATCH 070/267] Remove unused import --- .../java/hudson/plugins/ec2/util/EC2AgentFactoryMockImpl.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/hudson/plugins/ec2/util/EC2AgentFactoryMockImpl.java b/src/test/java/hudson/plugins/ec2/util/EC2AgentFactoryMockImpl.java index 67aa26b84..ffdeda1f7 100644 --- a/src/test/java/hudson/plugins/ec2/util/EC2AgentFactoryMockImpl.java +++ b/src/test/java/hudson/plugins/ec2/util/EC2AgentFactoryMockImpl.java @@ -5,7 +5,6 @@ import hudson.model.Computer; import hudson.model.Descriptor; import hudson.plugins.ec2.*; -import hudson.plugins.ec2.Tenancy; import hudson.slaves.NodeProperty; import java.io.IOException; import java.util.List; From 826630cab0f7e2daf39f5fba094f835fa08ec041 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:10:10 -0800 Subject: [PATCH 071/267] Simplify assertions --- .../hudson/plugins/ec2/SlaveTemplateTest.java | 9 ++--- .../plugins/ec2/SlaveTemplateUnitTest.java | 2 +- .../plugins/ec2/TemplateLabelsTest.java | 35 ++++++++++--------- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java b/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java index a3be7d3c5..1094c04df 100644 --- a/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java +++ b/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java @@ -26,6 +26,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -504,8 +505,8 @@ public void testMinimumNumberOfInstancesActiveRangeConfig() throws Exception { Assert.assertNotNull(stored); Assert.assertEquals("11:00", stored.getMinimumNoInstancesActiveTimeRangeFrom()); Assert.assertEquals("15:00", stored.getMinimumNoInstancesActiveTimeRangeTo()); - Assert.assertEquals(false, stored.getDay("monday")); - Assert.assertEquals(true, stored.getDay("tuesday")); + Assert.assertFalse(stored.getDay("monday")); + Assert.assertTrue(stored.getDay("tuesday")); } @Test @@ -698,7 +699,7 @@ public void provisionOndemandSetsAwsNetworkingOnNetworkInterface() throws Except assertEquals(actualNet.getSubnetId(), Util.fixEmpty(template.getSubnetId())); assertEquals(actualNet.getGroups(), Stream.of("some-group-id").collect(Collectors.toList())); - assertEquals(actualRequest.getSubnetId(), null); + assertNull(actualRequest.getSubnetId()); assertEquals(actualRequest.getSecurityGroupIds(), Collections.emptyList()); assertEquals(actualRequest.getSecurityGroups(), Collections.emptyList()); } @@ -1090,7 +1091,7 @@ public void provisionOnDemandWithUnsupportedInstanceMetadata() throws Exception RunInstancesRequest actualRequest = riRequestCaptor.getValue(); InstanceMetadataOptionsRequest metadataOptionsRequest = actualRequest.getMetadataOptions(); - assertEquals(metadataOptionsRequest, null); + assertNull(metadataOptionsRequest); } @Test diff --git a/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java b/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java index 69ae18086..5ed327b4e 100644 --- a/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java +++ b/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java @@ -704,7 +704,7 @@ public void testAssociatePublicIpSetting() { null, true, ""); - assertEquals(true, st.getAssociatePublicIp()); + assertTrue(st.getAssociatePublicIp()); } @Test diff --git a/src/test/java/hudson/plugins/ec2/TemplateLabelsTest.java b/src/test/java/hudson/plugins/ec2/TemplateLabelsTest.java index 7e4476972..eb4bd2a46 100644 --- a/src/test/java/hudson/plugins/ec2/TemplateLabelsTest.java +++ b/src/test/java/hudson/plugins/ec2/TemplateLabelsTest.java @@ -23,7 +23,8 @@ */ package hudson.plugins.ec2; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import com.amazonaws.services.ec2.model.InstanceType; import hudson.model.Label; @@ -95,45 +96,45 @@ private void setUpCloud(String label, Node.Mode mode) throws Exception { public void testLabelAtom() throws Exception { setUpCloud(LABEL1 + " " + LABEL2); - assertEquals(true, ac.canProvision(new LabelAtom(LABEL1))); - assertEquals(true, ac.canProvision(new LabelAtom(LABEL2))); - assertEquals(false, ac.canProvision(new LabelAtom("aaa"))); - assertEquals(true, ac.canProvision((Label) null)); + assertTrue(ac.canProvision(new LabelAtom(LABEL1))); + assertTrue(ac.canProvision(new LabelAtom(LABEL2))); + assertFalse(ac.canProvision(new LabelAtom("aaa"))); + assertTrue(ac.canProvision((Label) null)); } @Test public void testLabelExpression() throws Exception { setUpCloud(LABEL1 + " " + LABEL2); - assertEquals(true, ac.canProvision(Label.parseExpression(LABEL1 + " || " + LABEL2))); - assertEquals(true, ac.canProvision(Label.parseExpression(LABEL1 + " && " + LABEL2))); - assertEquals(true, ac.canProvision(Label.parseExpression(LABEL1 + " || aaa"))); - assertEquals(false, ac.canProvision(Label.parseExpression(LABEL1 + " && aaa"))); - assertEquals(false, ac.canProvision(Label.parseExpression("aaa || bbb"))); - assertEquals(false, ac.canProvision(Label.parseExpression("aaa || bbb"))); + assertTrue(ac.canProvision(Label.parseExpression(LABEL1 + " || " + LABEL2))); + assertTrue(ac.canProvision(Label.parseExpression(LABEL1 + " && " + LABEL2))); + assertTrue(ac.canProvision(Label.parseExpression(LABEL1 + " || aaa"))); + assertFalse(ac.canProvision(Label.parseExpression(LABEL1 + " && aaa"))); + assertFalse(ac.canProvision(Label.parseExpression("aaa || bbb"))); + assertFalse(ac.canProvision(Label.parseExpression("aaa || bbb"))); } @Test public void testEmptyLabel() throws Exception { setUpCloud(""); - assertEquals(true, ac.canProvision((Label) null)); + assertTrue(ac.canProvision((Label) null)); } @Test public void testExclusiveMode() throws Exception { setUpCloud(LABEL1 + " " + LABEL2, Node.Mode.EXCLUSIVE); - assertEquals(true, ac.canProvision(new LabelAtom(LABEL1))); - assertEquals(true, ac.canProvision(new LabelAtom(LABEL2))); - assertEquals(false, ac.canProvision(new LabelAtom("aaa"))); - assertEquals(false, ac.canProvision((Label) null)); + assertTrue(ac.canProvision(new LabelAtom(LABEL1))); + assertTrue(ac.canProvision(new LabelAtom(LABEL2))); + assertFalse(ac.canProvision(new LabelAtom("aaa"))); + assertFalse(ac.canProvision((Label) null)); } @Test public void testExclusiveModeEmptyLabel() throws Exception { setUpCloud("", Node.Mode.EXCLUSIVE); - assertEquals(false, ac.canProvision((Label) null)); + assertFalse(ac.canProvision((Label) null)); } } From 420a3906ae5d23a55c95bf87b5c4b8002a96907c Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:10:40 -0800 Subject: [PATCH 072/267] Simplify negative expression --- src/main/java/hudson/plugins/ec2/EC2Cloud.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index f5553a14e..3e6cb3b02 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -260,7 +260,7 @@ public void updateTemplate(SlaveTemplate newTemplate, String oldTemplateDescript Optional optionalOldTemplate = templates.stream() .filter(template -> Objects.equals(template.description, oldTemplateDescription)) .findFirst(); - if (!optionalOldTemplate.isPresent()) { + if (optionalOldTemplate.isEmpty()) { throw new Exception( String.format("A SlaveTemplate with description %s does not exist", oldTemplateDescription)); } From 972af6ecbbf1b1241c8dbef1772c67e44d2b434c Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:11:03 -0800 Subject: [PATCH 073/267] Simplify string expression --- src/main/java/hudson/plugins/ec2/win/WinConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/hudson/plugins/ec2/win/WinConnection.java b/src/main/java/hudson/plugins/ec2/win/WinConnection.java index 687deb8c3..15bcf1e62 100644 --- a/src/main/java/hudson/plugins/ec2/win/WinConnection.java +++ b/src/main/java/hudson/plugins/ec2/win/WinConnection.java @@ -106,7 +106,7 @@ public boolean exists(String path) throws IOException { private static String toAdministrativeShare(String path) { // administrative windows share are DRIVE$path like - return path.substring(0, 1) + "$"; + return path.charAt(0) + "$"; } private static String toFilePath(String path) { From 0f3662fa2e1993e2c43f14dde31f0e8a5f4c5b57 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:11:18 -0800 Subject: [PATCH 074/267] Remove redundant casts --- src/main/java/hudson/plugins/ec2/EC2Cloud.java | 2 +- .../hudson/plugins/ec2/util/MinimumInstanceChecker.java | 2 +- .../java/hudson/plugins/ec2/EC2RetentionStrategyTest.java | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 3e6cb3b02..44cd7057d 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -1105,7 +1105,7 @@ private static AmazonWebServicesCredentials getCredentials(@CheckForNull String if (StringUtils.isBlank(credentialsId)) { return null; } - return (AmazonWebServicesCredentials) CredentialsMatchers.firstOrNull( + return CredentialsMatchers.firstOrNull( CredentialsProvider.lookupCredentials( AmazonWebServicesCredentials.class, Jenkins.get(), ACL.SYSTEM, Collections.emptyList()), CredentialsMatchers.withId(credentialsId)); diff --git a/src/main/java/hudson/plugins/ec2/util/MinimumInstanceChecker.java b/src/main/java/hudson/plugins/ec2/util/MinimumInstanceChecker.java index 5841d3e5c..8443a531b 100644 --- a/src/main/java/hudson/plugins/ec2/util/MinimumInstanceChecker.java +++ b/src/main/java/hudson/plugins/ec2/util/MinimumInstanceChecker.java @@ -25,7 +25,7 @@ public class MinimumInstanceChecker { public static Clock clock = Clock.systemDefaultZone(); private static Stream agentsForTemplate(@NonNull SlaveTemplate agentTemplate) { - return (Stream) Arrays.stream(Jenkins.get().getComputers()) + return Arrays.stream(Jenkins.get().getComputers()) .filter(EC2Computer.class::isInstance) .filter(computer -> { SlaveTemplate computerTemplate = ((EC2Computer) computer).getSlaveTemplate(); diff --git a/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java b/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java index 2812ff288..ce5e8766a 100644 --- a/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java @@ -84,7 +84,7 @@ public void testOnBillingHourRetention() throws Exception { checkRetentionStrategy(rs, computer); assertEquals( "Expected " + t[0] + "m" + t[1] + "s to be " + expected.get(i), - (boolean) expected.get(i), + expected.get(i), idleTimeoutCalled.get()); // reset the assumption idleTimeoutCalled.set(false); @@ -474,13 +474,13 @@ public void testOnUsageCountRetention() throws Exception { } // As we want to terminate agent both for usageCount 1 & 0 - setting this to true if (usageCount == 1 || usageCount == 0) { - assertEquals("Expected " + usageCount + " to be " + true, (boolean) true, terminateCalled.get()); + assertEquals("Expected " + usageCount + " to be " + true, true, terminateCalled.get()); // Reset the assumption terminateCalled.set(false); } else { assertEquals( "Expected " + usageCount + " to be " + expected.get(i), - (boolean) expected.get(i), + expected.get(i), terminateCalled.get()); } } From ab79086da42c0a75151ab80c0e1bff3ed4d523af Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:11:44 -0800 Subject: [PATCH 075/267] Simplify stream expressions --- src/main/java/hudson/plugins/ec2/SlaveTemplate.java | 3 ++- .../SshHostKeyVerificationAdministrativeMonitor.java | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java index fcd624143..eb878be26 100644 --- a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java +++ b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java @@ -2203,7 +2203,8 @@ private DescribeImagesRequest makeDescribeImagesRequest() throws AmazonClientExc // Raise an exception if there were no search attributes. // This is legal but not what anyone wants - it will // launch random recently created public AMIs. - int numAttrs = Stream.of(imageIds, owners, users, filters).collect(Collectors.summingInt(List::size)); + int numAttrs = + Stream.of(imageIds, owners, users, filters).mapToInt(List::size).sum(); if (numAttrs == 0) { throw new AmazonClientException("Neither AMI ID nor AMI search attributes provided"); } diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationAdministrativeMonitor.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationAdministrativeMonitor.java index cb0fe6d3b..814f0a1ea 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationAdministrativeMonitor.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationAdministrativeMonitor.java @@ -35,7 +35,6 @@ import java.util.ArrayList; import java.util.List; import java.util.ListIterator; -import java.util.stream.Collectors; import jenkins.model.Jenkins; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; @@ -55,11 +54,11 @@ public String getDisplayName() { } public String getVeryInsecureTemplates() { - return veryInsecureTemplates.stream().collect(Collectors.joining(", ")); + return String.join(", ", veryInsecureTemplates); } public String getInsecureTemplates() { - return insecureTemplates.stream().collect(Collectors.joining(", ")); + return String.join(", ", insecureTemplates); } public boolean showVeryInsecureTemplates() { From 40c4a249992e78eafb43e1a7e339ab7d3b31e233 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:12:14 -0800 Subject: [PATCH 076/267] Remove unnecessary return --- src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java b/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java index 1c007fcd6..f38b95755 100644 --- a/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java +++ b/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java @@ -330,7 +330,6 @@ public void taskAccepted(Executor executor, Queue.Task task) { if (maxTotalUses <= -1) { LOGGER.fine("maxTotalUses set to unlimited (" + slaveNode.maxTotalUses + ") for agent " + slaveNode.instanceId); - return; } else if (maxTotalUses <= 1) { LOGGER.info("maxTotalUses drained - suspending agent " + slaveNode.instanceId); computer.setAcceptingTasks(false); From 5f53f07b5ac42d0389f837ea0495a9814bd9917f Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:23:08 -0800 Subject: [PATCH 077/267] Organize imports --- .../java/hudson/plugins/ec2/CloudHelper.java | 4 +- .../java/hudson/plugins/ec2/EC2Cloud.java | 43 ++++++++++++++----- .../java/hudson/plugins/ec2/EC2Computer.java | 6 ++- .../plugins/ec2/EC2HostAddressProvider.java | 6 +-- .../hudson/plugins/ec2/EC2OndemandSlave.java | 2 +- .../hudson/plugins/ec2/EC2PrivateKey.java | 4 +- .../hudson/plugins/ec2/EC2SlaveMonitor.java | 5 +-- src/main/java/hudson/plugins/ec2/EC2Step.java | 12 +++++- .../hudson/plugins/ec2/SlaveTemplate.java | 40 ++++++++--------- .../hudson/plugins/ec2/SpotConfiguration.java | 5 +-- .../plugins/ec2/ssh/EC2MacLauncher.java | 23 ++++++++-- .../plugins/ec2/ssh/EC2UnixLauncher.java | 11 ++++- .../plugins/ec2/util/EC2AgentFactory.java | 3 +- .../plugins/ec2/util/EC2AgentFactoryImpl.java | 3 +- .../plugins/ec2/win/EC2WindowsLauncher.java | 7 ++- .../win/winrm/NegotiateNTLMSchemaFactory.java | 6 ++- .../plugins/ec2/AmazonEC2CloudTest.java | 3 +- .../plugins/ec2/AmazonEC2CloudUnitTest.java | 5 +-- .../plugins/ec2/ConfigurationAsCodeTest.java | 22 +++++----- .../plugins/ec2/EC2AbstractSlaveTest.java | 20 ++++----- .../plugins/ec2/EC2CloudMigrationTest.java | 4 +- .../java/hudson/plugins/ec2/EC2CloudTest.java | 4 +- .../ec2/EC2HostAddressProviderTest.java | 21 +++++---- .../plugins/ec2/SlaveTemplateUnitTest.java | 9 +++- .../ec2/util/AmazonEC2FactoryMockImpl.java | 34 ++++++++++++++- .../ec2/util/EC2AgentFactoryMockImpl.java | 9 +++- .../plugins/ec2/win/WinConnectionTest.java | 2 +- 27 files changed, 208 insertions(+), 105 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/CloudHelper.java b/src/main/java/hudson/plugins/ec2/CloudHelper.java index 158078051..86e631616 100644 --- a/src/main/java/hudson/plugins/ec2/CloudHelper.java +++ b/src/main/java/hudson/plugins/ec2/CloudHelper.java @@ -1,7 +1,5 @@ package hudson.plugins.ec2; -import static hudson.plugins.ec2.EC2Cloud.EC2_REQUEST_EXPIRED_ERROR_CODE; - import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.services.ec2.AmazonEC2; @@ -32,7 +30,7 @@ static Instance getInstanceWithRetry(String instanceId, EC2Cloud cloud) return getInstance(instanceId, cloud); } catch (AmazonServiceException e) { if (e.getErrorCode().equals("InvalidInstanceID.NotFound") - || EC2_REQUEST_EXPIRED_ERROR_CODE.equals(e.getErrorCode())) { + || EC2Cloud.EC2_REQUEST_EXPIRED_ERROR_CODE.equals(e.getErrorCode())) { // retry in 5 seconds. Thread.sleep(5000); continue; diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 44cd7057d..28302e62a 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -18,9 +18,6 @@ */ package hudson.plugins.ec2; -import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; -import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; - import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.ClientConfiguration; @@ -31,7 +28,19 @@ import com.amazonaws.auth.InstanceProfileCredentialsProvider; import com.amazonaws.auth.STSAssumeRoleSessionCredentialsProvider; import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.model.*; +import com.amazonaws.services.ec2.model.DescribeInstancesRequest; +import com.amazonaws.services.ec2.model.DescribeInstancesResult; +import com.amazonaws.services.ec2.model.DescribeSpotInstanceRequestsRequest; +import com.amazonaws.services.ec2.model.DescribeSpotInstanceRequestsResult; +import com.amazonaws.services.ec2.model.Filter; +import com.amazonaws.services.ec2.model.Instance; +import com.amazonaws.services.ec2.model.InstanceStateName; +import com.amazonaws.services.ec2.model.InstanceType; +import com.amazonaws.services.ec2.model.KeyPair; +import com.amazonaws.services.ec2.model.KeyPairInfo; +import com.amazonaws.services.ec2.model.Reservation; +import com.amazonaws.services.ec2.model.SpotInstanceRequest; +import com.amazonaws.services.ec2.model.Tag; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.AmazonS3ClientBuilder; import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; @@ -81,7 +90,17 @@ import java.net.MalformedURLException; import java.net.Proxy; import java.net.URL; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; @@ -90,6 +109,7 @@ import java.util.logging.Logger; import java.util.logging.SimpleFormatter; import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; import jenkins.model.Jenkins; import jenkins.model.JenkinsLocationConfiguration; import org.apache.commons.lang.StringUtils; @@ -505,25 +525,26 @@ public void doAttach(StaplerRequest req, StaplerResponse rsp, @QueryParameter St public HttpResponse doProvision(@QueryParameter String template) throws ServletException, IOException { checkPermission(PROVISION); if (template == null) { - throw HttpResponses.error(SC_BAD_REQUEST, "The 'template' query parameter is missing"); + throw HttpResponses.error(HttpServletResponse.SC_BAD_REQUEST, "The 'template' query parameter is missing"); } SlaveTemplate t = getTemplate(template); if (t == null) { - throw HttpResponses.error(SC_BAD_REQUEST, "No such template: " + template); + throw HttpResponses.error(HttpServletResponse.SC_BAD_REQUEST, "No such template: " + template); } final Jenkins jenkinsInstance = Jenkins.get(); if (jenkinsInstance.isQuietingDown()) { - throw HttpResponses.error(SC_BAD_REQUEST, "Jenkins instance is quieting down"); + throw HttpResponses.error(HttpServletResponse.SC_BAD_REQUEST, "Jenkins instance is quieting down"); } if (jenkinsInstance.isTerminating()) { - throw HttpResponses.error(SC_BAD_REQUEST, "Jenkins instance is terminating"); + throw HttpResponses.error(HttpServletResponse.SC_BAD_REQUEST, "Jenkins instance is terminating"); } try { List nodes = getNewOrExistingAvailableSlave(t, 1, true); if (nodes == null || nodes.isEmpty()) { throw HttpResponses.error( - SC_BAD_REQUEST, "Cloud or AMI instance cap would be exceeded for: " + template); + HttpServletResponse.SC_BAD_REQUEST, + "Cloud or AMI instance cap would be exceeded for: " + template); } // Reconnect a stopped instance, the ADD is invoking the connect only for the node creation @@ -536,7 +557,7 @@ public HttpResponse doProvision(@QueryParameter String template) throws ServletE return HttpResponses.redirectViaContextPath( "/computer/" + nodes.get(0).getNodeName()); } catch (AmazonClientException e) { - throw HttpResponses.error(SC_INTERNAL_SERVER_ERROR, e); + throw HttpResponses.error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e); } } diff --git a/src/main/java/hudson/plugins/ec2/EC2Computer.java b/src/main/java/hudson/plugins/ec2/EC2Computer.java index 03e7b2494..44aa9dc19 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Computer.java +++ b/src/main/java/hudson/plugins/ec2/EC2Computer.java @@ -25,7 +25,11 @@ import com.amazonaws.AmazonClientException; import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.model.*; +import com.amazonaws.services.ec2.model.DescribeInstanceTypesRequest; +import com.amazonaws.services.ec2.model.DescribeInstanceTypesResult; +import com.amazonaws.services.ec2.model.GetConsoleOutputRequest; +import com.amazonaws.services.ec2.model.GetConsoleOutputResult; +import com.amazonaws.services.ec2.model.Instance; import edu.umd.cs.findbugs.annotations.CheckForNull; import hudson.Util; import hudson.model.Node; diff --git a/src/main/java/hudson/plugins/ec2/EC2HostAddressProvider.java b/src/main/java/hudson/plugins/ec2/EC2HostAddressProvider.java index d36c04283..009ee0db9 100644 --- a/src/main/java/hudson/plugins/ec2/EC2HostAddressProvider.java +++ b/src/main/java/hudson/plugins/ec2/EC2HostAddressProvider.java @@ -1,7 +1,5 @@ package hudson.plugins.ec2; -import static hudson.plugins.ec2.ConnectionStrategy.*; - import com.amazonaws.services.ec2.model.Instance; import java.util.Optional; import org.apache.commons.lang.StringUtils; @@ -38,9 +36,9 @@ public static String mac(Instance instance, ConnectionStrategy strategy) { } public static String windows(Instance instance, ConnectionStrategy strategy) { - if (strategy.equals(PRIVATE_DNS) || strategy.equals(PRIVATE_IP)) { + if (strategy.equals(ConnectionStrategy.PRIVATE_DNS) || strategy.equals(ConnectionStrategy.PRIVATE_IP)) { return getPrivateIpAddress(instance); - } else if (strategy.equals(PUBLIC_DNS) || strategy.equals(PUBLIC_IP)) { + } else if (strategy.equals(ConnectionStrategy.PUBLIC_DNS) || strategy.equals(ConnectionStrategy.PUBLIC_IP)) { return getPublicIpAddress(instance); } else { throw new IllegalArgumentException("Could not windows host address for strategy = " + strategy); diff --git a/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java b/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java index fe759f14a..c56c427d5 100644 --- a/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java +++ b/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java @@ -2,7 +2,7 @@ import com.amazonaws.AmazonClientException; import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.model.*; +import com.amazonaws.services.ec2.model.TerminateInstancesRequest; import hudson.Extension; import hudson.model.Computer; import hudson.model.Descriptor.FormException; diff --git a/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java b/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java index 30f708bb9..b77a8fc06 100644 --- a/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java +++ b/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java @@ -23,8 +23,6 @@ */ package hudson.plugins.ec2; -import static hudson.plugins.ec2.EC2Cloud.SSH_PRIVATE_KEY_FILEPATH; - import com.amazonaws.AmazonClientException; import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.model.KeyPairInfo; @@ -155,7 +153,7 @@ public String decryptWindowsPassword(String encodedPassword) throws AmazonClient /* visible for testing */ @CheckForNull public static EC2PrivateKey fetchFromDisk() { - return fetchFromDisk(System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "")); + return fetchFromDisk(System.getProperty(EC2Cloud.SSH_PRIVATE_KEY_FILEPATH, "")); } @CheckForNull diff --git a/src/main/java/hudson/plugins/ec2/EC2SlaveMonitor.java b/src/main/java/hudson/plugins/ec2/EC2SlaveMonitor.java index 5209c89b6..192d3b64c 100644 --- a/src/main/java/hudson/plugins/ec2/EC2SlaveMonitor.java +++ b/src/main/java/hudson/plugins/ec2/EC2SlaveMonitor.java @@ -1,7 +1,5 @@ package hudson.plugins.ec2; -import static hudson.plugins.ec2.EC2Cloud.EC2_REQUEST_EXPIRED_ERROR_CODE; - import com.amazonaws.AmazonClientException; import com.amazonaws.services.ec2.model.AmazonEC2Exception; import hudson.Extension; @@ -52,7 +50,8 @@ private void removeDeadNodes() { } } catch (AmazonClientException e) { if (e instanceof AmazonEC2Exception - && EC2_REQUEST_EXPIRED_ERROR_CODE.equals(((AmazonEC2Exception) e).getErrorCode())) { + && EC2Cloud.EC2_REQUEST_EXPIRED_ERROR_CODE.equals( + ((AmazonEC2Exception) e).getErrorCode())) { LOGGER.info("EC2 request expired, skipping consideration of " + ec2Slave.getInstanceId() + " due to unknown state."); } else { diff --git a/src/main/java/hudson/plugins/ec2/EC2Step.java b/src/main/java/hudson/plugins/ec2/EC2Step.java index 9407462cf..57135242c 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Step.java +++ b/src/main/java/hudson/plugins/ec2/EC2Step.java @@ -29,9 +29,17 @@ import hudson.model.TaskListener; import hudson.slaves.Cloud; import hudson.util.ListBoxModel; -import java.util.*; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; import jenkins.model.Jenkins; -import org.jenkinsci.plugins.workflow.steps.*; +import org.jenkinsci.plugins.workflow.steps.Step; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jenkinsci.plugins.workflow.steps.StepDescriptor; +import org.jenkinsci.plugins.workflow.steps.StepExecution; +import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.verb.POST; diff --git a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java index eb878be26..859d2065d 100644 --- a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java +++ b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java @@ -18,12 +18,6 @@ */ package hudson.plugins.ec2; -import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_JAVA_PATH; -import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED; -import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT; -import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED; -import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED; - import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.auth.AWSCredentialsProvider; @@ -363,7 +357,7 @@ public SlaveTemplate( if (StringUtils.isNotBlank(javaPath)) { this.javaPath = javaPath; } else { - this.javaPath = DEFAULT_JAVA_PATH; + this.javaPath = EC2AbstractSlave.DEFAULT_JAVA_PATH; } this.jvmopts = jvmopts; @@ -409,12 +403,16 @@ public SlaveTemplate( : HostKeyVerificationStrategyEnum.CHECK_NEW_SOFT; this.tenancy = tenancy != null ? tenancy : Tenancy.Default; this.ebsEncryptRootVolume = ebsEncryptRootVolume != null ? ebsEncryptRootVolume : EbsEncryptRootVolume.DEFAULT; - this.metadataSupported = metadataSupported != null ? metadataSupported : DEFAULT_METADATA_SUPPORTED; - this.metadataEndpointEnabled = - metadataEndpointEnabled != null ? metadataEndpointEnabled : DEFAULT_METADATA_ENDPOINT_ENABLED; - this.metadataTokensRequired = - metadataTokensRequired != null ? metadataTokensRequired : DEFAULT_METADATA_TOKENS_REQUIRED; - this.metadataHopsLimit = metadataHopsLimit != null ? metadataHopsLimit : DEFAULT_METADATA_HOPS_LIMIT; + this.metadataSupported = + metadataSupported != null ? metadataSupported : EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED; + this.metadataEndpointEnabled = metadataEndpointEnabled != null + ? metadataEndpointEnabled + : EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED; + this.metadataTokensRequired = metadataTokensRequired != null + ? metadataTokensRequired + : EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED; + this.metadataHopsLimit = + metadataHopsLimit != null ? metadataHopsLimit : EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT; readResolve(); // initialize } @@ -481,7 +479,7 @@ public SlaveTemplate( numExecutors, remoteAdmin, amiType, - DEFAULT_JAVA_PATH, + EC2AbstractSlave.DEFAULT_JAVA_PATH, jvmopts, stopOnTerminate, subnetId, @@ -508,7 +506,7 @@ public SlaveTemplate( metadataEndpointEnabled, metadataTokensRequired, metadataHopsLimit, - DEFAULT_METADATA_SUPPORTED); + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); } @Deprecated @@ -569,7 +567,7 @@ public SlaveTemplate( numExecutors, remoteAdmin, amiType, - DEFAULT_JAVA_PATH, + EC2AbstractSlave.DEFAULT_JAVA_PATH, jvmopts, stopOnTerminate, subnetId, @@ -593,10 +591,10 @@ public SlaveTemplate( hostKeyVerificationStrategy, tenancy, null, - DEFAULT_METADATA_ENDPOINT_ENABLED, - DEFAULT_METADATA_TOKENS_REQUIRED, - DEFAULT_METADATA_HOPS_LIMIT, - DEFAULT_METADATA_SUPPORTED); + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); } @Deprecated @@ -2687,7 +2685,7 @@ protected Object readResolve() { metadataHopsLimit = EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT; } if (StringUtils.isBlank(javaPath)) { - javaPath = DEFAULT_JAVA_PATH; + javaPath = EC2AbstractSlave.DEFAULT_JAVA_PATH; } return this; diff --git a/src/main/java/hudson/plugins/ec2/SpotConfiguration.java b/src/main/java/hudson/plugins/ec2/SpotConfiguration.java index ee5a1bb52..577a55370 100644 --- a/src/main/java/hudson/plugins/ec2/SpotConfiguration.java +++ b/src/main/java/hudson/plugins/ec2/SpotConfiguration.java @@ -1,7 +1,5 @@ package hudson.plugins.ec2; -import static hudson.Functions.checkPermission; - import com.amazonaws.AmazonServiceException; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.services.ec2.AmazonEC2; @@ -11,6 +9,7 @@ import com.amazonaws.services.ec2.model.InstanceType; import com.amazonaws.services.ec2.model.SpotPrice; import hudson.Extension; +import hudson.Functions; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; import hudson.plugins.ec2.util.AmazonEC2Factory; @@ -150,7 +149,7 @@ public FormValidation doCurrentSpotPrice( @QueryParameter String ami) throws IOException, ServletException { - checkPermission(EC2Cloud.PROVISION); + Functions.checkPermission(EC2Cloud.PROVISION); String cp = ""; String zoneStr = ""; diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index c26805e23..9ab15d263 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -26,20 +26,37 @@ import com.amazonaws.AmazonClientException; import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.ec2.model.KeyPair; -import com.trilead.ssh2.*; +import com.trilead.ssh2.Connection; +import com.trilead.ssh2.HTTPProxyData; +import com.trilead.ssh2.SCPClient; +import com.trilead.ssh2.ServerHostKeyVerifier; +import com.trilead.ssh2.Session; import hudson.FilePath; import hudson.ProxyConfiguration; import hudson.Util; import hudson.model.Descriptor; import hudson.model.TaskListener; -import hudson.plugins.ec2.*; +import hudson.plugins.ec2.ConnectionStrategy; +import hudson.plugins.ec2.EC2AbstractSlave; +import hudson.plugins.ec2.EC2Cloud; +import hudson.plugins.ec2.EC2Computer; +import hudson.plugins.ec2.EC2ComputerLauncher; +import hudson.plugins.ec2.EC2HostAddressProvider; +import hudson.plugins.ec2.EC2PrivateKey; +import hudson.plugins.ec2.EC2Readiness; +import hudson.plugins.ec2.EC2SpotSlave; +import hudson.plugins.ec2.SlaveTemplate; import hudson.plugins.ec2.ssh.verifiers.HostKey; import hudson.plugins.ec2.ssh.verifiers.Messages; import hudson.remoting.Channel; import hudson.remoting.Channel.Listener; import hudson.slaves.CommandLauncher; import hudson.slaves.ComputerLauncher; -import java.io.*; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintStream; import java.net.InetSocketAddress; import java.net.Proxy; import java.nio.charset.StandardCharsets; diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index c81803fa5..f6194df8e 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -36,7 +36,16 @@ import hudson.Util; import hudson.model.Descriptor; import hudson.model.TaskListener; -import hudson.plugins.ec2.*; +import hudson.plugins.ec2.ConnectionStrategy; +import hudson.plugins.ec2.EC2AbstractSlave; +import hudson.plugins.ec2.EC2Cloud; +import hudson.plugins.ec2.EC2Computer; +import hudson.plugins.ec2.EC2ComputerLauncher; +import hudson.plugins.ec2.EC2HostAddressProvider; +import hudson.plugins.ec2.EC2PrivateKey; +import hudson.plugins.ec2.EC2Readiness; +import hudson.plugins.ec2.EC2SpotSlave; +import hudson.plugins.ec2.SlaveTemplate; import hudson.plugins.ec2.ssh.verifiers.HostKey; import hudson.plugins.ec2.ssh.verifiers.HostKeyHelper; import hudson.plugins.ec2.ssh.verifiers.Messages; diff --git a/src/main/java/hudson/plugins/ec2/util/EC2AgentFactory.java b/src/main/java/hudson/plugins/ec2/util/EC2AgentFactory.java index aca9d136d..21dd3f56d 100644 --- a/src/main/java/hudson/plugins/ec2/util/EC2AgentFactory.java +++ b/src/main/java/hudson/plugins/ec2/util/EC2AgentFactory.java @@ -1,7 +1,8 @@ package hudson.plugins.ec2.util; import hudson.model.Descriptor; -import hudson.plugins.ec2.*; +import hudson.plugins.ec2.EC2OndemandSlave; +import hudson.plugins.ec2.EC2SpotSlave; import java.io.IOException; import jenkins.model.Jenkins; diff --git a/src/main/java/hudson/plugins/ec2/util/EC2AgentFactoryImpl.java b/src/main/java/hudson/plugins/ec2/util/EC2AgentFactoryImpl.java index 3e1dd6d95..1f73e6400 100644 --- a/src/main/java/hudson/plugins/ec2/util/EC2AgentFactoryImpl.java +++ b/src/main/java/hudson/plugins/ec2/util/EC2AgentFactoryImpl.java @@ -2,7 +2,8 @@ import hudson.Extension; import hudson.model.Descriptor; -import hudson.plugins.ec2.*; +import hudson.plugins.ec2.EC2OndemandSlave; +import hudson.plugins.ec2.EC2SpotSlave; import java.io.IOException; @Extension diff --git a/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java b/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java index 4f6951535..a91a54c6c 100644 --- a/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java +++ b/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java @@ -9,7 +9,12 @@ import hudson.model.Descriptor; import hudson.model.TaskListener; import hudson.os.WindowsUtil; -import hudson.plugins.ec2.*; +import hudson.plugins.ec2.EC2AbstractSlave; +import hudson.plugins.ec2.EC2Computer; +import hudson.plugins.ec2.EC2ComputerLauncher; +import hudson.plugins.ec2.EC2HostAddressProvider; +import hudson.plugins.ec2.EC2PrivateKey; +import hudson.plugins.ec2.SlaveTemplate; import hudson.plugins.ec2.win.winrm.WindowsProcess; import hudson.remoting.Channel; import hudson.remoting.Channel.Listener; diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/NegotiateNTLMSchemaFactory.java b/src/main/java/hudson/plugins/ec2/win/winrm/NegotiateNTLMSchemaFactory.java index 749f331d7..9c0b7e7a5 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/NegotiateNTLMSchemaFactory.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/NegotiateNTLMSchemaFactory.java @@ -2,7 +2,11 @@ import org.apache.http.Header; import org.apache.http.HttpRequest; -import org.apache.http.auth.*; +import org.apache.http.auth.AuthScheme; +import org.apache.http.auth.AuthSchemeProvider; +import org.apache.http.auth.AuthenticationException; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.NTCredentials; import org.apache.http.client.config.AuthSchemes; import org.apache.http.impl.auth.NTLMScheme; import org.apache.http.message.BufferedHeader; diff --git a/src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java b/src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java index 716b3f929..9defc3ee2 100644 --- a/src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java +++ b/src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java @@ -24,7 +24,8 @@ package hudson.plugins.ec2; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; diff --git a/src/test/java/hudson/plugins/ec2/AmazonEC2CloudUnitTest.java b/src/test/java/hudson/plugins/ec2/AmazonEC2CloudUnitTest.java index 08a2bbdee..fd285cc2e 100644 --- a/src/test/java/hudson/plugins/ec2/AmazonEC2CloudUnitTest.java +++ b/src/test/java/hudson/plugins/ec2/AmazonEC2CloudUnitTest.java @@ -23,7 +23,6 @@ */ package hudson.plugins.ec2; -import static hudson.plugins.ec2.EC2Cloud.DEFAULT_EC2_ENDPOINT; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -56,8 +55,8 @@ public class AmazonEC2CloudUnitTest { public void testEC2EndpointURLCreation() throws MalformedURLException { AmazonEC2Cloud.DescriptorImpl descriptor = new AmazonEC2Cloud.DescriptorImpl(); - assertEquals(new URL(DEFAULT_EC2_ENDPOINT), descriptor.determineEC2EndpointURL(null)); - assertEquals(new URL(DEFAULT_EC2_ENDPOINT), descriptor.determineEC2EndpointURL("")); + assertEquals(new URL(EC2Cloud.DEFAULT_EC2_ENDPOINT), descriptor.determineEC2EndpointURL(null)); + assertEquals(new URL(EC2Cloud.DEFAULT_EC2_ENDPOINT), descriptor.determineEC2EndpointURL("")); assertEquals(new URL("https://www.abc.com"), descriptor.determineEC2EndpointURL("https://www.abc.com")); } diff --git a/src/test/java/hudson/plugins/ec2/ConfigurationAsCodeTest.java b/src/test/java/hudson/plugins/ec2/ConfigurationAsCodeTest.java index 7f188817b..24aa6d8fa 100644 --- a/src/test/java/hudson/plugins/ec2/ConfigurationAsCodeTest.java +++ b/src/test/java/hudson/plugins/ec2/ConfigurationAsCodeTest.java @@ -1,8 +1,5 @@ package hudson.plugins.ec2; -import static io.jenkins.plugins.casc.misc.Util.getJenkinsRoot; -import static io.jenkins.plugins.casc.misc.Util.toStringFromYamlFile; -import static io.jenkins.plugins.casc.misc.Util.toYamlString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; @@ -17,6 +14,7 @@ import io.jenkins.plugins.casc.ConfiguratorRegistry; import io.jenkins.plugins.casc.misc.ConfiguredWithCode; import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; +import io.jenkins.plugins.casc.misc.Util; import io.jenkins.plugins.casc.model.CNode; import java.time.LocalTime; import java.util.Arrays; @@ -144,9 +142,9 @@ public void testBackwardsCompatibleConnectionStrategy() throws Exception { public void testConfigAsCodeExport() throws Exception { ConfiguratorRegistry registry = ConfiguratorRegistry.get(); ConfigurationContext context = new ConfigurationContext(registry); - CNode clouds = getJenkinsRoot(context).get("clouds"); - String exported = toYamlString(clouds); - String expected = toStringFromYamlFile(this, "UnixDataExport.yml"); + CNode clouds = Util.getJenkinsRoot(context).get("clouds"); + String exported = Util.toYamlString(clouds); + String expected = Util.toStringFromYamlFile(this, "UnixDataExport.yml"); assertEquals(expected, exported); } @@ -155,9 +153,9 @@ public void testConfigAsCodeExport() throws Exception { public void testConfigAsCodeWithAltEndpointAndJavaPathExport() throws Exception { ConfiguratorRegistry registry = ConfiguratorRegistry.get(); ConfigurationContext context = new ConfigurationContext(registry); - CNode clouds = getJenkinsRoot(context).get("clouds"); - String exported = toYamlString(clouds); - String expected = toStringFromYamlFile(this, "UnixDataExport-withAltEndpointAndJavaPath.yml"); + CNode clouds = Util.getJenkinsRoot(context).get("clouds"); + String exported = Util.toYamlString(clouds); + String expected = Util.toStringFromYamlFile(this, "UnixDataExport-withAltEndpointAndJavaPath.yml"); assertEquals(expected, exported); } @@ -280,9 +278,9 @@ public void testMac() throws Exception { public void testMacCloudConfigAsCodeExport() throws Exception { ConfiguratorRegistry registry = ConfiguratorRegistry.get(); ConfigurationContext context = new ConfigurationContext(registry); - CNode clouds = getJenkinsRoot(context).get("clouds"); - String exported = toYamlString(clouds); - String expected = toStringFromYamlFile(this, "MacDataExport.yml"); + CNode clouds = Util.getJenkinsRoot(context).get("clouds"); + String exported = Util.toYamlString(clouds); + String expected = Util.toStringFromYamlFile(this, "MacDataExport.yml"); assertEquals(expected, exported); } } diff --git a/src/test/java/hudson/plugins/ec2/EC2AbstractSlaveTest.java b/src/test/java/hudson/plugins/ec2/EC2AbstractSlaveTest.java index 1cd1374ac..96baff2ab 100644 --- a/src/test/java/hudson/plugins/ec2/EC2AbstractSlaveTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2AbstractSlaveTest.java @@ -1,9 +1,5 @@ package hudson.plugins.ec2; -import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED; -import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT; -import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED; -import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED; import static org.junit.Assert.assertEquals; import com.amazonaws.services.ec2.model.InstanceType; @@ -49,10 +45,10 @@ public void testGetLaunchTimeoutInMillisShouldNotOverflow() throws Exception { ConnectionStrategy.PRIVATE_IP, -1, Tenancy.Default, - DEFAULT_METADATA_ENDPOINT_ENABLED, - DEFAULT_METADATA_TOKENS_REQUIRED, - DEFAULT_METADATA_HOPS_LIMIT, - DEFAULT_METADATA_SUPPORTED) { + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED) { @Override public void terminate() { @@ -114,10 +110,10 @@ public void testMaxUsesBackwardCompat() throws Exception { HostKeyVerificationStrategyEnum.CHECK_NEW_HARD, Tenancy.Default, EbsEncryptRootVolume.DEFAULT, - DEFAULT_METADATA_ENDPOINT_ENABLED, - DEFAULT_METADATA_TOKENS_REQUIRED, - DEFAULT_METADATA_HOPS_LIMIT, - DEFAULT_METADATA_SUPPORTED); + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); List templates = new ArrayList<>(); templates.add(orig); String cloudName = "us-east-1"; diff --git a/src/test/java/hudson/plugins/ec2/EC2CloudMigrationTest.java b/src/test/java/hudson/plugins/ec2/EC2CloudMigrationTest.java index ae0382c6c..bb2eebd0d 100644 --- a/src/test/java/hudson/plugins/ec2/EC2CloudMigrationTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2CloudMigrationTest.java @@ -1,6 +1,8 @@ package hudson.plugins.ec2; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey; import com.cloudbees.plugins.credentials.SystemCredentialsProvider; diff --git a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java index 68c672fd3..60c37cb4a 100644 --- a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java @@ -1,6 +1,8 @@ package hudson.plugins.ec2; -import static org.junit.Assert.*; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; diff --git a/src/test/java/hudson/plugins/ec2/EC2HostAddressProviderTest.java b/src/test/java/hudson/plugins/ec2/EC2HostAddressProviderTest.java index e8b6fec9c..7ea9e3c71 100644 --- a/src/test/java/hudson/plugins/ec2/EC2HostAddressProviderTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2HostAddressProviderTest.java @@ -1,6 +1,5 @@ package hudson.plugins.ec2; -import static hudson.plugins.ec2.EC2HostAddressProvider.*; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.Mockito.mock; @@ -18,7 +17,7 @@ public void unix_publicDnsStrategy_isPresent() { when(instance.getPublicDnsName()).thenReturn("ec2-0-0-0-0.compute-1.amazonaws.com"); - assertThat(unix(instance, strategy), equalTo("ec2-0-0-0-0.compute-1.amazonaws.com")); + assertThat(EC2HostAddressProvider.unix(instance, strategy), equalTo("ec2-0-0-0-0.compute-1.amazonaws.com")); } @Test @@ -29,7 +28,7 @@ public void unix_publicDnsStrategy_notPresent() { when(instance.getPublicDnsName()).thenReturn(""); when(instance.getPublicIpAddress()).thenReturn("0.0.0.0"); - assertThat(unix(instance, strategy), equalTo("0.0.0.0")); + assertThat(EC2HostAddressProvider.unix(instance, strategy), equalTo("0.0.0.0")); } @Test @@ -39,7 +38,7 @@ public void unix_publicIpStrategy() { when(instance.getPublicIpAddress()).thenReturn("0.0.0.0"); - assertThat(unix(instance, strategy), equalTo("0.0.0.0")); + assertThat(EC2HostAddressProvider.unix(instance, strategy), equalTo("0.0.0.0")); } @Test @@ -49,7 +48,7 @@ public void unix_privateDnsStrategy_isPresent() { when(instance.getPrivateDnsName()).thenReturn("0-0-0-0.ec2.internal"); - assertThat(unix(instance, strategy), equalTo("0-0-0-0.ec2.internal")); + assertThat(EC2HostAddressProvider.unix(instance, strategy), equalTo("0-0-0-0.ec2.internal")); } @Test @@ -60,7 +59,7 @@ public void unix_privateDnsStrategy_notPresent() { when(instance.getPrivateDnsName()).thenReturn(""); when(instance.getPrivateIpAddress()).thenReturn("0.0.0.0"); - assertThat(unix(instance, strategy), equalTo("0.0.0.0")); + assertThat(EC2HostAddressProvider.unix(instance, strategy), equalTo("0.0.0.0")); } @Test @@ -70,7 +69,7 @@ public void unix_privateIpStrategy() { when(instance.getPrivateIpAddress()).thenReturn("0.0.0.0"); - assertThat(unix(instance, strategy), equalTo("0.0.0.0")); + assertThat(EC2HostAddressProvider.unix(instance, strategy), equalTo("0.0.0.0")); } @Test @@ -81,7 +80,7 @@ public void windows_privateDnsStrategy() { when(instance.getPrivateDnsName()).thenReturn("0-0-0-0.ec2.internal"); when(instance.getPrivateIpAddress()).thenReturn("0.0.0.0"); - assertThat(windows(instance, strategy), equalTo("0.0.0.0")); + assertThat(EC2HostAddressProvider.windows(instance, strategy), equalTo("0.0.0.0")); } @Test @@ -92,7 +91,7 @@ public void windows_privateIpStrategy() { when(instance.getPrivateDnsName()).thenReturn(""); when(instance.getPrivateIpAddress()).thenReturn("0.0.0.0"); - assertThat(windows(instance, strategy), equalTo("0.0.0.0")); + assertThat(EC2HostAddressProvider.windows(instance, strategy), equalTo("0.0.0.0")); } @Test @@ -103,7 +102,7 @@ public void windows_publicDnsStrategy() { when(instance.getPublicDnsName()).thenReturn("ec2-0-0-0-0.compute-1.amazonaws.com"); when(instance.getPublicIpAddress()).thenReturn("0.0.0.0"); - assertThat(windows(instance, strategy), equalTo("0.0.0.0")); + assertThat(EC2HostAddressProvider.windows(instance, strategy), equalTo("0.0.0.0")); } @Test @@ -114,6 +113,6 @@ public void windows_publicIpStrategy() { when(instance.getPublicDnsName()).thenReturn(""); when(instance.getPublicIpAddress()).thenReturn("0.0.0.0"); - assertThat(windows(instance, strategy), equalTo("0.0.0.0")); + assertThat(EC2HostAddressProvider.windows(instance, strategy), equalTo("0.0.0.0")); } } diff --git a/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java b/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java index 5ed327b4e..b43487263 100644 --- a/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java +++ b/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java @@ -11,7 +11,14 @@ import com.amazonaws.AmazonServiceException; import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.AmazonEC2Client; -import com.amazonaws.services.ec2.model.*; +import com.amazonaws.services.ec2.model.BlockDeviceMapping; +import com.amazonaws.services.ec2.model.CreateTagsResult; +import com.amazonaws.services.ec2.model.DescribeImagesRequest; +import com.amazonaws.services.ec2.model.EbsBlockDevice; +import com.amazonaws.services.ec2.model.Filter; +import com.amazonaws.services.ec2.model.Image; +import com.amazonaws.services.ec2.model.InstanceType; +import com.amazonaws.services.ec2.model.Tag; import hudson.model.Node; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; diff --git a/src/test/java/hudson/plugins/ec2/util/AmazonEC2FactoryMockImpl.java b/src/test/java/hudson/plugins/ec2/util/AmazonEC2FactoryMockImpl.java index 6b1b7f7e1..f7dd1c87b 100644 --- a/src/test/java/hudson/plugins/ec2/util/AmazonEC2FactoryMockImpl.java +++ b/src/test/java/hudson/plugins/ec2/util/AmazonEC2FactoryMockImpl.java @@ -5,7 +5,39 @@ import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.AmazonEC2Client; -import com.amazonaws.services.ec2.model.*; +import com.amazonaws.services.ec2.model.BlockDeviceMapping; +import com.amazonaws.services.ec2.model.DescribeImagesRequest; +import com.amazonaws.services.ec2.model.DescribeImagesResult; +import com.amazonaws.services.ec2.model.DescribeInstancesRequest; +import com.amazonaws.services.ec2.model.DescribeInstancesResult; +import com.amazonaws.services.ec2.model.DescribeKeyPairsResult; +import com.amazonaws.services.ec2.model.DescribeRegionsResult; +import com.amazonaws.services.ec2.model.DescribeSecurityGroupsRequest; +import com.amazonaws.services.ec2.model.DescribeSecurityGroupsResult; +import com.amazonaws.services.ec2.model.DescribeSpotInstanceRequestsRequest; +import com.amazonaws.services.ec2.model.DescribeSpotInstanceRequestsResult; +import com.amazonaws.services.ec2.model.DescribeSubnetsRequest; +import com.amazonaws.services.ec2.model.DescribeSubnetsResult; +import com.amazonaws.services.ec2.model.EbsBlockDevice; +import com.amazonaws.services.ec2.model.Image; +import com.amazonaws.services.ec2.model.Instance; +import com.amazonaws.services.ec2.model.InstanceState; +import com.amazonaws.services.ec2.model.InstanceStateChange; +import com.amazonaws.services.ec2.model.InstanceStateName; +import com.amazonaws.services.ec2.model.KeyPairInfo; +import com.amazonaws.services.ec2.model.Region; +import com.amazonaws.services.ec2.model.Reservation; +import com.amazonaws.services.ec2.model.RunInstancesRequest; +import com.amazonaws.services.ec2.model.RunInstancesResult; +import com.amazonaws.services.ec2.model.SecurityGroup; +import com.amazonaws.services.ec2.model.SpotInstanceRequest; +import com.amazonaws.services.ec2.model.SpotInstanceState; +import com.amazonaws.services.ec2.model.SpotInstanceType; +import com.amazonaws.services.ec2.model.Subnet; +import com.amazonaws.services.ec2.model.Tag; +import com.amazonaws.services.ec2.model.TagSpecification; +import com.amazonaws.services.ec2.model.TerminateInstancesRequest; +import com.amazonaws.services.ec2.model.TerminateInstancesResult; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; import hudson.plugins.ec2.AmazonEC2Cloud; diff --git a/src/test/java/hudson/plugins/ec2/util/EC2AgentFactoryMockImpl.java b/src/test/java/hudson/plugins/ec2/util/EC2AgentFactoryMockImpl.java index ffdeda1f7..9ffbeda9f 100644 --- a/src/test/java/hudson/plugins/ec2/util/EC2AgentFactoryMockImpl.java +++ b/src/test/java/hudson/plugins/ec2/util/EC2AgentFactoryMockImpl.java @@ -4,7 +4,14 @@ import hudson.Extension; import hudson.model.Computer; import hudson.model.Descriptor; -import hudson.plugins.ec2.*; +import hudson.plugins.ec2.AMITypeData; +import hudson.plugins.ec2.ConnectionStrategy; +import hudson.plugins.ec2.EC2AbstractSlave; +import hudson.plugins.ec2.EC2Computer; +import hudson.plugins.ec2.EC2OndemandSlave; +import hudson.plugins.ec2.EC2SpotSlave; +import hudson.plugins.ec2.EC2Tag; +import hudson.plugins.ec2.Tenancy; import hudson.slaves.NodeProperty; import java.io.IOException; import java.util.List; diff --git a/src/test/java/hudson/plugins/ec2/win/WinConnectionTest.java b/src/test/java/hudson/plugins/ec2/win/WinConnectionTest.java index 61d56c64f..257189afa 100644 --- a/src/test/java/hudson/plugins/ec2/win/WinConnectionTest.java +++ b/src/test/java/hudson/plugins/ec2/win/WinConnectionTest.java @@ -1,8 +1,8 @@ package hudson.plugins.ec2.win; -import static junit.framework.TestCase.assertTrue; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeThat; import hudson.plugins.ec2.win.winrm.WindowsProcess; From e2ae5e0bd90d06ebfd891f6792f4a76a8f453436 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 16 Dec 2024 17:34:42 -0800 Subject: [PATCH 078/267] Simplify assertion --- src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java b/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java index ce5e8766a..c86ac19a1 100644 --- a/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java @@ -474,7 +474,7 @@ public void testOnUsageCountRetention() throws Exception { } // As we want to terminate agent both for usageCount 1 & 0 - setting this to true if (usageCount == 1 || usageCount == 0) { - assertEquals("Expected " + usageCount + " to be " + true, true, terminateCalled.get()); + assertTrue("Expected " + usageCount + " to be " + true, terminateCalled.get()); // Reset the assumption terminateCalled.set(false); } else { From 92c7809e2d8dbd4e9c71caf616edc11c9b991216 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Tue, 17 Dec 2024 14:52:16 +0100 Subject: [PATCH 079/267] Use Apache Mina as ssh transport layer, remove trilead --- pom.xml | 12 +- .../plugins/ec2/EC2ComputerLauncher.java | 27 ++ .../plugins/ec2/ssh/EC2MacLauncher.java | 441 ++++++++++-------- .../plugins/ec2/ssh/EC2UnixLauncher.java | 431 ++++++++++------- .../plugins/ec2/ssh/HostKeyVerifierImpl.java | 4 +- .../ec2/ssh/proxy/ProxyCONNECTListener.java | 73 +++ .../plugins/ec2/ssh/verifiers/HostKey.java | 11 +- .../hudson/plugins/ec2/util/PEMParser.java | 74 +++ .../java/hudson/plugins/ec2/HostKeyTest.java | 122 +++++ .../SshHostKeyVerificationStrategyTest.java | 35 +- .../plugins/ec2/util/ConnectionRule.java | 38 +- 11 files changed, 867 insertions(+), 401 deletions(-) create mode 100644 src/main/java/hudson/plugins/ec2/ssh/proxy/ProxyCONNECTListener.java create mode 100644 src/main/java/hudson/plugins/ec2/util/PEMParser.java create mode 100644 src/test/java/hudson/plugins/ec2/HostKeyTest.java diff --git a/pom.xml b/pom.xml index 9b7ab734f..e27c56c9d 100644 --- a/pom.xml +++ b/pom.xml @@ -108,6 +108,14 @@ THE SOFTWARE.
+ + 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 @@ -137,10 +145,6 @@ THE SOFTWARE. org.jenkins-ci.plugins ssh-credentials - - org.jenkins-ci.plugins - trilead-api - org.jenkins-ci.plugins.aws-java-sdk aws-java-sdk-ec2 diff --git a/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java b/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java index b268ed941..9a041503c 100644 --- a/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java +++ b/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java @@ -23,13 +23,23 @@ */ package hudson.plugins.ec2; +import static org.apache.sshd.client.session.ClientSession.REMOTE_COMMAND_WAIT_EVENTS; + import com.amazonaws.AmazonClientException; import hudson.model.TaskListener; import hudson.slaves.ComputerLauncher; import hudson.slaves.SlaveComputer; import java.io.IOException; +import java.time.Duration; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import org.apache.sshd.client.channel.ClientChannel; +import org.apache.sshd.client.channel.ClientChannelEvent; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.scp.client.CloseableScpClient; +import org.apache.sshd.scp.client.ScpClient; +import org.apache.sshd.scp.client.ScpClientCreator; /** * {@link ComputerLauncher} for EC2 that wraps the real user-specified {@link ComputerLauncher}. @@ -39,6 +49,8 @@ public abstract class EC2ComputerLauncher extends ComputerLauncher { private static final Logger LOGGER = Logger.getLogger(EC2ComputerLauncher.class.getName()); + private static final long timeout = Duration.ofSeconds(10).toMillis(); + @Override public void launch(SlaveComputer slaveComputer, TaskListener listener) { try { @@ -81,4 +93,19 @@ public void launch(SlaveComputer slaveComputer, TaskListener listener) { */ protected abstract void launchScript(EC2Computer computer, TaskListener listener) throws AmazonClientException, IOException, InterruptedException; + + protected int waitCompletion(ClientChannel clientChannel) { + Set clientChannelEvents = clientChannel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); + if (clientChannelEvents.contains(ClientChannelEvent.TIMEOUT)) { + return -1; + } else { + return clientChannel.getExitStatus(); + } + } + + protected CloseableScpClient createScpClient(ClientSession session) { + ScpClientCreator creator = ScpClientCreator.instance(); + ScpClient client = creator.createScpClient(session); + return CloseableScpClient.singleSessionInstance(client); + } } diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index 9ab15d263..1e5d1483c 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -23,14 +23,11 @@ */ package hudson.plugins.ec2.ssh; +import static org.apache.sshd.client.session.ClientSession.REMOTE_COMMAND_WAIT_EVENTS; + import com.amazonaws.AmazonClientException; import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.ec2.model.KeyPair; -import com.trilead.ssh2.Connection; -import com.trilead.ssh2.HTTPProxyData; -import com.trilead.ssh2.SCPClient; -import com.trilead.ssh2.ServerHostKeyVerifier; -import com.trilead.ssh2.Session; import hudson.FilePath; import hudson.ProxyConfiguration; import hudson.Util; @@ -46,8 +43,10 @@ import hudson.plugins.ec2.EC2Readiness; import hudson.plugins.ec2.EC2SpotSlave; import hudson.plugins.ec2.SlaveTemplate; +import hudson.plugins.ec2.ssh.proxy.ProxyCONNECTListener; import hudson.plugins.ec2.ssh.verifiers.HostKey; import hudson.plugins.ec2.ssh.verifiers.Messages; +import hudson.plugins.ec2.util.PEMParser; import hudson.remoting.Channel; import hudson.remoting.Channel.Listener; import hudson.slaves.CommandLauncher; @@ -55,17 +54,33 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintStream; import java.net.InetSocketAddress; import java.net.Proxy; +import java.net.SocketAddress; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermission; +import java.security.PublicKey; +import java.time.Duration; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.Jenkins; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.channel.ClientChannel; +import org.apache.sshd.client.channel.ClientChannelEvent; +import org.apache.sshd.client.future.ConnectFuture; +import org.apache.sshd.client.keyverifier.ServerKeyVerifier; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.scp.client.CloseableScpClient; /** * {@link ComputerLauncher} that connects to a Unix agent on EC2 by using SSH. @@ -87,6 +102,8 @@ public class EC2MacLauncher extends EC2ComputerLauncher { private static int readinessSleepMs = 1000; private static int readinessTries = 120; + private static final long timeout = Duration.ofSeconds(10).toMillis(); + static { String prop = System.getProperty(BOOTSTRAP_AUTH_SLEEP_MS); if (prop != null) { @@ -133,8 +150,8 @@ protected String buildUpCommand(EC2Computer computer, String command) { @Override protected void launchScript(EC2Computer computer, TaskListener listener) throws IOException, AmazonClientException, InterruptedException { - final Connection conn; - Connection cleanupConn = null; // java's code path analysis for final + final ClientSession clientSession; + ClientSession cleanupClientSession = null; // java's code path analysis for final // doesn't work that well. boolean successful = false; PrintStream logger = listener.getLogger(); @@ -174,7 +191,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) logInfo(computer, listener, "Launching instance: " + node.getInstanceId()); - try { + try (SshClient client = SshClient.setUpDefaultClient()) { boolean isBootstrapped = bootstrap(computer, listener, template); if (isBootstrapped) { int bootDelay = node.getBootDelay(); @@ -186,13 +203,21 @@ protected void launchScript(EC2Computer computer, TaskListener listener) Thread.sleep(bootDelay); logInfo(computer, listener, "SSH service should have stabilized"); } + // connect fresh as ROOT logInfo(computer, listener, "connect fresh as root"); - cleanupConn = connectToSsh(computer, listener, template); + cleanupClientSession = connectToSsh(client, computer, listener, template); KeyPair key = computer.getCloud().getKeyPair(); - if (key == null - || !cleanupConn.authenticateWithPublicKey( - computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), "")) { + + final boolean isAuthenticated; + if (key == null) { + isAuthenticated = false; + } else { + cleanupClientSession.addPublicKeyIdentity(PEMParser.decodeKeyPair(key.getKeyMaterial(), "")); + cleanupClientSession.auth().await(timeout); + isAuthenticated = cleanupClientSession.isAuthenticated(); + } + if (!isAuthenticated) { logWarning(computer, listener, "Authentication failed"); return; // failed to connect as root. } @@ -200,158 +225,201 @@ protected void launchScript(EC2Computer computer, TaskListener listener) logWarning(computer, listener, "bootstrapresult failed"); return; // bootstrap closed for us. } - conn = cleanupConn; - - SCPClient scp = conn.createSCPClient(); - String initScript = node.initScript; - String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); - - logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); - conn.exec("mkdir -p " + tmpDir, logger); - - if (initScript != null - && !initScript.trim().isEmpty() - && conn.exec("test -e ~/.hudson-run-init", logger) != 0) { - logInfo(computer, listener, "Executing init script"); - scp.put(initScript.getBytes(StandardCharsets.UTF_8), "init.sh", tmpDir, "0700"); - Session sess = conn.openSession(); - sess.requestDumbPTY(); // so that the remote side bundles stdout - // and stderr - sess.execCommand(buildUpCommand(computer, tmpDir + "/init.sh")); - - sess.getStdin().close(); // nothing to write here - sess.getStderr().close(); // we are not supposed to get anything - // from stderr - IOUtils.copy(sess.getStdout(), logger); - - int exitStatus = waitCompletion(sess); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } - sess.close(); + clientSession = cleanupClientSession; + + try (CloseableScpClient scp = createScpClient(clientSession)) { + String initScript = node.initScript; + String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); + + logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); + executeRemote(clientSession, "mkdir -p " + tmpDir, logger); + + if (initScript != null + && !initScript.trim().isEmpty() + && executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { + logInfo(computer, listener, "Executing init script"); + scp.upload( + initScript.getBytes(StandardCharsets.UTF_8), + tmpDir + "/init.sh", + List.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_READ), + null); + + String initCommand = buildUpCommand(computer, tmpDir + "/init.sh"); + try (ClientChannel channel = clientSession.createExecChannel( + initCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { + + channel.getInvertedIn().close(); // nothing to write here + channel.open().await(timeout); + + Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); + + if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { + logWarning(computer, listener, "init script timed out"); + return; + } + + int exitStatus = waitCompletion(channel); + if (exitStatus != 0) { + logWarning(computer, listener, "init script failed: exit code=" + exitStatus); + return; + } + + channel.getInvertedErr().close(); // we are not supposed to get anything from stderr + IOUtils.copy(channel.getInvertedOut(), logger); + } - logInfo(computer, listener, "Creating ~/.hudson-run-init"); + logInfo(computer, listener, "Creating ~/.hudson-run-init"); + String createHudsonRunInitCommand = buildUpCommand(computer, "touch ~/.hudson-run-init"); + try (ClientChannel channel = clientSession.createExecChannel( + createHudsonRunInitCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { + channel.getInvertedIn().close(); // nothing to write here + channel.open().await(timeout); - // Needs a tty to run sudo. - sess = conn.openSession(); - sess.requestDumbPTY(); // so that the remote side bundles stdout - // and stderr - sess.execCommand(buildUpCommand(computer, "touch ~/.hudson-run-init")); + Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); - sess.getStdin().close(); // nothing to write here - sess.getStderr().close(); // we are not supposed to get anything - // from stderr - IOUtils.copy(sess.getStdout(), logger); + if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { + logWarning(computer, listener, "init script timed out"); + return; + } - exitStatus = waitCompletion(sess); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } - sess.close(); - } + int exitStatus = waitCompletion(channel); + if (exitStatus != 0) { + logWarning(computer, listener, "init script failed: exit code=" + exitStatus); + return; + } - // TODO: parse the version number. maven-enforcer-plugin might help - final String javaPath = node.javaPath; - try { - Instance nodeInstance = computer.describeInstance(); - if (nodeInstance.getInstanceType().equals("mac2.metal")) { - LOGGER.info("Running Command for mac2.metal"); - executeRemote( - computer, - conn, - javaPath + " -fullversion", - "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-aarch64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-aarch64-macos-jdk.pkg -target /", - logger, - listener); - } else { - executeRemote( - computer, - conn, - javaPath + " -fullversion", - "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-x64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-x64-macos-jdk.pkg -target /", - logger, - listener); + channel.getInvertedErr().close(); // we are not supposed to get anything from stderr + IOUtils.copy(channel.getInvertedOut(), logger); + } } - } catch (InterruptedException ex) { - LOGGER.warning(ex.getMessage()); - } - - // Always copy so we get the most recent remoting.jar - logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); - scp.put(Jenkins.get().getJnlpJars("remoting.jar").readFully(), "remoting.jar", tmpDir); - - final String jvmopts = node.jvmopts; - final String prefix = computer.getSlaveCommandPrefix(); - final String suffix = computer.getSlaveCommandSuffix(); - final String remoteFS = node.getRemoteFS(); - final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; - String launchString = prefix + " " + javaPath + " " + (jvmopts != null ? jvmopts : "") + " -jar " + tmpDir - + "/remoting.jar -workDir " + workDir + suffix; - // launchString = launchString.trim(); - - SlaveTemplate slaveTemplate = computer.getSlaveTemplate(); - - if (slaveTemplate != null && slaveTemplate.isConnectBySSHProcess()) { - File identityKeyFile = createIdentityKeyFile(computer); + // TODO: parse the version number. maven-enforcer-plugin might help + final String javaPath = node.javaPath; try { - // Obviously the controller must have an installed ssh client. - // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag - String sshClientLaunchString = String.format( - "ssh -o StrictHostKeyChecking=%s -i %s %s@%s -p %d %s", - slaveTemplate.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), - identityKeyFile.getAbsolutePath(), - node.remoteAdmin, - getEC2HostAddress(computer, template), - node.getSshPort(), - launchString); - - logInfo( - computer, - listener, - "Launching remoting agent (via SSH client process): " + sshClientLaunchString); - CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); - commandLauncher.launch(computer, listener); - } finally { - if (!identityKeyFile.delete()) { - LOGGER.log(Level.WARNING, "Failed to delete identity key file"); + Instance nodeInstance = computer.describeInstance(); + if (nodeInstance.getInstanceType().equals("mac2.metal")) { + LOGGER.info("Running Command for mac2.metal"); + executeRemote( + computer, + clientSession, + javaPath + " -fullversion", + "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-aarch64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-aarch64-macos-jdk.pkg -target /", + logger, + listener); + } else { + executeRemote( + computer, + clientSession, + javaPath + " -fullversion", + "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-x64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-x64-macos-jdk.pkg -target /", + logger, + listener); } + } catch (InterruptedException ex) { + LOGGER.warning(ex.getMessage()); } - } else { - logInfo(computer, listener, "Launching remoting agent (via Trilead SSH2 Connection): " + launchString); - final Session sess = conn.openSession(); - sess.execCommand(launchString); - computer.setChannel(sess.getStdout(), sess.getStdin(), logger, new Listener() { - @Override - public void onClosed(Channel channel, IOException cause) { - sess.close(); - conn.close(); + + // Always copy so we get the most recent remoting.jar + logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); + scp.upload( + Jenkins.get().getJnlpJars("remoting.jar").readFully(), + tmpDir + "/remoting.jar", + List.of(PosixFilePermission.OWNER_READ), + null); + + final String jvmopts = node.jvmopts; + final String prefix = computer.getSlaveCommandPrefix(); + final String suffix = computer.getSlaveCommandSuffix(); + final String remoteFS = node.getRemoteFS(); + final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; + String launchString = prefix + + " " + + javaPath + + " " + + (jvmopts != null ? jvmopts : "") + + " -jar " + + tmpDir + + "/remoting.jar -workDir " + + workDir + + suffix; + // launchString = launchString.trim(); + + SlaveTemplate slaveTemplate = computer.getSlaveTemplate(); + + if (slaveTemplate != null && slaveTemplate.isConnectBySSHProcess()) { + File identityKeyFile = createIdentityKeyFile(computer); + + try { + // Obviously the controller must have an installed ssh client. + // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag + String sshClientLaunchString = String.format( + "ssh -o StrictHostKeyChecking=%s -i %s %s@%s -p %d %s", + slaveTemplate.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), + identityKeyFile.getAbsolutePath(), + node.remoteAdmin, + getEC2HostAddress(computer, template), + node.getSshPort(), + launchString); + + logInfo( + computer, + listener, + "Launching remoting agent (via SSH client process): " + sshClientLaunchString); + CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); + commandLauncher.launch(computer, listener); + } finally { + if (!identityKeyFile.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete identity key file"); + } } - }); - } + } else { + logInfo(computer, listener, "Launching remoting agent (via SSH2 Connection): " + launchString); + + try (ClientChannel channel = clientSession.createExecChannel( + launchString, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { + computer.setChannel(channel.getInvertedOut(), channel.getInvertedIn(), logger, new Listener() { + @Override + public void onClosed(Channel channel, IOException cause) { + try { + clientSession.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Error when closing the session", e); + } + } + }); + } + } - successful = true; + successful = true; + } } finally { - if (cleanupConn != null && !successful) { - cleanupConn.close(); + if (cleanupClientSession != null && !successful) { + cleanupClientSession.close(); } } } + private boolean executeRemote(ClientSession session, String command, OutputStream logger) { + try { + session.executeRemoteCommand(command, logger, logger, null); + return true; + } catch (IOException e) { + return false; + } + } + private boolean executeRemote( EC2Computer computer, - Connection conn, + ClientSession clientSession, String checkCommand, String command, PrintStream logger, TaskListener listener) throws IOException, InterruptedException { logInfo(computer, listener, "Verifying: " + checkCommand); - if (conn.exec(checkCommand, logger) != 0) { + if (executeRemote(clientSession, checkCommand, logger)) { logInfo(computer, listener, "Installing: " + command); - if (conn.exec(command, logger) != 0) { + if (executeRemote(clientSession, command, logger)) { logWarning(computer, listener, "Failed to install: " + command); return false; } @@ -388,8 +456,8 @@ private File createIdentityKeyFile(EC2Computer computer) throws IOException { private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemplate template) throws IOException, InterruptedException, AmazonClientException { logInfo(computer, listener, "bootstrap()"); - Connection bootstrapConn = null; - try { + ClientSession bootstrapSession = null; + try (SshClient client = SshClient.setUpDefaultClient()) { int tries = bootstrapAuthTries; boolean isAuthenticated = false; logInfo(computer, listener, "Getting keypair..."); @@ -406,12 +474,14 @@ private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemp while (tries-- > 0) { logInfo(computer, listener, "Authenticating as " + computer.getRemoteAdmin()); try { - bootstrapConn = connectToSsh(computer, listener, template); - isAuthenticated = bootstrapConn.authenticateWithPublicKey( - computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), ""); + bootstrapSession = connectToSsh(client, computer, listener, template); + bootstrapSession.addPublicKeyIdentity(PEMParser.decodeKeyPair(key.getKeyMaterial(), "")); + bootstrapSession.auth().await(timeout); + + isAuthenticated = bootstrapSession.isAuthenticated(); } catch (IOException e) { logException(computer, listener, "Exception trying to authenticate", e); - bootstrapConn.close(); + bootstrapSession.close(); } if (isAuthenticated) { break; @@ -424,20 +494,22 @@ private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemp return false; } } finally { - if (bootstrapConn != null) { - bootstrapConn.close(); + if (bootstrapSession != null) { + bootstrapSession.close(); } } return true; } - private Connection connectToSsh(EC2Computer computer, TaskListener listener, SlaveTemplate template) + private ClientSession connectToSsh( + SshClient client, EC2Computer computer, TaskListener listener, SlaveTemplate template) throws AmazonClientException, InterruptedException { final EC2AbstractSlave node = computer.getNode(); final long timeout = node == null ? 0L : node.getLaunchTimeoutInMillis(); final long startTime = System.currentTimeMillis(); while (true) { try { + long waitTime = System.currentTimeMillis() - startTime; if (timeout > 0 && waitTime > timeout) { throw new AmazonClientException("Timed out after " + (waitTime / 1000) @@ -472,29 +544,35 @@ private Connection connectToSsh(EC2Computer computer, TaskListener listener, Sla computer, listener, "Connecting to " + host + " on port " + port + ", with timeout " + slaveConnectTimeout + "."); - Connection conn = new Connection(host, port); + + // Configure Host key verification + client.setServerKeyVerifier(new ServerKeyVerifierImpl(computer, listener)); + client.start(); + + ConnectFuture connectFuture; + ProxyConfiguration proxyConfig = Jenkins.get().proxy; Proxy proxy = proxyConfig == null ? Proxy.NO_PROXY : proxyConfig.createProxy(host); if (!proxy.equals(Proxy.NO_PROXY) && proxy.address() instanceof InetSocketAddress) { InetSocketAddress address = (InetSocketAddress) proxy.address(); - HTTPProxyData proxyData = null; - if (null != proxyConfig.getUserName()) { - proxyData = new HTTPProxyData( - address.getHostName(), - address.getPort(), - proxyConfig.getUserName(), - proxyConfig.getPassword()); - } else { - proxyData = new HTTPProxyData(address.getHostName(), address.getPort()); - } - conn.setProxyData(proxyData); + String username = proxyConfig.getUserName(); + String password = proxyConfig.getPassword(); + + client.setClientProxyConnector(new ProxyCONNECTListener(host, port, username, password)); + + connectFuture = client.connect(computer.getRemoteAdmin(), address); + logInfo(computer, listener, "Using HTTP Proxy Configuration"); + } else { + connectFuture = client.connect(computer.getRemoteAdmin(), host, port); } - conn.connect( - new ServerHostKeyVerifierImpl(computer, listener), slaveConnectTimeout, slaveConnectTimeout); + ClientSession clientSession = connectFuture + .verify(slaveConnectTimeout, TimeUnit.SECONDS) // successfully connected + .getClientSession(); + logInfo(computer, listener, "Connected via SSH."); - return conn; // successfully connected + return clientSession; } catch (IOException e) { // keep retrying until SSH comes up logInfo(computer, listener, "Failed to connect via ssh: " + e.getMessage()); @@ -517,24 +595,32 @@ private Connection connectToSsh(EC2Computer computer, TaskListener listener, Sla /** * Our host key verifier just pick up the right strategy and call its verify method. */ - private static class ServerHostKeyVerifierImpl implements ServerHostKeyVerifier { - + private static class ServerKeyVerifierImpl implements ServerKeyVerifier { private final EC2Computer computer; private final TaskListener listener; - public ServerHostKeyVerifierImpl(final EC2Computer computer, final TaskListener listener) { + public ServerKeyVerifierImpl(final EC2Computer computer, final TaskListener listener) { this.computer = computer; this.listener = listener; } @Override - public boolean verifyServerHostKey( - String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception { + public boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) { SlaveTemplate template = computer.getSlaveTemplate(); - return template != null - && template.getHostKeyVerificationStrategy() - .getStrategy() - .verify(computer, new HostKey(serverHostKeyAlgorithm, serverHostKey), listener); + try { + return template != null + && template.getHostKeyVerificationStrategy() + .getStrategy() + .verify( + computer, + new HostKey(serverKey.getAlgorithm(), serverKey.getEncoded()), + listener); + } catch (Exception exception) { + // false will trigger a SSHException which is a subclass of IOException. + // Therefore, it is not needed to throw a RuntimeException. + EC2Cloud.log(LOGGER, Level.WARNING, listener, "Unable to check the server key", exception); + return false; + } } } @@ -544,19 +630,6 @@ private static String getEC2HostAddress(EC2Computer computer, SlaveTemplate temp return EC2HostAddressProvider.unix(instance, strategy); } - private int waitCompletion(Session session) throws InterruptedException { - // I noticed that the exit status delivery often gets delayed. Wait up - // to 1 sec. - for (int i = 0; i < 10; i++) { - Integer r = session.getExitStatus(); - if (r != null) { - return r; - } - Thread.sleep(100); - } - return -1; - } - @Override public Descriptor getDescriptor() { throw new UnsupportedOperationException(); diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index f6194df8e..90d730cfd 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -23,14 +23,11 @@ */ package hudson.plugins.ec2.ssh; +import static org.apache.sshd.client.session.ClientSession.REMOTE_COMMAND_WAIT_EVENTS; + import com.amazonaws.AmazonClientException; import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.ec2.model.KeyPair; -import com.trilead.ssh2.Connection; -import com.trilead.ssh2.HTTPProxyData; -import com.trilead.ssh2.SCPClient; -import com.trilead.ssh2.ServerHostKeyVerifier; -import com.trilead.ssh2.Session; import hudson.FilePath; import hudson.ProxyConfiguration; import hudson.Util; @@ -46,9 +43,11 @@ import hudson.plugins.ec2.EC2Readiness; import hudson.plugins.ec2.EC2SpotSlave; import hudson.plugins.ec2.SlaveTemplate; +import hudson.plugins.ec2.ssh.proxy.ProxyCONNECTListener; import hudson.plugins.ec2.ssh.verifiers.HostKey; import hudson.plugins.ec2.ssh.verifiers.HostKeyHelper; import hudson.plugins.ec2.ssh.verifiers.Messages; +import hudson.plugins.ec2.util.PEMParser; import hudson.remoting.Channel; import hudson.remoting.Channel.Listener; import hudson.slaves.CommandLauncher; @@ -56,18 +55,34 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintStream; import java.net.InetSocketAddress; import java.net.Proxy; +import java.net.SocketAddress; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermission; +import java.security.PublicKey; +import java.time.Duration; import java.util.Base64; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.Jenkins; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.channel.ClientChannel; +import org.apache.sshd.client.channel.ClientChannelEvent; +import org.apache.sshd.client.future.ConnectFuture; +import org.apache.sshd.client.keyverifier.ServerKeyVerifier; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.scp.client.CloseableScpClient; /** * {@link ComputerLauncher} that connects to a Unix agent on EC2 by using SSH. @@ -89,6 +104,8 @@ public class EC2UnixLauncher extends EC2ComputerLauncher { private static int readinessSleepMs = 1000; private static int readinessTries = 120; + private static final long timeout = Duration.ofSeconds(10).toMillis(); + static { String prop = System.getProperty(BOOTSTRAP_AUTH_SLEEP_MS); if (prop != null) { @@ -135,8 +152,8 @@ protected String buildUpCommand(EC2Computer computer, String command) { @Override protected void launchScript(EC2Computer computer, TaskListener listener) throws IOException, AmazonClientException, InterruptedException { - final Connection conn; - Connection cleanupConn = null; // java's code path analysis for final + final ClientSession clientSession; + ClientSession cleanupClientSession = null; // java's code path analysis for final // doesn't work that well. boolean successful = false; PrintStream logger = listener.getLogger(); @@ -176,7 +193,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) logInfo(computer, listener, "Launching instance: " + node.getInstanceId()); - try { + try (SshClient client = SshClient.setUpDefaultClient()) { boolean isBootstrapped = bootstrap(computer, listener, template); if (isBootstrapped) { int bootDelay = node.getBootDelay(); @@ -191,11 +208,18 @@ protected void launchScript(EC2Computer computer, TaskListener listener) // connect fresh as ROOT logInfo(computer, listener, "connect fresh as root"); - cleanupConn = connectToSsh(computer, listener, template); + cleanupClientSession = connectToSsh(client, computer, listener, template); KeyPair key = computer.getCloud().getKeyPair(); - if (key == null - || !cleanupConn.authenticateWithPublicKey( - computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), "")) { + + final boolean isAuthenticated; + if (key == null) { + isAuthenticated = false; + } else { + cleanupClientSession.addPublicKeyIdentity(PEMParser.decodeKeyPair(key.getKeyMaterial(), "")); + cleanupClientSession.auth().await(timeout); + isAuthenticated = cleanupClientSession.isAuthenticated(); + } + if (!isAuthenticated) { logWarning(computer, listener, "Authentication failed"); return; // failed to connect as root. } @@ -203,153 +227,189 @@ protected void launchScript(EC2Computer computer, TaskListener listener) logWarning(computer, listener, "bootstrapresult failed"); return; // bootstrap closed for us. } - conn = cleanupConn; - - SCPClient scp = conn.createSCPClient(); - String initScript = node.initScript; - String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); - - logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); - conn.exec("mkdir -p " + tmpDir, logger); - - if (initScript != null - && !initScript.trim().isEmpty() - && conn.exec("test -e ~/.hudson-run-init", logger) != 0) { - logInfo(computer, listener, "Executing init script"); - scp.put(initScript.getBytes(StandardCharsets.UTF_8), "init.sh", tmpDir, "0700"); - Session sess = conn.openSession(); - sess.requestDumbPTY(); // so that the remote side bundles stdout - // and stderr - sess.execCommand(buildUpCommand(computer, tmpDir + "/init.sh")); - - sess.getStdin().close(); // nothing to write here - sess.getStderr().close(); // we are not supposed to get anything - // from stderr - IOUtils.copy(sess.getStdout(), logger); - - int exitStatus = waitCompletion(sess); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } - sess.close(); + clientSession = cleanupClientSession; + + try (CloseableScpClient scp = createScpClient(clientSession)) { + String initScript = node.initScript; + String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); + + logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); + executeRemote(clientSession, "mkdir -p " + tmpDir, logger); + + if (initScript != null + && !initScript.trim().isEmpty() + && executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { + logInfo(computer, listener, "Executing init script"); + scp.upload( + initScript.getBytes(StandardCharsets.UTF_8), + tmpDir + "/init.sh", + List.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_READ), + null); + + String initCommand = buildUpCommand(computer, tmpDir + "/init.sh"); + try (ClientChannel channel = clientSession.createExecChannel( + initCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { + + channel.getInvertedIn().close(); // nothing to write here + channel.open().await(timeout); + + Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); + + if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { + logWarning(computer, listener, "init script timed out"); + return; + } + + int exitStatus = waitCompletion(channel); + if (exitStatus != 0) { + logWarning(computer, listener, "init script failed: exit code=" + exitStatus); + return; + } + + channel.getInvertedErr().close(); // we are not supposed to get anything from stderr + IOUtils.copy(channel.getInvertedOut(), logger); + } - logInfo(computer, listener, "Creating ~/.hudson-run-init"); + logInfo(computer, listener, "Creating ~/.hudson-run-init"); - // Needs a tty to run sudo. - sess = conn.openSession(); - sess.requestDumbPTY(); // so that the remote side bundles stdout - // and stderr - sess.execCommand(buildUpCommand(computer, "touch ~/.hudson-run-init")); + String createHudsonRunInitCommand = buildUpCommand(computer, "touch ~/.hudson-run-init"); + try (ClientChannel channel = clientSession.createExecChannel( + createHudsonRunInitCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { + channel.getInvertedIn().close(); // nothing to write here + channel.open().await(timeout); - sess.getStdin().close(); // nothing to write here - sess.getStderr().close(); // we are not supposed to get anything - // from stderr - IOUtils.copy(sess.getStdout(), logger); + Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); - exitStatus = waitCompletion(sess); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } - sess.close(); - } + if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { + logWarning(computer, listener, "init script timed out"); + return; + } - // TODO: parse the version number. maven-enforcer-plugin might help - final String javaPath = node.javaPath; - executeRemote( - computer, - conn, - javaPath + " -fullversion", - "sudo amazon-linux-extras install java-openjdk11 -y; sudo yum install -y fontconfig java-11-openjdk", - logger, - listener); - executeRemote(computer, conn, "which scp", "sudo yum install -y openssh-clients", logger, listener); - - // Always copy so we get the most recent remoting.jar - logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); - scp.put(Jenkins.get().getJnlpJars("remoting.jar").readFully(), "remoting.jar", tmpDir); - - final String jvmopts = node.jvmopts; - final String prefix = computer.getSlaveCommandPrefix(); - final String suffix = computer.getSlaveCommandSuffix(); - final String remoteFS = node.getRemoteFS(); - final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; - String launchString = prefix + " " + javaPath + " " + (jvmopts != null ? jvmopts : "") + " -jar " + tmpDir - + "/remoting.jar -workDir " + workDir + suffix; - // launchString = launchString.trim(); - - if (template.isConnectBySSHProcess()) { - File identityKeyFile = createIdentityKeyFile(computer); - String ec2HostAddress = getEC2HostAddress(computer, template); - File hostKeyFile = createHostKeyFile(computer, ec2HostAddress, listener); - String userKnownHostsFileFlag = ""; - if (hostKeyFile != null) { - userKnownHostsFileFlag = - String.format(" -o \"UserKnownHostsFile=%s\"", hostKeyFile.getAbsolutePath()); + int exitStatus = waitCompletion(channel); + if (exitStatus != 0) { + logWarning(computer, listener, "init script failed: exit code=" + exitStatus); + return; + } + + channel.getInvertedErr().close(); // we are not supposed to get anything from stderr + IOUtils.copy(channel.getInvertedOut(), logger); + } } - try { - // Obviously the controller must have an installed ssh client. - // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag - String sshClientLaunchString = String.format( - "ssh -o StrictHostKeyChecking=%s%s%s -i %s %s@%s -p %d %s", - template.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), - userKnownHostsFileFlag, - getEC2HostKeyAlgorithmFlag(computer), - identityKeyFile.getAbsolutePath(), - node.remoteAdmin, - ec2HostAddress, - node.getSshPort(), - launchString); + // TODO: parse the version number. maven-enforcer-plugin might help + final String javaPath = node.javaPath; + executeRemote( + computer, + clientSession, + javaPath + " -fullversion", + "sudo amazon-linux-extras install java-openjdk11 -y; sudo yum install -y fontconfig java-11-openjdk", + logger, + listener); + executeRemote( + computer, clientSession, "which scp", "sudo yum install -y openssh-clients", logger, listener); + + // Always copy so we get the most recent remoting.jar + logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); + scp.upload( + Jenkins.get().getJnlpJars("remoting.jar").readFully(), + tmpDir + "/remoting.jar", + List.of(PosixFilePermission.OWNER_READ), + null); + + final String jvmopts = node.jvmopts; + final String prefix = computer.getSlaveCommandPrefix(); + final String suffix = computer.getSlaveCommandSuffix(); + final String remoteFS = node.getRemoteFS(); + final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; + String launchString = prefix + + " " + + javaPath + + " " + + (jvmopts != null ? jvmopts : "") + + " -jar " + + tmpDir + + "/remoting.jar -workDir " + + workDir + + suffix; + // launchString = launchString.trim(); + + if (template.isConnectBySSHProcess()) { + File identityKeyFile = createIdentityKeyFile(computer); + String ec2HostAddress = getEC2HostAddress(computer, template); + File hostKeyFile = createHostKeyFile(computer, ec2HostAddress, listener); + String userKnownHostsFileFlag = ""; + if (hostKeyFile != null) { + userKnownHostsFileFlag = + String.format(" -o \"UserKnownHostsFile=%s\"", hostKeyFile.getAbsolutePath()); + } - logInfo( - computer, - listener, - "Launching remoting agent (via SSH client process): " + sshClientLaunchString); - CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); - commandLauncher.launch(computer, listener); - } finally { - if (!identityKeyFile.delete()) { - LOGGER.log(Level.WARNING, "Failed to delete identity key file"); + try { + // Obviously the controller must have an installed ssh client. + // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag + String sshClientLaunchString = String.format( + "ssh -o StrictHostKeyChecking=%s%s%s -i %s %s@%s -p %d %s", + template.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), + userKnownHostsFileFlag, + getEC2HostKeyAlgorithmFlag(computer), + identityKeyFile.getAbsolutePath(), + node.remoteAdmin, + ec2HostAddress, + node.getSshPort(), + launchString); + + logInfo( + computer, + listener, + "Launching remoting agent (via SSH client process): " + sshClientLaunchString); + CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); + commandLauncher.launch(computer, listener); + } finally { + if (!identityKeyFile.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete identity key file"); + } + if (hostKeyFile != null && !hostKeyFile.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete host key file"); + } } - if (hostKeyFile != null && !hostKeyFile.delete()) { - LOGGER.log(Level.WARNING, "Failed to delete host key file"); + } else { + logInfo(computer, listener, "Launching remoting agent (via SSH2 Connection): " + launchString); + + try (ClientChannel channel = clientSession.createExecChannel( + launchString, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { + computer.setChannel(channel.getInvertedOut(), channel.getInvertedIn(), logger, new Listener() { + @Override + public void onClosed(Channel channel, IOException cause) { + try { + clientSession.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Error when closing the session", e); + } + } + }); } } - } else { - logInfo(computer, listener, "Launching remoting agent (via Trilead SSH2 Connection): " + launchString); - final Session sess = conn.openSession(); - sess.execCommand(launchString); - computer.setChannel(sess.getStdout(), sess.getStdin(), logger, new Listener() { - @Override - public void onClosed(Channel channel, IOException cause) { - sess.close(); - conn.close(); - } - }); - } - successful = true; + successful = true; + } } finally { - if (cleanupConn != null && (!successful || template.isConnectBySSHProcess())) { - cleanupConn.close(); + if (cleanupClientSession != null && (!successful || template.isConnectBySSHProcess())) { + cleanupClientSession.close(); } } } private boolean executeRemote( EC2Computer computer, - Connection conn, + ClientSession clientSession, String checkCommand, String command, PrintStream logger, TaskListener listener) throws IOException, InterruptedException { logInfo(computer, listener, "Verifying: " + checkCommand); - if (conn.exec(checkCommand, logger) != 0) { + if (executeRemote(clientSession, checkCommand, logger)) { logInfo(computer, listener, "Installing: " + command); - if (conn.exec(command, logger) != 0) { + if (executeRemote(clientSession, command, logger)) { logWarning(computer, listener, "Failed to install: " + command); return false; } @@ -357,6 +417,15 @@ private boolean executeRemote( return true; } + private boolean executeRemote(ClientSession session, String command, OutputStream logger) { + try { + session.executeRemoteCommand(command, logger, logger, null); + return true; + } catch (IOException e) { + return false; + } + } + private File createIdentityKeyFile(EC2Computer computer) throws IOException { EC2PrivateKey ec2PrivateKey = computer.getCloud().resolvePrivateKey(); String privateKey = ""; @@ -413,8 +482,8 @@ private File createHostKeyFile(EC2Computer computer, String ec2HostAddress, Task private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemplate template) throws IOException, InterruptedException, AmazonClientException { logInfo(computer, listener, "bootstrap()"); - Connection bootstrapConn = null; - try { + ClientSession bootstrapSession = null; + try (SshClient client = SshClient.setUpDefaultClient()) { int tries = bootstrapAuthTries; boolean isAuthenticated = false; logInfo(computer, listener, "Getting keypair..."); @@ -431,12 +500,14 @@ private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemp while (tries-- > 0) { logInfo(computer, listener, "Authenticating as " + computer.getRemoteAdmin()); try { - bootstrapConn = connectToSsh(computer, listener, template); - isAuthenticated = bootstrapConn.authenticateWithPublicKey( - computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), ""); + bootstrapSession = connectToSsh(client, computer, listener, template); + bootstrapSession.addPublicKeyIdentity(PEMParser.decodeKeyPair(key.getKeyMaterial(), "")); + bootstrapSession.auth().await(timeout); + + isAuthenticated = bootstrapSession.isAuthenticated(); } catch (IOException e) { logException(computer, listener, "Exception trying to authenticate", e); - bootstrapConn.close(); + bootstrapSession.close(); } if (isAuthenticated) { break; @@ -449,14 +520,15 @@ private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemp return false; } } finally { - if (bootstrapConn != null) { - bootstrapConn.close(); + if (bootstrapSession != null) { + bootstrapSession.close(); } } return true; } - private Connection connectToSsh(EC2Computer computer, TaskListener listener, SlaveTemplate template) + private ClientSession connectToSsh( + SshClient client, EC2Computer computer, TaskListener listener, SlaveTemplate template) throws AmazonClientException, InterruptedException { final EC2AbstractSlave node = computer.getNode(); final long timeout = node == null ? 0L : node.getLaunchTimeoutInMillis(); @@ -497,29 +569,35 @@ private Connection connectToSsh(EC2Computer computer, TaskListener listener, Sla computer, listener, "Connecting to " + host + " on port " + port + ", with timeout " + slaveConnectTimeout + "."); - Connection conn = new Connection(host, port); + + // Configure Host key verification + client.setServerKeyVerifier(new ServerKeyVerifierImpl(computer, listener)); + client.start(); + + ConnectFuture connectFuture; + ProxyConfiguration proxyConfig = Jenkins.get().proxy; Proxy proxy = proxyConfig == null ? Proxy.NO_PROXY : proxyConfig.createProxy(host); if (!proxy.equals(Proxy.NO_PROXY) && proxy.address() instanceof InetSocketAddress) { InetSocketAddress address = (InetSocketAddress) proxy.address(); - HTTPProxyData proxyData = null; - if (null != proxyConfig.getUserName()) { - proxyData = new HTTPProxyData( - address.getHostName(), - address.getPort(), - proxyConfig.getUserName(), - proxyConfig.getPassword()); - } else { - proxyData = new HTTPProxyData(address.getHostName(), address.getPort()); - } - conn.setProxyData(proxyData); + String username = proxyConfig.getUserName(); + String password = proxyConfig.getPassword(); + + client.setClientProxyConnector(new ProxyCONNECTListener(host, port, username, password)); + + connectFuture = client.connect(computer.getRemoteAdmin(), address); + logInfo(computer, listener, "Using HTTP Proxy Configuration"); + } else { + connectFuture = client.connect(computer.getRemoteAdmin(), host, port); } - conn.connect( - new ServerHostKeyVerifierImpl(computer, listener), slaveConnectTimeout, slaveConnectTimeout); + ClientSession clientSession = connectFuture + .verify(slaveConnectTimeout, TimeUnit.SECONDS) // successfully connected + .getClientSession(); + logInfo(computer, listener, "Connected via SSH."); - return conn; // successfully connected + return clientSession; } catch (IOException e) { // keep retrying until SSH comes up logInfo(computer, listener, "Failed to connect via ssh: " + e.getMessage()); @@ -542,24 +620,32 @@ private Connection connectToSsh(EC2Computer computer, TaskListener listener, Sla /** * Our host key verifier just pick up the right strategy and call its verify method. */ - private static class ServerHostKeyVerifierImpl implements ServerHostKeyVerifier { - + private static class ServerKeyVerifierImpl implements ServerKeyVerifier { private final EC2Computer computer; private final TaskListener listener; - public ServerHostKeyVerifierImpl(final EC2Computer computer, final TaskListener listener) { + public ServerKeyVerifierImpl(final EC2Computer computer, final TaskListener listener) { this.computer = computer; this.listener = listener; } @Override - public boolean verifyServerHostKey( - String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception { + public boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) { SlaveTemplate template = computer.getSlaveTemplate(); - return template != null - && template.getHostKeyVerificationStrategy() - .getStrategy() - .verify(computer, new HostKey(serverHostKeyAlgorithm, serverHostKey), listener); + try { + return template != null + && template.getHostKeyVerificationStrategy() + .getStrategy() + .verify( + computer, + new HostKey(serverKey.getAlgorithm(), serverKey.getEncoded()), + listener); + } catch (Exception exception) { + // false will trigger a SSHException which is a subclass of IOException. + // Therefore, it is not needed to throw a RuntimeException. + EC2Cloud.log(LOGGER, Level.WARNING, listener, "Unable to check the server key", exception); + return false; + } } } @@ -577,19 +663,6 @@ private static String getEC2HostKeyAlgorithmFlag(EC2Computer computer) throws IO return ""; } - private int waitCompletion(Session session) throws InterruptedException { - // I noticed that the exit status delivery often gets delayed. Wait up - // to 1 sec. - for (int i = 0; i < 10; i++) { - Integer r = session.getExitStatus(); - if (r != null) { - return r; - } - Thread.sleep(100); - } - return -1; - } - @Override public Descriptor getDescriptor() { throw new UnsupportedOperationException(); diff --git a/src/main/java/hudson/plugins/ec2/ssh/HostKeyVerifierImpl.java b/src/main/java/hudson/plugins/ec2/ssh/HostKeyVerifierImpl.java index ac44fceb1..818bb9a4c 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/HostKeyVerifierImpl.java +++ b/src/main/java/hudson/plugins/ec2/ssh/HostKeyVerifierImpl.java @@ -23,11 +23,10 @@ */ package hudson.plugins.ec2.ssh; -import com.trilead.ssh2.ServerHostKeyVerifier; import java.security.MessageDigest; import java.util.logging.Logger; -public class HostKeyVerifierImpl implements ServerHostKeyVerifier { +public class HostKeyVerifierImpl { private static final Logger LOGGER = Logger.getLogger(HostKeyVerifierImpl.class.getName()); private final String console; @@ -51,7 +50,6 @@ private String getFingerprint(byte[] serverHostKey) throws Exception { return buf.toString(); } - @Override public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception { String fingerprint = getFingerprint(serverHostKey); diff --git a/src/main/java/hudson/plugins/ec2/ssh/proxy/ProxyCONNECTListener.java b/src/main/java/hudson/plugins/ec2/ssh/proxy/ProxyCONNECTListener.java new file mode 100644 index 000000000..5dd832a1f --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/ssh/proxy/ProxyCONNECTListener.java @@ -0,0 +1,73 @@ +package hudson.plugins.ec2.ssh.proxy; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Base64; +import org.apache.sshd.client.session.ClientProxyConnector; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.io.IoSession; +import org.apache.sshd.common.util.buffer.ByteArrayBuffer; + +/** + * {@link ClientProxyConnector} that issue an HTTP CONNECT to connect through an HTTP proxy. + */ +public class ProxyCONNECTListener implements ClientProxyConnector { + + private static final long timeout = Duration.ofSeconds(10).toMillis(); + + public final String targetHost; + public final int targetPort; + public final String proxyUser; + public final String proxyPass; + + public ProxyCONNECTListener(String targetHost, int targetPort, String proxyUser, String proxyPass) { + this.targetHost = targetHost; + this.targetPort = targetPort; + this.proxyUser = proxyUser; + this.proxyPass = proxyPass; + } + + @Override + public void sendClientProxyMetadata(ClientSession session) throws Exception { + proxyCONNECT(session.getIoSession()); + } + + public void proxyCONNECT(IoSession ioSession) { + StringBuilder connectRequest = new StringBuilder(); + + // Based on https://www.rfc-editor.org/rfc/rfc7231#section-4.3.6 + connectRequest + .append("CONNECT ") + .append(targetHost) + .append(':') + .append(targetPort) + .append(" HTTP/1.0\r\n"); + // Host should be included https://datatracker.ietf.org/doc/html/rfc2616#section-14.23 + connectRequest + .append("Host: ") + .append(targetHost) + .append(':') + .append(targetPort) + .append("\r\n"); + + if ((proxyUser != null) && (proxyPass != null)) { + String credentials = proxyUser + ":" + proxyPass; + String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.ISO_8859_1)); + connectRequest.append("Proxy-Authorization: Basic "); + connectRequest.append(encoded); + connectRequest.append("\r\n"); + } + + // End of the header + connectRequest.append("\r\n"); + + try { + ioSession + .writeBuffer(new ByteArrayBuffer(connectRequest.toString().getBytes(StandardCharsets.US_ASCII))) + .await(timeout); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java index d88e04cb4..1d543c64d 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java @@ -26,10 +26,12 @@ */ package hudson.plugins.ec2.ssh.verifiers; -import com.trilead.ssh2.KnownHosts; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.Serializable; import java.util.Arrays; +import org.apache.sshd.common.digest.BuiltinDigests; +import org.apache.sshd.common.digest.DigestUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; /** * A representation of the SSH key provided by a remote host to verify itself @@ -69,7 +71,12 @@ public byte[] getKey() { } public String getFingerprint() { - return KnownHosts.createHexFingerprint(getAlgorithm(), getKey()); + try { + byte[] rawFingerprint = DigestUtils.getRawFingerprint(BuiltinDigests.md5.get(), getKey()); + return BufferUtils.toHex(':', rawFingerprint).toLowerCase(); + } catch (Exception e) { + return ""; + } } @Override diff --git a/src/main/java/hudson/plugins/ec2/util/PEMParser.java b/src/main/java/hudson/plugins/ec2/util/PEMParser.java new file mode 100644 index 000000000..8e8db67cb --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/util/PEMParser.java @@ -0,0 +1,74 @@ +package hudson.plugins.ec2.util; + +import java.io.IOException; +import java.io.StringReader; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.RSAPrivateCrtKeySpec; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.openssl.PEMEncryptedKeyPair; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; + +/** + * Utility class to parse PEM. + */ +public abstract class PEMParser { + private PEMParser() {} + + public static KeyPair decodeKeyPair(String pem, String password) throws IOException { + try (org.bouncycastle.openssl.PEMParser pemParser = + new org.bouncycastle.openssl.PEMParser(new StringReader(pem))) { + Object object = pemParser.readObject(); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); + + if (object instanceof PEMEncryptedKeyPair) { + PEMKeyPair decryptedKeyPair = ((PEMEncryptedKeyPair) object) + .decryptKeyPair(new JcePEMDecryptorProviderBuilder().build(password.toCharArray())); + PrivateKey privateKey = converter.getPrivateKey(decryptedKeyPair.getPrivateKeyInfo()); + PublicKey publicKey = converter.getPublicKey(decryptedKeyPair.getPublicKeyInfo()); + return new KeyPair(publicKey, privateKey); + } else if (object instanceof PrivateKeyInfo) { + PrivateKey privateKey = converter.getPrivateKey((PrivateKeyInfo) object); + PublicKey publicKey = generatePublicKeyFromPrivateKey(privateKey); + return new KeyPair(publicKey, privateKey); + } else if (object instanceof SubjectPublicKeyInfo) { + PublicKey publicKey = converter.getPublicKey((SubjectPublicKeyInfo) object); + return new KeyPair(publicKey, null); + } else if (object instanceof PEMKeyPair) { + SubjectPublicKeyInfo publicKeyInfo = ((PEMKeyPair) object).getPublicKeyInfo(); + PrivateKeyInfo privateKeyInfo = ((PEMKeyPair) object).getPrivateKeyInfo(); + return new KeyPair(converter.getPublicKey(publicKeyInfo), converter.getPrivateKey(privateKeyInfo)); + } else { + throw new IllegalArgumentException( + "Unsupported PEM object type: " + object.getClass().getName()); + } + } catch (Exception e) { + throw new IOException("Failed to parse PEM input", e); + } + } + + private static PublicKey generatePublicKeyFromPrivateKey(PrivateKey privateKey) { + try { + KeyFactory keyFactory = KeyFactory.getInstance(privateKey.getAlgorithm()); + + if ("RSA".equalsIgnoreCase(privateKey.getAlgorithm())) { + RSAPrivateCrtKeySpec rsaPrivateCrtKeySpec = + keyFactory.getKeySpec(privateKey, RSAPrivateCrtKeySpec.class); + return keyFactory.generatePublic(rsaPrivateCrtKeySpec); + } else if ("EC".equalsIgnoreCase(privateKey.getAlgorithm())) { + ECPrivateKeySpec ecPrivateKeySpec = keyFactory.getKeySpec(privateKey, ECPrivateKeySpec.class); + return keyFactory.generatePublic(ecPrivateKeySpec); + } else { + return null; + } + } catch (Exception e) { + return null; + } + } +} diff --git a/src/test/java/hudson/plugins/ec2/HostKeyTest.java b/src/test/java/hudson/plugins/ec2/HostKeyTest.java new file mode 100644 index 000000000..76d5e18a4 --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/HostKeyTest.java @@ -0,0 +1,122 @@ +package hudson.plugins.ec2; + +import static org.junit.Assert.assertEquals; + +import hudson.plugins.ec2.ssh.verifiers.HostKey; +import java.security.Security; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class HostKeyTest { + + public static String PUBLIC_KEY_SSH_DSS_1024 = + "AAAAB3NzaC1kc3MAAACBAMsQrriFgun2KVgmsGd8drsplZLXyU8uU6r90aIZ+evRpxvoLCJf317Wnu5qVBCzGgEZ8iygYB0bDB/JFch+UVgtyXGH358ClJCDDgNWdOSogTl2gCF+W+8KoRSF+i3ObnEPOTa2akByP5FDzOO+mruVPl8kg8NHYcadCtJizRjhAAAAFQCV9uGT1Mchfbm6uFxEmZf09DwjSQAAAIAyyLw64QIHel17rzdyMyvepkvW4q64WYb7xCVLffaYJA8x1pxHtH4Mmmm0fGG7GFgdnCeD95524CYZR7TDhzKFGcEX607qKg0v5sXs6z8U8lGOeARq/IXQphb7YPZ9PdKUIuJImQEXriI0p5G7aGMmSYjnyEpKhUsM12xpDb2qBAAAAIBbIZPuZzBbbeesmzmGoG63w0tFc+tpPV3lNkAeYYcWpVhpSdHGFatr1lU+8LNT6OXekV2CFyF5kuuYw/B3OFkmHasURnT1+yC49OEpSzA3KOtQzqO2BZqIxDG/IEajKtSPGOWWPaVrHdgDXo3EZ6yCJtCiOMxW5Xz3fiUufp1sdQ=="; + + public static String PUBLIC_KEY_SSH_RSA_1024 = + "AAAAB3NzaC1yc2EAAAADAQABAAAAgQC8nIRjuQr9gGfGTdq7BL4l3s4n4qEe3w7imhv1cT5cy2HT7DXvnA2gGVmFn4izbWlFlQG1lrtMgIiiwXH/shRx+2FnqayNsOmRJ37TiA0ICjkOrdR4JaYWRafQ0TEC0+EdHl+3iJYOhw9scFpJ2M9kB6W5HJsf4gmXoGGz8SsfsQ=="; + + public static String PUBLIC_KEY_SSH_RSA_2048 = + "AAAAB3NzaC1yc2EAAAADAQABAAABAQCjw8Wgl1usvj9LCzF1c8PufEIG11V2PHCDNlYc66ccIiojQX79st1Lbp0BJXsa2bvZLYjfqyYP5gqkX7jLslmXPN+Vew91sRTmXJTlANlm/fChHg+Fq+lQK0IKGBIn9RlPDFH+NNoUIw4LbZ4etRJuOfMwiVKsOVOYuuLjiIJTkda9eS9zrhTRUXhUuMIxBLdeJEAYve6oBpcnTKpUbTV+DYlru3Yh6lSIevhA361s65oJauNHFQLQ7Ysi9apiF5hmqt1sThv/NPM/xLwlPrSGqKZWnclJbBKaWFlCijuM7W3Q5zbcdmtvKhxEJMobu+KMbt/LVhV7kD3BBLhADKnZ"; + + public static String PUBLIC_KEY_SSH_RSA_3072 = + "AAAAB3NzaC1yc2EAAAADAQABAAABgQDzvqmjwxE3UgKOVZDoji9npAu7Uee47sdS60cTN0yx3Aj+c5IznoBLDYt7HwUcjKoj6soRJALFMGvrKe6n4H1+9jF5vrstMB40Ga8858wweehIAEzw/ONAORZdHA2y0WG8K3+bNOVSeXZwASsjbKrYcdotsZarhQtGVks6xQwd7qXUD44DDdFuWsuj5//hSSYSIgjJE3gAfeI2qVoe6Cl6gTGoK9zd+hNrYDehpN7bDgX45ulcaMw7N2kLf+Sg5QqOYL3Xdav/SeNEefNUyE058uRK8Br3WhZh5BJ2qFjzUYe20cFKHJ3gqKiY+8aor6YrDAS5AOEAEdCw1GWHJutGeApouTSqpNZf1uHspKEgLCUu6gb+i14k3YGSqUW/3fRdqmtN5qBYGvOoqgUEG1wlsxjf6lvJxSh6551MEiM3dpXBq3wniFjK56pj9nVjW0erJsOXPIqh9KeQL1dB+fzNX0r3oAog3EEl+x+V/YLE12b8MR9qnaZwyxWPGKoRE70="; + + public static String PUBLIC_KEY_SSH_RSA_4096 = + "AAAAB3NzaC1yc2EAAAADAQABAAACAQCvoE/zGFhSYP/aXLGYl27P+Bq5KJ0E53Er163GJRZ119kgPTB17JOKEG1k25tmspoNYVVaSIM81zBi4RUIrP7ft+1wj2FlsMchrEHlrqR31HCCsPmf/YGgzaBBgL2KDNHEsnxIzyZTsY4ZGPS4LZMP8McUXfwvOFkVs12AUNH5hrB0SgMv6sor1VyW43p6u1o1w9MX5omANopOv+Rqm7In0UXNmOocOhOFYqDJVKt05+fI+fduHIwO4Wi3e0K1jK1EmC9YlIJJIz0Ce1+CyGK0Cm7lHj+W2Ea5tERO0DsK/etGbn1w8NcW9XmPVzO4vSvsMm7XrL0hIdNQZKSxas4NNwxr0TZN70T+H3WKRK9VAxCEp5IdahsSevKyrcsRnKX3mcemqJZZ+ODAarPdHemNacywzoaEt2AOSOl1PcW/sA49R4yMYHj8RS6xDv9jeA4Vogj58ynqzaB2F4fCkaV4bmgb2vL0Fkw96Tvq0+Gs902zvtDmnneicCWhNnj+3jRKZjqiQRvA3/BgYrokFGcDra4j9C1vrDVajMitcY+dr0XeA+n9ot29GSx36Fwg3j3QUhamS6/nsKTeIdmEHeym7FT6LKweKL/XcUCs+tkaJFxsJ+S1E+vF2M7SmqkNuB0S17EijZtw01v1zbzocscnfpLXo3UEfBdIe7pjT/IGtw=="; + + public static String PUBLIC_KEY_ECDSA_SHA2_NISTP256_256 = + "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB1ZWbuchcMR2NMc4wu4sB2sFvnxZ45tAyijmSx9pUkQ51InNU7t1qzf2p29VhwdJ7kQSX3HdUcwBP1NfUSEoFw="; + + public static String PUBLIC_KEY_ECDSA_SHA2_NISTP384_384 = + "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEfdfV0luXJ0NJGMpeAMDZvfDjfwpC9U+YDSlgM0Gh2eCCtYCGv41G4tZd5+L1gjPEiS4Y8r+jb3JoAX6JdQfHecK6+NHpZsF0uwrn8zTfA9PT+I9nTtEyBgNWM/v/A5wQ=="; + + public static String PUBLIC_KEY_ECDSA_SHA2_NISTP521_521 = + "AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFrY2jC8sarrQqI13e9fDhzeUvTFt5j2krHfFfqDrP/M7L5RJzbg4jOSOly7FdOi7JhFkYaEguddhRh2DIUWKHR9ADR9/m4n9WxHR9QaVLUYUyZdQzgdtlY6KfLYJyO5PBSulMhpfDKGoycNKmr6Av1gyESAIBq+bINsgpUby+h9jkC7Q=="; + + private final String description; + + private final String algorithm; + + private final String publicKey; + + private final String expected; + + public HostKeyTest(String description, String algorithm, String publicKey, String expected) { + this.description = description; + this.algorithm = algorithm; + this.publicKey = publicKey; + this.expected = expected; + } + + @Before + public void before() { + // Add provider manually to avoid requiring jenkinsrule + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + } + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][] { + { + "SSH-DSS with key size 1024", + "ssh-dss", + PUBLIC_KEY_SSH_DSS_1024, + "48:0b:8d:f6:6b:8d:99:11:e5:6a:02:9b:fb:f0:20:4e" + }, + { + "SSH-RSA with key size 1024", + "ssh-rsa", + PUBLIC_KEY_SSH_RSA_1024, + "17:1d:65:4e:bd:6e:3b:e2:51:46:35:84:db:ff:c2:53" + }, + { + "SSH-RSA with key size 2048", + "ssh-rsa", + PUBLIC_KEY_SSH_RSA_2048, + "6a:8c:88:49:9f:fe:47:3e:27:a5:c2:d4:45:6b:28:45" + }, + { + "SSH-RSA with key size 3072", + "ssh-rsa", + PUBLIC_KEY_SSH_RSA_3072, + "29:7b:fe:5b:e3:bb:7a:28:9d:41:2a:f3:bf:95:96:2a" + }, + { + "SSH-RSA with key size 4096", + "ssh-rsa", + PUBLIC_KEY_SSH_RSA_4096, + "1d:21:8f:0e:97:38:f8:3b:a7:a6:d6:72:f0:c2:ca:20" + }, + { + "ECDSA-SHA2-NISTP256 with key size 256", + "ecdsa-sha2-nistp256", + PUBLIC_KEY_ECDSA_SHA2_NISTP256_256, + "a4:59:c0:2b:66:42:24:df:36:ca:d8:55:ae:b9:65:5b" + }, + { + "ECDSA-SHA2-NISTP384 with key size 384", + "ecdsa-sha2-nistp384", + PUBLIC_KEY_ECDSA_SHA2_NISTP384_384, + "ec:79:dd:bd:30:26:df:ce:84:5e:83:c1:8b:28:b8:ff" + }, + { + "ECDSA-SHA2-NISTP521 with key size 521", + "ecdsa-sha2-nistp521", + PUBLIC_KEY_ECDSA_SHA2_NISTP521_521, + "27:a9:ed:e3:8e:17:00:e1:db:a2:85:e1:f8:ab:f5:60" + } + }); + } + + @Test + public void testPublicKeyValidation() { + String fingerprint = new HostKey(algorithm, Base64.getDecoder().decode(publicKey)).getFingerprint(); + assertEquals(description, expected, fingerprint); + } +} diff --git a/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java b/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java index 96c4b09cf..133e4090d 100644 --- a/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java +++ b/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java @@ -6,8 +6,6 @@ import com.amazonaws.AmazonClientException; import com.amazonaws.services.ec2.model.InstanceType; -import com.trilead.ssh2.Connection; -import com.trilead.ssh2.ServerHostKeyVerifier; import hudson.model.Node; import hudson.plugins.ec2.ConnectionStrategy; import hudson.plugins.ec2.EC2AbstractSlave; @@ -16,9 +14,13 @@ import hudson.plugins.ec2.SlaveTemplate; import hudson.plugins.ec2.util.ConnectionRule; import java.io.IOException; +import java.net.SocketAddress; +import java.security.PublicKey; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; +import org.apache.sshd.client.keyverifier.ServerKeyVerifier; +import org.apache.sshd.client.session.ClientSession; import org.hamcrest.core.StringContains; import org.junit.ClassRule; import org.junit.Test; @@ -219,7 +221,7 @@ private static class ConnectionAttempt { // The computer and verifier used during the try of connection private MockEC2Computer computer; - private ServerHostKeyVerifierImpl verifier; + private ServerKeyVerifier verifier; // The number of this attempt (for logging purposes) private int stage; @@ -255,7 +257,7 @@ private void configure() throws IOException, InterruptedException { private void connect() throws Exception { try { // Try to connect to it - Connection con = conRule.connect(verifier); + ClientSession con = conRule.connect(verifier); con.close(); } catch (IOException ignored) { // When the connection is not verified, the connect method throws an IOException @@ -267,17 +269,13 @@ private void assertState() { assertThat( String.format( "Stage %d. isOffline failed on %s using %s strategy", - stage, - computer.getName(), - verifier.strategy.getClass().getSimpleName()), + stage, computer.getName(), verifier.getClass().getSimpleName()), computer.isOffline(), is(true)); assertThat( String.format( "Stage %d. Offline reason failed on %s using %s strategy", - stage, - computer.getName(), - verifier.strategy.getClass().getSimpleName()), + stage, computer.getName(), verifier.getClass().getSimpleName()), computer.getOfflineCauseReason(), is(Messages.OfflineCause_SSHKeyCheckFailed())); } @@ -286,9 +284,7 @@ private void assertState() { assertThat( String.format( "Stage %d. Log message not found on %s using %s strategy", - stage, - computer.getName(), - verifier.strategy.getClass().getSimpleName()), + stage, computer.getName(), verifier.getClass().getSimpleName()), loggerRule, LoggerRule.recorded(StringContains.containsString(messageInLog))); } @@ -329,7 +325,7 @@ Builder isChangeHostKey(boolean changeHostKey) { connectionAttempt = new ConnectionAttempt(); } - private ConnectionAttempt build(MockEC2Computer computer, ServerHostKeyVerifierImpl verifier, int stage) { + private ConnectionAttempt build(MockEC2Computer computer, ServerKeyVerifier verifier, int stage) { connectionAttempt.stage = stage; connectionAttempt.computer = computer; connectionAttempt.verifier = verifier; @@ -439,7 +435,7 @@ public SlaveTemplate getSlaveTemplate() { } // A verifier using the set strategy - private static class ServerHostKeyVerifierImpl implements ServerHostKeyVerifier { + private static class ServerHostKeyVerifierImpl implements ServerKeyVerifier { private final EC2Computer computer; private final SshHostKeyVerificationStrategy strategy; @@ -449,10 +445,13 @@ public ServerHostKeyVerifierImpl(final EC2Computer computer, final SshHostKeyVer } @Override - public boolean verifyServerHostKey( - String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception { + public boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) { // TODO: change by the verifier defined on the instance template or the default one - return strategy.verify(computer, new HostKey(serverHostKeyAlgorithm, serverHostKey), null); + try { + return strategy.verify(computer, new HostKey(serverKey.getAlgorithm(), serverKey.getEncoded()), null); + } catch (Exception e) { + throw new RuntimeException(e); + } } } } diff --git a/src/test/java/hudson/plugins/ec2/util/ConnectionRule.java b/src/test/java/hudson/plugins/ec2/util/ConnectionRule.java index 37d44f2ed..2b23bb640 100644 --- a/src/test/java/hudson/plugins/ec2/util/ConnectionRule.java +++ b/src/test/java/hudson/plugins/ec2/util/ConnectionRule.java @@ -1,12 +1,16 @@ package hudson.plugins.ec2.util; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; -import com.trilead.ssh2.Connection; -import com.trilead.ssh2.ServerHostKeyVerifier; import hudson.plugins.ec2.win.winrm.RuntimeIOException; import java.io.IOException; +import java.time.Duration; import java.util.logging.Logger; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.future.ConnectFuture; +import org.apache.sshd.client.keyverifier.ServerKeyVerifier; +import org.apache.sshd.client.session.ClientSession; import org.junit.AssumptionViolatedException; import org.junit.rules.ExternalResource; import org.testcontainers.containers.Container; @@ -55,24 +59,29 @@ public class ConnectionRule extends ExternalResource { // The public ed-25510 host key of the server public String ED255219_PUB_KEY; - private Connection connection; + private SshClient sshClient = SshClient.setUpDefaultClient(); - public Connection connect(ServerHostKeyVerifier verifier) throws Exception { + private ClientSession connection; + + public ClientSession connect(ServerKeyVerifier verifier) throws Exception { int port = sshContainer.getMappedPort(SSH_PORT); String ip = sshContainer.getContainerIpAddress(); Logger log = Logger.getLogger(this.getClass().getName()); - connection = new Connection(ip, port); - connection.setTCPNoDelay(true); - connection.connect(verifier, 0, 0); + sshClient.setServerKeyVerifier(verifier); + + ConnectFuture connectFuture = sshClient.connect(USER, ip, port); + + connection = connectFuture.verify().getSession(); + connection.addPublicKeyIdentity(PEMParser.decodeKeyPair(privateKey, null)); + connection.auth().await(Duration.ofSeconds(10)); - connection.authenticateWithPublicKey(USER, privateKey.toCharArray(), null); - assertTrue(connection.isAuthenticationComplete()); + assertTrue(connection.isAuthenticated()); return connection; } - public void close() { + public void close() throws IOException { if (connection != null) { connection.close(); connection = null; @@ -93,6 +102,8 @@ public void before() { sshContainer.start(); + sshClient.start(); + } catch (RuntimeException re) { throw new AssumptionViolatedException("The container to connect to cannot be started", re); } @@ -110,10 +121,15 @@ public void before() { @Override public void after() { + sshClient.start(); sshContainer.stop(); if (connection != null) { - connection.close(); + try { + connection.close(); + } catch (IOException e) { + fail(e.getMessage()); + } } if (sshContainer.isRunning()) { From f0f56838520a37d26f50fe090eec15b5ffee6883 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 21:27:08 +0000 Subject: [PATCH 080/267] build(deps): bump com.hierynomus:smbj from 0.13.0 to 0.14.0 (#1018) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9b7ab734f..65ad566c4 100644 --- a/pom.xml +++ b/pom.xml @@ -96,7 +96,7 @@ THE SOFTWARE. com.hierynomus smbj - 0.13.0 + 0.14.0 org.bouncycastle From 4d2067f41df31b5a99590cf35818e9462bc0e082 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Tue, 17 Dec 2024 12:45:04 -1000 Subject: [PATCH 081/267] Remove usages of deprecated constructor in tests (#1023) --- .../java/hudson/plugins/ec2/EC2CloudTest.java | 36 +- .../plugins/ec2/EC2RetentionStrategyTest.java | 103 +++++- .../plugins/ec2/EC2SlaveMonitorTest.java | 26 +- .../hudson/plugins/ec2/SlaveTemplateTest.java | 253 +++++++++++--- .../plugins/ec2/SlaveTemplateUnitTest.java | 330 +++++++++++++++--- .../plugins/ec2/TemplateLabelsTest.java | 20 +- .../SshHostKeyVerificationStrategyTest.java | 22 +- 7 files changed, 675 insertions(+), 115 deletions(-) diff --git a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java index 60c37cb4a..d7257f1f8 100644 --- a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java @@ -57,6 +57,7 @@ public void testSlaveTemplateAddition() throws Exception { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", @@ -76,10 +77,14 @@ public void testSlaveTemplateAddition() throws Exception { false, ConnectionStrategy.PUBLIC_IP, -1, - null, + Collections.emptyList(), null, Tenancy.Default, - EbsEncryptRootVolume.DEFAULT); + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); cloud.addTemplate(orig); assertNotNull(cloud.getTemplate(orig.description)); } @@ -114,6 +119,7 @@ public void testSlaveTemplateUpdate() throws Exception { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", @@ -133,10 +139,14 @@ public void testSlaveTemplateUpdate() throws Exception { false, ConnectionStrategy.PUBLIC_IP, -1, - null, + Collections.emptyList(), null, Tenancy.Default, - EbsEncryptRootVolume.DEFAULT); + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); SlaveTemplate secondSlaveTemplate = new SlaveTemplate( "ami-123", EC2AbstractSlave.TEST_ZONE, @@ -154,6 +164,7 @@ public void testSlaveTemplateUpdate() throws Exception { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", @@ -173,10 +184,14 @@ public void testSlaveTemplateUpdate() throws Exception { false, ConnectionStrategy.PUBLIC_IP, -1, - null, + Collections.emptyList(), null, Tenancy.Default, - EbsEncryptRootVolume.DEFAULT); + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); cloud.addTemplate(oldSlaveTemplate); cloud.addTemplate(secondSlaveTemplate); SlaveTemplate newSlaveTemplate = new SlaveTemplate( @@ -196,6 +211,7 @@ public void testSlaveTemplateUpdate() throws Exception { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", @@ -215,10 +231,14 @@ public void testSlaveTemplateUpdate() throws Exception { false, ConnectionStrategy.PUBLIC_IP, -1, - null, + Collections.emptyList(), null, Tenancy.Default, - EbsEncryptRootVolume.DEFAULT); + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); int index = cloud.getTemplates().indexOf(oldSlaveTemplate); cloud.updateTemplate(newSlaveTemplate, "OldSlaveDescription"); diff --git a/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java b/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java index c86ac19a1..29b3277ed 100644 --- a/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java @@ -305,19 +305,34 @@ public SlaveTemplate getSlaveTemplate() { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet-123 subnet-456", null, null, - true, + 0, + 0, null, "", false, false, "", false, - ""); + "", + false, + false, + false, + ConnectionStrategy.PRIVATE_DNS, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); } @Override @@ -429,19 +444,34 @@ public SlaveTemplate getSlaveTemplate() { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet-123 subnet-456", null, null, - true, + 0, + 0, null, "", false, false, "", false, - ""); + "", + false, + false, + false, + ConnectionStrategy.PRIVATE_DNS, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); } @Override @@ -703,6 +733,7 @@ public void testRetentionDespiteIdleWithMinimumInstances() throws Exception { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", @@ -714,7 +745,6 @@ public void testRetentionDespiteIdleWithMinimumInstances() throws Exception { null, true, true, - false, "", false, "", @@ -722,8 +752,15 @@ public void testRetentionDespiteIdleWithMinimumInstances() throws Exception { false, true, ConnectionStrategy.PRIVATE_IP, - 0, - Collections.emptyList()); + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); SSHCredentialHelper.assureSshCredentialAvailableThroughCredentialProviders("ghi"); AmazonEC2Cloud cloud = new AmazonEC2Cloud( "us-east-1", @@ -805,6 +842,7 @@ public void testRetentionDespiteIdleWithMinimumInstanceActiveTimeRange() throws "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", @@ -816,7 +854,6 @@ public void testRetentionDespiteIdleWithMinimumInstanceActiveTimeRange() throws null, true, true, - false, "", false, "", @@ -824,8 +861,15 @@ public void testRetentionDespiteIdleWithMinimumInstanceActiveTimeRange() throws false, true, ConnectionStrategy.PRIVATE_IP, - 0, - Collections.emptyList()); + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); MinimumNumberOfInstancesTimeRangeConfig minimumNumberOfInstancesTimeRangeConfig = new MinimumNumberOfInstancesTimeRangeConfig(); @@ -897,6 +941,7 @@ public void testRetentionIdleWithMinimumInstanceInactiveTimeRange() throws Excep "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", @@ -908,7 +953,6 @@ public void testRetentionIdleWithMinimumInstanceInactiveTimeRange() throws Excep null, true, true, - false, "", false, "", @@ -916,8 +960,15 @@ public void testRetentionIdleWithMinimumInstanceInactiveTimeRange() throws Excep false, true, ConnectionStrategy.PRIVATE_IP, - 0, - Collections.emptyList()); + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); MinimumNumberOfInstancesTimeRangeConfig minimumNumberOfInstancesTimeRangeConfig = new MinimumNumberOfInstancesTimeRangeConfig(); @@ -974,6 +1025,7 @@ public void testRetentionDespiteIdleWithMinimumInstanceActiveTimeRangeAfterMidni "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", @@ -985,7 +1037,6 @@ public void testRetentionDespiteIdleWithMinimumInstanceActiveTimeRangeAfterMidni null, true, true, - false, "", false, "", @@ -993,8 +1044,15 @@ public void testRetentionDespiteIdleWithMinimumInstanceActiveTimeRangeAfterMidni false, true, ConnectionStrategy.PRIVATE_IP, - 0, - Collections.emptyList()); + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); MinimumNumberOfInstancesTimeRangeConfig minimumNumberOfInstancesTimeRangeConfig = new MinimumNumberOfInstancesTimeRangeConfig(); @@ -1066,6 +1124,7 @@ public void testRetentionStopsAfterActiveRangeEnds() throws Exception { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", @@ -1077,7 +1136,6 @@ public void testRetentionStopsAfterActiveRangeEnds() throws Exception { null, true, true, - false, "", false, "", @@ -1085,8 +1143,15 @@ public void testRetentionStopsAfterActiveRangeEnds() throws Exception { false, true, ConnectionStrategy.PRIVATE_IP, - 0, - Collections.emptyList()); + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); MinimumNumberOfInstancesTimeRangeConfig minimumNumberOfInstancesTimeRangeConfig = new MinimumNumberOfInstancesTimeRangeConfig(); diff --git a/src/test/java/hudson/plugins/ec2/EC2SlaveMonitorTest.java b/src/test/java/hudson/plugins/ec2/EC2SlaveMonitorTest.java index b599a4bf2..4ed16c698 100644 --- a/src/test/java/hudson/plugins/ec2/EC2SlaveMonitorTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2SlaveMonitorTest.java @@ -43,17 +43,18 @@ public void testMinimumNumberOfInstances() throws Exception { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", null, null, 2, + 0, null, null, true, true, - false, "", false, "", @@ -61,7 +62,15 @@ public void testMinimumNumberOfInstances() throws Exception { false, true, ConnectionStrategy.PRIVATE_IP, - 0); + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); SSHCredentialHelper.assureSshCredentialAvailableThroughCredentialProviders("ghi"); AmazonEC2Cloud cloud = new AmazonEC2Cloud( "us-east-1", @@ -104,6 +113,7 @@ public void testMinimumNumberOfSpareInstances() throws Exception { "10", "remoteadmin", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", @@ -115,7 +125,6 @@ public void testMinimumNumberOfSpareInstances() throws Exception { null, true, true, - false, "", false, "", @@ -123,8 +132,15 @@ public void testMinimumNumberOfSpareInstances() throws Exception { false, true, ConnectionStrategy.PRIVATE_IP, - 0, - null); + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); SSHCredentialHelper.assureSshCredentialAvailableThroughCredentialProviders("ghi"); AmazonEC2Cloud cloud = new AmazonEC2Cloud( "us-east-1", diff --git a/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java b/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java index 1094c04df..b86b9ed14 100644 --- a/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java +++ b/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java @@ -120,6 +120,7 @@ public void testConfigRoundtrip() throws Exception { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", @@ -139,10 +140,14 @@ public void testConfigRoundtrip() throws Exception { false, ConnectionStrategy.PUBLIC_IP, -1, - null, + Collections.emptyList(), null, Tenancy.Default, - EbsEncryptRootVolume.DEFAULT); + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); List templates = new ArrayList<>(); templates.add(orig); @@ -185,6 +190,7 @@ public void testConfigRoundtripWithCustomSSHHostKeyVerificationStrategy() throws "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", @@ -196,7 +202,6 @@ public void testConfigRoundtripWithCustomSSHHostKeyVerificationStrategy() throws "", true, false, - false, "", false, "", @@ -206,7 +211,13 @@ public void testConfigRoundtripWithCustomSSHHostKeyVerificationStrategy() throws ConnectionStrategy.PUBLIC_IP, -1, null, - STRATEGY_TO_CHECK); + STRATEGY_TO_CHECK, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); List templates = new ArrayList<>(); templates.add(orig); @@ -257,19 +268,34 @@ public void testConfigWithSpotBidPrice() throws Exception { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", null, null, - true, + 0, + 0, null, "", false, false, "", false, - ""); + "", + false, + false, + false, + ConnectionStrategy.PRIVATE_DNS, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); List templates = new ArrayList<>(); templates.add(orig); @@ -314,19 +340,34 @@ public void testSpotConfigWithoutBidPrice() throws Exception { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", null, null, - true, + 0, + 0, null, "", false, false, "", false, - ""); + "", + false, + false, + false, + ConnectionStrategy.PRIVATE_DNS, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); List templates = new ArrayList<>(); templates.add(orig); @@ -363,19 +404,34 @@ public void testWindowsConfigRoundTrip() throws Exception { "10", "rrr", new WindowsData("password", false, ""), + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", null, null, - false, + 0, + 0, null, "", + false, true, + "", false, "", false, - ""); + false, + false, + ConnectionStrategy.PRIVATE_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); List templates = new ArrayList<>(); templates.add(orig); @@ -412,19 +468,34 @@ public void testUnixConfigRoundTrip() throws Exception { "10", "rrr", new UnixData("sudo", "", "", "22", ""), + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", null, null, - false, + 0, + 0, null, "", + false, true, + "", false, "", false, - ""); + false, + false, + ConnectionStrategy.PRIVATE_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); List templates = new ArrayList<>(); templates.add(orig); @@ -466,17 +537,18 @@ public void testMinimumNumberOfInstancesActiveRangeConfig() throws Exception { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", null, null, 2, + 0, null, null, true, true, - false, "", false, "", @@ -484,7 +556,15 @@ public void testMinimumNumberOfInstancesActiveRangeConfig() throws Exception { false, true, ConnectionStrategy.PRIVATE_IP, - 0); + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); slaveTemplate.setMinimumNumberOfInstancesTimeRangeConfig(minimumNumberOfInstancesTimeRangeConfig); List templates = new ArrayList<>(); @@ -534,19 +614,34 @@ public void provisionOndemandSetsAwsNetworkingOnEc2Request() throws Exception { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, subnetId, null, null, - false, + 0, + 0, null, iamInstanceProfile, - true, false, + true, "", associatePublicIp, - ""); + "", + false, + false, + false, + ConnectionStrategy.backwardsCompatible(false, false, associatePublicIp), + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); SlaveTemplate noSubnet = new SlaveTemplate( TEST_AMI, TEST_ZONE, @@ -564,19 +659,34 @@ public void provisionOndemandSetsAwsNetworkingOnEc2Request() throws Exception { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "", null, null, - false, + 0, + 0, null, iamInstanceProfile, - true, false, + true, "", associatePublicIp, - ""); + "", + false, + false, + false, + ConnectionStrategy.backwardsCompatible(false, false, associatePublicIp), + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); List templates = new ArrayList<>(); templates.add(orig); @@ -638,19 +748,34 @@ public void provisionOndemandSetsAwsNetworkingOnNetworkInterface() throws Except "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, subnetId, tags, null, - false, + 0, + 0, null, iamInstanceProfile, - true, false, + true, "", associatePublicIp, - ""); + "", + false, + false, + false, + ConnectionStrategy.backwardsCompatible(false, false, associatePublicIp), + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); SlaveTemplate noSubnet = new SlaveTemplate( TEST_AMI, TEST_ZONE, @@ -668,19 +793,34 @@ public void provisionOndemandSetsAwsNetworkingOnNetworkInterface() throws Except "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "", tags, null, - false, + 0, + 0, null, iamInstanceProfile, - true, false, + true, "", associatePublicIp, - ""); + "", + false, + false, + false, + ConnectionStrategy.backwardsCompatible(false, false, associatePublicIp), + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); List templates = new ArrayList<>(); templates.add(orig); @@ -736,19 +876,34 @@ public void provisionSpotFallsBackToOndemandWhenSpotQuotaExceeded() throws Excep "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, subnetId, null, null, - false, + 0, + 0, null, iamInstanceProfile, - true, false, + true, "", associatePublicIp, - ""); + "", + false, + false, + false, + ConnectionStrategy.backwardsCompatible(false, false, associatePublicIp), + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); AmazonEC2 mockedEC2 = setupTestForProvisioning(template); @@ -842,6 +997,7 @@ public void testMacConfig() throws Exception { "10", "fff", new MacData("sudo", null, null, "22", null), + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", @@ -861,9 +1017,14 @@ public void testMacConfig() throws Exception { false, ConnectionStrategy.PUBLIC_IP, -1, + Collections.emptyList(), null, - null, - Tenancy.Default); + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); List templates = new ArrayList<>(); templates.add(orig); @@ -897,6 +1058,7 @@ public void testAgentName() { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", @@ -916,9 +1078,14 @@ public void testAgentName() { false, ConnectionStrategy.PUBLIC_IP, -1, + Collections.emptyList(), null, - null, - Tenancy.Default); + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); SlaveTemplate working = new SlaveTemplate( TEST_AMI, TEST_ZONE, @@ -936,6 +1103,7 @@ public void testAgentName() { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", @@ -955,9 +1123,14 @@ public void testAgentName() { false, ConnectionStrategy.PUBLIC_IP, -1, + Collections.emptyList(), null, - null, - Tenancy.Default); + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); List templates = new ArrayList<>(); templates.add(broken); templates.add(working); @@ -1011,8 +1184,8 @@ public void testMetadataV2Config() throws Exception { false, ConnectionStrategy.PUBLIC_IP, -1, + Collections.emptyList(), null, - HostKeyVerificationStrategyEnum.CHECK_NEW_HARD, Tenancy.Default, EbsEncryptRootVolume.DEFAULT, true, @@ -1073,8 +1246,8 @@ public void provisionOnDemandWithUnsupportedInstanceMetadata() throws Exception false, ConnectionStrategy.PUBLIC_IP, -1, + Collections.emptyList(), null, - HostKeyVerificationStrategyEnum.CHECK_NEW_HARD, Tenancy.Default, EbsEncryptRootVolume.DEFAULT, true, @@ -1133,8 +1306,8 @@ public void provisionOnDemandSetsMetadataV1Options() throws Exception { false, ConnectionStrategy.PUBLIC_IP, -1, + Collections.emptyList(), null, - HostKeyVerificationStrategyEnum.CHECK_NEW_HARD, Tenancy.Default, EbsEncryptRootVolume.DEFAULT, true, @@ -1195,8 +1368,8 @@ public void provisionOnDemandSetsMetadataV2Options() throws Exception { false, ConnectionStrategy.PUBLIC_IP, -1, + Collections.emptyList(), null, - HostKeyVerificationStrategyEnum.CHECK_NEW_HARD, Tenancy.Default, EbsEncryptRootVolume.DEFAULT, true, @@ -1257,8 +1430,8 @@ public void provisionOnDemandSetsMetadataDefaultOptions() throws Exception { false, ConnectionStrategy.PUBLIC_IP, -1, + Collections.emptyList(), null, - HostKeyVerificationStrategyEnum.CHECK_NEW_HARD, Tenancy.Default, EbsEncryptRootVolume.DEFAULT, null, diff --git a/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java b/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java index b43487263..861622086 100644 --- a/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java +++ b/src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java @@ -86,19 +86,34 @@ public CreateTagsResult createTags(com.amazonaws.services.ec2.model.CreateTagsRe "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", tags, null, - false, + 0, + 0, null, "", + false, true, + "", false, "", false, - "") { + false, + false, + ConnectionStrategy.PRIVATE_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED) { @Override protected Object readResolve() { return null; @@ -157,19 +172,34 @@ public CreateTagsResult createTags(com.amazonaws.services.ec2.model.CreateTagsRe "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", tags, null, - false, + 0, + 0, null, "", + false, true, + "", false, "", false, - "") { + false, + false, + ConnectionStrategy.PRIVATE_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED) { @Override protected Object readResolve() { return null; @@ -250,19 +280,34 @@ public void testMakeDescribeImagesRequest() throws Exception { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", null, null, - false, + 0, + 0, null, "", + false, true, + "", false, "", false, - "") { + false, + false, + ConnectionStrategy.PRIVATE_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED) { @Override protected Object readResolve() { return null; @@ -441,19 +486,34 @@ private Boolean checkEncryptedForSetupRootDevice(EbsEncryptRootVolume rootVolume "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", null, null, - false, + 0, + 0, null, "", + false, true, + "", false, "", false, - "") { + false, + false, + ConnectionStrategy.PRIVATE_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED) { @Override protected Object readResolve() { return null; @@ -519,19 +579,34 @@ public void testNullTimeoutShouldReturnMaxInt() { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", null, null, - false, + 0, + 0, null, "iamInstanceProfile", false, false, null, false, - ""); + "", + false, + false, + false, + ConnectionStrategy.PRIVATE_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); assertEquals(Integer.MAX_VALUE, st.getLaunchTimeout()); } @@ -554,19 +629,34 @@ public void testUpdateAmi() { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", null, null, - false, + 0, + 0, null, "iamInstanceProfile", false, false, "0", false, - ""); + "", + false, + false, + false, + ConnectionStrategy.PRIVATE_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); assertEquals("ami1", st.getAmi()); st.setAmi("ami2"); assertEquals("ami2", st.getAmi()); @@ -593,19 +683,34 @@ public void test0TimeoutShouldReturnMaxInt() { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", null, null, - false, + 0, + 0, null, "iamInstanceProfile", false, false, "0", false, - ""); + "", + false, + false, + false, + ConnectionStrategy.PRIVATE_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); assertEquals(Integer.MAX_VALUE, st.getLaunchTimeout()); } @@ -628,19 +733,34 @@ public void testNegativeTimeoutShouldReturnMaxInt() { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", null, null, - false, + 0, + 0, null, "iamInstanceProfile", false, false, "-1", false, - ""); + "", + false, + false, + false, + ConnectionStrategy.PRIVATE_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); assertEquals(Integer.MAX_VALUE, st.getLaunchTimeout()); } @@ -663,19 +783,34 @@ public void testNonNumericTimeoutShouldReturnMaxInt() { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", null, null, - false, + 0, + 0, null, "iamInstanceProfile", false, false, "NotANumber", false, - ""); + "", + false, + false, + false, + ConnectionStrategy.PRIVATE_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); assertEquals(Integer.MAX_VALUE, st.getLaunchTimeout()); } @@ -698,19 +833,34 @@ public void testAssociatePublicIpSetting() { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", null, null, - false, + 0, + 0, null, "iamInstanceProfile", false, false, null, true, - ""); + "", + false, + false, + false, + ConnectionStrategy.PUBLIC_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); assertTrue(st.getAssociatePublicIp()); } @@ -733,22 +883,34 @@ public void testConnectUsingPublicIpSetting() { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", null, null, - false, + 0, + 0, null, "iamInstanceProfile", false, false, - false, null, true, "", false, - true); + false, + false, + ConnectionStrategy.PUBLIC_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); assertEquals(st.connectionStrategy, ConnectionStrategy.PUBLIC_IP); } @@ -771,19 +933,34 @@ public void testConnectUsingPublicIpSettingWithDefaultSetting() { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", null, null, - false, + 0, + 0, null, "iamInstanceProfile", false, false, null, true, - ""); + "", + false, + false, + false, + ConnectionStrategy.PUBLIC_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); assertEquals(st.connectionStrategy, ConnectionStrategy.PUBLIC_IP); } @@ -795,7 +972,6 @@ public void testBackwardCompatibleUnixData() { null, "default", "foo", - "22", InstanceType.M1Large, false, "ttt", @@ -806,19 +982,35 @@ public void testBackwardCompatibleUnixData() { "aaa", "10", "rrr", - "sudo", - null, - null, + new UnixData("sudo", null, null, "22", null), + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", null, null, - false, + 0, + 0, null, "iamInstanceProfile", false, - "NotANumber"); + false, + "NotANumber", + false, + null, + false, + false, + false, + ConnectionStrategy.PRIVATE_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); assertFalse(st.isWindowsSlave()); assertEquals(22, st.getSshPort()); assertEquals("sudo", st.getRootCommandPrefix()); @@ -843,19 +1035,34 @@ public void testChooseSpaceDelimitedSubnetId() throws Exception { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet-123 subnet-456", null, null, - true, + 0, + 0, null, "", false, false, "", false, - ""); + "", + false, + false, + false, + ConnectionStrategy.PRIVATE_DNS, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); String subnet1 = slaveTemplate.chooseSubnetId(); String subnet2 = slaveTemplate.chooseSubnetId(); @@ -885,19 +1092,34 @@ public void testChooseCommaDelimitedSubnetId() throws Exception { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet-123,subnet-456", null, null, - true, + 0, + 0, null, "", false, false, "", false, - ""); + "", + false, + false, + false, + ConnectionStrategy.PRIVATE_DNS, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); String subnet1 = slaveTemplate.chooseSubnetId(); String subnet2 = slaveTemplate.chooseSubnetId(); @@ -927,19 +1149,34 @@ public void testChooseSemicolonDelimitedSubnetId() throws Exception { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet-123;subnet-456", null, null, - true, + 0, + 0, null, "", false, false, "", false, - ""); + "", + false, + false, + false, + ConnectionStrategy.PRIVATE_DNS, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); String subnet1 = slaveTemplate.chooseSubnetId(); String subnet2 = slaveTemplate.chooseSubnetId(); @@ -970,19 +1207,34 @@ public void testConnectionStrategyDeprecatedFieldsAreExported() { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet 456", Collections.singletonList(new EC2Tag("name1", "value1")), null, - false, + 0, + 0, null, "", + false, true, + "", false, "", false, - ""); + false, + false, + ConnectionStrategy.PRIVATE_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); String exported = Jenkins.XSTREAM.toXML(template); assertThat(exported, containsString("usePrivateDnsName")); diff --git a/src/test/java/hudson/plugins/ec2/TemplateLabelsTest.java b/src/test/java/hudson/plugins/ec2/TemplateLabelsTest.java index eb4bd2a46..4baded87e 100644 --- a/src/test/java/hudson/plugins/ec2/TemplateLabelsTest.java +++ b/src/test/java/hudson/plugins/ec2/TemplateLabelsTest.java @@ -31,6 +31,7 @@ import hudson.model.Node; import hudson.model.labels.LabelAtom; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.junit.Rule; import org.junit.Test; @@ -73,19 +74,34 @@ private void setUpCloud(String label, Node.Mode mode) throws Exception { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", true, "subnet 456", tags, null, - false, + 0, + 0, null, "", false, false, null, false, - ""); + "", + false, + false, + false, + ConnectionStrategy.PRIVATE_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); List templates = new ArrayList<>(); templates.add(template); diff --git a/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java b/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java index 96c4b09cf..2e8bbda0f 100644 --- a/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java +++ b/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java @@ -12,11 +12,14 @@ import hudson.plugins.ec2.ConnectionStrategy; import hudson.plugins.ec2.EC2AbstractSlave; import hudson.plugins.ec2.EC2Computer; +import hudson.plugins.ec2.EbsEncryptRootVolume; import hudson.plugins.ec2.InstanceState; import hudson.plugins.ec2.SlaveTemplate; +import hudson.plugins.ec2.Tenancy; import hudson.plugins.ec2.util.ConnectionRule; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.logging.Level; import org.hamcrest.core.StringContains; @@ -422,19 +425,34 @@ public SlaveTemplate getSlaveTemplate() { "10", "fff", null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, "subnet-123 subnet-456", null, null, - true, + 0, + 0, null, "", false, false, "", false, - ""); + "", + false, + false, + false, + ConnectionStrategy.PRIVATE_DNS, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); } } From f7deaff19bbda78fc70f9a42030444a30f42cba4 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Wed, 18 Dec 2024 17:08:12 +0100 Subject: [PATCH 082/267] NullPointerException prevention --- src/main/java/hudson/plugins/ec2/util/PEMParser.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/hudson/plugins/ec2/util/PEMParser.java b/src/main/java/hudson/plugins/ec2/util/PEMParser.java index 8e8db67cb..1abc90964 100644 --- a/src/main/java/hudson/plugins/ec2/util/PEMParser.java +++ b/src/main/java/hudson/plugins/ec2/util/PEMParser.java @@ -25,6 +25,9 @@ public static KeyPair decodeKeyPair(String pem, String password) throws IOExcept try (org.bouncycastle.openssl.PEMParser pemParser = new org.bouncycastle.openssl.PEMParser(new StringReader(pem))) { Object object = pemParser.readObject(); + if (object == null) { + throw new IllegalArgumentException("Failed to parse PEM input"); + } JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); if (object instanceof PEMEncryptedKeyPair) { From 4549402f47b0389add0beb914e7b22c573f9b0f5 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Wed, 18 Dec 2024 17:16:45 +0100 Subject: [PATCH 083/267] Spotbugs fix --- src/main/java/hudson/plugins/ec2/util/PEMParser.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/hudson/plugins/ec2/util/PEMParser.java b/src/main/java/hudson/plugins/ec2/util/PEMParser.java index 1abc90964..6c1bbe720 100644 --- a/src/main/java/hudson/plugins/ec2/util/PEMParser.java +++ b/src/main/java/hudson/plugins/ec2/util/PEMParser.java @@ -4,9 +4,11 @@ import java.io.StringReader; import java.security.KeyFactory; import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.spec.ECPrivateKeySpec; +import java.security.spec.InvalidKeySpecException; import java.security.spec.RSAPrivateCrtKeySpec; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; @@ -70,7 +72,7 @@ private static PublicKey generatePublicKeyFromPrivateKey(PrivateKey privateKey) } else { return null; } - } catch (Exception e) { + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { return null; } } From bcb70f185487a5934f46669eef9b37ee9bc6d7c5 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Fri, 20 Dec 2024 09:09:11 -0800 Subject: [PATCH 084/267] Remove more usages of deprecated constructors in tests (#1024) --- .../plugins/ec2/EC2AbstractSlaveTest.java | 12 ++++++-- .../plugins/ec2/EC2OndemandSlaveTest.java | 18 +++++++++--- .../plugins/ec2/EC2RetentionStrategyTest.java | 28 +++++++++++++++---- .../hudson/plugins/ec2/SlaveTemplateTest.java | 24 ++++++++-------- .../plugins/ec2/TemplateLabelsTest.java | 2 +- .../SshHostKeyVerificationStrategyTest.java | 9 ++++-- .../plugins/ec2/win/WinConnectionTest.java | 3 +- 7 files changed, 67 insertions(+), 29 deletions(-) diff --git a/src/test/java/hudson/plugins/ec2/EC2AbstractSlaveTest.java b/src/test/java/hudson/plugins/ec2/EC2AbstractSlaveTest.java index 96baff2ab..9d37d5c4d 100644 --- a/src/test/java/hudson/plugins/ec2/EC2AbstractSlaveTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2AbstractSlaveTest.java @@ -117,7 +117,8 @@ public void testMaxUsesBackwardCompat() throws Exception { List templates = new ArrayList<>(); templates.add(orig); String cloudName = "us-east-1"; - AmazonEC2Cloud ac = new AmazonEC2Cloud(cloudName, false, "abc", "us-east-1", "ghi", "3", templates, null, null); + AmazonEC2Cloud ac = + new AmazonEC2Cloud(cloudName, false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); EC2AbstractSlave slave = new EC2AbstractSlave( @@ -134,16 +135,21 @@ public void testMaxUsesBackwardCompat() throws Exception { "tmpDir", new ArrayList<>(), "root", + EC2AbstractSlave.DEFAULT_JAVA_PATH, "jvm", false, "idle", null, cloudName, - false, Integer.MAX_VALUE, new UnixData("remote", null, null, "22", null), ConnectionStrategy.PRIVATE_IP, - 0) { + -1, + Tenancy.Default, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED) { @Override public void terminate() {} diff --git a/src/test/java/hudson/plugins/ec2/EC2OndemandSlaveTest.java b/src/test/java/hudson/plugins/ec2/EC2OndemandSlaveTest.java index 87f295c42..535848f84 100644 --- a/src/test/java/hudson/plugins/ec2/EC2OndemandSlaveTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2OndemandSlaveTest.java @@ -27,6 +27,7 @@ public void testSpecifyMode() throws Exception { "tmpDir", Collections.emptyList(), "remoteAdmin", + EC2AbstractSlave.DEFAULT_JAVA_PATH, "jvmopts", false, "30", @@ -34,11 +35,15 @@ public void testSpecifyMode() throws Exception { "privateDNS", Collections.emptyList(), "cloudName", - false, 0, new UnixData("a", null, null, "b", null), ConnectionStrategy.PRIVATE_IP, - -1); + -1, + Tenancy.Default, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); assertEquals(Node.Mode.NORMAL, slaveNormal.getMode()); EC2OndemandSlave slaveExclusive = new EC2OndemandSlave( @@ -53,6 +58,7 @@ public void testSpecifyMode() throws Exception { "tmpDir", Collections.emptyList(), "remoteAdmin", + EC2AbstractSlave.DEFAULT_JAVA_PATH, "jvmopts", false, "30", @@ -60,11 +66,15 @@ public void testSpecifyMode() throws Exception { "privateDNS", Collections.emptyList(), "cloudName", - false, 0, new UnixData("a", null, null, "b", null), ConnectionStrategy.PRIVATE_IP, - -1); + -1, + Tenancy.Default, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); assertEquals(Node.Mode.EXCLUSIVE, slaveExclusive.getMode()); } diff --git a/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java b/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java index 29b3277ed..0ba04ce41 100644 --- a/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java @@ -232,16 +232,21 @@ private EC2Computer computerWithIdleTime( "tmpDir", new ArrayList<>(), "remote", + EC2AbstractSlave.DEFAULT_JAVA_PATH, "jvm", false, "idle", null, "cloud", - false, Integer.MAX_VALUE, null, ConnectionStrategy.PRIVATE_IP, - -1) { + -1, + Tenancy.Default, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED) { @Override public void terminate() {} @@ -372,16 +377,21 @@ private EC2Computer computerWithUpTime( "tmpDir", new ArrayList<>(), "remote", + EC2AbstractSlave.DEFAULT_JAVA_PATH, "jvm", false, "idle", null, "cloud", - false, Integer.MAX_VALUE, null, ConnectionStrategy.PRIVATE_IP, - -1) { + -1, + Tenancy.Default, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED) { @Override public void terminate() {} @@ -533,16 +543,21 @@ private EC2Computer computerWithUsageLimit(final int usageLimit) throws Exceptio "tmpDir", new ArrayList<>(), "remote", + EC2AbstractSlave.DEFAULT_JAVA_PATH, "jvm", false, "idle", null, "cloud", - false, Integer.MAX_VALUE, null, ConnectionStrategy.PRIVATE_IP, - usageLimit) { + usageLimit, + Tenancy.Default, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED) { @Override public void terminate() { terminateCalled.set(true); @@ -990,6 +1005,7 @@ public void testRetentionIdleWithMinimumInstanceInactiveTimeRange() throws Excep "abc", "us-east-1", PrivateKeyHelper.generate(), + null, "3", Collections.singletonList(template), "roleArn", diff --git a/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java b/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java index b86b9ed14..82f0396a1 100644 --- a/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java +++ b/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java @@ -153,7 +153,7 @@ public void testConfigRoundtrip() throws Exception { templates.add(orig); AmazonEC2Cloud ac = - new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", "3", templates, null, null); + new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); r.submit(getConfigForm(ac)); @@ -223,7 +223,7 @@ public void testConfigRoundtripWithCustomSSHHostKeyVerificationStrategy() throws templates.add(orig); AmazonEC2Cloud ac = - new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", "3", templates, null, null); + new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); r.submit(getConfigForm(ac)); @@ -300,7 +300,7 @@ public void testConfigWithSpotBidPrice() throws Exception { templates.add(orig); AmazonEC2Cloud ac = - new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", "3", templates, null, null); + new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); r.submit(getConfigForm(ac)); @@ -372,7 +372,7 @@ public void testSpotConfigWithoutBidPrice() throws Exception { templates.add(orig); AmazonEC2Cloud ac = - new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", "3", templates, null, null); + new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); r.submit(getConfigForm(ac)); @@ -403,7 +403,7 @@ public void testWindowsConfigRoundTrip() throws Exception { "aaa", "10", "rrr", - new WindowsData("password", false, ""), + new WindowsData("password", false, "", false, true), EC2AbstractSlave.DEFAULT_JAVA_PATH, "-Xmx1g", false, @@ -437,7 +437,7 @@ public void testWindowsConfigRoundTrip() throws Exception { templates.add(orig); AmazonEC2Cloud ac = - new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", "3", templates, null, null); + new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); r.submit(getConfigForm(ac)); @@ -500,7 +500,7 @@ public void testUnixConfigRoundTrip() throws Exception { templates.add(orig); AmazonEC2Cloud ac = - new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", "3", templates, null, null); + new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); r.submit(getConfigForm(ac)); @@ -571,7 +571,7 @@ public void testMinimumNumberOfInstancesActiveRangeConfig() throws Exception { templates.add(slaveTemplate); AmazonEC2Cloud ac = - new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", "3", templates, null, null); + new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); r.configRoundtrip(); @@ -1030,7 +1030,7 @@ public void testMacConfig() throws Exception { templates.add(orig); AmazonEC2Cloud ac = - new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", "3", templates, null, null); + new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); r.submit(getConfigForm(ac)); @@ -1135,11 +1135,11 @@ public void testAgentName() { templates.add(broken); templates.add(working); AmazonEC2Cloud brokenCloud = - new AmazonEC2Cloud("broken/cloud", false, "abc", "us-east-1", "ghi", "3", templates, null, null); + new AmazonEC2Cloud("broken/cloud", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); assertThat(broken.getSlaveName("test"), is("test")); assertThat(working.getSlaveName("test"), is("test")); AmazonEC2Cloud workingCloud = - new AmazonEC2Cloud("cloud", false, "abc", "us-east-1", "ghi", "3", templates, null, null); + new AmazonEC2Cloud("cloud", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); assertThat(broken.getSlaveName("test"), is("test")); assertThat(working.getSlaveName("test"), is("EC2 (cloud) - working (test)")); } @@ -1196,7 +1196,7 @@ public void testMetadataV2Config() throws Exception { List templates = Collections.singletonList(orig); AmazonEC2Cloud ac = - new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", "3", templates, null, null); + new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); r.submit(r.createWebClient().goTo("configure").getFormByName("config")); diff --git a/src/test/java/hudson/plugins/ec2/TemplateLabelsTest.java b/src/test/java/hudson/plugins/ec2/TemplateLabelsTest.java index 4baded87e..32f3f99e4 100644 --- a/src/test/java/hudson/plugins/ec2/TemplateLabelsTest.java +++ b/src/test/java/hudson/plugins/ec2/TemplateLabelsTest.java @@ -105,7 +105,7 @@ private void setUpCloud(String label, Node.Mode mode) throws Exception { List templates = new ArrayList<>(); templates.add(template); - ac = new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", "3", templates, null, null); + ac = new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); } @Test diff --git a/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java b/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java index 2e8bbda0f..18d0b3aab 100644 --- a/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java +++ b/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java @@ -369,16 +369,21 @@ private static MockEC2Computer createComputer(String suffix) throws Exception { "tmpDir", new ArrayList<>(), "remote", + EC2AbstractSlave.DEFAULT_JAVA_PATH, "jvm", false, "idle", null, "cloud", - false, Integer.MAX_VALUE, null, ConnectionStrategy.PRIVATE_IP, - -1) { + -1, + Tenancy.Default, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED) { @Override public void terminate() {} diff --git a/src/test/java/hudson/plugins/ec2/win/WinConnectionTest.java b/src/test/java/hudson/plugins/ec2/win/WinConnectionTest.java index 257189afa..b2556c665 100644 --- a/src/test/java/hudson/plugins/ec2/win/WinConnectionTest.java +++ b/src/test/java/hudson/plugins/ec2/win/WinConnectionTest.java @@ -17,7 +17,8 @@ public void testExecute() throws Exception { WinConnection connect = new WinConnection( System.getProperty("winrm.host"), System.getProperty("winrm.username", "Administrator"), - System.getProperty("winrm.password")); + System.getProperty("winrm.password"), + true); connect.setUseHTTPS(true); WindowsProcess process = connect.execute("dir c:\\"); process.waitFor(); From 3b52047c81d253b5c61e1a8b7cfaf4a329a9ca10 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Fri, 20 Dec 2024 18:55:31 +0100 Subject: [PATCH 085/267] Null pointer prevention --- .../plugins/ec2/ssh/EC2MacLauncher.java | 21 +++++++++++++++---- .../plugins/ec2/ssh/EC2UnixLauncher.java | 21 +++++++++++++++---- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index 1e5d1483c..beae5ecec 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -54,6 +54,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintStream; @@ -248,7 +249,10 @@ && executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { try (ClientChannel channel = clientSession.createExecChannel( initCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - channel.getInvertedIn().close(); // nothing to write here + OutputStream invertedIn = channel.getInvertedIn(); + if (invertedIn != null) { + invertedIn.close(); // nothing to write here + } channel.open().await(timeout); Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); @@ -264,7 +268,10 @@ && executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { return; } - channel.getInvertedErr().close(); // we are not supposed to get anything from stderr + InputStream invertedErr = channel.getInvertedErr(); + if (invertedErr != null) { + invertedErr.close(); // we are not supposed to get anything from stderr + } IOUtils.copy(channel.getInvertedOut(), logger); } @@ -272,7 +279,10 @@ && executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { String createHudsonRunInitCommand = buildUpCommand(computer, "touch ~/.hudson-run-init"); try (ClientChannel channel = clientSession.createExecChannel( createHudsonRunInitCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - channel.getInvertedIn().close(); // nothing to write here + OutputStream invertedIn = channel.getInvertedIn(); + if (invertedIn != null) { + invertedIn.close(); // nothing to write here + } channel.open().await(timeout); Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); @@ -288,7 +298,10 @@ && executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { return; } - channel.getInvertedErr().close(); // we are not supposed to get anything from stderr + InputStream invertedErr = channel.getInvertedErr(); + if (invertedErr != null) { + invertedErr.close(); // we are not supposed to get anything from stderr + } IOUtils.copy(channel.getInvertedOut(), logger); } } diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index 90d730cfd..b4c27752b 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -55,6 +55,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintStream; @@ -250,7 +251,10 @@ && executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { try (ClientChannel channel = clientSession.createExecChannel( initCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - channel.getInvertedIn().close(); // nothing to write here + OutputStream invertedIn = channel.getInvertedIn(); + if (invertedIn != null) { + invertedIn.close(); // nothing to write here + } channel.open().await(timeout); Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); @@ -266,7 +270,10 @@ && executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { return; } - channel.getInvertedErr().close(); // we are not supposed to get anything from stderr + InputStream invertedErr = channel.getInvertedErr(); + if (invertedErr != null) { + invertedErr.close(); // we are not supposed to get anything from stderr + } IOUtils.copy(channel.getInvertedOut(), logger); } @@ -275,7 +282,10 @@ && executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { String createHudsonRunInitCommand = buildUpCommand(computer, "touch ~/.hudson-run-init"); try (ClientChannel channel = clientSession.createExecChannel( createHudsonRunInitCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - channel.getInvertedIn().close(); // nothing to write here + OutputStream invertedIn = channel.getInvertedIn(); + if (invertedIn != null) { + invertedIn.close(); // nothing to write here + } channel.open().await(timeout); Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); @@ -291,7 +301,10 @@ && executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { return; } - channel.getInvertedErr().close(); // we are not supposed to get anything from stderr + InputStream invertedErr = channel.getInvertedErr(); + if (invertedErr != null) { + invertedErr.close(); // we are not supposed to get anything from stderr + } IOUtils.copy(channel.getInvertedOut(), logger); } } From bcfbd7cf1bed63650fdb69cce83a769a6c274949 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Fri, 20 Dec 2024 19:00:09 +0100 Subject: [PATCH 086/267] Add a timestamp to the SCP Without a timestamp, the permissions are ignored --- src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java | 8 ++++++-- src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index beae5ecec..225fed07a 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -82,6 +82,7 @@ import org.apache.sshd.client.keyverifier.ServerKeyVerifier; import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.scp.client.CloseableScpClient; +import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; /** * {@link ComputerLauncher} that connects to a Unix agent on EC2 by using SSH. @@ -229,6 +230,9 @@ protected void launchScript(EC2Computer computer, TaskListener listener) clientSession = cleanupClientSession; try (CloseableScpClient scp = createScpClient(clientSession)) { + String timestamp = Duration.ofMillis(System.currentTimeMillis()).toSeconds() + " 0"; + ScpTimestampCommandDetails scpTimestamp = + ScpTimestampCommandDetails.parse("T" + timestamp + " " + timestamp); String initScript = node.initScript; String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); @@ -243,7 +247,7 @@ && executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { initScript.getBytes(StandardCharsets.UTF_8), tmpDir + "/init.sh", List.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_READ), - null); + scpTimestamp); String initCommand = buildUpCommand(computer, tmpDir + "/init.sh"); try (ClientChannel channel = clientSession.createExecChannel( @@ -338,7 +342,7 @@ && executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { Jenkins.get().getJnlpJars("remoting.jar").readFully(), tmpDir + "/remoting.jar", List.of(PosixFilePermission.OWNER_READ), - null); + scpTimestamp); final String jvmopts = node.jvmopts; final String prefix = computer.getSlaveCommandPrefix(); diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index b4c27752b..243a67728 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -84,6 +84,7 @@ import org.apache.sshd.client.keyverifier.ServerKeyVerifier; import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.scp.client.CloseableScpClient; +import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; /** * {@link ComputerLauncher} that connects to a Unix agent on EC2 by using SSH. @@ -231,6 +232,9 @@ protected void launchScript(EC2Computer computer, TaskListener listener) clientSession = cleanupClientSession; try (CloseableScpClient scp = createScpClient(clientSession)) { + String timestamp = Duration.ofMillis(System.currentTimeMillis()).toSeconds() + " 0"; + ScpTimestampCommandDetails scpTimestamp = + ScpTimestampCommandDetails.parse("T" + timestamp + " " + timestamp); String initScript = node.initScript; String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); @@ -245,7 +249,7 @@ && executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { initScript.getBytes(StandardCharsets.UTF_8), tmpDir + "/init.sh", List.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_READ), - null); + scpTimestamp); String initCommand = buildUpCommand(computer, tmpDir + "/init.sh"); try (ClientChannel channel = clientSession.createExecChannel( @@ -327,7 +331,7 @@ && executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { Jenkins.get().getJnlpJars("remoting.jar").readFully(), tmpDir + "/remoting.jar", List.of(PosixFilePermission.OWNER_READ), - null); + scpTimestamp); final String jvmopts = node.jvmopts; final String prefix = computer.getSlaveCommandPrefix(); From 40112c546d7b7b86e7a0ff10a18fb48728b48501 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Fri, 20 Dec 2024 16:12:20 -0800 Subject: [PATCH 087/267] Migrate from Acegi compatibility layer to Spring Security (#1026) --- src/main/java/hudson/plugins/ec2/EC2Cloud.java | 15 +++++++-------- .../hudson/plugins/ec2/EC2RetentionStrategy.java | 2 +- .../plugins/ec2/EC2RetentionStrategyTest.java | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 28302e62a..b929a6637 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -58,7 +58,6 @@ import com.cloudbees.plugins.credentials.common.AbstractIdCredentialsListBoxModel; import com.cloudbees.plugins.credentials.common.StandardListBoxModel; import com.cloudbees.plugins.credentials.domains.Domain; -import com.cloudbees.plugins.credentials.domains.DomainRequirement; import com.google.common.annotations.VisibleForTesting; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; @@ -1127,8 +1126,8 @@ private static AmazonWebServicesCredentials getCredentials(@CheckForNull String return null; } return CredentialsMatchers.firstOrNull( - CredentialsProvider.lookupCredentials( - AmazonWebServicesCredentials.class, Jenkins.get(), ACL.SYSTEM, Collections.emptyList()), + CredentialsProvider.lookupCredentialsInItemGroup( + AmazonWebServicesCredentials.class, Jenkins.get(), ACL.SYSTEM2, Collections.emptyList()), CredentialsMatchers.withId(credentialsId)); } @@ -1281,16 +1280,16 @@ public ListBoxModel doFillSshKeysCredentialsIdItems( if (Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { result = result.includeEmptyValue() .includeMatchingAs( - Jenkins.getAuthentication(), + Jenkins.getAuthentication2(), context, SSHUserPrivateKey.class, - Collections.emptyList(), + Collections.emptyList(), CredentialsMatchers.always()) .includeMatchingAs( - ACL.SYSTEM, + ACL.SYSTEM2, context, SSHUserPrivateKey.class, - Collections.emptyList(), + Collections.emptyList(), CredentialsMatchers.always()) .includeCurrentValue(sshKeysCredentialsId); } @@ -1479,7 +1478,7 @@ public ListBoxModel doFillCredentialsIdItems(@AncestorInPath ItemGroup context) return new StandardListBoxModel() .includeEmptyValue() .includeMatchingAs( - ACL.SYSTEM, + ACL.SYSTEM2, context, AmazonWebServicesCredentials.class, Collections.emptyList(), diff --git a/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java b/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java index f38b95755..0ec4b4a80 100644 --- a/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java +++ b/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java @@ -264,7 +264,7 @@ private boolean itemsInQueueForThisSlave(EC2Computer c) { return false; } final Label selfLabel = selfNode.getSelfLabel(); - Queue.Item[] items = Jenkins.getInstance().getQueue().getItems(); + Queue.Item[] items = Jenkins.get().getQueue().getItems(); for (Queue.Item item : items) { final Label assignedLabel = item.getAssignedLabel(); if (assignedLabel == selfLabel) { diff --git a/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java b/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java index 0ba04ce41..e885d148e 100644 --- a/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java @@ -44,11 +44,11 @@ import java.util.stream.Collectors; import jenkins.model.Jenkins; import jenkins.util.NonLocalizable; -import org.acegisecurity.Authentication; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.LoggerRule; +import org.springframework.security.core.Authentication; public class EC2RetentionStrategyTest { @@ -140,7 +140,7 @@ public String getShortDescription() { public ACL getACL() { return new ACL() { @Override - public boolean hasPermission(@NonNull Authentication a, @NonNull Permission permission) { + public boolean hasPermission2(@NonNull Authentication a, @NonNull Permission permission) { return true; } }; From 4b7fcc52595d5920f377f152ae607a5b2000c921 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Fri, 27 Dec 2024 10:40:16 -0800 Subject: [PATCH 088/267] Remove Eucalyptus support (#1027) --- README.md | 27 +- .../hudson/plugins/ec2/AmazonEC2Cloud.java | 223 +-------- .../hudson/plugins/ec2/EC2AbstractSlave.java | 4 +- .../java/hudson/plugins/ec2/EC2Cloud.java | 231 +++++++-- src/main/java/hudson/plugins/ec2/EC2Step.java | 15 +- .../java/hudson/plugins/ec2/Eucalyptus.java | 167 ------- .../ec2/NoDelayProvisionerStrategy.java | 4 +- .../java/hudson/plugins/ec2/PluginImpl.java | 2 +- .../hudson/plugins/ec2/SlaveTemplate.java | 3 +- .../hudson/plugins/ec2/SpotConfiguration.java | 4 +- .../config-entries.jelly | 0 .../ec2/EC2Cloud/help-ec2EndpointUrl.html | 33 -- .../help-noDelayProvisioning.html | 0 .../help-region.html | 0 .../ec2/EC2Cloud/help-s3EndpointUrl.html | 33 -- .../ec2/Eucalyptus/config-entries.jelly | 46 -- .../plugins/ec2/Eucalyptus/help-url.html | 27 -- .../hudson/plugins/ec2/Messages.properties | 4 +- .../plugins/ec2/AmazonEC2CloudTest.java | 213 -------- .../plugins/ec2/AmazonEC2CloudUnitTest.java | 155 ------ .../hudson/plugins/ec2/CloudHelperTest.java | 4 +- .../plugins/ec2/ConfigurationAsCodeTest.java | 18 +- .../plugins/ec2/EC2AbstractSlaveTest.java | 3 +- .../java/hudson/plugins/ec2/EC2CloudTest.java | 453 +++++++----------- .../hudson/plugins/ec2/EC2CloudUnitTest.java | 453 ++++++++++++++++++ .../plugins/ec2/EC2RetentionStrategyTest.java | 10 +- .../plugins/ec2/EC2SlaveMonitorTest.java | 4 +- .../java/hudson/plugins/ec2/EC2StepTest.java | 2 +- .../hudson/plugins/ec2/EucalyptusTest.java | 39 -- .../plugins/ec2/FileBasedSSHKeyTest.java | 4 +- .../hudson/plugins/ec2/SlaveTemplateTest.java | 47 +- .../plugins/ec2/TemplateLabelsTest.java | 4 +- .../ec2/util/AmazonEC2FactoryMockImpl.java | 3 +- 33 files changed, 908 insertions(+), 1327 deletions(-) delete mode 100644 src/main/java/hudson/plugins/ec2/Eucalyptus.java rename src/main/resources/hudson/plugins/ec2/{AmazonEC2Cloud => EC2Cloud}/config-entries.jelly (100%) delete mode 100644 src/main/resources/hudson/plugins/ec2/EC2Cloud/help-ec2EndpointUrl.html rename src/main/resources/hudson/plugins/ec2/{AmazonEC2Cloud => EC2Cloud}/help-noDelayProvisioning.html (100%) rename src/main/resources/hudson/plugins/ec2/{AmazonEC2Cloud => EC2Cloud}/help-region.html (100%) delete mode 100644 src/main/resources/hudson/plugins/ec2/EC2Cloud/help-s3EndpointUrl.html delete mode 100644 src/main/resources/hudson/plugins/ec2/Eucalyptus/config-entries.jelly delete mode 100644 src/main/resources/hudson/plugins/ec2/Eucalyptus/help-url.html delete mode 100644 src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java delete mode 100644 src/test/java/hudson/plugins/ec2/AmazonEC2CloudUnitTest.java create mode 100644 src/test/java/hudson/plugins/ec2/EC2CloudUnitTest.java delete mode 100644 src/test/java/hudson/plugins/ec2/EucalyptusTest.java diff --git a/README.md b/README.md index a9492c135..b461c8638 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,7 @@ # Introduction Allow Jenkins to start agents on -[EC2](http://aws.amazon.com/ec2/) or -[Eucalyptus](https://www.eucalyptus.cloud/) on demand, and +[EC2](http://aws.amazon.com/ec2/) on demand, and kill them as they get unused. With this plugin, if Jenkins notices that your build cluster is @@ -299,7 +298,7 @@ import com.cloudbees.jenkins.plugins.awscredentials.AWSCredentialsImpl import com.cloudbees.plugins.credentials.* import com.cloudbees.plugins.credentials.domains.Domain import hudson.model.* -import hudson.plugins.ec2.AmazonEC2Cloud +import hudson.plugins.ec2.EC2Cloud import hudson.plugins.ec2.AMITypeData import hudson.plugins.ec2.EC2Tag import hudson.plugins.ec2.SlaveTemplate @@ -363,7 +362,7 @@ def slaveTemplateUsEast1Parameters = [ nodeProperties: null ] -def AmazonEC2CloudParameters = [ +def EC2CloudParameters = [ name: 'MyCompany', credentialsId: 'jenkins-aws-key', instanceCapStr: '2', @@ -462,14 +461,14 @@ SlaveTemplate slaveTemplateUsEast1 = new SlaveTemplate( slaveTemplateUsEast1Parameters.metadataHopsLimit, ) -// https://javadoc.jenkins.io/plugin/ec2/index.html?hudson/plugins/ec2/AmazonEC2Cloud.html -AmazonEC2Cloud amazonEC2Cloud = new AmazonEC2Cloud( - AmazonEC2CloudParameters.name, - AmazonEC2CloudParameters.useInstanceProfileForCredentials, - AmazonEC2CloudParameters.credentialsId, - AmazonEC2CloudParameters.region, - AmazonEC2CloudParameters.privateKey, - AmazonEC2CloudParameters.instanceCapStr, +// https://javadoc.jenkins.io/plugin/ec2/hudson/plugins/ec2/EC2Cloud.html +EC2Cloud ec2Cloud = new EC2Cloud( + EC2CloudParameters.name, + EC2CloudParameters.useInstanceProfileForCredentials, + EC2CloudParameters.credentialsId, + EC2CloudParameters.region, + EC2CloudParameters.privateKey, + EC2CloudParameters.instanceCapStr, [slaveTemplateUsEast1], '', '' @@ -488,7 +487,7 @@ def store = jenkins.getExtensionList('com.cloudbees.plugins.credentials.SystemCr store.addCredentials(domain, aWSCredentialsImpl) // add cloud configuration to Jenkins -jenkins.clouds.add(amazonEC2Cloud) +jenkins.clouds.add(ec2Cloud) // save current Jenkins state to disk jenkins.save() @@ -504,7 +503,7 @@ Example: ```java // Assuming on the Jenkins instance, there exists an EC2Cloud with the name "AwsCloud" - AmazonEC2Cloud cloud = (AmazonEC2Cloud) Jenkins.get().clouds.stream().filter(cloud1 -> Objects.equals(cloud.getDisplayName(), "AwsCloud")).findFirst().get(); + EC2Cloud cloud = (EC2Cloud) Jenkins.get().clouds.stream().filter(cloud1 -> Objects.equals(cloud.getDisplayName(), "AwsCloud")).findFirst().get(); SlaveTemplate template = new SlaveTemplate(/*constructor*/); // View available constructors at https://github.com/jenkinsci/ec2-plugin/blob/master/src/main/java/hudson/plugins/ec2/SlaveTemplate.java diff --git a/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java b/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java index 950fc104b..31083accd 100644 --- a/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java @@ -23,53 +23,13 @@ */ package hudson.plugins.ec2; -import com.amazonaws.SdkClientException; -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.model.DescribeRegionsResult; -import com.amazonaws.services.ec2.model.Region; -import edu.umd.cs.findbugs.annotations.Nullable; -import hudson.Extension; -import hudson.Util; -import hudson.model.Failure; -import hudson.model.ItemGroup; -import hudson.plugins.ec2.util.AmazonEC2Factory; -import hudson.util.FormValidation; -import hudson.util.ListBoxModel; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; import java.util.List; -import java.util.Locale; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.servlet.ServletException; -import jenkins.model.Jenkins; -import org.kohsuke.stapler.AncestorInPath; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.DataBoundSetter; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.interceptor.RequirePOST; -import org.kohsuke.stapler.verb.POST; /** - * The original implementation of {@link EC2Cloud}. - * - * @author Kohsuke Kawaguchi + * @deprecated use {@link EC2Cloud} */ +@Deprecated public class AmazonEC2Cloud extends EC2Cloud { - private static final Logger LOGGER = Logger.getLogger(AmazonEC2Cloud.class.getName()); - - /** - * Represents the region. Can be null for backward compatibility reasons. - */ - private String region; - - private String altEC2Endpoint; - - private boolean noDelayProvisioning; - - @DataBoundConstructor public AmazonEC2Cloud( String name, boolean useInstanceProfileForCredentials, @@ -85,16 +45,15 @@ public AmazonEC2Cloud( name, useInstanceProfileForCredentials, credentialsId, + region, privateKey, sshKeysCredentialsId, instanceCapStr, templates, roleArn, roleSessionName); - this.region = region; } - @Deprecated public AmazonEC2Cloud( String name, boolean useInstanceProfileForCredentials, @@ -109,185 +68,11 @@ public AmazonEC2Cloud( name, useInstanceProfileForCredentials, credentialsId, + region, privateKey, instanceCapStr, templates, roleArn, roleSessionName); - this.region = region; - } - - /** - * @deprecated Use public field "name" instead. - */ - @Deprecated - public String getCloudName() { - return name; - } - - public String getRegion() { - if (region == null) { - region = DEFAULT_EC2_HOST; // Backward compatibility - } - // Handles pre 1.14 region names that used the old AwsRegion enum, note we don't change - // the region here to keep the meta-data compatible in the case of a downgrade (is that right?) - if (region.indexOf('_') > 0) { - return region.replace('_', '-').toLowerCase(Locale.ENGLISH); - } - return region; - } - - public static URL getEc2EndpointUrl(String region) { - try { - return new URL("https://" + getAwsPartitionHostForService(region, "ec2")); - } catch (MalformedURLException e) { - throw new Error(e); // Impossible - } - } - - @Override - public URL getEc2EndpointUrl() { - return getEc2EndpointUrl(getRegion()); - } - - @Override - public URL getS3EndpointUrl() { - try { - return new URL("https://" + getAwsPartitionHostForService(getRegion(), "s3") + "/"); - } catch (MalformedURLException e) { - throw new Error(e); // Impossible - } - } - - public boolean isNoDelayProvisioning() { - return noDelayProvisioning; - } - - @DataBoundSetter - public void setNoDelayProvisioning(boolean noDelayProvisioning) { - this.noDelayProvisioning = noDelayProvisioning; - } - - public String getAltEC2Endpoint() { - return altEC2Endpoint; - } - - @DataBoundSetter - public void setAltEC2Endpoint(String altEC2Endpoint) { - this.altEC2Endpoint = altEC2Endpoint; - } - - @Override - protected AWSCredentialsProvider createCredentialsProvider() { - return createCredentialsProvider( - isUseInstanceProfileForCredentials(), - getCredentialsId(), - getRoleArn(), - getRoleSessionName(), - getRegion()); - } - - @Extension - public static class DescriptorImpl extends EC2Cloud.DescriptorImpl { - - @Override - public String getDisplayName() { - return "Amazon EC2"; - } - - @POST - public FormValidation doCheckCloudName(@QueryParameter String value) { - if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { - return FormValidation.ok(); - } - try { - Jenkins.checkGoodName(value); - } catch (Failure e) { - return FormValidation.error(e.getMessage()); - } - return FormValidation.ok(); - } - - @POST - public FormValidation doCheckAltEC2Endpoint(@QueryParameter String value) { - if (Util.fixEmpty(value) != null) { - try { - new URL(value); - } catch (MalformedURLException ignored) { - return FormValidation.error(Messages.AmazonEC2Cloud_MalformedUrl()); - } - } - return FormValidation.ok(); - } - - @RequirePOST - public ListBoxModel doFillRegionItems( - @QueryParameter String altEC2Endpoint, - @QueryParameter boolean useInstanceProfileForCredentials, - @QueryParameter String credentialsId) - throws IOException, ServletException { - ListBoxModel model = new ListBoxModel(); - if (Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { - try { - AWSCredentialsProvider credentialsProvider = - createCredentialsProvider(useInstanceProfileForCredentials, credentialsId); - AmazonEC2 client = AmazonEC2Factory.getInstance() - .connect(credentialsProvider, determineEC2EndpointURL(altEC2Endpoint)); - DescribeRegionsResult regions = client.describeRegions(); - List regionList = regions.getRegions(); - for (Region r : regionList) { - String name = r.getRegionName(); - model.add(name, name); - } - } catch (SdkClientException ex) { - // Ignore, as this may happen before the credentials are specified - } - } - return model; - } - - // Will use the alternate EC2 endpoint if provided by the UI (via a @QueryParameter field), or use the default - // value if not specified. - // VisibleForTesting - URL determineEC2EndpointURL(@Nullable String altEC2Endpoint) throws MalformedURLException { - if (Util.fixEmpty(altEC2Endpoint) == null) { - return new URL(DEFAULT_EC2_ENDPOINT); - } - try { - return new URL(altEC2Endpoint); - } catch (MalformedURLException e) { - LOGGER.log( - Level.WARNING, - "The alternate EC2 endpoint is malformed ({0}). Using the default endpoint ({1})", - new Object[] {altEC2Endpoint, DEFAULT_EC2_ENDPOINT}); - return new URL(DEFAULT_EC2_ENDPOINT); - } - } - - @RequirePOST - public FormValidation doTestConnection( - @AncestorInPath ItemGroup context, - @QueryParameter String region, - @QueryParameter boolean useInstanceProfileForCredentials, - @QueryParameter String credentialsId, - @QueryParameter String sshKeysCredentialsId, - @QueryParameter String roleArn, - @QueryParameter String roleSessionName) - throws IOException, ServletException { - - if (Util.fixEmpty(region) == null) { - region = DEFAULT_EC2_HOST; - } - - return super.doTestConnection( - context, - getEc2EndpointUrl(region), - useInstanceProfileForCredentials, - credentialsId, - sshKeysCredentialsId, - roleArn, - roleSessionName, - region); - } } } diff --git a/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java b/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java index 618f6d65b..45dc4d9f9 100644 --- a/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java +++ b/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java @@ -1041,8 +1041,8 @@ public static ListBoxModel fillZoneItems(AWSCredentialsProvider credentialsProvi ListBoxModel model = new ListBoxModel(); if (!StringUtils.isEmpty(region)) { - AmazonEC2 client = AmazonEC2Factory.getInstance() - .connect(credentialsProvider, AmazonEC2Cloud.getEc2EndpointUrl(region)); + AmazonEC2 client = + AmazonEC2Factory.getInstance().connect(credentialsProvider, EC2Cloud.getEc2EndpointUrl(region)); DescribeAvailabilityZonesResult zones = client.describeAvailabilityZones(); List zoneList = zones.getAvailabilityZones(); model.add("", ""); diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index b929a6637..e0163c61c 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -21,6 +21,7 @@ import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.ClientConfiguration; +import com.amazonaws.SdkClientException; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.AWSStaticCredentialsProvider; @@ -30,6 +31,7 @@ import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.model.DescribeInstancesRequest; import com.amazonaws.services.ec2.model.DescribeInstancesResult; +import com.amazonaws.services.ec2.model.DescribeRegionsResult; import com.amazonaws.services.ec2.model.DescribeSpotInstanceRequestsRequest; import com.amazonaws.services.ec2.model.DescribeSpotInstanceRequestsResult; import com.amazonaws.services.ec2.model.Filter; @@ -38,6 +40,7 @@ import com.amazonaws.services.ec2.model.InstanceType; import com.amazonaws.services.ec2.model.KeyPair; import com.amazonaws.services.ec2.model.KeyPairInfo; +import com.amazonaws.services.ec2.model.Region; import com.amazonaws.services.ec2.model.Reservation; import com.amazonaws.services.ec2.model.SpotInstanceRequest; import com.amazonaws.services.ec2.model.Tag; @@ -61,11 +64,13 @@ import com.google.common.annotations.VisibleForTesting; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; import hudson.ProxyConfiguration; import hudson.Util; import hudson.model.Computer; import hudson.model.Descriptor; +import hudson.model.Failure; import hudson.model.ItemGroup; import hudson.model.Label; import hudson.model.Node; @@ -96,6 +101,7 @@ import java.util.EnumSet; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -112,9 +118,12 @@ import jenkins.model.Jenkins; import jenkins.model.JenkinsLocationConfiguration; import org.apache.commons.lang.StringUtils; +import org.jenkinsci.Symbol; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; @@ -127,7 +136,7 @@ * * @author Kohsuke Kawaguchi */ -public abstract class EC2Cloud extends Cloud { +public class EC2Cloud extends Cloud { private static final Logger LOGGER = Logger.getLogger(EC2Cloud.class.getName()); @@ -189,12 +198,23 @@ public abstract class EC2Cloud extends Cloud { private transient KeyPair usableKeyPair; + /** + * Represents the region. Can be null for backward compatibility reasons. + */ + private String region; + + private String altEC2Endpoint; + + private boolean noDelayProvisioning; + private transient volatile AmazonEC2 connection; - protected EC2Cloud( + @DataBoundConstructor + public EC2Cloud( String name, boolean useInstanceProfileForCredentials, String credentialsId, + String region, String privateKey, String sshKeysCredentialsId, String instanceCapStr, @@ -206,6 +226,7 @@ protected EC2Cloud( this.roleArn = roleArn; this.roleSessionName = roleSessionName; this.credentialsId = Util.fixEmpty(credentialsId); + this.region = Util.fixEmpty(region); this.sshKeysCredentialsId = Util.fixEmpty(sshKeysCredentialsId); if (templates == null) { @@ -223,6 +244,30 @@ protected EC2Cloud( readResolve(); // set parents } + @Deprecated + public EC2Cloud( + String name, + boolean useInstanceProfileForCredentials, + String credentialsId, + String region, + String privateKey, + String instanceCapStr, + List templates, + String roleArn, + String roleSessionName) { + this( + name, + useInstanceProfileForCredentials, + credentialsId, + region, + privateKey, + null, + instanceCapStr, + templates, + roleArn, + roleSessionName); + } + @Deprecated protected EC2Cloud( String id, @@ -239,6 +284,7 @@ protected EC2Cloud( credentialsId, privateKey, null, + null, instanceCapStr, templates, roleArn, @@ -260,9 +306,63 @@ public EC2PrivateKey resolvePrivateKey() { return null; } - public abstract URL getEc2EndpointUrl() throws IOException; + /** + * @deprecated Use public field "name" instead. + */ + @Deprecated + public String getCloudName() { + return name; + } + + public String getRegion() { + if (region == null) { + region = DEFAULT_EC2_HOST; // Backward compatibility + } + // Handles pre 1.14 region names that used the old AwsRegion enum, note we don't change + // the region here to keep the meta-data compatible in the case of a downgrade (is that right?) + if (region.indexOf('_') > 0) { + return region.replace('_', '-').toLowerCase(Locale.ENGLISH); + } + return region; + } + + public static URL getEc2EndpointUrl(String region) { + try { + return new URL("https://" + getAwsPartitionHostForService(region, "ec2")); + } catch (MalformedURLException e) { + throw new Error(e); // Impossible + } + } + + public URL getEc2EndpointUrl() { + return getEc2EndpointUrl(getRegion()); + } + + public URL getS3EndpointUrl() { + try { + return new URL("https://" + getAwsPartitionHostForService(getRegion(), "s3") + "/"); + } catch (MalformedURLException e) { + throw new Error(e); // Impossible + } + } + + public boolean isNoDelayProvisioning() { + return noDelayProvisioning; + } + + @DataBoundSetter + public void setNoDelayProvisioning(boolean noDelayProvisioning) { + this.noDelayProvisioning = noDelayProvisioning; + } + + public String getAltEC2Endpoint() { + return altEC2Endpoint; + } - public abstract URL getS3EndpointUrl() throws IOException; + @DataBoundSetter + public void setAltEC2Endpoint(String altEC2Endpoint) { + this.altEC2Endpoint = altEC2Endpoint; + } public void addTemplate(SlaveTemplate newTemplate) throws Exception { String newTemplateDescription = newTemplate.description; @@ -636,15 +736,9 @@ private int countCurrentEC2SpotSlaves(SlaveTemplate template, String jenkinsServ DescribeSpotInstanceRequestsResult sirResp = null; do { - try { - sirResp = connect().describeSpotInstanceRequests(dsir); - sirs = sirResp.getSpotInstanceRequests(); - dsir.setNextToken(sirResp.getNextToken()); - } catch (Exception ex) { - // Some ec2 implementations don't implement spot requests (Eucalyptus) - LOGGER.log(Level.FINEST, "Describe spot instance requests failed", ex); - break; - } + sirResp = connect().describeSpotInstanceRequests(dsir); + sirs = sirResp.getSpotInstanceRequests(); + dsir.setNextToken(sirResp.getNextToken()); if (sirs != null) { for (SpotInstanceRequest sir : sirs) { @@ -1075,7 +1169,12 @@ public boolean canProvision(Label label) { } protected AWSCredentialsProvider createCredentialsProvider() { - return createCredentialsProvider(useInstanceProfileForCredentials, credentialsId); + return createCredentialsProvider( + isUseInstanceProfileForCredentials(), + getCredentialsId(), + getRoleArn(), + getRoleSessionName(), + getRegion()); } public static String getSlaveTypeTagValue(String slaveType, String templateDescription) { @@ -1254,7 +1353,14 @@ private static SSHUserPrivateKey getSshCredential(String id, ItemGroup context) return credential; } - public abstract static class DescriptorImpl extends Descriptor { + @Extension + @Symbol("amazonEC2") + public static class DescriptorImpl extends Descriptor { + + @Override + public String getDisplayName() { + return "Amazon EC2"; + } public InstanceType[] getInstanceTypes() { return InstanceType.values(); @@ -1385,27 +1491,25 @@ public FormValidation doCheckSshKeysCredentialsId( * Tests the connection settings. * * Overriding needs to {@code @RequirePOST} - * @param ec2endpoint + * @param region * @param useInstanceProfileForCredentials * @param credentialsId * @param sshKeysCredentialsId * @param roleArn * @param roleSessionName - * @param region * @return the validation result * @throws IOException * @throws ServletException */ - @POST - protected FormValidation doTestConnection( + @RequirePOST + public FormValidation doTestConnection( @AncestorInPath ItemGroup context, - URL ec2endpoint, - boolean useInstanceProfileForCredentials, - String credentialsId, - String sshKeysCredentialsId, - String roleArn, - String roleSessionName, - String region) + @QueryParameter String region, + @QueryParameter boolean useInstanceProfileForCredentials, + @QueryParameter String credentialsId, + @QueryParameter String sshKeysCredentialsId, + @QueryParameter String roleArn, + @QueryParameter String roleSessionName) throws IOException, ServletException { if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { return FormValidation.ok(); @@ -1439,9 +1543,13 @@ protected FormValidation doTestConnection( } LOGGER.fine(() -> "private key found ok"); + if (Util.fixEmpty(region) == null) { + region = DEFAULT_EC2_HOST; + } + AWSCredentialsProvider credentialsProvider = createCredentialsProvider( useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); - AmazonEC2 ec2 = AmazonEC2Factory.getInstance().connect(credentialsProvider, ec2endpoint); + AmazonEC2 ec2 = AmazonEC2Factory.getInstance().connect(credentialsProvider, getEc2EndpointUrl(region)); ec2.describeInstances(); if (!privateKey.trim().isEmpty()) { @@ -1484,6 +1592,75 @@ public ListBoxModel doFillCredentialsIdItems(@AncestorInPath ItemGroup context) Collections.emptyList(), CredentialsMatchers.always()); } + + @POST + public FormValidation doCheckCloudName(@QueryParameter String value) { + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + return FormValidation.ok(); + } + try { + Jenkins.checkGoodName(value); + } catch (Failure e) { + return FormValidation.error(e.getMessage()); + } + return FormValidation.ok(); + } + + @POST + public FormValidation doCheckAltEC2Endpoint(@QueryParameter String value) { + if (Util.fixEmpty(value) != null) { + try { + new URL(value); + } catch (MalformedURLException ignored) { + return FormValidation.error(Messages.EC2Cloud_MalformedUrl()); + } + } + return FormValidation.ok(); + } + + @RequirePOST + public ListBoxModel doFillRegionItems( + @QueryParameter String altEC2Endpoint, + @QueryParameter boolean useInstanceProfileForCredentials, + @QueryParameter String credentialsId) + throws IOException, ServletException { + ListBoxModel model = new ListBoxModel(); + if (Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + try { + AWSCredentialsProvider credentialsProvider = + createCredentialsProvider(useInstanceProfileForCredentials, credentialsId); + AmazonEC2 client = AmazonEC2Factory.getInstance() + .connect(credentialsProvider, determineEC2EndpointURL(altEC2Endpoint)); + DescribeRegionsResult regions = client.describeRegions(); + List regionList = regions.getRegions(); + for (Region r : regionList) { + String name = r.getRegionName(); + model.add(name, name); + } + } catch (SdkClientException ex) { + // Ignore, as this may happen before the credentials are specified + } + } + return model; + } + + // Will use the alternate EC2 endpoint if provided by the UI (via a @QueryParameter field), or use the default + // value if not specified. + // VisibleForTesting + URL determineEC2EndpointURL(@Nullable String altEC2Endpoint) throws MalformedURLException { + if (Util.fixEmpty(altEC2Endpoint) == null) { + return new URL(DEFAULT_EC2_ENDPOINT); + } + try { + return new URL(altEC2Endpoint); + } catch (MalformedURLException e) { + LOGGER.log( + Level.WARNING, + "The alternate EC2 endpoint is malformed ({0}). Using the default endpoint ({1})", + new Object[] {altEC2Endpoint, DEFAULT_EC2_ENDPOINT}); + return new URL(DEFAULT_EC2_ENDPOINT); + } + } } public static void log(Logger logger, Level level, TaskListener listener, String message) { diff --git a/src/main/java/hudson/plugins/ec2/EC2Step.java b/src/main/java/hudson/plugins/ec2/EC2Step.java index 57135242c..86580abe9 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Step.java +++ b/src/main/java/hudson/plugins/ec2/EC2Step.java @@ -99,10 +99,7 @@ public ListBoxModel doFillCloudItems() { Jenkins.get().checkPermission(Jenkins.SYSTEM_READ); ListBoxModel r = new ListBoxModel(); r.add("", ""); - Jenkins.get() - .clouds - .getAll(AmazonEC2Cloud.class) - .forEach(c -> r.add(c.getDisplayName(), c.getDisplayName())); + Jenkins.get().clouds.getAll(EC2Cloud.class).forEach(c -> r.add(c.getDisplayName(), c.getDisplayName())); return r; } @@ -111,8 +108,8 @@ public ListBoxModel doFillTemplateItems(@QueryParameter String cloudName) { Jenkins.get().checkPermission(Jenkins.SYSTEM_READ); ListBoxModel r = new ListBoxModel(); Cloud cloud = Jenkins.get().getCloud(Util.fixEmpty(cloudName)); - if (cloud instanceof AmazonEC2Cloud) { - AmazonEC2Cloud ec2Cloud = (AmazonEC2Cloud) cloud; + if (cloud instanceof EC2Cloud) { + EC2Cloud ec2Cloud = (EC2Cloud) cloud; for (SlaveTemplate template : ec2Cloud.getTemplates()) { for (String labelList : template.labels.split(" ")) { r.add( @@ -144,9 +141,9 @@ public static class Execution extends SynchronousNonBlockingStepExecution opt = EnumSet.noneOf(SlaveTemplate.ProvisionOptions.class); @@ -159,7 +156,7 @@ protected Instance run() throws Exception { } EC2AbstractSlave slave = instances.get(0); - return CloudHelper.getInstanceWithRetry(slave.getInstanceId(), (AmazonEC2Cloud) cl); + return CloudHelper.getInstanceWithRetry(slave.getInstanceId(), (EC2Cloud) cl); } else { throw new IllegalArgumentException( "Error in AWS Cloud. Please review AWS template defined in Jenkins configuration."); diff --git a/src/main/java/hudson/plugins/ec2/Eucalyptus.java b/src/main/java/hudson/plugins/ec2/Eucalyptus.java deleted file mode 100644 index 788f4e5f0..000000000 --- a/src/main/java/hudson/plugins/ec2/Eucalyptus.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.plugins.ec2; - -import hudson.Extension; -import hudson.model.ItemGroup; -import hudson.util.FormValidation; -import java.io.IOException; -import java.net.URL; -import java.util.List; -import javax.servlet.ServletException; -import org.kohsuke.stapler.AncestorInPath; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.interceptor.RequirePOST; - -/** - * Eucalyptus. - * - * @author Kohsuke Kawaguchi - */ -public class Eucalyptus extends EC2Cloud { - private final URL ec2endpoint; - private final URL s3endpoint; - - @DataBoundConstructor - public Eucalyptus( - String name, - URL ec2EndpointUrl, - URL s3EndpointUrl, - boolean useInstanceProfileForCredentials, - String credentialsId, - String privateKey, - String sshKeysCredentialsId, - String instanceCapStr, - List templates, - String roleArn, - String roleSessionName) { - super( - name, - useInstanceProfileForCredentials, - credentialsId, - privateKey, - sshKeysCredentialsId, - instanceCapStr, - templates, - roleArn, - roleSessionName); - this.ec2endpoint = ec2EndpointUrl; - this.s3endpoint = s3EndpointUrl; - } - - @Deprecated - public Eucalyptus( - URL ec2EndpointUrl, - URL s3EndpointUrl, - boolean useInstanceProfileForCredentials, - String credentialsId, - String privateKey, - String sshKeysCredentialsId, - String instanceCapStr, - List templates, - String roleArn, - String roleSessionName) - throws IOException { - this( - "eucalyptus", - ec2EndpointUrl, - s3EndpointUrl, - useInstanceProfileForCredentials, - credentialsId, - privateKey, - sshKeysCredentialsId, - instanceCapStr, - templates, - roleArn, - roleSessionName); - } - - @Deprecated - public Eucalyptus( - URL ec2EndpointUrl, - URL s3EndpointUrl, - boolean useInstanceProfileForCredentials, - String credentialsId, - String privateKey, - String instanceCapStr, - List templates, - String roleArn, - String roleSessionName) - throws IOException { - this( - "eucalyptus", - ec2EndpointUrl, - s3EndpointUrl, - useInstanceProfileForCredentials, - credentialsId, - privateKey, - null, - instanceCapStr, - templates, - roleArn, - roleSessionName); - } - - @Override - public URL getEc2EndpointUrl() throws IOException { - return this.ec2endpoint; - } - - @Override - public URL getS3EndpointUrl() throws IOException { - return this.s3endpoint; - } - - @Extension - public static class DescriptorImpl extends EC2Cloud.DescriptorImpl { - @Override - public String getDisplayName() { - return "Eucalyptus"; - } - - @Override - @RequirePOST - public FormValidation doTestConnection( - @AncestorInPath ItemGroup context, - @QueryParameter URL ec2endpoint, - @QueryParameter boolean useInstanceProfileForCredentials, - @QueryParameter String credentialsId, - @QueryParameter String sshKeysCredentialsId, - @QueryParameter String roleArn, - @QueryParameter String roleSessionName, - @QueryParameter String region) - throws IOException, ServletException { - return super.doTestConnection( - context, - ec2endpoint, - useInstanceProfileForCredentials, - credentialsId, - sshKeysCredentialsId, - roleArn, - roleSessionName, - region); - } - } -} diff --git a/src/main/java/hudson/plugins/ec2/NoDelayProvisionerStrategy.java b/src/main/java/hudson/plugins/ec2/NoDelayProvisionerStrategy.java index 056ba5ab3..7fab52c01 100644 --- a/src/main/java/hudson/plugins/ec2/NoDelayProvisionerStrategy.java +++ b/src/main/java/hudson/plugins/ec2/NoDelayProvisionerStrategy.java @@ -39,13 +39,13 @@ public NodeProvisioner.StrategyDecision apply(NodeProvisioner.StrategyState stra if (availableCapacity < currentDemand) { Jenkins jenkinsInstance = Jenkins.get(); for (Cloud cloud : jenkinsInstance.clouds) { - if (!(cloud instanceof AmazonEC2Cloud)) { + if (!(cloud instanceof EC2Cloud)) { continue; } if (!cloud.canProvision(label)) { continue; } - AmazonEC2Cloud ec2 = (AmazonEC2Cloud) cloud; + EC2Cloud ec2 = (EC2Cloud) cloud; if (!ec2.isNoDelayProvisioning()) { continue; } diff --git a/src/main/java/hudson/plugins/ec2/PluginImpl.java b/src/main/java/hudson/plugins/ec2/PluginImpl.java index 6f4dc0928..666046144 100644 --- a/src/main/java/hudson/plugins/ec2/PluginImpl.java +++ b/src/main/java/hudson/plugins/ec2/PluginImpl.java @@ -77,7 +77,7 @@ public String getDisplayName() { @Override public void postInitialize() throws IOException { // backward compatibility with the legacy class name - Jenkins.XSTREAM.alias("hudson.plugins.ec2.EC2Cloud", AmazonEC2Cloud.class); + Jenkins.XSTREAM.alias("hudson.plugins.ec2.EC2Cloud", AmazonEC2Cloud.class, EC2Cloud.class); Jenkins.XSTREAM.alias("hudson.plugins.ec2.EC2Slave", EC2OndemandSlave.class); // backward compatibility with the legacy instance type Jenkins.XSTREAM.registerConverter(new InstanceTypeConverter()); diff --git a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java index 859d2065d..adaa017f9 100644 --- a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java +++ b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java @@ -2841,8 +2841,7 @@ public FormValidation doValidateAmi( useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); AmazonEC2 ec2; if (region != null) { - ec2 = AmazonEC2Factory.getInstance() - .connect(credentialsProvider, AmazonEC2Cloud.getEc2EndpointUrl(region)); + ec2 = AmazonEC2Factory.getInstance().connect(credentialsProvider, EC2Cloud.getEc2EndpointUrl(region)); } else { ec2 = AmazonEC2Factory.getInstance().connect(credentialsProvider, new URL(ec2endpoint)); } diff --git a/src/main/java/hudson/plugins/ec2/SpotConfiguration.java b/src/main/java/hudson/plugins/ec2/SpotConfiguration.java index 577a55370..50328ce66 100644 --- a/src/main/java/hudson/plugins/ec2/SpotConfiguration.java +++ b/src/main/java/hudson/plugins/ec2/SpotConfiguration.java @@ -158,8 +158,8 @@ public FormValidation doCurrentSpotPrice( // region queried from the created cloud AWSCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider( useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); - AmazonEC2 ec2 = AmazonEC2Factory.getInstance() - .connect(credentialsProvider, AmazonEC2Cloud.getEc2EndpointUrl(region)); + AmazonEC2 ec2 = + AmazonEC2Factory.getInstance().connect(credentialsProvider, EC2Cloud.getEc2EndpointUrl(region)); if (ec2 != null) { diff --git a/src/main/resources/hudson/plugins/ec2/AmazonEC2Cloud/config-entries.jelly b/src/main/resources/hudson/plugins/ec2/EC2Cloud/config-entries.jelly similarity index 100% rename from src/main/resources/hudson/plugins/ec2/AmazonEC2Cloud/config-entries.jelly rename to src/main/resources/hudson/plugins/ec2/EC2Cloud/config-entries.jelly diff --git a/src/main/resources/hudson/plugins/ec2/EC2Cloud/help-ec2EndpointUrl.html b/src/main/resources/hudson/plugins/ec2/EC2Cloud/help-ec2EndpointUrl.html deleted file mode 100644 index 656704393..000000000 --- a/src/main/resources/hudson/plugins/ec2/EC2Cloud/help-ec2EndpointUrl.html +++ /dev/null @@ -1,33 +0,0 @@ - -
- This field is optional. It controls what EC2 API endpoint to connect to. - By default https://ec2.us-east-1.amazonaws.com/ is used. To use other - Amazon AWS zones, change this appropriately. - - When using Ubuntu Enterprise Cloud (Eucalyptus) a url like - http://cluster-controler:8773/services/Eucalyptus is needed. - - For other EC2 API-compatible clouds please see your product documentation. -
diff --git a/src/main/resources/hudson/plugins/ec2/AmazonEC2Cloud/help-noDelayProvisioning.html b/src/main/resources/hudson/plugins/ec2/EC2Cloud/help-noDelayProvisioning.html similarity index 100% rename from src/main/resources/hudson/plugins/ec2/AmazonEC2Cloud/help-noDelayProvisioning.html rename to src/main/resources/hudson/plugins/ec2/EC2Cloud/help-noDelayProvisioning.html diff --git a/src/main/resources/hudson/plugins/ec2/AmazonEC2Cloud/help-region.html b/src/main/resources/hudson/plugins/ec2/EC2Cloud/help-region.html similarity index 100% rename from src/main/resources/hudson/plugins/ec2/AmazonEC2Cloud/help-region.html rename to src/main/resources/hudson/plugins/ec2/EC2Cloud/help-region.html diff --git a/src/main/resources/hudson/plugins/ec2/EC2Cloud/help-s3EndpointUrl.html b/src/main/resources/hudson/plugins/ec2/EC2Cloud/help-s3EndpointUrl.html deleted file mode 100644 index ebc8732f7..000000000 --- a/src/main/resources/hudson/plugins/ec2/EC2Cloud/help-s3EndpointUrl.html +++ /dev/null @@ -1,33 +0,0 @@ - -
- This field is optional. It controls what S3 API endpoint to connect to. - By default https://s3.amazonaws.com/ is used (note that this works with all - amazonaws zones - s3 transparently redirects between zones). - - When using Ubuntu Enterprise Cloud (Eucalyptus) the default URL should be - something like http://cluster-controller-ip:8773/services/Walrus. - - For other EC2/S3 compatible servers please see your product documentation. -
diff --git a/src/main/resources/hudson/plugins/ec2/Eucalyptus/config-entries.jelly b/src/main/resources/hudson/plugins/ec2/Eucalyptus/config-entries.jelly deleted file mode 100644 index 7036fd28b..000000000 --- a/src/main/resources/hudson/plugins/ec2/Eucalyptus/config-entries.jelly +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/hudson/plugins/ec2/Eucalyptus/help-url.html b/src/main/resources/hudson/plugins/ec2/Eucalyptus/help-url.html deleted file mode 100644 index f27813798..000000000 --- a/src/main/resources/hudson/plugins/ec2/Eucalyptus/help-url.html +++ /dev/null @@ -1,27 +0,0 @@ - -
- Specify the HTTPS URL of the Eucalyptus web UI. - Normally, this is something like https://yourmachine:8443/ -
diff --git a/src/main/resources/hudson/plugins/ec2/Messages.properties b/src/main/resources/hudson/plugins/ec2/Messages.properties index 0152a90c3..b0842f148 100644 --- a/src/main/resources/hudson/plugins/ec2/Messages.properties +++ b/src/main/resources/hudson/plugins/ec2/Messages.properties @@ -8,6 +8,6 @@ EC2SpotSlave.AmazonEC2SpotInstance=Amazon EC2 Spot Instance EC2SpotSlave.Spot1=Spot $ EC2SpotSlave.Spot2= max bid price -AmazonEC2Cloud.NonUniqName=Cloud name must be unique across EC2 clouds -AmazonEC2Cloud.MalformedUrl=The URL is malformed. The default endpoint will be used +EC2Cloud.NonUniqName=Cloud name must be unique across EC2 clouds +EC2Cloud.MalformedUrl=The URL is malformed. The default endpoint will be used General.MissingPermission=You do not have the Overall/Administer right to modify this field diff --git a/src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java b/src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java deleted file mode 100644 index 9defc3ee2..000000000 --- a/src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.plugins.ec2; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -import com.amazonaws.services.ec2.AmazonEC2; -import com.cloudbees.jenkins.plugins.awscredentials.AWSCredentialsImpl; -import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; -import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey; -import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.cloudbees.plugins.credentials.CredentialsScope; -import com.cloudbees.plugins.credentials.CredentialsStore; -import com.cloudbees.plugins.credentials.SystemCredentialsProvider; -import com.cloudbees.plugins.credentials.domains.Domain; -import hudson.plugins.ec2.util.TestSSHUserPrivateKey; -import hudson.util.FormValidation; -import hudson.util.ListBoxModel; -import java.io.IOException; -import java.util.Collections; -import jenkins.model.Jenkins; -import org.htmlunit.html.HtmlForm; -import org.htmlunit.html.HtmlTextInput; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.jvnet.hudson.test.Issue; -import org.jvnet.hudson.test.JenkinsRule; -import org.mockito.Mockito; -import org.xml.sax.SAXException; - -/** - * @author Kohsuke Kawaguchi - */ -public class AmazonEC2CloudTest { - - @Rule - public JenkinsRule r = new JenkinsRule(); - - private AmazonEC2Cloud cloud; - - @Before - public void setUp() throws Exception { - cloud = new AmazonEC2Cloud( - "us-east-1", - true, - "abc", - "us-east-1", - null, - "ghi", - "3", - Collections.emptyList(), - "roleArn", - "roleSessionName"); - r.jenkins.clouds.add(cloud); - } - - @Test - public void testConfigRoundtrip() throws Exception { - r.submit(getConfigForm()); - r.assertEqualBeans( - cloud, - r.jenkins.clouds.get(AmazonEC2Cloud.class), - "name,region,useInstanceProfileForCredentials,privateKey,instanceCap,roleArn,roleSessionName"); - } - - @Test - public void testAmazonEC2FactoryGetInstance() throws Exception { - AmazonEC2Cloud cloud = r.jenkins.clouds.get(AmazonEC2Cloud.class); - AmazonEC2 connection = cloud.connect(); - Assert.assertNotNull(connection); - Assert.assertTrue(Mockito.mockingDetails(connection).isMock()); - } - - @Test - public void testAmazonEC2FactoryWorksIfSessionNameMissing() throws Exception { - r.jenkins.clouds.replace(new AmazonEC2Cloud( - "us-east-1", true, "abc", "us-east-1", null, "ghi", "3", Collections.emptyList(), "roleArn", null)); - AmazonEC2Cloud cloud = r.jenkins.clouds.get(AmazonEC2Cloud.class); - AmazonEC2 connection = cloud.connect(); - Assert.assertNotNull(connection); - Assert.assertTrue(Mockito.mockingDetails(connection).isMock()); - } - - @Test - public void testSessionNameMissingWarning() { - AmazonEC2Cloud actual = r.jenkins.clouds.get(AmazonEC2Cloud.class); - AmazonEC2Cloud.DescriptorImpl descriptor = (AmazonEC2Cloud.DescriptorImpl) actual.getDescriptor(); - assertThat(descriptor.doCheckRoleSessionName("roleArn", "").kind, is(FormValidation.Kind.WARNING)); - assertThat(descriptor.doCheckRoleSessionName("roleArn", "roleSessionName").kind, is(FormValidation.Kind.OK)); - } - - @Test - public void testSshKeysCredentialsIdRemainsUnchangedAfterUpdatingOtherFields() throws Exception { - HtmlForm form = getConfigForm(); - HtmlTextInput input = form.getInputByName("_.roleSessionName"); - - input.setText("updatedSessionName"); - r.submit(form); - AmazonEC2Cloud actual = r.jenkins.clouds.get(AmazonEC2Cloud.class); - assertEquals("updatedSessionName", actual.getRoleSessionName()); - r.assertEqualBeans( - cloud, actual, "name,region,useInstanceProfileForCredentials,sshKeysCredentialsId,instanceCap,roleArn"); - } - - @Test - public void testAWSCredentials() throws IOException { - AmazonEC2Cloud actual = r.jenkins.clouds.get(AmazonEC2Cloud.class); - AmazonEC2Cloud.DescriptorImpl descriptor = (AmazonEC2Cloud.DescriptorImpl) actual.getDescriptor(); - assertNotNull(descriptor); - ListBoxModel m = descriptor.doFillCredentialsIdItems(Jenkins.get()); - assertThat(m.size(), is(1)); - SystemCredentialsProvider.getInstance() - .getCredentials() - .add(new AWSCredentialsImpl( - CredentialsScope.SYSTEM, "system_id", "system_ak", "system_sk", "system_desc")); - // Ensure added credential is displayed - m = descriptor.doFillCredentialsIdItems(Jenkins.get()); - assertThat(m.size(), is(2)); - SystemCredentialsProvider.getInstance() - .getCredentials() - .add(new AWSCredentialsImpl( - CredentialsScope.GLOBAL, "global_id", "global_ak", "global_sk", "global_desc")); - m = descriptor.doFillCredentialsIdItems(Jenkins.get()); - assertThat(m.size(), is(3)); - } - - @Test - public void testSshCredentials() throws IOException { - AmazonEC2Cloud actual = r.jenkins.clouds.get(AmazonEC2Cloud.class); - AmazonEC2Cloud.DescriptorImpl descriptor = (AmazonEC2Cloud.DescriptorImpl) actual.getDescriptor(); - assertNotNull(descriptor); - ListBoxModel m = descriptor.doFillSshKeysCredentialsIdItems(Jenkins.get(), ""); - assertThat(m.size(), is(1)); - BasicSSHUserPrivateKey sshKeyCredentials = new BasicSSHUserPrivateKey( - CredentialsScope.SYSTEM, - "ghi", - "key", - new BasicSSHUserPrivateKey.DirectEntryPrivateKeySource("somekey"), - "", - ""); - for (CredentialsStore credentialsStore : CredentialsProvider.lookupStores(r.jenkins)) { - if (credentialsStore instanceof SystemCredentialsProvider.StoreImpl) { - credentialsStore.addCredentials(Domain.global(), sshKeyCredentials); - } - } - // Ensure added credential is displayed - m = descriptor.doFillSshKeysCredentialsIdItems(Jenkins.get(), ""); - assertThat(m.size(), is(2)); - // Ensure that the cloud can resolve the new key - assertThat(actual.resolvePrivateKey(), notNullValue()); - } - - /** - * Ensure that EC2 plugin can use any implementation of SSHUserPrivateKey (not just the default implementation, BasicSSHUserPrivateKey). - */ - @Test - @Issue("JENKINS-63986") - public void testCustomSshCredentialTypes() throws IOException { - AmazonEC2Cloud actual = r.jenkins.clouds.get(AmazonEC2Cloud.class); - AmazonEC2Cloud.DescriptorImpl descriptor = (AmazonEC2Cloud.DescriptorImpl) actual.getDescriptor(); - assertNotNull(descriptor); - ListBoxModel m = descriptor.doFillSshKeysCredentialsIdItems(Jenkins.get(), ""); - assertThat(m.size(), is(1)); - SSHUserPrivateKey sshKeyCredentials = new TestSSHUserPrivateKey( - CredentialsScope.SYSTEM, - "ghi", - "key", - new BasicSSHUserPrivateKey.DirectEntryPrivateKeySource("somekey"), - "", - ""); - for (CredentialsStore credentialsStore : CredentialsProvider.lookupStores(r.jenkins)) { - if (credentialsStore instanceof SystemCredentialsProvider.StoreImpl) { - credentialsStore.addCredentials(Domain.global(), sshKeyCredentials); - } - } - // Ensure added credential is displayed - m = descriptor.doFillSshKeysCredentialsIdItems(Jenkins.get(), ""); - assertThat(m.size(), is(2)); - // Ensure that the cloud can resolve the new key - assertThat(actual.resolvePrivateKey(), notNullValue()); - } - - private HtmlForm getConfigForm() throws IOException, SAXException { - return r.createWebClient().goTo(cloud.getUrl() + "configure").getFormByName("config"); - } -} diff --git a/src/test/java/hudson/plugins/ec2/AmazonEC2CloudUnitTest.java b/src/test/java/hudson/plugins/ec2/AmazonEC2CloudUnitTest.java deleted file mode 100644 index fd285cc2e..000000000 --- a/src/test/java/hudson/plugins/ec2/AmazonEC2CloudUnitTest.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.plugins.ec2; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.amazonaws.services.ec2.model.Instance; -import com.amazonaws.services.ec2.model.Tag; -import hudson.plugins.ec2.util.AmazonEC2FactoryMockImpl; -import java.lang.reflect.Method; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import jenkins.model.Jenkins; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** - * Unit tests related to {@link AmazonEC2Cloud}, but do not require a Jenkins instance. - */ -@RunWith(MockitoJUnitRunner.Silent.class) -public class AmazonEC2CloudUnitTest { - - @Test - public void testEC2EndpointURLCreation() throws MalformedURLException { - AmazonEC2Cloud.DescriptorImpl descriptor = new AmazonEC2Cloud.DescriptorImpl(); - - assertEquals(new URL(EC2Cloud.DEFAULT_EC2_ENDPOINT), descriptor.determineEC2EndpointURL(null)); - assertEquals(new URL(EC2Cloud.DEFAULT_EC2_ENDPOINT), descriptor.determineEC2EndpointURL("")); - assertEquals(new URL("https://www.abc.com"), descriptor.determineEC2EndpointURL("https://www.abc.com")); - } - - @Test - public void testInstaceCap() throws Exception { - AmazonEC2Cloud cloud = new AmazonEC2Cloud( - "us-east-1", - true, - "abc", - "us-east-1", - null, - "key", - null, - Collections.emptyList(), - "roleArn", - "roleSessionName"); - assertEquals(cloud.getInstanceCap(), Integer.MAX_VALUE); - assertEquals(cloud.getInstanceCapStr(), ""); - - final int cap = 3; - final String capStr = String.valueOf(cap); - cloud = new AmazonEC2Cloud( - "us-east-1", - true, - "abc", - "us-east-1", - null, - "key", - capStr, - Collections.emptyList(), - "roleArn", - "roleSessionName"); - assertEquals(cloud.getInstanceCap(), cap); - assertEquals(cloud.getInstanceCapStr(), capStr); - } - - @Test - public void testSpotInstanceCount() throws Exception { - final int numberOfSpotInstanceRequests = 105; - AmazonEC2Cloud cloud = Mockito.spy(new AmazonEC2Cloud( - "us-east-1", - true, - "abc", - "us-east-1", - null, - "key", - null, - Collections.emptyList(), - "roleArn", - "roleSessionName")); - Jenkins jenkinsMock = mock(Jenkins.class); - EC2SpotSlave spotSlaveMock = mock(EC2SpotSlave.class); - try (MockedStatic mocked = Mockito.mockStatic(Jenkins.class)) { - mocked.when(Jenkins::get).thenReturn(jenkinsMock); - Mockito.when(jenkinsMock.getNodes()).thenReturn(Collections.singletonList(spotSlaveMock)); - when(spotSlaveMock.getSpotRequest()).thenReturn(null); - when(spotSlaveMock.getSpotInstanceRequestId()).thenReturn("sir-id"); - - List instances = new ArrayList<>(); - for (int i = 0; i <= numberOfSpotInstanceRequests; i++) { - instances.add(new Instance() - .withInstanceId("id" + i) - .withTags(new Tag().withKey("jenkins_slave_type").withValue("spot"))); - } - - AmazonEC2FactoryMockImpl.instances = instances; - - Mockito.doReturn(AmazonEC2FactoryMockImpl.createAmazonEC2Mock(null)) - .when(cloud) - .connect(); - - Method countCurrentEC2SpotSlaves = EC2Cloud.class.getDeclaredMethod( - "countCurrentEC2SpotSlaves", SlaveTemplate.class, String.class, Set.class); - countCurrentEC2SpotSlaves.setAccessible(true); - Object[] params = {null, "jenkinsurl", new HashSet()}; - int n = (int) countCurrentEC2SpotSlaves.invoke(cloud, params); - - // Should equal number of spot instance requests + 1 for spot nodes not having a spot instance request - assertEquals(numberOfSpotInstanceRequests + 1, n); - } - } - - @Test - public void testCNPartition() { - assertEquals( - EC2Cloud.getAwsPartitionHostForService("cn-northwest-1", "ec2"), "ec2.cn-northwest-1.amazonaws.com.cn"); - assertEquals( - EC2Cloud.getAwsPartitionHostForService("cn-northwest-1", "s3"), "s3.cn-northwest-1.amazonaws.com.cn"); - } - - @Test - public void testNormalPartition() { - assertEquals(EC2Cloud.getAwsPartitionHostForService("us-east-1", "ec2"), "ec2.us-east-1.amazonaws.com"); - assertEquals(EC2Cloud.getAwsPartitionHostForService("us-east-1", "s3"), "s3.us-east-1.amazonaws.com"); - } -} diff --git a/src/test/java/hudson/plugins/ec2/CloudHelperTest.java b/src/test/java/hudson/plugins/ec2/CloudHelperTest.java index 50008c4bb..b22219df1 100644 --- a/src/test/java/hudson/plugins/ec2/CloudHelperTest.java +++ b/src/test/java/hudson/plugins/ec2/CloudHelperTest.java @@ -23,11 +23,11 @@ public class CloudHelperTest { @Mock - private AmazonEC2Cloud cloud; + private EC2Cloud cloud; @Before public void init() throws Exception { - cloud = new AmazonEC2Cloud( + cloud = new EC2Cloud( "us-east-1", true, "abc", diff --git a/src/test/java/hudson/plugins/ec2/ConfigurationAsCodeTest.java b/src/test/java/hudson/plugins/ec2/ConfigurationAsCodeTest.java index 24aa6d8fa..b61381a8c 100644 --- a/src/test/java/hudson/plugins/ec2/ConfigurationAsCodeTest.java +++ b/src/test/java/hudson/plugins/ec2/ConfigurationAsCodeTest.java @@ -32,7 +32,7 @@ public class ConfigurationAsCodeTest { @Test @ConfiguredWithCode("EC2CloudEmpty.yml") public void testEmptyConfig() throws Exception { - final AmazonEC2Cloud ec2Cloud = (AmazonEC2Cloud) Jenkins.get().getCloud("empty"); + final EC2Cloud ec2Cloud = (EC2Cloud) Jenkins.get().getCloud("empty"); assertNotNull(ec2Cloud); assertEquals(0, ec2Cloud.getTemplates().size()); } @@ -40,7 +40,7 @@ public void testEmptyConfig() throws Exception { @Test @ConfiguredWithCode("UnixData.yml") public void testUnixData() throws Exception { - final AmazonEC2Cloud ec2Cloud = (AmazonEC2Cloud) Jenkins.get().getCloud("production"); + final EC2Cloud ec2Cloud = (EC2Cloud) Jenkins.get().getCloud("production"); assertNotNull(ec2Cloud); assertTrue(ec2Cloud.isUseInstanceProfileForCredentials()); @@ -77,7 +77,7 @@ public void testUnixData() throws Exception { @Test @ConfiguredWithCode("Unix.yml") public void testUnix() throws Exception { - final AmazonEC2Cloud ec2Cloud = (AmazonEC2Cloud) Jenkins.get().getCloud("staging"); + final EC2Cloud ec2Cloud = (EC2Cloud) Jenkins.get().getCloud("staging"); assertNotNull(ec2Cloud); assertTrue(ec2Cloud.isUseInstanceProfileForCredentials()); @@ -99,7 +99,7 @@ public void testUnix() throws Exception { @Test @ConfiguredWithCode("WindowsData.yml") public void testWindowsData() throws Exception { - final AmazonEC2Cloud ec2Cloud = (AmazonEC2Cloud) Jenkins.get().getCloud("development"); + final EC2Cloud ec2Cloud = (EC2Cloud) Jenkins.get().getCloud("development"); assertNotNull(ec2Cloud); assertTrue(ec2Cloud.isUseInstanceProfileForCredentials()); @@ -128,7 +128,7 @@ public void testWindowsData() throws Exception { @Test @ConfiguredWithCode("BackwardsCompatibleConnectionStrategy.yml") public void testBackwardsCompatibleConnectionStrategy() throws Exception { - final AmazonEC2Cloud ec2Cloud = (AmazonEC2Cloud) Jenkins.get().getCloud("us-east-1"); + final EC2Cloud ec2Cloud = (EC2Cloud) Jenkins.get().getCloud("us-east-1"); assertNotNull(ec2Cloud); final List templates = ec2Cloud.getTemplates(); @@ -162,7 +162,7 @@ public void testConfigAsCodeWithAltEndpointAndJavaPathExport() throws Exception @Test @ConfiguredWithCode("Unix-withMinimumInstancesTimeRange.yml") public void testConfigAsCodeWithMinimumInstancesTimeRange() throws Exception { - final AmazonEC2Cloud ec2Cloud = (AmazonEC2Cloud) Jenkins.get().getCloud("timed"); + final EC2Cloud ec2Cloud = (EC2Cloud) Jenkins.get().getCloud("timed"); assertNotNull(ec2Cloud); assertTrue(ec2Cloud.isUseInstanceProfileForCredentials()); @@ -191,7 +191,7 @@ public void testConfigAsCodeWithMinimumInstancesTimeRange() throws Exception { @Test @ConfiguredWithCode("Ami.yml") public void testAmi() throws Exception { - final AmazonEC2Cloud ec2Cloud = (AmazonEC2Cloud) Jenkins.get().getCloud("test"); + final EC2Cloud ec2Cloud = (EC2Cloud) Jenkins.get().getCloud("test"); assertNotNull(ec2Cloud); final List templates = ec2Cloud.getTemplates(); @@ -224,7 +224,7 @@ public void testAmi() throws Exception { @Test @ConfiguredWithCode("MacData.yml") public void testMacData() throws Exception { - final AmazonEC2Cloud ec2Cloud = (AmazonEC2Cloud) Jenkins.get().getCloud("production"); + final EC2Cloud ec2Cloud = (EC2Cloud) Jenkins.get().getCloud("production"); assertNotNull(ec2Cloud); assertTrue(ec2Cloud.isUseInstanceProfileForCredentials()); @@ -254,7 +254,7 @@ public void testMacData() throws Exception { @Test @ConfiguredWithCode("Mac.yml") public void testMac() throws Exception { - final AmazonEC2Cloud ec2Cloud = (AmazonEC2Cloud) Jenkins.get().getCloud("staging"); + final EC2Cloud ec2Cloud = (EC2Cloud) Jenkins.get().getCloud("staging"); assertNotNull(ec2Cloud); assertTrue(ec2Cloud.isUseInstanceProfileForCredentials()); diff --git a/src/test/java/hudson/plugins/ec2/EC2AbstractSlaveTest.java b/src/test/java/hudson/plugins/ec2/EC2AbstractSlaveTest.java index 9d37d5c4d..a4856d658 100644 --- a/src/test/java/hudson/plugins/ec2/EC2AbstractSlaveTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2AbstractSlaveTest.java @@ -117,8 +117,7 @@ public void testMaxUsesBackwardCompat() throws Exception { List templates = new ArrayList<>(); templates.add(orig); String cloudName = "us-east-1"; - AmazonEC2Cloud ac = - new AmazonEC2Cloud(cloudName, false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); + EC2Cloud ac = new EC2Cloud(cloudName, false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); EC2AbstractSlave slave = new EC2AbstractSlave( diff --git a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java index d7257f1f8..3a01e27fd 100644 --- a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java @@ -1,35 +1,73 @@ +/* + * The MIT License + * + * Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package hudson.plugins.ec2; -import static org.junit.Assert.assertArrayEquals; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.times; import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.model.DescribeInstancesResult; -import com.amazonaws.services.ec2.model.Instance; -import com.amazonaws.services.ec2.model.InstanceType; -import hudson.model.Node; -import java.util.ArrayList; -import java.util.Arrays; +import com.cloudbees.jenkins.plugins.awscredentials.AWSCredentialsImpl; +import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; +import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.CredentialsStore; +import com.cloudbees.plugins.credentials.SystemCredentialsProvider; +import com.cloudbees.plugins.credentials.domains.Domain; +import hudson.plugins.ec2.util.TestSSHUserPrivateKey; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; +import java.io.IOException; import java.util.Collections; -import java.util.List; import jenkins.model.Jenkins; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlTextInput; import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.MockedStatic; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; +import org.xml.sax.SAXException; -@RunWith(MockitoJUnitRunner.class) +/** + * @author Kohsuke Kawaguchi + */ public class EC2CloudTest { - @Test - public void testSlaveTemplateAddition() throws Exception { - AmazonEC2Cloud cloud = new AmazonEC2Cloud( + + @Rule + public JenkinsRule r = new JenkinsRule(); + + private EC2Cloud cloud; + + @Before + public void setUp() throws Exception { + cloud = new EC2Cloud( "us-east-1", true, "abc", @@ -40,275 +78,136 @@ public void testSlaveTemplateAddition() throws Exception { Collections.emptyList(), "roleArn", "roleSessionName"); - SlaveTemplate orig = new SlaveTemplate( - "ami-123", - EC2AbstractSlave.TEST_ZONE, - null, - "default", - "foo", - InstanceType.M1Large, - false, - "ttt", - Node.Mode.NORMAL, - "description", - "bar", - "bbb", - "aaa", - "10", - "fff", - null, - EC2AbstractSlave.DEFAULT_JAVA_PATH, - "-Xmx1g", - false, - "subnet 456", - null, - null, - 0, - 0, - null, - "iamInstanceProfile", - true, - false, - "", - false, - "", - false, - false, - false, - ConnectionStrategy.PUBLIC_IP, - -1, - Collections.emptyList(), - null, - Tenancy.Default, - EbsEncryptRootVolume.DEFAULT, - EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, - EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, - EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, - EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); - cloud.addTemplate(orig); - assertNotNull(cloud.getTemplate(orig.description)); + r.jenkins.clouds.add(cloud); } @Test - public void testSlaveTemplateUpdate() throws Exception { - AmazonEC2Cloud cloud = new AmazonEC2Cloud( - "us-east-1", - true, - "abc", - "us-east-1", - null, - "ghi", - "3", - Collections.emptyList(), - "roleArn", - "roleSessionName"); - SlaveTemplate oldSlaveTemplate = new SlaveTemplate( - "ami-123", - EC2AbstractSlave.TEST_ZONE, - null, - "default", - "foo", - InstanceType.M1Large, - false, - "ttt", - Node.Mode.NORMAL, - "OldSlaveDescription", - "bar", - "bbb", - "aaa", - "10", - "fff", - null, - EC2AbstractSlave.DEFAULT_JAVA_PATH, - "-Xmx1g", - false, - "subnet 456", - null, - null, - 0, - 0, - null, - "iamInstanceProfile", - true, - false, - "", - false, - "", - false, - false, - false, - ConnectionStrategy.PUBLIC_IP, - -1, - Collections.emptyList(), - null, - Tenancy.Default, - EbsEncryptRootVolume.DEFAULT, - EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, - EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, - EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, - EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); - SlaveTemplate secondSlaveTemplate = new SlaveTemplate( - "ami-123", - EC2AbstractSlave.TEST_ZONE, - null, - "default", - "foo", - InstanceType.M1Large, - false, - "ttt", - Node.Mode.NORMAL, - "SecondSlaveDescription", - "bar", - "bbb", - "aaa", - "10", - "fff", - null, - EC2AbstractSlave.DEFAULT_JAVA_PATH, - "-Xmx1g", - false, - "subnet 456", - null, - null, - 0, - 0, - null, - "iamInstanceProfile", - true, - false, - "", - false, - "", - false, - false, - false, - ConnectionStrategy.PUBLIC_IP, - -1, - Collections.emptyList(), - null, - Tenancy.Default, - EbsEncryptRootVolume.DEFAULT, - EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, - EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, - EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, - EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); - cloud.addTemplate(oldSlaveTemplate); - cloud.addTemplate(secondSlaveTemplate); - SlaveTemplate newSlaveTemplate = new SlaveTemplate( - "ami-456", - EC2AbstractSlave.TEST_ZONE, - null, - "default", - "foo", - InstanceType.M1Large, - false, - "ttt", - Node.Mode.NORMAL, - "NewSlaveDescription", - "bar", - "bbb", - "aaa", - "10", - "fff", - null, - EC2AbstractSlave.DEFAULT_JAVA_PATH, - "-Xmx1g", - false, - "subnet 456", - null, - null, - 0, - 0, - null, - "iamInstanceProfile", - true, - false, - "", - false, - "", - false, - false, - false, - ConnectionStrategy.PUBLIC_IP, - -1, - Collections.emptyList(), - null, - Tenancy.Default, - EbsEncryptRootVolume.DEFAULT, - EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, - EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, - EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, - EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); - int index = cloud.getTemplates().indexOf(oldSlaveTemplate); + public void testConfigRoundtrip() throws Exception { + r.submit(getConfigForm()); + r.assertEqualBeans( + cloud, + r.jenkins.clouds.get(EC2Cloud.class), + "name,region,useInstanceProfileForCredentials,privateKey,instanceCap,roleArn,roleSessionName"); + } - cloud.updateTemplate(newSlaveTemplate, "OldSlaveDescription"); - assertNull(cloud.getTemplate("OldSlaveDescription")); - assertNotNull(cloud.getTemplate("NewSlaveDescription")); - Assert.assertEquals(index, cloud.getTemplates().indexOf(newSlaveTemplate)); // assert order of templates is kept + @Test + public void testAmazonEC2FactoryGetInstance() throws Exception { + EC2Cloud cloud = r.jenkins.clouds.get(EC2Cloud.class); + AmazonEC2 connection = cloud.connect(); + Assert.assertNotNull(connection); + Assert.assertTrue(Mockito.mockingDetails(connection).isMock()); } @Test - public void testReattachOrphanStoppedNodes() throws Exception { - /* Mocked items */ - AmazonEC2Cloud cloud = new AmazonEC2Cloud( - "us-east-1", - true, - "abc", - "us-east-1", - null, - "ghi", - "3", - Collections.emptyList(), - "roleArn", - "roleSessionName"); - EC2Cloud spyCloud = Mockito.spy(cloud); - AmazonEC2 mockEc2 = Mockito.mock(AmazonEC2.class); - Jenkins mockJenkins = Mockito.mock(Jenkins.class); - EC2AbstractSlave mockOrphanNode = Mockito.mock(EC2AbstractSlave.class); - SlaveTemplate mockSlaveTemplate = Mockito.mock(SlaveTemplate.class); - DescribeInstancesResult mockedDIResult = Mockito.mock(DescribeInstancesResult.class); - Instance mockedInstance = Mockito.mock(Instance.class); - List listOfMockedInstances = new ArrayList<>(); - listOfMockedInstances.add(mockedInstance); + public void testAmazonEC2FactoryWorksIfSessionNameMissing() throws Exception { + r.jenkins.clouds.replace(new EC2Cloud( + "us-east-1", true, "abc", "us-east-1", null, "ghi", "3", Collections.emptyList(), "roleArn", null)); + EC2Cloud cloud = r.jenkins.clouds.get(EC2Cloud.class); + AmazonEC2 connection = cloud.connect(); + Assert.assertNotNull(connection); + Assert.assertTrue(Mockito.mockingDetails(connection).isMock()); + } - try (MockedStatic mocked = Mockito.mockStatic(Jenkins.class)) { - mocked.when(Jenkins::getInstanceOrNull).thenReturn(mockJenkins); - EC2AbstractSlave[] orphanNodes = {mockOrphanNode}; - Mockito.doReturn(Arrays.asList(orphanNodes)).when(mockSlaveTemplate).toSlaves(eq(listOfMockedInstances)); - List listOfJenkinsNodes = new ArrayList<>(); + @Test + public void testSessionNameMissingWarning() { + EC2Cloud actual = r.jenkins.clouds.get(EC2Cloud.class); + EC2Cloud.DescriptorImpl descriptor = (EC2Cloud.DescriptorImpl) actual.getDescriptor(); + assertThat(descriptor.doCheckRoleSessionName("roleArn", "").kind, is(FormValidation.Kind.WARNING)); + assertThat(descriptor.doCheckRoleSessionName("roleArn", "roleSessionName").kind, is(FormValidation.Kind.OK)); + } - Mockito.doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocation) { - Node n = (Node) invocation.getArguments()[0]; - listOfJenkinsNodes.add(n); - return null; - } - }) - .when(mockJenkins) - .addNode(Mockito.any(Node.class)); + @Test + public void testSshKeysCredentialsIdRemainsUnchangedAfterUpdatingOtherFields() throws Exception { + HtmlForm form = getConfigForm(); + HtmlTextInput input = form.getInputByName("_.roleSessionName"); - Mockito.doReturn(null).when(mockOrphanNode).toComputer(); - Mockito.doReturn(false).when(mockOrphanNode).getStopOnTerminate(); - Mockito.doReturn(mockEc2).when(spyCloud).connect(); - Mockito.doReturn(mockedDIResult) - .when(mockSlaveTemplate) - .getDescribeInstanceResult(Mockito.any(AmazonEC2.class), eq(true)); - Mockito.doReturn(listOfMockedInstances) - .when(mockSlaveTemplate) - .findOrphansOrStopped(eq(mockedDIResult), Mockito.anyInt()); - Mockito.doNothing() - .when(mockSlaveTemplate) - .wakeOrphansOrStoppedUp(Mockito.any(AmazonEC2.class), eq(listOfMockedInstances)); + input.setText("updatedSessionName"); + r.submit(form); + EC2Cloud actual = r.jenkins.clouds.get(EC2Cloud.class); + assertEquals("updatedSessionName", actual.getRoleSessionName()); + r.assertEqualBeans( + cloud, actual, "name,region,useInstanceProfileForCredentials,sshKeysCredentialsId,instanceCap,roleArn"); + } - /* Actual call to test*/ - spyCloud.attemptReattachOrphanOrStoppedNodes(mockJenkins, mockSlaveTemplate, 1); + @Test + public void testAWSCredentials() throws IOException { + EC2Cloud actual = r.jenkins.clouds.get(EC2Cloud.class); + EC2Cloud.DescriptorImpl descriptor = (EC2Cloud.DescriptorImpl) actual.getDescriptor(); + assertNotNull(descriptor); + ListBoxModel m = descriptor.doFillCredentialsIdItems(Jenkins.get()); + assertThat(m.size(), is(1)); + SystemCredentialsProvider.getInstance() + .getCredentials() + .add(new AWSCredentialsImpl( + CredentialsScope.SYSTEM, "system_id", "system_ak", "system_sk", "system_desc")); + // Ensure added credential is displayed + m = descriptor.doFillCredentialsIdItems(Jenkins.get()); + assertThat(m.size(), is(2)); + SystemCredentialsProvider.getInstance() + .getCredentials() + .add(new AWSCredentialsImpl( + CredentialsScope.GLOBAL, "global_id", "global_ak", "global_sk", "global_desc")); + m = descriptor.doFillCredentialsIdItems(Jenkins.get()); + assertThat(m.size(), is(3)); + } - /* Checks */ - Mockito.verify(mockSlaveTemplate, times(1)) - .wakeOrphansOrStoppedUp(Mockito.any(AmazonEC2.class), eq(listOfMockedInstances)); - Node[] expectedNodes = {mockOrphanNode}; - assertArrayEquals(expectedNodes, listOfJenkinsNodes.toArray()); + @Test + public void testSshCredentials() throws IOException { + EC2Cloud actual = r.jenkins.clouds.get(EC2Cloud.class); + EC2Cloud.DescriptorImpl descriptor = (EC2Cloud.DescriptorImpl) actual.getDescriptor(); + assertNotNull(descriptor); + ListBoxModel m = descriptor.doFillSshKeysCredentialsIdItems(Jenkins.get(), ""); + assertThat(m.size(), is(1)); + BasicSSHUserPrivateKey sshKeyCredentials = new BasicSSHUserPrivateKey( + CredentialsScope.SYSTEM, + "ghi", + "key", + new BasicSSHUserPrivateKey.DirectEntryPrivateKeySource("somekey"), + "", + ""); + for (CredentialsStore credentialsStore : CredentialsProvider.lookupStores(r.jenkins)) { + if (credentialsStore instanceof SystemCredentialsProvider.StoreImpl) { + credentialsStore.addCredentials(Domain.global(), sshKeyCredentials); + } + } + // Ensure added credential is displayed + m = descriptor.doFillSshKeysCredentialsIdItems(Jenkins.get(), ""); + assertThat(m.size(), is(2)); + // Ensure that the cloud can resolve the new key + assertThat(actual.resolvePrivateKey(), notNullValue()); + } + + /** + * Ensure that EC2 plugin can use any implementation of SSHUserPrivateKey (not just the default implementation, BasicSSHUserPrivateKey). + */ + @Test + @Issue("JENKINS-63986") + public void testCustomSshCredentialTypes() throws IOException { + EC2Cloud actual = r.jenkins.clouds.get(EC2Cloud.class); + EC2Cloud.DescriptorImpl descriptor = (EC2Cloud.DescriptorImpl) actual.getDescriptor(); + assertNotNull(descriptor); + ListBoxModel m = descriptor.doFillSshKeysCredentialsIdItems(Jenkins.get(), ""); + assertThat(m.size(), is(1)); + SSHUserPrivateKey sshKeyCredentials = new TestSSHUserPrivateKey( + CredentialsScope.SYSTEM, + "ghi", + "key", + new BasicSSHUserPrivateKey.DirectEntryPrivateKeySource("somekey"), + "", + ""); + for (CredentialsStore credentialsStore : CredentialsProvider.lookupStores(r.jenkins)) { + if (credentialsStore instanceof SystemCredentialsProvider.StoreImpl) { + credentialsStore.addCredentials(Domain.global(), sshKeyCredentials); + } } + // Ensure added credential is displayed + m = descriptor.doFillSshKeysCredentialsIdItems(Jenkins.get(), ""); + assertThat(m.size(), is(2)); + // Ensure that the cloud can resolve the new key + assertThat(actual.resolvePrivateKey(), notNullValue()); + } + + private HtmlForm getConfigForm() throws IOException, SAXException { + return r.createWebClient().goTo(cloud.getUrl() + "configure").getFormByName("config"); } } diff --git a/src/test/java/hudson/plugins/ec2/EC2CloudUnitTest.java b/src/test/java/hudson/plugins/ec2/EC2CloudUnitTest.java new file mode 100644 index 000000000..15f613822 --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/EC2CloudUnitTest.java @@ -0,0 +1,453 @@ +/* + * The MIT License + * + * Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.plugins.ec2; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +import com.amazonaws.services.ec2.AmazonEC2; +import com.amazonaws.services.ec2.model.DescribeInstancesResult; +import com.amazonaws.services.ec2.model.Instance; +import com.amazonaws.services.ec2.model.InstanceType; +import com.amazonaws.services.ec2.model.Tag; +import hudson.model.Node; +import hudson.plugins.ec2.util.AmazonEC2FactoryMockImpl; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import jenkins.model.Jenkins; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +/** + * Unit tests related to {@link EC2Cloud}, but do not require a Jenkins instance. + */ +@RunWith(MockitoJUnitRunner.Silent.class) +public class EC2CloudUnitTest { + + @Test + public void testEC2EndpointURLCreation() throws MalformedURLException { + EC2Cloud.DescriptorImpl descriptor = new EC2Cloud.DescriptorImpl(); + + assertEquals(new URL(EC2Cloud.DEFAULT_EC2_ENDPOINT), descriptor.determineEC2EndpointURL(null)); + assertEquals(new URL(EC2Cloud.DEFAULT_EC2_ENDPOINT), descriptor.determineEC2EndpointURL("")); + assertEquals(new URL("https://www.abc.com"), descriptor.determineEC2EndpointURL("https://www.abc.com")); + } + + @Test + public void testInstaceCap() throws Exception { + EC2Cloud cloud = new EC2Cloud( + "us-east-1", + true, + "abc", + "us-east-1", + null, + "key", + null, + Collections.emptyList(), + "roleArn", + "roleSessionName"); + assertEquals(cloud.getInstanceCap(), Integer.MAX_VALUE); + assertEquals(cloud.getInstanceCapStr(), ""); + + final int cap = 3; + final String capStr = String.valueOf(cap); + cloud = new EC2Cloud( + "us-east-1", + true, + "abc", + "us-east-1", + null, + "key", + capStr, + Collections.emptyList(), + "roleArn", + "roleSessionName"); + assertEquals(cloud.getInstanceCap(), cap); + assertEquals(cloud.getInstanceCapStr(), capStr); + } + + @Test + public void testSpotInstanceCount() throws Exception { + final int numberOfSpotInstanceRequests = 105; + EC2Cloud cloud = Mockito.spy(new EC2Cloud( + "us-east-1", + true, + "abc", + "us-east-1", + null, + "key", + null, + Collections.emptyList(), + "roleArn", + "roleSessionName")); + Jenkins jenkinsMock = mock(Jenkins.class); + EC2SpotSlave spotSlaveMock = mock(EC2SpotSlave.class); + try (MockedStatic mocked = Mockito.mockStatic(Jenkins.class)) { + mocked.when(Jenkins::get).thenReturn(jenkinsMock); + Mockito.when(jenkinsMock.getNodes()).thenReturn(Collections.singletonList(spotSlaveMock)); + when(spotSlaveMock.getSpotRequest()).thenReturn(null); + when(spotSlaveMock.getSpotInstanceRequestId()).thenReturn("sir-id"); + + List instances = new ArrayList<>(); + for (int i = 0; i <= numberOfSpotInstanceRequests; i++) { + instances.add(new Instance() + .withInstanceId("id" + i) + .withTags(new Tag().withKey("jenkins_slave_type").withValue("spot"))); + } + + AmazonEC2FactoryMockImpl.instances = instances; + + Mockito.doReturn(AmazonEC2FactoryMockImpl.createAmazonEC2Mock(null)) + .when(cloud) + .connect(); + + Method countCurrentEC2SpotSlaves = EC2Cloud.class.getDeclaredMethod( + "countCurrentEC2SpotSlaves", SlaveTemplate.class, String.class, Set.class); + countCurrentEC2SpotSlaves.setAccessible(true); + Object[] params = {null, "jenkinsurl", new HashSet()}; + int n = (int) countCurrentEC2SpotSlaves.invoke(cloud, params); + + // Should equal number of spot instance requests + 1 for spot nodes not having a spot instance request + assertEquals(numberOfSpotInstanceRequests + 1, n); + } + } + + @Test + public void testCNPartition() { + assertEquals( + EC2Cloud.getAwsPartitionHostForService("cn-northwest-1", "ec2"), "ec2.cn-northwest-1.amazonaws.com.cn"); + assertEquals( + EC2Cloud.getAwsPartitionHostForService("cn-northwest-1", "s3"), "s3.cn-northwest-1.amazonaws.com.cn"); + } + + @Test + public void testNormalPartition() { + assertEquals(EC2Cloud.getAwsPartitionHostForService("us-east-1", "ec2"), "ec2.us-east-1.amazonaws.com"); + assertEquals(EC2Cloud.getAwsPartitionHostForService("us-east-1", "s3"), "s3.us-east-1.amazonaws.com"); + } + + @Test + public void testSlaveTemplateAddition() throws Exception { + EC2Cloud cloud = new EC2Cloud( + "us-east-1", + true, + "abc", + "us-east-1", + null, + "ghi", + "3", + Collections.emptyList(), + "roleArn", + "roleSessionName"); + SlaveTemplate orig = new SlaveTemplate( + "ami-123", + EC2AbstractSlave.TEST_ZONE, + null, + "default", + "foo", + InstanceType.M1Large, + false, + "ttt", + Node.Mode.NORMAL, + "description", + "bar", + "bbb", + "aaa", + "10", + "fff", + null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, + "-Xmx1g", + false, + "subnet 456", + null, + null, + 0, + 0, + null, + "iamInstanceProfile", + true, + false, + "", + false, + "", + false, + false, + false, + ConnectionStrategy.PUBLIC_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); + cloud.addTemplate(orig); + assertNotNull(cloud.getTemplate(orig.description)); + } + + @Test + public void testSlaveTemplateUpdate() throws Exception { + EC2Cloud cloud = new EC2Cloud( + "us-east-1", + true, + "abc", + "us-east-1", + null, + "ghi", + "3", + Collections.emptyList(), + "roleArn", + "roleSessionName"); + SlaveTemplate oldSlaveTemplate = new SlaveTemplate( + "ami-123", + EC2AbstractSlave.TEST_ZONE, + null, + "default", + "foo", + InstanceType.M1Large, + false, + "ttt", + Node.Mode.NORMAL, + "OldSlaveDescription", + "bar", + "bbb", + "aaa", + "10", + "fff", + null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, + "-Xmx1g", + false, + "subnet 456", + null, + null, + 0, + 0, + null, + "iamInstanceProfile", + true, + false, + "", + false, + "", + false, + false, + false, + ConnectionStrategy.PUBLIC_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); + SlaveTemplate secondSlaveTemplate = new SlaveTemplate( + "ami-123", + EC2AbstractSlave.TEST_ZONE, + null, + "default", + "foo", + InstanceType.M1Large, + false, + "ttt", + Node.Mode.NORMAL, + "SecondSlaveDescription", + "bar", + "bbb", + "aaa", + "10", + "fff", + null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, + "-Xmx1g", + false, + "subnet 456", + null, + null, + 0, + 0, + null, + "iamInstanceProfile", + true, + false, + "", + false, + "", + false, + false, + false, + ConnectionStrategy.PUBLIC_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); + cloud.addTemplate(oldSlaveTemplate); + cloud.addTemplate(secondSlaveTemplate); + SlaveTemplate newSlaveTemplate = new SlaveTemplate( + "ami-456", + EC2AbstractSlave.TEST_ZONE, + null, + "default", + "foo", + InstanceType.M1Large, + false, + "ttt", + Node.Mode.NORMAL, + "NewSlaveDescription", + "bar", + "bbb", + "aaa", + "10", + "fff", + null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, + "-Xmx1g", + false, + "subnet 456", + null, + null, + 0, + 0, + null, + "iamInstanceProfile", + true, + false, + "", + false, + "", + false, + false, + false, + ConnectionStrategy.PUBLIC_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); + int index = cloud.getTemplates().indexOf(oldSlaveTemplate); + + cloud.updateTemplate(newSlaveTemplate, "OldSlaveDescription"); + assertNull(cloud.getTemplate("OldSlaveDescription")); + assertNotNull(cloud.getTemplate("NewSlaveDescription")); + Assert.assertEquals(index, cloud.getTemplates().indexOf(newSlaveTemplate)); // assert order of templates is kept + } + + @Test + public void testReattachOrphanStoppedNodes() throws Exception { + /* Mocked items */ + EC2Cloud cloud = new EC2Cloud( + "us-east-1", + true, + "abc", + "us-east-1", + null, + "ghi", + "3", + Collections.emptyList(), + "roleArn", + "roleSessionName"); + EC2Cloud spyCloud = Mockito.spy(cloud); + AmazonEC2 mockEc2 = Mockito.mock(AmazonEC2.class); + Jenkins mockJenkins = Mockito.mock(Jenkins.class); + EC2AbstractSlave mockOrphanNode = Mockito.mock(EC2AbstractSlave.class); + SlaveTemplate mockSlaveTemplate = Mockito.mock(SlaveTemplate.class); + DescribeInstancesResult mockedDIResult = Mockito.mock(DescribeInstancesResult.class); + Instance mockedInstance = Mockito.mock(Instance.class); + List listOfMockedInstances = new ArrayList<>(); + listOfMockedInstances.add(mockedInstance); + + try (MockedStatic mocked = Mockito.mockStatic(Jenkins.class)) { + mocked.when(Jenkins::getInstanceOrNull).thenReturn(mockJenkins); + EC2AbstractSlave[] orphanNodes = {mockOrphanNode}; + Mockito.doReturn(Arrays.asList(orphanNodes)).when(mockSlaveTemplate).toSlaves(eq(listOfMockedInstances)); + List listOfJenkinsNodes = new ArrayList<>(); + + Mockito.doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) { + Node n = (Node) invocation.getArguments()[0]; + listOfJenkinsNodes.add(n); + return null; + } + }) + .when(mockJenkins) + .addNode(Mockito.any(Node.class)); + + Mockito.doReturn(null).when(mockOrphanNode).toComputer(); + Mockito.doReturn(false).when(mockOrphanNode).getStopOnTerminate(); + Mockito.doReturn(mockEc2).when(spyCloud).connect(); + Mockito.doReturn(mockedDIResult) + .when(mockSlaveTemplate) + .getDescribeInstanceResult(Mockito.any(AmazonEC2.class), eq(true)); + Mockito.doReturn(listOfMockedInstances) + .when(mockSlaveTemplate) + .findOrphansOrStopped(eq(mockedDIResult), Mockito.anyInt()); + Mockito.doNothing() + .when(mockSlaveTemplate) + .wakeOrphansOrStoppedUp(Mockito.any(AmazonEC2.class), eq(listOfMockedInstances)); + + /* Actual call to test*/ + spyCloud.attemptReattachOrphanOrStoppedNodes(mockJenkins, mockSlaveTemplate, 1); + + /* Checks */ + Mockito.verify(mockSlaveTemplate, times(1)) + .wakeOrphansOrStoppedUp(Mockito.any(AmazonEC2.class), eq(listOfMockedInstances)); + Node[] expectedNodes = {mockOrphanNode}; + assertArrayEquals(expectedNodes, listOfJenkinsNodes.toArray()); + } + } +} diff --git a/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java b/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java index e885d148e..8857c3fb2 100644 --- a/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2RetentionStrategyTest.java @@ -777,7 +777,7 @@ public void testRetentionDespiteIdleWithMinimumInstances() throws Exception { EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); SSHCredentialHelper.assureSshCredentialAvailableThroughCredentialProviders("ghi"); - AmazonEC2Cloud cloud = new AmazonEC2Cloud( + EC2Cloud cloud = new EC2Cloud( "us-east-1", true, "abc", @@ -900,7 +900,7 @@ public void testRetentionDespiteIdleWithMinimumInstanceActiveTimeRange() throws MinimumInstanceChecker.clock = Clock.fixed(localDateTime.atZone(ZoneId.systemDefault()).toInstant(), ZoneId.systemDefault()); SSHCredentialHelper.assureSshCredentialAvailableThroughCredentialProviders("ghi"); - AmazonEC2Cloud cloud = new AmazonEC2Cloud( + EC2Cloud cloud = new EC2Cloud( "us-east-1", true, "abc", @@ -999,7 +999,7 @@ public void testRetentionIdleWithMinimumInstanceInactiveTimeRange() throws Excep MinimumInstanceChecker.clock = Clock.fixed(localDateTime.atZone(ZoneId.systemDefault()).toInstant(), ZoneId.systemDefault()); - AmazonEC2Cloud cloud = new AmazonEC2Cloud( + EC2Cloud cloud = new EC2Cloud( "us-east-1", true, "abc", @@ -1084,7 +1084,7 @@ public void testRetentionDespiteIdleWithMinimumInstanceActiveTimeRangeAfterMidni MinimumInstanceChecker.clock = Clock.fixed(localDateTime.atZone(ZoneId.systemDefault()).toInstant(), ZoneId.systemDefault()); SSHCredentialHelper.assureSshCredentialAvailableThroughCredentialProviders("ghi"); - AmazonEC2Cloud cloud = new AmazonEC2Cloud( + EC2Cloud cloud = new EC2Cloud( "us-east-1", true, "abc", @@ -1183,7 +1183,7 @@ public void testRetentionStopsAfterActiveRangeEnds() throws Exception { Clock.fixed(localDateTime.atZone(ZoneId.systemDefault()).toInstant(), ZoneId.systemDefault()); SSHCredentialHelper.assureSshCredentialAvailableThroughCredentialProviders("ghi"); - AmazonEC2Cloud cloud = new AmazonEC2Cloud( + EC2Cloud cloud = new EC2Cloud( "us-east-1", true, "abc", diff --git a/src/test/java/hudson/plugins/ec2/EC2SlaveMonitorTest.java b/src/test/java/hudson/plugins/ec2/EC2SlaveMonitorTest.java index 4ed16c698..17a45742e 100644 --- a/src/test/java/hudson/plugins/ec2/EC2SlaveMonitorTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2SlaveMonitorTest.java @@ -72,7 +72,7 @@ public void testMinimumNumberOfInstances() throws Exception { EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); SSHCredentialHelper.assureSshCredentialAvailableThroughCredentialProviders("ghi"); - AmazonEC2Cloud cloud = new AmazonEC2Cloud( + EC2Cloud cloud = new EC2Cloud( "us-east-1", true, "abc", @@ -142,7 +142,7 @@ public void testMinimumNumberOfSpareInstances() throws Exception { EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); SSHCredentialHelper.assureSshCredentialAvailableThroughCredentialProviders("ghi"); - AmazonEC2Cloud cloud = new AmazonEC2Cloud( + EC2Cloud cloud = new EC2Cloud( "us-east-1", true, "abc", diff --git a/src/test/java/hudson/plugins/ec2/EC2StepTest.java b/src/test/java/hudson/plugins/ec2/EC2StepTest.java index aad919976..0a2ac9251 100644 --- a/src/test/java/hudson/plugins/ec2/EC2StepTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2StepTest.java @@ -37,7 +37,7 @@ public class EC2StepTest { public JenkinsRule r = new JenkinsRule(); @Mock - private AmazonEC2Cloud cl; + private EC2Cloud cl; @Mock private SlaveTemplate st; diff --git a/src/test/java/hudson/plugins/ec2/EucalyptusTest.java b/src/test/java/hudson/plugins/ec2/EucalyptusTest.java deleted file mode 100644 index b24c7e40e..000000000 --- a/src/test/java/hudson/plugins/ec2/EucalyptusTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package hudson.plugins.ec2; - -import java.net.URL; -import org.htmlunit.html.HtmlForm; -import org.htmlunit.html.HtmlPage; -import org.junit.Rule; -import org.junit.Test; -import org.jvnet.hudson.test.JenkinsRule; - -public class EucalyptusTest { - @Rule - public JenkinsRule r = new JenkinsRule(); - - @Test - public void configRoundTrip() throws Exception { - Eucalyptus cloud = new Eucalyptus( - "test", - new URL("https://ec2"), - new URL("https://s3"), - false, - null, - "test", - null, - "0", - null, - null, - null); - r.jenkins.clouds.add(cloud); - r.jenkins.save(); - JenkinsRule.WebClient wc = r.createWebClient(); - HtmlPage p = wc.goTo(cloud.getUrl() + "configure"); - HtmlForm f = p.getFormByName("config"); - r.submit(f); - r.assertEqualBeans( - cloud, - r.jenkins.getCloud("test"), - "name,ec2EndpointUrl,s3EndpointUrl,useInstanceProfileForCredentials,roleArn,roleSessionName,credentialsId,sshKeysCredentialsId,instanceCap,templates"); - } -} diff --git a/src/test/java/hudson/plugins/ec2/FileBasedSSHKeyTest.java b/src/test/java/hudson/plugins/ec2/FileBasedSSHKeyTest.java index 34f68c152..1179356b0 100644 --- a/src/test/java/hudson/plugins/ec2/FileBasedSSHKeyTest.java +++ b/src/test/java/hudson/plugins/ec2/FileBasedSSHKeyTest.java @@ -31,7 +31,7 @@ private static void verifyKeyFile(JenkinsRule r) throws Throwable { } private static void verifyCorrectKeyIsResolved(JenkinsRule r) throws Throwable { - AmazonEC2Cloud cloud = new AmazonEC2Cloud( + EC2Cloud cloud = new EC2Cloud( "us-east-1", true, "abc", @@ -43,7 +43,7 @@ private static void verifyCorrectKeyIsResolved(JenkinsRule r) throws Throwable { "roleArn", "roleSessionName"); r.jenkins.clouds.add(cloud); - AmazonEC2Cloud c = r.jenkins.clouds.get(AmazonEC2Cloud.class); + EC2Cloud c = r.jenkins.clouds.get(EC2Cloud.class); assertEquals("An unexpected key was returned!", c.resolvePrivateKey().getPrivateKey(), "hello, world!"); } } diff --git a/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java b/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java index 82f0396a1..9ec683015 100644 --- a/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java +++ b/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java @@ -152,8 +152,7 @@ public void testConfigRoundtrip() throws Exception { List templates = new ArrayList<>(); templates.add(orig); - AmazonEC2Cloud ac = - new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); + EC2Cloud ac = new EC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); r.submit(getConfigForm(ac)); @@ -222,8 +221,7 @@ public void testConfigRoundtripWithCustomSSHHostKeyVerificationStrategy() throws List templates = new ArrayList<>(); templates.add(orig); - AmazonEC2Cloud ac = - new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); + EC2Cloud ac = new EC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); r.submit(getConfigForm(ac)); @@ -299,8 +297,7 @@ public void testConfigWithSpotBidPrice() throws Exception { List templates = new ArrayList<>(); templates.add(orig); - AmazonEC2Cloud ac = - new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); + EC2Cloud ac = new EC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); r.submit(getConfigForm(ac)); @@ -371,8 +368,7 @@ public void testSpotConfigWithoutBidPrice() throws Exception { List templates = new ArrayList<>(); templates.add(orig); - AmazonEC2Cloud ac = - new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); + EC2Cloud ac = new EC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); r.submit(getConfigForm(ac)); @@ -436,8 +432,7 @@ public void testWindowsConfigRoundTrip() throws Exception { List templates = new ArrayList<>(); templates.add(orig); - AmazonEC2Cloud ac = - new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); + EC2Cloud ac = new EC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); r.submit(getConfigForm(ac)); @@ -499,8 +494,7 @@ public void testUnixConfigRoundTrip() throws Exception { List templates = new ArrayList<>(); templates.add(orig); - AmazonEC2Cloud ac = - new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); + EC2Cloud ac = new EC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); r.submit(getConfigForm(ac)); @@ -570,18 +564,13 @@ public void testMinimumNumberOfInstancesActiveRangeConfig() throws Exception { List templates = new ArrayList<>(); templates.add(slaveTemplate); - AmazonEC2Cloud ac = - new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); + EC2Cloud ac = new EC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); r.configRoundtrip(); - MinimumNumberOfInstancesTimeRangeConfig stored = r.jenkins - .clouds - .get(AmazonEC2Cloud.class) - .getTemplates() - .get(0) - .getMinimumNumberOfInstancesTimeRangeConfig(); + MinimumNumberOfInstancesTimeRangeConfig stored = + r.jenkins.clouds.get(EC2Cloud.class).getTemplates().get(0).getMinimumNumberOfInstancesTimeRangeConfig(); Assert.assertNotNull(stored); Assert.assertEquals("11:00", stored.getMinimumNoInstancesActiveTimeRangeFrom()); Assert.assertEquals("15:00", stored.getMinimumNoInstancesActiveTimeRangeTo()); @@ -921,7 +910,7 @@ public void provisionSpotFallsBackToOndemandWhenSpotQuotaExceeded() throws Excep } private AmazonEC2 setupTestForProvisioning(SlaveTemplate template) throws Exception { - AmazonEC2Cloud mockedCloud = mock(AmazonEC2Cloud.class); + EC2Cloud mockedCloud = mock(EC2Cloud.class); AmazonEC2 mockedEC2 = mock(AmazonEC2.class); EC2PrivateKey mockedPrivateKey = mock(EC2PrivateKey.class); KeyPair mockedKeyPair = new KeyPair(); @@ -1029,8 +1018,7 @@ public void testMacConfig() throws Exception { List templates = new ArrayList<>(); templates.add(orig); - AmazonEC2Cloud ac = - new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); + EC2Cloud ac = new EC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); r.submit(getConfigForm(ac)); @@ -1134,12 +1122,12 @@ public void testAgentName() { List templates = new ArrayList<>(); templates.add(broken); templates.add(working); - AmazonEC2Cloud brokenCloud = - new AmazonEC2Cloud("broken/cloud", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); + EC2Cloud brokenCloud = + new EC2Cloud("broken/cloud", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); assertThat(broken.getSlaveName("test"), is("test")); assertThat(working.getSlaveName("test"), is("test")); - AmazonEC2Cloud workingCloud = - new AmazonEC2Cloud("cloud", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); + EC2Cloud workingCloud = + new EC2Cloud("cloud", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); assertThat(broken.getSlaveName("test"), is("test")); assertThat(working.getSlaveName("test"), is("EC2 (cloud) - working (test)")); } @@ -1195,8 +1183,7 @@ public void testMetadataV2Config() throws Exception { List templates = Collections.singletonList(orig); - AmazonEC2Cloud ac = - new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); + EC2Cloud ac = new EC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); r.jenkins.clouds.add(ac); r.submit(r.createWebClient().goTo("configure").getFormByName("config")); @@ -1453,7 +1440,7 @@ public void provisionOnDemandSetsMetadataDefaultOptions() throws Exception { assertEquals(metadataOptionsRequest.getHttpPutResponseHopLimit(), Integer.valueOf(1)); } - private HtmlForm getConfigForm(AmazonEC2Cloud ac) throws IOException, SAXException { + private HtmlForm getConfigForm(EC2Cloud ac) throws IOException, SAXException { return r.createWebClient().goTo(ac.getUrl() + "configure").getFormByName("config"); } } diff --git a/src/test/java/hudson/plugins/ec2/TemplateLabelsTest.java b/src/test/java/hudson/plugins/ec2/TemplateLabelsTest.java index 32f3f99e4..3844c5991 100644 --- a/src/test/java/hudson/plugins/ec2/TemplateLabelsTest.java +++ b/src/test/java/hudson/plugins/ec2/TemplateLabelsTest.java @@ -42,7 +42,7 @@ public class TemplateLabelsTest { @Rule public JenkinsRule r = new JenkinsRule(); - private AmazonEC2Cloud ac; + private EC2Cloud ac; private final String LABEL1 = "label1"; private final String LABEL2 = "label2"; @@ -105,7 +105,7 @@ private void setUpCloud(String label, Node.Mode mode) throws Exception { List templates = new ArrayList<>(); templates.add(template); - ac = new AmazonEC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); + ac = new EC2Cloud("us-east-1", false, "abc", "us-east-1", "ghi", null, "3", templates, null, null); } @Test diff --git a/src/test/java/hudson/plugins/ec2/util/AmazonEC2FactoryMockImpl.java b/src/test/java/hudson/plugins/ec2/util/AmazonEC2FactoryMockImpl.java index f7dd1c87b..7d04514e2 100644 --- a/src/test/java/hudson/plugins/ec2/util/AmazonEC2FactoryMockImpl.java +++ b/src/test/java/hudson/plugins/ec2/util/AmazonEC2FactoryMockImpl.java @@ -40,7 +40,6 @@ import com.amazonaws.services.ec2.model.TerminateInstancesResult; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; -import hudson.plugins.ec2.AmazonEC2Cloud; import hudson.plugins.ec2.EC2Cloud; import java.net.URL; import java.util.ArrayList; @@ -189,7 +188,7 @@ private static void mockDescribeKeyPairs(AmazonEC2Client mock) { KeyPairInfo keyPairInfo = new KeyPairInfo(); keyPairInfo.setKeyFingerprint(Jenkins.get() .clouds - .get(AmazonEC2Cloud.class) + .get(EC2Cloud.class) .resolvePrivateKey() .getFingerprint()); return new DescribeKeyPairsResult().withKeyPairs(keyPairInfo); From d1fae8d037ed1f60e638ecea0ab88180856fa353 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 6 Jan 2025 11:05:20 -0800 Subject: [PATCH 089/267] Remove `ZpoolMonitor` and `ZPoolExpandNotice` --- pom.xml | 5 - .../plugins/ec2/ebs/ZPoolExpandNotice.java | 54 ----------- .../hudson/plugins/ec2/ebs/ZPoolMonitor.java | 91 ------------------- .../plugins/ec2/ebs/Messages.properties | 1 - .../ec2/ebs/ZPoolExpandNotice/index.jelly | 40 -------- .../ec2/ebs/ZPoolExpandNotice/message.jelly | 33 ------- 6 files changed, 224 deletions(-) delete mode 100644 src/main/java/hudson/plugins/ec2/ebs/ZPoolExpandNotice.java delete mode 100644 src/main/java/hudson/plugins/ec2/ebs/ZPoolMonitor.java delete mode 100644 src/main/resources/hudson/plugins/ec2/ebs/Messages.properties delete mode 100644 src/main/resources/hudson/plugins/ec2/ebs/ZPoolExpandNotice/index.jelly delete mode 100644 src/main/resources/hudson/plugins/ec2/ebs/ZPoolExpandNotice/message.jelly diff --git a/pom.xml b/pom.xml index 65ad566c4..bf8133186 100644 --- a/pom.xml +++ b/pom.xml @@ -155,11 +155,6 @@ THE SOFTWARE. org.jenkins-ci.plugins.workflow workflow-step-api
- - org.kohsuke - libzfs - 0.8 - io.jenkins configuration-as-code diff --git a/src/main/java/hudson/plugins/ec2/ebs/ZPoolExpandNotice.java b/src/main/java/hudson/plugins/ec2/ebs/ZPoolExpandNotice.java deleted file mode 100644 index a11eb5a9e..000000000 --- a/src/main/java/hudson/plugins/ec2/ebs/ZPoolExpandNotice.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.plugins.ec2.ebs; - -import hudson.Extension; -import hudson.model.AdministrativeMonitor; - -/** - * {@link AdministrativeMonitor} that tells the user that ZFS pool is filling up and they need to add more storage. - * - * @author Kohsuke Kawaguchi - */ -@Extension -public class ZPoolExpandNotice extends AdministrativeMonitor { - /** - * Set by {@link ZPoolMonitor}. - */ - /* package */ boolean activated = false; - - public ZPoolExpandNotice() { - super("zpool.ebs"); - } - - @Override - public boolean isActivated() { - return activated; - } - - @Override - public String getDisplayName() { - return Messages.ZPoolExpandNotice_DisplayName(); - } -} diff --git a/src/main/java/hudson/plugins/ec2/ebs/ZPoolMonitor.java b/src/main/java/hudson/plugins/ec2/ebs/ZPoolMonitor.java deleted file mode 100644 index 68dbb79e5..000000000 --- a/src/main/java/hudson/plugins/ec2/ebs/ZPoolMonitor.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.plugins.ec2.ebs; - -import hudson.Extension; -import hudson.model.AdministrativeMonitor; -import hudson.model.PeriodicWork; -import java.io.IOException; -import java.net.URL; -import java.util.concurrent.TimeUnit; -import jenkins.model.Jenkins; -import org.jvnet.solaris.libzfs.LibZFS; -import org.jvnet.solaris.libzfs.ZFSFileSystem; -import org.jvnet.solaris.libzfs.ZFSPool; - -/** - * Once an hour, check if the main zpool is that hosts $HUDSON_HOME has still enough free space. - * - * @author Kohsuke Kawaguchi - */ -@Extension -public class ZPoolMonitor extends PeriodicWork { - private static Boolean isInsideEC2; - - @Override - public long getRecurrencePeriod() { - return TimeUnit.HOURS.toMillis(1); - } - - @Override - protected void doRun() { - ZPoolExpandNotice zen = AdministrativeMonitor.all().get(ZPoolExpandNotice.class); - Jenkins jenkinsInstance = Jenkins.getInstanceOrNull(); - ZFSFileSystem fs = null; - try { - if (isInsideEC2() && jenkinsInstance != null) { - fs = new LibZFS().getFileSystemByMountPoint(jenkinsInstance.getRootDir()); - } - } catch (LinkageError e) { - // probably not running on OpenSolaris - } - if (fs == null || zen == null) { - cancel(); - return; - } - ZFSPool pool = fs.getPool(); - long a = pool.getAvailableSize(); - long t = pool.getSize(); - - // if the disk is 90% filled up and the available space is less than - // 1GB, - // notify the user - zen.activated = t / a > 10 && a < 1000L * 1000 * 1000; - } - - /** - * Returns true if this JVM runs inside EC2. - */ - public static synchronized boolean isInsideEC2() { - if (isInsideEC2 == null) { - try { - new URL("http://169.254.169.254/latest").openStream().close(); - isInsideEC2 = true; - } catch (IOException e) { - isInsideEC2 = false; - } - } - return isInsideEC2; - } -} diff --git a/src/main/resources/hudson/plugins/ec2/ebs/Messages.properties b/src/main/resources/hudson/plugins/ec2/ebs/Messages.properties deleted file mode 100644 index 5ca743d62..000000000 --- a/src/main/resources/hudson/plugins/ec2/ebs/Messages.properties +++ /dev/null @@ -1 +0,0 @@ -ZPoolExpandNotice.DisplayName=ZFS Pool Monitor diff --git a/src/main/resources/hudson/plugins/ec2/ebs/ZPoolExpandNotice/index.jelly b/src/main/resources/hudson/plugins/ec2/ebs/ZPoolExpandNotice/index.jelly deleted file mode 100644 index 08f2db56b..000000000 --- a/src/main/resources/hudson/plugins/ec2/ebs/ZPoolExpandNotice/index.jelly +++ /dev/null @@ -1,40 +0,0 @@ - - - - - -

JENKINS_HOME is almost full

- -

- Your JENKINS_HOME (${app.rootDir}) is almost full. - When this directory completely fills up, it will cause problems - because Jenkins can't store any more data. -

- -

- -
-
-
\ No newline at end of file diff --git a/src/main/resources/hudson/plugins/ec2/ebs/ZPoolExpandNotice/message.jelly b/src/main/resources/hudson/plugins/ec2/ebs/ZPoolExpandNotice/message.jelly deleted file mode 100644 index 78ef3cc04..000000000 --- a/src/main/resources/hudson/plugins/ec2/ebs/ZPoolExpandNotice/message.jelly +++ /dev/null @@ -1,33 +0,0 @@ - - - -
-
- Your data directory in Jenkins (${app.rootDir} - AKA HUDSON_HOME) is getting close to full. - You should act on it before it's completely full. - - -
-
\ No newline at end of file From 9822497c9ae3db002bbb9a4aeb303b6baea8e0cd Mon Sep 17 00:00:00 2001 From: Valentin Delaye Date: Tue, 7 Jan 2025 06:47:49 +0100 Subject: [PATCH 090/267] Applied recipe MigrateToJenkinsBaseLineProperty --- pom.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index bf8133186..816d36173 100644 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,9 @@ THE SOFTWARE. 999999-SNAPSHOT - 2.452.4 + + 2.452 + ${jenkins.baseline}.4 jenkinsci/${project.artifactId}-plugin 1626 false @@ -84,7 +86,7 @@ THE SOFTWARE. io.jenkins.tools.bom - bom-2.452.x + bom-${jenkins.baseline}.x 3761.vd922730f0fd2 pom import @@ -131,7 +133,6 @@ THE SOFTWARE. org.jenkins-ci.plugins node-iterator-api - 55.v3b_77d4032326 org.jenkins-ci.plugins From 8225f35d196ee10e653e06675d3dbddb17bc839d Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Tue, 7 Jan 2025 15:58:34 +0100 Subject: [PATCH 091/267] Make remoting.jar readable for everyone --- src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java | 5 ++++- src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index 225fed07a..acbb6deb9 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -341,7 +341,10 @@ && executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { scp.upload( Jenkins.get().getJnlpJars("remoting.jar").readFully(), tmpDir + "/remoting.jar", - List.of(PosixFilePermission.OWNER_READ), + List.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.GROUP_READ, + PosixFilePermission.OTHERS_READ), scpTimestamp); final String jvmopts = node.jvmopts; diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index 243a67728..5930c56fa 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -330,7 +330,10 @@ && executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { scp.upload( Jenkins.get().getJnlpJars("remoting.jar").readFully(), tmpDir + "/remoting.jar", - List.of(PosixFilePermission.OWNER_READ), + List.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.GROUP_READ, + PosixFilePermission.OTHERS_READ), scpTimestamp); final String jvmopts = node.jvmopts; From bda7f3c0c564b0f8de73ec6fd2a5fbf4dc2f3caa Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Tue, 7 Jan 2025 15:59:22 +0100 Subject: [PATCH 092/267] Refactor PEMParser to KeyHelper --- .../hudson/plugins/ec2/ssh/EC2MacLauncher.java | 6 +++--- .../hudson/plugins/ec2/ssh/EC2UnixLauncher.java | 6 +++--- .../ec2/util/{PEMParser.java => KeyHelper.java} | 15 +++++++++++++-- .../hudson/plugins/ec2/util/ConnectionRule.java | 2 +- 4 files changed, 20 insertions(+), 9 deletions(-) rename src/main/java/hudson/plugins/ec2/util/{PEMParser.java => KeyHelper.java} (81%) diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index acbb6deb9..c081d8caa 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -46,7 +46,7 @@ import hudson.plugins.ec2.ssh.proxy.ProxyCONNECTListener; import hudson.plugins.ec2.ssh.verifiers.HostKey; import hudson.plugins.ec2.ssh.verifiers.Messages; -import hudson.plugins.ec2.util.PEMParser; +import hudson.plugins.ec2.util.KeyHelper; import hudson.remoting.Channel; import hudson.remoting.Channel.Listener; import hudson.slaves.CommandLauncher; @@ -215,7 +215,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) if (key == null) { isAuthenticated = false; } else { - cleanupClientSession.addPublicKeyIdentity(PEMParser.decodeKeyPair(key.getKeyMaterial(), "")); + cleanupClientSession.addPublicKeyIdentity(KeyHelper.decodeKeyPair(key.getKeyMaterial(), "")); cleanupClientSession.auth().await(timeout); isAuthenticated = cleanupClientSession.isAuthenticated(); } @@ -495,7 +495,7 @@ private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemp logInfo(computer, listener, "Authenticating as " + computer.getRemoteAdmin()); try { bootstrapSession = connectToSsh(client, computer, listener, template); - bootstrapSession.addPublicKeyIdentity(PEMParser.decodeKeyPair(key.getKeyMaterial(), "")); + bootstrapSession.addPublicKeyIdentity(KeyHelper.decodeKeyPair(key.getKeyMaterial(), "")); bootstrapSession.auth().await(timeout); isAuthenticated = bootstrapSession.isAuthenticated(); diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index 5930c56fa..a0d8afef7 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -47,7 +47,7 @@ import hudson.plugins.ec2.ssh.verifiers.HostKey; import hudson.plugins.ec2.ssh.verifiers.HostKeyHelper; import hudson.plugins.ec2.ssh.verifiers.Messages; -import hudson.plugins.ec2.util.PEMParser; +import hudson.plugins.ec2.util.KeyHelper; import hudson.remoting.Channel; import hudson.remoting.Channel.Listener; import hudson.slaves.CommandLauncher; @@ -217,7 +217,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) if (key == null) { isAuthenticated = false; } else { - cleanupClientSession.addPublicKeyIdentity(PEMParser.decodeKeyPair(key.getKeyMaterial(), "")); + cleanupClientSession.addPublicKeyIdentity(KeyHelper.decodeKeyPair(key.getKeyMaterial(), "")); cleanupClientSession.auth().await(timeout); isAuthenticated = cleanupClientSession.isAuthenticated(); } @@ -521,7 +521,7 @@ private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemp logInfo(computer, listener, "Authenticating as " + computer.getRemoteAdmin()); try { bootstrapSession = connectToSsh(client, computer, listener, template); - bootstrapSession.addPublicKeyIdentity(PEMParser.decodeKeyPair(key.getKeyMaterial(), "")); + bootstrapSession.addPublicKeyIdentity(KeyHelper.decodeKeyPair(key.getKeyMaterial(), "")); bootstrapSession.auth().await(timeout); isAuthenticated = bootstrapSession.isAuthenticated(); diff --git a/src/main/java/hudson/plugins/ec2/util/PEMParser.java b/src/main/java/hudson/plugins/ec2/util/KeyHelper.java similarity index 81% rename from src/main/java/hudson/plugins/ec2/util/PEMParser.java rename to src/main/java/hudson/plugins/ec2/util/KeyHelper.java index 6c1bbe720..cfe58df3b 100644 --- a/src/main/java/hudson/plugins/ec2/util/PEMParser.java +++ b/src/main/java/hudson/plugins/ec2/util/KeyHelper.java @@ -20,9 +20,20 @@ /** * Utility class to parse PEM. */ -public abstract class PEMParser { - private PEMParser() {} +public abstract class KeyHelper { + private KeyHelper() {} + /** + * Decodes a PEM-encoded key pair into a {@link KeyPair} object. This method supports + * various types of PEM input such as encrypted private keys, public keys, and key pairs. + * + * @param pem The PEM-formatted string containing the key data. + * @param password The password used to decrypt encrypted key pairs, if applicable. Can be null if no password is required. + * @return A {@link KeyPair} containing the public and private keys. If a public key is provided without a matching private key, + * the private key in the returned {@link KeyPair} will be null. + * @throws IOException If an error occurs during parsing or decryption of the PEM input. + * @throws IllegalArgumentException If the provided PEM input cannot be parsed or is of an unsupported type. + */ public static KeyPair decodeKeyPair(String pem, String password) throws IOException { try (org.bouncycastle.openssl.PEMParser pemParser = new org.bouncycastle.openssl.PEMParser(new StringReader(pem))) { diff --git a/src/test/java/hudson/plugins/ec2/util/ConnectionRule.java b/src/test/java/hudson/plugins/ec2/util/ConnectionRule.java index 2b23bb640..ff119405a 100644 --- a/src/test/java/hudson/plugins/ec2/util/ConnectionRule.java +++ b/src/test/java/hudson/plugins/ec2/util/ConnectionRule.java @@ -73,7 +73,7 @@ public ClientSession connect(ServerKeyVerifier verifier) throws Exception { ConnectFuture connectFuture = sshClient.connect(USER, ip, port); connection = connectFuture.verify().getSession(); - connection.addPublicKeyIdentity(PEMParser.decodeKeyPair(privateKey, null)); + connection.addPublicKeyIdentity(KeyHelper.decodeKeyPair(privateKey, null)); connection.auth().await(Duration.ofSeconds(10)); assertTrue(connection.isAuthenticated()); From 471b5d0151b3c0d3907a0be7d242ff08a3e8d82f Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Tue, 7 Jan 2025 16:00:00 +0100 Subject: [PATCH 093/267] Fix ServerKeyVerifier implementation --- .../plugins/ec2/ssh/EC2MacLauncher.java | 9 ++- .../plugins/ec2/ssh/EC2UnixLauncher.java | 9 ++- .../hudson/plugins/ec2/util/KeyHelper.java | 41 ++++++++++ .../plugins/ec2/util/KeyHelperTest.java | 81 +++++++++++++++++++ 4 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 src/test/java/hudson/plugins/ec2/util/KeyHelperTest.java diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index c081d8caa..2f1a022dc 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -626,15 +626,16 @@ public ServerKeyVerifierImpl(final EC2Computer computer, final TaskListener list @Override public boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) { + String sshAlgorithm = KeyHelper.getSshAlgorithm(serverKey); + if (sshAlgorithm == null) { + return false; + } SlaveTemplate template = computer.getSlaveTemplate(); try { return template != null && template.getHostKeyVerificationStrategy() .getStrategy() - .verify( - computer, - new HostKey(serverKey.getAlgorithm(), serverKey.getEncoded()), - listener); + .verify(computer, new HostKey(sshAlgorithm, serverKey.getEncoded()), listener); } catch (Exception exception) { // false will trigger a SSHException which is a subclass of IOException. // Therefore, it is not needed to throw a RuntimeException. diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index a0d8afef7..3f2805bb2 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -651,15 +651,16 @@ public ServerKeyVerifierImpl(final EC2Computer computer, final TaskListener list @Override public boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) { + String sshAlgorithm = KeyHelper.getSshAlgorithm(serverKey); + if (sshAlgorithm == null) { + return false; + } SlaveTemplate template = computer.getSlaveTemplate(); try { return template != null && template.getHostKeyVerificationStrategy() .getStrategy() - .verify( - computer, - new HostKey(serverKey.getAlgorithm(), serverKey.getEncoded()), - listener); + .verify(computer, new HostKey(sshAlgorithm, serverKey.getEncoded()), listener); } catch (Exception exception) { // false will trigger a SSHException which is a subclass of IOException. // Therefore, it is not needed to throw a RuntimeException. diff --git a/src/main/java/hudson/plugins/ec2/util/KeyHelper.java b/src/main/java/hudson/plugins/ec2/util/KeyHelper.java index cfe58df3b..0d16fac13 100644 --- a/src/main/java/hudson/plugins/ec2/util/KeyHelper.java +++ b/src/main/java/hudson/plugins/ec2/util/KeyHelper.java @@ -7,7 +7,11 @@ import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECField; +import java.security.spec.ECParameterSpec; import java.security.spec.ECPrivateKeySpec; +import java.security.spec.EllipticCurve; import java.security.spec.InvalidKeySpecException; import java.security.spec.RSAPrivateCrtKeySpec; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; @@ -87,4 +91,41 @@ private static PublicKey generatePublicKeyFromPrivateKey(PrivateKey privateKey) return null; } } + + /** + * Determines the SSH algorithm identifier corresponding to the given server public key. + * This method matches the key type to the appropriate SSH algorithm string. + * When an {@link ECPublicKey} is given, an NIST curse will be assumed. + * + * @param serverKey The server's {@link PublicKey} object for which the SSH algorithm identifier + * needs to be determined. + * @return A {@code String} representing the SSH algorithm identifier for the given server key, + * or {@code null} if the key type is unsupported or cannot be determined. + */ + public static String getSshAlgorithm(PublicKey serverKey) { + switch (serverKey.getAlgorithm()) { + case "RSA": + return "ssh-rsa"; + case "EC": + if (serverKey instanceof ECPublicKey) { + ECPublicKey ecPublicKey = (ECPublicKey) serverKey; + ECParameterSpec params = ecPublicKey.getParams(); + if (params != null) { + + EllipticCurve curve = params.getCurve(); + ECField field = (curve != null) ? curve.getField() : null; + if (field != null) { + int fieldSize = field.getFieldSize(); + // Assume NIST curve + return "ecdsa-sha2-nistp" + fieldSize; + } + } + } + return null; + case "EdDSA": + return "ssh-ed25519"; + default: + return null; + } + } } diff --git a/src/test/java/hudson/plugins/ec2/util/KeyHelperTest.java b/src/test/java/hudson/plugins/ec2/util/KeyHelperTest.java new file mode 100644 index 000000000..92eec75e0 --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/util/KeyHelperTest.java @@ -0,0 +1,81 @@ +package hudson.plugins.ec2.util; + +import static org.junit.Assert.assertEquals; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PublicKey; +import java.security.spec.ECGenParameterSpec; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class KeyHelperTest { + + public static final PublicKey MOCK_PUBLIC_KEY = new PublicKey() { + @Override + public String getAlgorithm() { + return "Mock"; + } + + @Override + public String getFormat() { + return "Mock"; + } + + @Override + public byte[] getEncoded() { + return new byte[0]; + } + }; + + public KeyHelperTest(String description, PublicKey publicKey, String expected) { + this.description = description; + this.publicKey = publicKey; + this.expected = expected; + } + + private final String description; + + private final PublicKey publicKey; + + private final String expected; + + @Parameterized.Parameters + public static Object[] data() throws Exception { + return new Object[][] { + {"EC curve NIST P-256", generateECKey("secp256r1").getPublic(), "ecdsa-sha2-nistp256"}, + {"EC curve NIST P-384", generateECKey("secp384r1").getPublic(), "ecdsa-sha2-nistp384"}, + {"EC curve NIST P-521", generateECKey("secp521r1").getPublic(), "ecdsa-sha2-nistp521"}, + {"RSA 1024", generateRSAKey(1024).getPublic(), "ssh-rsa"}, + {"RSA 2048", generateRSAKey(2048).getPublic(), "ssh-rsa"}, + {"RSA 4096", generateRSAKey(4096).getPublic(), "ssh-rsa"}, + {"EdDSA", generateEdDSAKey().getPublic(), "ssh-ed25519"}, + {"unknown", MOCK_PUBLIC_KEY, null} + }; + } + + @Test + public void testSSHAlgorithm() throws Exception { + assertEquals(description, expected, KeyHelper.getSshAlgorithm(publicKey)); + } + + public static KeyPair generateECKey(String curveName) throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC"); + ECGenParameterSpec ecSpec = new ECGenParameterSpec(curveName); + keyPairGenerator.initialize(ecSpec); + return keyPairGenerator.generateKeyPair(); + } + + public static KeyPair generateRSAKey(int size) throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(size); + return keyPairGenerator.generateKeyPair(); + } + + public static KeyPair generateEdDSAKey() throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EdDSA"); + return keyPairGenerator.generateKeyPair(); + } +} From 3bc68a3f75f5f5840ce1be46acd12a0c7d3ede28 Mon Sep 17 00:00:00 2001 From: Mohamed Mansour Date: Wed, 8 Jan 2025 06:47:10 -0800 Subject: [PATCH 094/267] JENKINS-75098:Log EC2 errors while calling RunInstances for reserved capacity (#1032) * Log EC2 errors while calling RunInstances for reserved capacity * Address format violations --- .../hudson/plugins/ec2/SlaveTemplate.java | 7 ++- .../hudson/plugins/ec2/SlaveTemplateTest.java | 55 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java index adaa017f9..59d984060 100644 --- a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java +++ b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java @@ -2029,7 +2029,12 @@ private List provisionOndemand( } } } else { - newInstances = ec2.runInstances(riRequest).getReservation().getInstances(); + try { + newInstances = ec2.runInstances(riRequest).getReservation().getInstances(); + } catch (AmazonEC2Exception e) { + logProvisionInfo("Error while trying to run instances for reserved capacity: " + e.getMessage()); + throw e; + } } // Have to create a new instance diff --git a/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java b/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java index 9ec683015..d75ed108b 100644 --- a/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java +++ b/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java @@ -1440,6 +1440,61 @@ public void provisionOnDemandSetsMetadataDefaultOptions() throws Exception { assertEquals(metadataOptionsRequest.getHttpPutResponseHopLimit(), Integer.valueOf(1)); } + @Test(expected = AmazonEC2Exception.class) + public void provisionOnDemandSetsMetadataDefaultOptionsWithEC2Exception() throws Exception { + SlaveTemplate template = new SlaveTemplate( + TEST_AMI, + TEST_ZONE, + TEST_SPOT_CFG, + TEST_SEC_GROUPS, + TEST_REMOTE_FS, + TEST_INSTANCE_TYPE, + TEST_EBSO, + TEST_LABEL, + Node.Mode.NORMAL, + "", + "bar", + "bbb", + "aaa", + "10", + "fff", + null, + "java", + "-Xmx1g", + false, + "subnet 456", + null, + null, + 0, + 0, + null, + "", + true, + false, + "", + false, + "", + true, + false, + false, + ConnectionStrategy.PUBLIC_IP, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + null, + true, + null, + true); + + AmazonEC2 mockedEC2 = setupTestForProvisioning(template); + when(mockedEC2.runInstances(any(RunInstancesRequest.class))) + .thenThrow(new AmazonEC2Exception("InsufficientInstanceCapacity")); + + template.provision(2, EnumSet.noneOf(ProvisionOptions.class)); + } + private HtmlForm getConfigForm(EC2Cloud ac) throws IOException, SAXException { return r.createWebClient().goTo(ac.getUrl() + "configure").getFormByName("config"); } From 9f45558ca32bb6391497f8bc121db5d5c0bb0e9e Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Wed, 8 Jan 2025 19:03:34 +0100 Subject: [PATCH 095/267] Fix hudson-run-init verification --- src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java | 2 +- src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index 2f1a022dc..f9cb6790d 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -241,7 +241,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) if (initScript != null && !initScript.trim().isEmpty() - && executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { + && !executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { logInfo(computer, listener, "Executing init script"); scp.upload( initScript.getBytes(StandardCharsets.UTF_8), diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index 3f2805bb2..2bdf8e6d1 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -243,7 +243,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) if (initScript != null && !initScript.trim().isEmpty() - && executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { + && !executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { logInfo(computer, listener, "Executing init script"); scp.upload( initScript.getBytes(StandardCharsets.UTF_8), From 526399543dcab2da028fd42e442a1069f239e704 Mon Sep 17 00:00:00 2001 From: Mohamed Mansour Date: Fri, 10 Jan 2025 06:15:57 -0800 Subject: [PATCH 096/267] Update log message from EC2 RunInstances call to include number of nodes requested (#1033) * Log EC2 errors while calling RunInstances for reserved capacity * Update log message to include number of nodes requested --- src/main/java/hudson/plugins/ec2/SlaveTemplate.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java index 59d984060..935c6c71f 100644 --- a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java +++ b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java @@ -2032,7 +2032,9 @@ private List provisionOndemand( try { newInstances = ec2.runInstances(riRequest).getReservation().getInstances(); } catch (AmazonEC2Exception e) { - logProvisionInfo("Error while trying to run instances for reserved capacity: " + e.getMessage()); + logProvisionInfo("Jenkins attempted to reserve " + + riRequest.getMaxCount() + + " instances and received this EC2 exception: " + e.getMessage()); throw e; } } From 480c37fde26036956b615adee31058f5e853838f Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Wed, 15 Jan 2025 10:56:25 +0100 Subject: [PATCH 097/267] Replace try-with-resource by manual close. Use the timeout from the node. --- .../plugins/ec2/ssh/EC2MacLauncher.java | 388 ++++++++++-------- .../plugins/ec2/ssh/EC2UnixLauncher.java | 382 +++++++++-------- 2 files changed, 429 insertions(+), 341 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index f9cb6790d..71fb92f3e 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -104,8 +104,6 @@ public class EC2MacLauncher extends EC2ComputerLauncher { private static int readinessSleepMs = 1000; private static int readinessTries = 120; - private static final long timeout = Duration.ofSeconds(10).toMillis(); - static { String prop = System.getProperty(BOOTSTRAP_AUTH_SLEEP_MS); if (prop != null) { @@ -152,13 +150,20 @@ protected String buildUpCommand(EC2Computer computer, String command) { @Override protected void launchScript(EC2Computer computer, TaskListener listener) throws IOException, AmazonClientException, InterruptedException { + final SshClient client; + SshClient cleanupClient = null; + final CloseableScpClient scpClient; + CloseableScpClient cleanupScpClient = null; final ClientSession clientSession; ClientSession cleanupClientSession = null; // java's code path analysis for final // doesn't work that well. + final ClientChannel agentExecChannel; + ClientChannel cleanupAgentExecChannel = null; boolean successful = false; PrintStream logger = listener.getLogger(); EC2AbstractSlave node = computer.getNode(); SlaveTemplate template = computer.getSlaveTemplate(); + final long timeout = node == null ? 0L : node.getLaunchTimeoutInMillis(); if (node == null) { throw new IllegalStateException(); @@ -193,7 +198,8 @@ protected void launchScript(EC2Computer computer, TaskListener listener) logInfo(computer, listener, "Launching instance: " + node.getInstanceId()); - try (SshClient client = SshClient.setUpDefaultClient()) { + try { + cleanupClient = SshClient.setUpDefaultClient(); boolean isBootstrapped = bootstrap(computer, listener, template); if (isBootstrapped) { int bootDelay = node.getBootDelay(); @@ -208,7 +214,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) // connect fresh as ROOT logInfo(computer, listener, "connect fresh as root"); - cleanupClientSession = connectToSsh(client, computer, listener, template); + cleanupClientSession = connectToSsh(cleanupClient, computer, listener, template); KeyPair key = computer.getCloud().getKeyPair(); final boolean isAuthenticated; @@ -227,194 +233,231 @@ protected void launchScript(EC2Computer computer, TaskListener listener) logWarning(computer, listener, "bootstrapresult failed"); return; // bootstrap closed for us. } + client = cleanupClient; clientSession = cleanupClientSession; - try (CloseableScpClient scp = createScpClient(clientSession)) { - String timestamp = Duration.ofMillis(System.currentTimeMillis()).toSeconds() + " 0"; - ScpTimestampCommandDetails scpTimestamp = - ScpTimestampCommandDetails.parse("T" + timestamp + " " + timestamp); - String initScript = node.initScript; - String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); - - logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); - executeRemote(clientSession, "mkdir -p " + tmpDir, logger); - - if (initScript != null - && !initScript.trim().isEmpty() - && !executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { - logInfo(computer, listener, "Executing init script"); - scp.upload( - initScript.getBytes(StandardCharsets.UTF_8), - tmpDir + "/init.sh", - List.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_READ), - scpTimestamp); - - String initCommand = buildUpCommand(computer, tmpDir + "/init.sh"); - try (ClientChannel channel = clientSession.createExecChannel( - initCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - - OutputStream invertedIn = channel.getInvertedIn(); - if (invertedIn != null) { - invertedIn.close(); // nothing to write here - } - channel.open().await(timeout); + scpClient = createScpClient(clientSession); + cleanupScpClient = scpClient; + String timestamp = Duration.ofMillis(System.currentTimeMillis()).toSeconds() + " 0"; + ScpTimestampCommandDetails scpTimestamp = + ScpTimestampCommandDetails.parse("T" + timestamp + " " + timestamp); + String initScript = node.initScript; + String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); + + logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); + executeRemote(clientSession, "mkdir -p " + tmpDir, logger); + + if (initScript != null + && !initScript.trim().isEmpty() + && !executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { + logInfo(computer, listener, "Executing init script"); + scpClient.upload( + initScript.getBytes(StandardCharsets.UTF_8), + tmpDir + "/init.sh", + List.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_READ), + scpTimestamp); - Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); + String initCommand = buildUpCommand(computer, tmpDir + "/init.sh"); + try (ClientChannel channel = clientSession.createExecChannel( + initCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { - logWarning(computer, listener, "init script timed out"); - return; - } + channel.open().await(timeout); - int exitStatus = waitCompletion(channel); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } + OutputStream invertedIn = channel.getInvertedIn(); + if (invertedIn != null) { + invertedIn.close(); // nothing to write here + } - InputStream invertedErr = channel.getInvertedErr(); - if (invertedErr != null) { - invertedErr.close(); // we are not supposed to get anything from stderr - } - IOUtils.copy(channel.getInvertedOut(), logger); + Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); + + if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { + logWarning(computer, listener, "init script timed out"); + return; } - logInfo(computer, listener, "Creating ~/.hudson-run-init"); - String createHudsonRunInitCommand = buildUpCommand(computer, "touch ~/.hudson-run-init"); - try (ClientChannel channel = clientSession.createExecChannel( - createHudsonRunInitCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - OutputStream invertedIn = channel.getInvertedIn(); - if (invertedIn != null) { - invertedIn.close(); // nothing to write here - } - channel.open().await(timeout); + int exitStatus = waitCompletion(channel); + if (exitStatus != 0) { + logWarning(computer, listener, "init script failed: exit code=" + exitStatus); + return; + } - Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); + InputStream invertedErr = channel.getInvertedErr(); + if (invertedErr != null) { + invertedErr.close(); // we are not supposed to get anything from stderr + } + IOUtils.copy(channel.getInvertedOut(), logger); + } - if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { - logWarning(computer, listener, "init script timed out"); - return; - } + logInfo(computer, listener, "Creating ~/.hudson-run-init"); + String createHudsonRunInitCommand = buildUpCommand(computer, "touch ~/.hudson-run-init"); + try (ClientChannel channel = clientSession.createExecChannel( + createHudsonRunInitCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { + OutputStream invertedIn = channel.getInvertedIn(); + if (invertedIn != null) { + invertedIn.close(); // nothing to write here + } + channel.open().await(timeout); - int exitStatus = waitCompletion(channel); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } + Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); - InputStream invertedErr = channel.getInvertedErr(); - if (invertedErr != null) { - invertedErr.close(); // we are not supposed to get anything from stderr - } - IOUtils.copy(channel.getInvertedOut(), logger); + if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { + logWarning(computer, listener, "init script timed out"); + return; + } + + int exitStatus = waitCompletion(channel); + if (exitStatus != 0) { + logWarning(computer, listener, "init script failed: exit code=" + exitStatus); + return; } + + InputStream invertedErr = channel.getInvertedErr(); + if (invertedErr != null) { + invertedErr.close(); // we are not supposed to get anything from stderr + } + IOUtils.copy(channel.getInvertedOut(), logger); + } + } + + // TODO: parse the version number. maven-enforcer-plugin might help + final String javaPath = node.javaPath; + try { + Instance nodeInstance = computer.describeInstance(); + if (nodeInstance.getInstanceType().equals("mac2.metal")) { + LOGGER.info("Running Command for mac2.metal"); + executeRemote( + computer, + clientSession, + javaPath + " -fullversion", + "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-aarch64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-aarch64-macos-jdk.pkg -target /", + logger, + listener); + } else { + executeRemote( + computer, + clientSession, + javaPath + " -fullversion", + "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-x64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-x64-macos-jdk.pkg -target /", + logger, + listener); } + } catch (InterruptedException ex) { + LOGGER.warning(ex.getMessage()); + } + + // Always copy so we get the most recent remoting.jar + logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); + scpClient.upload( + Jenkins.get().getJnlpJars("remoting.jar").readFully(), + tmpDir + "/remoting.jar", + List.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.GROUP_READ, + PosixFilePermission.OTHERS_READ), + scpTimestamp); + + final String jvmopts = node.jvmopts; + final String prefix = computer.getSlaveCommandPrefix(); + final String suffix = computer.getSlaveCommandSuffix(); + final String remoteFS = node.getRemoteFS(); + final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; + String launchString = prefix + + " " + + javaPath + + " " + + (jvmopts != null ? jvmopts : "") + + " -jar " + + tmpDir + + "/remoting.jar -workDir " + + workDir + + suffix; + // launchString = launchString.trim(); + + SlaveTemplate slaveTemplate = computer.getSlaveTemplate(); + + if (slaveTemplate != null && slaveTemplate.isConnectBySSHProcess()) { + File identityKeyFile = createIdentityKeyFile(computer); - // TODO: parse the version number. maven-enforcer-plugin might help - final String javaPath = node.javaPath; try { - Instance nodeInstance = computer.describeInstance(); - if (nodeInstance.getInstanceType().equals("mac2.metal")) { - LOGGER.info("Running Command for mac2.metal"); - executeRemote( - computer, - clientSession, - javaPath + " -fullversion", - "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-aarch64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-aarch64-macos-jdk.pkg -target /", - logger, - listener); - } else { - executeRemote( - computer, - clientSession, - javaPath + " -fullversion", - "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-x64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-x64-macos-jdk.pkg -target /", - logger, - listener); + // Obviously the controller must have an installed ssh client. + // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag + String sshClientLaunchString = String.format( + "ssh -o StrictHostKeyChecking=%s -i %s %s@%s -p %d %s", + slaveTemplate.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), + identityKeyFile.getAbsolutePath(), + node.remoteAdmin, + getEC2HostAddress(computer, template), + node.getSshPort(), + launchString); + + logInfo( + computer, + listener, + "Launching remoting agent (via SSH client process): " + sshClientLaunchString); + CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); + commandLauncher.launch(computer, listener); + } finally { + if (!identityKeyFile.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete identity key file"); } - } catch (InterruptedException ex) { - LOGGER.warning(ex.getMessage()); } + } else { + logInfo(computer, listener, "Launching remoting agent (via SSH2 Connection): " + launchString); - // Always copy so we get the most recent remoting.jar - logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); - scp.upload( - Jenkins.get().getJnlpJars("remoting.jar").readFully(), - tmpDir + "/remoting.jar", - List.of( - PosixFilePermission.OWNER_READ, - PosixFilePermission.GROUP_READ, - PosixFilePermission.OTHERS_READ), - scpTimestamp); + agentExecChannel = clientSession.createExecChannel( + launchString, StandardCharsets.US_ASCII, null, Collections.emptyMap()); + agentExecChannel.setRedirectErrorStream(true); + agentExecChannel.open().verify(timeout); - final String jvmopts = node.jvmopts; - final String prefix = computer.getSlaveCommandPrefix(); - final String suffix = computer.getSlaveCommandSuffix(); - final String remoteFS = node.getRemoteFS(); - final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; - String launchString = prefix - + " " - + javaPath - + " " - + (jvmopts != null ? jvmopts : "") - + " -jar " - + tmpDir - + "/remoting.jar -workDir " - + workDir - + suffix; - // launchString = launchString.trim(); - - SlaveTemplate slaveTemplate = computer.getSlaveTemplate(); - - if (slaveTemplate != null && slaveTemplate.isConnectBySSHProcess()) { - File identityKeyFile = createIdentityKeyFile(computer); - - try { - // Obviously the controller must have an installed ssh client. - // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag - String sshClientLaunchString = String.format( - "ssh -o StrictHostKeyChecking=%s -i %s %s@%s -p %d %s", - slaveTemplate.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), - identityKeyFile.getAbsolutePath(), - node.remoteAdmin, - getEC2HostAddress(computer, template), - node.getSshPort(), - launchString); - - logInfo( - computer, - listener, - "Launching remoting agent (via SSH client process): " + sshClientLaunchString); - CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); - commandLauncher.launch(computer, listener); - } finally { - if (!identityKeyFile.delete()) { - LOGGER.log(Level.WARNING, "Failed to delete identity key file"); + cleanupAgentExecChannel = agentExecChannel; + InputStream invertedOut = agentExecChannel.getInvertedOut(); + OutputStream invertedIn = agentExecChannel.getInvertedIn(); + + computer.setChannel(invertedOut, invertedIn, logger, new Listener() { + + @Override + public void onClosed(Channel channel, IOException cause) { + try { + agentExecChannel.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Channel Listener Error when closing the channel", e); + } + try { + scpClient.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Channel Listener Error when closing the SCP session", e); + } + try { + clientSession.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Error when closing the session", e); + } + try { + client.stop(); + client.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Channel Listener Error when closing the client", e); } } - } else { - logInfo(computer, listener, "Launching remoting agent (via SSH2 Connection): " + launchString); - - try (ClientChannel channel = clientSession.createExecChannel( - launchString, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - computer.setChannel(channel.getInvertedOut(), channel.getInvertedIn(), logger, new Listener() { - @Override - public void onClosed(Channel channel, IOException cause) { - try { - clientSession.close(); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Error when closing the session", e); - } - } - }); - } - } - - successful = true; + }); } + + successful = true; } finally { - if (cleanupClientSession != null && !successful) { - cleanupClientSession.close(); + if (!successful || template.isConnectBySSHProcess()) { + if (cleanupAgentExecChannel != null) { + cleanupAgentExecChannel.close(); + } + if (cleanupScpClient != null) { + cleanupScpClient.close(); + } + if (cleanupClientSession != null) { + cleanupClientSession.close(); + } + if (cleanupClient != null) { + cleanupClient.stop(); + cleanupClient.close(); + } } } } @@ -434,12 +477,11 @@ private boolean executeRemote( String checkCommand, String command, PrintStream logger, - TaskListener listener) - throws IOException, InterruptedException { + TaskListener listener) { logInfo(computer, listener, "Verifying: " + checkCommand); - if (executeRemote(clientSession, checkCommand, logger)) { + if (!executeRemote(clientSession, checkCommand, logger)) { logInfo(computer, listener, "Installing: " + command); - if (executeRemote(clientSession, command, logger)) { + if (!executeRemote(clientSession, command, logger)) { logWarning(computer, listener, "Failed to install: " + command); return false; } @@ -476,6 +518,8 @@ private File createIdentityKeyFile(EC2Computer computer) throws IOException { private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemplate template) throws IOException, InterruptedException, AmazonClientException { logInfo(computer, listener, "bootstrap()"); + final EC2AbstractSlave node = computer.getNode(); + final long timeout = node == null ? 0L : node.getLaunchTimeoutInMillis(); ClientSession bootstrapSession = null; try (SshClient client = SshClient.setUpDefaultClient()) { int tries = bootstrapAuthTries; diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index 2bdf8e6d1..d2c895b68 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -106,8 +106,6 @@ public class EC2UnixLauncher extends EC2ComputerLauncher { private static int readinessSleepMs = 1000; private static int readinessTries = 120; - private static final long timeout = Duration.ofSeconds(10).toMillis(); - static { String prop = System.getProperty(BOOTSTRAP_AUTH_SLEEP_MS); if (prop != null) { @@ -154,13 +152,20 @@ protected String buildUpCommand(EC2Computer computer, String command) { @Override protected void launchScript(EC2Computer computer, TaskListener listener) throws IOException, AmazonClientException, InterruptedException { + final SshClient client; + SshClient cleanupClient = null; + final CloseableScpClient scpClient; + CloseableScpClient cleanupScpClient = null; final ClientSession clientSession; ClientSession cleanupClientSession = null; // java's code path analysis for final // doesn't work that well. + final ClientChannel agentExecChannel; + ClientChannel cleanupAgentExecChannel = null; boolean successful = false; PrintStream logger = listener.getLogger(); EC2AbstractSlave node = computer.getNode(); SlaveTemplate template = computer.getSlaveTemplate(); + final long timeout = node == null ? 0L : node.getLaunchTimeoutInMillis(); if (node == null) { throw new IllegalStateException(); @@ -195,7 +200,8 @@ protected void launchScript(EC2Computer computer, TaskListener listener) logInfo(computer, listener, "Launching instance: " + node.getInstanceId()); - try (SshClient client = SshClient.setUpDefaultClient()) { + try { + cleanupClient = SshClient.setUpDefaultClient(); boolean isBootstrapped = bootstrap(computer, listener, template); if (isBootstrapped) { int bootDelay = node.getBootDelay(); @@ -210,7 +216,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) // connect fresh as ROOT logInfo(computer, listener, "connect fresh as root"); - cleanupClientSession = connectToSsh(client, computer, listener, template); + cleanupClientSession = connectToSsh(cleanupClient, computer, listener, template); KeyPair key = computer.getCloud().getKeyPair(); final boolean isAuthenticated; @@ -229,191 +235,228 @@ protected void launchScript(EC2Computer computer, TaskListener listener) logWarning(computer, listener, "bootstrapresult failed"); return; // bootstrap closed for us. } + client = cleanupClient; clientSession = cleanupClientSession; - try (CloseableScpClient scp = createScpClient(clientSession)) { - String timestamp = Duration.ofMillis(System.currentTimeMillis()).toSeconds() + " 0"; - ScpTimestampCommandDetails scpTimestamp = - ScpTimestampCommandDetails.parse("T" + timestamp + " " + timestamp); - String initScript = node.initScript; - String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); - - logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); - executeRemote(clientSession, "mkdir -p " + tmpDir, logger); - - if (initScript != null - && !initScript.trim().isEmpty() - && !executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { - logInfo(computer, listener, "Executing init script"); - scp.upload( - initScript.getBytes(StandardCharsets.UTF_8), - tmpDir + "/init.sh", - List.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_READ), - scpTimestamp); - - String initCommand = buildUpCommand(computer, tmpDir + "/init.sh"); - try (ClientChannel channel = clientSession.createExecChannel( - initCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - - OutputStream invertedIn = channel.getInvertedIn(); - if (invertedIn != null) { - invertedIn.close(); // nothing to write here - } - channel.open().await(timeout); + scpClient = createScpClient(clientSession); + cleanupScpClient = scpClient; + String timestamp = Duration.ofMillis(System.currentTimeMillis()).toSeconds() + " 0"; + ScpTimestampCommandDetails scpTimestamp = + ScpTimestampCommandDetails.parse("T" + timestamp + " " + timestamp); + String initScript = node.initScript; + String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); + + logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); + executeRemote(clientSession, "mkdir -p " + tmpDir, logger); + + if (initScript != null + && !initScript.trim().isEmpty() + && !executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { + logInfo(computer, listener, "Executing init script"); + scpClient.upload( + initScript.getBytes(StandardCharsets.UTF_8), + tmpDir + "/init.sh", + List.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_READ), + scpTimestamp); - Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); + String initCommand = buildUpCommand(computer, tmpDir + "/init.sh"); + try (ClientChannel channel = clientSession.createExecChannel( + initCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { - logWarning(computer, listener, "init script timed out"); - return; - } + channel.open().await(timeout); - int exitStatus = waitCompletion(channel); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } + OutputStream invertedIn = channel.getInvertedIn(); + if (invertedIn != null) { + invertedIn.close(); // nothing to write here + } - InputStream invertedErr = channel.getInvertedErr(); - if (invertedErr != null) { - invertedErr.close(); // we are not supposed to get anything from stderr - } - IOUtils.copy(channel.getInvertedOut(), logger); + Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); + + if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { + logWarning(computer, listener, "init script timed out"); + return; } - logInfo(computer, listener, "Creating ~/.hudson-run-init"); + int exitStatus = waitCompletion(channel); + if (exitStatus != 0) { + logWarning(computer, listener, "init script failed: exit code=" + exitStatus); + return; + } - String createHudsonRunInitCommand = buildUpCommand(computer, "touch ~/.hudson-run-init"); - try (ClientChannel channel = clientSession.createExecChannel( - createHudsonRunInitCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - OutputStream invertedIn = channel.getInvertedIn(); - if (invertedIn != null) { - invertedIn.close(); // nothing to write here - } - channel.open().await(timeout); + InputStream invertedErr = channel.getInvertedErr(); + if (invertedErr != null) { + invertedErr.close(); // we are not supposed to get anything from stderr + } + IOUtils.copy(channel.getInvertedOut(), logger); + } - Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); + logInfo(computer, listener, "Creating ~/.hudson-run-init"); - if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { - logWarning(computer, listener, "init script timed out"); - return; - } + String createHudsonRunInitCommand = buildUpCommand(computer, "touch ~/.hudson-run-init"); + try (ClientChannel channel = clientSession.createExecChannel( + createHudsonRunInitCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { + OutputStream invertedIn = channel.getInvertedIn(); + if (invertedIn != null) { + invertedIn.close(); // nothing to write here + } + channel.open().await(timeout); - int exitStatus = waitCompletion(channel); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } + Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); - InputStream invertedErr = channel.getInvertedErr(); - if (invertedErr != null) { - invertedErr.close(); // we are not supposed to get anything from stderr - } - IOUtils.copy(channel.getInvertedOut(), logger); + if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { + logWarning(computer, listener, "init script timed out"); + return; } - } - // TODO: parse the version number. maven-enforcer-plugin might help - final String javaPath = node.javaPath; - executeRemote( - computer, - clientSession, - javaPath + " -fullversion", - "sudo amazon-linux-extras install java-openjdk11 -y; sudo yum install -y fontconfig java-11-openjdk", - logger, - listener); - executeRemote( - computer, clientSession, "which scp", "sudo yum install -y openssh-clients", logger, listener); - - // Always copy so we get the most recent remoting.jar - logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); - scp.upload( - Jenkins.get().getJnlpJars("remoting.jar").readFully(), - tmpDir + "/remoting.jar", - List.of( - PosixFilePermission.OWNER_READ, - PosixFilePermission.GROUP_READ, - PosixFilePermission.OTHERS_READ), - scpTimestamp); + int exitStatus = waitCompletion(channel); + if (exitStatus != 0) { + logWarning(computer, listener, "init script failed: exit code=" + exitStatus); + return; + } - final String jvmopts = node.jvmopts; - final String prefix = computer.getSlaveCommandPrefix(); - final String suffix = computer.getSlaveCommandSuffix(); - final String remoteFS = node.getRemoteFS(); - final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; - String launchString = prefix - + " " - + javaPath - + " " - + (jvmopts != null ? jvmopts : "") - + " -jar " - + tmpDir - + "/remoting.jar -workDir " - + workDir - + suffix; - // launchString = launchString.trim(); - - if (template.isConnectBySSHProcess()) { - File identityKeyFile = createIdentityKeyFile(computer); - String ec2HostAddress = getEC2HostAddress(computer, template); - File hostKeyFile = createHostKeyFile(computer, ec2HostAddress, listener); - String userKnownHostsFileFlag = ""; - if (hostKeyFile != null) { - userKnownHostsFileFlag = - String.format(" -o \"UserKnownHostsFile=%s\"", hostKeyFile.getAbsolutePath()); + InputStream invertedErr = channel.getInvertedErr(); + if (invertedErr != null) { + invertedErr.close(); // we are not supposed to get anything from stderr } + IOUtils.copy(channel.getInvertedOut(), logger); + } + } - try { - // Obviously the controller must have an installed ssh client. - // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag - String sshClientLaunchString = String.format( - "ssh -o StrictHostKeyChecking=%s%s%s -i %s %s@%s -p %d %s", - template.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), - userKnownHostsFileFlag, - getEC2HostKeyAlgorithmFlag(computer), - identityKeyFile.getAbsolutePath(), - node.remoteAdmin, - ec2HostAddress, - node.getSshPort(), - launchString); - - logInfo( - computer, - listener, - "Launching remoting agent (via SSH client process): " + sshClientLaunchString); - CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); - commandLauncher.launch(computer, listener); - } finally { - if (!identityKeyFile.delete()) { - LOGGER.log(Level.WARNING, "Failed to delete identity key file"); - } - if (hostKeyFile != null && !hostKeyFile.delete()) { - LOGGER.log(Level.WARNING, "Failed to delete host key file"); - } + // TODO: parse the version number. maven-enforcer-plugin might help + final String javaPath = node.javaPath; + executeRemote( + computer, + clientSession, + javaPath + " -fullversion", + "sudo amazon-linux-extras install java-openjdk11 -y; sudo yum install -y fontconfig java-11-openjdk", + logger, + listener); + executeRemote( + computer, clientSession, "which scp", "sudo yum install -y openssh-clients", logger, listener); + + // Always copy so we get the most recent remoting.jar + logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); + scpClient.upload( + Jenkins.get().getJnlpJars("remoting.jar").readFully(), + tmpDir + "/remoting.jar", + List.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.GROUP_READ, + PosixFilePermission.OTHERS_READ), + scpTimestamp); + + final String jvmopts = node.jvmopts; + final String prefix = computer.getSlaveCommandPrefix(); + final String suffix = computer.getSlaveCommandSuffix(); + final String remoteFS = node.getRemoteFS(); + final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; + String launchString = prefix + + " " + + javaPath + + " " + + (jvmopts != null ? jvmopts : "") + + " -jar " + + tmpDir + + "/remoting.jar -workDir " + + workDir + + suffix; + // launchString = launchString.trim(); + + if (template.isConnectBySSHProcess()) { + File identityKeyFile = createIdentityKeyFile(computer); + String ec2HostAddress = getEC2HostAddress(computer, template); + File hostKeyFile = createHostKeyFile(computer, ec2HostAddress, listener); + String userKnownHostsFileFlag = ""; + if (hostKeyFile != null) { + userKnownHostsFileFlag = + String.format(" -o \"UserKnownHostsFile=%s\"", hostKeyFile.getAbsolutePath()); + } + + try { + // Obviously the controller must have an installed ssh client. + // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag + String sshClientLaunchString = String.format( + "ssh -o StrictHostKeyChecking=%s%s%s -i %s %s@%s -p %d %s", + template.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), + userKnownHostsFileFlag, + getEC2HostKeyAlgorithmFlag(computer), + identityKeyFile.getAbsolutePath(), + node.remoteAdmin, + ec2HostAddress, + node.getSshPort(), + launchString); + + logInfo( + computer, + listener, + "Launching remoting agent (via SSH client process): " + sshClientLaunchString); + CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); + commandLauncher.launch(computer, listener); + } finally { + if (!identityKeyFile.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete identity key file"); } - } else { - logInfo(computer, listener, "Launching remoting agent (via SSH2 Connection): " + launchString); - - try (ClientChannel channel = clientSession.createExecChannel( - launchString, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - computer.setChannel(channel.getInvertedOut(), channel.getInvertedIn(), logger, new Listener() { - @Override - public void onClosed(Channel channel, IOException cause) { - try { - clientSession.close(); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Error when closing the session", e); - } - } - }); + if (hostKeyFile != null && !hostKeyFile.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete host key file"); } } + } else { + logInfo(computer, listener, "Launching remoting agent (via SSH2 Connection): " + launchString); + + agentExecChannel = clientSession.createExecChannel( + launchString, StandardCharsets.US_ASCII, null, Collections.emptyMap()); + agentExecChannel.setRedirectErrorStream(true); + agentExecChannel.open().verify(timeout); + + cleanupAgentExecChannel = agentExecChannel; + InputStream invertedOut = agentExecChannel.getInvertedOut(); + OutputStream invertedIn = agentExecChannel.getInvertedIn(); - successful = true; + computer.setChannel(invertedOut, invertedIn, logger, new Listener() { + + @Override + public void onClosed(Channel channel, IOException cause) { + try { + agentExecChannel.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Channel Listener Error when closing the channel", e); + } + try { + scpClient.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Channel Listener Error when closing the SCP session", e); + } + try { + clientSession.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Error when closing the session", e); + } + try { + client.stop(); + client.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Channel Listener Error when closing the client", e); + } + } + }); } + + successful = true; } finally { - if (cleanupClientSession != null && (!successful || template.isConnectBySSHProcess())) { - cleanupClientSession.close(); + if (!successful || template.isConnectBySSHProcess()) { + if (cleanupAgentExecChannel != null) { + cleanupAgentExecChannel.close(); + } + if (cleanupScpClient != null) { + cleanupScpClient.close(); + } + if (cleanupClientSession != null) { + cleanupClientSession.close(); + } + if (cleanupClient != null) { + cleanupClient.stop(); + cleanupClient.close(); + } } } } @@ -424,12 +467,11 @@ private boolean executeRemote( String checkCommand, String command, PrintStream logger, - TaskListener listener) - throws IOException, InterruptedException { + TaskListener listener) { logInfo(computer, listener, "Verifying: " + checkCommand); - if (executeRemote(clientSession, checkCommand, logger)) { + if (!executeRemote(clientSession, checkCommand, logger)) { logInfo(computer, listener, "Installing: " + command); - if (executeRemote(clientSession, command, logger)) { + if (!executeRemote(clientSession, command, logger)) { logWarning(computer, listener, "Failed to install: " + command); return false; } @@ -502,6 +544,8 @@ private File createHostKeyFile(EC2Computer computer, String ec2HostAddress, Task private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemplate template) throws IOException, InterruptedException, AmazonClientException { logInfo(computer, listener, "bootstrap()"); + final EC2AbstractSlave node = computer.getNode(); + final long timeout = node == null ? 0L : node.getLaunchTimeoutInMillis(); ClientSession bootstrapSession = null; try (SshClient client = SshClient.setUpDefaultClient()) { int tries = bootstrapAuthTries; From 243088d367881bcb6342cdec246a9c5eb464daac Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Thu, 16 Jan 2025 13:45:59 +0100 Subject: [PATCH 098/267] Use dedicated client for the remoting jar --- .../plugins/ec2/ssh/EC2MacLauncher.java | 480 +++++++++--------- .../plugins/ec2/ssh/EC2UnixLauncher.java | 470 +++++++++-------- 2 files changed, 465 insertions(+), 485 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index 71fb92f3e..f96d0fc94 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -76,6 +76,7 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.channel.ChannelExec; import org.apache.sshd.client.channel.ClientChannel; import org.apache.sshd.client.channel.ClientChannelEvent; import org.apache.sshd.client.future.ConnectFuture; @@ -150,16 +151,6 @@ protected String buildUpCommand(EC2Computer computer, String command) { @Override protected void launchScript(EC2Computer computer, TaskListener listener) throws IOException, AmazonClientException, InterruptedException { - final SshClient client; - SshClient cleanupClient = null; - final CloseableScpClient scpClient; - CloseableScpClient cleanupScpClient = null; - final ClientSession clientSession; - ClientSession cleanupClientSession = null; // java's code path analysis for final - // doesn't work that well. - final ClientChannel agentExecChannel; - ClientChannel cleanupAgentExecChannel = null; - boolean successful = false; PrintStream logger = listener.getLogger(); EC2AbstractSlave node = computer.getNode(); SlaveTemplate template = computer.getSlaveTemplate(); @@ -198,267 +189,214 @@ protected void launchScript(EC2Computer computer, TaskListener listener) logInfo(computer, listener, "Launching instance: " + node.getInstanceId()); - try { - cleanupClient = SshClient.setUpDefaultClient(); + // TODO: parse the version number. maven-enforcer-plugin might help + final String javaPath = node.javaPath; + String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); + + try (SshClient client = SshClient.setUpDefaultClient()) { boolean isBootstrapped = bootstrap(computer, listener, template); - if (isBootstrapped) { - int bootDelay = node.getBootDelay(); - if (bootDelay > 0) { - logInfo( - computer, - listener, - "SSH service responded. Waiting " + bootDelay + "ms for service to stabilize"); - Thread.sleep(bootDelay); - logInfo(computer, listener, "SSH service should have stabilized"); - } + if (!isBootstrapped) { + logWarning(computer, listener, "bootstrapresult failed"); + return; // bootstrap closed for us. + } + int bootDelay = node.getBootDelay(); + if (bootDelay > 0) { + logInfo( + computer, + listener, + "SSH service responded. Waiting " + bootDelay + "ms for service to stabilize"); + Thread.sleep(bootDelay); + logInfo(computer, listener, "SSH service should have stabilized"); + } - // connect fresh as ROOT - logInfo(computer, listener, "connect fresh as root"); - cleanupClientSession = connectToSsh(cleanupClient, computer, listener, template); + // connect fresh as ROOT + logInfo(computer, listener, "connect fresh as root"); + try (ClientSession clientSession = connectToSsh(client, computer, listener, template)) { KeyPair key = computer.getCloud().getKeyPair(); final boolean isAuthenticated; if (key == null) { isAuthenticated = false; } else { - cleanupClientSession.addPublicKeyIdentity(KeyHelper.decodeKeyPair(key.getKeyMaterial(), "")); - cleanupClientSession.auth().await(timeout); - isAuthenticated = cleanupClientSession.isAuthenticated(); + clientSession.addPublicKeyIdentity(KeyHelper.decodeKeyPair(key.getKeyMaterial(), "")); + clientSession.auth().await(timeout); + isAuthenticated = clientSession.isAuthenticated(); } if (!isAuthenticated) { logWarning(computer, listener, "Authentication failed"); return; // failed to connect as root. } - } else { - logWarning(computer, listener, "bootstrapresult failed"); - return; // bootstrap closed for us. - } - client = cleanupClient; - clientSession = cleanupClientSession; - - scpClient = createScpClient(clientSession); - cleanupScpClient = scpClient; - String timestamp = Duration.ofMillis(System.currentTimeMillis()).toSeconds() + " 0"; - ScpTimestampCommandDetails scpTimestamp = - ScpTimestampCommandDetails.parse("T" + timestamp + " " + timestamp); - String initScript = node.initScript; - String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); - - logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); - executeRemote(clientSession, "mkdir -p " + tmpDir, logger); - - if (initScript != null - && !initScript.trim().isEmpty() - && !executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { - logInfo(computer, listener, "Executing init script"); - scpClient.upload( - initScript.getBytes(StandardCharsets.UTF_8), - tmpDir + "/init.sh", - List.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_READ), - scpTimestamp); - - String initCommand = buildUpCommand(computer, tmpDir + "/init.sh"); - try (ClientChannel channel = clientSession.createExecChannel( - initCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - - channel.open().await(timeout); - - OutputStream invertedIn = channel.getInvertedIn(); - if (invertedIn != null) { - invertedIn.close(); // nothing to write here - } - - Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); - - if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { - logWarning(computer, listener, "init script timed out"); - return; - } - - int exitStatus = waitCompletion(channel); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } - InputStream invertedErr = channel.getInvertedErr(); - if (invertedErr != null) { - invertedErr.close(); // we are not supposed to get anything from stderr - } - IOUtils.copy(channel.getInvertedOut(), logger); - } - - logInfo(computer, listener, "Creating ~/.hudson-run-init"); - String createHudsonRunInitCommand = buildUpCommand(computer, "touch ~/.hudson-run-init"); - try (ClientChannel channel = clientSession.createExecChannel( - createHudsonRunInitCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - OutputStream invertedIn = channel.getInvertedIn(); - if (invertedIn != null) { - invertedIn.close(); // nothing to write here - } - channel.open().await(timeout); - - Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); + try (CloseableScpClient scp = createScpClient(clientSession)) { + String timestamp = + Duration.ofMillis(System.currentTimeMillis()).toSeconds() + " 0"; + ScpTimestampCommandDetails scpTimestamp = + ScpTimestampCommandDetails.parse("T" + timestamp + " " + timestamp); + String initScript = node.initScript; + + logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); + executeRemote(clientSession, "mkdir -p " + tmpDir, logger); + + if (initScript != null + && !initScript.trim().isEmpty() + && !executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { + logInfo(computer, listener, "Executing init script"); + scp.upload( + initScript.getBytes(StandardCharsets.UTF_8), + tmpDir + "/init.sh", + List.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_READ), + scpTimestamp); + + String initCommand = buildUpCommand(computer, tmpDir + "/init.sh"); + try (ClientChannel channel = clientSession.createExecChannel( + initCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { + + channel.open().await(timeout); + + OutputStream invertedIn = channel.getInvertedIn(); + if (invertedIn != null) { + invertedIn.close(); // nothing to write here + } + + Collection waitMask = + channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); + + if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { + logWarning(computer, listener, "init script timed out"); + return; + } + + int exitStatus = waitCompletion(channel); + if (exitStatus != 0) { + logWarning(computer, listener, "init script failed: exit code=" + exitStatus); + return; + } + + InputStream invertedErr = channel.getInvertedErr(); + if (invertedErr != null) { + invertedErr.close(); // we are not supposed to get anything from stderr + } + IOUtils.copy(channel.getInvertedOut(), logger); + } - if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { - logWarning(computer, listener, "init script timed out"); - return; - } + logInfo(computer, listener, "Creating ~/.hudson-run-init"); + String createHudsonRunInitCommand = buildUpCommand(computer, "touch ~/.hudson-run-init"); + try (ClientChannel channel = clientSession.createExecChannel( + createHudsonRunInitCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { + OutputStream invertedIn = channel.getInvertedIn(); + if (invertedIn != null) { + invertedIn.close(); // nothing to write here + } + channel.open().await(timeout); + + Collection waitMask = + channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); + + if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { + logWarning(computer, listener, "init script timed out"); + return; + } + + int exitStatus = waitCompletion(channel); + if (exitStatus != 0) { + logWarning(computer, listener, "init script failed: exit code=" + exitStatus); + return; + } + + InputStream invertedErr = channel.getInvertedErr(); + if (invertedErr != null) { + invertedErr.close(); // we are not supposed to get anything from stderr + } + IOUtils.copy(channel.getInvertedOut(), logger); + } - int exitStatus = waitCompletion(channel); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } + try { + Instance nodeInstance = computer.describeInstance(); + if (nodeInstance.getInstanceType().equals("mac2.metal")) { + LOGGER.info("Running Command for mac2.metal"); + executeRemote( + computer, + clientSession, + javaPath + " -fullversion", + "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-aarch64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-aarch64-macos-jdk.pkg -target /", + logger, + listener); + } else { + executeRemote( + computer, + clientSession, + javaPath + " -fullversion", + "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-x64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-x64-macos-jdk.pkg -target /", + logger, + listener); + } + } catch (InterruptedException ex) { + LOGGER.warning(ex.getMessage()); + } - InputStream invertedErr = channel.getInvertedErr(); - if (invertedErr != null) { - invertedErr.close(); // we are not supposed to get anything from stderr + // Always copy so we get the most recent remoting.jar + logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); + scp.upload( + Jenkins.get().getJnlpJars("remoting.jar").readFully(), + tmpDir + "/remoting.jar", + List.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.GROUP_READ, + PosixFilePermission.OTHERS_READ), + scpTimestamp); } - IOUtils.copy(channel.getInvertedOut(), logger); } } + client.stop(); + } - // TODO: parse the version number. maven-enforcer-plugin might help - final String javaPath = node.javaPath; - try { - Instance nodeInstance = computer.describeInstance(); - if (nodeInstance.getInstanceType().equals("mac2.metal")) { - LOGGER.info("Running Command for mac2.metal"); - executeRemote( - computer, - clientSession, - javaPath + " -fullversion", - "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-aarch64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-aarch64-macos-jdk.pkg -target /", - logger, - listener); - } else { - executeRemote( - computer, - clientSession, - javaPath + " -fullversion", - "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-x64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-x64-macos-jdk.pkg -target /", - logger, - listener); - } - } catch (InterruptedException ex) { - LOGGER.warning(ex.getMessage()); - } - - // Always copy so we get the most recent remoting.jar - logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); - scpClient.upload( - Jenkins.get().getJnlpJars("remoting.jar").readFully(), - tmpDir + "/remoting.jar", - List.of( - PosixFilePermission.OWNER_READ, - PosixFilePermission.GROUP_READ, - PosixFilePermission.OTHERS_READ), - scpTimestamp); - - final String jvmopts = node.jvmopts; - final String prefix = computer.getSlaveCommandPrefix(); - final String suffix = computer.getSlaveCommandSuffix(); - final String remoteFS = node.getRemoteFS(); - final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; - String launchString = prefix - + " " - + javaPath - + " " - + (jvmopts != null ? jvmopts : "") - + " -jar " - + tmpDir - + "/remoting.jar -workDir " - + workDir - + suffix; - // launchString = launchString.trim(); - - SlaveTemplate slaveTemplate = computer.getSlaveTemplate(); - - if (slaveTemplate != null && slaveTemplate.isConnectBySSHProcess()) { - File identityKeyFile = createIdentityKeyFile(computer); - - try { - // Obviously the controller must have an installed ssh client. - // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag - String sshClientLaunchString = String.format( - "ssh -o StrictHostKeyChecking=%s -i %s %s@%s -p %d %s", - slaveTemplate.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), - identityKeyFile.getAbsolutePath(), - node.remoteAdmin, - getEC2HostAddress(computer, template), - node.getSshPort(), - launchString); - - logInfo( - computer, - listener, - "Launching remoting agent (via SSH client process): " + sshClientLaunchString); - CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); - commandLauncher.launch(computer, listener); - } finally { - if (!identityKeyFile.delete()) { - LOGGER.log(Level.WARNING, "Failed to delete identity key file"); - } - } - } else { - logInfo(computer, listener, "Launching remoting agent (via SSH2 Connection): " + launchString); - - agentExecChannel = clientSession.createExecChannel( - launchString, StandardCharsets.US_ASCII, null, Collections.emptyMap()); - agentExecChannel.setRedirectErrorStream(true); - agentExecChannel.open().verify(timeout); - - cleanupAgentExecChannel = agentExecChannel; - InputStream invertedOut = agentExecChannel.getInvertedOut(); - OutputStream invertedIn = agentExecChannel.getInvertedIn(); - - computer.setChannel(invertedOut, invertedIn, logger, new Listener() { + final String jvmopts = node.jvmopts; + final String prefix = computer.getSlaveCommandPrefix(); + final String suffix = computer.getSlaveCommandSuffix(); + final String remoteFS = node.getRemoteFS(); + final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; + String launchString = prefix + + " " + + javaPath + + " " + + (jvmopts != null ? jvmopts : "") + + " -jar " + + tmpDir + + "/remoting.jar -workDir " + + workDir + + suffix; + // launchString = launchString.trim(); + + SlaveTemplate slaveTemplate = computer.getSlaveTemplate(); + + if (slaveTemplate != null && slaveTemplate.isConnectBySSHProcess()) { + File identityKeyFile = createIdentityKeyFile(computer); - @Override - public void onClosed(Channel channel, IOException cause) { - try { - agentExecChannel.close(); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Channel Listener Error when closing the channel", e); - } - try { - scpClient.close(); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Channel Listener Error when closing the SCP session", e); - } - try { - clientSession.close(); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Error when closing the session", e); - } - try { - client.stop(); - client.close(); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Channel Listener Error when closing the client", e); - } - } - }); - } + try { + // Obviously the controller must have an installed ssh client. + // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag + String sshClientLaunchString = String.format( + "ssh -o StrictHostKeyChecking=%s -i %s %s@%s -p %d %s", + slaveTemplate.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), + identityKeyFile.getAbsolutePath(), + node.remoteAdmin, + getEC2HostAddress(computer, template), + node.getSshPort(), + launchString); - successful = true; - } finally { - if (!successful || template.isConnectBySSHProcess()) { - if (cleanupAgentExecChannel != null) { - cleanupAgentExecChannel.close(); - } - if (cleanupScpClient != null) { - cleanupScpClient.close(); - } - if (cleanupClientSession != null) { - cleanupClientSession.close(); - } - if (cleanupClient != null) { - cleanupClient.stop(); - cleanupClient.close(); + logInfo( + computer, + listener, + "Launching remoting agent (via SSH client process): " + sshClientLaunchString); + CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); + commandLauncher.launch(computer, listener); + } finally { + if (!identityKeyFile.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete identity key file"); } } + } else { + launchRemotingAgent(computer, listener, launchString, template, timeout, logger); } } @@ -471,6 +409,56 @@ private boolean executeRemote(ClientSession session, String command, OutputStrea } } + private void launchRemotingAgent( + EC2Computer computer, + TaskListener listener, + String launchString, + SlaveTemplate template, + long timeout, + PrintStream logger) + throws InterruptedException, IOException { + logInfo(computer, listener, "Launching remoting agent (via SSH2 Connection): " + launchString); + + final SshClient remotingClient = SshClient.setUpDefaultClient(); + final ClientSession remotingSession = connectToSsh(remotingClient, computer, listener, template); + KeyPair key = computer.getCloud().getKeyPair(); + if (key != null) { + remotingSession.addPublicKeyIdentity(KeyHelper.decodeKeyPair(key.getKeyMaterial(), "")); + } + remotingSession.auth().await(timeout); + ChannelExec agentExecChannel = remotingSession.createExecChannel( + launchString, StandardCharsets.US_ASCII, null, Collections.emptyMap()); + agentExecChannel.open().verify(timeout); + + InputStream invertedOut = agentExecChannel.getInvertedOut(); + OutputStream invertedIn = agentExecChannel.getInvertedIn(); + + Listener channelListener = new Listener() { + + @Override + public void onClosed(Channel channel, IOException cause) { + try { + agentExecChannel.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Error when closing the channel", e); + } + try { + remotingSession.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Error when closing the session", e); + } + try { + remotingClient.stop(); + remotingClient.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Error when closing the client", e); + } + } + }; + + computer.setChannel(invertedOut, invertedIn, logger, channelListener); + } + private boolean executeRemote( EC2Computer computer, ClientSession clientSession, diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index d2c895b68..16989748f 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -78,6 +78,7 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.channel.ChannelExec; import org.apache.sshd.client.channel.ClientChannel; import org.apache.sshd.client.channel.ClientChannelEvent; import org.apache.sshd.client.future.ConnectFuture; @@ -152,16 +153,6 @@ protected String buildUpCommand(EC2Computer computer, String command) { @Override protected void launchScript(EC2Computer computer, TaskListener listener) throws IOException, AmazonClientException, InterruptedException { - final SshClient client; - SshClient cleanupClient = null; - final CloseableScpClient scpClient; - CloseableScpClient cleanupScpClient = null; - final ClientSession clientSession; - ClientSession cleanupClientSession = null; // java's code path analysis for final - // doesn't work that well. - final ClientChannel agentExecChannel; - ClientChannel cleanupAgentExecChannel = null; - boolean successful = false; PrintStream logger = listener.getLogger(); EC2AbstractSlave node = computer.getNode(); SlaveTemplate template = computer.getSlaveTemplate(); @@ -200,265 +191,266 @@ protected void launchScript(EC2Computer computer, TaskListener listener) logInfo(computer, listener, "Launching instance: " + node.getInstanceId()); - try { - cleanupClient = SshClient.setUpDefaultClient(); + // TODO: parse the version number. maven-enforcer-plugin might help + final String javaPath = node.javaPath; + String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); + + try (SshClient client = SshClient.setUpDefaultClient()) { boolean isBootstrapped = bootstrap(computer, listener, template); - if (isBootstrapped) { - int bootDelay = node.getBootDelay(); - if (bootDelay > 0) { - logInfo( - computer, - listener, - "SSH service responded. Waiting " + bootDelay + "ms for service to stabilize"); - Thread.sleep(bootDelay); - logInfo(computer, listener, "SSH service should have stabilized"); - } + if (!isBootstrapped) { + logWarning(computer, listener, "bootstrapresult failed"); + return; // bootstrap closed for us. + } + int bootDelay = node.getBootDelay(); + if (bootDelay > 0) { + logInfo( + computer, + listener, + "SSH service responded. Waiting " + bootDelay + "ms for service to stabilize"); + Thread.sleep(bootDelay); + logInfo(computer, listener, "SSH service should have stabilized"); + } - // connect fresh as ROOT - logInfo(computer, listener, "connect fresh as root"); - cleanupClientSession = connectToSsh(cleanupClient, computer, listener, template); + // connect fresh as ROOT + logInfo(computer, listener, "connect fresh as root"); + try (ClientSession clientSession = connectToSsh(client, computer, listener, template)) { KeyPair key = computer.getCloud().getKeyPair(); final boolean isAuthenticated; if (key == null) { isAuthenticated = false; } else { - cleanupClientSession.addPublicKeyIdentity(KeyHelper.decodeKeyPair(key.getKeyMaterial(), "")); - cleanupClientSession.auth().await(timeout); - isAuthenticated = cleanupClientSession.isAuthenticated(); + clientSession.addPublicKeyIdentity(KeyHelper.decodeKeyPair(key.getKeyMaterial(), "")); + clientSession.auth().await(timeout); + isAuthenticated = clientSession.isAuthenticated(); } if (!isAuthenticated) { logWarning(computer, listener, "Authentication failed"); return; // failed to connect as root. } - } else { - logWarning(computer, listener, "bootstrapresult failed"); - return; // bootstrap closed for us. - } - client = cleanupClient; - clientSession = cleanupClientSession; - - scpClient = createScpClient(clientSession); - cleanupScpClient = scpClient; - String timestamp = Duration.ofMillis(System.currentTimeMillis()).toSeconds() + " 0"; - ScpTimestampCommandDetails scpTimestamp = - ScpTimestampCommandDetails.parse("T" + timestamp + " " + timestamp); - String initScript = node.initScript; - String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); - - logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); - executeRemote(clientSession, "mkdir -p " + tmpDir, logger); - - if (initScript != null - && !initScript.trim().isEmpty() - && !executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { - logInfo(computer, listener, "Executing init script"); - scpClient.upload( - initScript.getBytes(StandardCharsets.UTF_8), - tmpDir + "/init.sh", - List.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_READ), - scpTimestamp); - - String initCommand = buildUpCommand(computer, tmpDir + "/init.sh"); - try (ClientChannel channel = clientSession.createExecChannel( - initCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - - channel.open().await(timeout); - - OutputStream invertedIn = channel.getInvertedIn(); - if (invertedIn != null) { - invertedIn.close(); // nothing to write here - } - Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); - - if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { - logWarning(computer, listener, "init script timed out"); - return; - } + try (CloseableScpClient scp = createScpClient(clientSession)) { + String timestamp = + Duration.ofMillis(System.currentTimeMillis()).toSeconds() + " 0"; + ScpTimestampCommandDetails scpTimestamp = + ScpTimestampCommandDetails.parse("T" + timestamp + " " + timestamp); + String initScript = node.initScript; + + logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); + executeRemote(clientSession, "mkdir -p " + tmpDir, logger); + + if (initScript != null + && !initScript.trim().isEmpty() + && !executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { + logInfo(computer, listener, "Executing init script"); + scp.upload( + initScript.getBytes(StandardCharsets.UTF_8), + tmpDir + "/init.sh", + List.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_READ), + scpTimestamp); + + String initCommand = buildUpCommand(computer, tmpDir + "/init.sh"); + try (ClientChannel channel = clientSession.createExecChannel( + initCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { + + channel.open().await(timeout); + + OutputStream invertedIn = channel.getInvertedIn(); + if (invertedIn != null) { + invertedIn.close(); // nothing to write here + } + + Collection waitMask = + channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); + + if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { + logWarning(computer, listener, "init script timed out"); + return; + } + + int exitStatus = waitCompletion(channel); + if (exitStatus != 0) { + logWarning(computer, listener, "init script failed: exit code=" + exitStatus); + return; + } + + InputStream invertedErr = channel.getInvertedErr(); + if (invertedErr != null) { + invertedErr.close(); // we are not supposed to get anything from stderr + } + IOUtils.copy(channel.getInvertedOut(), logger); + } - int exitStatus = waitCompletion(channel); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } + logInfo(computer, listener, "Creating ~/.hudson-run-init"); + + String createHudsonRunInitCommand = buildUpCommand(computer, "touch ~/.hudson-run-init"); + try (ClientChannel channel = clientSession.createExecChannel( + createHudsonRunInitCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { + OutputStream invertedIn = channel.getInvertedIn(); + if (invertedIn != null) { + invertedIn.close(); // nothing to write here + } + channel.open().await(timeout); + + Collection waitMask = + channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); + + if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { + logWarning(computer, listener, "init script timed out"); + return; + } + + int exitStatus = waitCompletion(channel); + if (exitStatus != 0) { + logWarning(computer, listener, "init script failed: exit code=" + exitStatus); + return; + } + + InputStream invertedErr = channel.getInvertedErr(); + if (invertedErr != null) { + invertedErr.close(); // we are not supposed to get anything from stderr + } + IOUtils.copy(channel.getInvertedOut(), logger); + } - InputStream invertedErr = channel.getInvertedErr(); - if (invertedErr != null) { - invertedErr.close(); // we are not supposed to get anything from stderr + executeRemote( + computer, + clientSession, + javaPath + " -fullversion", + "sudo amazon-linux-extras install java-openjdk11 -y; sudo yum install -y fontconfig java-11-openjdk", + logger, + listener); + executeRemote( + computer, + clientSession, + "which scp", + "sudo yum install -y openssh-clients", + logger, + listener); + + // Always copy so we get the most recent remoting.jar + logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); + scp.upload( + Jenkins.get().getJnlpJars("remoting.jar").readFully(), + tmpDir + "/remoting.jar", + List.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.GROUP_READ, + PosixFilePermission.OTHERS_READ), + scpTimestamp); } - IOUtils.copy(channel.getInvertedOut(), logger); } + } + client.stop(); + } - logInfo(computer, listener, "Creating ~/.hudson-run-init"); - - String createHudsonRunInitCommand = buildUpCommand(computer, "touch ~/.hudson-run-init"); - try (ClientChannel channel = clientSession.createExecChannel( - createHudsonRunInitCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - OutputStream invertedIn = channel.getInvertedIn(); - if (invertedIn != null) { - invertedIn.close(); // nothing to write here - } - channel.open().await(timeout); - - Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); - - if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { - logWarning(computer, listener, "init script timed out"); - return; - } + final String jvmopts = node.jvmopts; + final String prefix = computer.getSlaveCommandPrefix(); + final String suffix = computer.getSlaveCommandSuffix(); + final String remoteFS = node.getRemoteFS(); + final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; + String launchString = prefix + + " " + + javaPath + + " " + + (jvmopts != null ? jvmopts : "") + + " -jar " + + tmpDir + + "/remoting.jar -workDir " + + workDir + + suffix; + // launchString = launchString.trim(); + + if (template.isConnectBySSHProcess()) { + File identityKeyFile = createIdentityKeyFile(computer); + String ec2HostAddress = getEC2HostAddress(computer, template); + File hostKeyFile = createHostKeyFile(computer, ec2HostAddress, listener); + String userKnownHostsFileFlag = ""; + if (hostKeyFile != null) { + userKnownHostsFileFlag = String.format(" -o \"UserKnownHostsFile=%s\"", hostKeyFile.getAbsolutePath()); + } - int exitStatus = waitCompletion(channel); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } + try { + // Obviously the controller must have an installed ssh client. + // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag + String sshClientLaunchString = String.format( + "ssh -o StrictHostKeyChecking=%s%s%s -i %s %s@%s -p %d %s", + template.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), + userKnownHostsFileFlag, + getEC2HostKeyAlgorithmFlag(computer), + identityKeyFile.getAbsolutePath(), + node.remoteAdmin, + ec2HostAddress, + node.getSshPort(), + launchString); - InputStream invertedErr = channel.getInvertedErr(); - if (invertedErr != null) { - invertedErr.close(); // we are not supposed to get anything from stderr - } - IOUtils.copy(channel.getInvertedOut(), logger); + logInfo( + computer, + listener, + "Launching remoting agent (via SSH client process): " + sshClientLaunchString); + CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); + commandLauncher.launch(computer, listener); + } finally { + if (!identityKeyFile.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete identity key file"); + } + if (hostKeyFile != null && !hostKeyFile.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete host key file"); } } + } else { + launchRemotingAgent(computer, listener, launchString, template, timeout, logger); + } + } - // TODO: parse the version number. maven-enforcer-plugin might help - final String javaPath = node.javaPath; - executeRemote( - computer, - clientSession, - javaPath + " -fullversion", - "sudo amazon-linux-extras install java-openjdk11 -y; sudo yum install -y fontconfig java-11-openjdk", - logger, - listener); - executeRemote( - computer, clientSession, "which scp", "sudo yum install -y openssh-clients", logger, listener); - - // Always copy so we get the most recent remoting.jar - logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); - scpClient.upload( - Jenkins.get().getJnlpJars("remoting.jar").readFully(), - tmpDir + "/remoting.jar", - List.of( - PosixFilePermission.OWNER_READ, - PosixFilePermission.GROUP_READ, - PosixFilePermission.OTHERS_READ), - scpTimestamp); - - final String jvmopts = node.jvmopts; - final String prefix = computer.getSlaveCommandPrefix(); - final String suffix = computer.getSlaveCommandSuffix(); - final String remoteFS = node.getRemoteFS(); - final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; - String launchString = prefix - + " " - + javaPath - + " " - + (jvmopts != null ? jvmopts : "") - + " -jar " - + tmpDir - + "/remoting.jar -workDir " - + workDir - + suffix; - // launchString = launchString.trim(); - - if (template.isConnectBySSHProcess()) { - File identityKeyFile = createIdentityKeyFile(computer); - String ec2HostAddress = getEC2HostAddress(computer, template); - File hostKeyFile = createHostKeyFile(computer, ec2HostAddress, listener); - String userKnownHostsFileFlag = ""; - if (hostKeyFile != null) { - userKnownHostsFileFlag = - String.format(" -o \"UserKnownHostsFile=%s\"", hostKeyFile.getAbsolutePath()); - } + private void launchRemotingAgent( + EC2Computer computer, + TaskListener listener, + String launchString, + SlaveTemplate template, + long timeout, + PrintStream logger) + throws InterruptedException, IOException { + logInfo(computer, listener, "Launching remoting agent (via SSH2 Connection): " + launchString); + + final SshClient remotingClient = SshClient.setUpDefaultClient(); + final ClientSession remotingSession = connectToSsh(remotingClient, computer, listener, template); + KeyPair key = computer.getCloud().getKeyPair(); + if (key != null) { + remotingSession.addPublicKeyIdentity(KeyHelper.decodeKeyPair(key.getKeyMaterial(), "")); + } + remotingSession.auth().await(timeout); + ChannelExec agentExecChannel = remotingSession.createExecChannel( + launchString, StandardCharsets.US_ASCII, null, Collections.emptyMap()); + agentExecChannel.open().verify(timeout); - try { - // Obviously the controller must have an installed ssh client. - // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag - String sshClientLaunchString = String.format( - "ssh -o StrictHostKeyChecking=%s%s%s -i %s %s@%s -p %d %s", - template.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), - userKnownHostsFileFlag, - getEC2HostKeyAlgorithmFlag(computer), - identityKeyFile.getAbsolutePath(), - node.remoteAdmin, - ec2HostAddress, - node.getSshPort(), - launchString); - - logInfo( - computer, - listener, - "Launching remoting agent (via SSH client process): " + sshClientLaunchString); - CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); - commandLauncher.launch(computer, listener); - } finally { - if (!identityKeyFile.delete()) { - LOGGER.log(Level.WARNING, "Failed to delete identity key file"); - } - if (hostKeyFile != null && !hostKeyFile.delete()) { - LOGGER.log(Level.WARNING, "Failed to delete host key file"); - } - } - } else { - logInfo(computer, listener, "Launching remoting agent (via SSH2 Connection): " + launchString); - - agentExecChannel = clientSession.createExecChannel( - launchString, StandardCharsets.US_ASCII, null, Collections.emptyMap()); - agentExecChannel.setRedirectErrorStream(true); - agentExecChannel.open().verify(timeout); - - cleanupAgentExecChannel = agentExecChannel; - InputStream invertedOut = agentExecChannel.getInvertedOut(); - OutputStream invertedIn = agentExecChannel.getInvertedIn(); - - computer.setChannel(invertedOut, invertedIn, logger, new Listener() { - - @Override - public void onClosed(Channel channel, IOException cause) { - try { - agentExecChannel.close(); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Channel Listener Error when closing the channel", e); - } - try { - scpClient.close(); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Channel Listener Error when closing the SCP session", e); - } - try { - clientSession.close(); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Error when closing the session", e); - } - try { - client.stop(); - client.close(); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Channel Listener Error when closing the client", e); - } - } - }); - } + InputStream invertedOut = agentExecChannel.getInvertedOut(); + OutputStream invertedIn = agentExecChannel.getInvertedIn(); - successful = true; - } finally { - if (!successful || template.isConnectBySSHProcess()) { - if (cleanupAgentExecChannel != null) { - cleanupAgentExecChannel.close(); - } - if (cleanupScpClient != null) { - cleanupScpClient.close(); + Listener channelListener = new Listener() { + + @Override + public void onClosed(Channel channel, IOException cause) { + try { + agentExecChannel.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Error when closing the channel", e); } - if (cleanupClientSession != null) { - cleanupClientSession.close(); + try { + remotingSession.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Error when closing the session", e); } - if (cleanupClient != null) { - cleanupClient.stop(); - cleanupClient.close(); + try { + remotingClient.stop(); + remotingClient.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Error when closing the client", e); } } - } + }; + + computer.setChannel(invertedOut, invertedIn, logger, channelListener); } private boolean executeRemote( From 7680d4e013e5fd347b92fbbcdc435de3c99075d1 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Mon, 20 Jan 2025 16:53:10 +0100 Subject: [PATCH 099/267] Use timeout from the node --- src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java | 5 +---- src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java | 4 ++-- src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java | 4 ++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java b/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java index 9a041503c..497615608 100644 --- a/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java +++ b/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java @@ -30,7 +30,6 @@ import hudson.slaves.ComputerLauncher; import hudson.slaves.SlaveComputer; import java.io.IOException; -import java.time.Duration; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -49,8 +48,6 @@ public abstract class EC2ComputerLauncher extends ComputerLauncher { private static final Logger LOGGER = Logger.getLogger(EC2ComputerLauncher.class.getName()); - private static final long timeout = Duration.ofSeconds(10).toMillis(); - @Override public void launch(SlaveComputer slaveComputer, TaskListener listener) { try { @@ -94,7 +91,7 @@ public void launch(SlaveComputer slaveComputer, TaskListener listener) { protected abstract void launchScript(EC2Computer computer, TaskListener listener) throws AmazonClientException, IOException, InterruptedException; - protected int waitCompletion(ClientChannel clientChannel) { + protected int waitCompletion(ClientChannel clientChannel, long timeout) { Set clientChannelEvents = clientChannel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); if (clientChannelEvents.contains(ClientChannelEvent.TIMEOUT)) { return -1; diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index f96d0fc94..53afe42b4 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -266,7 +266,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) return; } - int exitStatus = waitCompletion(channel); + int exitStatus = waitCompletion(channel, timeout); if (exitStatus != 0) { logWarning(computer, listener, "init script failed: exit code=" + exitStatus); return; @@ -297,7 +297,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) return; } - int exitStatus = waitCompletion(channel); + int exitStatus = waitCompletion(channel, timeout); if (exitStatus != 0) { logWarning(computer, listener, "init script failed: exit code=" + exitStatus); return; diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index 16989748f..f460f2b16 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -268,7 +268,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) return; } - int exitStatus = waitCompletion(channel); + int exitStatus = waitCompletion(channel, timeout); if (exitStatus != 0) { logWarning(computer, listener, "init script failed: exit code=" + exitStatus); return; @@ -300,7 +300,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) return; } - int exitStatus = waitCompletion(channel); + int exitStatus = waitCompletion(channel, timeout); if (exitStatus != 0) { logWarning(computer, listener, "init script failed: exit code=" + exitStatus); return; From fe9a90165a226d3afc7b2ca32128b301ac0719c9 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Mon, 20 Jan 2025 17:18:11 +0100 Subject: [PATCH 100/267] Improve code readability --- src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java | 3 +-- src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index 53afe42b4..de20695f4 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -237,8 +237,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); executeRemote(clientSession, "mkdir -p " + tmpDir, logger); - if (initScript != null - && !initScript.trim().isEmpty() + if (StringUtils.isNotBlank(initScript) && !executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { logInfo(computer, listener, "Executing init script"); scp.upload( diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index f460f2b16..6f28eed33 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -239,8 +239,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); executeRemote(clientSession, "mkdir -p " + tmpDir, logger); - if (initScript != null - && !initScript.trim().isEmpty() + if (StringUtils.isNotBlank(initScript) && !executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { logInfo(computer, listener, "Executing init script"); scp.upload( From 9abf818b2ea2b725a4eb5016a1eeb065e62053a2 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Mon, 20 Jan 2025 17:30:01 +0100 Subject: [PATCH 101/267] NullPointerException prevention --- src/main/java/hudson/plugins/ec2/util/KeyHelper.java | 7 ++++--- src/test/java/hudson/plugins/ec2/util/ConnectionRule.java | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/util/KeyHelper.java b/src/main/java/hudson/plugins/ec2/util/KeyHelper.java index 0d16fac13..9c3d6d151 100644 --- a/src/main/java/hudson/plugins/ec2/util/KeyHelper.java +++ b/src/main/java/hudson/plugins/ec2/util/KeyHelper.java @@ -1,5 +1,6 @@ package hudson.plugins.ec2.util; +import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.io.StringReader; import java.security.KeyFactory; @@ -38,7 +39,7 @@ private KeyHelper() {} * @throws IOException If an error occurs during parsing or decryption of the PEM input. * @throws IllegalArgumentException If the provided PEM input cannot be parsed or is of an unsupported type. */ - public static KeyPair decodeKeyPair(String pem, String password) throws IOException { + public static KeyPair decodeKeyPair(@NonNull String pem, @NonNull String password) throws IOException { try (org.bouncycastle.openssl.PEMParser pemParser = new org.bouncycastle.openssl.PEMParser(new StringReader(pem))) { Object object = pemParser.readObject(); @@ -73,7 +74,7 @@ public static KeyPair decodeKeyPair(String pem, String password) throws IOExcept } } - private static PublicKey generatePublicKeyFromPrivateKey(PrivateKey privateKey) { + private static PublicKey generatePublicKeyFromPrivateKey(@NonNull PrivateKey privateKey) { try { KeyFactory keyFactory = KeyFactory.getInstance(privateKey.getAlgorithm()); @@ -102,7 +103,7 @@ private static PublicKey generatePublicKeyFromPrivateKey(PrivateKey privateKey) * @return A {@code String} representing the SSH algorithm identifier for the given server key, * or {@code null} if the key type is unsupported or cannot be determined. */ - public static String getSshAlgorithm(PublicKey serverKey) { + public static String getSshAlgorithm(@NonNull PublicKey serverKey) { switch (serverKey.getAlgorithm()) { case "RSA": return "ssh-rsa"; diff --git a/src/test/java/hudson/plugins/ec2/util/ConnectionRule.java b/src/test/java/hudson/plugins/ec2/util/ConnectionRule.java index ff119405a..6087cf5fb 100644 --- a/src/test/java/hudson/plugins/ec2/util/ConnectionRule.java +++ b/src/test/java/hudson/plugins/ec2/util/ConnectionRule.java @@ -73,7 +73,7 @@ public ClientSession connect(ServerKeyVerifier verifier) throws Exception { ConnectFuture connectFuture = sshClient.connect(USER, ip, port); connection = connectFuture.verify().getSession(); - connection.addPublicKeyIdentity(KeyHelper.decodeKeyPair(privateKey, null)); + connection.addPublicKeyIdentity(KeyHelper.decodeKeyPair(privateKey, "")); connection.auth().await(Duration.ofSeconds(10)); assertTrue(connection.isAuthenticated()); From 9a4fc3b9dbf39940613426592e4b5d4e6b84f744 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Mon, 20 Jan 2025 17:55:25 +0100 Subject: [PATCH 102/267] Extract Corretto URL --- src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index de20695f4..ffa280dfe 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -98,6 +98,7 @@ public class EC2MacLauncher extends EC2ComputerLauncher { private static final String BOOTSTRAP_AUTH_TRIES = "jenkins.ec2.bootstrapAuthTries"; private static final String READINESS_SLEEP_MS = "jenkins.ec2.readinessSleepMs"; private static final String READINESS_TRIES = "jenkins.ec2.readinessTries"; + private static final String CORRETTO_LATEST_URL = "https://corretto.aws/downloads/latest"; private static int bootstrapAuthSleepMs = 30000; private static int bootstrapAuthTries = 30; @@ -317,7 +318,9 @@ protected void launchScript(EC2Computer computer, TaskListener listener) computer, clientSession, javaPath + " -fullversion", - "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-aarch64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-aarch64-macos-jdk.pkg -target /", + "curl -L -O " + + CORRETTO_LATEST_URL + + "/amazon-corretto-11-aarch64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-aarch64-macos-jdk.pkg -target /", logger, listener); } else { @@ -325,7 +328,9 @@ protected void launchScript(EC2Computer computer, TaskListener listener) computer, clientSession, javaPath + " -fullversion", - "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-x64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-x64-macos-jdk.pkg -target /", + "curl -L -O " + + CORRETTO_LATEST_URL + + "/amazon-corretto-11-x64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-x64-macos-jdk.pkg -target /", logger, listener); } From 7ceaeeab947ed1dfe7ac485d92e64a26b05aeb79 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Mon, 20 Jan 2025 18:04:46 +0100 Subject: [PATCH 103/267] Add debug log when the command fails --- src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java | 1 + src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java | 1 + 2 files changed, 2 insertions(+) diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index ffa280dfe..b272c41c2 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -409,6 +409,7 @@ private boolean executeRemote(ClientSession session, String command, OutputStrea session.executeRemoteCommand(command, logger, logger, null); return true; } catch (IOException e) { + LOGGER.log(Level.FINE, "Failed to execute remote command: " + command, e); return false; } } diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index 6f28eed33..6841ce4ff 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -475,6 +475,7 @@ private boolean executeRemote(ClientSession session, String command, OutputStrea session.executeRemoteCommand(command, logger, logger, null); return true; } catch (IOException e) { + LOGGER.log(Level.FINE, "Failed to execute remote command: " + command, e); return false; } } From 4e9d26d7c98455f130f603b8f46f27d98e525235 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Mon, 20 Jan 2025 19:34:58 +0100 Subject: [PATCH 104/267] Rewrite generatePublicKeyFromPrivateKey --- .../hudson/plugins/ec2/util/KeyHelper.java | 58 +++++++++++----- .../ec2/util/KeyHelperPublicKeyTest.java | 67 +++++++++++++++++++ ...st.java => KeyHelperSSHAlgorithmTest.java} | 35 ++-------- .../ec2/util/KeyHelperTestAbstract.java | 32 +++++++++ 4 files changed, 149 insertions(+), 43 deletions(-) create mode 100644 src/test/java/hudson/plugins/ec2/util/KeyHelperPublicKeyTest.java rename src/test/java/hudson/plugins/ec2/util/{KeyHelperTest.java => KeyHelperSSHAlgorithmTest.java} (63%) create mode 100644 src/test/java/hudson/plugins/ec2/util/KeyHelperTestAbstract.java diff --git a/src/main/java/hudson/plugins/ec2/util/KeyHelper.java b/src/main/java/hudson/plugins/ec2/util/KeyHelper.java index 9c3d6d151..e03096555 100644 --- a/src/main/java/hudson/plugins/ec2/util/KeyHelper.java +++ b/src/main/java/hudson/plugins/ec2/util/KeyHelper.java @@ -8,15 +8,22 @@ import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; +import java.security.interfaces.DSAParams; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.spec.DSAPublicKeySpec; import java.security.spec.ECField; import java.security.spec.ECParameterSpec; -import java.security.spec.ECPrivateKeySpec; import java.security.spec.EllipticCurve; import java.security.spec.InvalidKeySpecException; -import java.security.spec.RSAPrivateCrtKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.security.spec.X509EncodedKeySpec; +import org.bouncycastle.asn1.ASN1BitString; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.jcajce.interfaces.EdDSAPrivateKey; import org.bouncycastle.openssl.PEMEncryptedKeyPair; import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; @@ -55,8 +62,9 @@ public static KeyPair decodeKeyPair(@NonNull String pem, @NonNull String passwor PublicKey publicKey = converter.getPublicKey(decryptedKeyPair.getPublicKeyInfo()); return new KeyPair(publicKey, privateKey); } else if (object instanceof PrivateKeyInfo) { - PrivateKey privateKey = converter.getPrivateKey((PrivateKeyInfo) object); - PublicKey publicKey = generatePublicKeyFromPrivateKey(privateKey); + PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) object; + PrivateKey privateKey = converter.getPrivateKey(privateKeyInfo); + PublicKey publicKey = generatePublicKeyFromPrivateKey(privateKeyInfo, privateKey); return new KeyPair(publicKey, privateKey); } else if (object instanceof SubjectPublicKeyInfo) { PublicKey publicKey = converter.getPublicKey((SubjectPublicKeyInfo) object); @@ -74,21 +82,41 @@ public static KeyPair decodeKeyPair(@NonNull String pem, @NonNull String passwor } } - private static PublicKey generatePublicKeyFromPrivateKey(@NonNull PrivateKey privateKey) { + /* visible for testing */ + /** + * Extract a {@link PublicKey} from the given {@link PrivateKey} + * @param privateKey the private key to extract from + * @return the corresponding public key or null if the extraction is not possible + */ + static PublicKey generatePublicKeyFromPrivateKey(PrivateKeyInfo privateKeyInfo, @NonNull PrivateKey privateKey) { try { - KeyFactory keyFactory = KeyFactory.getInstance(privateKey.getAlgorithm()); - - if ("RSA".equalsIgnoreCase(privateKey.getAlgorithm())) { - RSAPrivateCrtKeySpec rsaPrivateCrtKeySpec = - keyFactory.getKeySpec(privateKey, RSAPrivateCrtKeySpec.class); - return keyFactory.generatePublic(rsaPrivateCrtKeySpec); - } else if ("EC".equalsIgnoreCase(privateKey.getAlgorithm())) { - ECPrivateKeySpec ecPrivateKeySpec = keyFactory.getKeySpec(privateKey, ECPrivateKeySpec.class); - return keyFactory.generatePublic(ecPrivateKeySpec); + if (privateKey instanceof RSAPrivateCrtKey) + return KeyFactory.getInstance("RSA") + .generatePublic(new RSAPublicKeySpec( + ((RSAPrivateCrtKey) privateKey).getModulus(), + ((RSAPrivateCrtKey) privateKey).getPublicExponent())); + else if (privateKey instanceof DSAPrivateKey) { + DSAParams dsaParams = ((DSAPrivateKey) privateKey).getParams(); + return KeyFactory.getInstance("DSA") + .generatePublic(new DSAPublicKeySpec( + dsaParams.getG().modPow(((DSAPrivateKey) privateKey).getX(), dsaParams.getP()), + dsaParams.getP(), + dsaParams.getQ(), + dsaParams.getG())); + } else if (privateKey instanceof ECPrivateKey) { + ASN1BitString asn1BitString = org.bouncycastle.asn1.sec.ECPrivateKey.getInstance( + privateKeyInfo.getPrivateKey().getOctets()) + .getPublicKey(); + return KeyFactory.getInstance("EC") + .generatePublic(new X509EncodedKeySpec( + new SubjectPublicKeyInfo(privateKeyInfo.getPrivateKeyAlgorithm(), asn1BitString) + .getEncoded())); + } else if (privateKey instanceof EdDSAPrivateKey) { + return ((EdDSAPrivateKey) privateKey).getPublicKey(); } else { return null; } - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + } catch (NoSuchAlgorithmException | IOException | InvalidKeySpecException e) { return null; } } diff --git a/src/test/java/hudson/plugins/ec2/util/KeyHelperPublicKeyTest.java b/src/test/java/hudson/plugins/ec2/util/KeyHelperPublicKeyTest.java new file mode 100644 index 000000000..55b687407 --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/util/KeyHelperPublicKeyTest.java @@ -0,0 +1,67 @@ +package hudson.plugins.ec2.util; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.security.PrivateKey; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class KeyHelperPublicKeyTest extends KeyHelperTestAbstract { + + public static final PrivateKey MOCK_PRIVATE_KEY = new PrivateKey() { + @Override + public String getAlgorithm() { + return "MOCK"; + } + + @Override + public String getFormat() { + return "MOCK"; + } + + @Override + public byte[] getEncoded() { + return new byte[0]; + } + }; + + private final String description; + + private final PrivateKey privateKey; + + private final boolean isNullExpected; + + public KeyHelperPublicKeyTest(String description, PrivateKey privateKey, boolean isNullExpected) { + this.description = description; + this.privateKey = privateKey; + this.isNullExpected = isNullExpected; + } + + @Parameterized.Parameters + public static Object[] data() throws Exception { + return new Object[][] { + {"EC curve NIST P-256", generateECKey("secp256r1").getPrivate(), false}, + {"EC curve NIST P-384", generateECKey("secp384r1").getPrivate(), false}, + {"EC curve NIST P-521", generateECKey("secp521r1").getPrivate(), false}, + {"RSA 1024", generateRSAKey(1024).getPrivate(), false}, + {"RSA 2048", generateRSAKey(2048).getPrivate(), false}, + {"RSA 4096", generateRSAKey(4096).getPrivate(), false}, + {"EdDSA", generateEdDSAKey().getPrivate(), false}, + {"unknown", MOCK_PRIVATE_KEY, true} + }; + } + + @Test + public void testGeneratePublicKeyFromPrivateKey() { + if (isNullExpected) { + assertNull(description, KeyHelper.generatePublicKeyFromPrivateKey(null, privateKey)); + } else { + PrivateKeyInfo info = PrivateKeyInfo.getInstance(privateKey.getEncoded()); + assertNotNull(description, KeyHelper.generatePublicKeyFromPrivateKey(info, privateKey)); + } + } +} diff --git a/src/test/java/hudson/plugins/ec2/util/KeyHelperTest.java b/src/test/java/hudson/plugins/ec2/util/KeyHelperSSHAlgorithmTest.java similarity index 63% rename from src/test/java/hudson/plugins/ec2/util/KeyHelperTest.java rename to src/test/java/hudson/plugins/ec2/util/KeyHelperSSHAlgorithmTest.java index 92eec75e0..1301a4b7a 100644 --- a/src/test/java/hudson/plugins/ec2/util/KeyHelperTest.java +++ b/src/test/java/hudson/plugins/ec2/util/KeyHelperSSHAlgorithmTest.java @@ -2,16 +2,13 @@ import static org.junit.Assert.assertEquals; -import java.security.KeyPair; -import java.security.KeyPairGenerator; import java.security.PublicKey; -import java.security.spec.ECGenParameterSpec; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @RunWith(Parameterized.class) -public class KeyHelperTest { +public class KeyHelperSSHAlgorithmTest extends KeyHelperTestAbstract { public static final PublicKey MOCK_PUBLIC_KEY = new PublicKey() { @Override @@ -30,18 +27,18 @@ public byte[] getEncoded() { } }; - public KeyHelperTest(String description, PublicKey publicKey, String expected) { - this.description = description; - this.publicKey = publicKey; - this.expected = expected; - } - private final String description; private final PublicKey publicKey; private final String expected; + public KeyHelperSSHAlgorithmTest(String description, PublicKey publicKey, String expected) { + this.description = description; + this.publicKey = publicKey; + this.expected = expected; + } + @Parameterized.Parameters public static Object[] data() throws Exception { return new Object[][] { @@ -60,22 +57,4 @@ public static Object[] data() throws Exception { public void testSSHAlgorithm() throws Exception { assertEquals(description, expected, KeyHelper.getSshAlgorithm(publicKey)); } - - public static KeyPair generateECKey(String curveName) throws Exception { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC"); - ECGenParameterSpec ecSpec = new ECGenParameterSpec(curveName); - keyPairGenerator.initialize(ecSpec); - return keyPairGenerator.generateKeyPair(); - } - - public static KeyPair generateRSAKey(int size) throws Exception { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); - keyPairGenerator.initialize(size); - return keyPairGenerator.generateKeyPair(); - } - - public static KeyPair generateEdDSAKey() throws Exception { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EdDSA"); - return keyPairGenerator.generateKeyPair(); - } } diff --git a/src/test/java/hudson/plugins/ec2/util/KeyHelperTestAbstract.java b/src/test/java/hudson/plugins/ec2/util/KeyHelperTestAbstract.java new file mode 100644 index 000000000..4748776e1 --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/util/KeyHelperTestAbstract.java @@ -0,0 +1,32 @@ +package hudson.plugins.ec2.util; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Security; +import java.security.spec.ECGenParameterSpec; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +public class KeyHelperTestAbstract { + static { + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + } + + public static KeyPair generateECKey(String curveName) throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME); + ECGenParameterSpec ecSpec = new ECGenParameterSpec(curveName); + keyPairGenerator.initialize(ecSpec); + return keyPairGenerator.generateKeyPair(); + } + + public static KeyPair generateRSAKey(int size) throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME); + keyPairGenerator.initialize(size); + return keyPairGenerator.generateKeyPair(); + } + + public static KeyPair generateEdDSAKey() throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EdDSA", BouncyCastleProvider.PROVIDER_NAME); + keyPairGenerator.initialize(255); + return keyPairGenerator.generateKeyPair(); + } +} From 7b68bae818e70ba013a099ba395937cca3870665 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Tue, 21 Jan 2025 10:03:07 +0100 Subject: [PATCH 105/267] Fix getSshAlgorithm when Bouncy Castle is used --- src/main/java/hudson/plugins/ec2/util/KeyHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/hudson/plugins/ec2/util/KeyHelper.java b/src/main/java/hudson/plugins/ec2/util/KeyHelper.java index e03096555..44fc2e611 100644 --- a/src/main/java/hudson/plugins/ec2/util/KeyHelper.java +++ b/src/main/java/hudson/plugins/ec2/util/KeyHelper.java @@ -152,6 +152,7 @@ public static String getSshAlgorithm(@NonNull PublicKey serverKey) { } return null; case "EdDSA": + case "Ed25519": return "ssh-ed25519"; default: return null; From abea87f273c09fd687d501fe73fcf9117616039e Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Tue, 21 Jan 2025 10:27:03 +0100 Subject: [PATCH 106/267] Move timeout initialization after checking the node --- src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java | 3 ++- src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index b272c41c2..caaabc5e1 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -155,12 +155,13 @@ protected void launchScript(EC2Computer computer, TaskListener listener) PrintStream logger = listener.getLogger(); EC2AbstractSlave node = computer.getNode(); SlaveTemplate template = computer.getSlaveTemplate(); - final long timeout = node == null ? 0L : node.getLaunchTimeoutInMillis(); if (node == null) { throw new IllegalStateException(); } + final long timeout = node.getLaunchTimeoutInMillis(); + if (template == null) { throw new IOException("Could not find corresponding agent template for " + computer.getDisplayName()); } diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index 6841ce4ff..bc695dda8 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -156,12 +156,13 @@ protected void launchScript(EC2Computer computer, TaskListener listener) PrintStream logger = listener.getLogger(); EC2AbstractSlave node = computer.getNode(); SlaveTemplate template = computer.getSlaveTemplate(); - final long timeout = node == null ? 0L : node.getLaunchTimeoutInMillis(); if (node == null) { throw new IllegalStateException(); } + final long timeout = node.getLaunchTimeoutInMillis(); + if (template == null) { throw new IOException("Could not find corresponding agent template for " + computer.getDisplayName()); } From 804446040fabcfb438a6a1d798a6096c3ba8a911 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Wed, 22 Jan 2025 12:01:28 +0100 Subject: [PATCH 107/267] Format repository with Spotless --- .../java/hudson/plugins/ec2/WindowsData.java | 5 +- .../hudson/plugins/ec2/util/FIPS140Utils.java | 26 ++++---- .../hudson/plugins/ec2/win/WinConnection.java | 15 +---- .../hudson/plugins/ec2/win/winrm/WinRM.java | 3 - .../plugins/ec2/win/winrm/WinRMClient.java | 2 - .../ec2/EC2CloudPrivateKeyWithFIPSTest.java | 31 +++++----- .../EC2CloudPrivateKeyWithoutFIPSTest.java | 40 ++++++------- .../plugins/ec2/HostKeyWithFIPSTest.java | 59 +++++++++++-------- .../plugins/ec2/HostKeyWithoutFIPSTest.java | 49 ++++++++------- .../plugins/ec2/WindowsDataWithFIPSTest.java | 29 +++++---- .../ec2/WindowsDataWithoutFIPSTest.java | 32 +++++----- .../ec2/util/FIPS140UtilsWithFIPSTest.java | 29 +++++---- .../ec2/util/FIPS140UtilsWithoutFIPSTest.java | 27 +++++---- .../ec2/win/WinConnectionWithFIPSTest.java | 25 +++----- .../ec2/win/WinConnectionWithoutFIPSTest.java | 31 ++++------ .../win/winrm/WinRMClientWithFIPSTest.java | 34 ++++------- .../win/winrm/WinRMClientWithoutFIPSTest.java | 49 ++++++--------- .../ec2/win/winrm/WinRMWithFIPSTest.java | 6 +- .../ec2/win/winrm/WinRMWithoutFIPSTest.java | 6 +- 19 files changed, 227 insertions(+), 271 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/WindowsData.java b/src/main/java/hudson/plugins/ec2/WindowsData.java index c232c188c..3093b1b80 100644 --- a/src/main/java/hudson/plugins/ec2/WindowsData.java +++ b/src/main/java/hudson/plugins/ec2/WindowsData.java @@ -2,19 +2,16 @@ import hudson.Extension; import hudson.model.Descriptor; - import hudson.plugins.ec2.util.FIPS140Utils; import hudson.util.FormValidation; import hudson.util.Secret; -import jenkins.model.Jenkins; -import jenkins.security.FIPS140; import java.util.Objects; import java.util.concurrent.TimeUnit; +import jenkins.model.Jenkins; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.verb.POST; - public class WindowsData extends AMITypeData { private final Secret password; diff --git a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java index 8178aa776..7961bafeb 100644 --- a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java +++ b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java @@ -4,10 +4,6 @@ import com.trilead.ssh2.signature.KeyAlgorithmManager; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.plugins.ec2.Messages; -import jenkins.bouncycastle.api.PEMEncodable; -import jenkins.security.FIPS140; -import org.apache.commons.lang.StringUtils; - import java.io.IOException; import java.net.URL; import java.security.Key; @@ -17,6 +13,9 @@ import java.security.interfaces.DSAKey; import java.security.interfaces.ECKey; import java.security.interfaces.RSAKey; +import jenkins.bouncycastle.api.PEMEncodable; +import jenkins.security.FIPS140; +import org.apache.commons.lang.StringUtils; /** * FIPS related utility methods (check Private and Public keys, ...) @@ -53,14 +52,14 @@ public static void ensureKeyInFipsMode(Key key) { throw new IllegalArgumentException(Messages.AmazonEC2Cloud_invalidKeySizeECInFIPSMode()); } } else { - throw new IllegalArgumentException(Messages.AmazonEC2Cloud_keyIsNotApprovedInFIPSMode(key.getAlgorithm())); + throw new IllegalArgumentException( + Messages.AmazonEC2Cloud_keyIsNotApprovedInFIPSMode(key.getAlgorithm())); } } catch (RuntimeException e) { throw new IllegalArgumentException(e.getMessage(), e); } } - /** * Password leak prevention when FIPS mode is requested. If FIPS mode is not requested, this method does nothing. * Otherwise, ensure that no password can be leaked @@ -139,18 +138,17 @@ public static void ensurePrivateKeyInFipsMode(String privateKeyString) { } } - public static void ensurePublicKeyInFipsMode(@NonNull - String algorithm, @NonNull byte[] key) { + public static void ensurePublicKeyInFipsMode(@NonNull String algorithm, @NonNull byte[] key) { if (!FIPS140.useCompliantAlgorithms()) { return; } - KeyAlgorithm publicKeyPrivateKeyKeyAlgorithm = KeyAlgorithmManager - .getSupportedAlgorithms() - .stream() - .filter((keyAlgorithm) -> keyAlgorithm.getKeyFormat().equals(algorithm)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException(Messages.AmazonEC2Cloud_keyIsNotApprovedInFIPSMode(algorithm))); + KeyAlgorithm publicKeyPrivateKeyKeyAlgorithm = + KeyAlgorithmManager.getSupportedAlgorithms().stream() + .filter((keyAlgorithm) -> keyAlgorithm.getKeyFormat().equals(algorithm)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException( + Messages.AmazonEC2Cloud_keyIsNotApprovedInFIPSMode(algorithm))); try { Key publicKey = publicKeyPrivateKeyKeyAlgorithm.decodePublicKey(key); ensureKeyInFipsMode(publicKey); diff --git a/src/main/java/hudson/plugins/ec2/win/WinConnection.java b/src/main/java/hudson/plugins/ec2/win/WinConnection.java index 24c4bc731..c4ad4e3f9 100644 --- a/src/main/java/hudson/plugins/ec2/win/WinConnection.java +++ b/src/main/java/hudson/plugins/ec2/win/WinConnection.java @@ -4,13 +4,12 @@ import com.hierynomus.mssmb2.SMB2CreateDisposition; import com.hierynomus.mssmb2.SMB2ShareAccess; import com.hierynomus.protocol.transport.TransportException; -import hudson.plugins.ec2.Messages; -import hudson.plugins.ec2.util.FIPS140Utils; import com.hierynomus.smbj.SMBClient; import com.hierynomus.smbj.auth.AuthenticationContext; import com.hierynomus.smbj.connection.Connection; import com.hierynomus.smbj.session.Session; import com.hierynomus.smbj.share.DiskShare; +import hudson.plugins.ec2.util.FIPS140Utils; import hudson.plugins.ec2.win.winrm.WinRM; import hudson.plugins.ec2.win.winrm.WindowsProcess; import java.io.IOException; @@ -19,18 +18,6 @@ import java.net.InetSocketAddress; import java.net.Socket; import java.util.EnumSet; - -import com.hierynomus.smbj.auth.AuthenticationContext; -import com.hierynomus.smbj.SMBClient; -import com.hierynomus.smbj.share.DiskShare; -import com.hierynomus.smbj.connection.Connection; -import com.hierynomus.smbj.session.Session; -import com.hierynomus.msdtyp.AccessMask; -import com.hierynomus.mssmb2.SMB2ShareAccess; -import com.hierynomus.mssmb2.SMB2CreateDisposition; -import jenkins.security.FIPS140; - -import javax.net.ssl.SSLException; import java.util.logging.Level; import java.util.logging.Logger; import javax.net.ssl.SSLException; diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/WinRM.java b/src/main/java/hudson/plugins/ec2/win/winrm/WinRM.java index c5c41d1d1..bce9dcbb5 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/WinRM.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/WinRM.java @@ -1,9 +1,6 @@ package hudson.plugins.ec2.win.winrm; -import hudson.plugins.ec2.Messages; import hudson.plugins.ec2.util.FIPS140Utils; -import jenkins.security.FIPS140; - import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java index 12067f6b8..f5a9aa7c7 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java @@ -1,6 +1,5 @@ package hudson.plugins.ec2.win.winrm; -import hudson.plugins.ec2.Messages; import hudson.plugins.ec2.util.FIPS140Utils; import hudson.plugins.ec2.win.winrm.request.RequestFactory; import hudson.plugins.ec2.win.winrm.soap.Namespaces; @@ -16,7 +15,6 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; -import jenkins.security.FIPS140; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.ParseException; diff --git a/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithFIPSTest.java index 7db5d93e9..3c038be7a 100644 --- a/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithFIPSTest.java @@ -1,6 +1,11 @@ package hudson.plugins.ec2; +import static org.junit.Assert.fail; + import hudson.plugins.ec2.util.FIPS140Utils; +import java.security.Security; +import java.util.Arrays; +import java.util.Collection; import jenkins.security.FIPS140; import org.junit.Before; import org.junit.ClassRule; @@ -9,17 +14,12 @@ import org.junit.runners.Parameterized; import org.jvnet.hudson.test.FlagRule; -import java.security.Security; -import java.util.Arrays; -import java.util.Collection; - -import static org.junit.Assert.fail; - @RunWith(Parameterized.class) public class EC2CloudPrivateKeyWithFIPSTest { @ClassRule - public static FlagRule fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); + public static FlagRule fipsSystemPropertyRule = + FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); public static String PRIVATE_KEY_DSA_1024 = "-----BEGIN DSA PRIVATE KEY-----\n" + "MIIBuwIBAAKBgQDdxPbkTmoDD5cK4W9bE5OVKO1Qu7vw32ZOAFHEdOCjK/JxLEyu\n" @@ -212,14 +212,14 @@ public void before() { @Parameterized.Parameters public static Collection data() { return Arrays.asList(new Object[][] { - { "DSA with key size 1024", PRIVATE_KEY_DSA_1024, false }, - { "RSA with key size 1024", PRIVATE_KEY_RSA_1024, false }, - { "RSA with key size 2048", PRIVATE_KEY_RSA_2048, true }, - { "RSA with key size 3072", PRIVATE_KEY_RSA_3072, true }, - { "RSA with key size 4096", PRIVATE_KEY_RSA_4096, true }, - { "ECDSA with key size 256", PRIVATE_KEY_ECDSA_256, true }, - { "ECDSA with key size 384", PRIVATE_KEY_ECDSA_384, true }, - { "ECDSA with key size 521", PRIVATE_KEY_ECDSA_521, true } + {"DSA with key size 1024", PRIVATE_KEY_DSA_1024, false}, + {"RSA with key size 1024", PRIVATE_KEY_RSA_1024, false}, + {"RSA with key size 2048", PRIVATE_KEY_RSA_2048, true}, + {"RSA with key size 3072", PRIVATE_KEY_RSA_3072, true}, + {"RSA with key size 4096", PRIVATE_KEY_RSA_4096, true}, + {"ECDSA with key size 256", PRIVATE_KEY_ECDSA_256, true}, + {"ECDSA with key size 384", PRIVATE_KEY_ECDSA_384, true}, + {"ECDSA with key size 521", PRIVATE_KEY_ECDSA_521, true} }); } @@ -236,5 +236,4 @@ public void testPrivateKeyValidation() { } } } - } diff --git a/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithoutFIPSTest.java b/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithoutFIPSTest.java index 2d698b7ef..a56a29653 100644 --- a/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithoutFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2CloudPrivateKeyWithoutFIPSTest.java @@ -1,16 +1,5 @@ package hudson.plugins.ec2; -import hudson.plugins.ec2.util.FIPS140Utils; -import jenkins.security.FIPS140; -import org.junit.ClassRule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.jvnet.hudson.test.FlagRule; - -import java.util.Arrays; -import java.util.Collection; - import static hudson.plugins.ec2.EC2CloudPrivateKeyWithFIPSTest.PRIVATE_KEY_DSA_1024; import static hudson.plugins.ec2.EC2CloudPrivateKeyWithFIPSTest.PRIVATE_KEY_ECDSA_256; import static hudson.plugins.ec2.EC2CloudPrivateKeyWithFIPSTest.PRIVATE_KEY_ECDSA_384; @@ -21,10 +10,21 @@ import static hudson.plugins.ec2.EC2CloudPrivateKeyWithFIPSTest.PRIVATE_KEY_RSA_4096; import static org.junit.Assert.fail; +import hudson.plugins.ec2.util.FIPS140Utils; +import java.util.Arrays; +import java.util.Collection; +import jenkins.security.FIPS140; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.jvnet.hudson.test.FlagRule; + @RunWith(Parameterized.class) public class EC2CloudPrivateKeyWithoutFIPSTest { @ClassRule - public static FlagRule fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); + public static FlagRule fipsSystemPropertyRule = + FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); private final String description; @@ -38,14 +38,14 @@ public EC2CloudPrivateKeyWithoutFIPSTest(String description, String privateKey) @Parameterized.Parameters public static Collection data() { return Arrays.asList(new Object[][] { - { "DSA with key size 1024", PRIVATE_KEY_DSA_1024 }, - { "RSA with key size 1024", PRIVATE_KEY_RSA_1024 }, - { "RSA with key size 2048", PRIVATE_KEY_RSA_2048 }, - { "RSA with key size 3072", PRIVATE_KEY_RSA_3072 }, - { "RSA with key size 4096", PRIVATE_KEY_RSA_4096 }, - { "ECDSA with key size 256", PRIVATE_KEY_ECDSA_256 }, - { "ECDSA with key size 384", PRIVATE_KEY_ECDSA_384 }, - { "ECDSA with key size 521", PRIVATE_KEY_ECDSA_521 }, + {"DSA with key size 1024", PRIVATE_KEY_DSA_1024}, + {"RSA with key size 1024", PRIVATE_KEY_RSA_1024}, + {"RSA with key size 2048", PRIVATE_KEY_RSA_2048}, + {"RSA with key size 3072", PRIVATE_KEY_RSA_3072}, + {"RSA with key size 4096", PRIVATE_KEY_RSA_4096}, + {"ECDSA with key size 256", PRIVATE_KEY_ECDSA_256}, + {"ECDSA with key size 384", PRIVATE_KEY_ECDSA_384}, + {"ECDSA with key size 521", PRIVATE_KEY_ECDSA_521}, }); } diff --git a/src/test/java/hudson/plugins/ec2/HostKeyWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/HostKeyWithFIPSTest.java index 8b142f29e..65317f031 100644 --- a/src/test/java/hudson/plugins/ec2/HostKeyWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/HostKeyWithFIPSTest.java @@ -1,6 +1,12 @@ package hudson.plugins.ec2; +import static org.junit.Assert.fail; + import hudson.plugins.ec2.ssh.verifiers.HostKey; +import java.security.Security; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; import jenkins.security.FIPS140; import org.junit.Before; import org.junit.ClassRule; @@ -9,27 +15,29 @@ import org.junit.runners.Parameterized; import org.jvnet.hudson.test.FlagRule; -import java.security.Security; -import java.util.Arrays; -import java.util.Base64; -import java.util.Collection; - -import static org.junit.Assert.fail; - @RunWith(Parameterized.class) public class HostKeyWithFIPSTest { @ClassRule - public static FlagRule fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); + public static FlagRule fipsSystemPropertyRule = + FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); - public static String PUBLIC_KEY_SSH_DSS_1024 = "AAAAB3NzaC1kc3MAAACBAMsQrriFgun2KVgmsGd8drsplZLXyU8uU6r90aIZ+evRpxvoLCJf317Wnu5qVBCzGgEZ8iygYB0bDB/JFch+UVgtyXGH358ClJCDDgNWdOSogTl2gCF+W+8KoRSF+i3ObnEPOTa2akByP5FDzOO+mruVPl8kg8NHYcadCtJizRjhAAAAFQCV9uGT1Mchfbm6uFxEmZf09DwjSQAAAIAyyLw64QIHel17rzdyMyvepkvW4q64WYb7xCVLffaYJA8x1pxHtH4Mmmm0fGG7GFgdnCeD95524CYZR7TDhzKFGcEX607qKg0v5sXs6z8U8lGOeARq/IXQphb7YPZ9PdKUIuJImQEXriI0p5G7aGMmSYjnyEpKhUsM12xpDb2qBAAAAIBbIZPuZzBbbeesmzmGoG63w0tFc+tpPV3lNkAeYYcWpVhpSdHGFatr1lU+8LNT6OXekV2CFyF5kuuYw/B3OFkmHasURnT1+yC49OEpSzA3KOtQzqO2BZqIxDG/IEajKtSPGOWWPaVrHdgDXo3EZ6yCJtCiOMxW5Xz3fiUufp1sdQ=="; - public static String PUBLIC_KEY_SSH_RSA_1024 = "AAAAB3NzaC1yc2EAAAADAQABAAAAgQC8nIRjuQr9gGfGTdq7BL4l3s4n4qEe3w7imhv1cT5cy2HT7DXvnA2gGVmFn4izbWlFlQG1lrtMgIiiwXH/shRx+2FnqayNsOmRJ37TiA0ICjkOrdR4JaYWRafQ0TEC0+EdHl+3iJYOhw9scFpJ2M9kB6W5HJsf4gmXoGGz8SsfsQ=="; - public static String PUBLIC_KEY_SSH_RSA_2048 = "AAAAB3NzaC1yc2EAAAADAQABAAABAQCjw8Wgl1usvj9LCzF1c8PufEIG11V2PHCDNlYc66ccIiojQX79st1Lbp0BJXsa2bvZLYjfqyYP5gqkX7jLslmXPN+Vew91sRTmXJTlANlm/fChHg+Fq+lQK0IKGBIn9RlPDFH+NNoUIw4LbZ4etRJuOfMwiVKsOVOYuuLjiIJTkda9eS9zrhTRUXhUuMIxBLdeJEAYve6oBpcnTKpUbTV+DYlru3Yh6lSIevhA361s65oJauNHFQLQ7Ysi9apiF5hmqt1sThv/NPM/xLwlPrSGqKZWnclJbBKaWFlCijuM7W3Q5zbcdmtvKhxEJMobu+KMbt/LVhV7kD3BBLhADKnZ"; - public static String PUBLIC_KEY_SSH_RSA_3072 = "AAAAB3NzaC1yc2EAAAADAQABAAABgQDzvqmjwxE3UgKOVZDoji9npAu7Uee47sdS60cTN0yx3Aj+c5IznoBLDYt7HwUcjKoj6soRJALFMGvrKe6n4H1+9jF5vrstMB40Ga8858wweehIAEzw/ONAORZdHA2y0WG8K3+bNOVSeXZwASsjbKrYcdotsZarhQtGVks6xQwd7qXUD44DDdFuWsuj5//hSSYSIgjJE3gAfeI2qVoe6Cl6gTGoK9zd+hNrYDehpN7bDgX45ulcaMw7N2kLf+Sg5QqOYL3Xdav/SeNEefNUyE058uRK8Br3WhZh5BJ2qFjzUYe20cFKHJ3gqKiY+8aor6YrDAS5AOEAEdCw1GWHJutGeApouTSqpNZf1uHspKEgLCUu6gb+i14k3YGSqUW/3fRdqmtN5qBYGvOoqgUEG1wlsxjf6lvJxSh6551MEiM3dpXBq3wniFjK56pj9nVjW0erJsOXPIqh9KeQL1dB+fzNX0r3oAog3EEl+x+V/YLE12b8MR9qnaZwyxWPGKoRE70="; - public static String PUBLIC_KEY_SSH_RSA_4096 = "AAAAB3NzaC1yc2EAAAADAQABAAACAQCvoE/zGFhSYP/aXLGYl27P+Bq5KJ0E53Er163GJRZ119kgPTB17JOKEG1k25tmspoNYVVaSIM81zBi4RUIrP7ft+1wj2FlsMchrEHlrqR31HCCsPmf/YGgzaBBgL2KDNHEsnxIzyZTsY4ZGPS4LZMP8McUXfwvOFkVs12AUNH5hrB0SgMv6sor1VyW43p6u1o1w9MX5omANopOv+Rqm7In0UXNmOocOhOFYqDJVKt05+fI+fduHIwO4Wi3e0K1jK1EmC9YlIJJIz0Ce1+CyGK0Cm7lHj+W2Ea5tERO0DsK/etGbn1w8NcW9XmPVzO4vSvsMm7XrL0hIdNQZKSxas4NNwxr0TZN70T+H3WKRK9VAxCEp5IdahsSevKyrcsRnKX3mcemqJZZ+ODAarPdHemNacywzoaEt2AOSOl1PcW/sA49R4yMYHj8RS6xDv9jeA4Vogj58ynqzaB2F4fCkaV4bmgb2vL0Fkw96Tvq0+Gs902zvtDmnneicCWhNnj+3jRKZjqiQRvA3/BgYrokFGcDra4j9C1vrDVajMitcY+dr0XeA+n9ot29GSx36Fwg3j3QUhamS6/nsKTeIdmEHeym7FT6LKweKL/XcUCs+tkaJFxsJ+S1E+vF2M7SmqkNuB0S17EijZtw01v1zbzocscnfpLXo3UEfBdIe7pjT/IGtw=="; - public static String PUBLIC_KEY_ECDSA_SHA2_NISTP256_256 = "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB1ZWbuchcMR2NMc4wu4sB2sFvnxZ45tAyijmSx9pUkQ51InNU7t1qzf2p29VhwdJ7kQSX3HdUcwBP1NfUSEoFw="; - public static String PUBLIC_KEY_ECDSA_SHA2_NISTP384_384 = "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEfdfV0luXJ0NJGMpeAMDZvfDjfwpC9U+YDSlgM0Gh2eCCtYCGv41G4tZd5+L1gjPEiS4Y8r+jb3JoAX6JdQfHecK6+NHpZsF0uwrn8zTfA9PT+I9nTtEyBgNWM/v/A5wQ=="; - public static String PUBLIC_KEY_ECDSA_SHA2_NISTP521_521 = "AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFrY2jC8sarrQqI13e9fDhzeUvTFt5j2krHfFfqDrP/M7L5RJzbg4jOSOly7FdOi7JhFkYaEguddhRh2DIUWKHR9ADR9/m4n9WxHR9QaVLUYUyZdQzgdtlY6KfLYJyO5PBSulMhpfDKGoycNKmr6Av1gyESAIBq+bINsgpUby+h9jkC7Q=="; + public static String PUBLIC_KEY_SSH_DSS_1024 = + "AAAAB3NzaC1kc3MAAACBAMsQrriFgun2KVgmsGd8drsplZLXyU8uU6r90aIZ+evRpxvoLCJf317Wnu5qVBCzGgEZ8iygYB0bDB/JFch+UVgtyXGH358ClJCDDgNWdOSogTl2gCF+W+8KoRSF+i3ObnEPOTa2akByP5FDzOO+mruVPl8kg8NHYcadCtJizRjhAAAAFQCV9uGT1Mchfbm6uFxEmZf09DwjSQAAAIAyyLw64QIHel17rzdyMyvepkvW4q64WYb7xCVLffaYJA8x1pxHtH4Mmmm0fGG7GFgdnCeD95524CYZR7TDhzKFGcEX607qKg0v5sXs6z8U8lGOeARq/IXQphb7YPZ9PdKUIuJImQEXriI0p5G7aGMmSYjnyEpKhUsM12xpDb2qBAAAAIBbIZPuZzBbbeesmzmGoG63w0tFc+tpPV3lNkAeYYcWpVhpSdHGFatr1lU+8LNT6OXekV2CFyF5kuuYw/B3OFkmHasURnT1+yC49OEpSzA3KOtQzqO2BZqIxDG/IEajKtSPGOWWPaVrHdgDXo3EZ6yCJtCiOMxW5Xz3fiUufp1sdQ=="; + public static String PUBLIC_KEY_SSH_RSA_1024 = + "AAAAB3NzaC1yc2EAAAADAQABAAAAgQC8nIRjuQr9gGfGTdq7BL4l3s4n4qEe3w7imhv1cT5cy2HT7DXvnA2gGVmFn4izbWlFlQG1lrtMgIiiwXH/shRx+2FnqayNsOmRJ37TiA0ICjkOrdR4JaYWRafQ0TEC0+EdHl+3iJYOhw9scFpJ2M9kB6W5HJsf4gmXoGGz8SsfsQ=="; + public static String PUBLIC_KEY_SSH_RSA_2048 = + "AAAAB3NzaC1yc2EAAAADAQABAAABAQCjw8Wgl1usvj9LCzF1c8PufEIG11V2PHCDNlYc66ccIiojQX79st1Lbp0BJXsa2bvZLYjfqyYP5gqkX7jLslmXPN+Vew91sRTmXJTlANlm/fChHg+Fq+lQK0IKGBIn9RlPDFH+NNoUIw4LbZ4etRJuOfMwiVKsOVOYuuLjiIJTkda9eS9zrhTRUXhUuMIxBLdeJEAYve6oBpcnTKpUbTV+DYlru3Yh6lSIevhA361s65oJauNHFQLQ7Ysi9apiF5hmqt1sThv/NPM/xLwlPrSGqKZWnclJbBKaWFlCijuM7W3Q5zbcdmtvKhxEJMobu+KMbt/LVhV7kD3BBLhADKnZ"; + public static String PUBLIC_KEY_SSH_RSA_3072 = + "AAAAB3NzaC1yc2EAAAADAQABAAABgQDzvqmjwxE3UgKOVZDoji9npAu7Uee47sdS60cTN0yx3Aj+c5IznoBLDYt7HwUcjKoj6soRJALFMGvrKe6n4H1+9jF5vrstMB40Ga8858wweehIAEzw/ONAORZdHA2y0WG8K3+bNOVSeXZwASsjbKrYcdotsZarhQtGVks6xQwd7qXUD44DDdFuWsuj5//hSSYSIgjJE3gAfeI2qVoe6Cl6gTGoK9zd+hNrYDehpN7bDgX45ulcaMw7N2kLf+Sg5QqOYL3Xdav/SeNEefNUyE058uRK8Br3WhZh5BJ2qFjzUYe20cFKHJ3gqKiY+8aor6YrDAS5AOEAEdCw1GWHJutGeApouTSqpNZf1uHspKEgLCUu6gb+i14k3YGSqUW/3fRdqmtN5qBYGvOoqgUEG1wlsxjf6lvJxSh6551MEiM3dpXBq3wniFjK56pj9nVjW0erJsOXPIqh9KeQL1dB+fzNX0r3oAog3EEl+x+V/YLE12b8MR9qnaZwyxWPGKoRE70="; + public static String PUBLIC_KEY_SSH_RSA_4096 = + "AAAAB3NzaC1yc2EAAAADAQABAAACAQCvoE/zGFhSYP/aXLGYl27P+Bq5KJ0E53Er163GJRZ119kgPTB17JOKEG1k25tmspoNYVVaSIM81zBi4RUIrP7ft+1wj2FlsMchrEHlrqR31HCCsPmf/YGgzaBBgL2KDNHEsnxIzyZTsY4ZGPS4LZMP8McUXfwvOFkVs12AUNH5hrB0SgMv6sor1VyW43p6u1o1w9MX5omANopOv+Rqm7In0UXNmOocOhOFYqDJVKt05+fI+fduHIwO4Wi3e0K1jK1EmC9YlIJJIz0Ce1+CyGK0Cm7lHj+W2Ea5tERO0DsK/etGbn1w8NcW9XmPVzO4vSvsMm7XrL0hIdNQZKSxas4NNwxr0TZN70T+H3WKRK9VAxCEp5IdahsSevKyrcsRnKX3mcemqJZZ+ODAarPdHemNacywzoaEt2AOSOl1PcW/sA49R4yMYHj8RS6xDv9jeA4Vogj58ynqzaB2F4fCkaV4bmgb2vL0Fkw96Tvq0+Gs902zvtDmnneicCWhNnj+3jRKZjqiQRvA3/BgYrokFGcDra4j9C1vrDVajMitcY+dr0XeA+n9ot29GSx36Fwg3j3QUhamS6/nsKTeIdmEHeym7FT6LKweKL/XcUCs+tkaJFxsJ+S1E+vF2M7SmqkNuB0S17EijZtw01v1zbzocscnfpLXo3UEfBdIe7pjT/IGtw=="; + public static String PUBLIC_KEY_ECDSA_SHA2_NISTP256_256 = + "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB1ZWbuchcMR2NMc4wu4sB2sFvnxZ45tAyijmSx9pUkQ51InNU7t1qzf2p29VhwdJ7kQSX3HdUcwBP1NfUSEoFw="; + public static String PUBLIC_KEY_ECDSA_SHA2_NISTP384_384 = + "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEfdfV0luXJ0NJGMpeAMDZvfDjfwpC9U+YDSlgM0Gh2eCCtYCGv41G4tZd5+L1gjPEiS4Y8r+jb3JoAX6JdQfHecK6+NHpZsF0uwrn8zTfA9PT+I9nTtEyBgNWM/v/A5wQ=="; + public static String PUBLIC_KEY_ECDSA_SHA2_NISTP521_521 = + "AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFrY2jC8sarrQqI13e9fDhzeUvTFt5j2krHfFfqDrP/M7L5RJzbg4jOSOly7FdOi7JhFkYaEguddhRh2DIUWKHR9ADR9/m4n9WxHR9QaVLUYUyZdQzgdtlY6KfLYJyO5PBSulMhpfDKGoycNKmr6Av1gyESAIBq+bINsgpUby+h9jkC7Q=="; private final String description; @@ -54,19 +62,18 @@ public void before() { @Parameterized.Parameters public static Collection data() { - return Arrays.asList(new Object[][]{ - { "SSH-DSS with key size 1024", "ssh-dss", PUBLIC_KEY_SSH_DSS_1024, false }, - { "SSH-RSA with key size 1024", "ssh-rsa", PUBLIC_KEY_SSH_RSA_1024, false }, - { "SSH-RSA with key size 2048", "ssh-rsa", PUBLIC_KEY_SSH_RSA_2048, true }, - { "SSH-RSA with key size 3072", "ssh-rsa", PUBLIC_KEY_SSH_RSA_3072, true }, - { "SSH-RSA with key size 4096", "ssh-rsa", PUBLIC_KEY_SSH_RSA_4096, true }, - { "ECDSA-SHA2-NISTP256 with key size 256", "ecdsa-sha2-nistp256", PUBLIC_KEY_ECDSA_SHA2_NISTP256_256, true }, - { "ECDSA-SHA2-NISTP384 with key size 384", "ecdsa-sha2-nistp384", PUBLIC_KEY_ECDSA_SHA2_NISTP384_384, true }, - { "ECDSA-SHA2-NISTP521 with key size 521", "ecdsa-sha2-nistp521", PUBLIC_KEY_ECDSA_SHA2_NISTP521_521, true } + return Arrays.asList(new Object[][] { + {"SSH-DSS with key size 1024", "ssh-dss", PUBLIC_KEY_SSH_DSS_1024, false}, + {"SSH-RSA with key size 1024", "ssh-rsa", PUBLIC_KEY_SSH_RSA_1024, false}, + {"SSH-RSA with key size 2048", "ssh-rsa", PUBLIC_KEY_SSH_RSA_2048, true}, + {"SSH-RSA with key size 3072", "ssh-rsa", PUBLIC_KEY_SSH_RSA_3072, true}, + {"SSH-RSA with key size 4096", "ssh-rsa", PUBLIC_KEY_SSH_RSA_4096, true}, + {"ECDSA-SHA2-NISTP256 with key size 256", "ecdsa-sha2-nistp256", PUBLIC_KEY_ECDSA_SHA2_NISTP256_256, true}, + {"ECDSA-SHA2-NISTP384 with key size 384", "ecdsa-sha2-nistp384", PUBLIC_KEY_ECDSA_SHA2_NISTP384_384, true}, + {"ECDSA-SHA2-NISTP521 with key size 521", "ecdsa-sha2-nistp521", PUBLIC_KEY_ECDSA_SHA2_NISTP521_521, true} }); } - @Test public void testPublicKeyValidation() { try { diff --git a/src/test/java/hudson/plugins/ec2/HostKeyWithoutFIPSTest.java b/src/test/java/hudson/plugins/ec2/HostKeyWithoutFIPSTest.java index c6533284a..11122a69b 100644 --- a/src/test/java/hudson/plugins/ec2/HostKeyWithoutFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/HostKeyWithoutFIPSTest.java @@ -1,19 +1,5 @@ package hudson.plugins.ec2; -import hudson.plugins.ec2.ssh.verifiers.HostKey; -import jenkins.security.FIPS140; -import org.junit.Before; -import org.junit.ClassRule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.jvnet.hudson.test.FlagRule; - -import java.security.Security; -import java.util.Arrays; -import java.util.Base64; -import java.util.Collection; - import static hudson.plugins.ec2.HostKeyWithFIPSTest.PUBLIC_KEY_ECDSA_SHA2_NISTP256_256; import static hudson.plugins.ec2.HostKeyWithFIPSTest.PUBLIC_KEY_ECDSA_SHA2_NISTP384_384; import static hudson.plugins.ec2.HostKeyWithFIPSTest.PUBLIC_KEY_ECDSA_SHA2_NISTP521_521; @@ -24,11 +10,25 @@ import static hudson.plugins.ec2.HostKeyWithFIPSTest.PUBLIC_KEY_SSH_RSA_4096; import static org.junit.Assert.fail; +import hudson.plugins.ec2.ssh.verifiers.HostKey; +import java.security.Security; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import jenkins.security.FIPS140; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.jvnet.hudson.test.FlagRule; + @RunWith(Parameterized.class) public class HostKeyWithoutFIPSTest { @ClassRule - public static FlagRule fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); + public static FlagRule fipsSystemPropertyRule = + FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); private final String description; @@ -50,19 +50,18 @@ public void before() { @Parameterized.Parameters public static Collection data() { - return Arrays.asList(new Object[][]{ - { "SSH-DSS with key size 1024", "ssh-dss", PUBLIC_KEY_SSH_DSS_1024 }, - { "SSH-RSA with key size 1024", "ssh-rsa", PUBLIC_KEY_SSH_RSA_1024 }, - { "SSH-RSA with key size 2048", "ssh-rsa", PUBLIC_KEY_SSH_RSA_2048 }, - { "SSH-RSA with key size 3072", "ssh-rsa", PUBLIC_KEY_SSH_RSA_3072 }, - { "SSH-RSA with key size 4096", "ssh-rsa", PUBLIC_KEY_SSH_RSA_4096 }, - { "ECDSA-SHA2-NISTP256 with key size 256", "ecdsa-sha2-nistp256", PUBLIC_KEY_ECDSA_SHA2_NISTP256_256 }, - { "ECDSA-SHA2-NISTP384 with key size 384", "ecdsa-sha2-nistp384", PUBLIC_KEY_ECDSA_SHA2_NISTP384_384 }, - { "ECDSA-SHA2-NISTP521 with key size 521", "ecdsa-sha2-nistp521", PUBLIC_KEY_ECDSA_SHA2_NISTP521_521 } + return Arrays.asList(new Object[][] { + {"SSH-DSS with key size 1024", "ssh-dss", PUBLIC_KEY_SSH_DSS_1024}, + {"SSH-RSA with key size 1024", "ssh-rsa", PUBLIC_KEY_SSH_RSA_1024}, + {"SSH-RSA with key size 2048", "ssh-rsa", PUBLIC_KEY_SSH_RSA_2048}, + {"SSH-RSA with key size 3072", "ssh-rsa", PUBLIC_KEY_SSH_RSA_3072}, + {"SSH-RSA with key size 4096", "ssh-rsa", PUBLIC_KEY_SSH_RSA_4096}, + {"ECDSA-SHA2-NISTP256 with key size 256", "ecdsa-sha2-nistp256", PUBLIC_KEY_ECDSA_SHA2_NISTP256_256}, + {"ECDSA-SHA2-NISTP384 with key size 384", "ecdsa-sha2-nistp384", PUBLIC_KEY_ECDSA_SHA2_NISTP384_384}, + {"ECDSA-SHA2-NISTP521 with key size 521", "ecdsa-sha2-nistp521", PUBLIC_KEY_ECDSA_SHA2_NISTP521_521} }); } - @Test public void testPublicKeyValidation() { try { diff --git a/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java index 913c34d22..cc8b5f9aa 100644 --- a/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java @@ -1,7 +1,9 @@ package hudson.plugins.ec2; -import hudson.model.Descriptor; +import static org.junit.Assert.*; + import hudson.ExtensionList; +import hudson.model.Descriptor; import hudson.util.FormValidation; import jenkins.security.FIPS140; import org.junit.ClassRule; @@ -12,11 +14,10 @@ import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.WithoutJenkins; -import static org.junit.Assert.*; - public class WindowsDataWithFIPSTest { @ClassRule - public static FlagRule fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); + public static FlagRule fipsSystemPropertyRule = + FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); @Rule public JenkinsRule j = new JenkinsRule(); @@ -68,42 +69,48 @@ public void testCreateWindowsDataWithoutPassword() throws Descriptor.FormExcepti @Test @Ignore("Disabled until the plugins dependencies are FIPS compliant") public void testDoCheckUseHTTPSWithPassword() { - FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(true, "yes"); + FormValidation formValidation = + ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(true, "yes"); assertEquals(FormValidation.Kind.OK, formValidation.kind); } @Test @Ignore("Disabled until the plugins dependencies are FIPS compliant") public void testDoCheckUseHTTPSWithoutPassword() { - FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(true, ""); + FormValidation formValidation = + ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(true, ""); assertEquals(FormValidation.Kind.OK, formValidation.kind); } @Test @Ignore("Disabled until the plugins dependencies are FIPS compliant") public void testDoCheckUseHTTPWithPassword() { - FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(false, "yes"); + FormValidation formValidation = + ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(false, "yes"); assertEquals(FormValidation.Kind.ERROR, formValidation.kind); } @Test @Ignore("Disabled until the plugins dependencies are FIPS compliant") public void testDoCheckUseHTTPWithoutPassword() { - FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(false, ""); + FormValidation formValidation = + ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(false, ""); assertEquals(FormValidation.Kind.OK, formValidation.kind); } @Test @Ignore("Disabled until the plugins dependencies are FIPS compliant") public void testDoCheckAllowSelfSignedCertificateChecked() { - FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckAllowSelfSignedCertificate(true); + FormValidation formValidation = + ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckAllowSelfSignedCertificate(true); assertEquals(FormValidation.Kind.ERROR, formValidation.kind); } @Test @Ignore("Disabled until the plugins dependencies are FIPS compliant") public void testDoCheckAllowSelfSignedCertificateNotChecked() { - FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckAllowSelfSignedCertificate(false); + FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class) + .doCheckAllowSelfSignedCertificate(false); assertEquals(FormValidation.Kind.OK, formValidation.kind); } -} \ No newline at end of file +} diff --git a/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java b/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java index e74e25e79..56ae28cb3 100644 --- a/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java @@ -1,7 +1,9 @@ package hudson.plugins.ec2; -import hudson.model.Descriptor; +import static org.junit.Assert.assertEquals; + import hudson.ExtensionList; +import hudson.model.Descriptor; import hudson.util.FormValidation; import jenkins.security.FIPS140; import org.junit.ClassRule; @@ -11,12 +13,10 @@ import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.WithoutJenkins; -import static org.junit.Assert.assertEquals; - public class WindowsDataWithoutFIPSTest { @ClassRule - public static FlagRule - fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); + public static FlagRule fipsSystemPropertyRule = + FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); @Rule public JenkinsRule j = new JenkinsRule(); @@ -37,7 +37,6 @@ public void testWinConnectionCreation() throws Descriptor.FormException { new WindowsData("", false, "", false, true); new WindowsData("yes", false, "", false, true); - new WindowsData("", true, "", true, false); new WindowsData("yes", true, "", true, false); new WindowsData("", false, "", true, false); @@ -49,40 +48,45 @@ public void testWinConnectionCreation() throws Descriptor.FormException { new WindowsData("yes", false, "", false, false); } - @Test public void testDoCheckUseHTTPSWithPassword() { - FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(true, "yes"); + FormValidation formValidation = + ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(true, "yes"); assertEquals(FormValidation.Kind.OK, formValidation.kind); } @Test public void testDoCheckUseHTTPSWithoutPassword() { - FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(true, ""); + FormValidation formValidation = + ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(true, ""); assertEquals(FormValidation.Kind.OK, formValidation.kind); } @Test public void testDoCheckUseHTTPWithPassword() { - FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(false, "yes"); + FormValidation formValidation = + ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(false, "yes"); assertEquals(FormValidation.Kind.OK, formValidation.kind); } @Test public void testDoCheckUseHTTPWithoutPassword() { - FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(false, ""); + FormValidation formValidation = + ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(false, ""); assertEquals(FormValidation.Kind.OK, formValidation.kind); } @Test public void testDoCheckAllowSelfSignedCertificateChecked() { - FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckAllowSelfSignedCertificate(true); + FormValidation formValidation = + ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckAllowSelfSignedCertificate(true); assertEquals(FormValidation.Kind.OK, formValidation.kind); } @Test public void testDoCheckAllowSelfSignedCertificateNotChecked() { - FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckAllowSelfSignedCertificate(false); + FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class) + .doCheckAllowSelfSignedCertificate(false); assertEquals(FormValidation.Kind.OK, formValidation.kind); } -} \ No newline at end of file +} diff --git a/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java index 97af54890..9f484f122 100644 --- a/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java @@ -1,21 +1,20 @@ package hudson.plugins.ec2.util; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + import hudson.plugins.ec2.Messages; import io.vavr.CheckedRunnable; -import jenkins.security.FIPS140; -import org.junit.ClassRule; -import org.junit.Test; -import org.jvnet.hudson.test.FlagRule; -import org.mockito.Mockito; - import java.net.URL; import java.security.Key; import java.security.interfaces.DSAPublicKey; import java.security.interfaces.ECKey; import java.security.interfaces.RSAPublicKey; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import jenkins.security.FIPS140; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.FlagRule; +import org.mockito.Mockito; public class FIPS140UtilsWithFIPSTest { @ClassRule @@ -94,7 +93,9 @@ public void testRSAValidKey() { @Test public void testECDSAInvalidKeyMessage() { - ECKey key = Mockito.mock(ECKey.class, Mockito.withSettings().extraInterfaces(Key.class).defaultAnswer(Mockito.RETURNS_DEEP_STUBS)); + ECKey key = Mockito.mock( + ECKey.class, + Mockito.withSettings().extraInterfaces(Key.class).defaultAnswer(Mockito.RETURNS_DEEP_STUBS)); Mockito.when(key.getParams().getCurve().getField().getFieldSize()).thenReturn(223); assertInvalidKey((Key) key, Messages.AmazonEC2Cloud_invalidKeySizeECInFIPSMode()); @@ -102,7 +103,9 @@ public void testECDSAInvalidKeyMessage() { @Test public void testECDSAValidKey() { - ECKey key = Mockito.mock(ECKey.class, Mockito.withSettings().extraInterfaces(Key.class).defaultAnswer(Mockito.RETURNS_DEEP_STUBS)); + ECKey key = Mockito.mock( + ECKey.class, + Mockito.withSettings().extraInterfaces(Key.class).defaultAnswer(Mockito.RETURNS_DEEP_STUBS)); Mockito.when(key.getParams().getCurve().getField().getFieldSize()).thenReturn(224); assertValidKey((Key) key); @@ -158,7 +161,7 @@ public void testTLSCheckWithHTTPSAndNoPassword() { @Test public void testNotAllowSelfSignedCertificate() { - try{ + try { FIPS140Utils.ensureNoSelfSignedCertificate(false); } catch (IllegalArgumentException e) { fail("Not allowing self-signed certificate should be valid, but got : " + e.getMessage()); @@ -175,4 +178,4 @@ public void testAllowSelfSignedCertificate() { assertEquals(expectedMessage, e.getMessage()); } } -} \ No newline at end of file +} diff --git a/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithoutFIPSTest.java b/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithoutFIPSTest.java index 4650bfe06..53725a226 100644 --- a/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithoutFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithoutFIPSTest.java @@ -1,19 +1,18 @@ package hudson.plugins.ec2.util; -import io.vavr.CheckedRunnable; -import jenkins.security.FIPS140; -import org.junit.ClassRule; -import org.junit.Test; -import org.jvnet.hudson.test.FlagRule; -import org.mockito.Mockito; +import static org.junit.Assert.fail; +import io.vavr.CheckedRunnable; import java.net.URL; import java.security.Key; import java.security.interfaces.DSAPublicKey; import java.security.interfaces.ECKey; import java.security.interfaces.RSAPublicKey; - -import static org.junit.Assert.fail; +import jenkins.security.FIPS140; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.FlagRule; +import org.mockito.Mockito; public class FIPS140UtilsWithoutFIPSTest { @ClassRule @@ -72,7 +71,9 @@ public void testRSAValidKey() { @Test public void testECDSAInvalidKeyMessage() { - ECKey key = Mockito.mock(ECKey.class, Mockito.withSettings().extraInterfaces(Key.class).defaultAnswer(Mockito.RETURNS_DEEP_STUBS)); + ECKey key = Mockito.mock( + ECKey.class, + Mockito.withSettings().extraInterfaces(Key.class).defaultAnswer(Mockito.RETURNS_DEEP_STUBS)); Mockito.when(key.getParams().getCurve().getField().getFieldSize()).thenReturn(223); assertValidKey((Key) key); @@ -80,7 +81,9 @@ public void testECDSAInvalidKeyMessage() { @Test public void testECDSAValidKey() { - ECKey key = Mockito.mock(ECKey.class, Mockito.withSettings().extraInterfaces(Key.class).defaultAnswer(Mockito.RETURNS_DEEP_STUBS)); + ECKey key = Mockito.mock( + ECKey.class, + Mockito.withSettings().extraInterfaces(Key.class).defaultAnswer(Mockito.RETURNS_DEEP_STUBS)); Mockito.when(key.getParams().getCurve().getField().getFieldSize()).thenReturn(224); assertValidKey((Key) key); @@ -136,7 +139,7 @@ public void testTLSCheckWithHTTPSAndNoPassword() { @Test public void testNotAllowSelfSignedCertificate() { - try{ + try { FIPS140Utils.ensureNoSelfSignedCertificate(false); } catch (IllegalArgumentException e) { fail("Not allowing self-signed certificate should be valid, but got : " + e.getMessage()); @@ -151,4 +154,4 @@ public void testAllowSelfSignedCertificate() { fail("Not allowing self-signed certificate should be valid, but got : " + e.getMessage()); } } -} \ No newline at end of file +} diff --git a/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java index 3512f1356..1eece09d3 100644 --- a/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java @@ -1,31 +1,24 @@ package hudson.plugins.ec2.win; -import hudson.plugins.ec2.win.winrm.WinRMConnectException; -import hudson.plugins.ec2.win.winrm.WindowsProcess; +import static org.junit.Assert.fail; + import jenkins.security.FIPS140; -import org.apache.commons.io.IOUtils; import org.junit.ClassRule; import org.junit.Test; import org.jvnet.hudson.test.FlagRule; -import static junit.framework.TestCase.assertTrue; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.fail; -import static org.junit.Assume.assumeThat; - public class WinConnectionWithFIPSTest { @ClassRule - public static FlagRule - fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); + public static FlagRule fipsSystemPropertyRule = + FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); /** * Self-signed certificate should not be allowed in FIPS mode, an {@link IllegalArgumentException} is expected */ @Test(expected = IllegalArgumentException.class) public void testSelfSignedCertificateNotAllowed() throws Exception { - new WinConnection("", "" , "", true); + new WinConnection("", "", "", true); } /** @@ -33,7 +26,7 @@ public void testSelfSignedCertificateNotAllowed() throws Exception { */ @Test public void testValidCreation() { - new WinConnection("", "" , "", false); + new WinConnection("", "", "", false); } /** @@ -41,8 +34,7 @@ public void testValidCreation() { */ @Test public void testSetUseHTTPSFalseWithoutPassword() { - new WinConnection("", "" , "", false) - .setUseHTTPS(false); + new WinConnection("", "", "", false).setUseHTTPS(false); } /** @@ -50,8 +42,7 @@ public void testSetUseHTTPSFalseWithoutPassword() { */ @Test(expected = IllegalArgumentException.class) public void testSetUseHTTPSFalseWithPassword() { - new WinConnection("", "alice" , "yes", false) - .setUseHTTPS(false); + new WinConnection("", "alice", "yes", false).setUseHTTPS(false); } /** diff --git a/src/test/java/hudson/plugins/ec2/win/WinConnectionWithoutFIPSTest.java b/src/test/java/hudson/plugins/ec2/win/WinConnectionWithoutFIPSTest.java index ba5a2d125..d4ab11e85 100644 --- a/src/test/java/hudson/plugins/ec2/win/WinConnectionWithoutFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/win/WinConnectionWithoutFIPSTest.java @@ -1,41 +1,30 @@ package hudson.plugins.ec2.win; -import hudson.plugins.ec2.win.winrm.WinRMClient; import jenkins.security.FIPS140; import org.junit.ClassRule; import org.junit.Test; import org.jvnet.hudson.test.FlagRule; -import static org.junit.Assert.fail; - public class WinConnectionWithoutFIPSTest { @ClassRule - public static FlagRule - fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); + public static FlagRule fipsSystemPropertyRule = + FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); /** * When FIPS is not enabled, it should always be allowed to create the {@link WinConnection}, an {@link IllegalArgumentException} is not expected */ @Test public void testWinConnectionCreation() { - new WinConnection("", "" , "", true) - .setUseHTTPS(false); - new WinConnection("", "" , "", true) - .setUseHTTPS(true); - new WinConnection("", "alice" , "yes", true) - .setUseHTTPS(false); - new WinConnection("", "alice" , "yes", true) - .setUseHTTPS(true); + new WinConnection("", "", "", true).setUseHTTPS(false); + new WinConnection("", "", "", true).setUseHTTPS(true); + new WinConnection("", "alice", "yes", true).setUseHTTPS(false); + new WinConnection("", "alice", "yes", true).setUseHTTPS(true); - new WinConnection("", "" , "", false) - .setUseHTTPS(false); - new WinConnection("", "" , "", false) - .setUseHTTPS(true); - new WinConnection("", "alice" , "yes", false) - .setUseHTTPS(false); - new WinConnection("", "alice" , "yes", false) - .setUseHTTPS(true); + new WinConnection("", "", "", false).setUseHTTPS(false); + new WinConnection("", "", "", false).setUseHTTPS(true); + new WinConnection("", "alice", "yes", false).setUseHTTPS(false); + new WinConnection("", "alice", "yes", false).setUseHTTPS(true); } /** diff --git a/src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithFIPSTest.java index 95e0fc79e..fb4a8f9f0 100644 --- a/src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithFIPSTest.java @@ -1,20 +1,19 @@ package hudson.plugins.ec2.win.winrm; +import static org.junit.Assert.fail; + +import java.net.MalformedURLException; +import java.net.URL; import jenkins.security.FIPS140; import org.junit.ClassRule; import org.junit.Test; import org.jvnet.hudson.test.FlagRule; -import java.net.MalformedURLException; -import java.net.URL; - -import static org.junit.Assert.fail; - public class WinRMClientWithFIPSTest { @ClassRule - public static FlagRule - fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); + public static FlagRule fipsSystemPropertyRule = + FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); /** * Self-signed certificate should not be allowed in FIPS mode, an {@link IllegalArgumentException} is expected @@ -54,12 +53,9 @@ public void testCreateClientWithoutPassword() throws MalformedURLException { */ @Test public void testSetUseHTTPSTrue() throws MalformedURLException { - new WinRMClient(new URL("https://localhost"), "username", "password", false) - .setUseHTTPS(true); - new WinRMClient(new URL("http://localhost"), "username", null, false) - .setUseHTTPS(true); - new WinRMClient(new URL("https://localhost"), "username", null, false) - .setUseHTTPS(true); + new WinRMClient(new URL("https://localhost"), "username", "password", false).setUseHTTPS(true); + new WinRMClient(new URL("http://localhost"), "username", null, false).setUseHTTPS(true); + new WinRMClient(new URL("https://localhost"), "username", null, false).setUseHTTPS(true); } /** @@ -67,10 +63,8 @@ public void testSetUseHTTPSTrue() throws MalformedURLException { */ @Test public void testSetUseHTTPSFalseWithoutPassword() throws MalformedURLException { - new WinRMClient(new URL("http://localhost"), "username", null, false) - .setUseHTTPS(false); - new WinRMClient(new URL("https://localhost"), "username", null, false) - .setUseHTTPS(false); + new WinRMClient(new URL("http://localhost"), "username", null, false).setUseHTTPS(false); + new WinRMClient(new URL("https://localhost"), "username", null, false).setUseHTTPS(false); } /** @@ -78,8 +72,7 @@ public void testSetUseHTTPSFalseWithoutPassword() throws MalformedURLException { */ @Test(expected = IllegalArgumentException.class) public void testSetUseHTTPSFalseWithPassword() throws MalformedURLException { - new WinRMClient(new URL("https://localhost"), "username", "password", false) - .setUseHTTPS(false); + new WinRMClient(new URL("https://localhost"), "username", "password", false).setUseHTTPS(false); } /** @@ -95,5 +88,4 @@ public void testBuildWinRMClientWithoutTLS() throws MalformedURLException { fail("The client should not attempt to connect"); } } - -} \ No newline at end of file +} diff --git a/src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithoutFIPSTest.java b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithoutFIPSTest.java index 6d51901ad..99aebd4fa 100644 --- a/src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithoutFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithoutFIPSTest.java @@ -1,50 +1,35 @@ package hudson.plugins.ec2.win.winrm; +import java.net.MalformedURLException; +import java.net.URL; import jenkins.security.FIPS140; import org.junit.ClassRule; import org.junit.Test; import org.jvnet.hudson.test.FlagRule; -import java.net.MalformedURLException; -import java.net.URL; - -import static org.junit.Assert.fail; - public class WinRMClientWithoutFIPSTest { @ClassRule - public static FlagRule - fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); + public static FlagRule fipsSystemPropertyRule = + FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); /** * When FIPS is not enabled, it should always be allowed to create the {@link WinRMClient}, an {@link IllegalArgumentException} is not expected */ @Test public void testClientCreation() throws MalformedURLException { - new WinRMClient(new URL("http://localhost"), "username", "password", true) - .setUseHTTPS(true); - new WinRMClient(new URL("https://localhost"), "username", "password", true) - .setUseHTTPS(true); - new WinRMClient(new URL("http://localhost"), "username", "password", false) - .setUseHTTPS(true); - new WinRMClient(new URL("https://localhost"), "username", "password", false) - .setUseHTTPS(true); - new WinRMClient(new URL("http://localhost"), "username", null, false) - .setUseHTTPS(true); - new WinRMClient(new URL("https://localhost"), "username", null, false) - .setUseHTTPS(true); + new WinRMClient(new URL("http://localhost"), "username", "password", true).setUseHTTPS(true); + new WinRMClient(new URL("https://localhost"), "username", "password", true).setUseHTTPS(true); + new WinRMClient(new URL("http://localhost"), "username", "password", false).setUseHTTPS(true); + new WinRMClient(new URL("https://localhost"), "username", "password", false).setUseHTTPS(true); + new WinRMClient(new URL("http://localhost"), "username", null, false).setUseHTTPS(true); + new WinRMClient(new URL("https://localhost"), "username", null, false).setUseHTTPS(true); - new WinRMClient(new URL("http://localhost"), "username", "password", true) - .setUseHTTPS(false); - new WinRMClient(new URL("https://localhost"), "username", "password", true) - .setUseHTTPS(false); - new WinRMClient(new URL("http://localhost"), "username", "password", false) - .setUseHTTPS(false); - new WinRMClient(new URL("https://localhost"), "username", "password", false) - .setUseHTTPS(false); - new WinRMClient(new URL("http://localhost"), "username", null, false) - .setUseHTTPS(false); - new WinRMClient(new URL("https://localhost"), "username", null, false) - .setUseHTTPS(false); + new WinRMClient(new URL("http://localhost"), "username", "password", true).setUseHTTPS(false); + new WinRMClient(new URL("https://localhost"), "username", "password", true).setUseHTTPS(false); + new WinRMClient(new URL("http://localhost"), "username", "password", false).setUseHTTPS(false); + new WinRMClient(new URL("https://localhost"), "username", "password", false).setUseHTTPS(false); + new WinRMClient(new URL("http://localhost"), "username", null, false).setUseHTTPS(false); + new WinRMClient(new URL("https://localhost"), "username", null, false).setUseHTTPS(false); } -} \ No newline at end of file +} diff --git a/src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithFIPSTest.java index ae2f8bdd5..6bec12bf8 100644 --- a/src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithFIPSTest.java @@ -8,8 +8,8 @@ public class WinRMWithFIPSTest { @ClassRule - public static FlagRule - fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); + public static FlagRule fipsSystemPropertyRule = + FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true"); /** * Self-signed certificate should not be allowed in FIPS mode, an {@link IllegalArgumentException} is expected @@ -47,4 +47,4 @@ public void testSetUseHTTPSWithoutPasswordLeak() { winRM.setUseHTTPS(false); winRM.buildURL(); } -} \ No newline at end of file +} diff --git a/src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithoutFIPSTest.java b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithoutFIPSTest.java index 96b05f335..748f42398 100644 --- a/src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithoutFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithoutFIPSTest.java @@ -8,8 +8,8 @@ public class WinRMWithoutFIPSTest { @ClassRule - public static FlagRule - fipsSystemPropertyRule = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); + public static FlagRule fipsSystemPropertyRule = + FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false"); /** * When FIPS mode is not activated, no FIPS check should be performed, an {@link IllegalArgumentException} is not expected @@ -47,4 +47,4 @@ public void testSetUseHTTPSWithoutPasswordLeak() { winRM.setUseHTTPS(false); winRM.buildURL(); } -} \ No newline at end of file +} From fd76311adb632ae3fbdb70b00838dbcb978ec917 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Wed, 22 Jan 2025 12:04:31 +0100 Subject: [PATCH 108/267] Update messages keys according to the properties file --- .../hudson/plugins/ec2/util/FIPS140Utils.java | 16 ++++++++-------- .../ec2/util/FIPS140UtilsWithFIPSTest.java | 14 +++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java index 7961bafeb..b6c6af509 100644 --- a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java +++ b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java @@ -41,19 +41,19 @@ public static void ensureKeyInFipsMode(Key key) { try { if (key instanceof RSAKey) { if (((RSAKey) key).getModulus().bitLength() < 2048) { - throw new IllegalArgumentException(Messages.AmazonEC2Cloud_invalidKeySizeInFIPSMode()); + throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeInFIPSMode()); } } else if (key instanceof DSAKey) { if (((DSAKey) key).getParams().getP().bitLength() < 2048) { - throw new IllegalArgumentException(Messages.AmazonEC2Cloud_invalidKeySizeInFIPSMode()); + throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeInFIPSMode()); } } else if (key instanceof ECKey) { if (((ECKey) key).getParams().getCurve().getField().getFieldSize() < 224) { - throw new IllegalArgumentException(Messages.AmazonEC2Cloud_invalidKeySizeECInFIPSMode()); + throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeECInFIPSMode()); } } else { throw new IllegalArgumentException( - Messages.AmazonEC2Cloud_keyIsNotApprovedInFIPSMode(key.getAlgorithm())); + Messages.EC2Cloud_keyIsNotApprovedInFIPSMode(key.getAlgorithm())); } } catch (RuntimeException e) { throw new IllegalArgumentException(e.getMessage(), e); @@ -92,7 +92,7 @@ public static void ensureNoPasswordLeak(boolean useHTTPS, String password) { public static void ensureNoPasswordLeak(boolean useHTTPS, boolean usePassword) { if (FIPS140.useCompliantAlgorithms()) { if (!useHTTPS && usePassword) { - throw new IllegalArgumentException(Messages.AmazonEC2Cloud_tlsIsRequiredInFIPSMode()); + throw new IllegalArgumentException(Messages.EC2Cloud_tlsIsRequiredInFIPSMode()); } } } @@ -106,7 +106,7 @@ public static void ensureNoPasswordLeak(boolean useHTTPS, boolean usePassword) { public static void ensureNoSelfSignedCertificate(boolean allowSelfSignedCertificate) { if (FIPS140.useCompliantAlgorithms()) { if (allowSelfSignedCertificate) { - throw new IllegalArgumentException(Messages.AmazonEC2Cloud_selfSignedCertificateNotAllowedInFIPSMode()); + throw new IllegalArgumentException(Messages.EC2Cloud_selfSignedCertificateNotAllowedInFIPSMode()); } } } @@ -128,7 +128,7 @@ public static void ensurePrivateKeyInFipsMode(String privateKeyString) { return; } if (StringUtils.isBlank(privateKeyString)) { - throw new IllegalArgumentException(Messages.AmazonEC2Cloud_keyIsMandatoryInFIPSMode()); + throw new IllegalArgumentException(Messages.EC2Cloud_keyIsMandatoryInFIPSMode()); } try { Key privateKey = PEMEncodable.decode(privateKeyString).toPrivateKey(); @@ -148,7 +148,7 @@ public static void ensurePublicKeyInFipsMode(@NonNull String algorithm, @NonNull .filter((keyAlgorithm) -> keyAlgorithm.getKeyFormat().equals(algorithm)) .findFirst() .orElseThrow(() -> new IllegalArgumentException( - Messages.AmazonEC2Cloud_keyIsNotApprovedInFIPSMode(algorithm))); + Messages.EC2Cloud_keyIsNotApprovedInFIPSMode(algorithm))); try { Key publicKey = publicKeyPrivateKeyKeyAlgorithm.decodePublicKey(key); ensureKeyInFipsMode(publicKey); diff --git a/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java index 9f484f122..7993d4c3c 100644 --- a/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java @@ -41,9 +41,9 @@ private void assertValidKey(Key key) { private void assertTLSIsNonCompliant(CheckedRunnable block) { try { block.run(); - fail("Should be invalid with message: " + Messages.AmazonEC2Cloud_tlsIsRequiredInFIPSMode()); + fail("Should be invalid with message: " + Messages.EC2Cloud_tlsIsRequiredInFIPSMode()); } catch (IllegalArgumentException e) { - assertEquals(Messages.AmazonEC2Cloud_tlsIsRequiredInFIPSMode(), e.getMessage()); + assertEquals(Messages.EC2Cloud_tlsIsRequiredInFIPSMode(), e.getMessage()); } catch (Throwable e) { fail("Unexpected error: " + e.getMessage()); } @@ -64,7 +64,7 @@ public void testDSAInvalidKeyMessage() { DSAPublicKey key = Mockito.mock(DSAPublicKey.class, Mockito.RETURNS_DEEP_STUBS); Mockito.when(key.getParams().getP().bitLength()).thenReturn(2047); - assertInvalidKey(key, Messages.AmazonEC2Cloud_invalidKeySizeInFIPSMode()); + assertInvalidKey(key, Messages.EC2Cloud_invalidKeySizeInFIPSMode()); } @Test @@ -80,7 +80,7 @@ public void testRSAInvalidKeyMessage() { RSAPublicKey key = Mockito.mock(RSAPublicKey.class, Mockito.RETURNS_DEEP_STUBS); Mockito.when(key.getModulus().bitLength()).thenReturn(2047); - assertInvalidKey(key, Messages.AmazonEC2Cloud_invalidKeySizeInFIPSMode()); + assertInvalidKey(key, Messages.EC2Cloud_invalidKeySizeInFIPSMode()); } @Test @@ -98,7 +98,7 @@ public void testECDSAInvalidKeyMessage() { Mockito.withSettings().extraInterfaces(Key.class).defaultAnswer(Mockito.RETURNS_DEEP_STUBS)); Mockito.when(key.getParams().getCurve().getField().getFieldSize()).thenReturn(223); - assertInvalidKey((Key) key, Messages.AmazonEC2Cloud_invalidKeySizeECInFIPSMode()); + assertInvalidKey((Key) key, Messages.EC2Cloud_invalidKeySizeECInFIPSMode()); } @Test @@ -117,7 +117,7 @@ public void testUnknownInstance() { Key key = Mockito.mock(Key.class); Mockito.when(key.getAlgorithm()).thenReturn(message); - assertInvalidKey(key, Messages.AmazonEC2Cloud_keyIsNotApprovedInFIPSMode(message)); + assertInvalidKey(key, Messages.EC2Cloud_keyIsNotApprovedInFIPSMode(message)); } @Test @@ -170,7 +170,7 @@ public void testNotAllowSelfSignedCertificate() { @Test public void testAllowSelfSignedCertificate() { - String expectedMessage = Messages.AmazonEC2Cloud_selfSignedCertificateNotAllowedInFIPSMode(); + String expectedMessage = Messages.EC2Cloud_selfSignedCertificateNotAllowedInFIPSMode(); try { FIPS140Utils.ensureNoSelfSignedCertificate(true); fail("Should be invalid with message: " + expectedMessage); From 126a1ef0ffbf431992ed7f94df80baa59ae7316d Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Wed, 22 Jan 2025 15:08:54 +0100 Subject: [PATCH 109/267] Implement ensurePublicKeyInFipsMode using mina --- .../hudson/plugins/ec2/util/FIPS140Utils.java | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java index b6c6af509..7d3e9c3f5 100644 --- a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java +++ b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java @@ -1,14 +1,10 @@ package hudson.plugins.ec2.util; -import com.trilead.ssh2.signature.KeyAlgorithm; -import com.trilead.ssh2.signature.KeyAlgorithmManager; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.plugins.ec2.Messages; import java.io.IOException; import java.net.URL; import java.security.Key; -import java.security.PrivateKey; -import java.security.PublicKey; import java.security.UnrecoverableKeyException; import java.security.interfaces.DSAKey; import java.security.interfaces.ECKey; @@ -16,6 +12,11 @@ import jenkins.bouncycastle.api.PEMEncodable; import jenkins.security.FIPS140; import org.apache.commons.lang.StringUtils; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.params.DSAPublicKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.RSAKeyParameters; +import org.bouncycastle.crypto.util.OpenSSHPublicKeyUtil; /** * FIPS related utility methods (check Private and Public keys, ...) @@ -143,17 +144,25 @@ public static void ensurePublicKeyInFipsMode(@NonNull String algorithm, @NonNull return; } - KeyAlgorithm publicKeyPrivateKeyKeyAlgorithm = - KeyAlgorithmManager.getSupportedAlgorithms().stream() - .filter((keyAlgorithm) -> keyAlgorithm.getKeyFormat().equals(algorithm)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException( - Messages.EC2Cloud_keyIsNotApprovedInFIPSMode(algorithm))); - try { - Key publicKey = publicKeyPrivateKeyKeyAlgorithm.decodePublicKey(key); - ensureKeyInFipsMode(publicKey); - } catch (RuntimeException | IOException e) { - throw new IllegalArgumentException(e.getMessage(), e); + AsymmetricKeyParameter asymmetricKeyParameter = OpenSSHPublicKeyUtil.parsePublicKey(key); + + if (asymmetricKeyParameter instanceof RSAKeyParameters) { + RSAKeyParameters rsaKeyParameters = (RSAKeyParameters) asymmetricKeyParameter; + if (rsaKeyParameters.getModulus().bitLength() < 2048) { + throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeInFIPSMode()); + } + } else if (asymmetricKeyParameter instanceof DSAPublicKeyParameters) { + DSAPublicKeyParameters dsaPublicKeyParameters = (DSAPublicKeyParameters) asymmetricKeyParameter; + if (dsaPublicKeyParameters.getParameters().getP().bitLength() < 2048) { + throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeInFIPSMode()); + } + } else if (asymmetricKeyParameter instanceof ECPublicKeyParameters) { + ECPublicKeyParameters ecPublicKeyParameters = (ECPublicKeyParameters) asymmetricKeyParameter; + if (ecPublicKeyParameters.getParameters().getCurve().getFieldSize() < 224) { + throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeECInFIPSMode()); + } + } else { + throw new IllegalArgumentException(Messages.EC2Cloud_keyIsNotApprovedInFIPSMode(algorithm)); } } } From da58504f9af8b995de4f542f77a0693bc24924de Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Wed, 22 Jan 2025 15:09:18 +0100 Subject: [PATCH 110/267] Remove the RuntimeException catch --- .../hudson/plugins/ec2/util/FIPS140Utils.java | 31 ++++++++----------- .../ec2/util/FIPS140UtilsWithFIPSTest.java | 9 ------ 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java index 7d3e9c3f5..54ad480db 100644 --- a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java +++ b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java @@ -39,25 +39,20 @@ public static void ensureKeyInFipsMode(Key key) { if (!FIPS140.useCompliantAlgorithms()) { return; } - try { - if (key instanceof RSAKey) { - if (((RSAKey) key).getModulus().bitLength() < 2048) { - throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeInFIPSMode()); - } - } else if (key instanceof DSAKey) { - if (((DSAKey) key).getParams().getP().bitLength() < 2048) { - throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeInFIPSMode()); - } - } else if (key instanceof ECKey) { - if (((ECKey) key).getParams().getCurve().getField().getFieldSize() < 224) { - throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeECInFIPSMode()); - } - } else { - throw new IllegalArgumentException( - Messages.EC2Cloud_keyIsNotApprovedInFIPSMode(key.getAlgorithm())); + if (key instanceof RSAKey) { + if (((RSAKey) key).getModulus().bitLength() < 2048) { + throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeInFIPSMode()); } - } catch (RuntimeException e) { - throw new IllegalArgumentException(e.getMessage(), e); + } else if (key instanceof DSAKey) { + if (((DSAKey) key).getParams().getP().bitLength() < 2048) { + throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeInFIPSMode()); + } + } else if (key instanceof ECKey) { + if (((ECKey) key).getParams().getCurve().getField().getFieldSize() < 224) { + throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeECInFIPSMode()); + } + } else { + throw new IllegalArgumentException(Messages.EC2Cloud_keyIsNotApprovedInFIPSMode(key.getAlgorithm())); } } diff --git a/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java index 7993d4c3c..b00a99421 100644 --- a/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/util/FIPS140UtilsWithFIPSTest.java @@ -120,15 +120,6 @@ public void testUnknownInstance() { assertInvalidKey(key, Messages.EC2Cloud_keyIsNotApprovedInFIPSMode(message)); } - @Test - public void testRuntimeException() { - String message = "The test message"; - Key key = Mockito.mock(Key.class); - Mockito.when(key.getAlgorithm()).thenThrow(new RuntimeException(message)); - - assertInvalidKey(key, message); - } - @Test public void testTLSCheckWithHTTPAndPassword() { assertTLSIsNonCompliant(() -> FIPS140Utils.ensureNoPasswordLeak(new URL("http://localhost"), "non-empty")); From 7e34b304ef87bb9c1f22fdd1b090acdb0f549d94 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Wed, 22 Jan 2025 15:09:40 +0100 Subject: [PATCH 111/267] Add FIPS validation of decoded KeyPair --- src/main/java/hudson/plugins/ec2/util/KeyHelper.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/hudson/plugins/ec2/util/KeyHelper.java b/src/main/java/hudson/plugins/ec2/util/KeyHelper.java index 44fc2e611..28bfd4314 100644 --- a/src/main/java/hudson/plugins/ec2/util/KeyHelper.java +++ b/src/main/java/hudson/plugins/ec2/util/KeyHelper.java @@ -59,20 +59,26 @@ public static KeyPair decodeKeyPair(@NonNull String pem, @NonNull String passwor PEMKeyPair decryptedKeyPair = ((PEMEncryptedKeyPair) object) .decryptKeyPair(new JcePEMDecryptorProviderBuilder().build(password.toCharArray())); PrivateKey privateKey = converter.getPrivateKey(decryptedKeyPair.getPrivateKeyInfo()); + FIPS140Utils.ensureKeyInFipsMode(privateKey); PublicKey publicKey = converter.getPublicKey(decryptedKeyPair.getPublicKeyInfo()); return new KeyPair(publicKey, privateKey); } else if (object instanceof PrivateKeyInfo) { PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) object; PrivateKey privateKey = converter.getPrivateKey(privateKeyInfo); + FIPS140Utils.ensureKeyInFipsMode(privateKey); PublicKey publicKey = generatePublicKeyFromPrivateKey(privateKeyInfo, privateKey); return new KeyPair(publicKey, privateKey); } else if (object instanceof SubjectPublicKeyInfo) { PublicKey publicKey = converter.getPublicKey((SubjectPublicKeyInfo) object); + FIPS140Utils.ensureKeyInFipsMode(publicKey); return new KeyPair(publicKey, null); } else if (object instanceof PEMKeyPair) { SubjectPublicKeyInfo publicKeyInfo = ((PEMKeyPair) object).getPublicKeyInfo(); PrivateKeyInfo privateKeyInfo = ((PEMKeyPair) object).getPrivateKeyInfo(); - return new KeyPair(converter.getPublicKey(publicKeyInfo), converter.getPrivateKey(privateKeyInfo)); + PrivateKey privateKey = converter.getPrivateKey(privateKeyInfo); + FIPS140Utils.ensureKeyInFipsMode(privateKey); + PublicKey publicKey = converter.getPublicKey(publicKeyInfo); + return new KeyPair(publicKey, privateKey); } else { throw new IllegalArgumentException( "Unsupported PEM object type: " + object.getClass().getName()); From 276ac811a880373ed0970a22d29a2c235b62b746 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Wed, 22 Jan 2025 15:32:58 +0100 Subject: [PATCH 112/267] Code cleanup --- .../java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java index 1eece09d3..c1894ea78 100644 --- a/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java @@ -17,7 +17,7 @@ public class WinConnectionWithFIPSTest { * Self-signed certificate should not be allowed in FIPS mode, an {@link IllegalArgumentException} is expected */ @Test(expected = IllegalArgumentException.class) - public void testSelfSignedCertificateNotAllowed() throws Exception { + public void testSelfSignedCertificateNotAllowed() { new WinConnection("", "", "", true); } From 8d87877103deeace6d6f01b972d5a5419b1cd3d8 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Wed, 22 Jan 2025 16:41:27 +0100 Subject: [PATCH 113/267] Enable tests --- .../java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java index cc8b5f9aa..a2b36873a 100644 --- a/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java @@ -67,7 +67,6 @@ public void testCreateWindowsDataWithoutPassword() throws Descriptor.FormExcepti } @Test - @Ignore("Disabled until the plugins dependencies are FIPS compliant") public void testDoCheckUseHTTPSWithPassword() { FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(true, "yes"); @@ -75,7 +74,6 @@ public void testDoCheckUseHTTPSWithPassword() { } @Test - @Ignore("Disabled until the plugins dependencies are FIPS compliant") public void testDoCheckUseHTTPSWithoutPassword() { FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(true, ""); @@ -83,7 +81,6 @@ public void testDoCheckUseHTTPSWithoutPassword() { } @Test - @Ignore("Disabled until the plugins dependencies are FIPS compliant") public void testDoCheckUseHTTPWithPassword() { FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(false, "yes"); @@ -91,7 +88,6 @@ public void testDoCheckUseHTTPWithPassword() { } @Test - @Ignore("Disabled until the plugins dependencies are FIPS compliant") public void testDoCheckUseHTTPWithoutPassword() { FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckUseHTTPS(false, ""); @@ -99,7 +95,6 @@ public void testDoCheckUseHTTPWithoutPassword() { } @Test - @Ignore("Disabled until the plugins dependencies are FIPS compliant") public void testDoCheckAllowSelfSignedCertificateChecked() { FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckAllowSelfSignedCertificate(true); @@ -107,7 +102,6 @@ public void testDoCheckAllowSelfSignedCertificateChecked() { } @Test - @Ignore("Disabled until the plugins dependencies are FIPS compliant") public void testDoCheckAllowSelfSignedCertificateNotChecked() { FormValidation formValidation = ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class) .doCheckAllowSelfSignedCertificate(false); From 8608cdc6c7f7ee11d6e0fc0256b72e2295c76fcd Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Wed, 22 Jan 2025 16:48:38 +0100 Subject: [PATCH 114/267] Add Windows password length validation --- src/main/java/hudson/plugins/ec2/WindowsData.java | 15 +++++++++++++++ .../hudson/plugins/ec2/util/FIPS140Utils.java | 14 ++++++++++++++ .../hudson/plugins/ec2/Messages.properties | 1 + .../plugins/ec2/WindowsDataWithFIPSTest.java | 15 ++++++++++++++- .../plugins/ec2/WindowsDataWithoutFIPSTest.java | 14 ++++++++++++++ 5 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/main/java/hudson/plugins/ec2/WindowsData.java b/src/main/java/hudson/plugins/ec2/WindowsData.java index 3093b1b80..d086ecb9a 100644 --- a/src/main/java/hudson/plugins/ec2/WindowsData.java +++ b/src/main/java/hudson/plugins/ec2/WindowsData.java @@ -113,6 +113,21 @@ public String getDisplayName() { return "windows"; } + @POST + @SuppressWarnings("unused") + public FormValidation doCheckPassword(@QueryParameter String password) { + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + // for security reasons, do not perform any check if the user is not an admin + return FormValidation.ok(); + } + try { + FIPS140Utils.ensurePasswordLength(password); + } catch (IllegalArgumentException ex) { + return FormValidation.error(ex, ex.getLocalizedMessage()); + } + return FormValidation.ok(); + } + @POST @SuppressWarnings("unused") public FormValidation doCheckUseHTTPS(@QueryParameter boolean useHTTPS, @QueryParameter String password) { diff --git a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java index 54ad480db..d93dd652f 100644 --- a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java +++ b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java @@ -93,6 +93,20 @@ public static void ensureNoPasswordLeak(boolean useHTTPS, boolean usePassword) { } } + /** + * Password length check chen FIPS mode is requested. If FIPS mode is not requested, this method does nothing. + * Otherwise, ensure that the password length is at least 14 char long. + * @param password the password to check + * @throws IllegalArgumentException if FIPS mode is requested and the password is too short + */ + public static void ensurePasswordLength(String password) { + if (FIPS140.useCompliantAlgorithms()) { + if (StringUtils.isBlank(password) || password.length() < 14) { + throw new IllegalArgumentException(Messages.EC2Cloud_passwordLengthInFIPSMode()); + } + } + } + /** * Password leak prevention when FIPS mode is requested. If FIPS mode is not requested, this method does nothing. * Otherwise, ensure that no password can be leaked. diff --git a/src/main/resources/hudson/plugins/ec2/Messages.properties b/src/main/resources/hudson/plugins/ec2/Messages.properties index 2bb37699f..40fbce940 100644 --- a/src/main/resources/hudson/plugins/ec2/Messages.properties +++ b/src/main/resources/hudson/plugins/ec2/Messages.properties @@ -17,4 +17,5 @@ EC2Cloud.keyIsNotApprovedInFIPSMode=Key is not valid: {0} EC2Cloud.invalidKeySizeInFIPSMode=Invalid key size, at least 2048 is needed in FIPS mode. EC2Cloud.invalidKeySizeECInFIPSMode=Invalid curve size, at least 224 is needed in FIPS mode. EC2Cloud.keyIsMandatoryInFIPSMode=The key is mandatory in FIPS mode. +EC2Cloud.passwordLengthInFIPSMode=When running in FIPS compliance mode, the password must be at least 14 characters long. General.MissingPermission=You do not have the Overall/Administer right to modify this field diff --git a/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java index a2b36873a..bfb5c98c3 100644 --- a/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java @@ -7,7 +7,6 @@ import hudson.util.FormValidation; import jenkins.security.FIPS140; import org.junit.ClassRule; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.FlagRule; @@ -107,4 +106,18 @@ public void testDoCheckAllowSelfSignedCertificateNotChecked() { .doCheckAllowSelfSignedCertificate(false); assertEquals(FormValidation.Kind.OK, formValidation.kind); } + + @Test + public void testDoCheckPasswordLengthLessThan14() { + FormValidation formValidation = + ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckPassword("123"); + assertEquals(FormValidation.Kind.ERROR, formValidation.kind); + } + + @Test + public void testDoCheckPasswordLengthGreaterThan14() { + FormValidation formValidation = + ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckPassword("12345678901234567890"); + assertEquals(FormValidation.Kind.OK, formValidation.kind); + } } diff --git a/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java b/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java index 56ae28cb3..7a6c53896 100644 --- a/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/WindowsDataWithoutFIPSTest.java @@ -89,4 +89,18 @@ public void testDoCheckAllowSelfSignedCertificateNotChecked() { .doCheckAllowSelfSignedCertificate(false); assertEquals(FormValidation.Kind.OK, formValidation.kind); } + + @Test + public void testDoCheckPasswordLengthLessThan14() { + FormValidation formValidation = + ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckPassword("123"); + assertEquals(FormValidation.Kind.OK, formValidation.kind); + } + + @Test + public void testDoCheckPasswordLengthGreaterThan14() { + FormValidation formValidation = + ExtensionList.lookupSingleton(WindowsData.DescriptorImpl.class).doCheckPassword("12345678901234567890"); + assertEquals(FormValidation.Kind.OK, formValidation.kind); + } } From bac89fb25fe9f1ea0fda61eda28fec805079ba0f Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Wed, 29 Jan 2025 13:50:58 +0100 Subject: [PATCH 115/267] Add password length validation in the constructor --- src/main/java/hudson/plugins/ec2/WindowsData.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/hudson/plugins/ec2/WindowsData.java b/src/main/java/hudson/plugins/ec2/WindowsData.java index d086ecb9a..fec5a696e 100644 --- a/src/main/java/hudson/plugins/ec2/WindowsData.java +++ b/src/main/java/hudson/plugins/ec2/WindowsData.java @@ -29,6 +29,11 @@ public WindowsData( boolean specifyPassword, boolean allowSelfSignedCertificate) throws Descriptor.FormException { + try { + FIPS140Utils.ensurePasswordLength(password); + } catch (IllegalArgumentException e) { + throw new Descriptor.FormException(e, "password"); + } try { FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); } catch (IllegalArgumentException e) { From 93c5a3c9ba06a35f3d5c95d6fd37431b7b25332a Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Wed, 29 Jan 2025 14:30:41 +0100 Subject: [PATCH 116/267] Only validate password when it is in use --- src/main/java/hudson/plugins/ec2/WindowsData.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/WindowsData.java b/src/main/java/hudson/plugins/ec2/WindowsData.java index fec5a696e..c6202fb88 100644 --- a/src/main/java/hudson/plugins/ec2/WindowsData.java +++ b/src/main/java/hudson/plugins/ec2/WindowsData.java @@ -29,11 +29,6 @@ public WindowsData( boolean specifyPassword, boolean allowSelfSignedCertificate) throws Descriptor.FormException { - try { - FIPS140Utils.ensurePasswordLength(password); - } catch (IllegalArgumentException e) { - throw new Descriptor.FormException(e, "password"); - } try { FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); } catch (IllegalArgumentException e) { @@ -53,6 +48,14 @@ public WindowsData( } this.specifyPassword = specifyPassword; + try { + if (specifyPassword) { + FIPS140Utils.ensurePasswordLength(password); + } + } catch (IllegalArgumentException e) { + throw new Descriptor.FormException(e, "password"); + } + this.allowSelfSignedCertificate = allowSelfSignedCertificate; } From bd6a541960df2685b28ff5f7068d1cb4a4925a1b Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Wed, 29 Jan 2025 18:04:10 +0100 Subject: [PATCH 117/267] Fix tests and add password length validation --- .../java/hudson/plugins/ec2/WindowsData.java | 6 +---- .../hudson/plugins/ec2/win/WinConnection.java | 1 + .../plugins/ec2/WindowsDataWithFIPSTest.java | 20 ++++++++++------- .../ec2/win/WinConnectionWithFIPSTest.java | 22 +++++++++---------- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/WindowsData.java b/src/main/java/hudson/plugins/ec2/WindowsData.java index c6202fb88..0c7204e10 100644 --- a/src/main/java/hudson/plugins/ec2/WindowsData.java +++ b/src/main/java/hudson/plugins/ec2/WindowsData.java @@ -29,11 +29,6 @@ public WindowsData( boolean specifyPassword, boolean allowSelfSignedCertificate) throws Descriptor.FormException { - try { - FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); - } catch (IllegalArgumentException e) { - throw new Descriptor.FormException(e, "password"); - } try { FIPS140Utils.ensureNoSelfSignedCertificate(allowSelfSignedCertificate); } catch (IllegalArgumentException e) { @@ -50,6 +45,7 @@ public WindowsData( try { if (specifyPassword) { + FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); FIPS140Utils.ensurePasswordLength(password); } } catch (IllegalArgumentException e) { diff --git a/src/main/java/hudson/plugins/ec2/win/WinConnection.java b/src/main/java/hudson/plugins/ec2/win/WinConnection.java index c4ad4e3f9..9e4a8db9b 100644 --- a/src/main/java/hudson/plugins/ec2/win/WinConnection.java +++ b/src/main/java/hudson/plugins/ec2/win/WinConnection.java @@ -46,6 +46,7 @@ public WinConnection(String host, String username, String password) { public WinConnection(String host, String username, String password, boolean allowSelfSignedCertificate) { FIPS140Utils.ensureNoSelfSignedCertificate(allowSelfSignedCertificate); + FIPS140Utils.ensurePasswordLength(password); this.host = host; this.username = username; diff --git a/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java index bfb5c98c3..7e11ff9ee 100644 --- a/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java @@ -36,7 +36,16 @@ public void testSelfSignedCertificateNotAllowed() throws Descriptor.FormExceptio @Test(expected = Descriptor.FormException.class) @WithoutJenkins public void testCreateWindowsDataWithPasswordWithoutTLS() throws Descriptor.FormException { - new WindowsData("yes", false, "", true, false); + new WindowsData("01234567890123456789", false, "", true, false); + } + + /** + * Using a password less than 14 chars, an {@link Descriptor.FormException} is expected + */ + @Test(expected = Descriptor.FormException.class) + @WithoutJenkins + public void testCreateWindowsDataWithShortPassword() throws Descriptor.FormException { + new WindowsData("0123", true, "", true, false); } /** @@ -45,9 +54,9 @@ public void testCreateWindowsDataWithPasswordWithoutTLS() throws Descriptor.Form @Test @WithoutJenkins public void testCreateWindowsDataWithPasswordWithTLS() throws Descriptor.FormException { - new WindowsData("yes", true, "", true, false); + new WindowsData("01234567890123456789", true, "", true, false); // specifyPassword is set to true in the constructor - new WindowsData("yes", true, "", false, false); + new WindowsData("01234567890123456789", true, "", false, false); } /** @@ -56,12 +65,7 @@ public void testCreateWindowsDataWithPasswordWithTLS() throws Descriptor.FormExc @Test @WithoutJenkins public void testCreateWindowsDataWithoutPassword() throws Descriptor.FormException { - new WindowsData("", false, "", true, false); - // specifyPassword is set to true in the constructor new WindowsData("", false, "", false, false); - - new WindowsData("", true, "", true, false); - // specifyPassword is set to true in the constructor new WindowsData("", true, "", false, false); } diff --git a/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java index c1894ea78..6f455863d 100644 --- a/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java @@ -26,15 +26,7 @@ public void testSelfSignedCertificateNotAllowed() { */ @Test public void testValidCreation() { - new WinConnection("", "", "", false); - } - - /** - * It should be allowed to disable useHTTPS only when no password is used, an {@link IllegalArgumentException} is not expected - */ - @Test - public void testSetUseHTTPSFalseWithoutPassword() { - new WinConnection("", "", "", false).setUseHTTPS(false); + new WinConnection("", "", "0123456790123456789", false); } /** @@ -42,7 +34,7 @@ public void testSetUseHTTPSFalseWithoutPassword() { */ @Test(expected = IllegalArgumentException.class) public void testSetUseHTTPSFalseWithPassword() { - new WinConnection("", "alice", "yes", false).setUseHTTPS(false); + new WinConnection("", "alice", "0123456790123456789", false).setUseHTTPS(false); } /** @@ -50,8 +42,16 @@ public void testSetUseHTTPSFalseWithPassword() { */ @Test(expected = IllegalArgumentException.class) public void testBuildWinRMClientWithoutTLS() { - WinConnection winConnection = new WinConnection("", "alice", "yes", false); + WinConnection winConnection = new WinConnection("", "alice", "0123456790123456789", false); winConnection.winrm(); fail("The creation of the WinRMClient should fail"); } + + /** + * When using a password less than 14 chars, an {@link IllegalArgumentException} is expected + */ + @Test(expected = IllegalArgumentException.class) + public void testSetShortPassword() { + new WinConnection("", "alice", "0123", false).setUseHTTPS(false); + } } From 828850f7f15566588c73bd498fde4d4d2c5ee706 Mon Sep 17 00:00:00 2001 From: Desprez Jean-Marc Date: Thu, 30 Jan 2025 14:23:29 +0100 Subject: [PATCH 118/267] [JENKINS-75187] Unable to Connect to Agents (#1041) * Fix java install and scp * Fix SSH host key verification --- .../plugins/ec2/ssh/EC2MacLauncher.java | 82 ++++++++++--------- .../plugins/ec2/ssh/EC2UnixLauncher.java | 60 ++++++++------ 2 files changed, 77 insertions(+), 65 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index caaabc5e1..d5e969916 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -84,6 +84,9 @@ import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.scp.client.CloseableScpClient; import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.util.OpenSSHPublicKeyUtil; +import org.bouncycastle.crypto.util.PublicKeyFactory; /** * {@link ComputerLauncher} that connects to a Unix agent on EC2 by using SSH. @@ -310,46 +313,46 @@ protected void launchScript(EC2Computer computer, TaskListener listener) } IOUtils.copy(channel.getInvertedOut(), logger); } + } - try { - Instance nodeInstance = computer.describeInstance(); - if (nodeInstance.getInstanceType().equals("mac2.metal")) { - LOGGER.info("Running Command for mac2.metal"); - executeRemote( - computer, - clientSession, - javaPath + " -fullversion", - "curl -L -O " - + CORRETTO_LATEST_URL - + "/amazon-corretto-11-aarch64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-aarch64-macos-jdk.pkg -target /", - logger, - listener); - } else { - executeRemote( - computer, - clientSession, - javaPath + " -fullversion", - "curl -L -O " - + CORRETTO_LATEST_URL - + "/amazon-corretto-11-x64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-x64-macos-jdk.pkg -target /", - logger, - listener); - } - } catch (InterruptedException ex) { - LOGGER.warning(ex.getMessage()); + try { + Instance nodeInstance = computer.describeInstance(); + if (nodeInstance.getInstanceType().equals("mac2.metal")) { + LOGGER.info("Running Command for mac2.metal"); + executeRemote( + computer, + clientSession, + javaPath + " -fullversion", + "curl -L -O " + + CORRETTO_LATEST_URL + + "/amazon-corretto-11-aarch64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-aarch64-macos-jdk.pkg -target /", + logger, + listener); + } else { + executeRemote( + computer, + clientSession, + javaPath + " -fullversion", + "curl -L -O " + + CORRETTO_LATEST_URL + + "/amazon-corretto-11-x64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-x64-macos-jdk.pkg -target /", + logger, + listener); } - - // Always copy so we get the most recent remoting.jar - logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); - scp.upload( - Jenkins.get().getJnlpJars("remoting.jar").readFully(), - tmpDir + "/remoting.jar", - List.of( - PosixFilePermission.OWNER_READ, - PosixFilePermission.GROUP_READ, - PosixFilePermission.OTHERS_READ), - scpTimestamp); + } catch (InterruptedException ex) { + LOGGER.warning(ex.getMessage()); } + + // Always copy so we get the most recent remoting.jar + logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); + scp.upload( + Jenkins.get().getJnlpJars("remoting.jar").readFully(), + tmpDir + "/remoting.jar", + List.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.GROUP_READ, + PosixFilePermission.OTHERS_READ), + scpTimestamp); } } client.stop(); @@ -670,10 +673,13 @@ public boolean verifyServerKey(ClientSession clientSession, SocketAddress remote } SlaveTemplate template = computer.getSlaveTemplate(); try { + AsymmetricKeyParameter parameters = PublicKeyFactory.createKey(serverKey.getEncoded()); + byte[] openSSHBytes = OpenSSHPublicKeyUtil.encodePublicKey(parameters); + return template != null && template.getHostKeyVerificationStrategy() .getStrategy() - .verify(computer, new HostKey(sshAlgorithm, serverKey.getEncoded()), listener); + .verify(computer, new HostKey(sshAlgorithm, openSSHBytes), listener); } catch (Exception exception) { // false will trigger a SSHException which is a subclass of IOException. // Therefore, it is not needed to throw a RuntimeException. diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index bc695dda8..b78278252 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -86,6 +86,9 @@ import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.scp.client.CloseableScpClient; import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.util.OpenSSHPublicKeyUtil; +import org.bouncycastle.crypto.util.PublicKeyFactory; /** * {@link ComputerLauncher} that connects to a Unix agent on EC2 by using SSH. @@ -312,33 +315,33 @@ protected void launchScript(EC2Computer computer, TaskListener listener) } IOUtils.copy(channel.getInvertedOut(), logger); } - - executeRemote( - computer, - clientSession, - javaPath + " -fullversion", - "sudo amazon-linux-extras install java-openjdk11 -y; sudo yum install -y fontconfig java-11-openjdk", - logger, - listener); - executeRemote( - computer, - clientSession, - "which scp", - "sudo yum install -y openssh-clients", - logger, - listener); - - // Always copy so we get the most recent remoting.jar - logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); - scp.upload( - Jenkins.get().getJnlpJars("remoting.jar").readFully(), - tmpDir + "/remoting.jar", - List.of( - PosixFilePermission.OWNER_READ, - PosixFilePermission.GROUP_READ, - PosixFilePermission.OTHERS_READ), - scpTimestamp); } + + executeRemote( + computer, + clientSession, + javaPath + " -fullversion", + "sudo amazon-linux-extras install java-openjdk11 -y; sudo yum install -y fontconfig java-11-openjdk", + logger, + listener); + executeRemote( + computer, + clientSession, + "which scp", + "sudo yum install -y openssh-clients", + logger, + listener); + + // Always copy so we get the most recent remoting.jar + logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); + scp.upload( + Jenkins.get().getJnlpJars("remoting.jar").readFully(), + tmpDir + "/remoting.jar", + List.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.GROUP_READ, + PosixFilePermission.OTHERS_READ), + scpTimestamp); } } client.stop(); @@ -694,10 +697,13 @@ public boolean verifyServerKey(ClientSession clientSession, SocketAddress remote } SlaveTemplate template = computer.getSlaveTemplate(); try { + AsymmetricKeyParameter parameters = PublicKeyFactory.createKey(serverKey.getEncoded()); + byte[] openSSHBytes = OpenSSHPublicKeyUtil.encodePublicKey(parameters); + return template != null && template.getHostKeyVerificationStrategy() .getStrategy() - .verify(computer, new HostKey(sshAlgorithm, serverKey.getEncoded()), listener); + .verify(computer, new HostKey(sshAlgorithm, openSSHBytes), listener); } catch (Exception exception) { // false will trigger a SSHException which is a subclass of IOException. // Therefore, it is not needed to throw a RuntimeException. From 22bda8fb5d07f38e51cb0e403dc6afb7a87e250d Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Fri, 31 Jan 2025 12:25:51 +0100 Subject: [PATCH 119/267] Code cleanup on tests --- .../plugins/ec2/WindowsDataWithFIPSTest.java | 19 ++++---- .../ec2/win/WinConnectionWithFIPSTest.java | 25 ++++++----- .../win/winrm/WinRMClientWithFIPSTest.java | 45 +++++++++++-------- .../ec2/win/winrm/WinRMWithFIPSTest.java | 14 +++--- 4 files changed, 60 insertions(+), 43 deletions(-) diff --git a/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java index 7e11ff9ee..224902c58 100644 --- a/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/WindowsDataWithFIPSTest.java @@ -24,28 +24,29 @@ public class WindowsDataWithFIPSTest { /** * Self-signed certificate should not be allowed in FIPS mode, an {@link Descriptor.FormException} is expected */ - @Test(expected = Descriptor.FormException.class) + @Test @WithoutJenkins - public void testSelfSignedCertificateNotAllowed() throws Descriptor.FormException { - new WindowsData("", true, "", true, true); + public void testSelfSignedCertificateNotAllowed() { + assertThrows(Descriptor.FormException.class, () -> new WindowsData("", true, "", true, true)); } /** * Using a password without using TLS, an {@link Descriptor.FormException} is expected */ - @Test(expected = Descriptor.FormException.class) + @Test @WithoutJenkins - public void testCreateWindowsDataWithPasswordWithoutTLS() throws Descriptor.FormException { - new WindowsData("01234567890123456789", false, "", true, false); + public void testCreateWindowsDataWithPasswordWithoutTLS() { + assertThrows( + Descriptor.FormException.class, () -> new WindowsData("01234567890123456789", false, "", true, false)); } /** * Using a password less than 14 chars, an {@link Descriptor.FormException} is expected */ - @Test(expected = Descriptor.FormException.class) + @Test @WithoutJenkins - public void testCreateWindowsDataWithShortPassword() throws Descriptor.FormException { - new WindowsData("0123", true, "", true, false); + public void testCreateWindowsDataWithShortPassword() { + assertThrows(Descriptor.FormException.class, () -> new WindowsData("0123", true, "", true, false)); } /** diff --git a/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java index 6f455863d..3c6ef7303 100644 --- a/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/win/WinConnectionWithFIPSTest.java @@ -1,6 +1,6 @@ package hudson.plugins.ec2.win; -import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; import jenkins.security.FIPS140; import org.junit.ClassRule; @@ -16,9 +16,9 @@ public class WinConnectionWithFIPSTest { /** * Self-signed certificate should not be allowed in FIPS mode, an {@link IllegalArgumentException} is expected */ - @Test(expected = IllegalArgumentException.class) + @Test public void testSelfSignedCertificateNotAllowed() { - new WinConnection("", "", "", true); + assertThrows(IllegalArgumentException.class, () -> new WinConnection("", "", "", true)); } /** @@ -32,26 +32,29 @@ public void testValidCreation() { /** * It should not be allowed to disable useHTTPS only when a password is used, an {@link IllegalArgumentException} is expected */ - @Test(expected = IllegalArgumentException.class) + @Test public void testSetUseHTTPSFalseWithPassword() { - new WinConnection("", "alice", "0123456790123456789", false).setUseHTTPS(false); + assertThrows(IllegalArgumentException.class, () -> new WinConnection("", "alice", "0123456790123456789", false) + .setUseHTTPS(false)); } /** * Password leak prevention when setUseHTTPS is not called */ - @Test(expected = IllegalArgumentException.class) + @Test public void testBuildWinRMClientWithoutTLS() { - WinConnection winConnection = new WinConnection("", "alice", "0123456790123456789", false); - winConnection.winrm(); - fail("The creation of the WinRMClient should fail"); + assertThrows(IllegalArgumentException.class, () -> { + WinConnection winConnection = new WinConnection("", "alice", "0123456790123456789", false); + winConnection.winrm(); + }); } /** * When using a password less than 14 chars, an {@link IllegalArgumentException} is expected */ - @Test(expected = IllegalArgumentException.class) + @Test public void testSetShortPassword() { - new WinConnection("", "alice", "0123", false).setUseHTTPS(false); + assertThrows( + IllegalArgumentException.class, () -> new WinConnection("", "alice", "0123", false).setUseHTTPS(false)); } } diff --git a/src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithFIPSTest.java index fb4a8f9f0..87614ebb1 100644 --- a/src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMClientWithFIPSTest.java @@ -1,5 +1,6 @@ package hudson.plugins.ec2.win.winrm; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import java.net.MalformedURLException; @@ -18,17 +19,21 @@ public class WinRMClientWithFIPSTest { /** * Self-signed certificate should not be allowed in FIPS mode, an {@link IllegalArgumentException} is expected */ - @Test(expected = IllegalArgumentException.class) - public void testSelfSignedCertificateNotAllowed() throws MalformedURLException { - new WinRMClient(new URL("https://localhost"), "username", "password", true); + @Test + public void testSelfSignedCertificateNotAllowed() { + assertThrows( + IllegalArgumentException.class, + () -> new WinRMClient(new URL("https://localhost"), "username", "password", true)); } /** * Using a password without using TLS, an {@link IllegalArgumentException} is expected */ - @Test(expected = IllegalArgumentException.class) - public void testCreateClientWithPasswordWithoutTLS() throws MalformedURLException { - new WinRMClient(new URL("http://localhost"), "username", "password", false); + @Test + public void testCreateClientWithPasswordWithoutTLS() { + assertThrows( + IllegalArgumentException.class, + () -> new WinRMClient(new URL("http://localhost"), "username", "password", false)); } /** @@ -70,22 +75,26 @@ public void testSetUseHTTPSFalseWithoutPassword() throws MalformedURLException { /** * It should not be allowed to disable useHTTPS only when a password is used, an {@link IllegalArgumentException} is expected */ - @Test(expected = IllegalArgumentException.class) - public void testSetUseHTTPSFalseWithPassword() throws MalformedURLException { - new WinRMClient(new URL("https://localhost"), "username", "password", false).setUseHTTPS(false); + @Test + public void testSetUseHTTPSFalseWithPassword() { + assertThrows( + IllegalArgumentException.class, + () -> new WinRMClient(new URL("https://localhost"), "username", "password", false).setUseHTTPS(false)); } /** * Password leak prevention when setUseHTTPS is not called */ - @Test(expected = IllegalArgumentException.class) - public void testBuildWinRMClientWithoutTLS() throws MalformedURLException { - try { - WinRMClient winRM = new WinRMClient(new URL("https://localhost"), "username", "password", false); - // do not call winRM.setUseHTTPS(false); to avoid trigger the check - winRM.openShell(); - } catch (WinRMConnectException e) { - fail("The client should not attempt to connect"); - } + @Test + public void testBuildWinRMClientWithoutTLS() { + assertThrows(IllegalArgumentException.class, () -> { + try { + WinRMClient winRM = new WinRMClient(new URL("https://localhost"), "username", "password", false); + // do not call winRM.setUseHTTPS(false); to avoid trigger the check + winRM.openShell(); + } catch (WinRMConnectException e) { + fail("The client should not attempt to connect"); + } + }); } } diff --git a/src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithFIPSTest.java b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithFIPSTest.java index 6bec12bf8..95a3abe86 100644 --- a/src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithFIPSTest.java +++ b/src/test/java/hudson/plugins/ec2/win/winrm/WinRMWithFIPSTest.java @@ -1,5 +1,7 @@ package hudson.plugins.ec2.win.winrm; +import static org.junit.Assert.assertThrows; + import jenkins.security.FIPS140; import org.junit.ClassRule; import org.junit.Test; @@ -14,9 +16,9 @@ public class WinRMWithFIPSTest { /** * Self-signed certificate should not be allowed in FIPS mode, an {@link IllegalArgumentException} is expected */ - @Test(expected = IllegalArgumentException.class) + @Test public void testSelfSignedCertificateNotAllowed() { - new WinRM("host", "username", "password", true); + assertThrows(IllegalArgumentException.class, () -> new WinRM("host", "username", "password", true)); } /** @@ -32,10 +34,12 @@ public void testBuildCompliantURL() { /** * Self-signed certificate should not be allowed in FIPS mode, an {@link IllegalArgumentException} is expected */ - @Test(expected = IllegalArgumentException.class) + @Test public void testSetUseHTTPSWithPasswordLeak() { - WinRM winRM = new WinRM("host", "username", "password", false); - winRM.setUseHTTPS(false); + assertThrows(IllegalArgumentException.class, () -> { + WinRM winRM = new WinRM("host", "username", "password", false); + winRM.setUseHTTPS(false); + }); } /** From b443d25572f03491d7fb60d0efa915cc2e9af3eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 11:59:17 +0800 Subject: [PATCH 120/267] build(deps): bump org.jenkins-ci.plugins.aws-java-sdk:aws-java-sdk-minimal (#1038) Bumps [org.jenkins-ci.plugins.aws-java-sdk:aws-java-sdk-minimal](https://github.com/jenkinsci/aws-java-sdk-plugin) from 1.12.767-467.vb_e93f0c614b_6 to 1.12.772-477.v650d756dcf6d. - [Release notes](https://github.com/jenkinsci/aws-java-sdk-plugin/releases) - [Changelog](https://github.com/jenkinsci/aws-java-sdk-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/jenkinsci/aws-java-sdk-plugin/commits/1.12.772-477.v650d756dcf6d) --- updated-dependencies: - dependency-name: org.jenkins-ci.plugins.aws-java-sdk:aws-java-sdk-minimal dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e6e9dc927..8b4dbe43a 100644 --- a/pom.xml +++ b/pom.xml @@ -154,7 +154,7 @@ THE SOFTWARE. org.jenkins-ci.plugins.aws-java-sdk aws-java-sdk-minimal - 1.12.767-467.vb_e93f0c614b_6 + 1.12.772-477.v650d756dcf6d org.jenkins-ci.plugins.workflow From 4da50ec6ff17d69e7fd22616ebc742ce0db82c2a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:12:12 +0800 Subject: [PATCH 121/267] build(deps): bump org.jenkins-ci.plugins.aws-java-sdk:aws-java-sdk-ec2 (#1039) Bumps [org.jenkins-ci.plugins.aws-java-sdk:aws-java-sdk-ec2](https://github.com/jenkinsci/aws-java-sdk-plugin) from 1.12.696-451.v0651a_da_9ca_ec to 1.12.772-477.v650d756dcf6d. - [Release notes](https://github.com/jenkinsci/aws-java-sdk-plugin/releases) - [Changelog](https://github.com/jenkinsci/aws-java-sdk-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/jenkinsci/aws-java-sdk-plugin/commits/1.12.772-477.v650d756dcf6d) --- updated-dependencies: - dependency-name: org.jenkins-ci.plugins.aws-java-sdk:aws-java-sdk-ec2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8b4dbe43a..a96b3f49b 100644 --- a/pom.xml +++ b/pom.xml @@ -149,7 +149,7 @@ THE SOFTWARE. org.jenkins-ci.plugins.aws-java-sdk aws-java-sdk-ec2 - 1.12.696-451.v0651a_da_9ca_ec + 1.12.772-477.v650d756dcf6d org.jenkins-ci.plugins.aws-java-sdk From eb62bd2d9548c5a64d425b6f25528c97a26f9a4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:24:21 +0800 Subject: [PATCH 122/267] build(deps): bump io.jenkins.tools.bom:bom-2.462.x (#1042) Bumps [io.jenkins.tools.bom:bom-2.462.x](https://github.com/jenkinsci/bom) from 3722.vcc62e7311580 to 4051.v78dce3ce8b_d6. - [Release notes](https://github.com/jenkinsci/bom/releases) - [Commits](https://github.com/jenkinsci/bom/commits) --- updated-dependencies: - dependency-name: io.jenkins.tools.bom:bom-2.462.x dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a96b3f49b..2140cd45a 100644 --- a/pom.xml +++ b/pom.xml @@ -87,7 +87,7 @@ THE SOFTWARE. io.jenkins.tools.bom bom-${jenkins.baseline}.x - 3722.vcc62e7311580 + 4051.v78dce3ce8b_d6 pom import From 456a3dfb9933e052666d4670d769436d6fc11636 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Mon, 10 Feb 2025 12:20:59 +0100 Subject: [PATCH 123/267] Improve logging --- src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java | 3 ++- src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index d5e969916..2aae6d7bb 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -244,13 +244,14 @@ protected void launchScript(EC2Computer computer, TaskListener listener) if (StringUtils.isNotBlank(initScript) && !executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { - logInfo(computer, listener, "Executing init script"); + logInfo(computer, listener, "Upload init script"); scp.upload( initScript.getBytes(StandardCharsets.UTF_8), tmpDir + "/init.sh", List.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_READ), scpTimestamp); + logInfo(computer, listener, "Executing init script"); String initCommand = buildUpCommand(computer, tmpDir + "/init.sh"); try (ClientChannel channel = clientSession.createExecChannel( initCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index b78278252..45ab425f2 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -245,13 +245,14 @@ protected void launchScript(EC2Computer computer, TaskListener listener) if (StringUtils.isNotBlank(initScript) && !executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { - logInfo(computer, listener, "Executing init script"); + logInfo(computer, listener, "Upload init script"); scp.upload( initScript.getBytes(StandardCharsets.UTF_8), tmpDir + "/init.sh", List.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_READ), scpTimestamp); + logInfo(computer, listener, "Executing init script"); String initCommand = buildUpCommand(computer, tmpDir + "/init.sh"); try (ClientChannel channel = clientSession.createExecChannel( initCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { From 1b055a39e8f9f8b004d28a96cae04ecb539ff1b2 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Mon, 10 Feb 2025 12:21:40 +0100 Subject: [PATCH 124/267] Simplify remote code --- .../plugins/ec2/ssh/EC2MacLauncher.java | 66 +----------------- .../plugins/ec2/ssh/EC2UnixLauncher.java | 67 +------------------ 2 files changed, 4 insertions(+), 129 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index 2aae6d7bb..7883875c3 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -23,8 +23,6 @@ */ package hudson.plugins.ec2.ssh; -import static org.apache.sshd.client.session.ClientSession.REMOTE_COMMAND_WAIT_EVENTS; - import com.amazonaws.AmazonClientException; import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.ec2.model.KeyPair; @@ -66,19 +64,15 @@ import java.nio.file.attribute.PosixFilePermission; import java.security.PublicKey; import java.time.Duration; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.Jenkins; -import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.sshd.client.SshClient; import org.apache.sshd.client.channel.ChannelExec; -import org.apache.sshd.client.channel.ClientChannel; -import org.apache.sshd.client.channel.ClientChannelEvent; import org.apache.sshd.client.future.ConnectFuture; import org.apache.sshd.client.keyverifier.ServerKeyVerifier; import org.apache.sshd.client.session.ClientSession; @@ -253,67 +247,11 @@ protected void launchScript(EC2Computer computer, TaskListener listener) logInfo(computer, listener, "Executing init script"); String initCommand = buildUpCommand(computer, tmpDir + "/init.sh"); - try (ClientChannel channel = clientSession.createExecChannel( - initCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - - channel.open().await(timeout); - - OutputStream invertedIn = channel.getInvertedIn(); - if (invertedIn != null) { - invertedIn.close(); // nothing to write here - } - - Collection waitMask = - channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); - - if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { - logWarning(computer, listener, "init script timed out"); - return; - } - - int exitStatus = waitCompletion(channel, timeout); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } - - InputStream invertedErr = channel.getInvertedErr(); - if (invertedErr != null) { - invertedErr.close(); // we are not supposed to get anything from stderr - } - IOUtils.copy(channel.getInvertedOut(), logger); - } + executeRemote(clientSession, initCommand, logger); logInfo(computer, listener, "Creating ~/.hudson-run-init"); String createHudsonRunInitCommand = buildUpCommand(computer, "touch ~/.hudson-run-init"); - try (ClientChannel channel = clientSession.createExecChannel( - createHudsonRunInitCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - OutputStream invertedIn = channel.getInvertedIn(); - if (invertedIn != null) { - invertedIn.close(); // nothing to write here - } - channel.open().await(timeout); - - Collection waitMask = - channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); - - if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { - logWarning(computer, listener, "init script timed out"); - return; - } - - int exitStatus = waitCompletion(channel, timeout); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } - - InputStream invertedErr = channel.getInvertedErr(); - if (invertedErr != null) { - invertedErr.close(); // we are not supposed to get anything from stderr - } - IOUtils.copy(channel.getInvertedOut(), logger); - } + executeRemote(clientSession, createHudsonRunInitCommand, logger); } try { diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index 45ab425f2..6d1572b1c 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -23,8 +23,6 @@ */ package hudson.plugins.ec2.ssh; -import static org.apache.sshd.client.session.ClientSession.REMOTE_COMMAND_WAIT_EVENTS; - import com.amazonaws.AmazonClientException; import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.ec2.model.KeyPair; @@ -68,19 +66,15 @@ import java.security.PublicKey; import java.time.Duration; import java.util.Base64; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.Jenkins; -import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.sshd.client.SshClient; import org.apache.sshd.client.channel.ChannelExec; -import org.apache.sshd.client.channel.ClientChannel; -import org.apache.sshd.client.channel.ClientChannelEvent; import org.apache.sshd.client.future.ConnectFuture; import org.apache.sshd.client.keyverifier.ServerKeyVerifier; import org.apache.sshd.client.session.ClientSession; @@ -254,68 +248,11 @@ protected void launchScript(EC2Computer computer, TaskListener listener) logInfo(computer, listener, "Executing init script"); String initCommand = buildUpCommand(computer, tmpDir + "/init.sh"); - try (ClientChannel channel = clientSession.createExecChannel( - initCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - - channel.open().await(timeout); - - OutputStream invertedIn = channel.getInvertedIn(); - if (invertedIn != null) { - invertedIn.close(); // nothing to write here - } - - Collection waitMask = - channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); - - if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { - logWarning(computer, listener, "init script timed out"); - return; - } - - int exitStatus = waitCompletion(channel, timeout); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } - - InputStream invertedErr = channel.getInvertedErr(); - if (invertedErr != null) { - invertedErr.close(); // we are not supposed to get anything from stderr - } - IOUtils.copy(channel.getInvertedOut(), logger); - } + executeRemote(clientSession, initCommand, logger); logInfo(computer, listener, "Creating ~/.hudson-run-init"); - String createHudsonRunInitCommand = buildUpCommand(computer, "touch ~/.hudson-run-init"); - try (ClientChannel channel = clientSession.createExecChannel( - createHudsonRunInitCommand, StandardCharsets.US_ASCII, null, Collections.emptyMap())) { - OutputStream invertedIn = channel.getInvertedIn(); - if (invertedIn != null) { - invertedIn.close(); // nothing to write here - } - channel.open().await(timeout); - - Collection waitMask = - channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); - - if (waitMask.contains(ClientChannelEvent.TIMEOUT)) { - logWarning(computer, listener, "init script timed out"); - return; - } - - int exitStatus = waitCompletion(channel, timeout); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } - - InputStream invertedErr = channel.getInvertedErr(); - if (invertedErr != null) { - invertedErr.close(); // we are not supposed to get anything from stderr - } - IOUtils.copy(channel.getInvertedOut(), logger); - } + executeRemote(clientSession, createHudsonRunInitCommand, logger); } executeRemote( From f9273ac4a7747d8dae3b09964720f74397a8c6ff Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Mon, 10 Feb 2025 12:22:15 +0100 Subject: [PATCH 125/267] Update file permissions --- .../hudson/plugins/ec2/ssh/EC2MacLauncher.java | 16 ++++++++++++++-- .../hudson/plugins/ec2/ssh/EC2UnixLauncher.java | 16 ++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index 7883875c3..fddc213ca 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -242,7 +242,16 @@ protected void launchScript(EC2Computer computer, TaskListener listener) scp.upload( initScript.getBytes(StandardCharsets.UTF_8), tmpDir + "/init.sh", - List.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_READ), + List.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.GROUP_READ, + PosixFilePermission.GROUP_WRITE, + PosixFilePermission.GROUP_EXECUTE, + PosixFilePermission.OTHERS_READ, + PosixFilePermission.OTHERS_WRITE, + PosixFilePermission.OTHERS_EXECUTE), scpTimestamp); logInfo(computer, listener, "Executing init script"); @@ -289,8 +298,11 @@ protected void launchScript(EC2Computer computer, TaskListener listener) tmpDir + "/remoting.jar", List.of( PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, PosixFilePermission.GROUP_READ, - PosixFilePermission.OTHERS_READ), + PosixFilePermission.GROUP_WRITE, + PosixFilePermission.OTHERS_READ, + PosixFilePermission.OTHERS_WRITE), scpTimestamp); } } diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index 6d1572b1c..e254e22f6 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -243,7 +243,16 @@ protected void launchScript(EC2Computer computer, TaskListener listener) scp.upload( initScript.getBytes(StandardCharsets.UTF_8), tmpDir + "/init.sh", - List.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_READ), + List.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.GROUP_READ, + PosixFilePermission.GROUP_WRITE, + PosixFilePermission.GROUP_EXECUTE, + PosixFilePermission.OTHERS_READ, + PosixFilePermission.OTHERS_WRITE, + PosixFilePermission.OTHERS_EXECUTE), scpTimestamp); logInfo(computer, listener, "Executing init script"); @@ -277,8 +286,11 @@ protected void launchScript(EC2Computer computer, TaskListener listener) tmpDir + "/remoting.jar", List.of( PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, PosixFilePermission.GROUP_READ, - PosixFilePermission.OTHERS_READ), + PosixFilePermission.GROUP_WRITE, + PosixFilePermission.OTHERS_READ, + PosixFilePermission.OTHERS_WRITE), scpTimestamp); } } From 4ede131d77aec00ae843fe3f6fbbcc114cb823b6 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Thu, 13 Feb 2025 18:44:39 +0100 Subject: [PATCH 126/267] Restrict permissions to owner only --- .../hudson/plugins/ec2/ssh/EC2MacLauncher.java | 16 ++-------------- .../hudson/plugins/ec2/ssh/EC2UnixLauncher.java | 16 ++-------------- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index fddc213ca..31580a827 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -245,13 +245,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) List.of( PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, - PosixFilePermission.OWNER_EXECUTE, - PosixFilePermission.GROUP_READ, - PosixFilePermission.GROUP_WRITE, - PosixFilePermission.GROUP_EXECUTE, - PosixFilePermission.OTHERS_READ, - PosixFilePermission.OTHERS_WRITE, - PosixFilePermission.OTHERS_EXECUTE), + PosixFilePermission.OWNER_EXECUTE), scpTimestamp); logInfo(computer, listener, "Executing init script"); @@ -296,13 +290,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) scp.upload( Jenkins.get().getJnlpJars("remoting.jar").readFully(), tmpDir + "/remoting.jar", - List.of( - PosixFilePermission.OWNER_READ, - PosixFilePermission.OWNER_WRITE, - PosixFilePermission.GROUP_READ, - PosixFilePermission.GROUP_WRITE, - PosixFilePermission.OTHERS_READ, - PosixFilePermission.OTHERS_WRITE), + List.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE), scpTimestamp); } } diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index e254e22f6..eb734f8b1 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -246,13 +246,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) List.of( PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, - PosixFilePermission.OWNER_EXECUTE, - PosixFilePermission.GROUP_READ, - PosixFilePermission.GROUP_WRITE, - PosixFilePermission.GROUP_EXECUTE, - PosixFilePermission.OTHERS_READ, - PosixFilePermission.OTHERS_WRITE, - PosixFilePermission.OTHERS_EXECUTE), + PosixFilePermission.OWNER_EXECUTE), scpTimestamp); logInfo(computer, listener, "Executing init script"); @@ -284,13 +278,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) scp.upload( Jenkins.get().getJnlpJars("remoting.jar").readFully(), tmpDir + "/remoting.jar", - List.of( - PosixFilePermission.OWNER_READ, - PosixFilePermission.OWNER_WRITE, - PosixFilePermission.GROUP_READ, - PosixFilePermission.GROUP_WRITE, - PosixFilePermission.OTHERS_READ, - PosixFilePermission.OTHERS_WRITE), + List.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE), scpTimestamp); } } From 1872ce20979ee8324b9d12cbc8dc74c790c2ad1a Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Mon, 17 Feb 2025 16:21:59 +0100 Subject: [PATCH 127/267] Replace custom implementation with the one from Mina --- .../hudson/plugins/ec2/util/KeyHelper.java | 43 +++++++------------ 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/util/KeyHelper.java b/src/main/java/hudson/plugins/ec2/util/KeyHelper.java index 28bfd4314..8917e21b9 100644 --- a/src/main/java/hudson/plugins/ec2/util/KeyHelper.java +++ b/src/main/java/hudson/plugins/ec2/util/KeyHelper.java @@ -14,12 +14,10 @@ import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateCrtKey; import java.security.spec.DSAPublicKeySpec; -import java.security.spec.ECField; -import java.security.spec.ECParameterSpec; -import java.security.spec.EllipticCurve; import java.security.spec.InvalidKeySpecException; import java.security.spec.RSAPublicKeySpec; import java.security.spec.X509EncodedKeySpec; +import org.apache.sshd.common.config.keys.KeyUtils; import org.bouncycastle.asn1.ASN1BitString; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; @@ -28,6 +26,7 @@ import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; +import org.bouncycastle.util.Properties; /** * Utility class to parse PEM. @@ -138,30 +137,20 @@ else if (privateKey instanceof DSAPrivateKey) { * or {@code null} if the key type is unsupported or cannot be determined. */ public static String getSshAlgorithm(@NonNull PublicKey serverKey) { - switch (serverKey.getAlgorithm()) { - case "RSA": - return "ssh-rsa"; - case "EC": - if (serverKey instanceof ECPublicKey) { - ECPublicKey ecPublicKey = (ECPublicKey) serverKey; - ECParameterSpec params = ecPublicKey.getParams(); - if (params != null) { - - EllipticCurve curve = params.getCurve(); - ECField field = (curve != null) ? curve.getField() : null; - if (field != null) { - int fieldSize = field.getFieldSize(); - // Assume NIST curve - return "ecdsa-sha2-nistp" + fieldSize; - } - } - } - return null; - case "EdDSA": - case "Ed25519": - return "ssh-ed25519"; - default: - return null; + // Emulate Oracle so that the algorithm returned by + // org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey.getAlgorithm + // is the one expected by org.apache.sshd.common.config.keys.KeyUtils + try { + Properties.setThreadOverride(Properties.EMULATE_ORACLE, true); + String sshAlgorithm = KeyUtils.getKeyType(serverKey); + // java.security takes precedence over thread local configuration. + // Check the algorithm name used by BC when EMULATE_ORACLE is not set. + if (sshAlgorithm == null && "Ed25519".equals(serverKey.getAlgorithm())) { + sshAlgorithm = "ssh-ed25519"; + } + return sshAlgorithm; + } finally { + Properties.removeThreadOverride(Properties.EMULATE_ORACLE); } } } From 79de4759b168a7088e6542c3970b69496b75de63 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Tue, 18 Feb 2025 17:23:32 +0100 Subject: [PATCH 128/267] Moved the MockEC2Computer class to its own file --- .../hudson/plugins/ec2/MockEC2Computer.java | 143 ++++++++++++++++++ .../SshHostKeyVerificationStrategyTest.java | 136 +---------------- 2 files changed, 146 insertions(+), 133 deletions(-) create mode 100644 src/test/java/hudson/plugins/ec2/MockEC2Computer.java diff --git a/src/test/java/hudson/plugins/ec2/MockEC2Computer.java b/src/test/java/hudson/plugins/ec2/MockEC2Computer.java new file mode 100644 index 000000000..e16fdad0b --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/MockEC2Computer.java @@ -0,0 +1,143 @@ +package hudson.plugins.ec2; + +import com.amazonaws.AmazonClientException; +import com.amazonaws.services.ec2.model.InstanceType; +import hudson.model.Node; +import java.util.ArrayList; +import java.util.Collections; + +// A mock ec2 computer returning the data we want +public class MockEC2Computer extends EC2Computer { + private static final String COMPUTER_NAME = "MockInstanceForTest"; + + private InstanceState state = InstanceState.PENDING; + + private String console = null; + + private final EC2AbstractSlave slave; + + public MockEC2Computer(EC2AbstractSlave slave) { + super(slave); + this.slave = slave; + } + + // Create a computer + public static MockEC2Computer createComputer(String suffix) throws Exception { + final EC2AbstractSlave slave = + new EC2AbstractSlave( + COMPUTER_NAME + suffix, + "id" + suffix, + "description" + suffix, + "fs", + 1, + null, + "label", + null, + null, + "init", + "tmpDir", + new ArrayList<>(), + "remote", + EC2AbstractSlave.DEFAULT_JAVA_PATH, + "jvm", + false, + "idle", + null, + "cloud", + Integer.MAX_VALUE, + null, + ConnectionStrategy.PRIVATE_IP, + -1, + Tenancy.Default, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED) { + @Override + public void terminate() {} + + @Override + public String getEc2Type() { + return null; + } + }; + + return new MockEC2Computer(slave); + } + + @Override + public String getDecodedConsoleOutput() throws AmazonClientException { + return getConsole(); + } + + @Override + public InstanceState getState() { + return state; + } + + @Override + public EC2AbstractSlave getNode() { + return slave; + } + + @Override + public SlaveTemplate getSlaveTemplate() { + return new SlaveTemplate( + "ami-123", + EC2AbstractSlave.TEST_ZONE, + null, + "default", + "foo", + InstanceType.M1Large, + false, + "ttt", + Node.Mode.NORMAL, + "AMI description", + "bar", + "bbb", + "aaa", + "10", + "fff", + null, + EC2AbstractSlave.DEFAULT_JAVA_PATH, + "-Xmx1g", + false, + "subnet-123 subnet-456", + null, + null, + 0, + 0, + null, + "", + false, + false, + "", + false, + "", + false, + false, + false, + ConnectionStrategy.PRIVATE_DNS, + -1, + Collections.emptyList(), + null, + Tenancy.Default, + EbsEncryptRootVolume.DEFAULT, + EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, + EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, + EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, + EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); + } + + public void setState(InstanceState state) { + this.state = state; + } + + public String getConsole() { + return console; + } + + public void setConsole(String console) { + this.console = console; + } +} diff --git a/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java b/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java index cdcaf6688..cbf655149 100644 --- a/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java +++ b/src/test/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategyTest.java @@ -4,22 +4,14 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.emptyString; -import com.amazonaws.AmazonClientException; -import com.amazonaws.services.ec2.model.InstanceType; -import hudson.model.Node; -import hudson.plugins.ec2.ConnectionStrategy; -import hudson.plugins.ec2.EC2AbstractSlave; import hudson.plugins.ec2.EC2Computer; -import hudson.plugins.ec2.EbsEncryptRootVolume; import hudson.plugins.ec2.InstanceState; -import hudson.plugins.ec2.SlaveTemplate; -import hudson.plugins.ec2.Tenancy; +import hudson.plugins.ec2.MockEC2Computer; import hudson.plugins.ec2.util.ConnectionRule; import java.io.IOException; import java.net.SocketAddress; import java.security.PublicKey; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.logging.Level; import org.apache.sshd.client.keyverifier.ServerKeyVerifier; @@ -32,8 +24,6 @@ import org.testcontainers.containers.Container; public class SshHostKeyVerificationStrategyTest { - private static final String COMPUTER_NAME = "MockInstanceForTest"; - @ClassRule public static ConnectionRule conRule = new ConnectionRule(); @@ -244,8 +234,8 @@ private void configure() throws IOException, InterruptedException { loggerRule = new LoggerRule(); loggerRule.recordPackage(CheckNewHardStrategy.class, Level.INFO).capture(10); - computer.console = console; - computer.state = state; + computer.setConsole(console); + computer.setState(state); if (changeHostKey) { // Regenerate all the keys in the container @@ -337,126 +327,6 @@ private ConnectionAttempt build(MockEC2Computer computer, ServerKeyVerifier veri } } - // A mock ec2 computer returning the data we want - private static class MockEC2Computer extends EC2Computer { - InstanceState state = InstanceState.PENDING; - String console = null; - EC2AbstractSlave slave; - - public MockEC2Computer(EC2AbstractSlave slave) { - super(slave); - this.slave = slave; - } - - // Create a computer - private static MockEC2Computer createComputer(String suffix) throws Exception { - final EC2AbstractSlave slave = - new EC2AbstractSlave( - COMPUTER_NAME + suffix, - "id" + suffix, - "description" + suffix, - "fs", - 1, - null, - "label", - null, - null, - "init", - "tmpDir", - new ArrayList<>(), - "remote", - EC2AbstractSlave.DEFAULT_JAVA_PATH, - "jvm", - false, - "idle", - null, - "cloud", - Integer.MAX_VALUE, - null, - ConnectionStrategy.PRIVATE_IP, - -1, - Tenancy.Default, - EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, - EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, - EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, - EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED) { - @Override - public void terminate() {} - - @Override - public String getEc2Type() { - return null; - } - }; - - return new MockEC2Computer(slave); - } - - @Override - public String getDecodedConsoleOutput() throws AmazonClientException { - return console; - } - - @Override - public InstanceState getState() { - return state; - } - - @Override - public EC2AbstractSlave getNode() { - return slave; - } - - @Override - public SlaveTemplate getSlaveTemplate() { - return new SlaveTemplate( - "ami-123", - EC2AbstractSlave.TEST_ZONE, - null, - "default", - "foo", - InstanceType.M1Large, - false, - "ttt", - Node.Mode.NORMAL, - "AMI description", - "bar", - "bbb", - "aaa", - "10", - "fff", - null, - EC2AbstractSlave.DEFAULT_JAVA_PATH, - "-Xmx1g", - false, - "subnet-123 subnet-456", - null, - null, - 0, - 0, - null, - "", - false, - false, - "", - false, - "", - false, - false, - false, - ConnectionStrategy.PRIVATE_DNS, - -1, - Collections.emptyList(), - null, - Tenancy.Default, - EbsEncryptRootVolume.DEFAULT, - EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED, - EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED, - EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT, - EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED); - } - } - // A verifier using the set strategy private static class ServerHostKeyVerifierImpl implements ServerKeyVerifier { private final EC2Computer computer; From 3ed65ecc475ab2a03a3b034bc3c49c1293832d13 Mon Sep 17 00:00:00 2001 From: Jean-Marc Desprez Date: Tue, 18 Feb 2025 17:24:38 +0100 Subject: [PATCH 129/267] Add a Helper class to create a custom SshClient per EC2Computer --- .../plugins/ec2/ssh/EC2MacLauncher.java | 7 +- .../plugins/ec2/ssh/EC2UnixLauncher.java | 7 +- .../plugins/ec2/util/SSHClientHelper.java | 93 ++++++++++ .../plugins/ec2/util/SSHClientHelperTest.java | 165 ++++++++++++++++++ .../ecdsaSha2Nistp256/config.xml | 20 +++ .../ssh-host-key.xml | 5 + .../ecdsaSha2Nistp384/config.xml | 20 +++ .../ssh-host-key.xml | 5 + .../ecdsaSha2Nistp521/config.xml | 20 +++ .../ssh-host-key.xml | 5 + .../SSHClientHelperTest/ed25519/config.xml | 20 +++ .../ssh-host-key.xml | 5 + .../setupSshClientWithNoHostKey/config.xml | 20 +++ 13 files changed, 386 insertions(+), 6 deletions(-) create mode 100644 src/main/java/hudson/plugins/ec2/util/SSHClientHelper.java create mode 100644 src/test/java/hudson/plugins/ec2/util/SSHClientHelperTest.java create mode 100644 src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp256/config.xml create mode 100644 src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp256/nodes/MockInstanceForTestHostKey/ssh-host-key.xml create mode 100644 src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp384/config.xml create mode 100644 src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp384/nodes/MockInstanceForTestHostKey/ssh-host-key.xml create mode 100644 src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp521/config.xml create mode 100644 src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp521/nodes/MockInstanceForTestHostKey/ssh-host-key.xml create mode 100644 src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ed25519/config.xml create mode 100644 src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ed25519/nodes/MockInstanceForTestHostKey/ssh-host-key.xml create mode 100644 src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/setupSshClientWithNoHostKey/config.xml diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index 31580a827..2f4cd2c08 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -45,6 +45,7 @@ import hudson.plugins.ec2.ssh.verifiers.HostKey; import hudson.plugins.ec2.ssh.verifiers.Messages; import hudson.plugins.ec2.util.KeyHelper; +import hudson.plugins.ec2.util.SSHClientHelper; import hudson.remoting.Channel; import hudson.remoting.Channel.Listener; import hudson.slaves.CommandLauncher; @@ -192,7 +193,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) final String javaPath = node.javaPath; String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); - try (SshClient client = SshClient.setUpDefaultClient()) { + try (SshClient client = SSHClientHelper.getInstance().setupSshClient(computer)) { boolean isBootstrapped = bootstrap(computer, listener, template); if (!isBootstrapped) { logWarning(computer, listener, "bootstrapresult failed"); @@ -367,7 +368,7 @@ private void launchRemotingAgent( throws InterruptedException, IOException { logInfo(computer, listener, "Launching remoting agent (via SSH2 Connection): " + launchString); - final SshClient remotingClient = SshClient.setUpDefaultClient(); + final SshClient remotingClient = SSHClientHelper.getInstance().setupSshClient(computer); final ClientSession remotingSession = connectToSsh(remotingClient, computer, listener, template); KeyPair key = computer.getCloud().getKeyPair(); if (key != null) { @@ -457,7 +458,7 @@ private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemp final EC2AbstractSlave node = computer.getNode(); final long timeout = node == null ? 0L : node.getLaunchTimeoutInMillis(); ClientSession bootstrapSession = null; - try (SshClient client = SshClient.setUpDefaultClient()) { + try (SshClient client = SSHClientHelper.getInstance().setupSshClient(computer)) { int tries = bootstrapAuthTries; boolean isAuthenticated = false; logInfo(computer, listener, "Getting keypair..."); diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index eb734f8b1..4bbddfeb4 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -46,6 +46,7 @@ import hudson.plugins.ec2.ssh.verifiers.HostKeyHelper; import hudson.plugins.ec2.ssh.verifiers.Messages; import hudson.plugins.ec2.util.KeyHelper; +import hudson.plugins.ec2.util.SSHClientHelper; import hudson.remoting.Channel; import hudson.remoting.Channel.Listener; import hudson.slaves.CommandLauncher; @@ -193,7 +194,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener) final String javaPath = node.javaPath; String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); - try (SshClient client = SshClient.setUpDefaultClient()) { + try (SshClient client = SSHClientHelper.getInstance().setupSshClient(computer)) { boolean isBootstrapped = bootstrap(computer, listener, template); if (!isBootstrapped) { logWarning(computer, listener, "bootstrapresult failed"); @@ -354,7 +355,7 @@ private void launchRemotingAgent( throws InterruptedException, IOException { logInfo(computer, listener, "Launching remoting agent (via SSH2 Connection): " + launchString); - final SshClient remotingClient = SshClient.setUpDefaultClient(); + final SshClient remotingClient = SSHClientHelper.getInstance().setupSshClient(computer); final ClientSession remotingSession = connectToSsh(remotingClient, computer, listener, template); KeyPair key = computer.getCloud().getKeyPair(); if (key != null) { @@ -481,7 +482,7 @@ private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemp final EC2AbstractSlave node = computer.getNode(); final long timeout = node == null ? 0L : node.getLaunchTimeoutInMillis(); ClientSession bootstrapSession = null; - try (SshClient client = SshClient.setUpDefaultClient()) { + try (SshClient client = SSHClientHelper.getInstance().setupSshClient(computer)) { int tries = bootstrapAuthTries; boolean isAuthenticated = false; logInfo(computer, listener, "Getting keypair..."); diff --git a/src/main/java/hudson/plugins/ec2/util/SSHClientHelper.java b/src/main/java/hudson/plugins/ec2/util/SSHClientHelper.java new file mode 100644 index 000000000..0682439ab --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/util/SSHClientHelper.java @@ -0,0 +1,93 @@ +package hudson.plugins.ec2.util; + +import hudson.plugins.ec2.EC2Computer; +import hudson.plugins.ec2.ssh.verifiers.HostKey; +import hudson.plugins.ec2.ssh.verifiers.HostKeyHelper; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.signature.BuiltinSignatures; +import org.apache.sshd.common.signature.Signature; + +public final class SSHClientHelper { + + private static final SSHClientHelper INSTANCE = new SSHClientHelper(); + + private SSHClientHelper() {} + + public static SSHClientHelper getInstance() { + return INSTANCE; + } + + /** + * Set up an SSH client configured for the given {@link EC2Computer}. + * + * @param computer the {@link EC2Computer} the created client will connect to + * @return an SSH client configured for this {@link EC2Computer} + */ + public SshClient setupSshClient(EC2Computer computer) { + SshClient client = SshClient.setUpDefaultClient(); + + List preferred = getPreferredSignatures(computer); + if (!preferred.isEmpty()) { + LinkedHashSet> signatureFactoriesSet = new LinkedHashSet<>(preferred); + signatureFactoriesSet.addAll(client.getSignatureFactories()); + client.setSignatureFactories(new ArrayList<>(signatureFactoriesSet)); + } + + return client; + } + + /** + * Return an ordered list of signature algorithms that should be used. Noticeably, if a {@link HostKey} already exists for this + * {@link EC2Computer}, the {@link HostKey} algorithm will be attempted first. + * + * @param computer return a list of signature for this computer. + * @return an ordered list of signature algorithms that should be used. + */ + public List getPreferredSignatures(EC2Computer computer) { + String trustedAlgorithm; + try { + HostKey trustedHostKey = HostKeyHelper.getInstance().getHostKey(computer); + if (trustedHostKey == null) { + return List.of(); + } + trustedAlgorithm = trustedHostKey.getAlgorithm(); + } catch (IOException e) { + return List.of(); + } + + List preferred; + switch (trustedAlgorithm) { + case "ssh-rsa": + preferred = List.of( + BuiltinSignatures.rsa, + BuiltinSignatures.rsaSHA256, + BuiltinSignatures.rsaSHA256_cert, + BuiltinSignatures.rsaSHA512, + BuiltinSignatures.rsaSHA512_cert); + break; + case "ecdsa-sha2-nistp256": + preferred = List.of(BuiltinSignatures.nistp256, BuiltinSignatures.nistp256_cert); + break; + case "ecdsa-sha2-nistp384": + preferred = List.of(BuiltinSignatures.nistp384, BuiltinSignatures.nistp384_cert); + break; + case "ecdsa-sha2-nistp521": + preferred = List.of(BuiltinSignatures.nistp521, BuiltinSignatures.nistp521_cert); + break; + case "ssh-ed25519": + preferred = List.of( + BuiltinSignatures.ed25519, BuiltinSignatures.ed25519_cert, BuiltinSignatures.sk_ssh_ed25519); + break; + default: + return List.of(); + } + + // Keep only supported algorithms + return NamedFactory.setUpBuiltinFactories(true, preferred); + } +} diff --git a/src/test/java/hudson/plugins/ec2/util/SSHClientHelperTest.java b/src/test/java/hudson/plugins/ec2/util/SSHClientHelperTest.java new file mode 100644 index 000000000..06bb2f9aa --- /dev/null +++ b/src/test/java/hudson/plugins/ec2/util/SSHClientHelperTest.java @@ -0,0 +1,165 @@ +package hudson.plugins.ec2.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import hudson.plugins.ec2.MockEC2Computer; +import hudson.plugins.ec2.ssh.verifiers.HostKeyHelper; +import java.security.Provider; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.signature.BuiltinSignatures; +import org.apache.sshd.common.signature.Signature; +import org.apache.sshd.common.util.security.AbstractSecurityProviderRegistrar; +import org.apache.sshd.common.util.security.SecurityUtils; +import org.junit.Assume; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.recipes.LocalData; +import org.mockito.Mockito; + +public class SSHClientHelperTest { + @Rule + public JenkinsRule r = new JenkinsRule(); + + @Test + @LocalData + public void setupSshClientWithNoHostKey() throws Exception { + MockEC2Computer computer = MockEC2Computer.createComputer("noHostKey"); + + try (SshClient client = SSHClientHelper.getInstance().setupSshClient(computer); + SshClient defaultClient = SshClient.setUpDefaultClient()) { + List> signatureFactories = client.getSignatureFactories(); + assertEquals( + "No existing host key found, created client should not have customized signature factories", + defaultClient.getSignatureFactories(), + signatureFactories); + } + } + + @Test + @LocalData("ecdsaSha2Nistp256") + public void setupSshClientWithHostKeyECDSASha2Nistp256Supported() throws Exception { + // don't run this if EC is not supported + Assume.assumeTrue(SecurityUtils.isECCSupported()); + + MockEC2Computer computer = MockEC2Computer.createComputer("HostKey"); + assertNotNull("Expected an HostKey file", HostKeyHelper.getInstance().getHostKey(computer)); + + List expected = List.of(BuiltinSignatures.nistp256, BuiltinSignatures.nistp256_cert); + String message = + "Existing host key found and EC provider exists, created client should have customized signature factories"; + + doTestPreferredAlgorithms(message, expected); + } + + @Test + @LocalData("ecdsaSha2Nistp384") + public void setupSshClientWithHostKeyECDSASha2Nistp384Supported() throws Exception { + // don't run this if EC is not supported + Assume.assumeTrue(SecurityUtils.isECCSupported()); + + MockEC2Computer computer = MockEC2Computer.createComputer("HostKey"); + assertNotNull("Expected an HostKey file", HostKeyHelper.getInstance().getHostKey(computer)); + + List expected = List.of(BuiltinSignatures.nistp384, BuiltinSignatures.nistp384_cert); + String message = + "Existing host key found and EC provider exists, created client should have customized signature factories"; + + doTestPreferredAlgorithms(message, expected); + } + + @Test + @LocalData("ecdsaSha2Nistp521") + public void setupSshClientWithHostKeyECDSASha2Nistp521Supported() throws Exception { + // don't run this if EC is not supported + Assume.assumeTrue(SecurityUtils.isECCSupported()); + + MockEC2Computer computer = MockEC2Computer.createComputer("HostKey"); + assertNotNull("Expected an HostKey file", HostKeyHelper.getInstance().getHostKey(computer)); + + List expected = List.of(BuiltinSignatures.nistp521, BuiltinSignatures.nistp521_cert); + String message = + "Existing host key found and EC provider exists, created client should have customized signature factories"; + + doTestPreferredAlgorithms(message, expected); + } + + @Test + @LocalData("ed25519") + public void setupSshClientWithHostKeyEDDSANotSupported() throws Exception { + SecurityUtils.registerSecurityProvider(new MockEDDSASecurityProviderRegistrar(false)); + // don't run this if EDDSA is supported + Assume.assumeFalse(SecurityUtils.isEDDSACurveSupported()); + + MockEC2Computer computer = MockEC2Computer.createComputer("HostKey"); + assertNotNull("Expected an HostKey file", HostKeyHelper.getInstance().getHostKey(computer)); + + try (SshClient client = SSHClientHelper.getInstance().setupSshClient(computer); + SshClient defaultClient = SshClient.setUpDefaultClient()) { + List> signatureFactories = client.getSignatureFactories(); + assertEquals( + "Existing host key found but no EDDSA provider, created client should not have customized signature factories", + defaultClient.getSignatureFactories(), + signatureFactories); + } + } + + @Test + @LocalData("ed25519") + public void setupSshClientWithHostKeyEDDSASupported() throws Exception { + SecurityUtils.registerSecurityProvider(new MockEDDSASecurityProviderRegistrar(true)); + // don't run this if EDDSA is not supported + Assume.assumeTrue(SecurityUtils.isEDDSACurveSupported()); + + List expected = + List.of(BuiltinSignatures.ed25519, BuiltinSignatures.ed25519_cert, BuiltinSignatures.sk_ssh_ed25519); + String message = + "Existing host key found and EDDSA provider exists, created client should have customized signature factories"; + + doTestPreferredAlgorithms(message, expected); + } + + private static void doTestPreferredAlgorithms(String message, List expected) throws Exception { + MockEC2Computer computer = MockEC2Computer.createComputer("HostKey"); + assertNotNull("Expected an HostKey file", HostKeyHelper.getInstance().getHostKey(computer)); + + try (SshClient client = SSHClientHelper.getInstance().setupSshClient(computer)) { + List> signatureFactories = client.getSignatureFactories(); + List> actual = + signatureFactories.stream().limit(expected.size()).collect(Collectors.toList()); + assertEquals(message, expected, actual); + } + } + + /** + * This class registers a mocked security provider for EDDSA. + */ + private static class MockEDDSASecurityProviderRegistrar extends AbstractSecurityProviderRegistrar { + + private final boolean isSupported; + + /** + * @param isSupported if true, EDDSA is supported + */ + public MockEDDSASecurityProviderRegistrar(boolean isSupported) { + super(SecurityUtils.EDDSA); + this.isSupported = isSupported; + } + + @Override + public boolean isSupported() { + return isSupported; + } + + @Override + public Provider getSecurityProvider() { + Provider mock = Mockito.mock(Provider.class); + Mockito.when(mock.getName()).thenReturn(SecurityUtils.EDDSA); + return mock; + } + } +} diff --git a/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp256/config.xml b/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp256/config.xml new file mode 100644 index 000000000..c7e6a3199 --- /dev/null +++ b/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp256/config.xml @@ -0,0 +1,20 @@ + + + 4 + + + ec2-myEc2Cloud + false + + + + + myPrivateKey + + 2147483647 + + + false + + + \ No newline at end of file diff --git a/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp256/nodes/MockInstanceForTestHostKey/ssh-host-key.xml b/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp256/nodes/MockInstanceForTestHostKey/ssh-host-key.xml new file mode 100644 index 000000000..973152e1b --- /dev/null +++ b/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp256/nodes/MockInstanceForTestHostKey/ssh-host-key.xml @@ -0,0 +1,5 @@ + + + ecdsa-sha2-nistp256 + AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB1ZWbuchcMR2NMc4wu4sB2sFvnxZ45tAyijmSx9pUkQ51InNU7t1qzf2p29VhwdJ7kQSX3HdUcwBP1NfUSEoFw= + \ No newline at end of file diff --git a/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp384/config.xml b/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp384/config.xml new file mode 100644 index 000000000..c7e6a3199 --- /dev/null +++ b/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp384/config.xml @@ -0,0 +1,20 @@ + + + 4 + + + ec2-myEc2Cloud + false + + + + + myPrivateKey + + 2147483647 + + + false + + + \ No newline at end of file diff --git a/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp384/nodes/MockInstanceForTestHostKey/ssh-host-key.xml b/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp384/nodes/MockInstanceForTestHostKey/ssh-host-key.xml new file mode 100644 index 000000000..214f526b9 --- /dev/null +++ b/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp384/nodes/MockInstanceForTestHostKey/ssh-host-key.xml @@ -0,0 +1,5 @@ + + + ecdsa-sha2-nistp384 + AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEfdfV0luXJ0NJGMpeAMDZvfDjfwpC9U+YDSlgM0Gh2eCCtYCGv41G4tZd5+L1gjPEiS4Y8r+jb3JoAX6JdQfHecK6+NHpZsF0uwrn8zTfA9PT+I9nTtEyBgNWM/v/A5wQ== + \ No newline at end of file diff --git a/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp521/config.xml b/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp521/config.xml new file mode 100644 index 000000000..c7e6a3199 --- /dev/null +++ b/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp521/config.xml @@ -0,0 +1,20 @@ + + + 4 + + + ec2-myEc2Cloud + false + + + + + myPrivateKey + + 2147483647 + + + false + + + \ No newline at end of file diff --git a/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp521/nodes/MockInstanceForTestHostKey/ssh-host-key.xml b/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp521/nodes/MockInstanceForTestHostKey/ssh-host-key.xml new file mode 100644 index 000000000..b36da4b1f --- /dev/null +++ b/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ecdsaSha2Nistp521/nodes/MockInstanceForTestHostKey/ssh-host-key.xml @@ -0,0 +1,5 @@ + + + ecdsa-sha2-nistp521 + AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFrY2jC8sarrQqI13e9fDhzeUvTFt5j2krHfFfqDrP/M7L5RJzbg4jOSOly7FdOi7JhFkYaEguddhRh2DIUWKHR9ADR9/m4n9WxHR9QaVLUYUyZdQzgdtlY6KfLYJyO5PBSulMhpfDKGoycNKmr6Av1gyESAIBq+bINsgpUby+h9jkC7Q== + \ No newline at end of file diff --git a/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ed25519/config.xml b/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ed25519/config.xml new file mode 100644 index 000000000..c7e6a3199 --- /dev/null +++ b/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ed25519/config.xml @@ -0,0 +1,20 @@ + + + 4 + + + ec2-myEc2Cloud + false + + + + + myPrivateKey + + 2147483647 + + + false + + + \ No newline at end of file diff --git a/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ed25519/nodes/MockInstanceForTestHostKey/ssh-host-key.xml b/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ed25519/nodes/MockInstanceForTestHostKey/ssh-host-key.xml new file mode 100644 index 000000000..1d35cd7ec --- /dev/null +++ b/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/ed25519/nodes/MockInstanceForTestHostKey/ssh-host-key.xml @@ -0,0 +1,5 @@ + + + ssh-ed25519 + AAAAC3NzaC1lZDI1NTE5AAAAIHp+UvRTxCZxJKZcExTuY4uOFEPj3pfX1FJGzRGIh+AC + \ No newline at end of file diff --git a/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/setupSshClientWithNoHostKey/config.xml b/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/setupSshClientWithNoHostKey/config.xml new file mode 100644 index 000000000..c7e6a3199 --- /dev/null +++ b/src/test/resources/hudson/plugins/ec2/util/SSHClientHelperTest/setupSshClientWithNoHostKey/config.xml @@ -0,0 +1,20 @@ + + + 4 + + + ec2-myEc2Cloud + false + + + + + myPrivateKey + + 2147483647 + + + false + + + \ No newline at end of file From 6bde925289d96b93638fea5a50b62d026fffb794 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Thu, 27 Feb 2025 17:59:44 -0800 Subject: [PATCH 130/267] Require Jenkins 2.479.1 and Jakarta EE (#1050) Co-authored-by: strangelookingnerd --- pom.xml | 6 +++--- src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java | 4 ++-- src/main/java/hudson/plugins/ec2/EC2Cloud.java | 10 +++++----- src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java | 4 ++-- src/main/java/hudson/plugins/ec2/SlaveTemplate.java | 4 ++-- .../java/hudson/plugins/ec2/SpotConfiguration.java | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pom.xml b/pom.xml index 2140cd45a..874322218 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ THE SOFTWARE. org.jenkins-ci.plugins plugin - 4.88 + 5.7 @@ -75,8 +75,8 @@ THE SOFTWARE. 999999-SNAPSHOT - 2.462 - ${jenkins.baseline}.3 + 2.479 + ${jenkins.baseline}.1 jenkinsci/${project.artifactId}-plugin 1626 false diff --git a/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java b/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java index 45dc4d9f9..d900ff435 100644 --- a/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java +++ b/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java @@ -63,7 +63,7 @@ import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerRequest2; import org.kohsuke.stapler.verb.POST; /** @@ -722,7 +722,7 @@ boolean terminateInstance() { } @Override - public Node reconfigure(final StaplerRequest req, JSONObject form) throws FormException { + public Node reconfigure(final StaplerRequest2 req, JSONObject form) throws FormException { if (form == null) { return null; } diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 3319165eb..da24f9b55 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -86,6 +86,8 @@ import hudson.util.ListBoxModel; import hudson.util.Secret; import hudson.util.StreamTaskListener; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.io.PrintStream; @@ -114,8 +116,6 @@ import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletResponse; import jenkins.model.Jenkins; import jenkins.model.JenkinsLocationConfiguration; import org.apache.commons.lang.StringUtils; @@ -127,8 +127,8 @@ import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.StaplerRequest2; +import org.kohsuke.stapler.StaplerResponse2; import org.kohsuke.stapler.interceptor.RequirePOST; import org.kohsuke.stapler.verb.POST; @@ -612,7 +612,7 @@ public synchronized KeyPair getKeyPair() throws AmazonClientException, IOExcepti * Debug command to attach to a running instance. */ @RequirePOST - public void doAttach(StaplerRequest req, StaplerResponse rsp, @QueryParameter String id) + public void doAttach(StaplerRequest2 req, StaplerResponse2 rsp, @QueryParameter String id) throws ServletException, IOException, AmazonClientException { checkPermission(PROVISION); SlaveTemplate t = getTemplates().get(0); diff --git a/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java b/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java index c56c427d5..49e78722b 100644 --- a/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java +++ b/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java @@ -19,7 +19,7 @@ import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerRequest2; /** * Agent running on EC2. @@ -470,7 +470,7 @@ public void terminate() { } @Override - public Node reconfigure(final StaplerRequest req, JSONObject form) throws FormException { + public Node reconfigure(final StaplerRequest2 req, JSONObject form) throws FormException { if (form == null) { return null; } diff --git a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java index 935c6c71f..10ac786a4 100644 --- a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java +++ b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java @@ -94,6 +94,7 @@ import hudson.util.FormValidation; import hudson.util.ListBoxModel; import hudson.util.Secret; +import jakarta.servlet.ServletException; import java.io.IOException; import java.io.PrintStream; import java.net.URL; @@ -116,7 +117,6 @@ import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.servlet.ServletException; import jenkins.model.Jenkins; import jenkins.model.JenkinsLocationConfiguration; import jenkins.slaves.iterators.api.NodeIterator; @@ -2865,7 +2865,7 @@ public FormValidation doValidateAmi( } private void checkPermission(Permission p) { - final EC2Cloud ancestorObject = Stapler.getCurrentRequest().findAncestorObject(EC2Cloud.class); + final EC2Cloud ancestorObject = Stapler.getCurrentRequest2().findAncestorObject(EC2Cloud.class); if (ancestorObject != null) { ancestorObject.checkPermission(p); } else { diff --git a/src/main/java/hudson/plugins/ec2/SpotConfiguration.java b/src/main/java/hudson/plugins/ec2/SpotConfiguration.java index 50328ce66..b203fd330 100644 --- a/src/main/java/hudson/plugins/ec2/SpotConfiguration.java +++ b/src/main/java/hudson/plugins/ec2/SpotConfiguration.java @@ -14,12 +14,12 @@ import hudson.model.Descriptor; import hudson.plugins.ec2.util.AmazonEC2Factory; import hudson.util.FormValidation; +import jakarta.servlet.ServletException; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Objects; -import javax.servlet.ServletException; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; From 3e5f46e3dcaf111fe7373c69c63524dda5d0d4cc Mon Sep 17 00:00:00 2001 From: strangelookingnerd <49242855+strangelookingnerd@users.noreply.github.com> Date: Fri, 7 Mar 2025 17:24:17 +0100 Subject: [PATCH 131/267] Remove deprecations and use Java 17 language features (#1044) Co-authored-by: strangelookingnerd --- .../hudson/plugins/ec2/EC2AbstractSlave.java | 2 + .../java/hudson/plugins/ec2/EC2Cloud.java | 21 ++--- .../java/hudson/plugins/ec2/EC2Computer.java | 2 +- .../plugins/ec2/EC2ComputerLauncher.java | 14 +--- .../hudson/plugins/ec2/EC2PrivateKey.java | 4 +- .../plugins/ec2/EC2RetentionStrategy.java | 2 +- .../hudson/plugins/ec2/EC2SlaveMonitor.java | 3 +- src/main/java/hudson/plugins/ec2/EC2Step.java | 5 +- .../ec2/NoDelayProvisionerStrategy.java | 7 +- .../hudson/plugins/ec2/SpotConfiguration.java | 4 +- .../plugins/ec2/ssh/EC2MacLauncher.java | 8 +- .../plugins/ec2/ssh/EC2UnixLauncher.java | 8 +- .../plugins/ec2/ssh/HostKeyVerifierImpl.java | 2 +- .../plugins/ec2/ssh/verifiers/HostKey.java | 2 + .../ec2/util/AmazonEC2FactoryImpl.java | 8 +- .../hudson/plugins/ec2/util/FIPS140Utils.java | 9 +- .../hudson/plugins/ec2/util/KeyHelper.java | 3 +- .../ec2/util/MinimumInstanceChecker.java | 82 +++++++++---------- .../plugins/ec2/win/EC2WindowsLauncher.java | 2 +- .../plugins/ec2/win/winrm/WinRMClient.java | 3 +- .../plugins/ec2/win/winrm/soap/Header.java | 4 +- .../ec2/win/winrm/soap/HeaderBuilder.java | 4 +- .../hudson/plugins/ec2/EC2CloudUnitTest.java | 26 +++--- .../hudson/plugins/ec2/EC2FilterTest.java | 4 +- .../plugins/ec2/EC2RetentionStrategyTest.java | 16 ++-- .../plugins/ec2/FileBasedSSHKeyTest.java | 10 ++- .../hudson/plugins/ec2/SlaveTemplateTest.java | 2 +- .../plugins/ec2/SlaveTemplateUnitTest.java | 22 ++--- .../plugins/ec2/util/ConnectionRule.java | 2 +- .../ec2/util/EC2AgentFactoryMockImpl.java | 3 + .../plugins/ec2/win/WinConnectionTest.java | 3 +- 31 files changed, 135 insertions(+), 152 deletions(-) diff --git a/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java b/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java index d900ff435..055326883 100644 --- a/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java +++ b/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java @@ -51,6 +51,7 @@ import hudson.util.ListBoxModel; import hudson.util.Secret; import java.io.IOException; +import java.io.Serial; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -436,6 +437,7 @@ public EC2AbstractSlave( -1); } + @Serial @Override protected Object readResolve() { var o = (EC2AbstractSlave) super.readResolve(); diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index da24f9b55..d39a317b1 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -451,8 +451,7 @@ protected Object readResolve() { // ITERATE ON EXISTING CREDS AND DON'T CREATE IF EXIST for (Credentials credentials : systemCredentialsProvider.getCredentials()) { - if (credentials instanceof AmazonWebServicesCredentials) { - AmazonWebServicesCredentials awsCreds = (AmazonWebServicesCredentials) credentials; + if (credentials instanceof AmazonWebServicesCredentials awsCreds) { AWSCredentials awsCredentials = awsCreds.getCredentials(); if (accessId.equals(awsCredentials.getAWSAccessKeyId()) && Secret.toString(this.secretKey).equals(awsCredentials.getAWSSecretKey())) { @@ -769,10 +768,9 @@ private int countCurrentEC2SpotSlaves(SlaveTemplate template, String jenkinsServ // Cancelled or otherwise dead for (Node node : Jenkins.get().getNodes()) { try { - if (!(node instanceof EC2SpotSlave)) { + if (!(node instanceof EC2SpotSlave ec2Slave)) { continue; } - EC2SpotSlave ec2Slave = (EC2SpotSlave) node; if (ec2Slave.getSpotInstanceRequestId().equals(sir.getSpotInstanceRequestId())) { LOGGER.log( Level.INFO, @@ -806,10 +804,9 @@ private int countJenkinsNodeSpotInstancesWithoutRequests( throws AmazonClientException { int n = 0; for (Node node : Jenkins.get().getNodes()) { - if (!(node instanceof EC2SpotSlave)) { + if (!(node instanceof EC2SpotSlave ec2Slave)) { continue; } - EC2SpotSlave ec2Slave = (EC2SpotSlave) node; SpotInstanceRequest sir = ec2Slave.getSpotRequest(); if (sir == null) { @@ -1265,13 +1262,12 @@ public static ClientConfiguration createClientConfiguration(final String host) { config.setSignerOverride("AWS4SignerType"); ProxyConfiguration proxyConfig = Jenkins.get().proxy; Proxy proxy = proxyConfig == null ? Proxy.NO_PROXY : proxyConfig.createProxy(host); - if (!proxy.equals(Proxy.NO_PROXY) && proxy.address() instanceof InetSocketAddress) { - InetSocketAddress address = (InetSocketAddress) proxy.address(); + if (!proxy.equals(Proxy.NO_PROXY) && proxy.address() instanceof InetSocketAddress address) { config.setProxyHost(address.getHostName()); config.setProxyPort(address.getPort()); if (null != proxyConfig.getUserName()) { config.setProxyUsername(proxyConfig.getUserName()); - config.setProxyPassword(proxyConfig.getPassword()); + config.setProxyPassword(proxyConfig.getSecretPassword().getPlainText()); } } return config; @@ -1341,7 +1337,7 @@ public static URL checkEndPoint(String url) throws FormValidation { private static SSHUserPrivateKey getSshCredential(String id, ItemGroup context) { SSHUserPrivateKey credential = CredentialsMatchers.firstOrNull( - CredentialsProvider.lookupCredentials( + CredentialsProvider.lookupCredentialsInItemGroup( SSHUserPrivateKey.class, // (1) context, null, @@ -1500,7 +1496,7 @@ public FormValidation doCheckSshKeysCredentialsId( /** * Tests the connection settings. - * + *

* Overriding needs to {@code @RequirePOST} * @param region * @param useInstanceProfileForCredentials @@ -1710,8 +1706,7 @@ protected void doRun() throws IOException { Jenkins instance = Jenkins.get(); if (instance.clouds != null) { for (Cloud cloud : instance.clouds) { - if (cloud instanceof EC2Cloud) { - EC2Cloud ec2_cloud = (EC2Cloud) cloud; + if (cloud instanceof EC2Cloud ec2_cloud) { LOGGER.finer(() -> "Checking EC2 Connection on: " + ec2_cloud.getDisplayName()); try { if (ec2_cloud.connection != null) { diff --git a/src/main/java/hudson/plugins/ec2/EC2Computer.java b/src/main/java/hudson/plugins/ec2/EC2Computer.java index 44aa9dc19..f3b035ae0 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Computer.java +++ b/src/main/java/hudson/plugins/ec2/EC2Computer.java @@ -164,7 +164,7 @@ private boolean checkIfNitro() throws AmazonClientException, InterruptedExceptio *

* This method returns a cached state, so it's not suitable to check {@link Instance#getState()} from the returned * instance (but all the other fields are valid as it won't change.) - * + *

* The cache can be flushed using {@link #updateInstanceDescription()} */ public Instance describeInstance() throws AmazonClientException, InterruptedException { diff --git a/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java b/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java index 497615608..3ac4c586c 100644 --- a/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java +++ b/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java @@ -55,32 +55,26 @@ public void launch(SlaveComputer slaveComputer, TaskListener listener) { launchScript(computer, listener); } catch (AmazonClientException | IOException e) { e.printStackTrace(listener.error(e.getMessage())); - if (slaveComputer.getNode() instanceof EC2AbstractSlave) { + if (slaveComputer.getNode() instanceof EC2AbstractSlave ec2AbstractSlave) { LOGGER.log( Level.FINE, String.format( "Terminating the ec2 agent %s due a problem launching or connecting to it", slaveComputer.getName()), e); - EC2AbstractSlave ec2AbstractSlave = (EC2AbstractSlave) slaveComputer.getNode(); - if (ec2AbstractSlave != null) { - ec2AbstractSlave.terminate(); - } + ec2AbstractSlave.terminate(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); e.printStackTrace(listener.error(e.getMessage())); - if (slaveComputer.getNode() instanceof EC2AbstractSlave) { + if (slaveComputer.getNode() instanceof EC2AbstractSlave ec2AbstractSlave) { LOGGER.log( Level.FINE, String.format( "Terminating the ec2 agent %s due a problem launching or connecting to it", slaveComputer.getName()), e); - EC2AbstractSlave ec2AbstractSlave = (EC2AbstractSlave) slaveComputer.getNode(); - if (ec2AbstractSlave != null) { - ec2AbstractSlave.terminate(); - } + ec2AbstractSlave.terminate(); } } } diff --git a/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java b/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java index b77a8fc06..d15cdbf20 100644 --- a/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java +++ b/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java @@ -46,7 +46,7 @@ /** * RSA private key (the one that you generate with ec2-add-keypair.) - * + *

* Starts with "----- BEGIN RSA PRIVATE KEY------\n". * * @author Kohsuke Kawaguchi @@ -146,7 +146,7 @@ public String decryptWindowsPassword(String encodedPassword) throws AmazonClient byte[] plainText = cipher.doFinal(cipherText); return new String(plainText, StandardCharsets.US_ASCII); } catch (Exception e) { - throw new AmazonClientException("Unable to decode password:\n" + e.toString()); + throw new AmazonClientException("Unable to decode password:\n" + e); } } diff --git a/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java b/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java index 0ec4b4a80..642a2c641 100644 --- a/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java +++ b/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java @@ -279,7 +279,7 @@ private boolean itemsInQueueForThisSlave(EC2Computer c) { /** * Called when a new {@link EC2Computer} object is introduced (such as when Hudson started, or when * a new agent is added.) - * + *

* When Jenkins has just started, we don't want to spin up all the instances, so we only start if * the EC2 instance is already running */ diff --git a/src/main/java/hudson/plugins/ec2/EC2SlaveMonitor.java b/src/main/java/hudson/plugins/ec2/EC2SlaveMonitor.java index 192d3b64c..84a30789e 100644 --- a/src/main/java/hudson/plugins/ec2/EC2SlaveMonitor.java +++ b/src/main/java/hudson/plugins/ec2/EC2SlaveMonitor.java @@ -41,8 +41,7 @@ protected void execute(TaskListener listener) throws IOException, InterruptedExc private void removeDeadNodes() { for (Node node : Jenkins.get().getNodes()) { - if (node instanceof EC2AbstractSlave) { - final EC2AbstractSlave ec2Slave = (EC2AbstractSlave) node; + if (node instanceof EC2AbstractSlave ec2Slave) { try { if (!ec2Slave.isAlive(true)) { LOGGER.info("EC2 instance is dead: " + ec2Slave.getInstanceId()); diff --git a/src/main/java/hudson/plugins/ec2/EC2Step.java b/src/main/java/hudson/plugins/ec2/EC2Step.java index 86580abe9..4d82022bf 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Step.java +++ b/src/main/java/hudson/plugins/ec2/EC2Step.java @@ -46,7 +46,7 @@ /** * Returns the instance provisioned. - * + *

* Used like: * *

@@ -108,8 +108,7 @@ public ListBoxModel doFillTemplateItems(@QueryParameter String cloudName) {
             Jenkins.get().checkPermission(Jenkins.SYSTEM_READ);
             ListBoxModel r = new ListBoxModel();
             Cloud cloud = Jenkins.get().getCloud(Util.fixEmpty(cloudName));
-            if (cloud instanceof EC2Cloud) {
-                EC2Cloud ec2Cloud = (EC2Cloud) cloud;
+            if (cloud instanceof EC2Cloud ec2Cloud) {
                 for (SlaveTemplate template : ec2Cloud.getTemplates()) {
                     for (String labelList : template.labels.split(" ")) {
                         r.add(
diff --git a/src/main/java/hudson/plugins/ec2/NoDelayProvisionerStrategy.java b/src/main/java/hudson/plugins/ec2/NoDelayProvisionerStrategy.java
index 7fab52c01..5420b4d78 100644
--- a/src/main/java/hudson/plugins/ec2/NoDelayProvisionerStrategy.java
+++ b/src/main/java/hudson/plugins/ec2/NoDelayProvisionerStrategy.java
@@ -39,19 +39,18 @@ public NodeProvisioner.StrategyDecision apply(NodeProvisioner.StrategyState stra
         if (availableCapacity < currentDemand) {
             Jenkins jenkinsInstance = Jenkins.get();
             for (Cloud cloud : jenkinsInstance.clouds) {
-                if (!(cloud instanceof EC2Cloud)) {
+                if (!(cloud instanceof EC2Cloud ec2)) {
                     continue;
                 }
-                if (!cloud.canProvision(label)) {
+                if (!cloud.canProvision(new Cloud.CloudState(label, 0))) {
                     continue;
                 }
-                EC2Cloud ec2 = (EC2Cloud) cloud;
                 if (!ec2.isNoDelayProvisioning()) {
                     continue;
                 }
 
                 Collection plannedNodes =
-                        cloud.provision(label, currentDemand - availableCapacity);
+                        cloud.provision(new Cloud.CloudState(label, 0), currentDemand - availableCapacity);
                 LOGGER.log(Level.FINE, "Planned {0} new nodes", plannedNodes.size());
                 strategyState.recordPendingLaunches(plannedNodes);
                 availableCapacity += plannedNodes.size();
diff --git a/src/main/java/hudson/plugins/ec2/SpotConfiguration.java b/src/main/java/hudson/plugins/ec2/SpotConfiguration.java
index b203fd330..a8cd2cef2 100644
--- a/src/main/java/hudson/plugins/ec2/SpotConfiguration.java
+++ b/src/main/java/hudson/plugins/ec2/SpotConfiguration.java
@@ -115,13 +115,13 @@ public int hashCode() {
      */
     public static String normalizeBid(String bid) {
         try {
-            Float spotPrice = Float.parseFloat(bid);
+            float spotPrice = Float.parseFloat(bid);
 
             /* The specified bid price cannot be less than 0.001 */
             if (spotPrice < 0.001) {
                 return null;
             }
-            return spotPrice.toString();
+            return Float.toString(spotPrice);
         } catch (NumberFormatException ex) {
             return null;
         }
diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java
index 2f4cd2c08..df195e3d5 100644
--- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java
+++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java
@@ -164,8 +164,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener)
             throw new IOException("Could not find corresponding agent template for " + computer.getDisplayName());
         }
 
-        if (node instanceof EC2Readiness) {
-            EC2Readiness readinessNode = (EC2Readiness) node;
+        if (node instanceof EC2Readiness readinessNode) {
             int tries = readinessTries;
 
             while (tries-- > 0) {
@@ -554,10 +553,9 @@ private ClientSession connectToSsh(
 
                 ProxyConfiguration proxyConfig = Jenkins.get().proxy;
                 Proxy proxy = proxyConfig == null ? Proxy.NO_PROXY : proxyConfig.createProxy(host);
-                if (!proxy.equals(Proxy.NO_PROXY) && proxy.address() instanceof InetSocketAddress) {
-                    InetSocketAddress address = (InetSocketAddress) proxy.address();
+                if (!proxy.equals(Proxy.NO_PROXY) && proxy.address() instanceof InetSocketAddress address) {
                     String username = proxyConfig.getUserName();
-                    String password = proxyConfig.getPassword();
+                    String password = proxyConfig.getSecretPassword().getPlainText();
 
                     client.setClientProxyConnector(new ProxyCONNECTListener(host, port, username, password));
 
diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java
index 4bbddfeb4..b2f93e5ad 100644
--- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java
+++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java
@@ -165,8 +165,7 @@ protected void launchScript(EC2Computer computer, TaskListener listener)
             throw new IOException("Could not find corresponding agent template for " + computer.getDisplayName());
         }
 
-        if (node instanceof EC2Readiness) {
-            EC2Readiness readinessNode = (EC2Readiness) node;
+        if (node instanceof EC2Readiness readinessNode) {
             int tries = readinessTries;
 
             while (tries-- > 0) {
@@ -577,10 +576,9 @@ private ClientSession connectToSsh(
 
                 ProxyConfiguration proxyConfig = Jenkins.get().proxy;
                 Proxy proxy = proxyConfig == null ? Proxy.NO_PROXY : proxyConfig.createProxy(host);
-                if (!proxy.equals(Proxy.NO_PROXY) && proxy.address() instanceof InetSocketAddress) {
-                    InetSocketAddress address = (InetSocketAddress) proxy.address();
+                if (!proxy.equals(Proxy.NO_PROXY) && proxy.address() instanceof InetSocketAddress address) {
                     String username = proxyConfig.getUserName();
-                    String password = proxyConfig.getPassword();
+                    String password = proxyConfig.getSecretPassword().getPlainText();
 
                     client.setClientProxyConnector(new ProxyCONNECTListener(host, port, username, password));
 
diff --git a/src/main/java/hudson/plugins/ec2/ssh/HostKeyVerifierImpl.java b/src/main/java/hudson/plugins/ec2/ssh/HostKeyVerifierImpl.java
index 818bb9a4c..664e08149 100644
--- a/src/main/java/hudson/plugins/ec2/ssh/HostKeyVerifierImpl.java
+++ b/src/main/java/hudson/plugins/ec2/ssh/HostKeyVerifierImpl.java
@@ -42,7 +42,7 @@ private String getFingerprint(byte[] serverHostKey) throws Exception {
 
         StringBuilder buf = new StringBuilder();
         for (byte b : fingerprint) {
-            if (buf.length() > 0) {
+            if (!buf.isEmpty()) {
                 buf.append(':');
             }
             buf.append(String.format("%02x", b));
diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java
index 110cdd6da..f6ce8e1a7 100644
--- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java
+++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java
@@ -28,6 +28,7 @@
 
 import edu.umd.cs.findbugs.annotations.NonNull;
 import hudson.plugins.ec2.util.FIPS140Utils;
+import java.io.Serial;
 import java.io.Serializable;
 import java.util.Arrays;
 import org.apache.sshd.common.digest.BuiltinDigests;
@@ -42,6 +43,7 @@
  */
 public final class HostKey implements Serializable {
 
+    @Serial
     private static final long serialVersionUID = -3873284593211178494L;
 
     private final String algorithm;
diff --git a/src/main/java/hudson/plugins/ec2/util/AmazonEC2FactoryImpl.java b/src/main/java/hudson/plugins/ec2/util/AmazonEC2FactoryImpl.java
index 190b1f921..54f6d6e40 100644
--- a/src/main/java/hudson/plugins/ec2/util/AmazonEC2FactoryImpl.java
+++ b/src/main/java/hudson/plugins/ec2/util/AmazonEC2FactoryImpl.java
@@ -2,7 +2,7 @@
 
 import com.amazonaws.auth.AWSCredentialsProvider;
 import com.amazonaws.services.ec2.AmazonEC2;
-import com.amazonaws.services.ec2.AmazonEC2Client;
+import com.amazonaws.services.ec2.AmazonEC2ClientBuilder;
 import hudson.Extension;
 import hudson.plugins.ec2.EC2Cloud;
 import java.net.URL;
@@ -12,8 +12,10 @@ public class AmazonEC2FactoryImpl implements AmazonEC2Factory {
 
     @Override
     public AmazonEC2 connect(AWSCredentialsProvider credentialsProvider, URL ec2Endpoint) {
-        AmazonEC2 client =
-                new AmazonEC2Client(credentialsProvider, EC2Cloud.createClientConfiguration(ec2Endpoint.getHost()));
+        AmazonEC2 client = AmazonEC2ClientBuilder.standard()
+                .withCredentials(credentialsProvider)
+                .withClientConfiguration(EC2Cloud.createClientConfiguration(ec2Endpoint.getHost()))
+                .build();
         client.setEndpoint(ec2Endpoint.toString());
         return client;
     }
diff --git a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java
index d93dd652f..11aa27d5b 100644
--- a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java
+++ b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java
@@ -155,18 +155,15 @@ public static void ensurePublicKeyInFipsMode(@NonNull String algorithm, @NonNull
 
         AsymmetricKeyParameter asymmetricKeyParameter = OpenSSHPublicKeyUtil.parsePublicKey(key);
 
-        if (asymmetricKeyParameter instanceof RSAKeyParameters) {
-            RSAKeyParameters rsaKeyParameters = (RSAKeyParameters) asymmetricKeyParameter;
+        if (asymmetricKeyParameter instanceof RSAKeyParameters rsaKeyParameters) {
             if (rsaKeyParameters.getModulus().bitLength() < 2048) {
                 throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeInFIPSMode());
             }
-        } else if (asymmetricKeyParameter instanceof DSAPublicKeyParameters) {
-            DSAPublicKeyParameters dsaPublicKeyParameters = (DSAPublicKeyParameters) asymmetricKeyParameter;
+        } else if (asymmetricKeyParameter instanceof DSAPublicKeyParameters dsaPublicKeyParameters) {
             if (dsaPublicKeyParameters.getParameters().getP().bitLength() < 2048) {
                 throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeInFIPSMode());
             }
-        } else if (asymmetricKeyParameter instanceof ECPublicKeyParameters) {
-            ECPublicKeyParameters ecPublicKeyParameters = (ECPublicKeyParameters) asymmetricKeyParameter;
+        } else if (asymmetricKeyParameter instanceof ECPublicKeyParameters ecPublicKeyParameters) {
             if (ecPublicKeyParameters.getParameters().getCurve().getFieldSize() < 224) {
                 throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeECInFIPSMode());
             }
diff --git a/src/main/java/hudson/plugins/ec2/util/KeyHelper.java b/src/main/java/hudson/plugins/ec2/util/KeyHelper.java
index 8917e21b9..a997c1991 100644
--- a/src/main/java/hudson/plugins/ec2/util/KeyHelper.java
+++ b/src/main/java/hudson/plugins/ec2/util/KeyHelper.java
@@ -61,8 +61,7 @@ public static KeyPair decodeKeyPair(@NonNull String pem, @NonNull String passwor
                 FIPS140Utils.ensureKeyInFipsMode(privateKey);
                 PublicKey publicKey = converter.getPublicKey(decryptedKeyPair.getPublicKeyInfo());
                 return new KeyPair(publicKey, privateKey);
-            } else if (object instanceof PrivateKeyInfo) {
-                PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) object;
+            } else if (object instanceof PrivateKeyInfo privateKeyInfo) {
                 PrivateKey privateKey = converter.getPrivateKey(privateKeyInfo);
                 FIPS140Utils.ensureKeyInFipsMode(privateKey);
                 PublicKey publicKey = generatePublicKeyFromPrivateKey(privateKeyInfo, privateKey);
diff --git a/src/main/java/hudson/plugins/ec2/util/MinimumInstanceChecker.java b/src/main/java/hudson/plugins/ec2/util/MinimumInstanceChecker.java
index 8443a531b..ff3ec2167 100644
--- a/src/main/java/hudson/plugins/ec2/util/MinimumInstanceChecker.java
+++ b/src/main/java/hudson/plugins/ec2/util/MinimumInstanceChecker.java
@@ -67,48 +67,46 @@ public static int countQueueItemsForAgentTemplate(@NonNull SlaveTemplate agentTe
     public static void checkForMinimumInstances() {
         Jenkins.get().clouds.stream()
                 .filter(EC2Cloud.class::isInstance)
-                .map(cloud -> (EC2Cloud) cloud)
-                .forEach(cloud -> {
-                    cloud.getTemplates().forEach(agentTemplate -> {
-                        // Minimum instances now have a time range, check to see
-                        // if we are within that time range and return early if not.
-                        if (!minimumInstancesActive(agentTemplate.getMinimumNumberOfInstancesTimeRangeConfig())) {
-                            return;
-                        }
-                        int requiredMinAgents = agentTemplate.getMinimumNumberOfInstances();
-                        int requiredMinSpareAgents = agentTemplate.getMinimumNumberOfSpareInstances();
-                        int currentNumberOfAgentsForTemplate = countCurrentNumberOfAgents(agentTemplate);
-                        int currentNumberOfSpareAgentsForTemplate = countCurrentNumberOfSpareAgents(agentTemplate);
-                        int currentNumberOfProvisioningAgentsForTemplate =
-                                countCurrentNumberOfProvisioningAgents(agentTemplate);
-                        int currentBuildsWaitingForTemplate = countQueueItemsForAgentTemplate(agentTemplate);
-                        int provisionForMinAgents = 0;
-                        int provisionForMinSpareAgents = 0;
-
-                        // Check if we need to provision any agents because we
-                        // don't have the minimum number of agents
-                        provisionForMinAgents = requiredMinAgents - currentNumberOfAgentsForTemplate;
-                        if (provisionForMinAgents < 0) {
-                            provisionForMinAgents = 0;
-                        }
-
-                        // Check if we need to provision any agents because we
-                        // don't have the minimum number of spare agents.
-                        // Don't double provision if minAgents and minSpareAgents are set.
-                        provisionForMinSpareAgents = (requiredMinSpareAgents + currentBuildsWaitingForTemplate)
-                                - (currentNumberOfSpareAgentsForTemplate
-                                        + provisionForMinAgents
-                                        + currentNumberOfProvisioningAgentsForTemplate);
-                        if (provisionForMinSpareAgents < 0) {
-                            provisionForMinSpareAgents = 0;
-                        }
-
-                        int numberToProvision = provisionForMinAgents + provisionForMinSpareAgents;
-                        if (numberToProvision > 0) {
-                            cloud.provision(agentTemplate, numberToProvision);
-                        }
-                    });
-                });
+                .map(EC2Cloud.class::cast)
+                .forEach(cloud -> cloud.getTemplates().forEach(agentTemplate -> {
+                    // Minimum instances now have a time range, check to see
+                    // if we are within that time range and return early if not.
+                    if (!minimumInstancesActive(agentTemplate.getMinimumNumberOfInstancesTimeRangeConfig())) {
+                        return;
+                    }
+                    int requiredMinAgents = agentTemplate.getMinimumNumberOfInstances();
+                    int requiredMinSpareAgents = agentTemplate.getMinimumNumberOfSpareInstances();
+                    int currentNumberOfAgentsForTemplate = countCurrentNumberOfAgents(agentTemplate);
+                    int currentNumberOfSpareAgentsForTemplate = countCurrentNumberOfSpareAgents(agentTemplate);
+                    int currentNumberOfProvisioningAgentsForTemplate =
+                            countCurrentNumberOfProvisioningAgents(agentTemplate);
+                    int currentBuildsWaitingForTemplate = countQueueItemsForAgentTemplate(agentTemplate);
+                    int provisionForMinAgents = 0;
+                    int provisionForMinSpareAgents = 0;
+
+                    // Check if we need to provision any agents because we
+                    // don't have the minimum number of agents
+                    provisionForMinAgents = requiredMinAgents - currentNumberOfAgentsForTemplate;
+                    if (provisionForMinAgents < 0) {
+                        provisionForMinAgents = 0;
+                    }
+
+                    // Check if we need to provision any agents because we
+                    // don't have the minimum number of spare agents.
+                    // Don't double provision if minAgents and minSpareAgents are set.
+                    provisionForMinSpareAgents = (requiredMinSpareAgents + currentBuildsWaitingForTemplate)
+                            - (currentNumberOfSpareAgentsForTemplate
+                                    + provisionForMinAgents
+                                    + currentNumberOfProvisioningAgentsForTemplate);
+                    if (provisionForMinSpareAgents < 0) {
+                        provisionForMinSpareAgents = 0;
+                    }
+
+                    int numberToProvision = provisionForMinAgents + provisionForMinSpareAgents;
+                    if (numberToProvision > 0) {
+                        cloud.provision(agentTemplate, numberToProvision);
+                    }
+                }));
     }
 
     public static boolean minimumInstancesActive(
diff --git a/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java b/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java
index a91a54c6c..7baf034bd 100644
--- a/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java
+++ b/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java
@@ -167,7 +167,7 @@ private WinConnection connectToWinRM(
                                     .connect()
                                     .getPasswordData(new GetPasswordDataRequest(instance.getInstanceId()));
                         } catch (Exception e) {
-                            logger.println("Unexpected Exception: " + e.toString());
+                            logger.println("Unexpected Exception: " + e);
                             Thread.sleep(sleepBetweenAttempts);
                             continue;
                         }
diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java
index f5a9aa7c7..e32f05259 100644
--- a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java
+++ b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java
@@ -144,8 +144,7 @@ public boolean slurpOutput(FastPipedOutputStream stdout, FastPipedOutputStream s
         xpath.setNamespaceContext(namespaceContext);
 
         for (Node node : xpath.selectNodes(response)) {
-            if (node instanceof Element) {
-                Element e = (Element) node;
+            if (node instanceof Element e) {
                 FastPipedOutputStream stream =
                         streams.get(e.attribute("Name").getText().toLowerCase());
                 final byte[] decode = Base64.getDecoder().decode(e.getText());
diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/soap/Header.java b/src/main/java/hudson/plugins/ec2/win/winrm/soap/Header.java
index c0ecdfa56..be21aebb2 100644
--- a/src/main/java/hudson/plugins/ec2/win/winrm/soap/Header.java
+++ b/src/main/java/hudson/plugins/ec2/win/winrm/soap/Header.java
@@ -1,6 +1,5 @@
 package hudson.plugins.ec2.win.winrm.soap;
 
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import org.dom4j.Element;
@@ -38,8 +37,7 @@ public class Header {
         this.action = action;
         this.shellId = shellId;
         this.resourceURI = resourceURI;
-        this.optionSet =
-                optionSet != null ? Collections.unmodifiableList(new ArrayList<>(optionSet)) : Collections.emptyList();
+        this.optionSet = optionSet != null ? List.copyOf(optionSet) : Collections.emptyList();
     }
 
     void toElement(Element header) {
diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/soap/HeaderBuilder.java b/src/main/java/hudson/plugins/ec2/win/winrm/soap/HeaderBuilder.java
index fb44542aa..c3396e94a 100644
--- a/src/main/java/hudson/plugins/ec2/win/winrm/soap/HeaderBuilder.java
+++ b/src/main/java/hudson/plugins/ec2/win/winrm/soap/HeaderBuilder.java
@@ -1,7 +1,6 @@
 package hudson.plugins.ec2.win.winrm.soap;
 
 import java.net.URI;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
@@ -65,8 +64,7 @@ public HeaderBuilder resourceURI(URI uri) {
     }
 
     public HeaderBuilder options(List