diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index c9e8433930f8a..1ddc674c07da5 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -49,6 +49,69 @@ to our CDK project directory. This is especially important when we want to share this construct through a library. Different programming languages will have different techniques for bundling resources into libraries. +When using `fromAsset` or `fromInline`, you can obtain the hash of source +through the `function.codeHash` property. This property will return `undefined` +if the code hash cannot be calculated during synthesis (e.g. when using code +from an S3 bucket). + +### Versions and Aliases + +You can use +[versions](https://docs.aws.amazon.com/lambda/latest/dg/configuration-versions.html) +to manage the deployment of your AWS Lambda functions. For example, you can +publish a new version of a function for beta testing without affecting users of +the stable production version. + +The function version includes the following information: + +- The function code and all associated dependencies. +- The Lambda runtime that executes the function. +- All of the function settings, including the environment variables. +- A unique Amazon Resource Name (ARN) to identify this version of the function. + +You can define one or more +[aliases](https://docs.aws.amazon.com/lambda/latest/dg/configuration-aliases.html) +for your AWS Lambda function. A Lambda alias is like a pointer to a specific +Lambda function version. Users can access the function version using the alias +ARN. + +The `fn.currentVersion` property can be used to obtain a `lambda.Version` +resource that represents the AWS Lambda function defined in your application. +Any change to your function's code or configuration will result in the creation +of a new version resource. You can specify options for this version through the +`currentVersionOptions` property. + +> The `currentVersion` property is only supported when your AWS Lambda function +> uses either `lambda.Code.fromAsset` or `lambda.Code.fromInline`. Other types +> of code providers (such as `lambda.Code.fromBucket`) require that you define a +> `lambda.Version` resource directly since the CDK is unable to determine if +> their contents had changed. + +The `version.addAlias()` method can be used to define an AWS Lambda alias that +points to a specific version. + +The following example defines an alias named `live` which will always point to a +version that represents the function as defined in your CDK app. When you change +your lambda code or configuration, a new resource will be created. You can +specify options for the current version through the `currentVersionOptions` +property. + +```ts +const fn = new lambda.Function(this, 'MyFunction', { + currentVersionOptions: { + removalPolicy: RemovalPolicy.RETAIN, // retain old versions + retryAttempts: 1 // async retry attempts + } +}); + +fn.currentVersion.addAlias('live'); +``` + +> NOTE: The `fn.latestVersion` property returns a `lambda.IVersion` which +> represents the `$LATEST` pseudo-version. Most AWS services require a specific +> AWS Lambda version, and won't allow you to use `$LATEST`. Therefore, you would +> normally want to use `lambda.currentVersion`. + ### Layers The `lambda.LayerVersion` class can be used to define Lambda layers and manage diff --git a/packages/@aws-cdk/aws-lambda/lib/alias.ts b/packages/@aws-cdk/aws-lambda/lib/alias.ts index d4dc36cb670c3..f533d19a45e17 100644 --- a/packages/@aws-cdk/aws-lambda/lib/alias.ts +++ b/packages/@aws-cdk/aws-lambda/lib/alias.ts @@ -20,9 +20,9 @@ export interface IAlias extends IFunction { } /** - * Properties for a new Lambda alias + * Options for `lambda.Alias`. */ -export interface AliasProps extends EventInvokeConfigOptions { +export interface AliasOptions extends EventInvokeConfigOptions { /** * Description for the alias * @@ -30,18 +30,6 @@ export interface AliasProps extends EventInvokeConfigOptions { */ readonly description?: string; - /** - * Function version this alias refers to - * - * Use lambda.addVersion() to obtain a new lambda version to refer to. - */ - readonly version: IVersion; - - /** - * Name of this alias - */ - readonly aliasName: string; - /** * Additional versions with individual weights this alias points to * @@ -69,6 +57,23 @@ export interface AliasProps extends EventInvokeConfigOptions { readonly provisionedConcurrentExecutions?: number; } +/** + * Properties for a new Lambda alias + */ +export interface AliasProps extends AliasOptions { + /** + * Name of this alias + */ + readonly aliasName: string; + + /** + * Function version this alias refers to + * + * Use lambda.addVersion() to obtain a new lambda version to refer to. + */ + readonly version: IVersion; +} + export interface AliasAttributes { readonly aliasName: string; readonly aliasVersion: IVersion; diff --git a/packages/@aws-cdk/aws-lambda/lib/function-base.ts b/packages/@aws-cdk/aws-lambda/lib/function-base.ts index 2207db443f1b4..c8d9a49764231 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function-base.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function-base.ts @@ -2,12 +2,14 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import { ConstructNode, IResource, Resource } from '@aws-cdk/core'; +import { AliasOptions } from './alias'; import { EventInvokeConfig, EventInvokeConfigOptions } from './event-invoke-config'; import { IEventSource } from './event-source'; import { EventSourceMapping, EventSourceMappingOptions } from './event-source-mapping'; import { IVersion } from './lambda-version'; import { CfnPermission } from './lambda.generated'; import { Permission } from './permission'; +import { addAlias } from './util'; export interface IFunction extends IResource, ec2.IConnectable, iam.IGrantable { @@ -39,6 +41,13 @@ export interface IFunction extends IResource, ec2.IConnectable, iam.IGrantable { /** * The `$LATEST` version of this function. + * + * Note that this is reference to a non-specific AWS Lambda version, which + * means the function this version refers to can return different results in + * different invocations. + * + * To obtain a reference to an explicit version which references the current + * function configuration, use `lambdaFunction.currentVersion` instead. */ readonly latestVersion: IVersion; @@ -102,7 +111,7 @@ export interface IFunction extends IResource, ec2.IConnectable, iam.IGrantable { /** * Configures options for asynchronous invocation. */ - configureAsyncInvoke(options: EventInvokeConfigOptions): void + configureAsyncInvoke(options: EventInvokeConfigOptions): void; } /** @@ -235,7 +244,7 @@ export abstract class FunctionBase extends Resource implements IFunction { } public get latestVersion(): IVersion { - // Dynamic to avoid invinite recursion when creating the LatestVersion instance... + // Dynamic to avoid infinite recursion when creating the LatestVersion instance... return new LatestVersion(this); } @@ -393,4 +402,8 @@ class LatestVersion extends FunctionBase implements IVersion { public get role() { return this.lambda.role; } + + public addAlias(aliasName: string, options: AliasOptions = {}) { + return addAlias(this, this, aliasName, options); + } } diff --git a/packages/@aws-cdk/aws-lambda/lib/function-hash.ts b/packages/@aws-cdk/aws-lambda/lib/function-hash.ts new file mode 100644 index 0000000000000..a0ca9b8becd92 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/lib/function-hash.ts @@ -0,0 +1,23 @@ +import { CfnResource, Stack } from "@aws-cdk/core"; +import * as crypto from 'crypto'; +import { Function as LambdaFunction } from "./function"; + +export function calculateFunctionHash(fn: LambdaFunction) { + const stack = Stack.of(fn); + + const functionResource = fn.node.defaultChild as CfnResource; + + // render the cloudformation resource from this function + const config = stack.resolve((functionResource as any)._toCloudFormation()); + + const hash = crypto.createHash('md5'); + hash.update(JSON.stringify(config)); + + return hash.digest('hex'); +} + +export function trimFromStart(s: string, maxLength: number) { + const desiredLength = Math.min(maxLength, s.length); + const newStart = s.length - desiredLength; + return s.substring(newStart); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index 2b76f8a21bd80..d659a4fede6f0 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -3,12 +3,13 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as logs from '@aws-cdk/aws-logs'; import * as sqs from '@aws-cdk/aws-sqs'; -import { Construct, Duration, Fn, Lazy } from '@aws-cdk/core'; +import { CfnResource, Construct, Duration, Fn, Lazy, Stack } from '@aws-cdk/core'; import { Code, CodeConfig } from './code'; import { EventInvokeConfigOptions } from './event-invoke-config'; import { IEventSource } from './event-source'; import { FunctionAttributes, FunctionBase, IFunction } from './function-base'; -import { Version } from './lambda-version'; +import { calculateFunctionHash, trimFromStart } from './function-hash'; +import { Version, VersionOptions } from './lambda-version'; import { CfnFunction } from './lambda.generated'; import { ILayerVersion } from './layers'; import { LogRetention } from './log-retention'; @@ -224,6 +225,13 @@ export interface FunctionOptions extends EventInvokeConfigOptions { * @default - A new role is created. */ readonly logRetentionRole?: iam.IRole; + + /** + * Options for the `lambda.Version` resource automatically created by the + * `fn.currentVersion` method. + * @default - default options as described in `VersionOptions` + */ + readonly currentVersionOptions?: VersionOptions; } export interface FunctionProps extends FunctionOptions { @@ -266,6 +274,28 @@ export interface FunctionProps extends FunctionOptions { * library. */ export class Function extends FunctionBase { + + /** + * Returns a `lambda.Version` which represents the current version of this + * Lambda function. A new version will be created every time the function's + * configuration changes. + * + * You can specify options for this version using the `currentVersionOptions` + * prop when initializing the `lambda.Function`. + */ + public get currentVersion(): Version { + if (this._currentVersion) { + return this._currentVersion; + } + + this._currentVersion = new Version(this, `CurrentVersion`, { + lambda: this, + ...this.currentVersionOptions + }); + + return this._currentVersion; + } + public static fromFunctionArn(scope: Construct, id: string, functionArn: string): IFunction { return Function.fromFunctionAttributes(scope, id, { functionArn }); } @@ -425,6 +455,9 @@ export class Function extends FunctionBase { */ private readonly environment: { [key: string]: string }; + private readonly currentVersionOptions?: VersionOptions; + private _currentVersion?: Version; + constructor(scope: Construct, id: string, props: FunctionProps) { super(scope, id, { physicalName: props.functionName, @@ -520,6 +553,8 @@ export class Function extends FunctionBase { retryAttempts: props.retryAttempts, }); } + + this.currentVersionOptions = props.currentVersionOptions; } /** @@ -557,19 +592,27 @@ export class Function extends FunctionBase { * Add a new version for this Lambda * * If you want to deploy through CloudFormation and use aliases, you need to - * add a new version (with a new name) to your Lambda every time you want - * to deploy an update. An alias can then refer to the newly created Version. + * add a new version (with a new name) to your Lambda every time you want to + * deploy an update. An alias can then refer to the newly created Version. * * All versions should have distinct names, and you should not delete versions * as long as your Alias needs to refer to them. * - * @param name A unique name for this version - * @param codeSha256 The SHA-256 hash of the most recently deployed Lambda source code, or - * omit to skip validation. + * @param name A unique name for this version. + * @param codeSha256 The SHA-256 hash of the most recently deployed Lambda + * source code, or omit to skip validation. * @param description A description for this version. - * @param provisionedExecutions A provisioned concurrency configuration for a function's version. - * @param asyncInvokeConfig configuration for this version when it is invoked asynchronously. + * @param provisionedExecutions A provisioned concurrency configuration for a + * function's version. + * @param asyncInvokeConfig configuration for this version when it is invoked + * asynchronously. * @returns A new Version object. + * + * @deprecated This method will create an AWS::Lambda::Version resource which + * snapshots the AWS Lambda function *at the time of its creation* and it + * won't get updated when the function changes. Instead, use + * `this.currentVersion` to obtain a reference to a version resource that gets + * automatically recreated when the function configuration (or code) changes. */ public addVersion( name: string, @@ -577,6 +620,7 @@ export class Function extends FunctionBase { description?: string, provisionedExecutions?: number, asyncInvokeConfig: EventInvokeConfigOptions = {}): Version { + return new Version(this, 'Version' + name, { lambda: this, codeSha256, @@ -607,14 +651,47 @@ export class Function extends FunctionBase { return this._logGroup; } + protected prepare() { + super.prepare(); + + // if we have a current version resource, override it's logical id + // so that it includes the hash of the function code and it's configuration. + if (this._currentVersion) { + const stack = Stack.of(this); + const cfn = this._currentVersion.node.defaultChild as CfnResource; + const originalLogicalId: string = stack.resolve(cfn.logicalId); + + const hash = calculateFunctionHash(this); + + const logicalId = trimFromStart(originalLogicalId, 255 - 32); + cfn.overrideLogicalId(`${logicalId}${hash}`); + } + } + private renderEnvironment() { if (!this.environment || Object.keys(this.environment).length === 0) { return undefined; } - return { - variables: this.environment - }; + // for backwards compatibility we do not sort environment variables in case + // _currentVersion is not defined. otherwise, this would have invalidated + // the template, and for example, may cause unneeded updates for nested + // stacks. + if (!this._currentVersion) { + return { + variables: this.environment + }; + } + + // sort environment so the hash of the function used to create + // `currentVersion` is not affected by key order (this is how lambda does + // it). + const variables: { [key: string]: string } = { }; + for (const key of Object.keys(this.environment).sort()) { + variables[key] = this.environment[key]; + } + + return { variables }; } /** @@ -749,4 +826,4 @@ export function verifyCodeConfig(code: CodeConfig, runtime: Runtime) { if (code.inlineCode && !runtime.supportsInlineCode) { throw new Error(`Inline source not allowed for ${runtime.name}`); } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts index bd5c132f89e44..b04fbace206e9 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts @@ -1,9 +1,11 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; -import { Construct, Fn } from '@aws-cdk/core'; +import { Construct, Fn, RemovalPolicy } from '@aws-cdk/core'; +import { Alias, AliasOptions } from './alias'; import { EventInvokeConfigOptions } from './event-invoke-config'; import { Function } from './function'; import { IFunction, QualifiedFunctionBase } from './function-base'; import { CfnVersion } from './lambda.generated'; +import { addAlias } from './util'; export interface IVersion extends IFunction { /** @@ -16,12 +18,19 @@ export interface IVersion extends IFunction { * The underlying AWS Lambda function. */ readonly lambda: IFunction; + + /** + * Defines an alias for this version. + * @param aliasName The name of the alias + * @param options Alias options + */ + addAlias(aliasName: string, options?: AliasOptions): Alias; } /** - * Properties for a new Lambda version + * Options for `lambda.Version` */ -export interface VersionProps extends EventInvokeConfigOptions { +export interface VersionOptions extends EventInvokeConfigOptions { /** * SHA256 of the version of the Lambda source code * @@ -38,17 +47,30 @@ export interface VersionProps extends EventInvokeConfigOptions { */ readonly description?: string; - /** - * Function to get the value of - */ - readonly lambda: IFunction; - /** * Specifies a provisioned concurrency configuration for a function's version. * * @default No provisioned concurrency */ readonly provisionedConcurrentExecutions?: number; + + /** + * Whether to retain old versions of this function when a new version is + * created. + * + * @default RemovalPolicy.DESTROY + */ + readonly removalPolicy?: RemovalPolicy; +} + +/** + * Properties for a new Lambda version + */ +export interface VersionProps extends VersionOptions { + /** + * Function to get the value of + */ + readonly lambda: IFunction; } export interface VersionAttributes { @@ -102,6 +124,10 @@ export class Version extends QualifiedFunctionBase implements IVersion { protected readonly qualifier = version; protected readonly canCreatePermissions = false; + + public addAlias(name: string, opts: AliasOptions = { }): Alias { + return addAlias(this, this, name, opts); + } } return new Import(scope, id); } @@ -117,6 +143,10 @@ export class Version extends QualifiedFunctionBase implements IVersion { protected readonly qualifier = attrs.version; protected readonly canCreatePermissions = false; + + public addAlias(name: string, opts: AliasOptions = { }): Alias { + return addAlias(this, this, name, opts); + } } return new Import(scope, id); } @@ -141,6 +171,12 @@ export class Version extends QualifiedFunctionBase implements IVersion { provisionedConcurrencyConfig: this.determineProvisionedConcurrency(props) }); + if (props.removalPolicy) { + version.applyRemovalPolicy(props.removalPolicy, { + default: RemovalPolicy.DESTROY + }); + } + this.version = version.attrVersion; this.functionArn = version.ref; this.functionName = `${this.lambda.functionName}:${this.version}`; @@ -178,6 +214,15 @@ export class Version extends QualifiedFunctionBase implements IVersion { }); } + /** + * Defines an alias for this version. + * @param aliasName The name of the alias (e.g. "live") + * @param options Alias options + */ + public addAlias(aliasName: string, options: AliasOptions = { }): Alias { + return addAlias(this, this, aliasName, options); + } + /** * Validate that the provisionedConcurrentExecutions makes sense * diff --git a/packages/@aws-cdk/aws-lambda/lib/util.ts b/packages/@aws-cdk/aws-lambda/lib/util.ts new file mode 100644 index 0000000000000..317699450b436 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/lib/util.ts @@ -0,0 +1,11 @@ +import { Construct } from "@aws-cdk/core"; +import { Alias, AliasOptions } from "./alias"; +import { IVersion } from "./lambda-version"; + +export function addAlias(scope: Construct, version: IVersion, aliasName: string, options: AliasOptions = {}) { + return new Alias(scope, `Alias${aliasName}`, { + aliasName, + version, + ...options + }); +} diff --git a/packages/@aws-cdk/aws-lambda/test/integ.current-version.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.current-version.expected.json new file mode 100644 index 0000000000000..96d17d375e9c1 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.current-version.expected.json @@ -0,0 +1,143 @@ +{ + "Resources": { + "MyLambdaServiceRole4539ECB6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "MyLambdaCCE802FB": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters45f085ecc03a1a22cf003fba3fab28e660c92bcfcd4d0c01b62c7cd191070a2dS3Bucket34E3DBD0" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters45f085ecc03a1a22cf003fba3fab28e660c92bcfcd4d0c01b62c7cd191070a2dS3VersionKey585C4BED" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters45f085ecc03a1a22cf003fba3fab28e660c92bcfcd4d0c01b62c7cd191070a2dS3VersionKey585C4BED" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.main", + "Role": { + "Fn::GetAtt": [ + "MyLambdaServiceRole4539ECB6", + "Arn" + ] + }, + "Runtime": "python3.8" + }, + "DependsOn": [ + "MyLambdaServiceRole4539ECB6" + ] + }, + "MyLambdaCurrentVersionE7A382CC1a5358ec9d2d5ef45baeba2fbb9fa9bd": { + "Type": "AWS::Lambda::Version", + "Properties": { + "FunctionName": { + "Ref": "MyLambdaCCE802FB" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "MyLambdaCurrentVersionEventInvokeConfigD120DC68": { + "Type": "AWS::Lambda::EventInvokeConfig", + "Properties": { + "FunctionName": { + "Ref": "MyLambdaCCE802FB" + }, + "Qualifier": { + "Fn::GetAtt": [ + "MyLambdaCurrentVersionE7A382CC1a5358ec9d2d5ef45baeba2fbb9fa9bd", + "Version" + ] + }, + "MaximumRetryAttempts": 1 + } + }, + "MyLambdaCurrentVersionAliaslive9151E913": { + "Type": "AWS::Lambda::Alias", + "Properties": { + "FunctionName": { + "Ref": "MyLambdaCCE802FB" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "MyLambdaCurrentVersionE7A382CC1a5358ec9d2d5ef45baeba2fbb9fa9bd", + "Version" + ] + }, + "Name": "live" + } + } + }, + "Parameters": { + "AssetParameters45f085ecc03a1a22cf003fba3fab28e660c92bcfcd4d0c01b62c7cd191070a2dS3Bucket34E3DBD0": { + "Type": "String", + "Description": "S3 bucket for asset \"45f085ecc03a1a22cf003fba3fab28e660c92bcfcd4d0c01b62c7cd191070a2d\"" + }, + "AssetParameters45f085ecc03a1a22cf003fba3fab28e660c92bcfcd4d0c01b62c7cd191070a2dS3VersionKey585C4BED": { + "Type": "String", + "Description": "S3 key for asset version \"45f085ecc03a1a22cf003fba3fab28e660c92bcfcd4d0c01b62c7cd191070a2d\"" + }, + "AssetParameters45f085ecc03a1a22cf003fba3fab28e660c92bcfcd4d0c01b62c7cd191070a2dArtifactHash20CDD3D4": { + "Type": "String", + "Description": "Artifact hash for asset \"45f085ecc03a1a22cf003fba3fab28e660c92bcfcd4d0c01b62c7cd191070a2d\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.current-version.ts b/packages/@aws-cdk/aws-lambda/test/integ.current-version.ts new file mode 100644 index 0000000000000..4a37f486e53f8 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.current-version.ts @@ -0,0 +1,27 @@ +import { App, RemovalPolicy, Stack } from '@aws-cdk/core'; +import * as path from 'path'; +import * as lambda from '../lib'; + +class TestStack extends Stack { + constructor(scope: App, id: string) { + super(scope, id); + + const handler = new lambda.Function(this, 'MyLambda', { + code: lambda.Code.fromAsset(path.join(__dirname, 'layer-code')), + handler: 'index.main', + runtime: lambda.Runtime.PYTHON_3_8, + currentVersionOptions: { + removalPolicy: RemovalPolicy.RETAIN, + retryAttempts: 1 + } + }); + + handler.currentVersion.addAlias('live'); + } +} + +const app = new App(); + +new TestStack(app, 'lambda-test-current-version'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-lambda/test/test.function-hash.ts b/packages/@aws-cdk/aws-lambda/test/test.function-hash.ts new file mode 100644 index 0000000000000..651884b83e575 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/test.function-hash.ts @@ -0,0 +1,194 @@ +import { CfnOutput, Stack } from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as path from 'path'; +import * as lambda from '../lib'; +import { calculateFunctionHash, trimFromStart } from '../lib/function-hash'; + +export = { + "trimFromStart": { + + 'trim not needed'(test: Test) { + test.deepEqual(trimFromStart('foo', 100), 'foo'); + test.deepEqual(trimFromStart('foo', 3), 'foo'); + test.deepEqual(trimFromStart('', 3), ''); + test.done(); + }, + + 'trim required'(test: Test) { + test.deepEqual(trimFromStart('hello', 3), 'llo'); + test.deepEqual(trimFromStart('hello', 4), 'ello'); + test.deepEqual(trimFromStart('hello', 1), 'o'); + test.done(); + } + + }, + + "calcHash": { + 'same configuration and code yields the same hash'(test: Test) { + const stack1 = new Stack(); + const fn1 = new lambda.Function(stack1, 'MyFunction1', { + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromAsset(path.join(__dirname, 'handler.zip')), + handler: 'index.handler' + }); + + const stack2 = new Stack(); + const fn2 = new lambda.Function(stack2, 'MyFunction1', { + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromAsset(path.join(__dirname, 'handler.zip')), + handler: 'index.handler' + }); + + test.deepEqual(calculateFunctionHash(fn1), calculateFunctionHash(fn2)); + test.deepEqual(calculateFunctionHash(fn1), 'aea5463dba236007afe91d2832b3c836'); + test.done(); + }, + }, + + 'code impacts hash'(test: Test) { + const stack1 = new Stack(); + const fn1 = new lambda.Function(stack1, 'MyFunction1', { + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromAsset(path.join(__dirname, 'my-lambda-handler')), + handler: 'index.handler' + }); + + test.notDeepEqual(calculateFunctionHash(fn1), 'aea5463dba236007afe91d2832b3c836'); + test.deepEqual(calculateFunctionHash(fn1), '979b4a14c6f174c745cdbcd1036cf844'); + test.done(); + }, + + 'environment variables impact hash'(test: Test) { + const stack1 = new Stack(); + const fn1 = new lambda.Function(stack1, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromAsset(path.join(__dirname, 'my-lambda-handler')), + handler: 'index.handler', + environment: { + Foo: 'bar' + } + }); + + const stack2 = new Stack(); + const fn2 = new lambda.Function(stack2, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromAsset(path.join(__dirname, 'my-lambda-handler')), + handler: 'index.handler', + environment: { + Foo: 'beer' + } + }); + + test.deepEqual(calculateFunctionHash(fn1), 'd1bc824ac5022b7d62d8b12dbae6580c'); + test.deepEqual(calculateFunctionHash(fn2), '3b683d05465012b0aa9c4ff53b32f014'); + test.done(); + }, + + 'runtime impacts hash'(test: Test) { + const stack1 = new Stack(); + const fn1 = new lambda.Function(stack1, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromAsset(path.join(__dirname, 'my-lambda-handler')), + handler: 'index.handler', + environment: { + Foo: 'bar' + } + }); + + const stack2 = new Stack(); + const fn2 = new lambda.Function(stack2, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_10_X, + code: lambda.Code.fromAsset(path.join(__dirname, 'my-lambda-handler')), + handler: 'index.handler', + environment: { + Foo: 'beer' + } + }); + + test.deepEqual(calculateFunctionHash(fn1), 'd1bc824ac5022b7d62d8b12dbae6580c'); + test.deepEqual(calculateFunctionHash(fn2), '0f168f0772463e8e547bb3800937e54d'); + test.done(); + }, + + 'inline code change impacts the hash'(test: Test) { + const stack1 = new Stack(); + const fn1 = new lambda.Function(stack1, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromInline('foo'), + handler: 'index.handler', + }); + + const stack2 = new Stack(); + const fn2 = new lambda.Function(stack2, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_10_X, + code: lambda.Code.fromInline('foo bar'), + handler: 'index.handler', + }); + + test.deepEqual(calculateFunctionHash(fn1), 'ebf2e871fc6a3062e8bdcc5ebe16db3f'); + test.deepEqual(calculateFunctionHash(fn2), 'ffedf6424a18a594a513129dc97bf53c'); + test.done(); + }, + + 'impact of env variables order on hash': { + + 'without "currentVersion", we preserve old behavior to avoid unnesesary invalidation of templates'(test: Test) { + const stack1 = new Stack(); + const fn1 = new lambda.Function(stack1, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromAsset(path.join(__dirname, 'my-lambda-handler')), + handler: 'index.handler', + environment: { + Foo: 'bar', + Bar: 'foo', + } + }); + + const stack2 = new Stack(); + const fn2 = new lambda.Function(stack2, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromAsset(path.join(__dirname, 'my-lambda-handler')), + handler: 'index.handler', + environment: { + Bar: 'foo', + Foo: 'bar', + } + }); + + test.notDeepEqual(calculateFunctionHash(fn1), calculateFunctionHash(fn2)); + test.done(); + }, + + 'with "currentVersion", we sort env keys so order is consistent'(test: Test) { + const stack1 = new Stack(); + const fn1 = new lambda.Function(stack1, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromAsset(path.join(__dirname, 'my-lambda-handler')), + handler: 'index.handler', + environment: { + Foo: 'bar', + Bar: 'foo', + } + }); + + new CfnOutput(stack1, 'VersionArn', { value: fn1.currentVersion.functionArn }); + + const stack2 = new Stack(); + const fn2 = new lambda.Function(stack2, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromAsset(path.join(__dirname, 'my-lambda-handler')), + handler: 'index.handler', + environment: { + Bar: 'foo', + Foo: 'bar', + } + }); + + new CfnOutput(stack2, 'VersionArn', { value: fn2.currentVersion.functionArn }); + + test.deepEqual(calculateFunctionHash(fn1), calculateFunctionHash(fn2)); + test.done(); + } + + }, +}; diff --git a/packages/@aws-cdk/aws-lambda/test/test.function.ts b/packages/@aws-cdk/aws-lambda/test/test.function.ts index 6a9e2fa343090..fd9316c822b54 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.function.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.function.ts @@ -1,9 +1,11 @@ +import { expect, haveOutput } from '@aws-cdk/assert'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as sqs from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; import * as _ from 'lodash'; import {Test, testCase} from 'nodeunit'; +import * as path from 'path'; import * as lambda from '../lib'; export = testCase({ @@ -178,5 +180,55 @@ export = testCase({ code: lambda.Code.fromInline('foo') }), /Inline source not allowed for/); test.done(); - } + }, + + 'currentVersion': { + + // see test.function-hash.ts for more coverage for this + 'logical id of version is based on the function hash'(test: Test) { + // GIVEN + const stack1 = new cdk.Stack(); + const fn1 = new lambda.Function(stack1, 'MyFunction', { + handler: 'foo', + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromAsset(path.join(__dirname, 'handler.zip')), + environment: { + FOO: 'bar' + } + }); + const stack2 = new cdk.Stack(); + const fn2 = new lambda.Function(stack2, 'MyFunction', { + handler: 'foo', + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromAsset(path.join(__dirname, 'handler.zip')), + environment: { + FOO: 'bear' + } + }); + + // WHEN + new cdk.CfnOutput(stack1, 'CurrentVersionArn', { + value: fn1.currentVersion.functionArn + }); + new cdk.CfnOutput(stack2, 'CurrentVersionArn', { + value: fn2.currentVersion.functionArn + }); + + // THEN + expect(stack1).to(haveOutput({ + outputName: 'CurrentVersionArn', + outputValue: { + Ref: "MyFunctionCurrentVersion197490AF1a9a73cf5c46aec5e40fb202042eb60b" + } + })); + expect(stack2).to(haveOutput({ + outputName: 'CurrentVersionArn', + outputValue: { + Ref: "MyFunctionCurrentVersion197490AF8360a045031060e3117269037b7bffd6" + } + })); + test.done(); + } + }, + }); diff --git a/packages/@aws-cdk/aws-lambda/test/test.lambda-version.ts b/packages/@aws-cdk/aws-lambda/test/test.lambda-version.ts index 80c02e56c9966..b667e85e2b779 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.lambda-version.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.lambda-version.ts @@ -111,6 +111,35 @@ export = { MaximumRetryAttempts: 0 })); + test.done(); + }, + + 'addAlias can be used to add an alias that points to a version'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'Fn', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.fromInline('foo'), + }); + const version = fn.currentVersion; + + // WHEN + version.addAlias('foo'); + + // THEN + expect(stack).to(haveResource('AWS::Lambda::Alias', { + "FunctionName": { + "Ref": "Fn9270CBC0" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FnCurrentVersion17A89ABB19ed45993ff69fd011ae9fd4ab6e2005", + "Version" + ] + }, + "Name": "foo" + })); test.done(); } };