Skip to content

Commit 477b9ee

Browse files
committed
Start adding CFN
1 parent ed9e490 commit 477b9ee

File tree

12 files changed

+335
-229
lines changed

12 files changed

+335
-229
lines changed

codegen/package-lock.json

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codegen/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"constructs": "^10.4.2",
1818
"fs-extra": "^11.3.0",
1919
"lodash": "^4.17.21",
20-
"prettier": "^3.5.3"
20+
"prettier": "^3.5.3",
21+
"yaml": "^2.7.1"
2122
}
2223
}

codegen/src/asset-account/index.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,38 @@
11
import { ArkErrors } from "arktype";
2-
import { inputs, InputsIn } from "./inputs";
2+
import { CloudFormationParams, TerraformParams } from "./params";
33
import * as mir from "./mir";
4-
import * as tf from "./orchestrator/terraform";
4+
import * as terraform from "./orchestrator/terraform";
5+
import * as cloudformation from "./orchestrator/cloudformation";
56

6-
export { inputs };
7+
export type CloudFormationParams = typeof CloudFormationParams.inferIn;
8+
export type TerraformParams = typeof TerraformParams.inferIn;
79

8-
export function generate(inputsIn: InputsIn) {
9-
const result = inputs(inputsIn);
10+
export function generateCloudFormation(paramsIn: CloudFormationParams) {
11+
const params = CloudFormationParams(paramsIn);
12+
return generate(params, cloudformation.generate);
13+
}
14+
15+
export function generateTerraform(paramsIn: TerraformParams) {
16+
const params = TerraformParams(paramsIn);
17+
return generate(params, terraform.generate);
18+
}
1019

11-
if (result instanceof ArkErrors) {
12-
throw new Error(`Invalid inputs: ${result}`);
20+
type Generator<P extends mir.Params, R> = (
21+
resources: Record<string, mir.Resource>,
22+
params: P,
23+
) => R;
24+
25+
function generate<P extends mir.Params, R, F extends Generator<P, R>>(
26+
params: P | ArkErrors,
27+
generator: F,
28+
): R {
29+
if (params instanceof ArkErrors) {
30+
throw new Error(`Invalid params: ${params}`);
1331
}
1432

15-
const resources = mir.resources(result);
33+
const resources = mir.resources(params);
1634

17-
tf.generate(resources);
18-
}
35+
params.tags["elastio:resource"] = "true";
1936

20-
generate({
21-
connectorAccountId: "123456789012",
22-
connectorRoleExternalId: "external-id",
23-
});
37+
return generator(resources, params);
38+
}

codegen/src/asset-account/mir/cloud-connector.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import * as iam from "../../common/iam";
22
import * as inventory from "../../common/inventory";
3-
import { Inputs } from "../inputs";
3+
import type { Params } from ".";
44
import _ from "lodash";
55
import { IamRole } from "./resource";
66

7-
export function cloudConnectorRole(inputs: Inputs): IamRole {
7+
export function cloudConnectorRole(params: Params): IamRole {
88
const otherStatements: Record<string, iam.PolicyStatement[]> = {
99
WriteEc2: [
1010
// Create and copy snapshots to the cloud connector account
@@ -41,7 +41,7 @@ export function cloudConnectorRole(inputs: Inputs): IamRole {
4141
Condition: {
4242
// Needed to add createVolumePermission for the connector account.
4343
StringEquals: {
44-
"ec2:Add/userId": inputs.connectorAccountId,
44+
"ec2:Add/userId": params.connectorAccountId,
4545
// Even though EC2 IAM reference says there are two more
4646
// things that could be used in the condition:
4747
// "ec2:Attribute" and "ec2:Attribute/${AttributeName}",
@@ -239,15 +239,15 @@ export function cloudConnectorRole(inputs: Inputs): IamRole {
239239

240240
Principal: {
241241
AWS:
242-
`arn:aws:iam::${inputs.connectorAccountId}:role/` +
243-
inputs.iamResourceNamesPrefix +
242+
`arn:aws:iam::${params.connectorAccountId}:role/` +
243+
params.iamResourceNamesPrefix +
244244
"ElastioCloudConnectorBastion" +
245-
inputs.iamResourceNamesSuffix,
245+
params.iamResourceNamesSuffix,
246246
},
247247

248248
Condition: {
249249
StringEquals: {
250-
"sts:ExternalId": inputs.connectorRoleExternalId,
250+
"sts:ExternalId": params.connectorRoleExternalId,
251251
},
252252
},
253253
},

codegen/src/asset-account/mir/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import type { Resource } from "./resource";
2-
import { Inputs } from "../inputs";
32
import { cloudConnectorRole } from "./cloud-connector";
3+
import { CloudFormationParams, TerraformParams } from "../params";
44

55
export { Resource };
66

7+
export type Params =
8+
| typeof CloudFormationParams.inferOut
9+
| typeof TerraformParams.inferOut;
10+
711
const version = "0.35.13";
812

9-
export function resources(inputs: Inputs): Record<string, Resource> {
13+
export function resources(inputs: Params): Record<string, Resource> {
1014
return {
1115
inventory_event_target: {
1216
type: "aws_iam_role",
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import _ from "lodash";
2+
import * as hclTools from "@cdktf/hcl-tools";
3+
import * as iam from "../../common/iam";
4+
import * as prettier from "prettier";
5+
import { CloudFormationParams } from "../params";
6+
import { Resource } from "../mir";
7+
8+
async function literal(value: unknown): Promise<string> {
9+
const json = JSON.stringify(value, null, 2);
10+
11+
const pretty = await prettier.format(json, { parser: "json" });
12+
return pretty.trim();
13+
}
14+
15+
async function jsonencode(value: unknown): Promise<string> {
16+
return `jsonencode(\n${await literal(value)}\n)`;
17+
}
18+
19+
function policyDocument(statements: iam.PolicyStatement[]) {
20+
return {
21+
Version: "2012-10-17",
22+
Statement: statements.map((statement) => ({
23+
Effect: statement.Effect ?? "Allow",
24+
...statement,
25+
})),
26+
};
27+
}
28+
29+
export type CloudFormationParams = typeof CloudFormationParams.inferOut;
30+
31+
export interface CloudFormationProject {
32+
files: Record<string, string>;
33+
}
34+
35+
export async function generate(
36+
resources: Record<string, Resource>,
37+
params: CloudFormationParams,
38+
): Promise<CloudFormationProject> {
39+
const parts = [
40+
`locals {
41+
tags = ${await literal(params.tags)}
42+
}`,
43+
];
44+
45+
for (const [id, resource] of Object.entries(resources)) {
46+
switch (resource.type) {
47+
case "aws_iam_role": {
48+
parts.push(
49+
`resource "aws_iam_role" ${literal(id)} {
50+
name = ${literal(resource.name)}
51+
tags = local.tags
52+
assume_role_policy = ${await jsonencode(policyDocument([resource.assumeRolePolicy]))}
53+
}`,
54+
);
55+
56+
const statements = Object.entries(resource.statements);
57+
58+
if (statements.length === 0) {
59+
continue;
60+
}
61+
62+
const policies = _.mapValues(
63+
resource.statements,
64+
(statement) => policyDocument(statement).Statement,
65+
);
66+
67+
parts.push(
68+
`resource "aws_iam_role_policy" ${literal(id)} {
69+
role = aws_iam_role.${id}.name
70+
name = each.key
71+
policy = jsonencode(
72+
{
73+
"Version": "2012-10-17",
74+
"Statement": each.value
75+
}
76+
)
77+
for_each = ${await literal(policies)}
78+
}`,
79+
);
80+
}
81+
case "aws_ssm_parameter": {
82+
}
83+
}
84+
}
85+
86+
parts.push(`
87+
data "aws_caller_identity" "current" {}
88+
locals {
89+
account_id = data.aws_caller_identity.current.account_id
90+
}
91+
`);
92+
93+
const content = parts
94+
.join("\n\n")
95+
.replaceAll("{{account_id}}", "${local.account_id}")
96+
.replaceAll(/\{\{(.*)\}\}/g, "${$1}");
97+
98+
const formatted = (await hclTools.format(content)).trim();
99+
100+
console.log(formatted);
101+
102+
return {
103+
files: {
104+
"main.tf": formatted,
105+
},
106+
};
107+
}

codegen/src/asset-account/orchestrator/terraform.ts

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import type { Resource } from "../mir";
21
import _ from "lodash";
32
import * as hclTools from "@cdktf/hcl-tools";
43
import * as iam from "../../common/iam";
54
import * as prettier from "prettier";
5+
import { TerraformParams } from "../params";
6+
import { Resource } from "../mir";
67

78
async function literal(value: unknown): Promise<string> {
89
const json = JSON.stringify(value, null, 2);
10+
911
const pretty = await prettier.format(json, { parser: "json" });
1012
return pretty.trim();
1113
}
@@ -18,21 +20,35 @@ function policyDocument(statements: iam.PolicyStatement[]) {
1820
return {
1921
Version: "2012-10-17",
2022
Statement: statements.map((statement) => ({
21-
...statement,
2223
Effect: statement.Effect ?? "Allow",
24+
...statement,
2325
})),
2426
};
2527
}
2628

27-
export async function generate(resources: Record<string, Resource>) {
28-
const parts = [];
29+
export type TerraformParams = typeof TerraformParams.inferOut;
30+
31+
export interface TerraformProject {
32+
files: Record<string, string>;
33+
}
34+
35+
export async function generate(
36+
resources: Record<string, Resource>,
37+
params: TerraformParams,
38+
): Promise<TerraformProject> {
39+
const parts = [
40+
`locals {
41+
tags = ${await literal(params.tags)}
42+
}`,
43+
];
2944

3045
for (const [id, resource] of Object.entries(resources)) {
3146
switch (resource.type) {
3247
case "aws_iam_role": {
3348
parts.push(
34-
`resource "aws_iam_role" "${id}" {
35-
name = "${resource.name}"
49+
`resource "aws_iam_role" ${literal(id)} {
50+
name = ${literal(resource.name)}
51+
tags = local.tags
3652
assume_role_policy = ${await jsonencode(policyDocument([resource.assumeRolePolicy]))}
3753
}`,
3854
);
@@ -49,7 +65,7 @@ export async function generate(resources: Record<string, Resource>) {
4965
);
5066

5167
parts.push(
52-
`resource "aws_iam_role_policy" "${id}" {
68+
`resource "aws_iam_role_policy" ${literal(id)} {
5369
role = aws_iam_role.${id}.name
5470
name = each.key
5571
policy = jsonencode(
@@ -79,9 +95,13 @@ export async function generate(resources: Record<string, Resource>) {
7995
.replaceAll("{{account_id}}", "${local.account_id}")
8096
.replaceAll(/\{\{(.*)\}\}/g, "${$1}");
8197

82-
console.log(await hclTools.format(content));
83-
}
98+
const formatted = (await hclTools.format(content)).trim();
99+
100+
console.log(formatted);
84101

85-
function camelCaseToSnakeCase(str: string): string {
86-
return str.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
102+
return {
103+
files: {
104+
"main.tf": formatted,
105+
},
106+
};
87107
}

0 commit comments

Comments
 (0)