Skip to content

Commit 9f12ba4

Browse files
committed
feat(riff-raff-generator)!: Support multiple Riff-Raff projects
1 parent 38b658d commit 9f12ba4

6 files changed

Lines changed: 453 additions & 122 deletions

File tree

.changeset/eleven-animals-pick.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
"@guardian/cdk": major
3+
---
4+
5+
Support generating multiple `riff-raff.yaml` files. To do this, set the `riffRaffProjectName` property of a `GuStack`.
6+
This is helpful in a few scenarios, for example if you have a singleton (INFRA) stack, and CODE/PROD application stacks.
7+
8+
In the following, two files will be produced:
9+
- `/path/to/cdk.out/deploy::core-infra/riff-raff.yaml`
10+
- `/path/to/cdk.out/deploy::my-app/riff-raff.yaml`
11+
12+
```ts
13+
class MyCoreInfraStack extends GuStack {}
14+
class MyApplicationStack extends GuStack {}
15+
16+
new MyCoreInfraStack(app, "MyCoreInfra", {
17+
stack: "deploy",
18+
stage: "INFRA",
19+
env: { region: "eu-west-1" },
20+
riffRaffProjectName: "deploy::core-infra",
21+
});
22+
23+
new MyApplicationStack(app, "MyApp-CODE", {
24+
stack: "deploy",
25+
stage: "CODE",
26+
env: { region: "eu-west-1" },
27+
riffRaffProjectName: "deploy::my-app",
28+
});
29+
30+
new MyApplicationStack(app, "MyApp-PROD", {
31+
stack: "deploy",
32+
stage: "PROD",
33+
env: { region: "eu-west-1" },
34+
riffRaffProjectName: "deploy::my-app",
35+
});
36+
```
37+
38+
BREAKING CHANGE: Within `RiffRaffYamlFile` the `riffRaffYaml` property has been removed.
39+
NOTE: If you're using `GuRoot`, this change should not impact you.
40+
41+
To migrate, use the `configuration` property:
42+
43+
```ts
44+
// BEFORE
45+
const app = new App();
46+
const riffRaff = new RiffRaffYamlFile(app);
47+
const deployments = riffRaff.riffRaffYaml.deployments;
48+
49+
const myStack = new MyStack(app, "my-stack", {
50+
stack: "playground",
51+
stage: "PROD",
52+
env: { region: "eu-west-1" },
53+
riffRaffProjectName: "playground::my-stack",
54+
});
55+
56+
deployments.set("additional-deployment", {
57+
type: "aws-s3",
58+
...
59+
});
60+
61+
// AFTER
62+
const app = new App();
63+
const riffRaff = new RiffRaffYamlFile(app);
64+
const { configuration } = riffRaff;
65+
66+
configuration.get("playground::my-stack")?.deployments.set("additional-deployment", {
67+
type: "aws-s3",
68+
...
69+
});
70+
```

src/riff-raff-yaml-file/README.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,52 @@ new MyStack(app, "my-stack-CODE", {});
3737
new MyStack(app, "my-stack-PROD", {});
3838
```
3939

40+
### Generating multiple `riff-raff.yaml` files
41+
Multiple `riff-raff.yaml` files can be generated by setting the `riffRaffProjectName` property of a `GuStack`.
42+
43+
In the example below, we have a singleton stack creating some core infrastructure and an application stack.
44+
We'll generate two `riff-raff.yaml` files:
45+
1. `/path/to/cdk.out/playground::core-infra/riff-raff.yaml`
46+
2. `/path/to/cdk.out/playground::api/riff-raff.yaml`.
47+
48+
```ts
49+
import { GuStack } from "@guardian/cdk/lib/constructs/core";
50+
import { GuRoot } from "@guardian/cdk/lib/constructs/root";
51+
52+
class CoreInfraStack extends GuStack {}
53+
class MyApi extends GuStack {}
54+
55+
const app = new GuRoot();
56+
57+
new CoreInfraStack(app, "core-infra", {
58+
stack: "playground",
59+
stage: "INFRA",
60+
riffRaffProjectName: "playground::core-infra",
61+
});
62+
63+
new MyApi(app, "api-code", {
64+
stack: "playground",
65+
stage: "CODE",
66+
riffRaffProjectName: "playground::api",
67+
});
68+
69+
new MyApi(app, "api-prod", {
70+
stack: "playground",
71+
stage: "PROD",
72+
riffRaffProjectName: "playground::api",
73+
});
74+
```
75+
4076
### Advanced usage
77+
#### Additional deployments
4178
As noted above, only specific deployment types are currently supported.
4279

4380
If you want to add additional deployment types, you can do so by instantiating `RiffRaffYamlFile` directly:
4481

4582
```ts
4683
import { App } from "aws-cdk-lib";
4784
import { RiffRaffYamlFile } from "@guardian/cdk/lib/riff-raff-yaml-file";
85+
import { UnknownRiffRaffProjectName } from "@guardian/cdk/lib/riff-raff-yaml-file/types";
4886

4987
const app = new App();
5088

@@ -55,9 +93,9 @@ const { stack, region } = new MyStack(app, "my-stack", {
5593
});
5694

5795
const riffRaff = new RiffRaffYamlFile(app);
58-
const { riffRaffYaml: { deployments } } = riffRaff;
96+
const { configuration } = riffRaff;
5997

60-
deployments.set("upload-my-static-files", {
98+
configuration.get(UnknownRiffRaffProjectName)?.deployments.set("upload-my-static-files", {
6199
actions: ["uploadStaticFiles"],
62100
app: "my-static-site",
63101
contentDirectory: "my-static-site",

src/riff-raff-yaml-file/group-by.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { GuStack } from "../constructs/core";
22
import { groupBy } from "../utils/array";
3-
import type { ClassName, Region, StackTag, StageTag } from "./types";
3+
import type { ClassName, Region, RiffRaffProjectName, StackTag, StageTag } from "./types";
4+
import { UnknownRiffRaffProjectName } from "./types";
45

56
export function groupByClassName(cdkStacks: GuStack[]): Record<ClassName, GuStack[]> {
67
return groupBy(cdkStacks, (stack) => stack.constructor.name);
@@ -17,3 +18,7 @@ export function groupByStageTag(cdkStacks: GuStack[]): Record<StageTag, GuStack[
1718
export function groupByRegion(cdkStacks: GuStack[]): Record<Region, GuStack[]> {
1819
return groupBy(cdkStacks, ({ region }) => region);
1920
}
21+
22+
export function groupByRiffRaffProjectName(cdkStacks: GuStack[]): Record<RiffRaffProjectName, GuStack[]> {
23+
return groupBy(cdkStacks, (_) => _.riffRaffProjectName ?? UnknownRiffRaffProjectName);
24+
}

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

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import { GuStack } from "../constructs/core";
99
import { GuLambdaFunction } from "../constructs/lambda";
1010
import { GuEc2AppExperimental } from "../experimental/patterns/ec2-app";
1111
import { GuEc2App, GuNodeApp, GuPlayApp, GuScheduledLambda } from "../patterns";
12-
import { RiffRaffYamlFile } from "./index";
12+
import { UnknownRiffRaffProjectName } from "./types";
13+
import { RiffRaffYamlFile, validateRiffRaffYamlDirectory } from "./index";
1314

1415
describe("The RiffRaffYamlFile class", () => {
1516
it("Should support deploying different GuStacks to multiple AWS accounts (aka Riff-Raff stacks), and regions", () => {
@@ -1110,7 +1111,7 @@ describe("The RiffRaffYamlFile class", () => {
11101111

11111112
const riffraff = new RiffRaffYamlFile(app);
11121113

1113-
riffraff.riffRaffYaml.deployments.set("upload-my-static-files", {
1114+
riffraff.configuration.get(UnknownRiffRaffProjectName)?.deployments.set("upload-my-static-files", {
11141115
app: "my-static-site",
11151116
contentDirectory: "my-static-site",
11161117
parameters: {
@@ -2008,5 +2009,157 @@ describe("The RiffRaffYamlFile class", () => {
20082009
new RiffRaffYamlFile(app);
20092010
}).toThrow("Unable to produce a working riff-raff.yaml file; missing 2 definitions");
20102011
});
2012+
2013+
it("should generate multiple configurations", () => {
2014+
const app = new App({ outdir: "/tmp/cdk.out" });
2015+
2016+
class MyCoreInfraStack extends GuStack {}
2017+
class MyApplicationStack extends GuStack {}
2018+
2019+
new MyCoreInfraStack(app, "MyCoreInfra", {
2020+
stack: "deploy",
2021+
stage: "INFRA",
2022+
env: { region: "eu-west-1" },
2023+
riffRaffProjectName: "deploy::core-infra",
2024+
});
2025+
2026+
new MyApplicationStack(app, "MyApp-CODE", {
2027+
stack: "deploy",
2028+
stage: "CODE",
2029+
env: { region: "eu-west-1" },
2030+
riffRaffProjectName: "deploy::my-app",
2031+
});
2032+
2033+
new MyApplicationStack(app, "MyApp-PROD", {
2034+
stack: "deploy",
2035+
stage: "PROD",
2036+
env: { region: "eu-west-1" },
2037+
riffRaffProjectName: "deploy::my-app",
2038+
});
2039+
2040+
const riffraff = new RiffRaffYamlFile(app);
2041+
2042+
expect(Array.from(riffraff.configuration.keys())).toEqual(["deploy::core-infra", "deploy::my-app"]);
2043+
2044+
expect(riffraff.toYAML("deploy::core-infra")).toMatchInlineSnapshot(`
2045+
"allowedStages:
2046+
- INFRA
2047+
deployments:
2048+
cfn-eu-west-1-deploy-my-core-infra-stack:
2049+
type: cloud-formation
2050+
regions:
2051+
- eu-west-1
2052+
stacks:
2053+
- deploy
2054+
app: my-core-infra-stack
2055+
contentDirectory: /tmp/cdk.out
2056+
parameters:
2057+
templateStagePaths:
2058+
INFRA: MyCoreInfra.template.json
2059+
"
2060+
`);
2061+
expect(riffraff.toYAML("deploy::my-app")).toMatchInlineSnapshot(`
2062+
"allowedStages:
2063+
- CODE
2064+
- PROD
2065+
deployments:
2066+
cfn-eu-west-1-deploy-my-application-stack:
2067+
type: cloud-formation
2068+
regions:
2069+
- eu-west-1
2070+
stacks:
2071+
- deploy
2072+
app: my-application-stack
2073+
contentDirectory: /tmp/cdk.out
2074+
parameters:
2075+
templateStagePaths:
2076+
CODE: MyApp-CODE.template.json
2077+
PROD: MyApp-PROD.template.json
2078+
"
2079+
`);
2080+
});
2081+
2082+
it("should throw when generating multiple configurations and there are missing definitions", () => {
2083+
const app = new App({ outdir: "/tmp/cdk.out" });
2084+
2085+
class MyCoreInfraStack extends GuStack {}
2086+
class MyApplicationStack extends GuStack {}
2087+
class MyDatabaseStack extends GuStack {}
2088+
2089+
new MyCoreInfraStack(app, "MyCoreInfra", {
2090+
stack: "deploy",
2091+
stage: "INFRA",
2092+
env: { region: "eu-west-1" },
2093+
riffRaffProjectName: "deploy::core-infra",
2094+
});
2095+
2096+
new MyApplicationStack(app, "MyApp-CODE", {
2097+
stack: "deploy",
2098+
stage: "CODE",
2099+
env: { region: "eu-west-1" },
2100+
riffRaffProjectName: "deploy::my-app",
2101+
});
2102+
2103+
new MyApplicationStack(app, "MyApp-PROD", {
2104+
stack: "deploy",
2105+
stage: "PROD",
2106+
env: { region: "eu-west-1" },
2107+
riffRaffProjectName: "deploy::my-app",
2108+
});
2109+
2110+
new MyDatabaseStack(app, "MyDatabase-PROD", {
2111+
stack: "deploy",
2112+
stage: "PROD",
2113+
env: { region: "eu-west-1" },
2114+
riffRaffProjectName: "deploy::my-app",
2115+
});
2116+
2117+
expect(() => {
2118+
new RiffRaffYamlFile(app);
2119+
}).toThrow("Unable to produce a working riff-raff.yaml file; missing 1 definitions");
2120+
});
2121+
});
2122+
});
2123+
2124+
describe("The validateRiffRaffYamlDirectory function", () => {
2125+
it("accepts a child directory", () => {
2126+
const actual = validateRiffRaffYamlDirectory("/tmp/cdk.out", "playground::my-project");
2127+
expect(actual).toBe("/tmp/cdk.out/playground::my-project");
2128+
});
2129+
2130+
it("accepts a grand-child directory", () => {
2131+
const actual = validateRiffRaffYamlDirectory("/tmp/cdk.out", "playground/my-project");
2132+
expect(actual).toBe("/tmp/cdk.out/playground/my-project");
2133+
});
2134+
2135+
it("accepts the empty string", () => {
2136+
const actual = validateRiffRaffYamlDirectory("/tmp/cdk.out", "");
2137+
expect(actual).toBe("/tmp/cdk.out");
2138+
});
2139+
2140+
it("accepts a .. directory name", () => {
2141+
const actual = validateRiffRaffYamlDirectory("/tmp/cdk.out", "..final");
2142+
expect(actual).toBe("/tmp/cdk.out/..final");
2143+
});
2144+
2145+
it("rejects a parent directory", () => {
2146+
expect(() => {
2147+
validateRiffRaffYamlDirectory("/tmp/cdk.out", "../playground::my-project");
2148+
}).toThrow(
2149+
"Directory traversal detected: '../playground::my-project' would create a directory outside of /tmp/cdk.out (riffRaffProjectName=../playground::my-project, resolvedPath='/tmp/playground::my-project', resolvedBase='/tmp/cdk.out')",
2150+
);
2151+
});
2152+
2153+
it("accepts an ancestor directory", () => {
2154+
const actual = validateRiffRaffYamlDirectory("/tmp/cdk.out", "child/../final");
2155+
expect(actual).toBe("/tmp/cdk.out/final");
2156+
});
2157+
2158+
it("rejects a multi-ancestor directory", () => {
2159+
expect(() => {
2160+
validateRiffRaffYamlDirectory("/tmp/cdk.out", "child/../somewhere/../../final");
2161+
}).toThrow(
2162+
"Directory traversal detected: 'child/../somewhere/../../final' would create a directory outside of /tmp/cdk.out (riffRaffProjectName=child/../somewhere/../../final, resolvedPath='/tmp/final', resolvedBase='/tmp/cdk.out')",
2163+
);
20112164
});
20122165
});

0 commit comments

Comments
 (0)