From 7de6619aa2618012f9d6b4b990b156e11dc38c6e Mon Sep 17 00:00:00 2001 From: Christopher Junk Date: Thu, 2 Oct 2025 15:41:01 +0200 Subject: [PATCH] feat(managedmetric)!: new target spec field BREAKING CHANGE: the new target GVK type replaces the single GVK fields to implement a consistent API across all metric types Users have to migrate any existing ManagedMetrics manually when upgrading the operator. On-behalf-of: @SAP christopher.junk@sap.com Signed-off-by: Christopher Junk --- api/v1alpha1/managedmetric_types.go | 17 ++++------- api/v1alpha1/zz_generated.deepcopy.go | 5 ++++ .../metrics.openmcp.cloud_managedmetrics.yaml | 24 ++++++++------- internal/orchestrator/managedhandler.go | 20 +++++++++---- internal/orchestrator/managedhandler_test.go | 30 ++++++++++--------- 5 files changed, 55 insertions(+), 41 deletions(-) diff --git a/api/v1alpha1/managedmetric_types.go b/api/v1alpha1/managedmetric_types.go index 04e76cc..98723e2 100644 --- a/api/v1alpha1/managedmetric_types.go +++ b/api/v1alpha1/managedmetric_types.go @@ -17,8 +17,6 @@ limitations under the License. package v1alpha1 import ( - "fmt" - "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -30,12 +28,9 @@ type ManagedMetricSpec struct { // Sets the description that will be used to identify the metric in Dynatrace(or other providers) // +optional Description string `json:"description,omitempty"` - // Decide which kind the metric should keep track of (needs to be plural version) - Kind string `json:"kind,omitempty"` - // Define the group of your object that should be instrumented (without version at the end) - Group string `json:"group,omitempty"` - // Define version of the object you want to intrsument - Version string `json:"version,omitempty"` + // Defines which managed resources to observe + // +optional + Target *GroupVersionKind `json:"target,omitempty"` // Define labels of your object to adapt filters of the query // +optional LabelSelector string `json:"labelSelector,omitempty"` @@ -92,10 +87,10 @@ type ManagedMetricStatus struct { // GvkToString returns group, version and kind as a string func (r *ManagedMetric) GvkToString() string { - if r.Spec.Group == "" { - return fmt.Sprintf("/%s, Kind=%s", r.Spec.Version, r.Spec.Kind) + if r.Spec.Target != nil { + return r.Spec.Target.GVK().String() } - return fmt.Sprintf("%s/%s, Kind=%s", r.Spec.Group, r.Spec.Version, r.Spec.Kind) + return "" } // SetConditions sets the conditions for the ManagedMetric diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 3c0d29f..e775681 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -642,6 +642,11 @@ func (in *ManagedMetricList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagedMetricSpec) DeepCopyInto(out *ManagedMetricSpec) { *out = *in + if in.Target != nil { + in, out := &in.Target, &out.Target + *out = new(GroupVersionKind) + **out = **in + } out.Interval = in.Interval if in.DataSinkRef != nil { in, out := &in.DataSinkRef, &out.DataSinkRef diff --git a/cmd/metrics-operator/embedded/crds/metrics.openmcp.cloud_managedmetrics.yaml b/cmd/metrics-operator/embedded/crds/metrics.openmcp.cloud_managedmetrics.yaml index f814041..c8afc89 100644 --- a/cmd/metrics-operator/embedded/crds/metrics.openmcp.cloud_managedmetrics.yaml +++ b/cmd/metrics-operator/embedded/crds/metrics.openmcp.cloud_managedmetrics.yaml @@ -69,18 +69,10 @@ spec: description: Define fields of your object to adapt filters of the query type: string - group: - description: Define the group of your object that should be instrumented - (without version at the end) - type: string interval: default: 10m description: Define in what interval the query should be recorded type: string - kind: - description: Decide which kind the metric should keep track of (needs - to be plural version) - type: string labelSelector: description: Define labels of your object to adapt filters of the query @@ -98,9 +90,19 @@ spec: namespace: type: string type: object - version: - description: Define version of the object you want to intrsument - type: string + target: + description: Defines which managed resources to observe + properties: + group: + description: Define the group of your object that should be instrumented + type: string + kind: + description: Define the kind of the object that should be instrumented + type: string + version: + description: Define version of the object you want to be instrumented + type: string + type: object type: object status: description: ManagedMetricStatus defines the observed state of ManagedMetric diff --git a/internal/orchestrator/managedhandler.go b/internal/orchestrator/managedhandler.go index 0e1e0d6..9948a7a 100644 --- a/internal/orchestrator/managedhandler.go +++ b/internal/orchestrator/managedhandler.go @@ -174,8 +174,9 @@ func (h *ManagedHandler) getManagedResources(ctx context.Context) ([]Managed, er if !crdv.Served { continue } - // only use the metric target version if provided - if h.metric.Spec.Version != "" && crdv.Name != h.metric.Spec.Version { + // drop versions that don't match the user provided target + target := h.metric.Spec.Target + if target != nil && target.Version != "" && target.Version != crdv.Name { continue } versionsToRetrieve = append(versionsToRetrieve, crdv.Name) @@ -249,17 +250,26 @@ type ClusterResourceStatus struct { } func (h *ManagedHandler) matchesGroupVersionKind(crd apiextensionsv1.CustomResourceDefinition) bool { + target := h.metric.Spec.Target + // if the user does not specify a GVK target, any managed CRD is considered a match + if target == nil { + return true + } + // CRDs may define multi-version APIs + // we consider a version to be a match if it exists in a CRD crdVersions := make([]string, 0, len(crd.Spec.Versions)) for _, version := range crd.Spec.Versions { crdVersions = append(crdVersions, version.Name) } - if h.metric.Spec.Version != "" && !slices.Contains(crdVersions, h.metric.Spec.Version) { + // if the user specifies a target, we consider each GVK attribute and check if it matches the user value + // if the user does not specify a single GVK part, that part is considered unconditional and always a match + if target.Version != "" && !slices.Contains(crdVersions, target.Version) { return false } - if h.metric.Spec.Group != "" && crd.Spec.Group != h.metric.Spec.Group { + if target.Group != "" && target.Group != crd.Spec.Group { return false } - if h.metric.Spec.Kind != "" && crd.Spec.Names.Kind != h.metric.Spec.Kind { + if target.Kind != "" && target.Kind != crd.Spec.Names.Kind { return false } return true diff --git a/internal/orchestrator/managedhandler_test.go b/internal/orchestrator/managedhandler_test.go index 59680e8..786cbfb 100644 --- a/internal/orchestrator/managedhandler_test.go +++ b/internal/orchestrator/managedhandler_test.go @@ -71,14 +71,14 @@ func TestGetManagedResources(t *testing.T) { tests := []struct { name string - filter schema.GroupVersionKind + gvkTarget schema.GroupVersionKind clusterCRDs []string clusterResources []string wantResources []string }{ { - name: "fully qualified target spec", - filter: k8sObjectGVK, + name: "fully qualified target spec", + gvkTarget: k8sObjectGVK, clusterCRDs: []string{ managedAndServedCRD(k8sObjectGVK), managedAndServedCRD(k8sObjectCollectionGVK), @@ -95,7 +95,7 @@ func TestGetManagedResources(t *testing.T) { }, { name: "group version target", - filter: schema.GroupVersionKind{ + gvkTarget: schema.GroupVersionKind{ Group: k8sObjectGVK.Group, Version: k8sObjectGVK.Version, }, @@ -118,7 +118,7 @@ func TestGetManagedResources(t *testing.T) { }, { name: "version target", - filter: schema.GroupVersionKind{ + gvkTarget: schema.GroupVersionKind{ Version: k8sObjectGVK.Version, }, clusterCRDs: []string{ @@ -140,8 +140,8 @@ func TestGetManagedResources(t *testing.T) { ), }, { - name: "unqualified target", - filter: schema.GroupVersionKind{}, + name: "unqualified target", + gvkTarget: schema.GroupVersionKind{}, clusterCRDs: []string{ managedAndServedCRD(k8sObjectGVK), managedAndServedCRD(k8sObjectCollectionGVK), @@ -162,8 +162,8 @@ func TestGetManagedResources(t *testing.T) { ), }, { - name: "unmanaged custom resources get filtered out", - filter: schema.GroupVersionKind{}, + name: "unmanaged custom resources get filtered out", + gvkTarget: schema.GroupVersionKind{}, clusterCRDs: []string{ unmanagedCRD(k8sObjectGVK), managedAndServedCRD(k8sObjectCollectionGVK), @@ -182,8 +182,8 @@ func TestGetManagedResources(t *testing.T) { ), }, { - name: "unserved custom resources are not retrievable", - filter: schema.GroupVersionKind{}, + name: "unserved custom resources are not retrievable", + gvkTarget: schema.GroupVersionKind{}, clusterCRDs: []string{ unservedCRD(k8sObjectGVK), managedAndServedCRD(k8sObjectCollectionGVK), @@ -211,9 +211,11 @@ func TestGetManagedResources(t *testing.T) { dCli: setupFakeDynamicClient(t, tt.clusterResources), metric: v1alpha1.ManagedMetric{ Spec: v1alpha1.ManagedMetricSpec{ - Kind: tt.filter.Kind, - Group: tt.filter.Group, - Version: tt.filter.Version, + Target: &v1alpha1.GroupVersionKind{ + Group: tt.gvkTarget.Group, + Version: tt.gvkTarget.Version, + Kind: tt.gvkTarget.Kind, + }, }, }, }