Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): write Metadata resource in core framework #10306

Merged
merged 18 commits into from
Sep 28, 2020
Merged
Show file tree
Hide file tree
Changes from 10 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
29 changes: 25 additions & 4 deletions packages/@aws-cdk/core/lib/cfn-fn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,18 @@ export class Fn {
* Returns true if all the specified conditions evaluate to true, or returns
* false if any one of the conditions evaluates to false. ``Fn::And`` acts as
* an AND operator. The minimum number of conditions that you can include is
* 2, and the maximum is 10.
* 1.
* @param conditions conditions to AND
* @returns an FnCondition token
*/
public static conditionAnd(...conditions: ICfnConditionExpression[]): ICfnConditionExpression {
return new FnAnd(...conditions);
if (conditions.length === 0) {
throw new Error('Fn.conditionAnd() needs at least one argument');
}
if (conditions.length === 1) {
return conditions[0];
}
return Fn.conditionAnd(..._inGroupsOf(conditions, 10).map(group => new FnAnd(...group)));
}

/**
Expand Down Expand Up @@ -240,12 +246,18 @@ export class Fn {
* Returns true if any one of the specified conditions evaluate to true, or
* returns false if all of the conditions evaluates to false. ``Fn::Or`` acts
* as an OR operator. The minimum number of conditions that you can include is
* 2, and the maximum is 10.
* 1.
* @param conditions conditions that evaluates to true or false.
* @returns an FnCondition token
*/
public static conditionOr(...conditions: ICfnConditionExpression[]): ICfnConditionExpression {
return new FnOr(...conditions);
if (conditions.length === 0) {
throw new Error('Fn.conditionOr() needs at least one argument');
}
if (conditions.length === 1) {
return conditions[0];
}
return Fn.conditionOr(..._inGroupsOf(conditions, 10).map(group => new FnOr(...group)));
}

/**
Expand Down Expand Up @@ -739,3 +751,12 @@ class FnJoin implements IResolvable {
return minimalCloudFormationJoin(this.delimiter, resolvedValues);
}
}


function _inGroupsOf<T>(array: T[], maxGroup: number): T[][] {
const result = new Array<T[]>();
for (let i = 0; i < array.length; i += maxGroup) {
result.push(array.slice(i, i + maxGroup));
}
return result;
}
93 changes: 93 additions & 0 deletions packages/@aws-cdk/core/lib/private/metadata-resource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as cxapi from '@aws-cdk/cx-api';
import { RegionInfo } from '@aws-cdk/region-info';
import { CfnCondition } from '../cfn-condition';
import { Fn } from '../cfn-fn';
import { Aws } from '../cfn-pseudo';
import { CfnResource } from '../cfn-resource';
import { Construct } from '../construct-compat';
import { Lazy } from '../lazy';
import { Stack } from '../stack';
import { Token } from '../token';
import { collectRuntimeInformation } from './runtime-info';

/**
* Construct that will render the metadata resource
*/
export class MetadataResource extends Construct {
/**
* Clear the modules cache
*
* The next time the MetadataResource is rendered, it will do a lookup of the
* modules from the NodeJS module cache again.
*
* Used only for unit tests.
*/
public static clearModulesCache() {
this._modulesPropertyCache = undefined;
}

/**
* Cached version of the _modulesProperty() accessor
*
* No point in calculating this fairly expensive list more than once.
*/
private static _modulesPropertyCache?: string;

/**
* Calculate the modules property
*/
private static modulesProperty(): string {
if (this._modulesPropertyCache === undefined) {
this._modulesPropertyCache = formatModules(collectRuntimeInformation());
}
return this._modulesPropertyCache;
}

constructor(scope: Stack, id: string) {
super(scope, id);

const metadataServiceExists = Token.isUnresolved(scope.region) || RegionInfo.get(scope.region).cdkMetadataResourceAvailable;
eladb marked this conversation as resolved.
Show resolved Hide resolved
if (metadataServiceExists) {
const resource = new CfnResource(this, 'Default', {
type: 'AWS::CDK::Metadata',
properties: {
Modules: Lazy.stringValue({ produce: () => MetadataResource.modulesProperty() }),
},
});

// In case we don't actually know the region, add a condition to determine it at deploy time
if (Token.isUnresolved(scope.region)) {
const condition = new CfnCondition(this, 'Condition', {
expression: makeCdkMetadataAvailableCondition(),
});

// To not cause undue template changes
condition.overrideLogicalId('CDKMetadataAvailable');

resource.cfnOptions.condition = condition;
}
}
}
}

function makeCdkMetadataAvailableCondition() {
return Fn.conditionOr(...RegionInfo.regions
.filter(ri => ri.cdkMetadataResourceAvailable)
.map(ri => Fn.conditionEquals(Aws.REGION, ri.name)));
}

function formatModules(runtime: cxapi.RuntimeInfo): string {
const modules = new Array<string>();

// inject toolkit version to list of modules
// eslint-disable-next-line @typescript-eslint/no-require-imports
rix0rrr marked this conversation as resolved.
Show resolved Hide resolved
const cliVersion = process.env[cxapi.CLI_VERSION_ENV];
if (cliVersion) {
rix0rrr marked this conversation as resolved.
Show resolved Hide resolved
modules.push(`aws-cdk=${cliVersion}`);
}

for (const key of Object.keys(runtime.libraries).sort()) {
modules.push(`${key}=${runtime.libraries[key]}`);
}
return modules.join(',');
}
42 changes: 39 additions & 3 deletions packages/@aws-cdk/core/lib/private/synthesis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Aspects, IAspect } from '../aspect';
import { Construct, IConstruct, SynthesisOptions, ValidationError } from '../construct-compat';
import { Stack } from '../stack';
import { Stage, StageSynthesisOptions } from '../stage';
import { MetadataResource } from './metadata-resource';
import { prepareApp } from './prepare-app';
import { TreeMetadata } from './tree-metadata';

Expand All @@ -14,6 +15,8 @@ export function synthesize(root: IConstruct, options: SynthesisOptions = { }): c

invokeAspects(root);

injectMetadataResources(root);

// This is mostly here for legacy purposes as the framework itself does not use prepare anymore.
prepareTree(root);

Expand All @@ -35,9 +38,7 @@ export function synthesize(root: IConstruct, options: SynthesisOptions = { }): c
// stacks to add themselves to the synthesized cloud assembly.
synthesizeTree(root, builder);

return builder.buildAssembly({
runtimeInfo: options.runtimeInfo,
});
return builder.buildAssembly();
}

/**
Expand Down Expand Up @@ -110,6 +111,41 @@ function prepareTree(root: IConstruct) {
visit(root, 'post', construct => construct.onPrepare());
}

/**
* Find all stacks and add Metadata Resources to all of them
*
* There is no good generic place to do this. Can't do it in the constructor
* (because adding a child construct makes it impossible to set context on the
* node), and the generic prepare phase is deprecated.
*
* Only do this on [parent] stacks (not nested stacks), don't do this when
* disabled by the user.
*
* Also, only when running via the CLI. If we do it unconditionally,
* all unit tests everywhere are going to break massively. I've spent a day
* fixing our own, but downstream users would be affected just as badly.
*
* Stop at Assembly boundaries.
*/
function injectMetadataResources(root: IConstruct) {
visit(root, 'post', construct => {
// Only on top-level stacks
if (!Stack.isStack(construct)
|| construct.parentStack
// Unless disabled
|| construct.node.tryGetContext(cxapi.DISABLE_VERSION_REPORTING)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a property of Stack such as stack.versionReportingEnabled which will take into account context and the nested stack posture instead of leaking the fact that this can he set through context?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was also thinking that this should be an opt-in switch ("enable") and not opt-out ("disable"). This way, we (and our users) won't need to fix all their unit tests because the metadata resource will only be attached if the CLI sets that context flag.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do that, but since we already have an existing opt-out flag: DISABLE_VERSION_REPORTING (which we need to support for backwards compatibility anyway) I didn't really want to add another ENABLE_VERSION_REPORTING flag as well.

The Stack property probably makes sense although it would need to be exposed as a public property or a (ew!) public @internal one so that this function external to the class can use it.

It fights with the App's { runtimeInfo?: @default true } property as well...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DISABLE_VERSION_REPORTING is not really public API, so I would recommend removing it in favor of ENABLE_VERSION_REPORTING and deprecate runtimeInfo in favor of versionReporting (in props and at the Stack level as public API.

// While running via CLI
|| !process.env[cxapi.CLI_VERSION_ENV]) { return; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe I described that twice.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't it make sense to just not include the CLI version in case this environment variable in not defined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not actually about the CLI_VERSION_ENV itself. I'm using CLI_VERSION_ENV as a trigger to determine whether we're running under the CLI (and hence emitting the metadata)


// Because of https://github.com/aws/aws-cdk/blob/master/packages/@aws-cdk/assert/lib/synth-utils.ts#L74
// synthesize() may be called more than once on a stack in unit tests, and the below would break
// if we execute it a second time. Guard against the constructs already existing.
if (construct.node.tryFindChild('CDKMetadata')) { return; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make 'CDKMetadata' a const


new MetadataResource(construct, 'CDKMetadata');
});
}

/**
* Synthesize children in post-order into the given builder
*
Expand Down
5 changes: 3 additions & 2 deletions packages/@aws-cdk/core/lib/stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -720,10 +720,11 @@ export class Stack extends Construct implements ITaggable {
// this right now, so some parts still happen here.
const builder = session.assembly;

const template = this._toCloudFormation();

// write the CloudFormation template as a JSON file
const outPath = path.join(builder.outdir, this.templateFile);
const text = JSON.stringify(this._toCloudFormation(), undefined, 2);
fs.writeFileSync(outPath, text);
fs.writeFileSync(outPath, JSON.stringify(template, undefined, 2));

for (const ctx of this._missingContext) {
builder.addMissing(ctx);
Expand Down
3 changes: 0 additions & 3 deletions packages/@aws-cdk/core/lib/stage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as cxapi from '@aws-cdk/cx-api';
import { Construct, IConstruct } from './construct-compat';
import { Environment } from './environment';
import { collectRuntimeInformation } from './private/runtime-info';
import { synthesize } from './private/synthesis';

/**
Expand Down Expand Up @@ -171,10 +170,8 @@ export class Stage extends Construct {
*/
public synth(options: StageSynthesisOptions = { }): cxapi.CloudAssembly {
if (!this.assembly || options.force) {
const runtimeInfo = this.node.tryGetContext(cxapi.DISABLE_VERSION_REPORTING) ? undefined : collectRuntimeInformation();
this.assembly = synthesize(this, {
skipValidation: options.skipValidation,
runtimeInfo,
});
}

Expand Down
4 changes: 3 additions & 1 deletion packages/@aws-cdk/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@
"dependencies": {
"@aws-cdk/cloud-assembly-schema": "0.0.0",
"@aws-cdk/cx-api": "0.0.0",
"@aws-cdk/region-info": "0.0.0",
"constructs": "^3.0.4",
"fs-extra": "^9.0.1",
"minimatch": "^3.0.4"
Expand All @@ -185,7 +186,8 @@
"peerDependencies": {
"@aws-cdk/cloud-assembly-schema": "0.0.0",
"@aws-cdk/cx-api": "0.0.0",
"constructs": "^3.0.4"
"constructs": "^3.0.4",
"@aws-cdk/region-info": "0.0.0"
},
"engines": {
"node": ">= 10.13.0 <13 || >=13.7.0"
Expand Down
Loading