Skip to content

Commit

Permalink
Merge pull request #977 from guardian/aa-stack-set-support
Browse files Browse the repository at this point in the history
feat: Add Stack Set support
  • Loading branch information
akash1810 authored Dec 22, 2021
2 parents 794ff42 + 64c2a36 commit ec0552a
Show file tree
Hide file tree
Showing 5 changed files with 504 additions and 2 deletions.
25 changes: 23 additions & 2 deletions src/constructs/core/stack.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Annotations, Stack, Tags } from "@aws-cdk/core";
import type { App, StackProps } from "@aws-cdk/core";
import { SynthUtils } from "@aws-cdk/assert";
import { Annotations, App, Stack, Tags } from "@aws-cdk/core";
import type { StackProps } from "@aws-cdk/core";
import execa from "execa";
import gitUrlParse from "git-url-parse";
import { StageForInfrastructure } from "../../constants";
Expand Down Expand Up @@ -126,6 +127,10 @@ export class GuStack extends Stack implements StackStageIdentity, GuMigratingSta
return this.params.get(key) as T;
}

get parameterKeys(): string[] {
return Array.from(this.params.keys());
}

// eslint-disable-next-line custom-rules/valid-constructors -- GuStack is the exception as it must take an App
constructor(app: App, id: string, props: GuStackProps) {
const mergedProps = {
Expand Down Expand Up @@ -196,3 +201,19 @@ export class GuStackForInfrastructure extends GuStack {
return super.withStageDependentValue(mappingValue);
}
}

/**
* A GuStack but designed for Stack Set instances.
*
* In a stack set application, `GuStackForStackSetInstance` is used to represent the infrastructure to provision in target AWS accounts.
*/
export class GuStackForStackSetInstance extends GuStackForInfrastructure {
// eslint-disable-next-line custom-rules/valid-constructors -- GuStackForStackSet should have a unique `App`
constructor(id: string, props: GuStackProps) {
super(new App(), id, props);
}

get cfnJson(): string {
return JSON.stringify(SynthUtils.toCloudFormation(this), null, 2);
}
}
237 changes: 237 additions & 0 deletions src/constructs/stack-set/__snapshots__/stack-set.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`The GuStackSet construct should correctly provision a stack with a stack set resource 1`] = `
Object {
"Resources": Object {
"StackSet": Object {
"Properties": Object {
"AutoDeployment": Object {
"Enabled": true,
"RetainStacksOnAccountRemoval": false,
},
"Description": "this stack set provisions some common infrastructure",
"Parameters": Array [],
"PermissionModel": "SERVICE_MANAGED",
"StackInstancesGroup": Array [
Object {
"DeploymentTargets": Object {
"OrganizationalUnitIds": Array [
"o-12345abcde",
],
},
"Regions": Array [
Object {
"Ref": "AWS::Region",
},
],
},
],
"StackSetName": "my-stack-set",
"Tags": Array [
Object {
"Key": "gu:cdk:version",
"Value": "TEST",
},
Object {
"Key": "gu:repo",
"Value": "guardian/cdk",
},
Object {
"Key": "Stack",
"Value": "test",
},
Object {
"Key": "Stage",
"Value": "INFRA",
},
],
"TemplateBody": "{
\\"Resources\\": {
\\"accountloggingstreamB8733874\\": {
\\"Type\\": \\"AWS::Kinesis::Stream\\",
\\"Properties\\": {
\\"ShardCount\\": 1,
\\"RetentionPeriodHours\\": 24,
\\"StreamEncryption\\": {
\\"Fn::If\\": [
\\"AwsCdkKinesisEncryptedStreamsUnsupportedRegions\\",
{
\\"Ref\\": \\"AWS::NoValue\\"
},
{
\\"EncryptionType\\": \\"KMS\\",
\\"KeyId\\": \\"alias/aws/kinesis\\"
}
]
},
\\"Tags\\": [
{
\\"Key\\": \\"gu:cdk:version\\",
\\"Value\\": \\"TEST\\"
},
{
\\"Key\\": \\"gu:repo\\",
\\"Value\\": \\"guardian/cdk\\"
},
{
\\"Key\\": \\"Stack\\",
\\"Value\\": \\"test\\"
},
{
\\"Key\\": \\"Stage\\",
\\"Value\\": \\"INFRA\\"
}
]
}
}
},
\\"Conditions\\": {
\\"AwsCdkKinesisEncryptedStreamsUnsupportedRegions\\": {
\\"Fn::Or\\": [
{
\\"Fn::Equals\\": [
{
\\"Ref\\": \\"AWS::Region\\"
},
\\"cn-north-1\\"
]
},
{
\\"Fn::Equals\\": [
{
\\"Ref\\": \\"AWS::Region\\"
},
\\"cn-northwest-1\\"
]
}
]
}
}
}",
},
"Type": "AWS::CloudFormation::StackSet",
},
},
}
`;

exports[`The GuStackSet construct should support parameters in the stack set instance 1`] = `
Object {
"Resources": Object {
"StackSet": Object {
"Properties": Object {
"AutoDeployment": Object {
"Enabled": true,
"RetainStacksOnAccountRemoval": false,
},
"Description": "this stack set provisions some common infrastructure",
"Parameters": Array [
Object {
"ParameterKey": "CentralSnsTopicArn",
"ParameterValue": Object {
"Ref": "accountalertsD9982E6F",
},
},
],
"PermissionModel": "SERVICE_MANAGED",
"StackInstancesGroup": Array [
Object {
"DeploymentTargets": Object {
"OrganizationalUnitIds": Array [
"o-12345abcde",
],
},
"Regions": Array [
Object {
"Ref": "AWS::Region",
},
],
},
],
"StackSetName": "my-stack-set",
"Tags": Array [
Object {
"Key": "gu:cdk:version",
"Value": "TEST",
},
Object {
"Key": "gu:repo",
"Value": "guardian/cdk",
},
Object {
"Key": "Stack",
"Value": "test",
},
Object {
"Key": "Stage",
"Value": "INFRA",
},
],
"TemplateBody": "{
\\"Parameters\\": {
\\"CentralSnsTopicArn\\": {
\\"Type\\": \\"String\\",
\\"AllowedPattern\\": \\"arn:aws:[a-z0-9]*:[a-z0-9\\\\\\\\-]*:[0-9]{12}:.*\\"
}
}
}",
},
"Type": "AWS::CloudFormation::StackSet",
},
"accountalertsD9982E6F": Object {
"Properties": Object {
"Tags": Array [
Object {
"Key": "gu:cdk:version",
"Value": "TEST",
},
Object {
"Key": "gu:repo",
"Value": "guardian/cdk",
},
Object {
"Key": "Stack",
"Value": "test",
},
Object {
"Key": "Stage",
"Value": "INFRA",
},
],
},
"Type": "AWS::SNS::Topic",
},
"accountalertsPolicy8E37A8FA": Object {
"Properties": Object {
"PolicyDocument": Object {
"Statement": Array [
Object {
"Action": "sns:Publish",
"Condition": Object {
"StringEquals": Object {
"aws:PrincipalOrgID": "o-12345abcde",
},
},
"Effect": "Allow",
"Principal": Object {
"AWS": "*",
},
"Resource": Object {
"Ref": "accountalertsD9982E6F",
},
"Sid": "0",
},
],
"Version": "2012-10-17",
},
"Topics": Array [
Object {
"Ref": "accountalertsD9982E6F",
},
],
},
"Type": "AWS::SNS::TopicPolicy",
},
},
}
`;
1 change: 1 addition & 0 deletions src/constructs/stack-set/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./stack-set";
70 changes: 70 additions & 0 deletions src/constructs/stack-set/stack-set.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { SynthUtils } from "@aws-cdk/assert";
import { OrganizationPrincipal } from "@aws-cdk/aws-iam";
import { Topic } from "@aws-cdk/aws-sns";
import { App } from "@aws-cdk/core";
import { RegexPattern } from "../../constants";
import { GuStackForInfrastructure, GuStackForStackSetInstance, GuStringParameter } from "../core";
import { GuKinesisStream } from "../kinesis";
import { GuSnsTopic } from "../sns";
import { GuStackSet } from "./stack-set";

describe("The GuStackSet construct", () => {
it("should correctly provision a stack with a stack set resource", () => {
const theStackSetInstance = new GuStackForStackSetInstance("the-stack-set", { stack: "test" });
new GuKinesisStream(theStackSetInstance, "account-logging-stream");

const parentStack = new GuStackForInfrastructure(new App(), "Test", { stack: "test" });

new GuStackSet(parentStack, "StackSet", {
name: "my-stack-set",
description: "this stack set provisions some common infrastructure",
organisationUnitTargets: ["o-12345abcde"],
stackSetInstance: theStackSetInstance,
});

expect(SynthUtils.toCloudFormation(parentStack)).toMatchSnapshot();
});

it("should support parameters in the stack set instance", () => {
const theStackSetInstance = new GuStackForStackSetInstance("the-stack-set", { stack: "test" });
Topic.fromTopicArn(
theStackSetInstance,
"central-topic",
new GuStringParameter(theStackSetInstance, "CentralSnsTopicArn", { allowedPattern: RegexPattern.ARN })
.valueAsString
);

const awsOrgId = "o-12345abcde";
const parentStack = new GuStackForInfrastructure(new App(), "Test", { stack: "test" });
const centralTopic = new GuSnsTopic(parentStack, "account-alerts");
centralTopic.grantPublish(new OrganizationPrincipal(awsOrgId));

new GuStackSet(parentStack, "StackSet", {
name: "my-stack-set",
description: "this stack set provisions some common infrastructure",
organisationUnitTargets: [awsOrgId],
stackSetInstance: theStackSetInstance,
stackSetInstanceParameters: {
CentralSnsTopicArn: centralTopic.topicArn,
},
});

expect(SynthUtils.toCloudFormation(parentStack)).toMatchSnapshot();
});

it("should error if the parent stack does not specify all parameters for the stack set instance template", () => {
const theStackSetInstance = new GuStackForStackSetInstance("the-stack-set", { stack: "test" });
new GuStringParameter(theStackSetInstance, "CentralSnsTopic", { allowedPattern: RegexPattern.ARN });

const parentStack = new GuStackForInfrastructure(new App(), "Test", { stack: "test" });

expect(() => {
new GuStackSet(parentStack, "StackSet", {
name: "my-stack-set",
description: "this stack set provisions some common infrastructure",
organisationUnitTargets: ["o-12345abcde"],
stackSetInstance: theStackSetInstance,
});
}).toThrow("There are undefined stack set parameters: CentralSnsTopic");
});
});
Loading

0 comments on commit ec0552a

Please sign in to comment.