@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,155 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { propsOf, readString } from "../../entity-props";
|
|
3
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
4
|
+
import { KINDS, kindByTypeName } from "../../kinds";
|
|
5
|
+
import { GVC, WORKLOAD, entitiesOfType, parseLink } from "./helpers";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Every string anywhere in a value, so links are found wherever they were
|
|
9
|
+
* written. Depth-bounded — a spec is deep but finite, and a cycle would not be.
|
|
10
|
+
*/
|
|
11
|
+
function* strings(value: unknown, depth = 0): Generator<string> {
|
|
12
|
+
if (depth > 12) return;
|
|
13
|
+
if (typeof value === "string") {
|
|
14
|
+
yield value;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
for (const item of value) yield* strings(item, depth + 1);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (value && typeof value === "object") {
|
|
22
|
+
for (const nested of Object.values(propsOf(value))) yield* strings(nested, depth + 1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A workload's identity must live in the workload's own GVC.
|
|
28
|
+
*
|
|
29
|
+
* Identities cannot be shared across GVCs — the documented remedy is to
|
|
30
|
+
* recreate the identity with the same spec in each GVC that needs it. A
|
|
31
|
+
* cross-GVC `identityLink` is therefore never right, however reasonable it
|
|
32
|
+
* reads.
|
|
33
|
+
*/
|
|
34
|
+
function identityGvcCheck(entities: Map<string, Declarable>): PostSynthDiagnostic[] {
|
|
35
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
36
|
+
|
|
37
|
+
for (const [name, entity] of entitiesOfType(entities, WORKLOAD)) {
|
|
38
|
+
const workloadGvc = readString(entity, "gvc");
|
|
39
|
+
const identityLink = readString(entity, "spec", "identityLink");
|
|
40
|
+
if (!workloadGvc || !identityLink) continue;
|
|
41
|
+
|
|
42
|
+
const link = parseLink(identityLink);
|
|
43
|
+
if (!link || link.kind !== "identity" || !link.gvc) continue;
|
|
44
|
+
if (link.gvc === workloadGvc) continue;
|
|
45
|
+
|
|
46
|
+
diagnostics.push({
|
|
47
|
+
checkId: "CPL029",
|
|
48
|
+
severity: "error",
|
|
49
|
+
message:
|
|
50
|
+
`Workload "${name}" is in GVC "${workloadGvc}" but its identityLink names GVC "${link.gvc}". ` +
|
|
51
|
+
`Identities cannot be shared across GVCs — declare an identity with the same spec in ` +
|
|
52
|
+
`"${workloadGvc}".`,
|
|
53
|
+
entity: name,
|
|
54
|
+
lexicon: "cpln",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return diagnostics;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const linkTargetsCheck: PostSynthCheck = {
|
|
62
|
+
id: "CPL029",
|
|
63
|
+
description: "Control Plane links must resolve to a declared resource of the right kind and GVC",
|
|
64
|
+
|
|
65
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
66
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
67
|
+
|
|
68
|
+
// Declared resources by kind → name → its GVC (undefined for org-scoped).
|
|
69
|
+
const declared = new Map<string, Map<string, string | undefined>>();
|
|
70
|
+
const declaredKinds = new Set<string>();
|
|
71
|
+
|
|
72
|
+
for (const [entityName, entity] of ctx.entities) {
|
|
73
|
+
const kind = kindByTypeName(entity.entityType);
|
|
74
|
+
if (!kind) continue;
|
|
75
|
+
declaredKinds.add(kind.kind);
|
|
76
|
+
const byName = declared.get(kind.kind) ?? new Map<string, string | undefined>();
|
|
77
|
+
byName.set(readString(entity, "name") ?? entityName, readString(entity, "gvc"));
|
|
78
|
+
declared.set(kind.kind, byName);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const knownKinds = new Set(KINDS.map((k) => k.kind));
|
|
82
|
+
|
|
83
|
+
for (const [entityName, entity] of ctx.entities) {
|
|
84
|
+
const kind = kindByTypeName(entity.entityType);
|
|
85
|
+
if (!kind) continue;
|
|
86
|
+
const ownGvc = readString(entity, "gvc");
|
|
87
|
+
|
|
88
|
+
for (const value of strings(propsOf(entity))) {
|
|
89
|
+
const link = parseLink(value);
|
|
90
|
+
if (!link || !knownKinds.has(link.kind)) continue;
|
|
91
|
+
|
|
92
|
+
// Only assert against kinds this stack actually declares. Referencing a
|
|
93
|
+
// secret managed by another team is ordinary, not a defect.
|
|
94
|
+
if (!declaredKinds.has(link.kind)) continue;
|
|
95
|
+
|
|
96
|
+
const byName = declared.get(link.kind)!;
|
|
97
|
+
if (!byName.has(link.name)) {
|
|
98
|
+
diagnostics.push({
|
|
99
|
+
checkId: "CPL029",
|
|
100
|
+
severity: "warning",
|
|
101
|
+
message:
|
|
102
|
+
`${kind.kind} "${entityName}" links to "${value}", but no ${link.kind} named "${link.name}" ` +
|
|
103
|
+
`is declared in this stack. Control Plane accepts a dangling link without error.`,
|
|
104
|
+
entity: entityName,
|
|
105
|
+
lexicon: "cpln",
|
|
106
|
+
});
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const targetGvc = byName.get(link.name);
|
|
111
|
+
if (link.gvc && targetGvc && link.gvc !== targetGvc) {
|
|
112
|
+
diagnostics.push({
|
|
113
|
+
checkId: "CPL029",
|
|
114
|
+
severity: "error",
|
|
115
|
+
message:
|
|
116
|
+
`${kind.kind} "${entityName}" links to "${value}", but ${link.kind} "${link.name}" is ` +
|
|
117
|
+
`declared in GVC "${targetGvc}".`,
|
|
118
|
+
entity: entityName,
|
|
119
|
+
lexicon: "cpln",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// A GVC-scoped resource whose GVC is declared here must name one that exists.
|
|
126
|
+
if (declaredKinds.has("gvc")) {
|
|
127
|
+
const gvcNames = new Set<string>();
|
|
128
|
+
for (const [entityName, entity] of entitiesOfType(ctx.entities, GVC)) {
|
|
129
|
+
gvcNames.add(readString(entity, "name") ?? entityName);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const [entityName, entity] of ctx.entities) {
|
|
133
|
+
const kind = kindByTypeName(entity.entityType);
|
|
134
|
+
if (!kind?.gvcScoped) continue;
|
|
135
|
+
const gvc = readString(entity, "gvc");
|
|
136
|
+
if (!gvc || gvcNames.has(gvc)) continue;
|
|
137
|
+
|
|
138
|
+
diagnostics.push({
|
|
139
|
+
checkId: "CPL029",
|
|
140
|
+
severity: "warning",
|
|
141
|
+
message:
|
|
142
|
+
`${kind.kind} "${entityName}" is placed in GVC "${gvc}", which this stack does not declare. ` +
|
|
143
|
+
`If the GVC is managed elsewhere this is fine; if it is a typo, the apply fails with a ` +
|
|
144
|
+
`not-found on the parent.`,
|
|
145
|
+
entity: entityName,
|
|
146
|
+
lexicon: "cpln",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
diagnostics.push(...identityGvcCheck(ctx.entities));
|
|
152
|
+
|
|
153
|
+
return diagnostics;
|
|
154
|
+
},
|
|
155
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { readArray, readPath, readString } from "../../entity-props";
|
|
3
|
+
import { DOMAIN, GVC, WORKLOAD, entitiesOfType, parseLink } from "./helpers";
|
|
4
|
+
|
|
5
|
+
/** An apex domain has exactly two labels: `example.com`, not `api.example.com`. */
|
|
6
|
+
function isApex(name: string): boolean {
|
|
7
|
+
return name.split(".").filter(Boolean).length === 2;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const domainRoutingCheck: PostSynthCheck = {
|
|
11
|
+
id: "CPL030",
|
|
12
|
+
description: "Domain DNS mode, certificate challenge, and routing target must be a valid combination",
|
|
13
|
+
|
|
14
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
15
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
16
|
+
|
|
17
|
+
// Workload types, so a `workloadLink` route can be checked for the stateful
|
|
18
|
+
// requirement that replica-direct routing carries.
|
|
19
|
+
const workloadTypes = new Map<string, string>();
|
|
20
|
+
for (const [entityName, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
|
|
21
|
+
workloadTypes.set(readString(entity, "name") ?? entityName, readString(entity, "spec", "type") ?? "serverless");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const [entityName, entity] of entitiesOfType(ctx.entities, DOMAIN)) {
|
|
25
|
+
const name = readString(entity, "name") ?? entityName;
|
|
26
|
+
const dnsMode = readString(entity, "spec", "dnsMode") ?? "cname";
|
|
27
|
+
const challenge = readString(entity, "spec", "certChallengeType");
|
|
28
|
+
const gvcLink = readString(entity, "spec", "gvcLink");
|
|
29
|
+
const workloadLink = readString(entity, "spec", "workloadLink");
|
|
30
|
+
const ports = readArray(entity, "spec", "ports");
|
|
31
|
+
const routed = ports.some((port) => readArray(port, "routes").length > 0);
|
|
32
|
+
|
|
33
|
+
if (dnsMode === "ns" && isApex(name)) {
|
|
34
|
+
diagnostics.push({
|
|
35
|
+
checkId: "CPL030",
|
|
36
|
+
severity: "error",
|
|
37
|
+
message:
|
|
38
|
+
`Domain "${name}" is an apex domain with dnsMode "ns". NS mode does not support apex domains — ` +
|
|
39
|
+
`use "cname".`,
|
|
40
|
+
entity: entityName,
|
|
41
|
+
lexicon: "cpln",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (dnsMode === "ns" && challenge === "http01") {
|
|
46
|
+
diagnostics.push({
|
|
47
|
+
checkId: "CPL030",
|
|
48
|
+
severity: "error",
|
|
49
|
+
message:
|
|
50
|
+
`Domain "${name}" uses dnsMode "ns" with certChallengeType "http01", which is rejected. ` +
|
|
51
|
+
`NS mode requires "dns01".`,
|
|
52
|
+
entity: entityName,
|
|
53
|
+
lexicon: "cpln",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const targets = [gvcLink && "gvcLink", workloadLink && "workloadLink", routed && "ports[].routes"].filter(
|
|
58
|
+
Boolean,
|
|
59
|
+
) as string[];
|
|
60
|
+
|
|
61
|
+
if (targets.length > 1) {
|
|
62
|
+
diagnostics.push({
|
|
63
|
+
checkId: "CPL030",
|
|
64
|
+
severity: "error",
|
|
65
|
+
message:
|
|
66
|
+
`Domain "${name}" sets ${targets.join(" and ")}. These are mutually exclusive ways of saying ` +
|
|
67
|
+
`where traffic goes — keep exactly one.`,
|
|
68
|
+
entity: entityName,
|
|
69
|
+
lexicon: "cpln",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (targets.length === 0) {
|
|
74
|
+
diagnostics.push({
|
|
75
|
+
checkId: "CPL030",
|
|
76
|
+
severity: "warning",
|
|
77
|
+
message:
|
|
78
|
+
`Domain "${name}" names no routing target. Set one of gvcLink, workloadLink, or ports[].routes, ` +
|
|
79
|
+
`or the domain resolves to nothing.`,
|
|
80
|
+
entity: entityName,
|
|
81
|
+
lexicon: "cpln",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// `workloadLink` is replica-direct routing, which only stateful supports.
|
|
86
|
+
if (workloadLink) {
|
|
87
|
+
const link = parseLink(workloadLink);
|
|
88
|
+
const type = link && workloadTypes.get(link.name);
|
|
89
|
+
if (type && type !== "stateful") {
|
|
90
|
+
diagnostics.push({
|
|
91
|
+
checkId: "CPL030",
|
|
92
|
+
severity: "error",
|
|
93
|
+
message:
|
|
94
|
+
`Domain "${name}" routes via workloadLink to "${link!.name}", which is type "${type}". ` +
|
|
95
|
+
`workloadLink is replica-direct routing and is supported on stateful workloads only.`,
|
|
96
|
+
entity: entityName,
|
|
97
|
+
lexicon: "cpln",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Every route in a domain must target workloads in one GVC.
|
|
103
|
+
const routeGvcs = new Set<string>();
|
|
104
|
+
for (const port of ports) {
|
|
105
|
+
for (const route of readArray(port, "routes")) {
|
|
106
|
+
const target = readString(route, "workloadLink");
|
|
107
|
+
const link = target ? parseLink(target) : undefined;
|
|
108
|
+
if (link?.gvc) routeGvcs.add(link.gvc);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (routeGvcs.size > 1) {
|
|
113
|
+
diagnostics.push({
|
|
114
|
+
checkId: "CPL030",
|
|
115
|
+
severity: "error",
|
|
116
|
+
message:
|
|
117
|
+
`Domain "${name}" routes to workloads in ${routeGvcs.size} GVCs (${[...routeGvcs].sort().join(", ")}). ` +
|
|
118
|
+
`All routes in a domain must target workloads in the same GVC.`,
|
|
119
|
+
entity: entityName,
|
|
120
|
+
lexicon: "cpln",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (readPath(entity, "spec", "acceptAllHosts") === true && readPath(entity, "spec", "acceptAllSubdomains") === true) {
|
|
125
|
+
diagnostics.push({
|
|
126
|
+
checkId: "CPL030",
|
|
127
|
+
severity: "error",
|
|
128
|
+
message: `Domain "${name}" sets both acceptAllHosts and acceptAllSubdomains; they are mutually exclusive.`,
|
|
129
|
+
entity: entityName,
|
|
130
|
+
lexicon: "cpln",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return diagnostics;
|
|
136
|
+
},
|
|
137
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { readArray, readString } from "../../entity-props";
|
|
3
|
+
import { WORKLOAD, entitiesOfType, imageTag } from "./helpers";
|
|
4
|
+
|
|
5
|
+
/** A digest pin — `@sha256:…` — is reproducible regardless of tag. */
|
|
6
|
+
const DIGEST = /@sha\d+:[0-9a-f]+$/i;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* CPL040: an image reference that is not pinned.
|
|
10
|
+
*
|
|
11
|
+
* `:latest`, or no tag at all (which *means* `:latest`), makes what runs a
|
|
12
|
+
* function of when the workload last happened to pull. On a serverless
|
|
13
|
+
* workload that scales to zero and cold-starts, that can be several times a
|
|
14
|
+
* day, so two replicas of "the same" deploy can be different builds.
|
|
15
|
+
*/
|
|
16
|
+
export const pinnedImageCheck: PostSynthCheck = {
|
|
17
|
+
id: "CPL040",
|
|
18
|
+
description: "Container images should be pinned to a specific tag or digest",
|
|
19
|
+
|
|
20
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
21
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
22
|
+
|
|
23
|
+
for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
|
|
24
|
+
for (const container of readArray(entity, "spec", "containers")) {
|
|
25
|
+
const containerName = readString(container, "name") ?? "?";
|
|
26
|
+
const image = readString(container, "image");
|
|
27
|
+
if (!image || DIGEST.test(image)) continue;
|
|
28
|
+
|
|
29
|
+
const tag = imageTag(image);
|
|
30
|
+
if (tag && tag !== "latest") continue;
|
|
31
|
+
|
|
32
|
+
diagnostics.push({
|
|
33
|
+
checkId: "CPL040",
|
|
34
|
+
severity: "warning",
|
|
35
|
+
message:
|
|
36
|
+
`Workload "${name}" container "${containerName}" uses image "${image}", which is ` +
|
|
37
|
+
(tag === "latest" ? `tagged \`latest\`` : `untagged (implicitly \`latest\`)`) +
|
|
38
|
+
`. Pin a version or a digest — a scale-from-zero cold start re-pulls, so what runs can change ` +
|
|
39
|
+
`without a deploy.`,
|
|
40
|
+
entity: name,
|
|
41
|
+
lexicon: "cpln",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return diagnostics;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { readArray, readString } from "../../entity-props";
|
|
3
|
+
import { WORKLOAD, entitiesOfType } from "./helpers";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CPL041: registry prefixes Control Plane does not want in a workload spec.
|
|
7
|
+
*
|
|
8
|
+
* Two documented traps. `docker.io/` must never be added to a public image —
|
|
9
|
+
* the exact string `nginx:latest` works and `docker.io/library/nginx:latest`
|
|
10
|
+
* does not. And `<org>.registry.cpln.io/…` is the hostname for `docker login`
|
|
11
|
+
* and `docker push` only; inside a workload spec the org's own images are
|
|
12
|
+
* addressed as `//image/NAME:TAG`.
|
|
13
|
+
*/
|
|
14
|
+
export const imageReferenceFormCheck: PostSynthCheck = {
|
|
15
|
+
id: "CPL041",
|
|
16
|
+
description: "Image references must not use docker.io/ or the registry hostname form",
|
|
17
|
+
|
|
18
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
19
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
20
|
+
|
|
21
|
+
for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
|
|
22
|
+
for (const container of readArray(entity, "spec", "containers")) {
|
|
23
|
+
const containerName = readString(container, "name") ?? "?";
|
|
24
|
+
const image = readString(container, "image");
|
|
25
|
+
if (!image) continue;
|
|
26
|
+
|
|
27
|
+
if (image.startsWith("docker.io/")) {
|
|
28
|
+
const bare = image.replace(/^docker\.io\/(library\/)?/, "");
|
|
29
|
+
diagnostics.push({
|
|
30
|
+
checkId: "CPL041",
|
|
31
|
+
severity: "error",
|
|
32
|
+
message:
|
|
33
|
+
`Workload "${name}" container "${containerName}" uses image "${image}". Control Plane rejects ` +
|
|
34
|
+
`the docker.io/ prefix on public images — use "${bare}".`,
|
|
35
|
+
entity: name,
|
|
36
|
+
lexicon: "cpln",
|
|
37
|
+
});
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const selfHosted = /^([a-z0-9-]+)\.registry\.cpln\.io\/(.+)$/.exec(image);
|
|
42
|
+
if (selfHosted) {
|
|
43
|
+
diagnostics.push({
|
|
44
|
+
checkId: "CPL041",
|
|
45
|
+
severity: "warning",
|
|
46
|
+
message:
|
|
47
|
+
`Workload "${name}" container "${containerName}" uses image "${image}". The ` +
|
|
48
|
+
`<org>.registry.cpln.io hostname is for \`docker login\` and \`docker push\`; in a workload ` +
|
|
49
|
+
`spec, an image in this org is "//image/${selfHosted[2]}". (A cross-org pull does use the ` +
|
|
50
|
+
`hostname form — ignore this if "${selfHosted[1]}" is another org.)`,
|
|
51
|
+
entity: name,
|
|
52
|
+
lexicon: "cpln",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return diagnostics;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { readArray, readPath, readString } from "../../entity-props";
|
|
3
|
+
import { GVC, entitiesOfType } from "./helpers";
|
|
4
|
+
|
|
5
|
+
/** Location links look like `/org/ORG/location/aws-us-east-1` or `//location/aws-us-east-1`. */
|
|
6
|
+
const LOCATION_LINK = /\/location\/((aws|gcp|azure)-[a-z0-9-]+|[a-z0-9-]+)$/;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* CPL042: a GVC with nowhere to run.
|
|
10
|
+
*
|
|
11
|
+
* Placement is a GVC-level decision, and a GVC with neither `locationLinks` nor
|
|
12
|
+
* a `locationQuery` accepts workloads and schedules them nowhere. The workloads
|
|
13
|
+
* apply cleanly and never become ready.
|
|
14
|
+
*/
|
|
15
|
+
export const gvcPlacementCheck: PostSynthCheck = {
|
|
16
|
+
id: "CPL042",
|
|
17
|
+
description: "A GVC must declare where its workloads run",
|
|
18
|
+
|
|
19
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
20
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
21
|
+
|
|
22
|
+
for (const [entityName, entity] of entitiesOfType(ctx.entities, GVC)) {
|
|
23
|
+
const name = readString(entity, "name") ?? entityName;
|
|
24
|
+
const links = readArray(entity, "spec", "staticPlacement", "locationLinks");
|
|
25
|
+
const query = readPath(entity, "spec", "staticPlacement", "locationQuery");
|
|
26
|
+
|
|
27
|
+
if (links.length === 0 && query === undefined) {
|
|
28
|
+
diagnostics.push({
|
|
29
|
+
checkId: "CPL042",
|
|
30
|
+
severity: "error",
|
|
31
|
+
message:
|
|
32
|
+
`GVC "${name}" declares no placement. Set spec.staticPlacement.locationLinks (or a ` +
|
|
33
|
+
`locationQuery) — without one, its workloads apply cleanly and are scheduled nowhere.`,
|
|
34
|
+
entity: entityName,
|
|
35
|
+
lexicon: "cpln",
|
|
36
|
+
});
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const link of links) {
|
|
41
|
+
if (typeof link !== "string") continue;
|
|
42
|
+
if (LOCATION_LINK.test(link)) continue;
|
|
43
|
+
diagnostics.push({
|
|
44
|
+
checkId: "CPL042",
|
|
45
|
+
severity: "warning",
|
|
46
|
+
message:
|
|
47
|
+
`GVC "${name}" has location link "${link}", which does not look like a location. Expected ` +
|
|
48
|
+
`/org/<org>/location/<provider>-<region>, e.g. /org/acme/location/aws-us-east-1.`,
|
|
49
|
+
entity: entityName,
|
|
50
|
+
lexicon: "cpln",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return diagnostics;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
Binary file
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared reading and parsing for the CPL post-synth checks.
|
|
3
|
+
*
|
|
4
|
+
* Each check lives in its own file named for its id, so `CPL027` is greppable
|
|
5
|
+
* to one place. What they have in common lands here rather than being repeated
|
|
6
|
+
* or, worse, drifting: the workload-type default, the port enumeration that has
|
|
7
|
+
* to span both `port` and `ports`, and the quantity parsers.
|
|
8
|
+
*
|
|
9
|
+
* Named `helpers.ts` deliberately — the completeness checker's post-synth count
|
|
10
|
+
* excludes that filename, so a support module is not miscounted as a check.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
14
|
+
import { readArray, readNumber, readPath, readString } from "../../entity-props";
|
|
15
|
+
import { kindByName } from "../../kinds";
|
|
16
|
+
|
|
17
|
+
export const WORKLOAD = kindByName("workload")!.typeName;
|
|
18
|
+
export const GVC = kindByName("gvc")!.typeName;
|
|
19
|
+
export const POLICY = kindByName("policy")!.typeName;
|
|
20
|
+
export const DOMAIN = kindByName("domain")!.typeName;
|
|
21
|
+
export const VOLUMESET = kindByName("volumeset")!.typeName;
|
|
22
|
+
|
|
23
|
+
/** Control Plane defaults an unset workload type to `serverless`. */
|
|
24
|
+
export function workloadType(entity: unknown): string {
|
|
25
|
+
return readString(entity, "spec", "type") ?? "serverless";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The autoscaling block from `defaultOptions`. */
|
|
29
|
+
export function autoscalingOf(entity: unknown): unknown {
|
|
30
|
+
return readPath(entity, "spec", "defaultOptions", "autoscaling");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Every port a workload's containers expose, across both `port` and `ports`. */
|
|
34
|
+
export function exposedPorts(entity: unknown): number[] {
|
|
35
|
+
const ports: number[] = [];
|
|
36
|
+
for (const container of readArray(entity, "spec", "containers")) {
|
|
37
|
+
const single = readNumber(container, "port");
|
|
38
|
+
if (single !== undefined) ports.push(single);
|
|
39
|
+
for (const port of readArray(container, "ports")) {
|
|
40
|
+
const number = readNumber(port, "number");
|
|
41
|
+
if (number !== undefined) ports.push(number);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return ports;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Parse a CPU quantity (`"50m"`, `"1"`, `"1.5"`) to millicores. */
|
|
48
|
+
export function parseCpuMillicores(value: string | undefined): number | undefined {
|
|
49
|
+
if (!value) return undefined;
|
|
50
|
+
const match = /^(\d+(?:\.\d+)?)(m?)$/.exec(value.trim());
|
|
51
|
+
if (!match) return undefined;
|
|
52
|
+
const amount = Number(match[1]);
|
|
53
|
+
return match[2] === "m" ? amount : amount * 1000;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Parse a memory quantity (`"128Mi"`, `"1Gi"`, `"512M"`) to MiB. */
|
|
57
|
+
export function parseMemoryMib(value: string | undefined): number | undefined {
|
|
58
|
+
if (!value) return undefined;
|
|
59
|
+
const match = /^(\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti|K|M|G|T)?$/.exec(value.trim());
|
|
60
|
+
if (!match) return undefined;
|
|
61
|
+
const amount = Number(match[1]);
|
|
62
|
+
const factors: Record<string, number> = {
|
|
63
|
+
Ki: 1 / 1024,
|
|
64
|
+
Mi: 1,
|
|
65
|
+
Gi: 1024,
|
|
66
|
+
Ti: 1024 * 1024,
|
|
67
|
+
// Decimal suffixes, converted to MiB so one comparison covers both families.
|
|
68
|
+
K: 1000 / (1024 * 1024),
|
|
69
|
+
M: 1_000_000 / (1024 * 1024),
|
|
70
|
+
G: 1_000_000_000 / (1024 * 1024),
|
|
71
|
+
T: 1_000_000_000_000 / (1024 * 1024),
|
|
72
|
+
};
|
|
73
|
+
const suffix = match[2];
|
|
74
|
+
if (!suffix) return amount / (1024 * 1024); // bare bytes
|
|
75
|
+
return amount * factors[suffix];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The tag of an image reference, or undefined when untagged.
|
|
80
|
+
*
|
|
81
|
+
* A colon in a registry host's port (`registry:5000/app`) is not a tag, so only
|
|
82
|
+
* a colon after the last slash counts.
|
|
83
|
+
*/
|
|
84
|
+
export function imageTag(image: string): string | undefined {
|
|
85
|
+
const lastSlash = image.lastIndexOf("/");
|
|
86
|
+
const colon = image.indexOf(":", lastSlash + 1);
|
|
87
|
+
return colon === -1 ? undefined : image.slice(colon + 1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface ParsedLink {
|
|
91
|
+
kind: string;
|
|
92
|
+
name: string;
|
|
93
|
+
gvc?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** `//gvc/prod/identity/api` → kind `identity`, name `api`, gvc `prod`. */
|
|
97
|
+
const GVC_SCOPED_LINK = /^\/\/gvc\/([^/]+)\/([a-z]+)\/([^/]+)$/;
|
|
98
|
+
/** `//secret/db-password` → kind `secret`, name `db-password`. */
|
|
99
|
+
const ORG_SCOPED_LINK = /^\/\/([a-z]+)\/([^/]+)$/;
|
|
100
|
+
|
|
101
|
+
/** Parse a Control Plane link, or return undefined if it is not one. */
|
|
102
|
+
export function parseLink(value: string): ParsedLink | undefined {
|
|
103
|
+
const scoped = GVC_SCOPED_LINK.exec(value);
|
|
104
|
+
if (scoped) return { gvc: scoped[1], kind: scoped[2], name: scoped[3] };
|
|
105
|
+
|
|
106
|
+
const org = ORG_SCOPED_LINK.exec(value);
|
|
107
|
+
if (org) return { kind: org[1], name: org[2] };
|
|
108
|
+
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Iterate entities of one type. Re-exported so checks import one module. */
|
|
113
|
+
export { entitiesOfType } from "../../entity-props";
|
|
114
|
+
export type { Declarable };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cpln serializer — emits the multi-document YAML that `cpln apply --file`
|
|
3
|
+
* consumes.
|
|
4
|
+
*
|
|
5
|
+
* Each declared resource becomes one document:
|
|
6
|
+
*
|
|
7
|
+
* kind: workload
|
|
8
|
+
* name: my-app
|
|
9
|
+
* gvc: my-gvc
|
|
10
|
+
* tags:
|
|
11
|
+
* chant.intentius.io/managed-by: chant
|
|
12
|
+
* spec:
|
|
13
|
+
* containers:
|
|
14
|
+
* - name: main
|
|
15
|
+
* image: nginx:1.27
|
|
16
|
+
*
|
|
17
|
+
* Three things this file is careful about.
|
|
18
|
+
*
|
|
19
|
+
* **Only authoring surface is emitted.** Control Plane's own guidance is to
|
|
20
|
+
* export with `-o yaml-slim` rather than `-o yaml` before re-applying, because
|
|
21
|
+
* the server-side fields (`status`, `id`, `created`, `lastModified`, `links`)
|
|
22
|
+
* break `cpln apply`. Those are attributes in this lexicon rather than
|
|
23
|
+
* properties, so they never reach a document in the first place — the shape a
|
|
24
|
+
* user can declare is already the slim shape.
|
|
25
|
+
*
|
|
26
|
+
* **References become links, not names.** Control Plane addresses resources by
|
|
27
|
+
* link (`//gvc/prod/identity/api`, `//secret/db-password`), and the GVC-scoped
|
|
28
|
+
* forms have to carry the GVC. Passing a declared resource where a link is
|
|
29
|
+
* expected resolves to the right link for its kind, which is the difference
|
|
30
|
+
* between a reference that survives a rename and a hand-spelled string that
|
|
31
|
+
* does not. The identity form matters especially: the bare `//identity/NAME`
|
|
32
|
+
* that reads perfectly plausibly is *silently ignored* by Control Plane's
|
|
33
|
+
* policy engine, and is documented as a common silent failure.
|
|
34
|
+
*
|
|
35
|
+
* **Document order is stable and dependency-first.** `cpln apply` resolves
|
|
36
|
+
* ordering itself for a multi-doc file, so this is not needed for correctness —
|
|
37
|
+
* it is so the emitted file diffs cleanly between builds and reads in the order
|
|
38
|
+
* a person would write it.
|
|
39
|
+
*/
|
|
40
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
41
|
+
import type { SerializeContext } from "@intentius/chant/serializer";
|
|
42
|
+
import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
43
|
+
import { type CplnKind } from "./kinds.js";
|
|
44
|
+
/**
|
|
45
|
+
* `satisfies` rather than a type annotation: `Serializer.serialize` returns
|
|
46
|
+
* `string | SerializerResult`, and cpln always emits a single YAML string.
|
|
47
|
+
* Annotating widens the export to the union and every caller — the tests
|
|
48
|
+
* included — has to narrow it back with a cast that asserts something the
|
|
49
|
+
* implementation already guarantees. `satisfies` checks the shape against the
|
|
50
|
+
* interface and keeps the narrower return type.
|
|
51
|
+
*/
|
|
52
|
+
export declare const cplnSerializer: {
|
|
53
|
+
name: string;
|
|
54
|
+
rulePrefix: string;
|
|
55
|
+
serialize(entities: Map<string, Declarable>, _outputs?: LexiconOutput[], context?: SerializeContext): string;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Build the Control Plane link for a declared resource.
|
|
59
|
+
*
|
|
60
|
+
* GVC-scoped kinds nest under their GVC. For identities in particular the
|
|
61
|
+
* nested form is not cosmetic: a policy binding written against the bare
|
|
62
|
+
* `//identity/NAME` is accepted and then ignored.
|
|
63
|
+
*/
|
|
64
|
+
export declare function cplnLink(kind: CplnKind, name: string, gvc?: unknown): string;
|
|
65
|
+
/** Kind order used for document sorting, exported for the serializer tests. */
|
|
66
|
+
export declare const DOCUMENT_KIND_ORDER: readonly string[];
|
|
67
|
+
/** Every modelled kind, re-exported so consumers need not reach into `kinds`. */
|
|
68
|
+
export declare const SERIALIZED_KINDS: CplnKind[];
|
|
69
|
+
//# sourceMappingURL=serializer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,KAAK,EAAc,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAIrE,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAmB/D;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc;;;wBAIL,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,aAAa,aAAa,EAAE,YAAY,gBAAgB,GAAG,MAAM;CA4CxF,CAAC;AA+BvB;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,GAAG,MAAM,CAa5E;AA2ED,+EAA+E;AAC/E,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAAe,CAAC;AAEjE,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,YAAQ,CAAC"}
|