diff --git a/cmd/main.go b/cmd/main.go index 211f7c535..c9c5c8e2f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -135,6 +135,13 @@ func main() { os.Exit(1) } + if err = (&controller.ClusterAddonCreateReconciler{ + Client: mgr.GetClient(), + }).SetupWithManager(ctx, mgr, controllerruntimecontroller.Options{}); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ClusterAddonCreate") + os.Exit(1) + } + //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index b8d343183..eeeedf7cc 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -39,6 +39,12 @@ rules: - patch - update - watch +- apiGroups: + - clusterstack.x-k8s.io + resources: + - clusteraddons + verbs: + - create - apiGroups: - clusterstack.x-k8s.io resources: diff --git a/internal/controller/clusteraddoncreate_controller.go b/internal/controller/clusteraddoncreate_controller.go new file mode 100644 index 000000000..9c2dbf102 --- /dev/null +++ b/internal/controller/clusteraddoncreate_controller.go @@ -0,0 +1,119 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + + csov1alpha1 "github.com/SovereignCloudStack/cluster-stack-operator/api/v1alpha1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/util/predicates" + ctrl "sigs.k8s.io/controller-runtime" + "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/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// ClusterAddonCreateReconciler reconciles a Cluster object. +type ClusterAddonCreateReconciler struct { + client.Client + WatchFilterValue string +} + +//+kubebuilder:rbac:groups=clusterstack.x-k8s.io,resources=clusteraddons,verbs=create + +// Reconcile is part of the main Kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +func (r *ClusterAddonCreateReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + cluster := &clusterv1.Cluster{} + if err := r.Get(ctx, req.NamespacedName, cluster); err != nil { + // if the cluster is not found, exit the reconciliation + if apierrors.IsNotFound(err) { + return reconcile.Result{}, nil + } + return reconcile.Result{}, fmt.Errorf("failed to get cluster: %w", err) + } + + clusterAddonName := fmt.Sprintf("cluster-addon-%s", cluster.Name) + + var existingClusterAddon csov1alpha1.ClusterAddon + err := r.Get(ctx, types.NamespacedName{Namespace: cluster.Namespace, Name: clusterAddonName}, &existingClusterAddon) + // no error means that the object already exists - nothing to do + if err == nil { + return reconcile.Result{}, nil + } + // unexpected error - return it + if !apierrors.IsNotFound(err) { + return reconcile.Result{}, fmt.Errorf("failed to get ClusterAddon object: %w", err) + } + + // clusterAddon could not be found - create it + clusterAddon := &csov1alpha1.ClusterAddon{ + ObjectMeta: metav1.ObjectMeta{Name: clusterAddonName, Namespace: cluster.Namespace}, + TypeMeta: metav1.TypeMeta{Kind: "ClusterAddon", APIVersion: "clusterstack.x-k8s.io/v1alpha1"}, + Spec: csov1alpha1.ClusterAddonSpec{ + ClusterRef: &corev1.ObjectReference{ + APIVersion: cluster.APIVersion, + Kind: cluster.Kind, + Name: cluster.Name, + UID: cluster.UID, + Namespace: cluster.Namespace, + }, + }, + } + + clusterAddon.OwnerReferences = append(clusterAddon.OwnerReferences, metav1.OwnerReference{ + APIVersion: cluster.APIVersion, + Kind: cluster.Kind, + Name: cluster.Name, + UID: cluster.UID, + }) + + if err := r.Create(ctx, clusterAddon); err != nil { + return reconcile.Result{}, fmt.Errorf("failed to create ClusterAddon object: %w", err) + } + return reconcile.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ClusterAddonCreateReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { + return ctrl.NewControllerManagedBy(mgr). + WithOptions(options). + For(&clusterv1.Cluster{}). + WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(log.FromContext(ctx), r.WatchFilterValue)). + WithEventFilter(predicate.Funcs{ + // We're only interested in the create events for a cluster object + DeleteFunc: func(e event.DeleteEvent) bool { + return false + }, + GenericFunc: func(e event.GenericEvent) bool { + return false + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return false + }, + }). + Complete(r) +} diff --git a/internal/controller/clusteraddoncreate_controller_test.go b/internal/controller/clusteraddoncreate_controller_test.go new file mode 100644 index 000000000..614a906f0 --- /dev/null +++ b/internal/controller/clusteraddoncreate_controller_test.go @@ -0,0 +1,91 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "fmt" + + csov1alpha1 "github.com/SovereignCloudStack/cluster-stack-operator/api/v1alpha1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" +) + +var _ = Describe("ClusterAddonCreateReconciler", func() { + var ( + cluster *clusterv1.Cluster + clusterStackRelease *csov1alpha1.ClusterStackRelease + testNs *corev1.Namespace + key types.NamespacedName + ) + + BeforeEach(func() { + var err error + testNs, err = testEnv.CreateNamespace(ctx, "clusteraddoncreate-reconciler") + Expect(err).NotTo(HaveOccurred()) + + clusterStackRelease = &csov1alpha1.ClusterStackRelease{ + ObjectMeta: metav1.ObjectMeta{ + Name: testClusterStackName, + Namespace: testNs.Name, + }, + } + Expect(testEnv.Create(ctx, clusterStackRelease)).To(Succeed()) + + cluster = &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testcluster", + Namespace: testNs.Name, + Finalizers: []string{clusterv1.ClusterFinalizer}, + }, + Spec: clusterv1.ClusterSpec{ + Topology: &clusterv1.Topology{ + Class: testClusterStackName, + }, + }, + } + Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + + key = types.NamespacedName{Name: fmt.Sprintf("cluster-addon-%s", cluster.Name), Namespace: testNs.Name} + }) + + AfterEach(func() { + Expect(testEnv.Cleanup(ctx, testNs, cluster, clusterStackRelease)).To(Succeed()) + }) + + Context("Basic test", func() { + It("creates the clusterAddon object", func() { + Eventually(func() bool { + var foundClusterAddon csov1alpha1.ClusterAddon + if err := testEnv.Get(ctx, key, &foundClusterAddon); err != nil { + testEnv.GetLogger().Info(err.Error()) + return false + } + + if foundClusterAddon.Spec.ClusterRef.Name != cluster.Name { + testEnv.GetLogger().Info("wrong cluster ref name", "got", foundClusterAddon.Spec.ClusterRef.Name, "want", cluster.Name) + return false + } + + return true + }, timeout, interval).Should(BeTrue()) + }) + }) +}) diff --git a/internal/controller/controller_suite_test.go b/internal/controller/controller_suite_test.go index b1f66ce8b..ccd8340dd 100644 --- a/internal/controller/controller_suite_test.go +++ b/internal/controller/controller_suite_test.go @@ -64,6 +64,10 @@ var _ = BeforeSuite(func() { ReleaseDirectory: "./../../test/releases", }).SetupWithManager(ctx, testEnv.Manager, c.Options{})).To(Succeed()) + Expect((&ClusterAddonCreateReconciler{ + Client: testEnv.Manager.GetClient(), + }).SetupWithManager(ctx, testEnv.Manager, c.Options{})).To(Succeed()) + go func() { defer GinkgoRecover() Expect(testEnv.StartManager(ctx)).To(Succeed())