Skip to content
4 changes: 3 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]()
Expand Down
41 changes: 37 additions & 4 deletions pkg/multicluster/routers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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.
128 changes: 120 additions & 8 deletions pkg/multicluster/routers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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,
},
{
Expand All @@ -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"},
Expand All @@ -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 {
Expand Down
Loading