@liflig/cdk-snapshot 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![license](https://img.shields.io/npm/l/@liflig/cdk-snapshot.svg)](LICENSE)
7
7
 
8
8
  Snapshot testing for AWS CDK stacks. A stack is synthesized to CloudFormation and
9
- normalized before it is snapshotted. The CDK bootstrap version is always dropped.
9
+ normalized before it is snapshotted. The CDK bootstrap version is dropped by default.
10
10
  Asset hashes, Lambda version suffixes and CDK Pipelines asset IDs change whenever
11
11
  an asset's content does; the [options](#options) mask them, so a snapshot fails only
12
12
  when the infrastructure itself changed.
@@ -137,6 +137,7 @@ correct the count.
137
137
  | Option | Type | Default | Effect |
138
138
  | --- | --- | --- | --- |
139
139
  | `ignoreAssets` | `boolean` | `false` | Replaces every `Code` property, every container definition's `Image` and the whole `Parameters` block with `Any<Object>` |
140
+ | `ignoreAssetHashes` | `boolean` | `false` | Replaces the hash of every asset in the app with `<ASSET_HASH>`, wherever it appears |
140
141
  | `ignoreBootstrapVersion` | `boolean` | `true` | Drops the `BootstrapVersion` parameter and its check rule |
141
142
  | `ignoreCurrentVersion` | `boolean` | `false` | Masks the content hash on Lambda `CurrentVersion` logical IDs and every reference to them |
142
143
  | `ignoreMetadata` | `boolean` | `false` | Drops template and resource `Metadata` |
@@ -158,11 +159,29 @@ only if it matches both.
158
159
  `Code.fromBucket` key or to a registry image tag such as `nginx:1.27` does not show.
159
160
  - Assets outside Lambda `Code` and container images keep their hash: Lambda layers,
160
161
  `BucketDeployment` sources, Step Functions and API Gateway definitions read from files,
161
- and nested stack templates.
162
+ and nested stack templates. `ignoreAssetHashes` covers them.
162
163
  - A function's `currentVersion` logical ID is a hash over its configuration, code
163
164
  included, so a stack that uses it also needs `ignoreCurrentVersion` to stay stable.
164
165
  - It does nothing to a template with no `Resources`.
165
166
 
167
+ `ignoreAssetHashes` is the precise alternative. It reads the hash of every file and
168
+ container image asset from the asset manifests the app synthesizes, CDK Pipelines stages
169
+ included, and replaces exactly those hashes wherever a string in the template holds one:
170
+
171
+ ```diff
172
+ "Code": {
173
+ "S3Bucket": "cdk-hnb659fds-assets-112233445566-eu-west-1",
174
+ - "S3Key": "9b8fce7ae7f25ef82fdbaf6b72523b99d0875c0c9c826642819fb51f11d9b125.zip",
175
+ + "S3Key": "<ASSET_HASH>.zip",
176
+ },
177
+ ```
178
+
179
+ Everything else stays visible: the stack's parameters, inline code, registry image tags
180
+ and any hash that belongs to no asset. A function using `currentVersion` still needs
181
+ `ignoreCurrentVersion`, and a CDK Pipeline still needs `ignorePipelineAssets` for its
182
+ destination suffixes, which are not asset hashes. The hashes come from the asset
183
+ manifests that CDK's default synthesizer writes.
184
+
166
185
  `ignoreTags` drops the `Tags` property of each resource. Tags nested deeper stay, such as
167
186
  those `Tags.of()` propagates into a launch template's `TagSpecifications`.
168
187
 
@@ -176,8 +195,8 @@ its own `expect`.
176
195
 
177
196
  ## How it works
178
197
 
179
- Everything is built around one pure function, `cdkTemplate`, which turns a stack into a
180
- normalized template object. Each runner gets a thin adapter that wraps that function in
198
+ Everything is built around one function, `cdkTemplate`, which synthesizes a stack and
199
+ hands the template to a pure normalizer. Each runner gets a thin adapter that wraps that function in
181
200
  whatever the runner's own snapshot assertion looks like, so snapshots keep the naming and
182
201
  format that runner already produces.
183
202
 
@@ -0,0 +1,11 @@
1
+ import { type Stack } from "aws-cdk-lib";
2
+ /**
3
+ * The IDs of every file and container image asset in the cloud assembly
4
+ * `stack` belongs to, nested assemblies such as CDK Pipelines stages included.
5
+ * Under CDK's default synthesizer an asset's ID is the hash it is published
6
+ * under.
7
+ *
8
+ * Synthesis is cached per stage, so this reads the assembly the stack's
9
+ * template came from.
10
+ */
11
+ export declare function assetHashes(stack: Stack): Set<string>;
package/lib/assets.js ADDED
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { Stage } from "aws-cdk-lib";
3
+ /**
4
+ * The IDs of every file and container image asset in the cloud assembly
5
+ * `stack` belongs to, nested assemblies such as CDK Pipelines stages included.
6
+ * Under CDK's default synthesizer an asset's ID is the hash it is published
7
+ * under.
8
+ *
9
+ * Synthesis is cached per stage, so this reads the assembly the stack's
10
+ * template came from.
11
+ */
12
+ export function assetHashes(stack) {
13
+ const hashes = new Set();
14
+ const stage = Stage.of(stack);
15
+ if (stage)
16
+ collect(stage.synth(), hashes);
17
+ return hashes;
18
+ }
19
+ // Artifacts are told apart by manifest type rather than instanceof, which
20
+ // fails when the app was built with a different copy of aws-cdk-lib.
21
+ function collect(assembly, hashes) {
22
+ for (const artifact of assembly.artifacts) {
23
+ switch (artifact.manifest.type) {
24
+ case "cdk:asset-manifest": {
25
+ const { file } = artifact;
26
+ const { files = {}, dockerImages = {} } = JSON.parse(readFileSync(file, "utf8"));
27
+ for (const id of Object.keys({ ...files, ...dockerImages })) {
28
+ hashes.add(id);
29
+ }
30
+ break;
31
+ }
32
+ case "cdk:cloud-assembly": {
33
+ const { nestedAssembly } = artifact;
34
+ collect(nestedAssembly, hashes);
35
+ break;
36
+ }
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,11 @@
1
+ import { type Stack } from "aws-cdk-lib";
2
+ /**
3
+ * The IDs of every file and container image asset in the cloud assembly
4
+ * `stack` belongs to, nested assemblies such as CDK Pipelines stages included.
5
+ * Under CDK's default synthesizer an asset's ID is the hash it is published
6
+ * under.
7
+ *
8
+ * Synthesis is cached per stage, so this reads the assembly the stack's
9
+ * template came from.
10
+ */
11
+ export declare function assetHashes(stack: Stack): Set<string>;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assetHashes = assetHashes;
4
+ const node_fs_1 = require("node:fs");
5
+ const aws_cdk_lib_1 = require("aws-cdk-lib");
6
+ /**
7
+ * The IDs of every file and container image asset in the cloud assembly
8
+ * `stack` belongs to, nested assemblies such as CDK Pipelines stages included.
9
+ * Under CDK's default synthesizer an asset's ID is the hash it is published
10
+ * under.
11
+ *
12
+ * Synthesis is cached per stage, so this reads the assembly the stack's
13
+ * template came from.
14
+ */
15
+ function assetHashes(stack) {
16
+ const hashes = new Set();
17
+ const stage = aws_cdk_lib_1.Stage.of(stack);
18
+ if (stage)
19
+ collect(stage.synth(), hashes);
20
+ return hashes;
21
+ }
22
+ // Artifacts are told apart by manifest type rather than instanceof, which
23
+ // fails when the app was built with a different copy of aws-cdk-lib.
24
+ function collect(assembly, hashes) {
25
+ for (const artifact of assembly.artifacts) {
26
+ switch (artifact.manifest.type) {
27
+ case "cdk:asset-manifest": {
28
+ const { file } = artifact;
29
+ const { files = {}, dockerImages = {} } = JSON.parse((0, node_fs_1.readFileSync)(file, "utf8"));
30
+ for (const id of Object.keys({ ...files, ...dockerImages })) {
31
+ hashes.add(id);
32
+ }
33
+ break;
34
+ }
35
+ case "cdk:cloud-assembly": {
36
+ const { nestedAssembly } = artifact;
37
+ collect(nestedAssembly, hashes);
38
+ break;
39
+ }
40
+ }
41
+ }
42
+ }
package/lib/cjs/index.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.anyObject = void 0;
4
4
  exports.cdkTemplate = cdkTemplate;
5
5
  const assertions_1 = require("aws-cdk-lib/assertions");
6
+ const assets_js_1 = require("./assets.js");
6
7
  const normalize_js_1 = require("./normalize.js");
7
8
  var placeholder_js_1 = require("./placeholder.js");
8
9
  Object.defineProperty(exports, "anyObject", { enumerable: true, get: function () { return placeholder_js_1.anyObject; } });
@@ -16,5 +17,9 @@ Object.defineProperty(exports, "anyObject", { enumerable: true, get: function ()
16
17
  * Bun users should import this from `@liflig/cdk-snapshot/bun` instead.
17
18
  */
18
19
  function cdkTemplate(stack, options = {}) {
19
- return (0, normalize_js_1.normalize)(assertions_1.Template.fromStack(stack).toJSON(), options);
20
+ const template = assertions_1.Template.fromStack(stack).toJSON();
21
+ const hashes = options.ignoreAssetHashes
22
+ ? (0, assets_js_1.assetHashes)(stack)
23
+ : new Set();
24
+ return (0, normalize_js_1.normalize)(template, options, hashes);
20
25
  }
@@ -9,5 +9,8 @@ export type Template = Record<string, any>;
9
9
  *
10
10
  * Step order is significant: earlier steps can remove structures that later
11
11
  * ones inspect.
12
+ *
13
+ * `assetHashes` are the hashes {@link CdkTemplateOptions.ignoreAssetHashes}
14
+ * masks, as read from the stack's cloud assembly.
12
15
  */
13
- export declare function normalize(template: Template, options?: CdkTemplateOptions): Template;
16
+ export declare function normalize(template: Template, options?: CdkTemplateOptions, assetHashes?: ReadonlySet<string>): Template;
@@ -5,7 +5,9 @@ const placeholder_js_1 = require("./placeholder.js");
5
5
  const currentVersionRegex = /^(.+CurrentVersion[0-9A-F]{8})[0-9a-f]{32}$/;
6
6
  const pipelineCdkAssetsRegex = /cdk-assets\s+--path\s+\\"([^\\/]+)\/.+?assets\.json\\"\s+--verbose\s+publish\s+\\"(.+?)\\"/g;
7
7
  const assetDestinationRegex = /:(.*?)(?:-[0-9a-f]{8})?$/;
8
+ const assetHashRegex = /(?<![0-9a-f])[0-9a-f]{64}(?![0-9a-f])/g;
8
9
  const maskedVersionSuffix = "x".repeat(32);
10
+ const maskedAssetHash = "<ASSET_HASH>";
9
11
  /**
10
12
  * Returns a copy of `template` with the configured normalizations applied. The
11
13
  * argument is left untouched: `Template.fromStack` hands out the assembly's
@@ -14,14 +16,19 @@ const maskedVersionSuffix = "x".repeat(32);
14
16
  *
15
17
  * Step order is significant: earlier steps can remove structures that later
16
18
  * ones inspect.
19
+ *
20
+ * `assetHashes` are the hashes {@link CdkTemplateOptions.ignoreAssetHashes}
21
+ * masks, as read from the stack's cloud assembly.
17
22
  */
18
- function normalize(template, options = {}) {
19
- const { ignoreAssets = false, ignoreBootstrapVersion = true, ignoreCurrentVersion = false, ignoreMetadata = false, ignoreTags = false, ignorePipelineAssets = false, subsetResourceTypes, subsetResourceKeys, assetPlaceholder = placeholder_js_1.anyObject, } = options;
23
+ function normalize(template, options = {}, assetHashes = new Set()) {
24
+ const { ignoreAssets = false, ignoreAssetHashes = false, ignoreBootstrapVersion = true, ignoreCurrentVersion = false, ignoreMetadata = false, ignoreTags = false, ignorePipelineAssets = false, subsetResourceTypes, subsetResourceKeys, assetPlaceholder = placeholder_js_1.anyObject, } = options;
20
25
  const result = structuredClone(template);
21
26
  if (ignoreBootstrapVersion)
22
27
  stripBootstrapVersion(result);
23
28
  if (ignoreAssets)
24
29
  stripAssets(result, assetPlaceholder);
30
+ if (ignoreAssetHashes)
31
+ maskAssetHashes(result, assetHashes);
25
32
  if (ignoreCurrentVersion)
26
33
  maskCurrentVersions(result);
27
34
  if (ignorePipelineAssets)
@@ -69,6 +76,13 @@ function stripAssets(template, placeholder) {
69
76
  }
70
77
  }
71
78
  }
79
+ /**
80
+ * Only a standalone 64-hex run is a candidate, so a longer hex string that
81
+ * happens to contain an asset hash is left intact.
82
+ */
83
+ function maskAssetHashes(tree, hashes) {
84
+ transformStrings(tree, (value) => value.replace(assetHashRegex, (hash) => hashes.has(hash) ? maskedAssetHash : hash));
85
+ }
72
86
  function maskCurrentVersions(tree) {
73
87
  transformStrings(tree, (value) => {
74
88
  const match = currentVersionRegex.exec(value);
@@ -10,6 +10,18 @@ export interface CdkTemplateOptions {
10
10
  * logical ID hashes the code.
11
11
  */
12
12
  ignoreAssets?: boolean;
13
+ /**
14
+ * Replace the hash of every asset in the app with `<ASSET_HASH>`, wherever
15
+ * a string in the template holds it: Lambda code and layers, container
16
+ * images, `BucketDeployment` sources, nested stack templates, CDK Pipelines
17
+ * commands. The rest of each value stays, and values that are not assets
18
+ * are untouched.
19
+ *
20
+ * The hashes are read from the asset manifests CDK's default synthesizer
21
+ * writes. A function using `currentVersion` also needs
22
+ * {@link CdkTemplateOptions.ignoreCurrentVersion}.
23
+ */
24
+ ignoreAssetHashes?: boolean;
13
25
  /**
14
26
  * Drop the CDK-managed `BootstrapVersion` parameter and its check rule.
15
27
  * Defaults to `true`.
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Template } from "aws-cdk-lib/assertions";
2
+ import { assetHashes } from "./assets.js";
2
3
  import { normalize } from "./normalize.js";
3
4
  export { anyObject } from "./placeholder.js";
4
5
  /**
@@ -11,5 +12,9 @@ export { anyObject } from "./placeholder.js";
11
12
  * Bun users should import this from `@liflig/cdk-snapshot/bun` instead.
12
13
  */
13
14
  export function cdkTemplate(stack, options = {}) {
14
- return normalize(Template.fromStack(stack).toJSON(), options);
15
+ const template = Template.fromStack(stack).toJSON();
16
+ const hashes = options.ignoreAssetHashes
17
+ ? assetHashes(stack)
18
+ : new Set();
19
+ return normalize(template, options, hashes);
15
20
  }
@@ -9,5 +9,8 @@ export type Template = Record<string, any>;
9
9
  *
10
10
  * Step order is significant: earlier steps can remove structures that later
11
11
  * ones inspect.
12
+ *
13
+ * `assetHashes` are the hashes {@link CdkTemplateOptions.ignoreAssetHashes}
14
+ * masks, as read from the stack's cloud assembly.
12
15
  */
13
- export declare function normalize(template: Template, options?: CdkTemplateOptions): Template;
16
+ export declare function normalize(template: Template, options?: CdkTemplateOptions, assetHashes?: ReadonlySet<string>): Template;
package/lib/normalize.js CHANGED
@@ -2,7 +2,9 @@ import { anyObject } from "./placeholder.js";
2
2
  const currentVersionRegex = /^(.+CurrentVersion[0-9A-F]{8})[0-9a-f]{32}$/;
3
3
  const pipelineCdkAssetsRegex = /cdk-assets\s+--path\s+\\"([^\\/]+)\/.+?assets\.json\\"\s+--verbose\s+publish\s+\\"(.+?)\\"/g;
4
4
  const assetDestinationRegex = /:(.*?)(?:-[0-9a-f]{8})?$/;
5
+ const assetHashRegex = /(?<![0-9a-f])[0-9a-f]{64}(?![0-9a-f])/g;
5
6
  const maskedVersionSuffix = "x".repeat(32);
7
+ const maskedAssetHash = "<ASSET_HASH>";
6
8
  /**
7
9
  * Returns a copy of `template` with the configured normalizations applied. The
8
10
  * argument is left untouched: `Template.fromStack` hands out the assembly's
@@ -11,14 +13,19 @@ const maskedVersionSuffix = "x".repeat(32);
11
13
  *
12
14
  * Step order is significant: earlier steps can remove structures that later
13
15
  * ones inspect.
16
+ *
17
+ * `assetHashes` are the hashes {@link CdkTemplateOptions.ignoreAssetHashes}
18
+ * masks, as read from the stack's cloud assembly.
14
19
  */
15
- export function normalize(template, options = {}) {
16
- const { ignoreAssets = false, ignoreBootstrapVersion = true, ignoreCurrentVersion = false, ignoreMetadata = false, ignoreTags = false, ignorePipelineAssets = false, subsetResourceTypes, subsetResourceKeys, assetPlaceholder = anyObject, } = options;
20
+ export function normalize(template, options = {}, assetHashes = new Set()) {
21
+ const { ignoreAssets = false, ignoreAssetHashes = false, ignoreBootstrapVersion = true, ignoreCurrentVersion = false, ignoreMetadata = false, ignoreTags = false, ignorePipelineAssets = false, subsetResourceTypes, subsetResourceKeys, assetPlaceholder = anyObject, } = options;
17
22
  const result = structuredClone(template);
18
23
  if (ignoreBootstrapVersion)
19
24
  stripBootstrapVersion(result);
20
25
  if (ignoreAssets)
21
26
  stripAssets(result, assetPlaceholder);
27
+ if (ignoreAssetHashes)
28
+ maskAssetHashes(result, assetHashes);
22
29
  if (ignoreCurrentVersion)
23
30
  maskCurrentVersions(result);
24
31
  if (ignorePipelineAssets)
@@ -66,6 +73,13 @@ function stripAssets(template, placeholder) {
66
73
  }
67
74
  }
68
75
  }
76
+ /**
77
+ * Only a standalone 64-hex run is a candidate, so a longer hex string that
78
+ * happens to contain an asset hash is left intact.
79
+ */
80
+ function maskAssetHashes(tree, hashes) {
81
+ transformStrings(tree, (value) => value.replace(assetHashRegex, (hash) => hashes.has(hash) ? maskedAssetHash : hash));
82
+ }
69
83
  function maskCurrentVersions(tree) {
70
84
  transformStrings(tree, (value) => {
71
85
  const match = currentVersionRegex.exec(value);
package/lib/options.d.ts CHANGED
@@ -10,6 +10,18 @@ export interface CdkTemplateOptions {
10
10
  * logical ID hashes the code.
11
11
  */
12
12
  ignoreAssets?: boolean;
13
+ /**
14
+ * Replace the hash of every asset in the app with `<ASSET_HASH>`, wherever
15
+ * a string in the template holds it: Lambda code and layers, container
16
+ * images, `BucketDeployment` sources, nested stack templates, CDK Pipelines
17
+ * commands. The rest of each value stays, and values that are not assets
18
+ * are untouched.
19
+ *
20
+ * The hashes are read from the asset manifests CDK's default synthesizer
21
+ * writes. A function using `currentVersion` also needs
22
+ * {@link CdkTemplateOptions.ignoreCurrentVersion}.
23
+ */
24
+ ignoreAssetHashes?: boolean;
13
25
  /**
14
26
  * Drop the CDK-managed `BootstrapVersion` parameter and its check rule.
15
27
  * Defaults to `true`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liflig/cdk-snapshot",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Normalizes synthesized AWS CDK stacks for snapshot testing",
5
5
  "type": "module",
6
6
  "license": "MIT",