@intentius/chant-lexicon-k8s 0.124.30 → 0.124.32

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 (270) hide show
  1. package/dist/actions/apps.js +18 -0
  2. package/dist/actions/batch.js +6 -0
  3. package/dist/actions/core.js +48 -0
  4. package/dist/actions/index.js +13 -0
  5. package/dist/actions/networking.js +11 -0
  6. package/dist/actions/rbac.js +8 -0
  7. package/dist/api/classify.js +123 -0
  8. package/dist/api/connect.js +62 -0
  9. package/dist/api/fake-cluster.js +175 -0
  10. package/dist/api/operation-surface.d.ts.map +1 -1
  11. package/dist/api/operation-surface.js +109 -0
  12. package/dist/api/owner-chain.js +100 -0
  13. package/dist/api/sweep-types.js +35 -0
  14. package/dist/behaviour-kinds.js +82 -0
  15. package/dist/codegen/docs-cli.js +2 -0
  16. package/dist/codegen/docs.js +146 -0
  17. package/dist/codegen/generate-cli.js +30 -0
  18. package/dist/codegen/generate-lexicon.js +74 -0
  19. package/dist/codegen/generate-list-map-keys.js +88 -0
  20. package/dist/codegen/generate-operations.js +53 -0
  21. package/dist/codegen/generate-typescript.js +124 -0
  22. package/dist/codegen/generate.js +173 -0
  23. package/dist/codegen/naming.js +174 -0
  24. package/dist/codegen/package.js +36 -0
  25. package/dist/codegen/patches.js +88 -0
  26. package/dist/codegen/typecheck.js +4 -0
  27. package/dist/codegen/versions.js +41 -0
  28. package/dist/components/argo-app.js +51 -0
  29. package/dist/components/builders.js +22 -0
  30. package/dist/components/capability-plugin.js +31 -0
  31. package/dist/components/flux-reconcile.js +61 -0
  32. package/dist/components/index.d.ts +1 -1
  33. package/dist/components/index.d.ts.map +1 -1
  34. package/dist/components/index.js +18 -0
  35. package/dist/components/kubectl-apply.d.ts +83 -1
  36. package/dist/components/kubectl-apply.d.ts.map +1 -1
  37. package/dist/components/kubectl-apply.js +274 -0
  38. package/dist/components/kustomize-apply.js +25 -0
  39. package/dist/composites/adot-collector.js +184 -0
  40. package/dist/composites/agic-ingress.js +85 -0
  41. package/dist/composites/aks-external-dns-agent.js +146 -0
  42. package/dist/composites/alb-ingress.js +86 -0
  43. package/dist/composites/argo-app.js +199 -0
  44. package/dist/composites/autoscaled-service.js +174 -0
  45. package/dist/composites/azure-disk-storage-class.js +47 -0
  46. package/dist/composites/azure-file-storage-class.js +48 -0
  47. package/dist/composites/azure-monitor-collector.js +176 -0
  48. package/dist/composites/batch-job.js +139 -0
  49. package/dist/composites/catalog.js +4110 -0
  50. package/dist/composites/cockroachdb-cluster.js +371 -0
  51. package/dist/composites/cockroachdb-region-stack.js +369 -0
  52. package/dist/composites/config-connector-context.js +40 -0
  53. package/dist/composites/configured-app.js +135 -0
  54. package/dist/composites/cron-schedule.js +38 -0
  55. package/dist/composites/cron-workload.js +109 -0
  56. package/dist/composites/ebs-storage-class.js +54 -0
  57. package/dist/composites/efs-storage-class.js +46 -0
  58. package/dist/composites/external-dns-agent.js +122 -0
  59. package/dist/composites/filestore-storage-class.js +49 -0
  60. package/dist/composites/fluent-bit-agent.js +157 -0
  61. package/dist/composites/flux-app.js +156 -0
  62. package/dist/composites/gce-ingress.js +83 -0
  63. package/dist/composites/gce-pd-storage-class.js +49 -0
  64. package/dist/composites/gke-external-dns-agent.js +129 -0
  65. package/dist/composites/gke-fluent-bit-agent.js +165 -0
  66. package/dist/composites/gke-gateway.js +91 -0
  67. package/dist/composites/gke-otel-collector.js +94 -0
  68. package/dist/composites/index.js +51 -0
  69. package/dist/composites/inference-service.js +76 -0
  70. package/dist/composites/irsa-service-account.js +77 -0
  71. package/dist/composites/member-defaults.js +14 -0
  72. package/dist/composites/metrics-server.js +181 -0
  73. package/dist/composites/model.js +92 -0
  74. package/dist/composites/monitored-service.js +152 -0
  75. package/dist/composites/namespace-env.js +114 -0
  76. package/dist/composites/network-isolated-app.js +124 -0
  77. package/dist/composites/node-agent.js +149 -0
  78. package/dist/composites/operator-stack.js +268 -0
  79. package/dist/composites/otel-collector-agent.js +142 -0
  80. package/dist/composites/otel-collector-gateway.js +216 -0
  81. package/dist/composites/otel-collector-node.js +176 -0
  82. package/dist/composites/otel-collector-rbac.js +115 -0
  83. package/dist/composites/otel-collector-shape.js +128 -0
  84. package/dist/composites/otel-collector.js +115 -0
  85. package/dist/composites/otel-operator-collector.js +123 -0
  86. package/dist/composites/ray-cluster.js +378 -0
  87. package/dist/composites/ray-job.js +136 -0
  88. package/dist/composites/ray-service.js +178 -0
  89. package/dist/composites/secure-ingress.js +100 -0
  90. package/dist/composites/security-context.js +1 -0
  91. package/dist/composites/sidecar-app.js +105 -0
  92. package/dist/composites/stateful-app.js +111 -0
  93. package/dist/composites/vllm-serving-runtime.js +114 -0
  94. package/dist/composites/web-app.js +157 -0
  95. package/dist/composites/worker-pool.js +167 -0
  96. package/dist/composites/workload-identity-sa.js +81 -0
  97. package/dist/composites/workload-identity-service-account.js +77 -0
  98. package/dist/config-schema.js +72 -0
  99. package/dist/config.js +59 -0
  100. package/dist/coverage.js +27 -0
  101. package/dist/crd/crd-sources.js +615 -0
  102. package/dist/crd/loader.js +181 -0
  103. package/dist/crd/oci-chart.js +144 -0
  104. package/dist/crd/parser.js +382 -0
  105. package/dist/crd/project-codegen.js +273 -0
  106. package/dist/crd/types.js +7 -0
  107. package/dist/deep-observe-hooks.js +306 -0
  108. package/dist/deep-observe.js +234 -0
  109. package/dist/default-labels.js +87 -0
  110. package/dist/describe-resources.js +699 -0
  111. package/dist/describe-stack-status.js +99 -0
  112. package/dist/detect.js +11 -0
  113. package/dist/effect-receipt-row.js +205 -0
  114. package/dist/export-resources.js +39 -0
  115. package/dist/generated/index.js +1092 -0
  116. package/dist/generated/runtime.js +4 -0
  117. package/dist/group-namespace.js +96 -0
  118. package/dist/import/embedded.js +197 -0
  119. package/dist/import/generator.js +288 -0
  120. package/dist/import/live-export.js +85 -0
  121. package/dist/import/parser.js +204 -0
  122. package/dist/import/testdata/embedded/fixtures.js +60 -0
  123. package/dist/include/entity.js +78 -0
  124. package/dist/include/resolve.js +170 -0
  125. package/dist/index.js +59 -0
  126. package/dist/integrity.json +3 -3
  127. package/dist/kube/apply.js +90 -0
  128. package/dist/kube/connect.js +46 -0
  129. package/dist/kube/delete.js +138 -0
  130. package/dist/kube/describe.js +169 -0
  131. package/dist/kube/events.js +132 -0
  132. package/dist/kube/flags.js +91 -0
  133. package/dist/kube/get.js +159 -0
  134. package/dist/kube/group.js +123 -0
  135. package/dist/kube/logs.js +82 -0
  136. package/dist/kube/project.js +94 -0
  137. package/dist/kube/render.js +227 -0
  138. package/dist/kube/source.js +122 -0
  139. package/dist/kube/target.js +63 -0
  140. package/dist/kube/testing.js +28 -0
  141. package/dist/kube/top.js +99 -0
  142. package/dist/kube/verdict.js +72 -0
  143. package/dist/kube/wait.js +129 -0
  144. package/dist/kustomize/render.js +73 -0
  145. package/dist/kustomize/rendered-entity.js +67 -0
  146. package/dist/kustomize/root.js +68 -0
  147. package/dist/lint/audit-catalog.js +61 -0
  148. package/dist/lint/audit-lineage.js +118 -0
  149. package/dist/lint/post-synth/argo-helpers.js +36 -0
  150. package/dist/lint/post-synth/argo002.js +31 -0
  151. package/dist/lint/post-synth/argo003.js +53 -0
  152. package/dist/lint/post-synth/argo005.js +54 -0
  153. package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
  154. package/dist/lint/post-synth/crd-schema-helpers.js +214 -0
  155. package/dist/lint/post-synth/flux002.js +56 -0
  156. package/dist/lint/post-synth/flux003.js +50 -0
  157. package/dist/lint/post-synth/index.js +104 -0
  158. package/dist/lint/post-synth/k8s-helpers.js +186 -0
  159. package/dist/lint/post-synth/monitoring-selection-helpers.js +163 -0
  160. package/dist/lint/post-synth/otel-placement-helpers.js +293 -0
  161. package/dist/lint/post-synth/sops-helpers.js +43 -0
  162. package/dist/lint/post-synth/wk8005.js +44 -0
  163. package/dist/lint/post-synth/wk8006.js +56 -0
  164. package/dist/lint/post-synth/wk8041.js +60 -0
  165. package/dist/lint/post-synth/wk8042.js +36 -0
  166. package/dist/lint/post-synth/wk8101.js +52 -0
  167. package/dist/lint/post-synth/wk8102.js +30 -0
  168. package/dist/lint/post-synth/wk8103.js +33 -0
  169. package/dist/lint/post-synth/wk8104.js +58 -0
  170. package/dist/lint/post-synth/wk8105.js +33 -0
  171. package/dist/lint/post-synth/wk8201.js +44 -0
  172. package/dist/lint/post-synth/wk8202.js +34 -0
  173. package/dist/lint/post-synth/wk8203.js +34 -0
  174. package/dist/lint/post-synth/wk8204.js +71 -0
  175. package/dist/lint/post-synth/wk8205.js +38 -0
  176. package/dist/lint/post-synth/wk8207.js +33 -0
  177. package/dist/lint/post-synth/wk8208.js +33 -0
  178. package/dist/lint/post-synth/wk8209.js +33 -0
  179. package/dist/lint/post-synth/wk8301.js +49 -0
  180. package/dist/lint/post-synth/wk8302.js +34 -0
  181. package/dist/lint/post-synth/wk8303.js +68 -0
  182. package/dist/lint/post-synth/wk8304.js +58 -0
  183. package/dist/lint/post-synth/wk8305.js +96 -0
  184. package/dist/lint/post-synth/wk8306.js +38 -0
  185. package/dist/lint/post-synth/wk8401.js +86 -0
  186. package/dist/lint/post-synth/wk8402.js +31 -0
  187. package/dist/lint/post-synth/wk8403.js +49 -0
  188. package/dist/lint/post-synth/wk8404.js +36 -0
  189. package/dist/lint/post-synth/wk8405.js +65 -0
  190. package/dist/lint/post-synth/wk8406.js +50 -0
  191. package/dist/lint/post-synth/wk8407.js +58 -0
  192. package/dist/lint/post-synth/wk8501.js +36 -0
  193. package/dist/lint/post-synth/wk8502.js +35 -0
  194. package/dist/lint/post-synth/wk8503.js +164 -0
  195. package/dist/lint/post-synth/wk8504.js +20 -0
  196. package/dist/lint/post-synth/wk8505.js +31 -0
  197. package/dist/lint/post-synth/wk8601.js +40 -0
  198. package/dist/lint/post-synth/wk8602.js +120 -0
  199. package/dist/lint/post-synth/wk8603.js +63 -0
  200. package/dist/lint/post-synth/wk8604.js +31 -0
  201. package/dist/lint/post-synth/wk8605.js +124 -0
  202. package/dist/lint/post-synth/wk8606.js +22 -0
  203. package/dist/lint/post-synth/wk8701.js +31 -0
  204. package/dist/lint/post-synth/wk8702.js +37 -0
  205. package/dist/lint/post-synth/wk8703.js +31 -0
  206. package/dist/lint/post-synth/wk8704.js +135 -0
  207. package/dist/lint/post-synth/wk8705.js +38 -0
  208. package/dist/lint/rules/argo-appset-single-project.js +56 -0
  209. package/dist/lint/rules/argo-ast.js +94 -0
  210. package/dist/lint/rules/argo-automated-prune.js +59 -0
  211. package/dist/lint/rules/flux-source-ref-pin.js +44 -0
  212. package/dist/lint/rules/hardcoded-namespace.js +57 -0
  213. package/dist/lint/rules/latest-image-tag.js +103 -0
  214. package/dist/lint/rules/missing-resource-limits.js +85 -0
  215. package/dist/lsp/completions.d.ts.map +1 -1
  216. package/dist/lsp/completions.js +18 -0
  217. package/dist/lsp/hover.d.ts.map +1 -1
  218. package/dist/lsp/hover.js +57 -0
  219. package/dist/manifest-entity.js +71 -0
  220. package/dist/manifest.json +1 -1
  221. package/dist/micro-time.js +49 -0
  222. package/dist/op/activities/apply-diff.d.ts +31 -0
  223. package/dist/op/activities/apply-diff.d.ts.map +1 -0
  224. package/dist/op/activities/apply-diff.js +79 -0
  225. package/dist/op/activities/argo.js +98 -0
  226. package/dist/op/activities/ensure-secret.js +38 -0
  227. package/dist/op/activities/index.js +51 -0
  228. package/dist/op/activities/kubectl.d.ts +64 -0
  229. package/dist/op/activities/kubectl.d.ts.map +1 -1
  230. package/dist/op/activities/kubectl.js +517 -0
  231. package/dist/op/activities/wait-for-ready.js +229 -0
  232. package/dist/op/builders.js +68 -0
  233. package/dist/package-cli.js +21 -0
  234. package/dist/patch/json-patch.js +187 -0
  235. package/dist/patch/rendered-patch.js +94 -0
  236. package/dist/plugin.js +741 -0
  237. package/dist/project-kinds.js +56 -0
  238. package/dist/receipt-store.js +216 -0
  239. package/dist/rules/crd-schema-helpers.ts +2 -1
  240. package/dist/secret-labels.js +33 -0
  241. package/dist/secret-store.js +116 -0
  242. package/dist/serializer.d.ts.map +1 -1
  243. package/dist/serializer.js +578 -0
  244. package/dist/sops/encrypted-secret-file.js +194 -0
  245. package/dist/sops/entity.js +32 -0
  246. package/dist/spec/fetch.js +58 -0
  247. package/dist/spec/parse.js +622 -0
  248. package/dist/subscribe-changes.js +168 -0
  249. package/dist/teardown.js +197 -0
  250. package/dist/telemetry-release.js +86 -0
  251. package/dist/validate-cli.js +17 -0
  252. package/dist/validate.js +47 -0
  253. package/dist/variables.js +34 -0
  254. package/dist/whoami.js +109 -0
  255. package/package.json +12 -12
  256. package/src/api/operation-surface.ts +2 -1
  257. package/src/codegen/package.ts +2 -2
  258. package/src/components/index.ts +3 -0
  259. package/src/components/kubectl-apply-plan.k3d.e2e.test.ts +127 -0
  260. package/src/components/kubectl-apply.test.ts +262 -0
  261. package/src/components/kubectl-apply.ts +341 -12
  262. package/src/deep-observe-hooks.ts +2 -2
  263. package/src/import/roundtrip-build.test.ts +46 -0
  264. package/src/lint/post-synth/crd-schema-helpers.ts +2 -1
  265. package/src/lsp/completions.ts +2 -1
  266. package/src/lsp/hover.ts +2 -1
  267. package/src/op/activities/apply-diff.ts +87 -0
  268. package/src/op/activities/kubectl-plan.test.ts +224 -0
  269. package/src/op/activities/kubectl.ts +204 -16
  270. package/src/serializer.ts +2 -1
@@ -0,0 +1,268 @@
1
+ /**
2
+ * OperatorStack composite (#1940, epic #1487) — the operating loop
3
+ * materialized in-cluster as a declared estate on gitops principles: one
4
+ * Namespace, one CronJob per hosted `ConvergeOp` tick, and RBAC scoped to
5
+ * what that tick can actually do. chant declares the loop that runs chant
6
+ * with its own k8s lexicon — the same estate discipline as any other app.
7
+ *
8
+ * ## Why a CronJob, not the operator daemon
9
+ *
10
+ * Issue #1485 ("chant operator: native durable ticks without Temporal")
11
+ * names cron, a systemd timer, a k8s CronJob, a CI schedule, and the
12
+ * `chant operator` daemon as interchangeable safe invokers of the same
13
+ * thing: one converge tick. A k8s CronJob invoking `chant run <name>` per
14
+ * tick is that issue's own "cron … invokes the tick" case, not a dependency
15
+ * on the daemon it proposes — #1485 is a separate, still-unmerged runtime
16
+ * (lease fencing, `chant operator status`, durable gate-as-fact semantics).
17
+ * `OperatorStack`'s container command is deliberately `chant run <name>`,
18
+ * the one-shot local tick `ConvergeOp` (#1484) already ships and tests
19
+ * against (`packages/core/src/op/composites/converge-op.ts`'s own doc:
20
+ * "one-shot runnable locally for a single tick"). When #1485 lands, a
21
+ * caller can override `command` to shell out to `chant operator tick`
22
+ * instead — this composite doesn't need to change for that; only the
23
+ * command a caller passes does.
24
+ *
25
+ * `concurrencyPolicy: "Forbid"` is the k8s-native analogue of
26
+ * `ConvergeOp`'s own `schedule.overlap: "skip"` policy — never queue a
27
+ * second tick behind one still running.
28
+ *
29
+ * ## RBAC derivation
30
+ *
31
+ * `ConvergeOp` adds no authority an environment did not already grant
32
+ * (#1484's Autonomy table). `OperatorStack` re-derives the same bound at the
33
+ * k8s RBAC layer, from the Op configs alone (see the Layering note below for
34
+ * what this module imports and what it restates): for each hosted
35
+ * ConvergeOp, walk its `dispatchTargets` (the OpConfigs its rule table's
36
+ * `run()` actions may name) through `classifyOpVerbClass`
37
+ * (`packages/core/src/op/op-verb-class.ts`, #1954), then keep only the
38
+ * highest verb class this host's `dial` could ever actually free-run —
39
+ * exactly `convergeTick`'s own `verbClassAllowedToDispatch` gate and
40
+ * `OPS014`'s build-time refusal, restated as an RBAC ceiling:
41
+ *
42
+ * - `dial: "observe"` never dispatches (report-only) → read-only RBAC,
43
+ * regardless of what the rule table's targets could otherwise do.
44
+ * - `dial: "reconcile"` only free-runs a read-only target (OPS014 refuses a
45
+ * mutating dispatch under reconcile in v1) → read-only RBAC.
46
+ * - `dial: "apply"` free-runs read-only and mutating targets → RBAC gains
47
+ * create/update/patch, never delete.
48
+ * - A `dispatchTargets` entry that itself classifies `destructive` is
49
+ * refused outright, at construction — OPS014 already refuses a
50
+ * destructive `run()` target under any dial in v1 (a converge tick runs
51
+ * unattended, and a destructive dispatch needs a person's approval before
52
+ * it is attempted, not a gate read after the fact), so a `destructive` target
53
+ * reaching this composite is either a config bypassing that build check
54
+ * or a target `OperatorStack` should never grant permission toward.
55
+ * `never delete`, unconditionally, in v1 — no verb class here ever grants
56
+ * `delete`/`deletecollection`, and no rule ever uses `"*"`.
57
+ *
58
+ * Each hosted ConvergeOp gets its own ServiceAccount + Role + RoleBinding
59
+ * (never one shared identity across differently-scoped loops) — a read-only
60
+ * loop and a mutating loop sharing a Namespace get RBAC as different as
61
+ * their own dials allow, never the union.
62
+ *
63
+ * ## Layering
64
+ *
65
+ * `OperatorStackConvergeHost` restates the handful of `ConvergeOp` fields
66
+ * this composite actually needs (`name`, `schedule`, `env`, `dial`)
67
+ * structurally rather than importing `ConvergeOpConfig`: the CronJob is
68
+ * built from a host description, not from an Op config, and a caller that
69
+ * has neither should still be able to describe one. `dispatchTargets` takes
70
+ * plain `OpConfig`-shaped values from `@intentius/chant/op` (a dependency
71
+ * this lexicon already has via the `@intentius/chant` peer dependency),
72
+ * the same type `classifyOpVerbClass` itself takes.
73
+ */
74
+ import { Composite, mergeDefaults } from "@intentius/chant";
75
+ import { classifyOpVerbClass } from "@intentius/chant/op";
76
+ import { Namespace, CronJob, ServiceAccount, Role, RoleBinding } from "../generated/index.js";
77
+ import { validateCronJobSchedule } from "./cron-schedule.js";
78
+ // ── RBAC derivation ──────────────────────────────────────────────────────
79
+ /** Default RBAC resource kinds — the common workload surface a converge tick observes/acts on. Deliberately excludes `secrets`: a host that needs secret access opts in explicitly via its own `resources`. */
80
+ export const DEFAULT_RESOURCE_RULES = [
81
+ { apiGroups: [""], resources: ["pods", "services", "configmaps", "persistentvolumeclaims", "events"] },
82
+ { apiGroups: ["apps"], resources: ["deployments", "statefulsets", "daemonsets", "replicasets"] },
83
+ { apiGroups: ["batch"], resources: ["jobs", "cronjobs"] },
84
+ ];
85
+ const READ_VERBS = ["get", "list", "watch"];
86
+ const MUTATE_VERBS = [...READ_VERBS, "create", "update", "patch"];
87
+ /** RBAC verbs for a derived class. Never returns `delete`/`deletecollection`/`"*"` — v1 has no path to a destructive dispatch (see this module's RBAC derivation doc). */
88
+ function rbacVerbsFor(verbClass) {
89
+ return verbClass === "mutating" ? MUTATE_VERBS : READ_VERBS;
90
+ }
91
+ /**
92
+ * Whether `dial` ever actually free-runs a dispatch classified `verbClass` —
93
+ * restates `convergeTick`'s own `verbClassAllowedToDispatch`
94
+ * (`packages/core/src/op/activities/converge.ts`) so the RBAC ceiling
95
+ * this composite grants matches the ceiling the tick itself enforces at
96
+ * runtime, without importing across the lexicon boundary (see this module's
97
+ * Layering doc).
98
+ */
99
+ function dialAllowsVerbClass(dial, verbClass) {
100
+ if (verbClass === "read-only")
101
+ return true;
102
+ if (verbClass === "mutating")
103
+ return dial === "apply";
104
+ return false; // destructive: never free-run under any dial in v1.
105
+ }
106
+ /**
107
+ * Derive the highest verb class a host's ServiceAccount actually needs:
108
+ * the max, across `dispatchTargets`, of each target's own class — but only
109
+ * counting a target `dial` could ever actually dispatch (one it can't just
110
+ * gets reported, per `OPS014`/`convergeTick`, and needs no elevated grant).
111
+ * A `dispatchTargets` entry that classifies `destructive` is refused
112
+ * outright rather than silently ignored — see this module's RBAC
113
+ * derivation doc on why a destructive target reaching this composite is
114
+ * itself a refusal, not a no-op.
115
+ */
116
+ export function deriveHostVerbClass(hostName, dial, dispatchTargets) {
117
+ let effective = "read-only";
118
+ for (const target of dispatchTargets) {
119
+ const verbClass = classifyOpVerbClass(target);
120
+ if (verbClass === "destructive") {
121
+ throw new Error(`OperatorStack host "${hostName}": a dispatchTargets entry classifies as destructive — ConvergeOp v1 refuses a destructive run() target under any dial ` +
122
+ `(OPS014; a converge tick runs unattended, and a destructive dispatch needs a person's approval before it is attempted, not a gate the tick reads after it has already committed to the run). ` +
123
+ `Remove it from dispatchTargets, and run the destructive op yourself through its gate.`);
124
+ }
125
+ if (!dialAllowsVerbClass(dial, verbClass))
126
+ continue; // dial refuses this dispatch — reported, not run; needs no elevated RBAC
127
+ if (verbClass === "mutating")
128
+ effective = "mutating";
129
+ }
130
+ return effective;
131
+ }
132
+ // ── Composite ────────────────────────────────────────────────────────────
133
+ /**
134
+ * Create an OperatorStack composite — a Namespace hosting one CronJob (+
135
+ * least-privilege ServiceAccount/Role/RoleBinding) per ConvergeOp, the
136
+ * in-cluster declared estate for the operating loop.
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * import { OperatorStack } from "@intentius/chant-lexicon-k8s";
141
+ *
142
+ * const stack = OperatorStack({
143
+ * name: "chant-operator",
144
+ * image: "ghcr.io/intentius/chant:0.49.0",
145
+ * converge: [
146
+ * { name: "fountain-observe", schedule: "*\/10 * * * *", env: "staging", dial: "observe" },
147
+ * { name: "fountain-converge", schedule: "*\/10 * * * *", env: "staging", dial: "apply", dispatchTargets: [fountainApplyOp] },
148
+ * ],
149
+ * });
150
+ * ```
151
+ */
152
+ export const OperatorStack = Composite((props) => {
153
+ const { name, namespace: namespaceName = name, image, converge, defaultResources = DEFAULT_RESOURCE_RULES, successfulJobsHistoryLimit = 3, failedJobsHistoryLimit = 1, labels: extraLabels = {}, defaults: defs, } = props;
154
+ if (converge.length === 0) {
155
+ throw new Error(`OperatorStack "${name}": at least one ConvergeOp to host is required — a namespace with no CronJob has nothing to converge.`);
156
+ }
157
+ const seen = new Set();
158
+ for (const host of converge) {
159
+ if (!host.name || host.name.trim().length === 0) {
160
+ throw new Error(`OperatorStack "${name}": every hosted ConvergeOp needs a non-empty name.`);
161
+ }
162
+ if (seen.has(host.name)) {
163
+ throw new Error(`OperatorStack "${name}": duplicate hosted ConvergeOp name "${host.name}" — CronJob/ServiceAccount names would collide.`);
164
+ }
165
+ seen.add(host.name);
166
+ validateCronJobSchedule(`OperatorStack "${name}", host "${host.name}"`, host.schedule);
167
+ }
168
+ if (!image || image.trim().length === 0) {
169
+ throw new Error(`OperatorStack "${name}": image is required — the CronJob has nothing to run.`);
170
+ }
171
+ const commonLabels = {
172
+ "app.kubernetes.io/name": name,
173
+ "app.kubernetes.io/managed-by": "chant",
174
+ "app.kubernetes.io/component": "operator",
175
+ ...extraLabels,
176
+ };
177
+ const namespace = new Namespace(mergeDefaults({
178
+ metadata: {
179
+ name: namespaceName,
180
+ labels: { ...commonLabels, "app.kubernetes.io/component": "namespace" },
181
+ },
182
+ }, defs?.namespace));
183
+ const result = { namespace };
184
+ for (const host of converge) {
185
+ const dial = host.dial ?? "observe";
186
+ const verbClass = deriveHostVerbClass(host.name, dial, host.dispatchTargets ?? []);
187
+ const resourceRules = host.resources ?? defaultResources;
188
+ const verbs = rbacVerbsFor(verbClass);
189
+ const saName = `${host.name}-sa`;
190
+ const roleName = `${host.name}-role`;
191
+ const bindingName = `${host.name}-binding`;
192
+ const hostLabels = {
193
+ ...commonLabels,
194
+ "app.kubernetes.io/instance": host.name,
195
+ };
196
+ const serviceAccount = new ServiceAccount(mergeDefaults({
197
+ metadata: {
198
+ name: saName,
199
+ namespace: namespaceName,
200
+ labels: hostLabels,
201
+ },
202
+ }, defs?.serviceAccount));
203
+ result[`serviceAccount_${host.name}`] = serviceAccount;
204
+ const role = new Role(mergeDefaults({
205
+ metadata: {
206
+ name: roleName,
207
+ namespace: namespaceName,
208
+ labels: { ...hostLabels, "app.kubernetes.io/component": "rbac" },
209
+ },
210
+ rules: resourceRules.map((r) => ({ apiGroups: r.apiGroups, resources: r.resources, verbs })),
211
+ }, defs?.role));
212
+ result[`role_${host.name}`] = role;
213
+ const roleBinding = new RoleBinding(mergeDefaults({
214
+ metadata: {
215
+ name: bindingName,
216
+ namespace: namespaceName,
217
+ labels: { ...hostLabels, "app.kubernetes.io/component": "rbac" },
218
+ },
219
+ roleRef: {
220
+ apiGroup: "rbac.authorization.k8s.io",
221
+ kind: "Role",
222
+ name: roleName,
223
+ },
224
+ subjects: [
225
+ { kind: "ServiceAccount", name: saName, namespace: namespaceName },
226
+ ],
227
+ }, defs?.roleBinding));
228
+ result[`roleBinding_${host.name}`] = roleBinding;
229
+ const command = host.command ?? ["chant", "run", host.name];
230
+ const cronJob = new CronJob(mergeDefaults({
231
+ metadata: {
232
+ name: host.name,
233
+ namespace: namespaceName,
234
+ labels: { ...hostLabels, "app.kubernetes.io/component": "converge-tick" },
235
+ },
236
+ spec: {
237
+ schedule: host.schedule,
238
+ concurrencyPolicy: "Forbid",
239
+ successfulJobsHistoryLimit,
240
+ failedJobsHistoryLimit,
241
+ jobTemplate: {
242
+ spec: {
243
+ template: {
244
+ metadata: { labels: { ...hostLabels, "app.kubernetes.io/component": "converge-tick" } },
245
+ spec: {
246
+ serviceAccountName: saName,
247
+ restartPolicy: "OnFailure",
248
+ containers: [
249
+ {
250
+ name: host.name,
251
+ image,
252
+ command,
253
+ env: [
254
+ { name: "CHANT_CONVERGE_ENV", value: host.env },
255
+ { name: "CHANT_CONVERGE_DIAL", value: dial },
256
+ ],
257
+ },
258
+ ],
259
+ },
260
+ },
261
+ },
262
+ },
263
+ },
264
+ }, defs?.cronJob));
265
+ result[`cronJob_${host.name}`] = cronJob;
266
+ }
267
+ return result;
268
+ }, "OperatorStack");
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The Kubernetes resources an OpenTelemetry Collector agent needs, shared by
3
+ * `OtelCollector` and `GkeOtelCollector`: a DaemonSet running the collector
4
+ * with its config mounted from a ConfigMap, and a ServiceAccount bound to a
5
+ * ClusterRole with the rules the caller works out from its config (see
6
+ * `agentClusterRules` in `otel-collector-rbac.ts`).
7
+ *
8
+ * The caller renders the collector config (with the otel lexicon's
9
+ * `collectorYaml`) and says which ports the container exposes. Everything
10
+ * platform-specific, such as a Workload Identity annotation or probes, comes
11
+ * in as an option, so each composite keeps its own output.
12
+ *
13
+ * `OtelCollectorGateway` runs the same container and ConfigMap
14
+ * (`collectorContainer`, `collectorConfigMap`) in a Deployment.
15
+ */
16
+ import { mergeDefaults } from "@intentius/chant";
17
+ import { DaemonSet, ServiceAccount, ClusterRole, ClusterRoleBinding, ConfigMap } from "../generated/index.js";
18
+ import { nodeVolumeMounts, nodeVolumes } from "./otel-collector-node.js";
19
+ export function collectorAgentResources(opts) {
20
+ const { name, namespace, commonLabels, extraLabels, defaults: defs } = opts;
21
+ const saName = `${name}-sa`;
22
+ const clusterRoleName = `${name}-role`;
23
+ const bindingName = `${name}-binding`;
24
+ const configMapName = `${name}-config`;
25
+ const access = opts.nodeAccess;
26
+ const asRoot = access?.readsLogs === true && opts.logAccess === "root";
27
+ const baseContainer = collectorContainer({
28
+ ...opts,
29
+ env: access?.env,
30
+ extraVolumeMounts: access ? nodeVolumeMounts(access.mounts) : undefined,
31
+ });
32
+ const container = asRoot ? runAsRoot(baseContainer) : baseContainer;
33
+ const podSecurity = access?.readsLogs && !asRoot ? { securityContext: { supplementalGroups: [0] } } : {};
34
+ const daemonSet = new DaemonSet(mergeDefaults({
35
+ metadata: {
36
+ name,
37
+ namespace,
38
+ labels: { ...commonLabels, "app.kubernetes.io/component": "agent" },
39
+ ...(opts.workloadAnnotations ? { annotations: opts.workloadAnnotations } : {}),
40
+ },
41
+ spec: {
42
+ selector: { matchLabels: { "app.kubernetes.io/name": name } },
43
+ template: {
44
+ metadata: { labels: { "app.kubernetes.io/name": name, ...extraLabels } },
45
+ spec: {
46
+ serviceAccountName: saName,
47
+ containers: [container],
48
+ volumes: [
49
+ { name: "config", configMap: { name: configMapName } },
50
+ ...(access ? nodeVolumes(access.mounts) : []),
51
+ ],
52
+ tolerations: [{ operator: "Exists" }],
53
+ ...podSecurity,
54
+ },
55
+ },
56
+ },
57
+ }, defs?.daemonSet));
58
+ const serviceAccount = new ServiceAccount(mergeDefaults({
59
+ metadata: {
60
+ name: saName,
61
+ namespace,
62
+ labels: { ...commonLabels, "app.kubernetes.io/component": "agent" },
63
+ ...(opts.serviceAccountAnnotations ? { annotations: opts.serviceAccountAnnotations } : {}),
64
+ },
65
+ }, defs?.serviceAccount));
66
+ const clusterRole = new ClusterRole(mergeDefaults({
67
+ metadata: {
68
+ name: clusterRoleName,
69
+ labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
70
+ },
71
+ rules: opts.clusterRules,
72
+ }, defs?.clusterRole));
73
+ const clusterRoleBinding = new ClusterRoleBinding(mergeDefaults({
74
+ metadata: {
75
+ name: bindingName,
76
+ labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
77
+ },
78
+ roleRef: {
79
+ apiGroup: "rbac.authorization.k8s.io",
80
+ kind: "ClusterRole",
81
+ name: clusterRoleName,
82
+ },
83
+ subjects: [
84
+ {
85
+ kind: "ServiceAccount",
86
+ name: saName,
87
+ namespace,
88
+ },
89
+ ],
90
+ }, defs?.clusterRoleBinding));
91
+ const configMap = collectorConfigMap(opts, configMapName, opts.configMapAnnotations);
92
+ return { daemonSet, serviceAccount, clusterRole, clusterRoleBinding, configMap };
93
+ }
94
+ /**
95
+ * The collector container: the image, the config file argument, the ports,
96
+ * requests and limits, the config volume mount and a non-root security
97
+ * context, with `containerExtra` laid over the top. The agent DaemonSet and
98
+ * the gateway Deployment run the same container.
99
+ */
100
+ export function collectorContainer(opts) {
101
+ return {
102
+ name: opts.name,
103
+ image: opts.image,
104
+ args: [`--config=${opts.configDir}/config.yaml`],
105
+ ...(opts.env?.length ? { env: opts.env } : {}),
106
+ ports: opts.ports,
107
+ resources: {
108
+ requests: { cpu: opts.cpuRequest, memory: opts.memoryRequest },
109
+ limits: { cpu: opts.cpuLimit, memory: opts.memoryLimit },
110
+ },
111
+ volumeMounts: [
112
+ { name: "config", mountPath: opts.configDir, readOnly: true },
113
+ ...(opts.extraVolumeMounts ?? []),
114
+ ],
115
+ securityContext: {
116
+ runAsNonRoot: true,
117
+ runAsUser: 10001,
118
+ readOnlyRootFilesystem: true,
119
+ allowPrivilegeEscalation: false,
120
+ },
121
+ ...opts.containerExtra,
122
+ };
123
+ }
124
+ /** The container run as user 0, for `logAccess: "root"`; the rest of its security context is kept. */
125
+ function runAsRoot(container) {
126
+ const sc = (container.securityContext ?? {});
127
+ return { ...container, securityContext: { ...sc, runAsNonRoot: false, runAsUser: 0 } };
128
+ }
129
+ /** The ConfigMap holding the rendered collector config under `config.yaml`. */
130
+ export function collectorConfigMap(opts, configMapName, annotations) {
131
+ return new ConfigMap(mergeDefaults({
132
+ metadata: {
133
+ name: configMapName,
134
+ namespace: opts.namespace,
135
+ labels: { ...opts.commonLabels, "app.kubernetes.io/component": "config" },
136
+ ...(annotations ? { annotations } : {}),
137
+ },
138
+ data: {
139
+ "config.yaml": opts.configYaml,
140
+ },
141
+ }, opts.defaults?.configMap));
142
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * OtelCollectorGateway composite: an OpenTelemetry Collector gateway, the
3
+ * central tier that per-node agents export to.
4
+ *
5
+ * A Deployment runs the collector with `replicas` pods, its config mounted
6
+ * from a ConfigMap. A ClusterIP Service takes the agents' OTLP traffic, and a
7
+ * headless Service lists the individual pods, which is what the agents'
8
+ * `loadbalancing` exporter resolves when every span of a trace has to reach
9
+ * the same replica (tail sampling, spanmetrics). The config is declared with
10
+ * the otel lexicon; ports and probes are read back from it, as `OtelCollector`
11
+ * does.
12
+ *
13
+ * `gatewayExporter()` builds the agent side: an exporter whose endpoint comes
14
+ * from this composite's Services, so renaming the gateway or moving it to
15
+ * another namespace moves the agents with it.
16
+ */
17
+ import { Composite, mergeDefaults } from "@intentius/chant";
18
+ import { otlpCollector, LoadBalancingExporter, OtlpExporter, OtlpHttpExporter, COLLECTOR_IMAGE, } from "@intentius/chant-lexicon-otel";
19
+ import { Deployment, Service, ServiceAccount, ClusterRole, ClusterRoleBinding, PodDisruptionBudget, } from "../generated/index.js";
20
+ import { collectorConfigMap, collectorContainer } from "./otel-collector-agent.js";
21
+ import { namespacedRoles } from "./otel-collector-rbac.js";
22
+ import { collectorRuntime, leaderElectorLeaseNamespaces, markGatewayExporter, OTEL_COLLECTOR_ANNOTATIONS, } from "./otel-collector-shape.js";
23
+ /**
24
+ * Create an OtelCollectorGateway composite. Returns a Deployment, a ClusterIP
25
+ * Service, a headless Service, a ServiceAccount and a ConfigMap, a
26
+ * PodDisruptionBudget when there is more than one replica, a ClusterRole
27
+ * and binding when `clusterRules` is set, and a Role and binding for Leases
28
+ * when the config enables a `k8s_leader_elector` extension.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { OtelCollector, OtelCollectorGateway, gatewayExporter } from "@intentius/chant-lexicon-k8s";
33
+ *
34
+ * export const gateway = OtelCollectorGateway({ replicas: 3 });
35
+ * export const agent = OtelCollector({ exporters: [gatewayExporter(gateway, { loadBalance: true })], signals: ["traces"] });
36
+ * ```
37
+ */
38
+ export const OtelCollectorGateway = Composite((props) => {
39
+ const { name = "otel-gateway", namespace = "observability", replicas = 2, image = COLLECTOR_IMAGE, labels: extraLabels = {}, cpuRequest = "200m", memoryRequest = "512Mi", cpuLimit = "1", memoryLimit = "1Gi", defaults: defs, } = props;
40
+ const commonLabels = {
41
+ "app.kubernetes.io/name": name,
42
+ "app.kubernetes.io/managed-by": "chant",
43
+ ...extraLabels,
44
+ };
45
+ const gatewayLabels = { ...commonLabels, "app.kubernetes.io/component": "gateway" };
46
+ const saName = `${name}-sa`;
47
+ const configMapName = `${name}-config`;
48
+ const entities = props.config ? [...props.config] : otlpCollector({ exporters: props.exporters, signals: props.signals });
49
+ const runtime = collectorRuntime(entities);
50
+ const container = collectorContainer({
51
+ name,
52
+ image,
53
+ configDir: runtime.configDir,
54
+ ports: runtime.containerPorts,
55
+ cpuRequest,
56
+ memoryRequest,
57
+ cpuLimit,
58
+ memoryLimit,
59
+ containerExtra: runtime.containerExtra,
60
+ });
61
+ const deployment = new Deployment(mergeDefaults({
62
+ metadata: {
63
+ name,
64
+ namespace,
65
+ labels: gatewayLabels,
66
+ annotations: {
67
+ [OTEL_COLLECTOR_ANNOTATIONS.role]: "gateway",
68
+ [OTEL_COLLECTOR_ANNOTATIONS.config]: configMapName,
69
+ },
70
+ },
71
+ spec: {
72
+ replicas,
73
+ selector: { matchLabels: { "app.kubernetes.io/name": name } },
74
+ template: {
75
+ metadata: { labels: { "app.kubernetes.io/name": name, "app.kubernetes.io/component": "gateway", ...extraLabels } },
76
+ spec: {
77
+ serviceAccountName: saName,
78
+ containers: [container],
79
+ volumes: [{ name: "config", configMap: { name: configMapName } }],
80
+ },
81
+ },
82
+ },
83
+ }, defs?.deployment));
84
+ const servicePorts = runtime.servicePorts.map((p) => ({ name: p.name, port: p.port, targetPort: p.name, protocol: p.protocol ?? "TCP" }));
85
+ const service = new Service(mergeDefaults({
86
+ metadata: { name, namespace, labels: gatewayLabels },
87
+ spec: {
88
+ type: "ClusterIP",
89
+ selector: { "app.kubernetes.io/name": name },
90
+ ports: servicePorts,
91
+ },
92
+ }, defs?.service));
93
+ // A headless Service's DNS name and Endpoints list every ready pod, which is
94
+ // what the agents' loadbalancing exporter hashes traces across.
95
+ const headlessService = new Service(mergeDefaults({
96
+ metadata: { name: `${name}-headless`, namespace, labels: gatewayLabels },
97
+ spec: {
98
+ clusterIP: "None",
99
+ selector: { "app.kubernetes.io/name": name },
100
+ ports: servicePorts,
101
+ },
102
+ }, defs?.headlessService));
103
+ const serviceAccount = new ServiceAccount(mergeDefaults({
104
+ metadata: { name: saName, namespace, labels: gatewayLabels },
105
+ }, defs?.serviceAccount));
106
+ const configMap = collectorConfigMap({ namespace, commonLabels, configYaml: runtime.configYaml, defaults: defs }, configMapName, {
107
+ [OTEL_COLLECTOR_ANNOTATIONS.role]: "gateway",
108
+ [OTEL_COLLECTOR_ANNOTATIONS.workload]: `Deployment/${name}`,
109
+ });
110
+ const pdb = replicas > 1
111
+ ? {
112
+ podDisruptionBudget: new PodDisruptionBudget(mergeDefaults({
113
+ metadata: { name, namespace, labels: gatewayLabels },
114
+ spec: { maxUnavailable: 1, selector: { matchLabels: { "app.kubernetes.io/name": name } } },
115
+ }, defs?.podDisruptionBudget)),
116
+ }
117
+ : {};
118
+ const rbac = props.clusterRules?.length
119
+ ? {
120
+ clusterRole: new ClusterRole(mergeDefaults({
121
+ metadata: { name: `${name}-role`, labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" } },
122
+ rules: props.clusterRules,
123
+ }, defs?.clusterRole)),
124
+ clusterRoleBinding: new ClusterRoleBinding(mergeDefaults({
125
+ metadata: { name: `${name}-binding`, labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" } },
126
+ roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "ClusterRole", name: `${name}-role` },
127
+ subjects: [{ kind: "ServiceAccount", name: saName, namespace }],
128
+ }, defs?.clusterRoleBinding)),
129
+ }
130
+ : {};
131
+ // A k8s_leader_elector extension takes a Lease in its lease_namespace, so
132
+ // the gateway's ServiceAccount gets the access the extension's README
133
+ // suggests (collector-contrib v0.130.0, extension/k8sleaderelector), with
134
+ // one Role and RoleBinding in each namespace the electors use.
135
+ const leaseRbac = namespacedRoles({
136
+ key: "lease",
137
+ name: `${name}-leases`,
138
+ namespaces: leaderElectorLeaseNamespaces(runtime.built),
139
+ rules: [
140
+ {
141
+ apiGroups: ["coordination.k8s.io"],
142
+ resources: ["leases"],
143
+ verbs: ["get", "list", "watch", "create", "update", "patch", "delete"],
144
+ },
145
+ ],
146
+ labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
147
+ serviceAccount: { name: saName, namespace },
148
+ roleDefaults: defs?.leaseRole,
149
+ roleBindingDefaults: defs?.leaseRoleBinding,
150
+ });
151
+ return { deployment, service, headlessService, serviceAccount, configMap, ...pdb, ...rbac, ...leaseRbac };
152
+ }, "OtelCollectorGateway");
153
+ function serviceMeta(service) {
154
+ const p = service.props;
155
+ return { name: p.metadata.name, namespace: p.metadata.namespace, ports: p.spec.ports ?? [] };
156
+ }
157
+ /**
158
+ * The exporter an agent uses to send to a gateway, built from the gateway's
159
+ * own declaration: its Services' names, its namespace, and the port its
160
+ * config listens on.
161
+ *
162
+ * By default an `otlp` exporter at `<service>.<namespace>.svc:<port>`. With
163
+ * `protocol: "http"`, an `otlphttp` exporter at
164
+ * `http://<service>.<namespace>.svc:<port>` (`https://` when `tls` is not
165
+ * `insecure`), sending to the gateway's OTLP HTTP port. With
166
+ * `loadBalance`, a `loadbalancing` exporter routing by trace id to the
167
+ * headless Service, through the `k8s` resolver (`<headless>.<namespace>`) or
168
+ * the `dns` resolver (`<headless>.<namespace>.svc`). Put it in the agent's
169
+ * pipelines like any other exporter; `OtelCollector` records the link in its
170
+ * annotations and adds the RBAC the `k8s` resolver needs.
171
+ */
172
+ export function gatewayExporter(gateway, options = {}) {
173
+ const { name = "gateway", protocol = "grpc", loadBalance = false, resolver = "k8s", routingKey = "traceID", tls = { insecure: true } } = options;
174
+ const portName = options.port ?? (protocol === "http" ? "otlp-http" : "otlp-grpc");
175
+ if (protocol === "http" && loadBalance) {
176
+ throw new Error(`gatewayExporter: the loadbalancing exporter sends OTLP over gRPC only; ` +
177
+ `use protocol "grpc" with loadBalance, or drop loadBalance to send OTLP over HTTP to the gateway's Service`);
178
+ }
179
+ const deploymentName = gateway.deployment.props.metadata.name;
180
+ const target = serviceMeta(loadBalance ? gateway.headlessService : gateway.service);
181
+ const port = target.ports.find((p) => p.name === portName)?.port;
182
+ if (port === undefined) {
183
+ const names = target.ports.map((p) => p.name).join(", ") || "none";
184
+ throw new Error(`gatewayExporter: Service ${target.namespace}/${target.name} has no port named "${portName}" (it has: ${names}); ` +
185
+ `give the gateway an OTLP ${protocol === "http" ? "HTTP" : "gRPC"} receiver or pass the port name`);
186
+ }
187
+ const routing = loadBalance ? "loadbalancing" : "service";
188
+ const exporter = loadBalance
189
+ ? new LoadBalancingExporter({
190
+ name,
191
+ routing_key: routingKey,
192
+ protocol: { otlp: { tls } },
193
+ resolver: resolver === "dns"
194
+ ? { dns: { hostname: `${target.name}.${target.namespace}.svc`, port: String(port) } }
195
+ : { k8s: { service: `${target.name}.${target.namespace}`, ports: [port] } },
196
+ })
197
+ : protocol === "http"
198
+ ? otlpHttpExporter(name, `${target.name}.${target.namespace}.svc:${port}`, tls)
199
+ : new OtlpExporter({ name, endpoint: `${target.name}.${target.namespace}.svc:${port}`, tls });
200
+ markGatewayExporter(exporter, { name: deploymentName, namespace: target.namespace, routing });
201
+ return exporter;
202
+ }
203
+ /**
204
+ * An `otlphttp` exporter to `host:port`. Its endpoint is a URL, so TLS is the
205
+ * scheme: `insecure` gives `http://`, anything else `https://` with the
206
+ * remaining TLS settings (a CA, a client certificate) kept.
207
+ */
208
+ function otlpHttpExporter(name, hostPort, tls) {
209
+ const { insecure, ...rest } = tls;
210
+ const scheme = insecure ? "http" : "https";
211
+ return new OtlpHttpExporter({
212
+ name,
213
+ endpoint: `${scheme}://${hostPort}`,
214
+ ...(Object.keys(rest).length ? { tls: rest } : {}),
215
+ });
216
+ }