Skip to content

Commit 56cc7ce

Browse files
authored
Merge pull request #6674 from gchq/6616-copy-docker-images
Issue 6616 - Deploy SleeperInstance with Docker images in remote repository
2 parents cca5041 + 05bbe7b commit 56cc7ce

File tree

48 files changed

+985
-309
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+985
-309
lines changed

java/clients/src/main/java/sleeper/clients/deploy/UploadArtefacts.java

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package sleeper.clients.deploy;
1717

18+
import org.apache.commons.lang3.EnumUtils;
1819
import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain;
1920
import software.amazon.awssdk.services.ecr.EcrClient;
2021
import software.amazon.awssdk.services.s3.S3Client;
@@ -65,7 +66,8 @@ public static void main(String[] rawArgs) throws IOException, InterruptedExcepti
6566
CommandOption.longOption("extra-images"),
6667
CommandOption.longFlag("create-builder"),
6768
CommandOption.longFlag("create-deployment"),
68-
CommandOption.longFlag("overwrite-existing")))
69+
CommandOption.longFlag("overwrite-existing"),
70+
CommandOption.shortOption('u', "upload")))
6971
.helpSummary("Uploads jars and Docker images to AWS. You must set either an instance properties file " +
7072
"or an artefacts deployment ID to upload to.\n" +
7173
"\n" +
@@ -98,7 +100,11 @@ public static void main(String[] rawArgs) throws IOException, InterruptedExcepti
98100
"\n" +
99101
"--overwrite-existing\n" +
100102
"By default, images are only uploaded if they do not already exist for this version of " +
101-
"Sleeper. This flag disables that check.")
103+
"Sleeper. This flag disables that check.\n" +
104+
"\n" +
105+
"--upload, -u\n" +
106+
"By default, all artefacts are uploaded. You can use \"--upload jars\" to only upload the " +
107+
"jars, or \"--upload images\" to only upload the container images.")
102108
.build();
103109
Arguments args = CommandArguments.parseAndValidateOrExit(usage, rawArgs, arguments -> new Arguments(
104110
Path.of(arguments.getString("scripts directory")),
@@ -114,7 +120,8 @@ public static void main(String[] rawArgs) throws IOException, InterruptedExcepti
114120
.orElse(List.of()),
115121
arguments.isFlagSetWithDefault("create-builder", true),
116122
arguments.isFlagSetWithDefault("create-deployment", false),
117-
arguments.isFlagSetWithDefault("overwrite-existing", false)));
123+
arguments.isFlagSetWithDefault("overwrite-existing", false),
124+
arguments.getOptionalString("upload").map(ToUpload::fromString).orElse(ToUpload.ALL)));
118125

119126
String deploymentId;
120127
String jarsBucket;
@@ -154,21 +161,31 @@ public static void main(String[] rawArgs) throws IOException, InterruptedExcepti
154161
InvokeCdk.fromScriptsDirectory(args.scriptsDir())
155162
.invoke(InvokeCdk.Type.ARTEFACTS, CdkCommand.deployArtefacts(deploymentId, List.of()));
156163
}
157-
syncJars.sync(SyncJarsRequest.builder()
158-
.bucketName(jarsBucket)
159-
.build());
160-
uploadImages.upload(UploadDockerImagesToEcrRequest.builder()
161-
.ecrPrefix(ecrPrefix)
162-
.images(images)
163-
.extraImages(args.extraImages())
164-
.overwriteExistingTag(args.overwriteExisting())
165-
.build());
164+
if (args.toUpload().isUploadJars()) {
165+
syncJars.sync(SyncJarsRequest.builder()
166+
.bucketName(jarsBucket)
167+
.build());
168+
}
169+
if (args.toUpload().isUploadImages()) {
170+
uploadImages.upload(UploadDockerImagesToEcrRequest.builder()
171+
.ecrPrefix(ecrPrefix)
172+
.images(images)
173+
.extraImages(args.extraImages())
174+
.overwriteExistingTag(args.overwriteExisting())
175+
.build());
176+
}
166177
}
167178
}
168179

169180
public record Arguments(
170-
Path scriptsDir, InstanceProperties instanceProperties, String deploymentId,
171-
List<StackDockerImage> extraImages, boolean createMultiplatformBuilder, boolean createDeployment, boolean overwriteExisting) {
181+
Path scriptsDir,
182+
InstanceProperties instanceProperties,
183+
String deploymentId,
184+
List<StackDockerImage> extraImages,
185+
boolean createMultiplatformBuilder,
186+
boolean createDeployment,
187+
boolean overwriteExisting,
188+
ToUpload toUpload) {
172189

173190
public Arguments {
174191
if (instanceProperties == null && deploymentId == null) {
@@ -177,4 +194,24 @@ public record Arguments(
177194
}
178195
}
179196

197+
public enum ToUpload {
198+
ALL, JARS, IMAGES;
199+
200+
public static ToUpload fromString(String string) {
201+
ToUpload upload = EnumUtils.getEnumIgnoreCase(ToUpload.class, string);
202+
if (upload == null) {
203+
throw new IllegalArgumentException("Unknown identifier for artefacts to upload: " + string);
204+
}
205+
return upload;
206+
}
207+
208+
public boolean isUploadJars() {
209+
return this == ALL || this == JARS;
210+
}
211+
212+
public boolean isUploadImages() {
213+
return this == ALL || this == IMAGES;
214+
}
215+
}
216+
180217
}

java/core/src/main/java/sleeper/core/deploy/LambdaHandler.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ public class LambdaHandler {
147147
.jar(LambdaJar.CUSTOM_RESOURCES)
148148
.handler("sleeper.cdk.custom.VpcCheckLambda::handleEvent")
149149
.core().add();
150+
public static final LambdaHandler COPY_CONTAINER = builder()
151+
.jar(LambdaJar.CUSTOM_RESOURCES)
152+
.handler("sleeper.cdk.custom.CopyContainerImageLambda::handleRequest")
153+
.core().add();
150154
public static final LambdaHandler METRICS_TRIGGER = builder()
151155
.jar(LambdaJar.METRICS)
152156
.handler("sleeper.metrics.TableMetricsTriggerLambda::handleRequest")

java/core/src/main/java/sleeper/core/deploy/LambdaJar.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
import java.util.stream.Stream;
2626

2727
import static java.util.Objects.requireNonNull;
28+
import static sleeper.core.properties.instance.CdkDefinedInstanceProperty.ACCOUNT;
29+
import static sleeper.core.properties.instance.CdkDefinedInstanceProperty.REGION;
30+
import static sleeper.core.properties.instance.CdkDefinedInstanceProperty.VERSION;
2831
import static sleeper.core.properties.instance.CommonProperty.ECR_REPOSITORY_PREFIX;
2932

3033
/**
@@ -149,6 +152,20 @@ public String getFormattedFilename(String version) {
149152
return String.format(filenameFormat, version);
150153
}
151154

155+
/**
156+
* Retrieves the Docker image name for deploying this jar as a Docker container. Includes the repository URL and the
157+
* tag. This method requires that CDK defined properties are set due to requiring account and region.
158+
*
159+
* @param properties the instance properties
160+
* @return the Docker image name
161+
*/
162+
public String getDockerImageName(InstanceProperties properties) {
163+
return properties.get(ACCOUNT) + ".dkr.ecr." +
164+
properties.get(REGION) + ".amazonaws.com/" +
165+
getEcrRepositoryName(properties) +
166+
":" + properties.get(VERSION);
167+
}
168+
152169
/**
153170
* Retrieves the name of the ECR repository for deploying this jar as a Docker container.
154171
*

java/deployment/cdk-custom-resources/src/main/java/sleeper/cdk/custom/CopyContainerImageLambda.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,27 @@
3737

3838
import java.io.IOException;
3939
import java.io.UncheckedIOException;
40+
import java.nio.file.Files;
41+
import java.nio.file.Path;
4042
import java.util.Map;
4143
import java.util.concurrent.ExecutionException;
4244

4345
public class CopyContainerImageLambda extends AbstractCustomResourceHandler {
4446
private static final Logger LOGGER = LoggerFactory.getLogger(CopyContainerImageLambda.class);
4547

48+
private final Path cacheDir;
4649
private final boolean allowInsecureRegistries;
4750
private final CredentialRetriever sourceCredentialRetriever;
4851
private final CredentialRetriever targetCredentialRetriever;
4952

5053
public CopyContainerImageLambda() {
51-
this(builder().targetCredentialRetriever(new EcrCredentialRetriever(EcrClient.create())));
54+
this(builder()
55+
.targetCredentialRetriever(new EcrCredentialRetriever(EcrClient.create()))
56+
.cacheDir(createTemporaryDirectory()));
5257
}
5358

5459
protected CopyContainerImageLambda(Builder builder) {
60+
cacheDir = builder.cacheDir;
5561
allowInsecureRegistries = builder.allowInsecureRegistries;
5662
sourceCredentialRetriever = builder.sourceCredentialRetriever;
5763
targetCredentialRetriever = builder.targetCredentialRetriever;
@@ -107,14 +113,31 @@ private static RegistryImage registryImage(String imageName, CredentialRetriever
107113
}
108114

109115
private Containerizer configure(Containerizer containerizer) {
110-
return JibEvents.logEvents(LOGGER, containerizer.setAllowInsecureRegistries(allowInsecureRegistries));
116+
containerizer.setBaseImageLayersCache(cacheDir);
117+
containerizer.setApplicationLayersCache(cacheDir);
118+
containerizer.setAllowInsecureRegistries(allowInsecureRegistries);
119+
return JibEvents.logEvents(LOGGER, containerizer);
120+
}
121+
122+
private static Path createTemporaryDirectory() {
123+
try {
124+
return Files.createTempDirectory(null);
125+
} catch (IOException e) {
126+
throw new UncheckedIOException(e);
127+
}
111128
}
112129

113130
public static class Builder {
131+
private Path cacheDir;
114132
private boolean allowInsecureRegistries;
115133
private CredentialRetriever sourceCredentialRetriever;
116134
private CredentialRetriever targetCredentialRetriever;
117135

136+
public Builder cacheDir(Path cacheDir) {
137+
this.cacheDir = cacheDir;
138+
return this;
139+
}
140+
118141
public Builder allowInsecureRegistries(boolean allowInsecureRegistries) {
119142
this.allowInsecureRegistries = allowInsecureRegistries;
120143
return this;

java/deployment/cdk-custom-resources/src/test/java/sleeper/cdk/custom/CopyContainerImageLambdaIT.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.google.cloud.tools.jib.registry.RegistryClient;
3030
import org.junit.jupiter.api.BeforeEach;
3131
import org.junit.jupiter.api.Test;
32+
import org.junit.jupiter.api.io.TempDir;
3233
import org.slf4j.Logger;
3334
import org.slf4j.LoggerFactory;
3435
import org.testcontainers.containers.GenericContainer;
@@ -39,6 +40,7 @@
3940
import sleeper.cdk.custom.containers.JibEvents;
4041
import sleeper.cdk.custom.testutil.FakeLambdaContext;
4142

43+
import java.nio.file.Path;
4244
import java.util.Map;
4345

4446
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
@@ -64,6 +66,9 @@ public class CopyContainerImageLambdaIT {
6466
@Container
6567
public GenericContainer<?> destination = createDockerRegistryContainer();
6668

69+
@TempDir
70+
private Path cacheDir;
71+
6772
@BeforeEach
6873
void setUp() {
6974
stubFor(put("/report-response").willReturn(aResponse().withStatus(200)));
@@ -89,6 +94,7 @@ void shouldCopyDockerImageOnCreate(WireMockRuntimeInfo runtimeInfo) throws Excep
8994
verify(putRequestedFor(urlEqualTo("/report-response"))
9095
.withRequestBody(matchingJsonPath("$.Data.digest", matching("sha256:[a-z0-9]+"))
9196
.and(matchingJsonPath("$.Status", equalTo("SUCCESS")))));
97+
assertThat(cacheDir).isNotEmptyDirectory();
9298
}
9399

94100
@Test
@@ -118,6 +124,7 @@ void shouldCopyNewDockerImageOnUpdate(WireMockRuntimeInfo runtimeInfo) throws Ex
118124
verify(putRequestedFor(urlEqualTo("/report-response"))
119125
.withRequestBody(matchingJsonPath("$.Data.digest", matching("sha256:[a-z0-9]+"))
120126
.and(matchingJsonPath("$.Status", equalTo("SUCCESS")))));
127+
assertThat(cacheDir).isNotEmptyDirectory();
121128
}
122129

123130
@Test
@@ -133,6 +140,7 @@ void shouldDoNothingOnDelete(WireMockRuntimeInfo runtimeInfo) throws Exception {
133140
// Then
134141
verify(putRequestedFor(urlEqualTo("/report-response"))
135142
.withRequestBody(equalToJson("{\"Status\":\"SUCCESS\",\"Data\":null}", true, true)));
143+
assertThat(cacheDir).isEmptyDirectory();
136144
}
137145

138146
private static void copyImage(String baseImage, String target) throws Exception {
@@ -169,6 +177,7 @@ private CloudFormationCustomResourceEventBuilder event(WireMockRuntimeInfo runti
169177
private CopyContainerImageLambda lambda() {
170178
return CopyContainerImageLambda.builder()
171179
.allowInsecureRegistries(true)
180+
.cacheDir(cacheDir)
172181
.build();
173182
}
174183

java/deployment/cdk/src/main/java/sleeper/cdk/SleeperArtefactsCdkApp.java

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,12 @@
1818
import software.amazon.awscdk.App;
1919
import software.amazon.awscdk.AppProps;
2020
import software.amazon.awscdk.Environment;
21+
import software.amazon.awscdk.Stack;
2122
import software.amazon.awscdk.StackProps;
2223

2324
import sleeper.cdk.artefacts.SleeperArtefactRepositories;
25+
import sleeper.cdk.artefacts.SleeperArtefactRepositories.ToDeploy;
2426
import sleeper.cdk.util.CdkContext;
25-
import sleeper.core.properties.model.SleeperPropertyValueUtils;
26-
27-
import java.util.List;
2827

2928
/**
3029
* A CDK app to deploy AWS resources that will hold artefacts used to deploy Sleeper.
@@ -46,16 +45,14 @@ public static void main(String[] args) {
4645
.build();
4746
CdkContext context = CdkContext.from(app);
4847
String deploymentId = context.tryGetContext("id");
49-
if (deploymentId == null) {
50-
throw new IllegalArgumentException("ID for artefacts deployment not found in context. Please set \"id\".");
51-
}
52-
List<String> extraEcrImages = SleeperPropertyValueUtils.readList(context.tryGetContext("extraEcrImages"));
53-
SleeperArtefactRepositories.createAsRootStack(app, "SleeperArtefacts",
54-
StackProps.builder()
55-
.stackName(deploymentId + "-artefacts")
56-
.env(environment)
57-
.build(),
58-
deploymentId, extraEcrImages);
48+
Stack stack = new Stack(app, "SleeperArtefacts", StackProps.builder()
49+
.stackName(deploymentId + "-artefacts")
50+
.env(environment)
51+
.build());
52+
SleeperArtefactRepositories.Builder.create(stack, deploymentId)
53+
.extraEcrImages(context.getList("extraEcrImages"))
54+
.deploy(ToDeploy.fromString(context.tryGetContext("deploy")))
55+
.build();
5956
app.synth();
6057
}
6158

java/deployment/cdk/src/main/java/sleeper/cdk/SleeperInstance.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,19 @@ public static SleeperInstance createAsNestedStack(Construct scope, String id, Ne
6565
return create(stack, sleeperProps);
6666
}
6767

68+
/**
69+
* Declares an instance of Sleeper as a nested stack.
70+
*
71+
* @param scope the scope to add the instance to, usually a Stack or NestedStack
72+
* @param id the nested stack ID
73+
* @param sleeperProps configuration to deploy the instance
74+
* @return a reference to interact with constructs in the Sleeper instance
75+
*/
76+
public static SleeperInstance createAsNestedStack(Construct scope, String id, SleeperInstanceProps sleeperProps) {
77+
NestedStack stack = new NestedStack(scope, id);
78+
return create(stack, sleeperProps);
79+
}
80+
6881
/**
6982
* Declares an instance of Sleeper as a root level stack.
7083
*

java/deployment/cdk/src/main/java/sleeper/cdk/SleeperInstanceProps.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import software.constructs.Construct;
2323

2424
import sleeper.cdk.artefacts.SleeperArtefacts;
25+
import sleeper.cdk.artefacts.SleeperInstanceArtefacts;
2526
import sleeper.cdk.networking.SleeperNetworking;
2627
import sleeper.cdk.networking.SleeperNetworkingProvider;
2728
import sleeper.cdk.util.CdkContext;
@@ -97,7 +98,7 @@ public static Builder builder(SleeperInstanceConfiguration configuration, S3Clie
9798
public static Builder builder(InstanceProperties instanceProperties, S3Client s3Client, DynamoDbClient dynamoClient) {
9899
return builder()
99100
.instanceProperties(instanceProperties)
100-
.artefacts(SleeperArtefacts.from(s3Client, instanceProperties))
101+
.artefacts(SleeperArtefacts.fromProperties(s3Client))
101102
.newInstanceValidator(new NewInstanceValidator(s3Client, dynamoClient));
102103
}
103104

@@ -136,7 +137,7 @@ public static SleeperInstanceProps fromContext(CdkContext context, S3Client s3Cl
136137
}
137138
return builder(configuration.getInstanceProperties(), s3Client, dynamoClient)
138139
.tableProperties(configuration.getTableProperties())
139-
.networkingProvider(scope -> SleeperNetworking.createByContext(scope, context, configuration))
140+
.networkingProvider(scope -> SleeperNetworking.createByContext(scope, context, configuration.getInstanceProperties()))
140141
.validateProperties(context.getBooleanOrDefault("validate", true))
141142
.ensureInstanceDoesNotExist(context.getBooleanOrDefault("newinstance", false))
142143
.skipCheckingVersionMatchesProperties(context.getBooleanOrDefault("skipVersionCheck", false))
@@ -179,8 +180,8 @@ public List<TableProperties> getTableProperties() {
179180
return tableProperties;
180181
}
181182

182-
public SleeperArtefacts getArtefacts() {
183-
return artefacts;
183+
public SleeperInstanceArtefacts getArtefacts() {
184+
return artefacts.forInstance(instanceProperties);
184185
}
185186

186187
public boolean isDeployPaused() {
@@ -196,7 +197,7 @@ public static class Builder {
196197
private SleeperArtefacts artefacts;
197198
private NewInstanceValidator newInstanceValidator;
198199
private List<TableProperties> tableProperties = List.of();
199-
private SleeperNetworkingProvider networkingProvider = scope -> SleeperNetworking.createByProperties(scope, instanceProperties);
200+
private SleeperNetworkingProvider networkingProvider = scope -> SleeperNetworking.createByContext(scope, CdkContext.from(scope), instanceProperties);
200201
private String version = SleeperVersion.getVersion();
201202
private boolean validateProperties = true;
202203
private boolean ensureInstanceDoesNotExist = false;

0 commit comments

Comments
 (0)