@intentius/chant-lexicon-k8s 0.0.12

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.
Files changed (123) hide show
  1. package/dist/integrity.json +32 -0
  2. package/dist/manifest.json +8 -0
  3. package/dist/meta.json +1413 -0
  4. package/dist/rules/hardcoded-namespace.ts +56 -0
  5. package/dist/rules/k8s-helpers.ts +149 -0
  6. package/dist/rules/wk8005.ts +59 -0
  7. package/dist/rules/wk8006.ts +68 -0
  8. package/dist/rules/wk8041.ts +73 -0
  9. package/dist/rules/wk8042.ts +48 -0
  10. package/dist/rules/wk8101.ts +65 -0
  11. package/dist/rules/wk8102.ts +42 -0
  12. package/dist/rules/wk8103.ts +45 -0
  13. package/dist/rules/wk8104.ts +69 -0
  14. package/dist/rules/wk8105.ts +45 -0
  15. package/dist/rules/wk8201.ts +55 -0
  16. package/dist/rules/wk8202.ts +46 -0
  17. package/dist/rules/wk8203.ts +46 -0
  18. package/dist/rules/wk8204.ts +54 -0
  19. package/dist/rules/wk8205.ts +56 -0
  20. package/dist/rules/wk8207.ts +45 -0
  21. package/dist/rules/wk8208.ts +45 -0
  22. package/dist/rules/wk8209.ts +45 -0
  23. package/dist/rules/wk8301.ts +51 -0
  24. package/dist/rules/wk8302.ts +46 -0
  25. package/dist/rules/wk8303.ts +96 -0
  26. package/dist/skills/chant-k8s.md +433 -0
  27. package/dist/types/index.d.ts +2934 -0
  28. package/package.json +30 -0
  29. package/src/actions/actions.test.ts +83 -0
  30. package/src/actions/apps.ts +23 -0
  31. package/src/actions/batch.ts +9 -0
  32. package/src/actions/core.ts +62 -0
  33. package/src/actions/index.ts +50 -0
  34. package/src/actions/networking.ts +15 -0
  35. package/src/actions/rbac.ts +13 -0
  36. package/src/codegen/docs-cli.ts +3 -0
  37. package/src/codegen/docs.ts +1147 -0
  38. package/src/codegen/generate-cli.ts +41 -0
  39. package/src/codegen/generate-lexicon.ts +69 -0
  40. package/src/codegen/generate-typescript.ts +97 -0
  41. package/src/codegen/generate.ts +144 -0
  42. package/src/codegen/naming.test.ts +63 -0
  43. package/src/codegen/naming.ts +187 -0
  44. package/src/codegen/package.ts +56 -0
  45. package/src/codegen/patches.ts +108 -0
  46. package/src/codegen/snapshot.test.ts +95 -0
  47. package/src/codegen/typecheck.test.ts +24 -0
  48. package/src/codegen/typecheck.ts +4 -0
  49. package/src/codegen/versions.ts +43 -0
  50. package/src/composites/autoscaled-service.ts +236 -0
  51. package/src/composites/composites.test.ts +1109 -0
  52. package/src/composites/cron-workload.ts +167 -0
  53. package/src/composites/index.ts +14 -0
  54. package/src/composites/namespace-env.ts +163 -0
  55. package/src/composites/node-agent.ts +224 -0
  56. package/src/composites/stateful-app.ts +134 -0
  57. package/src/composites/web-app.ts +180 -0
  58. package/src/composites/worker-pool.ts +230 -0
  59. package/src/coverage.test.ts +27 -0
  60. package/src/coverage.ts +35 -0
  61. package/src/crd/loader.ts +112 -0
  62. package/src/crd/parser.test.ts +217 -0
  63. package/src/crd/parser.ts +279 -0
  64. package/src/crd/types.ts +54 -0
  65. package/src/default-labels.test.ts +111 -0
  66. package/src/default-labels.ts +122 -0
  67. package/src/generated/index.d.ts +2934 -0
  68. package/src/generated/index.ts +203 -0
  69. package/src/generated/lexicon-k8s.json +1413 -0
  70. package/src/generated/runtime.ts +4 -0
  71. package/src/import/generator.test.ts +121 -0
  72. package/src/import/generator.ts +285 -0
  73. package/src/import/parser.test.ts +156 -0
  74. package/src/import/parser.ts +204 -0
  75. package/src/import/roundtrip.test.ts +86 -0
  76. package/src/index.ts +38 -0
  77. package/src/lint/post-synth/k8s-helpers.test.ts +219 -0
  78. package/src/lint/post-synth/k8s-helpers.ts +149 -0
  79. package/src/lint/post-synth/post-synth.test.ts +969 -0
  80. package/src/lint/post-synth/wk8005.ts +59 -0
  81. package/src/lint/post-synth/wk8006.ts +68 -0
  82. package/src/lint/post-synth/wk8041.ts +73 -0
  83. package/src/lint/post-synth/wk8042.ts +48 -0
  84. package/src/lint/post-synth/wk8101.ts +65 -0
  85. package/src/lint/post-synth/wk8102.ts +42 -0
  86. package/src/lint/post-synth/wk8103.ts +45 -0
  87. package/src/lint/post-synth/wk8104.ts +69 -0
  88. package/src/lint/post-synth/wk8105.ts +45 -0
  89. package/src/lint/post-synth/wk8201.ts +55 -0
  90. package/src/lint/post-synth/wk8202.ts +46 -0
  91. package/src/lint/post-synth/wk8203.ts +46 -0
  92. package/src/lint/post-synth/wk8204.ts +54 -0
  93. package/src/lint/post-synth/wk8205.ts +56 -0
  94. package/src/lint/post-synth/wk8207.ts +45 -0
  95. package/src/lint/post-synth/wk8208.ts +45 -0
  96. package/src/lint/post-synth/wk8209.ts +45 -0
  97. package/src/lint/post-synth/wk8301.ts +51 -0
  98. package/src/lint/post-synth/wk8302.ts +46 -0
  99. package/src/lint/post-synth/wk8303.ts +96 -0
  100. package/src/lint/rules/hardcoded-namespace.ts +56 -0
  101. package/src/lint/rules/rules.test.ts +69 -0
  102. package/src/lsp/completions.test.ts +64 -0
  103. package/src/lsp/completions.ts +20 -0
  104. package/src/lsp/hover.test.ts +69 -0
  105. package/src/lsp/hover.ts +68 -0
  106. package/src/package-cli.ts +28 -0
  107. package/src/plugin.test.ts +209 -0
  108. package/src/plugin.ts +915 -0
  109. package/src/serializer.test.ts +275 -0
  110. package/src/serializer.ts +278 -0
  111. package/src/spec/fetch.test.ts +24 -0
  112. package/src/spec/fetch.ts +68 -0
  113. package/src/spec/parse.test.ts +102 -0
  114. package/src/spec/parse.ts +477 -0
  115. package/src/testdata/manifests/configmap.yaml +7 -0
  116. package/src/testdata/manifests/deployment.yaml +22 -0
  117. package/src/testdata/manifests/full-app.yaml +61 -0
  118. package/src/testdata/manifests/secret.yaml +7 -0
  119. package/src/testdata/manifests/service.yaml +15 -0
  120. package/src/validate-cli.ts +21 -0
  121. package/src/validate.test.ts +29 -0
  122. package/src/validate.ts +46 -0
  123. package/src/variables.ts +36 -0
@@ -0,0 +1,54 @@
1
+ /**
2
+ * WK8204: RunAsNonRoot Recommended
3
+ *
4
+ * Containers should set securityContext.runAsNonRoot to true at either
5
+ * the container level or pod level. Running as root inside a container
6
+ * increases the blast radius of a container breakout.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { getPrimaryOutput, parseK8sManifests, extractContainers, extractPodSpec, WORKLOAD_KINDS } from "./k8s-helpers";
11
+
12
+ export const wk8204: PostSynthCheck = {
13
+ id: "WK8204",
14
+ description: "RunAsNonRoot recommended — running as root increases container breakout risk",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [, output] of ctx.outputs) {
20
+ const yaml = getPrimaryOutput(output);
21
+ const manifests = parseK8sManifests(yaml);
22
+
23
+ for (const manifest of manifests) {
24
+ if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind)) continue;
25
+
26
+ const resourceName = manifest.metadata?.name ?? manifest.kind;
27
+ const podSpec = extractPodSpec(manifest);
28
+ if (!podSpec) continue;
29
+
30
+ // Check pod-level securityContext
31
+ const podSecCtx = podSpec.securityContext as Record<string, unknown> | undefined;
32
+ const podRunAsNonRoot = podSecCtx?.runAsNonRoot === true;
33
+
34
+ const containers = extractContainers(manifest);
35
+ for (const container of containers) {
36
+ const secCtx = container.securityContext;
37
+ const containerRunAsNonRoot = secCtx?.runAsNonRoot === true;
38
+
39
+ if (!podRunAsNonRoot && !containerRunAsNonRoot) {
40
+ diagnostics.push({
41
+ checkId: "WK8204",
42
+ severity: "warning",
43
+ message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" does not set runAsNonRoot: true — set it at container or pod level`,
44
+ entity: resourceName,
45
+ lexicon: "k8s",
46
+ });
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ return diagnostics;
53
+ },
54
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * WK8205: Drop ALL Capabilities
3
+ *
4
+ * Containers should drop all Linux capabilities and only add back those
5
+ * explicitly needed. This follows the principle of least privilege and
6
+ * reduces the attack surface.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { getPrimaryOutput, parseK8sManifests, extractContainers, WORKLOAD_KINDS } from "./k8s-helpers";
11
+
12
+ export const wk8205: PostSynthCheck = {
13
+ id: "WK8205",
14
+ description: "Drop ALL capabilities — containers should drop all capabilities and add only what is needed",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [, output] of ctx.outputs) {
20
+ const yaml = getPrimaryOutput(output);
21
+ const manifests = parseK8sManifests(yaml);
22
+
23
+ for (const manifest of manifests) {
24
+ if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind)) continue;
25
+
26
+ const containers = extractContainers(manifest);
27
+ const resourceName = manifest.metadata?.name ?? manifest.kind;
28
+
29
+ for (const container of containers) {
30
+ const secCtx = container.securityContext;
31
+ const capabilities = secCtx?.capabilities as Record<string, unknown> | undefined;
32
+ const drop = capabilities?.drop;
33
+
34
+ const dropsAll =
35
+ Array.isArray(drop) &&
36
+ drop.some(
37
+ (cap) =>
38
+ (typeof cap === "string" && cap.toUpperCase() === "ALL"),
39
+ );
40
+
41
+ if (!dropsAll) {
42
+ diagnostics.push({
43
+ checkId: "WK8205",
44
+ severity: "warning",
45
+ message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" does not drop ALL capabilities — add securityContext.capabilities.drop: ["ALL"]`,
46
+ entity: resourceName,
47
+ lexicon: "k8s",
48
+ });
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ return diagnostics;
55
+ },
56
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * WK8207: No hostNetwork
3
+ *
4
+ * Pods should not use hostNetwork: true. Using the host network namespace
5
+ * bypasses network isolation and allows the pod to access all network
6
+ * interfaces on the host.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { getPrimaryOutput, parseK8sManifests, extractPodSpec, WORKLOAD_KINDS } from "./k8s-helpers";
11
+
12
+ export const wk8207: PostSynthCheck = {
13
+ id: "WK8207",
14
+ description: "No hostNetwork — using host network bypasses network isolation",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [, output] of ctx.outputs) {
20
+ const yaml = getPrimaryOutput(output);
21
+ const manifests = parseK8sManifests(yaml);
22
+
23
+ for (const manifest of manifests) {
24
+ if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind)) continue;
25
+
26
+ const podSpec = extractPodSpec(manifest);
27
+ if (!podSpec) continue;
28
+
29
+ const resourceName = manifest.metadata?.name ?? manifest.kind;
30
+
31
+ if (podSpec.hostNetwork === true) {
32
+ diagnostics.push({
33
+ checkId: "WK8207",
34
+ severity: "warning",
35
+ message: `${manifest.kind} "${resourceName}" uses hostNetwork: true — this bypasses network isolation and should be avoided`,
36
+ entity: resourceName,
37
+ lexicon: "k8s",
38
+ });
39
+ }
40
+ }
41
+ }
42
+
43
+ return diagnostics;
44
+ },
45
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * WK8208: No hostPID
3
+ *
4
+ * Pods should not use hostPID: true. Sharing the host PID namespace
5
+ * allows the pod to see and interact with all processes on the host,
6
+ * which is a significant security risk.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { getPrimaryOutput, parseK8sManifests, extractPodSpec, WORKLOAD_KINDS } from "./k8s-helpers";
11
+
12
+ export const wk8208: PostSynthCheck = {
13
+ id: "WK8208",
14
+ description: "No hostPID — sharing host PID namespace allows visibility into all host processes",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [, output] of ctx.outputs) {
20
+ const yaml = getPrimaryOutput(output);
21
+ const manifests = parseK8sManifests(yaml);
22
+
23
+ for (const manifest of manifests) {
24
+ if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind)) continue;
25
+
26
+ const podSpec = extractPodSpec(manifest);
27
+ if (!podSpec) continue;
28
+
29
+ const resourceName = manifest.metadata?.name ?? manifest.kind;
30
+
31
+ if (podSpec.hostPID === true) {
32
+ diagnostics.push({
33
+ checkId: "WK8208",
34
+ severity: "warning",
35
+ message: `${manifest.kind} "${resourceName}" uses hostPID: true — this allows visibility into all host processes and should be avoided`,
36
+ entity: resourceName,
37
+ lexicon: "k8s",
38
+ });
39
+ }
40
+ }
41
+ }
42
+
43
+ return diagnostics;
44
+ },
45
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * WK8209: No hostIPC
3
+ *
4
+ * Pods should not use hostIPC: true. Sharing the host IPC namespace
5
+ * allows the pod to access shared memory segments on the host, which
6
+ * can be used to escalate privileges or exfiltrate data.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { getPrimaryOutput, parseK8sManifests, extractPodSpec, WORKLOAD_KINDS } from "./k8s-helpers";
11
+
12
+ export const wk8209: PostSynthCheck = {
13
+ id: "WK8209",
14
+ description: "No hostIPC — sharing host IPC namespace can expose shared memory segments",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [, output] of ctx.outputs) {
20
+ const yaml = getPrimaryOutput(output);
21
+ const manifests = parseK8sManifests(yaml);
22
+
23
+ for (const manifest of manifests) {
24
+ if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind)) continue;
25
+
26
+ const podSpec = extractPodSpec(manifest);
27
+ if (!podSpec) continue;
28
+
29
+ const resourceName = manifest.metadata?.name ?? manifest.kind;
30
+
31
+ if (podSpec.hostIPC === true) {
32
+ diagnostics.push({
33
+ checkId: "WK8209",
34
+ severity: "warning",
35
+ message: `${manifest.kind} "${resourceName}" uses hostIPC: true — this exposes host shared memory segments and should be avoided`,
36
+ entity: resourceName,
37
+ lexicon: "k8s",
38
+ });
39
+ }
40
+ }
41
+ }
42
+
43
+ return diagnostics;
44
+ },
45
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * WK8301: Probes Required
3
+ *
4
+ * Containers should have both livenessProbe and readinessProbe configured.
5
+ * Without probes, Kubernetes cannot detect unhealthy containers or know
6
+ * when a container is ready to receive traffic.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { getPrimaryOutput, parseK8sManifests, extractContainers, WORKLOAD_KINDS } from "./k8s-helpers";
11
+
12
+ export const wk8301: PostSynthCheck = {
13
+ id: "WK8301",
14
+ description: "Probes required — containers should have livenessProbe and readinessProbe",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [, output] of ctx.outputs) {
20
+ const yaml = getPrimaryOutput(output);
21
+ const manifests = parseK8sManifests(yaml);
22
+
23
+ for (const manifest of manifests) {
24
+ if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind)) continue;
25
+ // CronJobs and Jobs are short-lived — probes are less relevant
26
+ if (manifest.kind === "Job" || manifest.kind === "CronJob") continue;
27
+
28
+ const containers = extractContainers(manifest);
29
+ const resourceName = manifest.metadata?.name ?? manifest.kind;
30
+
31
+ for (const container of containers) {
32
+ const missing: string[] = [];
33
+ if (!container.livenessProbe) missing.push("livenessProbe");
34
+ if (!container.readinessProbe) missing.push("readinessProbe");
35
+
36
+ if (missing.length > 0) {
37
+ diagnostics.push({
38
+ checkId: "WK8301",
39
+ severity: "warning",
40
+ message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" is missing ${missing.join(" and ")}`,
41
+ entity: resourceName,
42
+ lexicon: "k8s",
43
+ });
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ return diagnostics;
50
+ },
51
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * WK8302: Replicas >= 2 for High Availability
3
+ *
4
+ * Deployments should have at least 2 replicas for high availability.
5
+ * A single replica means any pod disruption causes downtime.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { getPrimaryOutput, parseK8sManifests } from "./k8s-helpers";
10
+
11
+ export const wk8302: PostSynthCheck = {
12
+ id: "WK8302",
13
+ description: "Replicas >= 2 recommended — single-replica Deployments have no high availability",
14
+
15
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
16
+ const diagnostics: PostSynthDiagnostic[] = [];
17
+
18
+ for (const [, output] of ctx.outputs) {
19
+ const yaml = getPrimaryOutput(output);
20
+ const manifests = parseK8sManifests(yaml);
21
+
22
+ for (const manifest of manifests) {
23
+ if (manifest.kind !== "Deployment") continue;
24
+
25
+ const resourceName = manifest.metadata?.name ?? "Deployment";
26
+ const spec = manifest.spec;
27
+ if (!spec) continue;
28
+
29
+ const replicas = spec.replicas;
30
+
31
+ // If replicas is omitted, Kubernetes defaults to 1
32
+ if (replicas === undefined || replicas === null || replicas === 1) {
33
+ diagnostics.push({
34
+ checkId: "WK8302",
35
+ severity: "info",
36
+ message: `Deployment "${resourceName}" has ${replicas ?? 1} replica(s) — consider at least 2 for high availability`,
37
+ entity: resourceName,
38
+ lexicon: "k8s",
39
+ });
40
+ }
41
+ }
42
+ }
43
+
44
+ return diagnostics;
45
+ },
46
+ };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * WK8303: PodDisruptionBudget Recommended for HA Deployments
3
+ *
4
+ * Deployments with 2 or more replicas should have a corresponding
5
+ * PodDisruptionBudget (PDB) to ensure minimum availability during
6
+ * voluntary disruptions like node drains and cluster upgrades.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { getPrimaryOutput, parseK8sManifests } from "./k8s-helpers";
11
+ import type { K8sManifest } from "./k8s-helpers";
12
+
13
+ export const wk8303: PostSynthCheck = {
14
+ id: "WK8303",
15
+ description: "PDB recommended for HA Deployments — ensures availability during voluntary disruptions",
16
+
17
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
18
+ const diagnostics: PostSynthDiagnostic[] = [];
19
+
20
+ for (const [, output] of ctx.outputs) {
21
+ const yaml = getPrimaryOutput(output);
22
+ const manifests = parseK8sManifests(yaml);
23
+
24
+ // Collect all PDB selectors to match against Deployments
25
+ const pdbSelectors = collectPdbSelectors(manifests);
26
+
27
+ for (const manifest of manifests) {
28
+ if (manifest.kind !== "Deployment") continue;
29
+
30
+ const spec = manifest.spec;
31
+ if (!spec) continue;
32
+
33
+ const replicas = spec.replicas;
34
+ // Only check HA deployments (replicas >= 2)
35
+ if (typeof replicas !== "number" || replicas < 2) continue;
36
+
37
+ const resourceName = manifest.metadata?.name ?? "Deployment";
38
+
39
+ // Check if any PDB targets this Deployment's labels
40
+ const selector = spec.selector as Record<string, unknown> | undefined;
41
+ const matchLabels = selector?.matchLabels as Record<string, string> | undefined;
42
+
43
+ if (!matchLabels || !hasCoveringPdb(matchLabels, pdbSelectors)) {
44
+ diagnostics.push({
45
+ checkId: "WK8303",
46
+ severity: "info",
47
+ message: `Deployment "${resourceName}" has ${replicas} replicas but no PodDisruptionBudget — add a PDB to ensure availability during voluntary disruptions`,
48
+ entity: resourceName,
49
+ lexicon: "k8s",
50
+ });
51
+ }
52
+ }
53
+ }
54
+
55
+ return diagnostics;
56
+ },
57
+ };
58
+
59
+ /**
60
+ * Collect matchLabels from all PodDisruptionBudget resources.
61
+ */
62
+ function collectPdbSelectors(
63
+ manifests: K8sManifest[],
64
+ ): Array<Record<string, string>> {
65
+ const selectors: Array<Record<string, string>> = [];
66
+
67
+ for (const manifest of manifests) {
68
+ if (manifest.kind !== "PodDisruptionBudget") continue;
69
+
70
+ const spec = manifest.spec;
71
+ if (!spec) continue;
72
+
73
+ const selector = spec.selector as Record<string, unknown> | undefined;
74
+ const matchLabels = selector?.matchLabels as Record<string, string> | undefined;
75
+
76
+ if (matchLabels && typeof matchLabels === "object") {
77
+ selectors.push(matchLabels);
78
+ }
79
+ }
80
+
81
+ return selectors;
82
+ }
83
+
84
+ /**
85
+ * Check if any PDB selector covers (is a subset of) the given labels.
86
+ */
87
+ function hasCoveringPdb(
88
+ deploymentLabels: Record<string, string>,
89
+ pdbSelectors: Array<Record<string, string>>,
90
+ ): boolean {
91
+ return pdbSelectors.some((pdbLabels) =>
92
+ Object.entries(pdbLabels).every(
93
+ ([key, value]) => deploymentLabels[key] === value,
94
+ ),
95
+ );
96
+ }
@@ -0,0 +1,56 @@
1
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
2
+ import * as ts from "typescript";
3
+
4
+ /**
5
+ * WK8001: Hardcoded Namespace
6
+ *
7
+ * Detects hardcoded namespace strings in Kubernetes resource constructors.
8
+ * Namespaces should be parameterized or derived from configuration, not
9
+ * hardcoded as string literals.
10
+ *
11
+ * Bad: new Deployment({ metadata: { namespace: "production" } })
12
+ * Good: new Deployment({ metadata: { namespace: config.namespace } })
13
+ */
14
+ export const hardcodedNamespaceRule: LintRule = {
15
+ id: "WK8001",
16
+ severity: "warning",
17
+ category: "correctness",
18
+ description:
19
+ "Detects hardcoded namespace strings — namespaces should be parameterized",
20
+
21
+ check(context: LintContext): LintDiagnostic[] {
22
+ const { sourceFile } = context;
23
+ const diagnostics: LintDiagnostic[] = [];
24
+
25
+ function visit(node: ts.Node): void {
26
+ // Look for property assignments like `namespace: "production"`
27
+ if (
28
+ ts.isPropertyAssignment(node) &&
29
+ ts.isIdentifier(node.name) &&
30
+ node.name.text === "namespace" &&
31
+ ts.isStringLiteral(node.initializer)
32
+ ) {
33
+ const value = node.initializer.text;
34
+ // Skip empty strings — those are likely intentional placeholders
35
+ if (value !== "") {
36
+ const { line, character } =
37
+ sourceFile.getLineAndCharacterOfPosition(
38
+ node.initializer.getStart(),
39
+ );
40
+ diagnostics.push({
41
+ file: sourceFile.fileName,
42
+ line: line + 1,
43
+ column: character + 1,
44
+ ruleId: "WK8001",
45
+ severity: "warning",
46
+ message: `Hardcoded namespace "${value}" detected. Use a variable or configuration parameter instead.`,
47
+ });
48
+ }
49
+ }
50
+ ts.forEachChild(node, visit);
51
+ }
52
+
53
+ visit(sourceFile);
54
+ return diagnostics;
55
+ },
56
+ };
@@ -0,0 +1,69 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { hardcodedNamespaceRule } from "./hardcoded-namespace";
3
+ import * as ts from "typescript";
4
+
5
+ function createContext(code: string) {
6
+ const sourceFile = ts.createSourceFile(
7
+ "test.ts",
8
+ code,
9
+ ts.ScriptTarget.Latest,
10
+ true,
11
+ ts.ScriptKind.TS,
12
+ );
13
+ return { sourceFile } as any;
14
+ }
15
+
16
+ describe("WK8001: Hardcoded Namespace", () => {
17
+ test("rule metadata", () => {
18
+ expect(hardcodedNamespaceRule.id).toBe("WK8001");
19
+ expect(hardcodedNamespaceRule.severity).toBe("warning");
20
+ expect(hardcodedNamespaceRule.category).toBe("correctness");
21
+ });
22
+
23
+ test("flags namespace: 'production' string literal", () => {
24
+ const ctx = createContext(
25
+ `new Deployment({ metadata: { namespace: "production" } });`,
26
+ );
27
+ const diags = hardcodedNamespaceRule.check(ctx);
28
+ expect(diags.length).toBe(1);
29
+ expect(diags[0].ruleId).toBe("WK8001");
30
+ expect(diags[0].message).toContain("production");
31
+ });
32
+
33
+ test("flags namespace: 'default' string literal", () => {
34
+ const ctx = createContext(
35
+ `new Deployment({ metadata: { namespace: "default" } });`,
36
+ );
37
+ const diags = hardcodedNamespaceRule.check(ctx);
38
+ expect(diags.length).toBe(1);
39
+ expect(diags[0].message).toContain("default");
40
+ });
41
+
42
+ test("does NOT flag namespace: myVar (variable reference)", () => {
43
+ const ctx = createContext(
44
+ `const ns = "production"; new Deployment({ metadata: { namespace: ns } });`,
45
+ );
46
+ const diags = hardcodedNamespaceRule.check(ctx);
47
+ // Only the string literal assignment to `namespace` key counts
48
+ // Variable references should not be flagged
49
+ const nsFlags = diags.filter((d) => d.ruleId === "WK8001");
50
+ expect(nsFlags.length).toBe(0);
51
+ });
52
+
53
+ test("does NOT flag empty namespace string", () => {
54
+ const ctx = createContext(
55
+ `new Deployment({ metadata: { namespace: "" } });`,
56
+ );
57
+ const diags = hardcodedNamespaceRule.check(ctx);
58
+ expect(diags.length).toBe(0);
59
+ });
60
+
61
+ test("flags multiple hardcoded namespaces", () => {
62
+ const ctx = createContext(`
63
+ const a = new Deployment({ metadata: { namespace: "staging" } });
64
+ const b = new Service({ metadata: { namespace: "prod" } });
65
+ `);
66
+ const diags = hardcodedNamespaceRule.check(ctx);
67
+ expect(diags.length).toBe(2);
68
+ });
69
+ });
@@ -0,0 +1,64 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { existsSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
7
+ const hasGenerated = existsSync(
8
+ join(pkgDir, "src", "generated", "lexicon-k8s.json"),
9
+ );
10
+
11
+ describe("LSP completions", () => {
12
+ test.skipIf(!hasGenerated)(
13
+ "returns completions for 'new D' prefix",
14
+ async () => {
15
+ const { k8sCompletions } = await import("./completions");
16
+ const result = k8sCompletions({
17
+ uri: "file:///test.ts",
18
+ content: "const x = new D",
19
+ linePrefix: "const x = new D",
20
+ wordAtCursor: "D",
21
+ position: { line: 0, character: 15 },
22
+ } as any);
23
+
24
+ expect(result).toBeDefined();
25
+ expect(Array.isArray(result)).toBe(true);
26
+ const labels = result.map((c: any) => c.label ?? c);
27
+ expect(labels.some((l: string) => l.includes("Deployment"))).toBe(true);
28
+ },
29
+ );
30
+
31
+ test.skipIf(!hasGenerated)(
32
+ "returns empty for non-constructor context",
33
+ async () => {
34
+ const { k8sCompletions } = await import("./completions");
35
+ const result = k8sCompletions({
36
+ uri: "file:///test.ts",
37
+ content: "const x = 42",
38
+ linePrefix: "const x = 42",
39
+ wordAtCursor: "",
40
+ position: { line: 0, character: 12 },
41
+ } as any);
42
+
43
+ expect(Array.isArray(result)).toBe(true);
44
+ expect(result.length).toBe(0);
45
+ },
46
+ );
47
+
48
+ test.skipIf(!hasGenerated)("filters by prefix correctly", async () => {
49
+ const { k8sCompletions } = await import("./completions");
50
+ const result = k8sCompletions({
51
+ uri: "file:///test.ts",
52
+ content: "const x = new StatefulS",
53
+ linePrefix: "const x = new StatefulS",
54
+ wordAtCursor: "StatefulS",
55
+ position: { line: 0, character: 23 },
56
+ } as any);
57
+
58
+ expect(Array.isArray(result)).toBe(true);
59
+ if (result.length > 0) {
60
+ const labels = result.map((c: any) => c.label ?? c);
61
+ expect(labels.some((l: string) => l.includes("StatefulSet"))).toBe(true);
62
+ }
63
+ });
64
+ });
@@ -0,0 +1,20 @@
1
+ import { createRequire } from "module";
2
+ import type { CompletionContext, CompletionItem } from "@intentius/chant/lsp/types";
3
+ import { LexiconIndex, lexiconCompletions, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
4
+ const require = createRequire(import.meta.url);
5
+
6
+ let cachedIndex: LexiconIndex | null = null;
7
+
8
+ function getIndex(): LexiconIndex {
9
+ if (cachedIndex) return cachedIndex;
10
+ const data = require("../generated/lexicon-k8s.json") as Record<string, LexiconEntry>;
11
+ cachedIndex = new LexiconIndex(data);
12
+ return cachedIndex;
13
+ }
14
+
15
+ /**
16
+ * Provide Kubernetes resource completions based on context.
17
+ */
18
+ export function k8sCompletions(ctx: CompletionContext): CompletionItem[] {
19
+ return lexiconCompletions(ctx, getIndex(), "Kubernetes resource");
20
+ }