Skip to content

Commit 1929ef2

Browse files
committed
Add support for remote agents with SSM
1 parent 44286f6 commit 1929ef2

14 files changed

Lines changed: 997 additions & 71 deletions

File tree

pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ THE SOFTWARE.
121121
<groupId>io.jenkins.plugins.aws-java-sdk2</groupId>
122122
<artifactId>aws-java-sdk2-ec2</artifactId>
123123
</dependency>
124+
<dependency>
125+
<groupId>io.jenkins.plugins.aws-java-sdk2</groupId>
126+
<artifactId>aws-java-sdk2-ssm</artifactId>
127+
</dependency>
124128
<dependency>
125129
<groupId>io.jenkins.plugins.mina-sshd-api</groupId>
126130
<artifactId>mina-sshd-api-core</artifactId>

src/main/java/hudson/plugins/ec2/EC2Cloud.java

Lines changed: 84 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import hudson.model.PeriodicWork;
4747
import hudson.model.TaskListener;
4848
import hudson.plugins.ec2.util.AmazonEC2Factory;
49+
import hudson.plugins.ec2.util.AmazonSSMFactory;
4950
import hudson.plugins.ec2.util.FIPS140Utils;
5051
import hudson.plugins.ec2.util.KeyPair;
5152
import hudson.security.ACL;
@@ -136,6 +137,7 @@
136137
import software.amazon.awssdk.services.ec2.model.SpotInstanceRequest;
137138
import software.amazon.awssdk.services.ec2.model.SpotInstanceState;
138139
import software.amazon.awssdk.services.ec2.model.Tag;
140+
import software.amazon.awssdk.services.ssm.SsmClient;
139141
import software.amazon.awssdk.services.sts.StsClient;
140142
import software.amazon.awssdk.services.sts.StsClientBuilder;
141143
import software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider;
@@ -211,6 +213,8 @@ public class EC2Cloud extends Cloud {
211213

212214
private boolean noDelayProvisioning;
213215

216+
private boolean useSSM;
217+
214218
private boolean cleanUpOrphanedNodes;
215219

216220
private transient volatile Ec2Client connection;
@@ -299,6 +303,9 @@ protected EC2Cloud(
299303

300304
@CheckForNull
301305
public EC2PrivateKey resolvePrivateKey() {
306+
if (useSSM) {
307+
return null;
308+
}
302309
if (!System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) {
303310
LOGGER.fine(() -> "(resolvePrivateKey) secret key file configured, will load from disk");
304311
return EC2PrivateKey.fetchFromDisk();
@@ -391,6 +398,15 @@ public void setNoDelayProvisioning(boolean noDelayProvisioning) {
391398
this.noDelayProvisioning = noDelayProvisioning;
392399
}
393400

401+
public boolean isUseSSM() {
402+
return useSSM;
403+
}
404+
405+
@DataBoundSetter
406+
public void setUseSSM(boolean useSSM) {
407+
this.useSSM = useSSM;
408+
}
409+
394410
public boolean isCleanUpOrphanedNodes() {
395411
return cleanUpOrphanedNodes;
396412
}
@@ -640,6 +656,9 @@ public Collection<SlaveTemplate> getTemplates(Label label) {
640656
*/
641657
@CheckForNull
642658
public synchronized KeyPair getKeyPair() throws SdkException, IOException {
659+
if (useSSM) {
660+
return null;
661+
}
643662
if (usableKeyPair == null) {
644663
EC2PrivateKey ec2PrivateKey = this.resolvePrivateKey();
645664
if (ec2PrivateKey != null) {
@@ -1334,6 +1353,11 @@ public Ec2Client connect() throws SdkException {
13341353
}
13351354
}
13361355

1356+
public SsmClient createSsmClient() {
1357+
return AmazonSSMFactory.getInstance()
1358+
.connect(createCredentialsProvider(), parseRegion(getRegion()), parseEndpoint(getAltEC2Endpoint()));
1359+
}
1360+
13371361
public static SdkHttpClient getHttpClient() {
13381362
Jenkins instance = Jenkins.getInstanceOrNull();
13391363

@@ -1463,12 +1487,19 @@ public FormValidation doCheckRoleSessionName(
14631487

14641488
@RequirePOST
14651489
public FormValidation doCheckSshKeysCredentialsId(
1466-
@AncestorInPath ItemGroup context, @QueryParameter String value) throws IOException, ServletException {
1490+
@AncestorInPath ItemGroup context,
1491+
@QueryParameter String value,
1492+
@QueryParameter boolean useSSM)
1493+
throws IOException, ServletException {
14671494
if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {
14681495
// Don't do anything if the user is only reading the configuration
14691496
return FormValidation.ok();
14701497
}
14711498

1499+
if (useSSM) {
1500+
return FormValidation.ok();
1501+
}
1502+
14721503
String privateKey;
14731504
List<FormValidation> validations = new ArrayList<>();
14741505

@@ -1559,7 +1590,8 @@ public FormValidation doTestConnection(
15591590
@QueryParameter String credentialsId,
15601591
@QueryParameter String sshKeysCredentialsId,
15611592
@QueryParameter String roleArn,
1562-
@QueryParameter String roleSessionName)
1593+
@QueryParameter String roleSessionName,
1594+
@QueryParameter boolean useSSM)
15631595
throws IOException, ServletException {
15641596
if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {
15651597
return FormValidation.ok();
@@ -1568,30 +1600,6 @@ public FormValidation doTestConnection(
15681600
List<FormValidation> validations = new ArrayList<>();
15691601

15701602
LOGGER.fine(() -> "begin doTestConnection()");
1571-
String privateKey = "";
1572-
if (System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) {
1573-
LOGGER.fine(() -> "static credential is in use");
1574-
SSHUserPrivateKey sshCredential = getSshCredential(sshKeysCredentialsId, context);
1575-
if (sshCredential != null) {
1576-
privateKey = sshCredential.getPrivateKey();
1577-
} else {
1578-
return FormValidation.error(
1579-
"Failed to find credential \"" + sshKeysCredentialsId + "\" in store.");
1580-
}
1581-
} else {
1582-
EC2PrivateKey k = EC2PrivateKey.fetchFromDisk();
1583-
if (k == null) {
1584-
validations.add(FormValidation.error(
1585-
"Failed to find private key file " + System.getProperty(SSH_PRIVATE_KEY_FILEPATH)));
1586-
if (!StringUtils.isEmpty(sshKeysCredentialsId)) {
1587-
validations.add(FormValidation.warning(
1588-
"Private key file path defined, selected credential will be ignored"));
1589-
}
1590-
return FormValidation.aggregate(validations);
1591-
}
1592-
privateKey = k.getPrivateKey();
1593-
}
1594-
LOGGER.fine(() -> "private key found ok");
15951603

15961604
if (Util.fixEmpty(region) == null) {
15971605
region = DEFAULT_EC2_HOST;
@@ -1603,29 +1611,60 @@ public FormValidation doTestConnection(
16031611
.connect(credentialsProvider, parseRegion(region), parseEndpoint(altEC2Endpoint));
16041612
ec2.describeInstances();
16051613

1606-
if (!privateKey.trim().isEmpty()) {
1607-
// check if this key exists
1608-
EC2PrivateKey pk = new EC2PrivateKey(privateKey);
1609-
if (pk.find(ec2) == null) {
1610-
validations.add(FormValidation.error(
1611-
"The EC2 key pair private key isn't registered to this EC2 region (fingerprint is "
1612-
+ pk.getFingerprint() + ")"));
1614+
if (useSSM) {
1615+
validations.add(
1616+
FormValidation.ok("SSM connection mode selected. SSH key validation skipped."));
1617+
} else {
1618+
String privateKey = "";
1619+
if (System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) {
1620+
LOGGER.fine(() -> "static credential is in use");
1621+
SSHUserPrivateKey sshCredential = getSshCredential(sshKeysCredentialsId, context);
1622+
if (sshCredential != null) {
1623+
privateKey = sshCredential.getPrivateKey();
1624+
} else {
1625+
return FormValidation.error(
1626+
"Failed to find credential \"" + sshKeysCredentialsId + "\" in store.");
1627+
}
1628+
} else {
1629+
EC2PrivateKey k = EC2PrivateKey.fetchFromDisk();
1630+
if (k == null) {
1631+
validations.add(FormValidation.error(
1632+
"Failed to find private key file "
1633+
+ System.getProperty(SSH_PRIVATE_KEY_FILEPATH)));
1634+
if (!StringUtils.isEmpty(sshKeysCredentialsId)) {
1635+
validations.add(FormValidation.warning(
1636+
"Private key file path defined, selected credential will be ignored"));
1637+
}
1638+
return FormValidation.aggregate(validations);
1639+
}
1640+
privateKey = k.getPrivateKey();
1641+
}
1642+
LOGGER.fine(() -> "private key found ok");
1643+
1644+
if (!privateKey.trim().isEmpty()) {
1645+
// check if this key exists
1646+
EC2PrivateKey pk = new EC2PrivateKey(privateKey);
1647+
if (pk.find(ec2) == null) {
1648+
validations.add(FormValidation.error(
1649+
"The EC2 key pair private key isn't registered to this EC2 region (fingerprint is "
1650+
+ pk.getFingerprint() + ")"));
1651+
}
16131652
}
1614-
}
16151653

1616-
if (!System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) {
1617-
if (!StringUtils.isEmpty(sshKeysCredentialsId)) {
1618-
validations.add(
1619-
FormValidation.warning("Using private key file instead of selected credential"));
1620-
} else {
1621-
validations.add(FormValidation.ok("Using private key file"));
1654+
if (!System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) {
1655+
if (!StringUtils.isEmpty(sshKeysCredentialsId)) {
1656+
validations.add(
1657+
FormValidation.warning("Using private key file instead of selected credential"));
1658+
} else {
1659+
validations.add(FormValidation.ok("Using private key file"));
1660+
}
16221661
}
1623-
}
16241662

1625-
try {
1626-
FIPS140Utils.ensurePrivateKeyInFipsMode(privateKey);
1627-
} catch (IllegalArgumentException ex) {
1628-
validations.add(FormValidation.error(ex, ex.getLocalizedMessage()));
1663+
try {
1664+
FIPS140Utils.ensurePrivateKeyInFipsMode(privateKey);
1665+
} catch (IllegalArgumentException ex) {
1666+
validations.add(FormValidation.error(ex, ex.getLocalizedMessage()));
1667+
}
16291668
}
16301669

16311670
validations.add(FormValidation.ok(Messages.EC2Cloud_Success()));

src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import hudson.plugins.ec2.ssh.EC2MacLauncher;
88
import hudson.plugins.ec2.ssh.EC2UnixLauncher;
99
import hudson.plugins.ec2.ssh.EC2WindowsSSHLauncher;
10+
import hudson.plugins.ec2.ssm.EC2SSMLauncher;
1011
import hudson.plugins.ec2.win.EC2WindowsLauncher;
1112
import hudson.slaves.NodeProperty;
1213
import java.io.IOException;
@@ -433,6 +434,71 @@ public EC2OndemandSlave(
433434
Boolean metadataSupported,
434435
Boolean enclaveEnabled)
435436
throws FormException, IOException {
437+
this(
438+
name,
439+
instanceId,
440+
templateDescription,
441+
remoteFS,
442+
numExecutors,
443+
labelString,
444+
mode,
445+
initScript,
446+
tmpDir,
447+
nodeProperties,
448+
remoteAdmin,
449+
javaPath,
450+
jvmopts,
451+
stopOnTerminate,
452+
idleTerminationMinutes,
453+
publicDNS,
454+
privateDNS,
455+
tags,
456+
cloudName,
457+
launchTimeout,
458+
amiType,
459+
connectionStrategy,
460+
maxTotalUses,
461+
tenancy,
462+
metadataEndpointEnabled,
463+
metadataTokensRequired,
464+
metadataHopsLimit,
465+
metadataSupported,
466+
enclaveEnabled,
467+
false);
468+
}
469+
470+
public EC2OndemandSlave(
471+
String name,
472+
String instanceId,
473+
String templateDescription,
474+
String remoteFS,
475+
int numExecutors,
476+
String labelString,
477+
Mode mode,
478+
String initScript,
479+
String tmpDir,
480+
List<? extends NodeProperty<?>> nodeProperties,
481+
String remoteAdmin,
482+
String javaPath,
483+
String jvmopts,
484+
boolean stopOnTerminate,
485+
String idleTerminationMinutes,
486+
String publicDNS,
487+
String privateDNS,
488+
List<EC2Tag> tags,
489+
String cloudName,
490+
int launchTimeout,
491+
AMITypeData amiType,
492+
ConnectionStrategy connectionStrategy,
493+
int maxTotalUses,
494+
Tenancy tenancy,
495+
Boolean metadataEndpointEnabled,
496+
Boolean metadataTokensRequired,
497+
Integer metadataHopsLimit,
498+
Boolean metadataSupported,
499+
Boolean enclaveEnabled,
500+
boolean useSSM)
501+
throws FormException, IOException {
436502
super(
437503
name,
438504
instanceId,
@@ -441,11 +507,13 @@ public EC2OndemandSlave(
441507
numExecutors,
442508
mode,
443509
labelString,
444-
(amiType.isWinRMAgent()
445-
? new EC2WindowsLauncher()
446-
: (amiType.isWindows()
447-
? new EC2WindowsSSHLauncher()
448-
: (amiType.isMac() ? new EC2MacLauncher() : new EC2UnixLauncher()))),
510+
useSSM
511+
? new EC2SSMLauncher()
512+
: (amiType.isWinRMAgent()
513+
? new EC2WindowsLauncher()
514+
: (amiType.isWindows()
515+
? new EC2WindowsSSHLauncher()
516+
: (amiType.isMac() ? new EC2MacLauncher() : new EC2UnixLauncher()))),
449517
new EC2RetentionStrategy(idleTerminationMinutes),
450518
initScript,
451519
tmpDir,

0 commit comments

Comments
 (0)