From 870625a17f1d8b3bd2bdb684edf943c3dd9240ef Mon Sep 17 00:00:00 2001 From: Michele Mancioppi Date: Mon, 18 May 2026 18:33:38 +0200 Subject: [PATCH] feat: add support for spam filters v1alpha2 --- .../v1alpha1/dash0spamfilters_types.go | 91 ++++- .../v1alpha1/dash0spamfilters_types_test.go | 75 ++++ .../v1alpha2/dash0spamfilters_types.go | 87 +++++ .../v1alpha2/dash0spamfilters_types_test.go | 105 ++++++ api/operator/v1alpha2/groupversion_info.go | 32 ++ api/operator/v1alpha2/v1alpha2_suite_test.go | 16 + .../v1alpha2/zz_generated.deepcopy.go | 154 ++++++++ .../operator.dash0.com_dash0spamfilters.yaml | 150 +++++++- ...stom-resource-definition-spam-filters.yaml | 154 -------- .../operator/deployment-and-webhooks.yaml | 337 ++++++++++++++++++ internal/controller/controller_suite_test.go | 2 + internal/controller/spam_filter_controller.go | 30 +- .../controller/spam_filter_controller_test.go | 24 +- internal/startup/operator_manager_startup.go | 2 + internal/startup/startup_suite_test.go | 2 + ...ter.yaml => dash0spamfilter_v1alpha1.yaml} | 0 .../dash0spamfilter_v1alpha2.yaml | 10 + test/e2e/dash0_api_sync_resources.go | 49 ++- ...=> dash0spamfilter_v1alpha1.yaml.template} | 0 .../dash0spamfilter_v1alpha2.yaml.template | 15 + test/e2e/e2e_test.go | 44 ++- 21 files changed, 1186 insertions(+), 193 deletions(-) create mode 100644 api/operator/v1alpha1/dash0spamfilters_types_test.go create mode 100644 api/operator/v1alpha2/dash0spamfilters_types.go create mode 100644 api/operator/v1alpha2/dash0spamfilters_types_test.go create mode 100644 api/operator/v1alpha2/groupversion_info.go create mode 100644 api/operator/v1alpha2/v1alpha2_suite_test.go create mode 100644 api/operator/v1alpha2/zz_generated.deepcopy.go delete mode 100644 helm-chart/dash0-operator/templates/operator/custom-resource-definition-spam-filters.yaml rename test-resources/customresources/dash0spamfilter/{dash0spamfilter.yaml => dash0spamfilter_v1alpha1.yaml} (100%) create mode 100644 test-resources/customresources/dash0spamfilter/dash0spamfilter_v1alpha2.yaml rename test/e2e/{dash0spamfilter.yaml.template => dash0spamfilter_v1alpha1.yaml.template} (100%) create mode 100644 test/e2e/dash0spamfilter_v1alpha2.yaml.template diff --git a/api/operator/v1alpha1/dash0spamfilters_types.go b/api/operator/v1alpha1/dash0spamfilters_types.go index 6536ae907..34610401f 100644 --- a/api/operator/v1alpha1/dash0spamfilters_types.go +++ b/api/operator/v1alpha1/dash0spamfilters_types.go @@ -5,8 +5,10 @@ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/conversion" dash0common "github.com/dash0hq/dash0-operator/api/operator/common" + dash0v1alpha2 "github.com/dash0hq/dash0-operator/api/operator/v1alpha2" ) // Dash0SpamFilter is the Schema for the Dash0SpamFilter API @@ -14,6 +16,7 @@ import ( // +kubebuilder:object:root=true // +groupName=operator.dash0.com // +kubebuilder:subresource:status +// +kubebuilder:conversion:spoke type Dash0SpamFilter struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -24,9 +27,12 @@ type Dash0SpamFilter struct { // Dash0SpamFilterSpec defines the desired state of Dash0SpamFilter type Dash0SpamFilterSpec struct { - // The signal contexts this spam filter applies to (e.g., log, span, metric). + // The signal contexts this spam filter applies to. Only the first element is used; + // additional elements are ignored when converting to the v1alpha2 hub version. // +kubebuilder:validation:Required // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=1 + // +kubebuilder:validation:items:Enum=datapoint;log;span;web_event Contexts []string `json:"contexts"` // The filter conditions that define which telemetry data to drop. @@ -80,3 +86,86 @@ type Dash0SpamFilterList struct { metav1.ListMeta `json:"metadata,omitempty"` Items []Dash0SpamFilter `json:"items"` } + +// Ensure Dash0SpamFilter implements the conversion.Convertible interface. +var _ conversion.Convertible = &Dash0SpamFilter{} + +// ConvertTo converts this Dash0SpamFilter resource (v1alpha1) to the hub version (v1alpha2). +func (src *Dash0SpamFilter) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*dash0v1alpha2.Dash0SpamFilter) + dst.ObjectMeta = src.ObjectMeta + + // Spec: contexts (array) → context (scalar) + if len(src.Spec.Contexts) > 0 { + dst.Spec.Context = src.Spec.Contexts[0] + } + dst.Spec.Filter = make([]dash0v1alpha2.Dash0SpamFilterCondition, len(src.Spec.Filter)) + for i, f := range src.Spec.Filter { + dst.Spec.Filter[i] = dash0v1alpha2.Dash0SpamFilterCondition{ + Key: f.Key, + Operator: f.Operator, + Value: f.Value, + } + } + + // Status + dst.Status.SynchronizationStatus = src.Status.SynchronizationStatus + dst.Status.SynchronizedAt = src.Status.SynchronizedAt + dst.Status.ValidationIssues = src.Status.ValidationIssues + dst.Status.SynchronizationResults = make( + []dash0v1alpha2.Dash0SpamFilterSynchronizationResultPerEndpointAndDataset, + len(src.Status.SynchronizationResults), + ) + for i, r := range src.Status.SynchronizationResults { + dst.Status.SynchronizationResults[i] = dash0v1alpha2.Dash0SpamFilterSynchronizationResultPerEndpointAndDataset{ + SynchronizationStatus: r.SynchronizationStatus, + Dash0ApiEndpoint: r.Dash0ApiEndpoint, + Dash0Dataset: r.Dash0Dataset, + Dash0Id: r.Dash0Id, + Dash0Origin: r.Dash0Origin, + SynchronizationError: r.SynchronizationError, + } + } + + return nil +} + +// ConvertFrom converts the hub version (v1alpha2) to this Dash0SpamFilter resource version (v1alpha1). +func (dst *Dash0SpamFilter) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*dash0v1alpha2.Dash0SpamFilter) + dst.ObjectMeta = src.ObjectMeta + + // Spec: context (scalar) → contexts (array) + if src.Spec.Context != "" { + dst.Spec.Contexts = []string{src.Spec.Context} + } + dst.Spec.Filter = make([]Dash0SpamFilterCondition, len(src.Spec.Filter)) + for i, f := range src.Spec.Filter { + dst.Spec.Filter[i] = Dash0SpamFilterCondition{ + Key: f.Key, + Operator: f.Operator, + Value: f.Value, + } + } + + // Status + dst.Status.SynchronizationStatus = src.Status.SynchronizationStatus + dst.Status.SynchronizedAt = src.Status.SynchronizedAt + dst.Status.ValidationIssues = src.Status.ValidationIssues + dst.Status.SynchronizationResults = make( + []Dash0SpamFilterSynchronizationResultPerEndpointAndDataset, + len(src.Status.SynchronizationResults), + ) + for i, r := range src.Status.SynchronizationResults { + dst.Status.SynchronizationResults[i] = Dash0SpamFilterSynchronizationResultPerEndpointAndDataset{ + SynchronizationStatus: r.SynchronizationStatus, + Dash0ApiEndpoint: r.Dash0ApiEndpoint, + Dash0Dataset: r.Dash0Dataset, + Dash0Id: r.Dash0Id, + Dash0Origin: r.Dash0Origin, + SynchronizationError: r.SynchronizationError, + } + } + + return nil +} diff --git a/api/operator/v1alpha1/dash0spamfilters_types_test.go b/api/operator/v1alpha1/dash0spamfilters_types_test.go new file mode 100644 index 000000000..84284e555 --- /dev/null +++ b/api/operator/v1alpha1/dash0spamfilters_types_test.go @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: Copyright 2026 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + dash0v1alpha2 "github.com/dash0hq/dash0-operator/api/operator/v1alpha2" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("v1alpha1 Dash0 spam filter CRD", func() { + + Describe("converting to and from hub version", func() { + + It("should convert v1alpha1 to v1alpha2 (contexts → context)", func() { + src := &Dash0SpamFilter{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-spam-filter", + }, + Spec: Dash0SpamFilterSpec{ + Contexts: []string{"log"}, + Filter: []Dash0SpamFilterCondition{ + { + Key: "k8s.namespace.name", + Operator: "is", + Value: ptr.To("kube-system"), + }, + }, + }, + } + + dst := &dash0v1alpha2.Dash0SpamFilter{} + Expect(src.ConvertTo(dst)).To(Succeed()) + + Expect(dst.ObjectMeta).To(Equal(src.ObjectMeta)) + Expect(dst.Spec.Context).To(Equal("log")) + Expect(dst.Spec.Filter).To(HaveLen(1)) + Expect(dst.Spec.Filter[0].Key).To(Equal("k8s.namespace.name")) + Expect(dst.Spec.Filter[0].Operator).To(Equal("is")) + Expect(*dst.Spec.Filter[0].Value).To(Equal("kube-system")) + }) + + It("should preserve data through a round-trip starting from v1alpha1", func() { + original := &Dash0SpamFilter{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-spam-filter", + }, + Spec: Dash0SpamFilterSpec{ + Contexts: []string{"log"}, + Filter: []Dash0SpamFilterCondition{ + {Key: "k8s.namespace.name", Operator: "is", Value: ptr.To("kube-system")}, + {Key: "level", Operator: "is_set"}, + }, + }, + } + + hub := &dash0v1alpha2.Dash0SpamFilter{} + Expect(original.ConvertTo(hub)).To(Succeed()) + + roundTripped := &Dash0SpamFilter{} + Expect(roundTripped.ConvertFrom(hub)).To(Succeed()) + + Expect(roundTripped.ObjectMeta).To(Equal(original.ObjectMeta)) + Expect(roundTripped.Spec.Contexts).To(Equal(original.Spec.Contexts)) + Expect(roundTripped.Spec.Filter).To(Equal(original.Spec.Filter)) + }) + }) +}) diff --git a/api/operator/v1alpha2/dash0spamfilters_types.go b/api/operator/v1alpha2/dash0spamfilters_types.go new file mode 100644 index 000000000..a2fb2f8f5 --- /dev/null +++ b/api/operator/v1alpha2/dash0spamfilters_types.go @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright 2026 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + dash0common "github.com/dash0hq/dash0-operator/api/operator/common" +) + +// Dash0SpamFilter is the Schema for the Dash0SpamFilter API +// +// +kubebuilder:object:root=true +// +groupName=operator.dash0.com +// +kubebuilder:subresource:status +// +kubebuilder:storageversion +// +kubebuilder:conversion:hub +type Dash0SpamFilter struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec Dash0SpamFilterSpec `json:"spec,omitempty"` + Status Dash0SpamFilterStatus `json:"status,omitempty"` +} + +// Hub marks this version as the hub for conversions; all other versions are implicitly spokes. +func (*Dash0SpamFilter) Hub() {} + +// Dash0SpamFilterSpec defines the desired state of Dash0SpamFilter +type Dash0SpamFilterSpec struct { + // The signal context this spam filter applies to. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=datapoint;log;span;web_event + Context string `json:"context"` + + // The filter conditions that define which telemetry data to drop. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + Filter []Dash0SpamFilterCondition `json:"filter"` +} + +// Dash0SpamFilterCondition defines a single filter condition for spam filtering, +// matching the Dash0 API's AttributeFilter schema. +type Dash0SpamFilterCondition struct { + // The attribute key to match against. + // +kubebuilder:validation:Required + Key string `json:"key"` + + // The comparison operator (e.g., "is", "is_not", "contains", "starts_with"). + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=is;is_not;is_set;is_not_set;is_one_of;is_not_one_of;gt;lt;gte;lte;matches;does_not_match;contains;does_not_contain;starts_with;does_not_start_with;ends_with;does_not_end_with;is_any + Operator string `json:"operator"` + + // The value to compare against. Optional for operators like "is_set" and "is_not_set". + // +kubebuilder:validation:Optional + Value *string `json:"value,omitempty"` +} + +// Dash0SpamFilterStatus defines the observed state of Dash0SpamFilter +type Dash0SpamFilterStatus struct { + SynchronizationStatus dash0common.Dash0ApiResourceSynchronizationStatus `json:"synchronizationStatus"` + SynchronizedAt metav1.Time `json:"synchronizedAt"` + ValidationIssues []string `json:"validationIssues,omitempty"` + SynchronizationResults []Dash0SpamFilterSynchronizationResultPerEndpointAndDataset `json:"synchronizationResults"` +} + +// Dash0SpamFilterSynchronizationResultPerEndpointAndDataset defines the synchronization result per endpoint and dataset +type Dash0SpamFilterSynchronizationResultPerEndpointAndDataset struct { + SynchronizationStatus dash0common.Dash0ApiResourceSynchronizationStatus `json:"synchronizationStatus"` + Dash0ApiEndpoint string `json:"dash0ApiEndpoint,omitempty"` + Dash0Dataset string `json:"dash0Dataset,omitempty"` + // +kubebuilder:validation:Optional + Dash0Id string `json:"dash0Id,omitempty"` + // +kubebuilder:validation:Optional + Dash0Origin string `json:"dash0Origin,omitempty"` + SynchronizationError string `json:"synchronizationError,omitempty"` +} + +//+kubebuilder:object:root=true + +// Dash0SpamFilterList contains a list of Dash0SpamFilter resources. +type Dash0SpamFilterList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Dash0SpamFilter `json:"items"` +} diff --git a/api/operator/v1alpha2/dash0spamfilters_types_test.go b/api/operator/v1alpha2/dash0spamfilters_types_test.go new file mode 100644 index 000000000..29371ddc1 --- /dev/null +++ b/api/operator/v1alpha2/dash0spamfilters_types_test.go @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: Copyright 2026 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha2_test + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/conversion" + + dash0v1alpha1 "github.com/dash0hq/dash0-operator/api/operator/v1alpha1" + dash0v1alpha2 "github.com/dash0hq/dash0-operator/api/operator/v1alpha2" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("v1alpha2 Dash0 spam filter CRD", func() { + + It("should be marked as the conversion hub", func() { + var _ conversion.Hub = &dash0v1alpha2.Dash0SpamFilter{} + (&dash0v1alpha2.Dash0SpamFilter{}).Hub() + }) + + Describe("converting to and from the v1alpha1 spoke", func() { + + It("should convert v1alpha2 to v1alpha1 (context → contexts)", func() { + src := &dash0v1alpha2.Dash0SpamFilter{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-spam-filter", + }, + Spec: dash0v1alpha2.Dash0SpamFilterSpec{ + Context: "span", + Filter: []dash0v1alpha2.Dash0SpamFilterCondition{ + { + Key: "service.name", + Operator: "is_not", + Value: ptr.To("noisy-service"), + }, + }, + }, + } + + dst := &dash0v1alpha1.Dash0SpamFilter{} + Expect(dst.ConvertFrom(src)).To(Succeed()) + + Expect(dst.ObjectMeta).To(Equal(src.ObjectMeta)) + Expect(dst.Spec.Contexts).To(Equal([]string{"span"})) + Expect(dst.Spec.Filter).To(HaveLen(1)) + Expect(dst.Spec.Filter[0].Key).To(Equal("service.name")) + Expect(dst.Spec.Filter[0].Operator).To(Equal("is_not")) + Expect(*dst.Spec.Filter[0].Value).To(Equal("noisy-service")) + }) + + It("should preserve data through a round-trip starting from v1alpha2", func() { + original := &dash0v1alpha2.Dash0SpamFilter{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-spam-filter", + }, + Spec: dash0v1alpha2.Dash0SpamFilterSpec{ + Context: "log", + Filter: []dash0v1alpha2.Dash0SpamFilterCondition{ + {Key: "k8s.namespace.name", Operator: "is", Value: ptr.To("kube-system")}, + {Key: "level", Operator: "is_set"}, + }, + }, + } + + spoke := &dash0v1alpha1.Dash0SpamFilter{} + Expect(spoke.ConvertFrom(original)).To(Succeed()) + + roundTripped := &dash0v1alpha2.Dash0SpamFilter{} + Expect(spoke.ConvertTo(roundTripped)).To(Succeed()) + + Expect(roundTripped.ObjectMeta).To(Equal(original.ObjectMeta)) + Expect(roundTripped.Spec.Context).To(Equal(original.Spec.Context)) + Expect(roundTripped.Spec.Filter).To(Equal(original.Spec.Filter)) + }) + + It("should handle empty context when round-tripping (no Contexts entry produced)", func() { + original := &dash0v1alpha2.Dash0SpamFilter{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-spam-filter", + }, + Spec: dash0v1alpha2.Dash0SpamFilterSpec{ + Context: "", + Filter: []dash0v1alpha2.Dash0SpamFilterCondition{ + {Key: "level", Operator: "is_set"}, + }, + }, + } + + spoke := &dash0v1alpha1.Dash0SpamFilter{} + Expect(spoke.ConvertFrom(original)).To(Succeed()) + Expect(spoke.Spec.Contexts).To(BeEmpty()) + + roundTripped := &dash0v1alpha2.Dash0SpamFilter{} + Expect(spoke.ConvertTo(roundTripped)).To(Succeed()) + Expect(roundTripped.Spec.Context).To(BeEmpty()) + }) + }) +}) diff --git a/api/operator/v1alpha2/groupversion_info.go b/api/operator/v1alpha2/groupversion_info.go new file mode 100644 index 000000000..750aeac7b --- /dev/null +++ b/api/operator/v1alpha2/groupversion_info.go @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: Copyright 2026 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Package v1alpha2 contains API Schema definitions for the operator v1alpha2 API group +// +kubebuilder:object:generate=true +// +groupName=operator.dash0.com +package v1alpha2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "operator.dash0.com", Version: "v1alpha2"} + + // SchemeBuilder collects the functions that register this group-version's types into a runtime.Scheme. + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(GroupVersion, + &Dash0SpamFilter{}, &Dash0SpamFilterList{}, + ) + metav1.AddToGroupVersion(scheme, GroupVersion) + return nil +} diff --git a/api/operator/v1alpha2/v1alpha2_suite_test.go b/api/operator/v1alpha2/v1alpha2_suite_test.go new file mode 100644 index 000000000..308a9c464 --- /dev/null +++ b/api/operator/v1alpha2/v1alpha2_suite_test.go @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright 2026 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha2_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestV1Alpha2(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "api/v1alpha2 suite") +} diff --git a/api/operator/v1alpha2/zz_generated.deepcopy.go b/api/operator/v1alpha2/zz_generated.deepcopy.go new file mode 100644 index 000000000..9c2a7a2dd --- /dev/null +++ b/api/operator/v1alpha2/zz_generated.deepcopy.go @@ -0,0 +1,154 @@ +//go:build !ignore_autogenerated + +// SPDX-FileCopyrightText: Copyright 2025 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Dash0SpamFilter) DeepCopyInto(out *Dash0SpamFilter) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Dash0SpamFilter. +func (in *Dash0SpamFilter) DeepCopy() *Dash0SpamFilter { + if in == nil { + return nil + } + out := new(Dash0SpamFilter) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Dash0SpamFilter) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Dash0SpamFilterCondition) DeepCopyInto(out *Dash0SpamFilterCondition) { + *out = *in + if in.Value != nil { + in, out := &in.Value, &out.Value + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Dash0SpamFilterCondition. +func (in *Dash0SpamFilterCondition) DeepCopy() *Dash0SpamFilterCondition { + if in == nil { + return nil + } + out := new(Dash0SpamFilterCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Dash0SpamFilterList) DeepCopyInto(out *Dash0SpamFilterList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Dash0SpamFilter, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Dash0SpamFilterList. +func (in *Dash0SpamFilterList) DeepCopy() *Dash0SpamFilterList { + if in == nil { + return nil + } + out := new(Dash0SpamFilterList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Dash0SpamFilterList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Dash0SpamFilterSpec) DeepCopyInto(out *Dash0SpamFilterSpec) { + *out = *in + if in.Filter != nil { + in, out := &in.Filter, &out.Filter + *out = make([]Dash0SpamFilterCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Dash0SpamFilterSpec. +func (in *Dash0SpamFilterSpec) DeepCopy() *Dash0SpamFilterSpec { + if in == nil { + return nil + } + out := new(Dash0SpamFilterSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Dash0SpamFilterStatus) DeepCopyInto(out *Dash0SpamFilterStatus) { + *out = *in + in.SynchronizedAt.DeepCopyInto(&out.SynchronizedAt) + if in.ValidationIssues != nil { + in, out := &in.ValidationIssues, &out.ValidationIssues + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.SynchronizationResults != nil { + in, out := &in.SynchronizationResults, &out.SynchronizationResults + *out = make([]Dash0SpamFilterSynchronizationResultPerEndpointAndDataset, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Dash0SpamFilterStatus. +func (in *Dash0SpamFilterStatus) DeepCopy() *Dash0SpamFilterStatus { + if in == nil { + return nil + } + out := new(Dash0SpamFilterStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Dash0SpamFilterSynchronizationResultPerEndpointAndDataset) DeepCopyInto(out *Dash0SpamFilterSynchronizationResultPerEndpointAndDataset) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Dash0SpamFilterSynchronizationResultPerEndpointAndDataset. +func (in *Dash0SpamFilterSynchronizationResultPerEndpointAndDataset) DeepCopy() *Dash0SpamFilterSynchronizationResultPerEndpointAndDataset { + if in == nil { + return nil + } + out := new(Dash0SpamFilterSynchronizationResultPerEndpointAndDataset) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/operator.dash0.com_dash0spamfilters.yaml b/config/crd/bases/operator.dash0.com_dash0spamfilters.yaml index f4e1de641..5bd61f86e 100644 --- a/config/crd/bases/operator.dash0.com_dash0spamfilters.yaml +++ b/config/crd/bases/operator.dash0.com_dash0spamfilters.yaml @@ -40,10 +40,17 @@ spec: description: Dash0SpamFilterSpec defines the desired state of Dash0SpamFilter properties: contexts: - description: The signal contexts this spam filter applies to (e.g., - log, span, metric). + description: |- + The signal contexts this spam filter applies to. Only the first element is used; + additional elements are ignored when converting to the v1alpha2 hub version. items: + enum: + - datapoint + - log + - span + - web_event type: string + maxItems: 1 minItems: 1 type: array filter: @@ -149,6 +156,145 @@ spec: type: object type: object served: true + storage: false + subresources: + status: {} + - name: v1alpha2 + schema: + openAPIV3Schema: + description: Dash0SpamFilter is the Schema for the Dash0SpamFilter API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Dash0SpamFilterSpec defines the desired state of Dash0SpamFilter + properties: + context: + description: The signal context this spam filter applies to. + enum: + - datapoint + - log + - span + - web_event + type: string + filter: + description: The filter conditions that define which telemetry data + to drop. + items: + description: |- + Dash0SpamFilterCondition defines a single filter condition for spam filtering, + matching the Dash0 API's AttributeFilter schema. + properties: + key: + description: The attribute key to match against. + type: string + operator: + description: The comparison operator (e.g., "is", "is_not", + "contains", "starts_with"). + enum: + - is + - is_not + - is_set + - is_not_set + - is_one_of + - is_not_one_of + - gt + - lt + - gte + - lte + - matches + - does_not_match + - contains + - does_not_contain + - starts_with + - does_not_start_with + - ends_with + - does_not_end_with + - is_any + type: string + value: + description: The value to compare against. Optional for operators + like "is_set" and "is_not_set". + type: string + required: + - key + - operator + type: object + minItems: 1 + type: array + required: + - context + - filter + type: object + status: + description: Dash0SpamFilterStatus defines the observed state of Dash0SpamFilter + properties: + synchronizationResults: + items: + description: Dash0SpamFilterSynchronizationResultPerEndpointAndDataset + defines the synchronization result per endpoint and dataset + properties: + dash0ApiEndpoint: + type: string + dash0Dataset: + type: string + dash0Id: + type: string + dash0Origin: + type: string + synchronizationError: + type: string + synchronizationStatus: + description: |- + Dash0ApiResourceSynchronizationStatus describes the result of synchronizing a (non-third-party) Kubernetes resource + (e.g. synthetic checks) to the Dash0 API. + enum: + - successful + - partially-successful + - failed + type: string + required: + - synchronizationStatus + type: object + type: array + synchronizationStatus: + description: |- + Dash0ApiResourceSynchronizationStatus describes the result of synchronizing a (non-third-party) Kubernetes resource + (e.g. synthetic checks) to the Dash0 API. + enum: + - successful + - partially-successful + - failed + type: string + synchronizedAt: + format: date-time + type: string + validationIssues: + items: + type: string + type: array + required: + - synchronizationResults + - synchronizationStatus + - synchronizedAt + type: object + type: object + served: true storage: true subresources: status: {} diff --git a/helm-chart/dash0-operator/templates/operator/custom-resource-definition-spam-filters.yaml b/helm-chart/dash0-operator/templates/operator/custom-resource-definition-spam-filters.yaml deleted file mode 100644 index f4e1de641..000000000 --- a/helm-chart/dash0-operator/templates/operator/custom-resource-definition-spam-filters.yaml +++ /dev/null @@ -1,154 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.19.0 - name: dash0spamfilters.operator.dash0.com -spec: - group: operator.dash0.com - names: - kind: Dash0SpamFilter - listKind: Dash0SpamFilterList - plural: dash0spamfilters - singular: dash0spamfilter - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: Dash0SpamFilter is the Schema for the Dash0SpamFilter API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: Dash0SpamFilterSpec defines the desired state of Dash0SpamFilter - properties: - contexts: - description: The signal contexts this spam filter applies to (e.g., - log, span, metric). - items: - type: string - minItems: 1 - type: array - filter: - description: The filter conditions that define which telemetry data - to drop. - items: - description: |- - Dash0SpamFilterCondition defines a single filter condition for spam filtering, - matching the Dash0 API's AttributeFilter schema. - properties: - key: - description: The attribute key to match against. - type: string - operator: - description: The comparison operator (e.g., "is", "is_not", - "contains", "starts_with"). - enum: - - is - - is_not - - is_set - - is_not_set - - is_one_of - - is_not_one_of - - gt - - lt - - gte - - lte - - matches - - does_not_match - - contains - - does_not_contain - - starts_with - - does_not_start_with - - ends_with - - does_not_end_with - - is_any - type: string - value: - description: The value to compare against. Optional for operators - like "is_set" and "is_not_set". - type: string - required: - - key - - operator - type: object - minItems: 1 - type: array - required: - - contexts - - filter - type: object - status: - description: Dash0SpamFilterStatus defines the observed state of Dash0SpamFilter - properties: - synchronizationResults: - items: - description: Dash0SpamFilterSynchronizationResultPerEndpointAndDataset - defines the synchronization result per endpoint and dataset - properties: - dash0ApiEndpoint: - type: string - dash0Dataset: - type: string - dash0Id: - type: string - dash0Origin: - type: string - synchronizationError: - type: string - synchronizationStatus: - description: |- - Dash0ApiResourceSynchronizationStatus describes the result of synchronizing a (non-third-party) Kubernetes resource - (e.g. synthetic checks) to the Dash0 API. - enum: - - successful - - partially-successful - - failed - type: string - required: - - synchronizationStatus - type: object - type: array - synchronizationStatus: - description: |- - Dash0ApiResourceSynchronizationStatus describes the result of synchronizing a (non-third-party) Kubernetes resource - (e.g. synthetic checks) to the Dash0 API. - enum: - - successful - - partially-successful - - failed - type: string - synchronizedAt: - format: date-time - type: string - validationIssues: - items: - type: string - type: array - required: - - synchronizationResults - - synchronizationStatus - - synchronizedAt - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/helm-chart/dash0-operator/templates/operator/deployment-and-webhooks.yaml b/helm-chart/dash0-operator/templates/operator/deployment-and-webhooks.yaml index 9d2171935..cffe82b89 100644 --- a/helm-chart/dash0-operator/templates/operator/deployment-and-webhooks.yaml +++ b/helm-chart/dash0-operator/templates/operator/deployment-and-webhooks.yaml @@ -781,6 +781,13 @@ spec: ------------------------------------------- Custom Resource Definition: Dash0Monitoring ------------------------------------------- + +The Dash0Monitoring and Dash0SpamFilter CRDs live in this template (and not in their own +custom-resource-definition-*.yaml file like the other CRDs) because they declare a conversion +webhook (spec.conversion.strategy: Webhook) whose clientConfig points at the operator's webhook +service defined above in this same template. Keeping them here keeps the cert-manager / caBundle +wiring and the webhook service reference templated in lockstep. If a CRD does not need a conversion +webhook, it belongs in its own custom-resource-definition-*.yaml template instead. */}} --- apiVersion: apiextensions.k8s.io/v1 @@ -3025,3 +3032,333 @@ path than "/convert". Something like "/monitoring/conversion" would be better. T a conversion webhook for the Dash0OperatorConfiguration resource as well. */}} path: /convert +{{/* +------------------------------------------- +Custom Resource Definition: Dash0SpamFilter +------------------------------------------- + +This CRD lives in this template (rather than its own custom-resource-definition-*.yaml file) for the +same reason as Dash0Monitoring above: it declares a conversion webhook whose clientConfig points at +the operator's webhook service defined earlier in this file. See the banner above the Dash0Monitoring +CRD for the full rationale. +*/}} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: dash0spamfilters.operator.dash0.com +spec: + group: operator.dash0.com + names: + kind: Dash0SpamFilter + listKind: Dash0SpamFilterList + plural: dash0spamfilters + singular: dash0spamfilter + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Dash0SpamFilter is the Schema for the Dash0SpamFilter API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Dash0SpamFilterSpec defines the desired state of Dash0SpamFilter + properties: + contexts: + description: |- + The signal contexts this spam filter applies to. Only the first element is used; + additional elements are ignored when converting to the v1alpha2 hub version. + items: + enum: + - datapoint + - log + - span + - web_event + type: string + maxItems: 1 + minItems: 1 + type: array + filter: + description: The filter conditions that define which telemetry data + to drop. + items: + description: |- + Dash0SpamFilterCondition defines a single filter condition for spam filtering, + matching the Dash0 API's AttributeFilter schema. + properties: + key: + description: The attribute key to match against. + type: string + operator: + description: The comparison operator (e.g., "is", "is_not", + "contains", "starts_with"). + enum: + - is + - is_not + - is_set + - is_not_set + - is_one_of + - is_not_one_of + - gt + - lt + - gte + - lte + - matches + - does_not_match + - contains + - does_not_contain + - starts_with + - does_not_start_with + - ends_with + - does_not_end_with + - is_any + type: string + value: + description: The value to compare against. Optional for operators + like "is_set" and "is_not_set". + type: string + required: + - key + - operator + type: object + minItems: 1 + type: array + required: + - contexts + - filter + type: object + status: + description: Dash0SpamFilterStatus defines the observed state of Dash0SpamFilter + properties: + synchronizationResults: + items: + description: Dash0SpamFilterSynchronizationResultPerEndpointAndDataset + defines the synchronization result per endpoint and dataset + properties: + dash0ApiEndpoint: + type: string + dash0Dataset: + type: string + dash0Id: + type: string + dash0Origin: + type: string + synchronizationError: + type: string + synchronizationStatus: + description: |- + Dash0ApiResourceSynchronizationStatus describes the result of synchronizing a (non-third-party) Kubernetes resource + (e.g. synthetic checks) to the Dash0 API. + enum: + - successful + - partially-successful + - failed + type: string + required: + - synchronizationStatus + type: object + type: array + synchronizationStatus: + description: |- + Dash0ApiResourceSynchronizationStatus describes the result of synchronizing a (non-third-party) Kubernetes resource + (e.g. synthetic checks) to the Dash0 API. + enum: + - successful + - partially-successful + - failed + type: string + synchronizedAt: + format: date-time + type: string + validationIssues: + items: + type: string + type: array + required: + - synchronizationResults + - synchronizationStatus + - synchronizedAt + type: object + type: object + served: true + storage: false + subresources: + status: {} + - name: v1alpha2 + schema: + openAPIV3Schema: + description: Dash0SpamFilter is the Schema for the Dash0SpamFilter API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Dash0SpamFilterSpec defines the desired state of Dash0SpamFilter + properties: + context: + description: The signal context this spam filter applies to. + enum: + - datapoint + - log + - span + - web_event + type: string + filter: + description: The filter conditions that define which telemetry data + to drop. + items: + description: |- + Dash0SpamFilterCondition defines a single filter condition for spam filtering, + matching the Dash0 API's AttributeFilter schema. + properties: + key: + description: The attribute key to match against. + type: string + operator: + description: The comparison operator (e.g., "is", "is_not", + "contains", "starts_with"). + enum: + - is + - is_not + - is_set + - is_not_set + - is_one_of + - is_not_one_of + - gt + - lt + - gte + - lte + - matches + - does_not_match + - contains + - does_not_contain + - starts_with + - does_not_start_with + - ends_with + - does_not_end_with + - is_any + type: string + value: + description: The value to compare against. Optional for operators + like "is_set" and "is_not_set". + type: string + required: + - key + - operator + type: object + minItems: 1 + type: array + required: + - context + - filter + type: object + status: + description: Dash0SpamFilterStatus defines the observed state of Dash0SpamFilter + properties: + synchronizationResults: + items: + description: Dash0SpamFilterSynchronizationResultPerEndpointAndDataset + defines the synchronization result per endpoint and dataset + properties: + dash0ApiEndpoint: + type: string + dash0Dataset: + type: string + dash0Id: + type: string + dash0Origin: + type: string + synchronizationError: + type: string + synchronizationStatus: + description: |- + Dash0ApiResourceSynchronizationStatus describes the result of synchronizing a (non-third-party) Kubernetes resource + (e.g. synthetic checks) to the Dash0 API. + enum: + - successful + - partially-successful + - failed + type: string + required: + - synchronizationStatus + type: object + type: array + synchronizationStatus: + description: |- + Dash0ApiResourceSynchronizationStatus describes the result of synchronizing a (non-third-party) Kubernetes resource + (e.g. synthetic checks) to the Dash0 API. + enum: + - successful + - partially-successful + - failed + type: string + synchronizedAt: + format: date-time + type: string + validationIssues: + items: + type: string + type: array + required: + - synchronizationResults + - synchronizationStatus + - synchronizedAt + type: object + type: object + served: true + storage: true + subresources: + status: {} +{{/* +Maintenance note: When synchronizing helm-chart/dash0-operator/templates/operator/deployment-and-webhooks.yaml +with config/crd/bases/operator.dash0.com_dash0spamfilters.yaml after changing the CRD Golang types under +api/operator, note that the the conversion webhook will not be generated in +config/crd/bases/operator.dash0.com_dash0spamfilters.yaml; hence, this diff is to be ignored. +*/}} + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: + - v1 + clientConfig: + {{- if not .Values.operator.certManager.useCertManager }} + caBundle: {{ default "" ( $ca.Cert | b64enc ) }} + {{- end }} + service: + name: {{ template "dash0-operator.webhookServiceName" . }} + namespace: {{ .Release.Namespace }} + port: {{ template "dash0-operator.webhookServicePort" . }} + path: /convert diff --git a/internal/controller/controller_suite_test.go b/internal/controller/controller_suite_test.go index 11becd29a..5ad55f722 100644 --- a/internal/controller/controller_suite_test.go +++ b/internal/controller/controller_suite_test.go @@ -21,6 +21,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" dash0v1alpha1 "github.com/dash0hq/dash0-operator/api/operator/v1alpha1" + dash0v1alpha2 "github.com/dash0hq/dash0-operator/api/operator/v1alpha2" dash0v1beta1 "github.com/dash0hq/dash0-operator/api/operator/v1beta1" . "github.com/onsi/ginkgo/v2" @@ -69,6 +70,7 @@ var _ = BeforeSuite(func() { Expect(cfg).NotTo(BeNil()) Expect(dash0v1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(dash0v1alpha2.AddToScheme(scheme.Scheme)).To(Succeed()) Expect(dash0v1beta1.AddToScheme(scheme.Scheme)).To(Succeed()) Expect(persesv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) diff --git a/internal/controller/spam_filter_controller.go b/internal/controller/spam_filter_controller.go index 224c50173..fced334c3 100644 --- a/internal/controller/spam_filter_controller.go +++ b/internal/controller/spam_filter_controller.go @@ -13,6 +13,7 @@ import ( "net/url" "reflect" "slices" + "strings" "sync" "sync/atomic" "time" @@ -29,7 +30,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" dash0common "github.com/dash0hq/dash0-operator/api/operator/common" - dash0v1alpha1 "github.com/dash0hq/dash0-operator/api/operator/v1alpha1" + dash0v1alpha2 "github.com/dash0hq/dash0-operator/api/operator/v1alpha2" dash0v1beta1 "github.com/dash0hq/dash0-operator/api/operator/v1beta1" "github.com/dash0hq/dash0-operator/internal/selfmonitoringapiaccess" "github.com/dash0hq/dash0-operator/internal/util" @@ -72,7 +73,7 @@ func NewSpamFilterReconciler( func (r *SpamFilterReconciler) SetupWithManager(mgr manager.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&dash0v1alpha1.Dash0SpamFilter{}). + For(&dash0v1alpha2.Dash0SpamFilter{}). // ignore changes in the status subresource, but react on changes to spec, label and annotations WithEventFilter(spamFilterPredicate{}). Complete(r) @@ -206,7 +207,7 @@ func (r *SpamFilterReconciler) maybeDoInitialSynchronizationOfAllResources(ctx c go func() { defer r.initialSyncInProgress.Store(false) - allResources := dash0v1alpha1.Dash0SpamFilterList{} + allResources := dash0v1alpha2.Dash0SpamFilterList{} if err := r.List( ctx, &allResources, @@ -253,7 +254,7 @@ func (r *SpamFilterReconciler) synchronizeNamespacedResources(ctx context.Contex go func() { defer r.namespacedSyncMutex.Unlock(namespace) - allResources := dash0v1alpha1.Dash0SpamFilterList{} + allResources := dash0v1alpha2.Dash0SpamFilterList{} if err := r.List( ctx, &allResources, @@ -290,12 +291,12 @@ func (r *SpamFilterReconciler) Reconcile(ctx context.Context, req reconcile.Requ logger.Info("processing reconcile request for a spam filter resource", "name", qualifiedName) action := upsertAction - spamFilterResource := &dash0v1alpha1.Dash0SpamFilter{} + spamFilterResource := &dash0v1alpha2.Dash0SpamFilter{} if err := r.Get(ctx, req.NamespacedName, spamFilterResource); err != nil { if apierrors.IsNotFound(err) { action = deleteAction logger.Info("reconciling the deletion of the spam filter resource", "name", qualifiedName) - spamFilterResource = &dash0v1alpha1.Dash0SpamFilter{ + spamFilterResource = &dash0v1alpha2.Dash0SpamFilter{ ObjectMeta: metav1.ObjectMeta{ Namespace: req.Namespace, Name: req.Name, @@ -352,6 +353,13 @@ func (r *SpamFilterReconciler) MapResourceToHttpRequests( switch action { case upsertAction: resource := preconditionChecksResult.resource + // The Dash0 API recognizes only the bare version token (e.g. "v1alpha2") and treats anything else, + // including the K8s group-qualified form "operator.dash0.com/v1alpha2", as v1alpha1. + if apiVersion, ok := resource["apiVersion"].(string); ok { + if idx := strings.LastIndex(apiVersion, "/"); idx >= 0 { + resource["apiVersion"] = apiVersion[idx+1:] + } + } serializedResource, _ := json.Marshal(resource) requestPayload := bytes.NewBuffer(serializedResource) method = http.MethodPut @@ -443,7 +451,7 @@ func (r *SpamFilterReconciler) WriteSynchronizationResultToSynchronizedResource( syncResults synchronizationResults, logger logd.Logger, ) { - spamFilter := synchronizedResource.(*dash0v1alpha1.Dash0SpamFilter) + spamFilter := synchronizedResource.(*dash0v1alpha2.Dash0SpamFilter) // common result spamFilter.Status.SynchronizationStatus = syncResults.resourceSyncStatus() @@ -451,7 +459,7 @@ func (r *SpamFilterReconciler) WriteSynchronizationResultToSynchronizedResource( spamFilter.Status.ValidationIssues = nil // we do not validate anything for spam filters // result(s) per apiConfig - spamFilterSyncResults := make([]dash0v1alpha1.Dash0SpamFilterSynchronizationResultPerEndpointAndDataset, 0, + spamFilterSyncResults := make([]dash0v1alpha2.Dash0SpamFilterSynchronizationResultPerEndpointAndDataset, 0, len(syncResults.resultsPerApiConfig)) for _, res := range syncResults.resultsPerApiConfig { synchronizationStatus := dash0common.Dash0ApiResourceSynchronizationStatusFailed @@ -464,7 +472,7 @@ func (r *SpamFilterReconciler) WriteSynchronizationResultToSynchronizedResource( synchronizationError = "" synchronizationStatus = dash0common.Dash0ApiResourceSynchronizationStatusSuccessful } - syncResultPerEndpointAndDataset := dash0v1alpha1.Dash0SpamFilterSynchronizationResultPerEndpointAndDataset{ + syncResultPerEndpointAndDataset := dash0v1alpha2.Dash0SpamFilterSynchronizationResultPerEndpointAndDataset{ SynchronizationStatus: synchronizationStatus, Dash0ApiEndpoint: res.apiConfig.Endpoint, Dash0Dataset: res.apiConfig.Dataset, @@ -502,8 +510,8 @@ func (p spamFilterPredicate) Update(e event.UpdateEvent) bool { return true } - oldObj, okOld := e.ObjectOld.(*dash0v1alpha1.Dash0SpamFilter) - newObj, okNew := e.ObjectNew.(*dash0v1alpha1.Dash0SpamFilter) + oldObj, okOld := e.ObjectOld.(*dash0v1alpha2.Dash0SpamFilter) + newObj, okNew := e.ObjectNew.(*dash0v1alpha2.Dash0SpamFilter) if !okOld || !okNew { return true diff --git a/internal/controller/spam_filter_controller_test.go b/internal/controller/spam_filter_controller_test.go index ed5d4e1cb..6a0aebccc 100644 --- a/internal/controller/spam_filter_controller_test.go +++ b/internal/controller/spam_filter_controller_test.go @@ -20,7 +20,7 @@ import ( "sigs.k8s.io/yaml" dash0common "github.com/dash0hq/dash0-operator/api/operator/common" - dash0v1alpha1 "github.com/dash0hq/dash0-operator/api/operator/v1alpha1" + dash0v1alpha2 "github.com/dash0hq/dash0-operator/api/operator/v1alpha2" "github.com/dash0hq/dash0-operator/internal/util" "github.com/dash0hq/dash0-operator/internal/util/logd" @@ -283,7 +283,7 @@ var _ = Describe( Expect(k8sClient.Create(ctx, spamFilterResource)).To(Succeed()) // Modify the resource - spamFilterResource.Spec.Contexts = []string{"log", "span"} + spamFilterResource.Spec.Context = "span" Expect(k8sClient.Update(ctx, spamFilterResource)).To(Succeed()) result, err := spamFilterReconciler.Reconcile( @@ -714,7 +714,7 @@ func expectSpamFilterDeleteRequestWithHttpStatus(expectedPath string, status int Reply(status) } -func createSpamFilterResource(namespace string, name string) *dash0v1alpha1.Dash0SpamFilter { +func createSpamFilterResource(namespace string, name string) *dash0v1alpha2.Dash0SpamFilter { return createSpamFilterResourceWithEnableLabel(namespace, name, "") } @@ -722,7 +722,7 @@ func createSpamFilterResourceWithEnableLabel( namespace string, name string, dash0EnableLabelValue string, -) *dash0v1alpha1.Dash0SpamFilter { +) *dash0v1alpha2.Dash0SpamFilter { objectMeta := metav1.ObjectMeta{ Name: name, Namespace: namespace, @@ -732,15 +732,15 @@ func createSpamFilterResourceWithEnableLabel( "dash0.com/enable": dash0EnableLabelValue, } } - return &dash0v1alpha1.Dash0SpamFilter{ + return &dash0v1alpha2.Dash0SpamFilter{ TypeMeta: metav1.TypeMeta{ - APIVersion: "operator.dash0.com/v1alpha1", + APIVersion: "operator.dash0.com/v1alpha2", Kind: "Dash0SpamFilter", }, ObjectMeta: objectMeta, - Spec: dash0v1alpha1.Dash0SpamFilterSpec{ - Contexts: []string{"log"}, - Filter: []dash0v1alpha1.Dash0SpamFilterCondition{ + Spec: dash0v1alpha2.Dash0SpamFilterSpec{ + Context: "log", + Filter: []dash0v1alpha2.Dash0SpamFilterCondition{ { Key: "k8s.namespace.name", Operator: "is", @@ -752,7 +752,7 @@ func createSpamFilterResourceWithEnableLabel( } func deleteSpamFilterResourceIfItExists(ctx context.Context, k8sClient client.Client, namespace string, name string) { - spamFilter := &dash0v1alpha1.Dash0SpamFilter{ + spamFilter := &dash0v1alpha2.Dash0SpamFilter{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, @@ -783,7 +783,7 @@ func verifySpamFilterSynchronizationStatus( ) { Eventually( func(g Gomega) { - spamFilter := &dash0v1alpha1.Dash0SpamFilter{} + spamFilter := &dash0v1alpha2.Dash0SpamFilter{} err := k8sClient.Get( ctx, types.NamespacedName{ Namespace: namespace, @@ -817,7 +817,7 @@ func verifySpamFilterHasNoSynchronizationStatus( ) { Eventually( func(g Gomega) { - spamFilter := &dash0v1alpha1.Dash0SpamFilter{} + spamFilter := &dash0v1alpha2.Dash0SpamFilter{} err := k8sClient.Get( ctx, types.NamespacedName{ Namespace: namespace, diff --git a/internal/startup/operator_manager_startup.go b/internal/startup/operator_manager_startup.go index 015353add..4fc9cb52e 100644 --- a/internal/startup/operator_manager_startup.go +++ b/internal/startup/operator_manager_startup.go @@ -42,6 +42,7 @@ import ( k8swebhook "sigs.k8s.io/controller-runtime/pkg/webhook" dash0v1alpha1 "github.com/dash0hq/dash0-operator/api/operator/v1alpha1" + dash0v1alpha2 "github.com/dash0hq/dash0-operator/api/operator/v1alpha2" dash0v1beta1 "github.com/dash0hq/dash0-operator/api/operator/v1beta1" "github.com/dash0hq/dash0-operator/internal/collectors" "github.com/dash0hq/dash0-operator/internal/collectors/otelcolresources" @@ -208,6 +209,7 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(runtimeScheme)) utilruntime.Must(dash0v1alpha1.AddToScheme(runtimeScheme)) + utilruntime.Must(dash0v1alpha2.AddToScheme(runtimeScheme)) utilruntime.Must(dash0v1beta1.AddToScheme(runtimeScheme)) // required for Perses dashboard controller and Prometheus rules controller. diff --git a/internal/startup/startup_suite_test.go b/internal/startup/startup_suite_test.go index 85fdf5794..e45ec8cb8 100644 --- a/internal/startup/startup_suite_test.go +++ b/internal/startup/startup_suite_test.go @@ -17,6 +17,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" dash0v1alpha1 "github.com/dash0hq/dash0-operator/api/operator/v1alpha1" + dash0v1alpha2 "github.com/dash0hq/dash0-operator/api/operator/v1alpha2" dash0v1beta1 "github.com/dash0hq/dash0-operator/api/operator/v1beta1" . "github.com/onsi/ginkgo/v2" @@ -57,6 +58,7 @@ var _ = BeforeSuite(func() { Expect(cfg).NotTo(BeNil()) Expect(dash0v1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(dash0v1alpha2.AddToScheme(scheme.Scheme)).To(Succeed()) Expect(dash0v1beta1.AddToScheme(scheme.Scheme)).To(Succeed()) k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) diff --git a/test-resources/customresources/dash0spamfilter/dash0spamfilter.yaml b/test-resources/customresources/dash0spamfilter/dash0spamfilter_v1alpha1.yaml similarity index 100% rename from test-resources/customresources/dash0spamfilter/dash0spamfilter.yaml rename to test-resources/customresources/dash0spamfilter/dash0spamfilter_v1alpha1.yaml diff --git a/test-resources/customresources/dash0spamfilter/dash0spamfilter_v1alpha2.yaml b/test-resources/customresources/dash0spamfilter/dash0spamfilter_v1alpha2.yaml new file mode 100644 index 000000000..93ee5973a --- /dev/null +++ b/test-resources/customresources/dash0spamfilter/dash0spamfilter_v1alpha2.yaml @@ -0,0 +1,10 @@ +apiVersion: operator.dash0.com/v1alpha2 +kind: Dash0SpamFilter +metadata: + name: drop-health-checks +spec: + context: log + filter: + - key: "k8s.namespace.name" + operator: "is" + value: "kube-system" diff --git a/test/e2e/dash0_api_sync_resources.go b/test/e2e/dash0_api_sync_resources.go index a1ad4c3f5..015c865fe 100644 --- a/test/e2e/dash0_api_sync_resources.go +++ b/test/e2e/dash0_api_sync_resources.go @@ -48,9 +48,18 @@ var ( notificationChannelSource string notificationChannelTemplate *template.Template - //go:embed dash0spamfilter.yaml.template - spamFilterSource string - spamFilterTemplate *template.Template + //go:embed dash0spamfilter_v1alpha1.yaml.template + spamFilterV1Alpha1Source string + spamFilterV1Alpha1Template *template.Template + + //go:embed dash0spamfilter_v1alpha2.yaml.template + spamFilterV1Alpha2Source string + spamFilterV1Alpha2Template *template.Template +) + +const ( + spamFilterAPIVersionV1Alpha1 = "v1alpha1" + spamFilterAPIVersionV1Alpha2 = "v1alpha2" ) func deployThirdPartyCrds(cleanupSteps *neccessaryCleanupSteps) { @@ -381,26 +390,41 @@ func removeNotificationChannelResource(namespace string) { )) } -func renderSpamFilterTemplate(values dash0ApiResourceValues) string { - spamFilterTemplate = initTemplateOnce( - spamFilterTemplate, - spamFilterSource, - "dash0spamfilter", - ) - return renderResourceTemplate(spamFilterTemplate, values, "dash0spamfilter") +func renderSpamFilterTemplate(values dash0ApiResourceValues, apiVersion string) string { + switch apiVersion { + case spamFilterAPIVersionV1Alpha1: + spamFilterV1Alpha1Template = initTemplateOnce( + spamFilterV1Alpha1Template, + spamFilterV1Alpha1Source, + "dash0spamfilter_v1alpha1", + ) + return renderResourceTemplate(spamFilterV1Alpha1Template, values, "dash0spamfilter_v1alpha1") + case spamFilterAPIVersionV1Alpha2: + spamFilterV1Alpha2Template = initTemplateOnce( + spamFilterV1Alpha2Template, + spamFilterV1Alpha2Source, + "dash0spamfilter_v1alpha2", + ) + return renderResourceTemplate(spamFilterV1Alpha2Template, values, "dash0spamfilter_v1alpha2") + default: + Fail(fmt.Sprintf("unsupported Dash0SpamFilter API version %q", apiVersion)) + return "" + } } func deploySpamFilterResource( namespace string, values dash0ApiResourceValues, + apiVersion string, ) { - renderedResourceFileName := renderSpamFilterTemplate(values) + renderedResourceFileName := renderSpamFilterTemplate(values, apiVersion) defer func() { Expect(os.Remove(renderedResourceFileName)).To(Succeed()) }() By(fmt.Sprintf( - "deploying a Dash0SpamFilter resource to namespace %s with values %v", namespace, values)) + "deploying a Dash0SpamFilter resource (%s) to namespace %s with values %v", + apiVersion, namespace, values)) Expect(runAndIgnoreOutput(exec.Command( "kubectl", "apply", @@ -411,6 +435,7 @@ func deploySpamFilterResource( ))).To(Succeed()) } +//nolint:unparam // namespace is always applicationUnderTestNamespace, kept for symmetry with peer setOptOutLabel* functions func setOptOutLabelInSpamFilter(namespace string, value string) { By(fmt.Sprintf("setting the opt-out label in the spam filter with value %s", value)) Expect( diff --git a/test/e2e/dash0spamfilter.yaml.template b/test/e2e/dash0spamfilter_v1alpha1.yaml.template similarity index 100% rename from test/e2e/dash0spamfilter.yaml.template rename to test/e2e/dash0spamfilter_v1alpha1.yaml.template diff --git a/test/e2e/dash0spamfilter_v1alpha2.yaml.template b/test/e2e/dash0spamfilter_v1alpha2.yaml.template new file mode 100644 index 000000000..5ac9cfb9c --- /dev/null +++ b/test/e2e/dash0spamfilter_v1alpha2.yaml.template @@ -0,0 +1,15 @@ +apiVersion: operator.dash0.com/v1alpha2 +kind: Dash0SpamFilter +metadata: + name: spam-filter-e2e-test + labels: + app.kubernetes.io/name: spam-filter-e2e-test +{{- if .Dash0ComEnabled }} + dash0.com/enabled: {{ .Dash0ComEnabled | quote }} +{{- end }} +spec: + context: log + filter: + - key: "k8s.namespace.name" + operator: "is" + value: "kube-system" diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index f89164407..ed8af04a8 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -710,10 +710,52 @@ var _ = Describe("Dash0 Operator", Ordered, ContinueOnFailure, func() { }) //nolint:dupl - It("should synchronize a spam filter to the Dash0 API", func() { + It("should synchronize a v1alpha1 spam filter to the Dash0 API", func() { deploySpamFilterResource( applicationUnderTestNamespace, dash0ApiResourceValues{}, + spamFilterAPIVersionV1Alpha1, + ) + + //nolint:lll + routeRegex := "/api/spam-filters/dash0-operator_.*_default_e2e-test-ns_spam-filter-e2e-test\\?dataset=default" + + By("verifying the spam filter has been synchronized to the Dash0 API via PUT") + req := fetchCapturedApiRequest(0) + Expect(req.Method).To(Equal("PUT")) + Expect(req.Url).To(MatchRegexp(routeRegex)) + Expect(req.Body).ToNot(BeNil()) + Expect(*req.Body).To(ContainSubstring("k8s.namespace.name")) + verifyApiSyncRequest(req) + + setOptOutLabelInSpamFilter(applicationUnderTestNamespace, "false") + By("verifying the spam filter has been deleted via the Dash0 API (after setting dash0.com/enable=false)\"") + req = fetchCapturedApiRequest(1) + Expect(req.Method).To(Equal("DELETE")) + Expect(req.Url).To(MatchRegexp(routeRegex)) + + setOptOutLabelInSpamFilter(applicationUnderTestNamespace, "true") + //nolint:lll + By("verifying the spam filter has been synchronized to the Dash0 API via PUT (after setting dash0.com/enable=true)") + req = fetchCapturedApiRequest(2) + Expect(req.Method).To(Equal("PUT")) + Expect(req.Url).To(MatchRegexp(routeRegex)) + Expect(*req.Body).To(ContainSubstring("k8s.namespace.name")) + verifyApiSyncRequest(req) + + removeSpamFilterResource(applicationUnderTestNamespace) + By("verifying the spam filter has been deleted via the Dash0 API (after removing the resource)") + req = fetchCapturedApiRequest(3) + Expect(req.Method).To(Equal("DELETE")) + Expect(req.Url).To(MatchRegexp(routeRegex)) + }) + + //nolint:dupl + It("should synchronize a v1alpha2 spam filter to the Dash0 API", func() { + deploySpamFilterResource( + applicationUnderTestNamespace, + dash0ApiResourceValues{}, + spamFilterAPIVersionV1Alpha2, ) //nolint:lll