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: [