@aws-amplify/amplify-category-api
Version:
Amplify CLI API Category Plugin
173 lines (146 loc) • 5.61 kB
text/typescript
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;
}