@intentius/chant-lexicon-k8s 0.0.14 → 0.0.16
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/integrity.json +8 -4
- package/dist/manifest.json +1 -1
- package/dist/rules/latest-image-tag.ts +121 -0
- package/dist/rules/missing-resource-limits.ts +111 -0
- package/dist/rules/wk8204.ts +33 -1
- package/dist/rules/wk8304.ts +70 -0
- package/dist/rules/wk8305.ts +115 -0
- package/dist/rules/wk8306.ts +50 -0
- package/package.json +27 -24
- package/src/codegen/docs.ts +1 -1
- package/src/composites/adot-collector.ts +8 -2
- package/src/composites/agic-ingress.ts +148 -0
- package/src/composites/aks-external-dns-agent.ts +199 -0
- package/src/composites/alb-ingress.ts +2 -1
- package/src/composites/autoscaled-service.ts +25 -7
- package/src/composites/azure-disk-storage-class.ts +82 -0
- package/src/composites/azure-file-storage-class.ts +77 -0
- package/src/composites/azure-monitor-collector.ts +232 -0
- package/src/composites/batch-job.ts +36 -3
- package/src/composites/composites.test.ts +1060 -0
- package/src/composites/config-connector-context.ts +62 -0
- package/src/composites/configured-app.ts +6 -0
- package/src/composites/cron-workload.ts +6 -0
- package/src/composites/ebs-storage-class.ts +4 -4
- package/src/composites/external-dns-agent.ts +6 -0
- package/src/composites/filestore-storage-class.ts +79 -0
- package/src/composites/fluent-bit-agent.ts +5 -0
- package/src/composites/gce-ingress.ts +143 -0
- package/src/composites/gce-pd-storage-class.ts +85 -0
- package/src/composites/gke-external-dns-agent.ts +175 -0
- package/src/composites/gke-fluent-bit-agent.ts +219 -0
- package/src/composites/gke-gateway.ts +143 -0
- package/src/composites/gke-otel-collector.ts +229 -0
- package/src/composites/index.ts +31 -0
- package/src/composites/metrics-server.ts +1 -1
- package/src/composites/monitored-service.ts +6 -0
- package/src/composites/network-isolated-app.ts +6 -0
- package/src/composites/node-agent.ts +6 -0
- package/src/composites/security-context.ts +10 -0
- package/src/composites/sidecar-app.ts +6 -0
- package/src/composites/stateful-app.ts +4 -7
- package/src/composites/web-app.ts +4 -7
- package/src/composites/worker-pool.ts +4 -7
- package/src/composites/workload-identity-sa.ts +118 -0
- package/src/composites/workload-identity-service-account.ts +116 -0
- package/src/index.ts +20 -1
- package/src/lint/post-synth/post-synth.test.ts +362 -1
- package/src/lint/post-synth/wk8204.ts +33 -1
- package/src/lint/post-synth/wk8304.ts +70 -0
- package/src/lint/post-synth/wk8305.ts +115 -0
- package/src/lint/post-synth/wk8306.ts +50 -0
- package/src/lint/rules/latest-image-tag.ts +121 -0
- package/src/lint/rules/missing-resource-limits.ts +111 -0
- package/src/lint/rules/rules.test.ts +192 -0
- package/src/plugin.test.ts +2 -2
- package/src/plugin.ts +129 -209
- package/src/serializer.test.ts +120 -0
- package/src/serializer.ts +16 -4
- package/src/skills/chant-k8s-aks.md +146 -0
- package/src/skills/chant-k8s-gke.md +191 -0
- package/src/skills/kubernetes-patterns.md +183 -0
- package/src/skills/kubernetes-security.md +237 -0
- /package/{dist → src}/skills/chant-k8s-eks.md +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* WK8002: Latest Image Tag
|
|
6
|
+
*
|
|
7
|
+
* Detects when a K8s workload resource uses the `:latest` image tag or no tag
|
|
8
|
+
* at all in a container image string literal. Untagged or `:latest` images are
|
|
9
|
+
* non-deterministic and can cause unexpected rollouts.
|
|
10
|
+
*
|
|
11
|
+
* Bad: new Deployment({ spec: { template: { spec: { containers: [{ image: "nginx:latest" }] } } } })
|
|
12
|
+
* Bad: new Deployment({ spec: { template: { spec: { containers: [{ image: "nginx" }] } } } })
|
|
13
|
+
* Good: new Deployment({ spec: { template: { spec: { containers: [{ image: "nginx:1.25" }] } } } })
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const WORKLOAD_KINDS = new Set([
|
|
17
|
+
"Deployment",
|
|
18
|
+
"StatefulSet",
|
|
19
|
+
"DaemonSet",
|
|
20
|
+
"CronJob",
|
|
21
|
+
"Job",
|
|
22
|
+
"ReplicaSet",
|
|
23
|
+
"Pod",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returns true if the string looks like a container image reference that is
|
|
28
|
+
* either untagged or using `:latest`.
|
|
29
|
+
*
|
|
30
|
+
* A string is considered a container image if it:
|
|
31
|
+
* - Contains at least one alphabetic character
|
|
32
|
+
* - Does not contain spaces
|
|
33
|
+
* - Is not a simple keyword like "true", "false", etc.
|
|
34
|
+
*/
|
|
35
|
+
function isProblematicImage(value: string): boolean {
|
|
36
|
+
if (!value || value.includes(" ") || value.length === 0) return false;
|
|
37
|
+
|
|
38
|
+
// Skip values that are clearly not images
|
|
39
|
+
const nonImagePatterns = [
|
|
40
|
+
/^(true|false|null|undefined|yes|no)$/i,
|
|
41
|
+
/^\d+$/, // pure numbers
|
|
42
|
+
/^[.\/]/, // relative/absolute paths without image-like structure
|
|
43
|
+
];
|
|
44
|
+
for (const pat of nonImagePatterns) {
|
|
45
|
+
if (pat.test(value)) return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check for :latest explicitly
|
|
49
|
+
if (value.endsWith(":latest")) return true;
|
|
50
|
+
|
|
51
|
+
// Check for untagged image: no colon at all, but looks like an image name
|
|
52
|
+
// Images contain alphanumeric chars and may have / for registry prefix
|
|
53
|
+
// Must have at least one alpha char and match image naming conventions
|
|
54
|
+
if (!value.includes(":") && !value.includes("@") && /^[a-zA-Z0-9._\-\/]+$/.test(value) && /[a-zA-Z]/.test(value)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const latestImageTagRule: LintRule = {
|
|
62
|
+
id: "WK8002",
|
|
63
|
+
severity: "warning",
|
|
64
|
+
category: "security",
|
|
65
|
+
description:
|
|
66
|
+
"Detects :latest or untagged container images — use explicit version tags for reproducibility",
|
|
67
|
+
|
|
68
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
69
|
+
const { sourceFile } = context;
|
|
70
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
71
|
+
|
|
72
|
+
function isInsideWorkloadConstructor(node: ts.Node): boolean {
|
|
73
|
+
let current: ts.Node | undefined = node.parent;
|
|
74
|
+
while (current) {
|
|
75
|
+
if (
|
|
76
|
+
ts.isNewExpression(current) &&
|
|
77
|
+
ts.isIdentifier(current.expression) &&
|
|
78
|
+
WORKLOAD_KINDS.has(current.expression.text)
|
|
79
|
+
) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
current = current.parent;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function visit(node: ts.Node): void {
|
|
88
|
+
// Look for property assignments like `image: "nginx:latest"` or `image: "nginx"`
|
|
89
|
+
if (
|
|
90
|
+
ts.isPropertyAssignment(node) &&
|
|
91
|
+
ts.isIdentifier(node.name) &&
|
|
92
|
+
node.name.text === "image" &&
|
|
93
|
+
ts.isStringLiteral(node.initializer) &&
|
|
94
|
+
isInsideWorkloadConstructor(node)
|
|
95
|
+
) {
|
|
96
|
+
const value = node.initializer.text;
|
|
97
|
+
if (isProblematicImage(value)) {
|
|
98
|
+
const { line, character } =
|
|
99
|
+
sourceFile.getLineAndCharacterOfPosition(
|
|
100
|
+
node.initializer.getStart(),
|
|
101
|
+
);
|
|
102
|
+
const isLatest = value.endsWith(":latest");
|
|
103
|
+
diagnostics.push({
|
|
104
|
+
file: sourceFile.fileName,
|
|
105
|
+
line: line + 1,
|
|
106
|
+
column: character + 1,
|
|
107
|
+
ruleId: "WK8002",
|
|
108
|
+
severity: "warning",
|
|
109
|
+
message: isLatest
|
|
110
|
+
? `Container image "${value}" uses the :latest tag. Pin to a specific version for reproducibility.`
|
|
111
|
+
: `Container image "${value}" has no tag. Pin to a specific version for reproducibility.`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
ts.forEachChild(node, visit);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
visit(sourceFile);
|
|
119
|
+
return diagnostics;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* WK8003: Missing Resource Limits
|
|
6
|
+
*
|
|
7
|
+
* Detects when a container in a Deployment/StatefulSet spec doesn't have
|
|
8
|
+
* resource limits or requests. Without resource limits, a container can
|
|
9
|
+
* consume unbounded cluster resources and cause noisy-neighbour issues.
|
|
10
|
+
*
|
|
11
|
+
* Bad: new Deployment({ spec: { template: { spec: { containers: [{ name: "app", image: "app:1.0" }] } } } })
|
|
12
|
+
* Good: new Deployment({ spec: { template: { spec: { containers: [{ name: "app", image: "app:1.0", resources: { limits: { cpu: "500m", memory: "256Mi" } } }] } } } })
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const WORKLOAD_KINDS = new Set([
|
|
16
|
+
"Deployment",
|
|
17
|
+
"StatefulSet",
|
|
18
|
+
"DaemonSet",
|
|
19
|
+
"CronJob",
|
|
20
|
+
"Job",
|
|
21
|
+
"ReplicaSet",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export const missingResourceLimitsRule: LintRule = {
|
|
25
|
+
id: "WK8003",
|
|
26
|
+
severity: "warning",
|
|
27
|
+
category: "correctness",
|
|
28
|
+
description:
|
|
29
|
+
"Detects containers without resource limits/requests — always set resource constraints",
|
|
30
|
+
|
|
31
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
32
|
+
const { sourceFile } = context;
|
|
33
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
34
|
+
|
|
35
|
+
function isInsideWorkloadConstructor(node: ts.Node): boolean {
|
|
36
|
+
let current: ts.Node | undefined = node.parent;
|
|
37
|
+
while (current) {
|
|
38
|
+
if (
|
|
39
|
+
ts.isNewExpression(current) &&
|
|
40
|
+
ts.isIdentifier(current.expression) &&
|
|
41
|
+
WORKLOAD_KINDS.has(current.expression.text)
|
|
42
|
+
) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
current = current.parent;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function objectLiteralHasProperty(
|
|
51
|
+
obj: ts.ObjectLiteralExpression,
|
|
52
|
+
name: string,
|
|
53
|
+
): boolean {
|
|
54
|
+
return obj.properties.some(
|
|
55
|
+
(p) =>
|
|
56
|
+
ts.isPropertyAssignment(p) &&
|
|
57
|
+
ts.isIdentifier(p.name) &&
|
|
58
|
+
p.name.text === name,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function visit(node: ts.Node): void {
|
|
63
|
+
// Look for object literals inside arrays that represent container specs.
|
|
64
|
+
// A container object literal typically has "name" and "image" properties.
|
|
65
|
+
// We flag it if it lacks a "resources" property.
|
|
66
|
+
if (
|
|
67
|
+
ts.isObjectLiteralExpression(node) &&
|
|
68
|
+
isInsideWorkloadConstructor(node)
|
|
69
|
+
) {
|
|
70
|
+
const hasName = objectLiteralHasProperty(node, "name");
|
|
71
|
+
const hasImage = objectLiteralHasProperty(node, "image");
|
|
72
|
+
const hasResources = objectLiteralHasProperty(node, "resources");
|
|
73
|
+
|
|
74
|
+
if (hasName && hasImage && !hasResources) {
|
|
75
|
+
// Confirm we're inside an array literal (containers array)
|
|
76
|
+
if (node.parent && ts.isArrayLiteralExpression(node.parent)) {
|
|
77
|
+
const { line, character } =
|
|
78
|
+
sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
79
|
+
|
|
80
|
+
// Try to extract the container name for a better message
|
|
81
|
+
let containerName = "unknown";
|
|
82
|
+
for (const prop of node.properties) {
|
|
83
|
+
if (
|
|
84
|
+
ts.isPropertyAssignment(prop) &&
|
|
85
|
+
ts.isIdentifier(prop.name) &&
|
|
86
|
+
prop.name.text === "name" &&
|
|
87
|
+
ts.isStringLiteral(prop.initializer)
|
|
88
|
+
) {
|
|
89
|
+
containerName = prop.initializer.text;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
diagnostics.push({
|
|
95
|
+
file: sourceFile.fileName,
|
|
96
|
+
line: line + 1,
|
|
97
|
+
column: character + 1,
|
|
98
|
+
ruleId: "WK8003",
|
|
99
|
+
severity: "warning",
|
|
100
|
+
message: `Container "${containerName}" is missing resource limits/requests. Set resources.limits and resources.requests to prevent unbounded resource consumption.`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
ts.forEachChild(node, visit);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
visit(sourceFile);
|
|
109
|
+
return diagnostics;
|
|
110
|
+
},
|
|
111
|
+
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
2
|
import { hardcodedNamespaceRule } from "./hardcoded-namespace";
|
|
3
|
+
import { latestImageTagRule } from "./latest-image-tag";
|
|
4
|
+
import { missingResourceLimitsRule } from "./missing-resource-limits";
|
|
3
5
|
import * as ts from "typescript";
|
|
4
6
|
|
|
5
7
|
function createContext(code: string) {
|
|
@@ -67,3 +69,193 @@ describe("WK8001: Hardcoded Namespace", () => {
|
|
|
67
69
|
expect(diags.length).toBe(2);
|
|
68
70
|
});
|
|
69
71
|
});
|
|
72
|
+
|
|
73
|
+
// ── WK8002: Latest Image Tag ────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
describe("WK8002: Latest Image Tag", () => {
|
|
76
|
+
test("rule metadata", () => {
|
|
77
|
+
expect(latestImageTagRule.id).toBe("WK8002");
|
|
78
|
+
expect(latestImageTagRule.severity).toBe("warning");
|
|
79
|
+
expect(latestImageTagRule.category).toBe("security");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("flags image: 'nginx:latest'", () => {
|
|
83
|
+
const ctx = createContext(
|
|
84
|
+
`new Deployment({ spec: { template: { spec: { containers: [{ name: "app", image: "nginx:latest" }] } } } });`,
|
|
85
|
+
);
|
|
86
|
+
const diags = latestImageTagRule.check(ctx);
|
|
87
|
+
expect(diags.length).toBe(1);
|
|
88
|
+
expect(diags[0].ruleId).toBe("WK8002");
|
|
89
|
+
expect(diags[0].message).toContain(":latest");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("flags untagged image: 'nginx'", () => {
|
|
93
|
+
const ctx = createContext(
|
|
94
|
+
`new Deployment({ spec: { template: { spec: { containers: [{ name: "app", image: "nginx" }] } } } });`,
|
|
95
|
+
);
|
|
96
|
+
const diags = latestImageTagRule.check(ctx);
|
|
97
|
+
expect(diags.length).toBe(1);
|
|
98
|
+
expect(diags[0].ruleId).toBe("WK8002");
|
|
99
|
+
expect(diags[0].message).toContain("no tag");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("flags registry-prefixed untagged image: 'ghcr.io/org/app'", () => {
|
|
103
|
+
const ctx = createContext(
|
|
104
|
+
`new Deployment({ spec: { template: { spec: { containers: [{ name: "app", image: "ghcr.io/org/app" }] } } } });`,
|
|
105
|
+
);
|
|
106
|
+
const diags = latestImageTagRule.check(ctx);
|
|
107
|
+
expect(diags.length).toBe(1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("does NOT flag explicitly tagged image: 'nginx:1.25'", () => {
|
|
111
|
+
const ctx = createContext(
|
|
112
|
+
`new Deployment({ spec: { template: { spec: { containers: [{ name: "app", image: "nginx:1.25" }] } } } });`,
|
|
113
|
+
);
|
|
114
|
+
const diags = latestImageTagRule.check(ctx);
|
|
115
|
+
expect(diags.length).toBe(0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("does NOT flag image with digest", () => {
|
|
119
|
+
const ctx = createContext(
|
|
120
|
+
`new Deployment({ spec: { template: { spec: { containers: [{ name: "app", image: "nginx@sha256:abc123" }] } } } });`,
|
|
121
|
+
);
|
|
122
|
+
const diags = latestImageTagRule.check(ctx);
|
|
123
|
+
expect(diags.length).toBe(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("flags :latest in StatefulSet", () => {
|
|
127
|
+
const ctx = createContext(
|
|
128
|
+
`new StatefulSet({ spec: { template: { spec: { containers: [{ name: "db", image: "postgres:latest" }] } } } });`,
|
|
129
|
+
);
|
|
130
|
+
const diags = latestImageTagRule.check(ctx);
|
|
131
|
+
expect(diags.length).toBe(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("flags :latest in DaemonSet", () => {
|
|
135
|
+
const ctx = createContext(
|
|
136
|
+
`new DaemonSet({ spec: { template: { spec: { containers: [{ name: "agent", image: "datadog:latest" }] } } } });`,
|
|
137
|
+
);
|
|
138
|
+
const diags = latestImageTagRule.check(ctx);
|
|
139
|
+
expect(diags.length).toBe(1);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("flags :latest in CronJob", () => {
|
|
143
|
+
const ctx = createContext(
|
|
144
|
+
`new CronJob({ spec: { jobTemplate: { spec: { template: { spec: { containers: [{ name: "job", image: "worker:latest" }] } } } } } });`,
|
|
145
|
+
);
|
|
146
|
+
const diags = latestImageTagRule.check(ctx);
|
|
147
|
+
expect(diags.length).toBe(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("does NOT flag image property outside workload constructor", () => {
|
|
151
|
+
const ctx = createContext(
|
|
152
|
+
`const config = { image: "nginx:latest" };`,
|
|
153
|
+
);
|
|
154
|
+
const diags = latestImageTagRule.check(ctx);
|
|
155
|
+
expect(diags.length).toBe(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("flags multiple containers with bad images", () => {
|
|
159
|
+
const ctx = createContext(`
|
|
160
|
+
new Deployment({
|
|
161
|
+
spec: {
|
|
162
|
+
template: {
|
|
163
|
+
spec: {
|
|
164
|
+
containers: [
|
|
165
|
+
{ name: "app", image: "nginx:latest" },
|
|
166
|
+
{ name: "sidecar", image: "envoy" },
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
`);
|
|
173
|
+
const diags = latestImageTagRule.check(ctx);
|
|
174
|
+
expect(diags.length).toBe(2);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ── WK8003: Missing Resource Limits ─────────────────────────────────
|
|
179
|
+
|
|
180
|
+
describe("WK8003: Missing Resource Limits", () => {
|
|
181
|
+
test("rule metadata", () => {
|
|
182
|
+
expect(missingResourceLimitsRule.id).toBe("WK8003");
|
|
183
|
+
expect(missingResourceLimitsRule.severity).toBe("warning");
|
|
184
|
+
expect(missingResourceLimitsRule.category).toBe("correctness");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("flags container without resources property", () => {
|
|
188
|
+
const ctx = createContext(
|
|
189
|
+
`new Deployment({ spec: { template: { spec: { containers: [{ name: "app", image: "app:1.0" }] } } } });`,
|
|
190
|
+
);
|
|
191
|
+
const diags = missingResourceLimitsRule.check(ctx);
|
|
192
|
+
expect(diags.length).toBe(1);
|
|
193
|
+
expect(diags[0].ruleId).toBe("WK8003");
|
|
194
|
+
expect(diags[0].message).toContain("app");
|
|
195
|
+
expect(diags[0].message).toContain("resource limits");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("does NOT flag container with resources property", () => {
|
|
199
|
+
const ctx = createContext(
|
|
200
|
+
`new Deployment({ spec: { template: { spec: { containers: [{ name: "app", image: "app:1.0", resources: { limits: { cpu: "500m", memory: "256Mi" } } }] } } } });`,
|
|
201
|
+
);
|
|
202
|
+
const diags = missingResourceLimitsRule.check(ctx);
|
|
203
|
+
expect(diags.length).toBe(0);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("flags container in StatefulSet", () => {
|
|
207
|
+
const ctx = createContext(
|
|
208
|
+
`new StatefulSet({ spec: { template: { spec: { containers: [{ name: "db", image: "postgres:15" }] } } } });`,
|
|
209
|
+
);
|
|
210
|
+
const diags = missingResourceLimitsRule.check(ctx);
|
|
211
|
+
expect(diags.length).toBe(1);
|
|
212
|
+
expect(diags[0].message).toContain("db");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("flags only containers without resources in mixed array", () => {
|
|
216
|
+
const ctx = createContext(`
|
|
217
|
+
new Deployment({
|
|
218
|
+
spec: {
|
|
219
|
+
template: {
|
|
220
|
+
spec: {
|
|
221
|
+
containers: [
|
|
222
|
+
{ name: "app", image: "app:1.0", resources: { limits: { cpu: "1" } } },
|
|
223
|
+
{ name: "sidecar", image: "envoy:1.0" },
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
`);
|
|
230
|
+
const diags = missingResourceLimitsRule.check(ctx);
|
|
231
|
+
expect(diags.length).toBe(1);
|
|
232
|
+
expect(diags[0].message).toContain("sidecar");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("does NOT flag outside workload constructor", () => {
|
|
236
|
+
const ctx = createContext(
|
|
237
|
+
`const containers = [{ name: "app", image: "app:1.0" }];`,
|
|
238
|
+
);
|
|
239
|
+
const diags = missingResourceLimitsRule.check(ctx);
|
|
240
|
+
expect(diags.length).toBe(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("flags multiple containers without resources", () => {
|
|
244
|
+
const ctx = createContext(`
|
|
245
|
+
new Deployment({
|
|
246
|
+
spec: {
|
|
247
|
+
template: {
|
|
248
|
+
spec: {
|
|
249
|
+
containers: [
|
|
250
|
+
{ name: "web", image: "nginx:1.25" },
|
|
251
|
+
{ name: "api", image: "api:2.0" },
|
|
252
|
+
],
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
`);
|
|
258
|
+
const diags = missingResourceLimitsRule.check(ctx);
|
|
259
|
+
expect(diags.length).toBe(2);
|
|
260
|
+
});
|
|
261
|
+
});
|
package/src/plugin.test.ts
CHANGED
|
@@ -30,10 +30,10 @@ describe("k8sPlugin", () => {
|
|
|
30
30
|
expect(rules.some((r) => r.id === "WK8001")).toBe(true);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
test("postSynthChecks() returns array of
|
|
33
|
+
test("postSynthChecks() returns array of 22 checks", () => {
|
|
34
34
|
const checks = k8sPlugin.postSynthChecks!();
|
|
35
35
|
expect(Array.isArray(checks)).toBe(true);
|
|
36
|
-
expect(checks.length).toBe(
|
|
36
|
+
expect(checks.length).toBe(23);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
test("intrinsics() returns empty array", () => {
|