Added ClusterSet as singleton (#316)

* added ClusterSet as singleton

* fix tests
This commit is contained in:
Enrico Candino
2025-04-03 16:26:25 +02:00
committed by GitHub
parent 0843a9e313
commit 90568f24b1
4 changed files with 144 additions and 86 deletions
+14 -1
View File
@@ -14,7 +14,14 @@ spec:
singular: clusterset
scope: Namespaced
versions:
- name: v1alpha1
- additionalPrinterColumns:
- jsonPath: .spec.displayName
name: Display Name
type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: |-
@@ -73,6 +80,9 @@ spec:
description: DisableNetworkPolicy indicates whether to disable the
creation of a default network policy for cluster isolation.
type: boolean
displayName:
description: DisplayName is the human-readable name for the set.
type: string
limit:
description: |-
Limit specifies the LimitRange that will be applied to all pods within the ClusterSet
@@ -312,6 +322,9 @@ spec:
required:
- spec
type: object
x-kubernetes-validations:
- message: Name must match 'default'
rule: self.metadata.name == "default"
served: true
storage: true
subresources:
+8
View File
@@ -313,6 +313,9 @@ type ClusterList struct {
// +kubebuilder:storageversion
// +kubebuilder:subresource:status
// +kubebuilder:object:root=true
// +kubebuilder:validation:XValidation:rule="self.metadata.name == \"default\"",message="Name must match 'default'"
// +kubebuilder:printcolumn:JSONPath=".spec.displayName",name=Display Name,type=string
// +kubebuilder:printcolumn:JSONPath=".metadata.creationTimestamp",name=Age,type=date
// ClusterSet represents a group of virtual Kubernetes clusters managed by k3k.
// It allows defining common configurations and constraints for the clusters within the set.
@@ -334,6 +337,11 @@ type ClusterSet struct {
// ClusterSetSpec defines the desired state of a ClusterSet.
type ClusterSetSpec struct {
// DisplayName is the human-readable name for the set.
//
// +optional
DisplayName string `json:"displayName"`
// Quota specifies the resource limits for clusters within a clusterset.
//
// +optional
+58 -51
View File
@@ -48,6 +48,7 @@ func Add(ctx context.Context, mgr manager.Manager, clusterCIDR string) error {
return ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.ClusterSet{}).
Owns(&networkingv1.NetworkPolicy{}).
Owns(&v1.ResourceQuota{}).
WithOptions(controller.Options{
MaxConcurrentReconciles: maxConcurrentReconciles,
}).
@@ -58,54 +59,33 @@ func Add(ctx context.Context, mgr manager.Manager, clusterCIDR string) error {
).
Watches(
&v1alpha1.Cluster{},
handler.EnqueueRequestsFromMapFunc(sameNamespaceEventHandler(reconciler)),
handler.EnqueueRequestsFromMapFunc(namespaceEventHandler(reconciler)),
).
Complete(&reconciler)
}
// namespaceEventHandler will enqueue reconciling requests for all the ClusterSets in the changed namespace
// namespaceEventHandler will enqueue a reconcile request for the ClusterSet in the given namespace
func namespaceEventHandler(reconciler ClusterSetReconciler) handler.MapFunc {
return func(ctx context.Context, obj client.Object) []reconcile.Request {
var (
requests []reconcile.Request
set v1alpha1.ClusterSetList
)
// if the object is a Namespace, use the name as the namespace
namespace := obj.GetName()
_ = reconciler.Client.List(ctx, &set, client.InNamespace(obj.GetName()))
for _, clusterSet := range set.Items {
requests = append(requests, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: clusterSet.Name,
Namespace: obj.GetName(),
},
})
// if the object is a namespaced resource, use the namespace
if obj.GetNamespace() != "" {
namespace = obj.GetNamespace()
}
return requests
}
}
// sameNamespaceEventHandler will enqueue reconciling requests for all the ClusterSets in the changed namespace
func sameNamespaceEventHandler(reconciler ClusterSetReconciler) handler.MapFunc {
return func(ctx context.Context, obj client.Object) []reconcile.Request {
var (
requests []reconcile.Request
set v1alpha1.ClusterSetList
)
_ = reconciler.Client.List(ctx, &set, client.InNamespace(obj.GetNamespace()))
for _, clusterSet := range set.Items {
requests = append(requests, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: clusterSet.Name,
Namespace: obj.GetNamespace(),
},
})
key := types.NamespacedName{
Name: "default",
Namespace: namespace,
}
return requests
var clusterSet v1alpha1.ClusterSet
if err := reconciler.Client.Get(ctx, key, &clusterSet); err != nil {
return nil
}
return []reconcile.Request{{NamespacedName: key}}
}
}
@@ -130,29 +110,56 @@ func (c *ClusterSetReconciler) Reconcile(ctx context.Context, req reconcile.Requ
return reconcile.Result{}, client.IgnoreNotFound(err)
}
if err := c.reconcileNetworkPolicy(ctx, &clusterSet); err != nil {
return reconcile.Result{}, err
orig := clusterSet.DeepCopy()
reconcilerErr := c.reconcileClusterSet(ctx, &clusterSet)
// update Status if needed
if !reflect.DeepEqual(orig.Status, clusterSet.Status) {
if err := c.Client.Status().Update(ctx, &clusterSet); err != nil {
return reconcile.Result{}, err
}
}
if err := c.reconcileNamespacePodSecurityLabels(ctx, &clusterSet); err != nil {
return reconcile.Result{}, err
// if there was an error during the reconciliation, return
if reconcilerErr != nil {
return reconcile.Result{}, reconcilerErr
}
if err := c.reconcileClusters(ctx, &clusterSet); err != nil {
return reconcile.Result{}, err
}
if err := c.reconcileLimit(ctx, &clusterSet); err != nil {
return reconcile.Result{}, err
}
if err := c.reconcileQuota(ctx, &clusterSet); err != nil {
return reconcile.Result{}, err
// update ClusterSet if needed
if !reflect.DeepEqual(orig.Spec, clusterSet.Spec) {
if err := c.Client.Update(ctx, &clusterSet); err != nil {
return reconcile.Result{}, err
}
}
return reconcile.Result{}, nil
}
func (c *ClusterSetReconciler) reconcileClusterSet(ctx context.Context, clusterSet *v1alpha1.ClusterSet) error {
if err := c.reconcileNetworkPolicy(ctx, clusterSet); err != nil {
return err
}
if err := c.reconcileNamespacePodSecurityLabels(ctx, clusterSet); err != nil {
return err
}
if err := c.reconcileLimit(ctx, clusterSet); err != nil {
return err
}
if err := c.reconcileQuota(ctx, clusterSet); err != nil {
return err
}
if err := c.reconcileClusters(ctx, clusterSet); err != nil {
return err
}
return nil
}
func (c *ClusterSetReconciler) reconcileNetworkPolicy(ctx context.Context, clusterSet *v1alpha1.ClusterSet) error {
log := ctrl.LoggerFrom(ctx)
log.Info("reconciling NetworkPolicy")
+64 -34
View File
@@ -39,8 +39,8 @@ var _ = Describe("ClusterSet Controller", Label("controller"), Label("ClusterSet
It("should have only the 'shared' allowedModeTypes", func() {
clusterSet := &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "clusterset-",
Namespace: namespace,
Name: "default",
Namespace: namespace,
},
}
@@ -52,11 +52,39 @@ var _ = Describe("ClusterSet Controller", Label("controller"), Label("ClusterSet
Expect(allowedModeTypes).To(ContainElement(v1alpha1.SharedClusterMode))
})
It("should not be able to create a cluster with a non 'default' name", func() {
err := k8sClient.Create(ctx, &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
Name: "another-name",
Namespace: namespace,
},
})
Expect(err).To(HaveOccurred())
})
It("should not be able to create two ClusterSets in the same namespace", func() {
err := k8sClient.Create(ctx, &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: namespace,
},
})
Expect(err).To(Not(HaveOccurred()))
err = k8sClient.Create(ctx, &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
Name: "default-2",
Namespace: namespace,
},
})
Expect(err).To(HaveOccurred())
})
It("should create a NetworkPolicy", func() {
clusterSet := &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "clusterset-",
Namespace: namespace,
Name: "default",
Namespace: namespace,
},
}
@@ -119,8 +147,8 @@ var _ = Describe("ClusterSet Controller", Label("controller"), Label("ClusterSet
It("should not create a NetworkPolicy if true", func() {
clusterSet := &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "clusterset-",
Namespace: namespace,
Name: "default",
Namespace: namespace,
},
Spec: v1alpha1.ClusterSetSpec{
DisableNetworkPolicy: true,
@@ -148,8 +176,8 @@ var _ = Describe("ClusterSet Controller", Label("controller"), Label("ClusterSet
It("should delete the NetworkPolicy if changed to false", func() {
clusterSet := &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "clusterset-",
Namespace: namespace,
Name: "default",
Namespace: namespace,
},
}
@@ -192,8 +220,8 @@ var _ = Describe("ClusterSet Controller", Label("controller"), Label("ClusterSet
It("should recreate the NetworkPolicy if deleted", func() {
clusterSet := &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "clusterset-",
Namespace: namespace,
Name: "default",
Namespace: namespace,
},
}
@@ -243,8 +271,8 @@ var _ = Describe("ClusterSet Controller", Label("controller"), Label("ClusterSet
It("should have the 'virtual' mode if specified", func() {
clusterSet := &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "clusterset-",
Namespace: namespace,
Name: "default",
Namespace: namespace,
},
Spec: v1alpha1.ClusterSetSpec{
AllowedModeTypes: []v1alpha1.ClusterMode{
@@ -264,8 +292,8 @@ var _ = Describe("ClusterSet Controller", Label("controller"), Label("ClusterSet
It("should have both modes if specified", func() {
clusterSet := &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "clusterset-",
Namespace: namespace,
Name: "default",
Namespace: namespace,
},
Spec: v1alpha1.ClusterSetSpec{
AllowedModeTypes: []v1alpha1.ClusterMode{
@@ -289,8 +317,8 @@ var _ = Describe("ClusterSet Controller", Label("controller"), Label("ClusterSet
It("should fail for a non-existing mode", func() {
clusterSet := &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "clusterset-",
Namespace: namespace,
Name: "default",
Namespace: namespace,
},
Spec: v1alpha1.ClusterSetSpec{
AllowedModeTypes: []v1alpha1.ClusterMode{
@@ -316,8 +344,8 @@ var _ = Describe("ClusterSet Controller", Label("controller"), Label("ClusterSet
clusterSet := &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "clusterset-",
Namespace: namespace,
Name: "default",
Namespace: namespace,
},
Spec: v1alpha1.ClusterSetSpec{
PodSecurityAdmissionLevel: &privileged,
@@ -419,8 +447,8 @@ var _ = Describe("ClusterSet Controller", Label("controller"), Label("ClusterSet
clusterSet := &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "clusterset-",
Namespace: namespace,
Name: "default",
Namespace: namespace,
},
Spec: v1alpha1.ClusterSetSpec{
PodSecurityAdmissionLevel: &privileged,
@@ -470,8 +498,8 @@ var _ = Describe("ClusterSet Controller", Label("controller"), Label("ClusterSet
It("should update it if needed", func() {
clusterSet := &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "clusterset-",
Namespace: namespace,
Name: "default",
Namespace: namespace,
},
Spec: v1alpha1.ClusterSetSpec{
DefaultPriorityClass: "foobar",
@@ -511,8 +539,8 @@ var _ = Describe("ClusterSet Controller", Label("controller"), Label("ClusterSet
It("should update the nodeSelector", func() {
clusterSet := &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "clusterset-",
Namespace: namespace,
Name: "default",
Namespace: namespace,
},
Spec: v1alpha1.ClusterSetSpec{
DefaultNodeSelector: map[string]string{"label-1": "value-1"},
@@ -552,8 +580,8 @@ var _ = Describe("ClusterSet Controller", Label("controller"), Label("ClusterSet
It("should update the nodeSelector if changed", func() {
clusterSet := &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "clusterset-",
Namespace: namespace,
Name: "default",
Namespace: namespace,
},
Spec: v1alpha1.ClusterSetSpec{
DefaultNodeSelector: map[string]string{"label-1": "value-1"},
@@ -623,8 +651,8 @@ var _ = Describe("ClusterSet Controller", Label("controller"), Label("ClusterSet
It("should not be update", func() {
clusterSet := &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "clusterset-",
Namespace: namespace,
Name: "default",
Namespace: namespace,
},
Spec: v1alpha1.ClusterSetSpec{
DefaultPriorityClass: "foobar",
@@ -671,8 +699,8 @@ var _ = Describe("ClusterSet Controller", Label("controller"), Label("ClusterSet
It("should create resourceQuota if Quota is enabled", func() {
clusterSet := &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "clusterset-",
Namespace: namespace,
Name: "default",
Namespace: namespace,
},
Spec: v1alpha1.ClusterSetSpec{
Quota: &v1.ResourceQuotaSpec{
@@ -702,11 +730,12 @@ var _ = Describe("ClusterSet Controller", Label("controller"), Label("ClusterSet
Expect(resourceQuota.Spec.Hard.Cpu().String()).To(BeEquivalentTo("800m"))
Expect(resourceQuota.Spec.Hard.Memory().String()).To(BeEquivalentTo("1Gi"))
})
It("should delete the ResourceQuota if Quota is deleted", func() {
clusterSet := &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "clusterset-",
Namespace: namespace,
Name: "default",
Namespace: namespace,
},
Spec: v1alpha1.ClusterSetSpec{
Quota: &v1.ResourceQuotaSpec{
@@ -751,11 +780,12 @@ var _ = Describe("ClusterSet Controller", Label("controller"), Label("ClusterSet
WithPolling(time.Second).
Should(BeTrue())
})
It("should create resourceQuota if Quota is enabled", func() {
clusterSet := &v1alpha1.ClusterSet{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "clusterset-",
Namespace: namespace,
Name: "default",
Namespace: namespace,
},
Spec: v1alpha1.ClusterSetSpec{
Limit: &v1.LimitRangeSpec{