diff --git a/packages/@aws-cdk/aws-lambda-python/README.md b/packages/@aws-cdk/aws-lambda-python/README.md index 8f0387cb2e430..97229dc225fe3 100644 --- a/packages/@aws-cdk/aws-lambda-python/README.md +++ b/packages/@aws-cdk/aws-lambda-python/README.md @@ -32,11 +32,38 @@ All other properties of `lambda.Function` are supported, see also the [AWS Lambd ### Module Dependencies -If `requirements.txt` exists at the entry path, the construct will handle installing +If `requirements.txt` or `Pipfile` exists at the entry path, the construct will handle installing all required modules in a [Lambda compatible Docker container](https://hub.docker.com/r/amazon/aws-sam-cli-build-image-python3.7) according to the `runtime`. + +**Lambda with a requirements.txt** ``` . ├── lambda_function.py # exports a function named 'handler' ├── requirements.txt # has to be present at the entry path ``` + +**Lambda with a Pipfile** +``` +. +├── lambda_function.py # exports a function named 'handler' +├── Pipfile # has to be present at the entry path +├── Pipfile.lock # your lock file +``` + +**Lambda Layer Support** + +You may create a python-based lambda layer with `PythonLayerVersion`. If `PythonLayerVersion` detects a `requirements.txt` +or `Pipfile` at the entry path, then `PythonLayerVersion` will include the dependencies inline with your code in the +layer. + +```ts +new lambda.PythonFunction(this, 'MyFunction', { + entry: '/path/to/my/function', + layers: [ + new lambda.PythonLayerVersion(this, 'MyLayer', { + entry: '/path/to/my/layer', // point this to your library's directory + }), + ], +}); +``` diff --git a/packages/@aws-cdk/aws-lambda-python/lib/Dockerfile.dependencies b/packages/@aws-cdk/aws-lambda-python/lib/Dockerfile.dependencies new file mode 100644 index 0000000000000..4d02d028b187b --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/lib/Dockerfile.dependencies @@ -0,0 +1,18 @@ +# The correct AWS SAM build image based on the runtime of the function will be +# passed as build arg. The default allows to do `docker build .` when testing. +ARG IMAGE=amazon/aws-sam-cli-build-image-python3.7 +FROM $IMAGE + +# Ensure rsync is installed +RUN yum -q list installed rsync &>/dev/null || yum install -y rsync + +# Install pipenv so we can create a requirements.txt if we detect pipfile +RUN pip install pipenv + +# Install the dependencies in a cacheable layer +WORKDIR /var/dependencies +COPY Pipfile* requirements.tx[t] ./ +RUN [ -f 'Pipfile' ] && pipenv lock -r >requirements.txt; \ + [ -f 'requirements.txt' ] && pip install -r requirements.txt -t .; + +CMD [ "python" ] diff --git a/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts index 15d3126c48067..937d420397773 100644 --- a/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts +++ b/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts @@ -3,6 +3,16 @@ import * as path from 'path'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; +/** + * Dependency files to exclude from the asset hash. + */ +export const DEPENDENCY_EXCLUDES = ['*.pyc']; + +/** + * The location in the image that the bundler image caches dependencies. + */ +export const BUNDLER_DEPENDENCIES_CACHE = '/var/dependencies'; + /** * Options for bundling */ @@ -16,29 +26,44 @@ export interface BundlingOptions { * The runtime of the lambda function */ readonly runtime: lambda.Runtime; + + /** + * Output path suffix ('python' for a layer, '.' otherwise) + */ + readonly outputPathSuffix: string; } /** * Produce bundled Lambda asset code */ export function bundle(options: BundlingOptions): lambda.AssetCode { - // Bundling image derived from runtime bundling image (AWS SAM docker image) - const image = cdk.BundlingDockerImage.fromAsset(__dirname, { - buildArgs: { - IMAGE: options.runtime.bundlingDockerImage.image, - }, - }); - - let installer = options.runtime === lambda.Runtime.PYTHON_2_7 ? Installer.PIP : Installer.PIP3; + const { entry, runtime, outputPathSuffix } = options; - let hasRequirements = fs.existsSync(path.join(options.entry, 'requirements.txt')); + const hasDeps = hasDependencies(entry); - let depsCommand = chain([ - hasRequirements ? `${installer} install -r requirements.txt -t ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}` : '', - `rsync -r . ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}`, + const depsCommand = chain([ + hasDeps ? `rsync -r ${BUNDLER_DEPENDENCIES_CACHE}/. ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}/${outputPathSuffix}` : '', + `rsync -r . ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}/${outputPathSuffix}`, ]); - return lambda.Code.fromAsset(options.entry, { + // Determine which dockerfile to use. When dependencies are present, we use a + // Dockerfile that can create a cacheable layer. We can't use this Dockerfile + // if there aren't dependencies or the Dockerfile will complain about missing + // sources. + const dockerfile = hasDeps + ? 'Dockerfile.dependencies' + : 'Dockerfile'; + + const image = cdk.BundlingDockerImage.fromAsset(entry, { + buildArgs: { + IMAGE: runtime.bundlingDockerImage.image, + }, + file: path.join(__dirname, dockerfile), + }); + + return lambda.Code.fromAsset(entry, { + assetHashType: cdk.AssetHashType.BUNDLE, + exclude: DEPENDENCY_EXCLUDES, bundling: { image, command: ['bash', '-c', depsCommand], @@ -46,9 +71,20 @@ export function bundle(options: BundlingOptions): lambda.AssetCode { }); } -enum Installer { - PIP = 'pip', - PIP3 = 'pip3', +/** + * Checks to see if the `entry` directory contains a type of dependency that + * we know how to install. + */ +export function hasDependencies(entry: string): boolean { + if (fs.existsSync(path.join(entry, 'Pipfile'))) { + return true; + } + + if (fs.existsSync(path.join(entry, 'requirements.txt'))) { + return true; + } + + return false; } function chain(commands: string[]): string { diff --git a/packages/@aws-cdk/aws-lambda-python/lib/function.ts b/packages/@aws-cdk/aws-lambda-python/lib/function.ts index 7b3fbb1d71fe8..77f794704e967 100644 --- a/packages/@aws-cdk/aws-lambda-python/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda-python/lib/function.ts @@ -64,9 +64,9 @@ export class PythonFunction extends lambda.Function { ...props, runtime, code: bundle({ - ...props, - entry, runtime, + entry, + outputPathSuffix: '.', }), handler: `${index.slice(0, -3)}.${handler}`, }); diff --git a/packages/@aws-cdk/aws-lambda-python/lib/index.ts b/packages/@aws-cdk/aws-lambda-python/lib/index.ts index 2653adb2a89e8..5459e812abb91 100644 --- a/packages/@aws-cdk/aws-lambda-python/lib/index.ts +++ b/packages/@aws-cdk/aws-lambda-python/lib/index.ts @@ -1 +1,2 @@ export * from './function'; +export * from './layer'; diff --git a/packages/@aws-cdk/aws-lambda-python/lib/layer.ts b/packages/@aws-cdk/aws-lambda-python/lib/layer.ts new file mode 100644 index 0000000000000..8b090eed6a989 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/lib/layer.ts @@ -0,0 +1,54 @@ +import * as path from 'path'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from '@aws-cdk/core'; +import { bundle } from './bundling'; + +/** + * Properties for PythonLayerVersion + */ +export interface PythonLayerVersionProps extends lambda.LayerVersionOptions { + /** + * The path to the root directory of the lambda layer. + */ + readonly entry: string; + + /** + * The runtimes compatible with the python layer. + * + * @default - All runtimes are supported. + */ + readonly compatibleRuntimes?: lambda.Runtime[]; +} + +/** + * A lambda layer version. + * + * @experimental + */ +export class PythonLayerVersion extends lambda.LayerVersion { + constructor(scope: cdk.Construct, id: string, props: PythonLayerVersionProps) { + const compatibleRuntimes = props.compatibleRuntimes ?? [lambda.Runtime.PYTHON_3_7]; + + // Ensure that all compatible runtimes are python + for (const runtime of compatibleRuntimes) { + if (runtime && runtime.family !== lambda.RuntimeFamily.PYTHON) { + throw new Error('Only `PYTHON` runtimes are supported.'); + } + } + + // Entry and defaults + const entry = path.resolve(props.entry); + // Pick the first compatibleRuntime to use for bundling or PYTHON_3_7 + const runtime = compatibleRuntimes[0] ?? lambda.Runtime.PYTHON_3_7; + + super(scope, id, { + ...props, + compatibleRuntimes, + code: bundle({ + entry, + runtime, + outputPathSuffix: 'python', + }), + }); + } +} diff --git a/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts b/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts index 492f7b3dbd890..ca415f60e5476 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts @@ -1,79 +1,158 @@ import * as fs from 'fs'; +import * as path from 'path'; import { Code, Runtime } from '@aws-cdk/aws-lambda'; -import { bundle } from '../lib/bundling'; +import { hasDependencies, bundle } from '../lib/bundling'; jest.mock('@aws-cdk/aws-lambda'); const existsSyncOriginal = fs.existsSync; const existsSyncMock = jest.spyOn(fs, 'existsSync'); +jest.mock('child_process', () => ({ + spawnSync: jest.fn(() => { + return { + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('sha256:1234567890abcdef'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }; + }), +})); + beforeEach(() => { jest.clearAllMocks(); }); -test('Bundling', () => { +test('Bundling a function without dependencies', () => { + const entry = path.join(__dirname, 'lambda-handler-nodeps'); bundle({ - entry: '/project/folder', + entry: entry, runtime: Runtime.PYTHON_3_7, + outputPathSuffix: '.', }); // Correctly bundles - expect(Code.fromAsset).toHaveBeenCalledWith('/project/folder', { + expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({ bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'rsync -r . /asset-output', + 'rsync -r . /asset-output/.', ], }), - }); + })); // Searches for requirements.txt in entry - expect(existsSyncMock).toHaveBeenCalledWith('/project/folder/requirements.txt'); + expect(existsSyncMock).toHaveBeenCalledWith(path.join(entry, 'requirements.txt')); }); -test('Bundling with requirements.txt installed', () => { - existsSyncMock.mockImplementation((p: fs.PathLike) => { - if (/requirements.txt/.test(p.toString())) { - return true; - } - return existsSyncOriginal(p); +test('Bundling a function with requirements.txt installed', () => { + const entry = path.join(__dirname, 'lambda-handler'); + bundle({ + entry: entry, + runtime: Runtime.PYTHON_3_7, + outputPathSuffix: '.', }); + // Correctly bundles + expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({ + bundling: expect.objectContaining({ + command: [ + 'bash', '-c', + 'rsync -r /var/dependencies/. /asset-output/. && rsync -r . /asset-output/.', + ], + }), + })); + + // Searches for requirements.txt in entry + expect(existsSyncMock).toHaveBeenCalledWith(path.join(entry, 'requirements.txt')); +}); + +test('Bundling Python 2.7 with requirements.txt installed', () => { + const entry = path.join(__dirname, 'lambda-handler'); bundle({ - entry: '/project/folder', - runtime: Runtime.PYTHON_3_7, + entry: entry, + runtime: Runtime.PYTHON_2_7, + outputPathSuffix: '.', }); // Correctly bundles with requirements.txt pip installed - expect(Code.fromAsset).toHaveBeenCalledWith('/project/folder', { + expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({ bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'pip3 install -r requirements.txt -t /asset-output && rsync -r . /asset-output', + 'rsync -r /var/dependencies/. /asset-output/. && rsync -r . /asset-output/.', ], }), - }); + })); + + // Searches for requirements.txt in entry + expect(existsSyncMock).toHaveBeenCalledWith(path.join(entry, 'requirements.txt')); }); -test('Bundling Python 2.7 with requirements.txt installed', () => { - existsSyncMock.mockImplementation((p: fs.PathLike) => { - if (/requirements.txt/.test(p.toString())) { - return true; - } - return existsSyncOriginal(p); +test('Bundling a layer with dependencies', () => { + const entry = path.join(__dirname, 'lambda-handler'); + + bundle({ + entry: entry, + runtime: Runtime.PYTHON_2_7, + outputPathSuffix: 'python', }); + expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({ + bundling: expect.objectContaining({ + command: [ + 'bash', '-c', + 'rsync -r /var/dependencies/. /asset-output/python && rsync -r . /asset-output/python', + ], + }), + })); +}); + +test('Bundling a python code layer', () => { + const entry = path.join(__dirname, 'lambda-handler-nodeps'); + bundle({ - entry: '/project/folder', + entry: path.join(entry, '.'), runtime: Runtime.PYTHON_2_7, + outputPathSuffix: 'python', }); - // Correctly bundles with requirements.txt pip installed - expect(Code.fromAsset).toHaveBeenCalledWith('/project/folder', { + expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({ bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'pip install -r requirements.txt -t /asset-output && rsync -r . /asset-output', + 'rsync -r . /asset-output/python', ], }), + })); +}); + +describe('Dependency detection', () => { + test('Detects pipenv', () => { + existsSyncMock.mockImplementation((p: fs.PathLike) => { + if (/Pipfile/.test(p.toString())) { + return true; + } + return existsSyncOriginal(p); + }); + + expect(hasDependencies('/asset-input')).toEqual(true); + }); + + test('Detects requirements.txt', () => { + existsSyncMock.mockImplementation((p: fs.PathLike) => { + if (/requirements.txt/.test(p.toString())) { + return true; + } + return existsSyncOriginal(p); + }); + + expect(hasDependencies('/asset-input')).toEqual(true); + }); + + test('No known dependencies', () => { + existsSyncMock.mockImplementation(() => false); + expect(hasDependencies('/asset-input')).toEqual(false); }); }); diff --git a/packages/@aws-cdk/aws-lambda-python/test/function.test.ts b/packages/@aws-cdk/aws-lambda-python/test/function.test.ts index 9bc69a7757946..98bab1cf35be4 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/function.test.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/function.test.ts @@ -12,6 +12,7 @@ jest.mock('../lib/bundling', () => { }, bindToResource: () => { return; }, }), + hasDependencies: jest.fn().mockReturnValue(false), }; }); @@ -28,6 +29,7 @@ test('PythonFunction with defaults', () => { expect(bundle).toHaveBeenCalledWith(expect.objectContaining({ entry: expect.stringMatching(/@aws-cdk\/aws-lambda-python\/test\/lambda-handler$/), + outputPathSuffix: '.', })); expect(stack).toHaveResource('AWS::Lambda::Function', { @@ -44,6 +46,7 @@ test('PythonFunction with index in a subdirectory', () => { expect(bundle).toHaveBeenCalledWith(expect.objectContaining({ entry: expect.stringMatching(/@aws-cdk\/aws-lambda-python\/test\/lambda-handler-sub$/), + outputPathSuffix: '.', })); expect(stack).toHaveResource('AWS::Lambda::Function', { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json index ba3b9d456ca11..c75736f461667 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3Bucket5C76F19D" + "Ref": "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3Bucket40BC544C" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3VersionKey374DFF5D" + "Ref": "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3VersionKeyDD20BCAA" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3VersionKey374DFF5D" + "Ref": "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3VersionKeyDD20BCAA" } ] } @@ -87,17 +87,17 @@ } }, "Parameters": { - "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3Bucket5C76F19D": { + "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3Bucket40BC544C": { "Type": "String", - "Description": "S3 bucket for asset \"cc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25\"" + "Description": "S3 bucket for asset \"9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0e\"" }, - "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3VersionKey374DFF5D": { + "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3VersionKeyDD20BCAA": { "Type": "String", - "Description": "S3 key for asset version \"cc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25\"" + "Description": "S3 key for asset version \"9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0e\"" }, - "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25ArtifactHashB15DA742": { + "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eArtifactHash5F92CE57": { "Type": "String", - "Description": "Artifact hash for asset \"cc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25\"" + "Description": "Artifact hash for asset \"9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0e\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv-inline.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv-inline.expected.json new file mode 100644 index 0000000000000..19cc6231599e4 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv-inline.expected.json @@ -0,0 +1,110 @@ +{ + "Resources": { + "myhandlerServiceRole77891068": { + "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" + ] + ] + } + ] + } + }, + "myhandlerD202FA8E": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3BucketDF70124D" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3VersionKey530C68B0" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3VersionKey530C68B0" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "myhandlerServiceRole77891068", + "Arn" + ] + }, + "Runtime": "python3.6" + }, + "DependsOn": [ + "myhandlerServiceRole77891068" + ] + } + }, + "Parameters": { + "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3BucketDF70124D": { + "Type": "String", + "Description": "S3 bucket for asset \"eef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255b\"" + }, + "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3VersionKey530C68B0": { + "Type": "String", + "Description": "S3 key for asset version \"eef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255b\"" + }, + "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bArtifactHashEE8E0CE9": { + "Type": "String", + "Description": "Artifact hash for asset \"eef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255b\"" + } + }, + "Outputs": { + "FunctionName": { + "Value": { + "Ref": "myhandlerD202FA8E" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv-inline.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv-inline.ts new file mode 100644 index 0000000000000..17b17070e56e8 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv-inline.ts @@ -0,0 +1,29 @@ +import * as path from 'path'; +import { Runtime } from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as lambda from '../lib'; + +/* + * Stack verification steps: + * aws lambda invoke --function-name --invocation-type Event --payload $(base64 <<<'"OK"') response.json + */ + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const fn = new lambda.PythonFunction(this, 'my_handler', { + entry: path.join(__dirname, 'lambda-handler-pipenv'), + runtime: Runtime.PYTHON_3_6, + }); + + new CfnOutput(this, 'FunctionName', { + value: fn.functionName, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-lambda-python-inline'); +app.synth(); diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py27.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py27.expected.json new file mode 100644 index 0000000000000..c0cb2b260d146 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py27.expected.json @@ -0,0 +1,110 @@ +{ + "Resources": { + "myhandlerServiceRole77891068": { + "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" + ] + ] + } + ] + } + }, + "myhandlerD202FA8E": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3BucketB5A59BD8" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3VersionKey7657015C" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3VersionKey7657015C" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "myhandlerServiceRole77891068", + "Arn" + ] + }, + "Runtime": "python2.7" + }, + "DependsOn": [ + "myhandlerServiceRole77891068" + ] + } + }, + "Parameters": { + "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3BucketB5A59BD8": { + "Type": "String", + "Description": "S3 bucket for asset \"f37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014\"" + }, + "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3VersionKey7657015C": { + "Type": "String", + "Description": "S3 key for asset version \"f37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014\"" + }, + "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014ArtifactHash7768674B": { + "Type": "String", + "Description": "Artifact hash for asset \"f37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014\"" + } + }, + "Outputs": { + "FunctionName": { + "Value": { + "Ref": "myhandlerD202FA8E" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py27.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py27.ts new file mode 100644 index 0000000000000..68f2b2f18f4b4 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py27.ts @@ -0,0 +1,29 @@ +import * as path from 'path'; +import { Runtime } from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as lambda from '../lib'; + +/* + * Stack verification steps: + * aws lambda invoke --function-name --invocation-type Event --payload $(base64 <<<'"OK"') response.json + */ + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const fn = new lambda.PythonFunction(this, 'my_handler', { + entry: path.join(__dirname, 'lambda-handler-pipenv'), + runtime: Runtime.PYTHON_2_7, + }); + + new CfnOutput(this, 'FunctionName', { + value: fn.functionName, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-lambda-python-pipenv-py27'); +app.synth(); diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py38.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py38.expected.json new file mode 100644 index 0000000000000..07e4ea8cf45f9 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py38.expected.json @@ -0,0 +1,110 @@ +{ + "Resources": { + "myhandlerServiceRole77891068": { + "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" + ] + ] + } + ] + } + }, + "myhandlerD202FA8E": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3Bucket31144813" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3VersionKeyB48E8383" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3VersionKeyB48E8383" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "myhandlerServiceRole77891068", + "Arn" + ] + }, + "Runtime": "python3.8" + }, + "DependsOn": [ + "myhandlerServiceRole77891068" + ] + } + }, + "Parameters": { + "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3Bucket31144813": { + "Type": "String", + "Description": "S3 bucket for asset \"3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846a\"" + }, + "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3VersionKeyB48E8383": { + "Type": "String", + "Description": "S3 key for asset version \"3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846a\"" + }, + "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aArtifactHash652F614E": { + "Type": "String", + "Description": "Artifact hash for asset \"3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846a\"" + } + }, + "Outputs": { + "FunctionName": { + "Value": { + "Ref": "myhandlerD202FA8E" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py38.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py38.ts new file mode 100644 index 0000000000000..619dd270ec206 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py38.ts @@ -0,0 +1,29 @@ +import * as path from 'path'; +import { Runtime } from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as lambda from '../lib'; + +/* + * Stack verification steps: + * aws lambda invoke --function-name --invocation-type Event --payload $(base64 <<<'"OK"') response.json + */ + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const fn = new lambda.PythonFunction(this, 'my_handler', { + entry: path.join(__dirname, 'lambda-handler-pipenv'), + runtime: Runtime.PYTHON_3_8, + }); + + new CfnOutput(this, 'FunctionName', { + value: fn.functionName, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-lambda-python-pipenv-py38'); +app.synth(); diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.expected.json new file mode 100644 index 0000000000000..bbf2630c2be48 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.expected.json @@ -0,0 +1,176 @@ +{ + "Resources": { + "SharedDACC02AA": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": { + "S3Bucket": { + "Ref": "AssetParameters83e524c4077f05f723f2fd9068d80291a91fda4706ae5f4e84a1462e99e83e71S3BucketD683DA42" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters83e524c4077f05f723f2fd9068d80291a91fda4706ae5f4e84a1462e99e83e71S3VersionKeyA0299125" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters83e524c4077f05f723f2fd9068d80291a91fda4706ae5f4e84a1462e99e83e71S3VersionKeyA0299125" + } + ] + } + ] + } + ] + ] + } + }, + "CompatibleRuntimes": [ + "python3.6" + ] + } + }, + "myhandlerServiceRole77891068": { + "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" + ] + ] + } + ] + } + }, + "myhandlerD202FA8E": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3Bucket89C9DB12" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3VersionKey435DAD55" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3VersionKey435DAD55" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "myhandlerServiceRole77891068", + "Arn" + ] + }, + "Runtime": "python3.6", + "Layers": [ + { + "Ref": "SharedDACC02AA" + } + ] + }, + "DependsOn": [ + "myhandlerServiceRole77891068" + ] + } + }, + "Parameters": { + "AssetParameters83e524c4077f05f723f2fd9068d80291a91fda4706ae5f4e84a1462e99e83e71S3BucketD683DA42": { + "Type": "String", + "Description": "S3 bucket for asset \"83e524c4077f05f723f2fd9068d80291a91fda4706ae5f4e84a1462e99e83e71\"" + }, + "AssetParameters83e524c4077f05f723f2fd9068d80291a91fda4706ae5f4e84a1462e99e83e71S3VersionKeyA0299125": { + "Type": "String", + "Description": "S3 key for asset version \"83e524c4077f05f723f2fd9068d80291a91fda4706ae5f4e84a1462e99e83e71\"" + }, + "AssetParameters83e524c4077f05f723f2fd9068d80291a91fda4706ae5f4e84a1462e99e83e71ArtifactHash8CBB58BE": { + "Type": "String", + "Description": "Artifact hash for asset \"83e524c4077f05f723f2fd9068d80291a91fda4706ae5f4e84a1462e99e83e71\"" + }, + "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3Bucket89C9DB12": { + "Type": "String", + "Description": "S3 bucket for asset \"71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218\"" + }, + "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3VersionKey435DAD55": { + "Type": "String", + "Description": "S3 key for asset version \"71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218\"" + }, + "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218ArtifactHash0EDF3CD0": { + "Type": "String", + "Description": "Artifact hash for asset \"71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218\"" + } + }, + "Outputs": { + "FunctionArn": { + "Value": { + "Fn::GetAtt": [ + "myhandlerD202FA8E", + "Arn" + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.ts new file mode 100644 index 0000000000000..1259ba09bdfe1 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.ts @@ -0,0 +1,36 @@ +import * as path from 'path'; +import { Runtime } from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as lambda from '../lib'; + +/* + * Stack verification steps: + * * aws lambda invoke --function-name --invocation-type Event --payload '"OK"' response.json + */ + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const projectDirectory = path.join(__dirname, 'lambda-handler-project'); + const fn = new lambda.PythonFunction(this, 'my_handler', { + entry: path.join(projectDirectory, 'lambda'), + runtime: Runtime.PYTHON_3_6, + layers: [ + new lambda.PythonLayerVersion(this, 'Shared', { + entry: path.join(projectDirectory, 'shared'), + compatibleRuntimes: [Runtime.PYTHON_3_6], + }), + ], + }); + + new CfnOutput(this, 'FunctionArn', { + value: fn.functionArn, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-lambda-function-project'); +app.synth(); diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json index ff95a0ef74c51..fe005754fcfab 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters7173f882be924a92ff6019021219c38f84fdf7f12205fc7558dab5c2fa03d134S3Bucket4FDAE558" + "Ref": "AssetParameters58da14e3fe62df26811cb369b51aa83cab6a27ea3e638e5952ed6439b9cd2f2fS3Bucket3D9CB240" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters7173f882be924a92ff6019021219c38f84fdf7f12205fc7558dab5c2fa03d134S3VersionKey09B41633" + "Ref": "AssetParameters58da14e3fe62df26811cb369b51aa83cab6a27ea3e638e5952ed6439b9cd2f2fS3VersionKeyA12EF729" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters7173f882be924a92ff6019021219c38f84fdf7f12205fc7558dab5c2fa03d134S3VersionKey09B41633" + "Ref": "AssetParameters58da14e3fe62df26811cb369b51aa83cab6a27ea3e638e5952ed6439b9cd2f2fS3VersionKeyA12EF729" } ] } @@ -87,17 +87,17 @@ } }, "Parameters": { - "AssetParameters7173f882be924a92ff6019021219c38f84fdf7f12205fc7558dab5c2fa03d134S3Bucket4FDAE558": { + "AssetParameters58da14e3fe62df26811cb369b51aa83cab6a27ea3e638e5952ed6439b9cd2f2fS3Bucket3D9CB240": { "Type": "String", - "Description": "S3 bucket for asset \"7173f882be924a92ff6019021219c38f84fdf7f12205fc7558dab5c2fa03d134\"" + "Description": "S3 bucket for asset \"58da14e3fe62df26811cb369b51aa83cab6a27ea3e638e5952ed6439b9cd2f2f\"" }, - "AssetParameters7173f882be924a92ff6019021219c38f84fdf7f12205fc7558dab5c2fa03d134S3VersionKey09B41633": { + "AssetParameters58da14e3fe62df26811cb369b51aa83cab6a27ea3e638e5952ed6439b9cd2f2fS3VersionKeyA12EF729": { "Type": "String", - "Description": "S3 key for asset version \"7173f882be924a92ff6019021219c38f84fdf7f12205fc7558dab5c2fa03d134\"" + "Description": "S3 key for asset version \"58da14e3fe62df26811cb369b51aa83cab6a27ea3e638e5952ed6439b9cd2f2f\"" }, - "AssetParameters7173f882be924a92ff6019021219c38f84fdf7f12205fc7558dab5c2fa03d134ArtifactHash9ED905F8": { + "AssetParameters58da14e3fe62df26811cb369b51aa83cab6a27ea3e638e5952ed6439b9cd2f2fArtifactHash3C7CD7C2": { "Type": "String", - "Description": "Artifact hash for asset \"7173f882be924a92ff6019021219c38f84fdf7f12205fc7558dab5c2fa03d134\"" + "Description": "Artifact hash for asset \"58da14e3fe62df26811cb369b51aa83cab6a27ea3e638e5952ed6439b9cd2f2f\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.expected.json new file mode 100644 index 0000000000000..c7c29700ae02f --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.expected.json @@ -0,0 +1,110 @@ +{ + "Resources": { + "functionServiceRoleEF216095": { + "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" + ] + ] + } + ] + } + }, + "functionF19B1A04": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters07cc0ee6a3f6e318f2dbf8ca3bf77166e807eecb5c1be4caf0d554e5d329c690S3BucketC1F94008" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters07cc0ee6a3f6e318f2dbf8ca3bf77166e807eecb5c1be4caf0d554e5d329c690S3VersionKeyE1B3B5F5" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters07cc0ee6a3f6e318f2dbf8ca3bf77166e807eecb5c1be4caf0d554e5d329c690S3VersionKeyE1B3B5F5" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "functionServiceRoleEF216095", + "Arn" + ] + }, + "Runtime": "python2.7" + }, + "DependsOn": [ + "functionServiceRoleEF216095" + ] + } + }, + "Parameters": { + "AssetParameters07cc0ee6a3f6e318f2dbf8ca3bf77166e807eecb5c1be4caf0d554e5d329c690S3BucketC1F94008": { + "Type": "String", + "Description": "S3 bucket for asset \"07cc0ee6a3f6e318f2dbf8ca3bf77166e807eecb5c1be4caf0d554e5d329c690\"" + }, + "AssetParameters07cc0ee6a3f6e318f2dbf8ca3bf77166e807eecb5c1be4caf0d554e5d329c690S3VersionKeyE1B3B5F5": { + "Type": "String", + "Description": "S3 key for asset version \"07cc0ee6a3f6e318f2dbf8ca3bf77166e807eecb5c1be4caf0d554e5d329c690\"" + }, + "AssetParameters07cc0ee6a3f6e318f2dbf8ca3bf77166e807eecb5c1be4caf0d554e5d329c690ArtifactHashF0B9DCD1": { + "Type": "String", + "Description": "Artifact hash for asset \"07cc0ee6a3f6e318f2dbf8ca3bf77166e807eecb5c1be4caf0d554e5d329c690\"" + } + }, + "Outputs": { + "Function": { + "Value": { + "Ref": "functionF19B1A04" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.ts new file mode 100644 index 0000000000000..12ee929de9da6 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.ts @@ -0,0 +1,68 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { Runtime } from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Stack, StackProps } from '@aws-cdk/core'; +import { Construct, ConstructOrder } from 'constructs'; +import * as lambda from '../lib'; + +/* + * Stack verification steps: + * aws lambda invoke --function-name --invocation-type Event --payload $(base64 <<<'"OK"') response.json + */ + +class TestStack extends Stack { + public readonly dependenciesAssetHash: string; + + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const fn = new lambda.PythonFunction(this, 'function', { + entry: workDir, + runtime: Runtime.PYTHON_2_7, + }); + + new CfnOutput(this, 'Function', { + value: fn.functionName, + }); + + // Find the asset hash of the dependencies + this.dependenciesAssetHash = (fn.node.findAll(ConstructOrder.POSTORDER) + .find(c => c.node.path.endsWith('Code')) as any) + .assetHash; + } +} + +// This is a special integration test that synths twice to show that docker +// picks up a change in requirements.txt + +// Create a working directory for messing around with requirements.txt +const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-integ')); +fs.copyFileSync(path.join(__dirname, 'lambda-handler', 'index.py'), path.join(workDir, 'index.py')); +const requirementsTxtPath = path.join(workDir, 'requirements.txt'); + +// Write a requirements.txt with an extraneous dependency (colorama) +const beforeDeps = 'requests==2.23.0\npillow==6.2.2\ncolorama==0.4.3\n'; +fs.writeFileSync(requirementsTxtPath, beforeDeps); + +// Synth the first time +const app = new App(); +const stack1 = new TestStack(app, 'cdk-integ-lambda-python-requirements-removed'); +app.synth(); + +// Then, write a requirements.txt without the extraneous dependency and synth again +const afterDeps = 'requests==2.23.0\npillow==6.2.2\n'; +fs.writeFileSync(requirementsTxtPath, afterDeps); + +// Synth the same stack a second time with different requirements.txt contents +const app2 = new App(); +const stack2 = new TestStack(app2, 'cdk-integ-lambda-python-requirements-removed'); +app2.synth(); + +if (!stack1.dependenciesAssetHash || !stack2.dependenciesAssetHash) { + throw new Error('The asset hashes are not both truthy'); +} + +if (stack1.dependenciesAssetHash === stack2.dependenciesAssetHash) { + throw new Error('Removing a dependency did not change the asset hash'); +} diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json index cd0057d2898c2..e65f882540762 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json @@ -296,7 +296,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3Bucket5C76F19D" + "Ref": "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3Bucket40BC544C" }, "S3Key": { "Fn::Join": [ @@ -309,7 +309,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3VersionKey374DFF5D" + "Ref": "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3VersionKeyDD20BCAA" } ] } @@ -322,7 +322,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3VersionKey374DFF5D" + "Ref": "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3VersionKeyDD20BCAA" } ] } @@ -368,17 +368,17 @@ } }, "Parameters": { - "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3Bucket5C76F19D": { + "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3Bucket40BC544C": { "Type": "String", - "Description": "S3 bucket for asset \"cc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25\"" + "Description": "S3 bucket for asset \"9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0e\"" }, - "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25S3VersionKey374DFF5D": { + "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eS3VersionKeyDD20BCAA": { "Type": "String", - "Description": "S3 key for asset version \"cc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25\"" + "Description": "S3 key for asset version \"9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0e\"" }, - "AssetParameterscc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25ArtifactHashB15DA742": { + "AssetParameters9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0eArtifactHash5F92CE57": { "Type": "String", - "Description": "Artifact hash for asset \"cc7e935d2a5f0ec0cfecbc4c12eabb49f6a3e587f58c4a18cf59383a1d656f25\"" + "Description": "Artifact hash for asset \"9ebc366855b6de9384ae1a09604f243626f380e1bd5e7e9826eecded57ea5a0e\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-nodeps/index.py b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-nodeps/index.py new file mode 100644 index 0000000000000..f118d0551afb8 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-nodeps/index.py @@ -0,0 +1,2 @@ +def handler(event, context): + print('No dependencies') diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile new file mode 100644 index 0000000000000..2d72b30bc8be8 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile @@ -0,0 +1,8 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[packages] +requests = "==2.23.0" +pillow = "==6.2.2" diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile.lock b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile.lock new file mode 100644 index 0000000000000..0113c74bf7363 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/Pipfile.lock @@ -0,0 +1,94 @@ +{ + "_meta": { + "hash": { + "sha256": "0f782e44e69391c98999b575a7f93228f06d22716a144d955386b9c6abec2040" + }, + "pipfile-spec": 6, + "requires": {}, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + ], + "version": "==2020.6.20" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" + }, + "pillow": { + "hashes": [ + "sha256:00e0bbe9923adc5cc38a8da7d87d4ce16cde53b8d3bba8886cb928e84522d963", + "sha256:03457e439d073770d88afdd90318382084732a5b98b0eb6f49454746dbaae701", + "sha256:0d5c99f80068f13231ac206bd9b2e80ea357f5cf9ae0fa97fab21e32d5b61065", + "sha256:1a3bc8e1db5af40a81535a62a591fafdb30a8a1b319798ea8052aa65ef8f06d2", + "sha256:2b4a94be53dff02af90760c10a2e3634c3c7703410f38c98154d5ce71fe63d20", + "sha256:3ba7d8f1d962780f86aa747fef0baf3211b80cb13310fff0c375da879c0656d4", + "sha256:3e81485cec47c24f5fb27acb485a4fc97376b2b332ed633867dc68ac3077998c", + "sha256:43ef1cff7ee57f9c8c8e6fa02a62eae9fa23a7e34418c7ce88c0e3fe09d1fb38", + "sha256:4adc3302df4faf77c63ab3a83e1a3e34b94a6a992084f4aa1cb236d1deaf4b39", + "sha256:535e8e0e02c9f1fc2e307256149d6ee8ad3aa9a6e24144b7b6e6fb6126cb0e99", + "sha256:5ccfcb0a34ad9b77ad247c231edb781763198f405a5c8dc1b642449af821fb7f", + "sha256:5dcbbaa3a24d091a64560d3c439a8962866a79a033d40eb1a75f1b3413bfc2bc", + "sha256:6e2a7e74d1a626b817ecb7a28c433b471a395c010b2a1f511f976e9ea4363e64", + "sha256:82859575005408af81b3e9171ae326ff56a69af5439d3fc20e8cb76cd51c8246", + "sha256:834dd023b7f987d6b700ad93dc818098d7eb046bd445e9992b3093c6f9d7a95f", + "sha256:87ef0eca169f7f0bc050b22f05c7e174a65c36d584428431e802c0165c5856ea", + "sha256:900de1fdc93764be13f6b39dc0dd0207d9ff441d87ad7c6e97e49b81987dc0f3", + "sha256:92b83b380f9181cacc994f4c983d95a9c8b00b50bf786c66d235716b526a3332", + "sha256:aa1b0297e352007ec781a33f026afbb062a9a9895bb103c8f49af434b1666880", + "sha256:aa4792ab056f51b49e7d59ce5733155e10a918baf8ce50f64405db23d5627fa2", + "sha256:b72c39585f1837d946bd1a829a4820ccf86e361f28cbf60f5d646f06318b61e2", + "sha256:bb7861e4618a0c06c40a2e509c1bea207eea5fd4320d486e314e00745a402ca5", + "sha256:bc149dab804291a18e1186536519e5e122a2ac1316cb80f506e855a500b1cdd4", + "sha256:c424d35a5259be559b64490d0fd9e03fba81f1ce8e5b66e0a59de97547351d80", + "sha256:cbd5647097dc55e501f459dbac7f1d0402225636deeb9e0a98a8d2df649fc19d", + "sha256:ccf16fe444cc43800eeacd4f4769971200982200a71b1368f49410d0eb769543", + "sha256:d3a98444a00b4643b22b0685dbf9e0ddcaf4ebfd4ea23f84f228adf5a0765bb2", + "sha256:d6b4dc325170bee04ca8292bbd556c6f5398d52c6149ca881e67daf62215426f", + "sha256:db9ff0c251ed066d367f53b64827cc9e18ccea001b986d08c265e53625dab950", + "sha256:e3a797a079ce289e59dbd7eac9ca3bf682d52687f718686857281475b7ca8e6a" + ], + "index": "pypi", + "version": "==6.2.2" + }, + "requests": { + "hashes": [ + "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", + "sha256:5d2d0ffbb515f39417009a46c14256291061ac01ba8f875b90cad137de83beb4", + "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" + ], + "index": "pypi", + "version": "==2.23.0" + }, + "urllib3": { + "hashes": [ + "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", + "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.25.10" + } + }, + "develop": {} +} diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/index.py b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/index.py new file mode 100644 index 0000000000000..c033f37560534 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-pipenv/index.py @@ -0,0 +1,11 @@ +import requests +from PIL import Image + +def handler(event, context): + response = requests.get('https://a0.awsstatic.com/main/images/logos/aws_smile-header-desktop-en-white_59x35.png', stream=True) + img = Image.open(response.raw) + + print(response.status_code) + print(img.size) + + return response.status_code diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/lambda/index.py b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/lambda/index.py new file mode 100644 index 0000000000000..6ac592242c8fb --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/lambda/index.py @@ -0,0 +1,12 @@ +import requests +from PIL import Image +import shared + +def handler(event, context): + response = requests.get(shared.get_url(), stream=True) + img = Image.open(response.raw) + + print(response.status_code) + print(img.size) + + return response.status_code diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/shared/requirements.txt b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/shared/requirements.txt new file mode 100644 index 0000000000000..51c1bfbf03afe --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/shared/requirements.txt @@ -0,0 +1,2 @@ +requests==2.23.0 +pillow==6.2.2 diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/shared/shared.py b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/shared/shared.py new file mode 100644 index 0000000000000..b17623b83b881 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-project/shared/shared.py @@ -0,0 +1,2 @@ +def get_url() -> str: + return 'https://a0.awsstatic.com/main/images/logos/aws_smile-header-desktop-en-white_59x35.png' diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/requirements.txt b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/requirements.txt index 288e2971fb79a..7232ea84ef267 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/requirements.txt +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/requirements.txt @@ -1,2 +1,3 @@ requests==2.23.0 -pillow==7.2.0 +# Pillow 6.x so that python 2.7 and 3.x can both use this fixture +pillow==6.2.2 diff --git a/packages/@aws-cdk/aws-lambda-python/test/layer.test.ts b/packages/@aws-cdk/aws-lambda-python/test/layer.test.ts new file mode 100644 index 0000000000000..8ace2ec3f7a18 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/layer.test.ts @@ -0,0 +1,54 @@ +import '@aws-cdk/assert/jest'; +import * as path from 'path'; +import { Runtime } from '@aws-cdk/aws-lambda'; +import { Stack } from '@aws-cdk/core'; +import { hasDependencies, bundle } from '../lib/bundling'; +import { PythonLayerVersion } from '../lib/layer'; + +jest.mock('../lib/bundling', () => { + return { + bundle: jest.fn().mockReturnValue({ + bind: () => { + return { + s3Location: { + bucketName: 'bucket', + objectKey: 'key', + }, + }; + }, + bindToResource: () => { return; }, + }), + hasDependencies: jest.fn().mockReturnValue(true), + }; +}); + +const hasDependenciesMock = (hasDependencies as jest.Mock); + +let stack: Stack; +beforeEach(() => { + stack = new Stack(); + jest.clearAllMocks(); +}); + +test('Bundling a layer from files', () => { + hasDependenciesMock.mockReturnValue(false); + + const entry = path.join(__dirname, 'test/lambda-handler-project'); + new PythonLayerVersion(stack, 'layer', { + entry, + }); + + expect(bundle).toHaveBeenCalledWith(expect.objectContaining({ + entry, + outputPathSuffix: 'python', + })); +}); + +test('Fails when bundling a layer for a runtime not supported', () => { + expect(() => { + new PythonLayerVersion(stack, 'layer', { + entry: '/some/path', + compatibleRuntimes: [Runtime.PYTHON_2_7, Runtime.NODEJS], + }); + }).toThrow(/PYTHON.*support/); +}); diff --git a/packages/@aws-cdk/aws-lambda/lib/layers.ts b/packages/@aws-cdk/aws-lambda/lib/layers.ts index 12a92140b511a..4a58d5ce501ef 100644 --- a/packages/@aws-cdk/aws-lambda/lib/layers.ts +++ b/packages/@aws-cdk/aws-lambda/lib/layers.ts @@ -4,21 +4,10 @@ import { Code } from './code'; import { CfnLayerVersion, CfnLayerVersionPermission } from './lambda.generated'; import { Runtime } from './runtime'; -export interface LayerVersionProps { - /** - * The runtimes compatible with this Layer. - * - * @default - All runtimes are supported. - */ - readonly compatibleRuntimes?: Runtime[]; - - /** - * The content of this Layer. - * - * Using `Code.fromInline` is not supported. - */ - readonly code: Code; - +/** + * Non runtime options + */ +export interface LayerVersionOptions { /** * The description the this Lambda Layer. * @@ -41,6 +30,22 @@ export interface LayerVersionProps { readonly layerVersionName?: string; } +export interface LayerVersionProps extends LayerVersionOptions { + /** + * The runtimes compatible with this Layer. + * + * @default - All runtimes are supported. + */ + readonly compatibleRuntimes?: Runtime[]; + + /** + * The content of this Layer. + * + * Using `Code.fromInline` is not supported. + */ + readonly code: Code; +} + export interface ILayerVersion extends IResource { /** * The ARN of the Lambda Layer version that this Layer defines.