diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/CfnMappingFindInMapTestDefaultTestDeployAssertEA32B10D.assets.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/CfnMappingFindInMapTestDefaultTestDeployAssertEA32B10D.assets.json new file mode 100644 index 0000000000000..f51ac22b6fa4a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/CfnMappingFindInMapTestDefaultTestDeployAssertEA32B10D.assets.json @@ -0,0 +1,19 @@ +{ + "version": "32.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "CfnMappingFindInMapTestDefaultTestDeployAssertEA32B10D.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/CfnMappingFindInMapTestDefaultTestDeployAssertEA32B10D.template.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/CfnMappingFindInMapTestDefaultTestDeployAssertEA32B10D.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/CfnMappingFindInMapTestDefaultTestDeployAssertEA32B10D.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/cdk.out new file mode 100644 index 0000000000000..f0b901e7c06e5 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"32.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/core-cfn-mapping-1.assets.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/core-cfn-mapping-1.assets.json new file mode 100644 index 0000000000000..29d191b6fdd3a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/core-cfn-mapping-1.assets.json @@ -0,0 +1,19 @@ +{ + "version": "32.0.0", + "files": { + "175902e9c94a814a5739a092426ef29b46a5098db6118c5c464b507fa4e867e5": { + "source": { + "path": "core-cfn-mapping-1.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "175902e9c94a814a5739a092426ef29b46a5098db6118c5c464b507fa4e867e5.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/core-cfn-mapping-1.template.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/core-cfn-mapping-1.template.json new file mode 100644 index 0000000000000..c1c1b75dab061 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/core-cfn-mapping-1.template.json @@ -0,0 +1,120 @@ +{ + "Transform": [ + "AWS::LanguageExtensions" + ], + "Mappings": { + "Regularmapping": { + "TopLevelKey1": { + "SecondLevelKey1": "Yes", + "SecondLevelKey2": "No" + } + }, + "Lazymapping": { + "TopLevelKey1": { + "SecondLevelKey1": "Yes", + "SecondLevelKey2": "No" + } + } + }, + "Outputs": { + "Output0": { + "Value": { + "Fn::FindInMap": [ + "Regularmapping", + "TopLevelKey1", + "SecondLevelKey1", + { + "DefaultValue": "foob" + } + ] + } + }, + "Output1": { + "Value": { + "Fn::FindInMap": [ + "Regularmapping", + "TopLevelKey1", + { + "Ref": "AWS::Region" + }, + { + "DefaultValue": "foob" + } + ] + } + }, + "Output2": { + "Value": { + "Fn::FindInMap": [ + "Regularmapping", + "TopLevelKey1", + "SecondLevelKey3", + { + "DefaultValue": "foob" + } + ] + } + }, + "Output3": { + "Value": "No" + }, + "Output4": { + "Value": "bart" + }, + "Output5": { + "Value": { + "Fn::FindInMap": [ + "Lazymapping", + { + "Ref": "AWS::Region" + }, + "SecondLevelKey2", + { + "DefaultValue": "bart" + } + ] + } + } + }, + "Resources": { + "CfnMappingFindInMapBucket6F72FEE7": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/integ.json new file mode 100644 index 0000000000000..4e00e573e58c3 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "32.0.0", + "testCases": { + "CfnMappingFindInMapTest/DefaultTest": { + "stacks": [ + "core-cfn-mapping-1" + ], + "assertionStack": "CfnMappingFindInMapTest/DefaultTest/DeployAssert", + "assertionStackName": "CfnMappingFindInMapTestDefaultTestDeployAssertEA32B10D" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/manifest.json new file mode 100644 index 0000000000000..dc377e2dbb8af --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/manifest.json @@ -0,0 +1,159 @@ +{ + "version": "32.0.0", + "artifacts": { + "core-cfn-mapping-1.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "core-cfn-mapping-1.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "core-cfn-mapping-1": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "core-cfn-mapping-1.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/175902e9c94a814a5739a092426ef29b46a5098db6118c5c464b507fa4e867e5.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "core-cfn-mapping-1.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "core-cfn-mapping-1.assets" + ], + "metadata": { + "/core-cfn-mapping-1/Regular mapping": [ + { + "type": "aws:cdk:logicalId", + "data": "Regularmapping" + } + ], + "/core-cfn-mapping-1/Lazy mapping": [ + { + "type": "aws:cdk:logicalId", + "data": "Lazymapping" + } + ], + "/core-cfn-mapping-1/Output0": [ + { + "type": "aws:cdk:logicalId", + "data": "Output0" + } + ], + "/core-cfn-mapping-1/Output1": [ + { + "type": "aws:cdk:logicalId", + "data": "Output1" + } + ], + "/core-cfn-mapping-1/Output2": [ + { + "type": "aws:cdk:logicalId", + "data": "Output2" + } + ], + "/core-cfn-mapping-1/Output3": [ + { + "type": "aws:cdk:logicalId", + "data": "Output3" + } + ], + "/core-cfn-mapping-1/Output4": [ + { + "type": "aws:cdk:logicalId", + "data": "Output4" + } + ], + "/core-cfn-mapping-1/Output5": [ + { + "type": "aws:cdk:logicalId", + "data": "Output5" + } + ], + "/core-cfn-mapping-1/CfnMappingFindInMapBucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CfnMappingFindInMapBucket6F72FEE7" + } + ], + "/core-cfn-mapping-1/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/core-cfn-mapping-1/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "core-cfn-mapping-1" + }, + "CfnMappingFindInMapTestDefaultTestDeployAssertEA32B10D.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "CfnMappingFindInMapTestDefaultTestDeployAssertEA32B10D.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "CfnMappingFindInMapTestDefaultTestDeployAssertEA32B10D": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "CfnMappingFindInMapTestDefaultTestDeployAssertEA32B10D.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "CfnMappingFindInMapTestDefaultTestDeployAssertEA32B10D.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "CfnMappingFindInMapTestDefaultTestDeployAssertEA32B10D.assets" + ], + "metadata": { + "/CfnMappingFindInMapTest/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/CfnMappingFindInMapTest/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "CfnMappingFindInMapTest/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/tree.json new file mode 100644 index 0000000000000..551e1e60b027f --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.js.snapshot/tree.json @@ -0,0 +1,187 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "core-cfn-mapping-1": { + "id": "core-cfn-mapping-1", + "path": "core-cfn-mapping-1", + "children": { + "Regular mapping": { + "id": "Regular mapping", + "path": "core-cfn-mapping-1/Regular mapping", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + }, + "Lazy mapping": { + "id": "Lazy mapping", + "path": "core-cfn-mapping-1/Lazy mapping", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + }, + "Output0": { + "id": "Output0", + "path": "core-cfn-mapping-1/Output0", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + }, + "Output1": { + "id": "Output1", + "path": "core-cfn-mapping-1/Output1", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + }, + "Output2": { + "id": "Output2", + "path": "core-cfn-mapping-1/Output2", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + }, + "Output3": { + "id": "Output3", + "path": "core-cfn-mapping-1/Output3", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + }, + "Output4": { + "id": "Output4", + "path": "core-cfn-mapping-1/Output4", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + }, + "Output5": { + "id": "Output5", + "path": "core-cfn-mapping-1/Output5", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + }, + "CfnMappingFindInMapBucket": { + "id": "CfnMappingFindInMapBucket", + "path": "core-cfn-mapping-1/CfnMappingFindInMapBucket", + "children": { + "Resource": { + "id": "Resource", + "path": "core-cfn-mapping-1/CfnMappingFindInMapBucket/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::Bucket", + "aws:cdk:cloudformation:props": {} + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "core-cfn-mapping-1/BootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "core-cfn-mapping-1/CheckBootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + }, + "CfnMappingFindInMapTest": { + "id": "CfnMappingFindInMapTest", + "path": "CfnMappingFindInMapTest", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "CfnMappingFindInMapTest/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "CfnMappingFindInMapTest/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "CfnMappingFindInMapTest/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "CfnMappingFindInMapTest/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "CfnMappingFindInMapTest/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.55" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.ts b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.ts new file mode 100644 index 0000000000000..bdd4f7763043e --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.cfn-mappings-find-in-map.ts @@ -0,0 +1,49 @@ +import * as cdk from 'aws-cdk-lib/core'; +import { Bucket } from 'aws-cdk-lib/aws-s3'; +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'core-cfn-mapping-1'/*,{ env }*/); + +const backing = { + TopLevelKey1: { + SecondLevelKey1: 'Yes', + SecondLevelKey2: 'No', + }, +}; + +const mapping = new cdk.CfnMapping(stack, 'Regular mapping', { + mapping: backing, +}); + +const lazyMapping = new cdk.CfnMapping(stack, 'Lazy mapping', { + mapping: backing, + lazy: true, +}); + +const defValue = 'foob'; +const defValue2 = 'bart'; + +const mapYes = mapping.findInMap('TopLevelKey1', 'SecondLevelKey1', defValue); // resolve to 'Yes' +const mapDefault = mapping.findInMap('TopLevelKey1', cdk.Aws.REGION, defValue); // resolve to 'foob' +const mapFn = cdk.Fn.findInMap(mapping.logicalId, 'TopLevelKey1', 'SecondLevelKey3', defValue); // resolve to 'foob' + +const lazyNo = lazyMapping.findInMap('TopLevelKey1', 'SecondLevelKey2', defValue2); // short circuit to 'No' +const lazyDefault = lazyMapping.findInMap('TopLevelKey2', 'SecondLevelKey2', defValue2); // short circuit to 'bart' +const lazyResolve = lazyMapping.findInMap(cdk.Aws.REGION, 'SecondLevelKey2', defValue2); // resolve to 'bart' + +new cdk.CfnOutput(stack, 'Output0', { value: mapYes }); +new cdk.CfnOutput(stack, 'Output1', { value: mapDefault }); +new cdk.CfnOutput(stack, 'Output2', { value: mapFn }); +new cdk.CfnOutput(stack, 'Output3', { value: lazyNo }); +new cdk.CfnOutput(stack, 'Output4', { value: lazyDefault }); +new cdk.CfnOutput(stack, 'Output5', { value: lazyResolve }); + +new Bucket(stack, 'CfnMappingFindInMapBucket'); + +new IntegTest(app, 'CfnMappingFindInMapTest', { + testCases: [stack], +}); + +app.synth(); \ No newline at end of file diff --git a/packages/aws-cdk-lib/core/README.md b/packages/aws-cdk-lib/core/README.md index d69e3cf00d637..707d70d56e13d 100644 --- a/packages/aws-cdk-lib/core/README.md +++ b/packages/aws-cdk-lib/core/README.md @@ -1058,6 +1058,31 @@ declare const regionTable: CfnMapping; regionTable.findInMap(Aws.REGION, 'regionName'); ``` +An optional default value can also be passed to `findInMap`. If either key is not found in the map and the mapping is lazy, `findInMap` will return the default value and not render the mapping. +If the mapping is not lazy or either key is an unresolved token, the call to `findInMap` will return a token that resolves to +`{ "Fn::FindInMap": [ "MapName", "TopLevelKey", "SecondLevelKey", { "DefaultValue": "DefaultValue" } ] }`, and the mapping will be rendered. +Note that the `AWS::LanguageExtentions` transform is added to enable the default value functionality. + +For example, the following code will again not produce anything in the "Mappings" section. The +call to `findInMap` will be able to resolve the value during synthesis and simply return +`'Region not found'`. + +```ts +const regionTable = new CfnMapping(this, 'RegionTable', { + mapping: { + 'us-east-1': { + regionName: 'US East (N. Virginia)', + }, + 'us-east-2': { + regionName: 'US East (Ohio)', + }, + }, + lazy: true, +}); + +regionTable.findInMap('us-west-1', 'regionName', 'Region not found'); +``` + [cfn-mappings]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html ### Dynamic References diff --git a/packages/aws-cdk-lib/core/lib/cfn-fn.ts b/packages/aws-cdk-lib/core/lib/cfn-fn.ts index 755181dd952fe..06344e90b5bd6 100644 --- a/packages/aws-cdk-lib/core/lib/cfn-fn.ts +++ b/packages/aws-cdk-lib/core/lib/cfn-fn.ts @@ -233,10 +233,12 @@ export class Fn { /** * The intrinsic function ``Fn::FindInMap`` returns the value corresponding to * keys in a two-level map that is declared in the Mappings section. + * Warning: do not use with lazy mappings as this function will not guarentee a lazy mapping to render in the template. + * Prefer to use `CfnMapping.findInMap` in general. * @returns a token represented as a string */ - public static findInMap(mapName: string, topLevelKey: string, secondLevelKey: string): string { - return Fn._findInMap(mapName, topLevelKey, secondLevelKey).toString(); + public static findInMap(mapName: string, topLevelKey: string, secondLevelKey: string, defaultValue?: string): string { + return Fn._findInMap(mapName, topLevelKey, secondLevelKey, defaultValue).toString(); } /** @@ -245,8 +247,8 @@ export class Fn { * * @internal */ - public static _findInMap(mapName: string, topLevelKey: string, secondLevelKey: string): IResolvable { - return new FnFindInMap(mapName, topLevelKey, secondLevelKey); + public static _findInMap(mapName: string, topLevelKey: string, secondLevelKey: string, defaultValue?: string): IResolvable { + return new FnFindInMap(mapName, topLevelKey, secondLevelKey, defaultValue); } /** @@ -500,9 +502,27 @@ class FnFindInMap extends FnBase { * @param mapName The logical name of a mapping declared in the Mappings section that contains the keys and values. * @param topLevelKey The top-level key name. Its value is a list of key-value pairs. * @param secondLevelKey The second-level key name, which is set to one of the keys from the list assigned to TopLevelKey. + * @param defaultValue The value of the default value returned if either the key is not found in the map */ - constructor(mapName: string, topLevelKey: any, secondLevelKey: any) { - super('Fn::FindInMap', [mapName, topLevelKey, secondLevelKey]); + + private readonly mapName: string; + private readonly topLevelKey: string; + private readonly secondLevelKey: string; + private readonly defaultValue?: string; + + constructor(mapName: string, topLevelKey: any, secondLevelKey: any, defaultValue?: string) { + super('Fn::FindInMap', [mapName, topLevelKey, secondLevelKey, defaultValue !== undefined ? { DefaultValue: defaultValue } : undefined]); + this.mapName = mapName; + this.topLevelKey = topLevelKey; + this.secondLevelKey = secondLevelKey; + this.defaultValue = defaultValue; + } + + public resolve(context: IResolveContext): any { + if (this.defaultValue !== undefined) { + Stack.of(context.scope).addTransform('AWS::LanguageExtensions'); + } + return { 'Fn::FindInMap': [this.mapName, this.topLevelKey, this.secondLevelKey, this.defaultValue !== undefined ? { DefaultValue: this.defaultValue } : undefined] }; } } diff --git a/packages/aws-cdk-lib/core/lib/cfn-mapping.ts b/packages/aws-cdk-lib/core/lib/cfn-mapping.ts index 15be7e1054185..d64b9272f1db3 100644 --- a/packages/aws-cdk-lib/core/lib/cfn-mapping.ts +++ b/packages/aws-cdk-lib/core/lib/cfn-mapping.ts @@ -40,8 +40,8 @@ export interface CfnMappingProps { export class CfnMapping extends CfnRefElement { private mapping: Mapping; private readonly lazy?: boolean; - private lazyRender = false; - private lazyInformed = false; + private lazyRender = false; // prescribes `_toCloudFormation()` to pass nothing if value from map is returned lazily. + private lazyInformed = false; // keeps track if user has been sent a message informing them of the possibility to use lazy synthesis. constructor(scope: Construct, id: string, props: CfnMappingProps = {}) { super(scope, id); @@ -64,29 +64,41 @@ export class CfnMapping extends CfnRefElement { /** * @returns A reference to a value in the map based on the two keys. + * If mapping is lazy, the value from the map or default value is returned instead of the reference and the mapping is not rendered in the template. */ - public findInMap(key1: string, key2: string): string { + public findInMap(key1: string, key2: string, defaultValue?: string): string { let fullyResolved = false; + let notInMap = false; if (!Token.isUnresolved(key1)) { if (!(key1 in this.mapping)) { - throw new Error(`Mapping doesn't contain top-level key '${key1}'`); - } - if (!Token.isUnresolved(key2)) { + if (defaultValue === undefined) { + throw new Error(`Mapping doesn't contain top-level key '${key1}'`); + } else { + notInMap = true; + } + } else if (!Token.isUnresolved(key2)) { if (!(key2 in this.mapping[key1])) { - throw new Error(`Mapping doesn't contain second-level key '${key2}'`); + if (defaultValue === undefined) { + throw new Error(`Mapping doesn't contain second-level key '${key2}'`); + } else { + notInMap = true; + } } fullyResolved = true; } } - if (fullyResolved) { - if (this.lazy) { + + if (this.lazy) { + if (notInMap && defaultValue !== undefined) { + return defaultValue; + } else if (fullyResolved) { return this.mapping[key1][key2]; } - } else { - this.lazyRender = true; } - return new CfnMappingEmbedder(this, this.mapping, key1, key2).toString(); + this.lazyRender = !fullyResolved; + + return new CfnMappingEmbedder(this, this.mapping, key1, key2, defaultValue).toString(); } /** @@ -130,12 +142,16 @@ export class CfnMapping extends CfnRefElement { class CfnMappingEmbedder implements IResolvable { readonly creationStack: string[] = []; - constructor(private readonly cfnMapping: CfnMapping, readonly mapping: Mapping, private readonly key1: string, private readonly key2: string) { } + constructor(private readonly cfnMapping: CfnMapping, + readonly mapping: Mapping, + private readonly key1: string, + private readonly key2: string, + private readonly defaultValue?: string) { } public resolve(context: IResolveContext): string { const consumingStack = Stack.of(context.scope); if (consumingStack === Stack.of(this.cfnMapping)) { - return Fn.findInMap(this.cfnMapping.logicalId, this.key1, this.key2); + return Fn.findInMap(this.cfnMapping.logicalId, this.key1, this.key2, this.defaultValue); } const constructScope = consumingStack; @@ -148,7 +164,7 @@ class CfnMappingEmbedder implements IResolvable { }); } - return Fn.findInMap(mappingCopy.logicalId, this.key1, this.key2); + return Fn.findInMap(mappingCopy.logicalId, this.key1, this.key2, this.defaultValue); } public toString() { diff --git a/packages/aws-cdk-lib/core/test/mappings.test.ts b/packages/aws-cdk-lib/core/test/mappings.test.ts index 5f6ae172e1f97..00a3192063094 100644 --- a/packages/aws-cdk-lib/core/test/mappings.test.ts +++ b/packages/aws-cdk-lib/core/test/mappings.test.ts @@ -259,7 +259,7 @@ describe('lazy mapping', () => { }); }); - it('throws if keys can be resolved but are not found in backing', () => { + it('throws if keys can be resolved but are not found in mapping', () => { expect(() => mapping.findInMap('NonExistentKey', 'SecondLevelKey1')) .toThrowError(/Mapping doesn't contain top-level key .*/); expect(() => mapping.findInMap('TopLevelKey1', 'NonExistentKey')) @@ -316,6 +316,161 @@ describe('eager by default', () => { }); }); +describe('defaultValue included', () => { + const backing = { + TopLevelKey1: { + SecondLevelKey1: [1, 2, 3], + SecondLevelKey2: { Hello: 'World' }, + }, + }; + + const defValue = 'foo'; + + let app: App; + let stack: Stack; + let mapping: CfnMapping; + + describe('lazy mapping', () => { + beforeEach(() => { + stack = new Stack(); + mapping = new CfnMapping(stack, 'Lazy Mapping', { + mapping: backing, + lazy: true, + }); + }); + + it('does not create CfnMapping if findInMap keys can be resolved', () => { + const retrievedValue = mapping.findInMap('TopLevelKey1', 'SecondLevelKey1', defValue); + + expect(stack.resolve(retrievedValue)).toStrictEqual([1, 2, 3]); + expect(toCloudFormation(stack)).toStrictEqual({}); + }); + + it('creates CfnMapping if top level key cannot be resolved', () => { + const retrievedValue = mapping.findInMap(Aws.REGION, 'SecondLevelKey1', defValue); + + expect(stack.resolve(retrievedValue)).toStrictEqual({ 'Fn::FindInMap': ['LazyMapping', { Ref: 'AWS::Region' }, 'SecondLevelKey1', { DefaultValue: defValue }] }); + expect(toCloudFormation(stack)).toEqual({ + Mappings: { + LazyMapping: backing, + }, + Transform: 'AWS::LanguageExtensions', + }); + expect(stack.templateOptions.transforms).toEqual(expect.arrayContaining([ + 'AWS::LanguageExtensions', + ])); + }); + + it('creates CfnMapping if second level key cannot be resolved', () => { + const retrievedValue = mapping.findInMap('TopLevelKey1', Aws.REGION, defValue); + + expect(stack.resolve(retrievedValue)).toStrictEqual({ 'Fn::FindInMap': ['LazyMapping', 'TopLevelKey1', { Ref: 'AWS::Region' }, { DefaultValue: defValue }] }); + expect(toCloudFormation(stack)).toStrictEqual({ + Mappings: { + LazyMapping: backing, + }, + Transform: 'AWS::LanguageExtensions', + }); + expect(stack.templateOptions.transforms).toEqual(expect.arrayContaining([ + 'AWS::LanguageExtensions', + ])); + }); + + it('returns default value if keys can be resolved but are not found in mapping', () => { + const retrievedValue1 = mapping.findInMap('NonExistentKey', 'SecondLevelKey1', defValue); + const retrievedValue2 = mapping.findInMap('TopLevelKey1', 'NonExistentKey', defValue); + + expect(stack.resolve(retrievedValue1)).toStrictEqual('foo'); + expect(stack.resolve(retrievedValue2)).toStrictEqual('foo'); + + expect(toCloudFormation(stack)).toStrictEqual({}); + }); + + it('handles multiple unresolved calls', () => { + const retrievedValue1 = mapping.findInMap(Aws.REGION, 'SecondLevelKey1', defValue); + const retrievedValue2 = mapping.findInMap('TopLevelKey1', Aws.REGION, defValue); + + expect(stack.resolve(retrievedValue1)).toStrictEqual({ 'Fn::FindInMap': ['LazyMapping', { Ref: 'AWS::Region' }, 'SecondLevelKey1', { DefaultValue: defValue }] }); + expect(stack.resolve(retrievedValue2)).toStrictEqual({ 'Fn::FindInMap': ['LazyMapping', 'TopLevelKey1', { Ref: 'AWS::Region' }, { DefaultValue: defValue }] }); + + expect(toCloudFormation(stack)).toStrictEqual({ + Mappings: { + LazyMapping: backing, + }, + Transform: expect.arrayContaining(['AWS::LanguageExtensions']), + }); + + expect(stack.templateOptions.transforms).toEqual(expect.arrayContaining([ + 'AWS::LanguageExtensions', + ])); + }); + }); + + describe('eager by default', () => { + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack'); + mapping = new CfnMapping(stack, 'Lazy Mapping', { + mapping: backing, + }); + }); + + it('emits warning if every findInMap resolves immediately', () => { + mapping.findInMap('TopLevelKey1', 'SecondLevelKey1', defValue); + + const assembly = app.synth(); + + expect(getInfoAnnotations(assembly)).toStrictEqual([{ + path: '/Stack/Lazy Mapping', + message: 'Consider making this CfnMapping a lazy mapping by providing `lazy: true`: either no findInMap was called or every findInMap could be immediately resolved without using Fn::FindInMap', + }]); + }); + + it('does not emit warning if a findInMap could not resolve immediately', () => { + const retrievedValue = mapping.findInMap('TopLevelKey1', Aws.REGION, defValue); + + const assembly = app.synth(); + + expect(getInfoAnnotations(assembly)).toStrictEqual([]); + + stack.resolve(retrievedValue); + expect(stack.templateOptions.transforms).toEqual(expect.arrayContaining([ + 'AWS::LanguageExtensions', + ])); + }); + + it('creates CfnMapping if top level key cannot be resolved', () => { + const retrievedValue = mapping.findInMap(Aws.REGION, 'SecondLevelKey1', defValue); + + expect(stack.resolve(retrievedValue)).toStrictEqual({ 'Fn::FindInMap': ['LazyMapping', { Ref: 'AWS::Region' }, 'SecondLevelKey1', { DefaultValue: defValue }] }); // should I use string or variable here? variable works + expect(toCloudFormation(stack)).toStrictEqual({ + Mappings: { + LazyMapping: backing, + }, + Transform: 'AWS::LanguageExtensions', + }); + expect(stack.templateOptions.transforms).toEqual(expect.arrayContaining([ + 'AWS::LanguageExtensions', + ])); + }); + + it('creates CfnMapping if second level key cannot be resolved', () => { + const retrievedValue = mapping.findInMap('TopLevelKey1', Aws.REGION, defValue); + + expect(stack.resolve(retrievedValue)).toStrictEqual({ 'Fn::FindInMap': ['LazyMapping', 'TopLevelKey1', { Ref: 'AWS::Region' }, { DefaultValue: defValue }] }); + expect(toCloudFormation(stack)).toStrictEqual({ + Mappings: { + LazyMapping: backing, + }, + Transform: 'AWS::LanguageExtensions', + }); + expect(stack.templateOptions.transforms).toEqual(expect.arrayContaining([ + 'AWS::LanguageExtensions', + ])); + }); + }); +}); + function getInfoAnnotations(casm: CloudAssembly) { const result = new Array<{ path: string, message: string }>(); for (const stack of Object.values(casm.manifest.artifacts ?? {})) {