Skip to content

Commit

Permalink
[JENKINS-59180] Allow AMI filtering instead of AMI ID
Browse files Browse the repository at this point in the history
Currently whenever the image is updated the template configuration must
also be updated. Rather than requiring the AMI ID to be specified, allow
providing filters that can be passed to `describeImages` so that the
latest image matching some attributes can be used.
  • Loading branch information
dbnicholson committed Nov 18, 2020
1 parent a450ef5 commit 759e1fd
Show file tree
Hide file tree
Showing 9 changed files with 436 additions and 10 deletions.
99 changes: 89 additions & 10 deletions src/main/java/hudson/plugins/ec2/SlaveTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
Expand Down Expand Up @@ -311,6 +312,21 @@ public class SlaveTemplate implements Describable<SlaveTemplate> {

private transient/* almost final */Set<String> securityGroupSet;

/* FIXME: Ideally these would be List<String>, but Jenkins currently
* doesn't offer a usable way to represent those in forms. Instead
* the values are interpreted as a comma separated list.
*
* https://issues.jenkins.io/browse/JENKINS-27901
*/
@CheckForNull
private String amiOwners;

@CheckForNull
private String amiUsers;

@CheckForNull
private List<EC2Filter> amiFilters;

/*
* Necessary to handle reading from old configurations. The UnixData object is created in readResolve()
*/
Expand Down Expand Up @@ -405,7 +421,7 @@ public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, Stri
this.t2Unlimited = t2Unlimited;

this.hostKeyVerificationStrategy = hostKeyVerificationStrategy != null ? hostKeyVerificationStrategy : HostKeyVerificationStrategyEnum.CHECK_NEW_SOFT;

readResolve(); // initialize
}

Expand Down Expand Up @@ -787,6 +803,36 @@ public void setHostKeyVerificationStrategy(HostKeyVerificationStrategyEnum hostK
public HostKeyVerificationStrategyEnum getHostKeyVerificationStrategy() {
return hostKeyVerificationStrategy != null ? hostKeyVerificationStrategy : HostKeyVerificationStrategyEnum.CHECK_NEW_SOFT;
}

@CheckForNull
public String getAmiOwners() {
return amiOwners;
}

@DataBoundSetter
public void setAmiOwners(String amiOwners) {
this.amiOwners = amiOwners;
}

@CheckForNull
public String getAmiUsers() {
return amiUsers;
}

@DataBoundSetter
public void setAmiUsers(String amiUsers) {
this.amiUsers = amiUsers;
}

@CheckForNull
public List<EC2Filter> getAmiFilters() {
return amiFilters;
}

@DataBoundSetter
public void setAmiFilters(List<EC2Filter> amiFilters) {
this.amiFilters = amiFilters;
}

@Override
public String toString() {
Expand Down Expand Up @@ -1195,17 +1241,50 @@ private void setupEphemeralDeviceMapping(Image image, List<BlockDeviceMapping> d
deviceMappings.addAll(getNewEphemeralDeviceMapping(image));
}

private Image getImage() {
DescribeImagesRequest request = new DescribeImagesRequest().withImageIds(ami);
for (final Image image : getParent().connect().describeImages(request).getImages()) {

if (ami.equals(image.getImageId())) {
@NonNull
private static List<String> makeImageAttributeList(@CheckForNull String attr) {
return Stream.of(Util.tokenize(Util.fixNull(attr)))
.collect(Collectors.toList());
}

return image;
}
}
@NonNull
private DescribeImagesRequest makeDescribeImagesRequest() {
List<String> imageIds = Util.fixEmptyAndTrim(ami) == null ?
Collections.emptyList() :
Collections.singletonList(ami);
List<String> owners = makeImageAttributeList(amiOwners);
List<String> users = makeImageAttributeList(amiUsers);
List<Filter> filters = EC2Filter.toFilterList(amiFilters);

// Log a warning if there were no search attributes. This is
// legal but probably not what anyone wants. Might be better
// as an exception.
int numAttrs = Stream.of(imageIds, owners, users, filters)
.collect(Collectors.summingInt(List::size));
if (numAttrs == 0) {
LOGGER.warning("Neither AMI ID nor AMI search attributes provided");
}

return new DescribeImagesRequest()
.withImageIds(imageIds)
.withOwners(owners)
.withExecutableUsers(users)
.withFilters(filters);
}

throw new AmazonClientException("Unable to find AMI " + ami);
@NonNull
private Image getImage() throws AmazonClientException {
DescribeImagesRequest request = makeDescribeImagesRequest();

LOGGER.info("Getting image for request " + request);
List<Image> images = getParent().connect().describeImages(request).getImages();
if (images.isEmpty()) {
throw new AmazonClientException("Unable to find image for request " + request);
}

// Sort in reverse by creation date to get latest image
images.sort(Comparator.comparing(Image::getCreationDate).reversed());
return images.get(0);
}


Expand Down
22 changes: 22 additions & 0 deletions src/main/resources/hudson/plugins/ec2/SlaveTemplate/config.jelly
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,30 @@ THE SOFTWARE.
<f:textbox />
</f:entry>

<!-- FIXME: Ideally this would include owners and filters, but
validateButton only marshals simple types and can't handle the
filters repeatableProperty.
-->
<f:validateButton title="${%Check AMI}" progress="${%Checking...}" method="validateAmi" with="useInstanceProfileForCredentials,credentialsId,region,ec2endpoint,ami,roleArn,roleSessionName" />

<f:entry title="${%AMI Owners}" field="amiOwners">
<f:textbox />
</f:entry>

<f:entry title="${%AMI Users}" field="amiUsers">
<f:textbox />
</f:entry>

<f:entry title="${%AMI Filters}" help="/descriptor/hudson.plugins.ec2.SlaveTemplate/help/amiFilters">
<f:repeatableProperty field="amiFilters">
<f:block>
<div align="right">
<f:repeatableDeleteButton />
</div>
</f:block>
</f:repeatableProperty>
</f:entry>

<f:entry title="${%Instance Type}" field="type">
<f:enum>${it.name()}</f:enum>
</f:entry>
Expand Down
12 changes: 12 additions & 0 deletions src/main/resources/hudson/plugins/ec2/SlaveTemplate/help-ami.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<div>
A specific image ID to use for instances. Alternatively, the image can
be queried by criteria using the AMI Owners, Users and Filters options.

<br/>

See the <code>ImageId</code> parameter in the AWS
<a href="https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeImages.html">
describeImages API documentation
</a>
for details.
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<div>
Limits the AMI to be used by attributes of the image. Each filter is a
name and values pair. The filter values are a whitespace separated
list allowing the attribute to match against multiple values. Values
can be quoted to preserve embedded whitespace. Any quote characters
must be escaped with <code>\</code>. The latest image by creation date
will be used when multiple images are matched.

<br/>

See the <code>Filter</code> parameter in the AWS
<a href="https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeImages.html">
describeImages API documentation
</a>
for details.
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<div>
Limits the AMI to images owned by the specified AWS accounts. The
value is a whitespace separated list allowing images to match against
multiple accounts. The latest image by creation date will be used when
multiple images are matched.

<br/>

See the <code>Owner</code> parameter in the AWS
<a href="https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeImages.html">
describeImages API documentation
</a>
for details.
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<div>
Limits the AMI to images executable by the specified AWS accounts. The
value is a whitespace separated list allowing images to match against
multiple accounts. The latest image by creation date will be used when
multiple images are matched.

<br/>

See the <code>ExecutableBy</code> parameter in the AWS
<a href="https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeImages.html">
describeImages API documentation
</a>
for details.
</div>
37 changes: 37 additions & 0 deletions src/test/java/hudson/plugins/ec2/ConfigurationAsCodeTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package hudson.plugins.ec2;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import jenkins.model.Jenkins;
Expand All @@ -20,6 +22,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

public class ConfigurationAsCodeTest {
Expand Down Expand Up @@ -145,4 +148,38 @@ public void testConfigAsCodeExport() throws Exception {
String expected = toStringFromYamlFile(this, "UnixDataExport.yml");
assertEquals(expected, exported);
}

@Test
@ConfiguredWithCode("Ami.yml")
public void testAmi() throws Exception {
final AmazonEC2Cloud ec2Cloud = (AmazonEC2Cloud) Jenkins.get().getCloud("ec2-test");
assertNotNull(ec2Cloud);

final List<SlaveTemplate> templates = ec2Cloud.getTemplates();
assertEquals(3, templates.size());

SlaveTemplate slaveTemplate;
List<EC2Filter> expectedFilters;

slaveTemplate = templates.get(0);
assertEquals("ami-0123456789abcdefg", slaveTemplate.ami);
assertNull(slaveTemplate.getAmiOwners());
assertNull(slaveTemplate.getAmiUsers());
assertNull(slaveTemplate.getAmiFilters());

slaveTemplate = templates.get(1);
assertNull(slaveTemplate.ami);
assertEquals("self", slaveTemplate.getAmiOwners());
assertEquals("self", slaveTemplate.getAmiUsers());
expectedFilters = Arrays.asList(new EC2Filter("name", "foo-*"),
new EC2Filter("architecture", "x86_64"));
assertEquals(expectedFilters, slaveTemplate.getAmiFilters());

slaveTemplate = templates.get(2);
assertNull(slaveTemplate.ami);
assertNull(slaveTemplate.getAmiOwners());
assertNull(slaveTemplate.getAmiUsers());
expectedFilters = Collections.emptyList();
assertEquals(expectedFilters, slaveTemplate.getAmiFilters());
}
}
Loading

0 comments on commit 759e1fd

Please sign in to comment.