Skip to content

Commit 7918096

Browse files
committed
fix(riff-raff-generator): Correct checks for stack existence
1 parent 9ccc6c6 commit 7918096

2 files changed

Lines changed: 46 additions & 112 deletions

File tree

src/riff-raff-yaml-file/index.test.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,55 +1159,63 @@ describe("The RiffRaffYamlFile class", () => {
11591159
it("Should support cloudformation stacks that depend on other cloudformation stacks", () => {
11601160
const app = new App({ outdir: "/tmp/cdk.out" });
11611161

1162-
class SharedResourceStack extends GuStack {}
1162+
class DatabaseStack extends GuStack {}
11631163
class ApplicationStack extends GuStack {}
11641164

1165-
const sharedResources = new SharedResourceStack(app, "Shared-INFRA", {
1165+
const databaseCode = new DatabaseStack(app, "Database-CODE", {
11661166
env: {
11671167
region: "eu-west-1",
11681168
},
11691169
stack: "deploy",
1170-
stage: "INFRA",
1170+
stage: "CODE",
1171+
});
1172+
1173+
const databaseProd = new DatabaseStack(app, "Database-PROD", {
1174+
env: {
1175+
region: "eu-west-1",
1176+
},
1177+
stack: "deploy",
1178+
stage: "PROD",
11711179
});
11721180

1173-
const codeStack = new ApplicationStack(app, "App-CODE", {
1181+
const appCode = new ApplicationStack(app, "App-CODE", {
11741182
env: {
11751183
region: "eu-west-1",
11761184
},
11771185
stack: "deploy",
11781186
stage: "CODE",
11791187
});
11801188

1181-
const prodStack = new ApplicationStack(app, "App-PROD", {
1189+
const appProd = new ApplicationStack(app, "App-PROD", {
11821190
env: {
11831191
region: "eu-west-1",
11841192
},
11851193
stack: "deploy",
11861194
stage: "PROD",
11871195
});
11881196

1189-
codeStack.addDependency(sharedResources);
1190-
prodStack.addDependency(sharedResources);
1197+
appCode.addDependency(databaseCode);
1198+
appProd.addDependency(databaseProd);
11911199

11921200
const actual = new RiffRaffYamlFile(app).toYAML();
11931201

11941202
expect(actual).toMatchInlineSnapshot(`
11951203
"allowedStages:
1196-
- INFRA
11971204
- CODE
11981205
- PROD
11991206
deployments:
1200-
cfn-eu-west-1-deploy-shared-resource-stack:
1207+
cfn-eu-west-1-deploy-database-stack:
12011208
type: cloud-formation
12021209
regions:
12031210
- eu-west-1
12041211
stacks:
12051212
- deploy
1206-
app: shared-resource-stack
1213+
app: database-stack
12071214
contentDirectory: /tmp/cdk.out
12081215
parameters:
12091216
templateStagePaths:
1210-
INFRA: Shared-INFRA.template.json
1217+
CODE: Database-CODE.template.json
1218+
PROD: Database-PROD.template.json
12111219
cfn-eu-west-1-deploy-application-stack:
12121220
type: cloud-formation
12131221
regions:
@@ -1221,7 +1229,7 @@ describe("The RiffRaffYamlFile class", () => {
12211229
CODE: App-CODE.template.json
12221230
PROD: App-PROD.template.json
12231231
dependencies:
1224-
- cfn-eu-west-1-deploy-shared-resource-stack
1232+
- cfn-eu-west-1-deploy-database-stack
12251233
"
12261234
`);
12271235
});

src/riff-raff-yaml-file/index.ts

Lines changed: 26 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { updateLambdaDeployment, uploadLambdaArtifact } from "./deployments/lamb
1818
import { updateDeploymentParameters } from "./deployments/update-parameters";
1919
import { groupByClassName, groupByRegion, groupByStackTag, groupByStageTag } from "./group-by";
2020
import type {
21+
ClassName,
2122
Region,
2223
RiffRaffDeployment,
2324
RiffRaffDeploymentName,
@@ -47,100 +48,11 @@ function getGuStackDependencies(cdkStack: GuStack): GuStack[] {
4748
return cdkStack.dependencies.filter((_) => _ instanceof GuStack) as GuStack[];
4849
}
4950

50-
/**
51-
* Checks if a list of {@link GuStack} includes a stack with expected `stack` and `stage` properties.
52-
*/
53-
function isCdkStackPresent(stacks: GuStack[], expectedStack: StackTag, expectedStage: StageTag): boolean {
54-
const matches = stacks.find((cdkStack) => {
55-
const { stack, stage } = cdkStack;
56-
return stack === expectedStack && stage === expectedStage;
57-
});
58-
59-
return !!matches;
60-
}
61-
62-
/**
63-
* Check there are the appropriate number of `GuStack`s.
64-
* Expect to find an instance for each combination of `stack`, and `stage`.
65-
*
66-
* If not valid, a message is logged describing what is missing to aid debugging.
67-
*
68-
* Given the following:
69-
*
70-
* ```ts
71-
* const app = new App();
72-
*
73-
* class MyApplicationStack extends GuStack { }
74-
*
75-
* new MyApplicationStack(app, "App-CODE-deploy", {
76-
* env: {
77-
* region: "eu-west-1",
78-
* },
79-
* stack: "deploy",
80-
* stage: "CODE"
81-
* });
82-
*
83-
* new MyApplicationStack(app, "App-PROD-media-service", {
84-
* env: {
85-
* region: "eu-west-1",
86-
* },
87-
* stack: "media-service",
88-
* stage: "PROD",
89-
* });
90-
*
91-
* new MyApplicationStack(app, "App-PROD-deploy", {
92-
* env: {
93-
* region: "eu-west-1",
94-
* },
95-
* stack: "deploy",
96-
* stage: "PROD"
97-
* });
98-
* ```
99-
*
100-
* This will log a message like this, where ❌ denotes something missing,
101-
* specifically there is no `CODE` template for `media-service`.
102-
*
103-
* ```log
104-
* Unable to produce a working riff-raff.yaml file; missing 1 definitions (details below)
105-
*
106-
* ┌───────────────┬──────┬──────┐
107-
* │ (index) │ CODE │ PROD │
108-
* ├───────────────┼──────┼──────┤
109-
* │ deploy │ '✅' │ '✅' │
110-
* │ media-service │ '❌' │ '✅' │
111-
* └───────────────┴──────┴──────┘
112-
* ```
113-
*
114-
*/
115-
function validateStacksInApp(stacks: GuStack[], allStackTags: StackTag[], allStageTags: StageTag[]): void {
116-
type Found = "✅";
117-
type NotFound = "❌";
118-
type AppValidation = Record<StackTag, Record<StageTag, Found | NotFound>>;
119-
120-
const checks: AppValidation = allStackTags.reduce((accStackTag, stackTag) => {
121-
return {
122-
...accStackTag,
123-
[stackTag]: allStageTags.reduce((accStageTag, stageTag) => {
124-
return {
125-
...accStageTag,
126-
[stageTag]: isCdkStackPresent(stacks, stackTag, stageTag) ? "✅" : "❌",
127-
};
128-
}, {}),
129-
};
130-
}, {});
131-
132-
const missingDefinitions: Array<Found | NotFound> = Object.values(checks).flatMap((groupedByStackTag) => {
133-
return Object.values(groupedByStackTag).filter((_) => _ === "❌");
134-
});
135-
136-
if (missingDefinitions.length > 0) {
137-
const message = `Unable to produce a working riff-raff.yaml file; missing ${missingDefinitions.length} definitions`;
138-
139-
console.log(`${message} (details below)`);
140-
console.table(checks);
141-
142-
throw new Error(message);
143-
}
51+
interface MissingStack {
52+
className: ClassName;
53+
stack: StackTag;
54+
stage: StageTag;
55+
region: Region;
14456
}
14557

14658
/**
@@ -206,22 +118,28 @@ export class RiffRaffYamlFile {
206118
constructor(app: App) {
207119
const allCdkStacks = app.node.findAll().filter((_) => _ instanceof GuStack) as GuStack[];
208120
const allowedStages = new Set(allCdkStacks.map((_) => _.stage));
209-
const allStageTags = Array.from(allowedStages);
210-
const allStackTags = Array.from(new Set(allCdkStacks.map((_) => _.stack)));
211121
const allRegions = Array.from(new Set(allCdkStacks.map((_) => _.region)));
212122

213-
validateStacksInApp(allCdkStacks, allStackTags, allStageTags);
214123
validateAllRegionsAreResolved(allRegions);
215124

216125
this.outdir = app.outdir;
217126

218127
const deployments = new Map<RiffRaffDeploymentName, RiffRaffDeploymentProps>();
219128

220-
Object.values(groupByClassName(allCdkStacks)).forEach((stacksGroupedByClassName) => {
221-
Object.values(groupByStackTag(stacksGroupedByClassName)).forEach((stacksGroupedByStackTag) => {
222-
Object.values(groupByRegion(stacksGroupedByStackTag)).forEach((stacksGroupedByRegion) => {
129+
const missingStacks: MissingStack[] = [];
130+
const requiredStages = Array.from(allowedStages);
131+
132+
Object.entries(groupByClassName(allCdkStacks)).forEach(([className, stacksGroupedByClassName]) => {
133+
Object.entries(groupByStackTag(stacksGroupedByClassName)).forEach(([stackTag, stacksGroupedByStackTag]) => {
134+
Object.entries(groupByRegion(stacksGroupedByStackTag)).forEach(([region, stacksGroupedByRegion]) => {
223135
const stacks: GuStack[] = Object.values(groupByStageTag(stacksGroupedByRegion)).flat();
224136

137+
requiredStages.forEach((requiredStage) => {
138+
if (!stacks.find(({ stage }) => stage === requiredStage)) {
139+
missingStacks.push({ className, region, stack: stackTag, stage: requiredStage });
140+
}
141+
});
142+
225143
// The items in `stacks` only differ by stage, so we can just use the first item in the list.
226144
const [stack] = stacks;
227145

@@ -306,6 +224,14 @@ export class RiffRaffYamlFile {
306224
});
307225
});
308226

227+
if (missingStacks.length > 0) {
228+
const message = `Unable to produce a working riff-raff.yaml file; missing ${missingStacks.length} definitions`;
229+
console.log(`${message} (details below)`);
230+
console.table(missingStacks);
231+
232+
throw new Error(message);
233+
}
234+
309235
this.riffRaffYaml = {
310236
allowedStages,
311237
deployments,

0 commit comments

Comments
 (0)