diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index d678505444d77..d7c8f96da323a 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -7,7 +7,7 @@ import { debug, error, print } from '../logging'; import { toYAML } from '../serialize'; import { Mode } from './aws-auth/credentials'; import { ToolkitInfo } from './toolkit-info'; -import { describeStack, stackExists, stackFailedCreating, waitForChangeSet, waitForStack } from './util/cloudformation'; +import { changeSetHasNoChanges, describeStack, stackExists, stackFailedCreating, waitForChangeSet, waitForStack } from './util/cloudformation'; import { StackActivityMonitor } from './util/cloudformation/stack-activity-monitor'; import { StackStatus } from './util/cloudformation/stack-status'; import { SDK } from './util/sdk'; @@ -77,8 +77,9 @@ export async function deployStack(options: DeployStackOptions): Promise(valueProvider: () => Promise, ti /** * Waits for a ChangeSet to be available for triggering a StackUpdate. * + * Will return a changeset that is either ready to be executed or has no changes. + * Will throw in other cases. + * * @param cfn a CloudFormation client * @param stackName the name of the Stack that the ChangeSet belongs to * @param changeSetName the name of the ChangeSet @@ -93,25 +96,43 @@ async function waitFor(valueProvider: () => Promise, ti * @returns the CloudFormation description of the ChangeSet */ // tslint:disable-next-line:max-line-length -export async function waitForChangeSet(cfn: CloudFormation, stackName: string, changeSetName: string): Promise { +export async function waitForChangeSet(cfn: CloudFormation, stackName: string, changeSetName: string): Promise { debug('Waiting for changeset %s on stack %s to finish creating...', changeSetName, stackName); - return waitFor(async () => { + const ret = await waitFor(async () => { const description = await describeChangeSet(cfn, stackName, changeSetName); // The following doesn't use a switch because tsc will not allow fall-through, UNLESS it is allows // EVERYWHERE that uses this library directly or indirectly, which is undesirable. if (description.Status === 'CREATE_PENDING' || description.Status === 'CREATE_IN_PROGRESS') { debug('Changeset %s on stack %s is still creating', changeSetName, stackName); return undefined; - } else if (description.Status === 'CREATE_COMPLETE') { + } + + if (description.Status === 'CREATE_COMPLETE' || changeSetHasNoChanges(description)) { return description; - } else if (description.Status === 'FAILED') { - if (description.StatusReason && description.StatusReason.startsWith('The submitted information didn\'t contain changes.')) { - return description; - } } + // tslint:disable-next-line:max-line-length throw new Error(`Failed to create ChangeSet ${changeSetName} on ${stackName}: ${description.Status || 'NO_STATUS'}, ${description.StatusReason || 'no reason provided'}`); }); + + if (!ret) { + throw new Error('Change set took too long to be created; aborting'); + } + + return ret; +} + +/** + * Return true if the given change set has no changes + * + * This must be determined from the status, not the 'Changes' array on the + * object; the latter can be empty because no resources were changed, but if + * there are changes to Outputs, the change set can still be executed. + */ +export function changeSetHasNoChanges(description: CloudFormation.DescribeChangeSetOutput) { + return description.Status === 'FAILED' + && description.StatusReason + && description.StatusReason.startsWith('The submitted information didn\'t contain changes.'); } /** diff --git a/packages/aws-cdk/package-lock.json b/packages/aws-cdk/package-lock.json index bff15d42a0e8c..5afca94ce20d0 100644 --- a/packages/aws-cdk/package-lock.json +++ b/packages/aws-cdk/package-lock.json @@ -4,6 +4,42 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@sinonjs/commons": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", + "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", + "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.0.tgz", + "integrity": "sha512-beHeJM/RRAaLLsMJhsCvHK31rIqZuobfPLa/80yGH5hnD8PV1hyh9xJBJNFfNmO7yWqm+zomijHsXpI6iTQJfQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.0.2", + "array-from": "^2.1.1", + "lodash": "^4.17.11" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "@types/archiver": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-2.1.2.tgz", @@ -62,7 +98,7 @@ }, "@types/mockery": { "version": "1.4.29", - "resolved": "https://registry.npmjs.org/@types/mockery/-/mockery-1.4.29.tgz", + "resolved": "http://registry.npmjs.org/@types/mockery/-/mockery-1.4.29.tgz", "integrity": "sha1-m6It838H43gP/4Ux0aOOYz+UV6U=", "dev": true }, @@ -89,6 +125,12 @@ "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", "dev": true }, + "@types/sinon": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.0.10.tgz", + "integrity": "sha512-4w7SvsiUOtd4mUfund9QROPSJ5At/GQskDpqd87pJIRI6ULWSJqHI3GIZE337wQuN3aznroJGr94+o8fwvL37Q==", + "dev": true + }, "@types/table": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/table/-/table-4.0.5.tgz", @@ -182,6 +224,12 @@ "readable-stream": "^2.0.0" } }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -281,7 +329,7 @@ }, "bl": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "requires": { "readable-stream": "^2.3.5", @@ -357,7 +405,7 @@ }, "cli-color": { "version": "0.1.7", - "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-0.1.7.tgz", + "resolved": "http://registry.npmjs.org/cli-color/-/cli-color-0.1.7.tgz", "integrity": "sha1-rcMgD6RxzCEbDaf1ZrcemLnWc0c=", "requires": { "es5-ext": "0.8.x" @@ -388,7 +436,7 @@ }, "string-width": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "resolved": "http://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { "code-point-at": "^1.0.0", @@ -398,7 +446,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" @@ -547,6 +595,12 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, "difflib": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", @@ -600,7 +654,7 @@ }, "es6-promisify": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "resolved": "http://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", "requires": { "es6-promise": "^4.0.3" @@ -635,7 +689,7 @@ }, "events": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz", "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" }, "execa": { @@ -741,7 +795,7 @@ }, "readable-stream": { "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "requires": { "core-util-is": "~1.0.0", @@ -752,7 +806,7 @@ }, "string_decoder": { "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, "xregexp": { @@ -769,7 +823,7 @@ }, "get-stream": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" }, "get-uri": { @@ -845,6 +899,12 @@ "har-schema": "^2.0.0" } }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, "heap": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.6.tgz", @@ -857,7 +917,7 @@ }, "http-errors": { "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "requires": { "depd": "~1.1.2", @@ -953,7 +1013,7 @@ }, "is-builtin-module": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", "requires": { "builtin-modules": "^1.0.0" @@ -1001,7 +1061,7 @@ }, "json-diff": { "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-diff/-/json-diff-0.3.1.tgz", + "resolved": "http://registry.npmjs.org/json-diff/-/json-diff-0.3.1.tgz", "integrity": "sha1-bbw64tJeB1p/1xvNmHRFhmb7aBs=", "requires": { "cli-color": "~0.1.6", @@ -1043,6 +1103,12 @@ "verror": "1.10.0" } }, + "just-extend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "dev": true + }, "lazystream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", @@ -1070,7 +1136,7 @@ }, "load-json-file": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", "requires": { "graceful-fs": "^4.1.2", @@ -1093,6 +1159,12 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, + "lolex": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-3.1.0.tgz", + "integrity": "sha512-zFo5MgCJ0rZ7gQg69S4pqBsLURbFw11X68C18OcJjJQbqaXm2NoTrGl1IMM3TIz0/BnN1tIs2tzmmqvCsOMMjw==", + "dev": true + }, "lru-cache": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", @@ -1157,6 +1229,27 @@ "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=" }, + "nise": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.10.tgz", + "integrity": "sha512-sa0RRbj53dovjc7wombHmVli9ZihXbXCQ2uH3TNm03DyvOSIQbxg+pbqDKrk2oxMK1rtLGVlKxcB9rrc6X5YjA==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^3.1.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^2.3.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "lolex": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", + "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", + "dev": true + } + } + }, "normalize-package-data": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.2.tgz", @@ -1293,7 +1386,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { @@ -1301,6 +1394,23 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, "path-type": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", @@ -1316,7 +1426,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" }, "prelude-ls": { @@ -1422,7 +1532,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -1488,7 +1598,7 @@ }, "sax": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "resolved": "http://registry.npmjs.org/sax/-/sax-1.2.1.tgz", "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" }, "semver": { @@ -1524,6 +1634,21 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, + "sinon": { + "version": "7.2.7", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.2.7.tgz", + "integrity": "sha512-rlrre9F80pIQr3M36gOdoCEWzFAMDgHYD8+tocqOw+Zw9OZ8F84a80Ds69eZfcjnzDqqG88ulFld0oin/6rG/g==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.3.1", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.2.0", + "diff": "^3.5.0", + "lolex": "^3.1.0", + "nise": "^1.4.10", + "supports-color": "^5.5.0" + } + }, "slice-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", @@ -1631,7 +1756,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -1652,9 +1777,18 @@ }, "strip-eof": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, "table": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/table/-/table-5.2.2.tgz", @@ -1727,6 +1861,12 @@ "prelude-ls": "~1.1.2" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -1810,7 +1950,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "requires": { "string-width": "^1.0.1", @@ -1832,7 +1972,7 @@ }, "string-width": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "resolved": "http://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { "code-point-at": "^1.0.0", @@ -1842,7 +1982,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" @@ -1866,7 +2006,7 @@ }, "xmlbuilder": { "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" }, "xregexp": { diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 23aa0a719a6c2..7a9d76df37604 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -42,8 +42,10 @@ "@types/uuid": "^3.4.3", "@types/yaml": "^1.0.0", "@types/yargs": "^8.0.3", + "@types/sinon": "^7.0.10", "cdk-build-tools": "^0.25.3", "mockery": "^2.1.0", + "sinon": "^7.2.7", "pkglint": "^0.25.3" }, "dependencies": { diff --git a/packages/aws-cdk/test/api/test.deploy-stack.ts b/packages/aws-cdk/test/api/test.deploy-stack.ts new file mode 100644 index 0000000000000..ef5e4ed614e7a --- /dev/null +++ b/packages/aws-cdk/test/api/test.deploy-stack.ts @@ -0,0 +1,55 @@ +import { Test } from 'nodeunit'; +import { deployStack } from '../../lib'; +import { MockSDK } from '../util/mock-sdk'; + +const FAKE_STACK = { + name: 'withouterrors', + template: { resource: 'noerrorresource' }, + environment: { name: 'dev', account: '12345', region: 'here' }, + metadata: {}, +}; + +export = { + async 'do deploy executable change set with 0 changes'(test: Test) { + // GIVEN + const sdk = new MockSDK(); + + let executed = false; + + sdk.stubCloudFormation({ + describeStacks() { + return { + Stacks: [] + }; + }, + + createChangeSet() { + return {}; + }, + + describeChangeSet() { + return { + Status: 'CREATE_COMPLETE', + Changes: [], + }; + }, + + executeChangeSet() { + executed = true; + return {}; + } + }); + + // WHEN + const ret = await deployStack({ + stack: FAKE_STACK, + sdk, + }); + + // THEN + test.equals(ret.noOp, false); + test.equals(executed, true); + + test.done(); + }, +}; diff --git a/packages/aws-cdk/test/util/mock-sdk.ts b/packages/aws-cdk/test/util/mock-sdk.ts new file mode 100644 index 0000000000000..008e44904f5df --- /dev/null +++ b/packages/aws-cdk/test/util/mock-sdk.ts @@ -0,0 +1,100 @@ +import AWS = require('aws-sdk'); +import sinon = require('sinon'); +import { SDK } from "../../lib/api/util/sdk"; + +/** + * An SDK that allows replacing (some of) the clients + * + * Its the responsibility of the consumer to replace all calls that + * actually will be called. + */ +export class MockSDK extends SDK { + private readonly sandbox: sinon.SinonSandbox; + constructor() { + super(); + this.sandbox = sinon.createSandbox(); + } + + /** + * Replace the CloudFormation client with the given object + */ + public stubCloudFormation(stubs: SyncHandlerSubsetOf) { + this.sandbox.stub(this, 'cloudFormation').returns(Promise.resolve(partialAwsService(stubs))); + } +} + +/** + * Wrap synchronous fake handlers so that they sort-of function like a real AWS client + * + * For example, turns an object like this: + * + * ```ts + * { + * someCall(opts: AWS.Service.SomeCallInput): AWS.Service.SomeCallOutput { + * return {...whatever...}; + * } + * } + * ``` + * + * Into an object that in the type system pretends to be an 'AWS.Service' + * class (even though it really isn't) and can be called like this: + * + * ```ts + * const service = await sdk.someService(...); + * const response = await service.someCall(...).promise(); + * ``` + * + * We only implement the narrow subset of the AWS SDK API that the CDK actually + * uses, and we cheat on the types to make TypeScript happy on the rest of the API. + * + * Most important feature of this class is that it will derive the input and output + * types of the handlers on the input object from the ACTUAL AWS Service class, + * so that you don't have to declare them. + */ +function partialAwsService(fns: SyncHandlerSubsetOf): S { + // Super unsafe in here because I don't know how to make TypeScript happy, + // but at least the outer types make sure everything that happens in here works out. + const ret: any = {}; + + for (const [key, handler] of Object.entries(fns)) { + ret[key] = (args: any) => new FakeAWSResponse((handler as any)(args)); + } + + return ret; +} + +// Because of the overloads an AWS handler type looks like this: +// +// { +// (params: INPUTSTRUCT, callback?: ((err: AWSError, data: {}) => void) | undefined): Request; +// (callback?: ((err: AWS.AWSError, data: {}) => void) | undefined): AWS.Request<...>; +// } +// +// Get the first overload and extract the input and output struct types +type AwsCallInputOutput = + T extends { + (args: infer INPUT, callback?: ((err: AWS.AWSError, data: any) => void) | undefined): AWS.Request; + (callback?: ((err: AWS.AWSError, data: {}) => void) | undefined): AWS.Request; + } ? [INPUT, OUTPUT] : never; + +// Determine the type of the mock handler from the type of the Input/Output type pair. +// Don't need to worry about the 'never', TypeScript will propagate it upwards making it +// impossible to specify the field that has 'never' anywhere in its type. +type MockHandlerType = (input: AI[0]) => AI[1]; + +// Any subset of the full type that synchronously returns the output structure is okay +type SyncHandlerSubsetOf = {[K in keyof S]?: MockHandlerType>}; + +/** + * Fake AWS response. + * + * We only ever 'await response.promise()' so that's the only thing we implement here. + */ +class FakeAWSResponse { + constructor(private readonly x: T) { + } + + public promise(): Promise { + return Promise.resolve(this.x); + } +}