diff --git a/pom.xml b/pom.xml index 32e52899a..0eefc4809 100644 --- a/pom.xml +++ b/pom.xml @@ -105,11 +105,21 @@ THE SOFTWARE. + + org.jenkins-ci.plugins + credentials + 2.3.5 + org.jenkins-ci.plugins aws-credentials 1.11 + + org.jenkins-ci.plugins + ssh-credentials + 1.18.1 + org.jenkins-ci.plugins bouncycastle-api @@ -138,7 +148,7 @@ THE SOFTWARE. org.jenkins-ci.plugins trilead-api - 1.0.3 + 1.0.5 io.jenkins.temp.jelly @@ -227,6 +237,7 @@ THE SOFTWARE. org.jenkins-ci.plugins structs + 1.20 test @@ -258,6 +269,13 @@ THE SOFTWARE. + + org.jenkins-ci.main + jenkins-bom + ${jenkins.version} + pom + import + org.hamcrest hamcrest-core diff --git a/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java b/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java index ed7b1d016..71e3c990d 100644 --- a/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java @@ -46,13 +46,12 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerResponse; +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.interceptor.RequirePOST; /** * The original implementation of {@link EC2Cloud}. @@ -70,6 +69,12 @@ public class AmazonEC2Cloud extends EC2Cloud { private boolean noDelayProvisioning; @DataBoundConstructor + public AmazonEC2Cloud(String cloudName, boolean useInstanceProfileForCredentials, String credentialsId, String region, String privateKey, String sshKeysCredentialsId, String instanceCapStr, List templates, String roleArn, String roleSessionName) { + super(createCloudId(cloudName), useInstanceProfileForCredentials, credentialsId, privateKey, sshKeysCredentialsId, instanceCapStr, templates, roleArn, roleSessionName); + this.region = region; + } + + @Deprecated public AmazonEC2Cloud(String cloudName, boolean useInstanceProfileForCredentials, String credentialsId, String region, String privateKey, String instanceCapStr, List templates, String roleArn, String roleSessionName) { super(createCloudId(cloudName), useInstanceProfileForCredentials, credentialsId, privateKey, instanceCapStr, templates, roleArn, roleSessionName); this.region = region; @@ -205,7 +210,7 @@ public FormValidation doTestConnection( @QueryParameter String region, @QueryParameter boolean useInstanceProfileForCredentials, @QueryParameter String credentialsId, - @QueryParameter String privateKey, + @QueryParameter String sshKeysCredentialsId, @QueryParameter String roleArn, @QueryParameter String roleSessionName) @@ -215,7 +220,7 @@ public FormValidation doTestConnection( region = DEFAULT_EC2_HOST; } - return super.doTestConnection(getEc2EndpointUrl(region), useInstanceProfileForCredentials, credentialsId, privateKey, roleArn, roleSessionName, region); + return super.doTestConnection(getEc2EndpointUrl(region), useInstanceProfileForCredentials, credentialsId, sshKeysCredentialsId, roleArn, roleSessionName, region); } } } diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 57b9589e5..998b15660 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -34,6 +34,7 @@ import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder; import com.cloudbees.jenkins.plugins.awscredentials.AWSCredentialsImpl; import com.cloudbees.jenkins.plugins.awscredentials.AmazonWebServicesCredentials; +import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey; import com.cloudbees.plugins.credentials.Credentials; import com.cloudbees.plugins.credentials.CredentialsMatchers; import com.cloudbees.plugins.credentials.CredentialsProvider; @@ -42,10 +43,13 @@ import com.cloudbees.plugins.credentials.SystemCredentialsProvider; 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.NonNull; import hudson.Extension; import hudson.ProxyConfiguration; import hudson.model.Computer; import hudson.model.Descriptor; +import hudson.model.ItemGroup; import hudson.model.Label; import hudson.model.Node; import hudson.model.PeriodicWork; @@ -85,6 +89,7 @@ import java.util.EnumSet; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; @@ -98,17 +103,6 @@ import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; -import hudson.ProxyConfiguration; -import hudson.model.Computer; -import hudson.model.Descriptor; -import hudson.model.Label; -import hudson.model.Node; -import hudson.slaves.Cloud; -import hudson.slaves.NodeProvisioner.PlannedNode; -import hudson.util.FormValidation; -import hudson.util.HttpResponses; -import hudson.util.Secret; -import hudson.util.StreamTaskListener; import org.kohsuke.stapler.interceptor.RequirePOST; /** @@ -151,8 +145,11 @@ public abstract class EC2Cloud extends Cloud { @CheckForNull @Deprecated private transient Secret secretKey; - - private final EC2PrivateKey privateKey; + @CheckForNull + @Deprecated + private transient EC2PrivateKey privateKey; + @CheckForNull + private String sshKeysCredentialsId; /** * Upper bound on how many instances we may provision. @@ -165,14 +162,20 @@ public abstract class EC2Cloud extends Cloud { private transient volatile AmazonEC2 connection; - protected EC2Cloud(String id, boolean useInstanceProfileForCredentials, String credentialsId, String privateKey, - String instanceCapStr, List templates, String roleArn, String roleSessionName) { + protected EC2Cloud(String id, boolean useInstanceProfileForCredentials, String credentialsId, String privateKey, String sshKeysCredentialsId, + String instanceCapStr, List templates, String roleArn, String roleSessionName) { super(id); this.useInstanceProfileForCredentials = useInstanceProfileForCredentials; this.roleArn = roleArn; this.roleSessionName = roleSessionName; this.credentialsId = credentialsId; - this.privateKey = new EC2PrivateKey(privateKey); + this.sshKeysCredentialsId = sshKeysCredentialsId; + + if (this.sshKeysCredentialsId == null && ( this.privateKey != null || privateKey != null)){ + migratePrivateSshKeyToCredential(this.privateKey != null ? this.privateKey.getPrivateKey() : privateKey); + } + this.privateKey = null; // This enforces it not to be persisted and that CasC will never output privateKey on export + if (templates == null) { this.templates = Collections.emptyList(); @@ -189,19 +192,70 @@ protected EC2Cloud(String id, boolean useInstanceProfileForCredentials, String c readResolve(); // set parents } + @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); + } + + @CheckForNull + public EC2PrivateKey resolvePrivateKey(){ + if (sshKeysCredentialsId != null) { + BasicSSHUserPrivateKey privateKeyCredential = getSshCredential(sshKeysCredentialsId); + if (privateKeyCredential != null) { + return new EC2PrivateKey(privateKeyCredential.getPrivateKey()); + } + } + return null; + } + public abstract URL getEc2EndpointUrl() throws IOException; public abstract URL getS3EndpointUrl() throws IOException; + 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 BasicSSHUserPrivateKey) + .filter((cred) -> ((BasicSSHUserPrivateKey)cred).getPrivateKey().trim().equals(privateKey.trim())) + .map(cred -> (BasicSSHUserPrivateKey)cred) + .findFirst(); + + if (keyCredential.isPresent()){ + // SET this.sshKeysCredentialsId with the found credential + sshKeysCredentialsId = keyCredential.get().getId(); + } else { + // CREATE new credential + String credsId = UUID.randomUUID().toString(); + + BasicSSHUserPrivateKey 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()); + + addNewGlobalCredential(sshKeyCredentials); + + sshKeysCredentialsId = credsId; + } + } + protected Object readResolve() { this.slaveCountingLock = new ReentrantLock(); + for (SlaveTemplate t : templates) t.parent = this; + if (this.accessId != null && this.secretKey != null && credentialsId == null) { String secretKeyEncryptedValue = this.secretKey.getEncryptedValue(); // REPLACE this.accessId and this.secretId by a credential SystemCredentialsProvider systemCredentialsProvider = SystemCredentialsProvider.getInstance(); + // ITERATE ON EXISTING CREDS AND DON'T CREATE IF EXIST for (Credentials credentials: systemCredentialsProvider.getCredentials()) { if (credentials instanceof AmazonWebServicesCredentials) { @@ -217,33 +271,41 @@ protected Object readResolve() { } } } + // CREATE - for (CredentialsStore credentialsStore: CredentialsProvider.lookupStores(Jenkins.get())) { + String credsId = UUID.randomUUID().toString(); + addNewGlobalCredential(new AWSCredentialsImpl( + CredentialsScope.SYSTEM, credsId, this.accessId, secretKeyEncryptedValue, + "EC2 Cloud - " + getDisplayName())); - if (credentialsStore instanceof SystemCredentialsProvider.StoreImpl) { + this.credentialsId = credsId; + this.accessId = null; + this.secretKey = null; - try { - String credsId = UUID.randomUUID().toString(); - credentialsStore.addCredentials(Domain.global(), new AWSCredentialsImpl( - CredentialsScope.SYSTEM, credsId, this.accessId, secretKeyEncryptedValue, - "EC2 Cloud - " + getDisplayName())); - this.credentialsId = credsId; - this.accessId = null; - this.secretKey = null; - return this; - } catch (IOException e) { - this.credentialsId = null; - LOGGER.log(Level.WARNING, "Exception converting legacy configuration to the new credentials API", e); - } - } - } // 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()); } + return this; } + private void addNewGlobalCredential(Credentials credentials){ + for (CredentialsStore credentialsStore: CredentialsProvider.lookupStores(Jenkins.get())) { + + 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); + } + } + + } + } + public boolean isUseInstanceProfileForCredentials() { return useInstanceProfileForCredentials; } @@ -260,6 +322,12 @@ public String getCredentialsId() { return credentialsId; } + @CheckForNull + public String getSshKeysCredentialsId() { + return sshKeysCredentialsId; + } + + @Deprecated public EC2PrivateKey getPrivateKey() { return privateKey; } @@ -310,9 +378,14 @@ public SlaveTemplate getTemplate(Label label) { /** * Gets the {@link KeyPairInfo} used for the launch. */ + @CheckForNull public synchronized KeyPair getKeyPair() throws AmazonClientException, IOException { - if (usableKeyPair == null) - usableKeyPair = privateKey.find(connect()); + if (usableKeyPair == null) { + EC2PrivateKey ec2PrivateKey = this.resolvePrivateKey(); + if (ec2PrivateKey != null) { + usableKeyPair = ec2PrivateKey.find(connect()); + } + } return usableKeyPair; } @@ -946,6 +1019,24 @@ public static URL checkEndPoint(String url) throws FormValidation { } } + @CheckForNull + private static BasicSSHUserPrivateKey getSshCredential(String id){ + + BasicSSHUserPrivateKey credential = CredentialsMatchers.firstOrNull( + CredentialsProvider.lookupCredentials( + BasicSSHUserPrivateKey.class, // (1) + (ItemGroup) null, + 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 InstanceType[] getInstanceTypes() { @@ -964,9 +1055,35 @@ public FormValidation doCheckUseInstanceProfileForCredentials(@QueryParameter bo return FormValidation.ok(); } - public FormValidation doCheckPrivateKey(@QueryParameter String value) throws IOException, ServletException { + public ListBoxModel doFillSshKeysCredentialsIdItems(@QueryParameter String sshKeysCredentialsId) { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + + StandardListBoxModel result = new StandardListBoxModel(); + + return result + .includeMatchingAs(Jenkins.getAuthentication(), Jenkins.get(), BasicSSHUserPrivateKey.class, Collections.emptyList(), CredentialsMatchers.always()) + .includeMatchingAs(ACL.SYSTEM, Jenkins.get(), BasicSSHUserPrivateKey.class, Collections.emptyList(), CredentialsMatchers.always()) + .includeCurrentValue(sshKeysCredentialsId); + } + + @RequirePOST + public FormValidation doCheckSshKeysCredentialsId(@QueryParameter String value) throws IOException, ServletException { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + + if (value == null || value.isEmpty()){ + return FormValidation.error("No ssh credentials selected"); + } + + BasicSSHUserPrivateKey sshCredential = getSshCredential(value); + String privateKey = ""; + if (sshCredential != null) { + privateKey = sshCredential.getPrivateKey(); + } else { + return FormValidation.error("Failed to find credential \"" + value + "\" in store."); + } + boolean hasStart = false, hasEnd = false; - BufferedReader br = new BufferedReader(new StringReader(value)); + BufferedReader br = new BufferedReader(new StringReader(privateKey)); String line; while ((line = br.readLine()) != null) { if (line.equals("-----BEGIN RSA PRIVATE KEY-----")) @@ -989,7 +1106,7 @@ public FormValidation doCheckPrivateKey(@QueryParameter String value) throws IOE * @param ec2endpoint * @param useInstanceProfileForCredentials * @param credentialsId - * @param privateKey + * @param sshKeysCredentialsId * @param roleArn * @param roleSessionName * @param region @@ -997,17 +1114,23 @@ public FormValidation doCheckPrivateKey(@QueryParameter String value) throws IOE * @throws IOException * @throws ServletException */ - protected FormValidation doTestConnection(URL ec2endpoint, boolean useInstanceProfileForCredentials, String credentialsId, String privateKey, String roleArn, String roleSessionName, String region) + protected FormValidation doTestConnection(URL ec2endpoint, boolean useInstanceProfileForCredentials, String credentialsId, String sshKeysCredentialsId, String roleArn, String roleSessionName, String region) throws IOException, ServletException { Jenkins.get().checkPermission(Jenkins.ADMINISTER); try { + + BasicSSHUserPrivateKey sshCredential = getSshCredential(sshKeysCredentialsId); + String privateKey = ""; + if (sshCredential != null) { + privateKey = sshCredential.getPrivateKey(); + } else { + return FormValidation.error("Failed to find credential \"" + sshKeysCredentialsId + "\" in store."); + } + AWSCredentialsProvider credentialsProvider = createCredentialsProvider(useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); AmazonEC2 ec2 = AmazonEC2Factory.getInstance().connect(credentialsProvider, ec2endpoint); ec2.describeInstances(); - if (privateKey == null) - return FormValidation.error("Private key is not specified. Please fill the private key field with a valid one."); - if (privateKey.trim().length() > 0) { // check if this key exists EC2PrivateKey pk = new EC2PrivateKey(privateKey); diff --git a/src/main/java/hudson/plugins/ec2/Eucalyptus.java b/src/main/java/hudson/plugins/ec2/Eucalyptus.java index 666704c32..b53356066 100644 --- a/src/main/java/hudson/plugins/ec2/Eucalyptus.java +++ b/src/main/java/hudson/plugins/ec2/Eucalyptus.java @@ -34,7 +34,6 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.interceptor.RequirePOST; /** @@ -47,6 +46,14 @@ public class Eucalyptus extends EC2Cloud { private final URL s3endpoint; @DataBoundConstructor + public Eucalyptus(URL ec2endpoint, URL s3endpoint, boolean useInstanceProfileForCredentials, String credentialsId, String privateKey, String sshKeysCredentialsId, String instanceCapStr, List templates, String roleArn, String roleSessionName) + throws IOException { + super("eucalyptus", useInstanceProfileForCredentials, credentialsId, privateKey, sshKeysCredentialsId, instanceCapStr, templates, roleArn, roleSessionName); + this.ec2endpoint = ec2endpoint; + this.s3endpoint = s3endpoint; + } + + @Deprecated public Eucalyptus(URL ec2endpoint, URL s3endpoint, boolean useInstanceProfileForCredentials, String credentialsId, String privateKey, String instanceCapStr, List templates, String roleArn, String roleSessionName) throws IOException { super("eucalyptus", useInstanceProfileForCredentials, credentialsId, privateKey, instanceCapStr, templates, roleArn, roleSessionName); @@ -73,9 +80,9 @@ public String getDisplayName() { @Override @RequirePOST - public FormValidation doTestConnection(@QueryParameter URL ec2endpoint, @QueryParameter boolean useInstanceProfileForCredentials, @QueryParameter String credentialsId, @QueryParameter String privateKey, @QueryParameter String roleArn, @QueryParameter String roleSessionName, @QueryParameter String region) + public FormValidation doTestConnection(@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(ec2endpoint, useInstanceProfileForCredentials, credentialsId, privateKey, roleArn, roleSessionName, region); + return super.doTestConnection(ec2endpoint, useInstanceProfileForCredentials, credentialsId, sshKeysCredentialsId, roleArn, roleSessionName, region); } } } diff --git a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java index e402d3f85..2fc2ee180 100644 --- a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java +++ b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java @@ -112,11 +112,9 @@ import java.util.Base64; import java.util.Collection; import java.util.Collections; -import java.util.Date; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -127,10 +125,16 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.annotation.CheckForNull; import javax.servlet.ServletException; +import hudson.plugins.ec2.util.AmazonEC2Factory; +import hudson.plugins.ec2.util.DeviceMappingParser; +import hudson.plugins.ec2.util.EC2AgentConfig; +import hudson.plugins.ec2.util.EC2AgentFactory; +import hudson.plugins.ec2.util.MinimumInstanceChecker; +import hudson.plugins.ec2.util.MinimumNumberOfInstancesTimeRangeConfig; import edu.umd.cs.findbugs.annotations.NonNull; -import hudson.plugins.ec2.util.*; import hudson.XmlFile; import hudson.model.listeners.SaveableListener; @@ -151,12 +155,57 @@ import com.amazonaws.AmazonServiceException; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.model.*; +import com.amazonaws.services.ec2.model.AmazonEC2Exception; +import com.amazonaws.services.ec2.model.BlockDeviceMapping; +import com.amazonaws.services.ec2.model.CancelSpotInstanceRequestsRequest; +import com.amazonaws.services.ec2.model.CreateTagsRequest; +import com.amazonaws.services.ec2.model.CreditSpecificationRequest; +import com.amazonaws.services.ec2.model.DescribeImagesRequest; +import com.amazonaws.services.ec2.model.DescribeInstancesRequest; +import com.amazonaws.services.ec2.model.DescribeInstancesResult; +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.DescribeSubnetsRequest; +import com.amazonaws.services.ec2.model.DescribeSubnetsResult; +import com.amazonaws.services.ec2.model.Filter; +import com.amazonaws.services.ec2.model.IamInstanceProfileSpecification; +import com.amazonaws.services.ec2.model.Image; +import com.amazonaws.services.ec2.model.Instance; +import com.amazonaws.services.ec2.model.InstanceMarketOptionsRequest; +import com.amazonaws.services.ec2.model.InstanceNetworkInterfaceSpecification; +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.LaunchSpecification; +import com.amazonaws.services.ec2.model.MarketType; +import com.amazonaws.services.ec2.model.Placement; +import com.amazonaws.services.ec2.model.RequestSpotInstancesRequest; +import com.amazonaws.services.ec2.model.RequestSpotInstancesResult; +import com.amazonaws.services.ec2.model.Reservation; +import com.amazonaws.services.ec2.model.ResourceType; +import com.amazonaws.services.ec2.model.RunInstancesRequest; +import com.amazonaws.services.ec2.model.SecurityGroup; +import com.amazonaws.services.ec2.model.ShutdownBehavior; +import com.amazonaws.services.ec2.model.SpotInstanceRequest; +import com.amazonaws.services.ec2.model.SpotMarketOptions; +import com.amazonaws.services.ec2.model.SpotPlacement; +import com.amazonaws.services.ec2.model.StartInstancesRequest; +import com.amazonaws.services.ec2.model.StartInstancesResult; +import com.amazonaws.services.ec2.model.Subnet; +import com.amazonaws.services.ec2.model.Tag; +import com.amazonaws.services.ec2.model.TagSpecification; import hudson.Extension; import hudson.Util; -import hudson.model.*; +import hudson.model.Describable; +import hudson.model.Descriptor; import hudson.model.Descriptor.FormException; +import hudson.model.Hudson; +import hudson.model.Label; +import hudson.model.Node; +import hudson.model.Saveable; +import hudson.model.TaskListener; import hudson.model.labels.LabelAtom; import hudson.slaves.NodeProperty; import hudson.slaves.NodePropertyDescriptor; @@ -838,6 +887,10 @@ HashMap> makeRunInstancesRequestAndFilters(int diFilters.add(new Filter("instance-type").withValues(type.toString())); KeyPair keyPair = getKeyPair(ec2); + if (keyPair == null){ + logProvisionInfo("Could not retrieve a valid key pair."); + return null; + } riRequest.setUserData(Base64.getEncoder().encodeToString(userData.getBytes(StandardCharsets.UTF_8))); riRequest.setKeyName(keyPair.getKeyName()); diFilters.add(new Filter("key-name").withValues(keyPair.getKeyName())); @@ -1393,8 +1446,13 @@ protected EC2SpotSlave newSpotSlave(SpotInstanceRequest sir) throws FormExceptio /** * Get a KeyPair from the configured information for the slave template */ + @CheckForNull private KeyPair getKeyPair(AmazonEC2 ec2) throws IOException, AmazonClientException { - KeyPair keyPair = parent.getPrivateKey().find(ec2); + EC2PrivateKey ec2PrivateKey = getParent().resolvePrivateKey(); + if (ec2PrivateKey == null) { + throw new AmazonClientException("No keypair credential found. Please configure a credential in the Jenkins configuration."); + } + KeyPair keyPair = ec2PrivateKey.find(ec2); if (keyPair == null) { throw new AmazonClientException("No matching keypair found on EC2. Is the EC2 private key a valid one?"); } diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index 433820f75..23521a5ba 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -29,7 +29,6 @@ import hudson.model.Descriptor; import hudson.model.TaskListener; import hudson.plugins.ec2.*; -import hudson.plugins.ec2.ssh.verifiers.CheckNewHardStrategy; import hudson.plugins.ec2.ssh.verifiers.HostKey; import hudson.plugins.ec2.ssh.verifiers.Messages; import hudson.remoting.Channel; @@ -167,7 +166,7 @@ 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 (!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. } @@ -294,7 +293,12 @@ private boolean executeRemote(EC2Computer computer, Connection conn, String chec } private File createIdentityKeyFile(EC2Computer computer) throws IOException { - String privateKey = computer.getCloud().getPrivateKey().getPrivateKey(); + EC2PrivateKey ec2PrivateKey = computer.getCloud().resolvePrivateKey(); + String privateKey = ""; + if (ec2PrivateKey != null){ + privateKey = ec2PrivateKey.getPrivateKey(); + } + File tempFile = File.createTempFile("ec2_", ".pem"); try { @@ -327,6 +331,10 @@ private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemp boolean isAuthenticated = false; logInfo(computer, listener, "Getting keypair..."); KeyPair key = computer.getCloud().getKeyPair(); + 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())); while (tries-- > 0) { diff --git a/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java b/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java index a9fad33d3..7c8c9e38c 100644 --- a/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java +++ b/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java @@ -2,11 +2,7 @@ import hudson.model.Descriptor; import hudson.model.TaskListener; -import hudson.plugins.ec2.EC2AbstractSlave; -import hudson.plugins.ec2.EC2Computer; -import hudson.plugins.ec2.EC2ComputerLauncher; -import hudson.plugins.ec2.EC2HostAddressProvider; -import hudson.plugins.ec2.SlaveTemplate; +import hudson.plugins.ec2.*; import hudson.plugins.ec2.win.winrm.WindowsProcess; import hudson.remoting.Channel; import hudson.remoting.Channel.Listener; @@ -171,7 +167,13 @@ private WinConnection connectToWinRM(EC2Computer computer, EC2AbstractSlave node Thread.sleep(sleepBetweenAttempts); continue; } - String password = node.getCloud().getPrivateKey().decryptWindowsPassword(passwordData); + 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."); + 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"); } 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 e41b46992..b852445c4 100644 --- a/src/main/resources/hudson/plugins/ec2/AmazonEC2Cloud/config-entries.jelly +++ b/src/main/resources/hudson/plugins/ec2/AmazonEC2Cloud/config-entries.jelly @@ -36,8 +36,8 @@ THE SOFTWARE. - - + + @@ -53,5 +53,5 @@ THE SOFTWARE. - + diff --git a/src/main/resources/hudson/plugins/ec2/Eucalyptus/config-entries.jelly b/src/main/resources/hudson/plugins/ec2/Eucalyptus/config-entries.jelly index af7176e37..9e1b58a6e 100644 --- a/src/main/resources/hudson/plugins/ec2/Eucalyptus/config-entries.jelly +++ b/src/main/resources/hudson/plugins/ec2/Eucalyptus/config-entries.jelly @@ -31,8 +31,8 @@ THE SOFTWARE. - - + + diff --git a/src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java b/src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java index a14ea5a9c..6e962ae20 100644 --- a/src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java +++ b/src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java @@ -54,7 +54,7 @@ public class AmazonEC2CloudTest { @Before public void setUp() throws Exception { - cloud = new AmazonEC2Cloud("us-east-1", true, "abc", "us-east-1", "ghi", "3", Collections.emptyList(), "roleArn", "roleSessionName"); + cloud = new AmazonEC2Cloud("us-east-1", true, "abc", "us-east-1", null, "ghi", "3", Collections.emptyList(), "roleArn", "roleSessionName"); r.jenkins.clouds.add(cloud); } @@ -74,24 +74,15 @@ public void testAmazonEC2FactoryGetInstance() throws Exception { } @Test - public void testPrivateKeyRemainsUnchangedAfterUpdatingOtherFields() throws Exception { + public void testSshKeysCredentialsIdRemainsUnchangedAfterUpdatingOtherFields() throws Exception { HtmlForm form = getConfigForm(); HtmlTextInput input = form.getInputByName("_.cloudName"); + input.setText("test-cloud-2"); r.submit(form); AmazonEC2Cloud actual = r.jenkins.clouds.get(AmazonEC2Cloud.class); assertEquals("test-cloud-2", actual.getCloudName()); - r.assertEqualBeans(cloud, actual, "region,useInstanceProfileForCredentials,privateKey,instanceCap,roleArn,roleSessionName"); - } - - @Test - public void testPrivateKeyUpdate() throws Exception { - HtmlForm form = getConfigForm(); - form.getOneHtmlElementByAttribute("input", "class", "secret-update-btn").click(); - form.getTextAreaByName("_.privateKey").setText("new secret key"); - r.submit(form); - AmazonEC2Cloud actual = r.jenkins.clouds.get(AmazonEC2Cloud.class); - assertEquals("new secret key", actual.getPrivateKey().getPrivateKey()); + r.assertEqualBeans(cloud, actual, "region,useInstanceProfileForCredentials,sshKeysCredentialsId,instanceCap,roleArn,roleSessionName"); } private HtmlForm getConfigForm() throws IOException, SAXException { @@ -101,4 +92,5 @@ private HtmlForm getConfigForm() throws IOException, SAXException { return r.createWebClient().goTo("configure").getFormByName("config"); } } + } diff --git a/src/test/java/hudson/plugins/ec2/AmazonEC2CloudUnitTest.java b/src/test/java/hudson/plugins/ec2/AmazonEC2CloudUnitTest.java index aca97f130..06cf53481 100644 --- a/src/test/java/hudson/plugins/ec2/AmazonEC2CloudUnitTest.java +++ b/src/test/java/hudson/plugins/ec2/AmazonEC2CloudUnitTest.java @@ -24,6 +24,7 @@ package hudson.plugins.ec2; import org.junit.Test; + import com.amazonaws.services.ec2.model.Tag; import org.mockito.Mockito; @@ -59,6 +60,7 @@ @RunWith(PowerMockRunner.class) @PrepareForTest({EC2Cloud.class, Jenkins.class}) public class AmazonEC2CloudUnitTest { + @Test public void testEC2EndpointURLCreation() throws MalformedURLException { AmazonEC2Cloud.DescriptorImpl descriptor = new AmazonEC2Cloud.DescriptorImpl(); @@ -71,7 +73,7 @@ public void testEC2EndpointURLCreation() throws MalformedURLException { @Test public void testInstaceCap() throws Exception { AmazonEC2Cloud cloud = new AmazonEC2Cloud("us-east-1", true, "abc", "us-east-1", - "{}", null, Collections.emptyList(), + null, "key", null, Collections.emptyList(), "roleArn", "roleSessionName"); assertEquals(cloud.getInstanceCap(), Integer.MAX_VALUE); assertEquals(cloud.getInstanceCapStr(), ""); @@ -79,7 +81,7 @@ public void testInstaceCap() throws Exception { final int cap = 3; final String capStr = String.valueOf(cap); cloud = new AmazonEC2Cloud("us-east-1", true, "abc", "us-east-1", - "{}", capStr, Collections.emptyList(), + null, "key", capStr, Collections.emptyList(), "roleArn", "roleSessionName"); assertEquals(cloud.getInstanceCap(), cap); assertEquals(cloud.getInstanceCapStr(), capStr); @@ -89,7 +91,7 @@ public void testInstaceCap() throws Exception { public void testSpotInstanceCount() throws Exception { final int numberOfSpotInstanceRequests = 105; AmazonEC2Cloud cloud = PowerMockito.spy(new AmazonEC2Cloud("us-east-1", true, "abc", "us-east-1", - "{}", null, Collections.emptyList(), + null, "key", null, Collections.emptyList(), "roleArn", "roleSessionName")); PowerMockito.mockStatic(Jenkins.class); Jenkins jenkinsMock = mock(Jenkins.class); diff --git a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java index 12c393da3..40ac36588 100644 --- a/src/test/java/hudson/plugins/ec2/EC2CloudTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2CloudTest.java @@ -33,9 +33,9 @@ public class EC2CloudTest { @Test public void testReattachOrphanStoppedNodes() throws Exception { /* Mocked items */ - AmazonEC2Cloud cloud = new AmazonEC2Cloud("us-east-1", true, "abc", "us-east-1", - "{}", null, Collections.emptyList(), - "roleArn", "roleSessionName"); + AmazonEC2Cloud cloud = new AmazonEC2Cloud("us-east-1", true, + "abc", "us-east-1", null, "ghi", + "3", Collections.emptyList(), "roleArn", "roleSessionName"); EC2Cloud spyCloud = PowerMockito.spy(cloud); AmazonEC2 mockEc2 = PowerMockito.mock(AmazonEC2.class); Jenkins mockJenkins = PowerMockito.mock(Jenkins.class); diff --git a/src/test/java/hudson/plugins/ec2/EC2PrivateKeyTest.java b/src/test/java/hudson/plugins/ec2/EC2PrivateKeyTest.java index 06aab4fe3..2126b1635 100644 --- a/src/test/java/hudson/plugins/ec2/EC2PrivateKeyTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2PrivateKeyTest.java @@ -29,6 +29,7 @@ import org.jvnet.hudson.test.JenkinsRule; import java.io.IOException; + import com.amazonaws.AmazonClientException; import static org.junit.Assert.assertEquals; diff --git a/src/test/java/hudson/plugins/ec2/EC2SlaveMonitorTest.java b/src/test/java/hudson/plugins/ec2/EC2SlaveMonitorTest.java index a8d48c8c0..a8def2cc7 100644 --- a/src/test/java/hudson/plugins/ec2/EC2SlaveMonitorTest.java +++ b/src/test/java/hudson/plugins/ec2/EC2SlaveMonitorTest.java @@ -1,9 +1,11 @@ package hudson.plugins.ec2; +import java.security.Security; import java.util.Arrays; import java.util.Collections; import org.junit.Assert; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; @@ -19,6 +21,12 @@ public class EC2SlaveMonitorTest { @Rule public JenkinsRule r = new JenkinsRule(); + @Before + public void init(){ + // Tests using the BouncyCastleProvider failed without that + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + } + @Test public void testMinimumNumberOfInstances() throws Exception { SlaveTemplate template = new SlaveTemplate("ami1", EC2AbstractSlave.TEST_ZONE, null, "default", "foo", InstanceType.M1Large, false, "ttt", Node.Mode.NORMAL, "foo ami", "bar", "bbb", "aaa", "10", "fff", null, "-Xmx1g", false, "subnet 456", null, null, 2, null, null, true, true, false, "", false, "", false, false, true, ConnectionStrategy.PRIVATE_IP, 0); diff --git a/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java b/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java index c16af93cf..8e3e55806 100644 --- a/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java +++ b/src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java @@ -49,16 +49,22 @@ import hudson.plugins.ec2.SlaveTemplate.ProvisionOptions; import hudson.plugins.ec2.util.MinimumNumberOfInstancesTimeRangeConfig; import com.amazonaws.services.ec2.model.Reservation; +import hudson.plugins.ec2.util.PrivateKeyHelper; import jenkins.model.Jenkins; import net.sf.json.JSONObject; +import org.apache.commons.math3.analysis.function.Power; import org.junit.Assert; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.mockito.ArgumentCaptor; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.modules.junit4.PowerMockRunner; import java.util.ArrayList; import java.util.Collections; @@ -71,10 +77,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.verify; import static org.mockito.Matchers.any; +import static org.mockito.Mockito.*; /** * Basic test to validate SlaveTemplate. @@ -571,7 +575,8 @@ private AmazonEC2 setupTestForProvisioning(SlaveTemplate template) throws Except mockedKeyPair.setKeyName("some-key-name"); when(mockedPrivateKey.find(mockedEC2)).thenReturn(mockedKeyPair); when(mockedCloud.connect()).thenReturn(mockedEC2); - when(mockedCloud.getPrivateKey()).thenReturn(mockedPrivateKey); + when(mockedCloud.resolvePrivateKey()).thenReturn(mockedPrivateKey); + template.parent = mockedCloud; DescribeImagesResult mockedImagesResult = mock(DescribeImagesResult.class); diff --git a/src/test/java/hudson/plugins/ec2/util/AmazonEC2FactoryMockImpl.java b/src/test/java/hudson/plugins/ec2/util/AmazonEC2FactoryMockImpl.java index aee638dc8..6d033b3f6 100644 --- a/src/test/java/hudson/plugins/ec2/util/AmazonEC2FactoryMockImpl.java +++ b/src/test/java/hudson/plugins/ec2/util/AmazonEC2FactoryMockImpl.java @@ -131,7 +131,7 @@ private static Image createMockImage(String amiId) { private static void mockDescribeKeyPairs(AmazonEC2Client mock) { Mockito.doAnswer(invocationOnMock -> { KeyPairInfo keyPairInfo = new KeyPairInfo(); - keyPairInfo.setKeyFingerprint(Jenkins.get().clouds.get(AmazonEC2Cloud.class).getPrivateKey().getFingerprint()); + keyPairInfo.setKeyFingerprint(Jenkins.get().clouds.get(AmazonEC2Cloud.class).resolvePrivateKey().getFingerprint()); return new DescribeKeyPairsResult().withKeyPairs(keyPairInfo); }).when(mock).describeKeyPairs(); } diff --git a/src/test/resources/hudson/plugins/ec2/UnixData.yml b/src/test/resources/hudson/plugins/ec2/UnixData.yml index d724c02ca..ca48bc37a 100644 --- a/src/test/resources/hudson/plugins/ec2/UnixData.yml +++ b/src/test/resources/hudson/plugins/ec2/UnixData.yml @@ -4,7 +4,7 @@ jenkins: - amazonEC2: cloudName: "production" useInstanceProfileForCredentials: true - privateKey: "${PRIVATE_KEY}" + sshKeysCredentialsId: "random credentials id" templates: - description: ami: "ami-12345" diff --git a/src/test/resources/hudson/plugins/ec2/UnixDataExport.yml b/src/test/resources/hudson/plugins/ec2/UnixDataExport.yml index b86964ca6..9305771cc 100644 --- a/src/test/resources/hudson/plugins/ec2/UnixDataExport.yml +++ b/src/test/resources/hudson/plugins/ec2/UnixDataExport.yml @@ -1,6 +1,7 @@ - amazonEC2: cloudName: "production" region: "us-east-1" + sshKeysCredentialsId: "random credentials id" templates: - ami: "ami-12345" amiType: