@intentius/chant-lexicon-k8s 0.0.15 → 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 +4 -3
- 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/package.json +1 -1
- package/src/composites/agic-ingress.ts +0 -1
- package/src/composites/aks-external-dns-agent.ts +199 -0
- package/src/composites/azure-monitor-collector.ts +2 -2
- package/src/composites/composites.test.ts +359 -0
- package/src/composites/gce-ingress.ts +143 -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-otel-collector.ts +229 -0
- package/src/composites/index.ts +12 -0
- package/src/index.ts +14 -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.ts +125 -208
- 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,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.ts
CHANGED
|
@@ -18,7 +18,9 @@ export const k8sPlugin: LexiconPlugin = {
|
|
|
18
18
|
|
|
19
19
|
lintRules(): LintRule[] {
|
|
20
20
|
const { hardcodedNamespaceRule } = require("./lint/rules/hardcoded-namespace");
|
|
21
|
-
|
|
21
|
+
const { latestImageTagRule } = require("./lint/rules/latest-image-tag");
|
|
22
|
+
const { missingResourceLimitsRule } = require("./lint/rules/missing-resource-limits");
|
|
23
|
+
return [hardcodedNamespaceRule, latestImageTagRule, missingResourceLimitsRule];
|
|
22
24
|
},
|
|
23
25
|
|
|
24
26
|
postSynthChecks(): PostSynthCheck[] {
|
|
@@ -715,213 +717,6 @@ const { job, serviceAccount, role, roleBinding } = BatchJob({
|
|
|
715
717
|
command: ["python", "manage.py", "migrate"],
|
|
716
718
|
backoffLimit: 3,
|
|
717
719
|
ttlSecondsAfterFinished: 3600,
|
|
718
|
-
});`,
|
|
719
|
-
},
|
|
720
|
-
],
|
|
721
|
-
},
|
|
722
|
-
{
|
|
723
|
-
name: "chant-k8s-eks",
|
|
724
|
-
description: "EKS-specific Kubernetes composites — IRSA, ALB, EBS/EFS, Fluent Bit, ExternalDNS, ADOT",
|
|
725
|
-
content: `---
|
|
726
|
-
skill: chant-k8s-eks
|
|
727
|
-
description: EKS-specific Kubernetes patterns and composites
|
|
728
|
-
user-invocable: true
|
|
729
|
-
---
|
|
730
|
-
|
|
731
|
-
# EKS Kubernetes Patterns
|
|
732
|
-
|
|
733
|
-
## EKS Composites Overview
|
|
734
|
-
|
|
735
|
-
These composites produce K8s YAML with EKS-specific annotations and configurations.
|
|
736
|
-
|
|
737
|
-
### IrsaServiceAccount — ServiceAccount with IAM Role annotation
|
|
738
|
-
|
|
739
|
-
\`\`\`typescript
|
|
740
|
-
import { IrsaServiceAccount } from "@intentius/chant-lexicon-k8s";
|
|
741
|
-
|
|
742
|
-
const { serviceAccount, role, roleBinding } = IrsaServiceAccount({
|
|
743
|
-
name: "app-sa",
|
|
744
|
-
iamRoleArn: "arn:aws:iam::123456789012:role/my-app-role",
|
|
745
|
-
rbacRules: [
|
|
746
|
-
{ apiGroups: [""], resources: ["secrets"], verbs: ["get"] },
|
|
747
|
-
],
|
|
748
|
-
namespace: "prod",
|
|
749
|
-
});
|
|
750
|
-
\`\`\`
|
|
751
|
-
|
|
752
|
-
### AlbIngress — Ingress with AWS ALB Controller annotations
|
|
753
|
-
|
|
754
|
-
\`\`\`typescript
|
|
755
|
-
import { AlbIngress } from "@intentius/chant-lexicon-k8s";
|
|
756
|
-
|
|
757
|
-
const { ingress } = AlbIngress({
|
|
758
|
-
name: "api-ingress",
|
|
759
|
-
hosts: [
|
|
760
|
-
{
|
|
761
|
-
hostname: "api.example.com",
|
|
762
|
-
paths: [{ path: "/", serviceName: "api", servicePort: 80 }],
|
|
763
|
-
},
|
|
764
|
-
],
|
|
765
|
-
scheme: "internet-facing",
|
|
766
|
-
certificateArn: "arn:aws:acm:us-east-1:123456789012:certificate/abc-123",
|
|
767
|
-
groupName: "shared-alb",
|
|
768
|
-
healthCheckPath: "/healthz",
|
|
769
|
-
});
|
|
770
|
-
\`\`\`
|
|
771
|
-
|
|
772
|
-
Features:
|
|
773
|
-
- Auto-sets \`alb.ingress.kubernetes.io/*\` annotations
|
|
774
|
-
- SSL redirect enabled by default when \`certificateArn\` set
|
|
775
|
-
- \`groupName\` for shared ALB across multiple Ingresses
|
|
776
|
-
- \`wafAclArn\` for WAFv2 integration
|
|
777
|
-
|
|
778
|
-
### EbsStorageClass — StorageClass for EBS CSI
|
|
779
|
-
|
|
780
|
-
\`\`\`typescript
|
|
781
|
-
import { EbsStorageClass } from "@intentius/chant-lexicon-k8s";
|
|
782
|
-
|
|
783
|
-
const { storageClass } = EbsStorageClass({
|
|
784
|
-
name: "gp3-encrypted",
|
|
785
|
-
type: "gp3",
|
|
786
|
-
encrypted: true,
|
|
787
|
-
iops: "3000",
|
|
788
|
-
throughput: "125",
|
|
789
|
-
});
|
|
790
|
-
\`\`\`
|
|
791
|
-
|
|
792
|
-
### EfsStorageClass — StorageClass for EFS CSI (ReadWriteMany)
|
|
793
|
-
|
|
794
|
-
\`\`\`typescript
|
|
795
|
-
import { EfsStorageClass } from "@intentius/chant-lexicon-k8s";
|
|
796
|
-
|
|
797
|
-
const { storageClass } = EfsStorageClass({
|
|
798
|
-
name: "efs-shared",
|
|
799
|
-
fileSystemId: "fs-12345678",
|
|
800
|
-
});
|
|
801
|
-
\`\`\`
|
|
802
|
-
|
|
803
|
-
Use EFS when you need ReadWriteMany (shared across pods/nodes). Use EBS for ReadWriteOnce (single pod).
|
|
804
|
-
|
|
805
|
-
### FluentBitAgent — DaemonSet for CloudWatch logging
|
|
806
|
-
|
|
807
|
-
\`\`\`typescript
|
|
808
|
-
import { FluentBitAgent } from "@intentius/chant-lexicon-k8s";
|
|
809
|
-
|
|
810
|
-
const result = FluentBitAgent({
|
|
811
|
-
logGroup: "/aws/eks/my-cluster/containers",
|
|
812
|
-
region: "us-east-1",
|
|
813
|
-
clusterName: "my-cluster",
|
|
814
|
-
});
|
|
815
|
-
\`\`\`
|
|
816
|
-
|
|
817
|
-
### ExternalDnsAgent — ExternalDNS for Route53
|
|
818
|
-
|
|
819
|
-
\`\`\`typescript
|
|
820
|
-
import { ExternalDnsAgent } from "@intentius/chant-lexicon-k8s";
|
|
821
|
-
|
|
822
|
-
const result = ExternalDnsAgent({
|
|
823
|
-
iamRoleArn: "arn:aws:iam::123456789012:role/external-dns-role",
|
|
824
|
-
domainFilters: ["example.com"],
|
|
825
|
-
txtOwnerId: "my-cluster",
|
|
826
|
-
});
|
|
827
|
-
\`\`\`
|
|
828
|
-
|
|
829
|
-
### AdotCollector — ADOT for CloudWatch/X-Ray
|
|
830
|
-
|
|
831
|
-
\`\`\`typescript
|
|
832
|
-
import { AdotCollector } from "@intentius/chant-lexicon-k8s";
|
|
833
|
-
|
|
834
|
-
const result = AdotCollector({
|
|
835
|
-
region: "us-east-1",
|
|
836
|
-
clusterName: "my-cluster",
|
|
837
|
-
exporters: ["cloudwatch", "xray"],
|
|
838
|
-
});
|
|
839
|
-
\`\`\`
|
|
840
|
-
|
|
841
|
-
## Pod Identity vs IRSA
|
|
842
|
-
|
|
843
|
-
| Feature | IRSA | Pod Identity |
|
|
844
|
-
|---------|------|-------------|
|
|
845
|
-
| K8s annotation needed | Yes (\`eks.amazonaws.com/role-arn\`) | No |
|
|
846
|
-
| Composite available | **IrsaServiceAccount** | None needed |
|
|
847
|
-
| Setup | OIDC provider + IAM role trust policy | EKS Pod Identity Agent add-on + association |
|
|
848
|
-
| When to use | Existing clusters, broad compatibility | New clusters (EKS 1.28+), simpler management |
|
|
849
|
-
|
|
850
|
-
For Pod Identity, no K8s-side composite is needed — configure the association via AWS API/CloudFormation and use a plain ServiceAccount.
|
|
851
|
-
|
|
852
|
-
## Karpenter
|
|
853
|
-
|
|
854
|
-
Karpenter replaces Cluster Autoscaler for node provisioning. Karpenter NodePool and EC2NodeClass are simple CRDs — use CRD import rather than composites:
|
|
855
|
-
|
|
856
|
-
\`\`\`bash
|
|
857
|
-
# Import Karpenter CRDs into your chant project
|
|
858
|
-
chant import --url https://raw.githubusercontent.com/aws/karpenter/main/pkg/apis/crds/karpenter.sh_nodepools.yaml
|
|
859
|
-
\`\`\`
|
|
860
|
-
|
|
861
|
-
## Fargate Considerations
|
|
862
|
-
|
|
863
|
-
When running on EKS Fargate:
|
|
864
|
-
- **No DaemonSets** — FluentBitAgent and AdotCollector cannot run on Fargate nodes
|
|
865
|
-
- **No hostPath volumes** — use EFS for shared storage
|
|
866
|
-
- **No privileged containers** — security context restrictions apply
|
|
867
|
-
- For Fargate logging, use the built-in Fluent Bit log router (Fargate logging configuration)
|
|
868
|
-
|
|
869
|
-
## EKS Add-ons
|
|
870
|
-
|
|
871
|
-
Common add-ons managed via AWS (not K8s manifests):
|
|
872
|
-
- **vpc-cni** — Amazon VPC CNI plugin
|
|
873
|
-
- **coredns** — Cluster DNS
|
|
874
|
-
- **kube-proxy** — Network proxy
|
|
875
|
-
- **aws-ebs-csi-driver** — EBS CSI driver (required for EbsStorageClass)
|
|
876
|
-
- **aws-efs-csi-driver** — EFS CSI driver (required for EfsStorageClass)
|
|
877
|
-
- **adot** — AWS Distro for OpenTelemetry (alternative to AdotCollector composite)
|
|
878
|
-
- **aws-guardduty-agent** — Runtime threat detection
|
|
879
|
-
|
|
880
|
-
Configure add-ons via the AWS lexicon (\`@intentius/chant-lexicon-aws\`) CloudFormation resources.
|
|
881
|
-
`,
|
|
882
|
-
triggers: [
|
|
883
|
-
{ type: "context", value: "eks" },
|
|
884
|
-
{ type: "context", value: "irsa" },
|
|
885
|
-
{ type: "context", value: "alb" },
|
|
886
|
-
{ type: "context", value: "ebs" },
|
|
887
|
-
{ type: "context", value: "efs" },
|
|
888
|
-
{ type: "context", value: "fluent-bit" },
|
|
889
|
-
{ type: "context", value: "cloudwatch" },
|
|
890
|
-
{ type: "context", value: "karpenter" },
|
|
891
|
-
{ type: "context", value: "fargate" },
|
|
892
|
-
],
|
|
893
|
-
preConditions: [
|
|
894
|
-
"chant CLI is installed (chant --version succeeds)",
|
|
895
|
-
"EKS cluster is provisioned",
|
|
896
|
-
"kubectl is configured for the EKS cluster",
|
|
897
|
-
],
|
|
898
|
-
postConditions: [
|
|
899
|
-
"EKS-specific resources are deployed and functional",
|
|
900
|
-
],
|
|
901
|
-
parameters: [],
|
|
902
|
-
examples: [
|
|
903
|
-
{
|
|
904
|
-
title: "IRSA ServiceAccount",
|
|
905
|
-
description: "Create a ServiceAccount with IAM role for S3 access",
|
|
906
|
-
input: "Create an IRSA ServiceAccount for my app that needs S3 access",
|
|
907
|
-
output: `import { IrsaServiceAccount } from "@intentius/chant-lexicon-k8s";
|
|
908
|
-
|
|
909
|
-
const { serviceAccount } = IrsaServiceAccount({
|
|
910
|
-
name: "app-sa",
|
|
911
|
-
iamRoleArn: "arn:aws:iam::123456789012:role/app-s3-role",
|
|
912
|
-
namespace: "prod",
|
|
913
|
-
});`,
|
|
914
|
-
},
|
|
915
|
-
{
|
|
916
|
-
title: "ALB Ingress with TLS",
|
|
917
|
-
description: "Create an ALB Ingress with ACM certificate",
|
|
918
|
-
input: "Set up an internet-facing ALB with TLS for my API",
|
|
919
|
-
output: `import { AlbIngress } from "@intentius/chant-lexicon-k8s";
|
|
920
|
-
|
|
921
|
-
const { ingress } = AlbIngress({
|
|
922
|
-
name: "api-ingress",
|
|
923
|
-
hosts: [{ hostname: "api.example.com", paths: [{ path: "/", serviceName: "api", servicePort: 80 }] }],
|
|
924
|
-
certificateArn: "arn:aws:acm:us-east-1:123456789012:certificate/abc-123",
|
|
925
720
|
});`,
|
|
926
721
|
},
|
|
927
722
|
],
|
|
@@ -1225,5 +1020,127 @@ const { deployment, service, serviceMonitor, prometheusRule } = MonitoredService
|
|
|
1225
1020
|
],
|
|
1226
1021
|
},
|
|
1227
1022
|
];
|
|
1023
|
+
|
|
1024
|
+
// Load file-based skills from src/skills/
|
|
1025
|
+
const { readFileSync } = require("fs");
|
|
1026
|
+
const { join, dirname } = require("path");
|
|
1027
|
+
const { fileURLToPath } = require("url");
|
|
1028
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
1029
|
+
|
|
1030
|
+
const skillFiles = [
|
|
1031
|
+
{
|
|
1032
|
+
file: "kubernetes-patterns.md",
|
|
1033
|
+
name: "kubernetes-patterns",
|
|
1034
|
+
description: "Kubernetes deployment strategies, stateful workloads, RBAC, and networking patterns",
|
|
1035
|
+
triggers: [
|
|
1036
|
+
{ type: "context" as const, value: "deployment strategy" },
|
|
1037
|
+
{ type: "context" as const, value: "statefulset" },
|
|
1038
|
+
{ type: "context" as const, value: "rbac" },
|
|
1039
|
+
{ type: "context" as const, value: "network policy" },
|
|
1040
|
+
{ type: "context" as const, value: "rolling update" },
|
|
1041
|
+
{ type: "context" as const, value: "blue green" },
|
|
1042
|
+
],
|
|
1043
|
+
parameters: [],
|
|
1044
|
+
examples: [
|
|
1045
|
+
{
|
|
1046
|
+
title: "Blue/Green Deployment",
|
|
1047
|
+
input: "Set up a blue/green deployment for my app",
|
|
1048
|
+
output: "import { WebApp } from \"@intentius/chant-lexicon-k8s\";\n\nconst blue = WebApp({ name: \"app-blue\", image: \"app:1.0\" });\nconst green = WebApp({ name: \"app-green\", image: \"app:2.0\" });",
|
|
1049
|
+
},
|
|
1050
|
+
],
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
file: "kubernetes-security.md",
|
|
1054
|
+
name: "kubernetes-security",
|
|
1055
|
+
description: "Kubernetes pod security, image scanning, network policies, and secrets management",
|
|
1056
|
+
triggers: [
|
|
1057
|
+
{ type: "context" as const, value: "k8s security" },
|
|
1058
|
+
{ type: "context" as const, value: "pod security" },
|
|
1059
|
+
{ type: "context" as const, value: "image security" },
|
|
1060
|
+
{ type: "context" as const, value: "k8s secrets" },
|
|
1061
|
+
{ type: "context" as const, value: "security context" },
|
|
1062
|
+
],
|
|
1063
|
+
parameters: [],
|
|
1064
|
+
examples: [
|
|
1065
|
+
{
|
|
1066
|
+
title: "Hardened Container",
|
|
1067
|
+
input: "Create a hardened container with security context",
|
|
1068
|
+
output: "WebApp({ name: \"api\", image: \"api:1.0\", securityContext: { runAsNonRoot: true, readOnlyRootFilesystem: true, capabilities: { drop: [\"ALL\"] } } })",
|
|
1069
|
+
},
|
|
1070
|
+
],
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
file: "chant-k8s-eks.md",
|
|
1074
|
+
name: "chant-k8s-eks",
|
|
1075
|
+
description: "EKS-specific Kubernetes composites — IRSA, ALB, EBS/EFS, Fluent Bit, ExternalDNS, ADOT",
|
|
1076
|
+
triggers: [
|
|
1077
|
+
{ type: "context" as const, value: "eks composites" },
|
|
1078
|
+
{ type: "context" as const, value: "irsa" },
|
|
1079
|
+
{ type: "context" as const, value: "alb ingress" },
|
|
1080
|
+
{ type: "context" as const, value: "ebs storage" },
|
|
1081
|
+
{ type: "context" as const, value: "karpenter" },
|
|
1082
|
+
],
|
|
1083
|
+
parameters: [],
|
|
1084
|
+
examples: [
|
|
1085
|
+
{
|
|
1086
|
+
title: "IRSA ServiceAccount",
|
|
1087
|
+
input: "Create an IRSA ServiceAccount for my app",
|
|
1088
|
+
output: "import { IrsaServiceAccount } from \"@intentius/chant-lexicon-k8s\";\n\nconst { serviceAccount } = IrsaServiceAccount({ name: \"app-sa\", iamRoleArn: \"arn:aws:iam::123456789012:role/app-role\" });",
|
|
1089
|
+
},
|
|
1090
|
+
],
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
file: "chant-k8s-gke.md",
|
|
1094
|
+
name: "chant-k8s-gke",
|
|
1095
|
+
description: "GKE-specific Kubernetes composites — Workload Identity, GCE PD, Filestore, FluentBit, OTel, ExternalDNS, Gateway",
|
|
1096
|
+
triggers: [
|
|
1097
|
+
{ type: "context" as const, value: "gke composites" },
|
|
1098
|
+
{ type: "context" as const, value: "workload identity gke" },
|
|
1099
|
+
{ type: "context" as const, value: "config connector k8s" },
|
|
1100
|
+
],
|
|
1101
|
+
parameters: [],
|
|
1102
|
+
examples: [
|
|
1103
|
+
{
|
|
1104
|
+
title: "GKE Workload Identity",
|
|
1105
|
+
input: "Create a ServiceAccount with GKE Workload Identity",
|
|
1106
|
+
output: "import { WorkloadIdentityServiceAccount } from \"@intentius/chant-lexicon-k8s\";\n\nconst { serviceAccount } = WorkloadIdentityServiceAccount({ name: \"app-sa\", gcpServiceAccountEmail: \"app@project.iam.gserviceaccount.com\" });",
|
|
1107
|
+
},
|
|
1108
|
+
],
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
file: "chant-k8s-aks.md",
|
|
1112
|
+
name: "chant-k8s-aks",
|
|
1113
|
+
description: "AKS-specific Kubernetes composites — Workload Identity, AGIC, Azure Disk/File, ExternalDNS, Azure Monitor",
|
|
1114
|
+
triggers: [
|
|
1115
|
+
{ type: "context" as const, value: "aks composites" },
|
|
1116
|
+
{ type: "context" as const, value: "workload identity aks" },
|
|
1117
|
+
{ type: "context" as const, value: "agic ingress" },
|
|
1118
|
+
],
|
|
1119
|
+
parameters: [],
|
|
1120
|
+
examples: [
|
|
1121
|
+
{
|
|
1122
|
+
title: "AKS Workload Identity",
|
|
1123
|
+
input: "Create a ServiceAccount with AKS Workload Identity",
|
|
1124
|
+
output: "import { AksWorkloadIdentityServiceAccount } from \"@intentius/chant-lexicon-k8s\";\n\nconst { serviceAccount } = AksWorkloadIdentityServiceAccount({ name: \"app-sa\", clientId: \"12345678-abcd-1234-abcd-123456789012\" });",
|
|
1125
|
+
},
|
|
1126
|
+
],
|
|
1127
|
+
},
|
|
1128
|
+
];
|
|
1129
|
+
|
|
1130
|
+
for (const skill of skillFiles) {
|
|
1131
|
+
try {
|
|
1132
|
+
const content = readFileSync(join(dir, "skills", skill.file), "utf-8");
|
|
1133
|
+
skills.push({
|
|
1134
|
+
name: skill.name,
|
|
1135
|
+
description: skill.description,
|
|
1136
|
+
content,
|
|
1137
|
+
triggers: skill.triggers,
|
|
1138
|
+
parameters: skill.parameters,
|
|
1139
|
+
examples: skill.examples,
|
|
1140
|
+
});
|
|
1141
|
+
} catch { /* skip missing skills */ }
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
return skills;
|
|
1228
1145
|
},
|
|
1229
1146
|
};
|