Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ public class AwsQueryController {
"DescribeSecurityGroupRules", "ModifySecurityGroupRules",
"UpdateSecurityGroupRuleDescriptionsIngress", "UpdateSecurityGroupRuleDescriptionsEgress",
"CreateKeyPair", "DescribeKeyPairs", "DeleteKeyPair", "ImportKeyPair",
"DescribeImages", "RegisterImage", "DescribeSnapshots",
"DescribeImages", "RegisterImage", "DeregisterImage", "CreateImage", "CopyImage",
"DescribeSnapshots",
"CreateTags", "DeleteTags", "DescribeTags",
"CreateInternetGateway", "DescribeInternetGateways", "DeleteInternetGateway",
"AttachInternetGateway", "DetachInternetGateway",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1099,6 +1099,76 @@ public boolean isContainerRunning(Instance instance) {
return containerId != null && lifecycleManager.isContainerRunning(containerId);
}

/** Signals that an instance's file system could not be captured as a Docker image. */
public static class CaptureFailedException extends RuntimeException {
public CaptureFailedException(String message, Throwable cause) {
super(message, cause);
}
}

/**
* Captures an instance's file system as a new Docker image, so that an AMI created from it
* carries what was provisioned rather than pointing back at the base image.
*
* <p>Returns the image reference, or null when the instance has no container to capture.
* A commit that is attempted and fails throws instead of returning null: an AMI with no
* captured file system launches its ancestor, so reporting the failure as "no capture" would
* hand back an available AMI whose contents are silently not what was asked for.
*
* @param tag repository:tag to commit to, unique per AMI
* @throws CaptureFailedException if the commit was attempted and did not succeed
*/
public String commitInstance(Instance instance, String tag) {
String containerId = instance.getDockerContainerId();
if (containerId == null) {
return null;
}
try {
// Committing a running container is what AWS does for CreateImage without
// NoReboot; docker quiesces nothing either way, so the semantics match closely
// enough. The container is left running -- CreateImage does not terminate its
// source instance.
String imageId = dockerClient.commitCmd(containerId)
.withRepository(tag.contains(":") ? tag.substring(0, tag.indexOf(':')) : tag)
.withTag(tag.contains(":") ? tag.substring(tag.indexOf(':') + 1) : "latest")
.exec();
LOG.infov("Captured EC2 instance {0} as Docker image {1} ({2})",
instance.getInstanceId(), tag, imageId);
return tag;
} catch (Exception e) {
LOG.warnv("Could not capture EC2 instance {0} as an image: {1}",
instance.getInstanceId(), e.getMessage());
throw new CaptureFailedException("could not commit container " + containerId
+ " of instance " + instance.getInstanceId() + ": " + e.getMessage(), e);
}
}

/**
* Removes an image previously produced by {@link #commitInstance}. Called when the AMI that
* owns it is deregistered, so captures do not accumulate on disk indefinitely.
*
* @return true when the layer is known to be gone, either removed now or already absent;
* false when the daemon refused, in which case the caller must keep the reference
* so the layer can still be found and removed later
*/
public boolean removeCommittedImage(String tag) {
if (tag == null) {
return true;
}
try {
dockerClient.removeImageCmd(tag).withForce(true).exec();
LOG.infov("Removed captured Docker image {0}", tag);
return true;
} catch (NotFoundException e) {
// Already gone: deregistering twice, or the daemon was pruned. Not an error.
LOG.debugv("Captured Docker image {0} was already absent", tag);
return true;
} catch (Exception e) {
LOG.warnv("Could not remove captured Docker image {0}: {1}", tag, e.getMessage());
return false;
}
}

private void injectSshKey(String containerId, String publicKey) {
try {
// Ensure .ssh directory exists with correct permissions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ public Response handle(String action, MultivaluedMap<String, String> params, Str
case "DescribeImages" -> handleDescribeImages(params, region);
case "CreateImage" -> handleCreateImage(params, region);
case "RegisterImage" -> handleRegisterImage(params, region);
case "DeregisterImage" -> handleDeregisterImage(params, region);
case "CopyImage" -> handleCopyImage(params, region);
case "DescribeSnapshots" -> handleDescribeSnapshots(params, region);
// Tags
case "CreateTags" -> handleCreateTags(params, region);
Expand Down Expand Up @@ -3149,6 +3151,57 @@ private Response handleRegisterImage(MultivaluedMap<String, String> p, String re
return xmlResponse(xml.build());
}

/**
* DeregisterImage. The documented response is requestId plus {@code return} ("Returns true if
* the request succeeds; otherwise, it returns an error"), with deleteSnapshotResultSet present
* only when DeleteAssociatedSnapshots was requested.
*
* @see <a href="https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DeregisterImage.html">DeregisterImage</a>
*/
private Response handleDeregisterImage(MultivaluedMap<String, String> p, String region) {
List<Ec2Service.SnapshotDeletion> deletions = service.deregisterImage(
region,
p.getFirst("ImageId"),
Boolean.parseBoolean(p.getFirst("DeleteAssociatedSnapshots")));
XmlBuilder xml = new XmlBuilder()
.start("DeregisterImageResponse", AwsNamespaces.EC2)
.elem("requestId", UUID.randomUUID().toString())
.elem("return", "true");
if (!deletions.isEmpty()) {
xml.start("deleteSnapshotResultSet");
for (Ec2Service.SnapshotDeletion deletion : deletions) {
xml.start("item")
.elem("snapshotId", deletion.snapshotId())
.elem("returnCode", deletion.returnCode())
.end("item");
}
xml.end("deleteSnapshotResultSet");
}
xml.end("DeregisterImageResponse");
return xmlResponse(xml.build());
}

/**
* CopyImage. "The copy operation must be initiated in the destination Region", so the
* request's own region is the destination and SourceRegion names where the source AMI lives.
*
* @see <a href="https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CopyImage.html">CopyImage</a>
*/
private Response handleCopyImage(MultivaluedMap<String, String> p, String region) {
Image image = service.copyImage(
region,
p.getFirst("SourceRegion"),
p.getFirst("SourceImageId"),
p.getFirst("Name"),
p.getFirst("Description"));
XmlBuilder xml = new XmlBuilder()
.start("CopyImageResponse", AwsNamespaces.EC2)
.elem("requestId", UUID.randomUUID().toString())
.elem("imageId", image.getImageId())
.end("CopyImageResponse");
return xmlResponse(xml.build());
}

private Response handleDescribeSnapshots(MultivaluedMap<String, String> p, String region) {
List<String> ids = getList(p, "SnapshotId");
List<String> owners = getList(p, "Owner", "OwnerId", "OwnerIds");
Expand Down
Loading
Loading