@intentius/chant-lexicon-k8s 0.95.1 → 0.96.0

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 (61) hide show
  1. package/dist/composites/catalog.d.ts.map +1 -1
  2. package/dist/composites/index.d.ts +4 -0
  3. package/dist/composites/index.d.ts.map +1 -1
  4. package/dist/composites/monitored-service.d.ts +19 -0
  5. package/dist/composites/monitored-service.d.ts.map +1 -1
  6. package/dist/composites/otel-collector-agent.d.ts +22 -0
  7. package/dist/composites/otel-collector-agent.d.ts.map +1 -1
  8. package/dist/composites/otel-collector-gateway.d.ts +173 -0
  9. package/dist/composites/otel-collector-gateway.d.ts.map +1 -0
  10. package/dist/composites/otel-collector-shape.d.ts +73 -0
  11. package/dist/composites/otel-collector-shape.d.ts.map +1 -0
  12. package/dist/composites/otel-collector.d.ts +19 -1
  13. package/dist/composites/otel-collector.d.ts.map +1 -1
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/integrity.json +7 -3
  17. package/dist/lint/audit-catalog.d.ts.map +1 -1
  18. package/dist/lint/post-synth/index.d.ts.map +1 -1
  19. package/dist/lint/post-synth/otel-placement-helpers.d.ts +103 -0
  20. package/dist/lint/post-synth/otel-placement-helpers.d.ts.map +1 -0
  21. package/dist/lint/post-synth/wk8601.d.ts +17 -0
  22. package/dist/lint/post-synth/wk8601.d.ts.map +1 -0
  23. package/dist/lint/post-synth/wk8602.d.ts +23 -0
  24. package/dist/lint/post-synth/wk8602.d.ts.map +1 -0
  25. package/dist/lint/post-synth/wk8603.d.ts +19 -0
  26. package/dist/lint/post-synth/wk8603.d.ts.map +1 -0
  27. package/dist/manifest.json +1 -1
  28. package/dist/okf/index.md +3 -0
  29. package/dist/okf/rules/WK8601.md +15 -0
  30. package/dist/okf/rules/WK8602.md +16 -0
  31. package/dist/okf/rules/WK8603.md +15 -0
  32. package/dist/okf/types/DaemonSet.md +3 -0
  33. package/dist/okf/types/Service.md +1 -0
  34. package/dist/rules/otel-placement-helpers.ts +277 -0
  35. package/dist/rules/wk8601.ts +44 -0
  36. package/dist/rules/wk8602.ts +133 -0
  37. package/dist/rules/wk8603.ts +66 -0
  38. package/dist/serializer.d.ts.map +1 -1
  39. package/dist/skills/chant-k8s.md +2 -0
  40. package/package.json +4 -3
  41. package/src/composites/__snapshots__/gke-otel-collector.snapshot.test.ts.snap +401 -0
  42. package/src/composites/catalog.ts +109 -0
  43. package/src/composites/gke-otel-collector.snapshot.test.ts +51 -0
  44. package/src/composites/index.ts +9 -0
  45. package/src/composites/monitored-service.ts +40 -16
  46. package/src/composites/otel-collector-agent.ts +62 -26
  47. package/src/composites/otel-collector-gateway.test.ts +260 -0
  48. package/src/composites/otel-collector-gateway.ts +325 -0
  49. package/src/composites/otel-collector-shape.ts +141 -0
  50. package/src/composites/otel-collector.ts +74 -31
  51. package/src/index.ts +3 -0
  52. package/src/lint/audit-catalog.ts +3 -0
  53. package/src/lint/post-synth/index.ts +6 -0
  54. package/src/lint/post-synth/otel-placement-helpers.ts +277 -0
  55. package/src/lint/post-synth/otel-placement.test.ts +401 -0
  56. package/src/lint/post-synth/wk8601.ts +44 -0
  57. package/src/lint/post-synth/wk8602.ts +133 -0
  58. package/src/lint/post-synth/wk8603.ts +66 -0
  59. package/src/prometheus-rule-groups.test.ts +135 -0
  60. package/src/serializer.ts +9 -1
  61. package/src/skills/chant-k8s.md +2 -0
@@ -2153,6 +2153,7 @@ export const compositeCatalog: CompositeEntry[] = [
2153
2153
  "ClusterRoleBinding",
2154
2154
  "ConfigMap",
2155
2155
  "DaemonSet",
2156
+ "Pipeline",
2156
2157
  "ServiceAccount"
2157
2158
  ],
2158
2159
  "params": [
@@ -2460,6 +2461,12 @@ export const compositeCatalog: CompositeEntry[] = [
2460
2461
  "required": false,
2461
2462
  "description": "Alert rules — if provided, creates a PrometheusRule."
2462
2463
  },
2464
+ {
2465
+ "name": "ruleGroups",
2466
+ "type": "Array<RuleGroupEntity | RuleGroupProps>",
2467
+ "required": false,
2468
+ "description": "Rule groups from the prometheus lexicon — if provided, creates a PrometheusRule holding them, after the `alertRules` group when both are set."
2469
+ },
2463
2470
  {
2464
2471
  "name": "replicas",
2465
2472
  "type": "number",
@@ -2894,6 +2901,8 @@ export const compositeCatalog: CompositeEntry[] = [
2894
2901
  "ClusterRoleBinding",
2895
2902
  "ConfigMap",
2896
2903
  "DaemonSet",
2904
+ "Role",
2905
+ "RoleBinding",
2897
2906
  "Service",
2898
2907
  "ServiceAccount"
2899
2908
  ],
@@ -2972,6 +2981,106 @@ export const compositeCatalog: CompositeEntry[] = [
2972
2981
  }
2973
2982
  ]
2974
2983
  },
2984
+ {
2985
+ "name": "OtelCollectorGateway",
2986
+ "lexicon": "k8s",
2987
+ "description": "Create an OtelCollectorGateway composite.",
2988
+ "bundles": [
2989
+ "ClusterRole",
2990
+ "ClusterRoleBinding",
2991
+ "ConfigMap",
2992
+ "Deployment",
2993
+ "PodDisruptionBudget",
2994
+ "Service",
2995
+ "ServiceAccount"
2996
+ ],
2997
+ "params": [
2998
+ {
2999
+ "name": "name",
3000
+ "type": "string",
3001
+ "required": false,
3002
+ "description": "Gateway name (default: \"otel-gateway\")."
3003
+ },
3004
+ {
3005
+ "name": "namespace",
3006
+ "type": "string",
3007
+ "required": false,
3008
+ "description": "Namespace (default: \"observability\")."
3009
+ },
3010
+ {
3011
+ "name": "replicas",
3012
+ "type": "number",
3013
+ "required": false,
3014
+ "description": "Number of collector pods (default: 2)."
3015
+ },
3016
+ {
3017
+ "name": "image",
3018
+ "type": "string",
3019
+ "required": false,
3020
+ "description": "Collector image (default: the contrib image at the otel lexicon's pinned collector version)."
3021
+ },
3022
+ {
3023
+ "name": "config",
3024
+ "type": "Iterable<Declarable>",
3025
+ "required": false,
3026
+ "description": "The collector config, as otel lexicon entities (components and pipelines)."
3027
+ },
3028
+ {
3029
+ "name": "exporters",
3030
+ "type": "OTelComponent<\"exporter\", string, any>[]",
3031
+ "required": false,
3032
+ "description": "Exporters for the default config (default: one `debug` exporter)."
3033
+ },
3034
+ {
3035
+ "name": "signals",
3036
+ "type": "Signal[]",
3037
+ "required": false,
3038
+ "description": "Signals the default config has pipelines for (default: traces, metrics and logs)."
3039
+ },
3040
+ {
3041
+ "name": "clusterRules",
3042
+ "type": "CollectorPolicyRule[]",
3043
+ "required": false,
3044
+ "description": "Cluster-wide read access the config needs, e.g. for a `k8s_cluster` receiver or a `k8sattributes` processor."
3045
+ },
3046
+ {
3047
+ "name": "labels",
3048
+ "type": "Record<string, string>",
3049
+ "required": false,
3050
+ "description": "Additional labels."
3051
+ },
3052
+ {
3053
+ "name": "cpuRequest",
3054
+ "type": "string",
3055
+ "required": false,
3056
+ "description": "CPU request (default: \"200m\")."
3057
+ },
3058
+ {
3059
+ "name": "memoryRequest",
3060
+ "type": "string",
3061
+ "required": false,
3062
+ "description": "Memory request (default: \"512Mi\")."
3063
+ },
3064
+ {
3065
+ "name": "cpuLimit",
3066
+ "type": "string",
3067
+ "required": false,
3068
+ "description": "CPU limit (default: \"1\")."
3069
+ },
3070
+ {
3071
+ "name": "memoryLimit",
3072
+ "type": "string",
3073
+ "required": false,
3074
+ "description": "Memory limit (default: \"1Gi\")."
3075
+ },
3076
+ {
3077
+ "name": "defaults",
3078
+ "type": "{ deployment?: Partial<Record<string, unknown>>; service?: Partial<Record<string, unknown>>; headlessService?: Partia...",
3079
+ "required": false,
3080
+ "description": "Per-member defaults for fine-grained overrides."
3081
+ }
3082
+ ]
3083
+ },
2975
3084
  {
2976
3085
  "name": "RayCluster",
2977
3086
  "lexicon": "k8s",
@@ -0,0 +1,51 @@
1
+ /**
2
+ * GkeOtelCollector's rendered manifests, held to a snapshot (#2898).
3
+ *
4
+ * `OtelCollector`, `OtelCollectorGateway` and `GkeOtelCollector` share the
5
+ * collector resources in `otel-collector-agent.ts`. The snapshot was recorded
6
+ * before the gateway work touched that file, so any change to the GKE output
7
+ * made through the shared code fails here.
8
+ */
9
+ import { describe, expect, test } from "vitest";
10
+ import { expandComposite } from "@intentius/chant";
11
+ import { k8sSerializer } from "../serializer";
12
+ import { GkeOtelCollector } from "./gke-otel-collector";
13
+
14
+ function manifests(instance: ReturnType<typeof GkeOtelCollector>): string {
15
+ const out = k8sSerializer.serialize(expandComposite("collector", instance));
16
+ return typeof out === "string" ? out : out.primary;
17
+ }
18
+
19
+ describe("GkeOtelCollector output", () => {
20
+ test("with Workload Identity", () => {
21
+ expect(
22
+ manifests(
23
+ GkeOtelCollector({
24
+ clusterName: "test-cluster",
25
+ projectId: "test-project",
26
+ gcpServiceAccountEmail: "otel@test-project.iam.gserviceaccount.com",
27
+ }),
28
+ ),
29
+ ).toMatchSnapshot();
30
+ });
31
+
32
+ test("with every prop set", () => {
33
+ expect(
34
+ manifests(
35
+ GkeOtelCollector({
36
+ clusterName: "c1",
37
+ projectId: "p-123456",
38
+ name: "otel",
39
+ namespace: "telemetry",
40
+ image: "otel/opentelemetry-collector-contrib:0.100.0",
41
+ labels: { team: "obs" },
42
+ cpuRequest: "50m",
43
+ memoryRequest: "128Mi",
44
+ cpuLimit: "1",
45
+ memoryLimit: "1Gi",
46
+ defaults: { daemonSet: { spec: { updateStrategy: { type: "OnDelete" } } } },
47
+ }),
48
+ ),
49
+ ).toMatchSnapshot();
50
+ });
51
+ });
@@ -67,6 +67,15 @@ export { GkeFluentBitAgent } from "./gke-fluent-bit-agent";
67
67
  export type { GkeFluentBitAgentProps, GkeFluentBitAgentResult } from "./gke-fluent-bit-agent";
68
68
  export { OtelCollector } from "./otel-collector";
69
69
  export type { OtelCollectorProps, OtelCollectorResult } from "./otel-collector";
70
+ export { OtelCollectorGateway, gatewayExporter } from "./otel-collector-gateway";
71
+ export type {
72
+ OtelCollectorGatewayProps,
73
+ OtelCollectorGatewayResult,
74
+ GatewayExporterOptions,
75
+ CollectorPolicyRule,
76
+ } from "./otel-collector-gateway";
77
+ export { OTEL_COLLECTOR_ANNOTATIONS } from "./otel-collector-shape";
78
+ export type { CollectorRole, GatewayRouting } from "./otel-collector-shape";
70
79
  export { GkeOtelCollector } from "./gke-otel-collector";
71
80
  export type { GkeOtelCollectorProps, GkeOtelCollectorResult } from "./gke-otel-collector";
72
81
  export { GkeExternalDnsAgent } from "./gke-external-dns-agent";
@@ -4,9 +4,15 @@
4
4
  * Observability pattern. ServiceMonitor and PrometheusRule are CRDs
5
5
  * from the Prometheus Operator (monitoring.coreos.com/v1), returned
6
6
  * as raw objects that can be serialized alongside native K8s resources.
7
+ *
8
+ * Rules come in two forms. `ruleGroups` takes `RuleGroup`s from the
9
+ * prometheus lexicon (`@intentius/chant-lexicon-prometheus`), typed and
10
+ * checked there, and rendered here exactly as they would be in a rule file.
11
+ * `alertRules` is the older shorthand for one group of alerts and still works.
7
12
  */
8
13
 
9
14
  import { Composite, mergeDefaults } from "@intentius/chant";
15
+ import { ruleGroupConfig, type RuleGroupEntity, type RuleGroupProps } from "@intentius/chant-lexicon-prometheus";
10
16
  import { Deployment, Service, ServiceMonitor, PrometheusRule } from "../generated";
11
17
  import type { ContainerSecurityContext } from "./security-context";
12
18
 
@@ -38,6 +44,11 @@ export interface MonitoredServiceProps {
38
44
  scrapeInterval?: string;
39
45
  /** Alert rules — if provided, creates a PrometheusRule. */
40
46
  alertRules?: AlertRule[];
47
+ /**
48
+ * Rule groups from the prometheus lexicon — if provided, creates a
49
+ * PrometheusRule holding them, after the `alertRules` group when both are set.
50
+ */
51
+ ruleGroups?: Array<RuleGroupEntity | RuleGroupProps>;
41
52
  /** Number of replicas (default: 2). */
42
53
  replicas?: number;
43
54
  /** Additional labels to apply to all resources. */
@@ -89,6 +100,14 @@ export interface MonitoredServiceResult {
89
100
  * { name: "HighErrorRate", expr: 'rate(http_errors_total[5m]) > 0.1', for: "5m", severity: "critical" },
90
101
  * ],
91
102
  * });
103
+ *
104
+ * // Or with typed rule groups from the prometheus lexicon:
105
+ * import { RuleGroup } from "@intentius/chant-lexicon-prometheus";
106
+ * const apiRules = new RuleGroup({
107
+ * name: "api",
108
+ * rules: [{ alert: "ApiDown", expr: 'up{job="api"} == 0', for: "5m", labels: { severity: "page" } }],
109
+ * });
110
+ * MonitoredService({ name: "api", image: "api:1.0", ruleGroups: [apiRules] });
92
111
  * ```
93
112
  */
94
113
  export const MonitoredService = Composite((props: MonitoredServiceProps) => {
@@ -100,6 +119,7 @@ export const MonitoredService = Composite((props: MonitoredServiceProps) => {
100
119
  metricsPath = "/metrics",
101
120
  scrapeInterval = "30s",
102
121
  alertRules,
122
+ ruleGroups,
103
123
  replicas = 2,
104
124
  labels: extraLabels = {},
105
125
  cpuLimit = "500m",
@@ -197,29 +217,33 @@ export const MonitoredService = Composite((props: MonitoredServiceProps) => {
197
217
 
198
218
  const result: Record<string, any> = { deployment, service, serviceMonitor };
199
219
 
220
+ const groups: Array<Record<string, unknown>> = [];
200
221
  if (alertRules && alertRules.length > 0) {
222
+ groups.push({
223
+ name: `${name}.rules`,
224
+ rules: alertRules.map((rule) => ({
225
+ alert: rule.name,
226
+ expr: rule.expr,
227
+ ...(rule.for && { for: rule.for }),
228
+ labels: {
229
+ ...(rule.severity && { severity: rule.severity }),
230
+ },
231
+ ...(rule.annotations && { annotations: rule.annotations }),
232
+ })),
233
+ });
234
+ }
235
+ for (const group of ruleGroups ?? []) {
236
+ groups.push(ruleGroupConfig(group) as unknown as Record<string, unknown>);
237
+ }
238
+
239
+ if (groups.length > 0) {
201
240
  result.prometheusRule = new PrometheusRule(mergeDefaults({
202
241
  metadata: {
203
242
  name: `${name}-alerts`,
204
243
  ...(namespace && { namespace }),
205
244
  labels: { ...commonLabels, "app.kubernetes.io/component": "monitoring" },
206
245
  },
207
- spec: {
208
- groups: [
209
- {
210
- name: `${name}.rules`,
211
- rules: alertRules.map((rule) => ({
212
- alert: rule.name,
213
- expr: rule.expr,
214
- ...(rule.for && { for: rule.for }),
215
- labels: {
216
- ...(rule.severity && { severity: rule.severity }),
217
- },
218
- ...(rule.annotations && { annotations: rule.annotations }),
219
- })),
220
- },
221
- ],
222
- },
246
+ spec: { groups },
223
247
  }, defs?.prometheusRule));
224
248
  }
225
249
 
@@ -8,6 +8,9 @@
8
8
  * `collectorYaml`) and says which ports the container exposes. Everything
9
9
  * platform-specific, such as a Workload Identity annotation or probes, comes
10
10
  * in as an option, so each composite keeps its own output.
11
+ *
12
+ * `OtelCollectorGateway` runs the same container and ConfigMap
13
+ * (`collectorContainer`, `collectorConfigMap`) in a Deployment.
11
14
  */
12
15
 
13
16
  import { mergeDefaults } from "@intentius/chant";
@@ -34,6 +37,10 @@ export interface CollectorAgentOptions {
34
37
  containerExtra?: Record<string, unknown>;
35
38
  /** Annotations on the ServiceAccount, such as a cloud identity binding. */
36
39
  serviceAccountAnnotations?: Record<string, string>;
40
+ /** Annotations on the DaemonSet, such as the deployment-shape markers in `otel-collector-shape.ts`. */
41
+ workloadAnnotations?: Record<string, string>;
42
+ /** Annotations on the ConfigMap. */
43
+ configMapAnnotations?: Record<string, string>;
37
44
  defaults?: {
38
45
  daemonSet?: Partial<Record<string, unknown>>;
39
46
  serviceAccount?: Partial<Record<string, unknown>>;
@@ -58,32 +65,14 @@ export function collectorAgentResources(opts: CollectorAgentOptions): CollectorA
58
65
  const bindingName = `${name}-binding`;
59
66
  const configMapName = `${name}-config`;
60
67
 
61
- const container: Record<string, unknown> = {
62
- name,
63
- image: opts.image,
64
- args: [`--config=${opts.configDir}/config.yaml`],
65
- ports: opts.ports,
66
- resources: {
67
- requests: { cpu: opts.cpuRequest, memory: opts.memoryRequest },
68
- limits: { cpu: opts.cpuLimit, memory: opts.memoryLimit },
69
- },
70
- volumeMounts: [
71
- { name: "config", mountPath: opts.configDir, readOnly: true },
72
- ],
73
- securityContext: {
74
- runAsNonRoot: true,
75
- runAsUser: 10001,
76
- readOnlyRootFilesystem: true,
77
- allowPrivilegeEscalation: false,
78
- },
79
- ...opts.containerExtra,
80
- };
68
+ const container = collectorContainer(opts);
81
69
 
82
70
  const daemonSet = new DaemonSet(mergeDefaults({
83
71
  metadata: {
84
72
  name,
85
73
  namespace,
86
74
  labels: { ...commonLabels, "app.kubernetes.io/component": "agent" },
75
+ ...(opts.workloadAnnotations ? { annotations: opts.workloadAnnotations } : {}),
87
76
  },
88
77
  spec: {
89
78
  selector: { matchLabels: { "app.kubernetes.io/name": name } },
@@ -144,16 +133,63 @@ export function collectorAgentResources(opts: CollectorAgentOptions): CollectorA
144
133
  ],
145
134
  }, defs?.clusterRoleBinding));
146
135
 
147
- const configMap = new ConfigMap(mergeDefaults({
136
+ const configMap = collectorConfigMap(opts, configMapName, opts.configMapAnnotations);
137
+
138
+ return { daemonSet, serviceAccount, clusterRole, clusterRoleBinding, configMap };
139
+ }
140
+
141
+ /** The fields the collector container and its ConfigMap are built from. */
142
+ export type CollectorContainerOptions = Pick<
143
+ CollectorAgentOptions,
144
+ "name" | "image" | "configDir" | "ports" | "cpuRequest" | "memoryRequest" | "cpuLimit" | "memoryLimit" | "containerExtra"
145
+ >;
146
+
147
+ /**
148
+ * The collector container: the image, the config file argument, the ports,
149
+ * requests and limits, the config volume mount and a non-root security
150
+ * context, with `containerExtra` laid over the top. The agent DaemonSet and
151
+ * the gateway Deployment run the same container.
152
+ */
153
+ export function collectorContainer(opts: CollectorContainerOptions): Record<string, unknown> {
154
+ return {
155
+ name: opts.name,
156
+ image: opts.image,
157
+ args: [`--config=${opts.configDir}/config.yaml`],
158
+ ports: opts.ports,
159
+ resources: {
160
+ requests: { cpu: opts.cpuRequest, memory: opts.memoryRequest },
161
+ limits: { cpu: opts.cpuLimit, memory: opts.memoryLimit },
162
+ },
163
+ volumeMounts: [
164
+ { name: "config", mountPath: opts.configDir, readOnly: true },
165
+ ],
166
+ securityContext: {
167
+ runAsNonRoot: true,
168
+ runAsUser: 10001,
169
+ readOnlyRootFilesystem: true,
170
+ allowPrivilegeEscalation: false,
171
+ },
172
+ ...opts.containerExtra,
173
+ };
174
+ }
175
+
176
+ /** The ConfigMap holding the rendered collector config under `config.yaml`. */
177
+ export function collectorConfigMap(
178
+ opts: Pick<CollectorAgentOptions, "namespace" | "commonLabels" | "configYaml"> & {
179
+ defaults?: { configMap?: Partial<Record<string, unknown>> };
180
+ },
181
+ configMapName: string,
182
+ annotations?: Record<string, string>,
183
+ ): InstanceType<typeof ConfigMap> {
184
+ return new ConfigMap(mergeDefaults({
148
185
  metadata: {
149
186
  name: configMapName,
150
- namespace,
151
- labels: { ...commonLabels, "app.kubernetes.io/component": "config" },
187
+ namespace: opts.namespace,
188
+ labels: { ...opts.commonLabels, "app.kubernetes.io/component": "config" },
189
+ ...(annotations ? { annotations } : {}),
152
190
  },
153
191
  data: {
154
192
  "config.yaml": opts.configYaml,
155
193
  },
156
- }, defs?.configMap));
157
-
158
- return { daemonSet, serviceAccount, clusterRole, clusterRoleBinding, configMap };
194
+ }, opts.defaults?.configMap));
159
195
  }