Skip to content

Commit

Permalink
Add support for Fn::FindInMap intrinsic
Browse files Browse the repository at this point in the history
This adds support for the CloudFormation `Fn::FindInMap` intrinsic
function.

Docs on the `Mappings` section of a CloudFormation template can be found
[here](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html).

Docs on the `Fn::FindInMap` function can be found [here](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-findinmap.html)

closes #187
  • Loading branch information
corymhall committed Nov 15, 2024
1 parent 13e128e commit 88e1d0f
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 18 deletions.
3 changes: 1 addition & 2 deletions integration/apigateway/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ class ApiGatewayStack extends pulumicdk.Stack {
this.node.setContext('@aws-cdk/aws-apigateway:disableCloudWatchRole', 'true');

new RestApi(this, 'test-api');
// TODO[pulumi/pulumi-cdk#187]
// new SfnApi(this, 'test-sfn-api');
new SfnApi(this, 'test-sfn-api');
new SpecRestApi(this, 'test-spec-api');
}
}
Expand Down
7 changes: 6 additions & 1 deletion integration/apigateway/sfn-api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
import * as sfn from 'aws-cdk-lib/aws-stepfunctions';
import * as apigw from 'aws-cdk-lib/aws-apigateway';
Expand All @@ -15,8 +15,13 @@ export class SfnApi extends Construct {
stateMachineType: sfn.StateMachineType.EXPRESS,
});

// TODO[pulumi/pulumi-cdk#62] The auto created role has too long name
const role = new iam.Role(this, 'StartRole', {
assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
});
const api = new apigw.StepFunctionsRestApi(this, 'StepFunctionsRestApi', {
deploy: false,
role,
stateMachine: stateMachine,
headers: true,
path: false,
Expand Down
8 changes: 8 additions & 0 deletions integration/misc-services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ class MiscServicesStack extends pulumicdk.Stack {
}),
],
});

new lambda.Function(this, 'FindInMapFunc', {
runtime: lambda.Runtime.NODEJS_LATEST,
handler: 'index.handler',
code: lambda.Code.fromInline('def handler(event, context): return {}'),
// this adds a Fn::FindInMap
insightsVersion: lambda.LambdaInsightsVersion.VERSION_1_0_317_0,
});
}
}

Expand Down
9 changes: 8 additions & 1 deletion src/assembly/stack.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as path from 'path';
import { DestinationIdentifier, FileManifestEntry } from 'cdk-assets';
import { CloudFormationParameter, CloudFormationResource, CloudFormationTemplate } from '../cfn';
import { CloudFormationMapping, CloudFormationParameter, CloudFormationResource, CloudFormationTemplate } from '../cfn';
import { ConstructTree, StackMetadata } from './types';
import { FileAssetPackaging, FileDestination } from 'aws-cdk-lib/cloud-assembly-schema';

Expand Down Expand Up @@ -109,6 +109,12 @@ export class StackManifest {
*/
private readonly resources: { [logicalId: string]: CloudFormationResource };

/**
* The Mappings from the CFN Stack
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html
*/
public readonly mappings?: CloudFormationMapping;

/**
*
*/
Expand All @@ -118,6 +124,7 @@ export class StackManifest {
this.dependencies = props.dependencies;
this.outputs = props.template.Outputs;
this.parameters = props.template.Parameters;
this.mappings = props.template.Mappings;
this.metadata = props.metadata;
this.templatePath = props.templatePath;
this.id = props.id;
Expand Down
6 changes: 6 additions & 0 deletions src/cfn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,16 @@ export interface CloudFormationResource {
readonly DependsOn?: string | string[];
}

export type CloudFormationMapping = { [mappingLogicalName: string]: TopLevelMapping };
export type CloudFormationMappingValue = string | string[];
export type TopLevelMapping = { [key: string]: SecondLevelMapping };
export type SecondLevelMapping = { [key: string]: CloudFormationMappingValue };

export interface CloudFormationTemplate {
Parameters?: { [id: string]: CloudFormationParameter };
Resources?: { [id: string]: CloudFormationResource };
Conditions?: { [id: string]: any };
Mappings?: CloudFormationMapping;
Outputs?: { [id: string]: any };
}

Expand Down
41 changes: 39 additions & 2 deletions src/converters/app-converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ import {
} from '@pulumi/aws-native';
import { mapToAwsResource } from '../aws-resource-mappings';
import { attributePropertyName, mapToCfnResource } from '../cfn-resource-mappings';
import { CloudFormationResource, getDependsOn } from '../cfn';
import { CloudFormationMapping, CloudFormationResource, getDependsOn } from '../cfn';

Check warning on line 20 in src/converters/app-converter.ts

View workflow job for this annotation

GitHub Actions / Run lint

'CloudFormationMapping' is defined but never used
import { OutputMap, OutputRepr } from '../output-map';
import { parseSub } from '../sub';
import { getPartition } from '@pulumi/aws-native/getPartition';
import { processSecretsManagerReferenceValue, resolveSecretsManagerDynamicReference } from './secrets-manager-dynamic';
import { processSecretsManagerReferenceValue } from './secrets-manager-dynamic';

/**
* AppConverter will convert all CDK resources into Pulumi resources.
Expand Down Expand Up @@ -462,6 +462,43 @@ export class StackConverter extends ArtifactConverter {
throw new Error(`Fn::ImportValue is not yet supported.`);
}

case 'Fn::FindInMap': {
return lift(([mappingLogicalName, topLevelKey, secondLevelKey]) => {
if (params.length !== 3) {
throw new Error(`Fn::FindInMap requires exactly 3 parameters, got ${params.length}`);
}
if (!this.stack.mappings) {
throw new Error(`No mappings found in stack`);
}
if (!(mappingLogicalName in this.stack.mappings)) {
throw new Error(
`Mapping ${mappingLogicalName} not found in mappings. Available mappings are ${Object.keys(
this.stack.mappings,
)}`,
);
}
const topLevelMapping = this.stack.mappings[mappingLogicalName];
if (!(topLevelKey in topLevelMapping)) {
throw new Error(
`Key ${topLevelKey} not found in mapping ${mappingLogicalName}. Available keys are ${Object.keys(
topLevelMapping,
)}`,
);
}
const secondLevelMapping = topLevelMapping[topLevelKey];
if (!(secondLevelKey in secondLevelMapping)) {
throw new Error(
`Key ${secondLevelKey} not found in mapping ${mappingLogicalName}.${topLevelKey}. Available keys are ${Object.keys(
secondLevelMapping,
)}`,
);
}

const value = secondLevelMapping[secondLevelKey];
return value;
}, this.processIntrinsics(params));
}

default:
throw new Error(`unsupported intrinsic function ${fn} (params: ${JSON.stringify(params)})`);
}
Expand Down
196 changes: 184 additions & 12 deletions tests/converters/app-converter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,64 +191,236 @@ describe('App Converter', () => {
});

test.each([
['ref', createStackManifest({ Bucket: { Ref: 'resource1' } }), 'resource1_id'],
['ref', createStackManifest({ Bucket: { Ref: 'resource1' } }), 'resource1_id', undefined],
[
'GetAtt',
createStackManifest({
Bucket: { 'Fn::GetAtt': ['resource1', 'Arn'] },
}),
'resource1_arn',
undefined,
],
[
'Join-Ref',
createStackManifest({
Bucket: { 'Fn::Join': ['', ['arn:', { Ref: 'resource1' }]] },
}),
'arn:resource1_id',
undefined,
],
[
'Split-Select-Ref',
createStackManifest({
Bucket: { 'Fn::Select': ['1', { 'Fn::Split': ['_', { Ref: 'resource1' }] }] },
}),
'id',
undefined,
],
[
'Base64-Ref',
createStackManifest({
Bucket: { 'Fn::Base64': { Ref: 'resource1' } },
}),
Buffer.from('resource1_id').toString('base64'),
undefined,
],
[
'GetAZs-Select-Ref',
createStackManifest({
Bucket: { 'Fn::Select': ['1', { 'Fn::GetAZs': 'us-east-1' }] },
}),
'us-east-1b',
undefined,
],
[
'Sub-Ref',
createStackManifest({
Bucket: { 'Fn::Sub': 'www.${resource1}-${AWS::Region}-${AWS::AccountId}' },
}),
'www.resource1_id-us-east-2-12345678910',
undefined,
],
[
'FindInMap',
createStackManifest(
{
Bucket: { 'Fn::FindInMap': ['Map', 'Key', 'Value'] },
},
undefined,
undefined,
undefined,
{
Map: {
Key: {
Value: 'result',
},
},
},
),
'result',
undefined,
],
[
'FindInMap-Ref',
createStackManifest(
{
Bucket: { 'Fn::FindInMap': ['Map', { Ref: 'AWS::Region' }, 'Value'] },
},
undefined,
undefined,
undefined,
{
Map: {
['us-east-2']: {
Value: 'result',
},
},
},
),
'result',
undefined,
],
[
'Split-FindInMap-Ref',
createStackManifest(
{
Bucket: {
'Fn::Select': [
'1',
{
'Fn::Split': [
',',
{
'Fn::FindInMap': ['Map', { Ref: 'AWS::Region' }, 'Value'],
},
],
},
],
},
},
undefined,
undefined,
undefined,
{
Map: {
['us-east-2']: {
Value: 'result1,result2',
},
},
},
),
'result2',
undefined,
],
[
'FindInMap-id-error',
createStackManifest(
{
Bucket: { 'Fn::FindInMap': ['Map', 'Key', 'Value'] },
},
undefined,
undefined,
undefined,
{
OtherMap: {
Key: {
Value: 'result',
},
},
},
),
'result',
'Mapping Map not found in mappings. Available mappings are OtherMap',
],
[
'FindInMap-mappings-error',
createStackManifest(
{
Bucket: { 'Fn::FindInMap': ['Map', 'Key', 'Value'] },
},
undefined,
undefined,
undefined,
undefined,
),
'result',
'No mappings found in stack',
],
[
'FindInMap-mappings-input-error',
createStackManifest(
{
Bucket: { 'Fn::FindInMap': ['Map', 'Value'] },
},
undefined,
undefined,
undefined,
undefined,
),
'result',
'Fn::FindInMap requires exactly 3 parameters, got 2',
],
[
'FindInMap-topLevel-error',
createStackManifest(
{
Bucket: { 'Fn::FindInMap': ['Map', 'OtherKey', 'Value'] },
},
undefined,
undefined,
undefined,
{
Map: {
Key: {
Value: 'result',
},
},
},
),
'result',
'Key OtherKey not found in mapping Map. Available keys are Key',
],
[
'FindInMap-secondLevel-error',
createStackManifest(
{
Bucket: { 'Fn::FindInMap': ['Map', 'Key', 'OtherValue'] },
},
undefined,
undefined,
undefined,
{
Map: {
Key: {
Value: 'result',
},
},
},
),
'result',
'Key OtherValue not found in mapping Map.Key. Available keys are Value',
],
])(
'intrinsics %s',
async (_name, stackManifest, expected) => {
async (_name, stackManifest, expected, expectedError) => {
const mockStackComponent = new MockAppComponent('/tmp/foo/bar/does/not/exist');
const converter = new StackConverter(mockStackComponent, stackManifest);
converter.convert(new Set());
const promises = Array.from(converter.resources.values()).flatMap((res) => promiseOf(res.resource.urn));
await Promise.all(promises);
const bucket = converter.resources.get('resource1');
expect(bucket).toBeDefined();
const policy = converter.resources.get('resource2');
expect(policy).toBeDefined();
const policyResource = policy!.resource as BucketPolicy;
const policyBucket = await promiseOf(policyResource.bucket);
expect(policyBucket).toEqual(expected);
if (expectedError) {
expect(() => {
converter.convert(new Set());
}).toThrow(expectedError);
} else {
converter.convert(new Set());
const promises = Array.from(converter.resources.values()).flatMap((res) => promiseOf(res.resource.urn));
await Promise.all(promises);
const bucket = converter.resources.get('resource1');
expect(bucket).toBeDefined();
const policy = converter.resources.get('resource2');
expect(policy).toBeDefined();
const policyResource = policy!.resource as BucketPolicy;
const policyBucket = await promiseOf(policyResource.bucket);
expect(policyBucket).toEqual(expected);
}
},
10_000,
);
Expand Down
Loading

0 comments on commit 88e1d0f

Please sign in to comment.