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 extends SlaveTemplate> 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 extends SlaveTemplate> 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 extends SlaveTemplate> templates, String roleArn, String roleSessionName) {
+ protected EC2Cloud(String id, boolean useInstanceProfileForCredentials, String credentialsId, String privateKey, String sshKeysCredentialsId,
+ String instanceCapStr, List extends SlaveTemplate> 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 extends SlaveTemplate> 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: