diff --git a/pkg/multicluster/routers.go b/pkg/multicluster/routers.go index 16d74dc6f..f2c5ef6a8 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("flavor group capacity 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{}