Skip to content

Commit

Permalink
fix(riff-raff.yaml): Loosen restrictions on GuStacks in an App
Browse files Browse the repository at this point in the history
Support the use-case where an `App` includes multiple types of `GuStack`.
For example:

```ts
class ServiceRunningInDeployTools extends GuStack {}
class ServiceRunningInSecurity extends GuStack {}

new ServiceRunningInDeployTools(app, "App-CODE-deploy", {
  stack: "deploy",
  stage: "CODE",
});

new ServiceRunningInSecurity(app, "App-CODE-security", {
  stack: "security",
  stage: "CODE",
});
```

It's not clear why this validation was originally added.
The class name is used for the `app` property in a `cloud-formation`
deployment type. This functionality remains, as demonstrated by the tests.

Co-authored-by: Jacob <[email protected]>
Co-authored-by: Thalia <[email protected]>
  • Loading branch information
3 people committed Jan 19, 2023
1 parent fc4ad52 commit 61f9e59
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 96 deletions.
26 changes: 8 additions & 18 deletions src/experimental/riff-raff-yaml-file/group-by.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import type { GuStack } from "../../constructs/core";
import { groupBy } from "../../utils/array";
import type { ClassName, GroupedCdkStacks, Region, StackTag, StageTag } from "./types";

function groupByClassName(cdkStacks: GuStack[]): Record<ClassName, GuStack[]> {
return groupBy(cdkStacks, (stack) => stack.constructor.name);
}
import type { GroupedCdkStacks, Region, StackTag, StageTag } from "./types";

function groupByStackTag(cdkStacks: GuStack[]): Record<StackTag, GuStack[]> {
return groupBy(cdkStacks, ({ stack }) => stack);
Expand All @@ -19,19 +15,13 @@ function groupByRegion(cdkStacks: GuStack[]): Record<Region, GuStack[]> {
}

export function groupByClassNameStackRegionStage(cdkStacks: GuStack[]): GroupedCdkStacks {
return Object.entries(groupByClassName(cdkStacks)).reduce(
(accClassName, [className, stacksGroupedByClassName]) => ({
...accClassName,
[className]: Object.entries(groupByStackTag(stacksGroupedByClassName)).reduce(
(accStackTag, [stackTag, stacksGroupedByStackTag]) => ({
...accStackTag,
[stackTag]: Object.entries(groupByRegion(stacksGroupedByStackTag)).reduce(
(accRegion, [region, stacksGroupedByRegion]) => ({
...accRegion,
[region]: groupByStageTag(stacksGroupedByRegion),
}),
{}
),
return Object.entries(groupByStackTag(cdkStacks)).reduce(
(accStackTag, [stackTag, stacksGroupedByStackTag]) => ({
...accStackTag,
[stackTag]: Object.entries(groupByRegion(stacksGroupedByStackTag)).reduce(
(accRegion, [region, stacksGroupedByRegion]) => ({
...accRegion,
[region]: groupByStageTag(stacksGroupedByRegion),
}),
{}
),
Expand Down
2 changes: 1 addition & 1 deletion src/experimental/riff-raff-yaml-file/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe("The RiffRaffYamlFileExperimental class", () => {

expect(() => {
new RiffRaffYamlFileExperimental(app);
}).toThrowError("Unable to produce a working riff-raff.yaml file; missing 4 definitions");
}).toThrowError("Unable to produce a working riff-raff.yaml file; missing 1 definitions");
});

it("Should throw if there is an unresolved region", () => {
Expand Down
114 changes: 43 additions & 71 deletions src/experimental/riff-raff-yaml-file/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { writeFileSync } from "fs";
import path from "path";
import type { App } from "aws-cdk-lib";
import { Token } from "aws-cdk-lib";
import chalk from "chalk";
import { dump } from "js-yaml";
import { GuAutoScalingGroup } from "../../constructs/autoscaling";
import { GuStack } from "../../constructs/core";
Expand All @@ -12,7 +11,6 @@ import { addAmiParametersToCloudFormationDeployment, cloudFormationDeployment }
import { updateLambdaDeployment, uploadLambdaArtifact } from "./deployments/lambda";
import { groupByClassNameStackRegionStage } from "./group-by";
import type {
ClassName,
GroupedCdkStacks,
Region,
RiffRaffDeployment,
Expand Down Expand Up @@ -75,30 +73,18 @@ export class RiffRaffYamlFileExperimental {
private readonly riffRaffYaml: RiffRaffYaml;
private readonly outdir: string;

private isCdkStackPresent(
expectedClassName: ClassName,
expectedStack: StackTag,
expectedRegion: Region,
expectedStage: StageTag
): boolean {
private isCdkStackPresent(expectedStack: StackTag, expectedRegion: Region, expectedStage: StageTag): boolean {
const matches = this.allCdkStacks.find((cdkStack) => {
const {
constructor: { name },
stack,
stage,
region,
} = cdkStack;
return (
name === expectedClassName && stack === expectedStack && stage === expectedStage && region === expectedRegion
);
const { stack, stage, region } = cdkStack;
return stack === expectedStack && stage === expectedStage && region === expectedRegion;
});

return !!matches;
}

/**
* Check there are the appropriate number of `GuStack`s.
* Expect to find an instance for each combination of `GuStack`, `stack`, `region`, and `stage`.
* Expect to find an instance for each combination of `stack`, `region`, and `stage`.
*
* If not valid, a message is logged describing what is missing to aid debugging.
*
Expand Down Expand Up @@ -140,8 +126,6 @@ export class RiffRaffYamlFileExperimental {
* ```log
* Unable to produce a working riff-raff.yaml file; missing 1 definitions (details below)
*
* For the class: MyApplicationStack
*
* ┌───────────────┬────────────────────────────┐
* │ (index) │ eu-west-1 │
* ├───────────────┼────────────────────────────┤
Expand All @@ -155,48 +139,38 @@ export class RiffRaffYamlFileExperimental {
private validateStacksInApp(): void {
type Found = "✅";
type NotFound = "❌";
type AppValidation = Record<ClassName, Record<StackTag, Record<Region, Record<StageTag, Found | NotFound>>>>;
type AppValidation = Record<StackTag, Record<Region, Record<StageTag, Found | NotFound>>>;

const { allCdkStacks, allStackTags, allStageTags, allRegions } = this;
const { allStackTags, allStageTags, allRegions } = this;

const checks: AppValidation = allCdkStacks.reduce((accClassName, { constructor: { name } }) => {
const checks: AppValidation = allStackTags.reduce((accStackTag, stackTag) => {
return {
...accClassName,
[name]: allStackTags.reduce((accStackTag, stackTag) => {
...accStackTag,
[stackTag]: allRegions.reduce((accRegion, region) => {
return {
...accStackTag,
[stackTag]: allRegions.reduce((accRegion, region) => {
...accRegion,
[region]: allStageTags.reduce((accStageTag, stageTag) => {
return {
...accRegion,
[region]: allStageTags.reduce((accStageTag, stageTag) => {
return {
...accStageTag,
[stageTag]: this.isCdkStackPresent(name, stackTag, region, stageTag) ? "✅" : "❌",
};
}, {}),
...accStageTag,
[stageTag]: this.isCdkStackPresent(stackTag, region, stageTag) ? "✅" : "❌",
};
}, {}),
};
}, {}),
};
}, {});

const missingDefinitions = Object.values(checks).flatMap((groupedByStackTag) => {
return Object.values(groupedByStackTag).flatMap((groupedByRegion) => {
return Object.values(groupedByRegion).flatMap((groupedByStage) => {
return Object.values(groupedByStage).filter((_) => _ === "❌");
});
const missingDefinitions = Object.values(checks).flatMap((groupedByRegion) => {
return Object.values(groupedByRegion).flatMap((groupedByStage) => {
return Object.values(groupedByStage).filter((_) => _ === "❌");
});
});

if (missingDefinitions.length > 0) {
const message = `Unable to produce a working riff-raff.yaml file; missing ${missingDefinitions.length} definitions`;

console.log(`${message} (details below)`);
Object.entries(checks).forEach(([className, detail]) => {
console.log(`For the class: ${chalk.yellow(className)}`);
console.table(detail);
});
console.table(checks);

throw new Error(message);
}
Expand Down Expand Up @@ -235,42 +209,40 @@ export class RiffRaffYamlFileExperimental {

const groupedStacks: GroupedCdkStacks = groupByClassNameStackRegionStage(this.allCdkStacks);

Object.values(groupedStacks).forEach((stackTagGroup) => {
Object.values(stackTagGroup).forEach((regionGroup) => {
Object.values(regionGroup).forEach((stageGroup) => {
const stacks: GuStack[] = Object.values(stageGroup).flat();
Object.values(groupedStacks).forEach((regionGroup) => {
Object.values(regionGroup).forEach((stageGroup) => {
const stacks: GuStack[] = Object.values(stageGroup).flat();

if (stacks.length === 0) {
throw new Error("Unable to produce a working riff-raff.yaml file; there are no stacks!");
}
if (stacks.length === 0) {
throw new Error("Unable to produce a working riff-raff.yaml file; there are no stacks!");
}

// The items in `stacks` only differ by stage, so we can just use the first item in the list.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- length of `stacks` is checked above
const stack = stacks[0]!;
// The items in `stacks` only differ by stage, so we can just use the first item in the list.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- length of `stacks` is checked above
const stack = stacks[0]!;

const lambdas = this.getLambdas(stack);
const autoscalingGroups = this.getAutoScalingGroups(stack);
const lambdas = this.getLambdas(stack);
const autoscalingGroups = this.getAutoScalingGroups(stack);

const artifactUploads: RiffRaffDeployment[] = [
lambdas.map(uploadLambdaArtifact),
autoscalingGroups.map(uploadAutoscalingArtifact),
].flat();
artifactUploads.forEach(({ name, props }) => deployments.set(name, props));
const artifactUploads: RiffRaffDeployment[] = [
lambdas.map(uploadLambdaArtifact),
autoscalingGroups.map(uploadAutoscalingArtifact),
].flat();
artifactUploads.forEach(({ name, props }) => deployments.set(name, props));

const cfnDeployment = cloudFormationDeployment(stacks, artifactUploads, this.outdir);
deployments.set(cfnDeployment.name, cfnDeployment.props);
const cfnDeployment = cloudFormationDeployment(stacks, artifactUploads, this.outdir);
deployments.set(cfnDeployment.name, cfnDeployment.props);

lambdas.forEach((lambda) => {
const lambdaDeployment = updateLambdaDeployment(lambda, cfnDeployment);
deployments.set(lambdaDeployment.name, lambdaDeployment.props);
});
lambdas.forEach((lambda) => {
const lambdaDeployment = updateLambdaDeployment(lambda, cfnDeployment);
deployments.set(lambdaDeployment.name, lambdaDeployment.props);
});

autoscalingGroups.forEach((asg) => {
const asgDeployment = autoscalingDeployment(asg, cfnDeployment);
deployments.set(asgDeployment.name, asgDeployment.props);
autoscalingGroups.forEach((asg) => {
const asgDeployment = autoscalingDeployment(asg, cfnDeployment);
deployments.set(asgDeployment.name, asgDeployment.props);

deployments.set(cfnDeployment.name, addAmiParametersToCloudFormationDeployment(cfnDeployment, asg));
});
deployments.set(cfnDeployment.name, addAmiParametersToCloudFormationDeployment(cfnDeployment, asg));
});
});
});
Expand Down
7 changes: 1 addition & 6 deletions src/experimental/riff-raff-yaml-file/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// type aliases to, hopefully, improve readability
import type { GuStack } from "../../constructs/core";

export type ClassName = string;
export type StackTag = string;
export type StageTag = string;
export type Region = string;
Expand Down Expand Up @@ -32,12 +31,8 @@ export interface RiffRaffDeployment {

/*
The aim here is to produce an identity of a `GuStack` as a composite of:
- Class name
- Stack tag
- Region
Finally, group by stage to help form the `templateStagePaths` property for the `cloud-formation` deployment type in `riff-raff.yaml`.
*/
export type GroupedCdkStacks = Record<
ClassName,
Record<StackTag, Record<Region, Record<StageTag, CdkStacksDifferingOnlyByStage>>>
>;
export type GroupedCdkStacks = Record<StackTag, Record<Region, Record<StageTag, CdkStacksDifferingOnlyByStage>>>;

0 comments on commit 61f9e59

Please sign in to comment.