Skip to content

Commit

Permalink
refactor: encapsulate logicalId logic in an interface
Browse files Browse the repository at this point in the history
In an attempt to keep things DRY, encapsulate the logic around overriding a construct's logicalId in one place and add tests.
  • Loading branch information
akash1810 committed Apr 6, 2021
1 parent 3bbd312 commit c84498f
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 0 deletions.
111 changes: 111 additions & 0 deletions src/constructs/core/migrating.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import "@aws-cdk/assert/jest";
import { SynthUtils } from "@aws-cdk/assert";
import type { BucketProps } from "@aws-cdk/aws-s3";
import { Bucket } from "@aws-cdk/aws-s3";
import type { SynthedStack } from "../../utils/test";
import { simpleGuStackForTesting } from "../../utils/test";
import type { GuStatefulConstruct } from "./migrating";
import { GuMigratingResource } from "./migrating";
import type { GuStack } from "./stack";

class TestGuStatefulConstruct extends Bucket implements GuStatefulConstruct {
isStatefulConstruct: true;

constructor(scope: GuStack, id: string, props?: BucketProps) {
super(scope, id, props);
this.isStatefulConstruct = true;
}
}

/*
NOTE: In reality, we'd never directly call `GuMigratingResource.setLogicalId` as the GuConstruct will do that.
We're calling it here to test the function in isolation.
*/

describe("GuMigratingResource", () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function -- we are testing `console.info` being called, we don't need to see the message
const info = jest.spyOn(console, "info").mockImplementation(() => {});

// eslint-disable-next-line @typescript-eslint/no-empty-function -- we are testing `console.warn` being called, we don't need to see the message
const warn = jest.spyOn(console, "warn").mockImplementation(() => {});

afterEach(() => {
warn.mockReset();
info.mockReset();
});

test("Creating a new resource in a new stack produces a new auto-generated ID", () => {
const stack = simpleGuStackForTesting({ migratedFromCloudFormation: false });
const construct = new Bucket(stack, "MyBucket");

GuMigratingResource.setLogicalId(construct, stack, {});

expect(warn).toHaveBeenCalledTimes(0);
expect(info).toHaveBeenCalledTimes(0);

const json = SynthUtils.toCloudFormation(stack) as SynthedStack;
const resourceKeys = Object.keys(json.Resources);

expect(resourceKeys).toHaveLength(1);
expect(resourceKeys[0]).toMatch(/^MyBucket[A-Z0-9]+$/);
});

test("Keeping a resource's logicalId when migrating a stack", () => {
const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true });
const construct = new Bucket(stack, "MyBucket");

GuMigratingResource.setLogicalId(construct, stack, { existingLogicalId: "my-pre-existing-bucket" });

expect(warn).toHaveBeenCalledTimes(0);
expect(info).toHaveBeenCalledTimes(0);

const json = SynthUtils.toCloudFormation(stack) as SynthedStack;
expect(Object.keys(json.Resources)).toContain("my-pre-existing-bucket");
});

test("Creating a construct in a migrating stack, w/out setting existingLogicalId", () => {
const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true });
const construct = new Bucket(stack, "MyBucket");

GuMigratingResource.setLogicalId(construct, stack, {});

expect(info).toHaveBeenCalledTimes(0);
expect(warn).toHaveBeenCalledTimes(0);

const json = SynthUtils.toCloudFormation(stack) as SynthedStack;
const resourceKeys = Object.keys(json.Resources);

expect(resourceKeys).toHaveLength(1);
expect(resourceKeys[0]).toMatch(/^MyBucket[A-Z0-9]+$/);
});

test("Specifying a construct's existingLogicalId in a new stack", () => {
const stack = simpleGuStackForTesting({ migratedFromCloudFormation: false });
const construct = new Bucket(stack, "MyBucket");

GuMigratingResource.setLogicalId(construct, stack, { existingLogicalId: "my-pre-existing-bucket" });

expect(info).toHaveBeenCalledTimes(0);
expect(warn).toHaveBeenCalledTimes(1);
expect(warn).toHaveBeenCalledWith(
"GuStack has 'migratedFromCloudFormation' set to false. MyBucket has an 'existingLogicalId' set to my-pre-existing-bucket. This will have no effect - the logicalId will be auto-generated. Set 'migratedFromCloudFormation' to true for 'existingLogicalId' to be observed."
);

const json = SynthUtils.toCloudFormation(stack) as SynthedStack;
const resourceKeys = Object.keys(json.Resources);

expect(resourceKeys).toHaveLength(1);
expect(resourceKeys[0]).toMatch(/^MyBucket[A-Z0-9]+$/);
});

test("Creating a stateful construct in a migrating stack, w/out setting existingLogicalId", () => {
const stack = simpleGuStackForTesting({ migratedFromCloudFormation: true });
const construct = new TestGuStatefulConstruct(stack, "MyBucket");

GuMigratingResource.setLogicalId(construct, stack, {});
expect(warn).toHaveBeenCalledTimes(1);
expect(warn).toHaveBeenCalledWith(
"GuStack has 'migratedFromCloudFormation' set to true. MyBucket is a stateful construct and 'existingLogicalId' has not been set. MyBucket's logicalId will be auto-generated. This is not advised as downstream services, such as DNS, will likely need updating."
);
});
});
55 changes: 55 additions & 0 deletions src/constructs/core/migrating.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { CfnElement, IConstruct } from "@aws-cdk/core";
import type { GuStack } from "./stack";

export interface GuMigratingResource {
existingLogicalId?: string;
}

export interface GuStatefulConstruct extends IConstruct {
isStatefulConstruct: true;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- user defined type guard
function isGuStatefulConstruct(construct: any): construct is GuStatefulConstruct {
return "isStatefulConstruct" in construct;
}

export const GuMigratingResource = {
setLogicalId<T extends GuStatefulConstruct | IConstruct>(
construct: T,
{ migratedFromCloudFormation }: GuStack,
{ existingLogicalId }: GuMigratingResource
): void {
const overrideLogicalId = (logicalId: string) => {
const defaultChild = construct.node.defaultChild as CfnElement;
defaultChild.overrideLogicalId(logicalId);
};

const id = construct.node.id;
const isStateful = isGuStatefulConstruct(construct);

if (migratedFromCloudFormation) {
if (existingLogicalId) {
return overrideLogicalId(existingLogicalId);
}

if (isStateful) {
console.warn(
`GuStack has 'migratedFromCloudFormation' set to true. ${id} is a stateful construct and 'existingLogicalId' has not been set. ${id}'s logicalId will be auto-generated. This is not advised as downstream services, such as DNS, will likely need updating.`
);
}
} else {
if (existingLogicalId) {
console.warn(
`GuStack has 'migratedFromCloudFormation' set to false. ${id} has an 'existingLogicalId' set to ${existingLogicalId}. This will have no effect - the logicalId will be auto-generated. Set 'migratedFromCloudFormation' to true for 'existingLogicalId' to be observed.`
);
}

if (isStateful) {
console.info(
`GuStack has 'migratedFromCloudFormation' set to false. ${id} is a stateful construct, it's logicalId will be new and auto-generated.`
);
}
}
},
};

0 comments on commit c84498f

Please sign in to comment.