@intentius/chant-lexicon-cpln 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/README.md +85 -0
- package/dist/api.d.ts +77 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/codegen/docs-cli.d.ts +3 -0
- package/dist/codegen/docs-cli.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts +17 -0
- package/dist/codegen/docs.d.ts.map +1 -0
- package/dist/codegen/generate-cli.d.ts +6 -0
- package/dist/codegen/generate-cli.d.ts.map +1 -0
- package/dist/codegen/generate-lexicon.d.ts +36 -0
- package/dist/codegen/generate-lexicon.d.ts.map +1 -0
- package/dist/codegen/generate-typescript.d.ts +11 -0
- package/dist/codegen/generate-typescript.d.ts.map +1 -0
- package/dist/codegen/generate.d.ts +15 -0
- package/dist/codegen/generate.d.ts.map +1 -0
- package/dist/codegen/naming.d.ts +30 -0
- package/dist/codegen/naming.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +17 -0
- package/dist/codegen/package.d.ts.map +1 -0
- package/dist/composites/cron-job.d.ts +60 -0
- package/dist/composites/cron-job.d.ts.map +1 -0
- package/dist/composites/gvc-environment.d.ts +47 -0
- package/dist/composites/gvc-environment.d.ts.map +1 -0
- package/dist/composites/index.d.ts +13 -0
- package/dist/composites/index.d.ts.map +1 -0
- package/dist/composites/public-domain.d.ts +68 -0
- package/dist/composites/public-domain.d.ts.map +1 -0
- package/dist/composites/secret-access.d.ts +78 -0
- package/dist/composites/secret-access.d.ts.map +1 -0
- package/dist/composites/serverless-service.d.ts +78 -0
- package/dist/composites/serverless-service.d.ts.map +1 -0
- package/dist/composites/stateful-service.d.ts +79 -0
- package/dist/composites/stateful-service.d.ts.map +1 -0
- package/dist/coverage.d.ts +59 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/describe-resources.d.ts +71 -0
- package/dist/describe-resources.d.ts.map +1 -0
- package/dist/detect.d.ts +14 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/entity-props.d.ts +34 -0
- package/dist/entity-props.d.ts.map +1 -0
- package/dist/generated/index.d.ts +151 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/dist/generated/runtime.d.ts +5 -0
- package/dist/generated/runtime.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/init-templates.d.ts +16 -0
- package/dist/init-templates.d.ts.map +1 -0
- package/dist/integrity.json +35 -0
- package/dist/kinds.d.ts +70 -0
- package/dist/kinds.d.ts.map +1 -0
- package/dist/lint/audit-catalog.d.ts +19 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl010-unrestricted-outbound.d.ts +12 -0
- package/dist/lint/post-synth/cpl010-unrestricted-outbound.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl011-internal-firewall-scope.d.ts +11 -0
- package/dist/lint/post-synth/cpl011-internal-firewall-scope.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl012-no-literal-credential-env.d.ts +11 -0
- package/dist/lint/post-synth/cpl012-no-literal-credential-env.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl013-qualified-identity-principal.d.ts +11 -0
- package/dist/lint/post-synth/cpl013-qualified-identity-principal.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl014-qualified-secret-ref.d.ts +14 -0
- package/dist/lint/post-synth/cpl014-qualified-secret-ref.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl020-serverless-single-port.d.ts +10 -0
- package/dist/lint/post-synth/cpl020-serverless-single-port.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl021-cron-job-shape.d.ts +11 -0
- package/dist/lint/post-synth/cpl021-cron-job-shape.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl022-container-ports.d.ts +10 -0
- package/dist/lint/post-synth/cpl022-container-ports.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl023-container-resources.d.ts +12 -0
- package/dist/lint/post-synth/cpl023-container-resources.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl024-probe-handler.d.ts +10 -0
- package/dist/lint/post-synth/cpl024-probe-handler.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl025-autoscaling-shape.d.ts +11 -0
- package/dist/lint/post-synth/cpl025-autoscaling-shape.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl026-scale-to-zero.d.ts +11 -0
- package/dist/lint/post-synth/cpl026-scale-to-zero.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl027-capacity-ai-conflict.d.ts +13 -0
- package/dist/lint/post-synth/cpl027-capacity-ai-conflict.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl028-volume-set.d.ts +3 -0
- package/dist/lint/post-synth/cpl028-volume-set.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl029-link-targets.d.ts +3 -0
- package/dist/lint/post-synth/cpl029-link-targets.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl030-domain-routing.d.ts +3 -0
- package/dist/lint/post-synth/cpl030-domain-routing.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl040-pinned-image.d.ts +11 -0
- package/dist/lint/post-synth/cpl040-pinned-image.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl041-image-reference-form.d.ts +12 -0
- package/dist/lint/post-synth/cpl041-image-reference-form.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl042-gvc-placement.d.ts +10 -0
- package/dist/lint/post-synth/cpl042-gvc-placement.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl043-policy-scope.d.ts +16 -0
- package/dist/lint/post-synth/cpl043-policy-scope.d.ts.map +1 -0
- package/dist/lint/post-synth/helpers.d.ts +45 -0
- package/dist/lint/post-synth/helpers.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts +3 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -0
- package/dist/lint/rules/cpl001-no-secret-literals.d.ts +3 -0
- package/dist/lint/rules/cpl001-no-secret-literals.d.ts.map +1 -0
- package/dist/lint/rules/cpl002-prefer-resource-reference.d.ts +3 -0
- package/dist/lint/rules/cpl002-prefer-resource-reference.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +6 -0
- package/dist/lint/rules/index.d.ts.map +1 -0
- package/dist/lsp/completions.d.ts +7 -0
- package/dist/lsp/completions.d.ts.map +1 -0
- package/dist/lsp/hover.d.ts +4 -0
- package/dist/lsp/hover.d.ts.map +1 -0
- package/dist/manifest.json +6 -0
- package/dist/meta.json +2110 -0
- package/dist/okf/index.md +39 -0
- package/dist/okf/rules/CPL001.md +22 -0
- package/dist/okf/rules/CPL002.md +22 -0
- package/dist/okf/rules/CPL010.md +15 -0
- package/dist/okf/rules/CPL011.md +15 -0
- package/dist/okf/rules/CPL012.md +15 -0
- package/dist/okf/rules/CPL013.md +15 -0
- package/dist/okf/rules/CPL014.md +16 -0
- package/dist/okf/rules/CPL020.md +11 -0
- package/dist/okf/rules/CPL021.md +15 -0
- package/dist/okf/rules/CPL022.md +15 -0
- package/dist/okf/rules/CPL023.md +15 -0
- package/dist/okf/rules/CPL024.md +15 -0
- package/dist/okf/rules/CPL025.md +15 -0
- package/dist/okf/rules/CPL026.md +15 -0
- package/dist/okf/rules/CPL027.md +15 -0
- package/dist/okf/rules/CPL028.md +15 -0
- package/dist/okf/rules/CPL029.md +15 -0
- package/dist/okf/rules/CPL030.md +16 -0
- package/dist/okf/rules/CPL040.md +15 -0
- package/dist/okf/rules/CPL041.md +15 -0
- package/dist/okf/rules/CPL042.md +11 -0
- package/dist/okf/rules/CPL043.md +15 -0
- package/dist/okf/types/Domain.md +22 -0
- package/dist/okf/types/Gvc.md +21 -0
- package/dist/okf/types/Identity.md +29 -0
- package/dist/okf/types/IpSet.md +21 -0
- package/dist/okf/types/Policy.md +28 -0
- package/dist/okf/types/Secret.md +23 -0
- package/dist/okf/types/VolumeSet.md +22 -0
- package/dist/okf/types/Workload.md +38 -0
- package/dist/ownership.d.ts +23 -0
- package/dist/ownership.d.ts.map +1 -0
- package/dist/package-cli.d.ts +3 -0
- package/dist/package-cli.d.ts.map +1 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/reference-catalog.d.ts +24 -0
- package/dist/reference-catalog.d.ts.map +1 -0
- package/dist/rules/cpl001-no-secret-literals.ts +83 -0
- package/dist/rules/cpl002-prefer-resource-reference.ts +71 -0
- package/dist/rules/cpl010-unrestricted-outbound.ts +41 -0
- package/dist/rules/cpl011-internal-firewall-scope.ts +38 -0
- package/dist/rules/cpl012-no-literal-credential-env.ts +58 -0
- package/dist/rules/cpl013-qualified-identity-principal.ts +45 -0
- package/dist/rules/cpl014-qualified-secret-ref.ts +54 -0
- package/dist/rules/cpl020-serverless-single-port.ts +58 -0
- package/dist/rules/cpl021-cron-job-shape.ts +64 -0
- package/dist/rules/cpl022-container-ports.ts +64 -0
- package/dist/rules/cpl023-container-resources.ts +87 -0
- package/dist/rules/cpl024-probe-handler.ts +51 -0
- package/dist/rules/cpl025-autoscaling-shape.ts +100 -0
- package/dist/rules/cpl026-scale-to-zero.ts +67 -0
- package/dist/rules/cpl027-capacity-ai-conflict.ts +78 -0
- package/dist/rules/cpl028-volume-set.ts +183 -0
- package/dist/rules/cpl029-link-targets.ts +155 -0
- package/dist/rules/cpl030-domain-routing.ts +137 -0
- package/dist/rules/cpl040-pinned-image.ts +48 -0
- package/dist/rules/cpl041-image-reference-form.ts +60 -0
- package/dist/rules/cpl042-gvc-placement.ts +57 -0
- package/dist/rules/cpl043-policy-scope.ts +0 -0
- package/dist/rules/helpers.ts +114 -0
- package/dist/serializer.d.ts +69 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/skills/chant-cpln-secrets.md +110 -0
- package/dist/skills/chant-cpln-workloads.md +78 -0
- package/dist/skills/chant-cpln.md +103 -0
- package/dist/spec/fetch.d.ts +43 -0
- package/dist/spec/fetch.d.ts.map +1 -0
- package/dist/spec/parse.d.ts +79 -0
- package/dist/spec/parse.d.ts.map +1 -0
- package/dist/spec/snapshot-cli.d.ts +18 -0
- package/dist/spec/snapshot-cli.d.ts.map +1 -0
- package/dist/types/index.d.ts +1649 -0
- package/dist/validate-cli.d.ts +3 -0
- package/dist/validate-cli.d.ts.map +1 -0
- package/dist/validate.d.ts +26 -0
- package/dist/validate.d.ts.map +1 -0
- package/package.json +74 -0
- package/src/api.ts +134 -0
- package/src/codegen/docs-cli.ts +7 -0
- package/src/codegen/docs.ts +94 -0
- package/src/codegen/generate-cli.ts +34 -0
- package/src/codegen/generate-lexicon.ts +88 -0
- package/src/codegen/generate-typescript.ts +71 -0
- package/src/codegen/generate.ts +110 -0
- package/src/codegen/naming.ts +61 -0
- package/src/codegen/package.ts +35 -0
- package/src/composites/composites.test.ts +252 -0
- package/src/composites/cron-job.ts +124 -0
- package/src/composites/gvc-environment.ts +104 -0
- package/src/composites/index.ts +17 -0
- package/src/composites/public-domain.ts +144 -0
- package/src/composites/secret-access.ts +136 -0
- package/src/composites/serverless-service.ts +143 -0
- package/src/composites/stateful-service.ts +179 -0
- package/src/coverage.ts +149 -0
- package/src/describe-resources.test.ts +252 -0
- package/src/describe-resources.ts +310 -0
- package/src/detect.ts +58 -0
- package/src/entity-props.ts +98 -0
- package/src/generated/index.d.ts +1649 -0
- package/src/generated/index.ts +156 -0
- package/src/generated/lexicon-cpln.json +2110 -0
- package/src/generated/runtime.ts +4 -0
- package/src/index.ts +31 -0
- package/src/init-templates.ts +131 -0
- package/src/kinds.ts +173 -0
- package/src/lint/audit-catalog.ts +230 -0
- package/src/lint/post-synth/cpl010-unrestricted-outbound.ts +41 -0
- package/src/lint/post-synth/cpl011-internal-firewall-scope.ts +38 -0
- package/src/lint/post-synth/cpl012-no-literal-credential-env.ts +58 -0
- package/src/lint/post-synth/cpl013-qualified-identity-principal.ts +45 -0
- package/src/lint/post-synth/cpl014-qualified-secret-ref.ts +54 -0
- package/src/lint/post-synth/cpl020-serverless-single-port.ts +58 -0
- package/src/lint/post-synth/cpl021-cron-job-shape.ts +64 -0
- package/src/lint/post-synth/cpl022-container-ports.ts +64 -0
- package/src/lint/post-synth/cpl023-container-resources.ts +87 -0
- package/src/lint/post-synth/cpl024-probe-handler.ts +51 -0
- package/src/lint/post-synth/cpl025-autoscaling-shape.ts +100 -0
- package/src/lint/post-synth/cpl026-scale-to-zero.ts +67 -0
- package/src/lint/post-synth/cpl027-capacity-ai-conflict.ts +78 -0
- package/src/lint/post-synth/cpl028-volume-set.ts +183 -0
- package/src/lint/post-synth/cpl029-link-targets.ts +155 -0
- package/src/lint/post-synth/cpl030-domain-routing.ts +137 -0
- package/src/lint/post-synth/cpl040-pinned-image.ts +48 -0
- package/src/lint/post-synth/cpl041-image-reference-form.ts +60 -0
- package/src/lint/post-synth/cpl042-gvc-placement.ts +57 -0
- package/src/lint/post-synth/cpl043-policy-scope.ts +0 -0
- package/src/lint/post-synth/helpers.ts +114 -0
- package/src/lint/post-synth/index.ts +45 -0
- package/src/lint/post-synth/post-synth.test.ts +737 -0
- package/src/lint/rules/cpl001-no-secret-literals.ts +83 -0
- package/src/lint/rules/cpl002-prefer-resource-reference.ts +71 -0
- package/src/lint/rules/index.ts +9 -0
- package/src/lint/rules/rules.test.ts +108 -0
- package/src/lsp/completions.test.ts +44 -0
- package/src/lsp/completions.ts +27 -0
- package/src/lsp/hover.test.ts +53 -0
- package/src/lsp/hover.ts +130 -0
- package/src/ownership.ts +28 -0
- package/src/package-cli.ts +25 -0
- package/src/plugin.test.ts +131 -0
- package/src/plugin.ts +187 -0
- package/src/reference-catalog.ts +110 -0
- package/src/serializer.test.ts +233 -0
- package/src/serializer.ts +253 -0
- package/src/skills/chant-cpln-secrets.md +110 -0
- package/src/skills/chant-cpln-workloads.md +78 -0
- package/src/skills/chant-cpln.md +103 -0
- package/src/spec/cpln-openapi.snapshot.json +10651 -0
- package/src/spec/fetch.ts +77 -0
- package/src/spec/parse.ts +451 -0
- package/src/spec/snapshot-cli.ts +71 -0
- package/src/validate-cli.ts +8 -0
- package/src/validate.ts +102 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { isLexiconPlugin } from "@intentius/chant/lexicon";
|
|
3
|
+
import { cplnPlugin } from "./plugin";
|
|
4
|
+
import { cplnAuditCatalog } from "./lint/audit-catalog";
|
|
5
|
+
import { KINDS } from "./kinds";
|
|
6
|
+
|
|
7
|
+
describe("cpln plugin", () => {
|
|
8
|
+
it("is a valid LexiconPlugin", () => {
|
|
9
|
+
expect(isLexiconPlugin(cplnPlugin)).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("has the correct name and serializer", () => {
|
|
13
|
+
expect(cplnPlugin.name).toBe("cpln");
|
|
14
|
+
expect(cplnPlugin.serializer.name).toBe("cpln");
|
|
15
|
+
expect(cplnPlugin.serializer.rulePrefix).toBe("CPL");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("registers lint rules and post-synth checks", () => {
|
|
19
|
+
expect(cplnPlugin.lintRules?.()?.length).toBeGreaterThan(0);
|
|
20
|
+
expect(cplnPlugin.postSynthChecks?.()?.length).toBeGreaterThanOrEqual(15);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("gives every rule an id under the declared prefix", () => {
|
|
24
|
+
// Ids must not collide when lexicons load together, which is what the
|
|
25
|
+
// prefix is for. A stray id here is invisible until two lexicons meet.
|
|
26
|
+
const prefixes = [cplnPlugin.serializer.rulePrefix, ...(cplnPlugin.serializer.extraRulePrefixes ?? [])];
|
|
27
|
+
const ids = [
|
|
28
|
+
...(cplnPlugin.lintRules?.() ?? []).map((rule) => rule.id),
|
|
29
|
+
...(cplnPlugin.postSynthChecks?.() ?? []).map((check) => check.id),
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
expect(ids.length).toBeGreaterThan(0);
|
|
33
|
+
for (const id of ids) {
|
|
34
|
+
expect(prefixes.some((prefix) => id.startsWith(prefix)), `${id} is outside ${prefixes.join("/")}`).toBe(true);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("has no duplicate rule ids", () => {
|
|
39
|
+
const ids = [
|
|
40
|
+
...(cplnPlugin.lintRules?.() ?? []).map((rule) => rule.id),
|
|
41
|
+
...(cplnPlugin.postSynthChecks?.() ?? []).map((check) => check.id),
|
|
42
|
+
];
|
|
43
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("covers every registered check in the audit catalog", () => {
|
|
47
|
+
// A check with no catalog entry contributes nothing to `chant audit`, and
|
|
48
|
+
// does so silently — which is exactly the failure mode this asserts away.
|
|
49
|
+
const ids = [
|
|
50
|
+
...(cplnPlugin.lintRules?.() ?? []).map((rule) => rule.id),
|
|
51
|
+
...(cplnPlugin.postSynthChecks?.() ?? []).map((check) => check.id),
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
for (const id of ids) {
|
|
55
|
+
expect(cplnAuditCatalog[id], `${id} has no audit catalog entry`).toBeDefined();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("has no audit catalog entry without a check", () => {
|
|
60
|
+
const ids = new Set([
|
|
61
|
+
...(cplnPlugin.lintRules?.() ?? []).map((rule) => rule.id),
|
|
62
|
+
...(cplnPlugin.postSynthChecks?.() ?? []).map((check) => check.id),
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
for (const id of Object.keys(cplnAuditCatalog)) {
|
|
66
|
+
expect(ids.has(id), `${id} is catalogued but not registered`).toBe(true);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("registers LSP providers", () => {
|
|
71
|
+
// Registration is what `chant serve lsp` dispatches through — a file in
|
|
72
|
+
// src/lsp/ that nothing registers is unreachable in an editor.
|
|
73
|
+
expect(typeof cplnPlugin.completionProvider).toBe("function");
|
|
74
|
+
expect(typeof cplnPlugin.hoverProvider).toBe("function");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("registers docs, detection and init templates", () => {
|
|
78
|
+
expect(typeof cplnPlugin.docs).toBe("function");
|
|
79
|
+
expect(typeof cplnPlugin.detectTemplate).toBe("function");
|
|
80
|
+
expect(typeof cplnPlugin.initTemplates).toBe("function");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("ships at least three skills, each with content", () => {
|
|
84
|
+
const skills = cplnPlugin.skills?.() ?? [];
|
|
85
|
+
expect(skills.length).toBeGreaterThanOrEqual(3);
|
|
86
|
+
for (const skill of skills) {
|
|
87
|
+
expect(skill.content.length, `${skill.name} loaded empty`).toBeGreaterThan(0);
|
|
88
|
+
expect(skill.description.length).toBeGreaterThan(0);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("ships at least three init templates that differ", () => {
|
|
93
|
+
const names = ["default", "secrets", "stateful"];
|
|
94
|
+
const sources = names.map((name) => cplnPlugin.initTemplates?.(name)?.src["infra.ts"] ?? "");
|
|
95
|
+
for (const source of sources) expect(source.length).toBeGreaterThan(0);
|
|
96
|
+
expect(new Set(sources).size).toBe(names.length);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("namespaces MCP tools and resources under `cpln`", () => {
|
|
100
|
+
for (const tool of cplnPlugin.mcpTools?.() ?? []) {
|
|
101
|
+
expect(tool.name.startsWith("cpln:")).toBe(true);
|
|
102
|
+
}
|
|
103
|
+
for (const resource of cplnPlugin.mcpResources?.() ?? []) {
|
|
104
|
+
expect(resource.uri.startsWith("cpln:")).toBe(true);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("declares an ownership channel whose read paths it implements", () => {
|
|
109
|
+
const channel = cplnPlugin.ownershipChannel;
|
|
110
|
+
expect(channel).toBeDefined();
|
|
111
|
+
expect(channel!.keys.managedBy).toBe("chant.intentius.io/managed-by");
|
|
112
|
+
expect(channel!.keys.stack).toBeTruthy();
|
|
113
|
+
expect(channel!.keys.env).toBeTruthy();
|
|
114
|
+
|
|
115
|
+
// Declaring a path the plugin cannot resolve a verdict on would be a lie
|
|
116
|
+
// the completeness checker cannot catch.
|
|
117
|
+
expect(channel!.reads).toContain("describeResources");
|
|
118
|
+
expect(typeof cplnPlugin.describeResources).toBe("function");
|
|
119
|
+
for (const path of channel!.reads) {
|
|
120
|
+
if (path === "observeResourcesDeep") expect(cplnPlugin.observeResourcesDeep).toBeDefined();
|
|
121
|
+
if (path === "exportResources") expect(cplnPlugin.exportResources).toBeDefined();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("declares reference-catalog identities for every modelled kind", () => {
|
|
126
|
+
const identities = new Set(cplnPlugin.referenceCatalog?.identities.map((entry) => entry.kind));
|
|
127
|
+
for (const kind of KINDS) {
|
|
128
|
+
expect(identities.has(kind.typeName), `${kind.typeName} has no identity rule`).toBe(true);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LexiconPlugin,
|
|
3
|
+
SkillDefinition,
|
|
4
|
+
InitTemplateSet,
|
|
5
|
+
DescribeResourcesResult,
|
|
6
|
+
} from "@intentius/chant/lexicon";
|
|
7
|
+
import type { LintRule } from "@intentius/chant/lint/rule";
|
|
8
|
+
import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
|
|
9
|
+
import type { CompletionContext, CompletionItem, HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
|
|
10
|
+
import type { McpToolContribution, McpResourceContribution } from "@intentius/chant/mcp/types";
|
|
11
|
+
import type { OwnershipChannel } from "@intentius/chant/ownership";
|
|
12
|
+
import { createSkillsLoader, createDiffTool, createCatalogResource } from "@intentius/chant/lexicon-plugin-helpers";
|
|
13
|
+
import { cplnSerializer } from "./serializer";
|
|
14
|
+
import { rules } from "./lint/rules";
|
|
15
|
+
import { postSynthChecks } from "./lint/post-synth";
|
|
16
|
+
import { cplnAuditCatalog } from "./lint/audit-catalog";
|
|
17
|
+
import { cplnReferenceCatalog } from "./reference-catalog";
|
|
18
|
+
import { CPLN_TAG_OWNERSHIP_KEYS } from "./ownership";
|
|
19
|
+
import { detectCplnTemplate } from "./detect";
|
|
20
|
+
import { cplnInitTemplates } from "./init-templates";
|
|
21
|
+
import { completions } from "./lsp/completions";
|
|
22
|
+
import { hover } from "./lsp/hover";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The cpln lexicon plugin.
|
|
26
|
+
*
|
|
27
|
+
* Control Plane's `tags` map is the ownership channel, and unusually it works
|
|
28
|
+
* on the thin read: every kind carries `tags` and every read path returns
|
|
29
|
+
* them, so `describeResources` resolves a real verdict rather than `unknown`.
|
|
30
|
+
* That is why `describeResources` is declared among the channel's read paths
|
|
31
|
+
* where aws — whose `describe-stack-resources` returns no tags at all — cannot
|
|
32
|
+
* declare it.
|
|
33
|
+
*
|
|
34
|
+
* `observeResourcesDeep` and `exportResources` are not implemented yet, so
|
|
35
|
+
* they are absent from `reads` rather than listed optimistically: the contract
|
|
36
|
+
* is that a declared path resolves a verdict, and claiming one this plugin has
|
|
37
|
+
* no implementation for would be a lie the checker cannot catch.
|
|
38
|
+
*/
|
|
39
|
+
const ownershipChannel: OwnershipChannel = {
|
|
40
|
+
keys: CPLN_TAG_OWNERSHIP_KEYS,
|
|
41
|
+
reads: ["describeResources"],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const cplnPlugin: LexiconPlugin = {
|
|
45
|
+
name: "cpln",
|
|
46
|
+
serializer: cplnSerializer,
|
|
47
|
+
|
|
48
|
+
// ── Required lifecycle methods ────────────────────────────────
|
|
49
|
+
|
|
50
|
+
async generate(options?: { verbose?: boolean; force?: boolean }): Promise<void> {
|
|
51
|
+
const { generate, writeGeneratedFiles } = await import("./codegen/generate");
|
|
52
|
+
const { dirname } = await import("path");
|
|
53
|
+
const { fileURLToPath } = await import("url");
|
|
54
|
+
|
|
55
|
+
const result = await generate(options);
|
|
56
|
+
writeGeneratedFiles(result, dirname(dirname(fileURLToPath(import.meta.url))));
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
async validate(): Promise<void> {
|
|
60
|
+
const { validate } = await import("./validate");
|
|
61
|
+
const { printValidationResult } = await import("@intentius/chant/codegen/validate");
|
|
62
|
+
printValidationResult(await validate());
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
async coverage(options?: { verbose?: boolean; minOverall?: number }): Promise<void> {
|
|
66
|
+
const { analyzeCplnCoverage } = await import("./coverage");
|
|
67
|
+
await analyzeCplnCoverage(options);
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
async package(options?: { verbose?: boolean; force?: boolean }): Promise<void> {
|
|
71
|
+
const { packageLexicon } = await import("./codegen/package");
|
|
72
|
+
const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
|
|
73
|
+
const { join, dirname } = await import("path");
|
|
74
|
+
const { fileURLToPath } = await import("url");
|
|
75
|
+
|
|
76
|
+
const { spec, stats } = await packageLexicon(options);
|
|
77
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
78
|
+
writeBundleSpec(spec, join(pkgDir, "dist"));
|
|
79
|
+
|
|
80
|
+
console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// ── Lint and audit ────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
lintRules(): LintRule[] {
|
|
86
|
+
return rules;
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
postSynthChecks(): PostSynthCheck[] {
|
|
90
|
+
return postSynthChecks;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
auditCatalog: () => cplnAuditCatalog,
|
|
94
|
+
|
|
95
|
+
// ── Observation ───────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
ownershipChannel,
|
|
98
|
+
|
|
99
|
+
referenceCatalog: cplnReferenceCatalog,
|
|
100
|
+
|
|
101
|
+
async describeResources(options): Promise<DescribeResourcesResult> {
|
|
102
|
+
const { describeResources } = await import("./describe-resources");
|
|
103
|
+
return describeResources({
|
|
104
|
+
environment: options.environment,
|
|
105
|
+
buildOutput: options.buildOutput,
|
|
106
|
+
entityNames: options.entityNames,
|
|
107
|
+
entities: options.entities,
|
|
108
|
+
owned: options.owned,
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
// ── Language surface ──────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
completionProvider(ctx: CompletionContext): CompletionItem[] {
|
|
115
|
+
return completions(ctx);
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
hoverProvider(ctx: HoverContext): HoverInfo | undefined {
|
|
119
|
+
return hover(ctx);
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// ── Import and project surface ────────────────────────────────
|
|
123
|
+
|
|
124
|
+
detectTemplate(data: unknown): boolean {
|
|
125
|
+
return detectCplnTemplate(data);
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The three `chant init` scaffolds, named at the plugin surface so the
|
|
130
|
+
* available set is visible where a reader looks for it rather than only
|
|
131
|
+
* inside the switch that resolves them. An unrecognised name falls through
|
|
132
|
+
* to the default — `chant init` treats it as a hint, not a key.
|
|
133
|
+
*/
|
|
134
|
+
initTemplates(template?: string): InitTemplateSet {
|
|
135
|
+
if (template === "secrets") return cplnInitTemplates("secrets");
|
|
136
|
+
if (template === "stateful") return cplnInitTemplates("stateful");
|
|
137
|
+
return cplnInitTemplates();
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
skills: createSkillsLoader(import.meta.url, [
|
|
141
|
+
{
|
|
142
|
+
file: "chant-cpln.md",
|
|
143
|
+
name: "chant-cpln",
|
|
144
|
+
description:
|
|
145
|
+
"Declare Control Plane (cpln) infrastructure from chant — the kinds, GVC scoping, links, and what the serializer emits",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
file: "chant-cpln-workloads.md",
|
|
149
|
+
name: "chant-cpln-workloads",
|
|
150
|
+
description:
|
|
151
|
+
"Choose a Control Plane workload type and configure autoscaling, Capacity AI, resources and probes correctly",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
file: "chant-cpln-secrets.md",
|
|
155
|
+
name: "chant-cpln-secrets",
|
|
156
|
+
description:
|
|
157
|
+
"Wire Control Plane secrets, identities and policies so a workload can actually read them",
|
|
158
|
+
},
|
|
159
|
+
]) as () => SkillDefinition[],
|
|
160
|
+
|
|
161
|
+
mcpTools(): McpToolContribution[] {
|
|
162
|
+
return [
|
|
163
|
+
createDiffTool(
|
|
164
|
+
cplnSerializer,
|
|
165
|
+
"Diff the current cpln manifests against the previous build output",
|
|
166
|
+
"cpln",
|
|
167
|
+
),
|
|
168
|
+
];
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
mcpResources(): McpResourceContribution[] {
|
|
172
|
+
return [
|
|
173
|
+
createCatalogResource(
|
|
174
|
+
import.meta.url,
|
|
175
|
+
"Control Plane Resource Catalog",
|
|
176
|
+
"Every cpln resource and property type, with its Control Plane type name",
|
|
177
|
+
"lexicon-cpln.json",
|
|
178
|
+
"cpln",
|
|
179
|
+
),
|
|
180
|
+
];
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
async docs(options?: { verbose?: boolean }): Promise<void> {
|
|
184
|
+
const { generateDocs } = await import("./codegen/docs");
|
|
185
|
+
return generateDocs(options);
|
|
186
|
+
},
|
|
187
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How observed cpln resources reference each other, so `chant graph --live` can
|
|
3
|
+
* rebuild the topology from a bag of nodes.
|
|
4
|
+
*
|
|
5
|
+
* Control Plane addresses resources by **link string**
|
|
6
|
+
* (`//gvc/prod/identity/api`), and edge reconstruction matches identifiers
|
|
7
|
+
* exactly, so a raw link never equals the `name` it points at. Rather than add
|
|
8
|
+
* an enrichment pass, `describeResources` resolves each link down to the bare
|
|
9
|
+
* name it ends in and files it under a stable attribute — `refs.identity`,
|
|
10
|
+
* `refs.gvc`, `refs.pullSecrets` — and the rules below read those. See
|
|
11
|
+
* {@link referenceAttributes} in `describe-resources.ts`.
|
|
12
|
+
*
|
|
13
|
+
* Names are unique per kind within an org (per GVC for the GVC-scoped kinds),
|
|
14
|
+
* so `name` is the identity for every kind and `targetKind` disambiguates the
|
|
15
|
+
* cases where two kinds could hold the same name.
|
|
16
|
+
*
|
|
17
|
+
* Containment vs reference matters for the picture: "this workload is in this
|
|
18
|
+
* GVC" is a boundary the renderer draws as a box, not a line from every
|
|
19
|
+
* resource to its GVC. It still declares `viaAttr` so a traversal can walk it —
|
|
20
|
+
* "which GVCs have no workloads" is a containment question.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { ReferenceCatalog } from "@intentius/chant/lexicon";
|
|
24
|
+
import { KINDS, kindByName } from "./kinds";
|
|
25
|
+
|
|
26
|
+
const typeOf = (kind: string): string => kindByName(kind)!.typeName;
|
|
27
|
+
|
|
28
|
+
export const cplnReferenceCatalog: ReferenceCatalog = {
|
|
29
|
+
identities: KINDS.map((kind) => ({ kind: kind.typeName, ids: ["name"] })),
|
|
30
|
+
|
|
31
|
+
refs: [
|
|
32
|
+
// Every GVC-scoped resource lives inside its GVC — a boundary, not a line.
|
|
33
|
+
...KINDS.filter((kind) => kind.gvcScoped).map((kind) => ({
|
|
34
|
+
from: kind.typeName,
|
|
35
|
+
path: "refs.gvc",
|
|
36
|
+
targetKind: typeOf("gvc"),
|
|
37
|
+
relation: "containment" as const,
|
|
38
|
+
label: "in GVC",
|
|
39
|
+
viaAttr: "gvc",
|
|
40
|
+
})),
|
|
41
|
+
|
|
42
|
+
// A workload runs as one identity.
|
|
43
|
+
{
|
|
44
|
+
from: typeOf("workload"),
|
|
45
|
+
path: "refs.identity",
|
|
46
|
+
targetKind: typeOf("identity"),
|
|
47
|
+
relation: "reference",
|
|
48
|
+
label: "identity",
|
|
49
|
+
viaAttr: "identityLink",
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// A GVC pulls private images with org-scoped secrets.
|
|
53
|
+
{
|
|
54
|
+
from: typeOf("gvc"),
|
|
55
|
+
path: "refs.pullSecrets[]",
|
|
56
|
+
targetKind: typeOf("secret"),
|
|
57
|
+
relation: "reference",
|
|
58
|
+
label: "pull secret",
|
|
59
|
+
viaAttr: "pullSecretLinks",
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// A domain routes into a GVC, or straight at a stateful workload.
|
|
63
|
+
{
|
|
64
|
+
from: typeOf("domain"),
|
|
65
|
+
path: "refs.gvc",
|
|
66
|
+
targetKind: typeOf("gvc"),
|
|
67
|
+
relation: "reference",
|
|
68
|
+
label: "routes to",
|
|
69
|
+
viaAttr: "gvcLink",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
from: typeOf("domain"),
|
|
73
|
+
path: "refs.workloads[]",
|
|
74
|
+
targetKind: typeOf("workload"),
|
|
75
|
+
relation: "reference",
|
|
76
|
+
label: "routes to",
|
|
77
|
+
viaAttr: "workloadLink",
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// A policy's explicit target links.
|
|
81
|
+
{
|
|
82
|
+
from: typeOf("policy"),
|
|
83
|
+
path: "refs.targets[]",
|
|
84
|
+
targetKind: typeOf("secret"),
|
|
85
|
+
relation: "reference",
|
|
86
|
+
label: "grants on",
|
|
87
|
+
viaAttr: "targetLinks",
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// An IP set fronts the workload it is bound to.
|
|
91
|
+
{
|
|
92
|
+
from: typeOf("ipset"),
|
|
93
|
+
path: "refs.workloads[]",
|
|
94
|
+
targetKind: typeOf("workload"),
|
|
95
|
+
relation: "reference",
|
|
96
|
+
label: "fronts",
|
|
97
|
+
viaAttr: "link",
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// A workload mounts volume sets.
|
|
101
|
+
{
|
|
102
|
+
from: typeOf("workload"),
|
|
103
|
+
path: "refs.volumeSets[]",
|
|
104
|
+
targetKind: typeOf("volumeset"),
|
|
105
|
+
relation: "reference",
|
|
106
|
+
label: "mounts",
|
|
107
|
+
viaAttr: "volumes",
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
3
|
+
import { cplnSerializer, cplnLink } from "./serializer";
|
|
4
|
+
import { Gvc, Workload, Identity, Secret, Policy, VolumeSet } from "./generated/index";
|
|
5
|
+
import { kindByName } from "./kinds";
|
|
6
|
+
|
|
7
|
+
/** Build the entity map the serializer is handed. */
|
|
8
|
+
function entities(pairs: Array<[string, unknown]>): Map<string, Declarable> {
|
|
9
|
+
return new Map(pairs as Array<[string, Declarable]>);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Split the emitted multi-document YAML into documents. */
|
|
13
|
+
function documents(yaml: string): string[] {
|
|
14
|
+
return yaml
|
|
15
|
+
.split(/^---$/m)
|
|
16
|
+
.map((doc) => doc.trim())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("cpln serializer", () => {
|
|
21
|
+
it("has the correct name and rule prefix", () => {
|
|
22
|
+
expect(cplnSerializer.name).toBe("cpln");
|
|
23
|
+
expect(cplnSerializer.rulePrefix).toBe("CPL");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("serializes an empty map to an empty string", () => {
|
|
27
|
+
expect(cplnSerializer.serialize(new Map())).toBe("");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("emits kind and name first", () => {
|
|
31
|
+
const gvc = new Gvc({ name: "prod", spec: { staticPlacement: { locationLinks: ["//location/aws-us-east-1"] } } });
|
|
32
|
+
const output = cplnSerializer.serialize(entities([["gvc", gvc]]));
|
|
33
|
+
|
|
34
|
+
expect(output.startsWith("kind: gvc\nname: prod\n")).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("emits the gvc key for a GVC-scoped kind", () => {
|
|
38
|
+
const workload = new Workload({ name: "web", gvc: "prod", spec: { type: "serverless" } });
|
|
39
|
+
const output = cplnSerializer.serialize(entities([["web", workload]]));
|
|
40
|
+
|
|
41
|
+
expect(output).toContain("kind: workload");
|
|
42
|
+
expect(output).toContain("gvc: prod");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("falls back to the entity name when no name is declared", () => {
|
|
46
|
+
const secret = new Secret({ type: "opaque" } as never);
|
|
47
|
+
const output = cplnSerializer.serialize(entities([["fallback-name", secret]]));
|
|
48
|
+
|
|
49
|
+
expect(output).toContain("name: fallback-name");
|
|
50
|
+
expect(output).not.toContain("undefined");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("never emits server-side fields", () => {
|
|
54
|
+
// Control Plane's own guidance: `status`, `id`, `created`, `lastModified`
|
|
55
|
+
// and `links` break `cpln apply`. They are attributes here, so they cannot
|
|
56
|
+
// reach a document — this asserts that stays true.
|
|
57
|
+
const workload = new Workload({ name: "web", gvc: "prod", spec: { type: "serverless" } });
|
|
58
|
+
const output = cplnSerializer.serialize(entities([["web", workload]]));
|
|
59
|
+
|
|
60
|
+
for (const field of ["status:", "id:", "created:", "lastModified:", "links:", "version:"]) {
|
|
61
|
+
expect(output, `${field} reached the manifest`).not.toContain(field);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("orders documents dependency-first and sorts within a kind", () => {
|
|
66
|
+
const gvc = new Gvc({ name: "prod" });
|
|
67
|
+
const secret = new Secret({ name: "db", type: "opaque" });
|
|
68
|
+
const beta = new Workload({ name: "beta", gvc: "prod", spec: { type: "serverless" } });
|
|
69
|
+
const alpha = new Workload({ name: "alpha", gvc: "prod", spec: { type: "serverless" } });
|
|
70
|
+
|
|
71
|
+
const output = cplnSerializer.serialize(
|
|
72
|
+
entities([
|
|
73
|
+
["beta", beta],
|
|
74
|
+
["gvc", gvc],
|
|
75
|
+
["alpha", alpha],
|
|
76
|
+
["secret", secret],
|
|
77
|
+
]),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const kinds = documents(output).map((doc) => doc.split("\n")[0]);
|
|
81
|
+
expect(kinds).toEqual(["kind: gvc", "kind: secret", "kind: workload", "kind: workload"]);
|
|
82
|
+
|
|
83
|
+
const workloadNames = documents(output)
|
|
84
|
+
.filter((doc) => doc.startsWith("kind: workload"))
|
|
85
|
+
.map((doc) => doc.split("\n")[1]);
|
|
86
|
+
expect(workloadNames).toEqual(["name: alpha", "name: beta"]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("resolves a resource reference to its Control Plane link", () => {
|
|
90
|
+
const identity = new Identity({ name: "api-identity", gvc: "prod" });
|
|
91
|
+
const workload = new Workload({
|
|
92
|
+
name: "api",
|
|
93
|
+
gvc: "prod",
|
|
94
|
+
spec: { type: "serverless", identityLink: identity as never },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const output = cplnSerializer.serialize(
|
|
98
|
+
entities([
|
|
99
|
+
["identity", identity],
|
|
100
|
+
["api", workload],
|
|
101
|
+
]),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// The GVC-qualified form specifically: the bare //identity/NAME is
|
|
105
|
+
// silently ignored by Control Plane.
|
|
106
|
+
expect(output).toContain("identityLink: //gvc/prod/identity/api-identity");
|
|
107
|
+
expect(output).not.toContain("identityLink: //identity/");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("uses the declared name, not the entity name, when building a link", () => {
|
|
111
|
+
const secret = new Secret({ name: "db-password", type: "opaque" });
|
|
112
|
+
const policy = new Policy({
|
|
113
|
+
name: "grants",
|
|
114
|
+
targetKind: "secret",
|
|
115
|
+
targetLinks: [secret as never],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const output = cplnSerializer.serialize(
|
|
119
|
+
entities([
|
|
120
|
+
["secretEntity", secret],
|
|
121
|
+
["grants", policy],
|
|
122
|
+
]),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
expect(output).toContain("//secret/db-password");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("stamps the ownership marker even when no tags are declared", () => {
|
|
129
|
+
const gvc = new Gvc({ name: "prod" });
|
|
130
|
+
const output = cplnSerializer.serialize(entities([["gvc", gvc]]), undefined, {
|
|
131
|
+
ownership: { stack: "demo", env: "prod" },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(output).toContain("chant.intentius.io/managed-by: chant");
|
|
135
|
+
expect(output).toContain("chant.intentius.io/stack: demo");
|
|
136
|
+
expect(output).toContain("chant.intentius.io/env: prod");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("merges the ownership marker with declared tags", () => {
|
|
140
|
+
const gvc = new Gvc({ name: "prod", tags: { team: "platform" } });
|
|
141
|
+
const output = cplnSerializer.serialize(entities([["gvc", gvc]]), undefined, {
|
|
142
|
+
ownership: { stack: "demo" },
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(output).toContain("team: platform");
|
|
146
|
+
expect(output).toContain("chant.intentius.io/managed-by: chant");
|
|
147
|
+
// No env in the marker means no env key, not an empty one.
|
|
148
|
+
expect(output).not.toContain("chant.intentius.io/env");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("emits no tags block when there is nothing to stamp", () => {
|
|
152
|
+
const gvc = new Gvc({ name: "prod" });
|
|
153
|
+
expect(cplnSerializer.serialize(entities([["gvc", gvc]]))).not.toContain("tags:");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("ignores entities from other lexicons", () => {
|
|
157
|
+
const foreign = { entityType: "K8s::Apps::Deployment", props: { name: "x" } };
|
|
158
|
+
const gvc = new Gvc({ name: "prod" });
|
|
159
|
+
const output = cplnSerializer.serialize(
|
|
160
|
+
entities([
|
|
161
|
+
["foreign", foreign],
|
|
162
|
+
["gvc", gvc],
|
|
163
|
+
]),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
expect(documents(output)).toHaveLength(1);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("refuses to serialize an attribute reference", () => {
|
|
170
|
+
const gvc = new Gvc({ name: "prod" });
|
|
171
|
+
// The build pipeline assigns logical names before serializing; doing it
|
|
172
|
+
// here is what routes the walker into the visitor's `attrRef` rather than
|
|
173
|
+
// failing earlier on an unnamed ref.
|
|
174
|
+
(gvc.id as unknown as { _setLogicalName(name: string): void })._setLogicalName("gvc");
|
|
175
|
+
|
|
176
|
+
const workload = new Workload({
|
|
177
|
+
name: "web",
|
|
178
|
+
gvc: "prod",
|
|
179
|
+
spec: { type: "serverless", identityLink: gvc.id as never },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(() =>
|
|
183
|
+
cplnSerializer.serialize(
|
|
184
|
+
entities([
|
|
185
|
+
["gvc", gvc],
|
|
186
|
+
["web", workload],
|
|
187
|
+
]),
|
|
188
|
+
),
|
|
189
|
+
).toThrow(/no.*template-time reference language/i);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("serializes a volume set mount alongside its workload", () => {
|
|
193
|
+
const volumeSet = new VolumeSet({
|
|
194
|
+
name: "data",
|
|
195
|
+
gvc: "prod",
|
|
196
|
+
spec: { initialCapacity: 20, fileSystemType: "ext4", performanceClass: "general-purpose-ssd" },
|
|
197
|
+
});
|
|
198
|
+
const workload = new Workload({
|
|
199
|
+
name: "db",
|
|
200
|
+
gvc: "prod",
|
|
201
|
+
spec: {
|
|
202
|
+
type: "stateful",
|
|
203
|
+
containers: [
|
|
204
|
+
{ name: "main", image: "postgres:17", volumes: [{ uri: "cpln://volumeset/data", path: "/data" }] },
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const output = cplnSerializer.serialize(
|
|
210
|
+
entities([
|
|
211
|
+
["db", workload],
|
|
212
|
+
["data", volumeSet],
|
|
213
|
+
]),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const kinds = documents(output).map((doc) => doc.split("\n")[0]);
|
|
217
|
+
expect(kinds).toEqual(["kind: volumeset", "kind: workload"]);
|
|
218
|
+
expect(output).toContain("uri: cpln://volumeset/data");
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("cplnLink", () => {
|
|
223
|
+
it("builds each kind's link form", () => {
|
|
224
|
+
expect(cplnLink(kindByName("gvc")!, "prod")).toBe("//gvc/prod");
|
|
225
|
+
expect(cplnLink(kindByName("secret")!, "db")).toBe("//secret/db");
|
|
226
|
+
expect(cplnLink(kindByName("workload")!, "web", "prod")).toBe("//gvc/prod/workload/web");
|
|
227
|
+
expect(cplnLink(kindByName("identity")!, "api", "prod")).toBe("//gvc/prod/identity/api");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("refuses a GVC-scoped link with no resolvable GVC", () => {
|
|
231
|
+
expect(() => cplnLink(kindByName("identity")!, "api")).toThrow(/silently ignored/);
|
|
232
|
+
});
|
|
233
|
+
});
|