diff --git a/internal/controller/dash0_controller.go b/internal/controller/dash0_controller.go index 48382266..181ed43e 100644 --- a/internal/controller/dash0_controller.go +++ b/internal/controller/dash0_controller.go @@ -35,15 +35,6 @@ const ( workloadNameLabel = "workload name" ) -var ( - workloadsWithoutDash0InstrumentedLabelFilter = metav1.ListOptions{ - LabelSelector: fmt.Sprintf("!%s", util.InstrumentedLabelKey), - } - workloadsWithDash0InstrumentedLabelFilter = metav1.ListOptions{ - LabelSelector: util.InstrumentedLabelKey, - } -) - type Dash0Reconciler struct { client.Client ClientSet *kubernetes.Clientset @@ -297,7 +288,7 @@ func (r *Dash0Reconciler) findAndInstrumentCronJobs( logger *logr.Logger, ) error { matchingWorkloadsInNamespace, err := - r.ClientSet.BatchV1().CronJobs(namespace).List(ctx, workloadsWithoutDash0InstrumentedLabelFilter) + r.ClientSet.BatchV1().CronJobs(namespace).List(ctx, util.WorkloadsWithoutDash0InstrumentedLabelFilter) if err != nil { return fmt.Errorf("error when querying cron jobs: %w", err) } @@ -324,7 +315,7 @@ func (r *Dash0Reconciler) findAndInstrumentyDaemonSets( logger *logr.Logger, ) error { matchingWorkloadsInNamespace, err := - r.ClientSet.AppsV1().DaemonSets(namespace).List(ctx, workloadsWithoutDash0InstrumentedLabelFilter) + r.ClientSet.AppsV1().DaemonSets(namespace).List(ctx, util.WorkloadsWithoutDash0InstrumentedLabelFilter) if err != nil { return fmt.Errorf("error when querying daemon sets: %w", err) } @@ -351,7 +342,7 @@ func (r *Dash0Reconciler) findAndInstrumentDeployments( logger *logr.Logger, ) error { matchingWorkloadsInNamespace, err := - r.ClientSet.AppsV1().Deployments(namespace).List(ctx, workloadsWithoutDash0InstrumentedLabelFilter) + r.ClientSet.AppsV1().Deployments(namespace).List(ctx, util.WorkloadsWithoutDash0InstrumentedLabelFilter) if err != nil { return fmt.Errorf("error when querying deployments: %w", err) } @@ -378,7 +369,7 @@ func (r *Dash0Reconciler) findAndAddLabelsToImmutableJobsOnInstrumentation( logger *logr.Logger, ) error { matchingWorkloadsInNamespace, err := - r.ClientSet.BatchV1().Jobs(namespace).List(ctx, workloadsWithoutDash0InstrumentedLabelFilter) + r.ClientSet.BatchV1().Jobs(namespace).List(ctx, util.WorkloadsWithoutDash0InstrumentedLabelFilter) if err != nil { return fmt.Errorf("error when querying jobs: %w", err) } @@ -434,7 +425,7 @@ func (r *Dash0Reconciler) findAndInstrumentReplicaSets( logger *logr.Logger, ) error { matchingWorkloadsInNamespace, err := - r.ClientSet.AppsV1().ReplicaSets(namespace).List(ctx, workloadsWithoutDash0InstrumentedLabelFilter) + r.ClientSet.AppsV1().ReplicaSets(namespace).List(ctx, util.WorkloadsWithoutDash0InstrumentedLabelFilter) if err != nil { return fmt.Errorf("error when querying deployments: %w", err) } @@ -465,7 +456,7 @@ func (r *Dash0Reconciler) findAndInstrumentStatefulSets( namespace string, logger *logr.Logger, ) error { - matchingWorkloadsInNamespace, err := r.ClientSet.AppsV1().StatefulSets(namespace).List(ctx, workloadsWithoutDash0InstrumentedLabelFilter) + matchingWorkloadsInNamespace, err := r.ClientSet.AppsV1().StatefulSets(namespace).List(ctx, util.WorkloadsWithoutDash0InstrumentedLabelFilter) if err != nil { return fmt.Errorf("error when querying stateful sets: %w", err) } @@ -674,7 +665,7 @@ func (r *Dash0Reconciler) findAndUninstrumentCronJobs( logger *logr.Logger, ) error { matchingWorkloadsInNamespace, err := - r.ClientSet.BatchV1().CronJobs(namespace).List(ctx, workloadsWithDash0InstrumentedLabelFilter) + r.ClientSet.BatchV1().CronJobs(namespace).List(ctx, util.WorkloadsWithDash0InstrumentedLabelExlucdingOptOutFilter) if err != nil { return fmt.Errorf("error when querying instrumented cron jobs: %w", err) } @@ -697,7 +688,7 @@ func (r *Dash0Reconciler) uninstrumentCronJob( func (r *Dash0Reconciler) findAndUninstrumentDaemonSets(ctx context.Context, namespace string, logger *logr.Logger) error { matchingWorkloadsInNamespace, err := - r.ClientSet.AppsV1().DaemonSets(namespace).List(ctx, workloadsWithDash0InstrumentedLabelFilter) + r.ClientSet.AppsV1().DaemonSets(namespace).List(ctx, util.WorkloadsWithDash0InstrumentedLabelExlucdingOptOutFilter) if err != nil { return fmt.Errorf("error when querying instrumented daemon sets: %w", err) } @@ -724,7 +715,7 @@ func (r *Dash0Reconciler) findAndUninstrumentDeployments( logger *logr.Logger, ) error { matchingWorkloadsInNamespace, err := - r.ClientSet.AppsV1().Deployments(namespace).List(ctx, workloadsWithDash0InstrumentedLabelFilter) + r.ClientSet.AppsV1().Deployments(namespace).List(ctx, util.WorkloadsWithDash0InstrumentedLabelExlucdingOptOutFilter) if err != nil { return fmt.Errorf("error when querying instrumented deployments: %w", err) } @@ -750,7 +741,7 @@ func (r *Dash0Reconciler) findAndHandleJobOnUninstrumentation( namespace string, logger *logr.Logger, ) error { - matchingWorkloadsInNamespace, err := r.ClientSet.BatchV1().Jobs(namespace).List(ctx, workloadsWithDash0InstrumentedLabelFilter) + matchingWorkloadsInNamespace, err := r.ClientSet.BatchV1().Jobs(namespace).List(ctx, util.WorkloadsWithDash0InstrumentedLabelExlucdingOptOutFilter) if err != nil { return fmt.Errorf("error when querying instrumented jobs: %w", err) } @@ -783,8 +774,7 @@ func (r *Dash0Reconciler) handleJobOnUninstrumentation(ctx context.Context, job }, &job); err != nil { return fmt.Errorf("error when fetching job %s/%s: %w", job.GetNamespace(), job.GetName(), err) } - isInstrumented := job.GetObjectMeta().GetLabels()[util.InstrumentedLabelKey] - if isInstrumented == "true" { + if util.HasBeenInstrumentedSuccessfully(&job.ObjectMeta) { // This job has been instrumented, presumably by the webhook. We cannot undo the instrumentation here, since // jobs are immutable. @@ -792,7 +782,7 @@ func (r *Dash0Reconciler) handleJobOnUninstrumentation(ctx context.Context, job // since we cannot remove the instrumentation, so we also have to leave the labels in place. createImmutableWorkloadsError = true return nil - } else { + } else if util.InstrumenationAttemptHasFailed(&job.ObjectMeta) { // There was an attempt to instrument this job (probably by the controller), which has not been successful. // We only need remove the labels from that instrumentation attempt to clean up. newWorkloadModifier(r.Versions, &logger).RemoveLabelsFromImmutableJob(&job) @@ -800,6 +790,13 @@ func (r *Dash0Reconciler) handleJobOnUninstrumentation(ctx context.Context, job // Apparently for jobs we do not need to set the "dash0.webhook.ignore.once" label, since changing there // labels does not trigger a new admission request. return r.Client.Update(ctx, &job) + } else if util.HasOptedOutOfInstrumenation(&job.ObjectMeta) { + // There is an opt-out marker for this job, so we have never attempted to instrument it and also do not + // need to revert anything or remove any labels. + return nil + } else { + // No dash0.instrumented label is present, do nothing. + return nil } }, &logger) @@ -826,7 +823,7 @@ func (r *Dash0Reconciler) findAndUninstrumentReplicaSets( logger *logr.Logger, ) error { matchingWorkloadsInNamespace, err := - r.ClientSet.AppsV1().ReplicaSets(namespace).List(ctx, workloadsWithDash0InstrumentedLabelFilter) + r.ClientSet.AppsV1().ReplicaSets(namespace).List(ctx, util.WorkloadsWithDash0InstrumentedLabelExlucdingOptOutFilter) if err != nil { return fmt.Errorf("error when querying instrumented replica sets: %w", err) } @@ -853,7 +850,7 @@ func (r *Dash0Reconciler) findAndUninstrumentStatefulSets( logger *logr.Logger, ) error { matchingWorkloadsInNamespace, err := - r.ClientSet.AppsV1().StatefulSets(namespace).List(ctx, workloadsWithDash0InstrumentedLabelFilter) + r.ClientSet.AppsV1().StatefulSets(namespace).List(ctx, util.WorkloadsWithDash0InstrumentedLabelExlucdingOptOutFilter) if err != nil { return fmt.Errorf("error when querying instrumented stateful sets: %w", err) } diff --git a/internal/controller/dash0_controller_test.go b/internal/controller/dash0_controller_test.go index 6b818bbf..494ba53b 100644 --- a/internal/controller/dash0_controller_test.go +++ b/internal/controller/dash0_controller_test.go @@ -5,6 +5,7 @@ package controller import ( "context" + "fmt" "time" . "github.com/onsi/ginkgo/v2" @@ -14,7 +15,6 @@ import ( "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -45,62 +45,55 @@ var ( } ) -var _ = Describe("Dash0 Controller", func() { - Context("When reconciling the Dash0 custom resource", func() { - - ctx := context.Background() - var createdObjects []client.Object - - var reconciler *Dash0Reconciler - - BeforeEach(func() { - By("creating the custom resource for the Kind Dash0") - EnsureTestNamespaceExists(ctx, k8sClient, namespace) - EnsureDash0CustomResourceExists( - ctx, - k8sClient, - dash0CustomResourceQualifiedName, - &operatorv1alpha1.Dash0{}, - &operatorv1alpha1.Dash0{ - ObjectMeta: metav1.ObjectMeta{ - Name: dash0CustomResourceQualifiedName.Name, - Namespace: dash0CustomResourceQualifiedName.Namespace, - }, +var _ = Describe("The Dash0 controller", func() { + + ctx := context.Background() + var createdObjects []client.Object + + var reconciler *Dash0Reconciler + + BeforeEach(func() { + By("creating the custom resource for the Kind Dash0") + EnsureTestNamespaceExists(ctx, k8sClient, namespace) + EnsureDash0CustomResourceExists( + ctx, + k8sClient, + dash0CustomResourceQualifiedName, + &operatorv1alpha1.Dash0{}, + &operatorv1alpha1.Dash0{ + ObjectMeta: metav1.ObjectMeta{ + Name: dash0CustomResourceQualifiedName.Name, + Namespace: dash0CustomResourceQualifiedName.Namespace, }, - ) + }, + ) + + reconciler = &Dash0Reconciler{ + Client: k8sClient, + ClientSet: clientset, + Recorder: recorder, + Scheme: k8sClient.Scheme(), + Versions: versions, + } - reconciler = &Dash0Reconciler{ - Client: k8sClient, - ClientSet: clientset, - Recorder: recorder, - Scheme: k8sClient.Scheme(), - Versions: versions, - } - createdObjects = make([]client.Object, 0) - }) + createdObjects = make([]client.Object, 0) + }) - AfterEach(func() { - By("Remove all created objects") - for _, object := range createdObjects { - Expect(k8sClient.Delete(ctx, object, &client.DeleteOptions{ - GracePeriodSeconds: new(int64), - })).To(Succeed()) - } + AfterEach(func() { + createdObjects = DeleteAllCreatedObjects(ctx, k8sClient, createdObjects) - By("Cleanup the Dash0 custom resource instance") - if dash0CustomResource := loadDash0CustomResourceIfItExists(ctx); dash0CustomResource != nil { - // We want to delete the custom resource, but we need to remove the finalizer first, otherwise the first - // reconcile of the next test case will actually run the finalizers. - removeFinalizer(ctx, dash0CustomResource) - Expect(k8sClient.Delete(ctx, dash0CustomResource)).To(Succeed()) - } + By("Cleanup the Dash0 custom resource instance") + if dash0CustomResource := loadDash0CustomResourceIfItExists(ctx); dash0CustomResource != nil { + // We want to delete the custom resource, but we need to remove the finalizer first, otherwise the first + // reconcile of the next test case will actually run the finalizers. + removeFinalizer(ctx, dash0CustomResource) + Expect(k8sClient.Delete(ctx, dash0CustomResource)).To(Succeed()) + } - deleteAllEvents(ctx, clientset, namespace) - allEvents, err := clientset.CoreV1().Events(namespace).List(ctx, metav1.ListOptions{}) - Expect(err).NotTo(HaveOccurred()) - Expect(allEvents.Items).To(BeEmpty()) - }) + DeleteAllEvents(ctx, clientset, namespace) + }) + Describe("when reconciling", func() { It("should successfully run the first reconcile (no modifiable workloads exist)", func() { By("Trigger reconcile request") triggerReconcileRequest(ctx, reconciler, "") @@ -121,9 +114,11 @@ var _ = Describe("Dash0 Controller", func() { secondAvailableCondition := verifyDash0ResourceIsAvailable(ctx) Expect(secondAvailableCondition.LastTransitionTime.Time).To(Equal(originalTransitionTimestamp)) }) + }) + Describe("when instrumenting existing workloads", func() { It("should instrument an existing cron job", func() { - name := CronJobName + name := UniqueName(CronJobNamePrefix) By("Inititalize a cron job") cronJob := CreateBasicCronJob(ctx, k8sClient, namespace, name) createdObjects = append(createdObjects, cronJob) @@ -135,7 +130,7 @@ var _ = Describe("Dash0 Controller", func() { }) It("should instrument an existing daemon set", func() { - name := DaemonSetName + name := UniqueName(DaemonSetNamePrefix) By("Inititalize a daemon set") daemonSet := CreateBasicDaemonSet(ctx, k8sClient, namespace, name) createdObjects = append(createdObjects, daemonSet) @@ -147,7 +142,7 @@ var _ = Describe("Dash0 Controller", func() { }) It("should instrument an existing deployment", func() { - name := DeploymentName + name := UniqueName(DeploymentNamePrefix) By("Inititalize a deployment") deployment := CreateBasicDeployment(ctx, k8sClient, namespace, name) createdObjects = append(createdObjects, deployment) @@ -159,7 +154,7 @@ var _ = Describe("Dash0 Controller", func() { }) It("should record a failure event when attempting to instrument an existing job and add labels", func() { - name := JobName1 + name := UniqueName(JobNamePrefix) By("Inititalize a job") job := CreateBasicJob(ctx, k8sClient, namespace, name) createdObjects = append(createdObjects, job) @@ -172,14 +167,15 @@ var _ = Describe("Dash0 Controller", func() { clientset, namespace, name, - "Dash0 instrumentation of this workload by the controller has not been successful. Error message: Dash0 cannot"+ - " instrument the existing job test-namespace/job1, since the this type of workload is immutable.", + fmt.Sprintf("Dash0 instrumentation of this workload by the controller has not been successful. Error message: "+ + "Dash0 cannot instrument the existing job test-namespace/%s, since the this type of workload "+ + "is immutable.", name), ) VerifyImmutableJobCouldNotBeModified(GetJob(ctx, k8sClient, namespace, name)) }) It("should instrument an existing orphan replicaset", func() { - name := DeploymentName + name := UniqueName(ReplicaSetNamePrefix) By("Inititalize a replicaset") replicaSet := CreateBasicReplicaSet(ctx, k8sClient, namespace, name) createdObjects = append(createdObjects, replicaSet) @@ -190,8 +186,8 @@ var _ = Describe("Dash0 Controller", func() { VerifyModifiedReplicaSet(GetReplicaSet(ctx, k8sClient, namespace, name), BasicInstrumentedPodSpecExpectations) }) - It("should not modify an existing replicaset owned by a deployment", func() { - name := DeploymentName + It("should not instrument an existing replicaset owned by a deployment", func() { + name := UniqueName(ReplicaSetNamePrefix) By("Inititalize a replicaset") replicaSet := CreateReplicaSetOwnedByDeployment(ctx, k8sClient, namespace, name) createdObjects = append(createdObjects, replicaSet) @@ -202,8 +198,8 @@ var _ = Describe("Dash0 Controller", func() { VerifyUnmodifiedReplicaSet(GetReplicaSet(ctx, k8sClient, namespace, name)) }) - It("should instrument an stateful set", func() { - name := StatefulSetName + It("should instrument an existing stateful set", func() { + name := UniqueName(StatefulSetNamePrefix) By("Inititalize a stateful set") statefulSet := CreateBasicStatefulSet(ctx, k8sClient, namespace, name) createdObjects = append(createdObjects, statefulSet) @@ -213,7 +209,83 @@ var _ = Describe("Dash0 Controller", func() { verifyStatusConditionAndSuccessfulInstrumentationEvent(ctx, namespace, name) VerifyModifiedStatefulSet(GetStatefulSet(ctx, k8sClient, namespace, name), BasicInstrumentedPodSpecExpectations) }) + }) + Describe("when existing workloads have the opt-out label", func() { + It("should not instrument an existing cron job with the opt-out label", func() { + name := UniqueName(CronJobNamePrefix) + By("Inititalize a cron job") + job := CreateCronJobWithOptOutLabel(ctx, k8sClient, namespace, name) + createdObjects = append(createdObjects, job) + + triggerReconcileRequest(ctx, reconciler, "") + + VerifyNoEvents(ctx, clientset, namespace) + VerifyCronJobWithOptOutLabel(GetCronJob(ctx, k8sClient, namespace, name)) + }) + + It("should not instrument an existing daemon set with the opt-out label", func() { + name := UniqueName(DaemonSetNamePrefix) + By("Inititalize a daemon set") + daemonSet := CreateDaemonSetWithOptOutLabel(ctx, k8sClient, namespace, name) + createdObjects = append(createdObjects, daemonSet) + + triggerReconcileRequest(ctx, reconciler, "") + + VerifyNoEvents(ctx, clientset, namespace) + VerifyDaemonSetWithOptOutLabel(GetDaemonSet(ctx, k8sClient, namespace, name)) + }) + + It("should not instrument an existing deployment with the opt-out label", func() { + name := UniqueName(DeploymentNamePrefix) + By("Inititalize a deployment") + deployment := CreateDeploymentWithOptOutLabel(ctx, k8sClient, namespace, name) + createdObjects = append(createdObjects, deployment) + + triggerReconcileRequest(ctx, reconciler, "") + + VerifyNoEvents(ctx, clientset, namespace) + VerifyDeploymentWithOptOutLabel(GetDeployment(ctx, k8sClient, namespace, name)) + }) + + It("should not touch an existing job with the opt-out label", func() { + name := UniqueName(JobNamePrefix) + By("Inititalize a job") + job := CreateJobWithOptOutLabel(ctx, k8sClient, namespace, name) + createdObjects = append(createdObjects, job) + + triggerReconcileRequest(ctx, reconciler, "") + + VerifyNoEvents(ctx, clientset, namespace) + VerifyJobWithOptOutLabel(GetJob(ctx, k8sClient, namespace, name)) + }) + + It("should not instrument an existing orphan replicaset with the opt-out label", func() { + name := UniqueName(ReplicaSetNamePrefix) + By("Inititalize a replicaset") + replicaSet := CreateReplicaSetWithOptOutLabel(ctx, k8sClient, namespace, name) + createdObjects = append(createdObjects, replicaSet) + + triggerReconcileRequest(ctx, reconciler, "") + + VerifyNoEvents(ctx, clientset, namespace) + VerifyReplicaSetWithOptOutLabel(GetReplicaSet(ctx, k8sClient, namespace, name)) + }) + + It("should not instrument an stateful set with the opt-out label", func() { + name := UniqueName(StatefulSetNamePrefix) + By("Inititalize a stateful set") + statefulSet := CreateStatefulSetWithOptOutLabel(ctx, k8sClient, namespace, name) + createdObjects = append(createdObjects, statefulSet) + + triggerReconcileRequest(ctx, reconciler, "") + + VerifyNoEvents(ctx, clientset, namespace) + VerifyStatefulSetWithOptOutLabel(GetStatefulSet(ctx, k8sClient, namespace, name)) + }) + }) + + Describe("when reverting the instrumentation on cleanup", func() { It("should revert an instrumented cron job", func() { // We trigger one reconcile request before creating any workload and before deleting the Dash0 custom // resource, just to get the `isFirstReconcile` logic out of the way and to add the finalizer. @@ -221,7 +293,7 @@ var _ = Describe("Dash0 Controller", func() { // happens in production. triggerReconcileRequest(ctx, reconciler, "Trigger first reconcile request") - name := CronJobName + name := UniqueName(CronJobNamePrefix) By("Create an instrumented cron job") cronJob := CreateInstrumentedCronJob(ctx, k8sClient, namespace, name) createdObjects = append(createdObjects, cronJob) @@ -245,7 +317,7 @@ var _ = Describe("Dash0 Controller", func() { // happens in production. triggerReconcileRequest(ctx, reconciler, "Trigger first reconcile request") - name := DaemonSetName + name := UniqueName(DaemonSetNamePrefix) By("Create an instrumented daemon set") daemonSet := CreateInstrumentedDaemonSet(ctx, k8sClient, namespace, name) createdObjects = append(createdObjects, daemonSet) @@ -269,7 +341,7 @@ var _ = Describe("Dash0 Controller", func() { // happens in production. triggerReconcileRequest(ctx, reconciler, "Trigger first reconcile request") - name := DeploymentName + name := UniqueName(DeploymentNamePrefix) By("Create an instrumented deployment") deployment := CreateInstrumentedDeployment(ctx, k8sClient, namespace, name) createdObjects = append(createdObjects, deployment) @@ -293,7 +365,7 @@ var _ = Describe("Dash0 Controller", func() { // happens in production. triggerReconcileRequest(ctx, reconciler, "Trigger first reconcile request") - name := JobName2 + name := UniqueName(JobNamePrefix) By("Create an instrumented job") job := CreateInstrumentedJob(ctx, k8sClient, namespace, name) createdObjects = append(createdObjects, job) @@ -309,21 +381,23 @@ var _ = Describe("Dash0 Controller", func() { clientset, namespace, name, - "The controller's attempt to remove the Dash0 instrumentation from this workload has not been successful. Error message: Dash0 cannot remove the instrumentation from the existing job test-namespace/job2, since the this type of workload is immutable.", + fmt.Sprintf("The controller's attempt to remove the Dash0 instrumentation from this workload has not "+ + "been successful. Error message: Dash0 cannot remove the instrumentation from the existing job "+ + "test-namespace/%s, since the this type of workload is immutable.", name), ) VerifyModifiedJob(GetJob(ctx, k8sClient, namespace, name), BasicInstrumentedPodSpecExpectations) }) - It("should remove instrumentation labels from an existing uninstrumented job (which has been labelled by the controller without being instrumented)", func() { + It("should remove instrumentation labels from an existing job for which an instrumentation attempt has failed", func() { // We trigger one reconcile request before creating any workload and before deleting the Dash0 custom // resource, just to get the `isFirstReconcile` logic out of the way and to add the finalizer. // Alternatively, we could just add the finalizer here directly, but this approach is closer to what usually // happens in production. triggerReconcileRequest(ctx, reconciler, "Trigger first reconcile request") - name := JobName3 - By("Create a job with labels (dash0.instrumented=false)") - job := CreateJobWithInstrumentationLabels(ctx, k8sClient, namespace, name) + name := UniqueName(JobNamePrefix) + By("Create a job with labels (dash0.instrumented=unsuccessful)") + job := CreateJobForWhichAnInstrumentationAttemptHasFailed(ctx, k8sClient, namespace, name) createdObjects = append(createdObjects, job) By("Queue the deletion of the Dash0 custom resource") @@ -332,7 +406,7 @@ var _ = Describe("Dash0 Controller", func() { triggerReconcileRequest(ctx, reconciler, "Trigger a reconcile request to attempt to revert the instrumented job") - VerifyAlreadyNotInstrumented(ctx, clientset, namespace, name, "Dash0 instrumentation was not present on this workload, no modification by the controller has been necessary.") + VerifyNoUninstrumentationNecessaryEvent(ctx, clientset, namespace, name, "Dash0 instrumentation was not present on this workload, no modification by the controller has been necessary.") VerifyUnmodifiedJob(GetJob(ctx, k8sClient, namespace, name)) }) @@ -343,7 +417,7 @@ var _ = Describe("Dash0 Controller", func() { // happens in production. triggerReconcileRequest(ctx, reconciler, "Trigger first reconcile request") - name := ReplicaSetName + name := UniqueName(ReplicaSetNamePrefix) By("Create an instrumented replica set") replicaSet := CreateInstrumentedReplicaSet(ctx, k8sClient, namespace, name) createdObjects = append(createdObjects, replicaSet) @@ -367,7 +441,7 @@ var _ = Describe("Dash0 Controller", func() { // happens in production. triggerReconcileRequest(ctx, reconciler, "Trigger first reconcile request") - name := ReplicaSetName + name := UniqueName(ReplicaSetNamePrefix) By("Create an instrumented replica set owned by a deployment") replicaSet := CreateReplicaSetOwnedByDeployment(ctx, k8sClient, namespace, name) createdObjects = append(createdObjects, replicaSet) @@ -389,7 +463,7 @@ var _ = Describe("Dash0 Controller", func() { // happens in production. triggerReconcileRequest(ctx, reconciler, "Trigger first reconcile request") - name := StatefulSetName + name := UniqueName(StatefulSetNamePrefix) By("Create an instrumented stateful set") statefulSet := CreateInstrumentedStatefulSet(ctx, k8sClient, namespace, name) createdObjects = append(createdObjects, statefulSet) @@ -406,6 +480,152 @@ var _ = Describe("Dash0 Controller", func() { VerifyWebhookIgnoreOnceLabelIsPresent(&statefulSet.ObjectMeta) }) }) + + Describe("when attempting to revert the instrumentation on cleanup but the resource has an opt-out label", func() { + It("should not attempt to revert a cron job that has the opt-out label", func() { + // We trigger one reconcile request before creating any workload and before deleting the Dash0 custom + // resource, just to get the `isFirstReconcile` logic out of the way and to add the finalizer. + // Alternatively, we could just add the finalizer here directly, but this approach is closer to what usually + // happens in production. + triggerReconcileRequest(ctx, reconciler, "Trigger first reconcile request") + + name := UniqueName(CronJobNamePrefix) + By("Create a cron job") + cronJob := CreateCronJobWithOptOutLabel(ctx, k8sClient, namespace, name) + createdObjects = append(createdObjects, cronJob) + + By("Queue the deletion of the Dash0 custom resource") + dash0CustomResource := loadDash0CustomResourceOrFail(ctx, Default) + Expect(k8sClient.Delete(ctx, dash0CustomResource)).To(Succeed()) + + triggerReconcileRequest(ctx, reconciler, "") + + VerifyNoEvents(ctx, clientset, namespace) + cronJob = GetCronJob(ctx, k8sClient, namespace, name) + VerifyCronJobWithOptOutLabel(cronJob) + VerifyWebhookIgnoreOnceLabelIsAbesent(&cronJob.ObjectMeta) + }) + + It("should not attempt to revert a daemon set that has the opt-out label", func() { + // We trigger one reconcile request before creating any workload and before deleting the Dash0 custom + // resource, just to get the `isFirstReconcile` logic out of the way and to add the finalizer. + // Alternatively, we could just add the finalizer here directly, but this approach is closer to what usually + // happens in production. + triggerReconcileRequest(ctx, reconciler, "Trigger first reconcile request") + + name := UniqueName(DaemonSetNamePrefix) + By("Create a daemon set") + daemonSet := CreateDaemonSetWithOptOutLabel(ctx, k8sClient, namespace, name) + createdObjects = append(createdObjects, daemonSet) + + By("Queue the deletion of the Dash0 custom resource") + dash0CustomResource := loadDash0CustomResourceOrFail(ctx, Default) + Expect(k8sClient.Delete(ctx, dash0CustomResource)).To(Succeed()) + + triggerReconcileRequest(ctx, reconciler, "") + + VerifyNoEvents(ctx, clientset, namespace) + daemonSet = GetDaemonSet(ctx, k8sClient, namespace, name) + VerifyDaemonSetWithOptOutLabel(daemonSet) + VerifyWebhookIgnoreOnceLabelIsAbesent(&daemonSet.ObjectMeta) + }) + + It("should not attempt to revert a deployment that has the opt-out label", func() { + // We trigger one reconcile request before creating any workload and before deleting the Dash0 custom + // resource, just to get the `isFirstReconcile` logic out of the way and to add the finalizer. + // Alternatively, we could just add the finalizer here directly, but this approach is closer to what usually + // happens in production. + triggerReconcileRequest(ctx, reconciler, "Trigger first reconcile request") + + name := UniqueName(DeploymentNamePrefix) + By("Create a deployment") + deployment := CreateDeploymentWithOptOutLabel(ctx, k8sClient, namespace, name) + createdObjects = append(createdObjects, deployment) + + By("Queue the deletion of the Dash0 custom resource") + dash0CustomResource := loadDash0CustomResourceOrFail(ctx, Default) + Expect(k8sClient.Delete(ctx, dash0CustomResource)).To(Succeed()) + + triggerReconcileRequest(ctx, reconciler, "") + + VerifyNoEvents(ctx, clientset, namespace) + deployment = GetDeployment(ctx, k8sClient, namespace, name) + VerifyDeploymentWithOptOutLabel(deployment) + VerifyWebhookIgnoreOnceLabelIsAbesent(&deployment.ObjectMeta) + }) + + It("should not attempt to revert a job that has the opt-out label", func() { + // We trigger one reconcile request before creating any workload and before deleting the Dash0 custom + // resource, just to get the `isFirstReconcile` logic out of the way and to add the finalizer. + // Alternatively, we could just add the finalizer here directly, but this approach is closer to what usually + // happens in production. + triggerReconcileRequest(ctx, reconciler, "Trigger first reconcile request") + + name := UniqueName(JobNamePrefix) + By("Create a job") + job := CreateJobWithOptOutLabel(ctx, k8sClient, namespace, name) + createdObjects = append(createdObjects, job) + + By("Queue the deletion of the Dash0 custom resource") + dash0CustomResource := loadDash0CustomResourceOrFail(ctx, Default) + Expect(k8sClient.Delete(ctx, dash0CustomResource)).To(Succeed()) + + triggerReconcileRequest(ctx, reconciler, "Trigger a reconcile request to attempt to revert the instrumented job") + + VerifyNoEvents(ctx, clientset, namespace) + job = GetJob(ctx, k8sClient, namespace, name) + VerifyJobWithOptOutLabel(job) + VerifyWebhookIgnoreOnceLabelIsAbesent(&job.ObjectMeta) + }) + + It("should not attempt to revert an orphan replica set that has the opt-out label", func() { + // We trigger one reconcile request before creating any workload and before deleting the Dash0 custom + // resource, just to get the `isFirstReconcile` logic out of the way and to add the finalizer. + // Alternatively, we could just add the finalizer here directly, but this approach is closer to what usually + // happens in production. + triggerReconcileRequest(ctx, reconciler, "Trigger first reconcile request") + + name := UniqueName(ReplicaSetNamePrefix) + By("Create a replica set") + replicaSet := CreateReplicaSetWithOptOutLabel(ctx, k8sClient, namespace, name) + createdObjects = append(createdObjects, replicaSet) + + By("Queue the deletion of the Dash0 custom resource") + dash0CustomResource := loadDash0CustomResourceOrFail(ctx, Default) + Expect(k8sClient.Delete(ctx, dash0CustomResource)).To(Succeed()) + + triggerReconcileRequest(ctx, reconciler, "") + + VerifyNoEvents(ctx, clientset, namespace) + replicaSet = GetReplicaSet(ctx, k8sClient, namespace, name) + VerifyReplicaSetWithOptOutLabel(replicaSet) + VerifyWebhookIgnoreOnceLabelIsAbesent(&replicaSet.ObjectMeta) + }) + + It("should not attempt to revert a stateful set that has the opt-out label", func() { + // We trigger one reconcile request before creating any workload and before deleting the Dash0 custom + // resource, just to get the `isFirstReconcile` logic out of the way and to add the finalizer. + // Alternatively, we could just add the finalizer here directly, but this approach is closer to what usually + // happens in production. + triggerReconcileRequest(ctx, reconciler, "Trigger first reconcile request") + + name := UniqueName(StatefulSetNamePrefix) + By("Create a stateful set") + statefulSet := CreateStatefulSetWithOptOutLabel(ctx, k8sClient, namespace, name) + createdObjects = append(createdObjects, statefulSet) + + By("Queue the deletion of the Dash0 custom resource") + dash0CustomResource := loadDash0CustomResourceOrFail(ctx, Default) + Expect(k8sClient.Delete(ctx, dash0CustomResource)).To(Succeed()) + + triggerReconcileRequest(ctx, reconciler, "") + + VerifyNoEvents(ctx, clientset, namespace) + statefulSet = GetStatefulSet(ctx, k8sClient, namespace, name) + VerifyStatefulSetWithOptOutLabel(statefulSet) + VerifyWebhookIgnoreOnceLabelIsAbesent(&statefulSet.ObjectMeta) + }) + }) }) func triggerReconcileRequest(ctx context.Context, reconciler *Dash0Reconciler, stepMessage string) { @@ -475,14 +695,3 @@ func removeFinalizer(ctx context.Context, dash0CustomResource *operatorv1alpha1. Expect(k8sClient.Update(ctx, dash0CustomResource)).To(Succeed()) } } - -func deleteAllEvents( - ctx context.Context, - clientset *kubernetes.Clientset, - namespace string, -) { - err := clientset.CoreV1().Events(namespace).DeleteCollection(ctx, metav1.DeleteOptions{ - GracePeriodSeconds: new(int64), // delete immediately - }, metav1.ListOptions{}) - Expect(err).NotTo(HaveOccurred()) -} diff --git a/internal/util/k8sevents.go b/internal/util/k8sevents.go index f811ba4b..a47a271a 100644 --- a/internal/util/k8sevents.go +++ b/internal/util/k8sevents.go @@ -52,7 +52,7 @@ func QueueNoUninstrumentationNecessaryEvent(eventRecorder record.EventRecorder, eventRecorder.Event( resource, corev1.EventTypeNormal, - string(ReasonAlreadyNotInstrumented), + string(ReasonNoUninstrumentationNecessary), fmt.Sprintf("Dash0 instrumentation was not present on this workload, no modification by the %s has been necessary.", eventSource), ) } diff --git a/internal/util/labels.go b/internal/util/labels.go index 0ca6b30c..d9bd1c75 100644 --- a/internal/util/labels.go +++ b/internal/util/labels.go @@ -4,24 +4,120 @@ package util import ( + "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( - InstrumentedLabelKey = "dash0.instrumented" - OperatorVersionLabelKey = "dash0.operator.version" - InitContainerImageVersionLabelKey = "dash0.initcontainer.image.version" - InstrumentedByLabelKey = "dash0.instrumented.by" - WebhookIgnoreOnceLabelKey = "dash0.webhook.ignore.once" + instrumentedLabelKey = "dash0.instrumented" + + // InstrumentedLabelValueSuccessful os written by the operator when then instrumentation attempt has been + // successful. + instrumentedLabelValueSuccessful instrumentedState = "successful" + + // InstrumentedLabelValueUnsuccessful is written by the operator when then instrumentation attempt has failed. + instrumentedLabelValueUnsuccessful instrumentedState = "unsuccessful" + + // InstrumentedLabelValueOptOut is never written by the operator, this can be set externally to opt-out of + // instrumentation for a particular workload + instrumentedLabelValueOptOut instrumentedState = "false" + + // InstrumentedLabelValueUnknown should never occur in the wild, it is used as a fallback for inconsistent states + // or when the label is missing entirely. + instrumentedLabelValueUnknown instrumentedState = "unknown" + + operatorVersionLabelKey = "dash0.operator.version" + initContainerImageVersionLabelKey = "dash0.initcontainer.image.version" + instrumentedByLabelKey = "dash0.instrumented.by" + webhookIgnoreOnceLabelKey = "dash0.webhook.ignore.once" +) + +var ( + WorkloadsWithoutDash0InstrumentedLabelFilter = metav1.ListOptions{ + LabelSelector: fmt.Sprintf("!%s", instrumentedLabelKey), + } + WorkloadsWithDash0InstrumentedLabelExlucdingOptOutFilter = metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%[1]s,%[1]s != false", instrumentedLabelKey), + } ) +type instrumentedState string + +func AddInstrumentationLabels( + meta *metav1.ObjectMeta, + instrumenationSuccess bool, + instrumentationMetadata InstrumentationMetadata, +) { + if instrumenationSuccess { + addLabel(meta, instrumentedLabelKey, string(instrumentedLabelValueSuccessful)) + } else { + addLabel(meta, instrumentedLabelKey, string(instrumentedLabelValueUnsuccessful)) + } + addLabel(meta, operatorVersionLabelKey, instrumentationMetadata.OperatorVersion) + addLabel(meta, initContainerImageVersionLabelKey, instrumentationMetadata.InitContainerImageVersion) + addLabel(meta, instrumentedByLabelKey, instrumentationMetadata.InstrumentedBy) +} + func AddWebhookIgnoreOnceLabel(meta *metav1.ObjectMeta) { - AddLabel(meta, WebhookIgnoreOnceLabelKey, "true") + addLabel(meta, webhookIgnoreOnceLabelKey, "true") } -func AddLabel(meta *metav1.ObjectMeta, key string, value string) { +func addLabel(meta *metav1.ObjectMeta, key string, value string) { if meta.Labels == nil { meta.Labels = make(map[string]string, 1) } meta.Labels[key] = value } + +func RemoveInstrumentationLabels(meta *metav1.ObjectMeta) { + if meta.GetLabels()[instrumentedLabelKey] != "false" { + removeLabel(meta, instrumentedLabelKey) + } + removeLabel(meta, operatorVersionLabelKey) + removeLabel(meta, initContainerImageVersionLabelKey) + removeLabel(meta, instrumentedByLabelKey) +} + +func removeLabel(meta *metav1.ObjectMeta, key string) { + delete(meta.Labels, key) +} + +func HasBeenInstrumentedSuccessfully(meta *metav1.ObjectMeta) bool { + return readInstrumentationState(meta) == instrumentedLabelValueSuccessful +} + +func InstrumenationAttemptHasFailed(meta *metav1.ObjectMeta) bool { + return readInstrumentationState(meta) == instrumentedLabelValueUnsuccessful +} + +func HasOptedOutOfInstrumenation(meta *metav1.ObjectMeta) bool { + return readInstrumentationState(meta) == instrumentedLabelValueOptOut +} + +func readInstrumentationState(meta *metav1.ObjectMeta) instrumentedState { + if meta.Labels == nil { + return instrumentedLabelValueUnknown + } + switch meta.Labels[instrumentedLabelKey] { + case string(instrumentedLabelValueSuccessful): + return instrumentedLabelValueSuccessful + case string(instrumentedLabelValueUnsuccessful): + return instrumentedLabelValueUnsuccessful + case string(instrumentedLabelValueOptOut): + return instrumentedLabelValueOptOut + default: + return instrumentedLabelValueUnknown + } +} + +func CheckAndDeleteIgnoreOnceLabel(meta *metav1.ObjectMeta) bool { + if meta.Labels == nil { + return false + } + if value, ok := meta.Labels[webhookIgnoreOnceLabelKey]; ok && value == "true" { + delete(meta.Labels, webhookIgnoreOnceLabelKey) + return true + } + return false +} diff --git a/internal/util/types.go b/internal/util/types.go index 245aa2f8..d195244f 100644 --- a/internal/util/types.go +++ b/internal/util/types.go @@ -10,12 +10,12 @@ const ( ConditionTypeAvailable ConditionType = "Available" ConditionTypeDegraded ConditionType = "Degraded" - ReasonSuccessfulInstrumentation Reason = "SuccessfulInstrumentation" - ReasonNoInstrumentationNecessary Reason = "ReasonAlreadyInstrumented" - ReasonFailedInstrumentation Reason = "FailedInstrumentation" - ReasonSuccessfulUninstrumentation Reason = "SuccessfulUninstrumentation" - ReasonAlreadyNotInstrumented Reason = "ReasonAlreadyNotInstrumented" - ReasonFailedUninstrumentation Reason = "FailedUninstrumentation" + ReasonSuccessfulInstrumentation Reason = "SuccessfulInstrumentation" + ReasonNoInstrumentationNecessary Reason = "ReasonAlreadyInstrumented" + ReasonFailedInstrumentation Reason = "FailedInstrumentation" + ReasonSuccessfulUninstrumentation Reason = "SuccessfulUninstrumentation" + ReasonNoUninstrumentationNecessary Reason = "ReasonAlreadyNotInstrumented" + ReasonFailedUninstrumentation Reason = "FailedUninstrumentation" ) type Versions struct { diff --git a/internal/webhook/dash0_webhook.go b/internal/webhook/dash0_webhook.go index dfaf308d..2bc94481 100644 --- a/internal/webhook/dash0_webhook.go +++ b/internal/webhook/dash0_webhook.go @@ -12,7 +12,6 @@ import ( "github.com/go-logr/logr" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/tools/record" @@ -110,9 +109,12 @@ func (h *Handler) handleCronJob( if failed { return responseIfFailed } - if isIgnored(&cronJob.ObjectMeta) { + if util.CheckAndDeleteIgnoreOnceLabel(&cronJob.ObjectMeta) { return h.postProcess(request, cronJob, false, true, logger) } + if util.HasOptedOutOfInstrumenation(&cronJob.ObjectMeta) { + return admission.Allowed("not instrumenting this resource due to dash0.instrumented=false (opt-out)") + } hasBeenModified := h.newWorkloadModifier(logger).ModifyCronJob(cronJob) return h.postProcess(request, cronJob, hasBeenModified, false, logger) } @@ -127,9 +129,12 @@ func (h *Handler) handleDaemonSet( if failed { return responseIfFailed } - if isIgnored(&daemonSet.ObjectMeta) { + if util.CheckAndDeleteIgnoreOnceLabel(&daemonSet.ObjectMeta) { return h.postProcess(request, daemonSet, false, true, logger) } + if util.HasOptedOutOfInstrumenation(&daemonSet.ObjectMeta) { + return admission.Allowed("not instrumenting this resource due to dash0.instrumented=false (opt-out)") + } hasBeenModified := h.newWorkloadModifier(logger).ModifyDaemonSet(daemonSet) return h.postProcess(request, daemonSet, hasBeenModified, false, logger) } @@ -144,9 +149,12 @@ func (h *Handler) handleDeployment( if failed { return responseIfFailed } - if isIgnored(&deployment.ObjectMeta) { + if util.CheckAndDeleteIgnoreOnceLabel(&deployment.ObjectMeta) { return h.postProcess(request, deployment, false, true, logger) } + if util.HasOptedOutOfInstrumenation(&deployment.ObjectMeta) { + return admission.Allowed("not instrumenting this resource due to dash0.instrumented=false (opt-out)") + } hasBeenModified := h.newWorkloadModifier(logger).ModifyDeployment(deployment) return h.postProcess(request, deployment, hasBeenModified, false, logger) } @@ -161,9 +169,12 @@ func (h *Handler) handleJob( if failed { return responseIfFailed } - if isIgnored(&job.ObjectMeta) { + if util.CheckAndDeleteIgnoreOnceLabel(&job.ObjectMeta) { return h.postProcess(request, job, false, true, logger) } + if util.HasOptedOutOfInstrumenation(&job.ObjectMeta) { + return admission.Allowed("not instrumenting this resource due to dash0.instrumented=false (opt-out)") + } hasBeenModified := h.newWorkloadModifier(logger).ModifyJob(job) return h.postProcess(request, job, hasBeenModified, false, logger) } @@ -178,9 +189,12 @@ func (h *Handler) handleReplicaSet( if failed { return responseIfFailed } - if isIgnored(&replicaSet.ObjectMeta) { + if util.CheckAndDeleteIgnoreOnceLabel(&replicaSet.ObjectMeta) { return h.postProcess(request, replicaSet, false, true, logger) } + if util.HasOptedOutOfInstrumenation(&replicaSet.ObjectMeta) { + return admission.Allowed("not instrumenting this resource due to dash0.instrumented=false (opt-out)") + } hasBeenModified := h.newWorkloadModifier(logger).ModifyReplicaSet(replicaSet) return h.postProcess(request, replicaSet, hasBeenModified, false, logger) } @@ -195,9 +209,12 @@ func (h *Handler) handleStatefulSet( if failed { return responseIfFailed } - if isIgnored(&statefulSet.ObjectMeta) { + if util.CheckAndDeleteIgnoreOnceLabel(&statefulSet.ObjectMeta) { return h.postProcess(request, statefulSet, false, true, logger) } + if util.HasOptedOutOfInstrumenation(&statefulSet.ObjectMeta) { + return admission.Allowed("not instrumenting this resource due to dash0.instrumented=false (opt-out)") + } hasBeenModified := h.newWorkloadModifier(logger).ModifyStatefulSet(statefulSet) return h.postProcess(request, statefulSet, hasBeenModified, false, logger) } @@ -215,17 +232,6 @@ func (h *Handler) preProcess( return admission.Response{}, false } -func isIgnored(meta *metav1.ObjectMeta) bool { - if meta.Labels == nil { - return false - } - if value, ok := meta.Labels[util.WebhookIgnoreOnceLabelKey]; ok && value == "true" { - delete(meta.Labels, util.WebhookIgnoreOnceLabelKey) - return true - } - return false -} - func (h *Handler) postProcess( request admission.Request, resource runtime.Object, @@ -246,7 +252,7 @@ func (h *Handler) postProcess( } if ignored { - logger.Info(fmt.Sprintf("Ignoring this admission request due to the presence of %s.", util.WebhookIgnoreOnceLabelKey)) + logger.Info("Ignoring this admission request due to the presence of dash0.webhook.ignore.once") // deliberately not queueing an event for this case return admission.PatchResponseFromRaw(request.Object.Raw, marshalled) } diff --git a/internal/webhook/dash0_webhook_test.go b/internal/webhook/dash0_webhook_test.go index 7708c883..9dde62b8 100644 --- a/internal/webhook/dash0_webhook_test.go +++ b/internal/webhook/dash0_webhook_test.go @@ -4,13 +4,11 @@ package webhook import ( - "fmt" - - "github.com/dash0hq/dash0-operator/internal/util" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + . "github.com/dash0hq/dash0-operator/test/util" ) @@ -19,33 +17,36 @@ import ( // covered modify_test.go, while more fine-grained test cases and variations should rather be added to // k8sresources/modify_test.go. -var _ = Describe("Dash0 Webhook", func() { - AfterEach(func() { - _ = k8sClient.Delete(ctx, BasicCronJob(TestNamespaceName, CronJobName)) - _ = k8sClient.Delete(ctx, BasicDaemonSet(TestNamespaceName, DaemonSetName)) - _ = k8sClient.Delete(ctx, BasicDeployment(TestNamespaceName, DeploymentName)) - _ = k8sClient.Delete(ctx, BasicJob(TestNamespaceName, JobName1)) - err := k8sClient.Delete(ctx, BasicReplicaSet(TestNamespaceName, ReplicaSetName)) - if err != nil { - fmt.Fprintf(GinkgoWriter, "cannot delete replicaset: %v\n", err) - } - _ = k8sClient.Delete(ctx, BasicStatefulSet(TestNamespaceName, StatefulSetName)) +var _ = Describe("The Dash0 webhook", func() { + var createdObjects []client.Object + BeforeEach(func() { + createdObjects = make([]client.Object, 0) + }) + + AfterEach(func() { + createdObjects = DeleteAllCreatedObjects(ctx, k8sClient, createdObjects) + DeleteAllEvents(ctx, clientset, TestNamespaceName) }) Context("when mutating new deployments", func() { - It("should inject Dash0 into a new basic deployment", func() { - CreateBasicDeployment(ctx, k8sClient, TestNamespaceName, DeploymentName) - deployment := GetDeployment(ctx, k8sClient, TestNamespaceName, DeploymentName) - VerifyModifiedDeployment(deployment, BasicInstrumentedPodSpecExpectations) + It("should instrument a new basic deployment", func() { + name := UniqueName(DeploymentNamePrefix) + workload := CreateBasicDeployment(ctx, k8sClient, TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + workload = GetDeployment(ctx, k8sClient, TestNamespaceName, name) + VerifyModifiedDeployment(workload, BasicInstrumentedPodSpecExpectations) + VerifySuccessfulInstrumentationEvent(ctx, clientset, TestNamespaceName, name, "webhook") }) - It("should inject Dash0 into a new deployment that has multiple containers, and already has volumes and init containers", func() { - deployment := DeploymentWithMoreBellsAndWhistles(TestNamespaceName, DeploymentName) - Expect(k8sClient.Create(ctx, deployment)).Should(Succeed()) + It("should instrument a new deployment that has multiple containers, and already has volumes and init containers", func() { + name := UniqueName(DeploymentNamePrefix) + workload := DeploymentWithMoreBellsAndWhistles(TestNamespaceName, name) + Expect(k8sClient.Create(ctx, workload)).Should(Succeed()) + createdObjects = append(createdObjects, workload) - deployment = GetDeployment(ctx, k8sClient, TestNamespaceName, DeploymentName) - VerifyModifiedDeployment(deployment, PodSpecExpectations{ + workload = GetDeployment(ctx, k8sClient, TestNamespaceName, name) + VerifyModifiedDeployment(workload, PodSpecExpectations{ Volumes: 3, Dash0VolumeIdx: 2, InitContainers: 3, @@ -69,15 +70,17 @@ var _ = Describe("Dash0 Webhook", func() { }, }, }) - VerifySuccessfulInstrumentationEvent(ctx, clientset, TestNamespaceName, DeploymentName, "webhook") + VerifySuccessfulInstrumentationEvent(ctx, clientset, TestNamespaceName, name, "webhook") }) It("should update existing Dash0 artifacts in a new deployment", func() { - deployment := DeploymentWithExistingDash0Artifacts(TestNamespaceName, DeploymentName) - Expect(k8sClient.Create(ctx, deployment)).Should(Succeed()) + name := UniqueName(DeploymentNamePrefix) + workload := DeploymentWithExistingDash0Artifacts(TestNamespaceName, name) + Expect(k8sClient.Create(ctx, workload)).Should(Succeed()) + createdObjects = append(createdObjects, workload) - deployment = GetDeployment(ctx, k8sClient, TestNamespaceName, DeploymentName) - VerifyModifiedDeployment(deployment, PodSpecExpectations{ + workload = GetDeployment(ctx, k8sClient, TestNamespaceName, name) + VerifyModifiedDeployment(workload, PodSpecExpectations{ Volumes: 3, Dash0VolumeIdx: 1, InitContainers: 3, @@ -103,99 +106,203 @@ var _ = Describe("Dash0 Webhook", func() { }, }, }) + VerifySuccessfulInstrumentationEvent(ctx, clientset, TestNamespaceName, name, "webhook") + }) + + It("should instrument a new basic cron job", func() { + name := UniqueName(CronJobNamePrefix) + workload := CreateBasicCronJob(ctx, k8sClient, TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + workload = GetCronJob(ctx, k8sClient, TestNamespaceName, name) + VerifyModifiedCronJob(workload, BasicInstrumentedPodSpecExpectations) + VerifySuccessfulInstrumentationEvent(ctx, clientset, TestNamespaceName, name, "webhook") + }) + + It("should instrument a new basic daemon set", func() { + name := UniqueName(CronJobNamePrefix) + workload := CreateBasicDaemonSet(ctx, k8sClient, TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + workload = GetDaemonSet(ctx, k8sClient, TestNamespaceName, name) + VerifyModifiedDaemonSet(workload, BasicInstrumentedPodSpecExpectations) + VerifySuccessfulInstrumentationEvent(ctx, clientset, TestNamespaceName, name, "webhook") + }) + + It("should instrument a new basic job", func() { + name := UniqueName(CronJobNamePrefix) + workload := CreateBasicJob(ctx, k8sClient, TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + workload = GetJob(ctx, k8sClient, TestNamespaceName, name) + VerifyModifiedJob(workload, BasicInstrumentedPodSpecExpectations) + VerifySuccessfulInstrumentationEvent(ctx, clientset, TestNamespaceName, name, "webhook") + }) + + It("should instrument a new basic replica set", func() { + name := UniqueName(CronJobNamePrefix) + workload := CreateBasicReplicaSet(ctx, k8sClient, TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + workload = GetReplicaSet(ctx, k8sClient, TestNamespaceName, name) + VerifyModifiedReplicaSet(workload, BasicInstrumentedPodSpecExpectations) + VerifySuccessfulInstrumentationEvent(ctx, clientset, TestNamespaceName, name, "webhook") + }) + + It("should not instrument a new replica set owned by a deployment", func() { + name := UniqueName(ReplicaSetNamePrefix) + workload := CreateReplicaSetOwnedByDeployment(ctx, k8sClient, TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + workload = GetReplicaSet(ctx, k8sClient, TestNamespaceName, name) + VerifyUnmodifiedReplicaSet(workload) + VerifyNoInstrumentationNecessaryEvent(ctx, clientset, TestNamespaceName, name, "webhook") }) - It("should inject Dash0 into a new basic cron job", func() { - CreateBasicCronJob(ctx, k8sClient, TestNamespaceName, CronJobName) - cronJob := GetCronJob(ctx, k8sClient, TestNamespaceName, CronJobName) - VerifyModifiedCronJob(cronJob, BasicInstrumentedPodSpecExpectations) + It("should instrument a new basic stateful set", func() { + name := UniqueName(CronJobNamePrefix) + workload := CreateBasicStatefulSet(ctx, k8sClient, TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + workload = GetStatefulSet(ctx, k8sClient, TestNamespaceName, name) + VerifyModifiedStatefulSet(workload, BasicInstrumentedPodSpecExpectations) + VerifySuccessfulInstrumentationEvent(ctx, clientset, TestNamespaceName, name, "webhook") + }) + }) + + Context("when workload has opted out of instrumentation", func() { + It("should not instrument a cron job that has opted out of instrumentation", func() { + name := UniqueName(CronJobNamePrefix) + workload := CronJobWithOptOutLabel(TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + AddLabel(&workload.ObjectMeta, "dash0.instrumented", "false") + CreateWorkload(ctx, k8sClient, workload) + workload = GetCronJob(ctx, k8sClient, TestNamespaceName, name) + VerifyCronJobWithOptOutLabel(workload) + VerifyNoEvents(ctx, clientset, TestNamespaceName) }) - It("should inject Dash0 into a new basic daemon set", func() { - CreateBasicDaemonSet(ctx, k8sClient, TestNamespaceName, DaemonSetName) - daemonSet := GetDaemonSet(ctx, k8sClient, TestNamespaceName, DaemonSetName) - VerifyModifiedDaemonSet(daemonSet, BasicInstrumentedPodSpecExpectations) + It("should not instrument a daemonset that has opted out of instrumentation", func() { + name := UniqueName(DaemonSetNamePrefix) + workload := DaemonSetWithOptOutLabel(TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + AddLabel(&workload.ObjectMeta, "dash0.instrumented", "false") + CreateWorkload(ctx, k8sClient, workload) + workload = GetDaemonSet(ctx, k8sClient, TestNamespaceName, name) + VerifyDaemonSetWithOptOutLabel(workload) + VerifyNoEvents(ctx, clientset, TestNamespaceName) }) - It("should inject Dash0 into a new basic job", func() { - CreateBasicJob(ctx, k8sClient, TestNamespaceName, JobName1) - job := GetJob(ctx, k8sClient, TestNamespaceName, JobName1) - VerifyModifiedJob(job, BasicInstrumentedPodSpecExpectations) + It("should not instrument a deployment that has opted out of instrumentation", func() { + name := UniqueName(DeploymentNamePrefix) + workload := DeploymentWithOptOutLabel(TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + AddLabel(&workload.ObjectMeta, "dash0.instrumented", "false") + CreateWorkload(ctx, k8sClient, workload) + workload = GetDeployment(ctx, k8sClient, TestNamespaceName, name) + VerifyDeploymentWithOptOutLabel(workload) + VerifyNoEvents(ctx, clientset, TestNamespaceName) }) - It("should inject Dash0 into a new basic replica set", func() { - CreateBasicReplicaSet(ctx, k8sClient, TestNamespaceName, ReplicaSetName) - replicaSet := GetReplicaSet(ctx, k8sClient, TestNamespaceName, ReplicaSetName) - VerifyModifiedReplicaSet(replicaSet, BasicInstrumentedPodSpecExpectations) + It("should not instrument a job that has opted out of instrumentation", func() { + name := UniqueName(JobNamePrefix) + workload := JobWithOptOutLabel(TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + AddLabel(&workload.ObjectMeta, "dash0.instrumented", "false") + CreateWorkload(ctx, k8sClient, workload) + workload = GetJob(ctx, k8sClient, TestNamespaceName, name) + VerifyJobWithOptOutLabel(workload) + VerifyNoEvents(ctx, clientset, TestNamespaceName) }) - It("should not inject Dash0 into a new replica set owned by a deployment", func() { - CreateReplicaSetOwnedByDeployment(ctx, k8sClient, TestNamespaceName, ReplicaSetName) - replicaSet := GetReplicaSet(ctx, k8sClient, TestNamespaceName, ReplicaSetName) - VerifyUnmodifiedReplicaSet(replicaSet) + It("should not instrument an orphan replica set that has opted out of instrumentation", func() { + name := UniqueName(ReplicaSetNamePrefix) + workload := ReplicaSetWithOptOutLabel(TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + AddLabel(&workload.ObjectMeta, "dash0.instrumented", "false") + CreateWorkload(ctx, k8sClient, workload) + workload = GetReplicaSet(ctx, k8sClient, TestNamespaceName, name) + VerifyReplicaSetWithOptOutLabel(workload) + VerifyNoEvents(ctx, clientset, TestNamespaceName) }) - It("should inject Dash0 into a new basic stateful set", func() { - CreateBasicStatefulSet(ctx, k8sClient, TestNamespaceName, StatefulSetName) - statefulSet := GetStatefulSet(ctx, k8sClient, TestNamespaceName, StatefulSetName) - VerifyModifiedStatefulSet(statefulSet, BasicInstrumentedPodSpecExpectations) + It("should not instrument a stateful set that has opted out of instrumentation", func() { + name := UniqueName(StatefulSetNamePrefix) + workload := StatefulSetWithOptOutLabel(TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + AddLabel(&workload.ObjectMeta, "dash0.instrumented", "false") + CreateWorkload(ctx, k8sClient, workload) + workload = GetStatefulSet(ctx, k8sClient, TestNamespaceName, name) + VerifyStatefulSetWithOptOutLabel(workload) + VerifyNoEvents(ctx, clientset, TestNamespaceName) }) }) Context("when seeing the ignore once label", func() { It("should not instrument a cron job that has the label, but remove the label", func() { - workload := BasicCronJob(TestNamespaceName, DeploymentName) - AddLabel(&workload.ObjectMeta, util.WebhookIgnoreOnceLabelKey, "true") + name := UniqueName(CronJobNamePrefix) + workload := BasicCronJob(TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + AddLabel(&workload.ObjectMeta, "dash0.webhook.ignore.once", "true") CreateWorkload(ctx, k8sClient, workload) - workload = GetCronJob(ctx, k8sClient, TestNamespaceName, DeploymentName) + workload = GetCronJob(ctx, k8sClient, TestNamespaceName, name) VerifyUnmodifiedCronJob(workload) VerifyWebhookIgnoreOnceLabelIsAbesent(&workload.ObjectMeta) + VerifyNoEvents(ctx, clientset, TestNamespaceName) }) It("should not instrument a daemonset that has the label, but remove the label", func() { - workload := BasicDaemonSet(TestNamespaceName, DeploymentName) - AddLabel(&workload.ObjectMeta, util.WebhookIgnoreOnceLabelKey, "true") + name := UniqueName(DaemonSetNamePrefix) + workload := BasicDaemonSet(TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + AddLabel(&workload.ObjectMeta, "dash0.webhook.ignore.once", "true") CreateWorkload(ctx, k8sClient, workload) - workload = GetDaemonSet(ctx, k8sClient, TestNamespaceName, DeploymentName) + workload = GetDaemonSet(ctx, k8sClient, TestNamespaceName, name) VerifyUnmodifiedDaemonSet(workload) VerifyWebhookIgnoreOnceLabelIsAbesent(&workload.ObjectMeta) + VerifyNoEvents(ctx, clientset, TestNamespaceName) }) It("should not instrument a deployment that has the label, but remove the label", func() { - workload := BasicDeployment(TestNamespaceName, DeploymentName) - AddLabel(&workload.ObjectMeta, util.WebhookIgnoreOnceLabelKey, "true") + name := UniqueName(DeploymentNamePrefix) + workload := BasicDeployment(TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + AddLabel(&workload.ObjectMeta, "dash0.webhook.ignore.once", "true") CreateWorkload(ctx, k8sClient, workload) - workload = GetDeployment(ctx, k8sClient, TestNamespaceName, DeploymentName) + workload = GetDeployment(ctx, k8sClient, TestNamespaceName, name) VerifyUnmodifiedDeployment(workload) VerifyWebhookIgnoreOnceLabelIsAbesent(&workload.ObjectMeta) + VerifyNoEvents(ctx, clientset, TestNamespaceName) }) It("should not instrument a job that has the label, but remove the label", func() { - workload := BasicJob(TestNamespaceName, DeploymentName) - AddLabel(&workload.ObjectMeta, util.WebhookIgnoreOnceLabelKey, "true") + name := UniqueName(JobNamePrefix) + workload := BasicJob(TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + AddLabel(&workload.ObjectMeta, "dash0.webhook.ignore.once", "true") CreateWorkload(ctx, k8sClient, workload) - workload = GetJob(ctx, k8sClient, TestNamespaceName, DeploymentName) + workload = GetJob(ctx, k8sClient, TestNamespaceName, name) VerifyUnmodifiedJob(workload) VerifyWebhookIgnoreOnceLabelIsAbesent(&workload.ObjectMeta) + VerifyNoEvents(ctx, clientset, TestNamespaceName) }) It("should not instrument an orphan replica set that has the label, but remove the label", func() { - workload := BasicReplicaSet(TestNamespaceName, DeploymentName) - AddLabel(&workload.ObjectMeta, util.WebhookIgnoreOnceLabelKey, "true") + name := UniqueName(ReplicaSetNamePrefix) + workload := BasicReplicaSet(TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + AddLabel(&workload.ObjectMeta, "dash0.webhook.ignore.once", "true") CreateWorkload(ctx, k8sClient, workload) - workload = GetReplicaSet(ctx, k8sClient, TestNamespaceName, DeploymentName) + workload = GetReplicaSet(ctx, k8sClient, TestNamespaceName, name) VerifyUnmodifiedReplicaSet(workload) VerifyWebhookIgnoreOnceLabelIsAbesent(&workload.ObjectMeta) + VerifyNoEvents(ctx, clientset, TestNamespaceName) }) It("should not instrument a stateful set that has the label, but remove the label", func() { - workload := BasicStatefulSet(TestNamespaceName, DeploymentName) - AddLabel(&workload.ObjectMeta, util.WebhookIgnoreOnceLabelKey, "true") + name := UniqueName(StatefulSetNamePrefix) + workload := BasicStatefulSet(TestNamespaceName, name) + createdObjects = append(createdObjects, workload) + AddLabel(&workload.ObjectMeta, "dash0.webhook.ignore.once", "true") CreateWorkload(ctx, k8sClient, workload) - workload = GetStatefulSet(ctx, k8sClient, TestNamespaceName, DeploymentName) + workload = GetStatefulSet(ctx, k8sClient, TestNamespaceName, name) VerifyUnmodifiedStatefulSet(workload) VerifyWebhookIgnoreOnceLabelIsAbesent(&workload.ObjectMeta) + VerifyNoEvents(ctx, clientset, TestNamespaceName) }) }) - }) diff --git a/internal/workloads/workload_modifier.go b/internal/workloads/workload_modifier.go index c9dfa2c7..dee7d260 100644 --- a/internal/workloads/workload_modifier.go +++ b/internal/workloads/workload_modifier.go @@ -7,7 +7,6 @@ import ( "fmt" "reflect" "slices" - "strconv" "strings" "github.com/go-logr/logr" @@ -76,7 +75,7 @@ func (m *ResourceModifier) ModifyJob(job *batchv1.Job) bool { } func (m *ResourceModifier) AddLabelsToImmutableJob(job *batchv1.Job) { - m.addInstrumentationLabels(&job.ObjectMeta, false) + util.AddInstrumentationLabels(&job.ObjectMeta, false, m.instrumentationMetadata) } func (m *ResourceModifier) ModifyReplicaSet(replicaSet *appsv1.ReplicaSet) bool { @@ -91,13 +90,16 @@ func (m *ResourceModifier) ModifyStatefulSet(statefulSet *appsv1.StatefulSet) bo } func (m *ResourceModifier) modifyResource(podTemplateSpec *corev1.PodTemplateSpec, meta *metav1.ObjectMeta) bool { + if util.HasOptedOutOfInstrumenation(meta) { + return false + } hasBeenModified := m.modifyPodSpec( &podTemplateSpec.Spec, fmt.Sprintf(envVarDash0CollectorBaseUrlValueTemplate, meta.Namespace), ) if hasBeenModified { - m.addInstrumentationLabels(meta, true) - m.addInstrumentationLabels(&podTemplateSpec.ObjectMeta, true) + util.AddInstrumentationLabels(meta, true, m.instrumentationMetadata) + util.AddInstrumentationLabels(&podTemplateSpec.ObjectMeta, true, m.instrumentationMetadata) } return hasBeenModified } @@ -284,16 +286,6 @@ func (m *ResourceModifier) addOrReplaceEnvironmentVariable(container *corev1.Con } } -func (m *ResourceModifier) addInstrumentationLabels( - meta *metav1.ObjectMeta, - hasBeenInstrumented bool, -) { - util.AddLabel(meta, util.InstrumentedLabelKey, strconv.FormatBool(hasBeenInstrumented)) - util.AddLabel(meta, util.OperatorVersionLabelKey, m.instrumentationMetadata.OperatorVersion) - util.AddLabel(meta, util.InitContainerImageVersionLabelKey, m.instrumentationMetadata.InitContainerImageVersion) - util.AddLabel(meta, util.InstrumentedByLabelKey, m.instrumentationMetadata.InstrumentedBy) -} - func (m *ResourceModifier) RevertCronJob(cronJob *batchv1.CronJob) bool { return m.revertResource(&cronJob.Spec.JobTemplate.Spec.Template, &cronJob.ObjectMeta) } @@ -311,7 +303,7 @@ func (m *ResourceModifier) RevertJob(job *batchv1.Job) bool { } func (m *ResourceModifier) RemoveLabelsFromImmutableJob(job *batchv1.Job) { - m.removeInstrumentationLabels(&job.ObjectMeta) + util.RemoveInstrumentationLabels(&job.ObjectMeta) } func (m *ResourceModifier) RevertReplicaSet(replicaSet *appsv1.ReplicaSet) bool { @@ -326,16 +318,19 @@ func (m *ResourceModifier) RevertStatefulSet(statefulSet *appsv1.StatefulSet) bo } func (m *ResourceModifier) revertResource(podTemplateSpec *corev1.PodTemplateSpec, meta *metav1.ObjectMeta) bool { - if meta.GetLabels()[util.InstrumentedLabelKey] == "false" { + if util.HasOptedOutOfInstrumenation(meta) { + return false + } + if util.InstrumenationAttemptHasFailed(meta) { // resource has never been instrumented successfully, only remove labels - m.removeInstrumentationLabels(meta) - m.removeInstrumentationLabels(&podTemplateSpec.ObjectMeta) + util.RemoveInstrumentationLabels(meta) + util.RemoveInstrumentationLabels(&podTemplateSpec.ObjectMeta) return true } hasBeenModified := m.revertPodSpec(&podTemplateSpec.Spec) if hasBeenModified { - m.removeInstrumentationLabels(meta) - m.removeInstrumentationLabels(&podTemplateSpec.ObjectMeta) + util.RemoveInstrumentationLabels(meta) + util.RemoveInstrumentationLabels(&podTemplateSpec.ObjectMeta) return true } return false @@ -424,17 +419,6 @@ func (m *ResourceModifier) removeEnvironmentVariable(container *corev1.Container }) } -func (m *ResourceModifier) removeInstrumentationLabels(meta *metav1.ObjectMeta) { - m.removeLabel(meta, util.InstrumentedLabelKey) - m.removeLabel(meta, util.OperatorVersionLabelKey) - m.removeLabel(meta, util.InitContainerImageVersionLabelKey) - m.removeLabel(meta, util.InstrumentedByLabelKey) -} - -func (m *ResourceModifier) removeLabel(meta *metav1.ObjectMeta, key string) { - delete(meta.Labels, key) -} - func (m *ResourceModifier) hasDeploymentOwnerReference(replicaSet *appsv1.ReplicaSet) bool { ownerReferences := replicaSet.ObjectMeta.OwnerReferences if len(ownerReferences) > 0 { diff --git a/internal/workloads/workload_modifier_test.go b/internal/workloads/workload_modifier_test.go index 6f4aae9b..36d5e40c 100644 --- a/internal/workloads/workload_modifier_test.go +++ b/internal/workloads/workload_modifier_test.go @@ -37,15 +37,15 @@ var _ = Describe("Dash0 Workload Modification", func() { Context("when instrumenting workloads", func() { It("should add Dash0 to a basic deployment", func() { - deployment := BasicDeployment(TestNamespaceName, DeploymentName) + deployment := BasicDeployment(TestNamespaceName, DeploymentNamePrefix) result := workloadModifier.ModifyDeployment(deployment) Expect(result).To(BeTrue()) VerifyModifiedDeployment(deployment, BasicInstrumentedPodSpecExpectations) }) - It("should add Dash0 to a deployment that has multiple containers, and already has volumes and init containers", func() { - deployment := DeploymentWithMoreBellsAndWhistles(TestNamespaceName, DeploymentName) + It("should instrument a deployment that has multiple containers, and already has volumes and init containers", func() { + deployment := DeploymentWithMoreBellsAndWhistles(TestNamespaceName, DeploymentNamePrefix) result := workloadModifier.ModifyDeployment(deployment) Expect(result).To(BeTrue()) @@ -76,7 +76,7 @@ var _ = Describe("Dash0 Workload Modification", func() { }) It("should update existing Dash0 artifacts in a deployment", func() { - deployment := DeploymentWithExistingDash0Artifacts(TestNamespaceName, DeploymentName) + deployment := DeploymentWithExistingDash0Artifacts(TestNamespaceName, DeploymentNamePrefix) result := workloadModifier.ModifyDeployment(deployment) Expect(result).To(BeTrue()) @@ -108,78 +108,126 @@ var _ = Describe("Dash0 Workload Modification", func() { }) }) - It("should add Dash0 to a basic cron job", func() { - workload := BasicCronJob(TestNamespaceName, CronJobName) + It("should not touch a deployment that has opted out of instrumentation", func() { + workload := DeploymentWithOptOutLabel(TestNamespaceName, DeploymentNamePrefix) + result := workloadModifier.ModifyDeployment(workload) + + Expect(result).To(BeFalse()) + VerifyDeploymentWithOptOutLabel(workload) + }) + + It("should instrument a basic cron job", func() { + workload := BasicCronJob(TestNamespaceName, CronJobNamePrefix) result := workloadModifier.ModifyCronJob(workload) Expect(result).To(BeTrue()) VerifyModifiedCronJob(workload, BasicInstrumentedPodSpecExpectations) }) - It("should add Dash0 to a basic daemon set", func() { - workload := BasicDaemonSet(TestNamespaceName, DaemonSetName) + It("should not touch a cron job that has opted out of instrumentation", func() { + workload := CronJobWithOptOutLabel(TestNamespaceName, DeploymentNamePrefix) + result := workloadModifier.ModifyCronJob(workload) + + Expect(result).To(BeFalse()) + VerifyCronJobWithOptOutLabel(workload) + }) + + It("should instrument a basic daemon set", func() { + workload := BasicDaemonSet(TestNamespaceName, DaemonSetNamePrefix) result := workloadModifier.ModifyDaemonSet(workload) Expect(result).To(BeTrue()) VerifyModifiedDaemonSet(workload, BasicInstrumentedPodSpecExpectations) }) - It("should add Dash0 to a basic job", func() { - workload := BasicJob(TestNamespaceName, JobName1) + It("should not touch a daemon set that has opted out of instrumentation", func() { + workload := DaemonSetWithOptOutLabel(TestNamespaceName, DeploymentNamePrefix) + result := workloadModifier.ModifyDaemonSet(workload) + + Expect(result).To(BeFalse()) + VerifyDaemonSetWithOptOutLabel(workload) + }) + + It("should instrument a basic job", func() { + workload := BasicJob(TestNamespaceName, JobNamePrefix) result := workloadModifier.ModifyJob(workload) Expect(result).To(BeTrue()) VerifyModifiedJob(workload, BasicInstrumentedPodSpecExpectations) }) - It("should add Dash0 to a basic replica set", func() { - workload := BasicReplicaSet(TestNamespaceName, ReplicaSetName) + It("should not touch a job that has opted out of instrumentation", func() { + workload := JobWithOptOutLabel(TestNamespaceName, DeploymentNamePrefix) + result := workloadModifier.ModifyJob(workload) + + Expect(result).To(BeFalse()) + VerifyJobWithOptOutLabel(workload) + }) + + It("should instrument a basic replica set", func() { + workload := BasicReplicaSet(TestNamespaceName, ReplicaSetNamePrefix) result := workloadModifier.ModifyReplicaSet(workload) Expect(result).To(BeTrue()) VerifyModifiedReplicaSet(workload, BasicInstrumentedPodSpecExpectations) }) - It("should not add Dash0 to a basic replica set that is owned by a deployment", func() { - workload := ReplicaSetOwnedByDeployment(TestNamespaceName, ReplicaSetName) + It("should not instrument a basic replica set that is owned by a deployment", func() { + workload := ReplicaSetOwnedByDeployment(TestNamespaceName, ReplicaSetNamePrefix) result := workloadModifier.ModifyReplicaSet(workload) Expect(result).To(BeFalse()) VerifyUnmodifiedReplicaSet(workload) }) - It("should add Dash0 to a basic stateful set", func() { - workload := BasicStatefulSet(TestNamespaceName, StatefulSetName) + It("should not touch a replica set that has opted out of instrumentation", func() { + workload := ReplicaSetWithOptOutLabel(TestNamespaceName, DeploymentNamePrefix) + result := workloadModifier.ModifyReplicaSet(workload) + + Expect(result).To(BeFalse()) + VerifyReplicaSetWithOptOutLabel(workload) + }) + + It("should instrument a basic stateful set", func() { + workload := BasicStatefulSet(TestNamespaceName, StatefulSetNamePrefix) result := workloadModifier.ModifyStatefulSet(workload) Expect(result).To(BeTrue()) VerifyModifiedStatefulSet(workload, BasicInstrumentedPodSpecExpectations) }) + + It("should not touch a stateful set that has opted out of instrumentation", func() { + workload := StatefulSetWithOptOutLabel(TestNamespaceName, DeploymentNamePrefix) + result := workloadModifier.ModifyStatefulSet(workload) + + Expect(result).To(BeFalse()) + VerifyStatefulSetWithOptOutLabel(workload) + }) }) Context("when reverting workloads", func() { It("should remove Dash0 from an instrumented deployment", func() { - deployment := InstrumentedDeployment(TestNamespaceName, DeploymentName) - result := workloadModifier.RevertDeployment(deployment) + workload := InstrumentedDeployment(TestNamespaceName, DeploymentNamePrefix) + result := workloadModifier.RevertDeployment(workload) Expect(result).To(BeTrue()) - VerifyUnmodifiedDeployment(deployment) + VerifyUnmodifiedDeployment(workload) }) - It("should only remove labels from deployment that has dash0.instrumented=false", func() { - deployment := DeploymentWithInstrumentedFalseLabel(TestNamespaceName, DeploymentName) - result := workloadModifier.RevertDeployment(deployment) + It("should not touch deployments that have opted out of instrumentation", func() { + workload := DeploymentWithOptOutLabel(TestNamespaceName, DeploymentNamePrefix) + result := workloadModifier.RevertDeployment(workload) - Expect(result).To(BeTrue()) - VerifyUnmodifiedDeployment(deployment) + Expect(result).To(BeFalse()) + VerifyDeploymentWithOptOutLabel(workload) }) It("should remove Dash0 from a instrumented deployment that has multiple containers, and already has volumes and init containers previous to being instrumented", func() { - deployment := InstrumentedDeploymentWithMoreBellsAndWhistles(TestNamespaceName, DeploymentName) - result := workloadModifier.RevertDeployment(deployment) + workload := InstrumentedDeploymentWithMoreBellsAndWhistles(TestNamespaceName, DeploymentNamePrefix) + result := workloadModifier.RevertDeployment(workload) Expect(result).To(BeTrue()) - VerifyRevertedDeployment(deployment, PodSpecExpectations{ + VerifyRevertedDeployment(workload, PodSpecExpectations{ Volumes: 2, Dash0VolumeIdx: -1, InitContainers: 2, @@ -204,39 +252,71 @@ var _ = Describe("Dash0 Workload Modification", func() { }) It("should remove Dash0 from an instrumented cron job", func() { - workload := InstrumentedCronJob(TestNamespaceName, CronJobName) + workload := InstrumentedCronJob(TestNamespaceName, CronJobNamePrefix) result := workloadModifier.RevertCronJob(workload) Expect(result).To(BeTrue()) VerifyUnmodifiedCronJob(workload) }) + It("should not touch cron jobs that have opted out of instrumentation", func() { + workload := CronJobWithOptOutLabel(TestNamespaceName, DeploymentNamePrefix) + result := workloadModifier.RevertCronJob(workload) + + Expect(result).To(BeFalse()) + VerifyCronJobWithOptOutLabel(workload) + }) + It("should remove Dash0 from an instrumented daemon set", func() { - workload := InstrumentedDaemonSet(TestNamespaceName, DaemonSetName) + workload := InstrumentedDaemonSet(TestNamespaceName, DaemonSetNamePrefix) result := workloadModifier.RevertDaemonSet(workload) Expect(result).To(BeTrue()) VerifyUnmodifiedDaemonSet(workload) }) + It("should not touch daemon sets that have opted out of instrumentation", func() { + workload := DaemonSetWithOptOutLabel(TestNamespaceName, DeploymentNamePrefix) + result := workloadModifier.RevertDaemonSet(workload) + + Expect(result).To(BeFalse()) + VerifyDaemonSetWithOptOutLabel(workload) + }) + It("should remove Dash0 from an instrumented job", func() { - workload := InstrumentedJob(TestNamespaceName, JobName1) + workload := InstrumentedJob(TestNamespaceName, JobNamePrefix) result := workloadModifier.RevertJob(workload) Expect(result).To(BeTrue()) VerifyUnmodifiedJob(workload) }) + It("should not touch jobs that have opted out of instrumentation", func() { + workload := JobWithOptOutLabel(TestNamespaceName, DeploymentNamePrefix) + result := workloadModifier.RevertJob(workload) + + Expect(result).To(BeFalse()) + VerifyJobWithOptOutLabel(workload) + }) + It("should remove Dash0 from an instrumented replica set", func() { - workload := InstrumentedReplicaSet(TestNamespaceName, ReplicaSetName) + workload := InstrumentedReplicaSet(TestNamespaceName, ReplicaSetNamePrefix) result := workloadModifier.RevertReplicaSet(workload) Expect(result).To(BeTrue()) VerifyUnmodifiedReplicaSet(workload) }) + It("should not touch replica sets that have opted out of instrumentation", func() { + workload := ReplicaSetWithOptOutLabel(TestNamespaceName, DeploymentNamePrefix) + result := workloadModifier.RevertReplicaSet(workload) + + Expect(result).To(BeFalse()) + VerifyReplicaSetWithOptOutLabel(workload) + }) + It("should not remove Dash0 from a replica set that is owned by a deployment", func() { - workload := InstrumentedReplicaSetOwnedByDeployment(TestNamespaceName, ReplicaSetName) + workload := InstrumentedReplicaSetOwnedByDeployment(TestNamespaceName, ReplicaSetNamePrefix) result := workloadModifier.RevertReplicaSet(workload) Expect(result).To(BeFalse()) @@ -244,11 +324,19 @@ var _ = Describe("Dash0 Workload Modification", func() { }) It("should remove Dash0 from an instrumented stateful set", func() { - workload := InstrumentedStatefulSet(TestNamespaceName, StatefulSetName) + workload := InstrumentedStatefulSet(TestNamespaceName, StatefulSetNamePrefix) result := workloadModifier.RevertStatefulSet(workload) Expect(result).To(BeTrue()) VerifyUnmodifiedStatefulSet(workload) }) + + It("should not touch a stateful sets that have opted out of instrumentation", func() { + workload := StatefulSetWithOptOutLabel(TestNamespaceName, DeploymentNamePrefix) + result := workloadModifier.RevertStatefulSet(workload) + + Expect(result).To(BeFalse()) + VerifyStatefulSetWithOptOutLabel(workload) + }) }) }) diff --git a/test-resources/bin/ensure-namespace-exists.sh b/test-resources/bin/ensure-namespace-exists.sh index c8c10df8..eba58e6f 100755 --- a/test-resources/bin/ensure-namespace-exists.sh +++ b/test-resources/bin/ensure-namespace-exists.sh @@ -13,4 +13,4 @@ fi if ! kubectl get ns ${target_namespace} &> /dev/null; then kubectl create ns ${target_namespace} -fi \ No newline at end of file +fi diff --git a/test-resources/bin/test-cleanup.sh b/test-resources/bin/test-cleanup.sh index 8652c3d3..65170b71 100755 --- a/test-resources/bin/test-cleanup.sh +++ b/test-resources/bin/test-cleanup.sh @@ -8,6 +8,7 @@ set -euo pipefail cd "$(dirname ${BASH_SOURCE})"/../.. target_namespace=${1:-default} +delete_namespace=${2:-true} kubectl delete -n ${target_namespace} -k config/samples || true make uninstall || true @@ -20,6 +21,6 @@ done test-resources/collector/undeploy.sh ${target_namespace} -if [[ "${target_namespace}" != "default" ]]; then +if [[ "${target_namespace}" != "default" ]] && [[ "${delete_namespace}" == "true" ]]; then kubectl delete ns ${target_namespace} fi diff --git a/test-resources/bin/test-roundtrip-01-aum-cr-operator.sh b/test-resources/bin/test-roundtrip-01-aum-cr-operator.sh index 023dffd7..19bfb24a 100755 --- a/test-resources/bin/test-roundtrip-01-aum-cr-operator.sh +++ b/test-resources/bin/test-roundtrip-01-aum-cr-operator.sh @@ -21,7 +21,7 @@ echo "STEP 1: creating target namespace (if necessary)" test-resources/bin/ensure-namespace-exists.sh ${target_namespace} echo "STEP 2: remove old test resources" -test-resources/bin/test-cleanup.sh ${target_namespace} +test-resources/bin/test-cleanup.sh ${target_namespace} false echo echo diff --git a/test-resources/bin/test-roundtrip-02-aum-operator-cr.sh b/test-resources/bin/test-roundtrip-02-aum-operator-cr.sh index a12bd482..757bb08c 100755 --- a/test-resources/bin/test-roundtrip-02-aum-operator-cr.sh +++ b/test-resources/bin/test-roundtrip-02-aum-operator-cr.sh @@ -21,7 +21,7 @@ echo "STEP 1: creating target namespace (if necessary)" test-resources/bin/ensure-namespace-exists.sh ${target_namespace} echo "STEP 2: remove old test resources" -test-resources/bin/test-cleanup.sh ${target_namespace} +test-resources/bin/test-cleanup.sh ${target_namespace} false echo echo diff --git a/test-resources/bin/test-roundtrip-03-operator-cr-aum.sh b/test-resources/bin/test-roundtrip-03-operator-cr-aum.sh index 2ff48d48..50e956e2 100755 --- a/test-resources/bin/test-roundtrip-03-operator-cr-aum.sh +++ b/test-resources/bin/test-roundtrip-03-operator-cr-aum.sh @@ -21,7 +21,7 @@ echo "STEP 1: creating target namespace (if necessary)" test-resources/bin/ensure-namespace-exists.sh ${target_namespace} echo "STEP 2: remove old test resources" -test-resources/bin/test-cleanup.sh ${target_namespace} +test-resources/bin/test-cleanup.sh ${target_namespace} false echo echo diff --git a/test/e2e/e2e_helpers.go b/test/e2e/e2e_helpers.go index 248ef70d..2e2dd292 100644 --- a/test/e2e/e2e_helpers.go +++ b/test/e2e/e2e_helpers.go @@ -643,9 +643,15 @@ func VerifyThatFailedInstrumentationAttemptLabelsHaveBeenRemovedRemoved(namespac }, verifyTelemetryTimeout, verifyTelemetryPollingInterval).Should(Succeed()) } -func verifyLabels(g Gomega, namespace string, kind string, hasBeenInstrumented bool, instrumentationBy string) { +func verifyLabels(g Gomega, namespace string, kind string, successful bool, instrumentationBy string) { + var expectedDash0InstrumentationValue string + if successful { + expectedDash0InstrumentationValue = "successful" + } else { + expectedDash0InstrumentationValue = "unsuccessful" + } instrumented := readLabel(g, namespace, kind, "dash0.instrumented") - g.ExpectWithOffset(1, instrumented).To(Equal(strconv.FormatBool(hasBeenInstrumented))) + g.ExpectWithOffset(1, instrumented).To(Equal(expectedDash0InstrumentationValue)) operatorVersion := readLabel(g, namespace, kind, "dash0.operator.version") g.ExpectWithOffset(1, operatorVersion).To(MatchRegexp("\\d+\\.\\d+\\.\\d+")) initContainerImageVersion := readLabel(g, namespace, kind, "dash0.initcontainer.image.version") diff --git a/test/util/resources.go b/test/util/resources.go index f7e7be8f..d7d71206 100644 --- a/test/util/resources.go +++ b/test/util/resources.go @@ -6,8 +6,8 @@ package util import ( "context" "fmt" - "strconv" + "github.com/google/uuid" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" @@ -15,23 +15,21 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - - "github.com/dash0hq/dash0-operator/internal/util" ) const ( - TestNamespaceName = "test-namespace" - CronJobName = "cronjob" - DaemonSetName = "daemonset" - DeploymentName = "deployment" - JobName1 = "job1" - JobName2 = "job2" - JobName3 = "job3" - ReplicaSetName = "replicaset" - StatefulSetName = "statefulset" + TestNamespaceName = "test-namespace" + CronJobNamePrefix = "cronjob" + DaemonSetNamePrefix = "daemonset" + DeploymentNamePrefix = "deployment" + JobNamePrefix = "job" + ReplicaSetNamePrefix = "replicaset" + StatefulSetNamePrefix = "statefulset" ) var ( @@ -109,6 +107,10 @@ func EnsureTestNamespaceExists( return object.(*corev1.Namespace) } +func UniqueName(prefix string) string { + return fmt.Sprintf("%s-%s", prefix, uuid.New()) +} + func BasicCronJob(namespace string, name string) *batchv1.CronJob { workload := &batchv1.CronJob{} workload.Namespace = namespace @@ -144,6 +146,21 @@ func CreateInstrumentedCronJob( return CreateWorkload(ctx, k8sClient, InstrumentedCronJob(namespace, name)).(*batchv1.CronJob) } +func CronJobWithOptOutLabel(namespace string, name string) *batchv1.CronJob { + workload := BasicCronJob(namespace, name) + addOptOutLabel(&workload.ObjectMeta) + return workload +} + +func CreateCronJobWithOptOutLabel( + ctx context.Context, + k8sClient client.Client, + namespace string, + name string, +) *batchv1.CronJob { + return CreateWorkload(ctx, k8sClient, CronJobWithOptOutLabel(namespace, name)).(*batchv1.CronJob) +} + func BasicDaemonSet(namespace string, name string) *appsv1.DaemonSet { workload := &appsv1.DaemonSet{} workload.Namespace = namespace @@ -178,6 +195,21 @@ func CreateInstrumentedDaemonSet( return CreateWorkload(ctx, k8sClient, InstrumentedDaemonSet(namespace, name)).(*appsv1.DaemonSet) } +func DaemonSetWithOptOutLabel(namespace string, name string) *appsv1.DaemonSet { + workload := BasicDaemonSet(namespace, name) + addOptOutLabel(&workload.ObjectMeta) + return workload +} + +func CreateDaemonSetWithOptOutLabel( + ctx context.Context, + k8sClient client.Client, + namespace string, + name string, +) *appsv1.DaemonSet { + return CreateWorkload(ctx, k8sClient, DaemonSetWithOptOutLabel(namespace, name)).(*appsv1.DaemonSet) +} + func BasicDeployment(namespace string, name string) *appsv1.Deployment { workload := &appsv1.Deployment{} workload.Namespace = namespace @@ -212,12 +244,21 @@ func CreateInstrumentedDeployment( return CreateWorkload(ctx, k8sClient, InstrumentedDeployment(namespace, name)).(*appsv1.Deployment) } -func DeploymentWithInstrumentedFalseLabel(namespace string, name string) *appsv1.Deployment { +func DeploymentWithOptOutLabel(namespace string, name string) *appsv1.Deployment { workload := BasicDeployment(namespace, name) - addInstrumentationLabels(&workload.ObjectMeta, false) + addOptOutLabel(&workload.ObjectMeta) return workload } +func CreateDeploymentWithOptOutLabel( + ctx context.Context, + k8sClient client.Client, + namespace string, + name string, +) *appsv1.Deployment { + return CreateWorkload(ctx, k8sClient, DeploymentWithOptOutLabel(namespace, name)).(*appsv1.Deployment) +} + func BasicJob(namespace string, name string) *batchv1.Job { workload := &batchv1.Job{} workload.Namespace = namespace @@ -252,19 +293,34 @@ func CreateInstrumentedJob( return CreateWorkload(ctx, k8sClient, InstrumentedJob(namespace, name)).(*batchv1.Job) } -func JobWithInstrumentationLabels(namespace string, name string) *batchv1.Job { +func JobForWhichAnInstrumentationAttemptHasFailed(namespace string, name string) *batchv1.Job { workload := BasicJob(namespace, name) addInstrumentationLabels(&workload.ObjectMeta, false) return workload } -func CreateJobWithInstrumentationLabels( +func CreateJobForWhichAnInstrumentationAttemptHasFailed( ctx context.Context, k8sClient client.Client, namespace string, name string, ) *batchv1.Job { - return CreateWorkload(ctx, k8sClient, JobWithInstrumentationLabels(namespace, name)).(*batchv1.Job) + return CreateWorkload(ctx, k8sClient, JobForWhichAnInstrumentationAttemptHasFailed(namespace, name)).(*batchv1.Job) +} + +func JobWithOptOutLabel(namespace string, name string) *batchv1.Job { + workload := BasicJob(namespace, name) + addOptOutLabel(&workload.ObjectMeta) + return workload +} + +func CreateJobWithOptOutLabel( + ctx context.Context, + k8sClient client.Client, + namespace string, + name string, +) *batchv1.Job { + return CreateWorkload(ctx, k8sClient, JobWithOptOutLabel(namespace, name)).(*batchv1.Job) } func BasicReplicaSet(namespace string, name string) *appsv1.ReplicaSet { @@ -331,6 +387,21 @@ func InstrumentedReplicaSetOwnedByDeployment(namespace string, name string) *app return workload } +func ReplicaSetWithOptOutLabel(namespace string, name string) *appsv1.ReplicaSet { + workload := BasicReplicaSet(namespace, name) + addOptOutLabel(&workload.ObjectMeta) + return workload +} + +func CreateReplicaSetWithOptOutLabel( + ctx context.Context, + k8sClient client.Client, + namespace string, + name string, +) *appsv1.ReplicaSet { + return CreateWorkload(ctx, k8sClient, ReplicaSetWithOptOutLabel(namespace, name)).(*appsv1.ReplicaSet) +} + func BasicStatefulSet(namespace string, name string) *appsv1.StatefulSet { workload := &appsv1.StatefulSet{} workload.Namespace = namespace @@ -365,6 +436,21 @@ func CreateInstrumentedStatefulSet( return CreateWorkload(ctx, k8sClient, InstrumentedStatefulSet(namespace, name)).(*appsv1.StatefulSet) } +func StatefulSetWithOptOutLabel(namespace string, name string) *appsv1.StatefulSet { + workload := BasicStatefulSet(namespace, name) + addOptOutLabel(&workload.ObjectMeta) + return workload +} + +func CreateStatefulSetWithOptOutLabel( + ctx context.Context, + k8sClient client.Client, + namespace string, + name string, +) *appsv1.StatefulSet { + return CreateWorkload(ctx, k8sClient, StatefulSetWithOptOutLabel(namespace, name)).(*appsv1.StatefulSet) +} + func basicPodSpecTemplate() corev1.PodTemplateSpec { podSpecTemplate := corev1.PodTemplateSpec{} podSpecTemplate.Labels = map[string]string{"app": "test"} @@ -821,11 +907,21 @@ func GetStatefulSet( return workload } -func addInstrumentationLabels(meta *metav1.ObjectMeta, instrumented bool) { - AddLabel(meta, util.InstrumentedLabelKey, strconv.FormatBool(instrumented)) - AddLabel(meta, util.OperatorVersionLabelKey, "1.2.3") - AddLabel(meta, util.InitContainerImageVersionLabelKey, "4.5.6") - AddLabel(meta, util.InstrumentedByLabelKey, "someone") +func addInstrumentationLabels(meta *metav1.ObjectMeta, successful bool) { + var instrumentationState string + if successful { + instrumentationState = "successful" + } else { + instrumentationState = "unsuccessful" + } + AddLabel(meta, "dash0.instrumented", instrumentationState) + AddLabel(meta, "dash0.operator.version", "1.2.3") + AddLabel(meta, "dash0.initcontainer.image.version", "4.5.6") + AddLabel(meta, "dash0.instrumented.by", "someone") +} + +func addOptOutLabel(meta *metav1.ObjectMeta) { + AddLabel(meta, "dash0.instrumented", "false") } func AddLabel(meta *metav1.ObjectMeta, key string, value string) { @@ -834,3 +930,32 @@ func AddLabel(meta *metav1.ObjectMeta, key string, value string) { } meta.Labels[key] = value } + +func DeleteAllCreatedObjects( + ctx context.Context, + k8sClient client.Client, + createdObjects []client.Object, +) []client.Object { + By("Remove all created objects") + for _, object := range createdObjects { + Expect(k8sClient.Delete(ctx, object, &client.DeleteOptions{ + GracePeriodSeconds: new(int64), + })).To(Succeed()) + } + return make([]client.Object, 0) +} + +func DeleteAllEvents( + ctx context.Context, + clientset *kubernetes.Clientset, + namespace string, +) { + err := clientset.CoreV1().Events(namespace).DeleteCollection(ctx, metav1.DeleteOptions{ + GracePeriodSeconds: new(int64), // delete immediately + }, metav1.ListOptions{}) + Expect(err).NotTo(HaveOccurred()) + + allEvents, err := clientset.CoreV1().Events(namespace).List(ctx, metav1.ListOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(allEvents.Items).To(BeEmpty()) +} diff --git a/test/util/verification.go b/test/util/verification.go index ac22681f..0f31a20f 100644 --- a/test/util/verification.go +++ b/test/util/verification.go @@ -57,86 +57,122 @@ var ( func VerifyModifiedCronJob(resource *batchv1.CronJob, expectations PodSpecExpectations) { verifyPodSpec(resource.Spec.JobTemplate.Spec.Template.Spec, expectations) - verifyLabelsAfterSuccessfulModification(resource.Spec.JobTemplate.Spec.Template.ObjectMeta) verifyLabelsAfterSuccessfulModification(resource.ObjectMeta) + verifyLabelsAfterSuccessfulModification(resource.Spec.JobTemplate.Spec.Template.ObjectMeta) } func VerifyUnmodifiedCronJob(resource *batchv1.CronJob) { verifyUnmodifiedPodSpec(resource.Spec.JobTemplate.Spec.Template.Spec) - verifyNoDash0Labels(resource.Spec.JobTemplate.Spec.Template.ObjectMeta) verifyNoDash0Labels(resource.ObjectMeta) + verifyNoDash0Labels(resource.Spec.JobTemplate.Spec.Template.ObjectMeta) +} + +func VerifyCronJobWithOptOutLabel(resource *batchv1.CronJob) { + verifyUnmodifiedPodSpec(resource.Spec.JobTemplate.Spec.Template.Spec) + verifyLabelsForOptOutWorkload(resource.ObjectMeta) + verifyNoDash0Labels(resource.Spec.JobTemplate.Spec.Template.ObjectMeta) } func VerifyModifiedDaemonSet(resource *appsv1.DaemonSet, expectations PodSpecExpectations) { verifyPodSpec(resource.Spec.Template.Spec, expectations) - verifyLabelsAfterSuccessfulModification(resource.Spec.Template.ObjectMeta) verifyLabelsAfterSuccessfulModification(resource.ObjectMeta) + verifyLabelsAfterSuccessfulModification(resource.Spec.Template.ObjectMeta) } func VerifyUnmodifiedDaemonSet(resource *appsv1.DaemonSet) { verifyUnmodifiedPodSpec(resource.Spec.Template.Spec) - verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) verifyNoDash0Labels(resource.ObjectMeta) + verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) +} + +func VerifyDaemonSetWithOptOutLabel(resource *appsv1.DaemonSet) { + verifyUnmodifiedPodSpec(resource.Spec.Template.Spec) + verifyLabelsForOptOutWorkload(resource.ObjectMeta) + verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) } func VerifyModifiedDeployment(resource *appsv1.Deployment, expectations PodSpecExpectations) { verifyPodSpec(resource.Spec.Template.Spec, expectations) - verifyLabelsAfterSuccessfulModification(resource.Spec.Template.ObjectMeta) verifyLabelsAfterSuccessfulModification(resource.ObjectMeta) + verifyLabelsAfterSuccessfulModification(resource.Spec.Template.ObjectMeta) } func VerifyUnmodifiedDeployment(resource *appsv1.Deployment) { verifyUnmodifiedPodSpec(resource.Spec.Template.Spec) - verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) verifyNoDash0Labels(resource.ObjectMeta) + verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) } func VerifyRevertedDeployment(resource *appsv1.Deployment, expectations PodSpecExpectations) { verifyPodSpec(resource.Spec.Template.Spec, expectations) - verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) verifyNoDash0Labels(resource.ObjectMeta) + verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) +} + +func VerifyDeploymentWithOptOutLabel(resource *appsv1.Deployment) { + verifyUnmodifiedPodSpec(resource.Spec.Template.Spec) + verifyLabelsForOptOutWorkload(resource.ObjectMeta) + verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) } func VerifyModifiedJob(resource *batchv1.Job, expectations PodSpecExpectations) { verifyPodSpec(resource.Spec.Template.Spec, expectations) - verifyLabelsAfterSuccessfulModification(resource.Spec.Template.ObjectMeta) verifyLabelsAfterSuccessfulModification(resource.ObjectMeta) + verifyLabelsAfterSuccessfulModification(resource.Spec.Template.ObjectMeta) } func VerifyImmutableJobCouldNotBeModified(resource *batchv1.Job) { verifyUnmodifiedPodSpec(resource.Spec.Template.Spec) - verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) verifyLabelsAfterFailureToModify(resource.ObjectMeta) + verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) } func VerifyUnmodifiedJob(resource *batchv1.Job) { verifyUnmodifiedPodSpec(resource.Spec.Template.Spec) - verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) verifyNoDash0Labels(resource.ObjectMeta) + verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) +} + +func VerifyJobWithOptOutLabel(resource *batchv1.Job) { + verifyUnmodifiedPodSpec(resource.Spec.Template.Spec) + verifyLabelsForOptOutWorkload(resource.ObjectMeta) + verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) } func VerifyModifiedReplicaSet(resource *appsv1.ReplicaSet, expectations PodSpecExpectations) { verifyPodSpec(resource.Spec.Template.Spec, expectations) - verifyLabelsAfterSuccessfulModification(resource.Spec.Template.ObjectMeta) verifyLabelsAfterSuccessfulModification(resource.ObjectMeta) + verifyLabelsAfterSuccessfulModification(resource.Spec.Template.ObjectMeta) } func VerifyUnmodifiedReplicaSet(resource *appsv1.ReplicaSet) { verifyUnmodifiedPodSpec(resource.Spec.Template.Spec) - verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) verifyNoDash0Labels(resource.ObjectMeta) + verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) +} + +func VerifyReplicaSetWithOptOutLabel(resource *appsv1.ReplicaSet) { + verifyUnmodifiedPodSpec(resource.Spec.Template.Spec) + verifyLabelsForOptOutWorkload(resource.ObjectMeta) + verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) } func VerifyModifiedStatefulSet(resource *appsv1.StatefulSet, expectations PodSpecExpectations) { verifyPodSpec(resource.Spec.Template.Spec, expectations) - verifyLabelsAfterSuccessfulModification(resource.Spec.Template.ObjectMeta) verifyLabelsAfterSuccessfulModification(resource.ObjectMeta) + verifyLabelsAfterSuccessfulModification(resource.Spec.Template.ObjectMeta) } func VerifyUnmodifiedStatefulSet(resource *appsv1.StatefulSet) { verifyUnmodifiedPodSpec(resource.Spec.Template.Spec) - verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) verifyNoDash0Labels(resource.ObjectMeta) + verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) +} + +func VerifyStatefulSetWithOptOutLabel(resource *appsv1.StatefulSet) { + verifyUnmodifiedPodSpec(resource.Spec.Template.Spec) + verifyLabelsForOptOutWorkload(resource.ObjectMeta) + verifyNoDash0Labels(resource.Spec.Template.ObjectMeta) } func verifyPodSpec(podSpec corev1.PodSpec, expectations PodSpecExpectations) { @@ -216,14 +252,14 @@ func verifyUnmodifiedPodSpec(podSpec corev1.PodSpec) { } func verifyLabelsAfterSuccessfulModification(meta metav1.ObjectMeta) { - Expect(meta.Labels["dash0.instrumented"]).To(Equal("true")) + Expect(meta.Labels["dash0.instrumented"]).To(Equal("successful")) Expect(meta.Labels["dash0.operator.version"]).To(Equal("1.2.3")) Expect(meta.Labels["dash0.initcontainer.image.version"]).To(Equal("4.5.6")) Expect(meta.Labels["dash0.instrumented.by"]).NotTo(Equal("")) } func verifyLabelsAfterFailureToModify(meta metav1.ObjectMeta) { - Expect(meta.Labels["dash0.instrumented"]).To(Equal("false")) + Expect(meta.Labels["dash0.instrumented"]).To(Equal("unsuccessful")) Expect(meta.Labels["dash0.operator.version"]).To(Equal("1.2.3")) Expect(meta.Labels["dash0.initcontainer.image.version"]).To(Equal("4.5.6")) Expect(meta.Labels["dash0.instrumented.by"]).NotTo(Equal("")) @@ -236,6 +272,13 @@ func verifyNoDash0Labels(meta metav1.ObjectMeta) { Expect(meta.Labels["dash0.instrumented.by"]).To(Equal("")) } +func verifyLabelsForOptOutWorkload(meta metav1.ObjectMeta) { + Expect(meta.Labels["dash0.instrumented"]).To(Equal("false")) + Expect(meta.Labels["dash0.operator.version"]).To(Equal("")) + Expect(meta.Labels["dash0.initcontainer.image.version"]).To(Equal("")) + Expect(meta.Labels["dash0.instrumented.by"]).To(Equal("")) +} + func VerifyWebhookIgnoreOnceLabelIsPresent(objectMeta *metav1.ObjectMeta) bool { return Expect(objectMeta.Labels["dash0.webhook.ignore.once"]).To(Equal("true")) } @@ -271,6 +314,25 @@ func VerifySuccessfulInstrumentationEvent( ) } +func VerifyNoInstrumentationNecessaryEvent( + ctx context.Context, + clientset *kubernetes.Clientset, + namespace string, + resourceName string, + eventSource string, +) { + verifyEvent( + ctx, + clientset, + namespace, + resourceName, + util.ReasonNoInstrumentationNecessary, + fmt.Sprintf( + "Dash0 instrumentation was already present on this workload, or the workload is part of a higher order "+ + "workload that will be instrumented, no modification by the %s is necessary.", eventSource), + ) +} + func VerifyFailedInstrumentationEvent( ctx context.Context, clientset *kubernetes.Clientset, @@ -322,7 +384,7 @@ func VerifyFailedUninstrumentationEvent( ) } -func VerifyAlreadyNotInstrumented( +func VerifyNoUninstrumentationNecessaryEvent( ctx context.Context, clientset *kubernetes.Clientset, namespace string, @@ -334,7 +396,7 @@ func VerifyAlreadyNotInstrumented( clientset, namespace, resourceName, - util.ReasonAlreadyNotInstrumented, + util.ReasonNoUninstrumentationNecessary, message, ) }