Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 116 additions & 17 deletions pkg/controllers/namespacesync/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,24 +85,40 @@ func TestReconcileNewClusterClass(t *testing.T) {
targetNamespaces, err := createTargetNamespaces(3)
g.Expect(err).ToNot(HaveOccurred())

sourceClusterClassName, _, cleanup, err := createUniqueClusterClassAndTemplates(
sourceClusterClassNamespace,
)
g.Expect(err).ToNot(HaveOccurred())
defer func() {
g.Expect(cleanup()).To(Succeed())
}()

for _, targetNamespace := range targetNamespaces {
g.Eventually(func() error {
return verifyClusterClassAndTemplates(
env.Client,
sourceClusterClassName,
targetNamespace.Name,
)
testCases := []struct {
name string
create func(namespace string) (string, []client.Object, func() error, error)
}{
{
name: "cluster class",
create: createUniqueClusterClassAndTemplates,
},
timeout,
).Should(Succeed())
{
name: "managed cluster class",
create: createUniqueManagedClusterClassAndTemplates,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
g := NewWithT(t)
sourceClusterClassName, _, cleanup, err := tc.create(sourceClusterClassNamespace)
g.Expect(err).ToNot(HaveOccurred())
defer func() {
g.Expect(cleanup()).To(Succeed())
}()
for _, targetNamespace := range targetNamespaces {
g.Eventually(func() error {
return verifyClusterClassAndTemplates(
env.Client,
sourceClusterClassName,
targetNamespace.Name,
)
},
timeout,
).Should(Succeed())
}
})
}
}

Expand Down Expand Up @@ -292,6 +308,89 @@ func createClusterClassAndTemplates(
return clusterClass.Name, templates, cleanup, nil
}

func createUniqueManagedClusterClassAndTemplates(namespace string) (
clusterClassName string,
templates []client.Object,
cleanup func() error,
err error,
) {
return createManagedClusterClassAndTemplates(
names.SimpleNameGenerator.GenerateName("test-managed-"),
namespace,
)
}

// createManagedClusterClassAndTemplates creates a ClusterClass with a ControlPlane that does not have a MachineInfrastructure reference.
func createManagedClusterClassAndTemplates(
prefix,
namespace string,
) (
clusterClassName string,
templates []client.Object,
cleanup func() error,
err error,
) {
// The below objects are created in order to feed the reconcile loop all the information it needs to create a
// full tree of ClusterClass objects (the objects should have owner references to the ClusterClass).

// Bootstrap templates for the workers.
bootstrapTemplate := builder.BootstrapTemplate(namespace, prefix).Build()

// InfraMachineTemplates for the workers
infraMachineTemplateWorker := builder.InfrastructureMachineTemplate(
namespace,
fmt.Sprintf("%s-worker", prefix),
).Build()

// Control plane template.
controlPlaneTemplate := builder.ControlPlaneTemplate(namespace, prefix).Build()

// InfraClusterTemplate.
infraClusterTemplate := builder.InfrastructureClusterTemplate(namespace, prefix).Build()

// MachineDeploymentClasses that will be part of the ClusterClass.
machineDeploymentClass := builder.MachineDeploymentClass(fmt.Sprintf("%s-worker", prefix)).
WithBootstrapTemplate(bootstrapTemplate).
WithInfrastructureTemplate(infraMachineTemplateWorker).
Build()

// ClusterClass.
clusterClass := builder.ClusterClass(namespace, prefix).
WithInfrastructureClusterTemplate(infraClusterTemplate).
WithControlPlaneTemplate(controlPlaneTemplate).
WithWorkerMachineDeploymentClasses(*machineDeploymentClass).
Build()

// Create the set of initObjects from the objects above to add to the API server when the test environment starts.

templates = []client.Object{
bootstrapTemplate,
infraMachineTemplateWorker,
controlPlaneTemplate,
infraClusterTemplate,
}

for _, obj := range templates {
if err := env.CreateAndWait(ctx, obj); err != nil {
return "", nil, nil, err
}
}
if err := env.CreateAndWait(ctx, clusterClass); err != nil {
return "", nil, nil, err
}

cleanup = func() error {
for _, obj := range templates {
if err := env.CleanupAndWait(ctx, obj); err != nil {
return err
}
}
return env.CleanupAndWait(ctx, clusterClass)
}

return clusterClass.Name, templates, cleanup, nil
}

func createTargetNamespaces(number int) ([]*corev1.Namespace, error) {
targetNamespaces := []*corev1.Namespace{}
for i := 0; i < number; i++ {
Expand Down
36 changes: 24 additions & 12 deletions pkg/controllers/namespacesync/references.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,24 +57,36 @@ func walkReferences(
ref *corev1.ObjectReference,
) error,
) error {
for _, ref := range []*corev1.ObjectReference{
cc.Spec.Infrastructure.Ref,
cc.Spec.ControlPlane.Ref,
cc.Spec.ControlPlane.MachineInfrastructure.Ref,
} {
if err := fn(ctx, ref); err != nil {
if cc == nil {
return nil
}
if cc.Spec.Infrastructure.Ref != nil {
if err := fn(ctx, cc.Spec.Infrastructure.Ref); err != nil {
return err
}
}

if cc.Spec.ControlPlane.Ref != nil {
if err := fn(ctx, cc.Spec.ControlPlane.Ref); err != nil {
return err
}
}

if cpInfra := cc.Spec.ControlPlane.MachineInfrastructure; cpInfra != nil && cpInfra.Ref != nil {
if err := fn(ctx, cpInfra.Ref); err != nil {
return err
}
}

for mdIdx := range cc.Spec.Workers.MachineDeployments {
md := &cc.Spec.Workers.MachineDeployments[mdIdx]

for _, ref := range []*corev1.ObjectReference{
md.Template.Infrastructure.Ref,
md.Template.Bootstrap.Ref,
} {
if err := fn(ctx, ref); err != nil {
if md.Template.Infrastructure.Ref != nil {
if err := fn(ctx, md.Template.Infrastructure.Ref); err != nil {
return err
}
}
if md.Template.Bootstrap.Ref != nil {
if err := fn(ctx, md.Template.Bootstrap.Ref); err != nil {
return err
}
}
Expand Down
179 changes: 179 additions & 0 deletions pkg/controllers/namespacesync/references_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Copyright 2024 Nutanix. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package namespacesync

import (
"context"
"testing"

. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"

"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/internal/test/builder"
)

func TestWalkReferences(t *testing.T) {
tests := []struct {
name string
clusterClass *clusterv1.ClusterClass
}{
{
name: "nil ClusterClass should return nil without calling callback",
clusterClass: nil,
},
{
name: "empty ClusterClass with no template references",
clusterClass: builder.ClusterClass("default", "test-cc").Build(),
},
{
name: "ClusterClass with Infrastructure cluster template reference only",
clusterClass: builder.ClusterClass("default", "test-cc").
WithInfrastructureClusterTemplate(
builder.InfrastructureClusterTemplate("default", "infra-template").Build(),
).Build(),
},
{
name: "ClusterClass with ControlPlane template reference only",
clusterClass: builder.ClusterClass("default", "test-cc").
WithControlPlaneTemplate(
builder.ControlPlaneTemplate("default", "cp-template").Build(),
).Build(),
},
{
name: "ClusterClass with MachineInfrastructure template reference",
clusterClass: builder.ClusterClass("default", "test-cc").
WithControlPlaneInfrastructureMachineTemplate(
builder.InfrastructureMachineTemplate("default", "cp-machine-template").Build(),
).Build(),
},
{
name: "ClusterClass with MachineDeployments template references",
clusterClass: builder.ClusterClass("default", "test-cc").
WithWorkerMachineDeploymentClasses(
*builder.MachineDeploymentClass("worker-1").
WithInfrastructureTemplate(
builder.InfrastructureMachineTemplate("default", "worker-infra-template").Build(),
).
WithBootstrapTemplate(
builder.BootstrapTemplate("default", "worker-bootstrap-template").Build(),
).Build(),
).Build(),
},
{
name: "ClusterClass with MachineDeployments having nil Infrastructure template reference",
clusterClass: builder.ClusterClass("default", "test-cc").
WithWorkerMachineDeploymentClasses(
*builder.MachineDeploymentClass("worker-1").
WithBootstrapTemplate(
builder.BootstrapTemplate("default", "worker-bootstrap-template").Build(),
).Build(),
).Build(),
},
{
name: "ClusterClass with MachineDeployments having nil Bootstrap template reference",
clusterClass: builder.ClusterClass("default", "test-cc").
WithWorkerMachineDeploymentClasses(
*builder.MachineDeploymentClass("worker-1").
WithInfrastructureTemplate(
builder.InfrastructureMachineTemplate("default", "worker-infra-template").Build(),
).Build(),
).Build(),
},
{
name: "callback function returns error",
clusterClass: builder.ClusterClass("default", "test-cc").
WithInfrastructureClusterTemplate(
builder.InfrastructureClusterTemplate("default", "infra-template").Build(),
).Build(),
},
{
name: "complete ClusterClass with all template references",
clusterClass: builder.ClusterClass("default", "test-cc").
WithInfrastructureClusterTemplate(
builder.InfrastructureClusterTemplate("default", "infra-template").Build(),
).
WithControlPlaneTemplate(
builder.ControlPlaneTemplate("default", "cp-template").Build(),
).
WithControlPlaneInfrastructureMachineTemplate(
builder.InfrastructureMachineTemplate("default", "cp-machine-template").Build(),
).
WithWorkerMachineDeploymentClasses(
*builder.MachineDeploymentClass("worker-1").
WithInfrastructureTemplate(
builder.InfrastructureMachineTemplate("default", "worker-infra-template").Build(),
).
WithBootstrapTemplate(
builder.BootstrapTemplate("default", "worker-bootstrap-template").Build(),
).Build(),
*builder.MachineDeploymentClass("worker-2").
WithInfrastructureTemplate(
builder.InfrastructureMachineTemplate("default", "worker2-infra-template").Build(),
).
WithBootstrapTemplate(
builder.BootstrapTemplate("default", "worker2-bootstrap-template").Build(),
).Build(),
).Build(),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
g := NewWithT(t)
ctx := context.Background()
var capturedRefs []*corev1.ObjectReference

callback := func(ctx context.Context, ref *corev1.ObjectReference) error {
capturedRefs = append(capturedRefs, ref)
return nil
}

err := walkReferences(ctx, tt.clusterClass, callback)

g.Expect(err).ToNot(HaveOccurred())

// Verify that captured references match expected ones
if tt.clusterClass != nil {
expectedRefs := collectExpectedRefs(tt.clusterClass)
g.Expect(capturedRefs).
To(HaveLen(len(expectedRefs)), "Expected %d references, got %d", len(expectedRefs), len(capturedRefs))

for i, expectedRef := range expectedRefs {
if expectedRef != nil {
g.Expect(capturedRefs[i]).To(Equal(expectedRef), "Reference doesn't match")
}
}
}
})
}
}

func collectExpectedRefs(cc *clusterv1.ClusterClass) []*corev1.ObjectReference {
var refs []*corev1.ObjectReference

if cc.Spec.Infrastructure.Ref != nil {
refs = append(refs, cc.Spec.Infrastructure.Ref)
}

if cc.Spec.ControlPlane.Ref != nil {
refs = append(refs, cc.Spec.ControlPlane.Ref)
}

if cpInfra := cc.Spec.ControlPlane.MachineInfrastructure; cpInfra != nil && cpInfra.Ref != nil {
refs = append(refs, cpInfra.Ref)
}

for _, md := range cc.Spec.Workers.MachineDeployments {
if md.Template.Infrastructure.Ref != nil {
refs = append(refs, md.Template.Infrastructure.Ref)
}
if md.Template.Bootstrap.Ref != nil {
refs = append(refs, md.Template.Bootstrap.Ref)
}
}

return refs
}
Loading