packages/aws-cdk-lib/core/lib/stack.ts (783 lines of code) (raw):
import * as fs from 'fs';
import * as path from 'path';
import { IConstruct, Construct, Node } from 'constructs';
import { Annotations } from './annotations';
import { App } from './app';
import { Arn, ArnComponents, ArnFormat } from './arn';
import { Aspects } from './aspect';
import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from './assets';
import { CfnElement } from './cfn-element';
import { Fn } from './cfn-fn';
import { Aws, ScopedAws } from './cfn-pseudo';
import { CfnResource, TagType } from './cfn-resource';
import { ContextProvider } from './context-provider';
import { Environment } from './environment';
import { FeatureFlags } from './feature-flags';
import { PermissionsBoundary, PERMISSIONS_BOUNDARY_CONTEXT_KEY } from './permissions-boundary';
import { CLOUDFORMATION_TOKEN_RESOLVER, CloudFormationLang } from './private/cloudformation-lang';
import { LogicalIDs } from './private/logical-id';
import { resolve } from './private/resolve';
import { makeUniqueId } from './private/uniqueid';
import * as cxschema from '../../cloud-assembly-schema';
import { INCLUDE_PREFIX_IN_UNIQUE_NAME_GENERATION } from '../../cx-api';
import * as cxapi from '../../cx-api';
// Must be a 'require' to not run afoul of ESM module import rules
// eslint-disable-next-line @typescript-eslint/no-require-imports
const minimatch = require('minimatch');
const STACK_SYMBOL = Symbol.for('@aws-cdk/core.Stack');
const MY_STACK_CACHE = Symbol.for('@aws-cdk/core.Stack.myStack');
export const STACK_RESOURCE_LIMIT_CONTEXT = '@aws-cdk/core:stackResourceLimit';
const SUPPRESS_TEMPLATE_INDENTATION_CONTEXT = '@aws-cdk/core:suppressTemplateIndentation';
const TEMPLATE_BODY_MAXIMUM_SIZE = 1_000_000;
const VALID_STACK_NAME_REGEX = /^[A-Za-z][A-Za-z0-9-]*$/;
const MAX_RESOURCES = 500;
export interface StackProps {
/**
* A description of the stack.
*
* @default - No description.
*/
readonly description?: string;
/**
* The AWS environment (account/region) where this stack will be deployed.
*
* Set the `region`/`account` fields of `env` to either a concrete value to
* select the indicated environment (recommended for production stacks), or to
* the values of environment variables
* `CDK_DEFAULT_REGION`/`CDK_DEFAULT_ACCOUNT` to let the target environment
* depend on the AWS credentials/configuration that the CDK CLI is executed
* under (recommended for development stacks).
*
* If the `Stack` is instantiated inside a `Stage`, any undefined
* `region`/`account` fields from `env` will default to the same field on the
* encompassing `Stage`, if configured there.
*
* If either `region` or `account` are not set nor inherited from `Stage`, the
* Stack will be considered "*environment-agnostic*"". Environment-agnostic
* stacks can be deployed to any environment but may not be able to take
* advantage of all features of the CDK. For example, they will not be able to
* use environmental context lookups such as `ec2.Vpc.fromLookup` and will not
* automatically translate Service Principals to the right format based on the
* environment's AWS partition, and other such enhancements.
*
* @example
*
* // Use a concrete account and region to deploy this stack to:
* // `.account` and `.region` will simply return these values.
* new Stack(app, 'Stack1', {
* env: {
* account: '123456789012',
* region: 'us-east-1'
* },
* });
*
* // Use the CLI's current credentials to determine the target environment:
* // `.account` and `.region` will reflect the account+region the CLI
* // is configured to use (based on the user CLI credentials)
* new Stack(app, 'Stack2', {
* env: {
* account: process.env.CDK_DEFAULT_ACCOUNT,
* region: process.env.CDK_DEFAULT_REGION
* },
* });
*
* // Define multiple stacks stage associated with an environment
* const myStage = new Stage(app, 'MyStage', {
* env: {
* account: '123456789012',
* region: 'us-east-1'
* }
* });
*
* // both of these stacks will use the stage's account/region:
* // `.account` and `.region` will resolve to the concrete values as above
* new MyStack(myStage, 'Stack1');
* new YourStack(myStage, 'Stack2');
*
* // Define an environment-agnostic stack:
* // `.account` and `.region` will resolve to `{ "Ref": "AWS::AccountId" }` and `{ "Ref": "AWS::Region" }` respectively.
* // which will only resolve to actual values by CloudFormation during deployment.
* new MyStack(app, 'Stack1');
*
* @default - The environment of the containing `Stage` if available,
* otherwise create the stack will be environment-agnostic.
*/
readonly env?: Environment;
/**
* Name to deploy the stack with
*
* @default - Derived from construct path.
*/
readonly stackName?: string;
/**
* Stack tags that will be applied to all the taggable resources and the stack itself.
*
* @default {}
*/
readonly tags?: { [key: string]: string };
/**
* SNS Topic ARNs that will receive stack events.
*
* @default - no notfication arns.
*/
readonly notificationArns?: string[];
/**
* Synthesis method to use while deploying this stack
*
* The Stack Synthesizer controls aspects of synthesis and deployment,
* like how assets are referenced and what IAM roles to use. For more
* information, see the README of the main CDK package.
*
* If not specified, the `defaultStackSynthesizer` from `App` will be used.
* If that is not specified, `DefaultStackSynthesizer` is used if
* `@aws-cdk/core:newStyleStackSynthesis` is set to `true` or the CDK major
* version is v2. In CDK v1 `LegacyStackSynthesizer` is the default if no
* other synthesizer is specified.
*
* @default - The synthesizer specified on `App`, or `DefaultStackSynthesizer` otherwise.
*/
readonly synthesizer?: IStackSynthesizer;
/**
* Whether to enable termination protection for this stack.
*
* @default false
*/
readonly terminationProtection?: boolean;
/**
* Include runtime versioning information in this Stack
*
* @default `analyticsReporting` setting of containing `App`, or value of
* 'aws:cdk:version-reporting' context key
*/
readonly analyticsReporting?: boolean;
/**
* Enable this flag to allow native cross region stack references.
*
* Enabling this will create a CloudFormation custom resource
* in both the producing stack and consuming stack in order to perform the export/import
*
* This feature is currently experimental
*
* @default false
*/
readonly crossRegionReferences?: boolean;
/**
* Options for applying a permissions boundary to all IAM Roles
* and Users created within this Stage
*
* @default - no permissions boundary is applied
*/
readonly permissionsBoundary?: PermissionsBoundary;
/**
* Enable this flag to suppress indentation in generated
* CloudFormation templates.
*
* If not specified, the value of the `@aws-cdk/core:suppressTemplateIndentation`
* context key will be used. If that is not specified, then the
* default value `false` will be used.
*
* @default - the value of `@aws-cdk/core:suppressTemplateIndentation`, or `false` if that is not set.
*/
readonly suppressTemplateIndentation?: boolean;
}
/**
* A root construct which represents a single CloudFormation stack.
*/
export class Stack extends Construct implements ITaggable {
/**
* Return whether the given object is a Stack.
*
* We do attribute detection since we can't reliably use 'instanceof'.
*/
public static isStack(this: void, x: any): x is Stack {
return x !== null && typeof(x) === 'object' && STACK_SYMBOL in x;
}
/**
* Looks up the first stack scope in which `construct` is defined. Fails if there is no stack up the tree.
* @param construct The construct to start the search from.
*/
public static of(construct: IConstruct): Stack {
// we want this to be as cheap as possible. cache this result by mutating
// the object. anecdotally, at the time of this writing, @aws-cdk/core unit
// tests hit this cache 1,112 times, @aws-cdk/aws-cloudformation unit tests
// hit this 2,435 times).
const cache = (construct as any)[MY_STACK_CACHE] as Stack | undefined;
if (cache) {
return cache;
} else {
const value = _lookup(construct);
Object.defineProperty(construct, MY_STACK_CACHE, {
enumerable: false,
writable: false,
configurable: false,
value,
});
return value;
}
function _lookup(c: IConstruct): Stack {
if (Stack.isStack(c)) {
return c;
}
const _scope = Node.of(c).scope;
if (Stage.isStage(c) || !_scope) {
throw new Error(`${construct.constructor?.name ?? 'Construct'} at '${Node.of(construct).path}' should be created in the scope of a Stack, but no Stack found`);
}
return _lookup(_scope);
}
}
/**
* Tags to be applied to the stack.
*/
public readonly tags: TagManager;
/**
* Options for CloudFormation template (like version, transform, description).
*/
public readonly templateOptions: ITemplateOptions;
/**
* The AWS region into which this stack will be deployed (e.g. `us-west-2`).
*
* This value is resolved according to the following rules:
*
* 1. The value provided to `env.region` when the stack is defined. This can
* either be a concrete region (e.g. `us-west-2`) or the `Aws.REGION`
* token.
* 3. `Aws.REGION`, which is represents the CloudFormation intrinsic reference
* `{ "Ref": "AWS::Region" }` encoded as a string token.
*
* Preferably, you should use the return value as an opaque string and not
* attempt to parse it to implement your logic. If you do, you must first
* check that it is a concrete value an not an unresolved token. If this
* value is an unresolved token (`Token.isUnresolved(stack.region)` returns
* `true`), this implies that the user wishes that this stack will synthesize
* into a **region-agnostic template**. In this case, your code should either
* fail (throw an error, emit a synth error using `Annotations.of(construct).addError()`) or
* implement some other region-agnostic behavior.
*/
public readonly region: string;
/**
* The AWS account into which this stack will be deployed.
*
* This value is resolved according to the following rules:
*
* 1. The value provided to `env.account` when the stack is defined. This can
* either be a concrete account (e.g. `585695031111`) or the
* `Aws.ACCOUNT_ID` token.
* 3. `Aws.ACCOUNT_ID`, which represents the CloudFormation intrinsic reference
* `{ "Ref": "AWS::AccountId" }` encoded as a string token.
*
* Preferably, you should use the return value as an opaque string and not
* attempt to parse it to implement your logic. If you do, you must first
* check that it is a concrete value an not an unresolved token. If this
* value is an unresolved token (`Token.isUnresolved(stack.account)` returns
* `true`), this implies that the user wishes that this stack will synthesize
* into an **account-agnostic template**. In this case, your code should either
* fail (throw an error, emit a synth error using `Annotations.of(construct).addError()`) or
* implement some other account-agnostic behavior.
*/
public readonly account: string;
/**
* The environment coordinates in which this stack is deployed. In the form
* `aws://account/region`. Use `stack.account` and `stack.region` to obtain
* the specific values, no need to parse.
*
* You can use this value to determine if two stacks are targeting the same
* environment.
*
* If either `stack.account` or `stack.region` are not concrete values (e.g.
* `Aws.ACCOUNT_ID` or `Aws.REGION`) the special strings `unknown-account` and/or
* `unknown-region` will be used respectively to indicate this stack is
* region/account-agnostic.
*/
public readonly environment: string;
/**
* Whether termination protection is enabled for this stack.
*/
public get terminationProtection(): boolean {
return this._terminationProtection;
}
public set terminationProtection(value: boolean) {
this._terminationProtection = value;
}
/**
* If this is a nested stack, this represents its `AWS::CloudFormation::Stack`
* resource. `undefined` for top-level (non-nested) stacks.
*
*/
public readonly nestedStackResource?: CfnResource;
/**
* The name of the CloudFormation template file emitted to the output
* directory during synthesis.
*
* Example value: `MyStack.template.json`
*/
public readonly templateFile: string;
/**
* The ID of the cloud assembly artifact for this stack.
*/
public readonly artifactId: string;
/**
* Synthesis method for this stack
*
*/
public readonly synthesizer: IStackSynthesizer;
/**
* Whether version reporting is enabled for this stack
*
* Controls whether the CDK Metadata resource is injected
*
* @internal
*/
public readonly _versionReportingEnabled: boolean;
/**
* Whether cross region references are enabled for this stack
*
* @internal
*/
public readonly _crossRegionReferences: boolean;
/**
* SNS Notification ARNs to receive stack events.
*
* @internal
*/
public readonly _notificationArns?: string[];
/**
* Logical ID generation strategy
*/
private readonly _logicalIds: LogicalIDs;
/**
* Other stacks this stack depends on
*/
private readonly _stackDependencies: { [uniqueId: string]: StackDependency };
/**
* Lists all missing contextual information.
* This is returned when the stack is synthesized under the 'missing' attribute
* and allows tooling to obtain the context and re-synthesize.
*/
private readonly _missingContext: cxschema.MissingContext[];
private readonly _stackName: string;
/**
* Enable this flag to suppress indentation in generated
* CloudFormation templates.
*
* If not specified, the value of the `@aws-cdk/core:suppressTemplateIndentation`
* context key will be used. If that is not specified, then the
* default value `false` will be used.
*
* @default - the value of `@aws-cdk/core:suppressTemplateIndentation`, or `false` if that is not set.
*/
private readonly _suppressTemplateIndentation: boolean;
private _terminationProtection: boolean;
/**
* Creates a new stack.
*
* @param scope Parent of this stack, usually an `App` or a `Stage`, but could be any construct.
* @param id The construct ID of this stack. If `stackName` is not explicitly
* defined, this id (and any parent IDs) will be used to determine the
* physical ID of the stack.
* @param props Stack properties.
*/
public constructor(scope?: Construct, id?: string, props: StackProps = {}) {
// For unit test scope and id are optional for stacks, but we still want an App
// as the parent because apps implement much of the synthesis logic.
scope = scope ?? new App({
autoSynth: false,
outdir: FileSystem.mkdtemp('cdk-test-app-'),
});
// "Default" is a "hidden id" from a `node.uniqueId` perspective
id = id ?? 'Default';
super(scope, id);
this._missingContext = new Array<cxschema.MissingContext>();
this._stackDependencies = { };
this.templateOptions = { };
this._crossRegionReferences = !!props.crossRegionReferences;
this._suppressTemplateIndentation = props.suppressTemplateIndentation ?? this.node.tryGetContext(SUPPRESS_TEMPLATE_INDENTATION_CONTEXT) ?? false;
Object.defineProperty(this, STACK_SYMBOL, { value: true });
this._logicalIds = new LogicalIDs();
const { account, region, environment } = this.parseEnvironment(props.env);
this.account = account;
this.region = region;
this.environment = environment;
this._terminationProtection = props.terminationProtection ?? false;
if (props.description !== undefined) {
// Max length 1024 bytes
// Typically 2 bytes per character, may be more for more exotic characters
if (props.description.length > 512) {
throw new Error(`Stack description must be <= 1024 bytes. Received description: '${props.description}'`);
}
this.templateOptions.description = props.description;
}
this._stackName = props.stackName ?? this.generateStackName();
if (this._stackName.length > 128) {
throw new Error(`Stack name must be <= 128 characters. Stack name: '${this._stackName}'`);
}
this.tags = new TagManager(TagType.KEY_VALUE, 'aws:cdk:stack', props.tags);
for (const notificationArn of props.notificationArns ?? []) {
if (Token.isUnresolved(notificationArn)) {
throw new Error(`Stack '${id}' includes one or more tokens in its notification ARNs: ${props.notificationArns}`);
}
}
this._notificationArns = props.notificationArns;
if (!VALID_STACK_NAME_REGEX.test(this.stackName)) {
throw new Error(`Stack name must match the regular expression: ${VALID_STACK_NAME_REGEX.toString()}, got '${this.stackName}'`);
}
// the preferred behavior is to generate a unique id for this stack and use
// it as the artifact ID in the assembly. this allows multiple stacks to use
// the same name. however, this behavior is breaking for 1.x so it's only
// applied under a feature flag which is applied automatically for new
// projects created using `cdk init`.
//
// Also use the new behavior if we are using the new CI/CD-ready synthesizer; that way
// people only have to flip one flag.
const featureFlags = FeatureFlags.of(this);
const stackNameDupeContext = featureFlags.isEnabled(cxapi.ENABLE_STACK_NAME_DUPLICATES_CONTEXT);
const newStyleSynthesisContext = featureFlags.isEnabled(cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT);
const artifactId = (stackNameDupeContext || newStyleSynthesisContext)
? this.generateStackArtifactId()
: this.stackName;
// Sanitize artifact id, since it is used as part of a file name
this.artifactId = artifactId.replace(/[^A-Za-z0-9_\-\.]/g, '_');
this.templateFile = `${this.artifactId}.template.json`;
// Not for nested stacks
this._versionReportingEnabled = (props.analyticsReporting ?? this.node.tryGetContext(cxapi.ANALYTICS_REPORTING_ENABLED_CONTEXT))
&& !this.nestedStackParent;
const synthesizer = (props.synthesizer
?? this.node.tryGetContext(PRIVATE_CONTEXT_DEFAULT_STACK_SYNTHESIZER)
?? (newStyleSynthesisContext ? new DefaultStackSynthesizer() : new LegacyStackSynthesizer()));
if (isReusableStackSynthesizer(synthesizer)) {
// Produce a fresh instance for each stack (should have been the default behavior)
this.synthesizer = synthesizer.reusableBind(this);
} else {
// Bind the single instance in-place to the current stack (backwards compat)
this.synthesizer = synthesizer;
this.synthesizer.bind(this);
}
props.permissionsBoundary?._bind(this);
// add the permissions boundary aspect
this.addPermissionsBoundaryAspect();
}
/**
* If a permissions boundary has been applied on this scope or any parent scope
* then this will return the ARN of the permissions boundary.
*
* This will return the permissions boundary that has been applied to the most
* specific scope.
*
* For example:
*
* const stage = new Stage(app, 'stage', {
* permissionsBoundary: PermissionsBoundary.fromName('stage-pb'),
* });
*
* const stack = new Stack(stage, 'Stack', {
* permissionsBoundary: PermissionsBoundary.fromName('some-other-pb'),
* });
*
* Stack.permissionsBoundaryArn === 'arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/some-other-pb';
*
* @param scope the construct scope to retrieve the permissions boundary name from
* @returns the name of the permissions boundary or undefined if not set
*/
private get permissionsBoundaryArn(): string | undefined {
const qualifier = this.synthesizer.bootstrapQualifier
?? this.node.tryGetContext(BOOTSTRAP_QUALIFIER_CONTEXT)
?? DefaultStackSynthesizer.DEFAULT_QUALIFIER;
const spec = new StringSpecializer(this, qualifier);
const context = this.node.tryGetContext(PERMISSIONS_BOUNDARY_CONTEXT_KEY);
let arn: string | undefined;
if (context && context.arn) {
arn = spec.specialize(context.arn);
} else if (context && context.name) {
arn = spec.specialize(this.formatArn({
service: 'iam',
resource: 'policy',
region: '',
resourceName: context.name,
}));
}
if (arn &&
(arn.includes('${Qualifier}')
|| arn.includes('${AWS::AccountId}')
|| arn.includes('${AWS::Region}')
|| arn.includes('${AWS::Partition}'))) {
throw new Error(`The permissions boundary ${arn} includes a pseudo parameter, ` +
'which is not supported for environment agnostic stacks');
}
return arn;
}
/**
* Adds an aspect to the stack that will apply the permissions boundary.
* This will only add the aspect if the permissions boundary has been set
*/
private addPermissionsBoundaryAspect(): void {
const permissionsBoundaryArn = this.permissionsBoundaryArn;
if (permissionsBoundaryArn) {
Aspects.of(this).add({
visit(node: IConstruct) {
if (
CfnResource.isCfnResource(node) &&
(node.cfnResourceType == 'AWS::IAM::Role' || node.cfnResourceType == 'AWS::IAM::User')
) {
node.addPropertyOverride('PermissionsBoundary', permissionsBoundaryArn);
}
},
}, {
priority: mutatingAspectPrio32333(this),
});
}
}
/**
* Resolve a tokenized value in the context of the current stack.
*/
public resolve(obj: any): any {
return resolve(obj, {
scope: this,
prefix: [],
resolver: CLOUDFORMATION_TOKEN_RESOLVER,
preparing: false,
});
}
/**
* Convert an object, potentially containing tokens, to a JSON string
*/
public toJsonString(this: void, obj: any, space?: number): string {
return CloudFormationLang.toJSON(obj, space).toString();
}
/**
* Convert an object, potentially containing tokens, to a YAML string
*/
public toYamlString(obj: any): string {
return CloudFormationLang.toYAML(obj).toString();
}
/**
* DEPRECATED
* @deprecated use `reportMissingContextKey()`
*/
public reportMissingContext(report: cxapi.MissingContext) {
if (!Object.values(cxschema.ContextProvider).includes(report.provider as cxschema.ContextProvider)) {
throw new Error(`Unknown context provider requested in: ${JSON.stringify(report)}`);
}
this.reportMissingContextKey(report as cxschema.MissingContext);
}
/**
* Indicate that a context key was expected
*
* Contains instructions which will be emitted into the cloud assembly on how
* the key should be supplied.
*
* @param report The set of parameters needed to obtain the context
*/
public reportMissingContextKey(report: cxschema.MissingContext) {
this._missingContext.push(report);
}
/**
* Rename a generated logical identities
*
* To modify the naming scheme strategy, extend the `Stack` class and
* override the `allocateLogicalId` method.
*/
public renameLogicalId(oldId: string, newId: string) {
this._logicalIds.addRename(oldId, newId);
}
/**
* Allocates a stack-unique CloudFormation-compatible logical identity for a
* specific resource.
*
* This method is called when a `CfnElement` is created and used to render the
* initial logical identity of resources. Logical ID renames are applied at
* this stage.
*
* This method uses the protected method `allocateLogicalId` to render the
* logical ID for an element. To modify the naming scheme, extend the `Stack`
* class and override this method.
*
* @param element The CloudFormation element for which a logical identity is
* needed.
*/
public getLogicalId(element: CfnElement): string {
const logicalId = this.allocateLogicalId(element);
return this._logicalIds.applyRename(logicalId);
}
/**
* Add a dependency between this stack and another stack.
*
* This can be used to define dependencies between any two stacks within an
* app, and also supports nested stacks.
*/
public addDependency(target: Stack, reason?: string) {
addDependency(this, target, reason ?? `{${this.node.path}}.addDependency({${target.node.path}})`);
}
/**
* Return the stacks this stack depends on
*/
public get dependencies(): Stack[] {
return Object.values(this._stackDependencies).map(x => x.stack);
}
/**
* The concrete CloudFormation physical stack name.
*
* This is either the name defined explicitly in the `stackName` prop or
* allocated based on the stack's location in the construct tree. Stacks that
* are directly defined under the app use their construct `id` as their stack
* name. Stacks that are defined deeper within the tree will use a hashed naming
* scheme based on the construct path to ensure uniqueness.
*
* If you wish to obtain the deploy-time AWS::StackName intrinsic,
* you can use `Aws.STACK_NAME` directly.
*/
public get stackName(): string {
return this._stackName;
}
/**
* The partition in which this stack is defined
*/
public get partition(): string {
// Return a non-scoped partition intrinsic when the stack's region is
// unresolved or unknown. Otherwise we will return the partition name as
// a literal string.
if (!FeatureFlags.of(this).isEnabled(cxapi.ENABLE_PARTITION_LITERALS) || Token.isUnresolved(this.region)) {
return Aws.PARTITION;
} else {
const partition = RegionInfo.get(this.region).partition;
return partition ?? Aws.PARTITION;
}
}
/**
* The Amazon domain suffix for the region in which this stack is defined
*/
public get urlSuffix(): string {
// Since URL Suffix always follows partition, it is unscoped like partition is.
return Aws.URL_SUFFIX;
}
/**
* The ID of the stack
*
* @example
* // After resolving, looks like
* 'arn:aws:cloudformation:us-west-2:123456789012:stack/teststack/51af3dc0-da77-11e4-872e-1234567db123'
*/
public get stackId(): string {
return new ScopedAws(this).stackId;
}
/**
* Returns the list of notification Amazon Resource Names (ARNs) for the current stack.
*/
public get notificationArns(): string[] {
return new ScopedAws(this).notificationArns;
}
/**
* Indicates if this is a nested stack, in which case `parentStack` will include a reference to it's parent.
*/
public get nested(): boolean {
return this.nestedStackResource !== undefined;
}
/**
* Creates an ARN from components.
*
* If `partition`, `region` or `account` are not specified, the stack's
* partition, region and account will be used.
*
* If any component is the empty string, an empty string will be inserted
* into the generated ARN at the location that component corresponds to.
*
* The ARN will be formatted as follows:
*
* arn:{partition}:{service}:{region}:{account}:{resource}{sep}{resource-name}
*
* The required ARN pieces that are omitted will be taken from the stack that
* the 'scope' is attached to. If all ARN pieces are supplied, the supplied scope
* can be 'undefined'.
*/
public formatArn(components: ArnComponents): string {
return Arn.format(components, this);
}
/**
* Given an ARN, parses it and returns components.
*
* IF THE ARN IS A CONCRETE STRING...
*
* ...it will be parsed and validated. The separator (`sep`) will be set to '/'
* if the 6th component includes a '/', in which case, `resource` will be set
* to the value before the '/' and `resourceName` will be the rest. In case
* there is no '/', `resource` will be set to the 6th components and
* `resourceName` will be set to the rest of the string.
*
* IF THE ARN IS A TOKEN...
*
* ...it cannot be validated, since we don't have the actual value yet at the
* time of this function call. You will have to supply `sepIfToken` and
* whether or not ARNs of the expected format usually have resource names
* in order to parse it properly. The resulting `ArnComponents` object will
* contain tokens for the subexpressions of the ARN, not string literals.
*
* If the resource name could possibly contain the separator char, the actual
* resource name cannot be properly parsed. This only occurs if the separator
* char is '/', and happens for example for S3 object ARNs, IAM Role ARNs,
* IAM OIDC Provider ARNs, etc. To properly extract the resource name from a
* Tokenized ARN, you must know the resource type and call
* `Arn.extractResourceName`.
*
* @param arn The ARN string to parse
* @param sepIfToken The separator used to separate resource from resourceName
* @param hasName Whether there is a name component in the ARN at all. For
* example, SNS Topics ARNs have the 'resource' component contain the topic
* name, and no 'resourceName' component.
*
* @returns an ArnComponents object which allows access to the various
* components of the ARN.
*
* @returns an ArnComponents object which allows access to the various
* components of the ARN.
*
* @deprecated use splitArn instead
*/
public parseArn(arn: string, sepIfToken: string = '/', hasName: boolean = true): ArnComponents {
return Arn.parse(arn, sepIfToken, hasName);
}
/**
* Splits the provided ARN into its components.
* Works both if 'arn' is a string like 'arn:aws:s3:::bucket',
* and a Token representing a dynamic CloudFormation expression
* (in which case the returned components will also be dynamic CloudFormation expressions,
* encoded as Tokens).
*
* @param arn the ARN to split into its components
* @param arnFormat the expected format of 'arn' - depends on what format the service 'arn' represents uses
*/
public splitArn(arn: string, arnFormat: ArnFormat): ArnComponents {
return Arn.split(arn, arnFormat);
}
/**
* Returns the list of AZs that are available in the AWS environment
* (account/region) associated with this stack.
*
* If the stack is environment-agnostic (either account and/or region are
* tokens), this property will return an array with 2 tokens that will resolve
* at deploy-time to the first two availability zones returned from CloudFormation's
* `Fn::GetAZs` intrinsic function.
*
* If they are not available in the context, returns a set of dummy values and
* reports them as missing, and let the CLI resolve them by calling EC2
* `DescribeAvailabilityZones` on the target environment.
*
* To specify a different strategy for selecting availability zones override this method.
*/
public get availabilityZones(): string[] {
// if account/region are tokens, we can't obtain AZs through the context
// provider, so we fallback to use Fn::GetAZs. the current lowest common
// denominator is 2 AZs across all AWS regions.
const agnostic = Token.isUnresolved(this.account) || Token.isUnresolved(this.region);
if (agnostic) {
return this.node.tryGetContext(cxapi.AVAILABILITY_ZONE_FALLBACK_CONTEXT_KEY) || [
Fn.select(0, Fn.getAzs()),
Fn.select(1, Fn.getAzs()),
];
}
const value = ContextProvider.getValue(this, {
provider: cxschema.ContextProvider.AVAILABILITY_ZONE_PROVIDER,
dummyValue: ['dummy1a', 'dummy1b', 'dummy1c'],
}).value;
if (!Array.isArray(value)) {
throw new Error(`Provider ${cxschema.ContextProvider.AVAILABILITY_ZONE_PROVIDER} expects a list`);
}
return value;
}
/**
* Register a file asset on this Stack
*
* @deprecated Use `stack.synthesizer.addFileAsset()` if you are calling,
* and a different IStackSynthesizer class if you are implementing.
*/
public addFileAsset(asset: FileAssetSource): FileAssetLocation {
return this.synthesizer.addFileAsset(asset);
}
/**
* Register a docker image asset on this Stack
*
* @deprecated Use `stack.synthesizer.addDockerImageAsset()` if you are calling,
* and a different `IStackSynthesizer` class if you are implementing.
*/
public addDockerImageAsset(asset: DockerImageAssetSource): DockerImageAssetLocation {
return this.synthesizer.addDockerImageAsset(asset);
}
/**
* If this is a nested stack, returns it's parent stack.
*/
public get nestedStackParent() {
return this.nestedStackResource && Stack.of(this.nestedStackResource);
}
/**
* Returns the parent of a nested stack.
*
* @deprecated use `nestedStackParent`
*/
public get parentStack() {
return this.nestedStackParent;
}
/**
* Add a Transform to this stack. A Transform is a macro that AWS
* CloudFormation uses to process your template.
*
* Duplicate values are removed when stack is synthesized.
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/transform-section-structure.html
* @param transform The transform to add
*
* @example
* declare const stack: Stack;
*
* stack.addTransform('AWS::Serverless-2016-10-31')
*/
public addTransform(transform: string) {
if (!this.templateOptions.transforms) {
this.templateOptions.transforms = [];
}
this.templateOptions.transforms.push(transform);
}
/**
* Adds an arbitrary key-value pair, with information you want to record about the stack.
* These get translated to the Metadata section of the generated template.
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/metadata-section-structure.html
*/
public addMetadata(key: string, value: any) {
if (!this.templateOptions.metadata) {
this.templateOptions.metadata = {};
}
this.templateOptions.metadata[key] = value;
}
/**
* Called implicitly by the `addDependency` helper function in order to
* realize a dependency between two top-level stacks at the assembly level.
*
* Use `stack.addDependency` to define the dependency between any two stacks,
* and take into account nested stack relationships.
*
* @internal
*/
public _addAssemblyDependency(target: Stack, reason: StackDependencyReason = {}) {
// defensive: we should never get here for nested stacks
if (this.nested || target.nested) {
throw new Error('Cannot add assembly-level dependencies for nested stacks');
}
// Fill in reason details if not provided
if (!reason.source) {
reason.source = this;
}
if (!reason.target) {
reason.target = target;
}
if (!reason.description) {
reason.description = 'no description provided';
}
const cycle = target.stackDependencyReasons(this);
if (cycle !== undefined) {
const cycleDescription = cycle.map((cycleReason) => {
return cycleReason.description;
}).join(', ');
// eslint-disable-next-line max-len
throw new Error(`'${target.node.path}' depends on '${this.node.path}' (${cycleDescription}). Adding this dependency (${reason.description}) would create a cyclic reference.`);
}
let dep = this._stackDependencies[Names.uniqueId(target)];
if (!dep) {
dep = this._stackDependencies[Names.uniqueId(target)] = { stack: target, reasons: [] };
}
// Check for a duplicate reason already existing
let existingReasons: Set<StackDependencyReason> = new Set();
dep.reasons.forEach((existingReason) => {
if (existingReason.source == reason.source && existingReason.target == reason.target) {
existingReasons.add(existingReason);
}
});
if (existingReasons.size > 0) {
// Dependency already exists and for the provided reason
return;
}
dep.reasons.push(reason);
if (process.env.CDK_DEBUG_DEPS) {
// eslint-disable-next-line no-console
console.error(`[CDK_DEBUG_DEPS] stack "${reason.source.node.path}" depends on "${reason.target.node.path}"`);
}
}
/**
* Called implicitly by the `obtainDependencies` helper function in order to
* collect resource dependencies across two top-level stacks at the assembly level.
*
* Use `stack.obtainDependencies` to see the dependencies between any two stacks.
*
* @internal
*/
public _obtainAssemblyDependencies(reasonFilter: StackDependencyReason): Element[] {
if (!reasonFilter.source) {
throw new Error('reasonFilter.source must be defined!');
}
// Assume reasonFilter has only source defined
let dependencies: Set<Element> = new Set();
Object.values(this._stackDependencies).forEach((dep) => {
dep.reasons.forEach((reason) => {
if (reasonFilter.source == reason.source) {
if (!reason.target) {
throw new Error(`Encountered an invalid dependency target from source '${reasonFilter.source!.node.path}'`);
}
dependencies.add(reason.target);
}
});
});
return Array.from(dependencies);
}
/**
* Called implicitly by the `removeDependency` helper function in order to
* remove a dependency between two top-level stacks at the assembly level.
*
* Use `stack.addDependency` to define the dependency between any two stacks,
* and take into account nested stack relationships.
*
* @internal
*/
public _removeAssemblyDependency(target: Stack, reasonFilter: StackDependencyReason={}) {
// defensive: we should never get here for nested stacks
if (this.nested || target.nested) {
throw new Error('There cannot be assembly-level dependencies for nested stacks');
}
// No need to check for a dependency cycle when removing one
// Fill in reason details if not provided
if (!reasonFilter.source) {
reasonFilter.source = this;
}
if (!reasonFilter.target) {
reasonFilter.target = target;
}
let dep = this._stackDependencies[Names.uniqueId(target)];
if (!dep) {
// Dependency doesn't exist - return now
return;
}
// Find and remove the specified reason from the dependency
let matchedReasons: Set<StackDependencyReason> = new Set();
dep.reasons.forEach((reason) => {
if (reasonFilter.source == reason.source && reasonFilter.target == reason.target) {
matchedReasons.add(reason);
}
});
if (matchedReasons.size > 1) {
throw new Error(`There cannot be more than one reason for dependency removal, found: ${matchedReasons}`);
}
if (matchedReasons.size == 0) {
// Reason is already not there - return now
return;
}
let matchedReason = Array.from(matchedReasons)[0];
let index = dep.reasons.indexOf(matchedReason, 0);
dep.reasons.splice(index, 1);
// If that was the last reason, remove the dependency
if (dep.reasons.length == 0) {
delete this._stackDependencies[Names.uniqueId(target)];
}
if (process.env.CDK_DEBUG_DEPS) {
// eslint-disable-next-line no-console
console.log(`[CDK_DEBUG_DEPS] stack "${this.node.path}" no longer depends on "${target.node.path}" because: ${reasonFilter}`);
}
}
/**
* Synthesizes the cloudformation template into a cloud assembly.
* @internal
*/
public _synthesizeTemplate(session: ISynthesisSession,
lookupRoleArn?: string,
lookupRoleExternalId?: string,
lookupRoleAdditionalOptions?: { [key: string]: any }): void {
// In principle, stack synthesis is delegated to the
// StackSynthesis object.
//
// However, some parts of synthesis currently use some private
// methods on Stack, and I don't really see the value in refactoring
// 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);
if (this.maxResources > 0) {
const resources = template.Resources || {};
const numberOfResources = Object.keys(resources).length;
if (numberOfResources > this.maxResources) {
const counts = Object.entries(count(Object.values(resources).map((r: any) => `${r?.Type}`))).map(([type, c]) => `${type} (${c})`).join(', ');
throw new Error(`Number of resources in stack '${this.node.path}': ${numberOfResources} is greater than allowed maximum of ${this.maxResources}: ${counts}`);
} else if (numberOfResources >= (this.maxResources * 0.8)) {
Annotations.of(this).addInfo(`Number of resources: ${numberOfResources} is approaching allowed maximum of ${this.maxResources}`);
}
}
const indent = this._suppressTemplateIndentation ? undefined : 1;
const templateData = JSON.stringify(template, undefined, indent);
if (templateData.length > (TEMPLATE_BODY_MAXIMUM_SIZE * 0.8)) {
const verb = templateData.length > TEMPLATE_BODY_MAXIMUM_SIZE ? 'exceeds' : 'is approaching';
const advice = this._suppressTemplateIndentation ?
'Split resources into multiple stacks to reduce template size' :
'Split resources into multiple stacks or set suppressTemplateIndentation to reduce template size';
const message = `Template size ${verb} limit: ${templateData.length}/${TEMPLATE_BODY_MAXIMUM_SIZE}. ${advice}.`;
Annotations.of(this).addWarningV2('@aws-cdk/core:Stack.templateSize', message);
}
fs.writeFileSync(outPath, templateData);
for (const ctx of this._missingContext) {
// 'account' and 'region' are added to the schema at tree instantiation time.
// these options however are only known at synthesis, so are added here.
// see https://github.com/aws/aws-cdk/blob/v2.158.0/packages/aws-cdk-lib/core/lib/context-provider.ts#L71
const queryLookupOptions: Omit<cxschema.ContextLookupRoleOptions, 'account' | 'region'> = {
lookupRoleArn,
lookupRoleExternalId,
assumeRoleAdditionalOptions: lookupRoleAdditionalOptions,
};
builder.addMissing({ ...ctx, props: { ...ctx.props, ...queryLookupOptions } });
}
}
/**
* Look up a fact value for the given fact for the region of this stack
*
* Will return a definite value only if the region of the current stack is resolved.
* If not, a lookup map will be added to the stack and the lookup will be done at
* CDK deployment time.
*
* What regions will be included in the lookup map is controlled by the
* `@aws-cdk/core:target-partitions` context value: it must be set to a list
* of partitions, and only regions from the given partitions will be included.
* If no such context key is set, all regions will be included.
*
* This function is intended to be used by construct library authors. Application
* builders can rely on the abstractions offered by construct libraries and do
* not have to worry about regional facts.
*
* If `defaultValue` is not given, it is an error if the fact is unknown for
* the given region.
*/
public regionalFact(factName: string, defaultValue?: string): string {
if (!Token.isUnresolved(this.region)) {
const ret = Fact.find(this.region, factName) ?? defaultValue;
if (ret === undefined) {
throw new Error(`region-info: don't know ${factName} for region ${this.region}. Use 'Fact.register' to provide this value.`);
}
return ret;
}
const partitions = Node.of(this).tryGetContext(cxapi.TARGET_PARTITIONS);
if (partitions !== undefined && partitions !== 'undefined' && !Array.isArray(partitions)) {
throw new Error(`Context value '${cxapi.TARGET_PARTITIONS}' should be a list of strings, got: ${JSON.stringify(partitions)}`);
}
const lookupMap =
partitions !== undefined && partitions !== 'undefined'
? RegionInfo.limitedRegionMap(factName, partitions)
: RegionInfo.regionMap(factName);
return deployTimeLookup(this, factName, lookupMap, defaultValue);
}
/**
* Create a CloudFormation Export for a string value
*
* Returns a string representing the corresponding `Fn.importValue()`
* expression for this Export. You can control the name for the export by
* passing the `name` option.
*
* If you don't supply a value for `name`, the value you're exporting must be
* a Resource attribute (for example: `bucket.bucketName`) and it will be
* given the same name as the automatic cross-stack reference that would be created
* if you used the attribute in another Stack.
*
* One of the uses for this method is to *remove* the relationship between
* two Stacks established by automatic cross-stack references. It will
* temporarily ensure that the CloudFormation Export still exists while you
* remove the reference from the consuming stack. After that, you can remove
* the resource and the manual export.
*
* Here is how the process works. Let's say there are two stacks,
* `producerStack` and `consumerStack`, and `producerStack` has a bucket
* called `bucket`, which is referenced by `consumerStack` (perhaps because
* an AWS Lambda Function writes into it, or something like that).
*
* It is not safe to remove `producerStack.bucket` because as the bucket is being
* deleted, `consumerStack` might still be using it.
*
* Instead, the process takes two deployments:
*
* **Deployment 1: break the relationship**:
*
* - Make sure `consumerStack` no longer references `bucket.bucketName` (maybe the consumer
* stack now uses its own bucket, or it writes to an AWS DynamoDB table, or maybe you just
* remove the Lambda Function altogether).
* - In the `ProducerStack` class, call `this.exportValue(this.bucket.bucketName)`. This
* will make sure the CloudFormation Export continues to exist while the relationship
* between the two stacks is being broken.
* - Deploy (this will effectively only change the `consumerStack`, but it's safe to deploy both).
*
* **Deployment 2: remove the bucket resource**:
*
* - You are now free to remove the `bucket` resource from `producerStack`.
* - Don't forget to remove the `exportValue()` call as well.
* - Deploy again (this time only the `producerStack` will be changed -- the bucket will be deleted).
*/
public exportValue(exportedValue: any, options: ExportValueOptions = {}): string {
if (options.name) {
new CfnOutput(this, `Export${options.name}`, {
value: exportedValue,
exportName: options.name,
description: options.description,
});
return Fn.importValue(options.name);
}
const { exportName, exportsScope, id, exportable } = this.resolveExportedValue(exportedValue);
const output = exportsScope.node.tryFindChild(id) as CfnOutput;
if (!output) {
new CfnOutput(exportsScope, id, {
value: Token.asString(exportable),
exportName,
description: options.description,
});
}
const importValue = Fn.importValue(exportName);
if (Array.isArray(importValue)) {
throw new Error('Attempted to export a list value from `exportValue()`: use `exportStringListValue()` instead');
}
return importValue;
}
/**
* Create a CloudFormation Export for a string list value
*
* Returns a string list representing the corresponding `Fn.importValue()`
* expression for this Export. The export expression is automatically wrapped with an
* `Fn::Join` and the import value with an `Fn::Split`, since CloudFormation can only
* export strings. You can control the name for the export by passing the `name` option.
*
* If you don't supply a value for `name`, the value you're exporting must be
* a Resource attribute (for example: `bucket.bucketName`) and it will be
* given the same name as the automatic cross-stack reference that would be created
* if you used the attribute in another Stack.
*
* One of the uses for this method is to *remove* the relationship between
* two Stacks established by automatic cross-stack references. It will
* temporarily ensure that the CloudFormation Export still exists while you
* remove the reference from the consuming stack. After that, you can remove
* the resource and the manual export.
*
* See `exportValue` for an example of this process.
*/
public exportStringListValue(exportedValue: any, options: ExportValueOptions = {}): string[] {
if (options.name) {
new CfnOutput(this, `Export${options.name}`, {
value: Fn.join(STRING_LIST_REFERENCE_DELIMITER, exportedValue),
exportName: options.name,
description: options.description,
});
return Fn.split(STRING_LIST_REFERENCE_DELIMITER, Fn.importValue(options.name));
}
const { exportName, exportsScope, id, exportable } = this.resolveExportedValue(exportedValue);
const output = exportsScope.node.tryFindChild(id) as CfnOutput;
if (!output) {
new CfnOutput(exportsScope, id, {
// this is a list so export an Fn::Join expression
// and import an Fn::Split expression,
// since CloudFormation Outputs can only be strings
// (string lists are invalid)
value: Fn.join(STRING_LIST_REFERENCE_DELIMITER, Token.asList(exportable)),
exportName,
description: options.description,
});
}
// we don't use `Fn.importListValue()` since this array is a CFN attribute, and we don't know how long this attribute is
const importValue = Fn.split(STRING_LIST_REFERENCE_DELIMITER, Fn.importValue(exportName));
if (!Array.isArray(importValue)) {
throw new Error('Attempted to export a string value from `exportStringListValue()`: use `exportValue()` instead');
}
return importValue;
}
/**
* Returns the naming scheme used to allocate logical IDs. By default, uses
* the `HashedAddressingScheme` but this method can be overridden to customize
* this behavior.
*
* In order to make sure logical IDs are unique and stable, we hash the resource
* construct tree path (i.e. toplevel/secondlevel/.../myresource) and add it as
* a suffix to the path components joined without a separator (CloudFormation
* IDs only allow alphanumeric characters).
*
* The result will be:
*
* <path.join('')><md5(path.join('/')>
* "human" "hash"
*
* If the "human" part of the ID exceeds 240 characters, we simply trim it so
* the total ID doesn't exceed CloudFormation's 255 character limit.
*
* We only take 8 characters from the md5 hash (0.000005 chance of collision).
*
* Special cases:
*
* - If the path only contains a single component (i.e. it's a top-level
* resource), we won't add the hash to it. The hash is not needed for
* disambiguation and also, it allows for a more straightforward migration an
* existing CloudFormation template to a CDK stack without logical ID changes
* (or renames).
* - For aesthetic reasons, if the last components of the path are the same
* (i.e. `L1/L2/Pipeline/Pipeline`), they will be de-duplicated to make the
* resulting human portion of the ID more pleasing: `L1L2Pipeline<HASH>`
* instead of `L1L2PipelinePipeline<HASH>`
* - If a component is named "Default" it will be omitted from the path. This
* allows refactoring higher level abstractions around constructs without affecting
* the IDs of already deployed resources.
* - If a component is named "Resource" it will be omitted from the user-visible
* path, but included in the hash. This reduces visual noise in the human readable
* part of the identifier.
*
* @param cfnElement The element for which the logical ID is allocated.
*/
protected allocateLogicalId(cfnElement: CfnElement): string {
const scopes = cfnElement.node.scopes;
const stackIndex = scopes.indexOf(cfnElement.stack);
const pathComponents = scopes.slice(stackIndex + 1).map(x => x.node.id);
return makeUniqueId(pathComponents);
}
/**
* Validate stack name
*
* CloudFormation stack names can include dashes in addition to the regular identifier
* character classes, and we don't allow one of the magic markers.
*
* @internal
*/
protected _validateId(name: string) {
if (name && !VALID_STACK_NAME_REGEX.test(name)) {
throw new Error(`Stack name must match the regular expression: ${VALID_STACK_NAME_REGEX.toString()}, got '${name}'`);
}
}
/**
* Returns the CloudFormation template for this stack by traversing
* the tree and invoking _toCloudFormation() on all Entity objects.
*
* @internal
*/
protected _toCloudFormation() {
let transform: string | string[] | undefined;
if (this.templateOptions.transform) {
// eslint-disable-next-line max-len
Annotations.of(this).addWarningV2('@aws-cdk/core:stackDeprecatedTransform', 'This stack is using the deprecated `templateOptions.transform` property. Consider switching to `addTransform()`.');
this.addTransform(this.templateOptions.transform);
}
if (this.templateOptions.transforms) {
if (this.templateOptions.transforms.length === 1) { // Extract single value
transform = this.templateOptions.transforms[0];
} else { // Remove duplicate values
transform = Array.from(new Set(this.templateOptions.transforms));
}
}
const template: any = {
Description: this.templateOptions.description,
Transform: transform,
AWSTemplateFormatVersion: this.templateOptions.templateFormatVersion,
Metadata: this.templateOptions.metadata,
};
const elements = cfnElements(this);
const fragments = elements.map(e => this.resolve(e._toCloudFormation()));
// merge in all CloudFormation fragments collected from the tree
for (const fragment of fragments) {
merge(template, fragment);
}
// resolve all tokens and remove all empties
const ret = this.resolve(template) || {};
this._logicalIds.assertAllRenamesApplied();
return ret;
}
/**
* Deprecated.
*
* @see https://github.com/aws/aws-cdk/pull/7187
* @returns reference itself without any change
* @deprecated cross reference handling has been moved to `App.prepare()`.
*/
protected prepareCrossReference(_sourceStack: Stack, reference: Reference): IResolvable {
return reference;
}
/**
* Determine the various stack environment attributes.
*
*/
private parseEnvironment(env: Environment = {}) {
// if an environment property is explicitly specified when the stack is
// created, it will be used. if not, use tokens for account and region.
//
// (They do not need to be anchored to any construct like resource attributes
// are, because we'll never Export/Fn::ImportValue them -- the only situation
// in which Export/Fn::ImportValue would work is if the value are the same
// between producer and consumer anyway, so we can just assume that they are).
const containingAssembly = Stage.of(this);
if (env.account && typeof(env.account) !== 'string') {
throw new Error(`Account id of stack environment must be a 'string' but received '${typeof(env.account)}'`);
}
if (env.region && typeof(env.region) !== 'string') {
throw new Error(`Region of stack environment must be a 'string' but received '${typeof(env.region)}'`);
}
const account = env.account ?? containingAssembly?.account ?? Aws.ACCOUNT_ID;
const region = env.region ?? containingAssembly?.region ?? Aws.REGION;
// this is the "aws://" env specification that will be written to the cloud assembly
// manifest. it will use "unknown-account" and "unknown-region" to indicate
// environment-agnosticness.
const envAccount = !Token.isUnresolved(account) ? account : cxapi.UNKNOWN_ACCOUNT;
const envRegion = !Token.isUnresolved(region) ? region : cxapi.UNKNOWN_REGION;
return {
account,
region,
environment: cxapi.EnvironmentUtils.format(envAccount, envRegion),
};
}
/**
* Maximum number of resources in the stack
*
* Set to 0 to mean "unlimited".
*/
private get maxResources(): number {
const contextLimit = this.node.tryGetContext(STACK_RESOURCE_LIMIT_CONTEXT);
return contextLimit !== undefined ? parseInt(contextLimit, 10) : MAX_RESOURCES;
}
/**
* Check whether this stack has a (transitive) dependency on another stack
*
* Returns the list of reasons on the dependency path, or undefined
* if there is no dependency.
*/
private stackDependencyReasons(other: Stack): StackDependencyReason[] | undefined {
if (this === other) { return []; }
for (const dep of Object.values(this._stackDependencies)) {
const ret = dep.stack.stackDependencyReasons(other);
if (ret !== undefined) {
return [...dep.reasons, ...ret];
}
}
return undefined;
}
/**
* Calculate the stack name based on the construct path
*
* The stack name is the name under which we'll deploy the stack,
* and incorporates containing Stage names by default.
*
* Generally this looks a lot like how logical IDs are calculated.
* The stack name is calculated based on the construct root path,
* as follows:
*
* - Path is calculated with respect to containing App or Stage (if any)
* - If the path is one component long just use that component, otherwise
* combine them with a hash.
*
* Since the hash is quite ugly and we'd like to avoid it if possible -- but
* we can't anymore in the general case since it has been written into legacy
* stacks. The introduction of Stages makes it possible to make this nicer however.
* When a Stack is nested inside a Stage, we use the path components below the
* Stage, and prefix the path components of the Stage before it.
*/
private generateStackName() {
const assembly = Stage.of(this);
const prefix = (assembly && assembly.stageName) ? `${assembly.stageName}-` : '';
if (FeatureFlags.of(this).isEnabled(INCLUDE_PREFIX_IN_UNIQUE_NAME_GENERATION)) {
return `${this.generateStackId(assembly, prefix)}`;
} else {
return `${prefix}${this.generateStackId(assembly)}`;
}
}
/**
* The artifact ID for this stack
*
* Stack artifact ID is unique within the App's Cloud Assembly.
*/
private generateStackArtifactId() {
return this.generateStackId(this.node.root);
}
/**
* Generate an ID with respect to the given container construct.
*/
private generateStackId(container: IConstruct | undefined, prefix: string='') {
const rootPath = rootPathTo(this, container);
const ids = rootPath.map(c => Node.of(c).id);
// In unit tests our Stack (which is the only component) may not have an
// id, so in that case just pretend it's "Stack".
if (ids.length === 1 && !ids[0]) {
throw new Error('unexpected: stack id must always be defined');
}
return makeStackName(ids, prefix);
}
private resolveExportedValue(exportedValue: any): ResolvedExport {
const resolvable = Tokenization.reverse(exportedValue);
if (!resolvable || !Reference.isReference(resolvable)) {
throw new Error('exportValue: either supply \'name\' or make sure to export a resource attribute (like \'bucket.bucketName\')');
}
// "teleport" the value here, in case it comes from a nested stack. This will also
// ensure the value is from our own scope.
const exportable = getExportable(this, resolvable);
// Ensure a singleton "Exports" scoping Construct
// This mostly exists to trigger LogicalID munging, which would be
// disabled if we parented constructs directly under Stack.
// Also it nicely prevents likely construct name clashes
const exportsScope = getCreateExportsScope(this);
// Ensure a singleton CfnOutput for this value
const resolved = this.resolve(exportable);
const id = 'Output' + JSON.stringify(resolved);
const exportName = generateExportName(exportsScope, id);
if (Token.isUnresolved(exportName)) {
throw new Error(`unresolved token in generated export name: ${JSON.stringify(this.resolve(exportName))}`);
}
return {
exportable,
exportsScope,
id,
exportName,
};
}
/**
* Indicates whether the stack requires bundling or not
*/
public get bundlingRequired() {
const bundlingStacks: string[] = this.node.tryGetContext(cxapi.BUNDLING_STACKS) ?? ['**'];
return bundlingStacks.some(pattern => minimatch(
this.node.path, // use the same value for pattern matching as the aws-cdk CLI (displayName / hierarchicalId)
pattern,
));
}
}
function merge(template: any, fragment: any): void {
for (const section of Object.keys(fragment)) {
const src = fragment[section];
// create top-level section if it doesn't exist
const dest = template[section];
if (!dest) {
template[section] = src;
} else {
template[section] = mergeSection(section, dest, src);
}
}
}
function mergeSection(section: string, val1: any, val2: any): any {
switch (section) {
case 'Description':
return `${val1}\n${val2}`;
case 'AWSTemplateFormatVersion':
if (val1 != null && val2 != null && val1 !== val2) {
throw new Error(`Conflicting CloudFormation template versions provided: '${val1}' and '${val2}`);
}
return val1 ?? val2;
case 'Transform':
return mergeSets(val1, val2);
default:
return mergeObjectsWithoutDuplicates(section, val1, val2);
}
}
function mergeSets(val1: any, val2: any): any {
const array1 = val1 == null ? [] : (Array.isArray(val1) ? val1 : [val1]);
const array2 = val2 == null ? [] : (Array.isArray(val2) ? val2 : [val2]);
for (const value of array2) {
if (!array1.includes(value)) {
array1.push(value);
}
}
return array1.length === 1 ? array1[0] : array1;
}
function mergeObjectsWithoutDuplicates(section: string, dest: any, src: any): any {
if (typeof dest !== 'object') {
throw new Error(`Expecting ${JSON.stringify(dest)} to be an object`);
}
if (typeof src !== 'object') {
throw new Error(`Expecting ${JSON.stringify(src)} to be an object`);
}
// add all entities from source section to destination section
for (const id of Object.keys(src)) {
if (id in dest) {
throw new Error(`section '${section}' already contains '${id}'`);
}
dest[id] = src[id];
}
return dest;
}
/**
* CloudFormation template options for a stack.
*/
export interface ITemplateOptions {
/**
* Gets or sets the description of this stack.
* If provided, it will be included in the CloudFormation template's "Description" attribute.
*/
description?: string;
/**
* Gets or sets the AWSTemplateFormatVersion field of the CloudFormation template.
*/
templateFormatVersion?: string;
/**
* Gets or sets the top-level template transform for this stack (e.g. "AWS::Serverless-2016-10-31").
*
* @deprecated use `transforms` instead.
*/
transform?: string;
/**
* Gets or sets the top-level template transform(s) for this stack (e.g. `["AWS::Serverless-2016-10-31"]`).
*/
transforms?: string[];
/**
* Metadata associated with the CloudFormation template.
*/
metadata?: { [key: string]: any };
}
/**
* Collect all CfnElements from a Stack.
*
* @param node Root node to collect all CfnElements from
* @param into Array to append CfnElements to
* @returns The same array as is being collected into
*/
function cfnElements(node: IConstruct, into: CfnElement[] = []): CfnElement[] {
if (CfnElement.isCfnElement(node)) {
into.push(node);
}
for (const child of Node.of(node).children) {
// Don't recurse into a substack
if (Stack.isStack(child)) { continue; }
cfnElements(child, into);
}
return into;
}
/**
* Return the construct root path of the given construct relative to the given ancestor
*
* If no ancestor is given or the ancestor is not found, return the entire root path.
*/
export function rootPathTo(construct: IConstruct, ancestor?: IConstruct): IConstruct[] {
const scopes = Node.of(construct).scopes;
for (let i = scopes.length - 2; i >= 0; i--) {
if (scopes[i] === ancestor) {
return scopes.slice(i + 1);
}
}
return scopes;
}
/**
* makeUniqueId, specialized for Stack names
*
* Stack names may contain '-', so we allow that character if the stack name
* has only one component. Otherwise we fall back to the regular "makeUniqueId"
* behavior.
*/
function makeStackName(components: string[], prefix: string='') {
if (components.length === 1) {
const stack_name = prefix + components[0];
if (stack_name.length <= 128) {
return stack_name;
}
}
return makeUniqueResourceName(components, { maxLength: 128, prefix: prefix });
}
function getCreateExportsScope(stack: Stack) {
const exportsName = 'Exports';
let stackExports = stack.node.tryFindChild(exportsName) as Construct;
if (stackExports === undefined) {
stackExports = new Construct(stack, exportsName);
}
return stackExports;
}
function generateExportName(stackExports: Construct, id: string) {
const stackRelativeExports = FeatureFlags.of(stackExports).isEnabled(cxapi.STACK_RELATIVE_EXPORTS_CONTEXT);
const stack = Stack.of(stackExports);
const components = [
...stackExports.node.scopes
.slice(stackRelativeExports ? stack.node.scopes.length : 2)
.map(c => c.node.id),
id,
];
const prefix = stack.stackName ? stack.stackName + ':' : '';
const localPart = makeUniqueId(components);
const maxLength = 255;
return prefix + localPart.slice(Math.max(0, localPart.length - maxLength + prefix.length));
}
interface StackDependencyReason {
source?: Element;
target?: Element;
description?: string;
}
interface StackDependency {
stack: Stack;
reasons: StackDependencyReason[];
}
interface ResolvedExport {
exportable: Intrinsic;
exportsScope: Construct;
id: string;
exportName: string;
}
/**
* Options for the `stack.exportValue()` method
*/
export interface ExportValueOptions {
/**
* The name of the export to create
*
* @default - A name is automatically chosen
*/
readonly name?: string;
/**
* The description of the outputs
*
* @default - No description
*/
readonly description?: string;
}
function count(xs: string[]): Record<string, number> {
const ret: Record<string, number> = {};
for (const x of xs) {
if (x in ret) {
ret[x] += 1;
} else {
ret[x] = 1;
}
}
return ret;
}
// These imports have to be at the end to prevent circular imports
/* eslint-disable import/order */
import { CfnOutput } from './cfn-output';
import { addDependency, Element } from './deps';
import { FileSystem } from './fs';
import { Names } from './names';
import { Reference } from './reference';
import { IResolvable } from './resolvable';
import { DefaultStackSynthesizer, IStackSynthesizer, ISynthesisSession, LegacyStackSynthesizer, BOOTSTRAP_QUALIFIER_CONTEXT, isReusableStackSynthesizer } from './stack-synthesizers';
import { StringSpecializer } from './helpers-internal/string-specializer';
import { Stage } from './stage';
import { ITaggable, TagManager } from './tag-manager';
import { Token, Tokenization } from './token';
import { getExportable, STRING_LIST_REFERENCE_DELIMITER } from './private/refs';
import { Fact, RegionInfo } from '../../region-info';
import { deployTimeLookup } from './private/region-lookup';
import { makeUniqueResourceName } from './private/unique-resource-name';
import { PRIVATE_CONTEXT_DEFAULT_STACK_SYNTHESIZER } from './private/private-context';
import { Intrinsic } from './private/intrinsic';
import { mutatingAspectPrio32333 } from './private/aspect-prio';
/* eslint-enable import/order */