@intentius/chant-lexicon-k8s 0.95.1 → 0.97.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
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The prometheus lexicon's RuleGroup inside the k8s lexicon (#2901): the same
3
+ * group renders as a rule file and inside a PrometheusRule, whether it goes
4
+ * in directly or through MonitoredService, and MonitoredService's older
5
+ * alertRules input renders exactly as before.
6
+ */
7
+ import { describe, expect, test } from "vitest";
8
+ import { load, loadAll } from "js-yaml";
9
+ import type { Declarable } from "@intentius/chant/declarable";
10
+ import {
11
+ RuleGroup,
12
+ hasTool,
13
+ prometheusSerializer,
14
+ promtoolCheckRules,
15
+ emitYaml,
16
+ type RuleFileConfig,
17
+ } from "@intentius/chant-lexicon-prometheus";
18
+ import { k8sSerializer } from "./serializer";
19
+ import { PrometheusRule } from "./generated";
20
+ import { MonitoredService } from "./composites/monitored-service";
21
+
22
+ function entities(record: Record<string, unknown>): Map<string, Declarable> {
23
+ return new Map(Object.entries(record) as Array<[string, Declarable]>);
24
+ }
25
+
26
+ function k8sDocs(record: Record<string, unknown>): Array<Record<string, any>> {
27
+ const out = k8sSerializer.serialize(entities(record));
28
+ return loadAll(typeof out === "string" ? out : out.primary) as Array<Record<string, any>>;
29
+ }
30
+
31
+ function ruleFile(record: Record<string, unknown>): RuleFileConfig {
32
+ const out = prometheusSerializer.serialize(entities(record));
33
+ return load(typeof out === "string" ? out : out.primary) as RuleFileConfig;
34
+ }
35
+
36
+ const apiGroup = () =>
37
+ new RuleGroup({
38
+ name: "api",
39
+ interval: "30s",
40
+ rules: [
41
+ { record: "job:http_errors:ratio5m", expr: 'sum by (job) (rate(http_requests_total{code=~"5.."}[5m])) / sum by (job) (rate(http_requests_total[5m]))' },
42
+ { alert: "ApiErrors", expr: "job:http_errors:ratio5m > 0.05", for: "10m", labels: { severity: "page" }, annotations: { summary: "5xx above 5%" } },
43
+ ],
44
+ });
45
+
46
+ describe("PrometheusRule takes RuleGroups", () => {
47
+ test("an exported RuleGroup renders as the group the rule file holds, not as a reference", () => {
48
+ const api = apiGroup();
49
+ const rule = new PrometheusRule({ metadata: { name: "api-rules" }, spec: { groups: [api] } });
50
+ const [doc] = k8sDocs({ api, rule });
51
+ expect(doc.kind).toBe("PrometheusRule");
52
+ expect(doc.spec.groups).toEqual(ruleFile({ api }).groups);
53
+ });
54
+
55
+ test("an inline RuleGroup renders the same way", () => {
56
+ const rule = new PrometheusRule({ metadata: { name: "api-rules" }, spec: { groups: [apiGroup()] } });
57
+ const [doc] = k8sDocs({ rule });
58
+ expect(doc.spec.groups).toEqual(ruleFile({ api: apiGroup() }).groups);
59
+ });
60
+
61
+ test("plain group objects still pass through untouched", () => {
62
+ const groups = [{ name: "raw", rules: [{ alert: "A", expr: "vector(1)" }] }];
63
+ const [doc] = k8sDocs({ rule: new PrometheusRule({ metadata: { name: "raw" }, spec: { groups } }) });
64
+ expect(doc.spec.groups).toEqual(groups);
65
+ });
66
+
67
+ test.skipIf(!hasTool(process.env.PROMTOOL ?? "promtool"))("the CRD's groups pass promtool as a rule file", () => {
68
+ const [doc] = k8sDocs({ rule: new PrometheusRule({ metadata: { name: "api-rules" }, spec: { groups: [apiGroup()] } }) });
69
+ const r = promtoolCheckRules(emitYaml({ groups: doc.spec.groups }));
70
+ expect(r.ok, r.output).toBe(true);
71
+ });
72
+ });
73
+
74
+ describe("MonitoredService ruleGroups", () => {
75
+ const minProps = { name: "api", image: "api:1.0" };
76
+
77
+ test("creates a PrometheusRule holding the groups", () => {
78
+ const result = MonitoredService({ ...minProps, ruleGroups: [apiGroup()] });
79
+ const docs = k8sDocs({ prometheusRule: result.prometheusRule });
80
+ expect(docs[0].metadata.name).toBe("api-alerts");
81
+ expect(docs[0].spec.groups).toEqual(ruleFile({ api: apiGroup() }).groups);
82
+ });
83
+
84
+ test("takes plain group props too", () => {
85
+ const result = MonitoredService({ ...minProps, ruleGroups: [{ name: "plain", rules: [{ record: "a:b", expr: "sum(up)" }] }] });
86
+ expect(k8sDocs({ r: result.prometheusRule })[0].spec.groups).toEqual([{ name: "plain", rules: [{ record: "a:b", expr: "sum(up)" }] }]);
87
+ });
88
+
89
+ test("with alertRules, the alertRules group comes first", () => {
90
+ const result = MonitoredService({
91
+ ...minProps,
92
+ alertRules: [{ name: "HighError", expr: "rate(errors[5m]) > 0.1", severity: "critical" }],
93
+ ruleGroups: [apiGroup()],
94
+ });
95
+ expect(k8sDocs({ r: result.prometheusRule })[0].spec.groups.map((g: { name: string }) => g.name)).toEqual(["api.rules", "api"]);
96
+ });
97
+
98
+ test("alertRules alone renders exactly as it did before ruleGroups existed", () => {
99
+ const result = MonitoredService({
100
+ ...minProps,
101
+ alertRules: [
102
+ { name: "HighError", expr: "rate(errors[5m]) > 0.1", for: "5m", severity: "critical", annotations: { summary: "s" } },
103
+ { name: "NoSeverity", expr: "up == 0" },
104
+ ],
105
+ });
106
+ const out = k8sSerializer.serialize(entities({ r: result.prometheusRule }));
107
+ expect(typeof out === "string" ? out : out.primary).toBe(`apiVersion: monitoring.coreos.com/v1
108
+ kind: PrometheusRule
109
+ metadata:
110
+ name: api-alerts
111
+ labels:
112
+ app.kubernetes.io/name: api
113
+ app.kubernetes.io/managed-by: chant
114
+ app.kubernetes.io/component: monitoring
115
+ spec:
116
+ groups:
117
+ - name: api.rules
118
+ rules:
119
+ - alert: HighError
120
+ expr: rate(errors[5m]) > 0.1
121
+ for: '5m'
122
+ labels:
123
+ severity: critical
124
+ annotations:
125
+ summary: s
126
+ - alert: NoSeverity
127
+ expr: up == 0
128
+ labels: {}
129
+ `);
130
+ });
131
+
132
+ test("no rules, no PrometheusRule", () => {
133
+ expect(MonitoredService({ ...minProps, ruleGroups: [] }).prometheusRule).toBeUndefined();
134
+ });
135
+ });
package/src/serializer.ts CHANGED
@@ -31,6 +31,7 @@ import { emitYAML } from "@intentius/chant/yaml";
31
31
  import { isDefaultLabels, isDefaultAnnotations, type DefaultLabels, type DefaultAnnotations } from "./default-labels";
32
32
  import { isRenderedManifestEntity } from "./manifest-entity";
33
33
  import { isEncryptedSecretFileEntity } from "./sops/entity";
34
+ import { isRuleGroup, ruleGroupConfig } from "@intentius/chant-lexicon-prometheus";
34
35
 
35
36
  const require = createRequire(import.meta.url);
36
37
 
@@ -265,8 +266,15 @@ function k8sVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
265
266
 
266
267
  return {
267
268
  attrRef: (name, attr) => resolveK8sAttr(byName.get(name), name, attr),
268
- resourceRef: (name) => name,
269
+ // A prometheus-lexicon RuleGroup (in a PrometheusRule's spec.groups) is
270
+ // data, not a reference: it renders as the same group the rule file
271
+ // holds, whether it was exported as its own entity or written inline.
272
+ resourceRef: (name) => {
273
+ const entity = byName.get(name);
274
+ return isRuleGroup(entity) ? ruleGroupConfig(entity) : name;
275
+ },
269
276
  propertyDeclarable: (entity, walk) => {
277
+ if (isRuleGroup(entity)) return ruleGroupConfig(entity);
270
278
  if (!isResourceDeclarable(entity) || typeof entity.props !== "object" || entity.props === null) {
271
279
  return undefined;
272
280
  }
@@ -171,6 +171,8 @@ Composites are higher-level functions that produce multiple coordinated K8s reso
171
171
  | Multi-host TLS Ingress (cert-manager) | **SecureIngress** | Ingress + optional Certificate |
172
172
  | HPA metrics provider | **MetricsServer** | Deployment + Service + RBAC + APIService |
173
173
  | CockroachDB cluster | **CockroachDbCluster** | StatefulSet + Services + PVCs + RBAC + optional CertificateRequests |
174
+ | OpenTelemetry agent on every node | **OtelCollector** | DaemonSet + node-local Service + RBAC + ConfigMap |
175
+ | OpenTelemetry gateway tier | **OtelCollectorGateway** | Deployment + ClusterIP and headless Services + PDB + ConfigMap; point agents at it with `gatewayExporter(gateway, { loadBalance: true })` |
174
176
 
175
177
  ### Decision Tree — AWS (EKS) Composites
176
178