Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"eslint.useFlatConfig": false,
}
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export class CloudFormationStackArtifact extends CloudArtifact {

// We get the tags from 'properties' if available (cloud assembly format >= 6.0.0), otherwise
// from the stack metadata
this.tags = properties.tags ?? this.tagsFromMetadata();
this.tags = properties.tags ?? {};
this.notificationArns = properties.notificationArns;
this.assumeRoleArn = properties.assumeRoleArn;
this.assumeRoleExternalId = properties.assumeRoleExternalId;
Expand Down Expand Up @@ -215,16 +215,6 @@ export class CloudFormationStackArtifact extends CloudArtifact {
}
return this._template;
}

private tagsFromMetadata() {
const ret: Record<string, string> = {};
for (const metadataEntry of this.findMetadataByType(cxschema.ArtifactMetadataEntryType.STACK_TAGS)) {
for (const tag of (metadataEntry.data ?? []) as cxschema.StackTagsMetadataEntry) {
ret[tag.key] = tag.value;
}
}
return ret;
}
}

/**
Expand Down
47 changes: 43 additions & 4 deletions packages/@aws-cdk/cloud-assembly-api/lib/cloud-artifact.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as fs from 'fs';
import * as path from 'path';
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import type { CloudAssembly } from './cloud-assembly';
import type { MetadataEntryResult, SynthesisMessage } from './metadata';
Expand Down Expand Up @@ -36,6 +38,34 @@ export interface AwsCloudFormationStackProperties {
* Represents an artifact within a cloud assembly.
*/
export class CloudArtifact {
/**
* Read the metadata for the given artifact
*
* HISTORICAL OR PRIVATE USE ONLY
*
* This is publicly exposed as a static function for downstream libraries that
* don't use the `CloudAssembly`/`CloudArtifact` API, yet still need to read
* an artifact's metadata.
*
* 99% of consumers should just access `artifact.metadata`.
*/
public static readMetadata(assemblyDirectory: string, x: cxschema.ArtifactManifest): Record<string, cxschema.MetadataEntry[]> {
const ret: Record<string, cxschema.MetadataEntry[]> = {};
if (x.additionalMetadataFile) {
Object.assign(ret, JSON.parse(fs.readFileSync(path.join(assemblyDirectory, x.additionalMetadataFile), 'utf-8')));
}

for (const [p, entries] of Object.entries(x.metadata ?? {})) {
if (ret[p]) {
ret[p].push(...entries);
} else {
ret[p] = entries;
}
}

return ret;
}

/**
* Returns a subclass of `CloudArtifact` based on the artifact type defined in the artifact manifest.
*
Expand Down Expand Up @@ -77,6 +107,13 @@ export class CloudArtifact {
this._dependencyIDs = manifest.dependencies || [];
}

/**
* Returns the metadata associated with this Cloud Artifact
*/
public get metadata() {
return CloudArtifact.readMetadata(this.assembly.directory, this.manifest);
}

/**
* Returns all the artifacts that this artifact depends on.
*/
Expand All @@ -100,11 +137,13 @@ export class CloudArtifact {
* @returns all the metadata entries of a specific type in this artifact.
*/
public findMetadataByType(type: string): MetadataEntryResult[] {
const metadata = this.metadata;

const result = new Array<MetadataEntryResult>();
for (const path of Object.keys(this.manifest.metadata || {})) {
for (const entry of (this.manifest.metadata || {})[path]) {
for (const p of Object.keys(metadata || {})) {
for (const entry of (metadata || {})[p]) {
if (entry.type === type) {
result.push({ path, ...entry });
result.push({ path: p, ...entry });
}
}
}
Expand All @@ -114,7 +153,7 @@ export class CloudArtifact {
private renderMessages() {
const messages = new Array<SynthesisMessage>();

for (const [id, metadata] of Object.entries(this.manifest.metadata || { })) {
for (const [id, metadata] of Object.entries(this.metadata || { })) {
for (const entry of metadata) {
let level: SynthesisMessageLevel;
switch (entry.type) {
Expand Down
63 changes: 42 additions & 21 deletions packages/@aws-cdk/cloud-assembly-api/test/stack-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ test('read tags from artifact properties', () => {
expect(assembly.getStackByName('Stack').tags).toEqual({ foo: 'bar' });
});

test('stack tags get uppercased when written to Cloud Assembly', () => {
test('already uppercased stack tags get left alone', () => {
// Backwards compatibility test
// GIVEN
builder.addArtifact('Stack', {
Expand All @@ -67,7 +67,7 @@ test('stack tags get uppercased when written to Cloud Assembly', () => {
'/Stack': [
{
type: 'aws:cdk:stack-tags',
data: [{ key: 'foo', value: 'bar' }],
data: [{ Key: 'foo', Value: 'bar' } as any],
},
],
},
Expand All @@ -92,7 +92,7 @@ test('stack tags get uppercased when written to Cloud Assembly', () => {
]);
});

test('already uppercased stack tags get left alone', () => {
test('tags are NO LONGER read from stack metadata', () => {
// Backwards compatibility test
// GIVEN
builder.addArtifact('Stack', {
Expand All @@ -101,7 +101,7 @@ test('already uppercased stack tags get left alone', () => {
'/Stack': [
{
type: 'aws:cdk:stack-tags',
data: [{ Key: 'foo', Value: 'bar' } as any],
data: [{ key: 'foo', value: 'bar' }],
},
],
},
Expand All @@ -111,41 +111,62 @@ test('already uppercased stack tags get left alone', () => {
const assembly = builder.buildAssembly();

// THEN
const manifestStructure = JSON.parse(fs.readFileSync(path.join(assembly.directory, 'manifest.json'), { encoding: 'utf-8' }));
expect(manifestStructure.artifacts.Stack.metadata['/Stack']).toEqual([
{
type: 'aws:cdk:stack-tags',
data: [
{
// Note: uppercase due to historical accident
Key: 'foo',
Value: 'bar',
},
],
},
]);
expect(assembly.getStackByName('Stack').tags).toEqual({});
});

test('read tags from stack metadata', () => {
test('stack metadata can be read both from artifact and a file', () => {
// Backwards compatibility test
// GIVEN
builder.addArtifact('Stack', {
...stackBase,
metadata: {
'/Stack': [
{
type: 'aws:cdk:stack-tags',
data: [{ key: 'foo', value: 'bar' }],
type: 'custom:metadata',
data: 'custom 1',
},
],
},
additionalMetadataFile: 'addl-meta.json',
});

fs.writeFileSync(path.join(builder.outdir, 'addl-meta.json'), JSON.stringify({
'/Stack': [
{
type: 'custom:metadata',
data: 'custom 2',
},
],
'/Stack/Sub': [
{
type: 'custom:metadata',
data: 'custom 3',
},
],
}), 'utf-8');

// WHEN
const assembly = builder.buildAssembly();

// THEN
expect(assembly.getStackByName('Stack').tags).toEqual({ foo: 'bar' });
expect(assembly.getStackByName('Stack').metadata).toEqual({
'/Stack': [
{
type: 'custom:metadata',
data: 'custom 2',
},
{
type: 'custom:metadata',
data: 'custom 1',
},
],
'/Stack/Sub': [
{
type: 'custom:metadata',
data: 'custom 3',
},
],
});
});

test('user friendly id is the assembly display name', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ export interface AwsCloudFormationStackProperties {
/**
* Values for CloudFormation stack tags that should be passed when the stack is deployed.
*
* N.B.: Tags are also written to stack metadata, under the path of the Stack
* construct. Only in CDK CLI v1 are those tags found in metadata used for
* actual deployments; in all stable versions of CDK only the stack tags
* directly found in the `tags` property of `AwsCloudFormationStack` artifact
* (i.e., this property) are used.
*
* @default - No tags
*/
readonly tags?: { [id: string]: string };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,26 @@ export interface ArtifactManifest {
/**
* Associated metadata.
*
* Metadata can be stored directly in the assembly manifest, as well as in a
* separate file (see `additionalMetadataFile`). It should prefer to be stored
* in the additional file, as that will reduce the size of the assembly
* manifest in cases of a lot of metdata (which CDK does emit by default).
*
* @default - no metadata.
*/
readonly metadata?: { [path: string]: MetadataEntry[] };

/**
* A file with additional metadata entries.
*
* The schema of this file is exactly the same as the type of the `metadata` field.
* In other words, that file contains an object mapping construct paths to arrays
* of metadata entries.
*
* @default - no additional metadata
*/
readonly additionalMetadataFile?: string;

/**
* IDs of artifacts that must be deployed before this artifact.
*
Expand Down
Loading
Loading