Skip to content

Commit

Permalink
Fix resource mappings to multiple resources (#191)
Browse files Browse the repository at this point in the history
Typically there will be a couple of scenarios for how a CFN resource
maps to Pulumi resources:

1. The CFN resource maps to a single Pulumi aws-native resource
This is the most straightforward case because everything maps directly,
including the attributes

2. The CFN resource maps to a single Pulumi aws resources
In this case the mapping is to a single resource, but some times the
attributes available
on the CFN resource do not map to the attributes on the Pulumi resource.
In these cases
the mapping can return custom attributes that are later available when
references are resolved

3. The CFN resource maps to multiple Pulumi aws resources
One example would be the AWS::IAM::Policy resource which in CFN includes
Role, Group, and User
Polices, but in Pulumi aws these are broken out into separate resources.
In that case the \"main\"
resource would be the Policy and the other resources would be added to
the resources array.
The critical point here is that the \"main\" resource needs to have a
logicalId that matches the
logicalId of the CFN resource, while the other supporting resources must
have different logicalIds.

This PR fixes the 3rd case. Previously we were not handling the other
supporting resources.
  • Loading branch information
corymhall authored Nov 1, 2024
1 parent 8be5049 commit e1ce154
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 221 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ CustomResources, commonly by using the AWS Classic provider to implement the
missing resource.

```ts
remapCloudControlResource(logicalId: string, typeName: string, props: any, options: pulumi.ResourceOptions): { [key: string]: pulumi.CustomResource } | undefined
remapCloudControlResource(logicalId: string, typeName: string, props: any, options: pulumi.ResourceOptions): ResourceMapping | undefined;
```

Parameters:
Expand Down
60 changes: 0 additions & 60 deletions examples/cron-lambda/adapter.ts

This file was deleted.

36 changes: 17 additions & 19 deletions examples/lookups/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,25 +127,23 @@ const app = new pulumicdk.App(
remapCloudControlResource(logicalId, typeName, props, options) {
switch (typeName) {
case 'AWS::Route53::RecordSet':
return [
new aws.route53.Record(
logicalId,
{
zoneId: props.HostedZoneId,
aliases: [
{
name: props.AliasTarget.DNSName,
zoneId: props.AliasTarget.HostedZoneId,
evaluateTargetHealth: props.AliasTarget.EvaluateTargetHealth ?? false,
},
],
name: props.Name,
type: props.Type,
records: props.ResourceRecords,
},
options,
),
];
return new aws.route53.Record(
logicalId,
{
zoneId: props.HostedZoneId,
aliases: [
{
name: props.AliasTarget.DNSName,
zoneId: props.AliasTarget.HostedZoneId,
evaluateTargetHealth: props.AliasTarget.EvaluateTargetHealth ?? false,
},
],
name: props.Name,
type: props.Type,
records: props.ResourceRecords,
},
options,
);
default:
return undefined;
}
Expand Down
195 changes: 111 additions & 84 deletions src/aws-resource-mappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import * as pulumi from '@pulumi/pulumi';
import * as aws from '@pulumi/aws';
import { ResourceMapping, normalize } from './interop';
import { ResourceAttributeMappingArray, ResourceMapping, normalize } from './interop';

function maybe<T, U>(v: T | undefined, fn: (t: T) => U): U | undefined {
if (v === undefined) {
Expand Down Expand Up @@ -46,44 +46,40 @@ export function mapToAwsResource(
typeName: string,
rawProps: any,
options: pulumi.ResourceOptions,
): ResourceMapping[] | undefined {
): ResourceMapping | undefined {
const props = normalize(rawProps);
switch (typeName) {
// ApiGatewayV2
case 'AWS::ApiGatewayV2::Integration':
return [
new aws.apigatewayv2.Integration(
logicalId,
{
...props,
requestParameters: rawProps.RequestParameters,
requestTemplates: rawProps.RequestTemplates,
responseParameters: rawProps.ResponseParameters,
tlsConfig: maybe(props.tlsConfig, () => ({ insecureSkipVerification: true })),
},
options,
),
];
return new aws.apigatewayv2.Integration(
logicalId,
{
...props,
requestParameters: rawProps.RequestParameters,
requestTemplates: rawProps.RequestTemplates,
responseParameters: rawProps.ResponseParameters,
tlsConfig: maybe(props.tlsConfig, () => ({ insecureSkipVerification: true })),
},
options,
);
case 'AWS::ApiGatewayV2::Stage':
return [
new aws.apigatewayv2.Stage(
logicalId,
{
accessLogSettings: props.accessLogSettings,
apiId: props.apiId,
autoDeploy: props.autoDeploy,
clientCertificateId: props.clientCertificateId,
defaultRouteSettings: props.defaultRouteSettings,
deploymentId: props.deploymentId,
description: props.description,
name: props.stageName,
routeSettings: props.routeSettings,
stageVariables: rawProps.StageVariables,
tags: tags(props.tags),
},
options,
),
];
return new aws.apigatewayv2.Stage(
logicalId,
{
accessLogSettings: props.accessLogSettings,
apiId: props.apiId,
autoDeploy: props.autoDeploy,
clientCertificateId: props.clientCertificateId,
defaultRouteSettings: props.defaultRouteSettings,
deploymentId: props.deploymentId,
description: props.description,
name: props.stageName,
routeSettings: props.routeSettings,
stageVariables: rawProps.StageVariables,
tags: tags(props.tags),
},
options,
);

// SQS
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queuepolicy.html
Expand All @@ -92,76 +88,107 @@ export function mapToAwsResource(
throw new Error('QueuePolicy has an invalid value for `queues` property');
}

return (props.queues || []).flatMap((q: string) => {
return new aws.sqs.QueuePolicy(logicalId, {
policy: rawProps.PolicyDocument,
queueUrl: q,
});
const queues: string[] = props.queues ?? [];
return queues.flatMap((q: string, i: number) => {
const id = i === 0 ? logicalId : `${logicalId}-policy-${i}`;
return {
logicalId: id,
resource: new aws.sqs.QueuePolicy(id, {
policy: rawProps.PolicyDocument,
queueUrl: q,
}),
};
});
}

// SNS
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sns-topicpolicy.html
case 'AWS::SNS::TopicPolicy':
case 'AWS::SNS::TopicPolicy': {
if (!Array.isArray(props.topics)) {
throw new Error('TopicPolicy has an invalid value for `topics` property');
}

return (props.topics || []).flatMap((arn: string) => {
return new aws.sns.TopicPolicy(logicalId, {
policy: rawProps.PolicyDocument,
arn,
});
const topics: string[] = props.topics ?? [];
return topics.flatMap((arn: string, i: number) => {
const id = i === 0 ? logicalId : `${logicalId}-policy-${i}`;
return {
logicalId: id,
resource: new aws.sns.TopicPolicy(id, {
policy: rawProps.PolicyDocument,
arn,
}),
};
});
}

// IAM
case 'AWS::IAM::Policy': {
const resources: ResourceMapping[] = [];
const resources: ResourceAttributeMappingArray = [];
const policy = new aws.iam.Policy(
logicalId,
{
policy: rawProps.PolicyDocument,
},
options,
);
resources.push(policy);

for (let i = 0; i < (props.groups || []).length; i++) {
resources.push(
new aws.iam.GroupPolicyAttachment(
`${logicalId}-${i}`,
{
group: props.groups[i],
policyArn: policy.arn,
},
options,
),
);
}
for (let i = 0; i < (props.roles || []).length; i++) {
resources.push(
new aws.iam.RolePolicyAttachment(
`${logicalId}-${i}`,
{
role: props.roles[i],
policyArn: policy.arn,
},
options,
),
);
}
for (let i = 0; i < (props.users || []).length; i++) {
resources.push(
new aws.iam.UserPolicyAttachment(
`${logicalId}-${i}`,
{
user: props.users[i],
policyArn: policy.arn,
},
options,
),
);
}
resources.push({
resource: policy,
logicalId,
});

const groups: string[] = props.groups ?? [];
resources.push(
...groups.flatMap((group: string, i: number) => {
const id = `${logicalId}-group-${i}`;
return {
logicalId: id,
resource: new aws.iam.GroupPolicyAttachment(
id,
{
group,
policyArn: policy.arn,
},
options,
),
};
}),
);

const roles: string[] = props.roles ?? [];
resources.push(
...roles.flatMap((role: string, i: number) => {
const id = `${logicalId}-role-${i}`;
return {
logicalId: id,
resource: new aws.iam.RolePolicyAttachment(
id,
{
role,
policyArn: policy.arn,
},
options,
),
};
}),
);

const users: string[] = props.users ?? [];
resources.push(
...users.flatMap((user: string, i: number) => {
const id = `${logicalId}-user-${i}`;
return {
logicalId: id,
resource: new aws.iam.UserPolicyAttachment(
id,
{
user,
policyArn: policy.arn,
},
options,
),
};
}),
);

return resources;
}
Expand Down
Loading

0 comments on commit e1ce154

Please sign in to comment.