Skip to content

Commit

Permalink
Merge pull request #570 from anitabee/master
Browse files Browse the repository at this point in the history
Option to launch encrypted EBS root volume from unencrypted AMI
  • Loading branch information
res0nance authored Apr 13, 2021
2 parents 3fb92ff + 1b3389c commit 366e80d
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 109 deletions.
27 changes: 27 additions & 0 deletions src/main/java/hudson/plugins/ec2/EbsEncryptRootVolume.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package hudson.plugins.ec2;

import edu.umd.cs.findbugs.annotations.NonNull;

public enum EbsEncryptRootVolume {
DEFAULT("Based on AMI", null),
ENCRYPTED("Encrypted", true),
UNENCRYPTED("Not Encrypted", false);

private final String displayText;
private final Boolean value;

EbsEncryptRootVolume(String displayText, Boolean value) {
this.displayText = displayText;
this.value = value;
}

@NonNull
public String getDisplayText() {
return displayText;
}

public Boolean getValue() {
return value;
}

}
177 changes: 73 additions & 104 deletions src/main/java/hudson/plugins/ec2/SlaveTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,16 @@
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.model.AmazonEC2Exception;
import com.amazonaws.services.ec2.model.AvailabilityZone;
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.DescribeAvailabilityZonesResult;
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.DescribeSpotPriceHistoryRequest;
import com.amazonaws.services.ec2.model.DescribeSpotPriceHistoryResult;
import com.amazonaws.services.ec2.model.DescribeSubnetsRequest;
import com.amazonaws.services.ec2.model.DescribeSubnetsResult;
import com.amazonaws.services.ec2.model.Filter;
Expand All @@ -61,7 +57,6 @@
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.SpotPrice;
import com.amazonaws.services.ec2.model.StartInstancesRequest;
import com.amazonaws.services.ec2.model.StartInstancesResult;
import com.amazonaws.services.ec2.model.Subnet;
Expand Down Expand Up @@ -127,92 +122,11 @@
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.XmlFile;
import hudson.model.listeners.SaveableListener;
import hudson.security.Permission;
import hudson.util.Secret;
import jenkins.model.Jenkins;
import jenkins.model.JenkinsLocationConfiguration;
import jenkins.slaves.iterators.api.NodeIterator;

import org.apache.commons.lang.StringUtils;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;

import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.services.ec2.AmazonEC2;
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.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;
import hudson.util.DescribableList;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.interceptor.RequirePOST;

Expand Down Expand Up @@ -308,6 +222,8 @@ public class SlaveTemplate implements Describable<SlaveTemplate> {

public Tenancy tenancy;

public EbsEncryptRootVolume ebsEncryptRootVolume;

private transient/* almost final */ Set<LabelAtom> labelSet;

private transient/* almost final */Set<String> securityGroupSet;
Expand Down Expand Up @@ -360,7 +276,8 @@ public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, Stri
boolean useEphemeralDevices, String launchTimeoutStr, boolean associatePublicIp,
String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring,
boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses,
List<? extends NodeProperty<?>> nodeProperties, HostKeyVerificationStrategyEnum hostKeyVerificationStrategy, Tenancy tenancy) {
List<? extends NodeProperty<?>> nodeProperties, HostKeyVerificationStrategyEnum hostKeyVerificationStrategy, Tenancy tenancy, EbsEncryptRootVolume ebsEncryptRootVolume) {

if(StringUtils.isNotBlank(remoteAdmin) || StringUtils.isNotBlank(jvmopts) || StringUtils.isNotBlank(tmpDir)){
LOGGER.log(Level.FINE, "As remoteAdmin, jvmopts or tmpDir is not blank, we must ensure the user has ADMINISTER rights.");
// Can be null during tests
Expand Down Expand Up @@ -425,10 +342,31 @@ public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, Stri

this.hostKeyVerificationStrategy = hostKeyVerificationStrategy != null ? hostKeyVerificationStrategy : HostKeyVerificationStrategyEnum.CHECK_NEW_SOFT;
this.tenancy = tenancy != null ? tenancy : Tenancy.Default;

this.ebsEncryptRootVolume = ebsEncryptRootVolume != null ? ebsEncryptRootVolume : EbsEncryptRootVolume.DEFAULT;
readResolve(); // initialize
}

@Deprecated
public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript,
String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts,
boolean stopOnTerminate, String subnetId, List<EC2Tag> tags, String idleTerminationMinutes, int minimumNumberOfInstances,
int minimumNumberOfSpareInstances, String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination,
boolean useEphemeralDevices, String launchTimeoutStr, boolean associatePublicIp,
String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring,
boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses,
List<? extends NodeProperty<?>> nodeProperties, HostKeyVerificationStrategyEnum hostKeyVerificationStrategy, Tenancy tenancy) {
this(ami, zone, spotConfig, securityGroups, remoteFS,
type, ebsOptimized, labelString, mode, description, initScript,
tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts,
stopOnTerminate, subnetId, tags, idleTerminationMinutes, minimumNumberOfInstances,
minimumNumberOfSpareInstances, instanceCapStr, iamInstanceProfile, deleteRootOnTermination,
useEphemeralDevices, launchTimeoutStr, associatePublicIp,
customDeviceMapping, connectBySSHProcess, monitoring,
t2Unlimited, connectionStrategy, maxTotalUses,
nodeProperties, hostKeyVerificationStrategy, tenancy, null);
}

@Deprecated
public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript,
Expand Down Expand Up @@ -1215,17 +1153,24 @@ List<Instance> findOrphansOrStopped(DescribeInstancesResult diResult, int number
}

private void setupRootDevice(Image image, List<BlockDeviceMapping> deviceMappings) {
if (deleteRootOnTermination && image.getRootDeviceType().equals("ebs")) {
// get the root device (only one expected in the blockmappings)
final List<BlockDeviceMapping> rootDeviceMappings = image.getBlockDeviceMappings();
if (rootDeviceMappings.size() == 0) {
LOGGER.warning("AMI missing block devices");
return;
}
BlockDeviceMapping rootMapping = rootDeviceMappings.get(0);
LOGGER.info("AMI had " + rootMapping.getDeviceName());
LOGGER.info(rootMapping.getEbs().toString());
if (!"ebs".equals(image.getRootDeviceType())) {
return;
}

// get the root device (only one expected in the blockmappings)
final List<BlockDeviceMapping> rootDeviceMappings = image.getBlockDeviceMappings();
if (rootDeviceMappings.size() == 0) {
LOGGER.warning("AMI missing block devices");
return;
}
BlockDeviceMapping rootMapping = rootDeviceMappings.get(0);
LOGGER.info("AMI had " + rootMapping.getDeviceName());
LOGGER.info(rootMapping.getEbs().toString());

// Create a shadow of the AMI mapping (doesn't like reusing rootMapping directly)
BlockDeviceMapping newMapping = rootMapping.clone();

if (deleteRootOnTermination) {
// Check if the root device is already in the mapping and update it
for (final BlockDeviceMapping mapping : deviceMappings) {
LOGGER.info("Request had " + mapping.getDeviceName());
Expand All @@ -1235,14 +1180,15 @@ private void setupRootDevice(Image image, List<BlockDeviceMapping> deviceMapping
}
}

// Create a shadow of the AMI mapping (doesn't like reusing rootMapping directly)
BlockDeviceMapping newMapping = rootMapping.clone();
// pass deleteRootOnTermination to shadow of the AMI mapping
newMapping.getEbs().setDeleteOnTermination(Boolean.TRUE);
//Per the documentation, "If you are creating a volume from a snapshot, you can't specify an encryption value. This is because only blank volumes can be encrypted on creation. "
//The root volume will always have a snapshot, so this value needs to be set to null to work correctly
newMapping.getEbs().setEncrypted(null);
deviceMappings.add(0, newMapping);
}

newMapping.getEbs().setEncrypted(ebsEncryptRootVolume.getValue());
String message = String.format("EBS default encryption value set to: %s (%s)", ebsEncryptRootVolume.getDisplayText(), ebsEncryptRootVolume.getValue());
logProvisionInfo(message);
deviceMappings.add(0, newMapping);

}

private List<BlockDeviceMapping> getNewEphemeralDeviceMapping(Image image) {
Expand Down Expand Up @@ -2135,5 +2081,28 @@ public ListBoxModel doFillTenancyItems(@QueryParameter String tenancy) {
})
.collect(Collectors.toCollection(ListBoxModel::new));
}
public String getDefaultEbsEncryptRootVolume() {
return EbsEncryptRootVolume.DEFAULT.getDisplayText();
}

public ListBoxModel doFillEbsEncryptRootVolumeItems(@QueryParameter String ebsEncryptRootVolume ) {
return Stream.of(EbsEncryptRootVolume.values())
.map(v -> {
if (v.name().equals(ebsEncryptRootVolume)) {
return new ListBoxModel.Option(v.getDisplayText(), v.name(), true);
} else {
return new ListBoxModel.Option(v.getDisplayText(), v.name(), false);
}
})
.collect(Collectors.toCollection(ListBoxModel::new));
}

public FormValidation doEbsEncryptRootVolume(@QueryParameter String ebsEncryptRootVolume) {
Stream<EbsEncryptRootVolume> stream = Stream.of(EbsEncryptRootVolume.values());
Stream<EbsEncryptRootVolume> filteredStream = stream.filter(v -> v.name().equals(ebsEncryptRootVolume));
Optional<EbsEncryptRootVolume> matched = filteredStream.findFirst();
Optional<FormValidation> okResult = matched.map(s -> FormValidation.ok());
return okResult.orElse(FormValidation.error(String.format("Could not find selected option (%s)", ebsEncryptRootVolume)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ THE SOFTWARE.
<f:checkbox />
</f:entry>

<f:entry title="${%Encrypt EBS root volume}" field="ebsEncryptRootVolume">
<f:select default="${descriptor.getDefaultEbsEncryptRootVolume()}"/>
</f:entry>

<f:entry title="${%Block device mapping}" field="customDeviceMapping">
<f:textbox />
</f:entry>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div>
Option to launch encrypted EBS backed EC2 instances from unencrypted AMIs in a single step.
<ul>
<li><b><i>Based on AMI</i></b>: EBS will be either encrypted or not encrypted based on what is defined in AMI</li>
<li><b><i>Not Encrypted</i></b>: Launches not encrypted EBS</i></b></li>
<li><b><i>Encrypted</i></b>: Launches encrypted EBS</li>
</ul>
</div>

54 changes: 49 additions & 5 deletions src/test/java/hudson/plugins/ec2/SlaveTemplateUnitTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.AmazonEC2Client;
import com.amazonaws.services.ec2.model.CreateTagsResult;
import com.amazonaws.services.ec2.model.DescribeImagesRequest;
import com.amazonaws.services.ec2.model.Filter;
import com.amazonaws.services.ec2.model.InstanceType;
import com.amazonaws.services.ec2.model.Tag;
import com.amazonaws.services.ec2.model.*;
import hudson.model.Node;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.powermock.reflect.Whitebox;
import org.powermock.reflect.internal.WhiteboxImpl;

import java.util.Arrays;
import java.util.ArrayList;
Expand Down Expand Up @@ -320,6 +318,52 @@ protected Object readResolve() {
false);
}
}

private Boolean checkEncryptedForSetupRootDevice(EbsEncryptRootVolume rootVolumeEnum) throws Exception {
SlaveTemplate template = new SlaveTemplate(null, EC2AbstractSlave.TEST_ZONE, null, "default", "foo", InstanceType.M1Large, false, "ttt", Node.Mode.NORMAL, "foo", "bar", "bbb", "aaa", "10", "fff", null, "-Xmx1g", false, "subnet 456", null, null, false, null, "", true, false, "", false, "") {
@Override
protected Object readResolve() {
return null;
}
};
List deviceMappings = new ArrayList();
deviceMappings.add(deviceMappings);

Image image = new Image();
image.setRootDeviceType("ebs");
BlockDeviceMapping blockDeviceMapping = new BlockDeviceMapping();
blockDeviceMapping.setEbs(new EbsBlockDevice());
image.getBlockDeviceMappings().add(blockDeviceMapping);
if (rootVolumeEnum instanceof EbsEncryptRootVolume) {
template.ebsEncryptRootVolume = rootVolumeEnum;
};
WhiteboxImpl.invokeMethod(template, "setupRootDevice", image, deviceMappings);
return image.getBlockDeviceMappings().get(0).getEbs().getEncrypted();
}

@Test
public void testSetupRootDeviceNull() throws Exception {
Boolean test = checkEncryptedForSetupRootDevice(null);
Assert.assertNull(test);
}

@Test
public void testSetupRootDeviceDefault() throws Exception {
Boolean test = checkEncryptedForSetupRootDevice(EbsEncryptRootVolume.DEFAULT);
Assert.assertNull(test);
}

@Test
public void testSetupRootDeviceNotEncrypted() throws Exception {
Boolean test = checkEncryptedForSetupRootDevice(EbsEncryptRootVolume.UNENCRYPTED);
Assert.assertFalse(test);
}

@Test
public void testSetupRootDeviceEncrypted() throws Exception {
Boolean test = checkEncryptedForSetupRootDevice(EbsEncryptRootVolume.ENCRYPTED);
Assert.assertTrue(test);
}
}

class TestHandler extends Handler {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
connectBySSHProcess: false
connectionStrategy: PRIVATE_IP
deleteRootOnTermination: false
ebsEncryptRootVolume: DEFAULT
ebsOptimized: false
hostKeyVerificationStrategy: CHECK_NEW_SOFT
labelString: "linux ubuntu"
Expand Down
1 change: 1 addition & 0 deletions src/test/resources/hudson/plugins/ec2/UnixDataExport.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
connectBySSHProcess: false
connectionStrategy: PRIVATE_IP
deleteRootOnTermination: false
ebsEncryptRootVolume: DEFAULT
ebsOptimized: false
hostKeyVerificationStrategy: CHECK_NEW_SOFT
labelString: "linux ubuntu"
Expand Down

0 comments on commit 366e80d

Please sign in to comment.