@intentius/chant-lexicon-k8s 0.100.0 → 0.101.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/composites/catalog.d.ts.map +1 -1
- package/dist/composites/gke-otel-collector.d.ts.map +1 -1
- package/dist/composites/otel-collector-agent.d.ts +5 -1
- package/dist/composites/otel-collector-agent.d.ts.map +1 -1
- package/dist/composites/otel-collector-gateway.d.ts +37 -52
- package/dist/composites/otel-collector-gateway.d.ts.map +1 -1
- package/dist/composites/otel-collector-rbac.d.ts +74 -0
- package/dist/composites/otel-collector-rbac.d.ts.map +1 -0
- package/dist/composites/otel-collector-shape.d.ts +7 -0
- package/dist/composites/otel-collector-shape.d.ts.map +1 -1
- package/dist/composites/otel-collector.d.ts +8 -20
- package/dist/composites/otel-collector.d.ts.map +1 -1
- package/dist/import/live-export.d.ts +5 -0
- package/dist/import/live-export.d.ts.map +1 -1
- package/dist/integrity.json +4 -3
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/otel-placement-helpers.d.ts +5 -1
- package/dist/lint/post-synth/otel-placement-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8604.d.ts +19 -0
- package/dist/lint/post-synth/wk8604.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +1 -0
- package/dist/okf/rules/WK8604.md +11 -0
- package/dist/rules/otel-placement-helpers.ts +7 -11
- package/dist/rules/wk8604.ts +29 -0
- package/package.json +3 -3
- package/src/composites/__snapshots__/gke-otel-collector.snapshot.test.ts.snap +2 -68
- package/src/composites/catalog.ts +6 -0
- package/src/composites/gke-otel-collector.snapshot.test.ts +3 -1
- package/src/composites/gke-otel-collector.ts +8 -2
- package/src/composites/otel-collector-agent.ts +6 -9
- package/src/composites/otel-collector-gateway.test.ts +102 -5
- package/src/composites/otel-collector-gateway.ts +96 -18
- package/src/composites/otel-collector-rbac.test.ts +114 -0
- package/src/composites/otel-collector-rbac.ts +144 -0
- package/src/composites/otel-collector-shape.ts +19 -0
- package/src/composites/otel-collector.ts +25 -34
- package/src/import/embedded-live.test.ts +138 -0
- package/src/import/live-export.ts +10 -2
- package/src/lint/audit-catalog.ts +1 -0
- package/src/lint/post-synth/index.ts +2 -0
- package/src/lint/post-synth/otel-placement-helpers.ts +7 -11
- package/src/lint/post-synth/otel-placement.test.ts +35 -0
- package/src/lint/post-synth/wk8604.ts +29 -0
|
@@ -80,8 +80,8 @@ rules:
|
|
|
80
80
|
- ''
|
|
81
81
|
resources:
|
|
82
82
|
- pods
|
|
83
|
+
- namespaces
|
|
83
84
|
- nodes
|
|
84
|
-
- endpoints
|
|
85
85
|
verbs:
|
|
86
86
|
- get
|
|
87
87
|
- list
|
|
@@ -94,39 +94,6 @@ rules:
|
|
|
94
94
|
- get
|
|
95
95
|
- list
|
|
96
96
|
- watch
|
|
97
|
-
- apiGroups:
|
|
98
|
-
- batch
|
|
99
|
-
resources:
|
|
100
|
-
- jobs
|
|
101
|
-
verbs:
|
|
102
|
-
- get
|
|
103
|
-
- list
|
|
104
|
-
- watch
|
|
105
|
-
- apiGroups:
|
|
106
|
-
- ''
|
|
107
|
-
resources:
|
|
108
|
-
- nodes/proxy
|
|
109
|
-
verbs:
|
|
110
|
-
- get
|
|
111
|
-
- apiGroups:
|
|
112
|
-
- ''
|
|
113
|
-
resources:
|
|
114
|
-
- nodes/stats
|
|
115
|
-
- configmaps
|
|
116
|
-
- events
|
|
117
|
-
verbs:
|
|
118
|
-
- create
|
|
119
|
-
- get
|
|
120
|
-
- apiGroups:
|
|
121
|
-
- ''
|
|
122
|
-
resources:
|
|
123
|
-
- configmaps
|
|
124
|
-
verbs:
|
|
125
|
-
- get
|
|
126
|
-
- update
|
|
127
|
-
- create
|
|
128
|
-
resourceNames:
|
|
129
|
-
- otel-container-insight-clusterleader
|
|
130
97
|
|
|
131
98
|
---
|
|
132
99
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
@@ -281,8 +248,8 @@ rules:
|
|
|
281
248
|
- ''
|
|
282
249
|
resources:
|
|
283
250
|
- pods
|
|
251
|
+
- namespaces
|
|
284
252
|
- nodes
|
|
285
|
-
- endpoints
|
|
286
253
|
verbs:
|
|
287
254
|
- get
|
|
288
255
|
- list
|
|
@@ -295,39 +262,6 @@ rules:
|
|
|
295
262
|
- get
|
|
296
263
|
- list
|
|
297
264
|
- watch
|
|
298
|
-
- apiGroups:
|
|
299
|
-
- batch
|
|
300
|
-
resources:
|
|
301
|
-
- jobs
|
|
302
|
-
verbs:
|
|
303
|
-
- get
|
|
304
|
-
- list
|
|
305
|
-
- watch
|
|
306
|
-
- apiGroups:
|
|
307
|
-
- ''
|
|
308
|
-
resources:
|
|
309
|
-
- nodes/proxy
|
|
310
|
-
verbs:
|
|
311
|
-
- get
|
|
312
|
-
- apiGroups:
|
|
313
|
-
- ''
|
|
314
|
-
resources:
|
|
315
|
-
- nodes/stats
|
|
316
|
-
- configmaps
|
|
317
|
-
- events
|
|
318
|
-
verbs:
|
|
319
|
-
- create
|
|
320
|
-
- get
|
|
321
|
-
- apiGroups:
|
|
322
|
-
- ''
|
|
323
|
-
resources:
|
|
324
|
-
- configmaps
|
|
325
|
-
verbs:
|
|
326
|
-
- get
|
|
327
|
-
- update
|
|
328
|
-
- create
|
|
329
|
-
resourceNames:
|
|
330
|
-
- otel-container-insight-clusterleader
|
|
331
265
|
|
|
332
266
|
---
|
|
333
267
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
@@ -2149,11 +2149,15 @@ export const compositeCatalog: CompositeEntry[] = [
|
|
|
2149
2149
|
"lexicon": "k8s",
|
|
2150
2150
|
"description": "Create a GkeOtelCollector composite — returns prop objects for a DaemonSet, ServiceAccount, ClusterRole, ClusterRoleBinding, and ConfigMap.",
|
|
2151
2151
|
"bundles": [
|
|
2152
|
+
"BatchProcessor",
|
|
2152
2153
|
"ClusterRole",
|
|
2153
2154
|
"ClusterRoleBinding",
|
|
2154
2155
|
"ConfigMap",
|
|
2155
2156
|
"DaemonSet",
|
|
2157
|
+
"GoogleCloudExporter",
|
|
2158
|
+
"OtlpReceiver",
|
|
2156
2159
|
"Pipeline",
|
|
2160
|
+
"ResourceDetectionProcessor",
|
|
2157
2161
|
"ServiceAccount"
|
|
2158
2162
|
],
|
|
2159
2163
|
"params": [
|
|
@@ -2991,6 +2995,8 @@ export const compositeCatalog: CompositeEntry[] = [
|
|
|
2991
2995
|
"ConfigMap",
|
|
2992
2996
|
"Deployment",
|
|
2993
2997
|
"PodDisruptionBudget",
|
|
2998
|
+
"Role",
|
|
2999
|
+
"RoleBinding",
|
|
2994
3000
|
"Service",
|
|
2995
3001
|
"ServiceAccount"
|
|
2996
3002
|
],
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* `OtelCollector`, `OtelCollectorGateway` and `GkeOtelCollector` share the
|
|
5
5
|
* collector resources in `otel-collector-agent.ts`. The snapshot was recorded
|
|
6
6
|
* before the gateway work touched that file, so any change to the GKE output
|
|
7
|
-
* made through the shared code fails here.
|
|
7
|
+
* made through the shared code fails here. #2923 changed it on purpose: the
|
|
8
|
+
* shared ClusterRole was narrowed to what k8sattributes and the config's
|
|
9
|
+
* receivers need (see `agentClusterRules`).
|
|
8
10
|
*/
|
|
9
11
|
import { describe, expect, test } from "vitest";
|
|
10
12
|
import { expandComposite } from "@intentius/chant";
|
|
@@ -16,9 +16,11 @@ import {
|
|
|
16
16
|
ResourceDetectionProcessor,
|
|
17
17
|
GoogleCloudExporter,
|
|
18
18
|
Pipeline,
|
|
19
|
+
buildCollectorConfig,
|
|
19
20
|
collectorYaml,
|
|
20
21
|
} from "@intentius/chant-lexicon-otel";
|
|
21
22
|
import { collectorAgentResources, type CollectorAgentResources } from "./otel-collector-agent";
|
|
23
|
+
import { agentClusterRules } from "./otel-collector-rbac";
|
|
22
24
|
|
|
23
25
|
export interface GkeOtelCollectorProps {
|
|
24
26
|
/** GKE cluster name. */
|
|
@@ -110,14 +112,15 @@ export const GkeOtelCollector = Composite((props: GkeOtelCollectorProps) => {
|
|
|
110
112
|
attribute_mappings: [{ key: "service.name", replacement: "g.co/r/service/name" }],
|
|
111
113
|
},
|
|
112
114
|
});
|
|
113
|
-
const
|
|
115
|
+
const entities = [
|
|
114
116
|
otlp,
|
|
115
117
|
batch,
|
|
116
118
|
resourceDetection,
|
|
117
119
|
googleCloud,
|
|
118
120
|
new Pipeline({ signal: "metrics", receivers: [otlp], processors: [batch, resourceDetection], exporters: [googleCloud] }),
|
|
119
121
|
new Pipeline({ signal: "traces", receivers: [otlp], processors: [batch, resourceDetection], exporters: [googleCloud] }),
|
|
120
|
-
]
|
|
122
|
+
];
|
|
123
|
+
const otelConfig = collectorYaml(entities);
|
|
121
124
|
|
|
122
125
|
// The DaemonSet, RBAC and ConfigMap are the ones OtelCollector builds too.
|
|
123
126
|
return collectorAgentResources({
|
|
@@ -128,6 +131,9 @@ export const GkeOtelCollector = Composite((props: GkeOtelCollectorProps) => {
|
|
|
128
131
|
extraLabels,
|
|
129
132
|
configYaml: otelConfig,
|
|
130
133
|
configDir: "/etc/otel",
|
|
134
|
+
// The ClusterRole OtelCollector shares: k8sattributes' rules, and none
|
|
135
|
+
// for receivers, since the config's only receiver is otlp (#2923).
|
|
136
|
+
clusterRules: agentClusterRules(buildCollectorConfig(entities).config),
|
|
131
137
|
ports: [
|
|
132
138
|
{ containerPort: 4317, name: "otlp-grpc" },
|
|
133
139
|
{ containerPort: 4318, name: "otlp-http" },
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* The Kubernetes resources an OpenTelemetry Collector agent needs, shared by
|
|
3
3
|
* `OtelCollector` and `GkeOtelCollector`: a DaemonSet running the collector
|
|
4
4
|
* with its config mounted from a ConfigMap, and a ServiceAccount bound to a
|
|
5
|
-
* ClusterRole
|
|
5
|
+
* ClusterRole with the rules the caller works out from its config (see
|
|
6
|
+
* `agentClusterRules` in `otel-collector-rbac.ts`).
|
|
6
7
|
*
|
|
7
8
|
* The caller renders the collector config (with the otel lexicon's
|
|
8
9
|
* `collectorYaml`) and says which ports the container exposes. Everything
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
|
|
16
17
|
import { mergeDefaults } from "@intentius/chant";
|
|
17
18
|
import { DaemonSet, ServiceAccount, ClusterRole, ClusterRoleBinding, ConfigMap } from "../generated";
|
|
19
|
+
import type { CollectorPolicyRule } from "./otel-collector-rbac";
|
|
18
20
|
|
|
19
21
|
export interface CollectorAgentOptions {
|
|
20
22
|
name: string;
|
|
@@ -29,6 +31,8 @@ export interface CollectorAgentOptions {
|
|
|
29
31
|
/** Directory the config is mounted in. The file is `<dir>/config.yaml`. */
|
|
30
32
|
configDir: string;
|
|
31
33
|
ports: Array<{ containerPort: number; name: string }>;
|
|
34
|
+
/** The ClusterRole's rules, from `agentClusterRules`. */
|
|
35
|
+
clusterRules: CollectorPolicyRule[];
|
|
32
36
|
cpuRequest: string;
|
|
33
37
|
memoryRequest: string;
|
|
34
38
|
cpuLimit: string;
|
|
@@ -104,14 +108,7 @@ export function collectorAgentResources(opts: CollectorAgentOptions): CollectorA
|
|
|
104
108
|
name: clusterRoleName,
|
|
105
109
|
labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
|
|
106
110
|
},
|
|
107
|
-
rules:
|
|
108
|
-
{ apiGroups: [""], resources: ["pods", "nodes", "endpoints"], verbs: ["get", "list", "watch"] },
|
|
109
|
-
{ apiGroups: ["apps"], resources: ["replicasets"], verbs: ["get", "list", "watch"] },
|
|
110
|
-
{ apiGroups: ["batch"], resources: ["jobs"], verbs: ["get", "list", "watch"] },
|
|
111
|
-
{ apiGroups: [""], resources: ["nodes/proxy"], verbs: ["get"] },
|
|
112
|
-
{ apiGroups: [""], resources: ["nodes/stats", "configmaps", "events"], verbs: ["create", "get"] },
|
|
113
|
-
{ apiGroups: [""], resources: ["configmaps"], verbs: ["get", "update", "create"], resourceNames: ["otel-container-insight-clusterleader"] },
|
|
114
|
-
],
|
|
111
|
+
rules: opts.clusterRules,
|
|
115
112
|
}, defs?.clusterRole));
|
|
116
113
|
|
|
117
114
|
const clusterRoleBinding = new ClusterRoleBinding(mergeDefaults({
|
|
@@ -4,9 +4,12 @@ import { expandComposite } from "@intentius/chant";
|
|
|
4
4
|
import type { Declarable } from "@intentius/chant/declarable";
|
|
5
5
|
import {
|
|
6
6
|
DebugExporter,
|
|
7
|
+
K8sClusterReceiver,
|
|
8
|
+
K8sLeaderElectorExtension,
|
|
7
9
|
LoadBalancingExporter,
|
|
8
10
|
OtlpReceiver,
|
|
9
11
|
Pipeline,
|
|
12
|
+
Service as CollectorService,
|
|
10
13
|
TailSamplingProcessor,
|
|
11
14
|
collectorTopology,
|
|
12
15
|
validateCollectorConfig,
|
|
@@ -85,6 +88,48 @@ describe("OtelCollectorGateway", () => {
|
|
|
85
88
|
expect(p(gw.clusterRoleBinding).roleRef.name).toBe("gw-role");
|
|
86
89
|
});
|
|
87
90
|
|
|
91
|
+
test("an enabled k8s_leader_elector adds a Role for Leases in its lease_namespace", () => {
|
|
92
|
+
const elector = new K8sLeaderElectorExtension({ lease_name: "otel-cluster", lease_namespace: "leases" });
|
|
93
|
+
const cluster = new K8sClusterReceiver({ k8s_leader_elector: elector.componentId });
|
|
94
|
+
const debug = new DebugExporter({});
|
|
95
|
+
const pipeline = new Pipeline({ signal: "metrics", receivers: [cluster], exporters: [debug] });
|
|
96
|
+
const gw = OtelCollectorGateway({ name: "gw", namespace: "obs", config: [pipeline, new CollectorService({ extensions: [elector] })] });
|
|
97
|
+
expect(p(gw.leaseRole).metadata).toMatchObject({ name: "gw-leases", namespace: "leases" });
|
|
98
|
+
expect(p(gw.leaseRole).rules).toEqual([
|
|
99
|
+
{ apiGroups: ["coordination.k8s.io"], resources: ["leases"], verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] },
|
|
100
|
+
]);
|
|
101
|
+
expect(p(gw.leaseRoleBinding).roleRef).toEqual({ apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: "gw-leases" });
|
|
102
|
+
expect(p(gw.leaseRoleBinding).subjects).toEqual([{ kind: "ServiceAccount", name: "gw-sa", namespace: "obs" }]);
|
|
103
|
+
|
|
104
|
+
// Declared but left out of service.extensions: the collector never starts it, so no Role.
|
|
105
|
+
const idle = OtelCollectorGateway({ config: [pipeline, elector, new CollectorService({ extensions: [] })] });
|
|
106
|
+
expect(config(idle).extensions).toHaveProperty(["k8s_leader_elector"]);
|
|
107
|
+
expect(idle.leaseRole).toBeUndefined();
|
|
108
|
+
expect(OtelCollectorGateway({}).leaseRole).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("k8s_leader_elector extensions in two lease namespaces get a Role and RoleBinding in each", () => {
|
|
112
|
+
const a = new K8sLeaderElectorExtension({ name: "a", lease_name: "a", lease_namespace: "one" });
|
|
113
|
+
const b = new K8sLeaderElectorExtension({ name: "b", lease_name: "b", lease_namespace: "team-two" });
|
|
114
|
+
const debug = new DebugExporter({});
|
|
115
|
+
const otlp = new OtlpReceiver({ protocols: { grpc: { endpoint: "0.0.0.0:4317" } } });
|
|
116
|
+
const config = [new Pipeline({ signal: "traces", receivers: [otlp], exporters: [debug] }), new CollectorService({ extensions: [a, b] })];
|
|
117
|
+
const gw = OtelCollectorGateway({ name: "gw", namespace: "obs", config });
|
|
118
|
+
expect(p(gw.leaseRole).metadata).toMatchObject({ name: "gw-leases", namespace: "one" });
|
|
119
|
+
expect(p(gw.leaseRoleBinding).metadata).toMatchObject({ name: "gw-leases", namespace: "one" });
|
|
120
|
+
expect(p(gw.leaseRoleInTeamTwo).metadata).toMatchObject({ name: "gw-leases", namespace: "team-two" });
|
|
121
|
+
expect(p(gw.leaseRoleInTeamTwo).rules).toEqual(p(gw.leaseRole).rules);
|
|
122
|
+
expect(p(gw.leaseRoleBindingInTeamTwo).metadata).toMatchObject({ name: "gw-leases", namespace: "team-two" });
|
|
123
|
+
expect(p(gw.leaseRoleBindingInTeamTwo).subjects).toEqual([{ kind: "ServiceAccount", name: "gw-sa", namespace: "obs" }]);
|
|
124
|
+
const roles = manifests({ gw }).filter((d) => d.kind === "Role" || d.kind === "RoleBinding");
|
|
125
|
+
expect(roles.map((d) => `${d.kind}/${d.metadata.namespace}/${d.metadata.name}`).sort()).toEqual([
|
|
126
|
+
"Role/one/gw-leases",
|
|
127
|
+
"Role/team-two/gw-leases",
|
|
128
|
+
"RoleBinding/one/gw-leases",
|
|
129
|
+
"RoleBinding/team-two/gw-leases",
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
|
|
88
133
|
test("the Deployment and ConfigMap say they are a gateway and name each other", () => {
|
|
89
134
|
const gw = OtelCollectorGateway({ name: "gw" });
|
|
90
135
|
expect(p(gw.deployment).metadata.annotations).toEqual({ [A.role]: "gateway", [A.config]: "gw-config" });
|
|
@@ -147,6 +192,36 @@ describe("gatewayExporter", () => {
|
|
|
147
192
|
expect(() => gatewayExporter(gw)).toThrow(/no port named "otlp-grpc" \(it has: otlp-http\)/);
|
|
148
193
|
});
|
|
149
194
|
|
|
195
|
+
test("with protocol http, an otlphttp exporter at the gateway's OTLP HTTP port", () => {
|
|
196
|
+
const exporter = gatewayExporter(OtelCollectorGateway({}), { protocol: "http" });
|
|
197
|
+
expect(exporter.componentId).toBe("otlphttp/gateway");
|
|
198
|
+
expect(p(exporter)).toEqual({ name: "gateway", endpoint: "http://otel-gateway.observability.svc:4318" });
|
|
199
|
+
const agent = agentOf(exporter, { name: "agent" });
|
|
200
|
+
expect(config(agent).exporters?.["otlphttp/gateway"]).toEqual({ endpoint: "http://otel-gateway.observability.svc:4318" });
|
|
201
|
+
expect(p(agent.configMap).metadata.annotations[A.gateways]).toBe("observability/otel-gateway=service");
|
|
202
|
+
expect(validateCollectorConfig(config(agent))).toEqual([]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("with protocol http and TLS, https and the TLS settings without insecure", () => {
|
|
206
|
+
const exporter = gatewayExporter(OtelCollectorGateway({ name: "gw", namespace: "obs" }), { protocol: "http", tls: { ca_file: "/certs/ca.pem" } });
|
|
207
|
+
expect(p(exporter)).toEqual({ name: "gateway", endpoint: "https://gw.obs.svc:4318", tls: { ca_file: "/certs/ca.pem" } });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("with protocol grpc, the default otlp exporter", () => {
|
|
211
|
+
const gw = OtelCollectorGateway({});
|
|
212
|
+
expect(p(gatewayExporter(gw, { protocol: "grpc" }))).toEqual(p(gatewayExporter(gw)));
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("protocol http with loadBalance is refused", () => {
|
|
216
|
+
expect(() => gatewayExporter(OtelCollectorGateway({}), { protocol: "http", loadBalance: true })).toThrow(/gRPC only/);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("a gateway without an OTLP HTTP port is refused for protocol http", () => {
|
|
220
|
+
const otlp = new OtlpReceiver({ protocols: { grpc: { endpoint: "0.0.0.0:4317" } } });
|
|
221
|
+
const gw = OtelCollectorGateway({ config: [new Pipeline({ signal: "traces", receivers: [otlp], exporters: [new DebugExporter({})] })] });
|
|
222
|
+
expect(() => gatewayExporter(gw, { protocol: "http" })).toThrow(/no port named "otlp-http" \(it has: otlp-grpc\)/);
|
|
223
|
+
});
|
|
224
|
+
|
|
150
225
|
test("the otel topology reports the gateway as the exporter's endpoint", () => {
|
|
151
226
|
const agent = agentOf(gatewayExporter(OtelCollectorGateway({ namespace: "obs" }), { loadBalance: true }));
|
|
152
227
|
const exporters = collectorTopology(config(agent)).exporters;
|
|
@@ -182,13 +257,35 @@ describe("OtelCollector wired to a gateway", () => {
|
|
|
182
257
|
expect(p(agent.configMap).metadata.annotations[A.gateways]).toBeUndefined();
|
|
183
258
|
});
|
|
184
259
|
|
|
185
|
-
test("resolvers in two namespaces
|
|
260
|
+
test("resolvers in two namespaces get a Role and RoleBinding in each", () => {
|
|
186
261
|
const otlp = new OtlpReceiver({ protocols: { grpc: { endpoint: "0.0.0.0:4317" } } });
|
|
187
262
|
const a = gatewayExporter(OtelCollectorGateway({ namespace: "one" }), { loadBalance: true, name: "a" });
|
|
188
|
-
const b = gatewayExporter(OtelCollectorGateway({ namespace: "two" }), { loadBalance: true, name: "b" });
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
).
|
|
263
|
+
const b = gatewayExporter(OtelCollectorGateway({ namespace: "team-two" }), { loadBalance: true, name: "b" });
|
|
264
|
+
const agent = OtelCollector({ name: "agent", namespace: "agents", config: [new Pipeline({ signal: "traces", receivers: [otlp], exporters: [a, b] })] });
|
|
265
|
+
expect(p(agent.endpointsRole).metadata).toMatchObject({ name: "agent-endpoints", namespace: "one" });
|
|
266
|
+
expect(p(agent.endpointsRoleInTeamTwo).metadata).toMatchObject({ name: "agent-endpoints", namespace: "team-two" });
|
|
267
|
+
expect(p(agent.endpointsRoleInTeamTwo).rules).toEqual(p(agent.endpointsRole).rules);
|
|
268
|
+
for (const binding of [agent.endpointsRoleBinding, agent.endpointsRoleBindingInTeamTwo]) {
|
|
269
|
+
expect(p(binding).roleRef).toEqual({ apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: "agent-endpoints" });
|
|
270
|
+
expect(p(binding).subjects).toEqual([{ kind: "ServiceAccount", name: "agent-sa", namespace: "agents" }]);
|
|
271
|
+
}
|
|
272
|
+
expect(p(agent.endpointsRoleBindingInTeamTwo).metadata.namespace).toBe("team-two");
|
|
273
|
+
const roles = manifests({ agent }).filter((d) => d.kind === "Role" || d.kind === "RoleBinding");
|
|
274
|
+
expect(roles.map((d) => `${d.kind}/${d.metadata.namespace}/${d.metadata.name}`).sort()).toEqual([
|
|
275
|
+
"Role/one/agent-endpoints",
|
|
276
|
+
"Role/team-two/agent-endpoints",
|
|
277
|
+
"RoleBinding/one/agent-endpoints",
|
|
278
|
+
"RoleBinding/team-two/agent-endpoints",
|
|
279
|
+
]);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("resolvers in one namespace keep a single Role", () => {
|
|
283
|
+
const otlp = new OtlpReceiver({ protocols: { grpc: { endpoint: "0.0.0.0:4317" } } });
|
|
284
|
+
const gw = OtelCollectorGateway({ namespace: "one" });
|
|
285
|
+
const a = gatewayExporter(gw, { loadBalance: true, name: "a" });
|
|
286
|
+
const b = gatewayExporter(gw, { loadBalance: true, name: "b", routingKey: "service" });
|
|
287
|
+
const agent = OtelCollector({ config: [new Pipeline({ signal: "traces", receivers: [otlp], exporters: [a, b] })] });
|
|
288
|
+
expect(Object.keys((agent as any).members).filter((k) => k.startsWith("endpoints")).sort()).toEqual(["endpointsRole", "endpointsRoleBinding"]);
|
|
192
289
|
});
|
|
193
290
|
|
|
194
291
|
test("the agent's ConfigMap and DaemonSet name the gateway and how they reach it", () => {
|
|
@@ -21,28 +21,35 @@ import {
|
|
|
21
21
|
otlpCollector,
|
|
22
22
|
LoadBalancingExporter,
|
|
23
23
|
OtlpExporter,
|
|
24
|
+
OtlpHttpExporter,
|
|
24
25
|
COLLECTOR_IMAGE,
|
|
25
26
|
type LoadBalancingRoutingKey,
|
|
26
27
|
type OTelComponent,
|
|
27
28
|
type Signal,
|
|
28
29
|
type TLSClientSettings,
|
|
29
30
|
} from "@intentius/chant-lexicon-otel";
|
|
30
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
Deployment,
|
|
33
|
+
Service,
|
|
34
|
+
ServiceAccount,
|
|
35
|
+
ClusterRole,
|
|
36
|
+
ClusterRoleBinding,
|
|
37
|
+
ConfigMap,
|
|
38
|
+
PodDisruptionBudget,
|
|
39
|
+
Role,
|
|
40
|
+
RoleBinding,
|
|
41
|
+
} from "../generated";
|
|
31
42
|
import { collectorConfigMap, collectorContainer } from "./otel-collector-agent";
|
|
43
|
+
import { namespacedRoles, type CollectorPolicyRule } from "./otel-collector-rbac";
|
|
32
44
|
import {
|
|
33
45
|
collectorRuntime,
|
|
46
|
+
leaderElectorLeaseNamespaces,
|
|
34
47
|
markGatewayExporter,
|
|
35
48
|
OTEL_COLLECTOR_ANNOTATIONS,
|
|
36
49
|
type GatewayRouting,
|
|
37
50
|
} from "./otel-collector-shape";
|
|
38
51
|
|
|
39
|
-
|
|
40
|
-
export interface CollectorPolicyRule {
|
|
41
|
-
apiGroups: string[];
|
|
42
|
-
resources: string[];
|
|
43
|
-
verbs: string[];
|
|
44
|
-
resourceNames?: string[];
|
|
45
|
-
}
|
|
52
|
+
export type { CollectorPolicyRule };
|
|
46
53
|
|
|
47
54
|
export interface OtelCollectorGatewayProps {
|
|
48
55
|
/** Gateway name (default: "otel-gateway"). */
|
|
@@ -90,6 +97,8 @@ export interface OtelCollectorGatewayProps {
|
|
|
90
97
|
podDisruptionBudget?: Partial<Record<string, unknown>>;
|
|
91
98
|
clusterRole?: Partial<Record<string, unknown>>;
|
|
92
99
|
clusterRoleBinding?: Partial<Record<string, unknown>>;
|
|
100
|
+
leaseRole?: Partial<Record<string, unknown>>;
|
|
101
|
+
leaseRoleBinding?: Partial<Record<string, unknown>>;
|
|
93
102
|
};
|
|
94
103
|
}
|
|
95
104
|
|
|
@@ -103,13 +112,23 @@ export type OtelCollectorGatewayResult = {
|
|
|
103
112
|
podDisruptionBudget?: InstanceType<typeof PodDisruptionBudget>;
|
|
104
113
|
clusterRole?: InstanceType<typeof ClusterRole>;
|
|
105
114
|
clusterRoleBinding?: InstanceType<typeof ClusterRoleBinding>;
|
|
115
|
+
/**
|
|
116
|
+
* Access to Leases for the `k8s_leader_elector` extensions the config
|
|
117
|
+
* enables, in the first `lease_namespace`. Each further namespace gets
|
|
118
|
+
* `leaseRoleIn<Namespace>` and `leaseRoleBindingIn<Namespace>`.
|
|
119
|
+
*/
|
|
120
|
+
leaseRole?: InstanceType<typeof Role>;
|
|
121
|
+
leaseRoleBinding?: InstanceType<typeof RoleBinding>;
|
|
122
|
+
[member: `leaseRoleIn${string}`]: InstanceType<typeof Role>;
|
|
123
|
+
[member: `leaseRoleBindingIn${string}`]: InstanceType<typeof RoleBinding>;
|
|
106
124
|
};
|
|
107
125
|
|
|
108
126
|
/**
|
|
109
127
|
* Create an OtelCollectorGateway composite. Returns a Deployment, a ClusterIP
|
|
110
128
|
* Service, a headless Service, a ServiceAccount and a ConfigMap, a
|
|
111
|
-
* PodDisruptionBudget when there is more than one replica,
|
|
112
|
-
* and binding when `clusterRules` is set
|
|
129
|
+
* PodDisruptionBudget when there is more than one replica, a ClusterRole
|
|
130
|
+
* and binding when `clusterRules` is set, and a Role and binding for Leases
|
|
131
|
+
* when the config enables a `k8s_leader_elector` extension.
|
|
113
132
|
*
|
|
114
133
|
* @example
|
|
115
134
|
* ```ts
|
|
@@ -239,14 +258,42 @@ export const OtelCollectorGateway = Composite((props: OtelCollectorGatewayProps)
|
|
|
239
258
|
}
|
|
240
259
|
: {};
|
|
241
260
|
|
|
242
|
-
|
|
261
|
+
// A k8s_leader_elector extension takes a Lease in its lease_namespace, so
|
|
262
|
+
// the gateway's ServiceAccount gets the access the extension's README
|
|
263
|
+
// suggests (collector-contrib v0.130.0, extension/k8sleaderelector), with
|
|
264
|
+
// one Role and RoleBinding in each namespace the electors use.
|
|
265
|
+
const leaseRbac = namespacedRoles({
|
|
266
|
+
key: "lease",
|
|
267
|
+
name: `${name}-leases`,
|
|
268
|
+
namespaces: leaderElectorLeaseNamespaces(runtime.built),
|
|
269
|
+
rules: [
|
|
270
|
+
{
|
|
271
|
+
apiGroups: ["coordination.k8s.io"],
|
|
272
|
+
resources: ["leases"],
|
|
273
|
+
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"],
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
|
|
277
|
+
serviceAccount: { name: saName, namespace },
|
|
278
|
+
roleDefaults: defs?.leaseRole,
|
|
279
|
+
roleBindingDefaults: defs?.leaseRoleBinding,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return { deployment, service, headlessService, serviceAccount, configMap, ...pdb, ...rbac, ...leaseRbac } as OtelCollectorGatewayResult;
|
|
243
283
|
}, "OtelCollectorGateway");
|
|
244
284
|
|
|
245
285
|
// ── Agent side ───────────────────────────────────────────────────────
|
|
246
286
|
|
|
247
287
|
export interface GatewayExporterOptions {
|
|
248
|
-
/** Exporter name, so the id is `otlp/<name>` or `loadbalancing/<name>` (default: "gateway"). */
|
|
288
|
+
/** Exporter name, so the id is `otlp/<name>`, `otlphttp/<name>` or `loadbalancing/<name>` (default: "gateway"). */
|
|
249
289
|
name?: string;
|
|
290
|
+
/**
|
|
291
|
+
* OTLP over gRPC (an `otlp` exporter) or over HTTP (an `otlphttp`
|
|
292
|
+
* exporter). The `loadbalancing` exporter sends OTLP over gRPC only at the
|
|
293
|
+
* pinned collector version, so `http` cannot be combined with
|
|
294
|
+
* `loadBalance`. Default: "grpc".
|
|
295
|
+
*/
|
|
296
|
+
protocol?: "grpc" | "http";
|
|
250
297
|
/**
|
|
251
298
|
* Send each trace to one gateway replica through the headless Service with
|
|
252
299
|
* a `loadbalancing` exporter, instead of to the ClusterIP Service with an
|
|
@@ -262,9 +309,13 @@ export interface GatewayExporterOptions {
|
|
|
262
309
|
resolver?: "k8s" | "dns";
|
|
263
310
|
/** What the `loadbalancing` exporter hashes on (default: "traceID"). */
|
|
264
311
|
routingKey?: LoadBalancingRoutingKey;
|
|
265
|
-
/** The gateway Service port to send to, by name (default: "otlp-grpc"). */
|
|
312
|
+
/** The gateway Service port to send to, by name (default: "otlp-grpc", or "otlp-http" with `protocol: "http"`). */
|
|
266
313
|
port?: string;
|
|
267
|
-
/**
|
|
314
|
+
/**
|
|
315
|
+
* TLS for the connection to the gateway (default: `{ insecure: true }`,
|
|
316
|
+
* plaintext inside the cluster). Over HTTP, `insecure` picks the scheme
|
|
317
|
+
* (`http://`, or `https://` without it) and the other settings are passed on.
|
|
318
|
+
*/
|
|
268
319
|
tls?: TLSClientSettings;
|
|
269
320
|
}
|
|
270
321
|
|
|
@@ -285,6 +336,9 @@ function serviceMeta(service: InstanceType<typeof Service>): ServiceMeta {
|
|
|
285
336
|
* config listens on.
|
|
286
337
|
*
|
|
287
338
|
* By default an `otlp` exporter at `<service>.<namespace>.svc:<port>`. With
|
|
339
|
+
* `protocol: "http"`, an `otlphttp` exporter at
|
|
340
|
+
* `http://<service>.<namespace>.svc:<port>` (`https://` when `tls` is not
|
|
341
|
+
* `insecure`), sending to the gateway's OTLP HTTP port. With
|
|
288
342
|
* `loadBalance`, a `loadbalancing` exporter routing by trace id to the
|
|
289
343
|
* headless Service, through the `k8s` resolver (`<headless>.<namespace>`) or
|
|
290
344
|
* the `dns` resolver (`<headless>.<namespace>.svc`). Put it in the agent's
|
|
@@ -294,8 +348,15 @@ function serviceMeta(service: InstanceType<typeof Service>): ServiceMeta {
|
|
|
294
348
|
export function gatewayExporter(
|
|
295
349
|
gateway: OtelCollectorGatewayResult,
|
|
296
350
|
options: GatewayExporterOptions = {},
|
|
297
|
-
): InstanceType<typeof OtlpExporter> | InstanceType<typeof LoadBalancingExporter> {
|
|
298
|
-
const { name = "gateway", loadBalance = false, resolver = "k8s", routingKey = "traceID",
|
|
351
|
+
): InstanceType<typeof OtlpExporter> | InstanceType<typeof OtlpHttpExporter> | InstanceType<typeof LoadBalancingExporter> {
|
|
352
|
+
const { name = "gateway", protocol = "grpc", loadBalance = false, resolver = "k8s", routingKey = "traceID", tls = { insecure: true } } = options;
|
|
353
|
+
const portName = options.port ?? (protocol === "http" ? "otlp-http" : "otlp-grpc");
|
|
354
|
+
if (protocol === "http" && loadBalance) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`gatewayExporter: the loadbalancing exporter sends OTLP over gRPC only; ` +
|
|
357
|
+
`use protocol "grpc" with loadBalance, or drop loadBalance to send OTLP over HTTP to the gateway's Service`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
299
360
|
const deploymentName = (gateway.deployment as unknown as { props: { metadata: { name: string } } }).props.metadata.name;
|
|
300
361
|
const target = serviceMeta(loadBalance ? gateway.headlessService : gateway.service);
|
|
301
362
|
const port = target.ports.find((p) => p.name === portName)?.port;
|
|
@@ -303,7 +364,7 @@ export function gatewayExporter(
|
|
|
303
364
|
const names = target.ports.map((p) => p.name).join(", ") || "none";
|
|
304
365
|
throw new Error(
|
|
305
366
|
`gatewayExporter: Service ${target.namespace}/${target.name} has no port named "${portName}" (it has: ${names}); ` +
|
|
306
|
-
`give the gateway an OTLP gRPC receiver or pass the port name`,
|
|
367
|
+
`give the gateway an OTLP ${protocol === "http" ? "HTTP" : "gRPC"} receiver or pass the port name`,
|
|
307
368
|
);
|
|
308
369
|
}
|
|
309
370
|
const routing: GatewayRouting = loadBalance ? "loadbalancing" : "service";
|
|
@@ -318,8 +379,25 @@ export function gatewayExporter(
|
|
|
318
379
|
? { dns: { hostname: `${target.name}.${target.namespace}.svc`, port: String(port) } }
|
|
319
380
|
: { k8s: { service: `${target.name}.${target.namespace}`, ports: [port] } },
|
|
320
381
|
})
|
|
321
|
-
:
|
|
382
|
+
: protocol === "http"
|
|
383
|
+
? otlpHttpExporter(name, `${target.name}.${target.namespace}.svc:${port}`, tls)
|
|
384
|
+
: new OtlpExporter({ name, endpoint: `${target.name}.${target.namespace}.svc:${port}`, tls });
|
|
322
385
|
|
|
323
386
|
markGatewayExporter(exporter, { name: deploymentName, namespace: target.namespace, routing });
|
|
324
387
|
return exporter;
|
|
325
388
|
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* An `otlphttp` exporter to `host:port`. Its endpoint is a URL, so TLS is the
|
|
392
|
+
* scheme: `insecure` gives `http://`, anything else `https://` with the
|
|
393
|
+
* remaining TLS settings (a CA, a client certificate) kept.
|
|
394
|
+
*/
|
|
395
|
+
function otlpHttpExporter(name: string, hostPort: string, tls: TLSClientSettings): InstanceType<typeof OtlpHttpExporter> {
|
|
396
|
+
const { insecure, ...rest } = tls;
|
|
397
|
+
const scheme = insecure ? "http" : "https";
|
|
398
|
+
return new OtlpHttpExporter({
|
|
399
|
+
name,
|
|
400
|
+
endpoint: `${scheme}://${hostPort}`,
|
|
401
|
+
...(Object.keys(rest).length ? { tls: rest } : {}),
|
|
402
|
+
});
|
|
403
|
+
}
|