packages/@alicloud/ros-cdk-core/lib/stack.ts (441 lines of code) (raw):
import * as cxapi from "@alicloud/ros-cdk-cxapi";
import * as fs from "fs";
import * as path from "path";
import { Construct, IConstruct, ISynthesisSession } from "./construct-compat";
import { ROS_TOKEN_RESOLVER, RosTemplateLang } from "./private/template-lang";
import { LogicalIDs } from "./private/logical-id";
import { resolve } from "./private/resolve";
import { makeUniqueId } from "./private/uniqueid";
import { RosInfo } from "./ros-info";
const minimatch = require('minimatch');
const STACK_SYMBOL = Symbol.for("ros-cdk-core.Stack");
const MY_STACK_CACHE = Symbol.for("ros-cdk-core.Stack.myStack");
const VALID_STACK_NAME_REGEX = /^[A-Za-z][A-Za-z0-9-]*$/;
export interface RamRoles {
/**
* The RAM role ARN that grants FC function the required permissions.
*/
readonly fcRole: IResolvable | string;
}
export interface StackProps {
readonly version?: String;
/**
* A description of the stack.
*
* @default - No description.
*/
readonly description?: string;
/**
* The ALIYUN 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 ALIYUN 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.
*
* @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: 'cn-hangzhou'
* },
* });
*
* // 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: 'cn-hangzhou'
* }
* });
*
* // 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": "ALIYUN::AccountId" }` and `{ "Ref": "ALIYUN::Region" }` respectively.
* // which will only resolve to actual values by ROS during deployment.
* new MyStack(app, 'Stack1');
*
* @default - The environment of the containing `Stage` if available,
* otherwise create the stack will be environment-agnostic.
*
* @experimental
*/
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 };
/**
* Synthesis method to use while deploying this stack
*
* @default - `DefaultStackSynthesizer`
*/
readonly synthesizer?: IStackSynthesizer;
readonly metadata?: {[key: string]: any};
readonly enableResourcePropertyConstraint?: boolean;
}
/**
* A root construct which represents a single ROS 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(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.
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;
}
if (!c.node.scope) {
throw new Error(
`No stack could be identified for the construct at path ${construct.node.path}`
);
}
return _lookup(c.node.scope);
}
}
/**
* Tags to be applied to the stack.
*/
public readonly tags: TagManager;
/**
* The ALIYUN region into which this stack will be deployed (e.g. `cn-beijing`).
*
* 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 or the `ALIYUN.REGION` token.
* 2. `ALIYUN.REGION`, which is represents the ROS intrinsic reference
* `{ "Ref": "ALIYUN::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 ALIYUN 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 or the `ALIYUN.ACCOUNT_ID` token.
* 2. `ALIYUN.ACCOUNT_ID`, which represents the ROS intrinsic reference
* `{ "Ref": "ALIYUN::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 a **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 region-agnostic behavior.
*/
public readonly account: string;
public roles?: RamRoles;
/**
* Options for ROS template (like version, description).
*/
public readonly templateOptions: ITemplateOptions = {};
/**
* If this is a nested stack, this represents its `ALIYUN::ROS::Stack`
* resource. `undefined` for top-level (non-nested) stacks.
*
* @experimental
*/
public readonly nestedStackResource?: RosResource;
/**
* The name of the ROS template file emitted to the output
* directory during synthesis.
*
* @example 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
*
* @experimental
*/
public readonly synthesizer: IStackSynthesizer;
/**
* Logical ID generation strategy
*/
private readonly _logicalIds: LogicalIDs;
/**
* Other stacks this stack depends on
*/
private readonly _stackDependencies: {
[uniqueId: string]: StackDependency;
} = {};
private readonly _stackName: string;
public readonly enableResourcePropertyConstraint: boolean;
private readonly maxResources: number = 300;
/**
* Creates a new stack.
*
* @param scope Parent of this stack, usually a Program instance.
* @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 convenience parents are optional, so bypass the type check when calling the parent.
super(scope!, id!);
Object.defineProperty(this, STACK_SYMBOL, { value: true });
this._logicalIds = new LogicalIDs();
this.enableResourcePropertyConstraint =
props.enableResourcePropertyConstraint === undefined ? true : 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;
}
if (props.metadata == undefined) {
this.templateOptions.metadata = {'ALIYUN::ROS::Interface':{'TemplateTags':["Create by ROS CDK"]}};
}
this._stackName =
props.stackName !== undefined
? props.stackName
: this.generateStackName();
this.tags = new TagManager(
TagType.KEY_VALUE,
"aliyun:ros:stack",
props.tags
);
const { account, region } = this.parseEnvironment(props.env);
this.account = account;
this.region = region;
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.
// tslint:disable-next-line: max-line-length
this.artifactId =
this.node.tryGetContext(cxapi.ENABLE_STACK_NAME_DUPLICATES_CONTEXT) ||
this.node.tryGetContext(cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT)
? this.generateStackArtifactId()
: this.stackName;
this.templateFile = `${this.artifactId}.template.json`;
const synthesizer = props.synthesizer ?? new DefaultStackSynthesizer();
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);
}
new RosInfo(
this,
RosInfo.formatVersion,
props.version ? props.version : RosInfo.v20150901
);
}
/**
* Resolve a tokenized value in the context of the current stack.
*/
public resolve(obj: any): any {
return resolve(obj, {
scope: this,
prefix: [],
resolver: ROS_TOKEN_RESOLVER,
preparing: false,
});
}
/**
* 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.
const containingAssembly = Stage.of(this);
const account = env.account ?? containingAssembly?.account ?? RosPseudo.accountId;
const region = env.region ?? containingAssembly?.region ?? RosPseudo.region;
return {
account,
region
};
}
/**
* Convert an object, potentially containing tokens, to a JSON string
*/
public toJsonString(obj: any, space?: number): string {
return RosTemplateLang.toJSON(obj, space).toString();
}
/**
* 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 logical identity for a
* specific resource.
*
* This method is called when a `RosElement` 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 ROS element for which a logical identity is
* needed.
*/
public getLogicalId(element: RosElement): 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);
}
/**
* Return the stacks this stack depends on
*/
public get dependencies(): Stack[] {
return Object.values(this._stackDependencies).map((x) => x.stack);
}
/**
* The concrete ROS 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.
*
*/
public get stackName(): string {
return this._stackName;
}
/**
* The ID of the stack
*
*/
public get stackId(): string {
return RosPseudo.stackId;
}
/**
* 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;
}
/**
* 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;
}
/**
* 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?: string) {
// 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"
);
}
reason = reason || "dependency added using stack.addDependency()";
const cycle = target.stackDependencyReasons(this);
if (cycle !== undefined) {
// tslint:disable-next-line:max-line-length
throw new Error(
`'${target.node.path}' depends on '${this.node.path}' (${cycle.join(
", "
)}). Adding this dependency (${reason}) would create a cyclic reference.`
);
}
let dep = this._stackDependencies[target.node.uniqueId];
if (!dep) {
dep = this._stackDependencies[target.node.uniqueId] = {
stack: target,
reasons: [],
};
}
dep.reasons.push(reason);
if (process.env.CDK_DEBUG_DEPS) {
// tslint:disable-next-line:no-console
console.error(
`[CDK_DEBUG_DEPS] stack "${this.node.path}" depends on "${target.node.path}" because: ${reason}`
);
}
}
/**
* 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 (ROS
* 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 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
* disamiguation and also, it allows for a more straightforward migration an
* existing ROS 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 rosElement The element for which the logical ID is allocated.
*/
protected allocateLogicalId(rosElement: RosElement): string {
const scopes = rosElement.node.scopes;
const stackIndex = scopes.indexOf(rosElement.stack);
const pathComponents = scopes.slice(stackIndex + 1).map((x) => x.node.id);
return makeUniqueId(pathComponents);
}
/**
* Validate stack name
*
* ROS 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}'`
);
}
}
public synthesize(session: ISynthesisSession): 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._toRosTemplate();
// write the ROS template as a JSON file
const outPath = path.join(builder.outdir, this.templateFile);
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}`);
}
const text = JSON.stringify(template, undefined, 2);
fs.writeFileSync(outPath, text);
// Delegate adding artifacts to the Synthesizer
// this.synthesizer.synthesize(session);
}
/**
* Returns the RosTemplate template for this stack by traversing
* the tree and invoking _toRosTemplate() on all Entity objects.
*
* @internal
*/
protected _toRosTemplate() {
const template: any = {
Description: this.templateOptions.description,
Metadata: this.templateOptions.metadata,
};
const elements = rosElements(this);
const fragments = elements.map((e) => this.resolve(e._toRosTemplate()));
// merge in all ROS 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.
*
* @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;
}
/**
* 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): string[] | 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}-` : "";
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) {
const rootPath = rootPathTo(this, container);
const ids = rootPath.map((c) => c.node.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]) {
ids[0] = "Stack";
}
return makeStackName(ids);
}
/**
* 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 ALIYUN-cdk CLI (displayName / hierarchicalId)
pattern,
));
}
/**
* Splits the provided ARN into its components.
* Works both if 'arn' is a string like 'acs:ram::123456789012****:role/RoleName',
* and a Token representing a dynamic ROS expression
* (in which case the returned components will also be dynamic ROS 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 | IResolvable, arnFormat: ArnFormat): ArnComponents {
return Arn.split(arn, arnFormat);
}
}
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 "Resources":
case "Conditions":
case "Parameters":
case "Outputs":
case "Mappings":
case "Rules":
return mergeObjectsWithoutDuplicates(section, val1, val2);
case "Metadata":
return mergeMetadataObjectsWithoutDuplicates(val1, val2);
default:
throw new Error(
`CDK doesn't know how to merge two instances of the ROS template section '${section}' - ` +
"please remove one of them from your code"
);
}
}
function mergeMetadataObjectsWithoutDuplicates(
dest: any,
src: any
): any {
if (typeof dest !== "object" && typeof src !== "object") {
throw new Error(`Expecting Metadata Value to be an object`);
}
if (src.hasOwnProperty('ALIYUN::ROS::Interface')) {
if (typeof src["ALIYUN::ROS::Interface"] == "object") {
if (src["ALIYUN::ROS::Interface"].hasOwnProperty('TemplateTags')) {
if (src["ALIYUN::ROS::Interface"] ["TemplateTags"] instanceof Array) {
src["ALIYUN::ROS::Interface"] ["TemplateTags"].push("Create by ROS CDK")
dest["ALIYUN::ROS::Interface"] = src["ALIYUN::ROS::Interface"]
}
else {
throw new Error(`Expecting Metadata ALIYUN::ROS::Interface TemplateTags Value to be an Array`);
}
}
else {
dest["ALIYUN::ROS::Interface"] = src["ALIYUN::ROS::Interface"]
dest["ALIYUN::ROS::Interface"].TemplateTags = ["Create by ROS CDK"]
}
}
else {
throw new Error(`Expecting Metadata ALIYUN::ROS::Interface Value to be an object`);
}
}
else {
dest["ALIYUN::ROS::Interface"] = { TemplateTags : ["Create by ROS CDK"]}
}
for (const id of Object.keys(src)) {
if (id !== 'ALIYUN::ROS::Interface') {
dest[id] = src[id];
}
}
return dest;
}
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;
}
/**
* ROS template options for a stack.
*/
export interface ITemplateOptions {
/**
* Gets or sets the description of this stack.
* If provided, it will be included in the ROS template's "Description" attribute.
*/
description?: string;
/**
* Metadata associated with the ROS template.
*/
metadata?: { [key: string]: any };
}
/**
* Collect all rosElements from a Stack.
*
* @param node Root node to collect all rosElements from
* @param into Array to append rosElements to
* @returns The same array as is being collected into
*/
function rosElements(node: IConstruct, into: RosElement[] = []): RosElement[] {
if (RosElement.isRosElement(node)) {
into.push(node);
}
for (const child of node.node.children) {
// Don't recurse into a substack
if (Stack.isStack(child)) {
continue;
}
rosElements(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 = construct.node.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[]) {
if (components.length === 1) {
return components[0];
}
return makeUniqueId(components);
}
// These imports have to be at the end to prevent circular imports
import { RosElement } from "./ros-element";
import { RosPseudo } from "./ros-pseudo";
import { RosResource } from "./ros-resource";
import { TagType } from "./tag-manager";
import { addDependency } from "./deps";
import { Reference } from "./reference";
import { IResolvable } from "./resolvable";
import {
DefaultStackSynthesizer, isReusableStackSynthesizer,
IStackSynthesizer,
} from "./stack-synthesizers";
import { Stage } from "./stage";
import { ITaggable, TagManager } from "./tag-manager";
import {Environment} from "./environment";
import {Arn, ArnFormat, ArnComponents} from "./arn";
interface StackDependency {
stack: Stack;
reasons: 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;
}