diff --git a/cmd/main.go b/cmd/main.go index f8188f93a..389c980f7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -275,12 +275,14 @@ func main() { os.Exit(1) } hvGVK := schema.GroupVersionKind{Group: "kvm.cloud.sap", Version: "v1", Kind: "Hypervisor"} + reservationGVK := schema.GroupVersionKind{Group: "cortex.cloud", Version: "v1alpha1", Kind: "Reservation"} multiclusterClient := &multicluster.Client{ HomeCluster: homeCluster, HomeRestConfig: restConfig, HomeScheme: scheme, ResourceRouters: map[schema.GroupVersionKind]multicluster.ResourceRouter{ - hvGVK: multicluster.HypervisorResourceRouter{}, + hvGVK: multicluster.HypervisorResourceRouter{}, + reservationGVK: multicluster.ReservationsResourceRouter{}, }, } multiclusterClientConfig := conf.GetConfigOrDie[multicluster.ClientConfig]() diff --git a/pkg/multicluster/routers.go b/pkg/multicluster/routers.go index 1e0496791..c5b9036ad 100644 --- a/pkg/multicluster/routers.go +++ b/pkg/multicluster/routers.go @@ -6,6 +6,7 @@ package multicluster import ( "errors" + "github.com/cobaltcore-dev/cortex/api/v1alpha1" hv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" corev1 "k8s.io/api/core/v1" ) @@ -21,23 +22,55 @@ type HypervisorResourceRouter struct{} func (h HypervisorResourceRouter) Match(obj any, labels map[string]string) (bool, error) { var hv hv1.Hypervisor + switch v := obj.(type) { case *hv1.Hypervisor: + if v == nil { + return false, errors.New("object is nil") + } hv = *v case hv1.Hypervisor: hv = v default: return false, errors.New("object is not a Hypervisor") } - az, ok := labels["az"] + availabilityZone, ok := labels["availabilityZone"] if !ok { - return false, errors.New("cluster does not have availability zone label") + return false, errors.New("cluster does not have availabilityZone label") } - hvAZ, ok := hv.Labels[corev1.LabelTopologyZone] + hvAvailabilityZone, ok := hv.Labels[corev1.LabelTopologyZone] if !ok { return false, errors.New("hypervisor does not have availability zone label") } - return hvAZ == az, nil + return hvAvailabilityZone == availabilityZone, nil +} + +// ReservationsResourceRouter routes reservations to clusters based on availability zone. +type ReservationsResourceRouter struct{} + +func (r ReservationsResourceRouter) Match(obj any, labels map[string]string) (bool, error) { + var res v1alpha1.Reservation + + switch v := obj.(type) { + case *v1alpha1.Reservation: + if v == nil { + return false, errors.New("object is nil") + } + res = *v + case v1alpha1.Reservation: + res = v + default: + return false, errors.New("object is not a Reservation") + } + availabilityZone, ok := labels["availabilityZone"] + if !ok { + return false, errors.New("cluster does not have availability zone label") + } + reservationAvailabilityZone := res.Spec.AvailabilityZone + if reservationAvailabilityZone == "" { + return false, errors.New("reservation does not have availability zone in spec") + } + return reservationAvailabilityZone == availabilityZone, nil } // TODO: Add router for Decision CRD and reservations after their refactoring is done. diff --git a/pkg/multicluster/routers_test.go b/pkg/multicluster/routers_test.go index d9598b767..06b7a5d5c 100644 --- a/pkg/multicluster/routers_test.go +++ b/pkg/multicluster/routers_test.go @@ -6,6 +6,8 @@ package multicluster import ( "testing" + "github.com/cobaltcore-dev/cortex/api/v1alpha1" + testlib "github.com/cobaltcore-dev/cortex/pkg/testing" hv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -27,17 +29,17 @@ func TestHypervisorResourceRouter_Match(t *testing.T) { Labels: map[string]string{"topology.kubernetes.io/zone": "qa-de-1a"}, }, }, - labels: map[string]string{"az": "qa-de-1a"}, + labels: map[string]string{"availabilityZone": "qa-de-1a"}, wantMatch: true, }, { name: "matching AZ pointer", - obj: &hv1.Hypervisor{ + obj: testlib.Ptr(hv1.Hypervisor{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"topology.kubernetes.io/zone": "qa-de-1a"}, }, - }, - labels: map[string]string{"az": "qa-de-1a"}, + }), + labels: map[string]string{"availabilityZone": "qa-de-1a"}, wantMatch: true, }, { @@ -47,17 +49,17 @@ func TestHypervisorResourceRouter_Match(t *testing.T) { Labels: map[string]string{"topology.kubernetes.io/zone": "qa-de-1a"}, }, }, - labels: map[string]string{"az": "qa-de-1b"}, + labels: map[string]string{"availabilityZone": "qa-de-1b"}, wantMatch: false, }, { name: "not a Hypervisor", obj: "not-a-hypervisor", - labels: map[string]string{"az": "qa-de-1a"}, + labels: map[string]string{"availabilityZone": "qa-de-1a"}, wantErr: true, }, { - name: "cluster missing az label", + name: "cluster missing availabilityZone label", obj: hv1.Hypervisor{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"topology.kubernetes.io/zone": "qa-de-1a"}, @@ -73,9 +75,119 @@ func TestHypervisorResourceRouter_Match(t *testing.T) { Labels: map[string]string{}, }, }, - labels: map[string]string{"az": "qa-de-1a"}, + labels: map[string]string{"availabilityZone": "qa-de-1a"}, + wantErr: true, + }, + { + name: "typed nil pointer doesn't panic", + obj: (*hv1.Hypervisor)(nil), + labels: map[string]string{"availabilityZone": "qa-de-1a"}, + wantErr: true, + }, + { + name: "nil object doesn't panic", + obj: nil, + labels: map[string]string{"availabilityZone": "qa-de-1a"}, + 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{} + + tests := []struct { + name string + obj any + labels map[string]string + wantMatch bool + wantErr bool + }{ + { + name: "matching AZ", + obj: v1alpha1.Reservation{ + Spec: v1alpha1.ReservationSpec{ + AvailabilityZone: "qa-de-1a", + }, + }, + labels: map[string]string{"availabilityZone": "qa-de-1a"}, + wantMatch: true, + }, + { + name: "matching AZ pointer", + obj: testlib.Ptr(v1alpha1.Reservation{ + Spec: v1alpha1.ReservationSpec{ + AvailabilityZone: "qa-de-1a", + }, + }), + labels: map[string]string{"availabilityZone": "qa-de-1a"}, + wantMatch: true, + }, + { + name: "non-matching AZ", + obj: v1alpha1.Reservation{ + Spec: v1alpha1.ReservationSpec{ + AvailabilityZone: "qa-de-1a", + }, + }, + labels: map[string]string{"availabilityZone": "qa-de-1b"}, + wantMatch: false, + }, + { + name: "not a Reservation", + obj: "not-a-reservation", + labels: map[string]string{"availabilityZone": "qa-de-1a"}, wantErr: true, }, + { + name: "cluster missing availabilityZone label", + obj: v1alpha1.Reservation{ + Spec: v1alpha1.ReservationSpec{ + AvailabilityZone: "qa-de-1a", + }, + }, + labels: map[string]string{}, + wantErr: true, + }, + { + name: "reservation missing availability zone", + obj: v1alpha1.Reservation{ + Spec: v1alpha1.ReservationSpec{ + AvailabilityZone: "", + }, + }, + labels: map[string]string{"availabilityZone": "qa-de-1a"}, + wantErr: true, + }, + { + name: "typed nil pointer doesn't panic", + obj: (*v1alpha1.Reservation)(nil), + labels: map[string]string{"availabilityZone": "qa-de-1a"}, + wantErr: true, + }, + { + name: "nil object doesn't panic", + obj: nil, + labels: map[string]string{"availabilityZone": "qa-de-1a"}, + wantErr: true, + wantMatch: false, + }, } for _, tt := range tests {