Skip to content

Commit

Permalink
Add support for Fn::FindInMap intrinsic (#221)
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 authored Nov 15, 2024
1 parent 92fbc38 commit 7bb6000
Show file tree
Hide file tree
Showing 11 changed files with 295 additions and 81 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
20 changes: 4 additions & 16 deletions integration/custom-resource/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ class S3DeploymentStack extends pulumicdk.Stack {
autoDeleteObjects: true,
});


this.bucketWebsiteUrl = this.asOutput(bucket.bucketWebsiteUrl);

const deploy = new s3deploy.BucketDeployment(this, 'DeployWebsite', {
Expand All @@ -41,37 +40,26 @@ class S3DeploymentStack extends pulumicdk.Stack {
new iam.Role(this, 'CustomResourceRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
inlinePolicies: {
'CustomResourcePolicy': new iam.PolicyDocument({
CustomResourcePolicy: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: ['s3:GetObject'],
resources: [`${deploy.deployedBucket.bucketArn}/*`],
}),
],
}),
}
},
});
}
}

const cfg = new pulumi.Config();
const accountId = cfg.require('accountId');

class MyApp extends pulumicdk.App {
constructor() {
super('app', (scope: pulumicdk.App): pulumicdk.AppOutputs => {
const stack = new S3DeploymentStack(scope, 's3deployment', {
// configure the environment to prevent the bucket from using the unsupported FindInMap intrinsic (TODO[pulumi/pulumi-cdk#187])
props: {
env: {
account: accountId,
region: process.env.AWS_REGION,
}
}
});
const stack = new S3DeploymentStack(scope, 's3deployment');
return {
bucketWebsiteUrl: stack.bucketWebsiteUrl,
bucketObjectKeys: stack.bucketObjectKeys
bucketObjectKeys: stack.bucketObjectKeys,
};
});
}
Expand Down
15 changes: 0 additions & 15 deletions integration/examples_nodejs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@ import (
"testing"
"time"

"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/pulumi/pulumi/pkg/v3/testing/integration"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestApiGateway(t *testing.T) {
Expand Down Expand Up @@ -140,21 +137,9 @@ func TestErrors(t *testing.T) {
// S3 bucket on delete and another for uploading the index.html file for a static website to the bucket.
// The test validates that the website is deployed, displays the expected content and gets cleaned up on delete.
func TestCustomResource(t *testing.T) {
sess := session.Must(session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
}))
svc := sts.New(sess)

result, err := svc.GetCallerIdentity(&sts.GetCallerIdentityInput{})
require.NoError(t, err, "Failed to get AWS account ID")
accountId := *result.Account

test := getJSBaseOptions(t).
With(integration.ProgramTestOptions{
Dir: filepath.Join(getCwd(t), "custom-resource"),
Config: map[string]string{
"accountId": accountId,
},
// Workaround until TODO[pulumi/pulumi-aws-native#1816] is resolved.
Env: []string{"PULUMI_CDK_EXPERIMENTAL_MAX_NAME_LENGTH=56"},
ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) {
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
@@ -1,5 +1,5 @@
import * as cdk from 'aws-cdk-lib/core';
import * as aws from '@pulumi/aws-native'
import * as aws from '@pulumi/aws-native';
import * as pulumi from '@pulumi/pulumi';
import { AssemblyManifestReader, StackManifest } from '../assembly';
import { ConstructInfo, Graph, GraphBuilder, GraphNode } from '../graph';
Expand All @@ -18,7 +18,7 @@ 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 21 in src/converters/app-converter.ts

View workflow job for this annotation

GitHub Actions / Run lint

'CloudFormationMapping' is defined but never used

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

View workflow job for this annotation

GitHub Actions / Run lint

'CloudFormationMapping' is defined but never used

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

View workflow job for this annotation

GitHub Actions / Run lint

'CloudFormationMapping' is defined but never used

Check warning on line 21 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';
Expand Down Expand Up @@ -470,6 +470,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
Loading

0 comments on commit 7bb6000

Please sign in to comment.