packages/aws-cdk-lib/cloudformation-include/lib/cfn-include.ts (541 lines of code) (raw):
import { Construct } from 'constructs';
import * as cfn_type_to_l1_mapping from './cfn-type-to-l1-mapping';
import * as futils from './file-utils';
import * as core from '../../core';
import * as cfn_parse from '../../core/lib/helpers-internal';
/**
* Construction properties of `CfnInclude`.
*/
export interface CfnIncludeProps {
/**
* Path to the template file.
*
* Both JSON and YAML template formats are supported.
*/
readonly templateFile: string;
/**
* Whether the resources should have the same logical IDs in the resulting CDK template
* as they did in the original CloudFormation template file.
* If you're vending a Construct using an existing CloudFormation template,
* make sure to pass this as `false`.
*
* **Note**: regardless of whether this option is true or false,
* the `CfnInclude.getResource` and related methods always uses the original logical ID of the resource/element,
* as specified in the template file.
*
* @default true
*/
readonly preserveLogicalIds?: boolean;
/**
* Specifies the template files that define nested stacks that should be included.
*
* If your template specifies a stack that isn't included here, it won't be created as a NestedStack
* resource, and it won't be accessible from the `CfnInclude.getNestedStack` method
* (but will still be accessible from the `CfnInclude.getResource` method).
*
* If you include a stack here with an ID that isn't in the template,
* or is in the template but is not a nested stack,
* template creation will fail and an error will be thrown.
*
* @default - no nested stacks will be included
*/
readonly loadNestedStacks?: { [stackName: string]: CfnIncludeProps };
/**
* Specifies parameters to be replaced by the values in this mapping.
* Any parameters in the template that aren't specified here will be left unmodified.
* If you include a parameter here with an ID that isn't in the template,
* template creation will fail and an error will be thrown.
*
* If you are importing a parameter from a live stack, we cannot know the value of that
* parameter. You will need to supply a value for your parameters, else the default
* value will be used.
*
* @default - parameters will retain their original definitions
*/
readonly parameters?: { [parameterName: string]: any };
/**
* Specifies whether to allow cyclical references, effectively disregarding safeguards meant to avoid undeployable
* templates. This should only be set to true in the case of templates utilizing cloud transforms (e.g. SAM) that
* after processing the transform will no longer contain any circular references.
*
* @default - will throw an error on detecting any cyclical references
*/
readonly allowCyclicalReferences?: boolean;
/**
* Specifies a list of LogicalIDs for resources that will be included in the CDK Stack,
* but will not be parsed and converted to CDK types. This allows you to use CFN templates
* that rely on Intrinsic placement that `cfn-include`
* would otherwise reject, such as non-primitive values in resource update policies.
*
* @default - All resources are hydrated
*/
readonly dehydratedResources?: string[];
}
/**
* The type returned from `CfnInclude.getNestedStack`.
* Contains both the NestedStack object and
* CfnInclude representations of the child stack.
*/
export interface IncludedNestedStack {
/**
* The NestedStack object which represents the scope of the template.
*/
readonly stack: core.NestedStack;
/**
* The CfnInclude that represents the template, which can
* be used to access Resources and other template elements.
*/
readonly includedTemplate: CfnInclude;
}
/**
* Construct to import an existing CloudFormation template file into a CDK application.
* All resources defined in the template file can be retrieved by calling the `getResource` method.
* Any modifications made on the returned resource objects will be reflected in the resulting CDK template.
*/
export class CfnInclude extends core.CfnElement {
private readonly conditions: { [conditionName: string]: core.CfnCondition } = {};
private readonly conditionsScope: Construct;
private readonly resources: { [logicalId: string]: core.CfnResource } = {};
private readonly parameters: { [logicalId: string]: core.CfnParameter } = {};
private readonly parametersToReplace: { [parameterName: string]: any };
private readonly mappingsScope: Construct;
private readonly mappings: { [mappingName: string]: core.CfnMapping } = {};
private readonly rules: { [ruleName: string]: core.CfnRule } = {};
private readonly rulesScope: Construct;
private readonly hooks: { [hookName: string]: core.CfnHook } = {};
private readonly hooksScope: Construct;
private readonly outputs: { [logicalId: string]: core.CfnOutput } = {};
private readonly nestedStacks: { [logicalId: string]: IncludedNestedStack } = {};
private readonly nestedStacksToInclude: { [name: string]: CfnIncludeProps };
private readonly template: any;
private readonly preserveLogicalIds: boolean;
private readonly allowCyclicalReferences: boolean;
private readonly dehydratedResources: string[];
private logicalIdToPlaceholderMap: Map<string, string>;
constructor(scope: Construct, id: string, props: CfnIncludeProps) {
super(scope, id);
this.allowCyclicalReferences = props.allowCyclicalReferences ?? false;
this.logicalIdToPlaceholderMap = new Map<string, string>();
this.parametersToReplace = props.parameters || {};
// read the template into a JS object
this.template = futils.readYamlSync(props.templateFile);
this.preserveLogicalIds = props.preserveLogicalIds ?? true;
this.dehydratedResources = props.dehydratedResources ?? [];
for (const logicalId of this.dehydratedResources) {
if (!Object.keys(this.template.Resources).includes(logicalId)) {
throw new core.ValidationError(`Logical ID '${logicalId}' was specified in 'dehydratedResources', but does not belong to a resource in the template.`, this);
}
}
// check if all user specified parameter values exist in the template
for (const logicalId of Object.keys(this.parametersToReplace)) {
if (!(logicalId in (this.template.Parameters || {}))) {
throw new core.ValidationError(`Parameter with logical ID '${logicalId}' was not found in the template`, this);
}
}
// instantiate the Mappings
this.mappingsScope = new Construct(this, '$Mappings');
for (const mappingName of Object.keys(this.template.Mappings || {})) {
this.createMapping(mappingName);
}
// instantiate all parameters
for (const logicalId of Object.keys(this.template.Parameters || {})) {
this.createParameter(logicalId);
}
// instantiate the conditions
this.conditionsScope = new Construct(this, '$Conditions');
for (const conditionName of Object.keys(this.template.Conditions || {})) {
this.getOrCreateCondition(conditionName);
}
// instantiate the rules
this.rulesScope = new Construct(this, '$Rules');
for (const ruleName of Object.keys(this.template.Rules || {})) {
this.createRule(ruleName);
}
this.nestedStacksToInclude = props.loadNestedStacks || {};
// instantiate all resources as CDK L1 objects
for (const logicalId of Object.keys(this.template.Resources || {})) {
this.getOrCreateResource(logicalId);
}
// verify that all nestedStacks have been instantiated
for (const nestedStackId of Object.keys(props.loadNestedStacks || {})) {
if (!(nestedStackId in this.resources)) {
throw new core.ValidationError(`Nested Stack with logical ID '${nestedStackId}' was not found in the template`, this);
}
}
// instantiate the Hooks
this.hooksScope = new Construct(this, '$Hooks');
for (const hookName of Object.keys(this.template.Hooks || {})) {
this.createHook(hookName);
}
const outputScope = new Construct(this, '$Outputs');
for (const logicalId of Object.keys(this.template.Outputs || {})) {
this.createOutput(logicalId, outputScope);
}
}
/**
* Returns the low-level CfnResource from the template with the given logical ID.
* Any modifications performed on that resource will be reflected in the resulting CDK template.
*
* The returned object will be of the proper underlying class;
* you can always cast it to the correct type in your code:
*
* // assume the template contains an AWS::S3::Bucket with logical ID 'Bucket'
* const cfnBucket = cfnTemplate.getResource('Bucket') as s3.CfnBucket;
* // cfnBucket is of type s3.CfnBucket
*
* If the template does not contain a resource with the given logical ID,
* an exception will be thrown.
*
* @param logicalId the logical ID of the resource in the CloudFormation template file
*/
public getResource(logicalId: string): core.CfnResource {
const ret = this.resources[logicalId];
if (!ret) {
throw new core.ValidationError(`Resource with logical ID '${logicalId}' was not found in the template`, this);
}
return ret;
}
/**
* Returns the CfnCondition object from the 'Conditions'
* section of the CloudFormation template with the given name.
* Any modifications performed on that object will be reflected in the resulting CDK template.
*
* If a Condition with the given name is not present in the template,
* throws an exception.
*
* @param conditionName the name of the Condition in the CloudFormation template file
*/
public getCondition(conditionName: string): core.CfnCondition {
const ret = this.conditions[conditionName];
if (!ret) {
throw new core.ValidationError(`Condition with name '${conditionName}' was not found in the template`, this);
}
return ret;
}
/**
* Returns the CfnParameter object from the 'Parameters'
* section of the included template.
* Any modifications performed on that object will be reflected in the resulting CDK template.
*
* If a Parameter with the given name is not present in the template,
* throws an exception.
*
* @param parameterName the name of the parameter to retrieve
*/
public getParameter(parameterName: string): core.CfnParameter {
const ret = this.parameters[parameterName];
if (!ret) {
throw new core.ValidationError(`Parameter with name '${parameterName}' was not found in the template`, this);
}
return ret;
}
/**
* Returns the CfnMapping object from the 'Mappings' section of the included template.
* Any modifications performed on that object will be reflected in the resulting CDK template.
*
* If a Mapping with the given name is not present in the template,
* an exception will be thrown.
*
* @param mappingName the name of the Mapping in the template to retrieve
*/
public getMapping(mappingName: string): core.CfnMapping {
const ret = this.mappings[mappingName];
if (!ret) {
throw new core.ValidationError(`Mapping with name '${mappingName}' was not found in the template`, this);
}
return ret;
}
/**
* Returns the CfnOutput object from the 'Outputs'
* section of the included template.
* Any modifications performed on that object will be reflected in the resulting CDK template.
*
* If an Output with the given name is not present in the template,
* throws an exception.
*
* @param logicalId the name of the output to retrieve
*/
public getOutput(logicalId: string): core.CfnOutput {
const ret = this.outputs[logicalId];
if (!ret) {
throw new core.ValidationError(`Output with logical ID '${logicalId}' was not found in the template`, this);
}
return ret;
}
/**
* Returns the CfnRule object from the 'Rules'
* section of the CloudFormation template with the given name.
* Any modifications performed on that object will be reflected in the resulting CDK template.
*
* If a Rule with the given name is not present in the template,
* an exception will be thrown.
*
* @param ruleName the name of the Rule in the CloudFormation template
*/
public getRule(ruleName: string): core.CfnRule {
const ret = this.rules[ruleName];
if (!ret) {
throw new core.ValidationError(`Rule with name '${ruleName}' was not found in the template`, this);
}
return ret;
}
/**
* Returns the CfnHook object from the 'Hooks'
* section of the included CloudFormation template with the given logical ID.
* Any modifications performed on the returned object will be reflected in the resulting CDK template.
*
* If a Hook with the given logical ID is not present in the template,
* an exception will be thrown.
*
* @param hookLogicalId the logical ID of the Hook in the included CloudFormation template's 'Hooks' section
*/
public getHook(hookLogicalId: string): core.CfnHook {
const ret = this.hooks[hookLogicalId];
if (!ret) {
throw new core.ValidationError(`Hook with logical ID '${hookLogicalId}' was not found in the template`, this);
}
return ret;
}
/**
* Returns a loaded NestedStack with name logicalId.
* For a nested stack to be returned by this method,
* it must be specified either in the `CfnIncludeProps.loadNestedStacks` property,
* or through the `loadNestedStack` method.
*
* @param logicalId the ID of the stack to retrieve, as it appears in the template
*/
public getNestedStack(logicalId: string): IncludedNestedStack {
if (!this.nestedStacks[logicalId]) {
if (!this.template.Resources[logicalId]) {
throw new core.ValidationError(`Nested Stack with logical ID '${logicalId}' was not found in the template`, this);
} else if (this.template.Resources[logicalId].Type !== 'AWS::CloudFormation::Stack') {
throw new core.ValidationError(`Resource with logical ID '${logicalId}' is not a CloudFormation Stack`, this);
} else {
throw new core.ValidationError(`Nested Stack '${logicalId}' was not included in the parent template. ` +
'To retrieve an included nested stack, it must be specified either in the `loadNestedStacks` property, or through the `loadNestedStack` method', this);
}
}
return this.nestedStacks[logicalId];
}
/**
* Includes a template for a child stack inside of this parent template.
* A child with this logical ID must exist in the template,
* and be of type AWS::CloudFormation::Stack.
* This is equivalent to specifying the value in the `CfnIncludeProps.loadNestedStacks`
* property on object construction.
*
* @param logicalId the ID of the stack to retrieve, as it appears in the template
* @param nestedStackProps the properties of the included child Stack
* @returns the same `IncludedNestedStack` object that `getNestedStack` returns for this logical ID
*/
public loadNestedStack(logicalId: string, nestedStackProps: CfnIncludeProps): IncludedNestedStack {
if (logicalId in this.nestedStacks) {
throw new core.ValidationError(`Nested Stack '${logicalId}' was already included in its parent template`, this);
}
const cfnStack = this.resources[logicalId];
if (!cfnStack) {
throw new core.ValidationError(`Nested Stack with logical ID '${logicalId}' was not found in the template`, this);
}
if (cfnStack instanceof core.CfnStack) {
// delete the old CfnStack child - one will be created by the NestedStack object
this.node.tryRemoveChild(logicalId);
// remove the previously created CfnStack resource from the resources map
delete this.resources[logicalId];
// createNestedStack() (called by getOrCreateResource()) expects this to be filled
this.nestedStacksToInclude[logicalId] = nestedStackProps;
this.getOrCreateResource(logicalId);
return this.nestedStacks[logicalId];
} else {
throw new core.ValidationError(`Nested Stack with logical ID '${logicalId}' is not an AWS::CloudFormation::Stack resource`, this);
}
}
/** @internal */
public _toCloudFormation(): object {
const ret: { [section: string]: any } = {};
for (const section of Object.keys(this.template)) {
const self = this;
const finder: cfn_parse.ICfnFinder = {
findResource(lId): core.CfnResource | undefined {
return self.resources[lId];
},
findRefTarget(elementName: string): core.CfnElement | undefined {
return self.resources[elementName] ?? self.parameters[elementName];
},
findCondition(conditionName: string): core.CfnCondition | undefined {
return self.conditions[conditionName];
},
findMapping(mappingName): core.CfnMapping | undefined {
return self.mappings[mappingName];
},
};
const cfnParser = new cfn_parse.CfnParser({
finder,
parameters: this.parametersToReplace,
});
switch (section) {
case 'Conditions':
case 'Mappings':
case 'Resources':
case 'Parameters':
case 'Rules':
case 'Hooks':
case 'Outputs':
// these are rendered as a side effect of instantiating the L1s
break;
default:
ret[section] = cfnParser.parseValue(this.template[section]);
}
}
return ret;
}
private createMapping(mappingName: string): void {
const self = this;
const cfnParser = new cfn_parse.CfnParser({
finder: {
findCondition() { throw new core.ValidationError('Referring to Conditions in Mapping definitions is not allowed', self); },
findMapping() { throw new core.ValidationError('Referring to other Mappings in Mapping definitions is not allowed', self); },
findRefTarget() { throw new core.ValidationError('Using Ref expressions in Mapping definitions is not allowed', self); },
findResource() { throw new core.ValidationError('Using GetAtt expressions in Mapping definitions is not allowed', self); },
},
parameters: {},
});
const cfnMapping = new core.CfnMapping(this.mappingsScope, mappingName, {
mapping: cfnParser.parseValue(this.template.Mappings[mappingName]),
});
this.mappings[mappingName] = cfnMapping;
this.overrideLogicalIdIfNeeded(cfnMapping, mappingName);
}
private createParameter(logicalId: string): void {
if (logicalId in this.parametersToReplace) {
return;
}
const self = this;
const expression = new cfn_parse.CfnParser({
finder: {
findResource() { throw new core.ValidationError('Using GetAtt expressions in Parameter definitions is not allowed', self); },
findRefTarget() { throw new core.ValidationError('Using Ref expressions in Parameter definitions is not allowed', self); },
findCondition() { throw new core.ValidationError('Referring to Conditions in Parameter definitions is not allowed', self); },
findMapping() { throw new core.ValidationError('Referring to Mappings in Parameter definitions is not allowed', self); },
},
parameters: {},
}).parseValue(this.template.Parameters[logicalId]);
const cfnParameter = new core.CfnParameter(this, logicalId, {
type: expression.Type,
default: expression.Default,
allowedPattern: expression.AllowedPattern,
allowedValues: expression.AllowedValues,
constraintDescription: expression.ConstraintDescription,
description: expression.Description,
maxLength: expression.MaxLength,
maxValue: expression.MaxValue,
minLength: expression.MinLength,
minValue: expression.MinValue,
noEcho: expression.NoEcho,
});
this.overrideLogicalIdIfNeeded(cfnParameter, logicalId);
this.parameters[logicalId] = cfnParameter;
}
private createRule(ruleName: string): void {
const self = this;
const cfnParser = new cfn_parse.CfnParser({
finder: {
findRefTarget(refTarget: string): core.CfnElement | undefined {
// only parameters can be referenced in Rules
return self.parameters[refTarget];
},
findResource() { throw new core.ValidationError('Using GetAtt expressions in Rule definitions is not allowed', self); },
findCondition(conditionName: string): core.CfnCondition | undefined {
return self.conditions[conditionName];
},
findMapping(mappingName: string): core.CfnMapping | undefined {
return self.mappings[mappingName];
},
},
parameters: this.parametersToReplace,
context: cfn_parse.CfnParsingContext.RULES,
});
const ruleProperties = cfnParser.parseValue(this.template.Rules[ruleName]);
const rule = new core.CfnRule(this.rulesScope, ruleName, {
ruleCondition: ruleProperties.RuleCondition,
assertions: ruleProperties.Assertions,
});
this.rules[ruleName] = rule;
this.overrideLogicalIdIfNeeded(rule, ruleName);
}
private createHook(hookName: string): void {
const self = this;
const cfnParser = new cfn_parse.CfnParser({
finder: {
findResource(lId): core.CfnResource | undefined {
return self.resources[lId];
},
findRefTarget(elementName: string): core.CfnElement | undefined {
return self.resources[elementName] ?? self.parameters[elementName];
},
findCondition(conditionName: string): core.CfnCondition | undefined {
return self.conditions[conditionName];
},
findMapping(mappingName): core.CfnMapping | undefined {
return self.mappings[mappingName];
},
},
parameters: this.parametersToReplace,
});
const hookAttributes = this.template.Hooks[hookName];
let hook: core.CfnHook;
switch (hookAttributes.Type) {
case 'AWS::CodeDeploy::BlueGreen':
hook = (core.CfnCodeDeployBlueGreenHook as any)._fromCloudFormation(this.hooksScope, hookName, hookAttributes, {
parser: cfnParser,
});
break;
default: {
const hookProperties = cfnParser.parseValue(hookAttributes.Properties) ?? {};
hook = new core.CfnHook(this.hooksScope, hookName, {
type: hookAttributes.Type,
properties: hookProperties,
});
}
}
this.hooks[hookName] = hook;
this.overrideLogicalIdIfNeeded(hook, hookName);
}
private createOutput(logicalId: string, scope: Construct): void {
const self = this;
const outputAttributes = new cfn_parse.CfnParser({
finder: {
findResource(lId): core.CfnResource | undefined {
return self.resources[lId];
},
findRefTarget(elementName: string): core.CfnElement | undefined {
return self.resources[elementName] ?? self.parameters[elementName];
},
findCondition(conditionName: string): core.CfnCondition | undefined {
return self.conditions[conditionName];
},
findMapping(mappingName): core.CfnMapping | undefined {
return self.mappings[mappingName];
},
},
parameters: this.parametersToReplace,
}).parseValue(this.template.Outputs[logicalId]);
const cfnOutput = new core.CfnOutput(scope, logicalId, {
value: outputAttributes.Value,
description: outputAttributes.Description,
exportName: outputAttributes.Export ? outputAttributes.Export.Name : undefined,
condition: (() => {
if (!outputAttributes.Condition) {
return undefined;
} else if (this.conditions[outputAttributes.Condition]) {
return self.getCondition(outputAttributes.Condition);
}
throw new core.ValidationError(`Output with name '${logicalId}' refers to a Condition with name ` +
`'${outputAttributes.Condition}' which was not found in this template`, this);
})(),
});
this.overrideLogicalIdIfNeeded(cfnOutput, logicalId);
this.outputs[logicalId] = cfnOutput;
}
private getOrCreateCondition(conditionName: string): core.CfnCondition {
if (conditionName in this.conditions) {
return this.conditions[conditionName];
}
const self = this;
const cfnParser = new cfn_parse.CfnParser({
finder: {
findResource() { throw new core.ValidationError('Using GetAtt in Condition definitions is not allowed', self); },
findRefTarget(elementName: string): core.CfnElement | undefined {
// only Parameters can be referenced in the 'Conditions' section
return self.parameters[elementName];
},
findCondition(cName: string): core.CfnCondition | undefined {
return cName in (self.template.Conditions || {})
? self.getOrCreateCondition(cName)
: undefined;
},
findMapping(mappingName: string): core.CfnMapping | undefined {
return self.mappings[mappingName];
},
},
context: cfn_parse.CfnParsingContext.CONDITIONS,
parameters: this.parametersToReplace,
});
const cfnCondition = new core.CfnCondition(this.conditionsScope, conditionName, {
expression: cfnParser.parseValue(this.template.Conditions[conditionName]),
});
this.overrideLogicalIdIfNeeded(cfnCondition, conditionName);
this.conditions[conditionName] = cfnCondition;
return cfnCondition;
}
private getPlaceholderID(): string {
return `Placeholder${this.logicalIdToPlaceholderMap.size}`;
}
private getOrCreateResource(logicalId: string, cycleChain: string[] = []): core.CfnResource {
cycleChain = cycleChain.concat([logicalId]);
if (cycleChain.length !== new Set(cycleChain).size) {
if (!this.allowCyclicalReferences) {
throw new core.ValidationError(`Found a cycle between resources in the template: ${cycleChain.join(' depends on ')}`, this);
}
// only allow one placeholder per logical id
if (this.logicalIdToPlaceholderMap.get(logicalId)) {
return this.resources[this.logicalIdToPlaceholderMap.get(logicalId)!];
}
let placeholderResourceAttributes: any = this.template.Resources[logicalId];
let placeholderId: string = this.getPlaceholderID();
this.logicalIdToPlaceholderMap.set(logicalId, placeholderId);
let placeholderInstance = new core.CfnResource(this, placeholderId, {
type: placeholderResourceAttributes.Type,
properties: {},
});
placeholderInstance.overrideLogicalId(placeholderId);
this.resources[placeholderId] = placeholderInstance;
return placeholderInstance;
}
const ret = this.resources[logicalId];
if (ret) {
return ret;
}
const self = this;
const finder: cfn_parse.ICfnFinder = {
findCondition(conditionName: string): core.CfnCondition | undefined {
return self.conditions[conditionName];
},
findMapping(mappingName): core.CfnMapping | undefined {
return self.mappings[mappingName];
},
findResource(lId: string): core.CfnResource | undefined {
if (!(lId in (self.template.Resources || {}))) {
return undefined;
}
return self.getOrCreateResource(lId, cycleChain);
},
findRefTarget(elementName: string): core.CfnElement | undefined {
if (elementName in self.parameters) {
return self.parameters[elementName];
}
return this.findResource(elementName);
},
};
const cfnParser = new cfn_parse.CfnParser({
finder,
parameters: this.parametersToReplace,
});
const resourceAttributes: any = this.template.Resources[logicalId];
let l1Instance: core.CfnResource;
if (this.nestedStacksToInclude[logicalId] && this.dehydratedResources.includes(logicalId)) {
throw new core.ValidationError(`nested stack '${logicalId}' was marked as dehydrated - nested stacks cannot be dehydrated`, this);
} else if (this.nestedStacksToInclude[logicalId]) {
l1Instance = this.createNestedStack(logicalId, cfnParser);
} else if (this.dehydratedResources.includes(logicalId)) {
l1Instance = new core.CfnResource(this, logicalId, {
type: resourceAttributes.Type,
properties: resourceAttributes.Properties,
});
const cfnOptions = l1Instance.cfnOptions;
cfnOptions.creationPolicy = resourceAttributes.CreationPolicy;
cfnOptions.updatePolicy = resourceAttributes.UpdatePolicy;
cfnOptions.deletionPolicy = resourceAttributes.DeletionPolicy;
cfnOptions.updateReplacePolicy = resourceAttributes.UpdateReplacePolicy;
cfnOptions.version = resourceAttributes.Version;
cfnOptions.description = resourceAttributes.Description;
cfnOptions.metadata = resourceAttributes.Metadata;
this.resources[logicalId] = l1Instance;
return l1Instance;
} else {
const l1ClassFqn = cfn_type_to_l1_mapping.lookup(resourceAttributes.Type);
// The AWS::CloudFormation::CustomResource type corresponds to the CfnCustomResource class.
// Unfortunately, it's quite useless; it only has a single property, ServiceToken.
// For that reason, even the CustomResource class from @core doesn't use it!
// So, special-case the handling of this one resource type
if (l1ClassFqn && resourceAttributes.Type !== 'AWS::CloudFormation::CustomResource') {
const options: cfn_parse.FromCloudFormationOptions = {
parser: cfnParser,
};
const [moduleName, ...className] = l1ClassFqn.split('.');
const module = require(moduleName); // eslint-disable-line @typescript-eslint/no-require-imports
const jsClassFromModule = module[className.join('.')];
l1Instance = jsClassFromModule._fromCloudFormation(this, logicalId, resourceAttributes, options);
} else {
l1Instance = new core.CfnResource(this, logicalId, {
type: resourceAttributes.Type,
properties: cfnParser.parseValue(resourceAttributes.Properties),
});
cfnParser.handleAttributes(l1Instance, resourceAttributes, logicalId);
}
}
/*
1. remove placeholder version of object created for cycle breaking
2. override logical id before deletion so references to the placeholder instead reference the original
*/
if (this.logicalIdToPlaceholderMap.get(logicalId)) {
let placeholderId: string = this.logicalIdToPlaceholderMap.get(logicalId)!;
this.resources[placeholderId].overrideLogicalId(logicalId);
this.node.tryRemoveChild(placeholderId);
delete this.resources[placeholderId];
}
this.overrideLogicalIdIfNeeded(l1Instance, logicalId);
this.resources[logicalId] = l1Instance;
// handle any unknown attributes using overrides
const knownAttributes = [
'Condition', 'DependsOn', 'Description', 'Metadata', 'Properties', 'Type', 'Version',
'CreationPolicy', 'DeletionPolicy', 'UpdatePolicy', 'UpdateReplacePolicy',
];
for (const [attrName, attrValue] of Object.entries(resourceAttributes)) {
if (!knownAttributes.includes(attrName)) {
l1Instance.addOverride(attrName, cfnParser.parseValue(attrValue));
}
}
return l1Instance;
}
private createNestedStack(nestedStackId: string, cfnParser: cfn_parse.CfnParser): core.CfnResource {
const templateResources = this.template.Resources || {};
const nestedStackAttributes = templateResources[nestedStackId] || {};
if (nestedStackAttributes.Type !== 'AWS::CloudFormation::Stack') {
throw new core.ValidationError(`Nested Stack with logical ID '${nestedStackId}' is not an AWS::CloudFormation::Stack resource`, this);
}
if (nestedStackAttributes.CreationPolicy) {
throw new core.ValidationError('CreationPolicy is not supported by the AWS::CloudFormation::Stack resource', this);
}
if (nestedStackAttributes.UpdatePolicy) {
throw new core.ValidationError('UpdatePolicy is not supported by the AWS::CloudFormation::Stack resource', this);
}
const nestedStackProps = cfnParser.parseValue(nestedStackAttributes.Properties);
const nestedStack = new core.NestedStack(this, nestedStackId, {
parameters: this.parametersForNestedStack(nestedStackProps.Parameters, nestedStackId),
notificationArns: cfn_parse.FromCloudFormation.getStringArray(nestedStackProps.NotificationARNs).value,
timeout: this.timeoutForNestedStack(nestedStackProps.TimeoutInMinutes),
});
const template = new CfnInclude(nestedStack, nestedStackId, this.nestedStacksToInclude[nestedStackId]);
this.nestedStacks[nestedStackId] = { stack: nestedStack, includedTemplate: template };
// we know this is never undefined for nested stacks
const nestedStackResource: core.CfnResource = nestedStack.nestedStackResource!;
cfnParser.handleAttributes(nestedStackResource, nestedStackAttributes, nestedStackId);
return nestedStackResource;
}
private parametersForNestedStack(parameters: any, nestedStackId: string): { [key: string]: string } | undefined {
if (parameters == null) {
return undefined;
}
const parametersToReplace = this.nestedStacksToInclude[nestedStackId].parameters ?? {};
const ret: { [key: string]: string } = {};
for (const paramName of Object.keys(parameters)) {
if (!(paramName in parametersToReplace)) {
ret[paramName] = cfn_parse.FromCloudFormation.getString(parameters[paramName]).value;
}
}
return ret;
}
private timeoutForNestedStack(value: any): core.Duration | undefined {
if (value == null) {
return undefined;
}
return core.Duration.minutes(cfn_parse.FromCloudFormation.getNumber(value).value);
}
private overrideLogicalIdIfNeeded(element: core.CfnElement, id: string): void {
if (this.preserveLogicalIds) {
element.overrideLogicalId(id);
}
}
}