diff --git a/packages/@aws-cdk/aws-lambda-go/README.md b/packages/@aws-cdk/aws-lambda-go/README.md index 748ab32256ec0..530f3b56cf143 100644 --- a/packages/@aws-cdk/aws-lambda-go/README.md +++ b/packages/@aws-cdk/aws-lambda-go/README.md @@ -284,3 +284,17 @@ and Go only includes dependencies that are used in the executable. So in this ca if `cmd/api` used the `auth` & `middleware` packages, but `cmd/anotherApi` did not, then an update to `auth` or `middleware` would only trigger an update to the `cmd/api` Lambda Function. + +## Docker based bundling in complex Docker configurations + +By default the input and output of Docker based bundling is handled via bind mounts. +In situtations where this does not work, like Docker-in-Docker setups or when using a remote Docker socket, you can configure an alternative, but slower, variant that also works in these situations. + + ```ts +new go.GoFunction(this, 'GoFunction', { + entry: 'app/cmd/api', + bundling: { + bundlingFileAccess: BundlingFileAccess.VOLUME_COPY, + }, +}); +``` diff --git a/packages/@aws-cdk/aws-lambda-go/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-go/lib/bundling.ts index 62645d0ae8cc6..54fce951d488b 100644 --- a/packages/@aws-cdk/aws-lambda-go/lib/bundling.ts +++ b/packages/@aws-cdk/aws-lambda-go/lib/bundling.ts @@ -60,6 +60,12 @@ export interface BundlingProps extends BundlingOptions { * The system architecture of the lambda function */ readonly architecture: Architecture; + + /** + * Which option to use to copy the source files to the docker container and output files back + * @default - BundlingFileAccess.BIND_MOUNT + */ + readonly bundlingFileAccess?: cdk.BundlingFileAccess; } /** @@ -85,6 +91,7 @@ export class Bundling implements cdk.BundlingOptions { user: bundling.user, securityOpt: bundling.securityOpt, network: bundling.network, + bundlingFileAccess: bundling.bundlingFileAccess, }, }); } @@ -107,6 +114,7 @@ export class Bundling implements cdk.BundlingOptions { public readonly user?: string; public readonly securityOpt?: string; public readonly network?: string; + public readonly bundlingFileAccess?: cdk.BundlingFileAccess; private readonly relativeEntryPath: string; @@ -154,6 +162,7 @@ export class Bundling implements cdk.BundlingOptions { this.user = props.user; this.securityOpt = props.securityOpt; this.network = props.network; + this.bundlingFileAccess = props.bundlingFileAccess; // Local bundling if (!props.forcedDockerBundling) { // only if Docker is not forced diff --git a/packages/@aws-cdk/aws-lambda-go/lib/types.ts b/packages/@aws-cdk/aws-lambda-go/lib/types.ts index 5dc7beb8f886c..addadbe09a01b 100644 --- a/packages/@aws-cdk/aws-lambda-go/lib/types.ts +++ b/packages/@aws-cdk/aws-lambda-go/lib/types.ts @@ -1,4 +1,4 @@ -import { AssetHashType, DockerImage, DockerRunOptions } from '@aws-cdk/core'; +import { AssetHashType, BundlingFileAccess, DockerImage, DockerRunOptions } from '@aws-cdk/core'; /** * Bundling options @@ -111,6 +111,12 @@ export interface BundlingOptions extends DockerRunOptions { * @default - Direct access */ readonly goProxies?: string[]; + + /** + * Which option to use to copy the source files to the docker container and output files back + * @default - BundlingFileAccess.BIND_MOUNT + */ + readonly bundlingFileAccess?: BundlingFileAccess; } /** diff --git a/packages/@aws-cdk/aws-lambda-go/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-lambda-go/rosetta/default.ts-fixture index 5daf6825a50ad..6f58572f1bcf4 100644 --- a/packages/@aws-cdk/aws-lambda-go/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-lambda-go/rosetta/default.ts-fixture @@ -1,6 +1,6 @@ // Fixture with packages imported, but nothing else import { Construct } from 'constructs'; -import { DockerImage, Stack } from '@aws-cdk/core'; +import { DockerImage, Stack, BundlingFileAccess } from '@aws-cdk/core'; import * as go from '@aws-cdk/aws-lambda-go'; class Fixture extends Stack { diff --git a/packages/@aws-cdk/aws-lambda-go/test/bundling.test.ts b/packages/@aws-cdk/aws-lambda-go/test/bundling.test.ts index 134dd0f51716f..af0419d879699 100644 --- a/packages/@aws-cdk/aws-lambda-go/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-go/test/bundling.test.ts @@ -2,7 +2,7 @@ import * as child_process from 'child_process'; import * as os from 'os'; import * as path from 'path'; import { Architecture, Code, Runtime } from '@aws-cdk/aws-lambda'; -import { AssetHashType, DockerImage } from '@aws-cdk/core'; +import { AssetHashType, BundlingFileAccess, DockerImage } from '@aws-cdk/core'; import { Bundling } from '../lib/bundling'; import * as util from '../lib/util'; @@ -461,3 +461,21 @@ test('Custom bundling network', () => { }), }); }); + +test('Custom bundling file copy variant', () => { + Bundling.bundle({ + entry, + moduleDir, + runtime: Runtime.GO_1_X, + architecture: Architecture.X86_64, + forcedDockerBundling: true, + bundlingFileAccess: BundlingFileAccess.VOLUME_COPY, + }); + + expect(Code.fromAsset).toHaveBeenCalledWith('/project', { + assetHashType: AssetHashType.OUTPUT, + bundling: expect.objectContaining({ + bundlingFileAccess: BundlingFileAccess.VOLUME_COPY, + }), + }); +}); diff --git a/packages/@aws-cdk/aws-lambda-nodejs/README.md b/packages/@aws-cdk/aws-lambda-nodejs/README.md index 24d6582d67732..a895188db5206 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/README.md +++ b/packages/@aws-cdk/aws-lambda-nodejs/README.md @@ -337,3 +337,16 @@ new nodejs.NodejsFunction(this, 'my-handler', { If you chose to customize the hash, you will need to make sure it is updated every time the asset changes, or otherwise it is possible that some deployments will not be invalidated. + +## Docker based bundling in complex Docker configurations + +By default the input and output of Docker based bundling is handled via bind mounts. +In situtations where this does not work, like Docker-in-Docker setups or when using a remote Docker socket, you can configure an alternative, but slower, variant that also works in these situations. + + ```ts + new nodejs.NodejsFunction(this, 'my-handler', { + bundling: { + bundlingFileAccess: BundlingFileAccess.VOLUME_COPY, + }, +}); +``` diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts index 4c4d17d9a6dda..9402079a1253a 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts @@ -43,6 +43,11 @@ export interface BundlingProps extends BundlingOptions { */ readonly preCompilation?: boolean + /** + * Which option to use to copy the source files to the docker container and output files back + * @default - BundlingFileAccess.BIND_MOUNT + */ + readonly bundlingFileAccess?: cdk.BundlingFileAccess; } /** @@ -83,6 +88,7 @@ export class Bundling implements cdk.BundlingOptions { public readonly securityOpt?: string; public readonly network?: string; public readonly local?: cdk.ILocalBundling; + public readonly bundlingFileAccess?: cdk.BundlingFileAccess; private readonly projectRoot: string; private readonly relativeEntryPath: string; @@ -154,6 +160,7 @@ export class Bundling implements cdk.BundlingOptions { this.user = props.user; this.securityOpt = props.securityOpt; this.network = props.network; + this.bundlingFileAccess = props.bundlingFileAccess; // Local bundling if (!props.forceDockerBundling) { // only if Docker is not forced diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts index 0ca72b4149b1f..1c123166bfac1 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts @@ -1,4 +1,4 @@ -import { DockerImage, DockerRunOptions } from '@aws-cdk/core'; +import { BundlingFileAccess, DockerImage, DockerRunOptions } from '@aws-cdk/core'; /** * Bundling options @@ -301,6 +301,12 @@ export interface BundlingOptions extends DockerRunOptions { * @default - no code is injected */ readonly inject?: string[] + + /** + * Which option to use to copy the source files to the docker container and output files back + * @default - BundlingFileAccess.BIND_MOUNT + */ + readonly bundlingFileAccess?: BundlingFileAccess; } /** diff --git a/packages/@aws-cdk/aws-lambda-nodejs/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-lambda-nodejs/rosetta/default.ts-fixture index 0414866994604..0e9604aa39b08 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-lambda-nodejs/rosetta/default.ts-fixture @@ -1,6 +1,6 @@ // Fixture with packages imported, but nothing else import { Construct } from 'constructs'; -import { DockerImage, Stack } from '@aws-cdk/core'; +import { DockerImage, Stack, BundlingFileAccess } from '@aws-cdk/core'; import * as nodejs from '@aws-cdk/aws-lambda-nodejs'; class Fixture extends Stack { diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts index e5248724a0aaa..5d5943d036195 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts @@ -2,7 +2,7 @@ import * as child_process from 'child_process'; import * as os from 'os'; import * as path from 'path'; import { Architecture, Code, Runtime, RuntimeFamily } from '@aws-cdk/aws-lambda'; -import { AssetHashType, DockerImage } from '@aws-cdk/core'; +import { AssetHashType, BundlingFileAccess, DockerImage } from '@aws-cdk/core'; import { version as delayVersion } from 'delay/package.json'; import { Bundling } from '../lib/bundling'; import { PackageInstallation } from '../lib/package-installation'; @@ -826,3 +826,22 @@ test('Custom bundling network', () => { }), }); }); + +test('Custom bundling file copy variant', () => { + Bundling.bundle({ + entry, + projectRoot, + depsLockFilePath, + runtime: Runtime.NODEJS_14_X, + architecture: Architecture.X86_64, + forceDockerBundling: true, + bundlingFileAccess: BundlingFileAccess.VOLUME_COPY, + }); + + expect(Code.fromAsset).toHaveBeenCalledWith('/project', { + assetHashType: AssetHashType.OUTPUT, + bundling: expect.objectContaining({ + bundlingFileAccess: BundlingFileAccess.VOLUME_COPY, + }), + }); +}); diff --git a/packages/@aws-cdk/aws-lambda-python/README.md b/packages/@aws-cdk/aws-lambda-python/README.md index f7ca29de39e0a..f399d1559c81a 100644 --- a/packages/@aws-cdk/aws-lambda-python/README.md +++ b/packages/@aws-cdk/aws-lambda-python/README.md @@ -252,3 +252,20 @@ an array of commands to run. Commands are chained with `&&`. The commands will run in the environment in which bundling occurs: inside the container for Docker bundling or on the host OS for local bundling. + +## Docker based bundling in complex Docker configurations + +By default the input and output of Docker based bundling is handled via bind mounts. +In situtations where this does not work, like Docker-in-Docker setups or when using a remote Docker socket, you can configure an alternative, but slower, variant that also works in these situations. + +```ts +const entry = '/path/to/function'; + +new python.PythonFunction(this, 'function', { + entry, + runtime: Runtime.PYTHON_3_8, + bundling: { + bundlingFileAccess: BundlingFileAccess.VOLUME_COPY, + }, +}); +``` diff --git a/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts index f83756a334fe8..fad11266bb0cb 100644 --- a/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts +++ b/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import { Architecture, AssetCode, Code, Runtime } from '@aws-cdk/aws-lambda'; -import { AssetStaging, BundlingOptions as CdkBundlingOptions, DockerImage, DockerVolume } from '@aws-cdk/core'; +import { AssetStaging, BundlingFileAccess, BundlingOptions as CdkBundlingOptions, DockerImage, DockerVolume } from '@aws-cdk/core'; import { Packaging, DependenciesFile } from './packaging'; import { BundlingOptions, ICommandHooks } from './types'; @@ -41,6 +41,12 @@ export interface BundlingProps extends BundlingOptions { * @default - Does not skip bundling */ readonly skip?: boolean; + + /** + * Which option to use to copy the source files to the docker container and output files back + * @default - BundlingFileAccess.BIND_MOUNT + */ + bundlingFileAccess?: BundlingFileAccess } /** @@ -66,6 +72,7 @@ export class Bundling implements CdkBundlingOptions { public readonly user?: string; public readonly securityOpt?: string; public readonly network?: string; + public readonly bundlingFileAccess?: BundlingFileAccess; constructor(props: BundlingProps) { const { @@ -104,6 +111,7 @@ export class Bundling implements CdkBundlingOptions { this.user = props.user; this.securityOpt = props.securityOpt; this.network = props.network; + this.bundlingFileAccess = props.bundlingFileAccess; } private createBundlingCommand(options: BundlingCommandOptions): string[] { diff --git a/packages/@aws-cdk/aws-lambda-python/lib/types.ts b/packages/@aws-cdk/aws-lambda-python/lib/types.ts index ad0ff6f8ce09d..e0d328e68c858 100644 --- a/packages/@aws-cdk/aws-lambda-python/lib/types.ts +++ b/packages/@aws-cdk/aws-lambda-python/lib/types.ts @@ -1,4 +1,4 @@ -import { AssetHashType, DockerImage, DockerRunOptions } from '@aws-cdk/core'; +import { AssetHashType, BundlingFileAccess, DockerImage, DockerRunOptions } from '@aws-cdk/core'; /** @@ -86,6 +86,12 @@ export interface BundlingOptions extends DockerRunOptions { * @default - do not run additional commands */ readonly commandHooks?: ICommandHooks; + + /** + * Which option to use to copy the source files to the docker container and output files back + * @default - BundlingFileAccess.BIND_MOUNT + */ + readonly bundlingFileAccess?: BundlingFileAccess; } /** diff --git a/packages/@aws-cdk/aws-lambda-python/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-lambda-python/rosetta/default.ts-fixture index 45e67ca866edc..9d2d4ff5ee228 100644 --- a/packages/@aws-cdk/aws-lambda-python/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-lambda-python/rosetta/default.ts-fixture @@ -1,6 +1,6 @@ // Fixture with packages imported, but nothing else import { Construct } from 'constructs'; -import { DockerImage, Stack } from '@aws-cdk/core'; +import { DockerImage, Stack, BundlingFileAccess } from '@aws-cdk/core'; import { Runtime } from '@aws-cdk/aws-lambda'; import * as python from '@aws-cdk/aws-lambda-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 d6172ccfb705b..b51e8bb046acc 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { Architecture, Code, Runtime } from '@aws-cdk/aws-lambda'; -import { DockerImage } from '@aws-cdk/core'; +import { BundlingFileAccess, DockerImage } from '@aws-cdk/core'; import { Bundling } from '../lib/bundling'; jest.spyOn(Code, 'fromAsset'); @@ -374,6 +374,22 @@ test('Bundling with custom network', () => { })); }); +test('Bundling with docker copy variant', () => { + const entry = path.join(__dirname, 'lambda-handler'); + Bundling.bundle({ + entry: entry, + runtime: Runtime.PYTHON_3_7, + bundlingFileAccess: BundlingFileAccess.VOLUME_COPY, + + }); + + expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({ + bundling: expect.objectContaining({ + bundlingFileAccess: BundlingFileAccess.VOLUME_COPY, + }), + })); +}); + test('Do not build docker image when skipping bundling', () => { const entry = path.join(__dirname, 'lambda-handler'); Bundling.bundle({ diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/asset.278d42fa865f60954d898636503d0ee86a6689d73dc50eb912fac62def0ef6a4.bundle/index.js b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/asset.278d42fa865f60954d898636503d0ee86a6689d73dc50eb912fac62def0ef6a4.bundle/index.js new file mode 100644 index 0000000000000..2bf09d6726a42 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/asset.278d42fa865f60954d898636503d0ee86a6689d73dc50eb912fac62def0ef6a4.bundle/index.js @@ -0,0 +1,1052 @@ +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// lib/assertions/providers/lambda-handler/index.ts +var lambda_handler_exports = {}; +__export(lambda_handler_exports, { + handler: () => handler, + isComplete: () => isComplete, + onTimeout: () => onTimeout +}); +module.exports = __toCommonJS(lambda_handler_exports); + +// ../assertions/lib/matcher.ts +var Matcher = class { + static isMatcher(x) { + return x && x instanceof Matcher; + } +}; +var MatchResult = class { + constructor(target) { + this.failuresHere = /* @__PURE__ */ new Map(); + this.captures = /* @__PURE__ */ new Map(); + this.finalized = false; + this.innerMatchFailures = /* @__PURE__ */ new Map(); + this._hasFailed = false; + this._failCount = 0; + this._cost = 0; + this.target = target; + } + push(matcher, path, message) { + return this.recordFailure({ matcher, path, message }); + } + recordFailure(failure) { + const failKey = failure.path.join("."); + let list = this.failuresHere.get(failKey); + if (!list) { + list = []; + this.failuresHere.set(failKey, list); + } + this._failCount += 1; + this._cost += failure.cost ?? 1; + list.push(failure); + this._hasFailed = true; + return this; + } + get isSuccess() { + return !this._hasFailed; + } + hasFailed() { + return this._hasFailed; + } + get failCount() { + return this._failCount; + } + get failCost() { + return this._cost; + } + compose(id, inner) { + if (inner.hasFailed()) { + this._hasFailed = true; + this._failCount += inner.failCount; + this._cost += inner._cost; + this.innerMatchFailures.set(id, inner); + } + inner.captures.forEach((vals, capture) => { + vals.forEach((value) => this.recordCapture({ capture, value })); + }); + return this; + } + finished() { + if (this.finalized) { + return this; + } + if (this.failCount === 0) { + this.captures.forEach((vals, cap) => cap._captured.push(...vals)); + } + this.finalized = true; + return this; + } + toHumanStrings() { + const failures = new Array(); + debugger; + recurse(this, []); + return failures.map((r) => { + const loc = r.path.length === 0 ? "" : ` at /${r.path.join("/")}`; + return "" + r.message + loc + ` (using ${r.matcher.name} matcher)`; + }); + function recurse(x, prefix) { + for (const fail of Array.from(x.failuresHere.values()).flat()) { + failures.push({ + matcher: fail.matcher, + message: fail.message, + path: [...prefix, ...fail.path] + }); + } + for (const [key, inner] of x.innerMatchFailures.entries()) { + recurse(inner, [...prefix, key]); + } + } + } + renderMismatch() { + if (!this.hasFailed()) { + return ""; + } + const parts = new Array(); + const indents = new Array(); + emitFailures(this, ""); + recurse(this); + return moveMarkersToFront(parts.join("").trimEnd()); + function emit(x) { + if (x === void 0) { + debugger; + } + parts.push(x.replace(/\n/g, ` +${indents.join("")}`)); + } + function emitFailures(r, path, scrapSet) { + for (const fail of r.failuresHere.get(path) ?? []) { + emit(`!! ${fail.message} +`); + } + scrapSet == null ? void 0 : scrapSet.delete(path); + } + function recurse(r) { + const remainingFailures = new Set(Array.from(r.failuresHere.keys()).filter((x) => x !== "")); + if (Array.isArray(r.target)) { + indents.push(" "); + emit("[\n"); + for (const [first, i] of enumFirst(range(r.target.length))) { + if (!first) { + emit(",\n"); + } + emitFailures(r, `${i}`, remainingFailures); + const innerMatcher = r.innerMatchFailures.get(`${i}`); + if (innerMatcher) { + emitFailures(innerMatcher, ""); + recurseComparingValues(innerMatcher, r.target[i]); + } else { + emit(renderAbridged(r.target[i])); + } + } + emitRemaining(); + indents.pop(); + emit("\n]"); + return; + } + if (r.target && typeof r.target === "object") { + indents.push(" "); + emit("{\n"); + const keys = Array.from(/* @__PURE__ */ new Set([ + ...Object.keys(r.target), + ...Array.from(remainingFailures) + ])).sort(); + for (const [first, key] of enumFirst(keys)) { + if (!first) { + emit(",\n"); + } + emitFailures(r, key, remainingFailures); + const innerMatcher = r.innerMatchFailures.get(key); + if (innerMatcher) { + emitFailures(innerMatcher, ""); + emit(`${jsonify(key)}: `); + recurseComparingValues(innerMatcher, r.target[key]); + } else { + emit(`${jsonify(key)}: `); + emit(renderAbridged(r.target[key])); + } + } + emitRemaining(); + indents.pop(); + emit("\n}"); + return; + } + emitRemaining(); + emit(jsonify(r.target)); + function emitRemaining() { + if (remainingFailures.size > 0) { + emit("\n"); + } + for (const key of remainingFailures) { + emitFailures(r, key); + } + } + } + function recurseComparingValues(inner, actualValue) { + if (inner.target === actualValue) { + return recurse(inner); + } + emit(renderAbridged(actualValue)); + emit(" <*> "); + recurse(inner); + } + function renderAbridged(x) { + if (Array.isArray(x)) { + switch (x.length) { + case 0: + return "[]"; + case 1: + return `[ ${renderAbridged(x[0])} ]`; + case 2: + if (x.every((e) => ["number", "boolean", "string"].includes(typeof e))) { + return `[ ${x.map(renderAbridged).join(", ")} ]`; + } + return "[ ... ]"; + default: + return "[ ... ]"; + } + } + if (x && typeof x === "object") { + const keys = Object.keys(x); + switch (keys.length) { + case 0: + return "{}"; + case 1: + return `{ ${JSON.stringify(keys[0])}: ${renderAbridged(x[keys[0]])} }`; + default: + return "{ ... }"; + } + } + return jsonify(x); + } + function jsonify(x) { + return JSON.stringify(x) ?? "undefined"; + } + function moveMarkersToFront(x) { + const re = /^(\s+)!!/gm; + return x.replace(re, (_, spaces) => `!!${spaces.substring(0, spaces.length - 2)}`); + } + } + recordCapture(options) { + let values = this.captures.get(options.capture); + if (values === void 0) { + values = []; + } + values.push(options.value); + this.captures.set(options.capture, values); + } +}; +function* range(n) { + for (let i = 0; i < n; i++) { + yield i; + } +} +function* enumFirst(xs) { + let first = true; + for (const x of xs) { + yield [first, x]; + first = false; + } +} + +// ../assertions/lib/private/matchers/absent.ts +var AbsentMatch = class extends Matcher { + constructor(name) { + super(); + this.name = name; + } + test(actual) { + const result = new MatchResult(actual); + if (actual !== void 0) { + result.recordFailure({ + matcher: this, + path: [], + message: `Received ${actual}, but key should be absent` + }); + } + return result; + } +}; + +// ../assertions/lib/private/sorting.ts +function sortKeyComparator(keyFn) { + return (a, b) => { + const ak = keyFn(a); + const bk = keyFn(b); + for (let i = 0; i < ak.length && i < bk.length; i++) { + const av = ak[i]; + const bv = bk[i]; + let diff = 0; + if (typeof av === "number" && typeof bv === "number") { + diff = av - bv; + } else if (typeof av === "string" && typeof bv === "string") { + diff = av.localeCompare(bv); + } + if (diff !== 0) { + return diff; + } + } + return bk.length - ak.length; + }; +} + +// ../assertions/lib/private/sparse-matrix.ts +var SparseMatrix = class { + constructor() { + this.matrix = /* @__PURE__ */ new Map(); + } + get(row, col) { + var _a; + return (_a = this.matrix.get(row)) == null ? void 0 : _a.get(col); + } + row(row) { + var _a; + return Array.from(((_a = this.matrix.get(row)) == null ? void 0 : _a.entries()) ?? []); + } + set(row, col, value) { + let r = this.matrix.get(row); + if (!r) { + r = /* @__PURE__ */ new Map(); + this.matrix.set(row, r); + } + r.set(col, value); + } +}; + +// ../assertions/lib/private/type.ts +function getType(obj) { + return Array.isArray(obj) ? "array" : typeof obj; +} + +// ../assertions/lib/match.ts +var Match = class { + static absent() { + return new AbsentMatch("absent"); + } + static arrayWith(pattern) { + return new ArrayMatch("arrayWith", pattern); + } + static arrayEquals(pattern) { + return new ArrayMatch("arrayEquals", pattern, { subsequence: false }); + } + static exact(pattern) { + return new LiteralMatch("exact", pattern, { partialObjects: false }); + } + static objectLike(pattern) { + return new ObjectMatch("objectLike", pattern); + } + static objectEquals(pattern) { + return new ObjectMatch("objectEquals", pattern, { partial: false }); + } + static not(pattern) { + return new NotMatch("not", pattern); + } + static serializedJson(pattern) { + return new SerializedJson("serializedJson", pattern); + } + static anyValue() { + return new AnyMatch("anyValue"); + } + static stringLikeRegexp(pattern) { + return new StringLikeRegexpMatch("stringLikeRegexp", pattern); + } +}; +var LiteralMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.partialObjects = options.partialObjects ?? false; + if (Matcher.isMatcher(this.pattern)) { + throw new Error("LiteralMatch cannot directly contain another matcher. Remove the top-level matcher or nest it more deeply."); + } + } + test(actual) { + if (Array.isArray(this.pattern)) { + return new ArrayMatch(this.name, this.pattern, { subsequence: false, partialObjects: this.partialObjects }).test(actual); + } + if (typeof this.pattern === "object") { + return new ObjectMatch(this.name, this.pattern, { partial: this.partialObjects }).test(actual); + } + const result = new MatchResult(actual); + if (typeof this.pattern !== typeof actual) { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected type ${typeof this.pattern} but received ${getType(actual)}` + }); + return result; + } + if (actual !== this.pattern) { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected ${this.pattern} but received ${actual}` + }); + } + return result; + } +}; +var ArrayMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.subsequence = options.subsequence ?? true; + this.partialObjects = options.partialObjects ?? false; + } + test(actual) { + if (!Array.isArray(actual)) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected type array but received ${getType(actual)}` + }); + } + return this.subsequence ? this.testSubsequence(actual) : this.testFullArray(actual); + } + testFullArray(actual) { + const result = new MatchResult(actual); + let i = 0; + for (; i < this.pattern.length && i < actual.length; i++) { + const patternElement = this.pattern[i]; + const matcher = Matcher.isMatcher(patternElement) ? patternElement : new LiteralMatch(this.name, patternElement, { partialObjects: this.partialObjects }); + const innerResult = matcher.test(actual[i]); + result.compose(`${i}`, innerResult); + } + if (i < this.pattern.length) { + result.recordFailure({ + matcher: this, + message: `Not enough elements in array (expecting ${this.pattern.length}, got ${actual.length})`, + path: [`${i}`] + }); + } + if (i < actual.length) { + result.recordFailure({ + matcher: this, + message: `Too many elements in array (expecting ${this.pattern.length}, got ${actual.length})`, + path: [`${i}`] + }); + } + return result; + } + testSubsequence(actual) { + const result = new MatchResult(actual); + let patternIdx = 0; + let actualIdx = 0; + const matches = new SparseMatrix(); + while (patternIdx < this.pattern.length && actualIdx < actual.length) { + const patternElement = this.pattern[patternIdx]; + const matcher = Matcher.isMatcher(patternElement) ? patternElement : new LiteralMatch(this.name, patternElement, { partialObjects: this.partialObjects }); + const matcherName = matcher.name; + if (matcherName == "absent" || matcherName == "anyValue") { + throw new Error(`The Matcher ${matcherName}() cannot be nested within arrayWith()`); + } + const innerResult = matcher.test(actual[actualIdx]); + matches.set(patternIdx, actualIdx, innerResult); + actualIdx++; + if (innerResult.isSuccess) { + result.compose(`${actualIdx}`, innerResult); + patternIdx++; + } + } + if (patternIdx < this.pattern.length) { + for (let spi = 0; spi < patternIdx; spi++) { + const foundMatch = matches.row(spi).find(([, r]) => r.isSuccess); + if (!foundMatch) { + continue; + } + const [index] = foundMatch; + result.compose(`${index}`, new MatchResult(actual[index]).recordFailure({ + matcher: this, + message: `arrayWith pattern ${spi} matched here`, + path: [], + cost: 0 + })); + } + const failedMatches = matches.row(patternIdx); + failedMatches.sort(sortKeyComparator(([i, r]) => [r.failCost, i])); + if (failedMatches.length > 0) { + const [index, innerResult] = failedMatches[0]; + result.recordFailure({ + matcher: this, + message: `Could not match arrayWith pattern ${patternIdx}. This is the closest match`, + path: [`${index}`], + cost: 0 + }); + result.compose(`${index}`, innerResult); + } else { + result.recordFailure({ + matcher: this, + message: `Could not match arrayWith pattern ${patternIdx}. No more elements to try`, + path: [`${actual.length}`] + }); + } + } + return result; + } +}; +var ObjectMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.partial = options.partial ?? true; + } + test(actual) { + if (typeof actual !== "object" || Array.isArray(actual)) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected type object but received ${getType(actual)}` + }); + } + const result = new MatchResult(actual); + if (!this.partial) { + for (const a of Object.keys(actual)) { + if (!(a in this.pattern)) { + result.recordFailure({ + matcher: this, + path: [a], + message: `Unexpected key ${a}` + }); + } + } + } + for (const [patternKey, patternVal] of Object.entries(this.pattern)) { + if (!(patternKey in actual) && !(patternVal instanceof AbsentMatch)) { + result.recordFailure({ + matcher: this, + path: [patternKey], + message: `Missing key '${patternKey}'` + }); + continue; + } + const matcher = Matcher.isMatcher(patternVal) ? patternVal : new LiteralMatch(this.name, patternVal, { partialObjects: this.partial }); + const inner = matcher.test(actual[patternKey]); + result.compose(patternKey, inner); + } + return result; + } +}; +var SerializedJson = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + if (getType(actual) !== "string") { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected JSON as a string but found ${getType(actual)}` + }); + } + let parsed; + try { + parsed = JSON.parse(actual); + } catch (err) { + if (err instanceof SyntaxError) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Invalid JSON string: ${actual}` + }); + } else { + throw err; + } + } + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern); + const innerResult = matcher.test(parsed); + if (innerResult.hasFailed()) { + innerResult.recordFailure({ + matcher: this, + path: [], + message: "Encoded JSON value does not match" + }); + } + return innerResult; + } +}; +var NotMatch = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern); + const innerResult = matcher.test(actual); + const result = new MatchResult(actual); + if (innerResult.failCount === 0) { + result.recordFailure({ + matcher: this, + path: [], + message: `Found unexpected match: ${JSON.stringify(actual, void 0, 2)}` + }); + } + return result; + } +}; +var AnyMatch = class extends Matcher { + constructor(name) { + super(); + this.name = name; + } + test(actual) { + const result = new MatchResult(actual); + if (actual == null) { + result.recordFailure({ + matcher: this, + path: [], + message: "Expected a value but found none" + }); + } + return result; + } +}; +var StringLikeRegexpMatch = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const result = new MatchResult(actual); + const regex = new RegExp(this.pattern, "gm"); + if (typeof actual !== "string") { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected a string, but got '${typeof actual}'` + }); + } + if (!regex.test(actual)) { + result.recordFailure({ + matcher: this, + path: [], + message: `String '${actual}' did not match pattern '${this.pattern}'` + }); + } + return result; + } +}; + +// lib/assertions/providers/lambda-handler/base.ts +var https = __toESM(require("https")); +var url = __toESM(require("url")); +var AWS = __toESM(require("aws-sdk")); +var CustomResourceHandler = class { + constructor(event, context) { + this.event = event; + this.context = context; + this.timedOut = false; + this.timeout = setTimeout(async () => { + await this.respond({ + status: "FAILED", + reason: "Lambda Function Timeout", + data: this.context.logStreamName + }); + this.timedOut = true; + }, context.getRemainingTimeInMillis() - 1200); + this.event = event; + this.physicalResourceId = extractPhysicalResourceId(event); + } + async handle() { + try { + if ("stateMachineArn" in this.event.ResourceProperties) { + const req = { + stateMachineArn: this.event.ResourceProperties.stateMachineArn, + name: this.event.RequestId, + input: JSON.stringify(this.event) + }; + await this.startExecution(req); + return; + } else { + const response = await this.processEvent(this.event.ResourceProperties); + return response; + } + } catch (e) { + console.log(e); + throw e; + } finally { + clearTimeout(this.timeout); + } + } + async handleIsComplete() { + try { + const result = await this.processEvent(this.event.ResourceProperties); + return result; + } catch (e) { + console.log(e); + return; + } finally { + clearTimeout(this.timeout); + } + } + async startExecution(req) { + try { + const sfn = new AWS.StepFunctions(); + await sfn.startExecution(req).promise(); + } finally { + clearTimeout(this.timeout); + } + } + respond(response) { + if (this.timedOut) { + return; + } + const cfResponse = { + Status: response.status, + Reason: response.reason, + PhysicalResourceId: this.physicalResourceId, + StackId: this.event.StackId, + RequestId: this.event.RequestId, + LogicalResourceId: this.event.LogicalResourceId, + NoEcho: false, + Data: response.data + }; + const responseBody = JSON.stringify(cfResponse); + console.log("Responding to CloudFormation", responseBody); + const parsedUrl = url.parse(this.event.ResponseURL); + const requestOptions = { + hostname: parsedUrl.hostname, + path: parsedUrl.path, + method: "PUT", + headers: { "content-type": "", "content-length": responseBody.length } + }; + return new Promise((resolve, reject) => { + try { + const request2 = https.request(requestOptions, resolve); + request2.on("error", reject); + request2.write(responseBody); + request2.end(); + } catch (e) { + reject(e); + } finally { + clearTimeout(this.timeout); + } + }); + } +}; +function extractPhysicalResourceId(event) { + switch (event.RequestType) { + case "Create": + return event.LogicalResourceId; + case "Update": + case "Delete": + return event.PhysicalResourceId; + } +} + +// lib/assertions/providers/lambda-handler/assertion.ts +var AssertionHandler = class extends CustomResourceHandler { + async processEvent(request2) { + let actual = decodeCall(request2.actual); + const expected = decodeCall(request2.expected); + let result; + const matcher = new MatchCreator(expected).getMatcher(); + console.log(`Testing equality between ${JSON.stringify(request2.actual)} and ${JSON.stringify(request2.expected)}`); + const matchResult = matcher.test(actual); + matchResult.finished(); + if (matchResult.hasFailed()) { + result = { + failed: true, + assertion: JSON.stringify({ + status: "fail", + message: matchResult.renderMismatch() + }) + }; + if (request2.failDeployment) { + throw new Error(result.assertion); + } + } else { + result = { + assertion: JSON.stringify({ + status: "success" + }) + }; + } + return result; + } +}; +var MatchCreator = class { + constructor(obj) { + this.parsedObj = { + matcher: obj + }; + } + getMatcher() { + try { + const final = JSON.parse(JSON.stringify(this.parsedObj), function(_k, v) { + const nested = Object.keys(v)[0]; + switch (nested) { + case "$ArrayWith": + return Match.arrayWith(v[nested]); + case "$ObjectLike": + return Match.objectLike(v[nested]); + case "$StringLike": + return Match.stringLikeRegexp(v[nested]); + case "$SerializedJson": + return Match.serializedJson(v[nested]); + default: + return v; + } + }); + if (Matcher.isMatcher(final.matcher)) { + return final.matcher; + } + return Match.exact(final.matcher); + } catch { + return Match.exact(this.parsedObj.matcher); + } + } +}; +function decodeCall(call) { + if (!call) { + return void 0; + } + try { + const parsed = JSON.parse(call); + return parsed; + } catch (e) { + return call; + } +} + +// lib/assertions/providers/lambda-handler/utils.ts +function decode(object) { + return JSON.parse(JSON.stringify(object), (_k, v) => { + switch (v) { + case "TRUE:BOOLEAN": + return true; + case "FALSE:BOOLEAN": + return false; + default: + return v; + } + }); +} + +// lib/assertions/providers/lambda-handler/sdk.ts +function flatten(object) { + return Object.assign( + {}, + ...function _flatten(child, path = []) { + return [].concat(...Object.keys(child).map((key) => { + let childKey = Buffer.isBuffer(child[key]) ? child[key].toString("utf8") : child[key]; + if (typeof childKey === "string") { + childKey = isJsonString(childKey); + } + return typeof childKey === "object" && childKey !== null ? _flatten(childKey, path.concat([key])) : { [path.concat([key]).join(".")]: childKey }; + })); + }(object) + ); +} +var AwsApiCallHandler = class extends CustomResourceHandler { + async processEvent(request2) { + const AWS2 = require("aws-sdk"); + console.log(`AWS SDK VERSION: ${AWS2.VERSION}`); + if (!Object.prototype.hasOwnProperty.call(AWS2, request2.service)) { + throw Error(`Service ${request2.service} does not exist in AWS SDK version ${AWS2.VERSION}.`); + } + const service = new AWS2[request2.service](); + const response = await service[request2.api](request2.parameters && decode(request2.parameters)).promise(); + console.log(`SDK response received ${JSON.stringify(response)}`); + delete response.ResponseMetadata; + const respond = { + apiCallResponse: response + }; + const flatData = { + ...flatten(respond) + }; + let resp = respond; + if (request2.outputPaths) { + resp = filterKeys(flatData, request2.outputPaths); + } else if (request2.flattenResponse === "true") { + resp = flatData; + } + console.log(`Returning result ${JSON.stringify(resp)}`); + return resp; + } +}; +function filterKeys(object, searchStrings) { + return Object.entries(object).reduce((filteredObject, [key, value]) => { + for (const searchString of searchStrings) { + if (key.startsWith(`apiCallResponse.${searchString}`)) { + filteredObject[key] = value; + } + } + return filteredObject; + }, {}); +} +function isJsonString(value) { + try { + return JSON.parse(value); + } catch { + return value; + } +} + +// lib/assertions/providers/lambda-handler/types.ts +var ASSERT_RESOURCE_TYPE = "Custom::DeployAssert@AssertEquals"; +var SDK_RESOURCE_TYPE_PREFIX = "Custom::DeployAssert@SdkCall"; + +// lib/assertions/providers/lambda-handler/index.ts +async function handler(event, context) { + console.log(`Event: ${JSON.stringify({ ...event, ResponseURL: "..." })}`); + const provider = createResourceHandler(event, context); + try { + if (event.RequestType === "Delete") { + await provider.respond({ + status: "SUCCESS", + reason: "OK" + }); + return; + } + const result = await provider.handle(); + if ("stateMachineArn" in event.ResourceProperties) { + console.info('Found "stateMachineArn", waiter statemachine started'); + return; + } else if ("expected" in event.ResourceProperties) { + console.info('Found "expected", testing assertions'); + const actualPath = event.ResourceProperties.actualPath; + const actual = actualPath ? result[`apiCallResponse.${actualPath}`] : result.apiCallResponse; + const assertion = new AssertionHandler({ + ...event, + ResourceProperties: { + ServiceToken: event.ServiceToken, + actual, + expected: event.ResourceProperties.expected + } + }, context); + try { + const assertionResult = await assertion.handle(); + await provider.respond({ + status: "SUCCESS", + reason: "OK", + data: { + ...assertionResult, + ...result + } + }); + return; + } catch (e) { + await provider.respond({ + status: "FAILED", + reason: e.message ?? "Internal Error" + }); + return; + } + } + await provider.respond({ + status: "SUCCESS", + reason: "OK", + data: result + }); + } catch (e) { + await provider.respond({ + status: "FAILED", + reason: e.message ?? "Internal Error" + }); + return; + } + return; +} +async function onTimeout(timeoutEvent) { + const isCompleteRequest = JSON.parse(JSON.parse(timeoutEvent.Cause).errorMessage); + const provider = createResourceHandler(isCompleteRequest, standardContext); + await provider.respond({ + status: "FAILED", + reason: "Operation timed out: " + JSON.stringify(isCompleteRequest) + }); +} +async function isComplete(event, context) { + console.log(`Event: ${JSON.stringify({ ...event, ResponseURL: "..." })}`); + const provider = createResourceHandler(event, context); + try { + const result = await provider.handleIsComplete(); + const actualPath = event.ResourceProperties.actualPath; + if (result) { + const actual = actualPath ? result[`apiCallResponse.${actualPath}`] : result.apiCallResponse; + if ("expected" in event.ResourceProperties) { + const assertion = new AssertionHandler({ + ...event, + ResourceProperties: { + ServiceToken: event.ServiceToken, + actual, + expected: event.ResourceProperties.expected + } + }, context); + const assertionResult = await assertion.handleIsComplete(); + if (!(assertionResult == null ? void 0 : assertionResult.failed)) { + await provider.respond({ + status: "SUCCESS", + reason: "OK", + data: { + ...assertionResult, + ...result + } + }); + return; + } else { + console.log(`Assertion Failed: ${JSON.stringify(assertionResult)}`); + throw new Error(JSON.stringify(event)); + } + } + await provider.respond({ + status: "SUCCESS", + reason: "OK", + data: result + }); + } else { + console.log("No result"); + throw new Error(JSON.stringify(event)); + } + return; + } catch (e) { + console.log(e); + throw new Error(JSON.stringify(event)); + } +} +function createResourceHandler(event, context) { + if (event.ResourceType.startsWith(SDK_RESOURCE_TYPE_PREFIX)) { + return new AwsApiCallHandler(event, context); + } else if (event.ResourceType.startsWith(ASSERT_RESOURCE_TYPE)) { + return new AssertionHandler(event, context); + } else { + throw new Error(`Unsupported resource type "${event.ResourceType}`); + } +} +var standardContext = { + getRemainingTimeInMillis: () => 9e4 +}; +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + handler, + isComplete, + onTimeout +}); diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/cdk.out b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/cdk.out new file mode 100644 index 0000000000000..145739f539580 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"22.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/integ-lambda-python-function-dockercopy.assets.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/integ-lambda-python-function-dockercopy.assets.json new file mode 100644 index 0000000000000..a5f2dee381914 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/integ-lambda-python-function-dockercopy.assets.json @@ -0,0 +1,32 @@ +{ + "version": "22.0.0", + "files": { + "d7f71e188167c705d230ed88d3e1935d888bb138e142570c4c22139b79f23aa9": { + "source": { + "path": "asset.d7f71e188167c705d230ed88d3e1935d888bb138e142570c4c22139b79f23aa9", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "d7f71e188167c705d230ed88d3e1935d888bb138e142570c4c22139b79f23aa9.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "e358cdb3c29cca92c45125b79de88c4f42224ba81d32defb08b97b631d8f55e3": { + "source": { + "path": "integ-lambda-python-function-dockercopy.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "e358cdb3c29cca92c45125b79de88c4f42224ba81d32defb08b97b631d8f55e3.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/integ-lambda-python-function-dockercopy.template.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/integ-lambda-python-function-dockercopy.template.json new file mode 100644 index 0000000000000..8498a0dfcef8f --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/integ-lambda-python-function-dockercopy.template.json @@ -0,0 +1,109 @@ +{ + "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": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "d7f71e188167c705d230ed88d3e1935d888bb138e142570c4c22139b79f23aa9.zip" + }, + "Role": { + "Fn::GetAtt": [ + "myhandlerServiceRole77891068", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "python3.9" + }, + "DependsOn": [ + "myhandlerServiceRole77891068" + ] + } + }, + "Outputs": { + "FunctionArn": { + "Value": { + "Fn::GetAtt": [ + "myhandlerD202FA8E", + "Arn" + ] + } + }, + "ExportsOutputRefmyhandlerD202FA8E369E8804": { + "Value": { + "Ref": "myhandlerD202FA8E" + }, + "Export": { + "Name": "integ-lambda-python-function-dockercopy:ExportsOutputRefmyhandlerD202FA8E369E8804" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/integ.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/integ.json new file mode 100644 index 0000000000000..a38a97f7dcfd0 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/integ.json @@ -0,0 +1,13 @@ +{ + "version": "22.0.0", + "testCases": { + "lambda-python-function-dockercopy/DefaultTest": { + "stacks": [ + "integ-lambda-python-function-dockercopy" + ], + "stackUpdateWorkflow": false, + "assertionStack": "lambda-python-function-dockercopy/DefaultTest/DeployAssert", + "assertionStackName": "lambdapythonfunctiondockercopyDefaultTestDeployAssert9D2D26DD" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/lambdapythonfunctiondockercopyDefaultTestDeployAssert9D2D26DD.assets.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/lambdapythonfunctiondockercopyDefaultTestDeployAssert9D2D26DD.assets.json new file mode 100644 index 0000000000000..f9cf05dad8e75 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/lambdapythonfunctiondockercopyDefaultTestDeployAssert9D2D26DD.assets.json @@ -0,0 +1,32 @@ +{ + "version": "22.0.0", + "files": { + "278d42fa865f60954d898636503d0ee86a6689d73dc50eb912fac62def0ef6a4": { + "source": { + "path": "asset.278d42fa865f60954d898636503d0ee86a6689d73dc50eb912fac62def0ef6a4.bundle", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "278d42fa865f60954d898636503d0ee86a6689d73dc50eb912fac62def0ef6a4.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "3e42550e52b2738ac1d185ed45788d5ca72cd1c7ce5402d23c4c1097c53826c3": { + "source": { + "path": "lambdapythonfunctiondockercopyDefaultTestDeployAssert9D2D26DD.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "3e42550e52b2738ac1d185ed45788d5ca72cd1c7ce5402d23c4c1097c53826c3.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/lambdapythonfunctiondockercopyDefaultTestDeployAssert9D2D26DD.template.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/lambdapythonfunctiondockercopyDefaultTestDeployAssert9D2D26DD.template.json new file mode 100644 index 0000000000000..78a740c07125a --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/lambdapythonfunctiondockercopyDefaultTestDeployAssert9D2D26DD.template.json @@ -0,0 +1,178 @@ +{ + "Resources": { + "LambdaInvoke431773224924ebf10c8a31d363a6bf16": { + "Type": "Custom::DeployAssert@SdkCallLambdainvoke", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F", + "Arn" + ] + }, + "service": "Lambda", + "api": "invoke", + "expected": "{\"$ObjectLike\":{\"Payload\":\"200\"}}", + "parameters": { + "FunctionName": { + "Fn::ImportValue": "integ-lambda-python-function-dockercopy:ExportsOutputRefmyhandlerD202FA8E369E8804" + } + }, + "flattenResponse": "false", + "salt": "1672909861311" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "LambdaInvoke431773224924ebf10c8a31d363a6bf16Invoke9BC0E67F": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::ImportValue": "integ-lambda-python-function-dockercopy:ExportsOutputRefmyhandlerD202FA8E369E8804" + }, + "Principal": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73", + "Arn" + ] + } + } + }, + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ], + "Policies": [ + { + "PolicyName": "Inline", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "lambda:Invoke" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + }, + { + "Action": [ + "lambda:InvokeFunction" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":lambda:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":function:", + { + "Fn::ImportValue": "integ-lambda-python-function-dockercopy:ExportsOutputRefmyhandlerD202FA8E369E8804" + } + ] + ] + } + ] + } + ] + } + } + ] + } + }, + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "278d42fa865f60954d898636503d0ee86a6689d73dc50eb912fac62def0ef6a4.zip" + }, + "Timeout": 120, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73", + "Arn" + ] + } + } + } + }, + "Outputs": { + "AssertionResultsLambdaInvoke431773224924ebf10c8a31d363a6bf16": { + "Value": { + "Fn::GetAtt": [ + "LambdaInvoke431773224924ebf10c8a31d363a6bf16", + "assertion" + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/manifest.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/manifest.json new file mode 100644 index 0000000000000..5bca70ed12ebb --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/manifest.json @@ -0,0 +1,160 @@ +{ + "version": "22.0.0", + "artifacts": { + "integ-lambda-python-function-dockercopy.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "integ-lambda-python-function-dockercopy.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "integ-lambda-python-function-dockercopy": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integ-lambda-python-function-dockercopy.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/e358cdb3c29cca92c45125b79de88c4f42224ba81d32defb08b97b631d8f55e3.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "integ-lambda-python-function-dockercopy.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "integ-lambda-python-function-dockercopy.assets" + ], + "metadata": { + "/integ-lambda-python-function-dockercopy/my_handler/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "myhandlerServiceRole77891068" + } + ], + "/integ-lambda-python-function-dockercopy/my_handler/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "myhandlerD202FA8E" + } + ], + "/integ-lambda-python-function-dockercopy/FunctionArn": [ + { + "type": "aws:cdk:logicalId", + "data": "FunctionArn" + } + ], + "/integ-lambda-python-function-dockercopy/Exports/Output{\"Ref\":\"myhandlerD202FA8E\"}": [ + { + "type": "aws:cdk:logicalId", + "data": "ExportsOutputRefmyhandlerD202FA8E369E8804" + } + ], + "/integ-lambda-python-function-dockercopy/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/integ-lambda-python-function-dockercopy/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "integ-lambda-python-function-dockercopy" + }, + "lambdapythonfunctiondockercopyDefaultTestDeployAssert9D2D26DD.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "lambdapythonfunctiondockercopyDefaultTestDeployAssert9D2D26DD.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "lambdapythonfunctiondockercopyDefaultTestDeployAssert9D2D26DD": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "lambdapythonfunctiondockercopyDefaultTestDeployAssert9D2D26DD.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/3e42550e52b2738ac1d185ed45788d5ca72cd1c7ce5402d23c4c1097c53826c3.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "lambdapythonfunctiondockercopyDefaultTestDeployAssert9D2D26DD.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "integ-lambda-python-function-dockercopy", + "lambdapythonfunctiondockercopyDefaultTestDeployAssert9D2D26DD.assets" + ], + "metadata": { + "/lambda-python-function-dockercopy/DefaultTest/DeployAssert/LambdaInvoke431773224924ebf10c8a31d363a6bf16/Default/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "LambdaInvoke431773224924ebf10c8a31d363a6bf16" + } + ], + "/lambda-python-function-dockercopy/DefaultTest/DeployAssert/LambdaInvoke431773224924ebf10c8a31d363a6bf16/Invoke": [ + { + "type": "aws:cdk:logicalId", + "data": "LambdaInvoke431773224924ebf10c8a31d363a6bf16Invoke9BC0E67F" + } + ], + "/lambda-python-function-dockercopy/DefaultTest/DeployAssert/LambdaInvoke431773224924ebf10c8a31d363a6bf16/AssertionResults": [ + { + "type": "aws:cdk:logicalId", + "data": "AssertionResultsLambdaInvoke431773224924ebf10c8a31d363a6bf16" + } + ], + "/lambda-python-function-dockercopy/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Role": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73" + } + ], + "/lambda-python-function-dockercopy/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Handler": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F" + } + ], + "/lambda-python-function-dockercopy/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/lambda-python-function-dockercopy/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "lambda-python-function-dockercopy/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/tree.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/tree.json new file mode 100644 index 0000000000000..da9ab28f36cea --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.js.snapshot/tree.json @@ -0,0 +1,343 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "integ-lambda-python-function-dockercopy": { + "id": "integ-lambda-python-function-dockercopy", + "path": "integ-lambda-python-function-dockercopy", + "children": { + "my_handler": { + "id": "my_handler", + "path": "integ-lambda-python-function-dockercopy/my_handler", + "children": { + "ServiceRole": { + "id": "ServiceRole", + "path": "integ-lambda-python-function-dockercopy/my_handler/ServiceRole", + "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "integ-lambda-python-function-dockercopy/my_handler/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "integ-lambda-python-function-dockercopy/my_handler/ServiceRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "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" + ] + ] + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Code": { + "id": "Code", + "path": "integ-lambda-python-function-dockercopy/my_handler/Code", + "children": { + "Stage": { + "id": "Stage", + "path": "integ-lambda-python-function-dockercopy/my_handler/Code/Stage", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "AssetBucket": { + "id": "AssetBucket", + "path": "integ-lambda-python-function-dockercopy/my_handler/Code/AssetBucket", + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.BucketBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3-assets.Asset", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "integ-lambda-python-function-dockercopy/my_handler/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::Function", + "aws:cdk:cloudformation:props": { + "code": { + "s3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "s3Key": "d7f71e188167c705d230ed88d3e1935d888bb138e142570c4c22139b79f23aa9.zip" + }, + "role": { + "Fn::GetAtt": [ + "myhandlerServiceRole77891068", + "Arn" + ] + }, + "handler": "index.handler", + "runtime": "python3.9" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.CfnFunction", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda-python.PythonFunction", + "version": "0.0.0" + } + }, + "FunctionArn": { + "id": "FunctionArn", + "path": "integ-lambda-python-function-dockercopy/FunctionArn", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + }, + "Exports": { + "id": "Exports", + "path": "integ-lambda-python-function-dockercopy/Exports", + "children": { + "Output{\"Ref\":\"myhandlerD202FA8E\"}": { + "id": "Output{\"Ref\":\"myhandlerD202FA8E\"}", + "path": "integ-lambda-python-function-dockercopy/Exports/Output{\"Ref\":\"myhandlerD202FA8E\"}", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.189" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "integ-lambda-python-function-dockercopy/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "integ-lambda-python-function-dockercopy/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "lambda-python-function-dockercopy": { + "id": "lambda-python-function-dockercopy", + "path": "lambda-python-function-dockercopy", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "lambda-python-function-dockercopy/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "lambda-python-function-dockercopy/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.189" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "lambda-python-function-dockercopy/DefaultTest/DeployAssert", + "children": { + "LambdaInvoke431773224924ebf10c8a31d363a6bf16": { + "id": "LambdaInvoke431773224924ebf10c8a31d363a6bf16", + "path": "lambda-python-function-dockercopy/DefaultTest/DeployAssert/LambdaInvoke431773224924ebf10c8a31d363a6bf16", + "children": { + "SdkProvider": { + "id": "SdkProvider", + "path": "lambda-python-function-dockercopy/DefaultTest/DeployAssert/LambdaInvoke431773224924ebf10c8a31d363a6bf16/SdkProvider", + "children": { + "AssertionsProvider": { + "id": "AssertionsProvider", + "path": "lambda-python-function-dockercopy/DefaultTest/DeployAssert/LambdaInvoke431773224924ebf10c8a31d363a6bf16/SdkProvider/AssertionsProvider", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.189" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AssertionsProvider", + "version": "0.0.0" + } + }, + "Default": { + "id": "Default", + "path": "lambda-python-function-dockercopy/DefaultTest/DeployAssert/LambdaInvoke431773224924ebf10c8a31d363a6bf16/Default", + "children": { + "Default": { + "id": "Default", + "path": "lambda-python-function-dockercopy/DefaultTest/DeployAssert/LambdaInvoke431773224924ebf10c8a31d363a6bf16/Default/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + }, + "Invoke": { + "id": "Invoke", + "path": "lambda-python-function-dockercopy/DefaultTest/DeployAssert/LambdaInvoke431773224924ebf10c8a31d363a6bf16/Invoke", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "AssertionResults": { + "id": "AssertionResults", + "path": "lambda-python-function-dockercopy/DefaultTest/DeployAssert/LambdaInvoke431773224924ebf10c8a31d363a6bf16/AssertionResults", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.LambdaInvokeFunction", + "version": "0.0.0" + } + }, + "SingletonFunction1488541a7b23466481b69b4408076b81": { + "id": "SingletonFunction1488541a7b23466481b69b4408076b81", + "path": "lambda-python-function-dockercopy/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81", + "children": { + "Staging": { + "id": "Staging", + "path": "lambda-python-function-dockercopy/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Staging", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "Role": { + "id": "Role", + "path": "lambda-python-function-dockercopy/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Role", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "Handler": { + "id": "Handler", + "path": "lambda-python-function-dockercopy/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Handler", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.189" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "lambda-python-function-dockercopy/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "lambda-python-function-dockercopy/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.189" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.ts new file mode 100644 index 0000000000000..6cf03198fb624 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.dockercopy.ts @@ -0,0 +1,51 @@ +// disabling update workflow because we don't want to include the assets in the snapshot +// python bundling changes the asset hash pretty frequently +/// !cdk-integ pragma:disable-update-workflow +import * as path from 'path'; +import { Runtime } from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Stack, StackProps, BundlingFileAccess } from '@aws-cdk/core'; +import { IntegTest, ExpectedResult } from '@aws-cdk/integ-tests'; +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 { + public readonly functionName: string; + 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-dockercopy'), + runtime: Runtime.PYTHON_3_9, + bundling: { + bundlingFileAccess: BundlingFileAccess.VOLUME_COPY, + }, + }); + this.functionName = fn.functionName; + + new CfnOutput(this, 'FunctionArn', { + value: fn.functionArn, + }); + } +} + +const app = new App(); +const testCase = new TestStack(app, 'integ-lambda-python-function-dockercopy'); +const integ = new IntegTest(app, 'lambda-python-function-dockercopy', { + testCases: [testCase], + stackUpdateWorkflow: false, +}); + +const invoke = integ.assertions.invokeFunction({ + functionName: testCase.functionName, +}); + +invoke.expect(ExpectedResult.objectLike({ + Payload: '200', +})); + +app.synth(); diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-dockercopy/.ignorefile b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-dockercopy/.ignorefile new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-dockercopy/Pipfile b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-dockercopy/Pipfile new file mode 100644 index 0000000000000..78d783bc4b9b0 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-dockercopy/Pipfile @@ -0,0 +1,7 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[packages] +requests = "==2.26.0" diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-dockercopy/Pipfile.lock b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-dockercopy/Pipfile.lock new file mode 100644 index 0000000000000..1a9abf9618a62 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-dockercopy/Pipfile.lock @@ -0,0 +1,59 @@ +{ + "_meta": { + "hash": { + "sha256": "6cfaa5a495be5cf47942a14b04d50e639f14743101e621684e86449dbac8da61" + }, + "pipfile-spec": 6, + "requires": {}, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", + "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" + ], + "index": "pypi", + "version": "==2022.12.7" + }, + "charset-normalizer": { + "hashes": [ + "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", + "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" + ], + "markers": "python_version >= '3'", + "version": "==2.0.12" + }, + "idna": { + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "markers": "python_version >= '3'", + "version": "==3.4" + }, + "requests": { + "hashes": [ + "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", + "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" + ], + "index": "pypi", + "version": "==2.26.0" + }, + "urllib3": { + "hashes": [ + "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc", + "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.13" + } + }, + "develop": {} +} diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-dockercopy/index.py b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-dockercopy/index.py new file mode 100644 index 0000000000000..04f99eb108b30 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler-dockercopy/index.py @@ -0,0 +1,8 @@ +import requests + +def handler(event, context): + response = requests.get('https://a0.awsstatic.com/main/images/logos/aws_smile-header-desktop-en-white_59x35.png', stream=True) + + print(response.status_code) + + return response.status_code diff --git a/packages/@aws-cdk/core/lib/asset-staging.ts b/packages/@aws-cdk/core/lib/asset-staging.ts index 6831790d50938..aae0b1641f077 100644 --- a/packages/@aws-cdk/core/lib/asset-staging.ts +++ b/packages/@aws-cdk/core/lib/asset-staging.ts @@ -1,14 +1,14 @@ import * as crypto from 'crypto'; -import * as os from 'os'; import * as path from 'path'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import * as fs from 'fs-extra'; import { AssetHashType, AssetOptions, FileAssetPackaging } from './assets'; -import { BundlingOptions, BundlingOutput } from './bundling'; +import { BundlingFileAccess, BundlingOptions, BundlingOutput } from './bundling'; import { FileSystem, FingerprintOptions } from './fs'; import { clearLargeFileFingerprintCache } from './fs/fingerprint'; import { Names } from './names'; +import { AssetBundlingVolumeCopy, AssetBundlingBindMount } from './private/asset-staging'; import { Cache } from './private/cache'; import { Stack } from './stack'; import { Stage } from './stage'; @@ -432,45 +432,27 @@ export class AssetStaging extends Construct { // Chmod the bundleDir to full access. fs.chmodSync(bundleDir, 0o777); - // Always mount input and output dir - const volumes = [ - { - hostPath: this.sourcePath, - containerPath: AssetStaging.BUNDLING_INPUT_DIR, - }, - { - hostPath: bundleDir, - containerPath: AssetStaging.BUNDLING_OUTPUT_DIR, - }, - ...options.volumes ?? [], - ]; - let localBundling: boolean | undefined; try { process.stderr.write(`Bundling asset ${this.node.path}...\n`); localBundling = options.local?.tryBundle(bundleDir, options); if (!localBundling) { - let user: string; - if (options.user) { - user = options.user; - } else { // Default to current user - const userInfo = os.userInfo(); - user = userInfo.uid !== -1 // uid is -1 on Windows - ? `${userInfo.uid}:${userInfo.gid}` - : '1000:1000'; + const assetStagingOptions = { + sourcePath: this.sourcePath, + bundleDir, + ...options, + }; + + switch (options.bundlingFileAccess) { + case BundlingFileAccess.VOLUME_COPY: + new AssetBundlingVolumeCopy(assetStagingOptions).run(); + break; + case BundlingFileAccess.BIND_MOUNT: + default: + new AssetBundlingBindMount(assetStagingOptions).run(); + break; } - - options.image.run({ - command: options.command, - user, - volumes, - environment: options.environment, - entrypoint: options.entrypoint, - workingDirectory: options.workingDirectory ?? AssetStaging.BUNDLING_INPUT_DIR, - securityOpt: options.securityOpt ?? '', - volumesFrom: options.volumesFrom, - }); } } catch (err) { // When bundling fails, keep the bundle output for diagnosability, but @@ -641,3 +623,4 @@ function getExtension(source: string): string { return path.extname(source); }; + diff --git a/packages/@aws-cdk/core/lib/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts index a78f5e8a6aca0..d88a84043a652 100644 --- a/packages/@aws-cdk/core/lib/bundling.ts +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -1,7 +1,8 @@ -import { spawnSync, SpawnSyncOptions } from 'child_process'; +import { spawnSync } from 'child_process'; import * as crypto from 'crypto'; import { isAbsolute, join } from 'path'; import { FileSystem } from './fs'; +import { dockerExec } from './private/asset-staging'; import { quiet, reset } from './private/jsii-deprecated'; /** @@ -108,6 +109,12 @@ export interface BundlingOptions { * @default - no networking options */ readonly network?: string; + + /** + * The access mechanism used to make source files available to the bundling container and to return the bundling output back to the host. + * @default - BundlingFileAccess.BIND_MOUNT + */ + readonly bundlingFileAccess?: BundlingFileAccess; } /** @@ -151,6 +158,23 @@ export interface ILocalBundling { tryBundle(outputDir: string, options: BundlingOptions): boolean; } +/** + * The access mechanism used to make source files available to the bundling container and to return the bundling output back to the host + */ +export enum BundlingFileAccess { + /** + * Creates temporary volumes and containers to copy files from the host to the bundling container and back. + * This is slower, but works also in more complex situations with remote or shared docker sockets. + */ + VOLUME_COPY = 'VOLUME_COPY', + + /** + * The source and output folders will be mounted as bind mount from the host system + * This is faster and simpler, but less portable than `VOLUME_COPY`. + */ + BIND_MOUNT = 'BIND_MOUNT', +} + /** * A Docker image used for asset bundling * @@ -536,30 +560,6 @@ function flatten(x: string[][]) { return Array.prototype.concat([], ...x); } -function dockerExec(args: string[], options?: SpawnSyncOptions) { - const prog = process.env.CDK_DOCKER ?? 'docker'; - const proc = spawnSync(prog, args, options ?? { - stdio: [ // show Docker output - 'ignore', // ignore stdio - process.stderr, // redirect stdout to stderr - 'inherit', // inherit stderr - ], - }); - - if (proc.error) { - throw proc.error; - } - - if (proc.status !== 0) { - if (proc.stdout || proc.stderr) { - throw new Error(`[Status ${proc.status}] stdout: ${proc.stdout?.toString().trim()}\n\n\nstderr: ${proc.stderr?.toString().trim()}`); - } - throw new Error(`${prog} exited with status ${proc.status}`); - } - - return proc; -} - function isSeLinux() : boolean { if (process.platform != 'linux') { return false; diff --git a/packages/@aws-cdk/core/lib/private/asset-staging.ts b/packages/@aws-cdk/core/lib/private/asset-staging.ts new file mode 100644 index 0000000000000..27cfbfc604152 --- /dev/null +++ b/packages/@aws-cdk/core/lib/private/asset-staging.ts @@ -0,0 +1,221 @@ +import { spawnSync, SpawnSyncOptions } from 'child_process'; +import * as crypto from 'crypto'; +import * as os from 'os'; +import { AssetStaging } from '../asset-staging'; +import { BundlingOptions } from '../bundling'; + +/** + * Options for Docker based bundling of assets + */ +interface AssetBundlingOptions extends BundlingOptions { + /** + * Path where the source files are located + */ + readonly sourcePath: string; + /** + * Path where the output files should be stored + */ + readonly bundleDir: string; +} + +abstract class AssetBundlingBase { + protected options: AssetBundlingOptions; + constructor(options: AssetBundlingOptions) { + this.options = options; + } + /** + * Determines a useful default user if not given otherwise + */ + protected determineUser() { + let user: string; + if (this.options.user) { + user = this.options.user; + } else { + // Default to current user + const userInfo = os.userInfo(); + user = + userInfo.uid !== -1 // uid is -1 on Windows + ? `${userInfo.uid}:${userInfo.gid}` + : '1000:1000'; + } + return user; + } +} + +/** + * Bundles files with bind mount as copy method + */ +export class AssetBundlingBindMount extends AssetBundlingBase { + /** + * Bundle files with bind mount as copy method + */ + public run() { + this.options.image.run({ + command: this.options.command, + user: this.determineUser(), + environment: this.options.environment, + entrypoint: this.options.entrypoint, + workingDirectory: + this.options.workingDirectory ?? AssetStaging.BUNDLING_INPUT_DIR, + securityOpt: this.options.securityOpt ?? '', + volumesFrom: this.options.volumesFrom, + volumes: [ + { + hostPath: this.options.sourcePath, + containerPath: AssetStaging.BUNDLING_INPUT_DIR, + }, + { + hostPath: this.options.bundleDir, + containerPath: AssetStaging.BUNDLING_OUTPUT_DIR, + }, + ...(this.options.volumes ?? []), + ], + }); + } +} + +/** + * Provides a helper container for copying bundling related files to specific input and output volumes + */ +export class AssetBundlingVolumeCopy extends AssetBundlingBase { + /** + * Name of the Docker volume that is used for the asset input + */ + private inputVolumeName: string; + /** + * Name of the Docker volume that is used for the asset output + */ + private outputVolumeName: string; + /** + * Name of the Docker helper container to copy files into the volume + */ + public copyContainerName: string; + + constructor(options: AssetBundlingOptions) { + super(options); + const copySuffix = crypto.randomBytes(12).toString('hex'); + this.inputVolumeName = `assetInput${copySuffix}`; + this.outputVolumeName = `assetOutput${copySuffix}`; + this.copyContainerName = `copyContainer${copySuffix}`; + } + + /** + * Creates volumes for asset input and output + */ + private prepareVolumes() { + dockerExec(['volume', 'create', this.inputVolumeName]); + dockerExec(['volume', 'create', this.outputVolumeName]); + } + + /** + * Removes volumes for asset input and output + */ + private cleanVolumes() { + dockerExec(['volume', 'rm', this.inputVolumeName]); + dockerExec(['volume', 'rm', this.outputVolumeName]); + } + + /** + * runs a helper container that holds volumes and does some preparation tasks + * @param user The user that will later access these files and needs permissions to do so + */ + private startHelperContainer(user: string) { + dockerExec([ + 'run', + '--name', + this.copyContainerName, + '-v', + `${this.inputVolumeName}:${AssetStaging.BUNDLING_INPUT_DIR}`, + '-v', + `${this.outputVolumeName}:${AssetStaging.BUNDLING_OUTPUT_DIR}`, + 'alpine', + 'sh', + '-c', + `mkdir -p ${AssetStaging.BUNDLING_INPUT_DIR} && chown -R ${user} ${AssetStaging.BUNDLING_OUTPUT_DIR} && chown -R ${user} ${AssetStaging.BUNDLING_INPUT_DIR}`, + ]); + } + + /** + * removes the Docker helper container + */ + private cleanHelperContainer() { + dockerExec(['rm', this.copyContainerName]); + } + + /** + * copy files from the host where this is executed into the input volume + * @param sourcePath - path to folder where files should be copied from - without trailing slash + */ + private copyInputFrom(sourcePath: string) { + dockerExec([ + 'cp', + `${sourcePath}/.`, + `${this.copyContainerName}:${AssetStaging.BUNDLING_INPUT_DIR}`, + ]); + } + + /** + * copy files from the the output volume to the host where this is executed + * @param outputPath - path to folder where files should be copied to - without trailing slash + */ + private copyOutputTo(outputPath: string) { + dockerExec([ + 'cp', + `${this.copyContainerName}:${AssetStaging.BUNDLING_OUTPUT_DIR}/.`, + outputPath, + ]); + } + + /** + * Bundle files with VOLUME_COPY method + */ + public run() { + const user = this.determineUser(); + this.prepareVolumes(); + this.startHelperContainer(user); // TODO handle user properly + this.copyInputFrom(this.options.sourcePath); + + this.options.image.run({ + command: this.options.command, + user: user, + environment: this.options.environment, + entrypoint: this.options.entrypoint, + workingDirectory: + this.options.workingDirectory ?? AssetStaging.BUNDLING_INPUT_DIR, + securityOpt: this.options.securityOpt ?? '', + volumes: this.options.volumes, + volumesFrom: [ + this.copyContainerName, + ...(this.options.volumesFrom ?? []), + ], + }); + + this.copyOutputTo(this.options.bundleDir); + this.cleanHelperContainer(); + this.cleanVolumes(); + } +} + +export function dockerExec(args: string[], options?: SpawnSyncOptions) { + const prog = process.env.CDK_DOCKER ?? 'docker'; + const proc = spawnSync(prog, args, options ?? { + stdio: [ // show Docker output + 'ignore', // ignore stdio + process.stderr, // redirect stdout to stderr + 'inherit', // inherit stderr + ], + }); + + if (proc.error) { + throw proc.error; + } + + if (proc.status !== 0) { + if (proc.stdout || proc.stderr) { + throw new Error(`[Status ${proc.status}] stdout: ${proc.stdout?.toString().trim()}\n\n\nstderr: ${proc.stderr?.toString().trim()}`); + } + throw new Error(`${prog} exited with status ${proc.status}`); + } + + return proc; +} diff --git a/packages/@aws-cdk/core/test/docker-stub-cp.sh b/packages/@aws-cdk/core/test/docker-stub-cp.sh new file mode 100755 index 0000000000000..fec4009896d63 --- /dev/null +++ b/packages/@aws-cdk/core/test/docker-stub-cp.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -euo pipefail +# stub for the `docker` executable. it is used as CDK_DOCKER when executing unit +# tests in `test.staging.ts` This variant is specific for tests that use the docker copy method for files, instead of bind mounts + +echo "$@" >> /tmp/docker-stub-cp.input.concat +echo "$@" > /tmp/docker-stub-cp.input + +# create a fake zip to emulate created files, fetch the target path from the "docker cp" command +if echo "$@" | grep "cp"| grep "/asset-output"; then + outdir=$(echo "$@" | grep cp | grep "/asset-output" | xargs -n1 | grep "cdk.out" | head -n1 | cut -d":" -f1) + if [ -n "$outdir" ]; then + touch "${outdir}/test.zip" + fi +fi diff --git a/packages/@aws-cdk/core/test/private/asset-staging.test.ts b/packages/@aws-cdk/core/test/private/asset-staging.test.ts new file mode 100644 index 0000000000000..31d316eed30af --- /dev/null +++ b/packages/@aws-cdk/core/test/private/asset-staging.test.ts @@ -0,0 +1,112 @@ +import * as child_process from 'child_process'; +import * as sinon from 'sinon'; +import { AssetStaging, DockerImage } from '../../lib'; +import { AssetBundlingBindMount, AssetBundlingVolumeCopy } from '../../lib/private/asset-staging'; + +describe('bundling', () => { + afterEach(() => { + sinon.restore(); + }); + + test('AssetBundlingVolumeCopy bundles with volume copy ', () => { + // GIVEN + sinon.stub(process, 'platform').value('darwin'); + const spawnSyncStub = sinon.stub(child_process, 'spawnSync').returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + const options = { + sourcePath: '/tmp/source', + bundleDir: '/tmp/output', + image: DockerImage.fromRegistry('alpine'), + user: '1000', + }; + const helper = new AssetBundlingVolumeCopy(options); + helper.run(); + + // volume Creation + expect(spawnSyncStub.calledWith('docker', sinon.match([ + 'volume', 'create', sinon.match(/assetInput.*/g), + ]), { stdio: ['ignore', process.stderr, 'inherit'] })).toEqual(true); + + expect(spawnSyncStub.calledWith('docker', sinon.match([ + 'volume', 'create', sinon.match(/assetOutput.*/g), + ]), { stdio: ['ignore', process.stderr, 'inherit'] })).toEqual(true); + + // volume removal + expect(spawnSyncStub.calledWith('docker', sinon.match([ + 'volume', 'rm', sinon.match(/assetInput.*/g), + ]), { stdio: ['ignore', process.stderr, 'inherit'] })).toEqual(true); + + expect(spawnSyncStub.calledWith('docker', sinon.match([ + 'volume', 'rm', sinon.match(/assetOutput.*/g), + ]), { stdio: ['ignore', process.stderr, 'inherit'] })).toEqual(true); + + // prepare copy container + expect(spawnSyncStub.calledWith('docker', sinon.match([ + 'run', + '--name', sinon.match(/copyContainer.*/g), + '-v', sinon.match(/assetInput.*/g), + '-v', sinon.match(/assetOutput.*/g), + 'alpine', + 'sh', + '-c', + `mkdir -p ${AssetStaging.BUNDLING_INPUT_DIR} && chown -R ${options.user} ${AssetStaging.BUNDLING_OUTPUT_DIR} && chown -R ${options.user} ${AssetStaging.BUNDLING_INPUT_DIR}`, + ]), { stdio: ['ignore', process.stderr, 'inherit'] })).toEqual(true); + + // delete copy container + expect(spawnSyncStub.calledWith('docker', sinon.match([ + 'rm', sinon.match(/copyContainer.*/g), + ]), { stdio: ['ignore', process.stderr, 'inherit'] })).toEqual(true); + + // copy files to copy container + expect(spawnSyncStub.calledWith('docker', sinon.match([ + 'cp', `${options.sourcePath}/.`, `${helper.copyContainerName}:${AssetStaging.BUNDLING_INPUT_DIR}`, + ]), { stdio: ['ignore', process.stderr, 'inherit'] })).toEqual(true); + + // copy files from copy container to host + expect(spawnSyncStub.calledWith('docker', sinon.match([ + 'cp', `${helper.copyContainerName}:${AssetStaging.BUNDLING_OUTPUT_DIR}/.`, options.bundleDir, + ]), { stdio: ['ignore', process.stderr, 'inherit'] })).toEqual(true); + + // actual docker run + expect(spawnSyncStub.calledWith('docker', sinon.match.array.contains([ + 'run', '--rm', + '--volumes-from', helper.copyContainerName, + 'alpine', + ]), { stdio: ['ignore', process.stderr, 'inherit'] })).toEqual(true); + + }); + + test('AssetBundlingBindMount bundles with bind mount ', () => { + // GIVEN + sinon.stub(process, 'platform').value('darwin'); + const spawnSyncStub = sinon.stub(child_process, 'spawnSync').returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + const options = { + sourcePath: '/tmp/source', + bundleDir: '/tmp/output', + image: DockerImage.fromRegistry('alpine'), + user: '1000', + }; + const helper = new AssetBundlingBindMount(options); + helper.run(); + + // actual docker run with bind mount is called + expect(spawnSyncStub.calledWith('docker', sinon.match.array.contains([ + 'run', '--rm', + '-v', + 'alpine', + ]), { stdio: ['ignore', process.stderr, 'inherit'] })).toEqual(true); + }); +}); diff --git a/packages/@aws-cdk/core/test/staging.test.ts b/packages/@aws-cdk/core/test/staging.test.ts index 16e14f492adaf..cbde1eaf0fc26 100644 --- a/packages/@aws-cdk/core/test/staging.test.ts +++ b/packages/@aws-cdk/core/test/staging.test.ts @@ -5,17 +5,21 @@ import { FileAssetPackaging } from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; import * as sinon from 'sinon'; -import { App, AssetHashType, AssetStaging, DockerImage, BundlingOptions, BundlingOutput, FileSystem, Stack, Stage } from '../lib'; +import { App, AssetHashType, AssetStaging, DockerImage, BundlingOptions, BundlingOutput, FileSystem, Stack, Stage, BundlingFileAccess } from '../lib'; const STUB_INPUT_FILE = '/tmp/docker-stub.input'; const STUB_INPUT_CONCAT_FILE = '/tmp/docker-stub.input.concat'; +const STUB_INPUT_CP_FILE = '/tmp/docker-stub-cp.input'; +const STUB_INPUT_CP_CONCAT_FILE = '/tmp/docker-stub-cp.input.concat'; + enum DockerStubCommand { SUCCESS = 'DOCKER_STUB_SUCCESS', FAIL = 'DOCKER_STUB_FAIL', SUCCESS_NO_OUTPUT = 'DOCKER_STUB_SUCCESS_NO_OUTPUT', MULTIPLE_FILES = 'DOCKER_STUB_MULTIPLE_FILES', SINGLE_ARCHIVE = 'DOCKER_STUB_SINGLE_ARCHIVE', + VOLUME_SINGLE_ARCHIVE = 'DOCKER_STUB_VOLUME_SINGLE_ARCHIVE', } const FIXTURE_TEST1_DIR = path.join(__dirname, 'fs', 'fixtures', 'test1'); @@ -27,10 +31,16 @@ const ARCHIVE_TARBALL_TEST_HASH = '3e948ff54a277d6001e2452fdbc4a9ef61f916ff662ba const userInfo = os.userInfo(); const USER_ARG = `-u ${userInfo.uid}:${userInfo.gid}`; -// this is a way to provide a custom "docker" command for staging. -process.env.CDK_DOCKER = `${__dirname}/docker-stub.sh`; describe('staging', () => { + beforeAll(() => { + // this is a way to provide a custom "docker" command for staging. + process.env.CDK_DOCKER = `${__dirname}/docker-stub.sh`; + }); + + afterAll(() => { + delete process.env.CDK_DOCKER; + }); afterEach(() => { AssetStaging.clearAssetHashCache(); @@ -1252,6 +1262,73 @@ describe('staging', () => { }); }); +describe('staging with docker cp', () => { + beforeAll(() => { + // this is a way to provide a custom "docker" command for staging. + process.env.CDK_DOCKER = `${__dirname}/docker-stub-cp.sh`; + }); + + afterAll(() => { + delete process.env.CDK_DOCKER; + }); + + afterEach(() => { + AssetStaging.clearAssetHashCache(); + if (fs.existsSync(STUB_INPUT_CP_FILE)) { + fs.unlinkSync(STUB_INPUT_CP_FILE); + } + if (fs.existsSync(STUB_INPUT_CP_CONCAT_FILE)) { + fs.unlinkSync(STUB_INPUT_CP_CONCAT_FILE); + } + sinon.restore(); + }); + + test('bundling with docker image copy variant', () => { + // GIVEN + const app = new App({ context: { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false } }); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + const staging = new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: DockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.VOLUME_SINGLE_ARCHIVE], + bundlingFileAccess: BundlingFileAccess.VOLUME_COPY, + }, + }); + + // THEN + const assembly = app.synth(); + expect(fs.readdirSync(assembly.directory)).toEqual([ + 'asset.0ec371a2022d29dfd83f5df104e0f01b34233a4e3e839c3c4ec62008f0b9a0e8', // this is the bundle dir + 'asset.0ec371a2022d29dfd83f5df104e0f01b34233a4e3e839c3c4ec62008f0b9a0e8.zip', + 'cdk.out', + 'manifest.json', + 'stack.template.json', + 'tree.json', + ]); + expect(fs.readdirSync(path.join(assembly.directory, 'asset.0ec371a2022d29dfd83f5df104e0f01b34233a4e3e839c3c4ec62008f0b9a0e8'))).toEqual([ + 'test.zip', // bundle dir with "touched" bundled output file + ]); + expect(staging.packaging).toEqual(FileAssetPackaging.FILE); + expect(staging.isArchive).toEqual(true); + const dockerCalls: string[] = readDockerStubInputConcat(STUB_INPUT_CP_CONCAT_FILE).split(/\r?\n/); + expect(dockerCalls).toEqual(expect.arrayContaining([ + expect.stringContaining('volume create assetInput'), + expect.stringContaining('volume create assetOutput'), + expect.stringMatching('run --name copyContainer.* -v /input:/asset-input -v /output:/asset-output alpine sh -c mkdir -p /asset-input && chown -R .* /asset-output && chown -R .* /asset-input'), + expect.stringMatching('cp .*fs/fixtures/test1/\. copyContainer.*:/asset-input'), + expect.stringMatching('run --rm -u .* --volumes-from copyContainer.* -w /asset-input alpine DOCKER_STUB_VOLUME_SINGLE_ARCHIVE'), + expect.stringMatching('cp copyContainer.*:/asset-output/\. .*'), + expect.stringContaining('rm copyContainer'), + expect.stringContaining('volume rm assetInput'), + expect.stringContaining('volume rm assetOutput'), + ])); + }); +}); + // Reads a docker stub and cleans the volume paths out of the stub. function readAndCleanDockerStubInput(file: string) { return fs @@ -1262,10 +1339,10 @@ function readAndCleanDockerStubInput(file: string) { } // Last docker input since last teardown -function readDockerStubInput() { - return readAndCleanDockerStubInput(STUB_INPUT_FILE); +function readDockerStubInput(file?: string) { + return readAndCleanDockerStubInput(file ?? STUB_INPUT_FILE); } // Concatenated docker inputs since last teardown -function readDockerStubInputConcat() { - return readAndCleanDockerStubInput(STUB_INPUT_CONCAT_FILE); +function readDockerStubInputConcat(file?: string) { + return readAndCleanDockerStubInput(file ?? STUB_INPUT_CONCAT_FILE); }