From a25eaada88d3244fd751e346426e2e4bdf3bcdb3 Mon Sep 17 00:00:00 2001 From: Julius Clausnitzer Date: Fri, 8 May 2026 14:23:11 +0200 Subject: [PATCH 1/4] fix routing for crd --- pkg/multicluster/routers.go | 36 ++++++++++-- pkg/multicluster/routers_test.go | 95 ++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 4 deletions(-) diff --git a/pkg/multicluster/routers.go b/pkg/multicluster/routers.go index 16d74dc6f..b1aff76c8 100644 --- a/pkg/multicluster/routers.go +++ b/pkg/multicluster/routers.go @@ -16,10 +16,11 @@ import ( // for the multicluster client that cortex supports by default. This is used to // route resources to the correct cluster in a multicluster setup. var DefaultResourceRouters = map[schema.GroupVersionKind]ResourceRouter{ - {Group: "kvm.cloud.sap", Version: "v1", Kind: "Hypervisor"}: HypervisorResourceRouter{}, - {Group: "cortex.cloud", Version: "v1alpha1", Kind: "Reservation"}: ReservationsResourceRouter{}, - {Group: "cortex.cloud", Version: "v1alpha1", Kind: "History"}: HistoryResourceRouter{}, - {Group: "cortex.cloud", Version: "v1alpha1", Kind: "CommittedResource"}: CommittedResourceRouter{}, + {Group: "kvm.cloud.sap", Version: "v1", Kind: "Hypervisor"}: HypervisorResourceRouter{}, + {Group: "cortex.cloud", Version: "v1alpha1", Kind: "Reservation"}: ReservationsResourceRouter{}, + {Group: "cortex.cloud", Version: "v1alpha1", Kind: "History"}: HistoryResourceRouter{}, + {Group: "cortex.cloud", Version: "v1alpha1", Kind: "CommittedResource"}: CommittedResourceRouter{}, + {Group: "cortex.cloud", Version: "v1alpha1", Kind: "FlavorGroupCapacity"}: FlavorGroupCapacityResourceRouter{}, } // ResourceRouter determines which remote cluster a resource should be written to @@ -111,6 +112,33 @@ func (c CommittedResourceRouter) Match(obj any, labels map[string]string) (bool, return cr.Spec.AvailabilityZone == availabilityZone, nil } +// FlavorGroupCapacityResourceRouter routes flavor group capacity CRDs to clusters based on availability zone. +type FlavorGroupCapacityResourceRouter struct{} + +func (f FlavorGroupCapacityResourceRouter) Match(obj any, labels map[string]string) (bool, error) { + var fgc v1alpha1.FlavorGroupCapacity + + switch v := obj.(type) { + case *v1alpha1.FlavorGroupCapacity: + if v == nil { + return false, errors.New("object is nil") + } + fgc = *v + case v1alpha1.FlavorGroupCapacity: + fgc = v + default: + return false, errors.New("object is not a FlavorGroupCapacity") + } + availabilityZone, ok := labels["availabilityZone"] + if !ok { + return false, errors.New("cluster does not have availabilityZone label") + } + if fgc.Spec.AvailabilityZone == "" { + return false, errors.New("FlavorGroupCapacity does not have availability zone in spec") + } + return fgc.Spec.AvailabilityZone == availabilityZone, nil +} + // HistoryResourceRouter routes histories to clusters based on availability zone. type HistoryResourceRouter struct{} diff --git a/pkg/multicluster/routers_test.go b/pkg/multicluster/routers_test.go index c3e1a0c16..882bf1ba8 100644 --- a/pkg/multicluster/routers_test.go +++ b/pkg/multicluster/routers_test.go @@ -217,6 +217,101 @@ func TestHistoryResourceRouter_Match(t *testing.T) { } } +func TestFlavorGroupCapacityResourceRouter_Match(t *testing.T) { + router := FlavorGroupCapacityResourceRouter{} + + tests := []struct { + name string + obj any + labels map[string]string + wantMatch bool + wantErr bool + }{ + { + name: "matching AZ", + obj: v1alpha1.FlavorGroupCapacity{ + Spec: v1alpha1.FlavorGroupCapacitySpec{ + AvailabilityZone: "qa-de-1b", + }, + }, + labels: map[string]string{"availabilityZone": "qa-de-1b"}, + wantMatch: true, + }, + { + name: "matching AZ pointer", + obj: &v1alpha1.FlavorGroupCapacity{ + Spec: v1alpha1.FlavorGroupCapacitySpec{ + AvailabilityZone: "qa-de-1b", + }, + }, + labels: map[string]string{"availabilityZone": "qa-de-1b"}, + wantMatch: true, + }, + { + name: "non-matching AZ", + obj: v1alpha1.FlavorGroupCapacity{ + Spec: v1alpha1.FlavorGroupCapacitySpec{ + AvailabilityZone: "qa-de-1b", + }, + }, + labels: map[string]string{"availabilityZone": "qa-de-1a"}, + wantMatch: false, + }, + { + name: "not a FlavorGroupCapacity", + obj: "not-a-flavor-group-capacity", + labels: map[string]string{"availabilityZone": "qa-de-1b"}, + wantErr: true, + }, + { + name: "cluster missing availabilityZone label", + obj: v1alpha1.FlavorGroupCapacity{ + Spec: v1alpha1.FlavorGroupCapacitySpec{ + AvailabilityZone: "qa-de-1b", + }, + }, + labels: map[string]string{}, + wantErr: true, + }, + { + name: "FlavorGroupCapacity missing availability zone", + obj: v1alpha1.FlavorGroupCapacity{ + Spec: v1alpha1.FlavorGroupCapacitySpec{}, + }, + labels: map[string]string{"availabilityZone": "qa-de-1b"}, + wantErr: true, + }, + { + name: "typed nil pointer doesn't panic", + obj: (*v1alpha1.FlavorGroupCapacity)(nil), + labels: map[string]string{"availabilityZone": "qa-de-1b"}, + wantErr: true, + }, + { + name: "nil object doesn't panic", + obj: nil, + labels: map[string]string{"availabilityZone": "qa-de-1b"}, + wantErr: true, + wantMatch: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + match, err := router.Match(tt.obj, tt.labels) + if tt.wantErr && err == nil { + t.Fatal("expected error, got nil") + } + if !tt.wantErr && err != nil { + t.Fatalf("unexpected error: %v", err) + } + if match != tt.wantMatch { + t.Errorf("expected match=%v, got %v", tt.wantMatch, match) + } + }) + } +} + func TestReservationsResourceRouter_Match(t *testing.T) { router := ReservationsResourceRouter{} From e375dfe96d6e73e7d27e64349432977321a1b6ca Mon Sep 17 00:00:00 2001 From: Julius Clausnitzer Date: Fri, 8 May 2026 15:51:00 +0200 Subject: [PATCH 2/4] lint --- pkg/multicluster/routers.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/multicluster/routers.go b/pkg/multicluster/routers.go index b1aff76c8..972ef7d5f 100644 --- a/pkg/multicluster/routers.go +++ b/pkg/multicluster/routers.go @@ -16,11 +16,11 @@ import ( // for the multicluster client that cortex supports by default. This is used to // route resources to the correct cluster in a multicluster setup. var DefaultResourceRouters = map[schema.GroupVersionKind]ResourceRouter{ - {Group: "kvm.cloud.sap", Version: "v1", Kind: "Hypervisor"}: HypervisorResourceRouter{}, - {Group: "cortex.cloud", Version: "v1alpha1", Kind: "Reservation"}: ReservationsResourceRouter{}, - {Group: "cortex.cloud", Version: "v1alpha1", Kind: "History"}: HistoryResourceRouter{}, - {Group: "cortex.cloud", Version: "v1alpha1", Kind: "CommittedResource"}: CommittedResourceRouter{}, - {Group: "cortex.cloud", Version: "v1alpha1", Kind: "FlavorGroupCapacity"}: FlavorGroupCapacityResourceRouter{}, + {Group: "kvm.cloud.sap", Version: "v1", Kind: "Hypervisor"}: HypervisorResourceRouter{}, + {Group: "cortex.cloud", Version: "v1alpha1", Kind: "Reservation"}: ReservationsResourceRouter{}, + {Group: "cortex.cloud", Version: "v1alpha1", Kind: "History"}: HistoryResourceRouter{}, + {Group: "cortex.cloud", Version: "v1alpha1", Kind: "CommittedResource"}: CommittedResourceRouter{}, + {Group: "cortex.cloud", Version: "v1alpha1", Kind: "FlavorGroupCapacity"}: FlavorGroupCapacityResourceRouter{}, } // ResourceRouter determines which remote cluster a resource should be written to From dd0cfb2056aad62566186639fcba83ee0f9ac0f5 Mon Sep 17 00:00:00 2001 From: Julius Clausnitzer Date: Fri, 8 May 2026 15:55:11 +0200 Subject: [PATCH 3/4] fix --- pkg/multicluster/routers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/multicluster/routers.go b/pkg/multicluster/routers.go index 972ef7d5f..f2c5ef6a8 100644 --- a/pkg/multicluster/routers.go +++ b/pkg/multicluster/routers.go @@ -134,7 +134,7 @@ func (f FlavorGroupCapacityResourceRouter) Match(obj any, labels map[string]stri return false, errors.New("cluster does not have availabilityZone label") } if fgc.Spec.AvailabilityZone == "" { - return false, errors.New("FlavorGroupCapacity does not have availability zone in spec") + return false, errors.New("flavor group capacity does not have availability zone in spec") } return fgc.Spec.AvailabilityZone == availabilityZone, nil } From 5324a40f7e7eb1893899829c41b3c3106844a49c Mon Sep 17 00:00:00 2001 From: Julius Clausnitzer Date: Fri, 8 May 2026 17:07:26 +0200 Subject: [PATCH 4/4] fix: wire FlavorGroupCapacity router into multicluster client main.go built the ResourceRouters map inline and was missing the FlavorGroupCapacity entry added to DefaultResourceRouters, causing "no ResourceRouter configured" errors in multicluster staging. --- cmd/manager/main.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 6c1096512..d5b0dd5eb 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -343,15 +343,17 @@ func main() { reservationGVK := schema.GroupVersionKind{Group: "cortex.cloud", Version: "v1alpha1", Kind: "Reservation"} historyGVK := schema.GroupVersionKind{Group: "cortex.cloud", Version: "v1alpha1", Kind: "History"} committedResourceGVK := schema.GroupVersionKind{Group: "cortex.cloud", Version: "v1alpha1", Kind: "CommittedResource"} + flavorGroupCapacityGVK := schema.GroupVersionKind{Group: "cortex.cloud", Version: "v1alpha1", Kind: "FlavorGroupCapacity"} multiclusterClient := &multicluster.Client{ HomeCluster: homeCluster, HomeRestConfig: restConfig, HomeScheme: scheme, ResourceRouters: map[schema.GroupVersionKind]multicluster.ResourceRouter{ - hvGVK: multicluster.HypervisorResourceRouter{}, - reservationGVK: multicluster.ReservationsResourceRouter{}, - historyGVK: multicluster.HistoryResourceRouter{}, - committedResourceGVK: multicluster.CommittedResourceRouter{}, + hvGVK: multicluster.HypervisorResourceRouter{}, + reservationGVK: multicluster.ReservationsResourceRouter{}, + historyGVK: multicluster.HistoryResourceRouter{}, + committedResourceGVK: multicluster.CommittedResourceRouter{}, + flavorGroupCapacityGVK: multicluster.FlavorGroupCapacityResourceRouter{}, }, } multiclusterClientConfig := conf.GetConfigOrDie[multicluster.ClientConfig]()