Skip to content

Commit

Permalink
fix(lambda): SAM CLI asset metadata missing from image Functions (aws…
Browse files Browse the repository at this point in the history
…#17368)

Adds asset metadata to image-type lambda functions. This will allow SAM CLI to support local invocation of image-type lambdas from CDK-synthed templates.

It follows the same design and builds upon aws#1433

Fixes aws#14593

Uses some changes from aws#17293 to enable asset metadata generation in integration tests

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
torresxb1 authored and mpvosseller committed Nov 16, 2021
1 parent 308dd89 commit 123dea0
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 12 deletions.
71 changes: 66 additions & 5 deletions packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as fs from 'fs';
import * as path from 'path';
import * as ecr from '@aws-cdk/aws-ecr';
import { Annotations, AssetStaging, FeatureFlags, FileFingerprintOptions, IgnoreMode, Stack, SymlinkFollowMode, Token, Stage } from '@aws-cdk/core';
import { Annotations, AssetStaging, FeatureFlags, FileFingerprintOptions, IgnoreMode, Stack, SymlinkFollowMode, Token, Stage, CfnResource } from '@aws-cdk/core';
import * as cxapi from '@aws-cdk/cx-api';
import { Construct } from 'constructs';

Expand Down Expand Up @@ -147,6 +147,30 @@ export class DockerImageAsset extends CoreConstruct implements IAsset {
*/
public readonly assetHash: string;

/**
* The path to the asset, relative to the current Cloud Assembly
*
* If asset staging is disabled, this will just be the original path.
*
* If asset staging is enabled it will be the staged path.
*/
private readonly assetPath: string;

/**
* The path to the Dockerfile, relative to the assetPath
*/
private readonly dockerfilePath?: string;

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

/**
* Docker target to build to
*/
private readonly dockerBuildTarget?: string;

constructor(scope: Construct, id: string, props: DockerImageAssetProps) {
super(scope, id);

Expand All @@ -160,7 +184,8 @@ export class DockerImageAsset extends CoreConstruct implements IAsset {
}

// validate the docker file exists
const file = path.join(dir, props.file || 'Dockerfile');
this.dockerfilePath = props.file || 'Dockerfile';
const file = path.join(dir, this.dockerfilePath);
if (!fs.existsSync(file)) {
throw new Error(`Cannot find file at ${file}`);
}
Expand Down Expand Up @@ -223,17 +248,53 @@ export class DockerImageAsset extends CoreConstruct implements IAsset {
this.assetHash = staging.assetHash;

const stack = Stack.of(this);
this.assetPath = staging.relativeStagedPath(stack);
this.dockerBuildArgs = props.buildArgs;
this.dockerBuildTarget = props.target;

const location = stack.synthesizer.addDockerImageAsset({
directoryName: staging.relativeStagedPath(stack),
dockerBuildArgs: props.buildArgs,
dockerBuildTarget: props.target,
directoryName: this.assetPath,
dockerBuildArgs: this.dockerBuildArgs,
dockerBuildTarget: this.dockerBuildTarget,
dockerFile: props.file,
sourceHash: staging.assetHash,
});

this.repository = ecr.Repository.fromRepositoryName(this, 'Repository', location.repositoryName);
this.imageUri = location.imageUri;
}

/**
* Adds CloudFormation template metadata to the specified resource with
* information that indicates which resource property is mapped to this local
* asset. This can be used by tools such as SAM CLI to provide local
* experience such as local invocation and debugging of Lambda functions.
*
* Asset metadata will only be included if the stack is synthesized with the
* "aws:cdk:enable-asset-metadata" context key defined, which is the default
* behavior when synthesizing via the CDK Toolkit.
*
* @see https://github.com/aws/aws-cdk/issues/1432
*
* @param resource The CloudFormation resource which is using this asset [disable-awslint:ref-via-interface]
* @param resourceProperty The property name where this asset is referenced
*/
public addResourceMetadata(resource: CfnResource, resourceProperty: string) {
if (!this.node.tryGetContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT)) {
return; // not enabled
}

// tell tools such as SAM CLI that the resourceProperty of this resource
// points to a local path and include the path to de dockerfile, docker build args, and target,
// in order to enable local invocation of this function.
resource.cfnOptions.metadata = resource.cfnOptions.metadata || { };
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_TARGET_KEY] = this.dockerBuildTarget;
resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY] = resourceProperty;
}

}

function validateProps(props: DockerImageAssetProps) {
Expand Down
31 changes: 24 additions & 7 deletions packages/@aws-cdk/aws-lambda/lib/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,28 +517,45 @@ export interface AssetImageCodeProps extends ecr_assets.DockerImageAssetOptions
*/
export class AssetImageCode extends Code {
public readonly isInline: boolean = false;
private asset?: ecr_assets.DockerImageAsset;

constructor(private readonly directory: string, private readonly props: AssetImageCodeProps) {
super();
}

public bind(scope: Construct): CodeConfig {
const asset = new ecr_assets.DockerImageAsset(scope, 'AssetImage', {
directory: this.directory,
...this.props,
});

asset.repository.grantPull(new iam.ServicePrincipal('lambda.amazonaws.com'));
// If the same AssetImageCode is used multiple times, retain only the first instantiation.
if (!this.asset) {
this.asset = new ecr_assets.DockerImageAsset(scope, 'AssetImage', {
directory: this.directory,
...this.props,
});
this.asset.repository.grantPull(new iam.ServicePrincipal('lambda.amazonaws.com'));
} else if (cdk.Stack.of(this.asset) !== cdk.Stack.of(scope)) {
throw new Error(`Asset is already associated with another stack '${cdk.Stack.of(this.asset).stackName}'. ` +
'Create a new Code instance for every stack.');
}

return {
image: {
imageUri: asset.imageUri,
imageUri: this.asset.imageUri,
entrypoint: this.props.entrypoint,
cmd: this.props.cmd,
workingDirectory: this.props.workingDirectory,
},
};
}

public bindToResource(resource: cdk.CfnResource, options: ResourceBindOptions = { }) {
if (!this.asset) {
throw new Error('bindToResource() must be called after bind()');
}

const resourceProperty = options.resourceProperty || 'Code.ImageUri';

// https://github.com/aws/aws-cdk/issues/14593
this.asset.addResourceMetadata(resource, resourceProperty);
}
}

/**
Expand Down
104 changes: 104 additions & 0 deletions packages/@aws-cdk/aws-lambda/test/code.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,110 @@ describe('code', () => {
},
});
});

test('only one Asset object gets created even if multiple functions use the same AssetImageCode', () => {
// given
const app = new cdk.App();
const stack = new cdk.Stack(app, 'MyStack');
const directoryAsset = lambda.Code.fromAssetImage(path.join(__dirname, 'docker-lambda-handler'));

// when
new lambda.Function(stack, 'Fn1', {
code: directoryAsset,
handler: lambda.Handler.FROM_IMAGE,
runtime: lambda.Runtime.FROM_IMAGE,
});

new lambda.Function(stack, 'Fn2', {
code: directoryAsset,
handler: lambda.Handler.FROM_IMAGE,
runtime: lambda.Runtime.FROM_IMAGE,
});

// then
const assembly = app.synth();
const synthesized = assembly.stacks[0];

// Func1 has an asset, Func2 does not
expect(synthesized.assets.length).toEqual(1);
});

test('adds code asset metadata', () => {
// given
const stack = new cdk.Stack();
stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true);

const dockerfilePath = 'Dockerfile';
const dockerBuildTarget = 'stage';
const dockerBuildArgs = { arg1: 'val1', arg2: 'val2' };

// when
new lambda.Function(stack, 'Fn', {
code: lambda.Code.fromAssetImage(path.join(__dirname, 'docker-lambda-handler'), {
file: dockerfilePath,
target: dockerBuildTarget,
buildArgs: dockerBuildArgs,
}),
handler: lambda.Handler.FROM_IMAGE,
runtime: lambda.Runtime.FROM_IMAGE,
});

// then
expect(stack).toHaveResource('AWS::Lambda::Function', {
Metadata: {
[cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.650a009a909c30e767a843a84ff7812616447251d245e0ab65d9bfb37f413e32',
[cxapi.ASSET_RESOURCE_METADATA_DOCKERFILE_PATH_KEY]: dockerfilePath,
[cxapi.ASSET_RESOURCE_METADATA_DOCKER_BUILD_ARGS_KEY]: dockerBuildArgs,
[cxapi.ASSET_RESOURCE_METADATA_DOCKER_BUILD_TARGET_KEY]: dockerBuildTarget,
[cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code.ImageUri',
},
}, ResourcePart.CompleteDefinition);
});

test('adds code asset metadata with default dockerfile path', () => {
// given
const stack = new cdk.Stack();
stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true);

// when
new lambda.Function(stack, 'Fn', {
code: lambda.Code.fromAssetImage(path.join(__dirname, 'docker-lambda-handler')),
handler: lambda.Handler.FROM_IMAGE,
runtime: lambda.Runtime.FROM_IMAGE,
});

// then
expect(stack).toHaveResource('AWS::Lambda::Function', {
Metadata: {
[cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.a3cc4528c34874616814d9b3436ff0e5d01514c1d563ed8899657ca00982f308',
[cxapi.ASSET_RESOURCE_METADATA_DOCKERFILE_PATH_KEY]: 'Dockerfile',
[cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code.ImageUri',
},
}, ResourcePart.CompleteDefinition);
});

test('fails if asset is bound with a second stack', () => {
// given
const app = new cdk.App();
const asset = lambda.Code.fromAssetImage(path.join(__dirname, 'docker-lambda-handler'));

// when
const stack1 = new cdk.Stack(app, 'Stack1');
new lambda.Function(stack1, 'Fn', {
code: asset,
handler: lambda.Handler.FROM_IMAGE,
runtime: lambda.Runtime.FROM_IMAGE,
});

const stack2 = new cdk.Stack(app, 'Stack2');

// then
expect(() => new lambda.Function(stack2, 'Fn', {
code: asset,
handler: lambda.Handler.FROM_IMAGE,
runtime: lambda.Runtime.FROM_IMAGE,
})).toThrow(/already associated/);
});
});

describe('lambda.Code.fromDockerBuild', () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/@aws-cdk/cx-api/lib/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export const ASSET_RESOURCE_METADATA_ENABLED_CONTEXT = 'aws:cdk:enable-asset-met
* to resources.
*/
export const ASSET_RESOURCE_METADATA_PATH_KEY = 'aws:asset:path';
export const ASSET_RESOURCE_METADATA_DOCKERFILE_PATH_KEY = 'aws:asset:dockerfile-path';
export const ASSET_RESOURCE_METADATA_DOCKER_BUILD_ARGS_KEY = 'aws:asset:docker-build-args';
export const ASSET_RESOURCE_METADATA_DOCKER_BUILD_TARGET_KEY = 'aws:asset:docker-build-target';
export const ASSET_RESOURCE_METADATA_PROPERTY_KEY = 'aws:asset:property';

/**
Expand Down

0 comments on commit 123dea0

Please sign in to comment.