@intentius/chant-lexicon-aws 0.124.46 → 0.124.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/capability-plugin.d.ts +1 -1
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/capability-plugin.js +6 -4
- package/dist/components/publish.d.ts +28 -97
- package/dist/components/publish.d.ts.map +1 -1
- package/dist/components/publish.js +42 -109
- package/dist/import/parser.d.ts +2 -0
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/import/parser.js +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +17 -0
- package/dist/workspace-member.d.ts +34 -0
- package/dist/workspace-member.d.ts.map +1 -0
- package/dist/workspace-member.js +179 -0
- package/package.json +6 -3
- package/schemas/cdk.schema.json +14 -0
- package/src/components/capability-plugin.ts +6 -4
- package/src/components/publish.ts +61 -193
- package/src/import/parser.ts +1 -1
- package/src/plugin.ts +16 -0
- package/src/testdata/cdk-member/cdk.json +1 -0
- package/src/testdata/cdk-member/cdk.out/AppStack.template.json +20 -0
- package/src/testdata/cdk-member/cdk.out/manifest.json +1 -0
- package/src/testdata/cdk-unsynthesized/cdk.json +1 -0
- package/src/testdata/cfn-member/README.txt +1 -0
- package/src/testdata/cfn-member/stack.yaml +36 -0
- package/src/workspace-kinds.test.ts +47 -0
- package/src/workspace-member.test.ts +94 -0
- package/src/workspace-member.ts +197 -0
- package/workspace-kinds.json +66 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
AWSTemplateFormatVersion: "2010-09-09"
|
|
2
|
+
Parameters:
|
|
3
|
+
Env:
|
|
4
|
+
Type: String
|
|
5
|
+
Resources:
|
|
6
|
+
Assets:
|
|
7
|
+
Type: AWS::S3::Bucket
|
|
8
|
+
Properties:
|
|
9
|
+
BucketName: !Sub "assets-${Env}-${AWS::Region}"
|
|
10
|
+
WebRole:
|
|
11
|
+
Type: AWS::IAM::Role
|
|
12
|
+
Properties:
|
|
13
|
+
AssumeRolePolicyDocument:
|
|
14
|
+
Statement: []
|
|
15
|
+
WebPolicy:
|
|
16
|
+
Type: AWS::IAM::Policy
|
|
17
|
+
Properties:
|
|
18
|
+
PolicyName: web
|
|
19
|
+
Roles:
|
|
20
|
+
- !Ref WebRole
|
|
21
|
+
PolicyDocument:
|
|
22
|
+
Statement:
|
|
23
|
+
- Effect: Allow
|
|
24
|
+
Action: s3:GetObject
|
|
25
|
+
Resource: !Sub "${Assets.Arn}/*"
|
|
26
|
+
Db:
|
|
27
|
+
Type: AWS::RDS::DBInstance
|
|
28
|
+
DependsOn: WebRole
|
|
29
|
+
Properties:
|
|
30
|
+
DBInstanceClass: db.t3.micro
|
|
31
|
+
Tags:
|
|
32
|
+
- Key: bucket
|
|
33
|
+
Value: !GetAtt Assets.DomainName
|
|
34
|
+
Outputs:
|
|
35
|
+
Bucket:
|
|
36
|
+
Value: !Ref Assets
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cloudformation and cdk member kinds this lexicon supplies to a chant
|
|
3
|
+
* workspace (#3179): data in `workspace-kinds.json`, exported at
|
|
4
|
+
* `./workspace-kinds`.
|
|
5
|
+
*/
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { describeWorkspaceKindConformance } from "@intentius/chant-test-utils";
|
|
9
|
+
import { checkMemberFile, loadMemberFileSchema, readPackageKinds } from "../../../packages/core/src/workspace/kinds";
|
|
10
|
+
|
|
11
|
+
const packageDir = join(import.meta.dirname, "..");
|
|
12
|
+
|
|
13
|
+
/** What each kind's own file must do: pass for a good one, fail for a bad one (#3672). */
|
|
14
|
+
function memberFileChecks(kind: string, good: string, bad: string): void {
|
|
15
|
+
describe(`${kind} member file`, () => {
|
|
16
|
+
const memberFile = readPackageKinds(packageDir).kinds.find((k) => k.name === kind)!.memberFile!;
|
|
17
|
+
it("names a schema that loads", () => {
|
|
18
|
+
expect(loadMemberFileSchema(memberFile, packageDir)).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
it("passes a valid file", () => {
|
|
21
|
+
expect(checkMemberFile(memberFile, memberFile.path, good)).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
it("fails an invalid one", () => {
|
|
24
|
+
expect(checkMemberFile(memberFile, memberFile.path, bad).length).toBeGreaterThan(0);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const template = "AWSTemplateFormatVersion: '2010-09-09'\nResources:\n B:\n Type: AWS::S3::Bucket\n";
|
|
30
|
+
|
|
31
|
+
describeWorkspaceKindConformance({
|
|
32
|
+
packageDir,
|
|
33
|
+
scenarios: [
|
|
34
|
+
{ name: "a YAML template", kind: "cloudformation", files: { "stack.yaml": template }, claims: true },
|
|
35
|
+
{ name: "a JSON template with only Resources", kind: "cloudformation", files: { "stack.json": '{"Resources": {}}' }, claims: true },
|
|
36
|
+
{ name: "a YAML template using short-form tags", kind: "cloudformation", files: { "stack.yml": template + "Outputs:\n N:\n Value: !Ref B\n Export:\n Name: !Sub \"${AWS::StackName}-n\"\n" }, claims: true },
|
|
37
|
+
{ name: "a template one level down", kind: "cloudformation", files: { "infra/stack.yaml": template }, claims: false },
|
|
38
|
+
{ name: "a Kubernetes manifest", kind: "cloudformation", files: { "deploy.yaml": "apiVersion: apps/v1\nkind: Deployment\n" }, claims: false },
|
|
39
|
+
{ name: "a package.json", kind: "cloudformation", files: { "package.json": '{"name": "x"}' }, claims: false },
|
|
40
|
+
{ name: "a CDK app", kind: "cdk", files: { "cdk.json": '{"app": "npx ts-node bin/app.ts"}', "package.json": "{}" }, claims: true },
|
|
41
|
+
{ name: "a CDK app one level down", kind: "cdk", files: { "infra/cdk.json": "{}" }, claims: false },
|
|
42
|
+
// cloudformation (320) outranks cdk (315) when both claim a directory.
|
|
43
|
+
{ name: "a CDK app with a template beside it", kind: "cloudformation", files: { "cdk.json": '{"app": "x"}', "stack.yaml": template }, claims: true },
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
memberFileChecks("cdk", '{"app": "npx ts-node bin/app.ts"}', '{"context": {}}');
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cloudformation member kind's graph block (#3179): the reader project the
|
|
3
|
+
* kind's graph config describes, run through chant graph over a fixture
|
|
4
|
+
* template. Nothing here calls AWS.
|
|
5
|
+
*/
|
|
6
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import type { GraphIR } from "@intentius/chant/graph-ir";
|
|
12
|
+
import { runGraph } from "@intentius/chant/cli/handlers/graph";
|
|
13
|
+
import { readPackageKinds, substituteGraphConfig } from "../../../packages/core/src/workspace/kinds";
|
|
14
|
+
import { awsPlugin } from "./plugin";
|
|
15
|
+
|
|
16
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const packageDir = join(here, "..");
|
|
18
|
+
const member = join(here, "testdata", "cfn-member");
|
|
19
|
+
|
|
20
|
+
const scratch: string[] = [];
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
vi.restoreAllMocks();
|
|
23
|
+
await Promise.all(scratch.splice(0).map((d) => rm(d, { recursive: true, force: true })));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
async function graphOf(dir: string, kind = "cloudformation"): Promise<{ ir: GraphIR; err: string[]; exit: number }> {
|
|
27
|
+
const graph = readPackageKinds(packageDir).kinds.find((k) => k.name === kind)!.graph!;
|
|
28
|
+
expect(graph.lexicon).toBe("aws");
|
|
29
|
+
const config = substituteGraphConfig(graph.config, { member: "net", dir, workspace: dirname(dir) });
|
|
30
|
+
const project = await mkdtemp(join(tmpdir(), "chant-cfn-member-"));
|
|
31
|
+
scratch.push(project);
|
|
32
|
+
await writeFile(join(project, "chant.config.json"), JSON.stringify({ lexicons: ["aws"], aws: config, graph: { lint: "report" } }));
|
|
33
|
+
const out: string[] = [];
|
|
34
|
+
const err: string[] = [];
|
|
35
|
+
vi.spyOn(console, "log").mockImplementation((line: unknown) => void out.push(String(line)));
|
|
36
|
+
vi.spyOn(console, "error").mockImplementation((line: unknown) => void err.push(String(line)));
|
|
37
|
+
const cwd = process.cwd();
|
|
38
|
+
process.chdir(project);
|
|
39
|
+
try {
|
|
40
|
+
const exit = await runGraph({ args: { command: "graph", path: ".", format: "ir" } as never, plugins: [awsPlugin], serializers: [] });
|
|
41
|
+
return { ir: exit === 0 ? (JSON.parse(out.join("\n")) as GraphIR) : { nodes: [], edges: [], groups: {} }, err, exit };
|
|
42
|
+
} finally {
|
|
43
|
+
process.chdir(cwd);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("cloudformation member graph block (#3179)", () => {
|
|
48
|
+
test("a template's resources are nodes", async () => {
|
|
49
|
+
const { ir, err, exit } = await graphOf(member);
|
|
50
|
+
expect(exit, err.join("\n")).toBe(0);
|
|
51
|
+
expect(ir.nodes.map((n) => [n.id, n.kind]).sort()).toEqual([
|
|
52
|
+
["net/Assets", "AWS::S3::Bucket"],
|
|
53
|
+
["net/Db", "AWS::RDS::DBInstance"],
|
|
54
|
+
["net/WebPolicy", "AWS::IAM::Policy"],
|
|
55
|
+
["net/WebRole", "AWS::IAM::Role"],
|
|
56
|
+
]);
|
|
57
|
+
expect(ir.meta?.awsMember).toEqual({ templates: ["stack.yaml"], objects: 4 });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("Ref, GetAtt, Sub and DependsOn are edges; parameters and pseudo parameters are not", async () => {
|
|
61
|
+
const { ir } = await graphOf(member);
|
|
62
|
+
const edges = ir.edges.map((e) => `${e.from} -> ${e.to} via ${e.viaAttr}${e.toAttr ? ` (${e.toAttr})` : ""}`).sort();
|
|
63
|
+
expect(edges).toEqual([
|
|
64
|
+
"net/Db -> net/Assets via Tags (DomainName)",
|
|
65
|
+
"net/Db -> net/WebRole via DependsOn",
|
|
66
|
+
"net/WebPolicy -> net/Assets via PolicyDocument (Arn)",
|
|
67
|
+
"net/WebPolicy -> net/WebRole via Roles",
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("cdk member graph block (#3179)", () => {
|
|
73
|
+
test("the synthesized assembly's templates are read; the app is not run", async () => {
|
|
74
|
+
const { ir, err, exit } = await graphOf(join(here, "testdata", "cdk-member"), "cdk");
|
|
75
|
+
expect(exit, err.join("\n")).toBe(0);
|
|
76
|
+
expect(ir.nodes.map((n) => n.id).sort()).toEqual(["net/HandlerGHI789", "net/HandlerRoleDEF456", "net/QueueABC123"]);
|
|
77
|
+
expect(ir.edges.map((e) => `${e.from} -> ${e.to} via ${e.viaAttr}`).sort()).toEqual([
|
|
78
|
+
"net/HandlerGHI789 -> net/HandlerRoleDEF456 via DependsOn",
|
|
79
|
+
"net/HandlerGHI789 -> net/HandlerRoleDEF456 via Role",
|
|
80
|
+
"net/HandlerGHI789 -> net/QueueABC123 via Environment",
|
|
81
|
+
]);
|
|
82
|
+
expect(ir.meta?.awsMember).toEqual({ templates: ["AppStack.template.json"], objects: 3, synthesized: true });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("an app with no assembly says synthesized: false and what to run", async () => {
|
|
86
|
+
const { ir, err, exit } = await graphOf(join(here, "testdata", "cdk-unsynthesized"), "cdk");
|
|
87
|
+
expect(exit, err.join("\n")).toBe(0);
|
|
88
|
+
expect(ir.nodes).toEqual([]);
|
|
89
|
+
const report = ir.meta?.awsMember as { synthesized: boolean; reason: string };
|
|
90
|
+
expect(report.synthesized).toBe(false);
|
|
91
|
+
expect(report.reason).toMatch(/cdk synth/);
|
|
92
|
+
expect(err.join("\n")).toMatch(/was not read/);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a cloudformation workspace member into the graph (#3179).
|
|
3
|
+
*
|
|
4
|
+
* `chant workspace graph` writes a reader project for a member of kind
|
|
5
|
+
* `cloudformation`, with `aws.member` in its config (see `workspace-kinds.json`).
|
|
6
|
+
* The plugin's `buildRoots` hook calls {@link readCloudFormationMember}: each
|
|
7
|
+
* CloudFormation template directly in the member's directory is parsed with the
|
|
8
|
+
* import parser's YAML schema (short-form tags included), and every resource
|
|
9
|
+
* becomes a node keyed `<label>/<LogicalId>`.
|
|
10
|
+
*
|
|
11
|
+
* Edges come from what the template says: `Ref`, `Fn::GetAtt` and the `${...}`
|
|
12
|
+
* references in `Fn::Sub`, wherever they sit in a resource's properties, plus
|
|
13
|
+
* `DependsOn`. Parameters, pseudo parameters and names that are not a resource
|
|
14
|
+
* of the same template (`AWS::Region`, an `Fn::ImportValue`) draw no edge.
|
|
15
|
+
* Nothing runs and no AWS call is made.
|
|
16
|
+
*
|
|
17
|
+
* A `cdk` member is read the same way from the cloud assembly its app already
|
|
18
|
+
* synthesized (`cdk.out`, or the `output` its cdk.json names): the assembly's
|
|
19
|
+
* `*.template.json` files are CloudFormation templates, and core's CDK
|
|
20
|
+
* assembly reader says whether the directory is one. chant does not run the
|
|
21
|
+
* app. With no assembly there is nothing to read, and the report says
|
|
22
|
+
* `synthesized: false` with the reason.
|
|
23
|
+
*/
|
|
24
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
25
|
+
import { basename, extname, join } from "node:path";
|
|
26
|
+
import yaml from "js-yaml";
|
|
27
|
+
import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
|
|
28
|
+
import type { EntityReference } from "@intentius/chant/graph-ir";
|
|
29
|
+
import { isCloudAssembly } from "@intentius/chant/cdk/assembly";
|
|
30
|
+
import { CF_SCHEMA } from "./import/parser";
|
|
31
|
+
|
|
32
|
+
export interface AwsMemberConfig {
|
|
33
|
+
/** `cloudformation` (the default) or `cdk`. */
|
|
34
|
+
kind?: "cloudformation" | "cdk";
|
|
35
|
+
/** The member's directory, absolute. */
|
|
36
|
+
dir: string;
|
|
37
|
+
/** Names the entities (`<label>/Bucket`): the member's name. */
|
|
38
|
+
label?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What the read found, carried on the graph's `meta.awsMember`. */
|
|
42
|
+
export interface AwsMemberReport {
|
|
43
|
+
/** The template files read, relative to the member's directory (or its assembly's). */
|
|
44
|
+
templates: string[];
|
|
45
|
+
objects: number;
|
|
46
|
+
/** CDK only: false when the app has no synthesized assembly to read. */
|
|
47
|
+
synthesized?: boolean;
|
|
48
|
+
/** Why nothing was read; set with `synthesized: false`. */
|
|
49
|
+
reason?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type Template = { Resources?: Record<string, { Type?: unknown; Properties?: Record<string, unknown>; DependsOn?: unknown }> };
|
|
53
|
+
type Name = { id: string; attr?: string };
|
|
54
|
+
|
|
55
|
+
function parseTemplate(text: string): Template | undefined {
|
|
56
|
+
let parsed: unknown;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(text);
|
|
59
|
+
} catch {
|
|
60
|
+
try {
|
|
61
|
+
parsed = yaml.load(text, { schema: CF_SCHEMA });
|
|
62
|
+
} catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
67
|
+
const t = parsed as Record<string, unknown>;
|
|
68
|
+
// The probe's two claims: a format version, or a Resources section.
|
|
69
|
+
const resources = t.Resources;
|
|
70
|
+
if (t.AWSTemplateFormatVersion === undefined && (!resources || typeof resources !== "object")) return undefined;
|
|
71
|
+
return t as Template;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Logical ids named by `${X}` and `${X.Attr}` in an `Fn::Sub` string, with the attribute when one is named. */
|
|
75
|
+
function subReferences(text: string): Name[] {
|
|
76
|
+
const out: Name[] = [];
|
|
77
|
+
for (const m of text.matchAll(/\$\{([^}!][^}]*)\}/g)) {
|
|
78
|
+
const [id, ...rest] = m[1]!.trim().split(".");
|
|
79
|
+
if (id) out.push({ id, ...(rest.length ? { attr: rest.join(".") } : {}) });
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Every name a value reaches through Ref, Fn::GetAtt or Fn::Sub. */
|
|
85
|
+
function collectNames(value: unknown, out: Name[]): void {
|
|
86
|
+
if (Array.isArray(value)) {
|
|
87
|
+
for (const v of value) collectNames(v, out);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!value || typeof value !== "object") return;
|
|
91
|
+
for (const [k, v] of Object.entries(value)) {
|
|
92
|
+
if (k === "Ref" && typeof v === "string") {
|
|
93
|
+
out.push({ id: v });
|
|
94
|
+
} else if (k === "Fn::GetAtt") {
|
|
95
|
+
const [id, attr] = Array.isArray(v) ? v : typeof v === "string" ? [v.slice(0, v.indexOf(".")), v.slice(v.indexOf(".") + 1)] : [];
|
|
96
|
+
if (typeof id === "string" && id) out.push({ id, ...(typeof attr === "string" && attr ? { attr } : {}) });
|
|
97
|
+
} else if (k === "Fn::Sub") {
|
|
98
|
+
const text = Array.isArray(v) ? v[0] : v;
|
|
99
|
+
if (typeof text === "string") out.push(...subReferences(text));
|
|
100
|
+
if (Array.isArray(v)) collectNames(v[1], out);
|
|
101
|
+
} else {
|
|
102
|
+
collectNames(v, out);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The directory a CDK app synthesizes into: cdk.json's `output`, else `cdk.out`. */
|
|
108
|
+
function cdkOutput(dir: string): string {
|
|
109
|
+
try {
|
|
110
|
+
const output = (JSON.parse(readFileSync(join(dir, "cdk.json"), "utf-8")) as { output?: unknown }).output;
|
|
111
|
+
if (typeof output === "string" && output) return output;
|
|
112
|
+
} catch {
|
|
113
|
+
/* no readable cdk.json: the default */
|
|
114
|
+
}
|
|
115
|
+
return "cdk.out";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function readCloudFormationMember(opts: { member: AwsMemberConfig }): {
|
|
119
|
+
entities: Map<string, Declarable>;
|
|
120
|
+
warnings: string[];
|
|
121
|
+
report: AwsMemberReport;
|
|
122
|
+
} {
|
|
123
|
+
const { member } = opts;
|
|
124
|
+
const label = member.label ?? basename(member.dir);
|
|
125
|
+
const warnings: string[] = [];
|
|
126
|
+
const templates: Array<{ file: string; template: Template }> = [];
|
|
127
|
+
let source = member.dir;
|
|
128
|
+
if (member.kind === "cdk") {
|
|
129
|
+
source = join(member.dir, cdkOutput(member.dir));
|
|
130
|
+
if (!isCloudAssembly(source)) {
|
|
131
|
+
const reason = `no synthesized cloud assembly at ${source}; run \`cdk synth\` in the app, and chant will read the templates it writes`;
|
|
132
|
+
warnings.push(`cdk member "${label}" was not read: ${reason}`);
|
|
133
|
+
return { entities: new Map(), warnings, report: { templates: [], objects: 0, synthesized: false, reason } };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const pattern = member.kind === "cdk" ? /\.template\.json$/ : /\.(ya?ml|json|template)$/;
|
|
137
|
+
for (const file of readdirSync(source).filter((f) => pattern.test(f)).sort()) {
|
|
138
|
+
const template = parseTemplate(readFileSync(join(source, file), "utf-8"));
|
|
139
|
+
if (template) templates.push({ file, template });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const entities = new Map<string, Declarable>();
|
|
143
|
+
for (const { file, template } of templates) {
|
|
144
|
+
// One template per member is the usual case and keeps ids short; two share a directory, so the file name qualifies them.
|
|
145
|
+
const prefix = templates.length === 1 ? label : `${label}/${basename(file, extname(file)).replace(/\.template$/, "")}`;
|
|
146
|
+
const resources = Object.entries(template.Resources ?? {}).filter(([, r]) => typeof r?.Type === "string");
|
|
147
|
+
const keyOf = new Map(resources.map(([id]) => [id, `${prefix}/${id}`]));
|
|
148
|
+
for (const [id, resource] of resources) {
|
|
149
|
+
const references = new Map<string, EntityReference>();
|
|
150
|
+
const add = (name: Name, via: string) => {
|
|
151
|
+
const to = keyOf.get(name.id);
|
|
152
|
+
if (!to || to === keyOf.get(id)) return;
|
|
153
|
+
references.set(`${to}|${via}|${name.attr ?? ""}`, { to, viaAttr: via, ...(name.attr ? { toAttr: name.attr } : {}) });
|
|
154
|
+
};
|
|
155
|
+
for (const [prop, value] of Object.entries(resource.Properties ?? {})) {
|
|
156
|
+
const names: Name[] = [];
|
|
157
|
+
collectNames(value, names);
|
|
158
|
+
for (const n of names) add(n, prop);
|
|
159
|
+
}
|
|
160
|
+
const dependsOn = resource.DependsOn;
|
|
161
|
+
for (const d of Array.isArray(dependsOn) ? dependsOn : dependsOn === undefined ? [] : [dependsOn]) {
|
|
162
|
+
if (typeof d === "string") add({ id: d }, "DependsOn");
|
|
163
|
+
}
|
|
164
|
+
const entity = {
|
|
165
|
+
lexicon: "aws",
|
|
166
|
+
entityType: resource.Type as string,
|
|
167
|
+
kind: "resource",
|
|
168
|
+
props: { ...(resource.Properties ?? {}) },
|
|
169
|
+
references: [...references.values()],
|
|
170
|
+
[DECLARABLE_MARKER]: true,
|
|
171
|
+
} as unknown as Declarable;
|
|
172
|
+
entities.set(keyOf.get(id)!, entity);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (templates.length === 0) warnings.push(`${member.kind ?? "cloudformation"} member "${label}" has no CloudFormation template directly in ${source}`);
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
entities,
|
|
179
|
+
warnings,
|
|
180
|
+
report: { templates: templates.map((t) => t.file), objects: entities.size, ...(member.kind === "cdk" ? { synthesized: true } : {}) },
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let lastReport: AwsMemberReport | undefined;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The report of the read in this process. `graphMeta` receives only the
|
|
188
|
+
* entities, so the hook that read the member leaves its report here for the
|
|
189
|
+
* hook that publishes it; `chant graph` runs the two in order in one process.
|
|
190
|
+
*/
|
|
191
|
+
export function setLastMemberReport(report: AwsMemberReport | undefined): void {
|
|
192
|
+
lastReport = report;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function lastMemberReport(): AwsMemberReport | undefined {
|
|
196
|
+
return lastReport;
|
|
197
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Member kinds this lexicon supplies to a chant workspace (#3179). chant reads this file as data and never imports the lexicon. The cloudformation graph block (#3179) reads the template as data (src/workspace-member.ts). The cdk graph block reads the cloud assembly the app already synthesized (cdk.out, or the output cdk.json names) with the same template reader; chant never runs the app, and with no assembly the member says synthesized: false on meta.awsMember.",
|
|
3
|
+
"schema": 1,
|
|
4
|
+
"kinds": [
|
|
5
|
+
{
|
|
6
|
+
"name": "cloudformation",
|
|
7
|
+
"description": "a CloudFormation template: a YAML or JSON file with AWSTemplateFormatVersion or Resources at its top",
|
|
8
|
+
"precedence": 320,
|
|
9
|
+
"probe": {
|
|
10
|
+
"anyDocument": {
|
|
11
|
+
"in": [
|
|
12
|
+
"*.yaml",
|
|
13
|
+
"*.yml",
|
|
14
|
+
"*.json",
|
|
15
|
+
"*.template"
|
|
16
|
+
],
|
|
17
|
+
"match": [
|
|
18
|
+
{
|
|
19
|
+
"/AWSTemplateFormatVersion": [
|
|
20
|
+
"*"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"/Resources": [
|
|
25
|
+
"*"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"graph": {
|
|
32
|
+
"lexicon": "aws",
|
|
33
|
+
"config": {
|
|
34
|
+
"member": {
|
|
35
|
+
"dir": "{dir}",
|
|
36
|
+
"label": "{member}"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "cdk",
|
|
43
|
+
"description": "an AWS CDK app, with a cdk.json in its directory",
|
|
44
|
+
"precedence": 315,
|
|
45
|
+
"probe": {
|
|
46
|
+
"anyFile": [
|
|
47
|
+
"cdk.json"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"memberFile": {
|
|
51
|
+
"path": "cdk.json",
|
|
52
|
+
"schema": "schemas/cdk.schema.json"
|
|
53
|
+
},
|
|
54
|
+
"graph": {
|
|
55
|
+
"lexicon": "aws",
|
|
56
|
+
"config": {
|
|
57
|
+
"member": {
|
|
58
|
+
"kind": "cdk",
|
|
59
|
+
"dir": "{dir}",
|
|
60
|
+
"label": "{member}"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|