Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Fn.findInMap supports default value #26543

Merged
merged 18 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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();
22 changes: 22 additions & 0 deletions packages/aws-cdk-lib/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,28 @@ 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. 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" } ] }`.

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
Expand Down
36 changes: 30 additions & 6 deletions packages/aws-cdk-lib/core/lib/cfn-fn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* Prefer to use `CfnMapping.findInMap`.
* Warning: do not use with lazy maps.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this how we like to convey warnings / best practices?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should explain succinctly why you should not use with lazy maps

* @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();
}

/**
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -500,9 +502,31 @@ 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
kaizencc marked this conversation as resolved.
Show resolved Hide resolved
*/
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');
}
scanlonp marked this conversation as resolved.
Show resolved Hide resolved
return { 'Fn::FindInMap': [this.mapName, this.topLevelKey, this.secondLevelKey, this.defaultValue !== undefined ? { DefaultValue: this.defaultValue } : undefined] };
}

public toString() {
return Token.asString(this);
}
kaizencc marked this conversation as resolved.
Show resolved Hide resolved
}

Expand Down
47 changes: 31 additions & 16 deletions packages/aws-cdk-lib/core/lib/cfn-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -63,30 +63,41 @@ export class CfnMapping extends CfnRefElement {
}

/**
* @returns A reference to a value in the map based on the two keys.
* @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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure "lazy" means: only render the actual mapping to the template if there is at least one value that actually needs to be looked up from it at deploy time.

Conversely: if no value is ever read from the map, or all values can be resolved at synth time, then there is no need to render the map.

The line of documentation you added here doesn't help me understand that :).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Felt that it wasn't accurate to say it returns a reference is sometimes it just passes a value. Also could help debug if the short circuit was not documented. Can add a line about the mapping behavior too

*/
public findInMap(key1: string, key2: string): string {
public findInMap(key1: string, key2: string, defaultValue?: string): string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohmigod. Can this method be simplified if we reorganize it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be glad to, but not seeing a clear way. Think we ship it and someone enterprising can tidy it up later.

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();
}

/**
Expand Down Expand Up @@ -130,12 +141,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;
Expand All @@ -148,7 +163,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() {
Expand Down
Loading
Loading