Skip to content

Commit

Permalink
Merge pull request #948 from guardian/aa-infra-stage
Browse files Browse the repository at this point in the history
feat: Add `GuStackForInfrastructure` where `Stage` is always `INFRA`
  • Loading branch information
akash1810 authored Dec 2, 2021
2 parents d8eb0df + 0a5b08c commit b419317
Show file tree
Hide file tree
Showing 15 changed files with 232 additions and 134 deletions.
7 changes: 4 additions & 3 deletions src/constants/stage.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
enum Stage {
export enum Stage {
CODE = "CODE",
PROD = "PROD",
}

// for use in the `allowed values` property of a cloudformation parameter
const Stages: string[] = Object.values(Stage);
export const Stages: string[] = Object.values(Stage);

export { Stage, Stages };
export const StageForInfrastructure = "INFRA";
export type StageForInfrastructure = "INFRA";
55 changes: 35 additions & 20 deletions src/constructs/acm/certificate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { Certificate, CertificateValidation } from "@aws-cdk/aws-certificatemana
import type { CertificateProps } from "@aws-cdk/aws-certificatemanager/lib/certificate";
import { HostedZone } from "@aws-cdk/aws-route53";
import { RemovalPolicy } from "@aws-cdk/core";
import { Stage } from "../../constants";
import { Stage, StageForInfrastructure } from "../../constants";
import type { GuDomainNameProps } from "../../types/domain-names";
import { StageAwareValue } from "../../types/stage";
import { GuStatefulMigratableConstruct } from "../../utils/mixin";
import { GuAppAwareConstruct } from "../../utils/mixin/app-aware-construct";
import type { GuStack } from "../core";
Expand Down Expand Up @@ -56,29 +57,43 @@ export type GuCertificatePropsWithApp = GuDomainNameProps & AppIdentity & GuMigr
*/
export class GuCertificate extends GuStatefulMigratableConstruct(GuAppAwareConstruct(Certificate)) {
constructor(scope: GuStack, props: GuCertificatePropsWithApp) {
const maybeHostedZone =
props.CODE.hostedZoneId && props.PROD.hostedZoneId
? HostedZone.fromHostedZoneId(
scope,
AppIdentity.suffixText({ app: props.app }, "HostedZone"),
scope.withStageDependentValue({
app: props.app,
variableName: "hostedZoneId",
stageValues: {
[Stage.CODE]: props.CODE.hostedZoneId,
[Stage.PROD]: props.PROD.hostedZoneId,
},
})
)
: undefined;
const hasHostedZoneId: boolean = StageAwareValue.isStageValue(props)
? !!props.CODE.hostedZoneId && !!props.PROD.hostedZoneId
: !!props.INFRA.hostedZoneId;

const maybeHostedZone = !hasHostedZoneId
? undefined
: HostedZone.fromHostedZoneId(
scope,
AppIdentity.suffixText({ app: props.app }, "HostedZone"),
scope.withStageDependentValue({
app: props.app,
variableName: "hostedZoneId",
/* eslint-disable @typescript-eslint/no-non-null-assertion -- `hasHostedZoneId` is true, so we know `hostedZoneId` is present here */
stageValues: StageAwareValue.isStageValue(props)
? {
[Stage.CODE]: props.CODE.hostedZoneId!,
[Stage.PROD]: props.PROD.hostedZoneId!,
}
: {
[StageForInfrastructure]: props.INFRA.hostedZoneId!,
},
/* eslint-enable @typescript-eslint/no-non-null-assertion */
})
);

const awsCertificateProps: CertificateProps & GuMigratingResource & AppIdentity = {
domainName: scope.withStageDependentValue({
app: props.app,
variableName: "domainName",
stageValues: {
[Stage.CODE]: props.CODE.domainName,
[Stage.PROD]: props.PROD.domainName,
},
stageValues: StageAwareValue.isStageValue(props)
? {
[Stage.CODE]: props.CODE.domainName,
[Stage.PROD]: props.PROD.domainName,
}
: {
[StageForInfrastructure]: props.INFRA.domainName,
},
}),
validation: CertificateValidation.fromDns(maybeHostedZone),
existingLogicalId: props.existingLogicalId,
Expand Down
28 changes: 18 additions & 10 deletions src/constructs/autoscaling/asg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import type { AutoScalingGroupProps, CfnAutoScalingGroup } from "@aws-cdk/aws-au
import { OperatingSystemType, UserData } from "@aws-cdk/aws-ec2";
import type { ISecurityGroup, MachineImage, MachineImageConfig } from "@aws-cdk/aws-ec2";
import type { ApplicationTargetGroup } from "@aws-cdk/aws-elasticloadbalancingv2";
import { Stage } from "../../constants";
import { Stage, StageForInfrastructure } from "../../constants";
import { StageAwareValue } from "../../types/stage";
import { GuStatefulMigratableConstruct } from "../../utils/mixin";
import { GuAppAwareConstruct } from "../../utils/mixin/app-aware-construct";
import { GuAmiParameter } from "../core";
Expand Down Expand Up @@ -38,7 +39,7 @@ export interface GuAutoScalingGroupProps
targetGroup?: ApplicationTargetGroup;
}

type GuStageDependentAsgProps = Record<Stage, GuAsgCapacityProps>;
type GuStageDependentAsgProps = StageAwareValue<GuAsgCapacityProps>;

/**
* `minimumInstances` determines the number of ec2 instances running under normal circumstances
Expand Down Expand Up @@ -69,18 +70,25 @@ function wireStageDependentProps(
minCapacity: stack.withStageDependentValue({
app,
variableName: "minInstances",
stageValues: {
[Stage.CODE]: stageDependentProps.CODE.minimumInstances,
[Stage.PROD]: stageDependentProps.PROD.minimumInstances,
},
stageValues: StageAwareValue.isStageValue(stageDependentProps)
? {
[Stage.CODE]: stageDependentProps.CODE.minimumInstances,
[Stage.PROD]: stageDependentProps.PROD.minimumInstances,
}
: { [StageForInfrastructure]: stageDependentProps.INFRA.minimumInstances },
}),
maxCapacity: stack.withStageDependentValue({
app,
variableName: "maxInstances",
stageValues: {
[Stage.CODE]: stageDependentProps.CODE.maximumInstances ?? stageDependentProps.CODE.minimumInstances * 2,
[Stage.PROD]: stageDependentProps.PROD.maximumInstances ?? stageDependentProps.PROD.minimumInstances * 2,
},
stageValues: StageAwareValue.isStageValue(stageDependentProps)
? {
[Stage.CODE]: stageDependentProps.CODE.maximumInstances ?? stageDependentProps.CODE.minimumInstances * 2,
[Stage.PROD]: stageDependentProps.PROD.maximumInstances ?? stageDependentProps.PROD.minimumInstances * 2,
}
: {
[StageForInfrastructure]:
stageDependentProps.INFRA.maximumInstances ?? stageDependentProps.INFRA.minimumInstances * 2,
},
}),
};
}
Expand Down
13 changes: 8 additions & 5 deletions src/constructs/cloudwatch/alarm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { AlarmProps } from "@aws-cdk/aws-cloudwatch";
import { SnsAction } from "@aws-cdk/aws-cloudwatch-actions";
import { Topic } from "@aws-cdk/aws-sns";
import type { ITopic } from "@aws-cdk/aws-sns";
import { Stage } from "../../constants";
import { Stage, StageForInfrastructure } from "../../constants";
import type { GuStack } from "../core";
import type { AppIdentity } from "../core/identity";

Expand All @@ -28,10 +28,13 @@ export class GuAlarm extends Alarm {
actionsEnabled: scope.withStageDependentValue({
app: props.app,
variableName: "alarmActionsEnabled",
stageValues: {
[Stage.CODE]: props.actionsEnabledInCode ?? false,
[Stage.PROD]: true,
},
stageValues:
scope.stage === StageForInfrastructure
? { [StageForInfrastructure]: true }
: {
[Stage.CODE]: props.actionsEnabledInCode ?? false,
[Stage.PROD]: true,
},
}),
});
const topicArn: string = `arn:aws:sns:${scope.region}:${scope.account}:${props.snsTopicName}`;
Expand Down
4 changes: 2 additions & 2 deletions src/constructs/core/mappings.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { CfnMapping } from "@aws-cdk/core";
import type { Stage } from "../../constants";
import type { StageAwareValue } from "../../types/stage";
import type { AppIdentity } from "./identity";
import type { GuStack } from "./stack";

export type GuMappingValue = string | number | boolean;

export interface GuStageMappingValue<T extends GuMappingValue> extends AppIdentity {
variableName: string;
stageValues: Record<Stage, T>;
stageValues: StageAwareValue<T>;
}

export class GuStageMapping {
Expand Down
49 changes: 46 additions & 3 deletions src/constructs/core/stack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@ import "@aws-cdk/assert/jest";
import { SynthUtils } from "@aws-cdk/assert";
import "../../utils/test/jest";
import { Role, ServicePrincipal } from "@aws-cdk/aws-iam";
import { App } from "@aws-cdk/core";
import { Stage, Stages } from "../../constants";
import { Annotations, App } from "@aws-cdk/core";
import { Stage, StageForInfrastructure, Stages } from "../../constants";
import { ContextKeys } from "../../constants/context-keys";
import { TagKeys } from "../../constants/tag-keys";
import { simpleGuStackForTesting } from "../../utils/test";
import type { SynthedStack } from "../../utils/test";
import { GuParameter } from "./parameters";
import { GuStack } from "./stack";
import { GuStack, GuStackForInfrastructure } from "./stack";

describe("The GuStack construct", () => {
const warn = jest.spyOn(Annotations.prototype, "addWarning");

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

it("requires passing the stack value as props", function () {
const stack = simpleGuStackForTesting({ stack: "some-stack" });
expect(stack.stack).toEqual("some-stack");
Expand Down Expand Up @@ -78,4 +84,41 @@ describe("The GuStack construct", () => {
"Attempting to read parameter i-do-not-exist which does not exist"
);
});

it("should warn when calling withStageDependentValue with the INFRA stage", () => {
const stack = new GuStack(new App(), "Test", { stack: "test" });

stack.withStageDependentValue({
app: "test",
variableName: "test",
stageValues: {
[StageForInfrastructure]: 1,
},
});
expect(warn).toHaveBeenCalledWith(
"GuStack does not have a stage of INFRA. Setting a mapping value for it has no impact."
);
});
});

describe("The GuStackForInfrastructure construct", () => {
it("should have a stage of INFRA", () => {
const stack = new GuStackForInfrastructure(new App(), "Test", { stack: "test" });
expect(stack.stage).toBe("INFRA");
});

it("should throw when calling withStageDependentValue with a non-INFRA stage", () => {
const stack = new GuStackForInfrastructure(new App(), "Test", { stack: "test" });

expect(() => {
stack.withStageDependentValue({
app: "test",
variableName: "test",
stageValues: {
[Stage.CODE]: 1,
[Stage.PROD]: 2,
},
});
}).toThrowError("Mapping doesn't contain top-level key 'INFRA'");
});
});
37 changes: 33 additions & 4 deletions src/constructs/core/stack.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Stack, Tags } from "@aws-cdk/core";
import { Annotations, Stack, Tags } from "@aws-cdk/core";
import type { App, StackProps } from "@aws-cdk/core";
import execa from "execa";
import gitUrlParse from "git-url-parse";
import { StageForInfrastructure } from "../../constants";
import { ContextKeys } from "../../constants/context-keys";
import { TagKeys } from "../../constants/tag-keys";
import { TrackingTag } from "../../constants/tracking-tag";
Expand Down Expand Up @@ -54,15 +55,14 @@ export interface GuStackProps extends Omit<StackProps, "stackName">, Partial<GuM
*/
export class GuStack extends Stack implements StackStageIdentity, GuMigratingStack {
private readonly _stack: string;
private readonly _stage: string;

private _mappings?: GuStageMapping;
private params: Map<string, GuParameter>;

public readonly migratedFromCloudFormation: boolean;

get stage(): string {
return this._stage;
return GuStageParameter.getInstance(this).valueAsString;
}

get stack(): string {
Expand All @@ -83,6 +83,13 @@ export class GuStack extends Stack implements StackStageIdentity, GuMigratingSta
* Mappings to work around this limitation.
*/
withStageDependentValue<T extends GuMappingValue>(mappingValue: GuStageMappingValue<T>): T {
const isInfraStageProvided = Object.keys(mappingValue.stageValues).includes(StageForInfrastructure);
if (isInfraStageProvided) {
Annotations.of(this).addWarning(
`GuStack does not have a stage of ${StageForInfrastructure}. Setting a mapping value for it has no impact.`
);
}

return this.mappings.withValue(mappingValue);
}

Expand Down Expand Up @@ -127,7 +134,6 @@ export class GuStack extends Stack implements StackStageIdentity, GuMigratingSta
this.params = new Map<string, GuParameter>();

this._stack = props.stack;
this._stage = GuStageParameter.getInstance(this).valueAsString;

this.addTag(TrackingTag.Key, TrackingTag.Value);

Expand Down Expand Up @@ -160,3 +166,26 @@ export class GuStack extends Stack implements StackStageIdentity, GuMigratingSta
}
}
}

/**
* A GuStack but designed for infrastructure as `Stage` will always be `INFRA`.
*/
export class GuStackForInfrastructure extends GuStack {
override get stage(): string {
return StageForInfrastructure;
}

/**
* A helper function to switch between different values depending on the Stage being CloudFormed.
*
* As GuInfrastructureStack has a single stage (INFRA), calling withStageDependentValue is unnecessary complexity.
* Consider using a standard variable in code instead.
*
* Note: Specifying a stage other than `INFRA` will raise an exception.
*/
override withStageDependentValue<T extends GuMappingValue>(mappingValue: GuStageMappingValue<T>): T {
// Yep, we're just calling the super class's implementation...
// We're overriding to add some helpful documentation in the doc string.
return super.withStageDependentValue(mappingValue);
}
}
15 changes: 10 additions & 5 deletions src/constructs/dns/dns-records.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Duration } from "@aws-cdk/core";
import { CfnResource } from "@aws-cdk/core";
import { Stage } from "../../constants";
import { Stage, StageForInfrastructure } from "../../constants";
import type { GuDomainNameProps } from "../../types/domain-names";
import { StageAwareValue } from "../../types/stage";
import type { GuStack } from "../core";
import type { AppIdentity } from "../core/identity";

Expand Down Expand Up @@ -72,10 +73,14 @@ export class GuCname extends GuDnsRecordSet {
const domainName = scope.withStageDependentValue({
app: props.app,
variableName: "domainName",
stageValues: {
[Stage.CODE]: props.domainNameProps.CODE.domainName,
[Stage.PROD]: props.domainNameProps.PROD.domainName,
},
stageValues: StageAwareValue.isStageValue(props.domainNameProps)
? {
[Stage.CODE]: props.domainNameProps.CODE.domainName,
[Stage.PROD]: props.domainNameProps.PROD.domainName,
}
: {
[StageForInfrastructure]: props.domainNameProps.INFRA.domainName,
},
});
super(scope, id, {
name: domainName,
Expand Down
Loading

0 comments on commit b419317

Please sign in to comment.