UNPKG

@aws-amplify/amplify-category-api

Version:
173 lines (146 loc) 5.61 kB
import * as crypto from 'crypto'; import { Aws, CfnResource, CfnStack, FileAssetPackaging, Fn, IResolveContext, IStackSynthesizer, Lazy, NestedStackProps, Stack, Token, } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { TransformerRootStack } from './root-stack'; import { TransformerStackSythesizer } from './stack-synthesizer'; export type TransformerNestedStackProps = NestedStackProps & { synthesizer?: IStackSynthesizer; }; export class TransformerNestedStack extends TransformerRootStack { public readonly templateFile: string; public readonly nestedStackResource?: CfnResource; private readonly parameters: { [name: string]: string }; private readonly resource: CfnStack; private readonly _contextualStackId: string; private readonly _contextualStackName: string; private _templateUrl?: string; private _rootStack: Stack; constructor(scope: Construct, id: string, props: TransformerNestedStackProps = {}) { const rootStack = findRootStack(scope); const synthesizer = props.synthesizer || new TransformerStackSythesizer(); super(scope, id, { env: { account: rootStack.account, region: rootStack.region }, ...props, synthesizer, }); this._rootStack = rootStack; const parentScope = new Construct(scope, `${id}.NestedStack`); // Transformer synthesizes the stack in memory and stack names are going to be unique this.templateFile = `stacks/${id}.json`; this.parameters = props.parameters || {}; this.resource = new CfnStack(parentScope, `${id}.NestedStackResource`, { templateUrl: Lazy.uncachedString({ produce: () => { return this._templateUrl || '<unresolved>'; }, }), parameters: Lazy.any({ produce: () => (Object.keys(this.parameters).length > 0 ? this.parameters : undefined), }), notificationArns: props.notificationArns, timeoutInMinutes: props.timeout ? props.timeout.toMinutes() : undefined, }); this.nestedStackResource = this.resource; // context-aware stack name: if resolved from within this stack, return AWS::StackName // if resolved from the outer stack, use the { Ref } of the AWS::CloudFormation::Stack resource // which resolves the ARN of the stack. We need to extract the stack name, which is the second // component after splitting by "/" this._contextualStackName = this.contextualAttribute(Aws.STACK_NAME, Fn.select(1, Fn.split('/', this.resource.ref))); this._contextualStackId = this.contextualAttribute(Aws.STACK_ID, this.resource.ref); } /** * An attribute that represents the name of the nested stack. * * This is a context aware attribute: * - If this is referenced from the parent stack, it will return a token that parses the name from the stack ID. * - If this is referenced from the context of the nested stack, it will return `{ "Ref": "AWS::StackName" }` * * @attribute * @example mystack-mynestedstack-sggfrhxhum7w */ public get stackName() { return this._contextualStackName; } /** * An attribute that represents the ID of the stack. * * This is a context aware attribute: * - If this is referenced from the parent stack, it will return `{ "Ref": "LogicalIdOfNestedStackResource" }`. * - If this is referenced from the context of the nested stack, it will return `{ "Ref": "AWS::StackId" }` * * @attribute * @example "arn:aws:cloudformation:us-east-2:123456789012:stack/mystack-mynestedstack-sggfrhxhum7w/f449b250-b969-11e0-a185-5081d0136786" */ public get stackId() { return this._contextualStackId; } /** * Assign a value to one of the nested stack parameters. * @param name The parameter name (ID) * @param value The value to assign */ public setParameter(name: string, value: string) { this.parameters[name] = value; } /** * Defines an asset at the parent stack which represents the template of this * nested stack. * * This private API is used by `App.prepare()` within a loop that rectifies * references every time an asset is added. This is because (at the moment) * assets are addressed using CloudFormation parameters. * * @returns `true` if a new asset was added or `false` if an asset was * previously added. When this returns `true`, App will do another reference * rectification cycle. * * @internal */ public _prepareTemplateAsset() { if (this._templateUrl) { return false; } const cfn = JSON.stringify((this as any)._toCloudFormation()); const templateHash = crypto.createHash('sha256').update(cfn).digest('hex'); const templateLocation = this._rootStack.synthesizer.addFileAsset({ packaging: FileAssetPackaging.FILE, sourceHash: templateHash, fileName: this.templateFile, }); this._templateUrl = templateLocation.httpUrl; return true; } private contextualAttribute(innerValue: string, outerValue: string) { return Token.asString({ resolve: (context: IResolveContext) => { if (Stack.of(context.scope) === this) { return innerValue; } else { return outerValue; } }, }); } } function findRootStack(scope: Construct): Stack { if (!scope) { throw new Error('Nested stacks cannot be defined as a root construct'); } const rootStack = scope.node.scopes.find((p) => Stack.isStack(p)); if (!rootStack) { throw new Error('Nested stacks must be defined within scope of another non-nested stack'); } return rootStack as Stack; }