-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Controller that copies ClusterClasses to namespaces
- Loading branch information
1 parent
6f0f61e
commit 7d065bf
Showing
7 changed files
with
677 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
// Copyright 2024 Nutanix. All rights reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package namespacesync | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
corev1 "k8s.io/api/core/v1" | ||
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" | ||
ctrl "sigs.k8s.io/controller-runtime" | ||
"sigs.k8s.io/controller-runtime/pkg/builder" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
"sigs.k8s.io/controller-runtime/pkg/controller" | ||
"sigs.k8s.io/controller-runtime/pkg/event" | ||
"sigs.k8s.io/controller-runtime/pkg/handler" | ||
"sigs.k8s.io/controller-runtime/pkg/predicate" | ||
) | ||
|
||
type Reconciler struct { | ||
Client client.Client | ||
|
||
// UnstructuredCachingClient provides a client that forces caching of unstructured objects, | ||
// optimizing reads of provider-specific resources. | ||
UnstructuredCachingClient client.Client | ||
|
||
// SourceClusterClassNamespace is the namespace from which ClusterClasses are copied. | ||
SourceClusterClassNamespace string | ||
|
||
// TargetNamespaceFilter determines whether ClusterClasses should be copied to a given namespace. | ||
TargetNamespaceFilter func(ns *corev1.Namespace) bool | ||
} | ||
|
||
var NamespaceHasLabelKey = func(key string) func(ns *corev1.Namespace) bool { | ||
return func(ns *corev1.Namespace) bool { | ||
_, ok := ns.GetLabels()[key] | ||
return ok | ||
} | ||
} | ||
|
||
func (r *Reconciler) SetupWithManager( | ||
ctx context.Context, | ||
mgr ctrl.Manager, | ||
options controller.Options, | ||
) error { | ||
if r.TargetNamespaceFilter == nil { | ||
return fmt.Errorf("target Namespace filter is nil") | ||
} | ||
|
||
err := ctrl.NewControllerManagedBy(mgr). | ||
For(&corev1.Namespace{}, | ||
builder.WithPredicates( | ||
predicate.Funcs{ | ||
CreateFunc: func(e event.CreateEvent) bool { | ||
// Called when an object is first seen by the cache, i.e. when a new object is created, | ||
// or when the cache is populated on start. | ||
ns, ok := e.Object.(*corev1.Namespace) | ||
if !ok { | ||
return false | ||
} | ||
return r.TargetNamespaceFilter(ns) | ||
}, | ||
UpdateFunc: func(e event.UpdateEvent) bool { | ||
// Called when an object is already in the cache, and it is either updated, | ||
// or fetched as part of a re-list (aka re-sync). | ||
return false | ||
}, | ||
DeleteFunc: func(e event.DeleteEvent) bool { | ||
// Ignore deletes. | ||
return false | ||
}, | ||
GenericFunc: func(e event.GenericEvent) bool { | ||
// Ignore generic events, i.e. events that don't come from the API server. | ||
return false | ||
}, | ||
}, | ||
)). | ||
Watches(&clusterv1.ClusterClass{}, | ||
handler.EnqueueRequestsFromMapFunc(r.clusterClassToNamespaces), | ||
). | ||
Named("syncclusterclass"). | ||
WithOptions(options). | ||
Complete(r) | ||
if err != nil { | ||
return fmt.Errorf("failed to set up with controller manager: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (r *Reconciler) clusterClassToNamespaces(ctx context.Context, o client.Object) []ctrl.Request { | ||
namespaceList := &corev1.NamespaceList{} | ||
err := r.Client.List(ctx, namespaceList) | ||
if err != nil { | ||
// TODO Log the error, and record an Event. | ||
return nil | ||
} | ||
|
||
rs := []ctrl.Request{} | ||
for i := range namespaceList.Items { | ||
ns := &namespaceList.Items[i] | ||
if r.TargetNamespaceFilter(ns) { | ||
rs = append(rs, | ||
ctrl.Request{ | ||
NamespacedName: client.ObjectKeyFromObject(ns), | ||
}, | ||
) | ||
} | ||
} | ||
return rs | ||
} | ||
|
||
func (r *Reconciler) Reconcile( | ||
ctx context.Context, | ||
req ctrl.Request, | ||
) ( | ||
_ ctrl.Result, | ||
rerr error, | ||
) { | ||
namespace := req.Name | ||
|
||
sccs, err := r.listSourceClusterClasses(ctx) | ||
if err != nil { | ||
// TODO Record an Event. | ||
return ctrl.Result{}, fmt.Errorf("failed to list source ClusterClasses: %w", err) | ||
} | ||
|
||
// TODO Consider running in parallel. | ||
for i := range sccs { | ||
scc := &sccs[i] | ||
err := copyClusterClassAndTemplates( | ||
ctx, | ||
r.Client, | ||
r.UnstructuredCachingClient, | ||
scc, | ||
namespace, | ||
) | ||
if client.IgnoreAlreadyExists(err) != nil { | ||
// TODO Record an Event. | ||
return ctrl.Result{}, fmt.Errorf( | ||
"failed to copy source ClusterClass %s or its referenced Templates to namespace %s: %w", | ||
client.ObjectKeyFromObject(scc), | ||
namespace, | ||
err, | ||
) | ||
} | ||
} | ||
|
||
// TODO Record an Event. | ||
return ctrl.Result{}, nil | ||
} | ||
|
||
func (r *Reconciler) listSourceClusterClasses( | ||
ctx context.Context, | ||
) ( | ||
[]clusterv1.ClusterClass, | ||
error, | ||
) { | ||
ccl := &clusterv1.ClusterClassList{} | ||
err := r.Client.List(ctx, ccl, client.InNamespace(r.SourceClusterClassNamespace)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return ccl.Items, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
// Copyright 2024 Nutanix. All rights reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
package namespacesync | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"testing" | ||
"time" | ||
|
||
. "github.com/onsi/gomega" | ||
corev1 "k8s.io/api/core/v1" | ||
"k8s.io/apiserver/pkg/storage/names" | ||
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
|
||
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/internal/test/builder" | ||
) | ||
|
||
func TestReconcileNewNamespaces(t *testing.T) { | ||
g := NewWithT(t) | ||
timeout := 5 * time.Second | ||
|
||
sourceClusterClassName, cleanup, err := createUniqueClusterClassAndTemplates( | ||
sourceClusterClassNamespace, | ||
) | ||
g.Expect(err).ToNot(HaveOccurred()) | ||
defer func() { | ||
g.Expect(cleanup()).To(Succeed()) | ||
}() | ||
|
||
targetNamespaces, err := createTargetNamespaces(3) | ||
g.Expect(err).ToNot(HaveOccurred()) | ||
|
||
for _, targetNamespace := range targetNamespaces { | ||
g.Eventually(func() error { | ||
return verifyClusterClassAndTemplates( | ||
env.Client, | ||
sourceClusterClassName, | ||
targetNamespace.Name, | ||
) | ||
}, | ||
timeout, | ||
).Should(Succeed()) | ||
} | ||
} | ||
|
||
func TestReconcileNewClusterClass(t *testing.T) { | ||
g := NewWithT(t) | ||
timeout := 5 * time.Second | ||
|
||
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, | ||
) | ||
}, | ||
timeout, | ||
).Should(Succeed()) | ||
} | ||
} | ||
|
||
func verifyClusterClassAndTemplates( | ||
cli client.Reader, | ||
name, | ||
namespace string, | ||
) error { | ||
cc := &clusterv1.ClusterClass{} | ||
key := client.ObjectKey{ | ||
Name: name, | ||
Namespace: namespace, | ||
} | ||
err := cli.Get(ctx, key, cc) | ||
if err != nil { | ||
return fmt.Errorf("failed to get ClusterClass %s: %w", key, err) | ||
} | ||
|
||
return walkReferences(ctx, cc, func(ctx context.Context, ref *corev1.ObjectReference) error { | ||
_, err := getReference(ctx, cli, ref) | ||
return err | ||
}) | ||
} | ||
|
||
func createUniqueClusterClassAndTemplates(namespace string) ( | ||
clusterClassName string, | ||
cleanup func() error, | ||
err error, | ||
) { | ||
return createClusterClassAndTemplates( | ||
names.SimpleNameGenerator.GenerateName("test-"), | ||
namespace, | ||
) | ||
} | ||
|
||
func createClusterClassAndTemplates( | ||
prefix, | ||
namespace string, | ||
) ( | ||
clusterClassName string, | ||
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 and the control plane. | ||
infraMachineTemplateControlPlane := builder.InfrastructureMachineTemplate( | ||
namespace, | ||
fmt.Sprintf("%s-control-plane", prefix), | ||
).Build() | ||
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). | ||
WithControlPlaneInfrastructureMachineTemplate(infraMachineTemplateControlPlane). | ||
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, | ||
infraMachineTemplateControlPlane, | ||
controlPlaneTemplate, | ||
infraClusterTemplate, | ||
} | ||
|
||
for _, obj := range templates { | ||
if err := env.CreateAndWait(ctx, obj); err != nil { | ||
return "", nil, err | ||
} | ||
} | ||
if err := env.CreateAndWait(ctx, clusterClass); err != nil { | ||
return "", 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, cleanup, nil | ||
} | ||
|
||
func createTargetNamespaces(number int) ([]*corev1.Namespace, error) { | ||
targetNamespaces := []*corev1.Namespace{} | ||
for i := 0; i < number; i++ { | ||
targetNamespace, err := env.CreateNamespace(ctx, "target", map[string]string{ | ||
targetNamespaceLabelKey: "", | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
targetNamespaces = append(targetNamespaces, targetNamespace) | ||
} | ||
return targetNamespaces, nil | ||
} |
Oops, something went wrong.