Skip to content

Commit

Permalink
fix: correctly normalize json types (#147)
Browse files Browse the repository at this point in the history
Some properties in aws-native are Json types, meaning that their values
are sent to CloudControl as is. For all other types we do some
conversion between the CloudControl type and the pulumi type.

An example of this is any policy documents, such as the
`AWS::S3::AccessPoint.Policy` property. The pulumi type is
`pulumi.json#/Any`. In the below example, you can see the property keys
within `policy` have their first character uppercase.

```ts
new aws.s3.AccessPoint('access-point', {
  policy: {
    Version: '2012-10-17',
    Statement: [
      {
        Action: ['s3:GetObject'],
        ...
      }
    ]
  }
});
```

In order to handle these types in the `normalize` function I've added a
utility to parse the aws-native `metadata.json` schema and lookup the
types.

I've also removed the `cfn` mappings that exist in order to handle the
json mappings and the existing tests cover it.


closes #127
  • Loading branch information
corymhall authored Aug 27, 2024
1 parent 8825bca commit 1094919
Show file tree
Hide file tree
Showing 13 changed files with 215,134 additions and 150 deletions.
24 changes: 12 additions & 12 deletions examples/cron-lambda/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,15 @@ interface target {
}

export function remapCloudControlResource(
element: CfnElement,
_element: CfnElement,
logicalId: string,
typeName: string,
rawProps: any,
options: pulumi.ResourceOptions,
): pulumi.CustomResource | undefined {
const props = pulumicdk.interop.normalize(rawProps);
switch (typeName) {
case 'AWS::Events::Rule':
const resources: { [key: string]: pulumi.CustomResource } = {};
case 'AWS::Events::Rule': {
const rule = new aws.cloudwatch.EventRule(
logicalId,
{
Expand All @@ -43,17 +42,18 @@ export function remapCloudControlResource(
);
}
return rule;
}
case 'AWS::Lambda::Permission':
return new aws.lambda.Permission(
logicalId,
{
action: props['action'],
function: props['functionName'],
principal: props['principal'],
sourceArn: props['sourceArn'] ?? undefined,
},
options,
);
logicalId,
{
action: props['action'],
function: props['functionName'],
principal: props['principal'],
sourceArn: props['sourceArn'] ?? undefined,
},
options,
);
}

return undefined;
Expand Down
7 changes: 4 additions & 3 deletions examples/fargate/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ export function remapCloudControlResource(
switch (typeName) {
case 'AWS::ApplicationAutoScaling::ScalingPolicy':
debug(`AWS::ApplicationAutoScaling::ScalingPolicy props: ${JSON.stringify(props)}`);
return new aws.appautoscaling.Policy(logicalId,
return new aws.appautoscaling.Policy(
logicalId,
{
resourceId: props.resourceId ?? props.scalingTargetId,
scalableDimension: props.scalableDimension ?? "ecs:service:DesiredCount",
serviceNamespace: props.serviceNamespace ?? "ecs",
scalableDimension: props.scalableDimension ?? 'ecs:service:DesiredCount',
serviceNamespace: props.serviceNamespace ?? 'ecs',
policyType: props.policyType,
stepScalingPolicyConfiguration: props.stepScalingPolicyConfiguration,
name: props.policyName,
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"devDependencies": {
"@aws-cdk/aws-apprunner-alpha": "2.20.0-alpha.0",
"@pulumi/aws": "^6.32.0",
"@pulumi/aws-native": "^0.108.0",
"@pulumi/docker": "^4.5.0",
"@pulumi/pulumi": "^3.117.0",
"@types/archiver": "^6.0.2",
Expand All @@ -47,13 +46,13 @@
},
"peerDependencies": {
"@pulumi/aws": "^6.32.0",
"@pulumi/aws-native": "^0.108.0",
"@pulumi/docker": "^4.5.0",
"@pulumi/pulumi": "^3.117.0",
"aws-cdk-lib": "^2.20.0",
"constructs": "^10.0.111"
},
"dependencies": {
"@pulumi/aws-native": "0.121.0",
"@types/glob": "^8.1.0",
"archiver": "^7.0.1"
},
Expand Down
214,497 changes: 214,497 additions & 0 deletions schemas/aws-native-metadata.json

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions src/aws-resource-mappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function mapToAwsResource(
requestParameters: rawProps.RequestParameters,
requestTemplates: rawProps.RequestTemplates,
responseParameters: rawProps.ResponseParameters,
tlsConfig: maybe(props.tlsConfig, (_) => ({ insecureSkipVerification: true })),
tlsConfig: maybe(props.tlsConfig, () => ({ insecureSkipVerification: true })),
},
options,
);
Expand Down Expand Up @@ -450,7 +450,7 @@ export function mapToAwsResource(
);

for (let i = 0; i < (props.groups || []).length; i++) {
const attachment = new aws.iam.GroupPolicyAttachment(
new aws.iam.GroupPolicyAttachment(
`${logicalId}-${i}`,
{
group: props.groups[i],
Expand All @@ -460,7 +460,7 @@ export function mapToAwsResource(
);
}
for (let i = 0; i < (props.roles || []).length; i++) {
const attachment = new aws.iam.RolePolicyAttachment(
new aws.iam.RolePolicyAttachment(
`${logicalId}-${i}`,
{
role: props.roles[i],
Expand All @@ -470,7 +470,7 @@ export function mapToAwsResource(
);
}
for (let i = 0; i < (props.users || []).length; i++) {
const attachment = new aws.iam.UserPolicyAttachment(
new aws.iam.UserPolicyAttachment(
`${logicalId}-${i}`,
{
user: props.users[i],
Expand Down Expand Up @@ -517,10 +517,10 @@ export function mapToAwsResource(
}

function mapDynamoDBTable(
element: CfnElement,
_element: CfnElement,
logicalId: string,
typeName: string,
rawProps: any,
_typeName: string,
_rawProps: any,
props: any,
options: pulumi.ResourceOptions,
): aws.dynamodb.Table {
Expand Down
81 changes: 2 additions & 79 deletions src/cfn-resource-mappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// limitations under the License.

import * as pulumi from '@pulumi/pulumi';
import { ecs, iam, apprunner, lambda, s3, s3objectlambda } from '@pulumi/aws-native';
import { s3 } from '@pulumi/aws-native';
import { CfnElement, Token, Reference, Tokenization } from 'aws-cdk-lib';
import { CfnResource, ResourceMapping, normalize } from './interop';
import { debug } from '@pulumi/pulumi/log';
Expand All @@ -26,90 +26,13 @@ export function mapToCfnResource(
rawProps: any,
options: pulumi.ResourceOptions,
): ResourceMapping {
const props = normalize(rawProps);
const props = normalize(rawProps, typeName);
debug(`mapToCfnResource typeName: ${typeName} props: ${JSON.stringify(props)}`);
switch (typeName) {
case 'AWS::AppRunner::Service':
return new apprunner.Service(logicalId, props, options);
case 'AWS::ECS::Cluster':
return new ecs.Cluster(logicalId, props, options);
case 'AWS::ECS::TaskDefinition':
return new ecs.TaskDefinition(logicalId, props, options);
case 'AWS::IAM::Role': {
// policyDocument and assumeRolePolicyDocument are both Json types
// so we need the raw names
return new iam.Role(
logicalId,
{
...props,
policies:
rawProps.Policies === undefined
? undefined
: rawProps.Policies.flatMap((policy: any) => {
return {
policyName: policy.PolicyName,
policyDocument: policy.PolicyDocument,
};
}),
assumeRolePolicyDocument: rawProps.AssumeRolePolicyDocument,
},
options,
);
}
case 'AWS::Lambda::Function':
// The Environment.Variables property is a Json type so we need
// the raw names
return new lambda.Function(
logicalId,
{
...props,
environment:
rawProps.Environment === undefined ? undefined : { variables: rawProps.Environment.Variables },
},
options,
);
case 'AWS::S3::AccessPoint':
// the policy property is a Json type so we need the raw names
return new s3.AccessPoint(
logicalId,
{
...props,
policy: rawProps.Policy,
},
options,
);
case 'AWS::S3::Bucket':
// Lowercase the bucket name to comply with the Bucket resource's naming constraints, which only allow
// lowercase letters.
return new s3.Bucket(logicalId.toLowerCase(), props, options);
case 'AWS::S3ObjectLambda::AccessPoint': {
const transformations = rawProps.ObjectLambdaConfiguration.TransformationConfigurations;
return new s3objectlambda.AccessPoint(
logicalId,
{
name: props.name,
objectLambdaConfiguration: {
allowedFeatures: props.objectLambdaConfiguration.allowedFeatures,
cloudWatchMetricsEnabled: props.objectLambdaConfiguration.cloudWatchMetricsEnabled,
supportingAccessPoint: props.objectLambdaConfiguration.supportingAccessPoint,
transformationConfigurations:
transformations === undefined
? undefined
: transformations.map((config: any) => ({
actions: config.Actions,
contentTransformation: {
awsLambda: {
functionArn: config.ContentTransformation.AwsLambda.FunctionArn,
// functionPayload is a Json type so we need the raw value
functionPayload: config.ContentTransformation.AwsLambda.FunctionPayload,
},
},
})),
},
},
options,
);
}
default: {
// Scrape the attributes off of the construct.
//
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export * from './output';

import * as interop from './interop';
export { interop };
export * from './types';
47 changes: 18 additions & 29 deletions src/interop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,37 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import * as cdk from 'aws-cdk-lib';
import * as pulumi from '@pulumi/pulumi';
import { debug } from '@pulumi/pulumi/log';
import { IConstruct } from 'constructs';
import { moduleName, toSdkName, typeToken } from './naming';
import { normalizeObject } from './pulumi-metadata';
import { toSdkName, typeToken } from './naming';
import { PulumiProvider } from './types';

export function firstToLower(str: string) {
return str.replace(/\w\S*/g, function (txt) {
return txt.charAt(0).toLowerCase() + txt.substr(1);
return txt.charAt(0).toLowerCase() + txt.substring(1);
});
}

export function normalize(value: any): any {
/**
* normalize will take the resource properties for a specific CloudFormation resource and
* will covert those properties to be compatible with Pulumi properties.
*
* @param value - The resource properties to be normalized
* @param cfnType The CloudFormation resource type being normalized (e.g. AWS::S3::Bucket). If no value
* is provided then property conversion will be done without schema knowledge
* @param pulumiProvider The pulumi provider to read the schema from. If `cfnType` is provided then this defaults
* to PulumiProvider.AWS_NATIVE
* @returns The normalized resource properties
*/
export function normalize(value: any, cfnType?: string, pulumiProvider?: PulumiProvider): any {
if (!value) return value;

if (Array.isArray(value)) {
const result: any[] = [];
for (let i = 0; i < value.length; i++) {
result[i] = normalize(value[i]);
result[i] = normalize(value[i], cfnType);
}
return result;
}
Expand All @@ -41,7 +53,7 @@ export function normalize(value: any): any {

const result: any = {};
Object.entries(value).forEach(([k, v]) => {
result[toSdkName(k)] = normalize(v);
result[toSdkName(k)] = normalizeObject([k], v, cfnType, pulumiProvider);
});
return result;
}
Expand Down Expand Up @@ -95,26 +107,3 @@ export class CdkConstruct extends pulumi.ComponentResource {
this.registerOutputs({});
}
}

export class CdkComponent extends pulumi.ComponentResource {
constructor(name: string, args: (stack: cdk.Stack) => void, opts?: pulumi.CustomResourceOptions) {
super('cdk:index:Component', name, args, opts);

const app = new cdk.App();
const stack = new cdk.Stack(app);
args(stack);

//debugger;
const template = app.synth().getStackByName(stack.stackName).template;
console.debug(`template: ${JSON.stringify(template)}`);
const resources = template.Resources;

Object.entries(resources).forEach(([key, value]) => {
const typeName = (value as any).Type;
const sourceProps = (value as any).Properties;
console.debug(`resource[${key}] Type:${typeName} props: ${sourceProps}`);
opts = opts || { parent: this };
new CfnResource(key, typeName, normalize(sourceProps), [], opts);
});
}
}
Loading

0 comments on commit 1094919

Please sign in to comment.