diff --git a/src/constructs/autoscaling/asg.test.ts b/src/constructs/autoscaling/asg.test.ts index e7faac019d..22078eb44d 100644 --- a/src/constructs/autoscaling/asg.test.ts +++ b/src/constructs/autoscaling/asg.test.ts @@ -6,8 +6,8 @@ import { ApplicationProtocol } from "@aws-cdk/aws-elasticloadbalancingv2"; import { Stack } from "@aws-cdk/core"; import { Stage } from "../../constants"; import { TrackingTag } from "../../constants/library-info"; -import type { SynthedStack } from "../../utils/test"; -import { alphabeticalTags, simpleGuStackForTesting } from "../../utils/test"; +import type { Resource, SynthedStack } from "../../utils/test"; +import { alphabeticalTags, findResourceByTypeAndLogicalId, simpleGuStackForTesting } from "../../utils/test"; import { GuSecurityGroup } from "../ec2"; import { GuApplicationTargetGroup } from "../loadbalancing"; import type { GuAutoScalingGroupProps } from "./asg"; @@ -139,12 +139,12 @@ describe("The GuAutoScalingGroup", () => { }); test("adds any target groups passed through props", () => { - const stack = simpleGuStackForTesting(); + const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); const targetGroup = new GuApplicationTargetGroup(stack, "TargetGroup", { vpc: vpc, protocol: ApplicationProtocol.HTTP, - overrideId: true, + existingLogicalId: "MyTargetGroup", }); new GuAutoScalingGroup(stack, "AutoscalingGroup", { @@ -155,7 +155,7 @@ describe("The GuAutoScalingGroup", () => { expect(stack).toHaveResource("AWS::AutoScaling::AutoScalingGroup", { TargetGroupARNs: [ { - Ref: "TargetGroup", + Ref: "MyTargetGroup", }, ], }); @@ -165,9 +165,9 @@ describe("The GuAutoScalingGroup", () => { const app = "Testing"; const stack = simpleGuStackForTesting(); - const securityGroup = new GuSecurityGroup(stack, "SecurityGroup", { vpc, overrideId: true, app }); - const securityGroup1 = new GuSecurityGroup(stack, "SecurityGroup1", { vpc, overrideId: true, app }); - const securityGroup2 = new GuSecurityGroup(stack, "SecurityGroup2", { vpc, overrideId: true, app }); + const securityGroup = new GuSecurityGroup(stack, "SecurityGroup", { vpc, app }); + const securityGroup1 = new GuSecurityGroup(stack, "SecurityGroup1", { vpc, app }); + const securityGroup2 = new GuSecurityGroup(stack, "SecurityGroup2", { vpc, app }); new GuAutoScalingGroup(stack, "AutoscalingGroup", { ...defaultProps, @@ -180,13 +180,13 @@ describe("The GuAutoScalingGroup", () => { "Fn::GetAtt": [`GuHttpsEgressSecurityGroup${app}89CDDA4B`, "GroupId"], }, { - "Fn::GetAtt": [`SecurityGroup${app}`, "GroupId"], + "Fn::GetAtt": [`SecurityGroup${app}A32D34F9`, "GroupId"], }, { - "Fn::GetAtt": [`SecurityGroup1${app}`, "GroupId"], + "Fn::GetAtt": [`SecurityGroup1${app}CA3A17A4`, "GroupId"], }, { - "Fn::GetAtt": [`SecurityGroup2${app}`, "GroupId"], + "Fn::GetAtt": [`SecurityGroup2${app}6436C75B`, "GroupId"], }, ], }); @@ -194,19 +194,27 @@ describe("The GuAutoScalingGroup", () => { test("does not include the UpdatePolicy property", () => { const stack = simpleGuStackForTesting(); - new GuAutoScalingGroup(stack, "AutoscalingGroup", { ...defaultProps, overrideId: true }); + new GuAutoScalingGroup(stack, "AutoscalingGroup", { ...defaultProps }); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources.AutoscalingGroup)).not.toContain("UpdatePolicy"); - }); + expect(stack).toHaveResourceOfTypeAndLogicalId("AWS::AutoScaling::AutoScalingGroup", /AutoscalingGroup.+/); - test("overrides the id with the overrideId prop set to true", () => { - const stack = simpleGuStackForTesting(); - new GuAutoScalingGroup(stack, "AutoscalingGroup", { ...defaultProps, overrideId: true }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- the `toHaveResourceOfTypeAndLogicalId` line above confirms `asgResource` will not be `undefined` + const asgResource: Resource = findResourceByTypeAndLogicalId( + stack, + "AWS::AutoScaling::AutoScalingGroup", + /AutoscalingGroup.+/ + )!; - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; + // This is checking the properties of the ASG resource + // TODO improve the syntax + expect(Object.keys(Object.values(asgResource)[0])).not.toContain("UpdatePolicy"); + }); + + test("overrides the logicalId when existingLogicalId is set in a migrating stack", () => { + const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); + new GuAutoScalingGroup(stack, "AutoscalingGroup", { ...defaultProps, existingLogicalId: "MyASG" }); - expect(Object.keys(json.Resources)).toContain("AutoscalingGroup"); + expect(stack).toHaveResourceOfTypeAndLogicalId("AWS::AutoScaling::AutoScalingGroup", "MyASG"); }); test("does not override the id by default", () => { diff --git a/src/constructs/autoscaling/asg.ts b/src/constructs/autoscaling/asg.ts index 56095b07e5..c7d7623189 100644 --- a/src/constructs/autoscaling/asg.ts +++ b/src/constructs/autoscaling/asg.ts @@ -7,6 +7,8 @@ import { Stage } from "../../constants"; import type { GuStack } from "../core"; import { GuAmiParameter, GuInstanceTypeParameter } from "../core"; import { AppIdentity } from "../core/identity"; +import { GuMigratingResource } from "../core/migrating"; +import type { GuStatefulConstruct } from "../core/migrating"; import { GuHttpsEgressSecurityGroup } from "../ec2"; // Since we want to override the types of what gets passed in for the below props, @@ -25,7 +27,8 @@ export interface GuAutoScalingGroupProps | "desiredCapacity" | "securityGroup" >, - AppIdentity { + AppIdentity, + GuMigratingResource { stageDependentProps: GuStageDependentAsgProps; instanceType?: InstanceType; imageId?: GuAmiParameter; @@ -33,7 +36,6 @@ export interface GuAutoScalingGroupProps userData: UserData | string; additionalSecurityGroups?: ISecurityGroup[]; targetGroup?: ApplicationTargetGroup; - overrideId?: boolean; } type GuStageDependentAsgProps = Record; @@ -77,7 +79,9 @@ function wireStageDependentProps(stack: GuStack, stageDependentProps: GuStageDep }; } -export class GuAutoScalingGroup extends AutoScalingGroup { +export class GuAutoScalingGroup extends AutoScalingGroup implements GuStatefulConstruct { + public readonly isStatefulConstruct: true; + constructor(scope: GuStack, id: string, props: GuAutoScalingGroupProps) { const userData = props.userData instanceof UserData ? props.userData : UserData.custom(props.userData); @@ -101,10 +105,11 @@ export class GuAutoScalingGroup extends AutoScalingGroup { // Do not use the default AWS security group which allows egress on any port. // Favour HTTPS only egress rules by default. - securityGroup: GuHttpsEgressSecurityGroup.forVpc(scope, props), + securityGroup: GuHttpsEgressSecurityGroup.forVpc(scope, { app: props.app, vpc: props.vpc }), }; super(scope, AppIdentity.suffixText(props, id), mergedProps); + this.isStatefulConstruct = true; mergedProps.targetGroup && this.attachToApplicationTargetGroup(mergedProps.targetGroup); @@ -116,8 +121,7 @@ export class GuAutoScalingGroup extends AutoScalingGroup { // { UpdatePolicy: { autoScalingScheduledAction: { IgnoreUnmodifiedGroupSizeProperties: true }} cfnAsg.addDeletionOverride("UpdatePolicy"); - if (mergedProps.overrideId) cfnAsg.overrideLogicalId(id); - + GuMigratingResource.setLogicalId(this, scope, props); AppIdentity.taggedConstruct(props, this); } } diff --git a/src/constructs/ec2/security-groups/base.test.ts b/src/constructs/ec2/security-groups/base.test.ts index 130ffc7c07..963025da3d 100644 --- a/src/constructs/ec2/security-groups/base.test.ts +++ b/src/constructs/ec2/security-groups/base.test.ts @@ -1,4 +1,5 @@ import "@aws-cdk/assert/jest"; +import "../../../utils/test/jest"; import { SynthUtils } from "@aws-cdk/assert"; import { Peer, Port, Vpc } from "@aws-cdk/aws-ec2"; import { Stack } from "@aws-cdk/core"; @@ -13,13 +14,11 @@ describe("The GuSecurityGroup class", () => { publicSubnetIds: [""], }); - it("overrides the id if the prop is set to true", () => { - const stack = simpleGuStackForTesting(); - - new GuSecurityGroup(stack, "TestSecurityGroup", { vpc, overrideId: true, app: "testing" }); + it("overrides the logicalId when existingLogicalId is set in a migrating stack", () => { + const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); + new GuSecurityGroup(stack, "TestSecurityGroup", { vpc, existingLogicalId: "TestSG", app: "testing" }); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).toContain("TestSecurityGroupTesting"); + expect(stack).toHaveResourceOfTypeAndLogicalId("AWS::EC2::SecurityGroup", "TestSG"); }); it("does not overrides the id if the prop is set to false", () => { diff --git a/src/constructs/ec2/security-groups/base.ts b/src/constructs/ec2/security-groups/base.ts index 0f3d402e43..4cd9b74cbc 100644 --- a/src/constructs/ec2/security-groups/base.ts +++ b/src/constructs/ec2/security-groups/base.ts @@ -1,7 +1,8 @@ -import type { CfnSecurityGroup, IPeer, SecurityGroupProps } from "@aws-cdk/aws-ec2"; +import type { IPeer, SecurityGroupProps } from "@aws-cdk/aws-ec2"; import { Peer, Port, SecurityGroup } from "@aws-cdk/aws-ec2"; import type { GuStack } from "../../core"; import { AppIdentity } from "../../core/identity"; +import { GuMigratingResource } from "../../core/migrating"; /** * A way to describe an ingress or egress rule for a security group. @@ -28,8 +29,7 @@ export interface SecurityGroupAccessRule { description: string; } -export interface GuBaseSecurityGroupProps extends SecurityGroupProps { - overrideId?: boolean; +export interface GuBaseSecurityGroupProps extends SecurityGroupProps, GuMigratingResource { ingresses?: SecurityGroupAccessRule[]; egresses?: SecurityGroupAccessRule[]; } @@ -49,10 +49,7 @@ export interface GuSecurityGroupProps extends GuBaseSecurityGroupProps, AppIdent export abstract class GuBaseSecurityGroup extends SecurityGroup { protected constructor(scope: GuStack, id: string, props: GuBaseSecurityGroupProps) { super(scope, id, props); - - if (props.overrideId) { - (this.node.defaultChild as CfnSecurityGroup).overrideLogicalId(id); - } + GuMigratingResource.setLogicalId(this, scope, props); props.ingresses?.forEach(({ range, port, description }) => { const connection: Port = typeof port === "number" ? Port.tcp(port) : port; diff --git a/src/constructs/iam/policies/base-policy.ts b/src/constructs/iam/policies/base-policy.ts index e345f89561..3abca361bb 100644 --- a/src/constructs/iam/policies/base-policy.ts +++ b/src/constructs/iam/policies/base-policy.ts @@ -1,21 +1,16 @@ -import type { CfnPolicy, PolicyProps } from "@aws-cdk/aws-iam"; +import type { PolicyProps } from "@aws-cdk/aws-iam"; import { Effect, Policy, PolicyStatement } from "@aws-cdk/aws-iam"; import type { GuStack } from "../../core"; +import { GuMigratingResource } from "../../core/migrating"; -export interface GuPolicyProps extends PolicyProps { - overrideId?: boolean; -} +export interface GuPolicyProps extends PolicyProps, GuMigratingResource {} export type GuNoStatementsPolicyProps = Omit; export abstract class GuPolicy extends Policy { protected constructor(scope: GuStack, id: string, props: GuPolicyProps) { super(scope, id, props); - - if (props.overrideId) { - const child = this.node.defaultChild as CfnPolicy; - child.overrideLogicalId(id); - } + GuMigratingResource.setLogicalId(this, scope, props); } } diff --git a/src/constructs/iam/roles/__snapshots__/instance-role.test.ts.snap b/src/constructs/iam/roles/__snapshots__/instance-role.test.ts.snap index 08b07507e8..d7dccf35cd 100644 --- a/src/constructs/iam/roles/__snapshots__/instance-role.test.ts.snap +++ b/src/constructs/iam/roles/__snapshots__/instance-role.test.ts.snap @@ -39,7 +39,7 @@ Object { "PolicyName": "describe-ec2-policy", "Roles": Array [ Object { - "Ref": "InstanceRoleTesting", + "Ref": "InstanceRoleTestingCB7BD146", }, ], }, @@ -60,7 +60,7 @@ Object { "PolicyName": "GetConfigPolicy6F934A1C", "Roles": Array [ Object { - "Ref": "InstanceRoleTesting", + "Ref": "InstanceRoleTestingCB7BD146", }, ], }, @@ -96,13 +96,13 @@ Object { "PolicyName": "GetDistributablePolicyTestingF9D43A3E", "Roles": Array [ Object { - "Ref": "InstanceRoleTesting", + "Ref": "InstanceRoleTestingCB7BD146", }, ], }, "Type": "AWS::IAM::Policy", }, - "InstanceRoleTesting": Object { + "InstanceRoleTestingCB7BD146": Object { "Properties": Object { "AssumeRolePolicyDocument": Object { "Statement": Array [ @@ -184,7 +184,7 @@ Object { "PolicyName": "parameter-store-read-policy", "Roles": Array [ Object { - "Ref": "InstanceRoleTesting", + "Ref": "InstanceRoleTestingCB7BD146", }, ], }, @@ -220,7 +220,7 @@ Object { "PolicyName": "ssm-run-command-policy", "Roles": Array [ Object { - "Ref": "InstanceRoleTesting", + "Ref": "InstanceRoleTestingCB7BD146", }, ], }, @@ -274,10 +274,10 @@ Object { "PolicyName": "describe-ec2-policy", "Roles": Array [ Object { - "Ref": "InstanceRoleMyfirstapp", + "Ref": "InstanceRoleMyfirstapp5C11A22B", }, Object { - "Ref": "InstanceRoleMysecondapp", + "Ref": "InstanceRoleMysecondapp48DD15D7", }, ], }, @@ -313,7 +313,7 @@ Object { "PolicyName": "GetDistributablePolicyMyfirstappB56CBAB1", "Roles": Array [ Object { - "Ref": "InstanceRoleMyfirstapp", + "Ref": "InstanceRoleMyfirstapp5C11A22B", }, ], }, @@ -349,7 +349,7 @@ Object { "PolicyName": "GetDistributablePolicyMysecondapp5096BFDB", "Roles": Array [ Object { - "Ref": "InstanceRoleMysecondapp", + "Ref": "InstanceRoleMysecondapp48DD15D7", }, ], }, @@ -391,16 +391,16 @@ Object { "PolicyName": "GuLogShippingPolicy981BFE5A", "Roles": Array [ Object { - "Ref": "InstanceRoleMyfirstapp", + "Ref": "InstanceRoleMyfirstapp5C11A22B", }, Object { - "Ref": "InstanceRoleMysecondapp", + "Ref": "InstanceRoleMysecondapp48DD15D7", }, ], }, "Type": "AWS::IAM::Policy", }, - "InstanceRoleMyfirstapp": Object { + "InstanceRoleMyfirstapp5C11A22B": Object { "Properties": Object { "AssumeRolePolicyDocument": Object { "Statement": Array [ @@ -448,7 +448,7 @@ Object { }, "Type": "AWS::IAM::Role", }, - "InstanceRoleMysecondapp": Object { + "InstanceRoleMysecondapp48DD15D7": Object { "Properties": Object { "AssumeRolePolicyDocument": Object { "Statement": Array [ @@ -530,7 +530,7 @@ Object { "PolicyName": "parameter-store-read-policy", "Roles": Array [ Object { - "Ref": "InstanceRoleMyfirstapp", + "Ref": "InstanceRoleMyfirstapp5C11A22B", }, ], }, @@ -570,7 +570,7 @@ Object { "PolicyName": "parameter-store-read-policy", "Roles": Array [ Object { - "Ref": "InstanceRoleMysecondapp", + "Ref": "InstanceRoleMysecondapp48DD15D7", }, ], }, @@ -606,10 +606,10 @@ Object { "PolicyName": "ssm-run-command-policy", "Roles": Array [ Object { - "Ref": "InstanceRoleMyfirstapp", + "Ref": "InstanceRoleMyfirstapp5C11A22B", }, Object { - "Ref": "InstanceRoleMysecondapp", + "Ref": "InstanceRoleMysecondapp48DD15D7", }, ], }, @@ -663,7 +663,7 @@ Object { "PolicyName": "describe-ec2-policy", "Roles": Array [ Object { - "Ref": "InstanceRoleTesting", + "Ref": "InstanceRoleTestingCB7BD146", }, ], }, @@ -699,7 +699,7 @@ Object { "PolicyName": "GetDistributablePolicyTestingF9D43A3E", "Roles": Array [ Object { - "Ref": "InstanceRoleTesting", + "Ref": "InstanceRoleTestingCB7BD146", }, ], }, @@ -741,13 +741,13 @@ Object { "PolicyName": "GuLogShippingPolicy981BFE5A", "Roles": Array [ Object { - "Ref": "InstanceRoleTesting", + "Ref": "InstanceRoleTestingCB7BD146", }, ], }, "Type": "AWS::IAM::Policy", }, - "InstanceRoleTesting": Object { + "InstanceRoleTestingCB7BD146": Object { "Properties": Object { "AssumeRolePolicyDocument": Object { "Statement": Array [ @@ -829,7 +829,7 @@ Object { "PolicyName": "parameter-store-read-policy", "Roles": Array [ Object { - "Ref": "InstanceRoleTesting", + "Ref": "InstanceRoleTestingCB7BD146", }, ], }, @@ -865,7 +865,7 @@ Object { "PolicyName": "ssm-run-command-policy", "Roles": Array [ Object { - "Ref": "InstanceRoleTesting", + "Ref": "InstanceRoleTestingCB7BD146", }, ], }, @@ -914,7 +914,7 @@ Object { "PolicyName": "describe-ec2-policy", "Roles": Array [ Object { - "Ref": "InstanceRoleTesting", + "Ref": "InstanceRoleTestingCB7BD146", }, ], }, @@ -950,13 +950,13 @@ Object { "PolicyName": "GetDistributablePolicyTestingF9D43A3E", "Roles": Array [ Object { - "Ref": "InstanceRoleTesting", + "Ref": "InstanceRoleTestingCB7BD146", }, ], }, "Type": "AWS::IAM::Policy", }, - "InstanceRoleTesting": Object { + "InstanceRoleTestingCB7BD146": Object { "Properties": Object { "AssumeRolePolicyDocument": Object { "Statement": Array [ @@ -1038,7 +1038,7 @@ Object { "PolicyName": "parameter-store-read-policy", "Roles": Array [ Object { - "Ref": "InstanceRoleTesting", + "Ref": "InstanceRoleTestingCB7BD146", }, ], }, @@ -1074,7 +1074,7 @@ Object { "PolicyName": "ssm-run-command-policy", "Roles": Array [ Object { - "Ref": "InstanceRoleTesting", + "Ref": "InstanceRoleTestingCB7BD146", }, ], }, diff --git a/src/constructs/iam/roles/instance-role.ts b/src/constructs/iam/roles/instance-role.ts index 00e22d8e32..041c4731a0 100644 --- a/src/constructs/iam/roles/instance-role.ts +++ b/src/constructs/iam/roles/instance-role.ts @@ -19,7 +19,6 @@ interface GuInstanceRoleProps extends AppIdentity { export class GuInstanceRole extends GuRole { constructor(scope: GuStack, props: GuInstanceRoleProps) { super(scope, AppIdentity.suffixText(props, "InstanceRole"), { - overrideId: true, path: "/", assumedBy: new ServicePrincipal("ec2.amazonaws.com"), }); diff --git a/src/constructs/iam/roles/roles.test.ts b/src/constructs/iam/roles/roles.test.ts index e7dfd31c67..6828fece6f 100644 --- a/src/constructs/iam/roles/roles.test.ts +++ b/src/constructs/iam/roles/roles.test.ts @@ -1,21 +1,21 @@ -import { SynthUtils } from "@aws-cdk/assert"; import "@aws-cdk/assert/jest"; +import "../../../utils/test/jest"; +import { SynthUtils } from "@aws-cdk/assert"; import { ServicePrincipal } from "@aws-cdk/aws-iam"; import { simpleGuStackForTesting } from "../../../utils/test"; import type { SynthedStack } from "../../../utils/test"; import { GuRole } from "./roles"; describe("The GuRole class", () => { - it("overrides id if prop set to true", () => { - const stack = simpleGuStackForTesting(); + it("overrides the logicalId when existingLogicalId is set in a migrating stack", () => { + const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); new GuRole(stack, "TestRole", { - overrideId: true, + existingLogicalId: "MyRole", assumedBy: new ServicePrincipal("ec2.amazonaws.com"), }); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).toContain("TestRole"); + expect(stack).toHaveResourceOfTypeAndLogicalId("AWS::IAM::Role", "MyRole"); }); it("does not override id if prop set to false", () => { diff --git a/src/constructs/iam/roles/roles.ts b/src/constructs/iam/roles/roles.ts index 5548b08292..5f6cc055bc 100644 --- a/src/constructs/iam/roles/roles.ts +++ b/src/constructs/iam/roles/roles.ts @@ -1,21 +1,18 @@ import type { CfnRole, RoleProps } from "@aws-cdk/aws-iam"; import { Role } from "@aws-cdk/aws-iam"; import type { GuStack } from "../../core"; +import { GuMigratingResource } from "../../core/migrating"; -export interface GuRoleProps extends RoleProps { - overrideId?: boolean; -} +export interface GuRoleProps extends RoleProps, GuMigratingResource {} export class GuRole extends Role { private child: CfnRole; constructor(scope: GuStack, id: string, props: GuRoleProps) { super(scope, id, props); + GuMigratingResource.setLogicalId(this, scope, props); this.child = this.node.defaultChild as CfnRole; - if (props.overrideId) { - this.child.overrideLogicalId(id); - } } get ref(): string { diff --git a/src/constructs/kinesis/kinesis-stream.test.ts b/src/constructs/kinesis/kinesis-stream.test.ts index 3814c583ff..17d769052b 100644 --- a/src/constructs/kinesis/kinesis-stream.test.ts +++ b/src/constructs/kinesis/kinesis-stream.test.ts @@ -1,6 +1,5 @@ import "@aws-cdk/assert/jest"; -import { SynthUtils } from "@aws-cdk/assert/lib/synth-utils"; -import type { SynthedStack } from "../../utils/test"; +import "../../utils/test/jest"; import { simpleGuStackForTesting } from "../../utils/test"; import { GuKinesisStream } from "./kinesis-stream"; @@ -8,14 +7,14 @@ describe("The GuKinesisStream construct", () => { it("should not override the id by default", () => { const stack = simpleGuStackForTesting(); new GuKinesisStream(stack, "my-kinesis-stream"); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).not.toContain("my-kinesis-stream"); + + expect(stack).not.toHaveResourceOfTypeAndLogicalId("AWS::Kinesis::Stream", "my-kinesis-stream"); }); - it("should override the id with the overrideId prop set to true", () => { - const stack = simpleGuStackForTesting(); - new GuKinesisStream(stack, "my-kinesis-stream", { overrideId: true }); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).toContain("my-kinesis-stream"); + it("overrides the logicalId when existingLogicalId is set in a migrating stack", () => { + const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); + new GuKinesisStream(stack, "my-kinesis-stream", { existingLogicalId: "MyStream" }); + + expect(stack).toHaveResourceOfTypeAndLogicalId("AWS::Kinesis::Stream", "MyStream"); }); }); diff --git a/src/constructs/kinesis/kinesis-stream.ts b/src/constructs/kinesis/kinesis-stream.ts index 7b41b47f74..13d982fc9e 100644 --- a/src/constructs/kinesis/kinesis-stream.ts +++ b/src/constructs/kinesis/kinesis-stream.ts @@ -1,15 +1,13 @@ -import type { CfnStream, StreamProps } from "@aws-cdk/aws-kinesis"; +import type { StreamProps } from "@aws-cdk/aws-kinesis"; import { Stream } from "@aws-cdk/aws-kinesis"; import type { GuStack } from "../core"; +import { GuMigratingResource } from "../core/migrating"; -export interface GuKinesisStreamProps extends StreamProps { - overrideId?: boolean; -} +export interface GuKinesisStreamProps extends StreamProps, GuMigratingResource {} export class GuKinesisStream extends Stream { constructor(scope: GuStack, id: string, props?: GuKinesisStreamProps) { super(scope, id, props); - const cfnKinesisStream = this.node.defaultChild as CfnStream; - if (props?.overrideId) cfnKinesisStream.overrideLogicalId(id); + props && GuMigratingResource.setLogicalId(this, scope, props); } } diff --git a/src/constructs/loadbalancing/alb.test.ts b/src/constructs/loadbalancing/alb.test.ts index ab5faf2590..31e48c3ca1 100644 --- a/src/constructs/loadbalancing/alb.test.ts +++ b/src/constructs/loadbalancing/alb.test.ts @@ -1,4 +1,5 @@ import "@aws-cdk/assert/jest"; +import "../../utils/test/jest"; import { SynthUtils } from "@aws-cdk/assert/lib/synth-utils"; import { Vpc } from "@aws-cdk/aws-ec2"; import { ApplicationProtocol, ListenerAction } from "@aws-cdk/aws-elasticloadbalancingv2"; @@ -21,44 +22,32 @@ describe("The GuApplicationLoadBalancer class", () => { publicSubnetIds: [""], }); - test("overrides the id with the overrideId prop", () => { - const stack = simpleGuStackForTesting(); - new GuApplicationLoadBalancer(stack, "ApplicationLoadBalancer", { vpc, overrideId: true }); + test("overrides the logicalId when existingLogicalId is set in a migrating stack", () => { + const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); + new GuApplicationLoadBalancer(stack, "ApplicationLoadBalancer", { + vpc, + existingLogicalId: "AppLoadBalancer", + }); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).toContain("ApplicationLoadBalancer"); + expect(stack).toHaveResourceOfTypeAndLogicalId("AWS::ElasticLoadBalancingV2::LoadBalancer", "AppLoadBalancer"); }); - test("has an auto-generated ID by default", () => { + test("has an auto-generated logicalId by default", () => { const stack = simpleGuStackForTesting(); new GuApplicationLoadBalancer(stack, "ApplicationLoadBalancer", { vpc }); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).not.toContain("ApplicationLoadBalancer"); - }); - - test("overrides the id if the stack migrated value is true", () => { - const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); - new GuApplicationLoadBalancer(stack, "ApplicationLoadBalancer", { vpc }); - - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).toContain("ApplicationLoadBalancer"); - }); - - test("does not override the id if the stack migrated value is true but the override id value is false", () => { - const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); - new GuApplicationLoadBalancer(stack, "ApplicationLoadBalancer", { vpc, overrideId: false }); - - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).not.toContain("ApplicationLoadBalancer"); + expect(stack).toHaveResourceOfTypeAndLogicalId( + "AWS::ElasticLoadBalancingV2::LoadBalancer", + /ApplicationLoadBalancer.+/ + ); }); test("deletes the Type property", () => { - const stack = simpleGuStackForTesting(); - new GuApplicationLoadBalancer(stack, "ApplicationLoadBalancer", { vpc, overrideId: true }); + const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); + new GuApplicationLoadBalancer(stack, "ApplicationLoadBalancer", { vpc, existingLogicalId: "AppLoadBalancer" }); const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources.ApplicationLoadBalancer.Properties)).not.toContain("Type"); + expect(Object.keys(json.Resources.AppLoadBalancer.Properties)).not.toContain("Type"); }); test("sets the deletion protection value to true by default", () => { @@ -83,36 +72,21 @@ describe("The GuApplicationTargetGroup class", () => { publicSubnetIds: [""], }); - test("overrides the id if the prop is true", () => { - const stack = simpleGuStackForTesting(); - new GuApplicationTargetGroup(stack, "ApplicationTargetGroup", { vpc, overrideId: true }); - - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).toContain("ApplicationTargetGroup"); - }); - - test("does not override the id if the prop is false", () => { - const stack = simpleGuStackForTesting(); - new GuApplicationTargetGroup(stack, "ApplicationTargetGroup", { vpc }); - - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).not.toContain("ApplicationTargetGroup"); - }); - - test("overrides the id if the stack migrated value is true", () => { + test("overrides the logicalId when existingLogicalId is set in a migrating stack", () => { const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); - new GuApplicationTargetGroup(stack, "ApplicationTargetGroup", { vpc }); + new GuApplicationTargetGroup(stack, "ApplicationTargetGroup", { vpc, existingLogicalId: "ApplicationTargetGrp" }); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).toContain("ApplicationTargetGroup"); + expect(stack).toHaveResourceOfTypeAndLogicalId("AWS::ElasticLoadBalancingV2::TargetGroup", "ApplicationTargetGrp"); }); - test("does not override the id if the stack migrated value is true but the override id value is false", () => { + test("does not override the id if the stack migrated value is true but existingLogicalId is not set", () => { const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); - new GuApplicationTargetGroup(stack, "ApplicationTargetGroup", { vpc, overrideId: false }); + new GuApplicationTargetGroup(stack, "ApplicationTargetGroup", { vpc }); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).not.toContain("ApplicationTargetGroup"); + expect(stack).toHaveResourceOfTypeAndLogicalId( + "AWS::ElasticLoadBalancingV2::TargetGroup", + /^ApplicationTargetGroup[A-Z0-9]+$/ + ); }); test("uses default health check properties", () => { @@ -160,8 +134,8 @@ describe("The GuApplicationListener class", () => { publicSubnetIds: [""], }); - test("overrides the id if the prop is true", () => { - const stack = simpleGuStackForTesting(); + test("overrides the logicalId when existingLogicalId is set in a migrating stack", () => { + const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); const loadBalancer = new GuApplicationLoadBalancer(stack, "ApplicationLoadBalancer", { vpc }); const targetGroup = new GuApplicationTargetGroup(stack, "GrafanaInternalTargetGroup", { @@ -171,13 +145,12 @@ describe("The GuApplicationListener class", () => { new GuApplicationListener(stack, "ApplicationListener", { loadBalancer, - overrideId: true, + existingLogicalId: "AppListener", defaultAction: ListenerAction.forward([targetGroup]), certificates: [{ certificateArn: "" }], }); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).toContain("ApplicationListener"); + expect(stack).toHaveResourceOfTypeAndLogicalId("AWS::ElasticLoadBalancingV2::Listener", "AppListener"); }); test("does not override the id if the prop is false", () => { @@ -195,27 +168,8 @@ describe("The GuApplicationListener class", () => { certificates: [{ certificateArn: "" }], }); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).not.toContain("ApplicationListener"); - }); - - test("overrides the id if the stack migrated value is true", () => { - const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); - - const loadBalancer = new GuApplicationLoadBalancer(stack, "ApplicationLoadBalancer", { vpc }); - const targetGroup = new GuApplicationTargetGroup(stack, "GrafanaInternalTargetGroup", { - vpc: vpc, - protocol: ApplicationProtocol.HTTP, - }); - - new GuApplicationListener(stack, "ApplicationListener", { - loadBalancer, - defaultAction: ListenerAction.forward([targetGroup]), - certificates: [{ certificateArn: "" }], - }); - - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).toContain("ApplicationListener"); + expect(stack).not.toHaveResourceOfTypeAndLogicalId("AWS::ElasticLoadBalancingV2::Listener", "AppListener"); + expect(stack).toHaveResourceOfTypeAndLogicalId("AWS::ElasticLoadBalancingV2::Listener", /ApplicationListener.+/); }); test("does not override the id if the stack migrated value is true but the override id value is false", () => { @@ -229,13 +183,12 @@ describe("The GuApplicationListener class", () => { new GuApplicationListener(stack, "ApplicationListener", { loadBalancer, - overrideId: false, defaultAction: ListenerAction.forward([targetGroup]), certificates: [{ certificateArn: "" }], }); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).not.toContain("ApplicationListener"); + expect(stack).not.toHaveResourceOfTypeAndLogicalId("AWS::ElasticLoadBalancingV2::Listener", "AppListener"); + expect(stack).toHaveResourceOfTypeAndLogicalId("AWS::ElasticLoadBalancingV2::Listener", /ApplicationListener.+/); }); test("sets default props", () => { diff --git a/src/constructs/loadbalancing/alb.ts b/src/constructs/loadbalancing/alb.ts index f479f0ff28..5da003c07f 100644 --- a/src/constructs/loadbalancing/alb.ts +++ b/src/constructs/loadbalancing/alb.ts @@ -2,9 +2,7 @@ import type { ApplicationListenerProps, ApplicationLoadBalancerProps, ApplicationTargetGroupProps, - CfnListener, CfnLoadBalancer, - CfnTargetGroup, } from "@aws-cdk/aws-elasticloadbalancingv2"; import { ApplicationListener, @@ -19,29 +17,26 @@ import { RegexPattern } from "../../constants"; import type { GuStack } from "../core"; import { GuCertificateArnParameter } from "../core"; import type { AppIdentity } from "../core/identity"; +import { GuMigratingResource } from "../core/migrating"; +import type { GuStatefulConstruct } from "../core/migrating"; -interface GuApplicationLoadBalancerProps extends ApplicationLoadBalancerProps { - overrideId?: boolean; -} +interface GuApplicationLoadBalancerProps extends ApplicationLoadBalancerProps, GuMigratingResource {} -export class GuApplicationLoadBalancer extends ApplicationLoadBalancer { +export class GuApplicationLoadBalancer extends ApplicationLoadBalancer implements GuStatefulConstruct { + public readonly isStatefulConstruct: true; constructor(scope: GuStack, id: string, props: GuApplicationLoadBalancerProps) { super(scope, id, { deletionProtection: true, ...props }); + this.isStatefulConstruct = true; + GuMigratingResource.setLogicalId(this, scope, props); const cfnLb = this.node.defaultChild as CfnLoadBalancer; - - if (props.overrideId || (scope.migratedFromCloudFormation && props.overrideId !== false)) - cfnLb.overrideLogicalId(id); - cfnLb.addPropertyDeletionOverride("Type"); } } -export interface GuApplicationTargetGroupProps extends ApplicationTargetGroupProps { - overrideId?: boolean; -} +export interface GuApplicationTargetGroupProps extends ApplicationTargetGroupProps, GuMigratingResource {} -export class GuApplicationTargetGroup extends ApplicationTargetGroup { +export class GuApplicationTargetGroup extends ApplicationTargetGroup implements GuStatefulConstruct { static DefaultHealthCheck = { path: "/healthcheck", protocol: Protocol.HTTP, @@ -51,6 +46,8 @@ export class GuApplicationTargetGroup extends ApplicationTargetGroup { timeout: Duration.seconds(10), }; + public readonly isStatefulConstruct: true; + constructor(scope: GuStack, id: string, props: GuApplicationTargetGroupProps) { const mergedProps = { ...props, @@ -58,22 +55,20 @@ export class GuApplicationTargetGroup extends ApplicationTargetGroup { }; super(scope, id, mergedProps); - - if (mergedProps.overrideId || (scope.migratedFromCloudFormation && mergedProps.overrideId !== false)) - (this.node.defaultChild as CfnTargetGroup).overrideLogicalId(id); + this.isStatefulConstruct = true; + GuMigratingResource.setLogicalId(this, scope, props); } } -export interface GuApplicationListenerProps extends ApplicationListenerProps { - overrideId?: boolean; -} +export interface GuApplicationListenerProps extends ApplicationListenerProps, GuMigratingResource {} + +export class GuApplicationListener extends ApplicationListener implements GuStatefulConstruct { + public readonly isStatefulConstruct: true; -export class GuApplicationListener extends ApplicationListener { constructor(scope: GuStack, id: string, props: GuApplicationListenerProps) { super(scope, id, { port: 443, protocol: ApplicationProtocol.HTTPS, ...props }); - - if (props.overrideId || (scope.migratedFromCloudFormation && props.overrideId !== false)) - (this.node.defaultChild as CfnListener).overrideLogicalId(id); + this.isStatefulConstruct = true; + GuMigratingResource.setLogicalId(this, scope, props); } } diff --git a/src/constructs/loadbalancing/elb.test.ts b/src/constructs/loadbalancing/elb.test.ts index abb2173b88..ce049d7665 100644 --- a/src/constructs/loadbalancing/elb.test.ts +++ b/src/constructs/loadbalancing/elb.test.ts @@ -1,4 +1,5 @@ import "@aws-cdk/assert/jest"; +import "../../utils/test/jest"; import { SynthUtils } from "@aws-cdk/assert/lib/synth-utils"; import { Vpc } from "@aws-cdk/aws-ec2"; import { Stack } from "@aws-cdk/core"; @@ -14,43 +15,31 @@ describe("The GuClassicLoadBalancer class", () => { privateSubnetIds: [""], }); - test("overrides the id with the overrideId prop", () => { - const stack = simpleGuStackForTesting(); - new GuClassicLoadBalancer(stack, "ClassicLoadBalancer", { vpc, overrideId: true }); - - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).toContain("ClassicLoadBalancer"); - }); - test("has an auto-generated ID by default", () => { const stack = simpleGuStackForTesting(); new GuClassicLoadBalancer(stack, "ClassicLoadBalancer", { vpc }); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).not.toContain("ClassicLoadBalancer"); + expect(stack).toHaveResourceOfTypeAndLogicalId("AWS::ElasticLoadBalancing::LoadBalancer", /ClassicLoadBalancer.+/); }); - test("overrides the id if the stack migrated value is true", () => { + test("overrides the logicalId when existingLogicalId is set in a migrating stack", () => { const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); - new GuClassicLoadBalancer(stack, "ClassicLoadBalancer", { vpc }); + new GuClassicLoadBalancer(stack, "ClassicLoadBalancer", { vpc, existingLogicalId: "ClassicLB" }); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).toContain("ClassicLoadBalancer"); + expect(stack).toHaveResourceOfTypeAndLogicalId("AWS::ElasticLoadBalancing::LoadBalancer", "ClassicLB"); }); - test("does not override the id if the stack migrated value is true but the override id value is false", () => { + test("has an auto-generated logicalId by default", () => { const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); - new GuClassicLoadBalancer(stack, "ClassicLoadBalancer", { vpc, overrideId: false }); + new GuClassicLoadBalancer(stack, "ClassicLoadBalancer", { vpc }); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).not.toContain("ClassicLoadBalancer"); + expect(stack).toHaveResourceOfTypeAndLogicalId("AWS::ElasticLoadBalancing::LoadBalancer", /ClassicLoadBalancer.+/); }); test("overrides any properties as required", () => { const stack = simpleGuStackForTesting(); new GuClassicLoadBalancer(stack, "ClassicLoadBalancer", { vpc, - overrideId: true, propertiesToOverride: { AccessLoggingPolicy: { EmitInterval: 5, diff --git a/src/constructs/loadbalancing/elb.ts b/src/constructs/loadbalancing/elb.ts index d3f5d4fdb2..3df507e8c0 100644 --- a/src/constructs/loadbalancing/elb.ts +++ b/src/constructs/loadbalancing/elb.ts @@ -8,14 +8,15 @@ import { LoadBalancer, LoadBalancingProtocol } from "@aws-cdk/aws-elasticloadbal import { Duration } from "@aws-cdk/core"; import type { GuStack } from "../core"; import { GuArnParameter } from "../core"; +import type { GuStatefulConstruct } from "../core/migrating"; +import { GuMigratingResource } from "../core/migrating"; -interface GuClassicLoadBalancerProps extends Omit { - overrideId?: boolean; +interface GuClassicLoadBalancerProps extends Omit, GuMigratingResource { propertiesToOverride?: Record; healthCheck?: Partial; } -export class GuClassicLoadBalancer extends LoadBalancer { +export class GuClassicLoadBalancer extends LoadBalancer implements GuStatefulConstruct { static DefaultHealthCheck = { port: 9000, path: "/healthcheck", @@ -26,6 +27,8 @@ export class GuClassicLoadBalancer extends LoadBalancer { timeout: Duration.seconds(10), }; + public readonly isStatefulConstruct: true; + constructor(scope: GuStack, id: string, props: GuClassicLoadBalancerProps) { const mergedProps = { ...props, @@ -33,12 +36,10 @@ export class GuClassicLoadBalancer extends LoadBalancer { }; super(scope, id, mergedProps); + this.isStatefulConstruct = true; + GuMigratingResource.setLogicalId(this, scope, props); const cfnLb = this.node.defaultChild as CfnLoadBalancer; - - if (mergedProps.overrideId || (scope.migratedFromCloudFormation && mergedProps.overrideId !== false)) - cfnLb.overrideLogicalId(id); - mergedProps.propertiesToOverride && Object.entries(mergedProps.propertiesToOverride).forEach(([key, value]) => cfnLb.addPropertyOverride(key, value)); } diff --git a/src/constructs/rds/instance.test.ts b/src/constructs/rds/instance.test.ts index cbcb2490eb..e135b5f489 100644 --- a/src/constructs/rds/instance.test.ts +++ b/src/constructs/rds/instance.test.ts @@ -1,11 +1,10 @@ import "@aws-cdk/assert/jest"; -import { SynthUtils } from "@aws-cdk/assert/lib/synth-utils"; +import "../../utils/test/jest"; import { Vpc } from "@aws-cdk/aws-ec2"; import { DatabaseInstanceEngine, ParameterGroup, PostgresEngineVersion } from "@aws-cdk/aws-rds"; import { Stack } from "@aws-cdk/core"; import { TrackingTag } from "../../constants/library-info"; import { alphabeticalTags, simpleGuStackForTesting } from "../../utils/test"; -import type { SynthedStack } from "../../utils/test"; import { GuDatabaseInstance } from "./instance"; describe("The GuDatabaseInstance class", () => { @@ -107,43 +106,11 @@ describe("The GuDatabaseInstance class", () => { }); }); - it("overrides the id if the prop is true", () => { - const stack = simpleGuStackForTesting(); - new GuDatabaseInstance(stack, "DatabaseInstance", { - vpc, - overrideId: true, - instanceType: "t3.small", - engine: DatabaseInstanceEngine.postgres({ - version: PostgresEngineVersion.VER_11_8, - }), - app: "testing", - }); - - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - - expect(Object.keys(json.Resources)).toContain("DatabaseInstance"); - }); - - it("does not override the id if the prop is false", () => { - const stack = simpleGuStackForTesting(); - new GuDatabaseInstance(stack, "DatabaseInstance", { - vpc, - instanceType: "t3.small", - engine: DatabaseInstanceEngine.postgres({ - version: PostgresEngineVersion.VER_11_8, - }), - app: "testing", - }); - - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - - expect(Object.keys(json.Resources)).not.toContain("DatabaseInstance"); - }); - - it("overrides the id if the stack migrated value is true", () => { + it("overrides the logicalId when existingLogicalId is set in a migrating stack", () => { const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); new GuDatabaseInstance(stack, "DatabaseInstance", { vpc, + existingLogicalId: "MyDb", instanceType: "t3.small", engine: DatabaseInstanceEngine.postgres({ version: PostgresEngineVersion.VER_11_8, @@ -151,16 +118,13 @@ describe("The GuDatabaseInstance class", () => { app: "testing", }); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - - expect(Object.keys(json.Resources)).toContain("DatabaseInstance"); + expect(stack).toHaveResourceOfTypeAndLogicalId("AWS::RDS::DBInstance", "MyDb"); }); - it("does not override the id if the stack migrated value is true but the override id value is false", () => { + it("has an auto-generated logicalId by default", () => { const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); new GuDatabaseInstance(stack, "DatabaseInstance", { vpc, - overrideId: false, instanceType: "t3.small", engine: DatabaseInstanceEngine.postgres({ version: PostgresEngineVersion.VER_11_8, @@ -168,9 +132,7 @@ describe("The GuDatabaseInstance class", () => { app: "testing", }); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - - expect(Object.keys(json.Resources)).not.toContain("DatabaseInstance"); + expect(stack).toHaveResourceOfTypeAndLogicalId("AWS::RDS::DBInstance", /DatabaseInstance.+/); }); test("sets the deletion protection value to true by default", () => { diff --git a/src/constructs/rds/instance.ts b/src/constructs/rds/instance.ts index 7867731d5f..8c4b43f15f 100644 --- a/src/constructs/rds/instance.ts +++ b/src/constructs/rds/instance.ts @@ -1,17 +1,23 @@ import { InstanceType } from "@aws-cdk/aws-ec2"; -import type { CfnDBInstance, DatabaseInstanceProps, IParameterGroup } from "@aws-cdk/aws-rds"; +import type { DatabaseInstanceProps, IParameterGroup } from "@aws-cdk/aws-rds"; import { DatabaseInstance, ParameterGroup } from "@aws-cdk/aws-rds"; import { Fn } from "@aws-cdk/core"; import type { GuStack } from "../core"; import { AppIdentity } from "../core/identity"; +import { GuMigratingResource } from "../core/migrating"; +import type { GuStatefulConstruct } from "../core/migrating"; -export interface GuDatabaseInstanceProps extends Omit, AppIdentity { - overrideId?: boolean; +export interface GuDatabaseInstanceProps + extends Omit, + AppIdentity, + GuMigratingResource { instanceType: string; parameters?: Record; } -export class GuDatabaseInstance extends DatabaseInstance { +export class GuDatabaseInstance extends DatabaseInstance implements GuStatefulConstruct { + public readonly isStatefulConstruct: true; + constructor(scope: GuStack, id: string, props: GuDatabaseInstanceProps) { // CDK just wants "t3.micro" format, whereas // some CFN yaml might have the older "db.t3.micro" with the "db." prefix @@ -34,10 +40,8 @@ export class GuDatabaseInstance extends DatabaseInstance { instanceType, ...(parameterGroup && { parameterGroup }), }); - - if (props.overrideId || (scope.migratedFromCloudFormation && props.overrideId !== false)) { - (this.node.defaultChild as CfnDBInstance).overrideLogicalId(id); - } + this.isStatefulConstruct = true; + GuMigratingResource.setLogicalId(this, scope, props); parameterGroup && AppIdentity.taggedConstruct(props, parameterGroup); AppIdentity.taggedConstruct(props, this); diff --git a/src/constructs/sns/sns-topic.test.ts b/src/constructs/sns/sns-topic.test.ts index e81d8a3264..eeff461eb6 100644 --- a/src/constructs/sns/sns-topic.test.ts +++ b/src/constructs/sns/sns-topic.test.ts @@ -1,21 +1,20 @@ import "@aws-cdk/assert/jest"; -import { SynthUtils } from "@aws-cdk/assert/lib/synth-utils"; -import type { SynthedStack } from "../../utils/test"; +import "../../utils/test/jest"; import { simpleGuStackForTesting } from "../../utils/test"; import { GuSnsTopic } from "./sns-topic"; describe("The GuSnsTopic construct", () => { it("should not override the id by default", () => { const stack = simpleGuStackForTesting(); - new GuSnsTopic(stack, "my-sns-topic"); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).not.toContain("my-sns-topic"); + new GuSnsTopic(stack, "MySnsTopic"); + + expect(stack).toHaveResourceOfTypeAndLogicalId("AWS::SNS::Topic", /MySnsTopic.+/); }); - it("should override the id with the overrideId prop set to true", () => { - const stack = simpleGuStackForTesting(); - new GuSnsTopic(stack, "my-sns-topic", { overrideId: true }); - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - expect(Object.keys(json.Resources)).toContain("my-sns-topic"); + it("overrides the logicalId when existingLogicalId is set in a migrating stack", () => { + const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); + new GuSnsTopic(stack, "my-sns-topic", { existingLogicalId: "TheSnsTopic" }); + + expect(stack).toHaveResourceOfTypeAndLogicalId("AWS::SNS::Topic", "TheSnsTopic"); }); }); diff --git a/src/constructs/sns/sns-topic.ts b/src/constructs/sns/sns-topic.ts index 637773d96b..417b4c01f3 100644 --- a/src/constructs/sns/sns-topic.ts +++ b/src/constructs/sns/sns-topic.ts @@ -1,15 +1,13 @@ import { Topic } from "@aws-cdk/aws-sns"; -import type { CfnTopic, TopicProps } from "@aws-cdk/aws-sns"; +import type { TopicProps } from "@aws-cdk/aws-sns"; import type { GuStack } from "../core"; +import { GuMigratingResource } from "../core/migrating"; -interface GuSnsTopicProps extends TopicProps { - overrideId?: boolean; -} +interface GuSnsTopicProps extends TopicProps, GuMigratingResource {} export class GuSnsTopic extends Topic { constructor(scope: GuStack, id: string, props?: GuSnsTopicProps) { super(scope, id, props); - const cfnSnsTopic = this.node.defaultChild as CfnTopic; - if (props?.overrideId) cfnSnsTopic.overrideLogicalId(id); + props && GuMigratingResource.setLogicalId(this, scope, props); } } diff --git a/src/patterns/kinesis-lambda.test.ts b/src/patterns/kinesis-lambda.test.ts index 2020ee424e..a6b0278cf0 100644 --- a/src/patterns/kinesis-lambda.test.ts +++ b/src/patterns/kinesis-lambda.test.ts @@ -32,8 +32,8 @@ describe("The GuKinesisLambda pattern", () => { expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); }); - it("should inherit an existing Kinesis stream correctly if logicalIdFromCloudFormation is passed in", () => { - const stack = simpleGuStackForTesting(); + it("should inherit an existing Kinesis stream correctly if an existingLogicalId is passed via existingSnsTopic in a migrating stack", () => { + const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); const basicErrorHandling: StreamErrorHandlingProps = { bisectBatchOnError: false, retryBehaviour: StreamRetry.maxAttempts(1), @@ -46,7 +46,7 @@ describe("The GuKinesisLambda pattern", () => { runtime: Runtime.NODEJS_12_X, errorHandlingConfiguration: basicErrorHandling, monitoringConfiguration: noMonitoring, - existingKinesisStream: { logicalIdFromCloudFormation: "pre-existing-kinesis-stream" }, + existingKinesisStream: { existingLogicalId: "pre-existing-kinesis-stream" }, app: "testing", }; new GuKinesisLambda(stack, "my-lambda-function", props); diff --git a/src/patterns/kinesis-lambda.ts b/src/patterns/kinesis-lambda.ts index c70c7a5929..fa70874369 100644 --- a/src/patterns/kinesis-lambda.ts +++ b/src/patterns/kinesis-lambda.ts @@ -6,6 +6,7 @@ import { KinesisEventSource } from "@aws-cdk/aws-lambda-event-sources"; import type { GuLambdaErrorPercentageMonitoringProps, NoMonitoring } from "../constructs/cloudwatch"; import type { GuStack } from "../constructs/core"; import { AppIdentity } from "../constructs/core/identity"; +import type { GuMigratingResource } from "../constructs/core/migrating"; import type { GuKinesisStreamProps } from "../constructs/kinesis"; import { GuKinesisStream } from "../constructs/kinesis"; import type { GuFunctionProps } from "../constructs/lambda"; @@ -40,8 +41,7 @@ import { toAwsErrorHandlingProps } from "../utils/lambda"; * existingKinesisStream: { externalKinesisStreamName: "KinesisStreamFromAnotherStack" } * ``` */ -export interface ExistingKinesisStream { - logicalIdFromCloudFormation?: string; +export interface ExistingKinesisStream extends GuMigratingResource { externalKinesisStreamName?: string; } @@ -104,11 +104,11 @@ export class GuKinesisLambda extends GuLambdaFunction { errorPercentageMonitoring: props.monitoringConfiguration.noMonitoring ? undefined : props.monitoringConfiguration, }); const kinesisProps: GuKinesisStreamProps = { - overrideId: !!props.existingKinesisStream?.logicalIdFromCloudFormation, + existingLogicalId: props.existingKinesisStream?.existingLogicalId, encryption: StreamEncryption.MANAGED, ...props.kinesisStreamProps, }; - const streamId = props.existingKinesisStream?.logicalIdFromCloudFormation ?? "KinesisStream"; + const streamId = props.existingKinesisStream?.existingLogicalId ?? "KinesisStream"; const kinesisStream = props.existingKinesisStream?.externalKinesisStreamName ? Stream.fromStreamArn( diff --git a/src/patterns/sns-lambda.test.ts b/src/patterns/sns-lambda.test.ts index d5789714ed..539f0170c4 100644 --- a/src/patterns/sns-lambda.test.ts +++ b/src/patterns/sns-lambda.test.ts @@ -22,8 +22,8 @@ describe("The GuSnsLambda pattern", () => { expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); }); - it("should inherit an existing SNS topic correctly if a logicalIdFromCloudFormation is passed via existingSnsTopic", () => { - const stack = simpleGuStackForTesting(); + it("should inherit an existing SNS topic correctly if an existingLogicalId is passed via existingSnsTopic in a migrating stack", () => { + const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true }); const noMonitoring: NoMonitoring = { noMonitoring: true }; const props = { code: { bucket: "test-dist", key: "lambda.zip" }, @@ -31,7 +31,7 @@ describe("The GuSnsLambda pattern", () => { handler: "my-lambda/handler", runtime: Runtime.NODEJS_12_X, monitoringConfiguration: noMonitoring, - existingSnsTopic: { logicalIdFromCloudFormation: "in-use-sns-topic" }, + existingSnsTopic: { existingLogicalId: "in-use-sns-topic" }, app: "testing", }; new GuSnsLambda(stack, "my-lambda-function", props); diff --git a/src/patterns/sns-lambda.ts b/src/patterns/sns-lambda.ts index b911c98dcb..63380ed033 100644 --- a/src/patterns/sns-lambda.ts +++ b/src/patterns/sns-lambda.ts @@ -4,6 +4,7 @@ import { CfnOutput } from "@aws-cdk/core"; import type { GuLambdaErrorPercentageMonitoringProps, NoMonitoring } from "../constructs/cloudwatch"; import type { GuStack } from "../constructs/core"; import { AppIdentity } from "../constructs/core/identity"; +import type { GuMigratingResource } from "../constructs/core/migrating"; import type { GuFunctionProps } from "../constructs/lambda"; import { GuLambdaFunction } from "../constructs/lambda"; import { GuSnsTopic } from "../constructs/sns"; @@ -35,8 +36,7 @@ import { GuSnsTopic } from "../constructs/sns"; * existingSnsTopic: { externalTopicName: "MySnsTopicNameFromAnotherStack" } * ``` */ -export interface ExistingSnsTopic { - logicalIdFromCloudFormation?: string; +export interface ExistingSnsTopic extends GuMigratingResource { externalTopicName?: string; } @@ -85,7 +85,7 @@ export class GuSnsLambda extends GuLambdaFunction { ...props, errorPercentageMonitoring: props.monitoringConfiguration.noMonitoring ? undefined : props.monitoringConfiguration, }); - const topicId = props.existingSnsTopic?.logicalIdFromCloudFormation ?? "SnsIncomingEventsTopic"; + const topicId = props.existingSnsTopic?.existingLogicalId ?? "SnsIncomingEventsTopic"; const snsTopic = props.existingSnsTopic?.externalTopicName ? Topic.fromTopicArn( @@ -96,7 +96,7 @@ export class GuSnsLambda extends GuLambdaFunction { : AppIdentity.taggedConstruct( props, new GuSnsTopic(scope, topicId, { - overrideId: !!props.existingSnsTopic?.logicalIdFromCloudFormation, + existingLogicalId: props.existingSnsTopic?.existingLogicalId, }) ); this.addEventSource(new SnsEventSource(snsTopic)); diff --git a/src/utils/test/jest.ts b/src/utils/test/jest.ts index 6c5f71f40a..b1f4936fcc 100644 --- a/src/utils/test/jest.ts +++ b/src/utils/test/jest.ts @@ -1,6 +1,5 @@ -import { SynthUtils } from "@aws-cdk/assert"; import type { GuStack } from "../../constructs/core"; -import type { SynthedStack } from "./synthed-stack"; +import { findResourceByTypeAndLogicalId } from "./synthed-stack"; declare global { // eslint-disable-next-line @typescript-eslint/no-namespace -- custom Jest matcher @@ -20,13 +19,7 @@ expect.extend({ * @param logicalId a string or regex pattern to match against the resource's logicalId */ toHaveResourceOfTypeAndLogicalId(stack: GuStack, resourceType: string, logicalId: string | RegExp) { - const json = SynthUtils.toCloudFormation(stack) as SynthedStack; - - const matchResult = Object.entries(json.Resources).find(([key, { Type }]) => { - const logicalIdMatch = logicalId instanceof RegExp ? logicalId.test(key) : key === logicalId; - const typeMatch = Type === resourceType; - return logicalIdMatch && typeMatch; - }); + const matchResult = findResourceByTypeAndLogicalId(stack, resourceType, logicalId); return matchResult ? { diff --git a/src/utils/test/synthed-stack.ts b/src/utils/test/synthed-stack.ts index 92ebc3bfff..7d61fbb642 100644 --- a/src/utils/test/synthed-stack.ts +++ b/src/utils/test/synthed-stack.ts @@ -1,17 +1,47 @@ +import { SynthUtils } from "@aws-cdk/assert"; import type { Stage } from "../../constants"; +import type { GuStack } from "../../constructs/core"; -interface Parameter { +export interface Parameter { Type: string; Description: string; Default?: string | number; AllowedValues?: Array; } -type ResourceProperty = Record; -type Resource = Record; +export type ResourceProperty = Record; +export type Resource = Record; export interface SynthedStack { Parameters: Record; Mappings: Record; Resources: Resource; } + +/** + * A helper function to find a resource of a particular type and logicalId within a stack. + * Useful for when the logicalId is auto-generated. + * @param stack the stack to search in + * @param resourceType the AWS resource type string, for example "AWS::AutoScaling::AutoScalingGroup" + * @param logicalIdPattern a string or regex pattern to match against the resource's logicalId + */ +export const findResourceByTypeAndLogicalId = ( + stack: GuStack, + resourceType: string, + logicalIdPattern: string | RegExp +): Resource | undefined => { + const json = SynthUtils.toCloudFormation(stack) as SynthedStack; + + const match = Object.entries(json.Resources).find(([key, { Type }]) => { + const logicalIdMatch = logicalIdPattern instanceof RegExp ? logicalIdPattern.test(key) : key === logicalIdPattern; + const typeMatch = Type === resourceType; + return logicalIdMatch && typeMatch; + }); + + if (match) { + const [logicalId, resource] = match; + return { [logicalId]: resource }; + } + + return undefined; +};