From cb6de0adaec9e9942c7568939b33d7cb29cdeef2 Mon Sep 17 00:00:00 2001 From: comcalvi <66279577+comcalvi@users.noreply.github.com> Date: Thu, 13 Aug 2020 14:47:32 -0400 Subject: [PATCH] feat(cfn-include): allow passing Parameters to the included template (#9543) ---- Closes #4994 Cfn-Include can now be passed a mapping of parameters and their values. Specified parameters will have all references to them replaced with the passed value at build time, and their definitions will be removed from the template. Unspecified parameters and references to them will not be modified. *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/cloudformation-include/README.md | 14 ++ .../cloudformation-include/lib/cfn-include.ts | 67 +++++++-- .../bucket-with-parameters.json | 2 +- .../test-templates/fn-sub-parameters.json | 19 +++ .../fn-sub-shadow-parameter.json | 22 +++ .../test-templates/parameter-references.json | 54 ++++++++ .../test/valid-templates.test.ts | 130 +++++++++++++++++- packages/@aws-cdk/core/lib/cfn-parse.ts | 91 +++++++++--- packages/@aws-cdk/core/lib/from-cfn.ts | 45 ------ packages/@aws-cdk/core/lib/index.ts | 1 - tools/cfn2ts/lib/codegen.ts | 11 +- 11 files changed, 375 insertions(+), 81 deletions(-) create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/fn-sub-parameters.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/fn-sub-shadow-parameter.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/parameter-references.json delete mode 100644 packages/@aws-cdk/core/lib/from-cfn.ts diff --git a/packages/@aws-cdk/cloudformation-include/README.md b/packages/@aws-cdk/cloudformation-include/README.md index 0e51dc0402f16..180f7b7ea0ecc 100644 --- a/packages/@aws-cdk/cloudformation-include/README.md +++ b/packages/@aws-cdk/cloudformation-include/README.md @@ -131,6 +131,20 @@ and any changes you make to it will be reflected in the resulting template: param.default = 'MyDefault'; ``` +You can also provide values for them when including the template: + +```typescript +new inc.CfnInclude(stack, 'includeTemplate', { + templateFile: 'path/to/my/template' + parameters: { + 'MyParam': 'my-value', + }, +}); +``` + +This will replace all references to `MyParam` with the string 'my-value', +and `MyParam` will be removed from the Parameters section of the template. + ## Conditions If your template uses [CloudFormation Conditions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html), diff --git a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts index 157a0ebaf6ccf..584a5f97b420b 100644 --- a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts +++ b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts @@ -23,8 +23,20 @@ export interface CfnIncludeProps { * 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 nestedStacks?: { [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. + * + * @default - no parameters will be replaced + */ + readonly parameters?: { [parameterName: string]: any }; } /** @@ -55,6 +67,7 @@ export class CfnInclude extends core.CfnElement { private readonly conditionsScope: core.Construct; private readonly resources: { [logicalId: string]: core.CfnResource } = {}; private readonly parameters: { [logicalId: string]: core.CfnParameter } = {}; + private readonly parametersToReplace: { [parameterName: string]: any }; private readonly outputs: { [logicalId: string]: core.CfnOutput } = {}; private readonly nestedStacks: { [logicalId: string]: IncludedNestedStack } = {}; private readonly nestedStacksToInclude: { [name: string]: CfnIncludeProps }; @@ -64,12 +77,21 @@ export class CfnInclude extends core.CfnElement { constructor(scope: core.Construct, id: string, props: CfnIncludeProps) { super(scope, id); + this.parametersToReplace = props.parameters || {}; + // read the template into a JS object this.template = futils.readYamlSync(props.templateFile); // ToDo implement preserveLogicalIds=false this.preserveLogicalIds = true; + // 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 Error(`Parameter with logical ID '${logicalId}' was not found in the template`); + } + } + // instantiate all parameters for (const logicalId of Object.keys(this.template.Parameters || {})) { this.createParameter(logicalId); @@ -203,10 +225,32 @@ export class CfnInclude extends core.CfnElement { const ret: { [section: string]: any } = {}; for (const section of Object.keys(this.template)) { - // render all sections of the template unchanged, - // except Conditions, Resources, Parameters, and Outputs which will be taken care of by the created L1s - if (section !== 'Conditions' && section !== 'Resources' && section !== 'Parameters' && section !== 'Outputs') { - ret[section] = this.template[section]; + 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]; + }, + }; + const cfnParser = new cfn_parse.CfnParser({ + finder, + parameters: this.parametersToReplace, + }); + + switch (section) { + case 'Conditions': + case 'Resources': + case 'Parameters': + case 'Outputs': + // these are rendered as a side effect of instantiating the L1s + break; + default: + ret[section] = cfnParser.parseValue(this.template[section]); } } @@ -214,6 +258,10 @@ export class CfnInclude extends core.CfnElement { } private createParameter(logicalId: string): void { + if (logicalId in this.parametersToReplace) { + return; + } + const expression = new cfn_parse.CfnParser({ finder: { findResource() { throw new Error('Using GetAtt expressions in Parameter definitions is not allowed'); }, @@ -253,6 +301,7 @@ export class CfnInclude extends core.CfnElement { return undefined; }, }, + parameters: this.parametersToReplace, }).parseValue(this.template.Outputs[logicalId]); const cfnOutput = new core.CfnOutput(scope, logicalId, { value: outputAttributes.Value, @@ -294,6 +343,7 @@ export class CfnInclude extends core.CfnElement { }, }, context: cfn_parse.CfnParsingContext.CONDITIONS, + parameters: this.parametersToReplace, }); const cfnCondition = new core.CfnCondition(this.conditionsScope, conditionName, { expression: cfnParser.parseValue(this.template.Conditions[conditionName]), @@ -326,7 +376,7 @@ export class CfnInclude extends core.CfnElement { } const self = this; - const finder: core.ICfnFinder = { + const finder: cfn_parse.ICfnFinder = { findCondition(conditionName: string): core.CfnCondition | undefined { return self.conditions[conditionName]; }, @@ -348,6 +398,7 @@ export class CfnInclude extends core.CfnElement { }; const cfnParser = new cfn_parse.CfnParser({ finder, + parameters: this.parametersToReplace, }); let l1Instance: core.CfnResource; @@ -356,13 +407,13 @@ export class CfnInclude extends core.CfnElement { } else { const l1ClassFqn = cfn_type_to_l1_mapping.lookup(resourceAttributes.Type); if (l1ClassFqn) { - const options: core.FromCloudFormationOptions = { - finder, + 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); + l1Instance = jsClassFromModule._fromCloudFormation(this, logicalId, resourceAttributes, options); } else { l1Instance = new core.CfnResource(this, logicalId, { type: resourceAttributes.Type, diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/bucket-with-parameters.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/bucket-with-parameters.json index 0cf4552cb2951..5268e6495b074 100644 --- a/packages/@aws-cdk/cloudformation-include/test/test-templates/bucket-with-parameters.json +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/bucket-with-parameters.json @@ -45,4 +45,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/fn-sub-parameters.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/fn-sub-parameters.json new file mode 100644 index 0000000000000..d1bfe825b2514 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/fn-sub-parameters.json @@ -0,0 +1,19 @@ +{ + "Parameters": { + "MyParam": { + "Type": "String" + } + }, + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Fn::Sub": [ + "${MyParam}" + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/fn-sub-shadow-parameter.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/fn-sub-shadow-parameter.json new file mode 100644 index 0000000000000..1e47c582ba875 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/fn-sub-shadow-parameter.json @@ -0,0 +1,22 @@ +{ + "Parameters": { + "MyParam": { + "Type": "String" + } + }, + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Fn::Sub": [ + "${MyParam}", + { + "MyParam": { "Ref" : "MyParam" } + } + ] + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/parameter-references.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/parameter-references.json new file mode 100644 index 0000000000000..2da6e28215241 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/parameter-references.json @@ -0,0 +1,54 @@ +{ + "Transform" : { + "Name" : "AWS::Include", + "Parameters" : { + "Location" : { + "Ref": "MyParam" + } + } + }, + "Parameters": { + "MyParam": { + "Type": "String", + "Default": "MyValue" + } + }, + "Conditions": { + "AlwaysFalse": { + "Fn::Equals": [ { "Ref": "MyParam" }, "Invalid?BucketName"] + } + }, + "Metadata": { + "Field": { + "Fn::If": [ + "AlwaysFalse", + "AWS::NoValue", + { + "Ref": "MyParam" + } + ] + } + }, + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Metadata": { + "Field": { + "Ref": "MyParam" + } + }, + "Properties": { + "BucketName": { + "Ref": "MyParam" + } + } + } + }, + "Outputs": { + "MyOutput": { + "Value": { + "Ref": "MyParam" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts index 845aa001ba3d2..fec9ebf7f368c 100644 --- a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts @@ -657,16 +657,144 @@ describe('CDK Include', () => { cfnTemplate.getOutput('FakeOutput'); }).toThrow(/Output with logical ID 'FakeOutput' was not found in the template/); }); + + test('replaces references to parameters with the user-specified values in Resources, Conditions, Metadata, and Options', () => { + includeTestTemplate(stack, 'parameter-references.json', { + parameters: { + 'MyParam': 'my-s3-bucket', + }, + }); + + expect(stack).toMatchTemplate({ + "Transform": { + "Name": "AWS::Include", + "Parameters": { + "Location": "my-s3-bucket", + }, + }, + "Metadata": { + "Field": { + "Fn::If": [ + "AlwaysFalse", + "AWS::NoValue", + "my-s3-bucket", + ], + }, + }, + "Conditions": { + "AlwaysFalse": { + "Fn::Equals": [ "my-s3-bucket", "Invalid?BucketName"], + }, + }, + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Metadata": { + "Field": "my-s3-bucket", + }, + "Properties": { + "BucketName": "my-s3-bucket", + }, + }, + }, + "Outputs": { + "MyOutput": { + "Value": "my-s3-bucket", + }, + }, + }); + }); + + test('can replace parameters in Fn::Sub', () => { + includeTestTemplate(stack, 'fn-sub-parameters.json', { + parameters: { + 'MyParam': 'my-s3-bucket', + }, + }); + + expect(stack).toMatchTemplate({ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Fn::Sub": "my-s3-bucket", + }, + }, + }, + }, + }); + }); + + test('does not modify Fn::Sub variables shadowing a replaced parameter', () => { + includeTestTemplate(stack, 'fn-sub-shadow-parameter.json', { + parameters: { + 'MyParam': 'MyValue', + }, + }); + + expect(stack).toMatchTemplate({ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Fn::Sub": [ + "${MyParam}", + { + "MyParam": "MyValue", + }, + ], + }, + }, + }, + }, + }); + }); + + test('throws an exception when parameters are passed a resource name', () => { + expect(() => { + includeTestTemplate(stack, 'bucket-with-parameters.json', { + parameters: { + 'Bucket': 'noChange', + }, + }); + }).toThrow(/Parameter with logical ID 'Bucket' was not found in the template/); + }); + + test('throws an exception when provided a parameter to replace that is not in the template with parameters', () => { + expect(() => { + includeTestTemplate(stack, 'bucket-with-parameters.json', { + parameters: { + 'FakeParameter': 'DoesNotExist', + }, + }); + }).toThrow(/Parameter with logical ID 'FakeParameter' was not found in the template/); + }); + + test('throws an exception when provided a parameter to replace in a template with no parameters', () => { + expect(() => { + includeTestTemplate(stack, 'only-empty-bucket.json', { + parameters: { + 'FakeParameter': 'DoesNotExist', + }, + }); + }).toThrow(/Parameter with logical ID 'FakeParameter' was not found in the template/); + }); }); interface IncludeTestTemplateProps { /** @default true */ readonly preserveLogicalIds?: boolean; + + /** @default {} */ + readonly parameters?: { [parameterName: string]: any } } -function includeTestTemplate(scope: core.Construct, testTemplate: string, _props: IncludeTestTemplateProps = {}): inc.CfnInclude { +function includeTestTemplate(scope: core.Construct, testTemplate: string, props: IncludeTestTemplateProps = {}): inc.CfnInclude { return new inc.CfnInclude(scope, 'MyScope', { templateFile: _testTemplateFilePath(testTemplate), + parameters: props.parameters, // preserveLogicalIds: props.preserveLogicalIds, }); } diff --git a/packages/@aws-cdk/core/lib/cfn-parse.ts b/packages/@aws-cdk/core/lib/cfn-parse.ts index 5872399e0efee..13662e4633244 100644 --- a/packages/@aws-cdk/core/lib/cfn-parse.ts +++ b/packages/@aws-cdk/core/lib/cfn-parse.ts @@ -1,3 +1,5 @@ +import { CfnCondition } from './cfn-condition'; +import { CfnElement } from './cfn-element'; import { Fn } from './cfn-fn'; import { Aws } from './cfn-pseudo'; import { CfnResource } from './cfn-resource'; @@ -6,7 +8,6 @@ import { CfnCreationPolicy, CfnDeletionPolicy, CfnResourceAutoScalingCreationPolicy, CfnResourceSignal, CfnUpdatePolicy, } from './cfn-resource-policy'; import { CfnTag } from './cfn-tag'; -import { ICfnFinder } from './from-cfn'; import { Lazy } from './lazy'; import { CfnReference } from './private/cfn-reference'; import { IResolvable } from './resolvable'; @@ -142,6 +143,44 @@ export class FromCloudFormation { } } +/** + * An interface that represents callbacks into a CloudFormation template. + * Used by the fromCloudFormation methods in the generated L1 classes. + */ +export interface ICfnFinder { + /** + * Return the Condition with the given name from the template. + * If there is no Condition with that name in the template, + * returns undefined. + */ + findCondition(conditionName: string): CfnCondition | undefined; + + /** + * Returns the element referenced using a Ref expression with the given name. + * If there is no element with this name in the template, + * return undefined. + */ + findRefTarget(elementName: string): CfnElement | undefined; + + /** + * Returns the resource with the given logical ID in the template. + * If a resource with that logical ID was not found in the template, + * returns undefined. + */ + findResource(logicalId: string): CfnResource | undefined; +} + +/** + * The interface used as the last argument to the fromCloudFormation + * static method of the generated L1 classes. + */ +export interface FromCloudFormationOptions { + /** + * The parser used to convert CloudFormation to values the CDK understands. + */ + readonly parser: CfnParser; +} + /** * The context in which the parsing is taking place. * @@ -171,6 +210,12 @@ export interface ParseCfnOptions { * @default - the default context (no special behavior) */ readonly context?: CfnParsingContext; + + /** + * Values provided here will replace references to parameters in the parsed template. + * @default - no parameters will be replaced + */ + readonly parameters?: { [parameterName: string]: any } } /** @@ -357,7 +402,7 @@ export class CfnParser { return undefined; case 'Ref': { const refTarget = object[key]; - const specialRef = specialCaseRefs(refTarget); + const specialRef = this.specialCaseRefs(refTarget); if (specialRef) { return specialRef; } else { @@ -509,7 +554,7 @@ export class CfnParser { } // since it's not in the map, check if it's a pseudo parameter - const specialRef = specialCaseSubRefs(refTarget); + const specialRef = this.specialCaseSubRefs(refTarget); if (specialRef) { return leftHalf + specialRef + this.parseFnSubString(rightHalf, map); } @@ -532,24 +577,34 @@ export class CfnParser { return leftHalf + CfnReference.for(refResource, attribute, true).toString() + this.parseFnSubString(rightHalf, map); } } -} -function specialCaseRefs(value: any): any { - switch (value) { - case 'AWS::AccountId': return Aws.ACCOUNT_ID; - case 'AWS::Region': return Aws.REGION; - case 'AWS::Partition': return Aws.PARTITION; - case 'AWS::URLSuffix': return Aws.URL_SUFFIX; - case 'AWS::NotificationARNs': return Aws.NOTIFICATION_ARNS; - case 'AWS::StackId': return Aws.STACK_ID; - case 'AWS::StackName': return Aws.STACK_NAME; - case 'AWS::NoValue': return Aws.NO_VALUE; - default: return undefined; + private specialCaseRefs(value: any): any { + if (value in this.parameters) { + return this.parameters[value]; + } + switch (value) { + case 'AWS::AccountId': return Aws.ACCOUNT_ID; + case 'AWS::Region': return Aws.REGION; + case 'AWS::Partition': return Aws.PARTITION; + case 'AWS::URLSuffix': return Aws.URL_SUFFIX; + case 'AWS::NotificationARNs': return Aws.NOTIFICATION_ARNS; + case 'AWS::StackId': return Aws.STACK_ID; + case 'AWS::StackName': return Aws.STACK_NAME; + case 'AWS::NoValue': return Aws.NO_VALUE; + default: return undefined; + } } -} -function specialCaseSubRefs(value: string): string | undefined { - return value.indexOf('::') === -1 ? undefined: '${' + value + '}'; + private specialCaseSubRefs(value: string): string | undefined { + if (value in this.parameters) { + return this.parameters[value]; + } + return value.indexOf('::') === -1 ? undefined: '${' + value + '}'; + } + + private get parameters(): { [parameterName: string]: any } { + return this.options.parameters || {}; + } } function undefinedIfAllValuesAreEmpty(object: object): object | undefined { diff --git a/packages/@aws-cdk/core/lib/from-cfn.ts b/packages/@aws-cdk/core/lib/from-cfn.ts deleted file mode 100644 index 771af7775a74b..0000000000000 --- a/packages/@aws-cdk/core/lib/from-cfn.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { CfnCondition } from './cfn-condition'; -import { CfnElement } from './cfn-element'; -import { CfnResource } from './cfn-resource'; - -/** - * An interface that represents callbacks into a CloudFormation template. - * Used by the fromCloudFormation methods in the generated L1 classes. - * - * @experimental - */ -export interface ICfnFinder { - /** - * Return the Condition with the given name from the template. - * If there is no Condition with that name in the template, - * returns undefined. - */ - findCondition(conditionName: string): CfnCondition | undefined; - - /** - * Returns the element referenced using a Ref expression with the given name. - * If there is no element with this name in the template, - * return undefined. - */ - findRefTarget(elementName: string): CfnElement | undefined; - - /** - * Returns the resource with the given logical ID in the template. - * If a resource with that logical ID was not found in the template, - * returns undefined. - */ - findResource(logicalId: string): CfnResource | undefined; -} - -/** - * The interface used as the last argument to the fromCloudFormation - * static method of the generated L1 classes. - * - * @experimental - */ -export interface FromCloudFormationOptions { - /** - * The finder interface used to resolve references across the template. - */ - readonly finder: ICfnFinder; -} diff --git a/packages/@aws-cdk/core/lib/index.ts b/packages/@aws-cdk/core/lib/index.ts index 6c54a222901d6..92a6ae2f825ed 100644 --- a/packages/@aws-cdk/core/lib/index.ts +++ b/packages/@aws-cdk/core/lib/index.ts @@ -30,7 +30,6 @@ export * from './cfn-json'; export * from './removal-policy'; export * from './arn'; export * from './duration'; -export * from './from-cfn'; export * from './size'; export * from './stack-trace'; diff --git a/tools/cfn2ts/lib/codegen.ts b/tools/cfn2ts/lib/codegen.ts index 6624053395dc2..264f4c87c26b2 100644 --- a/tools/cfn2ts/lib/codegen.ts +++ b/tools/cfn2ts/lib/codegen.ts @@ -230,18 +230,15 @@ export default class CodeGenerator { this.code.line(' * containing the CloudFormation properties of this resource.'); this.code.line(' * Used in the @aws-cdk/cloudformation-include module.'); this.code.line(' *'); - this.code.line(' * @experimental'); + this.code.line(' * @internal'); this.code.line(' */'); // eslint-disable-next-line max-len - this.code.openBlock(`public static fromCloudFormation(scope: ${CONSTRUCT_CLASS}, id: string, resourceAttributes: any, options: ${CORE}.FromCloudFormationOptions): ` + + this.code.openBlock(`public static _fromCloudFormation(scope: ${CONSTRUCT_CLASS}, id: string, resourceAttributes: any, options: ${CFN_PARSE}.FromCloudFormationOptions): ` + `${resourceName.className}`); this.code.line('resourceAttributes = resourceAttributes || {};'); - this.code.indent('const cfnParser = new cfn_parse.CfnParser({'); - this.code.line('finder: options.finder,'); - this.code.unindent('});'); if (propsType) { // translate the template properties to CDK objects - this.code.line('const resourceProperties = cfnParser.parseValue(resourceAttributes.Properties);'); + this.code.line('const resourceProperties = options.parser.parseValue(resourceAttributes.Properties);'); // translate to props, using a (module-private) factory function this.code.line(`const props = ${genspec.fromCfnFactoryName(propsType).fqn}(resourceProperties);`); // finally, instantiate the resource class @@ -252,7 +249,7 @@ export default class CodeGenerator { } // handle all non-property attributes // (retention policies, conditions, metadata, etc.) - this.code.line('cfnParser.handleAttributes(ret, resourceAttributes, id);'); + this.code.line('options.parser.handleAttributes(ret, resourceAttributes, id);'); this.code.line('return ret;'); this.code.closeBlock();