@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.
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "algorithm": "xxhash64",
3
3
  "artifacts": {
4
- "manifest.json": "490e9ca9417db298",
4
+ "manifest.json": "12a8a5033319b618",
5
5
  "meta.json": "1ce194f36f9b5f90",
6
6
  "types/index.d.ts": "beec4cc869064186",
7
+ "rules/missing-resource-limits.ts": "a6f776d2ff477948",
8
+ "rules/latest-image-tag.ts": "eb48e8d61e4ca84e",
7
9
  "rules/hardcoded-namespace.ts": "54b216c71018e101",
8
10
  "rules/wk8201.ts": "4dbbd20e21b5fa04",
9
11
  "rules/wk8204.ts": "9244d6fdd6d2f7d",
@@ -30,8 +32,7 @@
30
32
  "rules/k8s-helpers.ts": "53a6d3bfbedb2852",
31
33
  "rules/wk8207.ts": "6f2bc621d530afa2",
32
34
  "skills/chant-k8s.md": "c7db82c3ba37c78",
33
- "skills/chant-k8s-eks.md": "f79f31f058c7f2ed",
34
35
  "skills/chant-k8s-patterns.md": "c5151ed799145c4b"
35
36
  },
36
- "composite": "2a6c7f09f87d9a38"
37
+ "composite": "592308cdbd70d2ee"
37
38
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k8s",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "K8s",
6
6
  "intrinsics": [],
@@ -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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-k8s",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "files": [
@@ -140,7 +140,6 @@ export function AgicIngress(props: AgicIngressProps): AgicIngressResult {
140
140
  annotations,
141
141
  },
142
142
  spec: {
143
- ingressClassName: "azure/application-gateway",
144
143
  rules: ingressRules,
145
144
  },
146
145
  };
@@ -0,0 +1,199 @@
1
+ /**
2
+ * AksExternalDnsAgent composite — Deployment + ServiceAccount + ClusterRole + ClusterRoleBinding.
3
+ *
4
+ * @aks Like ExternalDnsAgent but uses --provider=azure and AKS Workload Identity
5
+ * instead of IRSA for Azure DNS management.
6
+ */
7
+
8
+ export interface AksExternalDnsAgentProps {
9
+ /** Azure managed identity client ID for Workload Identity. */
10
+ clientId: string;
11
+ /** Azure resource group containing the DNS zone. */
12
+ resourceGroup: string;
13
+ /** Azure subscription ID. */
14
+ subscriptionId: string;
15
+ /** Azure tenant ID. */
16
+ tenantId: string;
17
+ /** Domain filters — only manage DNS records for these domains. */
18
+ domainFilters: string[];
19
+ /** TXT record owner ID for identifying managed records. */
20
+ txtOwnerId?: string;
21
+ /** Source of DNS records (default: "ingress"). */
22
+ source?: string;
23
+ /** Agent name (default: "external-dns"). */
24
+ name?: string;
25
+ /** Container image (default: "registry.k8s.io/external-dns/external-dns:v0.14.0"). */
26
+ image?: string;
27
+ /** Namespace (default: "kube-system"). */
28
+ namespace?: string;
29
+ /** Additional labels. */
30
+ labels?: Record<string, string>;
31
+ }
32
+
33
+ export interface AksExternalDnsAgentResult {
34
+ deployment: Record<string, unknown>;
35
+ serviceAccount: Record<string, unknown>;
36
+ clusterRole: Record<string, unknown>;
37
+ clusterRoleBinding: Record<string, unknown>;
38
+ }
39
+
40
+ /**
41
+ * Create an AksExternalDnsAgent composite — returns prop objects for
42
+ * a Deployment, ServiceAccount (with AKS Workload Identity), ClusterRole, and ClusterRoleBinding.
43
+ *
44
+ * @aks
45
+ * @example
46
+ * ```ts
47
+ * import { AksExternalDnsAgent } from "@intentius/chant-lexicon-k8s";
48
+ *
49
+ * const { deployment, serviceAccount, clusterRole, clusterRoleBinding } = AksExternalDnsAgent({
50
+ * clientId: "00000000-0000-0000-0000-000000000000",
51
+ * resourceGroup: "my-rg",
52
+ * subscriptionId: "00000000-0000-0000-0000-000000000000",
53
+ * tenantId: "00000000-0000-0000-0000-000000000000",
54
+ * domainFilters: ["example.com"],
55
+ * txtOwnerId: "my-cluster",
56
+ * });
57
+ * ```
58
+ */
59
+ export function AksExternalDnsAgent(props: AksExternalDnsAgentProps): AksExternalDnsAgentResult {
60
+ const {
61
+ clientId,
62
+ resourceGroup,
63
+ subscriptionId,
64
+ tenantId,
65
+ domainFilters,
66
+ txtOwnerId,
67
+ source = "ingress",
68
+ name = "external-dns",
69
+ image = "registry.k8s.io/external-dns/external-dns:v0.14.0",
70
+ namespace = "kube-system",
71
+ labels: extraLabels = {},
72
+ } = props;
73
+
74
+ const saName = `${name}-sa`;
75
+ const clusterRoleName = `${name}-role`;
76
+ const bindingName = `${name}-binding`;
77
+
78
+ const commonLabels: Record<string, string> = {
79
+ "app.kubernetes.io/name": name,
80
+ "app.kubernetes.io/managed-by": "chant",
81
+ ...extraLabels,
82
+ };
83
+
84
+ const args: string[] = [
85
+ `--source=${source}`,
86
+ "--provider=azure",
87
+ `--azure-resource-group=${resourceGroup}`,
88
+ `--azure-subscription-id=${subscriptionId}`,
89
+ "--policy=upsert-only",
90
+ "--registry=txt",
91
+ ];
92
+
93
+ for (const domain of domainFilters) {
94
+ args.push(`--domain-filter=${domain}`);
95
+ }
96
+
97
+ if (txtOwnerId) {
98
+ args.push(`--txt-owner-id=${txtOwnerId}`);
99
+ }
100
+
101
+ const deploymentProps: Record<string, unknown> = {
102
+ metadata: {
103
+ name,
104
+ namespace,
105
+ labels: { ...commonLabels, "app.kubernetes.io/component": "dns" },
106
+ },
107
+ spec: {
108
+ replicas: 1,
109
+ selector: { matchLabels: { "app.kubernetes.io/name": name } },
110
+ template: {
111
+ metadata: {
112
+ labels: {
113
+ "app.kubernetes.io/name": name,
114
+ "azure.workload.identity/use": "true",
115
+ ...extraLabels,
116
+ },
117
+ },
118
+ spec: {
119
+ serviceAccountName: saName,
120
+ containers: [
121
+ {
122
+ name,
123
+ image,
124
+ args,
125
+ env: [
126
+ { name: "AZURE_TENANT_ID", value: tenantId },
127
+ { name: "AZURE_SUBSCRIPTION_ID", value: subscriptionId },
128
+ { name: "AZURE_RESOURCE_GROUP", value: resourceGroup },
129
+ ],
130
+ resources: {
131
+ requests: { cpu: "50m", memory: "64Mi" },
132
+ limits: { cpu: "100m", memory: "128Mi" },
133
+ },
134
+ securityContext: {
135
+ runAsNonRoot: true,
136
+ runAsUser: 65534,
137
+ readOnlyRootFilesystem: true,
138
+ allowPrivilegeEscalation: false,
139
+ },
140
+ },
141
+ ],
142
+ },
143
+ },
144
+ },
145
+ };
146
+
147
+ const serviceAccountProps: Record<string, unknown> = {
148
+ metadata: {
149
+ name: saName,
150
+ namespace,
151
+ labels: {
152
+ ...commonLabels,
153
+ "app.kubernetes.io/component": "dns",
154
+ "azure.workload.identity/use": "true",
155
+ },
156
+ annotations: {
157
+ "azure.workload.identity/client-id": clientId,
158
+ },
159
+ },
160
+ };
161
+
162
+ const clusterRoleProps: Record<string, unknown> = {
163
+ metadata: {
164
+ name: clusterRoleName,
165
+ labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
166
+ },
167
+ rules: [
168
+ { apiGroups: [""], resources: ["services", "endpoints", "pods"], verbs: ["get", "watch", "list"] },
169
+ { apiGroups: ["extensions", "networking.k8s.io"], resources: ["ingresses"], verbs: ["get", "watch", "list"] },
170
+ { apiGroups: [""], resources: ["nodes"], verbs: ["list", "watch"] },
171
+ ],
172
+ };
173
+
174
+ const clusterRoleBindingProps: Record<string, unknown> = {
175
+ metadata: {
176
+ name: bindingName,
177
+ labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
178
+ },
179
+ roleRef: {
180
+ apiGroup: "rbac.authorization.k8s.io",
181
+ kind: "ClusterRole",
182
+ name: clusterRoleName,
183
+ },
184
+ subjects: [
185
+ {
186
+ kind: "ServiceAccount",
187
+ name: saName,
188
+ namespace,
189
+ },
190
+ ],
191
+ };
192
+
193
+ return {
194
+ deployment: deploymentProps,
195
+ serviceAccount: serviceAccountProps,
196
+ clusterRole: clusterRoleProps,
197
+ clusterRoleBinding: clusterRoleBindingProps,
198
+ };
199
+ }
@@ -12,7 +12,7 @@ export interface AzureMonitorCollectorProps {
12
12
  clusterName: string;
13
13
  /** Agent name (default: "azure-monitor-collector"). */
14
14
  name?: string;
15
- /** Collector image (default: "mcr.microsoft.com/azuremonitor/containerinsights/ciprod:latest"). */
15
+ /** Collector image (default: "mcr.microsoft.com/azuremonitor/containerinsights/ciprod:3.1.35"). */
16
16
  image?: string;
17
17
  /** Namespace (default: "azure-monitor"). */
18
18
  namespace?: string;
@@ -58,7 +58,7 @@ export function AzureMonitorCollector(props: AzureMonitorCollectorProps): AzureM
58
58
  workspaceId,
59
59
  clusterName,
60
60
  name = "azure-monitor-collector",
61
- image = "mcr.microsoft.com/azuremonitor/containerinsights/ciprod:latest",
61
+ image = "mcr.microsoft.com/azuremonitor/containerinsights/ciprod:3.1.35",
62
62
  namespace = "azure-monitor",
63
63
  labels: extraLabels = {},
64
64
  cpuRequest = "100m",