@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.
- package/dist/actions/apps.js +18 -0
- package/dist/actions/batch.js +6 -0
- package/dist/actions/core.js +48 -0
- package/dist/actions/index.js +13 -0
- package/dist/actions/networking.js +11 -0
- package/dist/actions/rbac.js +8 -0
- package/dist/api/classify.js +123 -0
- package/dist/api/connect.js +62 -0
- package/dist/api/fake-cluster.js +175 -0
- package/dist/api/operation-surface.d.ts.map +1 -1
- package/dist/api/operation-surface.js +109 -0
- package/dist/api/owner-chain.js +100 -0
- package/dist/api/sweep-types.js +35 -0
- package/dist/behaviour-kinds.js +82 -0
- package/dist/codegen/docs-cli.js +2 -0
- package/dist/codegen/docs.js +146 -0
- package/dist/codegen/generate-cli.js +30 -0
- package/dist/codegen/generate-lexicon.js +74 -0
- package/dist/codegen/generate-list-map-keys.js +88 -0
- package/dist/codegen/generate-operations.js +53 -0
- package/dist/codegen/generate-typescript.js +124 -0
- package/dist/codegen/generate.js +173 -0
- package/dist/codegen/naming.js +174 -0
- package/dist/codegen/package.js +36 -0
- package/dist/codegen/patches.js +88 -0
- package/dist/codegen/typecheck.js +4 -0
- package/dist/codegen/versions.js +41 -0
- package/dist/components/argo-app.js +51 -0
- package/dist/components/builders.js +22 -0
- package/dist/components/capability-plugin.js +31 -0
- package/dist/components/flux-reconcile.js +61 -0
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +18 -0
- package/dist/components/kubectl-apply.d.ts +83 -1
- package/dist/components/kubectl-apply.d.ts.map +1 -1
- package/dist/components/kubectl-apply.js +274 -0
- package/dist/components/kustomize-apply.js +25 -0
- package/dist/composites/adot-collector.js +184 -0
- package/dist/composites/agic-ingress.js +85 -0
- package/dist/composites/aks-external-dns-agent.js +146 -0
- package/dist/composites/alb-ingress.js +86 -0
- package/dist/composites/argo-app.js +199 -0
- package/dist/composites/autoscaled-service.js +174 -0
- package/dist/composites/azure-disk-storage-class.js +47 -0
- package/dist/composites/azure-file-storage-class.js +48 -0
- package/dist/composites/azure-monitor-collector.js +176 -0
- package/dist/composites/batch-job.js +139 -0
- package/dist/composites/catalog.js +4110 -0
- package/dist/composites/cockroachdb-cluster.js +371 -0
- package/dist/composites/cockroachdb-region-stack.js +369 -0
- package/dist/composites/config-connector-context.js +40 -0
- package/dist/composites/configured-app.js +135 -0
- package/dist/composites/cron-schedule.js +38 -0
- package/dist/composites/cron-workload.js +109 -0
- package/dist/composites/ebs-storage-class.js +54 -0
- package/dist/composites/efs-storage-class.js +46 -0
- package/dist/composites/external-dns-agent.js +122 -0
- package/dist/composites/filestore-storage-class.js +49 -0
- package/dist/composites/fluent-bit-agent.js +157 -0
- package/dist/composites/flux-app.js +156 -0
- package/dist/composites/gce-ingress.js +83 -0
- package/dist/composites/gce-pd-storage-class.js +49 -0
- package/dist/composites/gke-external-dns-agent.js +129 -0
- package/dist/composites/gke-fluent-bit-agent.js +165 -0
- package/dist/composites/gke-gateway.js +91 -0
- package/dist/composites/gke-otel-collector.js +94 -0
- package/dist/composites/index.js +51 -0
- package/dist/composites/inference-service.js +76 -0
- package/dist/composites/irsa-service-account.js +77 -0
- package/dist/composites/member-defaults.js +14 -0
- package/dist/composites/metrics-server.js +181 -0
- package/dist/composites/model.js +92 -0
- package/dist/composites/monitored-service.js +152 -0
- package/dist/composites/namespace-env.js +114 -0
- package/dist/composites/network-isolated-app.js +124 -0
- package/dist/composites/node-agent.js +149 -0
- package/dist/composites/operator-stack.js +268 -0
- package/dist/composites/otel-collector-agent.js +142 -0
- package/dist/composites/otel-collector-gateway.js +216 -0
- package/dist/composites/otel-collector-node.js +176 -0
- package/dist/composites/otel-collector-rbac.js +115 -0
- package/dist/composites/otel-collector-shape.js +128 -0
- package/dist/composites/otel-collector.js +115 -0
- package/dist/composites/otel-operator-collector.js +123 -0
- package/dist/composites/ray-cluster.js +378 -0
- package/dist/composites/ray-job.js +136 -0
- package/dist/composites/ray-service.js +178 -0
- package/dist/composites/secure-ingress.js +100 -0
- package/dist/composites/security-context.js +1 -0
- package/dist/composites/sidecar-app.js +105 -0
- package/dist/composites/stateful-app.js +111 -0
- package/dist/composites/vllm-serving-runtime.js +114 -0
- package/dist/composites/web-app.js +157 -0
- package/dist/composites/worker-pool.js +167 -0
- package/dist/composites/workload-identity-sa.js +81 -0
- package/dist/composites/workload-identity-service-account.js +77 -0
- package/dist/config-schema.js +72 -0
- package/dist/config.js +59 -0
- package/dist/coverage.js +27 -0
- package/dist/crd/crd-sources.js +615 -0
- package/dist/crd/loader.js +181 -0
- package/dist/crd/oci-chart.js +144 -0
- package/dist/crd/parser.js +382 -0
- package/dist/crd/project-codegen.js +273 -0
- package/dist/crd/types.js +7 -0
- package/dist/deep-observe-hooks.js +306 -0
- package/dist/deep-observe.js +234 -0
- package/dist/default-labels.js +87 -0
- package/dist/describe-resources.js +699 -0
- package/dist/describe-stack-status.js +99 -0
- package/dist/detect.js +11 -0
- package/dist/effect-receipt-row.js +205 -0
- package/dist/export-resources.js +39 -0
- package/dist/generated/index.js +1092 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/group-namespace.js +96 -0
- package/dist/import/embedded.js +197 -0
- package/dist/import/generator.js +288 -0
- package/dist/import/live-export.js +85 -0
- package/dist/import/parser.js +204 -0
- package/dist/import/testdata/embedded/fixtures.js +60 -0
- package/dist/include/entity.js +78 -0
- package/dist/include/resolve.js +170 -0
- package/dist/index.js +59 -0
- package/dist/integrity.json +3 -3
- package/dist/kube/apply.js +90 -0
- package/dist/kube/connect.js +46 -0
- package/dist/kube/delete.js +138 -0
- package/dist/kube/describe.js +169 -0
- package/dist/kube/events.js +132 -0
- package/dist/kube/flags.js +91 -0
- package/dist/kube/get.js +159 -0
- package/dist/kube/group.js +123 -0
- package/dist/kube/logs.js +82 -0
- package/dist/kube/project.js +94 -0
- package/dist/kube/render.js +227 -0
- package/dist/kube/source.js +122 -0
- package/dist/kube/target.js +63 -0
- package/dist/kube/testing.js +28 -0
- package/dist/kube/top.js +99 -0
- package/dist/kube/verdict.js +72 -0
- package/dist/kube/wait.js +129 -0
- package/dist/kustomize/render.js +73 -0
- package/dist/kustomize/rendered-entity.js +67 -0
- package/dist/kustomize/root.js +68 -0
- package/dist/lint/audit-catalog.js +61 -0
- package/dist/lint/audit-lineage.js +118 -0
- package/dist/lint/post-synth/argo-helpers.js +36 -0
- package/dist/lint/post-synth/argo002.js +31 -0
- package/dist/lint/post-synth/argo003.js +53 -0
- package/dist/lint/post-synth/argo005.js +54 -0
- package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/crd-schema-helpers.js +214 -0
- package/dist/lint/post-synth/flux002.js +56 -0
- package/dist/lint/post-synth/flux003.js +50 -0
- package/dist/lint/post-synth/index.js +104 -0
- package/dist/lint/post-synth/k8s-helpers.js +186 -0
- package/dist/lint/post-synth/monitoring-selection-helpers.js +163 -0
- package/dist/lint/post-synth/otel-placement-helpers.js +293 -0
- package/dist/lint/post-synth/sops-helpers.js +43 -0
- package/dist/lint/post-synth/wk8005.js +44 -0
- package/dist/lint/post-synth/wk8006.js +56 -0
- package/dist/lint/post-synth/wk8041.js +60 -0
- package/dist/lint/post-synth/wk8042.js +36 -0
- package/dist/lint/post-synth/wk8101.js +52 -0
- package/dist/lint/post-synth/wk8102.js +30 -0
- package/dist/lint/post-synth/wk8103.js +33 -0
- package/dist/lint/post-synth/wk8104.js +58 -0
- package/dist/lint/post-synth/wk8105.js +33 -0
- package/dist/lint/post-synth/wk8201.js +44 -0
- package/dist/lint/post-synth/wk8202.js +34 -0
- package/dist/lint/post-synth/wk8203.js +34 -0
- package/dist/lint/post-synth/wk8204.js +71 -0
- package/dist/lint/post-synth/wk8205.js +38 -0
- package/dist/lint/post-synth/wk8207.js +33 -0
- package/dist/lint/post-synth/wk8208.js +33 -0
- package/dist/lint/post-synth/wk8209.js +33 -0
- package/dist/lint/post-synth/wk8301.js +49 -0
- package/dist/lint/post-synth/wk8302.js +34 -0
- package/dist/lint/post-synth/wk8303.js +68 -0
- package/dist/lint/post-synth/wk8304.js +58 -0
- package/dist/lint/post-synth/wk8305.js +96 -0
- package/dist/lint/post-synth/wk8306.js +38 -0
- package/dist/lint/post-synth/wk8401.js +86 -0
- package/dist/lint/post-synth/wk8402.js +31 -0
- package/dist/lint/post-synth/wk8403.js +49 -0
- package/dist/lint/post-synth/wk8404.js +36 -0
- package/dist/lint/post-synth/wk8405.js +65 -0
- package/dist/lint/post-synth/wk8406.js +50 -0
- package/dist/lint/post-synth/wk8407.js +58 -0
- package/dist/lint/post-synth/wk8501.js +36 -0
- package/dist/lint/post-synth/wk8502.js +35 -0
- package/dist/lint/post-synth/wk8503.js +164 -0
- package/dist/lint/post-synth/wk8504.js +20 -0
- package/dist/lint/post-synth/wk8505.js +31 -0
- package/dist/lint/post-synth/wk8601.js +40 -0
- package/dist/lint/post-synth/wk8602.js +120 -0
- package/dist/lint/post-synth/wk8603.js +63 -0
- package/dist/lint/post-synth/wk8604.js +31 -0
- package/dist/lint/post-synth/wk8605.js +124 -0
- package/dist/lint/post-synth/wk8606.js +22 -0
- package/dist/lint/post-synth/wk8701.js +31 -0
- package/dist/lint/post-synth/wk8702.js +37 -0
- package/dist/lint/post-synth/wk8703.js +31 -0
- package/dist/lint/post-synth/wk8704.js +135 -0
- package/dist/lint/post-synth/wk8705.js +38 -0
- package/dist/lint/rules/argo-appset-single-project.js +56 -0
- package/dist/lint/rules/argo-ast.js +94 -0
- package/dist/lint/rules/argo-automated-prune.js +59 -0
- package/dist/lint/rules/flux-source-ref-pin.js +44 -0
- package/dist/lint/rules/hardcoded-namespace.js +57 -0
- package/dist/lint/rules/latest-image-tag.js +103 -0
- package/dist/lint/rules/missing-resource-limits.js +85 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +18 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +57 -0
- package/dist/manifest-entity.js +71 -0
- package/dist/manifest.json +1 -1
- package/dist/micro-time.js +49 -0
- package/dist/op/activities/apply-diff.d.ts +31 -0
- package/dist/op/activities/apply-diff.d.ts.map +1 -0
- package/dist/op/activities/apply-diff.js +79 -0
- package/dist/op/activities/argo.js +98 -0
- package/dist/op/activities/ensure-secret.js +38 -0
- package/dist/op/activities/index.js +51 -0
- package/dist/op/activities/kubectl.d.ts +64 -0
- package/dist/op/activities/kubectl.d.ts.map +1 -1
- package/dist/op/activities/kubectl.js +517 -0
- package/dist/op/activities/wait-for-ready.js +229 -0
- package/dist/op/builders.js +68 -0
- package/dist/package-cli.js +21 -0
- package/dist/patch/json-patch.js +187 -0
- package/dist/patch/rendered-patch.js +94 -0
- package/dist/plugin.js +741 -0
- package/dist/project-kinds.js +56 -0
- package/dist/receipt-store.js +216 -0
- package/dist/rules/crd-schema-helpers.ts +2 -1
- package/dist/secret-labels.js +33 -0
- package/dist/secret-store.js +116 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +578 -0
- package/dist/sops/encrypted-secret-file.js +194 -0
- package/dist/sops/entity.js +32 -0
- package/dist/spec/fetch.js +58 -0
- package/dist/spec/parse.js +622 -0
- package/dist/subscribe-changes.js +168 -0
- package/dist/teardown.js +197 -0
- package/dist/telemetry-release.js +86 -0
- package/dist/validate-cli.js +17 -0
- package/dist/validate.js +47 -0
- package/dist/variables.js +34 -0
- package/dist/whoami.js +109 -0
- package/package.json +12 -12
- package/src/api/operation-surface.ts +2 -1
- package/src/codegen/package.ts +2 -2
- package/src/components/index.ts +3 -0
- package/src/components/kubectl-apply-plan.k3d.e2e.test.ts +127 -0
- package/src/components/kubectl-apply.test.ts +262 -0
- package/src/components/kubectl-apply.ts +341 -12
- package/src/deep-observe-hooks.ts +2 -2
- package/src/import/roundtrip-build.test.ts +46 -0
- package/src/lint/post-synth/crd-schema-helpers.ts +2 -1
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
- package/src/op/activities/apply-diff.ts +87 -0
- package/src/op/activities/kubectl-plan.test.ts +224 -0
- package/src/op/activities/kubectl.ts +204 -16
- package/src/serializer.ts +2 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a collector pod needs from its node, worked out from the collector
|
|
3
|
+
* config it runs, the way `agentClusterRules` works out its RBAC (chant
|
|
4
|
+
* #3103). `OtelCollector` and `GkeOtelCollector` add the result to their
|
|
5
|
+
* DaemonSet, and WK8605 reports a workload that runs such a config without it.
|
|
6
|
+
*
|
|
7
|
+
* Only components a pipeline runs count, since the collector starts no
|
|
8
|
+
* other. A config that reads nothing from the node gets nothing, so its
|
|
9
|
+
* DaemonSet is unchanged. Each requirement cites the component's README at
|
|
10
|
+
* collector-contrib v0.130.0, the version the otel lexicon pins:
|
|
11
|
+
*
|
|
12
|
+
* - The node's name, from the downward API (`spec.nodeName`), in each
|
|
13
|
+
* variable a `k8sattributes` processor names in `filter.node_from_env_var`
|
|
14
|
+
* (processor/k8sattributesprocessor README, "Deployment scenarios": "Use
|
|
15
|
+
* the downward API to inject the node name as an environment variable"),
|
|
16
|
+
* and in each `${env:VAR}` a `kubeletstats` receiver's `node` uses, or its
|
|
17
|
+
* `endpoint` uses when the variable's name ends in `NODE_NAME`
|
|
18
|
+
* (receiver/kubeletstatsreceiver README, "Service Account Authentication
|
|
19
|
+
* Example"). An `endpoint` variable ending in `NODE_IP` or `HOST_IP` gets
|
|
20
|
+
* `status.hostIP` instead. Any other variable is the caller's to set.
|
|
21
|
+
* - The host root, read-only at a `hostmetrics` receiver's `root_path`
|
|
22
|
+
* (receiver/hostmetricsreceiver README, "Collecting host metrics from
|
|
23
|
+
* inside a container (Linux only)": bind mount the host filesystem, then
|
|
24
|
+
* configure `root_path`). Mounted with `HostToContainer` propagation so
|
|
25
|
+
* filesystems the node mounts later show up, as the upstream Helm chart's
|
|
26
|
+
* hostMetrics preset does.
|
|
27
|
+
* - The directories a `filelog` receiver's `include` patterns read, read-only
|
|
28
|
+
* at the same path (receiver/filelogreceiver README, "Configuration":
|
|
29
|
+
* `include` is a list of file glob patterns the receiver reads). The mount
|
|
30
|
+
* is the fixed directory part of each pattern, so `/var/log/pods/*\/*\/*.log`
|
|
31
|
+
* mounts `/var/log/pods`. `/var/log/containers` holds symlinks into
|
|
32
|
+
* `/var/log/pods`, so reading it mounts both. A pattern with no fixed
|
|
33
|
+
* directory below `/`, or one that would cover the config directory, is
|
|
34
|
+
* left to the caller.
|
|
35
|
+
*
|
|
36
|
+
* `readsLogs` is set when a `filelog` receiver reads a host directory: the
|
|
37
|
+
* kubelet's container logs there are owned by root, which the composites
|
|
38
|
+
* deal with (see `logAccess` on `OtelCollector`).
|
|
39
|
+
*/
|
|
40
|
+
import { canonicalTypeOf } from "@intentius/chant-lexicon-otel/model";
|
|
41
|
+
function isRecord(v) {
|
|
42
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
43
|
+
}
|
|
44
|
+
/** Component ids of one kind that some pipeline runs. */
|
|
45
|
+
function runningIds(config, kind) {
|
|
46
|
+
const ids = new Set();
|
|
47
|
+
const pipelines = isRecord(config.service) && isRecord(config.service.pipelines) ? config.service.pipelines : {};
|
|
48
|
+
for (const p of Object.values(pipelines)) {
|
|
49
|
+
const list = isRecord(p) ? p[kind] : undefined;
|
|
50
|
+
if (Array.isArray(list))
|
|
51
|
+
for (const id of list)
|
|
52
|
+
if (typeof id === "string")
|
|
53
|
+
ids.add(id);
|
|
54
|
+
}
|
|
55
|
+
return ids;
|
|
56
|
+
}
|
|
57
|
+
/** [id, settings] of each running component of `type`, under its old or new name (`kubelet_stats`). */
|
|
58
|
+
function running(config, kind, type) {
|
|
59
|
+
const ids = runningIds(config, kind);
|
|
60
|
+
const section = isRecord(config[kind]) ? config[kind] : {};
|
|
61
|
+
const componentKind = kind === "receivers" ? "receiver" : "processor";
|
|
62
|
+
return Object.entries(section)
|
|
63
|
+
.filter(([id]) => ids.has(id) && canonicalTypeOf(componentKind, id) === type)
|
|
64
|
+
.map(([id, cfg]) => [id, isRecord(cfg) ? cfg : {}]);
|
|
65
|
+
}
|
|
66
|
+
/** The variables a string reads as `${env:NAME}`. */
|
|
67
|
+
function envRefs(value) {
|
|
68
|
+
if (typeof value !== "string")
|
|
69
|
+
return [];
|
|
70
|
+
return [...value.matchAll(/\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g)].map((m) => m[1]);
|
|
71
|
+
}
|
|
72
|
+
/** The directory part of a glob before its first wildcard: `/var/log/pods/*\/x.log` is `/var/log/pods`. */
|
|
73
|
+
export function fixedDirectory(pattern) {
|
|
74
|
+
if (!pattern.startsWith("/"))
|
|
75
|
+
return undefined;
|
|
76
|
+
const parts = pattern.split("/");
|
|
77
|
+
const fixed = [];
|
|
78
|
+
for (const part of parts) {
|
|
79
|
+
if (/[*?[{]/.test(part))
|
|
80
|
+
break;
|
|
81
|
+
fixed.push(part);
|
|
82
|
+
}
|
|
83
|
+
// No wildcard at all: a file, so its directory.
|
|
84
|
+
if (fixed.length === parts.length)
|
|
85
|
+
fixed.pop();
|
|
86
|
+
const dir = fixed.join("/").replace(/\/+$/, "");
|
|
87
|
+
return dir === "" ? undefined : dir;
|
|
88
|
+
}
|
|
89
|
+
function within(path, dir) {
|
|
90
|
+
return path === dir || path.startsWith(`${dir}/`);
|
|
91
|
+
}
|
|
92
|
+
function volumeName(prefix, path) {
|
|
93
|
+
const slug = path.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
94
|
+
return `${prefix}-${slug}`.slice(0, 63).replace(/-+$/, "");
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* What a collector config needs from the node. `configDir` is where the
|
|
98
|
+
* collector's own config is mounted; a log directory covering it is skipped.
|
|
99
|
+
*/
|
|
100
|
+
export function collectorNodeAccess(config, configDir = "/etc/otel") {
|
|
101
|
+
const env = new Map();
|
|
102
|
+
const setEnv = (name, fieldPath) => {
|
|
103
|
+
if (!env.has(name))
|
|
104
|
+
env.set(name, fieldPath);
|
|
105
|
+
};
|
|
106
|
+
for (const [, p] of running(config, "processors", "k8sattributes")) {
|
|
107
|
+
const name = isRecord(p.filter) ? p.filter.node_from_env_var : undefined;
|
|
108
|
+
if (typeof name === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
109
|
+
setEnv(name, "spec.nodeName");
|
|
110
|
+
}
|
|
111
|
+
for (const [, r] of running(config, "receivers", "kubeletstats")) {
|
|
112
|
+
for (const name of envRefs(r.node))
|
|
113
|
+
setEnv(name, "spec.nodeName");
|
|
114
|
+
for (const name of envRefs(r.endpoint)) {
|
|
115
|
+
if (/(NODE|HOST)_IP$/.test(name))
|
|
116
|
+
setEnv(name, "status.hostIP");
|
|
117
|
+
else if (/NODE_NAME$/.test(name))
|
|
118
|
+
setEnv(name, "spec.nodeName");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const mounts = [];
|
|
122
|
+
const rootPaths = new Set();
|
|
123
|
+
for (const [id, r] of running(config, "receivers", "hostmetrics")) {
|
|
124
|
+
const root = typeof r.root_path === "string" ? r.root_path.replace(/\/+$/, "") : "";
|
|
125
|
+
if (!root.startsWith("/") || rootPaths.has(root))
|
|
126
|
+
continue;
|
|
127
|
+
rootPaths.add(root);
|
|
128
|
+
mounts.push({
|
|
129
|
+
name: rootPaths.size === 1 ? "hostfs" : volumeName("hostfs", root),
|
|
130
|
+
hostPath: "/",
|
|
131
|
+
mountPath: root,
|
|
132
|
+
component: id,
|
|
133
|
+
partial: true,
|
|
134
|
+
mountPropagation: "HostToContainer",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const logDirs = [];
|
|
138
|
+
for (const [id, r] of running(config, "receivers", "filelog")) {
|
|
139
|
+
const include = Array.isArray(r.include) ? r.include : [];
|
|
140
|
+
for (const pattern of include) {
|
|
141
|
+
if (typeof pattern !== "string")
|
|
142
|
+
continue;
|
|
143
|
+
const dir = fixedDirectory(pattern);
|
|
144
|
+
if (!dir || within(configDir, dir))
|
|
145
|
+
continue;
|
|
146
|
+
logDirs.push({ dir, component: id });
|
|
147
|
+
if (within(dir, "/var/log/containers"))
|
|
148
|
+
logDirs.push({ dir: "/var/log/pods", component: id });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// One mount per directory, the outermost of nested ones.
|
|
152
|
+
const kept = logDirs.filter(({ dir }, i) => !logDirs.some((o, j) => (o.dir !== dir && within(dir, o.dir)) || (o.dir === dir && j < i)));
|
|
153
|
+
for (const { dir, component } of kept) {
|
|
154
|
+
if ([...rootPaths].some((root) => within(dir, root)))
|
|
155
|
+
continue;
|
|
156
|
+
mounts.push({ name: volumeName("host", dir), hostPath: dir, mountPath: dir, component });
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
env: [...env].map(([name, fieldPath]) => ({ name, valueFrom: { fieldRef: { fieldPath } } })),
|
|
160
|
+
mounts,
|
|
161
|
+
readsLogs: kept.length > 0,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** The pod volumes for a set of mounts. */
|
|
165
|
+
export function nodeVolumes(mounts) {
|
|
166
|
+
return mounts.map((m) => ({ name: m.name, hostPath: { path: m.hostPath } }));
|
|
167
|
+
}
|
|
168
|
+
/** The container volume mounts for a set of mounts, all read-only. */
|
|
169
|
+
export function nodeVolumeMounts(mounts) {
|
|
170
|
+
return mounts.map((m) => ({
|
|
171
|
+
name: m.name,
|
|
172
|
+
mountPath: m.mountPath,
|
|
173
|
+
readOnly: true,
|
|
174
|
+
...(m.mountPropagation ? { mountPropagation: m.mountPropagation } : {}),
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RBAC the collector composites grant, worked out from the collector
|
|
3
|
+
* config they deploy.
|
|
4
|
+
*
|
|
5
|
+
* `agentClusterRules` is the ClusterRole `OtelCollector` and
|
|
6
|
+
* `GkeOtelCollector` share: what the `k8sattributes` processor needs, plus
|
|
7
|
+
* what the config's Kubernetes receivers need. Each rule cites the
|
|
8
|
+
* component's RBAC requirements at collector-contrib v0.130.0, the version the
|
|
9
|
+
* otel lexicon pins.
|
|
10
|
+
*
|
|
11
|
+
* `namespacedRoles` builds one Role and RoleBinding per namespace, for access
|
|
12
|
+
* a collector needs in namespaces other than its own: Endpoints for a
|
|
13
|
+
* `loadbalancing` exporter's `k8s` resolver, Leases for a
|
|
14
|
+
* `k8s_leader_elector` extension.
|
|
15
|
+
*/
|
|
16
|
+
import { mergeDefaults } from "@intentius/chant";
|
|
17
|
+
import { canonicalTypeOf } from "@intentius/chant-lexicon-otel/model";
|
|
18
|
+
import { Role, RoleBinding } from "../generated/index.js";
|
|
19
|
+
const READ = ["get", "list", "watch"];
|
|
20
|
+
/** The configs of the components of `kind` whose type is `type`, under its old or new name (`k8s_attributes`). */
|
|
21
|
+
function componentsOfType(kind, section, type) {
|
|
22
|
+
return Object.entries(section ?? {})
|
|
23
|
+
.filter(([id]) => canonicalTypeOf(kind, id) === type)
|
|
24
|
+
.map(([, cfg]) => (cfg && typeof cfg === "object" ? cfg : {}));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The agent ClusterRole's rules for a collector config.
|
|
28
|
+
*
|
|
29
|
+
* Always, for `k8sattributes` (processor/k8sattributesprocessor README,
|
|
30
|
+
* "Cluster-scoped RBAC"): `get`, `list` and `watch` on pods, namespaces and
|
|
31
|
+
* nodes, and on `apps` replicasets for `k8s.deployment.name`, which is
|
|
32
|
+
* extracted by default. The processor lists ReplicaSets through `apps/v1`
|
|
33
|
+
* only, so the README's `extensions` group is left out. When a
|
|
34
|
+
* `k8sattributes` processor extracts labels or annotations `from: deployment`,
|
|
35
|
+
* `apps` deployments are added.
|
|
36
|
+
*
|
|
37
|
+
* For each `kubeletstats` receiver (receiver/kubeletstatsreceiver README,
|
|
38
|
+
* "Role-based access control"): `get` on `nodes/stats`; `get` on
|
|
39
|
+
* `nodes/proxy` when it sets `extra_metadata_labels` or enables a
|
|
40
|
+
* `*_request_utilization` or `*_limit_utilization` metric; and, when it sets
|
|
41
|
+
* `k8s_api_config`, `get` on persistentvolumeclaims and persistentvolumes,
|
|
42
|
+
* which it reads for volume metadata (scraper.go).
|
|
43
|
+
*
|
|
44
|
+
* Other components that read the Kubernetes API, such as `k8s_cluster`
|
|
45
|
+
* (which belongs on a gateway, see WK8603), add their rules through
|
|
46
|
+
* `defaults.clusterRole.rules`, which are appended.
|
|
47
|
+
*/
|
|
48
|
+
export function agentClusterRules(config) {
|
|
49
|
+
const apps = ["replicasets"];
|
|
50
|
+
const k8sattributes = componentsOfType("processor", config.processors, "k8sattributes");
|
|
51
|
+
const fromDeployment = k8sattributes.some((p) => {
|
|
52
|
+
const extract = p.extract;
|
|
53
|
+
return [...(extract?.labels ?? []), ...(extract?.annotations ?? [])].some((f) => f?.from === "deployment");
|
|
54
|
+
});
|
|
55
|
+
if (fromDeployment)
|
|
56
|
+
apps.push("deployments");
|
|
57
|
+
const rules = [
|
|
58
|
+
{ apiGroups: [""], resources: ["pods", "namespaces", "nodes"], verbs: READ },
|
|
59
|
+
{ apiGroups: ["apps"], resources: apps, verbs: READ },
|
|
60
|
+
];
|
|
61
|
+
const kubeletstats = componentsOfType("receiver", config.receivers, "kubeletstats");
|
|
62
|
+
if (kubeletstats.length) {
|
|
63
|
+
const nodeSubresources = ["nodes/stats"];
|
|
64
|
+
const needsProxy = kubeletstats.some((r) => {
|
|
65
|
+
const labels = r.extra_metadata_labels;
|
|
66
|
+
if (Array.isArray(labels) && labels.length)
|
|
67
|
+
return true;
|
|
68
|
+
const metrics = (r.metrics ?? {});
|
|
69
|
+
return Object.entries(metrics).some(([metric, toggle]) => /_(request|limit)_utilization$/.test(metric) && toggle?.enabled === true);
|
|
70
|
+
});
|
|
71
|
+
if (needsProxy)
|
|
72
|
+
nodeSubresources.push("nodes/proxy");
|
|
73
|
+
rules.push({ apiGroups: [""], resources: nodeSubresources, verbs: ["get"] });
|
|
74
|
+
if (kubeletstats.some((r) => r.k8s_api_config)) {
|
|
75
|
+
rules.push({ apiGroups: [""], resources: ["persistentvolumeclaims", "persistentvolumes"], verbs: ["get"] });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return rules;
|
|
79
|
+
}
|
|
80
|
+
function pascal(namespace) {
|
|
81
|
+
return namespace
|
|
82
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
85
|
+
.join("");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* One Role and RoleBinding per namespace, as composite members. The first
|
|
89
|
+
* namespace's are `<key>Role` and `<key>RoleBinding`, so a single-namespace
|
|
90
|
+
* config keeps the member names it had before; each further namespace's are
|
|
91
|
+
* `<key>RoleIn<Namespace>` and `<key>RoleBindingIn<Namespace>`, with the
|
|
92
|
+
* namespace in PascalCase (`team-a` gives `endpointsRoleInTeamA`).
|
|
93
|
+
*/
|
|
94
|
+
export function namespacedRoles(opts) {
|
|
95
|
+
const out = {};
|
|
96
|
+
const seen = new Map();
|
|
97
|
+
opts.namespaces.forEach((namespace, i) => {
|
|
98
|
+
const suffix = i === 0 ? "" : `In${pascal(namespace)}`;
|
|
99
|
+
const clash = seen.get(suffix);
|
|
100
|
+
if (clash !== undefined) {
|
|
101
|
+
throw new Error(`${opts.name}: namespaces ${clash} and ${namespace} give the same member name ${opts.key}Role${suffix}`);
|
|
102
|
+
}
|
|
103
|
+
seen.set(suffix, namespace);
|
|
104
|
+
out[`${opts.key}Role${suffix}`] = new Role(mergeDefaults({
|
|
105
|
+
metadata: { name: opts.name, namespace, labels: opts.labels },
|
|
106
|
+
rules: opts.rules,
|
|
107
|
+
}, opts.roleDefaults));
|
|
108
|
+
out[`${opts.key}RoleBinding${suffix}`] = new RoleBinding(mergeDefaults({
|
|
109
|
+
metadata: { name: opts.name, namespace, labels: opts.labels },
|
|
110
|
+
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: opts.name },
|
|
111
|
+
subjects: [{ kind: "ServiceAccount", name: opts.serviceAccount.name, namespace: opts.serviceAccount.namespace }],
|
|
112
|
+
}, opts.roleBindingDefaults));
|
|
113
|
+
});
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `OtelCollector` and `OtelCollectorGateway` share beyond the container:
|
|
3
|
+
* reading ports and probes back from a declared config, and the annotations
|
|
4
|
+
* that record, in the built manifests, how each collector config is deployed.
|
|
5
|
+
*
|
|
6
|
+
* A post-synth check sees only the rendered YAML. The collector config is a
|
|
7
|
+
* string inside a ConfigMap, and whether it runs once per node or as N
|
|
8
|
+
* replicas behind a Service lives on another document. The annotations below
|
|
9
|
+
* join the two, so a check can take a ConfigMap, find its workload, and know
|
|
10
|
+
* the collector's role, its replica count, and (for an agent) which gateway
|
|
11
|
+
* it exports to and whether through `loadbalancing`.
|
|
12
|
+
*/
|
|
13
|
+
import { buildCollectorConfig, canonicalTypeOf, collectorEndpoints, collectorYaml, COLLECTOR_CONFIG_PATH, } from "@intentius/chant-lexicon-otel";
|
|
14
|
+
/**
|
|
15
|
+
* Annotation keys the collector composites write on their ConfigMap and
|
|
16
|
+
* workload. `GkeOtelCollector` writes none of them; its output is fixed.
|
|
17
|
+
*
|
|
18
|
+
* | Key | On | Value |
|
|
19
|
+
* |---|---|---|
|
|
20
|
+
* | `role` | ConfigMap, workload | `agent` (a DaemonSet, one collector per node) or `gateway` (a Deployment) |
|
|
21
|
+
* | `workload` | ConfigMap | the workload that mounts it, `DaemonSet/<name>` or `Deployment/<name>`, in the ConfigMap's namespace |
|
|
22
|
+
* | `config` | workload | the name of the ConfigMap holding its `config.yaml` |
|
|
23
|
+
* | `header` | `OtelOperatorCollector`'s `OpenTelemetryCollector` | the config's `# chant:` header lines (custom-component pins, semconv use), one per line, which an object-valued `spec.config` has no comments to hold |
|
|
24
|
+
* | `gateways` | agent ConfigMap and DaemonSet | comma-separated `<namespace>/<gateway name>=<routing>`, where routing is `loadbalancing` or `service` |
|
|
25
|
+
*
|
|
26
|
+
* The replica count is the workload's own `spec.replicas`.
|
|
27
|
+
*/
|
|
28
|
+
export const OTEL_COLLECTOR_ANNOTATIONS = {
|
|
29
|
+
role: "otel.chant.dev/role",
|
|
30
|
+
workload: "otel.chant.dev/workload",
|
|
31
|
+
config: "otel.chant.dev/config",
|
|
32
|
+
gateways: "otel.chant.dev/gateways",
|
|
33
|
+
header: "otel.chant.dev/header",
|
|
34
|
+
};
|
|
35
|
+
const gatewayTargets = new WeakMap();
|
|
36
|
+
/** Record which gateway an exporter entity was built for. */
|
|
37
|
+
export function markGatewayExporter(exporter, target) {
|
|
38
|
+
gatewayTargets.set(exporter, target);
|
|
39
|
+
}
|
|
40
|
+
/** The gateways a built config's exporters were built for, in declaration order. */
|
|
41
|
+
export function gatewayTargetsOf(built) {
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const c of built.components) {
|
|
44
|
+
const t = gatewayTargets.get(c);
|
|
45
|
+
if (t && !out.some((o) => o.name === t.name && o.namespace === t.namespace && o.routing === t.routing))
|
|
46
|
+
out.push(t);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/** The `gateways` annotation value for a list of targets. */
|
|
51
|
+
export function gatewaysAnnotation(targets) {
|
|
52
|
+
return targets.map((t) => `${t.namespace}/${t.name}=${t.routing}`).join(",");
|
|
53
|
+
}
|
|
54
|
+
export function collectorRuntime(entities) {
|
|
55
|
+
const built = buildCollectorConfig(entities);
|
|
56
|
+
const { ports, healthCheck } = collectorEndpoints(built.config);
|
|
57
|
+
const configDir = COLLECTOR_CONFIG_PATH.slice(0, COLLECTOR_CONFIG_PATH.lastIndexOf("/"));
|
|
58
|
+
// The probe names the port. A health_check on a receiver's port reuses that
|
|
59
|
+
// port's name, since a container port is declared once.
|
|
60
|
+
const healthPortName = healthCheck ? (ports.find((p) => p.port === healthCheck.port && !p.protocol)?.name ?? "health") : undefined;
|
|
61
|
+
const probe = healthCheck ? { httpGet: { path: healthCheck.path, port: healthPortName } } : undefined;
|
|
62
|
+
const containerPorts = [
|
|
63
|
+
...ports.map((p) => ({ containerPort: p.port, name: p.name, ...(p.protocol ? { protocol: p.protocol } : {}) })),
|
|
64
|
+
...(healthCheck && !ports.some((p) => p.port === healthCheck.port && !p.protocol)
|
|
65
|
+
? [{ containerPort: healthCheck.port, name: "health" }]
|
|
66
|
+
: []),
|
|
67
|
+
];
|
|
68
|
+
return {
|
|
69
|
+
built,
|
|
70
|
+
configYaml: collectorYaml(entities),
|
|
71
|
+
configDir,
|
|
72
|
+
containerPorts,
|
|
73
|
+
servicePorts: ports.map((p) => ({ name: p.name, port: p.port, ...(p.protocol ? { protocol: p.protocol } : {}) })),
|
|
74
|
+
containerExtra: {
|
|
75
|
+
imagePullPolicy: "IfNotPresent",
|
|
76
|
+
securityContext: {
|
|
77
|
+
runAsNonRoot: true,
|
|
78
|
+
runAsUser: 10001,
|
|
79
|
+
readOnlyRootFilesystem: true,
|
|
80
|
+
allowPrivilegeEscalation: false,
|
|
81
|
+
capabilities: { drop: ["ALL"] },
|
|
82
|
+
},
|
|
83
|
+
...(probe ? { livenessProbe: probe, readinessProbe: probe } : {}),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The namespaces whose Endpoints a config's `loadbalancing` exporters watch
|
|
89
|
+
* through the `k8s` resolver. A service given as `name` resolves in the
|
|
90
|
+
* collector's own namespace, `name.namespace` in that one.
|
|
91
|
+
*/
|
|
92
|
+
export function k8sResolverNamespaces(built, ownNamespace) {
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const [id, cfg] of Object.entries(built.config.exporters ?? {})) {
|
|
95
|
+
if (canonicalTypeOf("exporter", id) !== "loadbalancing")
|
|
96
|
+
continue;
|
|
97
|
+
const service = cfg?.resolver?.k8s?.service;
|
|
98
|
+
if (!service)
|
|
99
|
+
continue;
|
|
100
|
+
const dot = service.indexOf(".");
|
|
101
|
+
const ns = dot === -1 ? ownNamespace : service.slice(dot + 1);
|
|
102
|
+
if (!out.includes(ns))
|
|
103
|
+
out.push(ns);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The namespaces of the Leases a config's `k8s_leader_elector` extensions
|
|
109
|
+
* take, for those `service.extensions` enables (the collector starts no
|
|
110
|
+
* other). An elector without `lease_namespace` fails the otel lexicon's
|
|
111
|
+
* OTEL107 and is skipped here.
|
|
112
|
+
*/
|
|
113
|
+
export function leaderElectorLeaseNamespaces(built) {
|
|
114
|
+
const out = [];
|
|
115
|
+
const enabled = built.config.service?.extensions ?? [];
|
|
116
|
+
for (const [id, cfg] of Object.entries(built.config.extensions ?? {})) {
|
|
117
|
+
if (id !== "k8s_leader_elector" && !id.startsWith("k8s_leader_elector/"))
|
|
118
|
+
continue;
|
|
119
|
+
if (!enabled.includes(id))
|
|
120
|
+
continue;
|
|
121
|
+
const ns = cfg?.lease_namespace;
|
|
122
|
+
if (typeof ns !== "string" || ns === "")
|
|
123
|
+
continue;
|
|
124
|
+
if (!out.includes(ns))
|
|
125
|
+
out.push(ns);
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OtelCollector composite: an OpenTelemetry Collector agent on any
|
|
3
|
+
* Kubernetes cluster.
|
|
4
|
+
*
|
|
5
|
+
* A DaemonSet runs the collector on every node, with its config mounted from
|
|
6
|
+
* a ConfigMap, and a Service with `internalTrafficPolicy: Local` sends each
|
|
7
|
+
* pod's OTLP traffic to the collector on its own node. The config is declared
|
|
8
|
+
* with the otel lexicon (`@intentius/chant-lexicon-otel`) and rendered with
|
|
9
|
+
* `collectorYaml`. The container ports, the Service ports and the probes are
|
|
10
|
+
* read back from that config, so they follow whatever config is passed in.
|
|
11
|
+
*
|
|
12
|
+
* `GkeOtelCollector` builds the same DaemonSet, RBAC and ConfigMap for GKE.
|
|
13
|
+
* `OtelCollectorGateway` is the central tier agents export to; give this
|
|
14
|
+
* composite an exporter from `gatewayExporter()` to point it there.
|
|
15
|
+
*/
|
|
16
|
+
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
17
|
+
import { otlpCollector, COLLECTOR_IMAGE, } from "@intentius/chant-lexicon-otel";
|
|
18
|
+
import { Service } from "../generated/index.js";
|
|
19
|
+
import { collectorAgentResources } from "./otel-collector-agent.js";
|
|
20
|
+
import { collectorNodeAccess } from "./otel-collector-node.js";
|
|
21
|
+
import { agentClusterRules, namespacedRoles } from "./otel-collector-rbac.js";
|
|
22
|
+
import { collectorRuntime, gatewaysAnnotation, gatewayTargetsOf, k8sResolverNamespaces, OTEL_COLLECTOR_ANNOTATIONS, } from "./otel-collector-shape.js";
|
|
23
|
+
/**
|
|
24
|
+
* Create an OtelCollector composite. Returns a DaemonSet, Service,
|
|
25
|
+
* ServiceAccount, ClusterRole, ClusterRoleBinding and ConfigMap.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { OtelCollector } from "@intentius/chant-lexicon-k8s";
|
|
30
|
+
* import { OtlpExporter } from "@intentius/chant-lexicon-otel";
|
|
31
|
+
*
|
|
32
|
+
* export const collector = OtelCollector({
|
|
33
|
+
* exporters: [new OtlpExporter({ endpoint: "tempo.observability.svc:4317", tls: { insecure: true } })],
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export const OtelCollector = Composite((props) => {
|
|
38
|
+
const { name = "otel-collector", namespace = "observability", image = COLLECTOR_IMAGE, labels: extraLabels = {}, logAccess = "group", cpuRequest = "100m", memoryRequest = "256Mi", cpuLimit = "500m", memoryLimit = "512Mi", defaults: defs, } = props;
|
|
39
|
+
const commonLabels = {
|
|
40
|
+
"app.kubernetes.io/name": name,
|
|
41
|
+
"app.kubernetes.io/managed-by": "chant",
|
|
42
|
+
...extraLabels,
|
|
43
|
+
};
|
|
44
|
+
const entities = props.config ? [...props.config] : otlpCollector({ exporters: props.exporters, signals: props.signals });
|
|
45
|
+
const runtime = collectorRuntime(entities);
|
|
46
|
+
const configMapName = `${name}-config`;
|
|
47
|
+
// How this agent is deployed, and which gateways it sends to, recorded on
|
|
48
|
+
// the ConfigMap and the DaemonSet for post-synth checks (see
|
|
49
|
+
// OTEL_COLLECTOR_ANNOTATIONS).
|
|
50
|
+
const gateways = gatewayTargetsOf(runtime.built);
|
|
51
|
+
const shape = gateways.length ? { [OTEL_COLLECTOR_ANNOTATIONS.gateways]: gatewaysAnnotation(gateways) } : {};
|
|
52
|
+
const resources = collectorAgentResources({
|
|
53
|
+
name,
|
|
54
|
+
namespace,
|
|
55
|
+
image,
|
|
56
|
+
commonLabels,
|
|
57
|
+
extraLabels,
|
|
58
|
+
configYaml: runtime.configYaml,
|
|
59
|
+
configDir: runtime.configDir,
|
|
60
|
+
ports: runtime.containerPorts,
|
|
61
|
+
clusterRules: agentClusterRules(runtime.built.config),
|
|
62
|
+
// The node name, host mounts and log access the config's node-reading
|
|
63
|
+
// components need (#3103); nothing for a config that reads no node.
|
|
64
|
+
nodeAccess: collectorNodeAccess(runtime.built.config, runtime.configDir),
|
|
65
|
+
logAccess,
|
|
66
|
+
cpuRequest,
|
|
67
|
+
memoryRequest,
|
|
68
|
+
cpuLimit,
|
|
69
|
+
memoryLimit,
|
|
70
|
+
containerExtra: runtime.containerExtra,
|
|
71
|
+
workloadAnnotations: {
|
|
72
|
+
[OTEL_COLLECTOR_ANNOTATIONS.role]: "agent",
|
|
73
|
+
[OTEL_COLLECTOR_ANNOTATIONS.config]: configMapName,
|
|
74
|
+
...shape,
|
|
75
|
+
},
|
|
76
|
+
configMapAnnotations: {
|
|
77
|
+
[OTEL_COLLECTOR_ANNOTATIONS.role]: "agent",
|
|
78
|
+
[OTEL_COLLECTOR_ANNOTATIONS.workload]: `DaemonSet/${name}`,
|
|
79
|
+
...shape,
|
|
80
|
+
},
|
|
81
|
+
defaults: defs,
|
|
82
|
+
});
|
|
83
|
+
const service = new Service(mergeDefaults({
|
|
84
|
+
metadata: {
|
|
85
|
+
name,
|
|
86
|
+
namespace,
|
|
87
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "agent" },
|
|
88
|
+
},
|
|
89
|
+
spec: {
|
|
90
|
+
selector: { "app.kubernetes.io/name": name },
|
|
91
|
+
internalTrafficPolicy: "Local",
|
|
92
|
+
ports: runtime.servicePorts.map((p) => ({ name: p.name, port: p.port, targetPort: p.name, protocol: p.protocol ?? "TCP" })),
|
|
93
|
+
},
|
|
94
|
+
}, defs?.service));
|
|
95
|
+
// The loadbalancing exporter's k8s resolver runs in this agent and watches
|
|
96
|
+
// the gateway's headless Service, so the agent's ServiceAccount gets read
|
|
97
|
+
// access to Endpoints (what the resolver watches at the pinned collector
|
|
98
|
+
// version) and EndpointSlices (what later versions watch), with one Role
|
|
99
|
+
// and RoleBinding in each namespace the resolvers point at.
|
|
100
|
+
const saName = resources.serviceAccount.props.metadata.name;
|
|
101
|
+
const endpointsRbac = namespacedRoles({
|
|
102
|
+
key: "endpoints",
|
|
103
|
+
name: `${name}-endpoints`,
|
|
104
|
+
namespaces: k8sResolverNamespaces(runtime.built, namespace),
|
|
105
|
+
rules: [
|
|
106
|
+
{ apiGroups: [""], resources: ["endpoints"], verbs: ["get", "list", "watch"] },
|
|
107
|
+
{ apiGroups: ["discovery.k8s.io"], resources: ["endpointslices"], verbs: ["get", "list", "watch"] },
|
|
108
|
+
],
|
|
109
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
|
|
110
|
+
serviceAccount: { name: saName, namespace },
|
|
111
|
+
roleDefaults: defs?.endpointsRole,
|
|
112
|
+
roleBindingDefaults: defs?.endpointsRoleBinding,
|
|
113
|
+
});
|
|
114
|
+
return { ...resources, service, ...endpointsRbac };
|
|
115
|
+
}, "OtelCollector");
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OtelOperatorCollector composite: an OpenTelemetry Collector run by the
|
|
3
|
+
* OpenTelemetry Operator, as an `OpenTelemetryCollector` custom resource
|
|
4
|
+
* (opentelemetry.io/v1beta1).
|
|
5
|
+
*
|
|
6
|
+
* `OtelCollector` builds the DaemonSet, ConfigMap and Service itself. Here the
|
|
7
|
+
* operator builds them from one custom resource, and the otel lexicon's
|
|
8
|
+
* config goes straight into `spec.config`, which v1beta1 declares as an
|
|
9
|
+
* object (`exporters`, `receivers` and `service` required). The ports, the
|
|
10
|
+
* node access (downward API variables, host mounts, log group) and the RBAC
|
|
11
|
+
* come from the same readers `OtelCollector` uses, so the two follow a config
|
|
12
|
+
* the same way.
|
|
13
|
+
*
|
|
14
|
+
* An object has no comment lines, so the `# chant:` header that carries
|
|
15
|
+
* custom-component pins and semconv use (see `buildCollectorConfig`) is kept
|
|
16
|
+
* in the `otel.chant.dev/header` annotation, one line per entry. `chant
|
|
17
|
+
* import` puts it back on top of the config when it reads the CR.
|
|
18
|
+
*
|
|
19
|
+
* The operator names the workload `<name>-collector` and, with
|
|
20
|
+
* `spec.serviceAccount` set as it is here, runs it as the ServiceAccount this
|
|
21
|
+
* composite makes. The ClusterRole is bound to that ServiceAccount.
|
|
22
|
+
*/
|
|
23
|
+
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
24
|
+
import { otlpCollector, COLLECTOR_IMAGE, } from "@intentius/chant-lexicon-otel";
|
|
25
|
+
import { ServiceAccount, ClusterRole, ClusterRoleBinding, OpenTelemetryCollector } from "../generated/index.js";
|
|
26
|
+
import { collectorNodeAccess, nodeVolumeMounts, nodeVolumes } from "./otel-collector-node.js";
|
|
27
|
+
import { agentClusterRules, namespacedRoles } from "./otel-collector-rbac.js";
|
|
28
|
+
import { collectorRuntime, gatewaysAnnotation, gatewayTargetsOf, k8sResolverNamespaces, OTEL_COLLECTOR_ANNOTATIONS, } from "./otel-collector-shape.js";
|
|
29
|
+
/**
|
|
30
|
+
* Create an OtelOperatorCollector composite. Returns an OpenTelemetryCollector
|
|
31
|
+
* custom resource, a ServiceAccount, a ClusterRole and a ClusterRoleBinding.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { OtelOperatorCollector } from "@intentius/chant-lexicon-k8s";
|
|
36
|
+
* import { OtlpExporter } from "@intentius/chant-lexicon-otel";
|
|
37
|
+
*
|
|
38
|
+
* export const collector = OtelOperatorCollector({
|
|
39
|
+
* mode: "deployment",
|
|
40
|
+
* replicas: 2,
|
|
41
|
+
* exporters: [new OtlpExporter({ endpoint: "tempo.observability.svc:4317", tls: { insecure: true } })],
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export const OtelOperatorCollector = Composite((props) => {
|
|
46
|
+
const { name = "otel-collector", namespace = "observability", mode = "daemonset", image = COLLECTOR_IMAGE, labels: extraLabels = {}, logAccess = "group", cpuRequest = "100m", memoryRequest = "256Mi", cpuLimit = "500m", memoryLimit = "512Mi", defaults: defs, } = props;
|
|
47
|
+
if (mode === "daemonset" && props.replicas !== undefined) {
|
|
48
|
+
throw new Error(`${name}: a daemonset runs one collector per node, so replicas does not apply`);
|
|
49
|
+
}
|
|
50
|
+
const commonLabels = {
|
|
51
|
+
"app.kubernetes.io/name": name,
|
|
52
|
+
"app.kubernetes.io/managed-by": "chant",
|
|
53
|
+
...extraLabels,
|
|
54
|
+
};
|
|
55
|
+
const entities = props.config ? [...props.config] : otlpCollector({ exporters: props.exporters, signals: props.signals });
|
|
56
|
+
const runtime = collectorRuntime(entities);
|
|
57
|
+
const gateways = gatewayTargetsOf(runtime.built);
|
|
58
|
+
const saName = `${name}-sa`;
|
|
59
|
+
const access = collectorNodeAccess(runtime.built.config, runtime.configDir);
|
|
60
|
+
const asRoot = access.readsLogs && logAccess === "root";
|
|
61
|
+
const container = runtime.containerExtra.securityContext;
|
|
62
|
+
const annotations = {
|
|
63
|
+
[OTEL_COLLECTOR_ANNOTATIONS.role]: mode === "daemonset" ? "agent" : "gateway",
|
|
64
|
+
...(gateways.length ? { [OTEL_COLLECTOR_ANNOTATIONS.gateways]: gatewaysAnnotation(gateways) } : {}),
|
|
65
|
+
...(runtime.built.header.length ? { [OTEL_COLLECTOR_ANNOTATIONS.header]: runtime.built.header.join("\n") } : {}),
|
|
66
|
+
};
|
|
67
|
+
const collector = new OpenTelemetryCollector(mergeDefaults({
|
|
68
|
+
metadata: {
|
|
69
|
+
name,
|
|
70
|
+
namespace,
|
|
71
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": mode === "daemonset" ? "agent" : "gateway" },
|
|
72
|
+
annotations,
|
|
73
|
+
},
|
|
74
|
+
spec: {
|
|
75
|
+
mode,
|
|
76
|
+
...(props.replicas !== undefined ? { replicas: props.replicas } : {}),
|
|
77
|
+
image,
|
|
78
|
+
serviceAccount: saName,
|
|
79
|
+
config: runtime.built.config,
|
|
80
|
+
ports: runtime.servicePorts.map((p) => ({ name: p.name, port: p.port, protocol: p.protocol ?? "TCP" })),
|
|
81
|
+
resources: {
|
|
82
|
+
requests: { cpu: cpuRequest, memory: memoryRequest },
|
|
83
|
+
limits: { cpu: cpuLimit, memory: memoryLimit },
|
|
84
|
+
},
|
|
85
|
+
securityContext: asRoot ? { ...container, runAsNonRoot: false, runAsUser: 0 } : container,
|
|
86
|
+
...(access.readsLogs && !asRoot ? { podSecurityContext: { supplementalGroups: [0] } } : {}),
|
|
87
|
+
...(access.env.length ? { env: access.env } : {}),
|
|
88
|
+
...(access.mounts.length ? { volumes: nodeVolumes(access.mounts), volumeMounts: nodeVolumeMounts(access.mounts) } : {}),
|
|
89
|
+
...(mode === "daemonset" ? { tolerations: [{ operator: "Exists" }] } : {}),
|
|
90
|
+
},
|
|
91
|
+
}, defs?.collector));
|
|
92
|
+
const serviceAccount = new ServiceAccount(mergeDefaults({
|
|
93
|
+
metadata: {
|
|
94
|
+
name: saName,
|
|
95
|
+
namespace,
|
|
96
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
|
|
97
|
+
},
|
|
98
|
+
}, defs?.serviceAccount));
|
|
99
|
+
const clusterRoleName = `${name}-role`;
|
|
100
|
+
const clusterRole = new ClusterRole(mergeDefaults({
|
|
101
|
+
metadata: { name: clusterRoleName, labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" } },
|
|
102
|
+
rules: agentClusterRules(runtime.built.config),
|
|
103
|
+
}, defs?.clusterRole));
|
|
104
|
+
const clusterRoleBinding = new ClusterRoleBinding(mergeDefaults({
|
|
105
|
+
metadata: { name: `${name}-binding`, labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" } },
|
|
106
|
+
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "ClusterRole", name: clusterRoleName },
|
|
107
|
+
subjects: [{ kind: "ServiceAccount", name: saName, namespace }],
|
|
108
|
+
}, defs?.clusterRoleBinding));
|
|
109
|
+
const endpointsRbac = namespacedRoles({
|
|
110
|
+
key: "endpoints",
|
|
111
|
+
name: `${name}-endpoints`,
|
|
112
|
+
namespaces: k8sResolverNamespaces(runtime.built, namespace),
|
|
113
|
+
rules: [
|
|
114
|
+
{ apiGroups: [""], resources: ["endpoints"], verbs: ["get", "list", "watch"] },
|
|
115
|
+
{ apiGroups: ["discovery.k8s.io"], resources: ["endpointslices"], verbs: ["get", "list", "watch"] },
|
|
116
|
+
],
|
|
117
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
|
|
118
|
+
serviceAccount: { name: saName, namespace },
|
|
119
|
+
roleDefaults: defs?.endpointsRole,
|
|
120
|
+
roleBindingDefaults: defs?.endpointsRoleBinding,
|
|
121
|
+
});
|
|
122
|
+
return { collector, serviceAccount, clusterRole, clusterRoleBinding, ...endpointsRbac };
|
|
123
|
+
}, "OtelOperatorCollector");
|