From d1c49d0cf198fcc641e86bdc964528008fe1eadc Mon Sep 17 00:00:00 2001 From: Bastian Krol Date: Mon, 9 Sep 2024 15:19:52 +0200 Subject: [PATCH] feat(backendconnection): watch and reconcile OTel collector resources Watch the K8s resources for the OpenTelemetry collector daemonset and deployment which are managed by the operator, reconcile them back into their desired state if they have been changed externally. --- cmd/main.go | 18 +- go.mod | 8 +- go.sum | 10 +- .../backend_connection_controller.go | 159 +++++++++++ .../backendconnection_manager.go | 13 + .../backendconnection_manager_test.go | 20 +- .../otelcolresources/collector_config_maps.go | 4 +- .../otelcolresources/desired_state.go | 97 ++++--- .../otelcolresources/desired_state_test.go | 17 +- .../otelcolresources/otelcol_resources.go | 160 ++++++----- .../otelcol_resources_test.go | 139 ++++++--- internal/dash0/controller/dash0_controller.go | 30 +- .../dash0/controller/dash0_controller_test.go | 6 +- .../dash0/instrumentation/instrumenter.go | 8 +- .../dash0/selfmonitoring/self_monitoring.go | 31 +++ internal/dash0/util/controller.go | 4 +- test/util/collector.go | 263 ++++++++++++++---- 17 files changed, 723 insertions(+), 264 deletions(-) create mode 100644 internal/backendconnection/backend_connection_controller.go diff --git a/cmd/main.go b/cmd/main.go index 246478f..a308991 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -430,9 +430,19 @@ func startDash0Controllers( Clientset: clientset, OTelColResourceManager: oTelColResourceManager, } + backendConnectionReconciler := &backendconnection.BackendConnectionReconciler{ + Client: k8sClient, + BackendConnectionManager: backendConnectionManager, + Images: images, + OperatorNamespace: envVars.operatorNamespace, + OTelCollectorNamePrefix: envVars.oTelCollectorNamePrefix, + } + if err = backendConnectionReconciler.SetupWithManager(mgr); err != nil { + return fmt.Errorf("unable to set up the backend connection reconciler: %w", err) + } operatorConfigurationReconciler := &controller.OperatorConfigurationReconciler{ - Client: mgr.GetClient(), + Client: k8sClient, Clientset: clientset, Scheme: mgr.GetScheme(), Recorder: mgr.GetEventRecorderFor("dash0-operator-configuration-controller"), @@ -440,7 +450,7 @@ func startDash0Controllers( Images: images, DevelopmentMode: developmentMode, } - if err := operatorConfigurationReconciler.SetupWithManager(mgr); err != nil { + if err = operatorConfigurationReconciler.SetupWithManager(mgr); err != nil { return fmt.Errorf("unable to set up the operator configuration reconciler: %w", err) } operatorConfigurationReconciler.InitializeSelfMonitoringMetrics( @@ -457,7 +467,7 @@ func startDash0Controllers( Images: images, OperatorNamespace: envVars.operatorNamespace, } - if err := monitoringReconciler.SetupWithManager(mgr); err != nil { + if err = monitoringReconciler.SetupWithManager(mgr); err != nil { return fmt.Errorf("unable to set up the monitoring reconciler: %w", err) } monitoringReconciler.InitializeSelfMonitoringMetrics( @@ -467,7 +477,7 @@ func startDash0Controllers( ) if os.Getenv("ENABLE_WEBHOOK") != "false" { - if err := (&webhook.Handler{ + if err = (&webhook.Handler{ Client: k8sClient, Recorder: mgr.GetEventRecorderFor("dash0-webhook"), Images: images, diff --git a/go.mod b/go.mod index d6b3a68..e7aa6d2 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,14 @@ go 1.22.4 toolchain go1.22.6 require ( + github.com/cisco-open/k8s-objectmatcher v1.10.0 github.com/dash0hq/dash0-operator/images/pkg/common v0.0.0-00010101000000-000000000000 github.com/go-logr/logr v1.4.2 - github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 github.com/onsi/ginkgo/v2 v2.20.2 github.com/onsi/gomega v1.34.2 go.opentelemetry.io/collector/pdata v1.14.1 + go.opentelemetry.io/otel/metric v1.29.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.31.0 k8s.io/apimachinery v0.31.0 @@ -21,11 +22,13 @@ require ( ) require ( + emperror.dev/errors v0.8.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect + github.com/evanphx/json-patch v5.9.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect @@ -39,6 +42,7 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect @@ -59,7 +63,6 @@ require ( go.opentelemetry.io/otel v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0 // indirect - go.opentelemetry.io/otel/metric v1.29.0 // indirect go.opentelemetry.io/otel/sdk v1.29.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect @@ -79,6 +82,7 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/apiextensions-apiserver v0.31.0 // indirect diff --git a/go.sum b/go.sum index e105dbe..38ac86c 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,21 @@ +emperror.dev/errors v0.8.1 h1:UavXZ5cSX/4u9iyvH6aDcuGkVjeexUGJ7Ij7G4VfQT0= +emperror.dev/errors v0.8.1/go.mod h1:YcRvLPh626Ubn2xqtoprejnA5nFha+TJ+2vew48kWuE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cisco-open/k8s-objectmatcher v1.10.0 h1:1TdhMPqVaU+NqECqytAkRF1SFU0QIMqrqbNTnTl933A= +github.com/cisco-open/k8s-objectmatcher v1.10.0/go.mod h1:O/TFG3vW12jbKNQejpc8SGgSfujlaWYIOCZHcXeK514= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= -github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= +github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -121,8 +125,10 @@ go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt3 go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= diff --git a/internal/backendconnection/backend_connection_controller.go b/internal/backendconnection/backend_connection_controller.go new file mode 100644 index 0000000..11c389e --- /dev/null +++ b/internal/backendconnection/backend_connection_controller.go @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + +package backendconnection + +import ( + "context" + "slices" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + dash0v1alpha1 "github.com/dash0hq/dash0-operator/api/dash0monitoring/v1alpha1" + "github.com/dash0hq/dash0-operator/internal/backendconnection/otelcolresources" + "github.com/dash0hq/dash0-operator/internal/dash0/selfmonitoring" + "github.com/dash0hq/dash0-operator/internal/dash0/util" +) + +type BackendConnectionReconciler struct { + client.Client + BackendConnectionManager *BackendConnectionManager + Images util.Images + OperatorNamespace string + OTelCollectorNamePrefix string +} + +func (r *BackendConnectionReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + Named("dash0backendconnectioncontroller"). + Watches( + &corev1.ConfigMap{}, + &handler.EnqueueRequestForObject{}, + r.withNamePredicate([]string{ + otelcolresources.DaemonSetCollectorConfigConfigMapName(r.OTelCollectorNamePrefix), + otelcolresources.DeploymentCollectorConfigConfigMapName(r.OTelCollectorNamePrefix), + // Note: We are deliberately not watching the filelog receiver offsets ConfigMap, since it is updated + // frequently by the filelog offset synch container and does not require reconciliation. + })). + Watches( + &rbacv1.ClusterRole{}, + &handler.EnqueueRequestForObject{}, + r.withNamePredicate([]string{ + otelcolresources.DaemonSetClusterRoleName(r.OTelCollectorNamePrefix), + otelcolresources.DeploymentClusterRoleName(r.OTelCollectorNamePrefix), + })). + Watches( + &rbacv1.ClusterRoleBinding{}, + &handler.EnqueueRequestForObject{}, + r.withNamePredicate([]string{ + otelcolresources.DeploymentClusterRoleBindingName(r.OTelCollectorNamePrefix), + otelcolresources.DeploymentClusterRoleName(r.OTelCollectorNamePrefix), + })). + Watches( + &corev1.Service{}, + &handler.EnqueueRequestForObject{}, + r.withNamePredicate([]string{ + otelcolresources.ServiceName(r.OTelCollectorNamePrefix), + })). + Watches( + &appsv1.DaemonSet{}, + &handler.EnqueueRequestForObject{}, + r.withNamePredicate([]string{ + otelcolresources.DaemonSetName(r.OTelCollectorNamePrefix), + })). + Watches( + &appsv1.Deployment{}, + &handler.EnqueueRequestForObject{}, + r.withNamePredicate([]string{ + otelcolresources.DeploymentName(r.OTelCollectorNamePrefix), + })). + Complete(r) +} + +func (r *BackendConnectionReconciler) withNamePredicate(resourceNames []string) builder.Predicates { + return builder.WithPredicates(r.createFilterPredicate(resourceNames)) +} + +func (r *BackendConnectionReconciler) createFilterPredicate(resourceNames []string) predicate.Funcs { + resourceNamespace := r.OperatorNamespace + return predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return resourceMatches(e.Object, resourceNamespace, resourceNames) + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return resourceMatches(e.ObjectOld, resourceNamespace, resourceNames) || + resourceMatches(e.ObjectNew, resourceNamespace, resourceNames) + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return resourceMatches(e.Object, resourceNamespace, resourceNames) + }, + GenericFunc: func(e event.GenericEvent) bool { + return resourceMatches(e.Object, resourceNamespace, resourceNames) + }, + } +} + +func resourceMatches(object client.Object, resourceNamespace string, resourceNames []string) bool { + if object.GetNamespace() != resourceNamespace { + return false + } + return slices.Contains(resourceNames, object.GetName()) +} + +func (r *BackendConnectionReconciler) Reconcile( + ctx context.Context, + request reconcile.Request, +) (reconcile.Result, error) { + logger := log.FromContext(ctx) + logger.Info("reconciling backend connection resources", "request", request) + + allDash0MonitoringResouresInCluster := &dash0v1alpha1.Dash0MonitoringList{} + if err := r.List( + ctx, + allDash0MonitoringResouresInCluster, + &client.ListOptions{}, + ); err != nil { + logger.Error(err, "Failed to list all Dash0 monitoring resources when reconciling backend connection resources.") + return reconcile.Result{}, err + } + + if len(allDash0MonitoringResouresInCluster.Items) == 0 { + logger.Info("No Dash0 monitoring resources in cluster, aborting the backend connection resources reconciliation.") + return reconcile.Result{}, nil + } + + // TODO this needs to be fixed when we start to support sending telemetry to different backends per namespace. + // Ultimately we need to derive one consistent configuration including multiple pipelines and routing across all + // monitored namespaces. + arbitraryMonitoringResource := allDash0MonitoringResouresInCluster.Items[0] + + err := r.BackendConnectionManager.EnsureOpenTelemetryCollectorIsDeployedInOperatorNamespace( + ctx, + r.Images, + r.OperatorNamespace, + &arbitraryMonitoringResource, + selfmonitoring.ReadSelfMonitoringConfigurationFromOperatorConfigurationResource(ctx, r.Client, &logger), + ) + if err != nil { + logger.Error(err, "Failed to create/update backend connection resources.") + return reconcile.Result{}, err + } + + logger.Info( + "successfully reconciled backend connection resources", + "request", + request, + ) + + return reconcile.Result{}, nil +} diff --git a/internal/backendconnection/backendconnection_manager.go b/internal/backendconnection/backendconnection_manager.go index 04d998e..b8d5be5 100644 --- a/internal/backendconnection/backendconnection_manager.go +++ b/internal/backendconnection/backendconnection_manager.go @@ -21,6 +21,7 @@ type BackendConnectionManager struct { client.Client Clientset *kubernetes.Clientset *otelcolresources.OTelColResourceManager + updateInProgress bool } func (m *BackendConnectionManager) EnsureOpenTelemetryCollectorIsDeployedInOperatorNamespace( @@ -31,6 +32,18 @@ func (m *BackendConnectionManager) EnsureOpenTelemetryCollectorIsDeployedInOpera selfMonitoringConfiguration selfmonitoring.SelfMonitoringConfiguration, ) error { logger := log.FromContext(ctx) + if m.updateInProgress { + if m.DevelopmentMode { + logger.Info("creation/update of the OpenTelemetry collector resources is already in progress, skipping " + + "additional reconciliation request.") + } + return nil + } + + m.updateInProgress = true + defer func() { + m.updateInProgress = false + }() resourcesHaveBeenCreated, resourcesHaveBeenUpdated, err := m.OTelColResourceManager.CreateOrUpdateOpenTelemetryCollectorResources( diff --git a/internal/backendconnection/backendconnection_manager_test.go b/internal/backendconnection/backendconnection_manager_test.go index 9ad66c9..0195c43 100644 --- a/internal/backendconnection/backendconnection_manager_test.go +++ b/internal/backendconnection/backendconnection_manager_test.go @@ -142,7 +142,7 @@ var _ = Describe("The backend connection manager", Ordered, func() { selfmonitoring.SelfMonitoringConfiguration{}, ) Expect(err).ToNot(HaveOccurred()) - VerifyCollectorResourcesExist(ctx, k8sClient, operatorNamespace) + VerifyCollectorResources(ctx, k8sClient, operatorNamespace) }) }) @@ -150,7 +150,7 @@ var _ = Describe("The backend connection manager", Ordered, func() { It("should update the resources", func() { err := k8sClient.Create(ctx, &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: ExpectedConfigMapName, + Name: ExpectedDaemonSetCollectorConfigMapName, Namespace: operatorNamespace, Labels: map[string]string{ "wrong-key": "value", @@ -173,10 +173,10 @@ var _ = Describe("The backend connection manager", Ordered, func() { selfmonitoring.SelfMonitoringConfiguration{}, ) Expect(err).ToNot(HaveOccurred()) - VerifyCollectorResourcesExist(ctx, k8sClient, operatorNamespace) + VerifyCollectorResources(ctx, k8sClient, operatorNamespace) // verify that all wrong properties that we have set up initially have been removed - cm := VerifyCollectorConfigMapExists(ctx, k8sClient, operatorNamespace) + cm := GetOTelColDaemonSetConfigMap(ctx, k8sClient, operatorNamespace) Expect(cm.Data).To(HaveKey("config.yaml")) Expect(cm.Data).ToNot(HaveKey("wrong-key")) Expect(cm.Labels).ToNot(HaveKey("wrong-key")) @@ -208,7 +208,7 @@ var _ = Describe("The backend connection manager", Ordered, func() { selfmonitoring.SelfMonitoringConfiguration{}, ) Expect(err).ToNot(HaveOccurred()) - VerifyCollectorResourcesExist(ctx, k8sClient, operatorNamespace) + VerifyCollectorResources(ctx, k8sClient, operatorNamespace) err = manager.RemoveOpenTelemetryCollectorIfNoMonitoringResourceIsLeft( ctx, @@ -219,7 +219,7 @@ var _ = Describe("The backend connection manager", Ordered, func() { ) Expect(err).ToNot(HaveOccurred()) // since other Dash0 monitoring resources still exist, the collector resources should not be deleted - VerifyCollectorResourcesExist(ctx, k8sClient, operatorNamespace) + VerifyCollectorResources(ctx, k8sClient, operatorNamespace) }) It("should not delete the collector if there is only one Dash0 monitoring resource left but it is not the one being deleted", func() { @@ -236,7 +236,7 @@ var _ = Describe("The backend connection manager", Ordered, func() { selfmonitoring.SelfMonitoringConfiguration{}, ) Expect(err).ToNot(HaveOccurred()) - VerifyCollectorResourcesExist(ctx, k8sClient, operatorNamespace) + VerifyCollectorResources(ctx, k8sClient, operatorNamespace) err = manager.RemoveOpenTelemetryCollectorIfNoMonitoringResourceIsLeft( ctx, @@ -265,7 +265,7 @@ var _ = Describe("The backend connection manager", Ordered, func() { selfmonitoring.SelfMonitoringConfiguration{}, ) Expect(err).ToNot(HaveOccurred()) - VerifyCollectorResourcesExist(ctx, k8sClient, operatorNamespace) + VerifyCollectorResources(ctx, k8sClient, operatorNamespace) }) It("should delete the collector if the Dash0 monitoring resource that is being deleted is the only one left", func() { @@ -283,7 +283,7 @@ var _ = Describe("The backend connection manager", Ordered, func() { selfmonitoring.SelfMonitoringConfiguration{}, ) Expect(err).ToNot(HaveOccurred()) - VerifyCollectorResourcesExist(ctx, k8sClient, operatorNamespace) + VerifyCollectorResources(ctx, k8sClient, operatorNamespace) err = manager.RemoveOpenTelemetryCollectorIfNoMonitoringResourceIsLeft( ctx, @@ -308,7 +308,7 @@ var _ = Describe("The backend connection manager", Ordered, func() { selfmonitoring.SelfMonitoringConfiguration{}, ) Expect(err).ToNot(HaveOccurred()) - VerifyCollectorResourcesExist(ctx, k8sClient, operatorNamespace) + VerifyCollectorResources(ctx, k8sClient, operatorNamespace) err = manager.RemoveOpenTelemetryCollectorIfNoMonitoringResourceIsLeft( ctx, diff --git a/internal/backendconnection/otelcolresources/collector_config_maps.go b/internal/backendconnection/otelcolresources/collector_config_maps.go index 15deaa1..90a2cab 100644 --- a/internal/backendconnection/otelcolresources/collector_config_maps.go +++ b/internal/backendconnection/otelcolresources/collector_config_maps.go @@ -41,7 +41,7 @@ func assembleDaemonSetCollectorConfigMap(config *oTelColConfig) (*corev1.ConfigM return assembleCollectorConfigMap( config, daemonSetCollectorConfigurationTemplate, - daemonSetCollectorConfigConfigMapName(config.NamePrefix), + DaemonSetCollectorConfigConfigMapName(config.NamePrefix), ) } @@ -49,7 +49,7 @@ func assembleDeploymentCollectorConfigMap(config *oTelColConfig) (*corev1.Config return assembleCollectorConfigMap( config, deploymentCollectorConfigurationTemplate, - deploymentCollectorConfigConfigMapName(config.NamePrefix), + DeploymentCollectorConfigConfigMapName(config.NamePrefix), ) } diff --git a/internal/backendconnection/otelcolresources/desired_state.go b/internal/backendconnection/otelcolresources/desired_state.go index cb29461..10f8b45 100644 --- a/internal/backendconnection/otelcolresources/desired_state.go +++ b/internal/backendconnection/otelcolresources/desired_state.go @@ -6,6 +6,7 @@ package otelcolresources import ( "fmt" "path/filepath" + "strings" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -207,7 +208,7 @@ func assembleFilelogOffsetsConfigMap(config *oTelColConfig) *corev1.ConfigMap { APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: filelogReceiverOffsetsConfigMapName(config.NamePrefix), + Name: FilelogReceiverOffsetsConfigMapName(config.NamePrefix), Namespace: config.Namespace, Labels: labels(false), }, @@ -242,7 +243,7 @@ func assembleRoleBinding(config *oTelColConfig) *rbacv1.RoleBinding { APIVersion: rbacApiVersion, }, ObjectMeta: metav1.ObjectMeta{ - Name: name(config.NamePrefix, openTelemetryCollector), + Name: roleBindingName(config.NamePrefix), Namespace: config.Namespace, Labels: labels(false), }, @@ -266,9 +267,8 @@ func assembleClusterRoleForDaemonSet(config *oTelColConfig) *rbacv1.ClusterRole APIVersion: rbacApiVersion, }, ObjectMeta: metav1.ObjectMeta{ - Name: daemonSetClusterRoleName(config.NamePrefix), - Namespace: config.Namespace, - Labels: labels(false), + Name: DaemonSetClusterRoleName(config.NamePrefix), + Labels: labels(false), }, Rules: []rbacv1.PolicyRule{ { @@ -309,14 +309,13 @@ func assembleClusterRoleBindingForDaemonSet(config *oTelColConfig) *rbacv1.Clust APIVersion: rbacApiVersion, }, ObjectMeta: metav1.ObjectMeta{ - Name: name(config.NamePrefix, openTelemetryCollector), - Namespace: config.Namespace, - Labels: labels(false), + Name: DaemonSetClusterRoleBindingName(config.NamePrefix), + Labels: labels(false), }, RoleRef: rbacv1.RoleRef{ APIGroup: rbacApiGroup, Kind: "ClusterRole", - Name: daemonSetClusterRoleName(config.NamePrefix), + Name: DaemonSetClusterRoleName(config.NamePrefix), }, Subjects: []rbacv1.Subject{{ Kind: "ServiceAccount", @@ -333,7 +332,7 @@ func assembleService(config *oTelColConfig) *corev1.Service { APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: name(config.NamePrefix, openTelemetryCollector), + Name: ServiceName(config.NamePrefix), Namespace: config.Namespace, Labels: serviceLabels(), }, @@ -376,7 +375,7 @@ func assembleCollectorDaemonSet(config *oTelColConfig) (*appsv1.DaemonSet, error APIVersion: "apps/v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: name(config.NamePrefix, openTelemetryCollectorDaemonSetNameSuffix), + Name: DaemonSetName(config.NamePrefix), Namespace: config.Namespace, Labels: labels(true), }, @@ -442,7 +441,7 @@ func assembleFileLogOffsetSynchContainer(config *oTelColConfig) corev1.Container }, { Name: "K8S_CONFIGMAP_NAME", - Value: filelogReceiverOffsetsConfigMapName(config.NamePrefix), + Value: FilelogReceiverOffsetsConfigMapName(config.NamePrefix), }, { @@ -501,7 +500,7 @@ func assembleCollectorDaemonSetVolumes( VolumeSource: corev1.VolumeSource{ ConfigMap: &corev1.ConfigMapVolumeSource{ LocalObjectReference: corev1.LocalObjectReference{ - Name: daemonSetCollectorConfigConfigMapName(config.NamePrefix), + Name: DaemonSetCollectorConfigConfigMapName(config.NamePrefix), }, Items: configMapItems, }, @@ -667,7 +666,7 @@ func assembleFileLogOffsetSynchInitContainer(config *oTelColConfig) corev1.Conta }, { Name: "K8S_CONFIGMAP_NAME", - Value: filelogReceiverOffsetsConfigMapName(config.NamePrefix), + Value: FilelogReceiverOffsetsConfigMapName(config.NamePrefix), }, { @@ -711,9 +710,8 @@ func assembleClusterRoleForDeployment(config *oTelColConfig) *rbacv1.ClusterRole APIVersion: rbacApiVersion, }, ObjectMeta: metav1.ObjectMeta{ - Name: deploymentClusterRoleName(config.NamePrefix), - Namespace: config.Namespace, - Labels: labels(false), + Name: DeploymentClusterRoleName(config.NamePrefix), + Labels: labels(false), }, Rules: []rbacv1.PolicyRule{ { @@ -798,14 +796,13 @@ func assembleClusterRoleBindingForDeployment(config *oTelColConfig) *rbacv1.Clus APIVersion: rbacApiVersion, }, ObjectMeta: metav1.ObjectMeta{ - Name: name(config.NamePrefix, openTelemetryCollectorDeploymentNameSuffix), - Namespace: config.Namespace, - Labels: labels(false), + Name: DeploymentClusterRoleBindingName(config.NamePrefix), + Labels: labels(false), }, RoleRef: rbacv1.RoleRef{ APIGroup: rbacApiGroup, Kind: "ClusterRole", - Name: deploymentClusterRoleName(config.NamePrefix), + Name: DeploymentClusterRoleName(config.NamePrefix), }, Subjects: []rbacv1.Subject{{ Kind: "ServiceAccount", @@ -827,7 +824,7 @@ func assembleCollectorDeployment(config *oTelColConfig) (*appsv1.Deployment, err APIVersion: "apps/v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: name(config.NamePrefix, openTelemetryCollectorDeploymentNameSuffix), + Name: DeploymentName(config.NamePrefix), Namespace: config.Namespace, Labels: labels(true), }, @@ -886,7 +883,7 @@ func assembleCollectorDeploymentVolumes( VolumeSource: corev1.VolumeSource{ ConfigMap: &corev1.ConfigMapVolumeSource{ LocalObjectReference: corev1.LocalObjectReference{ - Name: deploymentCollectorConfigConfigMapName(config.NamePrefix), + Name: DeploymentCollectorConfigConfigMapName(config.NamePrefix), }, Items: configMapItems, }, @@ -937,35 +934,51 @@ func assembleDeploymentCollectorContainer( } func daemonsetServiceAccountName(namePrefix string) string { - return name(namePrefix, openTelemetryCollector) + return name(namePrefix, openTelemetryCollector, "sa") } func deploymentServiceAccountName(namePrefix string) string { - return name(namePrefix, openTelemetryCollectorDeploymentNameSuffix) + return name(namePrefix, openTelemetryCollectorDeploymentNameSuffix, "sa") +} + +func FilelogReceiverOffsetsConfigMapName(namePrefix string) string { + return name(namePrefix, "filelogoffsets", "cm") } -func filelogReceiverOffsetsConfigMapName(namePrefix string) string { - return name(namePrefix, "filelogoffsets") +func DaemonSetCollectorConfigConfigMapName(namePrefix string) string { + return name(namePrefix, openTelemetryCollectorDaemonSetNameSuffix, "cm") } -func daemonSetCollectorConfigConfigMapName(namePrefix string) string { - return name(namePrefix, openTelemetryCollectorDaemonSetNameSuffix) +func DeploymentCollectorConfigConfigMapName(namePrefix string) string { + return name(namePrefix, openTelemetryCollectorDeploymentNameSuffix, "cm") } -func deploymentCollectorConfigConfigMapName(namePrefix string) string { - return name(namePrefix, openTelemetryCollectorDeploymentNameSuffix) +func DaemonSetClusterRoleName(namePrefix string) string { + return name(namePrefix, openTelemetryCollector, "cr") } -func daemonSetClusterRoleName(namePrefix string) string { - return name(namePrefix, openTelemetryCollector) +func DeploymentClusterRoleName(namePrefix string) string { + return name(namePrefix, openTelemetryCollectorDeploymentNameSuffix, "cr") } -func deploymentClusterRoleName(namePrefix string) string { - return name(namePrefix, openTelemetryCollectorDeploymentNameSuffix) +func DaemonSetClusterRoleBindingName(namePrefix string) string { + return name(namePrefix, openTelemetryCollector, "crb") +} + +func DeploymentClusterRoleBindingName(namePrefix string) string { + return name(namePrefix, openTelemetryCollectorDeploymentNameSuffix, "crb") } func roleName(namePrefix string) string { - return name(namePrefix, openTelemetryCollector) + return name(namePrefix, openTelemetryCollector, "role") +} + +func roleBindingName(namePrefix string) string { + return name(namePrefix, openTelemetryCollector, "rolebinding") +} + +func ServiceName(namePrefix string) string { + return name(namePrefix, openTelemetryCollector, "service") } func serviceLabels() map[string]string { @@ -974,8 +987,16 @@ func serviceLabels() map[string]string { return lbls } -func name(prefix string, suffix string) string { - return fmt.Sprintf("%s-%s", prefix, suffix) +func DaemonSetName(namePrefix string) string { + return name(namePrefix, openTelemetryCollectorDaemonSetNameSuffix, "daemonset") +} + +func DeploymentName(namePrefix string) string { + return name(namePrefix, openTelemetryCollectorDeploymentNameSuffix, "deployment") +} + +func name(prefix string, parts ...string) string { + return strings.Join(append([]string{prefix}, parts...), "-") } func labels(addOptOutLabel bool) map[string]string { diff --git a/internal/backendconnection/otelcolresources/desired_state_test.go b/internal/backendconnection/otelcolresources/desired_state_test.go index b68a51c..9c6ad22 100644 --- a/internal/backendconnection/otelcolresources/desired_state_test.go +++ b/internal/backendconnection/otelcolresources/desired_state_test.go @@ -6,7 +6,6 @@ package otelcolresources import ( "fmt" "reflect" - "strings" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -72,7 +71,7 @@ var _ = Describe("The desired state of the OpenTelemetry Collector resources", f configMapVolume := findVolumeByName(podSpec.Volumes, "opentelemetry-collector-configmap") Expect(configMapVolume).NotTo(BeNil()) Expect(configMapVolume.VolumeSource.ConfigMap.LocalObjectReference.Name). - To(Equal("unit-test-opentelemetry-collector-agent")) + To(Equal(ExpectedDaemonSetCollectorConfigMapName)) Expect(findVolumeMountByName(findContainerByName(podSpec.Containers, "opentelemetry-collector").VolumeMounts, "opentelemetry-collector-configmap")).NotTo(BeNil()) Expect(findVolumeMountByName(findContainerByName(podSpec.Containers, "configuration-reloader").VolumeMounts, "opentelemetry-collector-configmap")).NotTo(BeNil()) @@ -126,7 +125,7 @@ var _ = Describe("The desired state of the OpenTelemetry Collector resources", f configMapVolume = findVolumeByName(podSpec.Volumes, "opentelemetry-collector-configmap") Expect(configMapVolume).NotTo(BeNil()) Expect(configMapVolume.VolumeSource.ConfigMap.LocalObjectReference.Name). - To(Equal("unit-test-cluster-metrics-collector")) + To(Equal(ExpectedDeploymentCollectorConfigMapName)) Expect(findVolumeMountByName(findContainerByName(podSpec.Containers, "opentelemetry-collector").VolumeMounts, "opentelemetry-collector-configmap")).NotTo(BeNil()) Expect(findVolumeMountByName(findContainerByName(podSpec.Containers, "configuration-reloader").VolumeMounts, "opentelemetry-collector-configmap")).NotTo(BeNil()) @@ -265,9 +264,9 @@ var _ = Describe("The desired state of the OpenTelemetry Collector resources", f }) }) -func getConfigMap(desiredState []client.Object, matcher func(c *corev1.ConfigMap) bool) *corev1.ConfigMap { +func getConfigMap(desiredState []client.Object, name string) *corev1.ConfigMap { for _, object := range desiredState { - if cm, ok := object.(*corev1.ConfigMap); ok && matcher(cm) { + if cm, ok := object.(*corev1.ConfigMap); ok && cm.Name == name { return cm } } @@ -275,16 +274,12 @@ func getConfigMap(desiredState []client.Object, matcher func(c *corev1.ConfigMap } func getCollectorConfigConfigMapContent(desiredState []client.Object) string { - cm := getConfigMap(desiredState, func(c *corev1.ConfigMap) bool { - return strings.HasSuffix(c.Name, "-opentelemetry-collector-agent") - }) + cm := getConfigMap(desiredState, ExpectedDaemonSetCollectorConfigMapName) return cm.Data["config.yaml"] } func getFileOffsetConfigMapContent(desiredState []client.Object) string { - cm := getConfigMap(desiredState, func(c *corev1.ConfigMap) bool { - return strings.HasSuffix(c.Name, "-filelogoffsets") - }) + cm := getConfigMap(desiredState, ExpectedDaemonSetFilelogOffsetSynchConfigMapName) return cm.Data["config.yaml"] } diff --git a/internal/backendconnection/otelcolresources/otelcol_resources.go b/internal/backendconnection/otelcolresources/otelcol_resources.go index 51d1db8..32bed78 100644 --- a/internal/backendconnection/otelcolresources/otelcol_resources.go +++ b/internal/backendconnection/otelcolresources/otelcol_resources.go @@ -6,10 +6,11 @@ package otelcolresources import ( "context" "errors" - "reflect" + "fmt" + "slices" + "github.com/cisco-open/k8s-objectmatcher/patch" "github.com/go-logr/logr" - "github.com/google/go-cmp/cmp" appsv1 "k8s.io/api/apps/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -32,6 +33,14 @@ type OTelColResourceManager struct { DevelopmentMode bool } +const ( + bogusDeploymentPatch = "{\"spec\":{\"strategy\":{\"$retainKeys\":[\"type\"],\"rollingUpdate\":null}}}" +) + +var ( + knownIrrelevantPatches = []string{bogusDeploymentPatch} +) + func (m *OTelColResourceManager) CreateOrUpdateOpenTelemetryCollectorResources( ctx context.Context, namespace string, @@ -74,26 +83,26 @@ func (m *OTelColResourceManager) CreateOrUpdateOpenTelemetryCollectorResources( func (m *OTelColResourceManager) createOrUpdateResource( ctx context.Context, - desiredObject client.Object, + desiredResource client.Object, logger *logr.Logger, ) (bool, bool, error) { - existingObject, err := m.createEmptyReceiverFor(desiredObject) + existingObject, err := m.createEmptyReceiverFor(desiredResource) if err != nil { return false, false, err } - err = m.Client.Get(ctx, client.ObjectKeyFromObject(desiredObject), existingObject) + err = m.Client.Get(ctx, client.ObjectKeyFromObject(desiredResource), existingObject) if err != nil { if !apierrors.IsNotFound(err) { return false, false, err } - err = m.createResource(ctx, desiredObject, logger) + err = m.createResource(ctx, desiredResource, logger) if err != nil { return false, false, err } return true, false, nil } else { // object needs to be updated - hasChanged, err := m.updateResource(ctx, existingObject, desiredObject, logger) + hasChanged, err := m.updateResource(ctx, existingObject, desiredResource, logger) if err != nil { return false, false, err } @@ -101,8 +110,8 @@ func (m *OTelColResourceManager) createOrUpdateResource( } } -func (m *OTelColResourceManager) createEmptyReceiverFor(desiredObject client.Object) (client.Object, error) { - objectKind := desiredObject.GetObjectKind() +func (m *OTelColResourceManager) createEmptyReceiverFor(desiredResource client.Object) (client.Object, error) { + objectKind := desiredResource.GetObjectKind() gvk := schema.GroupVersionKind{ Group: objectKind.GroupVersionKind().Group, Version: objectKind.GroupVersionKind().Version, @@ -117,79 +126,98 @@ func (m *OTelColResourceManager) createEmptyReceiverFor(desiredObject client.Obj func (m *OTelColResourceManager) createResource( ctx context.Context, - desiredObject client.Object, + desiredResource client.Object, logger *logr.Logger, ) error { - if err := m.setOwnerReference(desiredObject, logger); err != nil { + if err := m.setOwnerReference(desiredResource, logger); err != nil { + return err + } + if err := patch.DefaultAnnotator.SetLastAppliedAnnotation(desiredResource); err != nil { return err } - err := m.Client.Create(ctx, desiredObject) + err := m.Client.Create(ctx, desiredResource) if err != nil { return err } - logger.Info( - "created resource", - "name", - desiredObject.GetName(), - "namespace", - desiredObject.GetNamespace(), - "kind", - desiredObject.GetObjectKind().GroupVersionKind(), - ) + logger.Info(fmt.Sprintf( + "created resource %s/%s", + desiredResource.GetNamespace(), + desiredResource.GetName(), + )) return nil } func (m *OTelColResourceManager) updateResource( ctx context.Context, existingObject client.Object, - desiredObject client.Object, + desiredResource client.Object, logger *logr.Logger, ) (bool, error) { - logger.Info( - "updating resource", - "name", - desiredObject.GetName(), - "namespace", - desiredObject.GetNamespace(), - "kind", - desiredObject.GetObjectKind().GroupVersionKind(), - ) - if err := m.setOwnerReference(desiredObject, logger); err != nil { + if m.DevelopmentMode { + logger.Info(fmt.Sprintf( + "checking whether resource %s/%s requires update", + desiredResource.GetNamespace(), + desiredResource.GetName(), + )) + } + + if err := m.setOwnerReference(desiredResource, logger); err != nil { return false, err } - err := m.Client.Update(ctx, desiredObject) + + patchResult, err := patch.DefaultPatchMaker.Calculate( + existingObject, + desiredResource, + patch.IgnoreField("kind"), + patch.IgnoreField("apiVersion"), + ) if err != nil { return false, err } - updatedObject, err := m.createEmptyReceiverFor(desiredObject) - if err != nil { + hasChanged := !patchResult.IsEmpty() && !isKnownIrrelevantPatch(patchResult) + if !hasChanged { + if m.DevelopmentMode { + logger.Info(fmt.Sprintf("resource %s/%s is already up to date", + desiredResource.GetNamespace(), + desiredResource.GetName(), + )) + } + return false, nil + } + + if err = patch.DefaultAnnotator.SetLastAppliedAnnotation(desiredResource); err != nil { return false, err } - err = m.Client.Get(ctx, client.ObjectKeyFromObject(desiredObject), updatedObject) + + err = m.Client.Update(ctx, desiredResource) if err != nil { return false, err } - hasChanged := !reflect.DeepEqual(existingObject, updatedObject) - if hasChanged { - logger.Info( - "updated resource", - "name", - desiredObject.GetName(), - "namespace", - desiredObject.GetNamespace(), - "kind", - desiredObject.GetObjectKind().GroupVersionKind(), - "diff", - cmp.Diff(existingObject, updatedObject), - ) + + if m.DevelopmentMode { + logger.Info(fmt.Sprintf( + "resource %s/%s was out of sync and has been reconciled", + desiredResource.GetNamespace(), + desiredResource.GetName(), + )) } + return hasChanged, nil } +func isKnownIrrelevantPatch(patchResult *patch.PatchResult) bool { + patch := string(patchResult.Patch) + return slices.Contains(knownIrrelevantPatches, patch) +} + func (m *OTelColResourceManager) setOwnerReference( object client.Object, logger *logr.Logger, ) error { + if object.GetNamespace() == "" { + // cluster scoped resources like ClusterRole and ClusterRoleBinding cannot have a namespace-scoped owner. + return nil + } if err := controllerutil.SetControllerReference(&appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Namespace: m.DeploymentSelfReference.Namespace, @@ -219,37 +247,29 @@ func (m *OTelColResourceManager) DeleteResources( Images: images, DevelopmentMode: m.DevelopmentMode, } - allObjects, err := assembleDesiredState(config) + desiredResources, err := assembleDesiredState(config) if err != nil { return err } var allErrors []error - for _, object := range allObjects { - err = m.Client.Delete(ctx, object) + for _, desiredResource := range desiredResources { + err = m.Client.Delete(ctx, desiredResource) if err != nil { if apierrors.IsNotFound(err) { - logger.Info( - "wanted to delete a resource, but it did not exist", - "name", - object.GetName(), - "namespace", - object.GetNamespace(), - "kind", - object.GetObjectKind().GroupVersionKind(), - ) + logger.Info(fmt.Sprintf( + "wanted to delete resource %s/%s, but it did not exist", + desiredResource.GetNamespace(), + desiredResource.GetName(), + )) } else { allErrors = append(allErrors, err) } } else { - logger.Info( - "deleted resource", - "name", - object.GetName(), - "namespace", - object.GetNamespace(), - "kind", - object.GetObjectKind().GroupVersionKind(), - ) + logger.Info(fmt.Sprintf( + "deleted resource %s/%s", + desiredResource.GetNamespace(), + desiredResource.GetName(), + )) } } if len(allErrors) > 0 { diff --git a/internal/backendconnection/otelcolresources/otelcol_resources_test.go b/internal/backendconnection/otelcolresources/otelcol_resources_test.go index dcf0315..a819e8c 100644 --- a/internal/backendconnection/otelcolresources/otelcol_resources_test.go +++ b/internal/backendconnection/otelcolresources/otelcol_resources_test.go @@ -21,10 +21,7 @@ import ( ) var ( - expectedCollectorConfigConfigMapName = "unit-test-opentelemetry-collector-agent" - expectedFileOffsetsConfigMapName = "unit-test-filelogoffsets" - - testObject = &corev1.ConfigMap{ + testResource = &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ Kind: "ConfigMap", APIVersion: "v1", @@ -61,6 +58,7 @@ var _ = Describe("The OpenTelemetry Collector resource manager", Ordered, func() Scheme: k8sClient.Scheme(), DeploymentSelfReference: DeploymentSelfReference, OTelCollectorNamePrefix: "unit-test", + DevelopmentMode: true, } }) @@ -71,18 +69,18 @@ var _ = Describe("The OpenTelemetry Collector resource manager", Ordered, func() Describe("when dealing with individual resources", func() { It("should create a single resource", func() { - isNew, isChanged, err := oTelColResourceManager.createOrUpdateResource(ctx, testObject.DeepCopy(), &logger) + isNew, isChanged, err := oTelColResourceManager.createOrUpdateResource(ctx, testResource.DeepCopy(), &logger) Expect(err).ToNot(HaveOccurred()) Expect(isNew).To(BeTrue()) Expect(isChanged).To(BeFalse()) - verifyObject(ctx, testObject) + verifyObject(ctx, testResource) }) It("should update a single object", func() { - err := oTelColResourceManager.createResource(ctx, testObject.DeepCopy(), &logger) + err := oTelColResourceManager.createResource(ctx, testResource.DeepCopy(), &logger) Expect(err).ToNot(HaveOccurred()) - updated := testObject.DeepCopy() + updated := testResource.DeepCopy() updated.Data["key"] = "updated value" isNew, isChanged, err := oTelColResourceManager.createOrUpdateResource(ctx, updated, &logger) @@ -93,19 +91,19 @@ var _ = Describe("The OpenTelemetry Collector resource manager", Ordered, func() }) It("should report that nothing has changed for a single object", func() { - err := oTelColResourceManager.createResource(ctx, testObject.DeepCopy(), &logger) + err := oTelColResourceManager.createResource(ctx, testResource.DeepCopy(), &logger) Expect(err).ToNot(HaveOccurred()) isNew, isChanged, err := oTelColResourceManager.createOrUpdateResource( ctx, - testObject.DeepCopy(), + testResource.DeepCopy(), &logger, ) Expect(err).ToNot(HaveOccurred()) Expect(isNew).To(BeFalse()) Expect(isChanged).To(BeFalse()) - verifyObject(ctx, testObject) + verifyObject(ctx, testResource) }) }) @@ -124,34 +122,54 @@ var _ = Describe("The OpenTelemetry Collector resource manager", Ordered, func() Expect(resourcesHaveBeenCreated).To(BeTrue()) Expect(resourcesHaveBeenUpdated).To(BeFalse()) - VerifyCollectorResourcesExist(ctx, k8sClient, Dash0OperatorNamespace) + VerifyCollectorResources(ctx, k8sClient, Dash0OperatorNamespace) }) }) - Describe("when updating all OpenTelemetry collector resources", func() { - It("should update the resources", func() { - for _, configMapName := range []string{ - expectedCollectorConfigConfigMapName, - expectedFileOffsetsConfigMapName, - } { - Expect(k8sClient.Create(ctx, &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: configMapName, - Namespace: Dash0OperatorNamespace, - Labels: map[string]string{ - "wrong-key": "value", - }, - Annotations: map[string]string{ - "wrong-key": "value", - }, - }, - Data: map[string]string{ - "wrong-key": "{}", - }, - })).To(Succeed()) + Describe("when OpenTelemetry collector resources have been modified externally", func() { + It("should reconcile the resources back into the desired state", func() { + _, _, err := + oTelColResourceManager.CreateOrUpdateOpenTelemetryCollectorResources( + ctx, + Dash0OperatorNamespace, + TestImages, + dash0MonitoringResource, + selfmonitoring.SelfMonitoringConfiguration{}, + &logger, + ) + Expect(err).ToNot(HaveOccurred()) + + // Change some arbitrary fields in some resources, then simulate a reconcile cycle and verify that all + // resources are back in their desired state. + + daemonSetConifgMap := GetOTelColDaemonSetConfigMap(ctx, k8sClient, Dash0OperatorNamespace) + daemonSetConifgMap.Data["config.yaml"] = "{}" + daemonSetConifgMap.Data["bogus-key"] = "" + Expect(k8sClient.Update(ctx, daemonSetConifgMap)).To(Succeed()) + + daemonSet := GetOTelColDaemonSet(ctx, k8sClient, Dash0OperatorNamespace) + daemonSet.Spec.Template.Spec.InitContainers = []corev1.Container{} + daemonSet.Spec.Template.Spec.Containers[0].Image = "wrong-collector-image:latest" + daemonSet.Spec.Template.Spec.Containers[0].Ports = []corev1.ContainerPort{ + {ContainerPort: 1234}, + {ContainerPort: 1235}, + } + Expect(k8sClient.Update(ctx, daemonSet)).To(Succeed()) + + deploymentConfigMap := GetOTelColDeploymentConfigMap(ctx, k8sClient, Dash0OperatorNamespace) + deploymentConfigMap.Data["config.yaml"] = "{}" + deploymentConfigMap.Data["bogus-key"] = "" + Expect(k8sClient.Update(ctx, deploymentConfigMap)).To(Succeed()) + + deployment := GetOTelColDeployment(ctx, k8sClient, Dash0OperatorNamespace) + var changedReplicas int32 = 5 + deployment.Spec.Replicas = &changedReplicas + deployment.Spec.Template.Spec.Containers[0].Ports = []corev1.ContainerPort{ + {ContainerPort: 1234}, } + Expect(k8sClient.Update(ctx, deployment)).To(Succeed()) - _, resourcesHaveBeenUpdated, err := + resourcesHaveBeenCreated, resourcesHaveBeenUpdated, err := oTelColResourceManager.CreateOrUpdateOpenTelemetryCollectorResources( ctx, Dash0OperatorNamespace, @@ -161,9 +179,52 @@ var _ = Describe("The OpenTelemetry Collector resource manager", Ordered, func() &logger, ) Expect(err).ToNot(HaveOccurred()) + Expect(resourcesHaveBeenCreated).To(BeFalse()) Expect(resourcesHaveBeenUpdated).To(BeTrue()) - VerifyCollectorResourcesExist(ctx, k8sClient, Dash0OperatorNamespace) + VerifyCollectorResources(ctx, k8sClient, Dash0OperatorNamespace) + }) + }) + + Describe("when OpenTelemetry collector resources have been deleted externally", func() { + It("should re-created the resources", func() { + _, _, err := + oTelColResourceManager.CreateOrUpdateOpenTelemetryCollectorResources( + ctx, + Dash0OperatorNamespace, + TestImages, + dash0MonitoringResource, + selfmonitoring.SelfMonitoringConfiguration{}, + &logger, + ) + Expect(err).ToNot(HaveOccurred()) + + // Delete some arbitrary resources, then simulate a reconcile cycle and verify that all resources have been + // recreated. + + daemonSetConifgMap := GetOTelColDaemonSetConfigMap(ctx, k8sClient, Dash0OperatorNamespace) + Expect(k8sClient.Delete(ctx, daemonSetConifgMap)).To(Succeed()) + + deploymentConfigMap := GetOTelColDeploymentConfigMap(ctx, k8sClient, Dash0OperatorNamespace) + Expect(k8sClient.Delete(ctx, deploymentConfigMap)).To(Succeed()) + + deployment := GetOTelColDeployment(ctx, k8sClient, Dash0OperatorNamespace) + Expect(k8sClient.Delete(ctx, deployment)).To(Succeed()) + + resourcesHaveBeenCreated, resourcesHaveBeenUpdated, err := + oTelColResourceManager.CreateOrUpdateOpenTelemetryCollectorResources( + ctx, + Dash0OperatorNamespace, + TestImages, + dash0MonitoringResource, + selfmonitoring.SelfMonitoringConfiguration{}, + &logger, + ) + Expect(err).ToNot(HaveOccurred()) + Expect(resourcesHaveBeenCreated).To(BeTrue()) + Expect(resourcesHaveBeenUpdated).To(BeFalse()) + + VerifyCollectorResources(ctx, k8sClient, Dash0OperatorNamespace) }) }) @@ -180,8 +241,8 @@ var _ = Describe("The OpenTelemetry Collector resource manager", Ordered, func() ) Expect(err).ToNot(HaveOccurred()) - // now run another create/update, to make sure resourcesHaveBeenCreated/resourcesHaveBeenUpdated come back - // as false + // Now run another create/update, to make sure resourcesHaveBeenCreated/resourcesHaveBeenUpdated come back + // as false. resourcesHaveBeenCreated, resourcesHaveBeenUpdated, err := oTelColResourceManager.CreateOrUpdateOpenTelemetryCollectorResources( ctx, @@ -195,7 +256,7 @@ var _ = Describe("The OpenTelemetry Collector resource manager", Ordered, func() Expect(resourcesHaveBeenCreated).To(BeFalse()) Expect(resourcesHaveBeenUpdated).To(BeFalse()) - VerifyCollectorResourcesExist(ctx, k8sClient, Dash0OperatorNamespace) + VerifyCollectorResources(ctx, k8sClient, Dash0OperatorNamespace) }) }) @@ -211,7 +272,7 @@ var _ = Describe("The OpenTelemetry Collector resource manager", Ordered, func() &logger, ) Expect(err).ToNot(HaveOccurred()) - VerifyCollectorResourcesExist(ctx, k8sClient, Dash0OperatorNamespace) + VerifyCollectorResources(ctx, k8sClient, Dash0OperatorNamespace) // delete everything again err = oTelColResourceManager.DeleteResources( diff --git a/internal/dash0/controller/dash0_controller.go b/internal/dash0/controller/dash0_controller.go index aa1296a..2520d86 100644 --- a/internal/dash0/controller/dash0_controller.go +++ b/internal/dash0/controller/dash0_controller.go @@ -185,7 +185,7 @@ func (r *Dash0Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl r.Images, r.OperatorNamespace, monitoringResource, - r.readSelfMonitoringConfigurationFromOperatorConfigurationResource(ctx, &logger), + selfmonitoring.ReadSelfMonitoringConfigurationFromOperatorConfigurationResource(ctx, r.Client, &logger), ); err != nil { return ctrl.Result{}, err } @@ -388,31 +388,3 @@ func (r *Dash0Reconciler) attachDanglingEvents( } } } - -func (r *Dash0Reconciler) readSelfMonitoringConfigurationFromOperatorConfigurationResource( - ctx context.Context, - logger *logr.Logger, -) selfmonitoring.SelfMonitoringConfiguration { - operatorConfigurationResource, err := util.FindUniqueOrMostRecentResourceInScope( - ctx, - r.Client, - "", /* cluster-scope, thus no namespace */ - &dash0v1alpha1.Dash0OperatorConfiguration{}, - logger, - ) - if err != nil || operatorConfigurationResource == nil { - return selfmonitoring.SelfMonitoringConfiguration{ - Enabled: false, - } - } - config, err := selfmonitoring.ConvertOperatorConfigurationResourceToSelfMonitoringConfiguration( - *operatorConfigurationResource.(*dash0v1alpha1.Dash0OperatorConfiguration), - logger, - ) - if err != nil { - return selfmonitoring.SelfMonitoringConfiguration{ - Enabled: false, - } - } - return config -} diff --git a/internal/dash0/controller/dash0_controller_test.go b/internal/dash0/controller/dash0_controller_test.go index 5341055..a390cb2 100644 --- a/internal/dash0/controller/dash0_controller_test.go +++ b/internal/dash0/controller/dash0_controller_test.go @@ -104,7 +104,7 @@ var _ = Describe("The Dash0 controller", Ordered, func() { By("Trigger reconcile request") triggerReconcileRequest(ctx, reconciler, "") verifyDash0MonitoringResourceIsAvailable(ctx) - VerifyCollectorResourcesExist(ctx, k8sClient, operatorNamespace) + VerifyCollectorResources(ctx, k8sClient, operatorNamespace) }) It("should successfully run multiple reconciles (no modifiable workloads exist)", func() { @@ -121,7 +121,7 @@ var _ = Describe("The Dash0 controller", Ordered, func() { secondAvailableCondition := verifyDash0MonitoringResourceIsAvailable(ctx) Expect(secondAvailableCondition.LastTransitionTime.Time).To(Equal(originalTransitionTimestamp)) - VerifyCollectorResourcesExist(ctx, k8sClient, operatorNamespace) + VerifyCollectorResources(ctx, k8sClient, operatorNamespace) }) It("should mark only the most recent resource as available and the other ones as degraded when multiple resources exist", func() { @@ -797,7 +797,7 @@ var _ = Describe("The Dash0 controller", Ordered, func() { It("should remove the collector resources", func() { triggerReconcileRequest(ctx, reconciler, "Trigger first reconcile request") - VerifyCollectorResourcesExist(ctx, k8sClient, operatorNamespace) + VerifyCollectorResources(ctx, k8sClient, operatorNamespace) dash0MonitoringResource := LoadDash0MonitoringResourceOrFail(ctx, k8sClient, Default) Expect(k8sClient.Delete(ctx, dash0MonitoringResource)).To(Succeed()) diff --git a/internal/dash0/instrumentation/instrumenter.go b/internal/dash0/instrumentation/instrumenter.go index 74020a7..50f66f6 100644 --- a/internal/dash0/instrumentation/instrumenter.go +++ b/internal/dash0/instrumentation/instrumenter.go @@ -83,18 +83,18 @@ func (i *Instrumenter) InstrumentAtStartup( logger *logr.Logger, ) { logger.Info("Applying/updating instrumentation at controller startup.") - dash0MonitoringResourcesInNamespace := &dash0v1alpha1.Dash0MonitoringList{} + allDash0MonitoringResouresInCluster := &dash0v1alpha1.Dash0MonitoringList{} if err := k8sClient.List( ctx, - dash0MonitoringResourcesInNamespace, + allDash0MonitoringResouresInCluster, &client.ListOptions{}, ); err != nil { logger.Error(err, "Failed to list all Dash0 monitoring resources at controller startup.") return } - logger.Info(fmt.Sprintf("Found %d Dash0 monitoring resources.", len(dash0MonitoringResourcesInNamespace.Items))) - for _, dash0MonitoringResource := range dash0MonitoringResourcesInNamespace.Items { + logger.Info(fmt.Sprintf("Found %d Dash0 monitoring resources.", len(allDash0MonitoringResouresInCluster.Items))) + for _, dash0MonitoringResource := range allDash0MonitoringResouresInCluster.Items { logger.Info(fmt.Sprintf("Processing workloads in Dash0-enabled namespace %s", dash0MonitoringResource.Namespace)) if dash0MonitoringResource.IsMarkedForDeletion() { diff --git a/internal/dash0/selfmonitoring/self_monitoring.go b/internal/dash0/selfmonitoring/self_monitoring.go index 2768bc2..ac20f1c 100644 --- a/internal/dash0/selfmonitoring/self_monitoring.go +++ b/internal/dash0/selfmonitoring/self_monitoring.go @@ -4,6 +4,7 @@ package selfmonitoring import ( + "context" "fmt" "regexp" "slices" @@ -13,6 +14,7 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" dash0v1alpha1 "github.com/dash0hq/dash0-operator/api/dash0monitoring/v1alpha1" "github.com/dash0hq/dash0-operator/internal/dash0/util" @@ -686,3 +688,32 @@ func matchOtelExporterOtlpProtocolEnvVar(e corev1.EnvVar) bool { func matchSelfMonitoringAuthTokenEnvVar(e corev1.EnvVar) bool { return e.Name == selfMonitoringauthTokenEnvVarName } + +func ReadSelfMonitoringConfigurationFromOperatorConfigurationResource( + ctx context.Context, + k8sClient client.Client, + logger *logr.Logger, +) SelfMonitoringConfiguration { + operatorConfigurationResource, err := util.FindUniqueOrMostRecentResourceInScope( + ctx, + k8sClient, + "", /* cluster-scope, thus no namespace */ + &dash0v1alpha1.Dash0OperatorConfiguration{}, + logger, + ) + if err != nil || operatorConfigurationResource == nil { + return SelfMonitoringConfiguration{ + Enabled: false, + } + } + config, err := ConvertOperatorConfigurationResourceToSelfMonitoringConfiguration( + *operatorConfigurationResource.(*dash0v1alpha1.Dash0OperatorConfiguration), + logger, + ) + if err != nil { + return SelfMonitoringConfiguration{ + Enabled: false, + } + } + return config +} diff --git a/internal/dash0/util/controller.go b/internal/dash0/util/controller.go index dc97089..174e0e0 100644 --- a/internal/dash0/util/controller.go +++ b/internal/dash0/util/controller.go @@ -275,10 +275,10 @@ func FindUniqueOrMostRecentResourceInScope( return nil, err } - return findMosRecentResource(resourcePrototype, allResourcesInScope), nil + return findMostRecentResource(resourcePrototype, allResourcesInScope), nil } -func findMosRecentResource( +func findMostRecentResource( resourcePrototype dash0common.Dash0Resource, allResourcesInScope client.ObjectList, ) dash0common.Dash0Resource { diff --git a/test/util/collector.go b/test/util/collector.go index af6a27e..3a8d3bd 100644 --- a/test/util/collector.go +++ b/test/util/collector.go @@ -8,57 +8,154 @@ import ( "fmt" "slices" - . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/onsi/gomega" ) +type expectedResource struct { + name string + clusterScoped bool + receiver client.Object +} + const ( - ExpectedConfigMapName = "unit-test-opentelemetry-collector-agent" - ExpectedDaemonSetName = "unit-test-opentelemetry-collector-agent" - ExpectedDeploymentName = "unit-test-cluster-metrics-collector" + NamePrefix = "unit-test" ) -func VerifyCollectorResourcesExist( +var ( + ExpectedDaemonSetServiceAccountName = fmt.Sprintf("%s-opentelemetry-collector-sa", NamePrefix) + ExpectedDaemonSetCollectorConfigMapName = fmt.Sprintf("%s-opentelemetry-collector-agent-cm", NamePrefix) + ExpectedDaemonSetFilelogOffsetSynchConfigMapName = fmt.Sprintf("%s-filelogoffsets-cm", NamePrefix) + ExpectedDaemonSetClusterRoleName = fmt.Sprintf("%s-opentelemetry-collector-cr", NamePrefix) + ExpectedDaemonSetClusterRoleBinding = fmt.Sprintf("%s-opentelemetry-collector-crb", NamePrefix) + ExpectedDaemonSetRoleName = fmt.Sprintf("%s-opentelemetry-collector-role", NamePrefix) + ExpectedDaemonSetRoleBindingName = fmt.Sprintf("%s-opentelemetry-collector-rolebinding", NamePrefix) + ExpectedDaemonSetServiceName = fmt.Sprintf("%s-opentelemetry-collector-service", NamePrefix) + ExpectedDaemonSetName = fmt.Sprintf( + "%s-opentelemetry-collector-agent-daemonset", + NamePrefix, + ) + ExpectedDeploymentServiceAccountName = fmt.Sprintf("%s-cluster-metrics-collector-sa", NamePrefix) + ExpectedDeploymentClusterRoleName = fmt.Sprintf("%s-cluster-metrics-collector-cr", NamePrefix) + ExpectedDeploymentClusterRoleBindingName = fmt.Sprintf("%s-cluster-metrics-collector-crb", NamePrefix) + ExpectedDeploymentCollectorConfigMapName = fmt.Sprintf("%s-cluster-metrics-collector-cm", NamePrefix) + ExpectedDeploymentName = fmt.Sprintf("%s-cluster-metrics-collector-deployment", NamePrefix) + + expectedResourceDaemonSetConfigMap = expectedResource{ + name: ExpectedDaemonSetCollectorConfigMapName, + receiver: &corev1.ConfigMap{}, + } + expectedResourceDaemonSet = expectedResource{ + name: ExpectedDaemonSetName, + receiver: &appsv1.DaemonSet{}, + } + expectedResourceDeploymentConfigMap = expectedResource{ + name: ExpectedDeploymentCollectorConfigMapName, + receiver: &corev1.ConfigMap{}, + } + expectedResourceDeployment = expectedResource{ + name: ExpectedDeploymentName, + receiver: &appsv1.Deployment{}, + } + + allExpectedResources = []expectedResource{ + {name: ExpectedDaemonSetServiceAccountName, receiver: &corev1.ServiceAccount{}}, + expectedResourceDaemonSetConfigMap, + {name: ExpectedDaemonSetFilelogOffsetSynchConfigMapName, receiver: &corev1.ConfigMap{}}, + {name: ExpectedDaemonSetClusterRoleName, clusterScoped: true, receiver: &rbacv1.ClusterRole{}}, + {name: ExpectedDaemonSetClusterRoleBinding, clusterScoped: true, receiver: &rbacv1.ClusterRoleBinding{}}, + {name: ExpectedDaemonSetRoleName, receiver: &rbacv1.Role{}}, + {name: ExpectedDaemonSetRoleBindingName, receiver: &rbacv1.RoleBinding{}}, + {name: ExpectedDaemonSetServiceName, receiver: &corev1.Service{}}, + expectedResourceDaemonSet, + {name: ExpectedDeploymentServiceAccountName, receiver: &corev1.ServiceAccount{}}, + {name: ExpectedDeploymentClusterRoleName, clusterScoped: true, receiver: &rbacv1.ClusterRole{}}, + {name: ExpectedDeploymentClusterRoleBindingName, clusterScoped: true, receiver: &rbacv1.ClusterRoleBinding{}}, + expectedResourceDeploymentConfigMap, + expectedResourceDeployment, + } +) + +func VerifyCollectorResources( + ctx context.Context, + k8sClient client.Client, + operatorNamespace string, +) { + // verify that all expected resources exist and have the expected owner reference + VerifyAllResourcesExist(ctx, k8sClient, operatorNamespace) + + // verify a few arbitrary resource in more detail + VerifyDaemonSetCollectorConfigMap(ctx, k8sClient, operatorNamespace) + VerifyCollectorDaemonSet(ctx, k8sClient, operatorNamespace) + VerifyDeploymentCollectorConfigMap(ctx, k8sClient, operatorNamespace) + VerifyCollectorDeployment(ctx, k8sClient, operatorNamespace) +} + +func VerifyAllResourcesExist( ctx context.Context, k8sClient client.Client, operatorNamespace string, ) { - VerifyCollectorConfigMapExists(ctx, k8sClient, operatorNamespace) - VerifyCollectorDaemonSetExists(ctx, k8sClient, operatorNamespace) + for _, expectedRes := range allExpectedResources { + expectedNamespace := operatorNamespace + if expectedRes.clusterScoped { + expectedNamespace = "" + } + actualResource := verifyResourceExists( + ctx, + k8sClient, + expectedNamespace, + expectedRes.name, + expectedRes.receiver, + ) + if !expectedRes.clusterScoped { + verifyOwnerReference(actualResource) + } + } } -func VerifyCollectorConfigMapExists( +func VerifyDaemonSetCollectorConfigMap( ctx context.Context, k8sClient client.Client, operatorNamespace string, -) *corev1.ConfigMap { +) { cm_ := verifyResourceExists( ctx, k8sClient, operatorNamespace, - ExpectedConfigMapName, + ExpectedDaemonSetCollectorConfigMapName, &corev1.ConfigMap{}, ) cm := cm_.(*corev1.ConfigMap) + Expect(cm.Data).To(HaveLen(1)) Expect(cm.Data).To(HaveKey("config.yaml")) - - verifyOwnerReference(cm) - - return cm + config := cm.Data["config.yaml"] + Expect(config).To(ContainSubstring("endpoint: \"endpoint.dash0.com:4317\"")) + Expect(config).To(ContainSubstring("\"Authorization\": \"Bearer ${env:AUTH_TOKEN}\"")) } -func VerifyCollectorDaemonSetExists( +func VerifyCollectorDaemonSet( ctx context.Context, k8sClient client.Client, operatorNamespace string, ) *appsv1.DaemonSet { - ds_ := verifyResourceExists(ctx, k8sClient, operatorNamespace, ExpectedDaemonSetName, &appsv1.DaemonSet{}) + ds_ := verifyResourceExists( + ctx, + k8sClient, + operatorNamespace, + ExpectedDaemonSetName, + &appsv1.DaemonSet{}, + ) ds := ds_.(*appsv1.DaemonSet) // arbitrarily checking a couple of settings for the daemon set + initContainers := ds.Spec.Template.Spec.InitContainers + Expect(initContainers).To(HaveLen(1)) containers := ds.Spec.Template.Spec.Containers Expect(containers).To(HaveLen(3)) @@ -66,16 +163,58 @@ func VerifyCollectorDaemonSetExists( return c.Name == "opentelemetry-collector" }) collectorContainer := containers[collectorContainerIdx] - + Expect(collectorContainer.Image).To(Equal(CollectorImageTest)) ports := collectorContainer.Ports + Expect(ports).To(HaveLen(2)) Expect(ports[0].ContainerPort).To(Equal(int32(4317))) Expect(ports[1].ContainerPort).To(Equal(int32(4318))) - verifyOwnerReference(ds) - return ds } +func VerifyDeploymentCollectorConfigMap( + ctx context.Context, + k8sClient client.Client, + operatorNamespace string, +) { + cm_ := verifyResourceExists( + ctx, + k8sClient, + operatorNamespace, + ExpectedDeploymentCollectorConfigMapName, + &corev1.ConfigMap{}, + ) + cm := cm_.(*corev1.ConfigMap) + Expect(cm.Data).To(HaveLen(1)) + Expect(cm.Data).To(HaveKey("config.yaml")) + config := cm.Data["config.yaml"] + Expect(config).To(ContainSubstring("endpoint: \"endpoint.dash0.com:4317\"")) + Expect(config).To(ContainSubstring("\"Authorization\": \"Bearer ${env:AUTH_TOKEN}\"")) +} + +func VerifyCollectorDeployment( + ctx context.Context, + k8sClient client.Client, + operatorNamespace string, +) { + deployment_ := verifyResourceExists( + ctx, + k8sClient, + operatorNamespace, + ExpectedDeploymentName, + &appsv1.Deployment{}, + ) + deployment := deployment_.(*appsv1.Deployment) + + // arbitrarily check a couple of settings for the deployment + Expect(*deployment.Spec.Replicas).To(Equal(int32(1))) + containers := deployment.Spec.Template.Spec.Containers + Expect(containers).To(HaveLen(2)) + collectorContainer := containers[0] + ports := collectorContainer.Ports + Expect(ports).To(HaveLen(0)) +} + func verifyResourceExists( ctx context.Context, k8sClient client.Client, @@ -90,74 +229,102 @@ func verifyResourceExists( return receiver } -func VerifyCollectorResourcesDoNotExist( +func GetOTelColDaemonSetConfigMap( ctx context.Context, k8sClient client.Client, operatorNamespace string, -) { - verifyCollectorConfigMapDoesNotExist(ctx, k8sClient, operatorNamespace) - verifyCollectorDaemonSetDoesNotExist(ctx, k8sClient, operatorNamespace) - verifyCollectorDeploymentDoesNotExist(ctx, k8sClient, operatorNamespace) +) *corev1.ConfigMap { + return getOTelColResource(ctx, k8sClient, operatorNamespace, expectedResourceDaemonSetConfigMap).(*corev1.ConfigMap) } -func verifyCollectorConfigMapDoesNotExist( +func GetOTelColDaemonSet( ctx context.Context, k8sClient client.Client, operatorNamespace string, -) { - verifyResourceDoesNotExist( +) *appsv1.DaemonSet { + return getOTelColResource(ctx, k8sClient, operatorNamespace, expectedResourceDaemonSet).(*appsv1.DaemonSet) +} + +func GetOTelColDeploymentConfigMap( + ctx context.Context, + k8sClient client.Client, + operatorNamespace string, +) *corev1.ConfigMap { + return getOTelColResource( ctx, k8sClient, operatorNamespace, - ExpectedConfigMapName, - &corev1.ConfigMap{}, - "config map", - ) + expectedResourceDeploymentConfigMap, + ).(*corev1.ConfigMap) } -func verifyCollectorDaemonSetDoesNotExist( +func GetOTelColDeployment( ctx context.Context, k8sClient client.Client, operatorNamespace string, -) { - verifyResourceDoesNotExist( +) *appsv1.Deployment { + return getOTelColResource( ctx, k8sClient, operatorNamespace, - ExpectedDaemonSetName, - &appsv1.DaemonSet{}, - "daemon set", - ) + expectedResourceDeployment, + ).(*appsv1.Deployment) } -func verifyCollectorDeploymentDoesNotExist( +func getOTelColResource( ctx context.Context, k8sClient client.Client, operatorNamespace string, -) { - verifyResourceDoesNotExist( + expectedRes expectedResource, +) client.Object { + expectedNamespace := operatorNamespace + if expectedRes.clusterScoped { + expectedNamespace = "" + } + return verifyResourceExists( ctx, k8sClient, - operatorNamespace, - ExpectedDaemonSetName, - &appsv1.Deployment{}, - "daemon set", + expectedNamespace, + expectedRes.name, + expectedRes.receiver, ) } +func VerifyCollectorResourcesDoNotExist( + ctx context.Context, + k8sClient client.Client, + operatorNamespace string, +) { + for _, expectedRes := range allExpectedResources { + expectedNamespace := operatorNamespace + if expectedRes.clusterScoped { + expectedNamespace = "" + } + verifyResourceDoesNotExist( + ctx, + k8sClient, + expectedNamespace, + expectedRes.name, + expectedRes.receiver, + ) + } +} + func verifyResourceDoesNotExist( ctx context.Context, k8sClient client.Client, namespace string, expectedName string, receiver client.Object, - resourceLabel string, ) { key := client.ObjectKey{Name: expectedName, Namespace: namespace} err := k8sClient.Get(ctx, key, receiver) - Expect(err).To(HaveOccurred(), fmt.Sprintf("the %s still exists although it should have been deleted", resourceLabel)) + Expect(err).To( + HaveOccurred(), + fmt.Sprintf("the resource %s still exists although it should have been deleted", expectedName), + ) Expect(apierrors.IsNotFound(err)).To(BeTrue(), - fmt.Sprintf("loading the %s failed with an unexpected error: %v", resourceLabel, err)) + fmt.Sprintf("attempting to load the resource %s failed with an unexpected error: %v", expectedName, err)) } func verifyOwnerReference(object client.Object) {