diff --git a/.gitignore b/.gitignore
index 4dff9450..43fbe58f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,6 +32,7 @@ jspm_packages/
.yarn-integrity
.cache
cdk.out/
+lambda/out/bootstrap
/test-reports/
junit.xml
/coverage/
diff --git a/.projenrc.ts b/.projenrc.ts
index 7f273041..22bddb9a 100644
--- a/.projenrc.ts
+++ b/.projenrc.ts
@@ -36,6 +36,7 @@ const project = new CdklabsConstructLibrary({
repositoryUrl: 'https://github.com/cdklabs/cdk-ecr-deployment', /* The repository is the location where the actual code for your package lives. */
gitignore: [
'cdk.out/',
+ 'lambda/out/bootstrap',
], /* Additional entries to .gitignore. */
npmignore: [
'/cdk.out',
diff --git a/API.md b/API.md
index 397cdbd0..5488b851 100644
--- a/API.md
+++ b/API.md
@@ -148,6 +148,7 @@ const eCRDeploymentProps: ECRDeploymentProps = { ... }
| src
| IImageName
| The source of the docker image. |
| buildImage
| string
| Image to use to build Golang lambda for custom resource, if download fails or is not wanted. |
| environment
| {[ key: string ]: string}
| The environment variable to set. |
+| imageArch
| string[]
| The image architecture to be copied. |
| lambdaHandler
| string
| The name of the lambda handler. |
| lambdaRuntime
| aws-cdk-lib.aws_lambda.Runtime
| The lambda function runtime environment. |
| memoryLimit
| number
| The amount of memory (in MiB) to allocate to the AWS Lambda function which replicates the files from the CDK bucket to the destination bucket. |
@@ -211,6 +212,25 @@ The environment variable to set.
---
+##### `imageArch`Optional
+
+```typescript
+public readonly imageArch: string[];
+```
+
+- *Type:* string[]
+- *Default:* ['amd64']
+
+The image architecture to be copied.
+
+The 'amd64' architecture will be copied by default. Specify the
+architecture or architectures to copy here.
+
+It is currently not possible to copy more than one architecture
+at a time: the array you specify must contain exactly one string.
+
+---
+
##### `lambdaHandler`Optional
```typescript
diff --git a/lambda/main.go b/lambda/main.go
index 639d52c6..803ff736 100644
--- a/lambda/main.go
+++ b/lambda/main.go
@@ -53,6 +53,10 @@ func handler(ctx context.Context, event cfn.Event) (physicalResourceID string, d
if err != nil {
return physicalResourceID, data, err
}
+ imageArch, err := getStrPropsDefault(event.ResourceProperties, IMAGE_ARCH, "")
+ if err != nil {
+ return physicalResourceID, data, err
+ }
srcCreds, err := getStrPropsDefault(event.ResourceProperties, SRC_CREDS, "")
if err != nil {
return physicalResourceID, data, err
@@ -71,7 +75,7 @@ func handler(ctx context.Context, event cfn.Event) (physicalResourceID string, d
return physicalResourceID, data, err
}
- log.Printf("SrcImage: %v DestImage: %v", srcImage, destImage)
+ log.Printf("SrcImage: %v DestImage: %v ImageArch: %v", srcImage, destImage, imageArch)
srcRef, err := alltransports.ParseImageName(srcImage)
if err != nil {
@@ -82,13 +86,13 @@ func handler(ctx context.Context, event cfn.Event) (physicalResourceID string, d
return physicalResourceID, data, err
}
- srcOpts := NewImageOpts(srcImage)
+ srcOpts := NewImageOpts(srcImage, imageArch)
srcOpts.SetCreds(srcCreds)
srcCtx, err := srcOpts.NewSystemContext()
if err != nil {
return physicalResourceID, data, err
}
- destOpts := NewImageOpts(destImage)
+ destOpts := NewImageOpts(destImage, imageArch)
destOpts.SetCreds(destCreds)
destCtx, err := destOpts.NewSystemContext()
if err != nil {
diff --git a/lambda/main_test.go b/lambda/main_test.go
index a28b323f..46dd0345 100644
--- a/lambda/main_test.go
+++ b/lambda/main_test.go
@@ -33,10 +33,10 @@ func TestMain(t *testing.T) {
destRef, err := alltransports.ParseImageName(destImage)
assert.NoError(t, err)
- srcOpts := NewImageOpts(srcImage)
+ srcOpts := NewImageOpts(srcImage, "")
srcCtx, err := srcOpts.NewSystemContext()
assert.NoError(t, err)
- destOpts := NewImageOpts(destImage)
+ destOpts := NewImageOpts(destImage, "")
destCtx, err := destOpts.NewSystemContext()
assert.NoError(t, err)
@@ -53,3 +53,12 @@ func TestMain(t *testing.T) {
})
assert.NoError(t, err)
}
+
+func TestNewImageOpts(t *testing.T) {
+ srcOpts := NewImageOpts("s3://cdk-ecr-deployment/nginx.tar:nginx:latest", "arm64")
+ _, err := srcOpts.NewSystemContext()
+ assert.NoError(t, err)
+ destOpts := NewImageOpts("dir:/tmp/nginx.dir", "arm64")
+ _, err = destOpts.NewSystemContext()
+ assert.NoError(t, err)
+}
diff --git a/lambda/utils.go b/lambda/utils.go
index 461c3807..9d0311d8 100644
--- a/lambda/utils.go
+++ b/lambda/utils.go
@@ -23,6 +23,7 @@ import (
const (
SRC_IMAGE string = "SrcImage"
DEST_IMAGE string = "DestImage"
+ IMAGE_ARCH string = "ImageArch"
SRC_CREDS string = "SrcCreds"
DEST_CREDS string = "DestCreds"
)
@@ -86,14 +87,15 @@ type ImageOpts struct {
requireECRLogin bool
region string
creds string
+ arch string
}
-func NewImageOpts(uri string) *ImageOpts {
+func NewImageOpts(uri string, arch string) *ImageOpts {
requireECRLogin := strings.Contains(uri, "dkr.ecr")
if requireECRLogin {
- return &ImageOpts{uri, requireECRLogin, GetECRRegion(uri), ""}
+ return &ImageOpts{uri, requireECRLogin, GetECRRegion(uri), "", arch}
} else {
- return &ImageOpts{uri, requireECRLogin, "", ""}
+ return &ImageOpts{uri, requireECRLogin, "", "", arch}
}
}
@@ -109,6 +111,7 @@ func (s *ImageOpts) NewSystemContext() (*types.SystemContext, error) {
ctx := &types.SystemContext{
DockerRegistryUserAgent: "ecr-deployment",
DockerAuthConfig: &types.DockerAuthConfig{},
+ ArchitectureChoice: s.arch,
}
if s.creds != "" {
diff --git a/src/index.ts b/src/index.ts
index 9ab65116..85cbbbba 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -32,6 +32,19 @@ export interface ECRDeploymentProps {
*/
readonly dest: IImageName;
+ /**
+ * The image architecture to be copied.
+ *
+ * The 'amd64' architecture will be copied by default. Specify the
+ * architecture or architectures to copy here.
+ *
+ * It is currently not possible to copy more than one architecture
+ * at a time: the array you specify must contain exactly one string.
+ *
+ * @default ['amd64']
+ */
+ readonly imageArch?: string[];
+
/**
* The amount of memory (in MiB) to allocate to the AWS Lambda function which
* replicates the files from the CDK bucket to the destination bucket.
@@ -199,14 +212,21 @@ export class ECRDeployment extends Construct {
resources: ['*'],
}));
+ if (props.imageArch && props.imageArch.length !== 1) {
+ throw new Error(`imageArch must contain exactly 1 element, got ${JSON.stringify(props.imageArch)}`);
+ }
+ const imageArch = props.imageArch ? props.imageArch[0] : '';
+
new CustomResource(this, 'CustomResource', {
serviceToken: this.handler.functionArn,
+ // This has been copy/pasted and is a pure lie, but changing it is going to change people's infra!! X(
resourceType: 'Custom::CDKBucketDeployment',
properties: {
SrcImage: props.src.uri,
SrcCreds: props.src.creds,
DestImage: props.dest.uri,
DestCreds: props.dest.creds,
+ ...imageArch ? { ImageArch: imageArch } : {},
},
});
}
diff --git a/test/ecr-deployment.test.ts b/test/ecr-deployment.test.ts
new file mode 100644
index 00000000..bcc19473
--- /dev/null
+++ b/test/ecr-deployment.test.ts
@@ -0,0 +1,61 @@
+import { Stack, App, aws_ecr as ecr, assertions } from 'aws-cdk-lib';
+import { DockerImageName, ECRDeployment } from '../src';
+
+// Yes, it's a lie. It's also the truth.
+const CUSTOM_RESOURCE_TYPE = 'Custom::CDKBucketDeployment';
+
+let app: App;
+let stack: Stack;
+
+const src = new DockerImageName('javacs3/javacs3:latest', 'dockerhub');
+let dest: DockerImageName;
+beforeEach(() => {
+ app = new App();
+ stack = new Stack(app, 'Stack');
+
+ const repo = new ecr.Repository(stack, 'Repo', {
+ repositoryName: 'repo',
+ });
+ dest = new DockerImageName(`${repo.repositoryUri}:copied`);
+
+ // Otherwise we do a Docker build :x
+ process.env.FORCE_PREBUILT_LAMBDA = 'true';
+});
+
+test('ImageArch is missing from custom resource if argument not specified', () => {
+ // WHEN
+ new ECRDeployment(stack, 'ECR', {
+ src,
+ dest,
+ });
+
+ // THEN
+ const template = assertions.Template.fromStack(stack);
+ template.hasResourceProperties(CUSTOM_RESOURCE_TYPE, {
+ ImageArch: assertions.Match.absent(),
+ });
+});
+
+test('ImageArch is in custom resource properties if specified', () => {
+ // WHEN
+ new ECRDeployment(stack, 'ECR', {
+ src,
+ dest,
+ imageArch: ['banana'],
+ });
+
+ // THEN
+ const template = assertions.Template.fromStack(stack);
+ template.hasResourceProperties(CUSTOM_RESOURCE_TYPE, {
+ ImageArch: 'banana',
+ });
+});
+
+test('Cannot specify more or fewer than 1 elements in imageArch', () => {
+ // WHEN
+ expect(() => new ECRDeployment(stack, 'ECR', {
+ src,
+ dest,
+ imageArch: ['banana', 'pear'],
+ })).toThrow(/imageArch must contain exactly 1 element/);
+});
\ No newline at end of file
diff --git a/test/example.ecr-deployment.ts b/test/example.ecr-deployment.ts
index 2d5bd7c8..079aa95f 100644
--- a/test/example.ecr-deployment.ts
+++ b/test/example.ecr-deployment.ts
@@ -34,9 +34,27 @@ class TestECRDeployment extends Stack {
dest: new ecrDeploy.DockerImageName(`${repo.repositoryUri}:latest`),
});
+ new ecrDeploy.ECRDeployment(this, 'DeployECRImage', {
+ src: new ecrDeploy.DockerImageName(image.imageUri),
+ dest: new ecrDeploy.DockerImageName(`${repo.repositoryUri}:latest`),
+ imageArch: ['arm64'],
+ });
+
+ new ecrDeploy.ECRDeployment(this, 'DeployDockerImage', {
+ src: new ecrDeploy.DockerImageName('javacs3/javacs3:latest', 'dockerhub'),
+ dest: new ecrDeploy.DockerImageName(`${repo.repositoryUri}:dockerhub`),
+ }).addToPrincipalPolicy(new iam.PolicyStatement({
+ effect: iam.Effect.ALLOW,
+ actions: [
+ 'secretsmanager:GetSecretValue',
+ ],
+ resources: ['*'],
+ }));
+
new ecrDeploy.ECRDeployment(this, 'DeployDockerImage', {
src: new ecrDeploy.DockerImageName('javacs3/javacs3:latest', 'dockerhub'),
dest: new ecrDeploy.DockerImageName(`${repo.repositoryUri}:dockerhub`),
+ imageArch: ['amd64'],
}).addToPrincipalPolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [