@intentius/chant-lexicon-k8s 0.0.14 → 0.0.15

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 (48) hide show
  1. package/dist/integrity.json +6 -3
  2. package/dist/manifest.json +1 -1
  3. package/dist/rules/wk8204.ts +33 -1
  4. package/dist/rules/wk8304.ts +70 -0
  5. package/dist/rules/wk8305.ts +115 -0
  6. package/dist/rules/wk8306.ts +50 -0
  7. package/package.json +27 -24
  8. package/src/codegen/docs.ts +1 -1
  9. package/src/composites/adot-collector.ts +8 -2
  10. package/src/composites/agic-ingress.ts +149 -0
  11. package/src/composites/alb-ingress.ts +2 -1
  12. package/src/composites/autoscaled-service.ts +25 -7
  13. package/src/composites/azure-disk-storage-class.ts +82 -0
  14. package/src/composites/azure-file-storage-class.ts +77 -0
  15. package/src/composites/azure-monitor-collector.ts +232 -0
  16. package/src/composites/batch-job.ts +36 -3
  17. package/src/composites/composites.test.ts +701 -0
  18. package/src/composites/config-connector-context.ts +62 -0
  19. package/src/composites/configured-app.ts +6 -0
  20. package/src/composites/cron-workload.ts +6 -0
  21. package/src/composites/ebs-storage-class.ts +4 -4
  22. package/src/composites/external-dns-agent.ts +6 -0
  23. package/src/composites/filestore-storage-class.ts +79 -0
  24. package/src/composites/fluent-bit-agent.ts +5 -0
  25. package/src/composites/gce-pd-storage-class.ts +85 -0
  26. package/src/composites/gke-gateway.ts +143 -0
  27. package/src/composites/index.ts +19 -0
  28. package/src/composites/metrics-server.ts +1 -1
  29. package/src/composites/monitored-service.ts +6 -0
  30. package/src/composites/network-isolated-app.ts +6 -0
  31. package/src/composites/node-agent.ts +6 -0
  32. package/src/composites/security-context.ts +10 -0
  33. package/src/composites/sidecar-app.ts +6 -0
  34. package/src/composites/stateful-app.ts +4 -7
  35. package/src/composites/web-app.ts +4 -7
  36. package/src/composites/worker-pool.ts +4 -7
  37. package/src/composites/workload-identity-sa.ts +118 -0
  38. package/src/composites/workload-identity-service-account.ts +116 -0
  39. package/src/index.ts +6 -1
  40. package/src/lint/post-synth/post-synth.test.ts +362 -1
  41. package/src/lint/post-synth/wk8204.ts +33 -1
  42. package/src/lint/post-synth/wk8304.ts +70 -0
  43. package/src/lint/post-synth/wk8305.ts +115 -0
  44. package/src/lint/post-synth/wk8306.ts +50 -0
  45. package/src/plugin.test.ts +2 -2
  46. package/src/plugin.ts +4 -1
  47. package/src/serializer.test.ts +120 -0
  48. package/src/serializer.ts +16 -4
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "algorithm": "xxhash64",
3
3
  "artifacts": {
4
- "manifest.json": "59b066d7c2f8d709",
4
+ "manifest.json": "490e9ca9417db298",
5
5
  "meta.json": "1ce194f36f9b5f90",
6
6
  "types/index.d.ts": "beec4cc869064186",
7
7
  "rules/hardcoded-namespace.ts": "54b216c71018e101",
8
8
  "rules/wk8201.ts": "4dbbd20e21b5fa04",
9
- "rules/wk8204.ts": "31c3f8eac8455795",
9
+ "rules/wk8204.ts": "9244d6fdd6d2f7d",
10
10
  "rules/wk8301.ts": "283265ab0c5b8511",
11
+ "rules/wk8306.ts": "5338575bfb0f9251",
11
12
  "rules/wk8102.ts": "78d4aac387107b56",
13
+ "rules/wk8305.ts": "5c4b9482a0f3b91d",
12
14
  "rules/wk8101.ts": "f8ffcf6e5c89076b",
13
15
  "rules/wk8302.ts": "a80d1eab37c0dbe4",
14
16
  "rules/wk8005.ts": "a9a1b93b80f3aa51",
@@ -21,6 +23,7 @@
21
23
  "rules/wk8042.ts": "6064c84481ae8551",
22
24
  "rules/wk8006.ts": "6e04f754f79f076e",
23
25
  "rules/wk8041.ts": "4df512c93caaef50",
26
+ "rules/wk8304.ts": "f51bf894c5a08dbe",
24
27
  "rules/wk8202.ts": "6bd950ae2128256c",
25
28
  "rules/wk8208.ts": "1133f9e53c174ae9",
26
29
  "rules/wk8105.ts": "8dbcfe399f23656a",
@@ -30,5 +33,5 @@
30
33
  "skills/chant-k8s-eks.md": "f79f31f058c7f2ed",
31
34
  "skills/chant-k8s-patterns.md": "c5151ed799145c4b"
32
35
  },
33
- "composite": "586aea18d5400dd8"
36
+ "composite": "2a6c7f09f87d9a38"
34
37
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k8s",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "K8s",
6
6
  "intrinsics": [],
@@ -4,6 +4,10 @@
4
4
  * Containers should set securityContext.runAsNonRoot to true at either
5
5
  * the container level or pod level. Running as root inside a container
6
6
  * increases the blast radius of a container breakout.
7
+ *
8
+ * Additionally warns when runAsNonRoot: true is set but no explicit
9
+ * runAsUser is provided — without a numeric UID, K8s relies on the
10
+ * image's USER directive, which may be root (UID 0).
7
11
  */
8
12
 
9
13
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
@@ -30,13 +34,17 @@ export const wk8204: PostSynthCheck = {
30
34
  // Check pod-level securityContext
31
35
  const podSecCtx = podSpec.securityContext as Record<string, unknown> | undefined;
32
36
  const podRunAsNonRoot = podSecCtx?.runAsNonRoot === true;
37
+ const podRunAsUser = podSecCtx?.runAsUser;
33
38
 
34
39
  const containers = extractContainers(manifest);
35
40
  for (const container of containers) {
36
41
  const secCtx = container.securityContext;
37
42
  const containerRunAsNonRoot = secCtx?.runAsNonRoot === true;
43
+ const containerRunAsUser = secCtx?.runAsUser;
44
+
45
+ const hasRunAsNonRoot = podRunAsNonRoot || containerRunAsNonRoot;
38
46
 
39
- if (!podRunAsNonRoot && !containerRunAsNonRoot) {
47
+ if (!hasRunAsNonRoot) {
40
48
  diagnostics.push({
41
49
  checkId: "WK8204",
42
50
  severity: "warning",
@@ -44,6 +52,30 @@ export const wk8204: PostSynthCheck = {
44
52
  entity: resourceName,
45
53
  lexicon: "k8s",
46
54
  });
55
+ continue;
56
+ }
57
+
58
+ // runAsNonRoot is true — check for explicit runAsUser
59
+ const effectiveRunAsUser = containerRunAsUser ?? podRunAsUser;
60
+
61
+ if (effectiveRunAsUser === 0) {
62
+ // Contradictory: runAsNonRoot: true + runAsUser: 0
63
+ diagnostics.push({
64
+ checkId: "WK8204",
65
+ severity: "warning",
66
+ message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" has runAsNonRoot: true but runAsUser: 0 — these settings are contradictory and the container will fail to start`,
67
+ entity: resourceName,
68
+ lexicon: "k8s",
69
+ });
70
+ } else if (effectiveRunAsUser === undefined || effectiveRunAsUser === null) {
71
+ // runAsNonRoot: true but no explicit UID
72
+ diagnostics.push({
73
+ checkId: "WK8204",
74
+ severity: "warning",
75
+ message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" has runAsNonRoot: true but no explicit runAsUser — set a numeric UID to ensure the container doesn't run as root`,
76
+ entity: resourceName,
77
+ lexicon: "k8s",
78
+ });
47
79
  }
48
80
  }
49
81
  }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * WK8304: SSL Redirect Without Certificate
3
+ *
4
+ * Flags Ingress resources that have `alb.ingress.kubernetes.io/ssl-redirect`
5
+ * annotation set but are missing `alb.ingress.kubernetes.io/certificate-arn`
6
+ * or don't have HTTPS in their listen-ports.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { getPrimaryOutput, parseK8sManifests } from "./k8s-helpers";
11
+
12
+ export const wk8304: PostSynthCheck = {
13
+ id: "WK8304",
14
+ description: "SSL redirect without certificate — ssl-redirect annotation requires a valid certificate-arn and HTTPS listen-ports",
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 !== "Ingress") continue;
25
+
26
+ const annotations = manifest.metadata?.annotations as Record<string, string> | undefined;
27
+ if (!annotations) continue;
28
+
29
+ const sslRedirect = annotations["alb.ingress.kubernetes.io/ssl-redirect"];
30
+ if (!sslRedirect) continue;
31
+
32
+ const resourceName = manifest.metadata?.name ?? "Ingress";
33
+ const certArn = annotations["alb.ingress.kubernetes.io/certificate-arn"];
34
+
35
+ if (!certArn) {
36
+ diagnostics.push({
37
+ checkId: "WK8304",
38
+ severity: "warning",
39
+ message: `Ingress "${resourceName}" has ssl-redirect annotation but no certificate-arn — HTTPS redirect will fail without a TLS certificate`,
40
+ entity: resourceName,
41
+ lexicon: "k8s",
42
+ });
43
+ continue;
44
+ }
45
+
46
+ // Check listen-ports includes HTTPS
47
+ const listenPorts = annotations["alb.ingress.kubernetes.io/listen-ports"];
48
+ if (listenPorts) {
49
+ try {
50
+ const ports = JSON.parse(listenPorts) as Array<Record<string, number>>;
51
+ const hasHttps = ports.some((p) => "HTTPS" in p);
52
+ if (!hasHttps) {
53
+ diagnostics.push({
54
+ checkId: "WK8304",
55
+ severity: "warning",
56
+ message: `Ingress "${resourceName}" has ssl-redirect but listen-ports does not include HTTPS`,
57
+ entity: resourceName,
58
+ lexicon: "k8s",
59
+ });
60
+ }
61
+ } catch {
62
+ // Can't parse listen-ports — skip this check
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ return diagnostics;
69
+ },
70
+ };
@@ -0,0 +1,115 @@
1
+ /**
2
+ * WK8305: Ingress Port Not Matching Service
3
+ *
4
+ * Flags Ingress backends whose `service.port.number` does not match
5
+ * any declared port on the referenced Service in the manifest set.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { getPrimaryOutput, parseK8sManifests } from "./k8s-helpers";
10
+ import type { K8sManifest } from "./k8s-helpers";
11
+
12
+ export const wk8305: PostSynthCheck = {
13
+ id: "WK8305",
14
+ description: "Ingress port not matching Service — backend port must match a declared Service port",
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
+ // Build a map of Service name+namespace → set of port numbers
24
+ const servicePorts = collectServicePorts(manifests);
25
+
26
+ for (const manifest of manifests) {
27
+ if (manifest.kind !== "Ingress") continue;
28
+
29
+ const ingressName = manifest.metadata?.name ?? "Ingress";
30
+ const ingressNamespace = manifest.metadata?.namespace ?? "default";
31
+ const spec = manifest.spec;
32
+ if (!spec) continue;
33
+
34
+ const rules = spec.rules as Array<Record<string, unknown>> | undefined;
35
+ if (!rules) continue;
36
+
37
+ for (const rule of rules) {
38
+ const http = rule.http as Record<string, unknown> | undefined;
39
+ if (!http) continue;
40
+
41
+ const paths = http.paths as Array<Record<string, unknown>> | undefined;
42
+ if (!paths) continue;
43
+
44
+ for (const pathEntry of paths) {
45
+ const backend = pathEntry.backend as Record<string, unknown> | undefined;
46
+ if (!backend) continue;
47
+
48
+ const service = backend.service as Record<string, unknown> | undefined;
49
+ if (!service) continue;
50
+
51
+ const svcName = service.name as string | undefined;
52
+ const port = service.port as Record<string, unknown> | undefined;
53
+ const portNumber = port?.number as number | undefined;
54
+
55
+ if (!svcName || portNumber === undefined) continue;
56
+
57
+ // Look up the Service in the manifest set
58
+ const key = `${ingressNamespace}/${svcName}`;
59
+ const knownPorts = servicePorts.get(key);
60
+
61
+ // Skip if the Service is not in the manifest set (external service)
62
+ if (!knownPorts) continue;
63
+
64
+ if (!knownPorts.has(portNumber)) {
65
+ diagnostics.push({
66
+ checkId: "WK8305",
67
+ severity: "warning",
68
+ message: `Ingress "${ingressName}" references Service "${svcName}" port ${portNumber}, but the Service only declares ports [${[...knownPorts].join(", ")}]`,
69
+ entity: ingressName,
70
+ lexicon: "k8s",
71
+ });
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ return diagnostics;
79
+ },
80
+ };
81
+
82
+ /**
83
+ * Collect port numbers from all Service manifests, keyed by namespace/name.
84
+ */
85
+ function collectServicePorts(manifests: K8sManifest[]): Map<string, Set<number>> {
86
+ const result = new Map<string, Set<number>>();
87
+
88
+ for (const manifest of manifests) {
89
+ if (manifest.kind !== "Service") continue;
90
+
91
+ const name = manifest.metadata?.name;
92
+ const namespace = manifest.metadata?.namespace ?? "default";
93
+ if (!name) continue;
94
+
95
+ const key = `${namespace}/${name}`;
96
+ const ports = new Set<number>();
97
+
98
+ const spec = manifest.spec;
99
+ if (spec) {
100
+ const specPorts = spec.ports as Array<Record<string, unknown>> | undefined;
101
+ if (specPorts) {
102
+ for (const p of specPorts) {
103
+ const port = p.port as number | undefined;
104
+ if (port !== undefined) {
105
+ ports.add(port);
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ result.set(key, ports);
112
+ }
113
+
114
+ return result;
115
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * WK8306: Container Command Starts With Flag
3
+ *
4
+ * If `command[0]` starts with `-` or `--`, it's almost certainly a mistake —
5
+ * the first element should be the binary/entrypoint, flags belong in `args`.
6
+ * This causes OCI runtime errors because the container runtime tries to
7
+ * execute the flag as a binary.
8
+ */
9
+
10
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
11
+ import { getPrimaryOutput, parseK8sManifests, extractContainers, WORKLOAD_KINDS } from "./k8s-helpers";
12
+
13
+ export const wk8306: PostSynthCheck = {
14
+ id: "WK8306",
15
+ description: "Container command starts with flag — first element should be a binary, not a flag",
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
+ for (const manifest of manifests) {
25
+ if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind)) continue;
26
+
27
+ const resourceName = manifest.metadata?.name ?? manifest.kind;
28
+ const containers = extractContainers(manifest);
29
+
30
+ for (const container of containers) {
31
+ const command = (container as Record<string, unknown>).command as unknown[] | undefined;
32
+ if (!Array.isArray(command) || command.length === 0) continue;
33
+
34
+ const firstArg = String(command[0]);
35
+ if (firstArg.startsWith("-")) {
36
+ diagnostics.push({
37
+ checkId: "WK8306",
38
+ severity: "error",
39
+ message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" has command[0]="${firstArg}" which starts with a flag — the first element should be the binary, flags belong in args`,
40
+ entity: resourceName,
41
+ lexicon: "k8s",
42
+ });
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ return diagnostics;
49
+ },
50
+ };
package/package.json CHANGED
@@ -1,30 +1,33 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-k8s",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
- "files": ["src/", "dist/"],
6
+ "files": [
7
+ "src/",
8
+ "dist/"
9
+ ],
7
10
  "publishConfig": {
8
- "access": "public"
9
- },
10
- "exports": {
11
- ".": "./src/index.ts",
12
- "./*": "./src/*",
13
- "./manifest": "./dist/manifest.json",
14
- "./meta": "./dist/meta.json",
15
- "./types": "./dist/types/index.d.ts"
16
- },
17
- "scripts": {
18
- "generate": "bun run src/codegen/generate-cli.ts",
19
- "bundle": "bun run src/package-cli.ts",
20
- "validate": "bun run src/validate-cli.ts",
21
- "docs": "bun run src/codegen/docs-cli.ts",
22
- "prepack": "bun run generate && bun run bundle && bun run validate"
23
- },
24
- "dependencies": {
25
- "@intentius/chant": "0.0.13"
26
- },
27
- "devDependencies": {
28
- "typescript": "^5.9.3"
29
- }
11
+ "access": "public"
12
+ },
13
+ "exports": {
14
+ ".": "./src/index.ts",
15
+ "./*": "./src/*.ts",
16
+ "./manifest": "./dist/manifest.json",
17
+ "./meta": "./dist/meta.json",
18
+ "./types": "./dist/types/index.d.ts"
19
+ },
20
+ "scripts": {
21
+ "generate": "bun run src/codegen/generate-cli.ts",
22
+ "bundle": "bun run src/package-cli.ts",
23
+ "validate": "bun run src/validate-cli.ts",
24
+ "docs": "bun run src/codegen/docs-cli.ts",
25
+ "prepack": "bun run generate && bun run bundle && bun run validate"
26
+ },
27
+ "dependencies": {
28
+ "@intentius/chant": "0.0.15"
29
+ },
30
+ "devDependencies": {
31
+ "typescript": "^5.9.3"
32
+ }
30
33
  }
@@ -1102,7 +1102,7 @@ Skills are structured markdown documents bundled with a lexicon. When an AI agen
1102
1102
 
1103
1103
  ## Installation
1104
1104
 
1105
- When you scaffold a new project with \`chant init --lexicon k8s\`, the skill is installed to \`.claude/skills/chant-k8s/SKILL.md\` for automatic discovery by Claude Code.
1105
+ When you scaffold a new project with \`chant init --lexicon k8s\`, the skill is installed to \`skills/chant-k8s/SKILL.md\` for automatic discovery by Claude Code.
1106
1106
 
1107
1107
  ## Skill: chant-k8s
1108
1108
 
@@ -129,7 +129,7 @@ service:
129
129
  metrics:
130
130
  receivers: [otlp]
131
131
  processors: [batch]
132
- exporters: [${exporterNames.join(", ")}]
132
+ exporters: [${exporterNames.filter((e) => e !== "awsxray").join(", ") || "awsemf"}]
133
133
  traces:
134
134
  receivers: [otlp]
135
135
  processors: [batch]
@@ -139,7 +139,7 @@ service:
139
139
  const container: Record<string, unknown> = {
140
140
  name,
141
141
  image,
142
- command: ["--config=/etc/adot/config.yaml"],
142
+ args: ["--config=/etc/adot/config.yaml"],
143
143
  ports: [
144
144
  { containerPort: 4317, name: "otlp-grpc" },
145
145
  { containerPort: 4318, name: "otlp-http" },
@@ -151,6 +151,12 @@ service:
151
151
  volumeMounts: [
152
152
  { name: "config", mountPath: "/etc/adot", readOnly: true },
153
153
  ],
154
+ securityContext: {
155
+ runAsNonRoot: true,
156
+ runAsUser: 10001,
157
+ readOnlyRootFilesystem: true,
158
+ allowPrivilegeEscalation: false,
159
+ },
154
160
  };
155
161
 
156
162
  const daemonSetProps: Record<string, unknown> = {
@@ -0,0 +1,149 @@
1
+ /**
2
+ * AgicIngress composite — Ingress with Azure Application Gateway Ingress Controller annotations.
3
+ *
4
+ * @aks Full `appgw.ingress.kubernetes.io/*` annotation set including SSL redirect,
5
+ * WAF policy, backend path prefix, and cookie-based affinity.
6
+ */
7
+
8
+ export interface AgicIngressHost {
9
+ /** Hostname (e.g., "api.example.com"). */
10
+ hostname: string;
11
+ /** Path rules for this host. */
12
+ paths: Array<{
13
+ path: string;
14
+ pathType?: string;
15
+ serviceName: string;
16
+ servicePort: number;
17
+ }>;
18
+ }
19
+
20
+ export interface AgicIngressProps {
21
+ /** Ingress name — used in metadata and labels. */
22
+ name: string;
23
+ /** Host definitions with paths. */
24
+ hosts: AgicIngressHost[];
25
+ /** Azure Key Vault certificate URI or secret name for TLS. */
26
+ certificateArn?: string;
27
+ /** WAF policy resource ID. */
28
+ wafPolicyId?: string;
29
+ /** Health check path for backend. */
30
+ healthCheckPath?: string;
31
+ /** Enable HTTP->HTTPS redirect (default: true when certificateArn set). */
32
+ sslRedirect?: boolean;
33
+ /** Backend path prefix override. */
34
+ backendPathPrefix?: string;
35
+ /** Enable cookie-based affinity (default: false). */
36
+ cookieAffinity?: boolean;
37
+ /** Additional annotations on the Ingress. */
38
+ annotations?: Record<string, string>;
39
+ /** Additional labels to apply to all resources. */
40
+ labels?: Record<string, string>;
41
+ /** Namespace for all resources. */
42
+ namespace?: string;
43
+ }
44
+
45
+ export interface AgicIngressResult {
46
+ ingress: Record<string, unknown>;
47
+ }
48
+
49
+ /**
50
+ * Create an AgicIngress composite — returns prop objects for
51
+ * an Ingress with Azure Application Gateway Ingress Controller annotations.
52
+ *
53
+ * @aks
54
+ * @example
55
+ * ```ts
56
+ * import { AgicIngress } from "@intentius/chant-lexicon-k8s";
57
+ *
58
+ * const { ingress } = AgicIngress({
59
+ * name: "api-ingress",
60
+ * hosts: [
61
+ * {
62
+ * hostname: "api.example.com",
63
+ * paths: [{ path: "/", serviceName: "api", servicePort: 80 }],
64
+ * },
65
+ * ],
66
+ * certificateArn: "keyvault-secret-name",
67
+ * wafPolicyId: "/subscriptions/.../Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies/my-waf",
68
+ * });
69
+ * ```
70
+ */
71
+ export function AgicIngress(props: AgicIngressProps): AgicIngressResult {
72
+ const {
73
+ name,
74
+ hosts,
75
+ certificateArn,
76
+ wafPolicyId,
77
+ healthCheckPath,
78
+ sslRedirect,
79
+ backendPathPrefix,
80
+ cookieAffinity = false,
81
+ annotations: extraAnnotations = {},
82
+ labels: extraLabels = {},
83
+ namespace,
84
+ } = props;
85
+
86
+ const commonLabels: Record<string, string> = {
87
+ "app.kubernetes.io/name": name,
88
+ "app.kubernetes.io/managed-by": "chant",
89
+ ...extraLabels,
90
+ };
91
+
92
+ // Build AGIC annotations
93
+ const annotations: Record<string, string> = {
94
+ "kubernetes.io/ingress.class": "azure/application-gateway",
95
+ ...extraAnnotations,
96
+ };
97
+
98
+ if (sslRedirect ?? (certificateArn !== undefined)) {
99
+ annotations["appgw.ingress.kubernetes.io/ssl-redirect"] = "true";
100
+ }
101
+
102
+ if (certificateArn) {
103
+ annotations["appgw.ingress.kubernetes.io/appgw-ssl-certificate"] = certificateArn;
104
+ }
105
+
106
+ if (wafPolicyId) {
107
+ annotations["appgw.ingress.kubernetes.io/waf-policy-for-path"] = wafPolicyId;
108
+ }
109
+
110
+ if (healthCheckPath) {
111
+ annotations["appgw.ingress.kubernetes.io/health-probe-path"] = healthCheckPath;
112
+ }
113
+
114
+ if (backendPathPrefix) {
115
+ annotations["appgw.ingress.kubernetes.io/backend-path-prefix"] = backendPathPrefix;
116
+ }
117
+
118
+ if (cookieAffinity) {
119
+ annotations["appgw.ingress.kubernetes.io/cookie-based-affinity"] = "true";
120
+ }
121
+
122
+ const ingressRules = hosts.map((host) => ({
123
+ host: host.hostname,
124
+ http: {
125
+ paths: host.paths.map((p) => ({
126
+ path: p.path,
127
+ pathType: p.pathType ?? "Prefix",
128
+ backend: {
129
+ service: { name: p.serviceName, port: { number: p.servicePort } },
130
+ },
131
+ })),
132
+ },
133
+ }));
134
+
135
+ const ingressProps: Record<string, unknown> = {
136
+ metadata: {
137
+ name,
138
+ ...(namespace && { namespace }),
139
+ labels: { ...commonLabels, "app.kubernetes.io/component": "ingress" },
140
+ annotations,
141
+ },
142
+ spec: {
143
+ ingressClassName: "azure/application-gateway",
144
+ rules: ingressRules,
145
+ },
146
+ };
147
+
148
+ return { ingress: ingressProps };
149
+ }
@@ -13,6 +13,7 @@ export interface AlbIngressHost {
13
13
  path: string;
14
14
  pathType?: string;
15
15
  serviceName: string;
16
+ /** Port on the Kubernetes Service (not the container port). */
16
17
  servicePort: number;
17
18
  }>;
18
19
  }
@@ -105,7 +106,7 @@ export function AlbIngress(props: AlbIngressProps): AlbIngressResult {
105
106
  annotations["alb.ingress.kubernetes.io/listen-ports"] = '[{"HTTPS":443}]';
106
107
  }
107
108
 
108
- if (sslRedirect ?? (certificateArn !== undefined)) {
109
+ if (sslRedirect ?? !!certificateArn) {
109
110
  annotations["alb.ingress.kubernetes.io/ssl-redirect"] = "443";
110
111
  }
111
112