@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,87 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { propsOf, readArray, readString } from "../../entity-props";
|
|
3
|
+
import { WORKLOAD, entitiesOfType, parseCpuMillicores, parseMemoryMib } from "./helpers";
|
|
4
|
+
|
|
5
|
+
/** The tag that raises the memory-to-CPU ceiling from 8 to 32. */
|
|
6
|
+
const RELAX_RATIO_TAG = "cpln/relaxMemoryToCpuRatio";
|
|
7
|
+
|
|
8
|
+
const CPU_MIN_MILLICORES = 25;
|
|
9
|
+
const MEMORY_MIN_MIB = 32;
|
|
10
|
+
const RATIO_MAX = 8;
|
|
11
|
+
const RATIO_MAX_RELAXED = 32;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* CPL023: CPU and memory floors, and the memory-to-CPU ratio.
|
|
15
|
+
*
|
|
16
|
+
* The ratio is the non-obvious one. `memory(MiB) / cpu(millicores)` must be
|
|
17
|
+
* ≤ 8, so `2Gi` of memory needs at least 256 millicores of CPU — a workload
|
|
18
|
+
* asking for lots of memory and a little CPU is rejected, which surprises
|
|
19
|
+
* people coming from Kubernetes where the two are independent. The
|
|
20
|
+
* `cpln/relaxMemoryToCpuRatio` tag raises the ceiling to 32.
|
|
21
|
+
*/
|
|
22
|
+
export const containerResourcesCheck: PostSynthCheck = {
|
|
23
|
+
id: "CPL023",
|
|
24
|
+
description: "Container CPU and memory must clear the platform floors and the memory-to-CPU ratio",
|
|
25
|
+
|
|
26
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
27
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
28
|
+
|
|
29
|
+
for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
|
|
30
|
+
const tags = propsOf(entity).tags;
|
|
31
|
+
const relaxed = !!tags && typeof tags === "object" && RELAX_RATIO_TAG in (tags as Record<string, unknown>);
|
|
32
|
+
const ceiling = relaxed ? RATIO_MAX_RELAXED : RATIO_MAX;
|
|
33
|
+
|
|
34
|
+
for (const container of readArray(entity, "spec", "containers")) {
|
|
35
|
+
const containerName = readString(container, "name") ?? "?";
|
|
36
|
+
// Control Plane's defaults, so an omitted value is checked as what it
|
|
37
|
+
// will actually become rather than skipped.
|
|
38
|
+
const cpu = parseCpuMillicores(readString(container, "cpu") ?? "50m");
|
|
39
|
+
const memory = parseMemoryMib(readString(container, "memory") ?? "128Mi");
|
|
40
|
+
|
|
41
|
+
if (cpu !== undefined && cpu < CPU_MIN_MILLICORES) {
|
|
42
|
+
diagnostics.push({
|
|
43
|
+
checkId: "CPL023",
|
|
44
|
+
severity: "error",
|
|
45
|
+
message:
|
|
46
|
+
`Workload "${name}" container "${containerName}" requests ${cpu}m CPU; the minimum is ` +
|
|
47
|
+
`${CPU_MIN_MILLICORES}m.`,
|
|
48
|
+
entity: name,
|
|
49
|
+
lexicon: "cpln",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (memory !== undefined && memory < MEMORY_MIN_MIB) {
|
|
54
|
+
diagnostics.push({
|
|
55
|
+
checkId: "CPL023",
|
|
56
|
+
severity: "error",
|
|
57
|
+
message:
|
|
58
|
+
`Workload "${name}" container "${containerName}" requests ${Math.round(memory)}Mi memory; ` +
|
|
59
|
+
`the minimum is ${MEMORY_MIN_MIB}Mi.`,
|
|
60
|
+
entity: name,
|
|
61
|
+
lexicon: "cpln",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (cpu !== undefined && memory !== undefined && cpu > 0) {
|
|
66
|
+
const ratio = memory / cpu;
|
|
67
|
+
if (ratio > ceiling) {
|
|
68
|
+
const needed = Math.ceil(memory / ceiling);
|
|
69
|
+
diagnostics.push({
|
|
70
|
+
checkId: "CPL023",
|
|
71
|
+
severity: "error",
|
|
72
|
+
message:
|
|
73
|
+
`Workload "${name}" container "${containerName}" has a memory-to-CPU ratio of ` +
|
|
74
|
+
`${ratio.toFixed(1)} (${Math.round(memory)}Mi / ${cpu}m), above the limit of ${ceiling}. ` +
|
|
75
|
+
`Raise CPU to at least ${needed}m` +
|
|
76
|
+
(relaxed ? "." : `, or add the \`${RELAX_RATIO_TAG}\` tag to raise the ceiling to ${RATIO_MAX_RELAXED}.`),
|
|
77
|
+
entity: name,
|
|
78
|
+
lexicon: "cpln",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return diagnostics;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { readArray, readPath, readString } from "../../entity-props";
|
|
3
|
+
import { WORKLOAD, entitiesOfType } from "./helpers";
|
|
4
|
+
|
|
5
|
+
/** The four mutually exclusive probe handlers. */
|
|
6
|
+
const PROBE_HANDLERS = ["exec", "grpc", "tcpSocket", "httpGet"] as const;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* CPL024: a probe must set exactly one handler.
|
|
10
|
+
*
|
|
11
|
+
* Zero is the interesting case: a `readinessProbe` with only
|
|
12
|
+
* `initialDelaySeconds` and `periodSeconds` set looks configured and probes
|
|
13
|
+
* nothing.
|
|
14
|
+
*/
|
|
15
|
+
export const probeHandlerCheck: PostSynthCheck = {
|
|
16
|
+
id: "CPL024",
|
|
17
|
+
description: "Each health-check probe must set exactly one of exec, grpc, tcpSocket, httpGet",
|
|
18
|
+
|
|
19
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
20
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
21
|
+
|
|
22
|
+
for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
|
|
23
|
+
for (const container of readArray(entity, "spec", "containers")) {
|
|
24
|
+
const containerName = readString(container, "name") ?? "?";
|
|
25
|
+
|
|
26
|
+
for (const probeName of ["readinessProbe", "livenessProbe"]) {
|
|
27
|
+
const probe = readPath(container, probeName);
|
|
28
|
+
if (!probe || typeof probe !== "object") continue;
|
|
29
|
+
|
|
30
|
+
const set = PROBE_HANDLERS.filter((handler) => readPath(probe, handler) !== undefined);
|
|
31
|
+
if (set.length === 1) continue;
|
|
32
|
+
|
|
33
|
+
diagnostics.push({
|
|
34
|
+
checkId: "CPL024",
|
|
35
|
+
severity: "error",
|
|
36
|
+
message:
|
|
37
|
+
set.length === 0
|
|
38
|
+
? `Workload "${name}" container "${containerName}" declares a ${probeName} with no handler. ` +
|
|
39
|
+
`Set exactly one of ${PROBE_HANDLERS.join(", ")} — as written the probe checks nothing.`
|
|
40
|
+
: `Workload "${name}" container "${containerName}" ${probeName} sets ${set.length} handlers ` +
|
|
41
|
+
`(${set.join(", ")}). They are mutually exclusive; keep one.`,
|
|
42
|
+
entity: name,
|
|
43
|
+
lexicon: "cpln",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return diagnostics;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { readArray, readNumber, readString } from "../../entity-props";
|
|
3
|
+
import { WORKLOAD, autoscalingOf, entitiesOfType, workloadType } from "./helpers";
|
|
4
|
+
|
|
5
|
+
/** Which strategies each workload type supports at all. */
|
|
6
|
+
const SUPPORTED_METRICS: Record<string, string[]> = {
|
|
7
|
+
serverless: ["concurrency", "rps", "cpu", "memory", "disabled"],
|
|
8
|
+
standard: ["rps", "cpu", "memory", "latency", "keda", "disabled"],
|
|
9
|
+
stateful: ["rps", "cpu", "memory", "latency", "keda", "disabled"],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* CPL025: mutually exclusive autoscaling fields, and the `target` ceiling.
|
|
14
|
+
*
|
|
15
|
+
* `metric` and `multi` are alternatives, not layers; `target` belongs to the
|
|
16
|
+
* single-metric form and is rejected alongside `multi`; and with cpu or memory
|
|
17
|
+
* as the metric, `target` is a utilization percentage, so above 100 it is not
|
|
18
|
+
* an aggressive setting but an invalid one.
|
|
19
|
+
*/
|
|
20
|
+
export const autoscalingShapeCheck: PostSynthCheck = {
|
|
21
|
+
id: "CPL025",
|
|
22
|
+
description: "Autoscaling `metric`, `multi` and `target` must form a valid combination",
|
|
23
|
+
|
|
24
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
25
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
26
|
+
|
|
27
|
+
for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
|
|
28
|
+
const scaling = autoscalingOf(entity);
|
|
29
|
+
if (!scaling || typeof scaling !== "object") continue;
|
|
30
|
+
|
|
31
|
+
const metric = readString(scaling, "metric");
|
|
32
|
+
const multi = readArray(scaling, "multi");
|
|
33
|
+
const target = readNumber(scaling, "target");
|
|
34
|
+
const type = workloadType(entity);
|
|
35
|
+
|
|
36
|
+
if (metric && multi.length > 0) {
|
|
37
|
+
diagnostics.push({
|
|
38
|
+
checkId: "CPL025",
|
|
39
|
+
severity: "error",
|
|
40
|
+
message:
|
|
41
|
+
`Workload "${name}" sets both autoscaling.metric ("${metric}") and autoscaling.multi. ` +
|
|
42
|
+
`They are mutually exclusive — multi is the multi-metric form and replaces metric.`,
|
|
43
|
+
entity: name,
|
|
44
|
+
lexicon: "cpln",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (target !== undefined && multi.length > 0) {
|
|
49
|
+
diagnostics.push({
|
|
50
|
+
checkId: "CPL025",
|
|
51
|
+
severity: "error",
|
|
52
|
+
message:
|
|
53
|
+
`Workload "${name}" sets autoscaling.target alongside autoscaling.multi. ` +
|
|
54
|
+
`Each entry in multi carries its own target.`,
|
|
55
|
+
entity: name,
|
|
56
|
+
lexicon: "cpln",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (target !== undefined && (metric === "cpu" || metric === "memory") && target > 100) {
|
|
61
|
+
diagnostics.push({
|
|
62
|
+
checkId: "CPL025",
|
|
63
|
+
severity: "error",
|
|
64
|
+
message:
|
|
65
|
+
`Workload "${name}" sets autoscaling.target ${target} with metric "${metric}", which is a ` +
|
|
66
|
+
`utilization percentage capped at 100.`,
|
|
67
|
+
entity: name,
|
|
68
|
+
lexicon: "cpln",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const supported = SUPPORTED_METRICS[type];
|
|
73
|
+
if (metric && supported && !supported.includes(metric)) {
|
|
74
|
+
diagnostics.push({
|
|
75
|
+
checkId: "CPL025",
|
|
76
|
+
severity: "error",
|
|
77
|
+
message:
|
|
78
|
+
`Workload "${name}" is type "${type}" and uses autoscaling metric "${metric}", which that type ` +
|
|
79
|
+
`does not support. Available: ${supported.join(", ")}.`,
|
|
80
|
+
entity: name,
|
|
81
|
+
lexicon: "cpln",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (multi.length > 0 && type === "serverless") {
|
|
86
|
+
diagnostics.push({
|
|
87
|
+
checkId: "CPL025",
|
|
88
|
+
severity: "error",
|
|
89
|
+
message:
|
|
90
|
+
`Workload "${name}" is serverless and uses multi-metric autoscaling, which is available only ` +
|
|
91
|
+
`on standard and stateful workloads.`,
|
|
92
|
+
entity: name,
|
|
93
|
+
lexicon: "cpln",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return diagnostics;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { readNumber, readPath, readString } from "../../entity-props";
|
|
3
|
+
import { WORKLOAD, autoscalingOf, entitiesOfType, workloadType } from "./helpers";
|
|
4
|
+
|
|
5
|
+
/** Strategies that let a workload scale to zero, by workload type. */
|
|
6
|
+
const SCALE_TO_ZERO: Record<string, string[]> = {
|
|
7
|
+
serverless: ["rps", "concurrency"],
|
|
8
|
+
standard: ["keda"],
|
|
9
|
+
stateful: ["keda"],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* CPL026: scale-to-zero that will not happen.
|
|
14
|
+
*
|
|
15
|
+
* `minScale: 0` is accepted on any workload type. It only *takes effect* for
|
|
16
|
+
* serverless with `rps` or `concurrency`, or for standard/stateful under KEDA.
|
|
17
|
+
* Everywhere else the workload holds at one replica and the cost saving that
|
|
18
|
+
* motivated the setting never arrives — with nothing reported.
|
|
19
|
+
*/
|
|
20
|
+
export const scaleToZeroCheck: PostSynthCheck = {
|
|
21
|
+
id: "CPL026",
|
|
22
|
+
description: "minScale 0 must pair with a strategy that can actually scale to zero",
|
|
23
|
+
|
|
24
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
25
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
26
|
+
|
|
27
|
+
for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
|
|
28
|
+
const scaling = autoscalingOf(entity);
|
|
29
|
+
if (!scaling || typeof scaling !== "object") continue;
|
|
30
|
+
if (readNumber(scaling, "minScale") !== 0) continue;
|
|
31
|
+
|
|
32
|
+
const type = workloadType(entity);
|
|
33
|
+
const metric = readString(scaling, "metric") ?? (type === "serverless" ? "concurrency" : undefined);
|
|
34
|
+
|
|
35
|
+
if (type === "cron") {
|
|
36
|
+
diagnostics.push({
|
|
37
|
+
checkId: "CPL026",
|
|
38
|
+
severity: "error",
|
|
39
|
+
message: `Cron workload "${name}" sets minScale 0. Cron workloads cannot scale to zero.`,
|
|
40
|
+
entity: name,
|
|
41
|
+
lexicon: "cpln",
|
|
42
|
+
});
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const allowed = SCALE_TO_ZERO[type] ?? [];
|
|
47
|
+
const usingKeda = metric === "keda" || readPath(scaling, "keda") !== undefined;
|
|
48
|
+
if (allowed.includes(metric ?? "") || (allowed.includes("keda") && usingKeda)) continue;
|
|
49
|
+
|
|
50
|
+
diagnostics.push({
|
|
51
|
+
checkId: "CPL026",
|
|
52
|
+
severity: "warning",
|
|
53
|
+
message:
|
|
54
|
+
`Workload "${name}" (type "${type}") sets minScale 0 with metric "${metric ?? "unset"}", which ` +
|
|
55
|
+
`cannot scale to zero. ` +
|
|
56
|
+
(type === "serverless"
|
|
57
|
+
? `Serverless scales to zero only with "rps" or "concurrency".`
|
|
58
|
+
: `Standard and stateful workloads scale to zero only under KEDA, which must be enabled on the GVC.`) +
|
|
59
|
+
` As written the workload holds at one replica with no error.`,
|
|
60
|
+
entity: name,
|
|
61
|
+
lexicon: "cpln",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return diagnostics;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -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
|
+
}
|