Skip to content

Commit 78fa807

Browse files
fix(cli): remove legacy stack tags behavior (#1053)
Here's some nasty legacy around Stack Tags: * Before CDK v1.33.0, we wrote stack tags to the construct metadata, and they were written as a `{ Key, Value }` object that got directly passed into the CloudFormation CreateChangeSet API. * In CDK v1.33.0 (nearly 6 years ago), we formalized the cloud assembly schema into a jsii package, so we had to type the tag object as `{ key, value }` in memory (otherwise jsii would yell at us). For backwards compatibility with the on-disk format and the (extracted) JSONSchema Validation, we used to re-case the identifiers: just before they were written to disk we would change `key → Key` and just after reading from disk but before validation and we would change `Key → key`. * In CDK v1.66.0 (5 and a half years ago), we added a `tags` property to the CloudFormation Stack artifact in the CDK manifest, and the CLI and other tools stopped using the stack metadata to read stack tags. Since this is to support end-of-life CDKs, in this PR I'm taking some of that complexity out; we stop converting `key → Key` when writing a cloud assembly manifest -- but we do still have to convert `Key → key` in order to pass JSONSchema validation as we read the manifest. Tags are exclusively read from the artifact properties, there will be no fallback to the metadata anymore. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 937452b commit 78fa807

11 files changed

Lines changed: 80 additions & 190 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.projenrc.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,8 @@ new AdcPublishing(repoProject);
293293
new RecordPublishingTimestamp(repoProject);
294294
new BootstrapTemplateProtection(repoProject);
295295

296+
repoProject.gitignore.addPatterns('.vscode/settings.json');
297+
296298
// Eslint for projen config
297299
// @ts-ignore
298300
repoProject.eslint = new pj.javascript.Eslint(repoProject, {

.vscode/settings.example.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"eslint.useFlatConfig": false,
3+
}

packages/@aws-cdk/cloud-assembly-api/lib/artifacts/cloudformation-artifact.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ export class CloudFormationStackArtifact extends CloudArtifact {
175175

176176
// We get the tags from 'properties' if available (cloud assembly format >= 6.0.0), otherwise
177177
// from the stack metadata
178-
this.tags = properties.tags ?? this.tagsFromMetadata();
178+
this.tags = properties.tags ?? {};
179179
this.notificationArns = properties.notificationArns;
180180
this.assumeRoleArn = properties.assumeRoleArn;
181181
this.assumeRoleExternalId = properties.assumeRoleExternalId;
@@ -215,16 +215,6 @@ export class CloudFormationStackArtifact extends CloudArtifact {
215215
}
216216
return this._template;
217217
}
218-
219-
private tagsFromMetadata() {
220-
const ret: Record<string, string> = {};
221-
for (const metadataEntry of this.findMetadataByType(cxschema.ArtifactMetadataEntryType.STACK_TAGS)) {
222-
for (const tag of (metadataEntry.data ?? []) as cxschema.StackTagsMetadataEntry) {
223-
ret[tag.key] = tag.value;
224-
}
225-
}
226-
return ret;
227-
}
228218
}
229219

230220
/**

packages/@aws-cdk/cloud-assembly-api/test/stack-artifact.test.ts

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -58,40 +58,6 @@ test('read tags from artifact properties', () => {
5858
expect(assembly.getStackByName('Stack').tags).toEqual({ foo: 'bar' });
5959
});
6060

61-
test('stack tags get uppercased when written to Cloud Assembly', () => {
62-
// Backwards compatibility test
63-
// GIVEN
64-
builder.addArtifact('Stack', {
65-
...stackBase,
66-
metadata: {
67-
'/Stack': [
68-
{
69-
type: 'aws:cdk:stack-tags',
70-
data: [{ key: 'foo', value: 'bar' }],
71-
},
72-
],
73-
},
74-
});
75-
76-
// WHEN
77-
const assembly = builder.buildAssembly();
78-
79-
// THEN
80-
const manifestStructure = JSON.parse(fs.readFileSync(path.join(assembly.directory, 'manifest.json'), { encoding: 'utf-8' }));
81-
expect(manifestStructure.artifacts.Stack.metadata['/Stack']).toEqual([
82-
{
83-
type: 'aws:cdk:stack-tags',
84-
data: [
85-
{
86-
// Note: uppercase due to historical accident
87-
Key: 'foo',
88-
Value: 'bar',
89-
},
90-
],
91-
},
92-
]);
93-
});
94-
9561
test('already uppercased stack tags get left alone', () => {
9662
// Backwards compatibility test
9763
// GIVEN
@@ -126,7 +92,7 @@ test('already uppercased stack tags get left alone', () => {
12692
]);
12793
});
12894

129-
test('read tags from stack metadata', () => {
95+
test('tags are NO LONGER read from stack metadata', () => {
13096
// Backwards compatibility test
13197
// GIVEN
13298
builder.addArtifact('Stack', {
@@ -145,7 +111,7 @@ test('read tags from stack metadata', () => {
145111
const assembly = builder.buildAssembly();
146112

147113
// THEN
148-
expect(assembly.getStackByName('Stack').tags).toEqual({ foo: 'bar' });
114+
expect(assembly.getStackByName('Stack').tags).toEqual({});
149115
});
150116

151117
test('user friendly id is the assembly display name', () => {

packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ export interface AwsCloudFormationStackProperties {
6161
/**
6262
* Values for CloudFormation stack tags that should be passed when the stack is deployed.
6363
*
64+
* N.B.: Tags are also written to stack metadata, under the path of the Stack
65+
* construct. Only in CDK CLI v1 are those tags found in metadata used for
66+
* actual deployments; in all stable versions of CDK only the stack tags
67+
* directly found in the `tags` property of `AwsCloudFormationStack` artifact
68+
* (i.e., this property) are used.
69+
*
6470
* @default - No tags
6571
*/
6672
readonly tags?: { [id: string]: string };

packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts

Lines changed: 48 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,15 @@ export interface LoadManifestOptions {
7272
/**
7373
* Protocol utility class.
7474
*/
75-
export class Manifest {
75+
export abstract class Manifest {
7676
/**
7777
* Validates and saves the cloud assembly manifest to file.
7878
*
7979
* @param manifest - manifest.
8080
* @param filePath - output file path.
8181
*/
8282
public static saveAssemblyManifest(manifest: assembly.AssemblyManifest, filePath: string) {
83-
Manifest.saveManifest(manifest, filePath, ASSEMBLY_SCHEMA, Manifest.patchStackTagsOnWrite);
83+
Manifest.saveManifest(manifest, filePath, ASSEMBLY_SCHEMA);
8484
}
8585

8686
/**
@@ -102,7 +102,7 @@ export class Manifest {
102102
* @param filePath - output file path.
103103
*/
104104
public static saveAssetManifest(manifest: assets.AssetManifest, filePath: string) {
105-
Manifest.saveManifest(manifest, filePath, ASSETS_SCHEMA, Manifest.patchStackTagsOnRead);
105+
Manifest.saveManifest(manifest, filePath, ASSETS_SCHEMA);
106106
}
107107

108108
/**
@@ -111,7 +111,7 @@ export class Manifest {
111111
* @param filePath - path to the manifest file.
112112
*/
113113
public static loadAssetManifest(filePath: string): assets.AssetManifest {
114-
return this.loadManifest(filePath, ASSETS_SCHEMA);
114+
return Manifest.loadManifest(filePath, ASSETS_SCHEMA);
115115
}
116116

117117
/**
@@ -266,27 +266,56 @@ export class Manifest {
266266
}
267267

268268
/**
269-
* This requires some explaining...
269+
* Fix the casing of stack tags entries
270270
*
271-
* We previously used `{ Key, Value }` for the object that represents a stack tag. (Notice the casing)
272-
* @link https://github.com/aws/aws-cdk/blob/v1.27.0/packages/aws-cdk/lib/api/cxapp/stacks.ts#L427.
271+
* At the very beginning of the CDK we used to emit stack tags as an object with
272+
* `{ Key, Value }` keys; this had the "advantage" that we could stick those
273+
* tags directly into the `CreateChangeSet` call.
273274
*
274-
* When that object moved to this package, it had to be JSII compliant, which meant the property
275-
* names must be `camelCased`, and not `PascalCased`. This meant it no longer matches the structure in the `manifest.json` file.
276-
* In order to support current manifest files, we have to translate the `PascalCased` representation to the new `camelCased` one.
275+
* Then we later on used jsii on the assembly schema and we were forced to type
276+
* the in-memory objects as `{ key, value }` with lowercase letters. Now the
277+
* objects have a different on-disk and in-memory format, and we need to convert
278+
* between them.
277279
*
278-
* Note that the serialization itself still writes `PascalCased` because it relates to how CloudFormation expects it.
280+
* For backwards compatibility reasons, we used to convert lowercase in-memory
281+
* to uppercase on-disk variant until very recently. This is now unnecessary,
282+
* since no officially supported CDK tools read the stack tags from the
283+
* metadata; the CLI and toolkit library read stack tags from the artifact
284+
* properties.
279285
*
280-
* Ideally, we would start writing the `camelCased` and translate to how CloudFormation expects it when needed. But this requires nasty
281-
* backwards-compatibility code and it just doesn't seem to be worth the effort.
286+
* So although we don't emit uppercase stack tag objects anymore, we might still read
287+
* manifests that have them. Because the manifest we read must pass JSON Schema
288+
* validation (which expects lowercase tag objects), we have to fix the casing
289+
* of these objects after reading from disk and before validating.
290+
*
291+
* That's what this function does.
282292
*/
283293
private static patchStackTagsOnRead(this: void, manifest: assembly.AssemblyManifest) {
284-
return Manifest.replaceStackTags(manifest, (tags) =>
285-
tags.map((diskTag: any) => ({
286-
key: diskTag.Key,
287-
value: diskTag.Value,
288-
})),
289-
);
294+
const artifacts = Object.values(manifest.artifacts ?? {})
295+
.filter(artifact => artifact.type === assembly.ArtifactType.AWS_CLOUDFORMATION_STACK);
296+
297+
for (const artifact of artifacts) {
298+
const tagMetadata = Object.values(artifact.metadata ?? {})
299+
.flatMap(x => x)
300+
.filter(entry => entry.type === assembly.ArtifactMetadataEntryType.STACK_TAGS);
301+
302+
for (const entry of tagMetadata) {
303+
const tags = entry.data as unknown as assembly.StackTagsMetadataEntry[] | undefined;
304+
for (const tag of tags ?? []) {
305+
const t: any = tag;
306+
if ('Key' in t) {
307+
t.key = t.Key;
308+
delete t.Key;
309+
}
310+
if ('Value' in t) {
311+
t.value = t.Value;
312+
delete t.Value;
313+
}
314+
}
315+
}
316+
}
317+
318+
return manifest;
290319
}
291320

292321
/**
@@ -317,86 +346,6 @@ export class Manifest {
317346
throw new Error(`ExternalId is not allowed inside '${key}'`);
318347
}
319348
}
320-
321-
/**
322-
* See explanation on `patchStackTagsOnRead`
323-
*
324-
* Translate stack tags metadata if it has the "right" casing.
325-
*/
326-
private static patchStackTagsOnWrite(this: void, manifest: assembly.AssemblyManifest) {
327-
return Manifest.replaceStackTags(manifest, (tags) =>
328-
tags.map(
329-
(memTag) =>
330-
// Might already be uppercased (because stack synthesis generates it in final form yet)
331-
('Key' in memTag ? memTag : { Key: memTag.key, Value: memTag.value }) as any,
332-
),
333-
);
334-
}
335-
336-
/**
337-
* Recursively replace stack tags in the stack metadata
338-
*/
339-
private static replaceStackTags(
340-
manifest: assembly.AssemblyManifest,
341-
fn: Endofunctor<assembly.StackTagsMetadataEntry>,
342-
): assembly.AssemblyManifest {
343-
// Need to add in the `noUndefined`s because otherwise jest snapshot tests are going to freak out
344-
// about the keys with values that are `undefined` (even though they would never be JSON.stringified)
345-
return noUndefined({
346-
...manifest,
347-
artifacts: mapValues(manifest.artifacts, (artifact) => {
348-
if (artifact.type !== assembly.ArtifactType.AWS_CLOUDFORMATION_STACK) {
349-
return artifact;
350-
}
351-
return noUndefined({
352-
...artifact,
353-
metadata: mapValues(artifact.metadata, (metadataEntries) =>
354-
metadataEntries.map((metadataEntry) => {
355-
if (
356-
metadataEntry.type !== assembly.ArtifactMetadataEntryType.STACK_TAGS ||
357-
!metadataEntry.data
358-
) {
359-
return metadataEntry;
360-
}
361-
return {
362-
...metadataEntry,
363-
data: fn(metadataEntry.data as assembly.StackTagsMetadataEntry),
364-
};
365-
}),
366-
),
367-
} as assembly.ArtifactManifest);
368-
}),
369-
});
370-
}
371-
372-
private constructor() {
373-
}
374-
}
375-
376-
type Endofunctor<A> = (x: A) => A;
377-
378-
function mapValues<A, B>(
379-
xs: Record<string, A> | undefined,
380-
fn: (x: A) => B,
381-
): Record<string, B> | undefined {
382-
if (!xs) {
383-
return undefined;
384-
}
385-
const ret: Record<string, B> | undefined = {};
386-
for (const [k, v] of Object.entries(xs)) {
387-
ret[k] = fn(v);
388-
}
389-
return ret;
390-
}
391-
392-
function noUndefined<A extends object>(xs: A): A {
393-
const ret: any = {};
394-
for (const [k, v] of Object.entries(xs)) {
395-
if (v !== undefined) {
396-
ret[k] = v;
397-
}
398-
}
399-
return ret;
400349
}
401350

402351
function stripEnumErrors(errors: jsonschema.ValidationError[]) {

packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@
351351
}
352352
},
353353
"tags": {
354-
"description": "Values for CloudFormation stack tags that should be passed when the stack is deployed. (Default - No tags)",
354+
"description": "Values for CloudFormation stack tags that should be passed when the stack is deployed.\n\nN.B.: Tags are also written to stack metadata, under the path of the Stack\nconstruct. Only in CDK CLI v1 are those tags found in metadata used for\nactual deployments; in all stable versions of CDK only the stack tags\ndirectly found in the `tags` property of `AwsCloudFormationStack` artifact\n(i.e., this property) are used. (Default - No tags)",
355355
"type": "object",
356356
"additionalProperties": {
357357
"type": "string"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"schemaHash": "c0092c55280aa03c568e1d591f2034783597e942a400d1d58d7f05a8215f51f4",
2+
"schemaHash": "1b06659a117c44714e2e52854571bb1b45b765b277bb1c208bc4b7ea01f6a684",
33
"$comment": "Do not hold back the version on additions: jsonschema validation of the manifest by the consumer will trigger errors on unexpected fields.",
44
"revision": 50
55
}

packages/@aws-cdk/toolkit-lib/test/actions/list.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ const MOCK_STACK_A: TestStackArtifact = {
496496
'/Test-Stack-A': [
497497
{
498498
type: ArtifactMetadataEntryType.STACK_TAGS,
499+
data: [],
499500
},
500501
],
501502
},
@@ -508,6 +509,7 @@ const MOCK_STACK_B: TestStackArtifact = {
508509
'/Test-Stack-B': [
509510
{
510511
type: ArtifactMetadataEntryType.STACK_TAGS,
512+
data: [],
511513
},
512514
],
513515
},
@@ -530,6 +532,7 @@ const MOCK_STACK_C: TestStackArtifact = {
530532
'/Test-Stack-C': [
531533
{
532534
type: ArtifactMetadataEntryType.STACK_TAGS,
535+
data: [],
533536
},
534537
],
535538
},

0 commit comments

Comments
 (0)