From 4a14731ba1ad344de854c81bd6f0f0b77330103a Mon Sep 17 00:00:00 2001 From: tdakkota Date: Tue, 18 Aug 2026 12:19:54 +0300 Subject: [PATCH 1/2] feat(api): publish the admin API on an opt-in Service, and serve self-metrics The admin API is served in every storage pod (bind defaults to :8090, and the block defaults unconditionally) but nothing routed to it. Before #13 it was reachable by accident, under a port named "metrics"; moving self-metrics to 9464 took that with it. spec.admin now publishes it on a dedicated -admin Service, absent by default: the API triggers MaintainNow/CompactNow and StreamCosts, so it must not ride the client Service. The Service targets 8090 by number, leaving the pod template untouched. Also sets OTEL_METRICS_EXPORTER=prometheus. go-faster/sdk defaults it to otlp and only starts the /metrics server when it names prometheus, so 9464 was published and dead. This is a behaviour change: self-metrics previously left over OTLP only. --- README.md | 53 +++++++- api/v1alpha1/oteldbcluster_types.go | 33 +++++ internal/controller/admin.go | 65 ++++++++++ internal/controller/admin_test.go | 121 ++++++++++++++++++ internal/controller/naming.go | 15 ++- .../controller/oteldbcluster_controller.go | 20 +++ .../oteldbcluster_controller_test.go | 37 ++++++ internal/controller/resources.go | 6 + 8 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 internal/controller/admin.go create mode 100644 internal/controller/admin_test.go diff --git a/README.md b/README.md index 68a7df7..6a66792 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,7 @@ for a fuller example including the S3 backend. | `policy.recompress` | `{after, level}`. Rewrites fully-cold parts with a higher-ratio Zstandard profile. Decode-transparent and lossless. | | `policy.ec` | `{data, parity, after}`. Erasure-codes fully-cold parts across `data+parity` nodes instead of RF full copies — `{4,2}` is 1.5x the logical bytes for two tolerated node losses, against 3x for RF=3. See [Erasure coding](#erasure-coding). | | `service.type` / `annotations` | Client Service exposing the query/ingest APIs. | +| `admin` | Optional `-admin` Service publishing oteldb's admin API (`8090`). Absent ⇒ no Service; the API is still served inside every storage pod. See [Admin API](#admin-api). | | `resources`, `nodeSelector`, `affinity`, `tolerations`, `topologySpreadConstraints`, `podSecurityContext`, `securityContext`, `podAnnotations`, `podLabels`, `serviceAccountName` | Standard pod scheduling/security knobs. | | `extraConfig` | Arbitrary raw oteldb config **deep-merged** over the generated config — for fields the CRD does not model (auth, prometheus tuning, …). Nested objects merge key by key (`storage.policy` does not wipe `storage.backend`); operator-owned paths are [reserved](#reserved-extraconfig-paths). | @@ -278,7 +279,56 @@ Pyroscope and `9464` self-metrics, dropping any whose signal is disabled. `odbse its own listener, so these are published one-to-one; its health listener (`13133`) is probed, not published. -`8090` is deliberately not exposed: it is oteldb's admin API bind, not a self-metrics endpoint. +`8090` is oteldb's admin API bind, not a self-metrics endpoint. It is served in every storage pod +whether or not anything publishes it, and it is deliberately kept off the client Service — set +`spec.admin` to publish it on a Service of its own. See [Admin API](#admin-api). + +### Self-metrics + +Every pod exports its own metrics on `9464` in Prometheus format. The operator sets +`OTEL_METRICS_EXPORTER=prometheus` alongside `OTEL_EXPORTER_PROMETHEUS_HOST/PORT`, because +go-faster/sdk defaults that variable to `otlp` and only starts the `/metrics` server when it names +`prometheus` — without it the published port served nothing. + +> **Behaviour change.** Self-metrics previously left the pods over OTLP only (to the SDK's default +> endpoint), and `9464` was published but dead. They are now scraped from `9464` instead. If you +> were collecting oteldb's self-metrics via OTLP, point a scrape at the `metrics` port on the client, +> ingest and query Services. + +### Admin API + +oteldb's admin API (`8090`) reports build info, health, runtime and storage statistics, and it can +trigger the engine's maintenance and compaction passes and the per-stream storage-cost attribution +report — documented upstream as *the heaviest call the storage engine exposes*, since it decodes +every accounted byte column of every live part. + +`spec.admin` publishes it on a dedicated `-admin` Service. It is **opt-in and absent by +default**: + +```yaml +spec: + admin: + service: + type: ClusterIP # the default +``` + +Three notes: + +- **It is a separate Service, not a port on the client Service.** An endpoint that can start a + compaction should not ride the Service that PromQL and OTLP share, where an ingress or a broad + NetworkPolicy picks it up by default. A separate Service also keeps exposure and reachability + independent: admin can stay `ClusterIP` while the client Service is a `LoadBalancer`, and it + carries its own annotations. +- **Enabling it does not roll the pods.** oteldb registers the admin server unconditionally + (`cmd/oteldb/admin.go`), and its bind defaults to `:8090` whether or not the config declares the + block — so the listener already exists. The Service targets the port by number and the pod + template is untouched. +- **It has no auth of its own.** Treat the Service as privileged and restrict it with a + NetworkPolicy, or add oteldb's auth via `spec.extraConfig`. + +Before this, the admin port was *accidentally* reachable: it was published under a port named +`metrics`, and moving self-metrics to `9464` removed that exposure without anyone deciding it +should go. Neither state was a decision; `spec.admin` is. ## Getting Started @@ -318,4 +368,5 @@ Controller layout under `internal/controller/`: - `resources.go` — builds the ConfigMap, headless + client Services, and StatefulSet. - `ingest.go` / `query.go` — the stateless `odbingest` and `odbselect` pools: config, Service, Deployment. +- `admin.go` — the opt-in admin API Service. - `naming.go` — names, labels, ports. diff --git a/api/v1alpha1/oteldbcluster_types.go b/api/v1alpha1/oteldbcluster_types.go index 295c3b4..d8245a8 100644 --- a/api/v1alpha1/oteldbcluster_types.go +++ b/api/v1alpha1/oteldbcluster_types.go @@ -113,6 +113,12 @@ type OtelDBClusterSpec struct { // +optional Service ServiceSpec `json:"service,omitempty"` + // Admin optionally publishes oteldb's admin API on a Service of its own. Absent — the default — + // publishes nothing: the API is still served inside every storage pod, but no Service routes to + // it. + // +optional + Admin *AdminSpec `json:"admin,omitempty"` + // Resources are the compute resources for each oteldb container. // +optional Resources corev1.ResourceRequirements `json:"resources,omitempty"` @@ -778,6 +784,33 @@ type LimitsSpec struct { MaxPartSize *resource.Quantity `json:"maxPartSize,omitempty"` } +// AdminSpec publishes oteldb's admin API on a dedicated -admin Service. +// +// The API is not optional inside the pod: oteldb registers it unconditionally and its bind defaults +// to :8090, so every storage node is already serving it. What was missing is a way to reach it — +// before the self-metrics port moved to 9464 it happened to be published under a port named +// "metrics", and moving that port took the accidental exposure with it. Neither state was a +// decision; this field is. +// +// It is a separate Service, and opt-in, because of what the API can do: it triggers the engine's +// maintenance and compaction passes, and serves the stream-cost attribution report — documented +// upstream as the heaviest call the storage library exposes, decoding every accounted byte column +// of every live part. None of that belongs on the client Service that PromQL and OTLP share, where +// an ingress or a broad NetworkPolicy would pick it up by default. +// +// A separate Service also keeps the exposure decision separate from the reachability decision: the +// admin Service can stay ClusterIP while the client Service is a LoadBalancer, and it can carry its +// own annotations (auth proxy, internal-only load balancer) without touching client traffic. +// +// oteldb has no auth on the admin API beyond the global spec.extraConfig auth block, so treat this +// Service as privileged and restrict it with a NetworkPolicy. +type AdminSpec struct { + // Service configures the -admin Service. Its default type is ClusterIP, which is the + // intended shape: the admin API is an operator tool, not a client-facing endpoint. + // +optional + Service ServiceSpec `json:"service,omitempty"` +} + // ServiceSpec configures the client-facing Service. type ServiceSpec struct { // Type of the client Service. diff --git a/internal/controller/admin.go b/internal/controller/admin.go new file mode 100644 index 0000000..f20cee9 --- /dev/null +++ b/internal/controller/admin.go @@ -0,0 +1,65 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + dbv1alpha1 "github.com/oteldb/operator/api/v1alpha1" +) + +func adminEnabled(cr *dbv1alpha1.OtelDBCluster) bool { return cr.Spec.Admin != nil } + +// buildAdminService publishes oteldb's admin API, which every storage pod already serves on +// portAdmin, on a Service of its own. +// +// It targets the port by number rather than by name on purpose. oteldb registers the admin server +// unconditionally (cmd/oteldb/admin.go: "It is always registered", and its bind block defaults to +// :8090 whether or not the config declares it), so the listener exists regardless of this Service. +// Declaring a matching container port would therefore document nothing the pod does not already do, +// while changing the pod template — which would roll the whole StatefulSet on a change that only +// adds a Service. Toggling spec.admin stays a Service-only operation. +func buildAdminService(cr *dbv1alpha1.OtelDBCluster) *corev1.Service { + n := namesFor(cr) + svcType := cr.Spec.Admin.Service.Type + if svcType == "" { + svcType = corev1.ServiceTypeClusterIP + } + + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: n.adminService(), + Namespace: cr.Namespace, + // The component label separates this from the client Service, so a ServiceMonitor or a + // NetworkPolicy can select the admin endpoint without matching client traffic. + Labels: roleCommonLabels(cr, appName, valAdmin), + Annotations: cr.Spec.Admin.Service.Annotations, + }, + Spec: corev1.ServiceSpec{ + Type: svcType, + Selector: selectorLabels(cr), + Ports: []corev1.ServicePort{{ + Name: portNameAdmin, + Port: portAdmin, + TargetPort: intstr.FromInt32(portAdmin), + Protocol: corev1.ProtocolTCP, + }}, + }, + } +} diff --git a/internal/controller/admin_test.go b/internal/controller/admin_test.go new file mode 100644 index 0000000..3bc7e30 --- /dev/null +++ b/internal/controller/admin_test.go @@ -0,0 +1,121 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + + dbv1alpha1 "github.com/oteldb/operator/api/v1alpha1" +) + +func TestAdminDisabledByDefault(t *testing.T) { + require.False(t, adminEnabled(testCluster()), + "the admin API must not be published unless spec.admin asks for it") +} + +// The admin port must never reach the client Service: that is where PromQL and OTLP traffic lands, +// and the admin API can trigger maintenance and the stream-cost scan. +func TestClientServiceExcludesAdminPort(t *testing.T) { + for _, svc := range []*corev1.Service{ + buildClientService(testCluster()), + buildPeerService(testCluster()), + } { + for _, p := range svc.Spec.Ports { + require.NotEqual(t, int32(portAdmin), p.Port, "%s must not publish the admin API", svc.Name) + require.NotEqual(t, portNameAdmin, p.Name, "%s must not publish the admin API", svc.Name) + } + } +} + +func TestBuildAdminService(t *testing.T) { + cr := testCluster() + cr.Spec.Admin = &dbv1alpha1.AdminSpec{} + + svc := buildAdminService(cr) + require.Equal(t, "obs-admin", svc.Name) + require.Equal(t, "monitoring", svc.Namespace) + require.Equal(t, corev1.ServiceTypeClusterIP, svc.Spec.Type, + "the admin API is an operator tool, so it must default to a cluster-internal Service") + require.Equal(t, selectorLabels(cr), svc.Spec.Selector, + "the admin API is served by the storage pods") + require.Equal(t, "admin", svc.Labels["app.kubernetes.io/component"], + "the component label must separate the admin endpoint from client traffic") + + require.Len(t, svc.Spec.Ports, 1) + port := svc.Spec.Ports[0] + require.Equal(t, portNameAdmin, port.Name) + require.EqualValues(t, portAdmin, port.Port) + require.EqualValues(t, portAdmin, port.TargetPort.IntVal, + "the admin port is targeted by number: oteldb always serves it, so declaring a container "+ + "port would roll the StatefulSet for no behaviour change") +} + +func TestBuildAdminServiceOverrides(t *testing.T) { + cr := testCluster() + cr.Spec.Admin = &dbv1alpha1.AdminSpec{ + Service: dbv1alpha1.ServiceSpec{ + Type: corev1.ServiceTypeNodePort, + Annotations: map[string]string{"a": "b"}, + }, + } + + svc := buildAdminService(cr) + require.Equal(t, corev1.ServiceTypeNodePort, svc.Spec.Type) + require.Equal(t, map[string]string{"a": "b"}, svc.Annotations) +} + +// Enabling spec.admin must not touch the pod template: a Service is all that changes. +func TestAdminDoesNotRollPods(t *testing.T) { + off := testCluster() + on := testCluster() + on.Spec.Admin = &dbv1alpha1.AdminSpec{} + + require.Equal(t, + buildStatefulSet(off, "hash").Spec.Template, + buildStatefulSet(on, "hash").Spec.Template, + ) + + cfgOff, err := renderConfig(off, off.Spec.Etcd.Endpoints) + require.NoError(t, err) + cfgOn, err := renderConfig(on, on.Spec.Etcd.Endpoints) + require.NoError(t, err) + require.Equal(t, cfgOff, cfgOn, "spec.admin renders no config: oteldb serves the API either way") +} + +// 9464 is published on all three Services, so the exporter that serves it has to be selected: +// go-faster/sdk defaults OTEL_METRICS_EXPORTER to "otlp" and never starts the /metrics server. +func TestSelfMetricsExporterIsPrometheus(t *testing.T) { + cr := testCluster() + cr.Spec.Ingest = &dbv1alpha1.IngestSpec{} + cr.Spec.Query = &dbv1alpha1.QuerySpec{} + + for name, env := range map[string][]corev1.EnvVar{ + "storage": podEnv(cr), + "stateles": statelessPodEnv(cr), + } { + got := map[string]string{} + for _, e := range env { + got[e.Name] = e.Value + } + require.Equal(t, "prometheus", got[envMetricsExporter], "%s pods", name) + require.Equal(t, bindAllHost, got[envPrometheusHost], "%s pods", name) + require.Equal(t, "9464", got[envPrometheusPort], "%s pods", name) + } +} diff --git a/internal/controller/naming.go b/internal/controller/naming.go index 072b532..c0c5a90 100644 --- a/internal/controller/naming.go +++ b/internal/controller/naming.go @@ -33,6 +33,7 @@ const ( valStorage = "storage" // oteldb signal-backend value and component label valIngest = "ingest" // ingest component label valQuery = "query" // query component label + valAdmin = "admin" // admin Service component label defaultDataDir = "/var/lib/oteldb" // default storage.dir / WAL dir keyBind = "bind" // oteldb per-API bind config key // bindDisabled is odbselect's "do not serve this API" bind. An omitted block is not enough: @@ -44,7 +45,14 @@ const ( envAWSSecretAccessKey = "AWS_SECRET_ACCESS_KEY" envPrometheusHost = "OTEL_EXPORTER_PROMETHEUS_HOST" envPrometheusPort = "OTEL_EXPORTER_PROMETHEUS_PORT" - envLogLevel = "OTEL_LOG_LEVEL" + // envMetricsExporter selects go-faster/sdk's self-metrics exporter. It defaults to "otlp", and + // the Prometheus /metrics server is only started when this names "prometheus" — so without it + // the OTEL_EXPORTER_PROMETHEUS_HOST/PORT pair above configures a server that never runs. + envMetricsExporter = "OTEL_METRICS_EXPORTER" + // valMetricsExporter is what envMetricsExporter is set to, so the published self-metrics port + // actually serves. + valMetricsExporter = "prometheus" + envLogLevel = "OTEL_LOG_LEVEL" // annConfigHash carries the rendered config's digest on a pod template, so a config change rolls // the workload. @@ -108,6 +116,9 @@ const ( portSelfMetric = 9464 // portPeer is the default; the effective value comes from spec.cluster.peerPort. portPeer = 7946 + // portAdmin is oteldb's admin API bind (admin.bind defaults to :8090). Every storage pod serves + // it whether or not anything publishes it; spec.admin decides whether a Service does. + portAdmin = 8090 ) // Port names, shared by the container ports, the Services and the probes. @@ -122,6 +133,7 @@ const ( portNameSelfMetric = "metrics" portNameHealth = "health-check" portNamePeer = "peer" + portNameAdmin = "admin" // portNameIngestHTTP is odbingest's single HTTP listener: OTLP/HTTP, Prometheus remote write // and the health endpoints all share it (see cmd/odbingest/app.go, where otlp.Register and the // remote write handler are mounted on one mux bound to prometheus_remote_write.bind). @@ -153,6 +165,7 @@ func (n resourceNames) statefulSet() string { return n.base } func (n resourceNames) configMap() string { return n.base + "-config" } func (n resourceNames) peerService() string { return n.base + "-peers" } func (n resourceNames) clientService() string { return n.base } +func (n resourceNames) adminService() string { return n.base + "-admin" } func (n resourceNames) ingestDeployment() string { return n.base + "-ingest" } func (n resourceNames) ingestConfigMap() string { return n.base + "-ingest-config" } func (n resourceNames) ingestService() string { return n.base + "-ingest" } diff --git a/internal/controller/oteldbcluster_controller.go b/internal/controller/oteldbcluster_controller.go index 83356dd..56cb5b7 100644 --- a/internal/controller/oteldbcluster_controller.go +++ b/internal/controller/oteldbcluster_controller.go @@ -104,12 +104,32 @@ func (r *OtelDBClusterReconciler) reconcile(ctx context.Context, cr *dbv1alpha1. if err := r.apply(ctx, cr, buildStatefulSet(cr, hash)); err != nil { return fmt.Errorf("statefulset: %w", err) } + if err := r.reconcileAdmin(ctx, cr); err != nil { + return err + } if err := r.reconcileIngest(ctx, cr, endpoints); err != nil { return err } return r.reconcileQuery(ctx, cr, endpoints) } +// reconcileAdmin publishes the admin API's Service, or removes it when spec.admin is absent. Only +// the Service is conditional: oteldb serves the admin API in every storage pod either way. +func (r *OtelDBClusterReconciler) reconcileAdmin(ctx context.Context, cr *dbv1alpha1.OtelDBCluster) error { + n := namesFor(cr) + if !adminEnabled(cr) { + svc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: n.adminService(), Namespace: cr.Namespace}} + if err := r.Delete(ctx, svc); client.IgnoreNotFound(err) != nil { + return fmt.Errorf("delete admin service: %w", err) + } + return nil + } + if err := r.apply(ctx, cr, buildAdminService(cr)); err != nil { + return fmt.Errorf("admin service: %w", err) + } + return nil +} + // reconcileIngest applies the stateless odbingest pool, or removes it when spec.ingest is absent. func (r *OtelDBClusterReconciler) reconcileIngest(ctx context.Context, cr *dbv1alpha1.OtelDBCluster, endpoints []string) error { if !ingestEnabled(cr) { diff --git a/internal/controller/oteldbcluster_controller_test.go b/internal/controller/oteldbcluster_controller_test.go index 35102ae..ec8ecd3 100644 --- a/internal/controller/oteldbcluster_controller_test.go +++ b/internal/controller/oteldbcluster_controller_test.go @@ -99,5 +99,42 @@ var _ = Describe("OtelDBCluster Controller", func() { Expect(sts.Spec.ServiceName).To(Equal(resourceName + "-peers")) Expect(sts.OwnerReferences).NotTo(BeEmpty()) }) + + It("should publish and prune the admin Service with spec.admin", func() { + controllerReconciler := &OtelDBClusterReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + adminName := types.NamespacedName{Name: resourceName + "-admin", Namespace: resourceNamespace} + + By("not publishing the admin API by default") + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + Expect(errors.IsNotFound(k8sClient.Get(ctx, adminName, &corev1.Service{}))).To(BeTrue()) + + By("publishing it once spec.admin is set") + resource := &dbv1alpha1.OtelDBCluster{} + Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed()) + resource.Spec.Admin = &dbv1alpha1.AdminSpec{} + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + svc := &corev1.Service{} + Expect(k8sClient.Get(ctx, adminName, svc)).To(Succeed()) + Expect(svc.Spec.Ports).To(HaveLen(1)) + Expect(svc.Spec.Ports[0].Port).To(BeEquivalentTo(portAdmin)) + Expect(svc.OwnerReferences).NotTo(BeEmpty()) + + By("pruning it again when spec.admin is removed") + Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed()) + resource.Spec.Admin = nil + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + Expect(errors.IsNotFound(k8sClient.Get(ctx, adminName, &corev1.Service{}))).To(BeTrue()) + }) }) }) diff --git a/internal/controller/resources.go b/internal/controller/resources.go index 332da9b..14a09ff 100644 --- a/internal/controller/resources.go +++ b/internal/controller/resources.go @@ -208,10 +208,15 @@ func buildStatefulSet(cr *dbv1alpha1.OtelDBCluster, configHash string) *appsv1.S // podEnv builds the per-pod environment: self-observability plus the per-pod cluster identity // (id = pod name, addr = pod FQDN via the headless service). Kubernetes expands $(POD_NAME). +// +// The self-metrics exporter is named explicitly. go-faster/sdk defaults OTEL_METRICS_EXPORTER to +// "otlp" and only starts the Prometheus /metrics server when the variable names "prometheus", so +// setting the host and port alone published a port that served nothing. func podEnv(cr *dbv1alpha1.OtelDBCluster) []corev1.EnvVar { n := namesFor(cr) fqdnSuffix := fmt.Sprintf(".%s.%s.svc.cluster.local:%d", n.peerService(), cr.Namespace, peerPortOf(cr)) env := []corev1.EnvVar{ + {Name: envMetricsExporter, Value: valMetricsExporter}, {Name: envPrometheusHost, Value: bindAllHost}, {Name: envPrometheusPort, Value: fmt.Sprintf("%d", portSelfMetric)}, {Name: "POD_NAME", ValueFrom: &corev1.EnvVarSource{ @@ -288,6 +293,7 @@ func bindAll(port int32) string { return fmt.Sprintf("%s:%d", bindAllHost, port) // address or zone to advertise. func statelessPodEnv(cr *dbv1alpha1.OtelDBCluster) []corev1.EnvVar { env := []corev1.EnvVar{ + {Name: envMetricsExporter, Value: valMetricsExporter}, {Name: envPrometheusHost, Value: bindAllHost}, {Name: envPrometheusPort, Value: fmt.Sprintf("%d", portSelfMetric)}, } From a6db922aa97a9cb4ccc255b1e95c6ae16d0b3487 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Tue, 18 Aug 2026 12:20:01 +0300 Subject: [PATCH 2/2] chore(crd): regenerate manifests for spec.admin --- api/v1alpha1/zz_generated.deepcopy.go | 21 +++++++++++++++ .../bases/db.oteldb.io_oteldbclusters.yaml | 26 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 9703116..5c80b32 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -26,6 +26,22 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AdminSpec) DeepCopyInto(out *AdminSpec) { + *out = *in + in.Service.DeepCopyInto(&out.Service) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdminSpec. +func (in *AdminSpec) DeepCopy() *AdminSpec { + if in == nil { + return nil + } + out := new(AdminSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { *out = *in @@ -364,6 +380,11 @@ func (in *OtelDBClusterSpec) DeepCopyInto(out *OtelDBClusterSpec) { in.Engine.DeepCopyInto(&out.Engine) in.Policy.DeepCopyInto(&out.Policy) in.Service.DeepCopyInto(&out.Service) + if in.Admin != nil { + in, out := &in.Admin, &out.Admin + *out = new(AdminSpec) + (*in).DeepCopyInto(*out) + } in.Resources.DeepCopyInto(&out.Resources) if in.PodAnnotations != nil { in, out := &in.PodAnnotations, &out.PodAnnotations diff --git a/config/crd/bases/db.oteldb.io_oteldbclusters.yaml b/config/crd/bases/db.oteldb.io_oteldbclusters.yaml index b8db658..b1c7195 100644 --- a/config/crd/bases/db.oteldb.io_oteldbclusters.yaml +++ b/config/crd/bases/db.oteldb.io_oteldbclusters.yaml @@ -57,6 +57,32 @@ spec: spec: description: spec defines the desired state of OtelDBCluster properties: + admin: + description: |- + Admin optionally publishes oteldb's admin API on a Service of its own. Absent — the default — + publishes nothing: the API is still served inside every storage pod, but no Service routes to + it. + properties: + service: + description: |- + Service configures the -admin Service. Its default type is ClusterIP, which is the + intended shape: the admin API is an operator tool, not a client-facing endpoint. + properties: + annotations: + additionalProperties: + type: string + description: Annotations added to the client Service. + type: object + type: + default: ClusterIP + description: Type of the client Service. + enum: + - ClusterIP + - NodePort + - LoadBalancer + type: string + type: object + type: object affinity: description: |- Affinity for oteldb pods. When empty, the operator applies a soft anti-affinity that