@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,78 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { readArray, readBoolean, readPath, readString } from "../../entity-props";
|
|
3
|
+
import { WORKLOAD, autoscalingOf, entitiesOfType, workloadType } from "./helpers";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CPL027: Capacity AI combined with something it excludes.
|
|
7
|
+
*
|
|
8
|
+
* Capacity AI resizes CPU and memory from observed usage, and it is **on by
|
|
9
|
+
* default** for serverless, standard and cron — so these conflicts are usually
|
|
10
|
+
* reached by adding CPU autoscaling or a GPU to a workload that never opted
|
|
11
|
+
* into Capacity AI in the first place. Each conflict has the same root: the
|
|
12
|
+
* other feature needs a stable resource baseline that Capacity AI is actively
|
|
13
|
+
* moving.
|
|
14
|
+
*/
|
|
15
|
+
export const capacityAiConflictCheck: PostSynthCheck = {
|
|
16
|
+
id: "CPL027",
|
|
17
|
+
description: "Capacity AI conflicts with CPU-utilization autoscaling, multi-metric autoscaling and GPUs",
|
|
18
|
+
|
|
19
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
20
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
21
|
+
|
|
22
|
+
for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
|
|
23
|
+
const type = workloadType(entity);
|
|
24
|
+
// Default-on for every type except stateful, so `undefined` means enabled
|
|
25
|
+
// for those — which is exactly how these conflicts get reached unnoticed.
|
|
26
|
+
const declared = readBoolean(entity, "spec", "defaultOptions", "capacityAI");
|
|
27
|
+
const enabled = declared ?? type !== "stateful";
|
|
28
|
+
if (!enabled) continue;
|
|
29
|
+
|
|
30
|
+
const implicit = declared === undefined;
|
|
31
|
+
const how = implicit ? "Capacity AI is on by default for this workload type and" : "Capacity AI";
|
|
32
|
+
|
|
33
|
+
const scaling = autoscalingOf(entity);
|
|
34
|
+
const metric = scaling && typeof scaling === "object" ? readString(scaling, "metric") : undefined;
|
|
35
|
+
const multi = scaling && typeof scaling === "object" ? readArray(scaling, "multi") : [];
|
|
36
|
+
|
|
37
|
+
if (metric === "cpu") {
|
|
38
|
+
diagnostics.push({
|
|
39
|
+
checkId: "CPL027",
|
|
40
|
+
severity: "error",
|
|
41
|
+
message:
|
|
42
|
+
`Workload "${name}": ${how} conflicts with CPU-utilization autoscaling — dynamic CPU allocation ` +
|
|
43
|
+
`moves the baseline the metric scales against. Set defaultOptions.capacityAI false, or scale on ` +
|
|
44
|
+
`rps or memory.`,
|
|
45
|
+
entity: name,
|
|
46
|
+
lexicon: "cpln",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (multi.length > 0) {
|
|
51
|
+
diagnostics.push({
|
|
52
|
+
checkId: "CPL027",
|
|
53
|
+
severity: "error",
|
|
54
|
+
message:
|
|
55
|
+
`Workload "${name}": ${how} conflicts with multi-metric autoscaling, which requires stable ` +
|
|
56
|
+
`resource baselines. Set defaultOptions.capacityAI false.`,
|
|
57
|
+
entity: name,
|
|
58
|
+
lexicon: "cpln",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const container of readArray(entity, "spec", "containers")) {
|
|
63
|
+
if (readPath(container, "gpu") === undefined) continue;
|
|
64
|
+
diagnostics.push({
|
|
65
|
+
checkId: "CPL027",
|
|
66
|
+
severity: "error",
|
|
67
|
+
message:
|
|
68
|
+
`Workload "${name}" container "${readString(container, "name") ?? "?"}" requests a GPU, and ` +
|
|
69
|
+
`${how} conflicts with it — GPU allocation is fixed. Set defaultOptions.capacityAI false.`,
|
|
70
|
+
entity: name,
|
|
71
|
+
lexicon: "cpln",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return diagnostics;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { readArray, readNumber, readString } from "../../entity-props";
|
|
3
|
+
import { GVC, VOLUMESET, WORKLOAD, entitiesOfType } from "./helpers";
|
|
4
|
+
|
|
5
|
+
/** Minimum initial capacity in GB, by performance class. */
|
|
6
|
+
const MIN_CAPACITY_GB: Record<string, number> = {
|
|
7
|
+
"general-purpose-ssd": 10,
|
|
8
|
+
"high-throughput-ssd": 200,
|
|
9
|
+
shared: 10,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const MAX_CAPACITY_GB = 65_536;
|
|
13
|
+
|
|
14
|
+
/** Filesystems that bind to exactly one stateful workload. */
|
|
15
|
+
const EXCLUSIVE_FILESYSTEMS = new Set(["ext4", "xfs"]);
|
|
16
|
+
|
|
17
|
+
/** Control Plane caps a workload at 15 volumes. */
|
|
18
|
+
const MAX_VOLUMES = 15;
|
|
19
|
+
|
|
20
|
+
const VOLUMESET_URI = /^cpln:\/\/volumeset\/(.+)$/;
|
|
21
|
+
|
|
22
|
+
export const volumeSetCheck: PostSynthCheck = {
|
|
23
|
+
id: "CPL028",
|
|
24
|
+
description: "Volume set capacity, filesystem binding, and workload mount rules",
|
|
25
|
+
|
|
26
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
27
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
28
|
+
|
|
29
|
+
// Volume sets by their declared name, so a mount can be checked against the
|
|
30
|
+
// set it names rather than only against itself.
|
|
31
|
+
const volumeSets = new Map<string, { entity: unknown; gvc?: string; fileSystemType: string }>();
|
|
32
|
+
|
|
33
|
+
for (const [name, entity] of entitiesOfType(ctx.entities, VOLUMESET)) {
|
|
34
|
+
const declaredName = readString(entity, "name") ?? name;
|
|
35
|
+
const fileSystemType = readString(entity, "spec", "fileSystemType") ?? "ext4";
|
|
36
|
+
volumeSets.set(declaredName, { entity, gvc: readString(entity, "gvc"), fileSystemType });
|
|
37
|
+
|
|
38
|
+
const performanceClass =
|
|
39
|
+
readString(entity, "spec", "performanceClass") ?? (fileSystemType === "shared" ? "shared" : "general-purpose-ssd");
|
|
40
|
+
const capacity = readNumber(entity, "spec", "initialCapacity");
|
|
41
|
+
|
|
42
|
+
const floor = MIN_CAPACITY_GB[performanceClass];
|
|
43
|
+
if (capacity !== undefined && floor !== undefined && capacity < floor) {
|
|
44
|
+
diagnostics.push({
|
|
45
|
+
checkId: "CPL028",
|
|
46
|
+
severity: "error",
|
|
47
|
+
message:
|
|
48
|
+
`Volume set "${name}" requests ${capacity} GB with performanceClass "${performanceClass}", ` +
|
|
49
|
+
`below its ${floor} GB minimum. performanceClass is immutable after creation, so correcting ` +
|
|
50
|
+
`this later means recreating the volume set and losing its data.`,
|
|
51
|
+
entity: name,
|
|
52
|
+
lexicon: "cpln",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (capacity !== undefined && capacity > MAX_CAPACITY_GB) {
|
|
57
|
+
diagnostics.push({
|
|
58
|
+
checkId: "CPL028",
|
|
59
|
+
severity: "error",
|
|
60
|
+
message: `Volume set "${name}" requests ${capacity} GB, above the ${MAX_CAPACITY_GB} GB maximum.`,
|
|
61
|
+
entity: name,
|
|
62
|
+
lexicon: "cpln",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (fileSystemType === "shared" && performanceClass !== "shared") {
|
|
67
|
+
diagnostics.push({
|
|
68
|
+
checkId: "CPL028",
|
|
69
|
+
severity: "warning",
|
|
70
|
+
message:
|
|
71
|
+
`Volume set "${name}" has fileSystemType "shared" but performanceClass "${performanceClass}". ` +
|
|
72
|
+
`Control Plane forces "shared" for a shared filesystem, so the declared value will not match ` +
|
|
73
|
+
`what comes back and will read as drift on every plan.`,
|
|
74
|
+
entity: name,
|
|
75
|
+
lexicon: "cpln",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Custom encryption is AWS KMS on a block device; a shared filesystem has
|
|
80
|
+
// no device to encrypt.
|
|
81
|
+
if (fileSystemType === "shared" && readString(entity, "spec", "customEncryption", "secretLink")) {
|
|
82
|
+
diagnostics.push({
|
|
83
|
+
checkId: "CPL028",
|
|
84
|
+
severity: "error",
|
|
85
|
+
message: `Volume set "${name}" sets customEncryption with fileSystemType "shared"; it is supported on ext4 and xfs only.`,
|
|
86
|
+
entity: name,
|
|
87
|
+
lexicon: "cpln",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
|
|
93
|
+
const type = readString(entity, "spec", "type") ?? "serverless";
|
|
94
|
+
const workloadGvc = readString(entity, "gvc");
|
|
95
|
+
const paths: string[] = [];
|
|
96
|
+
let mountCount = 0;
|
|
97
|
+
|
|
98
|
+
for (const container of readArray(entity, "spec", "containers")) {
|
|
99
|
+
const containerName = readString(container, "name") ?? "?";
|
|
100
|
+
|
|
101
|
+
for (const volume of readArray(container, "volumes")) {
|
|
102
|
+
mountCount++;
|
|
103
|
+
const uri = readString(volume, "uri");
|
|
104
|
+
const path = readString(volume, "path");
|
|
105
|
+
if (path) paths.push(path);
|
|
106
|
+
|
|
107
|
+
const match = uri ? VOLUMESET_URI.exec(uri) : null;
|
|
108
|
+
if (!match) continue;
|
|
109
|
+
|
|
110
|
+
const target = volumeSets.get(match[1]);
|
|
111
|
+
if (!target) continue; // declared elsewhere; CPL029 covers dangling references
|
|
112
|
+
|
|
113
|
+
if (EXCLUSIVE_FILESYSTEMS.has(target.fileSystemType) && type !== "stateful") {
|
|
114
|
+
diagnostics.push({
|
|
115
|
+
checkId: "CPL028",
|
|
116
|
+
severity: "error",
|
|
117
|
+
message:
|
|
118
|
+
`Workload "${name}" is type "${type}" and container "${containerName}" mounts volume set ` +
|
|
119
|
+
`"${match[1]}", whose fileSystemType "${target.fileSystemType}" binds to exactly one ` +
|
|
120
|
+
`stateful workload. Use a "shared" volume set, or make the workload stateful.`,
|
|
121
|
+
entity: name,
|
|
122
|
+
lexicon: "cpln",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (target.gvc && workloadGvc && target.gvc !== workloadGvc) {
|
|
127
|
+
diagnostics.push({
|
|
128
|
+
checkId: "CPL028",
|
|
129
|
+
severity: "error",
|
|
130
|
+
message:
|
|
131
|
+
`Workload "${name}" (GVC "${workloadGvc}") mounts volume set "${match[1]}" from GVC ` +
|
|
132
|
+
`"${target.gvc}". Volume sets are only mountable from within their own GVC.`,
|
|
133
|
+
entity: name,
|
|
134
|
+
lexicon: "cpln",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (mountCount > MAX_VOLUMES) {
|
|
141
|
+
diagnostics.push({
|
|
142
|
+
checkId: "CPL028",
|
|
143
|
+
severity: "error",
|
|
144
|
+
message: `Workload "${name}" mounts ${mountCount} volumes; the maximum is ${MAX_VOLUMES}.`,
|
|
145
|
+
entity: name,
|
|
146
|
+
lexicon: "cpln",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const [i, path] of paths.entries()) {
|
|
151
|
+
for (const other of paths.slice(i + 1)) {
|
|
152
|
+
if (path === other) {
|
|
153
|
+
diagnostics.push({
|
|
154
|
+
checkId: "CPL028",
|
|
155
|
+
severity: "error",
|
|
156
|
+
message: `Workload "${name}" mounts two volumes at the same path "${path}".`,
|
|
157
|
+
entity: name,
|
|
158
|
+
lexicon: "cpln",
|
|
159
|
+
});
|
|
160
|
+
} else if (isParentPath(path, other) || isParentPath(other, path)) {
|
|
161
|
+
diagnostics.push({
|
|
162
|
+
checkId: "CPL028",
|
|
163
|
+
severity: "error",
|
|
164
|
+
message:
|
|
165
|
+
`Workload "${name}" mounts volumes at "${path}" and "${other}"; no mount path may be a ` +
|
|
166
|
+
`parent of another.`,
|
|
167
|
+
entity: name,
|
|
168
|
+
lexicon: "cpln",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return diagnostics;
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** Whether `parent` is a proper ancestor directory of `child`. */
|
|
180
|
+
function isParentPath(parent: string, child: string): boolean {
|
|
181
|
+
const normalized = parent.endsWith("/") ? parent : `${parent}/`;
|
|
182
|
+
return child.startsWith(normalized);
|
|
183
|
+
}
|
|
@@ -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
|
+
};
|