@intentius/chant 0.52.1 → 0.53.0
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/agents/checks.d.ts +35 -0
- package/dist/agents/checks.d.ts.map +1 -0
- package/dist/agents/discover.d.ts +86 -0
- package/dist/agents/discover.d.ts.map +1 -0
- package/dist/agents/importer.d.ts +46 -0
- package/dist/agents/importer.d.ts.map +1 -0
- package/dist/agents/index.d.ts +14 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/types.d.ts +196 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/audit/catalog.d.ts +4 -1
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/report.d.ts +8 -0
- package/dist/audit/report.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/cdk/advise.d.ts +29 -0
- package/dist/cdk/advise.d.ts.map +1 -0
- package/dist/cdk/assembly.d.ts +38 -0
- package/dist/cdk/assembly.d.ts.map +1 -0
- package/dist/cdk/graph.d.ts +68 -0
- package/dist/cdk/graph.d.ts.map +1 -0
- package/dist/cdk/tier-map.d.ts +44 -0
- package/dist/cdk/tier-map.d.ts.map +1 -0
- package/dist/cdk/types.d.ts +114 -0
- package/dist/cdk/types.d.ts.map +1 -0
- package/dist/cli/commands/audit-agents.d.ts +83 -0
- package/dist/cli/commands/audit-agents.d.ts.map +1 -0
- package/dist/cli/commands/carve-apply.d.ts.map +1 -1
- package/dist/cli/commands/carve-bridge.d.ts.map +1 -1
- package/dist/cli/commands/carve-emit.d.ts.map +1 -1
- package/dist/cli/commands/carve.d.ts +48 -6
- package/dist/cli/commands/carve.d.ts.map +1 -1
- package/dist/cli/commands/import-agents.d.ts +64 -0
- package/dist/cli/commands/import-agents.d.ts.map +1 -0
- package/dist/cli/handlers/carve-emit.d.ts.map +1 -1
- package/dist/cli/handlers/carve.d.ts +5 -4
- package/dist/cli/handlers/carve.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +10 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/misc.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +15 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/identity.d.ts +196 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon.d.ts +52 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/terraform/adopt-state.d.ts +17 -63
- package/dist/terraform/adopt-state.d.ts.map +1 -1
- package/dist/terraform/aws-resources.d.ts +1 -1
- package/dist/terraform/bridge.d.ts.map +1 -1
- package/dist/terraform/carve-provider.d.ts +142 -0
- package/dist/terraform/carve-provider.d.ts.map +1 -0
- package/dist/terraform/carve.d.ts +36 -3
- package/dist/terraform/carve.d.ts.map +1 -1
- package/dist/terraform/emit-source.d.ts +25 -0
- package/dist/terraform/emit-source.d.ts.map +1 -0
- package/dist/terraform/graduate.d.ts +11 -1
- package/dist/terraform/graduate.d.ts.map +1 -1
- package/dist/terraform/providers/aws.d.ts +19 -0
- package/dist/terraform/providers/aws.d.ts.map +1 -0
- package/dist/terraform/providers/gcp.d.ts +41 -0
- package/dist/terraform/providers/gcp.d.ts.map +1 -0
- package/dist/terraform/providers/index.d.ts +15 -0
- package/dist/terraform/providers/index.d.ts.map +1 -0
- package/dist/terraform/providers/kubernetes.d.ts +29 -0
- package/dist/terraform/providers/kubernetes.d.ts.map +1 -0
- package/dist/terraform/score.d.ts +70 -4
- package/dist/terraform/score.d.ts.map +1 -1
- package/dist/terraform/tier-map.d.ts +38 -26
- package/dist/terraform/tier-map.d.ts.map +1 -1
- package/dist/terraform/types.d.ts +6 -0
- package/dist/terraform/types.d.ts.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/agents/checks.test.ts +228 -0
- package/src/agents/checks.ts +429 -0
- package/src/agents/discover.test.ts +310 -0
- package/src/agents/discover.ts +939 -0
- package/src/agents/importer.ts +49 -0
- package/src/agents/index.ts +29 -0
- package/src/agents/types.ts +207 -0
- package/src/audit/catalog.ts +90 -1
- package/src/audit/report.ts +9 -1
- package/src/audit/rules-doc.ts +6 -0
- package/src/cdk/__fixtures__/cdk.out/AppStack.template.json +171 -0
- package/src/cdk/__fixtures__/cdk.out/DataStack.template.json +90 -0
- package/src/cdk/__fixtures__/cdk.out/cdk.out +1 -0
- package/src/cdk/__fixtures__/cdk.out/manifest.json +30 -0
- package/src/cdk/__fixtures__/cdk.out/tree.json +201 -0
- package/src/cdk/__fixtures__/cdk.out-dummy/LookupStack.template.json +29 -0
- package/src/cdk/__fixtures__/cdk.out-dummy/manifest.json +26 -0
- package/src/cdk/advise.test.ts +208 -0
- package/src/cdk/advise.ts +44 -0
- package/src/cdk/assembly.ts +133 -0
- package/src/cdk/graph.test.ts +206 -0
- package/src/cdk/graph.ts +525 -0
- package/src/cdk/tier-map.ts +71 -0
- package/src/cdk/types.ts +115 -0
- package/src/cli/commands/audit-agents.test.ts +260 -0
- package/src/cli/commands/audit-agents.ts +387 -0
- package/src/cli/commands/carve-apply.ts +20 -4
- package/src/cli/commands/carve-bridge.test.ts +30 -0
- package/src/cli/commands/carve-bridge.ts +24 -2
- package/src/cli/commands/carve-emit-k8s.test.ts +262 -0
- package/src/cli/commands/carve-emit-provider.test.ts +207 -0
- package/src/cli/commands/carve-emit.test.ts +72 -1
- package/src/cli/commands/carve-emit.ts +55 -28
- package/src/cli/commands/carve.ts +139 -36
- package/src/cli/commands/import-agents.test.ts +208 -0
- package/src/cli/commands/import-agents.ts +196 -0
- package/src/cli/handlers/carve-emit.ts +8 -1
- package/src/cli/handlers/carve.ts +8 -7
- package/src/cli/handlers/lifecycle.test.ts +187 -1
- package/src/cli/handlers/lifecycle.ts +125 -1
- package/src/cli/handlers/misc.ts +111 -0
- package/src/cli/main.ts +29 -5
- package/src/cli/registry.ts +15 -0
- package/src/identity.test.ts +199 -0
- package/src/identity.ts +346 -0
- package/src/index.ts +1 -0
- package/src/lexicon.ts +65 -0
- package/src/terraform/__fixtures__/gcp-estate/main.tf +60 -0
- package/src/terraform/adopt-state.test.ts +131 -0
- package/src/terraform/adopt-state.ts +22 -167
- package/src/terraform/aws-resources.test.ts +55 -16
- package/src/terraform/aws-resources.ts +1 -1
- package/src/terraform/bridge.test.ts +12 -0
- package/src/terraform/bridge.ts +4 -1
- package/src/terraform/carve-provider.test.ts +155 -0
- package/src/terraform/carve-provider.ts +237 -0
- package/src/terraform/carve.test.ts +55 -1
- package/src/terraform/carve.ts +0 -0
- package/src/terraform/emit-source.ts +39 -0
- package/src/terraform/graduate.test.ts +37 -0
- package/src/terraform/graduate.ts +55 -7
- package/src/terraform/graph.ts +3 -3
- package/src/terraform/providers/aws.ts +169 -0
- package/src/terraform/providers/gcp.test.ts +228 -0
- package/src/terraform/providers/gcp.ts +329 -0
- package/src/terraform/providers/index.ts +21 -0
- package/src/terraform/providers/kubernetes.ts +224 -0
- package/src/terraform/score.ts +111 -25
- package/src/terraform/tier-map.ts +55 -90
- package/src/terraform/types.ts +6 -0
- package/src/yaml.test.ts +54 -0
- package/src/yaml.ts +24 -3
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a CDK cloud assembly off disk (#1056).
|
|
3
|
+
*
|
|
4
|
+
* The only filesystem layer the CDK advisor needs. Detection is by content, not
|
|
5
|
+
* a flag: a directory holding `manifest.json` plus at least one
|
|
6
|
+
* `*.template.json` is a cloud assembly, so `chant carve advise --from ./cdk.out`
|
|
7
|
+
* routes itself.
|
|
8
|
+
*
|
|
9
|
+
* A malformed or truncated assembly degrades rather than throws — a stack whose
|
|
10
|
+
* template will not parse is dropped with a diagnostic, and the rest is still
|
|
11
|
+
* ranked. The advisor is read-only, so a partial read costs nothing but a
|
|
12
|
+
* shorter report.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import type { CdkManifest, CdkStack, CdkTreeFile, CdkTreeNode, CfnTemplate, CloudAssembly } from "./types";
|
|
18
|
+
|
|
19
|
+
/** Every `*.template.json` directly under `dir`. */
|
|
20
|
+
function listTemplates(dir: string): string[] {
|
|
21
|
+
return readdirSync(dir)
|
|
22
|
+
.filter((f) => f.endsWith(".template.json"))
|
|
23
|
+
.sort();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Is `dir` a CDK cloud assembly? `manifest.json` plus at least one synthesized
|
|
28
|
+
* template. Both are required: `manifest.json` alone is far too common a
|
|
29
|
+
* filename to claim on sight.
|
|
30
|
+
*/
|
|
31
|
+
export function isCloudAssembly(dir: string): boolean {
|
|
32
|
+
try {
|
|
33
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) return false;
|
|
34
|
+
if (!existsSync(join(dir, "manifest.json"))) return false;
|
|
35
|
+
return listTemplates(dir).length > 0;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Why a Terraform-only carve phase refuses a cloud assembly (#1056).
|
|
43
|
+
*
|
|
44
|
+
* Advise is the only phase that reads CDK. Bridging a carve means rewriting the
|
|
45
|
+
* surviving app's source so it reimports what was taken, and CDK source is
|
|
46
|
+
* TypeScript, Python, Java or C# depending on who wrote it — nothing that
|
|
47
|
+
* generalizes the way patching HCL does. Better to say so than to parse the
|
|
48
|
+
* assembly's `.tf` files, find none, and report an empty estate.
|
|
49
|
+
*/
|
|
50
|
+
export function cdkNotSupported(dir: string, phase: "emit" | "bridge" | "apply"): string {
|
|
51
|
+
return (
|
|
52
|
+
`${dir} is a CDK cloud assembly, and \`chant carve ${phase}\` reads Terraform. ` +
|
|
53
|
+
`\`chant carve advise --from ${dir}\` ranks it read-only; emit, bridge and apply are Terraform-only.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readJson<T>(path: string): T | undefined {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(readFileSync(path, "utf-8")) as T;
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The stack's construct path. `displayName` carries it when the stack sits in a
|
|
67
|
+
* Stage (`Stage/AppStack` for artifact id `Stage-AppStack`); otherwise the
|
|
68
|
+
* artifact id is the path.
|
|
69
|
+
*/
|
|
70
|
+
function stackPath(id: string, displayName?: string): string {
|
|
71
|
+
return typeof displayName === "string" && displayName.length > 0 ? displayName : id;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Index `tree.json` by construct path, so a resource's path resolves in one lookup. */
|
|
75
|
+
export function indexTree(root: CdkTreeNode | undefined): Map<string, CdkTreeNode> {
|
|
76
|
+
const byPath = new Map<string, CdkTreeNode>();
|
|
77
|
+
const visit = (node: CdkTreeNode): void => {
|
|
78
|
+
if (typeof node.path === "string") byPath.set(node.path, node);
|
|
79
|
+
for (const child of Object.values(node.children ?? {})) visit(child);
|
|
80
|
+
};
|
|
81
|
+
if (root) visit(root);
|
|
82
|
+
return byPath;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Read the assembly at `dir`. Throws only when the directory is not an assembly
|
|
87
|
+
* at all — everything past that point is a diagnostic.
|
|
88
|
+
*/
|
|
89
|
+
export function readCloudAssembly(dir: string): CloudAssembly {
|
|
90
|
+
const diagnostics: string[] = [];
|
|
91
|
+
const manifest = readJson<CdkManifest>(join(dir, "manifest.json"));
|
|
92
|
+
if (!manifest) {
|
|
93
|
+
throw new Error(`${join(dir, "manifest.json")} is not readable JSON — not a CDK cloud assembly.`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const tree = readJson<CdkTreeFile>(join(dir, "tree.json"))?.tree;
|
|
97
|
+
if (!tree) {
|
|
98
|
+
diagnostics.push(
|
|
99
|
+
"No tree.json in the assembly — CloudFormation resources are grouped by their construct path alone, " +
|
|
100
|
+
"so sibling constructs of one L2 (a function's role and policy, say) rank separately instead of folding in.",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const stacks: CdkStack[] = [];
|
|
105
|
+
const artifacts = Object.entries(manifest.artifacts ?? {});
|
|
106
|
+
for (const [id, artifact] of artifacts) {
|
|
107
|
+
if (artifact?.type !== "aws:cloudformation:stack") continue;
|
|
108
|
+
const templateFile = artifact.properties?.templateFile;
|
|
109
|
+
if (typeof templateFile !== "string") {
|
|
110
|
+
diagnostics.push(`Stack artifact ${id} names no templateFile — skipped.`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const template = readJson<CfnTemplate>(join(dir, templateFile));
|
|
114
|
+
if (!template) {
|
|
115
|
+
diagnostics.push(`Could not read ${templateFile} for stack ${id} — skipped.`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
stacks.push({ id, path: stackPath(id, artifact.displayName), templateFile, template });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (stacks.length === 0) {
|
|
122
|
+
// Templates exist (isCloudAssembly said so) but no artifact claims them —
|
|
123
|
+
// an assembly written by something other than `cdk synth`, or a manifest
|
|
124
|
+
// from a version this does not understand. Say so rather than reporting an
|
|
125
|
+
// empty estate as if the app were empty.
|
|
126
|
+
diagnostics.push(
|
|
127
|
+
"The manifest declares no aws:cloudformation:stack artifacts, so no template was ranked. " +
|
|
128
|
+
"Re-synthesize with `cdk synth` and point --from at the resulting cdk.out.",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { dir, manifest, tree, stacks: stacks.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)), diagnostics };
|
|
133
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { cdkNotSupported, isCloudAssembly, readCloudAssembly } from "./assembly";
|
|
4
|
+
import { buildCdkGraph, constructLevel, dummyAssemblyReason } from "./graph";
|
|
5
|
+
import { CFN_TIER_MAP, isScaffoldingParameter, resolveCfnTier } from "./tier-map";
|
|
6
|
+
import { AWS_CARVE_TYPES } from "../terraform/aws-resources";
|
|
7
|
+
|
|
8
|
+
const FIXTURES = join(__dirname, "__fixtures__");
|
|
9
|
+
const ASSEMBLY = join(FIXTURES, "cdk.out");
|
|
10
|
+
const DUMMY = join(FIXTURES, "cdk.out-dummy");
|
|
11
|
+
|
|
12
|
+
describe("cloud assembly detection", () => {
|
|
13
|
+
test("a directory with a manifest and a synthesized template is an assembly", () => {
|
|
14
|
+
expect(isCloudAssembly(ASSEMBLY)).toBe(true);
|
|
15
|
+
expect(isCloudAssembly(DUMMY)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("a directory that is not one is not claimed", () => {
|
|
19
|
+
// manifest.json alone is far too common a filename to claim on sight; a
|
|
20
|
+
// Terraform estate and a missing directory are both plainly not assemblies.
|
|
21
|
+
expect(isCloudAssembly(join(__dirname, "..", "terraform", "__fixtures__", "sample-estate"))).toBe(false);
|
|
22
|
+
expect(isCloudAssembly(join(FIXTURES, "definitely-not-here"))).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("the Terraform-only phases refuse an assembly by name", () => {
|
|
26
|
+
const message = cdkNotSupported("./cdk.out", "emit");
|
|
27
|
+
expect(message).toContain("carve emit");
|
|
28
|
+
expect(message).toContain("carve advise --from ./cdk.out");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("the CloudFormation tier map", () => {
|
|
33
|
+
test("is the AWS carve-out table read from the other end", () => {
|
|
34
|
+
// Every carvable Terraform type names a CloudFormation type, and that type
|
|
35
|
+
// resolves to the same tier from this side. No second table to keep in step.
|
|
36
|
+
for (const entry of AWS_CARVE_TYPES) {
|
|
37
|
+
expect(resolveCfnTier(entry.nativeType)).toEqual({ tier: entry.tier, mapsTo: entry.nativeType });
|
|
38
|
+
}
|
|
39
|
+
expect(Object.keys(CFN_TIER_MAP).length).toBeGreaterThan(70);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("an unmapped CloudFormation type resolves to nothing", () => {
|
|
43
|
+
expect(resolveCfnTier("AWS::CloudFormation::Stack")).toBeNull();
|
|
44
|
+
expect(resolveCfnTier("AWS::S3::BucketPolicy")).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("synthesizer parameters are told apart from an author's", () => {
|
|
48
|
+
expect(isScaffoldingParameter("BootstrapVersion")).toBe(true);
|
|
49
|
+
expect(isScaffoldingParameter("AssetParameters1a2b3c4dS3BucketA1B2C3")).toBe(true);
|
|
50
|
+
expect(isScaffoldingParameter("EnvName")).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("construct levels", () => {
|
|
55
|
+
test("classify by jsii class", () => {
|
|
56
|
+
expect(constructLevel("aws-cdk-lib.aws_s3.CfnBucket")).toBe("l1");
|
|
57
|
+
expect(constructLevel("aws-cdk-lib.aws_s3.Bucket")).toBe("l2");
|
|
58
|
+
expect(constructLevel("@aws-cdk/aws-s3.Bucket")).toBe("l2");
|
|
59
|
+
expect(constructLevel("constructs.Construct")).toBe("l3");
|
|
60
|
+
expect(constructLevel("my-app.ApiConstruct")).toBe("l3");
|
|
61
|
+
expect(constructLevel("aws-cdk-lib.NestedStack")).toBe("nested");
|
|
62
|
+
expect(constructLevel("aws-cdk-lib.Stack")).toBe("container");
|
|
63
|
+
expect(constructLevel(undefined)).toBe("unknown");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("buildCdkGraph", () => {
|
|
68
|
+
const { graph, signals, diagnostics } = buildCdkGraph(readCloudAssembly(ASSEMBLY));
|
|
69
|
+
const addresses = graph.nodes.map((n) => n.address);
|
|
70
|
+
const node = (address: string) => graph.nodes.find((n) => n.address === address);
|
|
71
|
+
|
|
72
|
+
test("ranks constructs, not CloudFormation resources", () => {
|
|
73
|
+
expect(addresses).toEqual([
|
|
74
|
+
"AppStack/Api",
|
|
75
|
+
"AppStack/Handler",
|
|
76
|
+
"AppStack/Legacy",
|
|
77
|
+
"AppStack/Reports.NestedStack",
|
|
78
|
+
"DataStack/Assets",
|
|
79
|
+
"DataStack/Table",
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("one L2 emitting three resources is one node, with its members named", () => {
|
|
84
|
+
expect(node("AppStack/Handler")).toMatchObject({ kind: "resource", type: "AWS::Lambda::Function" });
|
|
85
|
+
expect(signals.get("AppStack/Handler")?.members?.map((m) => m.type)).toEqual([
|
|
86
|
+
"AWS::Lambda::Function",
|
|
87
|
+
"AWS::IAM::Policy",
|
|
88
|
+
"AWS::IAM::Role",
|
|
89
|
+
]);
|
|
90
|
+
// The role and the policy are not separately rankable — they carve with it.
|
|
91
|
+
expect(addresses).not.toContain("AppStack/Handler/ServiceRole");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("an L3 subtree is one Composite candidate, ranked as a module", () => {
|
|
95
|
+
expect(node("AppStack/Api")).toMatchObject({ kind: "module" });
|
|
96
|
+
expect(node("AppStack/Api")?.type).toBeUndefined();
|
|
97
|
+
expect(signals.get("AppStack/Api")?.members).toHaveLength(3);
|
|
98
|
+
expect(signals.get("AppStack/Api")?.notes?.join(" ")).toContain("Composite candidate");
|
|
99
|
+
expect(addresses).not.toContain("AppStack/Api/Queue");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("a bare L1 stands on its own", () => {
|
|
103
|
+
expect(node("AppStack/Legacy")).toMatchObject({ kind: "resource", type: "AWS::S3::Bucket" });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("synthesis scaffolding never appears", () => {
|
|
107
|
+
expect(addresses.some((a) => a.includes("CDKMetadata"))).toBe(false);
|
|
108
|
+
for (const members of [...signals.values()].map((s) => s.members ?? [])) {
|
|
109
|
+
expect(members.some((m) => m.type === "AWS::CDK::Metadata")).toBe(false);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("references inside one construct are not edges", () => {
|
|
114
|
+
// The bucket policy Refs its bucket and the function Refs its own role;
|
|
115
|
+
// both are internal to a carve set, so neither is boundary work.
|
|
116
|
+
const internal = graph.edges.filter((e) => e.from === e.to);
|
|
117
|
+
expect(internal).toHaveLength(0);
|
|
118
|
+
expect(graph.edges.filter((e) => e.from === "DataStack/Assets")).toHaveLength(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("an intra-stack Ref is an edge carrying the property it sits in", () => {
|
|
122
|
+
expect(graph.edges).toContainEqual({
|
|
123
|
+
from: "AppStack/Handler",
|
|
124
|
+
to: "AppStack/Api",
|
|
125
|
+
attrs: ["Ref"],
|
|
126
|
+
via: ["Environment"],
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("Fn::ImportValue resolves through the exporting stack's Outputs", () => {
|
|
131
|
+
// Not a dangling export name: the edge lands on the construct that
|
|
132
|
+
// actually produces the value, in the other stack.
|
|
133
|
+
expect(graph.edges).toContainEqual({
|
|
134
|
+
from: "AppStack/Handler",
|
|
135
|
+
to: "DataStack/Assets",
|
|
136
|
+
attrs: ["Export"],
|
|
137
|
+
via: ["PolicyDocument"],
|
|
138
|
+
crossStack: true,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("an output whose export is imported is not counted a second time", () => {
|
|
143
|
+
// DataStack exports the bucket ARN and AppStack imports it. That is one
|
|
144
|
+
// dependency, already drawn between the two constructs, so the output
|
|
145
|
+
// block does not also charge the bucket.
|
|
146
|
+
const outputs = graph.edges.filter((e) => e.fromKind === "output");
|
|
147
|
+
expect(outputs.map((e) => e.from)).toEqual(["output.AppStack.QueueUrl", "output.DataStack.TableName"]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("a stack output nobody imports is an output edge on what it reads", () => {
|
|
151
|
+
expect(graph.edges).toContainEqual({
|
|
152
|
+
from: "output.DataStack.TableName",
|
|
153
|
+
to: "DataStack/Table",
|
|
154
|
+
attrs: ["Ref"],
|
|
155
|
+
via: ["Value"],
|
|
156
|
+
fromKind: "output",
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("a CloudFormation Condition or an author's parameter marks a construct dynamic", () => {
|
|
161
|
+
expect(node("AppStack/Legacy")?.hasDynamic).toBe(true);
|
|
162
|
+
expect(node("DataStack/Assets")?.hasDynamic).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("an asset-backed construct carries a penalty and says why", () => {
|
|
166
|
+
expect(signals.get("AppStack/Handler")?.penalties).toEqual({ asset: -10 });
|
|
167
|
+
expect(signals.get("AppStack/Handler")?.notes?.join(" ")).toContain("Asset-backed (Code)");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("a nested stack is disqualified rather than scored", () => {
|
|
171
|
+
expect(signals.get("AppStack/Reports.NestedStack")?.disqualified).toContain("Nested stack");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("a complete assembly has nothing to report about the read", () => {
|
|
175
|
+
expect(diagnostics).toEqual([]);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("a dummy-value assembly", () => {
|
|
180
|
+
const assembly = readCloudAssembly(DUMMY);
|
|
181
|
+
|
|
182
|
+
test("is recognized from the manifest's unresolved lookups", () => {
|
|
183
|
+
const reason = dummyAssemblyReason(assembly);
|
|
184
|
+
expect(reason).toContain("unresolved context lookup");
|
|
185
|
+
expect(reason).toContain("vpc-provider");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("is recognized from a placeholder left in a template", () => {
|
|
189
|
+
// The same verdict with the manifest's admission removed: the template
|
|
190
|
+
// still holds the value the lookup would have replaced.
|
|
191
|
+
const withoutMissing = { ...assembly, manifest: { ...assembly.manifest, missing: [] } };
|
|
192
|
+
expect(dummyAssemblyReason(withoutMissing)).toContain("vpc-12345678");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("disqualifies every construct in it", () => {
|
|
196
|
+
const { signals } = buildCdkGraph(assembly);
|
|
197
|
+
expect(signals.size).toBeGreaterThan(0);
|
|
198
|
+
for (const signal of signals.values()) expect(signal.disqualified).toContain("unresolved context lookup");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("without tree.json, grouping falls back to the construct path and says so", () => {
|
|
202
|
+
const { graph, diagnostics } = buildCdkGraph(assembly);
|
|
203
|
+
expect(graph.nodes.map((n) => n.address)).toEqual(["LookupStack/AppBucket", "LookupStack/AppSg"]);
|
|
204
|
+
expect(diagnostics.join(" ")).toContain("No tree.json");
|
|
205
|
+
});
|
|
206
|
+
});
|