From 3c75969c50bc61d97dd21bb5be9752e008eaf7c9 Mon Sep 17 00:00:00 2001 From: corymhall <43035978+corymhall@users.noreply.github.com> Date: Fri, 6 Sep 2024 08:34:46 -0400 Subject: [PATCH] Remove mappings to aws classic resources This PR removes the manual mappings to aws classic resources that now exist in aws-native. BREAKING CHANGE: resources that were previously deployed with the `aws` provider are now deployed with the `aws-native` provider. This will cause resource replacement re #142 --- examples/cron-lambda/adapter.ts | 60 ---- examples/cron-lambda/index.ts | 3 +- examples/fargate/adapter.ts | 35 --- examples/fargate/index.ts | 14 +- src/aws-resource-mappings.ts | 517 +------------------------------- tests/cdk-resource.test.ts | 121 ++++++++ 6 files changed, 133 insertions(+), 617 deletions(-) delete mode 100644 examples/cron-lambda/adapter.ts delete mode 100644 examples/fargate/adapter.ts create mode 100644 tests/cdk-resource.test.ts diff --git a/examples/cron-lambda/adapter.ts b/examples/cron-lambda/adapter.ts deleted file mode 100644 index 0ad7827e..00000000 --- a/examples/cron-lambda/adapter.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { CfnElement } from 'aws-cdk-lib'; -import * as pulumi from '@pulumi/pulumi'; -import * as pulumicdk from '@pulumi/cdk'; -import * as aws from '@pulumi/aws'; - -interface target { - arn: pulumi.Input; - id: string; -} - -export function remapCloudControlResource( - 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 } = {}; - const rule = new aws.cloudwatch.EventRule( - logicalId, - { - scheduleExpression: props['scheduleExpression'], - isEnabled: props['state'] == 'ENABLED' ? true : props.State === 'DISABLED' ? false : undefined, - description: props.Description, - eventBusName: props['eventBusName'] ?? undefined, - eventPattern: props['eventPattern'] ?? undefined, - roleArn: props['roleArn'] ?? undefined, - }, - options, - ); - const targets: target[] = props['targets'] ?? []; - for (const t of targets) { - new aws.cloudwatch.EventTarget( - t.id, - { - arn: t.arn, - rule: rule.name, - }, - options, - ); - } - 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, - ); - } - - return undefined; -} diff --git a/examples/cron-lambda/index.ts b/examples/cron-lambda/index.ts index 76df88a7..644fe821 100644 --- a/examples/cron-lambda/index.ts +++ b/examples/cron-lambda/index.ts @@ -5,13 +5,12 @@ import * as aws_lambda from 'aws-cdk-lib/aws-lambda'; import { Duration } from 'aws-cdk-lib'; import * as pulumi from '@pulumi/pulumi'; import * as pulumicdk from '@pulumi/cdk'; -import { remapCloudControlResource } from './adapter'; class LambdaStack extends pulumicdk.Stack { lambdaArn: pulumi.Output; constructor(id: string, options?: pulumicdk.StackOptions) { - super(id, { ...options, remapCloudControlResource }); + super(id, options); // Use the AWS CDK Lambda Function API directly. const lambdaFn = new aws_lambda.Function(this, 'lambda', { diff --git a/examples/fargate/adapter.ts b/examples/fargate/adapter.ts deleted file mode 100644 index 936a5a35..00000000 --- a/examples/fargate/adapter.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { CfnElement } from 'aws-cdk-lib'; -import * as pulumi from '@pulumi/pulumi'; -import * as pulumicdk from '@pulumi/cdk'; -import * as aws from '@pulumi/aws'; -import { debug } from '@pulumi/pulumi/log'; - -export function remapCloudControlResource( - element: CfnElement, - logicalId: string, - typeName: string, - rawProps: any, - options: pulumi.ResourceOptions, -): pulumi.CustomResource | undefined { - // Lower-case the raw cloudformation resource schema property keys. - const props = pulumicdk.interop.normalize(rawProps); - - switch (typeName) { - case 'AWS::ApplicationAutoScaling::ScalingPolicy': - debug(`AWS::ApplicationAutoScaling::ScalingPolicy props: ${JSON.stringify(props)}`); - return new aws.appautoscaling.Policy(logicalId, - { - resourceId: props.resourceId ?? props.scalingTargetId, - scalableDimension: props.scalableDimension ?? "ecs:service:DesiredCount", - serviceNamespace: props.serviceNamespace ?? "ecs", - policyType: props.policyType, - stepScalingPolicyConfiguration: props.stepScalingPolicyConfiguration, - name: props.policyName, - targetTrackingScalingPolicyConfiguration: props.targetTrackingScalingPolicyConfiguration, - }, - options, - ); - default: - return undefined; - } -} diff --git a/examples/fargate/index.ts b/examples/fargate/index.ts index e2489ac1..33defbf4 100644 --- a/examples/fargate/index.ts +++ b/examples/fargate/index.ts @@ -6,14 +6,11 @@ import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns'; -import { remapCloudControlResource } from './adapter'; - class FargateStack extends pulumicdk.Stack { - loadBalancerDNS: pulumi.Output; constructor(id: string, options?: pulumicdk.StackOptions) { - super(id, { ...options, remapCloudControlResource }); + super(id, options); // Create VPC and Fargate Cluster // NOTE: Limit AZs to avoid reaching resource quotas @@ -24,7 +21,7 @@ class FargateStack extends pulumicdk.Stack { const fargateService = new ecs_patterns.NetworkLoadBalancedFargateService(this, 'sample-app', { cluster, taskImageOptions: { - image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample") + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), }, }); @@ -32,7 +29,7 @@ class FargateStack extends pulumicdk.Stack { fargateService.service.connections.securityGroups[0].addIngressRule( ec2.Peer.ipv4(vpc.vpcCidrBlock), ec2.Port.tcp(80), - "allow http inbound from vpc", + 'allow http inbound from vpc', ); // Setup AutoScaling policy @@ -40,7 +37,7 @@ class FargateStack extends pulumicdk.Stack { scaling.scaleOnCpuUtilization('CpuScaling', { targetUtilizationPercent: 50, scaleInCooldown: Duration.seconds(60), - scaleOutCooldown: Duration.seconds(60) + scaleOutCooldown: Duration.seconds(60), }); this.loadBalancerDNS = this.asOutput(fargateService.loadBalancer.loadBalancerDnsName); @@ -48,8 +45,7 @@ class FargateStack extends pulumicdk.Stack { // Finalize the stack and deploy its resources. this.synth(); } -}; +} const stack = new FargateStack('fargatestack'); export const loadBalancerURL = stack.loadBalancerDNS; - diff --git a/src/aws-resource-mappings.ts b/src/aws-resource-mappings.ts index 71a3d88e..ef60dd0b 100644 --- a/src/aws-resource-mappings.ts +++ b/src/aws-resource-mappings.ts @@ -40,6 +40,9 @@ function tags(tags: pulumi.Input[]> | undefined): AwsTags ); } +/** + * Any resource that does not currently exist in CCAPI can be mapped to an aws classic resource. + */ export function mapToAwsResource( element: CfnElement, logicalId: string, @@ -50,10 +53,6 @@ export function mapToAwsResource( const props = normalize(rawProps); switch (typeName) { // ApiGatewayV2 - case 'AWS::ApiGatewayV2::Api': - return new aws.apigatewayv2.Api(logicalId, props, options); - case 'AWS::ApiGatewayV2::Deployment': - return new aws.apigatewayv2.Deployment(logicalId, props, options); case 'AWS::ApiGatewayV2::Integration': return new aws.apigatewayv2.Integration( logicalId, @@ -66,16 +65,6 @@ export function mapToAwsResource( }, options, ); - case 'AWS::ApiGatewayV2::Route': - return new aws.apigatewayv2.Route( - logicalId, - { - ...props, - requestModels: rawProps.RequestModels, - requestParameters: rawProps.RequestParameters, - }, - options, - ); case 'AWS::ApiGatewayV2::Stage': return new aws.apigatewayv2.Stage( logicalId, @@ -95,350 +84,6 @@ export function mapToAwsResource( options, ); - // DynamoDB - case 'AWS::DynamoDB::Table': - return mapDynamoDBTable(element, logicalId, typeName, rawProps, props, options); - - // EC2 - case 'AWS::EC2::EIP': - return new aws.ec2.Eip( - logicalId, - { - instance: props.instanceId, - publicIpv4Pool: props.publicIpv4Pool, - tags: tags(props.tags), - vpc: props.domain ? pulumi.output(props.domain).apply((domain) => domain === 'vpc') : undefined, - }, - options, - ); - case 'AWS::EC2::SecurityGroup': { - debug(`AWS::EC2::SecurityGroup props: ${JSON.stringify(props)}`); - const securityGroup = new aws.ec2.SecurityGroup( - logicalId, - { - description: props.groupDescription, - egress: (props.securityGroupEgress || []).map((e: any) => { - const egress = { - description: e.description, - protocol: e.ipProtocol, - fromPort: e.fromPort, - toPort: e.toPort, - cidrBlocks: e.cidrIp ? [e.cidrIp] : undefined, - ipv6CidrBlocks: e.cidrIpv6 ? [e.cidrIpv6] : undefined, - prefixListIds: e.destinationPrefixListId ? [e.destinationPrefixListId] : undefined, - securityGroups: e.destinationSecurityGroupId || [], - }; - if (egress.fromPort === undefined && egress.toPort === undefined && egress.protocol == '-1') { - egress.fromPort = 0; - egress.toPort = 0; - } - return egress; - }), - ingress: (props.securityGroupIngress || []).map((i: any) => { - return { - description: i.description, - protocol: i.ipProtocol, - fromPort: i.fromPort, - toPort: i.toPort, - cidrBlocks: i.cidrIp ? [i.cidrIp] : undefined, - ipv6CidrBlocks: i.cidrIpv6 ? [i.cidrIpv6] : undefined, - prefixListIds: i.destinationPrefixListId ? [i.destinationPrefixListId] : undefined, - securityGroups: (i.sourceSecurityGroupId || []).concat( - ...(i.sourceSecurityGroupName || []), - ), - }; - }), - tags: tags(props.tags), - vpcId: props.vpcId, - revokeRulesOnDelete: true, // FIXME: is this right? - }, - options, - ); - return { - resource: securityGroup, - attributes: { groupId: securityGroup.id }, - }; - } - case 'AWS::EC2::SecurityGroupEgress': - debug(`AWS::EC2::SecurityGroupEgress props: ${JSON.stringify(props)}`); - return new aws.ec2.SecurityGroupRule( - logicalId, - { - protocol: props.ipProtocol, - fromPort: props.fromPort, - toPort: props.toPort, - sourceSecurityGroupId: props.destinationSecurityGroupId, - securityGroupId: props.groupId, - prefixListIds: props.destinationPrefixListId, - cidrBlocks: props.cidrIp ? [props.cidrIp] : undefined, - ipv6CidrBlocks: props.cidrIpv6 ? [props.cidrIpv6] : undefined, - type: 'egress', - }, - options, - ); - case 'AWS::EC2::SecurityGroupIngress': - debug(`AWS::EC2::SecurityGroupIngress props: ${JSON.stringify(props)}: cidr_blocks: ${props.cidrIp}`); - return new aws.ec2.SecurityGroupRule( - logicalId, - { - protocol: props.ipProtocol, - fromPort: props.fromPort, - toPort: props.toPort, - securityGroupId: props.groupId, - prefixListIds: props.sourcePrefixListId, - sourceSecurityGroupId: props.sourceSecurityGroupId, - cidrBlocks: props.cidrIp ? [props.cidrIp] : undefined, - ipv6CidrBlocks: props.cidrIpv6 ? [props.cidrIpv6] : undefined, - type: 'ingress', - }, - options, - ); - case 'AWS::EC2::VPCGatewayAttachment': - // Create either an internet gateway attachment or a VPC gateway attachment - // depending on the payload. - if (props.vpnGatewayId === undefined) { - return new aws.ec2.InternetGatewayAttachment( - logicalId, - { - internetGatewayId: props.internetGatewayId, - vpcId: props.vpcId, - }, - options, - ); - } - return new aws.ec2.VpnGatewayAttachment( - logicalId, - { - vpcId: props.vpcId, - vpnGatewayId: props.vpnGatewayId, - }, - options, - ); - case 'AWS::ElasticLoadBalancingV2::LoadBalancer': { - debug(`AWS::ElasticLoadBalancingV2::LoadBalancer props: ${JSON.stringify(props)}`); - const lb = new aws.lb.LoadBalancer( - logicalId, - { - ipAddressType: props.ipAddressType, - loadBalancerType: props.type, - securityGroups: props.securityGroups, - subnets: props.subnets, - subnetMappings: props.subnetMappings?.map( - (m: any) => - { - allocationId: m.allocationId, - ipv6Address: m.iPv6Address, - privateIpv4Address: m.privateIPv4Address, - subnetId: m.subnetId, - }, - ), - tags: tags(props.tags), - internal: props.scheme ? props.scheme == 'internal' : false, - }, - options, - ); - return { - resource: lb, - attributes: { dNSName: lb.dnsName }, - }; - } - case 'AWS::ElasticLoadBalancingV2::TargetGroup': { - debug(`AWS::ElasticLoadBalancingV2::TargetGroup props: ${JSON.stringify(props)}`); - const tgAttributes = targetGroupAttributesMap(props.targetGroupAttributes); - debug(`${logicalId} tgAttributes ${JSON.stringify(tgAttributes)}`); - const tg = new aws.lb.TargetGroup( - logicalId, - { - healthCheck: { - enabled: props.healthCheckEnabled, - interval: props.healthCheckIntervalSeconds, - path: props.healthCheckPath, - port: props.healthCheckPort, - protocol: props.healthCheckProtocol, - timeout: props.healthCheckTimeoutSeconds, - matcher: props.matcher ? props.matcher.httpCode || props.matcher.grpcCode : undefined, - healthyThreshold: props.healthyThresholdCount, - }, - // logicalId can be too big and cause autonaming to spill beyond 32 char limit for names - name: props.name ?? (logicalId.length > 24 ? logicalId.slice(-32) : undefined), - port: props.port, - protocol: props.protocol, - protocolVersion: props.protocolVersion, - vpcId: props.vpcId, - tags: tags(props.tags), - targetType: props.targetType, - stickiness: stickiness(tgAttributes), - deregistrationDelay: maybeTargetGroupAttribute( - tgAttributes, - 'deregistration_delay.timeout_seconds', - ), - connectionTermination: maybeTargetGroupAttribute( - tgAttributes, - 'deregistration_delay.connection_termination.enabled', - ), - proxyProtocolV2: maybeTargetGroupAttribute(tgAttributes, 'proxy_protocol_v2.enabled'), - preserveClientIp: maybeTargetGroupAttribute(tgAttributes, 'preserve_client_ip.enabled'), - lambdaMultiValueHeadersEnabled: maybeTargetGroupAttribute( - tgAttributes, - 'lambda.multi_value_headers.enabled', - ), - slowStart: maybeTargetGroupAttribute(tgAttributes, 'slow_start.duration_seconds'), - loadBalancingAlgorithmType: maybeTargetGroupAttribute( - tgAttributes, - 'load_balancing.algorithm.type', - ), - }, - options, - ); - return { - resource: tg, - attributes: { - targetGroupFullName: tg.arnSuffix, - targetGroupName: tg.name, - }, - }; - } - case 'AWS::AutoScaling::AutoScalingGroup': { - debug(`AWS::AutoScaling::AutoScalingGroup props: ${JSON.stringify(props)}`); - return new aws.autoscaling.Group( - logicalId, - { - availabilityZones: props.availabilityZones, - maxSize: parseInt(props.maxSize), - minSize: parseInt(props.minSize), - capacityRebalance: props.capacityRebalance ? JSON.parse(props.capacityRebalance) : undefined, - defaultCooldown: props.cooldown ? parseInt(props.cooldown) : undefined, - desiredCapacity: props.desiredCapacity ? parseInt(props.desiredCapacity) : undefined, - healthCheckGracePeriod: props.healthCheckGracePeriod - ? parseInt(props.healthCheckGracePeriod) - : undefined, - healthCheckType: props.healthCheckType, - launchConfiguration: props.launchConfigurationName, - launchTemplate: props.launchTemplate?.map( - (t: any) => - { - id: t.launchTemplateId, - name: t.launchTemplateName, - version: t.version, - }, - ), - initialLifecycleHooks: props.lifecycleHookSpecificationList?.map( - (s: any) => - { - defaultResult: s.defaultReason, - heartbeatTimeout: s.heartbeatTimeout, - lifecycleTransition: s.lifecycleTransition, - notificationMetadata: s.notificationMetadata, - notificationTargetArn: s.notificationTargetArn, - roleArn: s.roleArn, - name: s.lifeCycleHookName, - }, - ), - loadBalancers: props.loadBalancerNames, - maxInstanceLifetime: props.maxInstanceLifetime, - // mixedInstancesPolicy: FIXME! - protectFromScaleIn: props.newInstancesProtectedFromScaleIn, - placementGroup: props.placementGroup, - serviceLinkedRoleArn: props.serviceLinkedRoleArn, - tags: props.tags?.map( - (m: { key: any; propagateAtLaunch: any; value: any }) => - { - key: m.key, - propagateAtLaunch: m.propagateAtLaunch, - value: m.value, - }, - ), - targetGroupArns: props.targetGroupArns, - terminationPolicies: props.terminationPolicies, - vpcZoneIdentifiers: props.vpcZoneIdentifier, - }, - options, - ); - } - case 'AWS::AutoScaling::ScalingPolicy': { - return new aws.autoscaling.Policy( - logicalId, - { - adjustmentType: props.adjustmentType, - autoscalingGroupName: props.autoScalingGroupName, - cooldown: props.cooldown ? parseInt(props.cooldown) : undefined, - estimatedInstanceWarmup: props.estimatedInstanceWarmup - ? parseInt(props.estimatedInstanceWarmup) - : undefined, - metricAggregationType: props.metricAggregationType, - minAdjustmentMagnitude: props.minAdjustmentMagnitude, - policyType: props.policyType, - predictiveScalingConfiguration: props.predictiveScalingConfiguration, - scalingAdjustment: props.scalingAdjustment, - stepAdjustments: props.stepAdjustments, - targetTrackingConfiguration: props.targetTrackingConfiguration, - }, - options, - ); - } - case 'AWS::EC2::Route': - return new aws.ec2.Route( - logicalId, - { - routeTableId: props.routeTableId, - carrierGatewayId: props.carrierGatewayId, - destinationCidrBlock: props.destinationCidrBlock, - destinationIpv6CidrBlock: props.destinationIpv6CidrBlock, - egressOnlyGatewayId: props.egressOnlyInternetGatewayId, - gatewayId: props.gatewayId, - localGatewayId: props.localGatewayId, - natGatewayId: props.natGatewayId, - networkInterfaceId: props.networkInterfaceId, - transitGatewayId: props.transitGatewayId, - vpcEndpointId: props.vpcEndpointId, - vpcPeeringConnectionId: props.vpcPeeringConnectionId, - }, - options, - ); - case 'AWS::EC2::NatGateway': - return new aws.ec2.NatGateway( - logicalId, - { - subnetId: props.subnetId, - allocationId: props.allocationId, - connectivityType: props.connectivityType, - tags: tags(props.tags), - }, - options, - ); - case 'AWS::ApplicationAutoScaling::ScalableTarget': { - const target = new aws.appautoscaling.Target( - logicalId, - { - maxCapacity: props.maxCapacity, - minCapacity: props.minCapacity, - resourceId: props.resourceId, - roleArn: props.roleArn, - scalableDimension: props.scalableDimension, - serviceNamespace: props.serviceNamespace, - }, - options, - ); - props.ScheduledActions?.map( - (action: any) => - new aws.appautoscaling.ScheduledAction( - logicalId + action.scheduledActionName, - { - resourceId: target.resourceId, - scalableDimension: target.scalableDimension, - scalableTargetAction: action.scalableTargetAction, - schedule: action.schedule, - serviceNamespace: target.serviceNamespace, - startTime: action.startTime, - endTime: action.endTime, - timezone: action.timezone, - name: action.scheduledActionName, - }, - options, - ), - ); - return target; - } // IAM case 'AWS::IAM::Policy': { const policy = new aws.iam.Policy( @@ -451,7 +96,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], @@ -461,7 +106,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], @@ -471,7 +116,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], @@ -484,157 +129,7 @@ export function mapToAwsResource( return policy; } - // Lambda - case 'AWS::Lambda::Permission': - // TODO: throw on the presence of functionUrlAuthType / principalOrgId? - return new aws.lambda.Permission( - logicalId, - { - action: props.action, - eventSourceToken: props.eventSourceToken, - function: props.functionName, - principal: props.principal, - sourceAccount: props.sourceAccount, - sourceArn: props.sourceArn, - statementId: logicalId, - }, - options, - ); - - // S3 - case 'AWS::S3::BucketPolicy': - return new aws.s3.BucketPolicy( - logicalId, - { - bucket: rawProps.Bucket, - policy: rawProps.PolicyDocument, - }, - options, - ); - default: return undefined; } } - -function mapDynamoDBTable( - element: CfnElement, - logicalId: string, - typeName: string, - rawProps: any, - props: any, - options: pulumi.ResourceOptions, -): aws.dynamodb.Table { - function hashKey(schema: any): string | undefined { - return schema.find((k: any) => k.keyType === 'HASH')?.attributeName; - } - - function rangeKey(schema: any): string | undefined { - return schema.find((k: any) => k.keyType === 'RANGE')?.attributeName; - } - - const attributes = props.attributeDefinitions?.map((attr: any) => ({ - name: attr.attributeName, - type: attr.attributeType, - })); - - const globalSecondaryIndexes = props.globalSecondaryIndexes?.map((index: any) => ({ - hashKey: hashKey(index.keySchema), - name: index.indexName, - nonKeyAttributes: props.projection.nonKeyAttributes, - projectionType: props.projection.projectionType, - rangeKey: rangeKey(index.keySchema), - readCapacity: props.provisionedThroughput?.readCapacityUnits, - writeCapacity: props.provisionedThroughput?.writeCapacityUnits, - })); - - const localSecondaryIndexes = props.localSecondaryIndexes?.map((index: any) => ({ - name: index.indexName, - nonKeyAttributes: index.projection.nonKeyAttributes, - projectionType: index.projection.projectionType, - rangeKey: rangeKey(index.keySchema), - })); - - const pointInTimeRecovery = maybe(props.pointInTimeRecoverySpecification, (spec) => ({ - enabled: spec.pointInTimeRecoveryEnabled, - })); - - const serverSideEncryption = maybe(props.sSESpecification, (spec) => ({ - enabled: spec.sSEEnabled, - kmsKeyArn: spec.kMSMasterKeyId, - })); - - return new aws.dynamodb.Table( - logicalId, - { - attributes: attributes, - billingMode: props.billingMode, - globalSecondaryIndexes: globalSecondaryIndexes, - hashKey: hashKey(props.keySchema), - localSecondaryIndexes: localSecondaryIndexes, - name: props.tableName, - pointInTimeRecovery: pointInTimeRecovery, - rangeKey: rangeKey(props.keySchema), - readCapacity: props.provisionedThroughput?.readCapacityUnits, - serverSideEncryption: serverSideEncryption, - streamEnabled: props.streamSpecification !== undefined, - streamViewType: props.streamSpecification?.streamViewType, - tableClass: props.tableClass, - tags: tags(props.tags), - ttl: props.timeToLiveSpecification, - writeCapacity: props.provisionedThroughput?.writeCapacityUnits, - }, - options, - ); -} - -function stickiness(targetGroupAttributes: any): pulumi.Input | undefined { - if (targetGroupAttributes === undefined) { - return undefined; - } - - const enabled = targetGroupAttributes['stickiness.enabled'] - ? JSON.parse(targetGroupAttributes['stickiness.enabled']) - : false; - if (!enabled) { - return undefined; - } - - let cookieDuration = undefined; - if ('stickiness.app_cookie.duration_seconds' in targetGroupAttributes) { - cookieDuration = targetGroupAttributes['stickiness.app_cookie.duration_seconds']; - } else if ('stickiness.lb_cookie.duration_seconds' in targetGroupAttributes) { - cookieDuration = targetGroupAttributes['stickiness.lb_cookie.duration_seconds']; - } - return { - enabled: enabled, - type: maybeTargetGroupAttribute(targetGroupAttributes, 'stickiness.type'), - cookieName: maybeTargetGroupAttribute(targetGroupAttributes, 'stickiness.app_cookie.cookie_name'), - cookieDuration: cookieDuration, - }; -} - -function maybeTargetGroupAttribute(targetGroupAttributes: any, key: string): any { - if (targetGroupAttributes === undefined) { - return undefined; - } - - let val = undefined; - if (key in targetGroupAttributes) { - val = targetGroupAttributes[key]; - } - return val; -} - -function targetGroupAttributesMap(targetGroupAttributes: any) { - if (targetGroupAttributes === undefined) { - return undefined; - } - - const attrsMap: { [name: string]: any } = {}; - const attrs = targetGroupAttributes as Array; - for (const attr of attrs) { - attrsMap[attr.key] = attr.value; - } - return attrsMap; -} diff --git a/tests/cdk-resource.test.ts b/tests/cdk-resource.test.ts new file mode 100644 index 00000000..4f1ee6b1 --- /dev/null +++ b/tests/cdk-resource.test.ts @@ -0,0 +1,121 @@ +import * as pulumi from '@pulumi/pulumi'; +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import { TableArgs } from '@pulumi/aws-native/dynamodb'; +import { Stack } from '../src/stack'; +import { Construct } from 'constructs'; +import { MockCallArgs, MockResourceArgs } from '@pulumi/pulumi/runtime'; +import { Key } from 'aws-cdk-lib/aws-kms'; + +function setMocks(assertFn: (args: MockResourceArgs) => void) { + pulumi.runtime.setMocks({ + call: (_args: MockCallArgs) => { + return {}; + }, + newResource: (args: MockResourceArgs): { id: string; state: any } => { + assertFn(args); + return { + id: args.name + '_id', + state: args.inputs, + }; + }, + }); +} + +function testStack(fn: (scope: Construct) => void) { + class TestStack extends Stack { + constructor(id: string) { + super(id); + + fn(this); + + this.synth(); + } + } + + new TestStack('teststack'); +} + +describe('CDK Construct tests', () => { + // DynamoDB table was previously mapped to the `aws` provider + // otherwise this level of testing wouldn't be necessary. + // We also don't need to do this type of testing for _every_ resource + test('dynamodb table', () => { + setMocks((args) => { + if (args.type === 'aws-native:dynamodb:Table') { + expect(args.inputs).toEqual({ + keySchema: [ + { attributeName: 'pk', keyType: 'HASH' }, + { attributeName: 'sort', keyType: 'RANGE' }, + ], + sseSpecification: { + kmsMasterKeyId: 'arn:aws:kms:us-west-2:123456789012:key/abcdefg', + sseEnabled: true, + sseType: 'KMS', + }, + attributeDefinitions: [ + { attributeName: 'pk', attributeType: 'S' }, + { attributeName: 'sort', attributeType: 'S' }, + { attributeName: 'lsiSort', attributeType: 'S' }, + { attributeName: 'gsiKey', attributeType: 'S' }, + ], + provisionedThroughput: { + readCapacityUnits: 5, + writeCapacityUnits: 5, + }, + globalSecondaryIndexes: [ + { + provisionedThroughput: { + readCapacityUnits: 5, + writeCapacityUnits: 5, + }, + indexName: 'gsi', + keySchema: [{ attributeName: 'gsiKey', keyType: 'HASH' }], + projection: { + projectionType: 'ALL', + }, + }, + ], + localSecondaryIndexes: [ + { + projection: { projectionType: 'ALL' }, + keySchema: [ + { attributeName: 'pk', keyType: 'HASH' }, + { attributeName: 'lsiSort', keyType: 'RANGE' }, + ], + indexName: 'lsi', + }, + ], + } as TableArgs); + } + }); + testStack((scope) => { + const key = Key.fromKeyArn(scope, 'key', 'arn:aws:kms:us-west-2:123456789012:key/abcdefg'); + const table = new dynamodb.Table(scope, 'Table', { + encryption: dynamodb.TableEncryption.CUSTOMER_MANAGED, + encryptionKey: key, + sortKey: { + name: 'sort', + type: dynamodb.AttributeType.STRING, + }, + partitionKey: { + name: 'pk', + type: dynamodb.AttributeType.STRING, + }, + }); + table.addLocalSecondaryIndex({ + indexName: 'lsi', + sortKey: { + type: dynamodb.AttributeType.STRING, + name: 'lsiSort', + }, + }); + table.addGlobalSecondaryIndex({ + indexName: 'gsi', + partitionKey: { + name: 'gsiKey', + type: dynamodb.AttributeType.STRING, + }, + }); + }); + }); +});