Skip to content

Commit

Permalink
feat(core): Allow passing Docker build secrets (#23778)
Browse files Browse the repository at this point in the history
Partially closes #14910 and #14395
----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### Adding new Construct Runtime Dependencies:

* [ ] This PR adds new construct runtime dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-construct-runtime-dependencies)

### New Features

* [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
danwiltshire committed Feb 10, 2023
1 parent be97e4e commit 74512fa
Show file tree
Hide file tree
Showing 32 changed files with 349 additions and 44 deletions.
3 changes: 3 additions & 0 deletions packages/@aws-cdk/aws-ecr-assets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ the `buildArgs` property. It is recommended to skip hashing of `buildArgs` for
values that can change between different machines to maintain a consistent
asset hash.

Additionally, you can supply `buildSecrets`. Your system must have Buildkit
enabled, see https://docs.docker.com/build/buildkit/.

```ts
import { DockerImageAsset } from '@aws-cdk/aws-ecr-assets';

Expand Down
48 changes: 45 additions & 3 deletions packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ export interface DockerImageAssetInvalidationOptions {
*/
readonly buildArgs?: boolean;

/**
* Use `buildSecrets` while calculating the asset hash
*
* @default true
*/
readonly buildSecrets?: boolean;

/**
* Use `target` while calculating the asset hash
*
Expand Down Expand Up @@ -170,6 +177,23 @@ export interface DockerImageAssetOptions extends FingerprintOptions, FileFingerp
*/
readonly buildArgs?: { [key: string]: string };

/**
* Build secrets.
*
* Docker BuildKit must be enabled to use build secrets.
*
* @see https://docs.docker.com/build/buildkit/
*
* @default - no build secrets
*
* @example
*
* {
* 'MY_SECRET': DockerBuildSecret.fromSrc('file.txt')
* }
*/
readonly buildSecrets?: { [key: string]: string }

/**
* Docker target to build to
*
Expand Down Expand Up @@ -282,6 +306,11 @@ export class DockerImageAsset extends Construct implements IAsset {
*/
private readonly dockerBuildArgs?: { [key: string]: string };

/**
* Build secrets to pass to the `docker build` command.
*/
private readonly dockerBuildSecrets?: { [key: string]: string };

/**
* Outputs to pass to the `docker build` command.
*/
Expand Down Expand Up @@ -345,6 +374,7 @@ export class DockerImageAsset extends Construct implements IAsset {
const extraHash: { [field: string]: any } = {};
if (props.invalidation?.extraHash !== false && props.extraHash) { extraHash.user = props.extraHash; }
if (props.invalidation?.buildArgs !== false && props.buildArgs) { extraHash.buildArgs = props.buildArgs; }
if (props.invalidation?.buildSecrets !== false && props.buildSecrets) { extraHash.buildSecrets = props.buildSecrets; }
if (props.invalidation?.target !== false && props.target) { extraHash.target = props.target; }
if (props.invalidation?.file !== false && props.file) { extraHash.file = props.file; }
if (props.invalidation?.repositoryName !== false && props.repositoryName) { extraHash.repositoryName = props.repositoryName; }
Expand Down Expand Up @@ -374,12 +404,14 @@ export class DockerImageAsset extends Construct implements IAsset {
const stack = Stack.of(this);
this.assetPath = staging.relativeStagedPath(stack);
this.dockerBuildArgs = props.buildArgs;
this.dockerBuildSecrets = props.buildSecrets;
this.dockerBuildTarget = props.target;
this.dockerOutputs = props.outputs;

const location = stack.synthesizer.addDockerImageAsset({
directoryName: this.assetPath,
dockerBuildArgs: this.dockerBuildArgs,
dockerBuildSecrets: this.dockerBuildSecrets,
dockerBuildTarget: this.dockerBuildTarget,
dockerFile: props.file,
sourceHash: staging.assetHash,
Expand Down Expand Up @@ -420,6 +452,7 @@ export class DockerImageAsset extends Construct implements IAsset {
resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_PATH_KEY] = this.assetPath;
resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKERFILE_PATH_KEY] = this.dockerfilePath;
resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_BUILD_ARGS_KEY] = this.dockerBuildArgs;
resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_BUILD_SECRETS_KEY] = this.dockerBuildSecrets;
resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_BUILD_TARGET_KEY] = this.dockerBuildTarget;
resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY] = resourceProperty;
resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_OUTPUTS_KEY] = this.dockerOutputs;
Expand All @@ -435,16 +468,25 @@ function validateProps(props: DockerImageAssetProps) {
}

validateBuildArgs(props.buildArgs);
validateBuildSecrets(props.buildSecrets);
}

function validateBuildArgs(buildArgs?: { [key: string]: string }) {
for (const [key, value] of Object.entries(buildArgs || {})) {
function validateBuildProps(buildPropName: string, buildProps?: { [key: string]: string }) {
for (const [key, value] of Object.entries(buildProps || {})) {
if (Token.isUnresolved(key) || Token.isUnresolved(value)) {
throw new Error('Cannot use tokens in keys or values of "buildArgs" since they are needed before deployment');
throw new Error(`Cannot use tokens in keys or values of "${buildPropName}" since they are needed before deployment`);
}
}
}

function validateBuildArgs(buildArgs?: { [key: string]: string }) {
validateBuildProps('buildArgs', buildArgs);
}

function validateBuildSecrets(buildSecrets?: { [key: string]: string }) {
validateBuildProps('buildSecrets', buildSecrets);
}

function toSymlinkFollow(follow?: FollowMode): SymlinkFollowMode | undefined {
switch (follow) {
case undefined: return undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM public.ecr.aws/lambda/python:3.6
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret
EXPOSE 8000
WORKDIR /src
ADD . /src
CMD python3 index.py
33 changes: 33 additions & 0 deletions packages/@aws-cdk/aws-ecr-assets/test/demo-image-secret/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/python
import sys
import textwrap
import http.server
import socketserver

PORT = 8000


class Handler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.end_headers()
self.wfile.write(textwrap.dedent('''\
<!doctype html>
<html><head><title>It works</title></head>
<body>
<h1>Hello from the integ test container</h1>
<p>This container got built and started as part of the integ test.</p>
<img src="https://media.giphy.com/media/nFjDu1LjEADh6/giphy.gif">
</body>
''').encode('utf-8'))


def main():
httpd = http.server.HTTPServer(("", PORT), Handler)
print("serving at port", PORT)
httpd.serve_forever()


if __name__ == '__main__':
main()
4 changes: 3 additions & 1 deletion packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { describeDeprecated, testDeprecated } from '@aws-cdk/cdk-build-tools';
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import { App, DefaultStackSynthesizer, IgnoreMode, Lazy, LegacyStackSynthesizer, Stack, Stage } from '@aws-cdk/core';
import { App, DefaultStackSynthesizer, DockerBuildSecret, IgnoreMode, Lazy, LegacyStackSynthesizer, Stack, Stage } from '@aws-cdk/core';
import * as cxapi from '@aws-cdk/cx-api';
import { DockerImageAsset } from '../lib';

Expand Down Expand Up @@ -150,6 +150,7 @@ describe('image asset', () => {
const asset5 = new DockerImageAsset(stack, 'Asset5', { directory, file: 'Dockerfile.Custom', target: 'NonDefaultTarget' });
const asset6 = new DockerImageAsset(stack, 'Asset6', { directory, extraHash: 'random-extra' });
const asset7 = new DockerImageAsset(stack, 'Asset7', { directory, outputs: ['123'] });
const asset8 = new DockerImageAsset(stack, 'Asset8', { directory, buildSecrets: { mySecret: DockerBuildSecret.fromSrc('abc.txt') } });

expect(asset1.assetHash).toEqual('13248c55633f3b198a628bb2ea4663cb5226f8b2801051bd0c725950266fd590');
expect(asset2.assetHash).toEqual('36bf205fb9adc5e45ba1c8d534158a0aed96d190eff433af1d90f3b94f96e751');
Expand All @@ -158,6 +159,7 @@ describe('image asset', () => {
expect(asset5.assetHash).toEqual('c02bfba13b2e7e1ff5c778a76e10296b9e8d17f7f8252d097f4170ae04ce0eb4');
expect(asset6.assetHash).toEqual('3528d6838647a5e9011b0f35aec514d03ad11af05a94653cdcf4dacdbb070a06');
expect(asset7.assetHash).toEqual('ced0a3076efe217f9cbdff0943e543f36ecf77f70b9a6fe28b8633deb728a462');
expect(asset8.assetHash).toEqual('ffc2718e616141d18c8f4623d13cdfd68cb8f010ca5db31c916c8b5f10c162be');

});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM public.ecr.aws/lambda/python:3.6
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret
EXPOSE 8000
WORKDIR /src
ADD . /src
CMD python3 index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/python
import sys
import textwrap
import http.server
import socketserver

PORT = 8000


class Handler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.end_headers()
self.wfile.write(textwrap.dedent('''\
<!doctype html>
<html><head><title>It works</title></head>
<body>
<h1>Hello from the integ test container</h1>
<p>This container got built and started as part of the integ test.</p>
<img src="https://media.giphy.com/media/nFjDu1LjEADh6/giphy.gif">
</body>
''').encode('utf-8'))


def main():
httpd = http.server.HTTPServer(("", PORT), Handler)
print("serving at port", PORT)
httpd.serve_forever()


if __name__ == '__main__':
main()
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":"24.0.0"}
{"version":"29.0.0"}
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"version": "24.0.0",
"version": "29.0.0",
"files": {
"3ef2c8ebbbb128e6fbd2f26a8c80b8154d5fe5157a29846585cb36feac29318e": {
"b1025f887a56783d23c02c714067f4e119f3a3393c9db47c7ce05076e52e58bd": {
"source": {
"path": "integ-assets-docker.template.json",
"packaging": "file"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "3ef2c8ebbbb128e6fbd2f26a8c80b8154d5fe5157a29846585cb36feac29318e.json",
"objectKey": "b1025f887a56783d23c02c714067f4e119f3a3393c9db47c7ce05076e52e58bd.json",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
Expand Down Expand Up @@ -55,6 +55,21 @@
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-image-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
},
"60dea2e16e94d1977b92fe03fa7085fea446233f1fe499702b69593438baa59f": {
"source": {
"directory": "asset.60dea2e16e94d1977b92fe03fa7085fea446233f1fe499702b69593438baa59f",
"dockerBuildSecrets": {
"mysecret": "src=index.py"
}
},
"destinations": {
"current_account-current_region": {
"repositoryName": "cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}",
"imageTag": "60dea2e16e94d1977b92fe03fa7085fea446233f1fe499702b69593438baa59f",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-image-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@
"Value": {
"Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:fa08370824fa0a7eab2c59a4f371fe7631019044d6c906b4268193120dc213b4"
}
},
"ImageUri5": {
"Value": {
"Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:60dea2e16e94d1977b92fe03fa7085fea446233f1fe499702b69593438baa59f"
}
}
},
"Parameters": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "24.0.0",
"version": "29.0.0",
"testCases": {
"integ.assets-docker": {
"stacks": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "24.0.0",
"version": "29.0.0",
"artifacts": {
"integ-assets-docker.assets": {
"type": "cdk:asset-manifest",
Expand All @@ -17,7 +17,7 @@
"validateOnSynth": false,
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}",
"cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}",
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/3ef2c8ebbbb128e6fbd2f26a8c80b8154d5fe5157a29846585cb36feac29318e.json",
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/b1025f887a56783d23c02c714067f4e119f3a3393c9db47c7ce05076e52e58bd.json",
"requiresBootstrapStackVersion": 6,
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
"additionalDependencies": [
Expand Down Expand Up @@ -69,6 +69,12 @@
"data": "ImageUri4"
}
],
"/integ-assets-docker/ImageUri5": [
{
"type": "aws:cdk:logicalId",
"data": "ImageUri5"
}
],
"/integ-assets-docker/BootstrapVersion": [
{
"type": "aws:cdk:logicalId",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,32 @@
"version": "0.0.0"
}
},
"DockerImage5": {
"id": "DockerImage5",
"path": "integ-assets-docker/DockerImage5",
"children": {
"Staging": {
"id": "Staging",
"path": "integ-assets-docker/DockerImage5/Staging",
"constructInfo": {
"fqn": "@aws-cdk/core.AssetStaging",
"version": "0.0.0"
}
},
"Repository": {
"id": "Repository",
"path": "integ-assets-docker/DockerImage5/Repository",
"constructInfo": {
"fqn": "@aws-cdk/aws-ecr.RepositoryBase",
"version": "0.0.0"
}
}
},
"constructInfo": {
"fqn": "@aws-cdk/aws-ecr-assets.DockerImageAsset",
"version": "0.0.0"
}
},
"MyUser": {
"id": "MyUser",
"path": "integ-assets-docker/MyUser",
Expand Down Expand Up @@ -236,6 +262,14 @@
"version": "0.0.0"
}
},
"ImageUri5": {
"id": "ImageUri5",
"path": "integ-assets-docker/ImageUri5",
"constructInfo": {
"fqn": "@aws-cdk/core.CfnOutput",
"version": "0.0.0"
}
},
"BootstrapVersion": {
"id": "BootstrapVersion",
"path": "integ-assets-docker/BootstrapVersion",
Expand Down Expand Up @@ -263,7 +297,7 @@
"path": "Tree",
"constructInfo": {
"fqn": "constructs.Construct",
"version": "10.1.182"
"version": "10.1.216"
}
}
},
Expand Down
Loading

0 comments on commit 74512fa

Please sign in to comment.