@intentius/chant-lexicon-k8s 0.49.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/composites/index.d.ts +2 -0
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/operator-stack.d.ts +183 -0
- package/dist/composites/operator-stack.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +35 -35
- package/dist/lint/post-synth/argo-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/k8s-helpers.d.ts +28 -5
- package/dist/lint/post-synth/k8s-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8005.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8006.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8041.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8042.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8101.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8102.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8103.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8104.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8105.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8201.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8202.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8203.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8204.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8205.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8207.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8208.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8209.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8301.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8302.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8303.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8304.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8305.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8306.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8401.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8402.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8403.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8404.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8405.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8406.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8407.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/op/builders.d.ts +69 -0
- package/dist/op/builders.d.ts.map +1 -0
- package/dist/rules/argo-helpers.ts +2 -6
- package/dist/rules/crd-schema-helpers.ts +4 -6
- package/dist/rules/k8s-helpers.ts +32 -23
- package/dist/rules/wk8005.ts +27 -32
- package/dist/rules/wk8006.ts +33 -38
- package/dist/rules/wk8041.ts +19 -24
- package/dist/rules/wk8042.ts +17 -22
- package/dist/rules/wk8101.ts +29 -34
- package/dist/rules/wk8102.ts +13 -18
- package/dist/rules/wk8103.ts +17 -22
- package/dist/rules/wk8104.ts +30 -35
- package/dist/rules/wk8105.ts +16 -21
- package/dist/rules/wk8201.ts +26 -31
- package/dist/rules/wk8202.ts +17 -22
- package/dist/rules/wk8203.ts +17 -22
- package/dist/rules/wk8204.ts +46 -51
- package/dist/rules/wk8205.ts +27 -32
- package/dist/rules/wk8207.ts +14 -19
- package/dist/rules/wk8208.ts +14 -19
- package/dist/rules/wk8209.ts +14 -19
- package/dist/rules/wk8301.ts +25 -30
- package/dist/rules/wk8302.ts +19 -24
- package/dist/rules/wk8303.ts +23 -26
- package/dist/rules/wk8304.ts +35 -40
- package/dist/rules/wk8305.ts +39 -42
- package/dist/rules/wk8306.ts +20 -25
- package/dist/rules/wk8401.ts +59 -64
- package/dist/rules/wk8402.ts +13 -18
- package/dist/rules/wk8403.ts +32 -37
- package/dist/rules/wk8404.ts +15 -21
- package/dist/rules/wk8405.ts +15 -18
- package/dist/rules/wk8406.ts +29 -34
- package/dist/rules/wk8407.ts +17 -22
- package/dist/serializer.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/composites/composites.test.ts +283 -0
- package/src/composites/index.ts +4 -0
- package/src/composites/operator-stack.ts +385 -0
- package/src/index.ts +11 -0
- package/src/lint/post-synth/argo-helpers.ts +2 -6
- package/src/lint/post-synth/crd-schema-helpers.ts +4 -6
- package/src/lint/post-synth/k8s-helpers.test.ts +26 -9
- package/src/lint/post-synth/k8s-helpers.ts +32 -23
- package/src/lint/post-synth/wk8005.ts +27 -32
- package/src/lint/post-synth/wk8006.ts +33 -38
- package/src/lint/post-synth/wk8041.ts +19 -24
- package/src/lint/post-synth/wk8042.ts +17 -22
- package/src/lint/post-synth/wk8101.ts +29 -34
- package/src/lint/post-synth/wk8102.ts +13 -18
- package/src/lint/post-synth/wk8103.ts +17 -22
- package/src/lint/post-synth/wk8104.ts +30 -35
- package/src/lint/post-synth/wk8105.ts +16 -21
- package/src/lint/post-synth/wk8201.ts +26 -31
- package/src/lint/post-synth/wk8202.ts +17 -22
- package/src/lint/post-synth/wk8203.ts +17 -22
- package/src/lint/post-synth/wk8204.ts +46 -51
- package/src/lint/post-synth/wk8205.ts +27 -32
- package/src/lint/post-synth/wk8207.ts +14 -19
- package/src/lint/post-synth/wk8208.ts +14 -19
- package/src/lint/post-synth/wk8209.ts +14 -19
- package/src/lint/post-synth/wk8301.ts +25 -30
- package/src/lint/post-synth/wk8302.ts +19 -24
- package/src/lint/post-synth/wk8303.ts +23 -26
- package/src/lint/post-synth/wk8304.ts +35 -40
- package/src/lint/post-synth/wk8305.ts +39 -42
- package/src/lint/post-synth/wk8306.ts +20 -25
- package/src/lint/post-synth/wk8401.ts +59 -64
- package/src/lint/post-synth/wk8402.ts +13 -18
- package/src/lint/post-synth/wk8403.ts +32 -37
- package/src/lint/post-synth/wk8404.ts +15 -21
- package/src/lint/post-synth/wk8405.ts +15 -18
- package/src/lint/post-synth/wk8406.ts +29 -34
- package/src/lint/post-synth/wk8407.ts +17 -22
- package/src/op/builders.test.ts +82 -0
- package/src/op/builders.ts +95 -0
- package/src/serializer.ts +9 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
10
|
-
import {
|
|
10
|
+
import { docsToManifests, extractRayVersion } from "./k8s-helpers";
|
|
11
11
|
|
|
12
12
|
export const wk8403: PostSynthCheck = {
|
|
13
13
|
id: "WK8403",
|
|
@@ -16,42 +16,37 @@ export const wk8403: PostSynthCheck = {
|
|
|
16
16
|
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
17
17
|
const diagnostics: PostSynthDiagnostic[] = [];
|
|
18
18
|
|
|
19
|
-
for (const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
message: `RayCluster "${clusterName}": spec.rayVersion "${rayVersion}" does not match head image tag "${imageVersion}" — autoscaler may run a mismatched Ray version`,
|
|
51
|
-
entity: clusterName,
|
|
52
|
-
lexicon: "k8s",
|
|
53
|
-
});
|
|
54
|
-
}
|
|
19
|
+
for (const manifest of docsToManifests(ctx)) {
|
|
20
|
+
if (manifest.kind !== "RayCluster") continue;
|
|
21
|
+
|
|
22
|
+
const clusterName = manifest.metadata?.name ?? "RayCluster";
|
|
23
|
+
const spec = manifest.spec as Record<string, unknown> | undefined;
|
|
24
|
+
if (!spec) continue;
|
|
25
|
+
|
|
26
|
+
const rayVersion = spec.rayVersion as string | undefined;
|
|
27
|
+
if (!rayVersion) continue; // WK8402 covers the missing case
|
|
28
|
+
|
|
29
|
+
// Extract version from head container image
|
|
30
|
+
const headGroupSpec = spec.headGroupSpec as Record<string, unknown> | undefined;
|
|
31
|
+
const tmpl = headGroupSpec?.template as Record<string, unknown> | undefined;
|
|
32
|
+
const podSpec = tmpl?.spec as Record<string, unknown> | undefined;
|
|
33
|
+
const containers = podSpec?.containers as Array<Record<string, unknown>> | undefined;
|
|
34
|
+
if (!Array.isArray(containers) || containers.length === 0) continue;
|
|
35
|
+
|
|
36
|
+
const image = containers[0].image as string | undefined;
|
|
37
|
+
if (!image) continue;
|
|
38
|
+
|
|
39
|
+
const imageVersion = extractRayVersion(image);
|
|
40
|
+
if (!imageVersion) continue; // Can't determine version from tag
|
|
41
|
+
|
|
42
|
+
if (imageVersion !== rayVersion) {
|
|
43
|
+
diagnostics.push({
|
|
44
|
+
checkId: "WK8403",
|
|
45
|
+
severity: "warning",
|
|
46
|
+
message: `RayCluster "${clusterName}": spec.rayVersion "${rayVersion}" does not match head image tag "${imageVersion}" — autoscaler may run a mismatched Ray version`,
|
|
47
|
+
entity: clusterName,
|
|
48
|
+
lexicon: "k8s",
|
|
49
|
+
});
|
|
55
50
|
}
|
|
56
51
|
}
|
|
57
52
|
|
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
12
12
|
import {
|
|
13
|
-
|
|
14
|
-
parseK8sManifests,
|
|
13
|
+
docsToManifests,
|
|
15
14
|
extractContainers,
|
|
16
15
|
extractPodSpec,
|
|
17
16
|
requestsGpu,
|
|
@@ -26,28 +25,23 @@ export const wk8404: PostSynthCheck = {
|
|
|
26
25
|
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
27
26
|
const diagnostics: PostSynthDiagnostic[] = [];
|
|
28
27
|
|
|
29
|
-
for (const
|
|
30
|
-
|
|
31
|
-
const manifests = parseK8sManifests(yaml);
|
|
28
|
+
for (const manifest of docsToManifests(ctx)) {
|
|
29
|
+
if (!manifest.kind || !GPU_POD_KINDS.has(manifest.kind)) continue;
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
const containers = extractContainers(manifest);
|
|
32
|
+
if (!requestsGpu(containers)) continue;
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
const podSpec = extractPodSpec(manifest);
|
|
35
|
+
if (toleratesGpu(podSpec?.tolerations)) continue;
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
entity: resourceName,
|
|
48
|
-
lexicon: "k8s",
|
|
49
|
-
});
|
|
50
|
-
}
|
|
37
|
+
const resourceName = manifest.metadata?.name ?? manifest.kind;
|
|
38
|
+
diagnostics.push({
|
|
39
|
+
checkId: "WK8404",
|
|
40
|
+
severity: "error",
|
|
41
|
+
message: `${manifest.kind} "${resourceName}" requests nvidia.com/gpu but has no toleration for the nvidia.com/gpu taint — it will not schedule onto a tainted GPU node pool`,
|
|
42
|
+
entity: resourceName,
|
|
43
|
+
lexicon: "k8s",
|
|
44
|
+
});
|
|
51
45
|
}
|
|
52
46
|
|
|
53
47
|
return diagnostics;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
13
|
-
import {
|
|
13
|
+
import { docsToManifests } from "./k8s-helpers";
|
|
14
14
|
import type { K8sManifest } from "./k8s-helpers";
|
|
15
15
|
|
|
16
16
|
/** `app.kubernetes.io/component` values this lexicon's composites use for serving workloads. */
|
|
@@ -30,27 +30,24 @@ export const wk8405: PostSynthCheck = {
|
|
|
30
30
|
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
31
31
|
const diagnostics: PostSynthDiagnostic[] = [];
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
const yaml = getPrimaryOutput(output);
|
|
35
|
-
const manifests = parseK8sManifests(yaml);
|
|
33
|
+
const manifests = docsToManifests(ctx);
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
const pdbSelectors = collectPdbSelectors(manifests);
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
for (const manifest of manifests) {
|
|
38
|
+
if (!isServingWorkload(manifest)) continue;
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
const labels = manifest.metadata?.labels;
|
|
41
|
+
const resourceName = manifest.metadata?.name ?? manifest.kind ?? "workload";
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
43
|
+
if (!labels || !hasCoveringPdb(labels, pdbSelectors)) {
|
|
44
|
+
diagnostics.push({
|
|
45
|
+
checkId: "WK8405",
|
|
46
|
+
severity: "info",
|
|
47
|
+
message: `${manifest.kind} "${resourceName}" is a serving workload with no PodDisruptionBudget — add a PDB so a node drain or cluster upgrade cannot take down every replica at once`,
|
|
48
|
+
entity: resourceName,
|
|
49
|
+
lexicon: "k8s",
|
|
50
|
+
});
|
|
54
51
|
}
|
|
55
52
|
}
|
|
56
53
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
12
|
-
import {
|
|
12
|
+
import { docsToManifests, extractContainers, GPU_POD_KINDS, NVIDIA_GPU_RESOURCE } from "./k8s-helpers";
|
|
13
13
|
|
|
14
14
|
export const wk8406: PostSynthCheck = {
|
|
15
15
|
id: "WK8406",
|
|
@@ -18,39 +18,34 @@ export const wk8406: PostSynthCheck = {
|
|
|
18
18
|
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
19
19
|
const diagnostics: PostSynthDiagnostic[] = [];
|
|
20
20
|
|
|
21
|
-
for (const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (!limits
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
message: `GPU container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" is missing resource limits for ${missing.join(", ")} — a GPU pod without cpu/memory limits can starve its (scarce, expensive) node`,
|
|
50
|
-
entity: resourceName,
|
|
51
|
-
lexicon: "k8s",
|
|
52
|
-
});
|
|
53
|
-
}
|
|
21
|
+
for (const manifest of docsToManifests(ctx)) {
|
|
22
|
+
if (!manifest.kind || !GPU_POD_KINDS.has(manifest.kind)) continue;
|
|
23
|
+
|
|
24
|
+
const containers = extractContainers(manifest);
|
|
25
|
+
const resourceName = manifest.metadata?.name ?? manifest.kind;
|
|
26
|
+
|
|
27
|
+
for (const container of containers) {
|
|
28
|
+
const requests = container.resources?.requests as Record<string, unknown> | undefined;
|
|
29
|
+
const limits = container.resources?.limits as Record<string, unknown> | undefined;
|
|
30
|
+
const requestsGpu = Boolean(requests?.[NVIDIA_GPU_RESOURCE]) || Boolean(limits?.[NVIDIA_GPU_RESOURCE]);
|
|
31
|
+
if (!requestsGpu) continue;
|
|
32
|
+
|
|
33
|
+
const missing: string[] = [];
|
|
34
|
+
if (!limits || typeof limits !== "object") {
|
|
35
|
+
missing.push("cpu", "memory");
|
|
36
|
+
} else {
|
|
37
|
+
if (!limits.cpu) missing.push("cpu");
|
|
38
|
+
if (!limits.memory) missing.push("memory");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (missing.length > 0) {
|
|
42
|
+
diagnostics.push({
|
|
43
|
+
checkId: "WK8406",
|
|
44
|
+
severity: "warning",
|
|
45
|
+
message: `GPU container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" is missing resource limits for ${missing.join(", ")} — a GPU pod without cpu/memory limits can starve its (scarce, expensive) node`,
|
|
46
|
+
entity: resourceName,
|
|
47
|
+
lexicon: "k8s",
|
|
48
|
+
});
|
|
54
49
|
}
|
|
55
50
|
}
|
|
56
51
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
15
|
-
import {
|
|
15
|
+
import { docsToManifests } from "./k8s-helpers";
|
|
16
16
|
|
|
17
17
|
/** Trailing path segments that mean "no explicit version" even though present. */
|
|
18
18
|
const FLOATING_TAGS = new Set(["latest", "main", "master", "head"]);
|
|
@@ -40,29 +40,24 @@ export const wk8407: PostSynthCheck = {
|
|
|
40
40
|
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
41
41
|
const diagnostics: PostSynthDiagnostic[] = [];
|
|
42
42
|
|
|
43
|
-
for (const
|
|
44
|
-
|
|
45
|
-
const manifests = parseK8sManifests(yaml);
|
|
43
|
+
for (const manifest of docsToManifests(ctx)) {
|
|
44
|
+
if (manifest.kind !== "InferenceService") continue;
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
const spec = manifest.spec as Record<string, unknown> | undefined;
|
|
47
|
+
const predictor = spec?.predictor as Record<string, unknown> | undefined;
|
|
48
|
+
const model = predictor?.model as Record<string, unknown> | undefined;
|
|
49
|
+
const storageUri = model?.storageUri as string | undefined;
|
|
50
|
+
if (!storageUri) continue;
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
checkId: "WK8407",
|
|
60
|
-
severity: "warning",
|
|
61
|
-
message: `InferenceService "${resourceName}": model storageUri "${storageUri}" has no explicit version segment — pin it (e.g. via the Model composite's required \`version\`) so a redeploy can't silently pick up different weights`,
|
|
62
|
-
entity: resourceName,
|
|
63
|
-
lexicon: "k8s",
|
|
64
|
-
});
|
|
65
|
-
}
|
|
52
|
+
if (!isPinnedStorageUri(storageUri)) {
|
|
53
|
+
const resourceName = manifest.metadata?.name ?? "InferenceService";
|
|
54
|
+
diagnostics.push({
|
|
55
|
+
checkId: "WK8407",
|
|
56
|
+
severity: "warning",
|
|
57
|
+
message: `InferenceService "${resourceName}": model storageUri "${storageUri}" has no explicit version segment — pin it (e.g. via the Model composite's required \`version\`) so a redeploy can't silently pick up different weights`,
|
|
58
|
+
entity: resourceName,
|
|
59
|
+
lexicon: "k8s",
|
|
60
|
+
});
|
|
66
61
|
}
|
|
67
62
|
}
|
|
68
63
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed step-builder wrappers (chant #1288 Stage 2). Two things asserted:
|
|
3
|
+
*
|
|
4
|
+
* - **Backward compat**: for every input core's original untyped builder
|
|
5
|
+
* accepted, the new typed twin here produces a byte-identical
|
|
6
|
+
* `ActivityStep` (own enumerable fields — `.out` is a non-enumerable
|
|
7
|
+
* getter on both, per `builders.ts`'s `activity()`, so `toEqual` never
|
|
8
|
+
* sees it). Same construction path (`activity()` + `takeProfileAndId`),
|
|
9
|
+
* so this is really asserting the two call sites agree, not re-deriving
|
|
10
|
+
* the logic.
|
|
11
|
+
* - **Step-output refs compose**: a {@link StepOutputRef} in a typed slot
|
|
12
|
+
* type-checks and lands in the step's `args` untouched (chant #1950).
|
|
13
|
+
*
|
|
14
|
+
* The `@ts-expect-error` block at the bottom is a compile-time-only check —
|
|
15
|
+
* never executed, just walked by `npm run typecheck` (tsc), which fails if a
|
|
16
|
+
* marked line does NOT error (an "unused '@ts-expect-error' directive").
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, test, expect } from "vitest";
|
|
20
|
+
import {
|
|
21
|
+
kubectlApply as kubectlApplyOld,
|
|
22
|
+
waitForReady as waitForReadyOld,
|
|
23
|
+
ensureSecret as ensureSecretOld,
|
|
24
|
+
stepOutput,
|
|
25
|
+
type StepOutputRef,
|
|
26
|
+
} from "@intentius/chant/op";
|
|
27
|
+
import { kubectlApply, waitForReady, ensureSecret } from "./builders";
|
|
28
|
+
|
|
29
|
+
describe("k8s typed step builders (#1288 Stage 2)", () => {
|
|
30
|
+
test("kubectlApply: identical ActivityStep to core's original for a minimal call", () => {
|
|
31
|
+
expect(kubectlApply("dist/k8s.yaml")).toEqual(kubectlApplyOld("dist/k8s.yaml"));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("kubectlApply: identical ActivityStep with opts (profile override, force, deleteMode)", () => {
|
|
35
|
+
const opts = { context: "kind-dev", force: true, deleteMode: "owned-only" as const, profile: "fastIdempotent" as const };
|
|
36
|
+
expect(kubectlApply("dist/k8s.yaml", opts)).toEqual(kubectlApplyOld("dist/k8s.yaml", opts));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("kubectlApply: accepts a StepOutputRef in a typed slot", () => {
|
|
40
|
+
const ref = stepOutput("build-step", "manifestPath");
|
|
41
|
+
const step = kubectlApply("dist/k8s.yaml", { context: ref });
|
|
42
|
+
expect(step.args?.context).toBe(ref);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("waitForReady: identical ActivityStep to core's original", () => {
|
|
46
|
+
const opts = { namespace: "prod", intervalMs: 5000 };
|
|
47
|
+
expect(waitForReady("certificate", "my-cert", opts)).toEqual(waitForReadyOld("certificate", "my-cert", opts));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("ensureSecret: identical ActivityStep to core's original", () => {
|
|
51
|
+
const opts = { metadata: { team: "platform" } };
|
|
52
|
+
expect(ensureSecret("db-creds", ["password"], opts)).toEqual(ensureSecretOld("db-creds", ["password"], opts));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("kubectlApply: .out is reachable when an id is given", () => {
|
|
56
|
+
const step = kubectlApply("dist/k8s.yaml", { id: "apply" });
|
|
57
|
+
const ref: StepOutputRef = step.out.fieldManager;
|
|
58
|
+
expect(ref.step).toBe("apply");
|
|
59
|
+
expect(ref.path).toBe("fieldManager");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ── Compile-time-only: authoring-time type errors (never executed) ──────────
|
|
64
|
+
function _typeChecksOnly(): void {
|
|
65
|
+
// @ts-expect-error — "nameSpace" is not a key of KubectlApplyArgs (the
|
|
66
|
+
// exact typo class chant #1288 names: helmInstall("api", "./chart", {
|
|
67
|
+
// nameSpace: "prod" }) silently no-opped before this).
|
|
68
|
+
kubectlApply("dist/k8s.yaml", { nameSpace: "prod" });
|
|
69
|
+
|
|
70
|
+
// @ts-expect-error — deleteMode must be one of ApplyDeleteMode's literals.
|
|
71
|
+
kubectlApply("dist/k8s.yaml", { deleteMode: "always" });
|
|
72
|
+
|
|
73
|
+
// @ts-expect-error — force must be boolean, not a string.
|
|
74
|
+
kubectlApply("dist/k8s.yaml", { force: "yes" });
|
|
75
|
+
|
|
76
|
+
// @ts-expect-error — waitForReady's readiness spec field is misspelled.
|
|
77
|
+
waitForReady("certificate", "my-cert", { specc: {} });
|
|
78
|
+
|
|
79
|
+
// @ts-expect-error — ensureSecret's keys must be string[], not a single string.
|
|
80
|
+
ensureSecret("db-creds", "password");
|
|
81
|
+
}
|
|
82
|
+
void _typeChecksOnly;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed step-builder wrappers for this lexicon's activities (chant #1288
|
|
3
|
+
* Stage 2 — "regenerate the step builders as fully typed wrappers"). Core
|
|
4
|
+
* cannot import a lexicon's `*Args` types (lexicons depend on core, not the
|
|
5
|
+
* other way around — see `packages/core/src/op/activity-contract.ts`'s module
|
|
6
|
+
* doc for the same layering constraint on Stage 1's contracts), so a fully
|
|
7
|
+
* typed `kubectlApply`/`waitForReady`/`ensureSecret` has to live beside the
|
|
8
|
+
* `*Args` interface it derives from: here, next to `./activities/kubectl.ts`
|
|
9
|
+
* etc. `opts`'s type in each wrapper below IS the activity's own `*Args`
|
|
10
|
+
* interface (via `Omit`/`WithStepRefs`) — never a hand-restated mirror — so
|
|
11
|
+
* `kubectlApply`/`waitForReady`/`ensureSecret`'s full option surface (the
|
|
12
|
+
* dozen-plus cross-field options on `kubectlApply` that Stage 1's zod
|
|
13
|
+
* contracts deliberately skipped rather than duplicate) is authoring-time
|
|
14
|
+
* typed for free, and stays in sync with the implementation by construction.
|
|
15
|
+
*
|
|
16
|
+
* Every field also accepts a {@link StepOutputRef} in its place
|
|
17
|
+
* ({@link WithStepRefs}, chant #1950) — a later step's args can consume an
|
|
18
|
+
* earlier step's declared output.
|
|
19
|
+
*
|
|
20
|
+
* `core`'s own `kubectlApply`/`waitForReady`/`ensureSecret` (in
|
|
21
|
+
* `@intentius/chant/op`, re-exported from `@intentius/chant-lexicon-temporal`
|
|
22
|
+
* for lexicon-agnostic single-import convenience) are UNCHANGED and produce
|
|
23
|
+
* byte-identical `ActivityStep` output for the same inputs — these are purely
|
|
24
|
+
* additive. Deliberately not swapped into the temporal barrel: that would
|
|
25
|
+
* make `@intentius/chant-lexicon-temporal` depend on this package (and on
|
|
26
|
+
* helm, for `helmInstall`) at runtime, which is exactly the "a worker image
|
|
27
|
+
* needs no kubectl binary" / "temporal stays product-agnostic" property the
|
|
28
|
+
* `op/activities/index.ts` module docs across this repo call out on purpose
|
|
29
|
+
* (activities were moved OUT of temporal into per-product lexicons for this
|
|
30
|
+
* reason; the step-builder layer shouldn't reintroduce the coupling from the
|
|
31
|
+
* other direction). An author who wants the typed surface imports it from
|
|
32
|
+
* here — `@intentius/chant-lexicon-k8s` — which a project using `kubectlApply`
|
|
33
|
+
* already depends on; existing `@intentius/chant-lexicon-temporal` imports
|
|
34
|
+
* keep working exactly as before, opting in only if the import is changed.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
activity,
|
|
39
|
+
takeProfileAndId,
|
|
40
|
+
type ActivityStep,
|
|
41
|
+
type NamedActivityStep,
|
|
42
|
+
type WithStepRefs,
|
|
43
|
+
} from "@intentius/chant/op";
|
|
44
|
+
import type { KubectlApplyArgs } from "./activities/kubectl";
|
|
45
|
+
import type { WaitForReadyArgs } from "./activities/wait-for-ready";
|
|
46
|
+
import type { EnsureSecretActivityArgs } from "./activities/ensure-secret";
|
|
47
|
+
|
|
48
|
+
/** Extra opts every wrapper below accepts alongside its activity's own fields. */
|
|
49
|
+
type StepOpts = { profile?: ActivityStep["profile"]; id?: string };
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run `kubectl apply -f <manifest>` — the fully typed twin of core's
|
|
53
|
+
* `kubectlApply` step-builder shortcut (chant #1288 Stage 2). `opts` is
|
|
54
|
+
* {@link KubectlApplyArgs} itself (minus the positional `manifest`) — every
|
|
55
|
+
* field `applyManifest` accepts, including `deleteMode`, `force`,
|
|
56
|
+
* `fieldManager`, `documents`, is authoring-time typed here, not just the
|
|
57
|
+
* subset a hand-written zod mirror would bother restating. Defaults to the
|
|
58
|
+
* `longInfra` profile (override via `opts.profile`).
|
|
59
|
+
*/
|
|
60
|
+
export const kubectlApply = (
|
|
61
|
+
manifest: string,
|
|
62
|
+
opts?: WithStepRefs<Omit<KubectlApplyArgs, "manifest">> & StepOpts,
|
|
63
|
+
): NamedActivityStep => {
|
|
64
|
+
const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
|
|
65
|
+
return activity("kubectlApply", { manifest, ...args }, { profile: profile ?? "longInfra", ...(id ? { id } : {}) });
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Poll a Kubernetes resource until it reports ready, driven by a data-only
|
|
70
|
+
* readiness spec (CRD-aware; #365) — the fully typed twin of core's
|
|
71
|
+
* `waitForReady`. `opts` is {@link WaitForReadyArgs} itself, minus the
|
|
72
|
+
* positional `kind`/`name`. Defaults to the `k8sWait` profile.
|
|
73
|
+
*/
|
|
74
|
+
export const waitForReady = (
|
|
75
|
+
kind: string,
|
|
76
|
+
name: string,
|
|
77
|
+
opts?: WithStepRefs<Omit<WaitForReadyArgs, "kind" | "name">> & StepOpts,
|
|
78
|
+
): NamedActivityStep => {
|
|
79
|
+
const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
|
|
80
|
+
return activity("waitForReady", { kind, name, ...args }, { profile: profile ?? "k8sWait", ...(id ? { id } : {}) });
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Ensure a `generated-once` secret exists in the target store (#1829) — the
|
|
85
|
+
* fully typed twin of core's `ensureSecret`. `opts` is
|
|
86
|
+
* {@link EnsureSecretActivityArgs} itself, minus the positional `name`/`keys`.
|
|
87
|
+
*/
|
|
88
|
+
export const ensureSecret = (
|
|
89
|
+
name: string,
|
|
90
|
+
keys: string[],
|
|
91
|
+
opts?: WithStepRefs<Omit<EnsureSecretActivityArgs, "name" | "keys">> & StepOpts,
|
|
92
|
+
): NamedActivityStep => {
|
|
93
|
+
const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
|
|
94
|
+
return activity("ensureSecret", { name, keys, ...args }, { profile, ...(id ? { id } : {}) });
|
|
95
|
+
};
|
package/src/serializer.ts
CHANGED
|
@@ -258,6 +258,8 @@ export const k8sSerializer: Serializer = {
|
|
|
258
258
|
const otherDocs: string[] = [];
|
|
259
259
|
/** Sidecar files — committed ciphertext, copied byte-for-byte. */
|
|
260
260
|
const files: Record<string, string> = {};
|
|
261
|
+
/** Basenames within `files` that must never be JSON.parse round-tripped (chant#1937). */
|
|
262
|
+
const verbatimFiles: string[] = [];
|
|
261
263
|
const warnings: string[] = [];
|
|
262
264
|
|
|
263
265
|
for (const [name, entity] of entities) {
|
|
@@ -280,6 +282,7 @@ export const k8sSerializer: Serializer = {
|
|
|
280
282
|
);
|
|
281
283
|
}
|
|
282
284
|
files[entity.filename] = entity.text;
|
|
285
|
+
if (!verbatimFiles.includes(entity.filename)) verbatimFiles.push(entity.filename);
|
|
283
286
|
continue;
|
|
284
287
|
}
|
|
285
288
|
|
|
@@ -417,7 +420,12 @@ export const k8sSerializer: Serializer = {
|
|
|
417
420
|
// A bare string when there is nothing extra to write, so the common case
|
|
418
421
|
// stays byte-identical to what every existing consumer already reads.
|
|
419
422
|
if (Object.keys(files).length === 0 && warnings.length === 0) return primary;
|
|
420
|
-
return {
|
|
423
|
+
return {
|
|
424
|
+
primary,
|
|
425
|
+
files,
|
|
426
|
+
...(verbatimFiles.length > 0 ? { verbatimFiles } : {}),
|
|
427
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
428
|
+
};
|
|
421
429
|
},
|
|
422
430
|
};
|
|
423
431
|
|