diff --git a/bundle/manifests/odf-multicluster-orchestrator.clusterserviceversion.yaml b/bundle/manifests/odf-multicluster-orchestrator.clusterserviceversion.yaml index 6e2b830c..dda49127 100644 --- a/bundle/manifests/odf-multicluster-orchestrator.clusterserviceversion.yaml +++ b/bundle/manifests/odf-multicluster-orchestrator.clusterserviceversion.yaml @@ -36,7 +36,7 @@ metadata: ] capabilities: Basic Install console.openshift.io/plugins: '["odf-multicluster-console"]' - createdAt: "2024-04-03T11:58:23Z" + createdAt: "2024-07-04T11:28:14Z" olm.skipRange: "" operators.openshift.io/infrastructure-features: '["disconnected"]' operators.operatorframework.io/builder: operator-sdk-v1.34.1 @@ -224,6 +224,12 @@ spec: - roles verbs: - '*' + - apiGroups: + - view.open-cluster-management.io + resources: + - managedclusterviews + verbs: + - '*' - apiGroups: - work.open-cluster-management.io resources: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 12794311..329c7e81 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -161,6 +161,12 @@ rules: - roles verbs: - '*' +- apiGroups: + - view.open-cluster-management.io + resources: + - managedclusterviews + verbs: + - '*' - apiGroups: - work.open-cluster-management.io resources: diff --git a/controllers/managedcluster_controller.go b/controllers/managedcluster_controller.go new file mode 100644 index 00000000..239951d6 --- /dev/null +++ b/controllers/managedcluster_controller.go @@ -0,0 +1,123 @@ +package controllers + +import ( + "context" + "log/slog" + + "github.com/red-hat-storage/odf-multicluster-orchestrator/controllers/utils" + viewv1beta1 "github.com/stolostron/multicloud-operators-foundation/pkg/apis/view/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + clusterv1 "open-cluster-management.io/api/cluster/v1" + 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/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type ManagedClusterReconciler struct { + Client client.Client + Scheme *runtime.Scheme + Logger *slog.Logger +} + +const odfConfigMapName = "odf-info" + +func (r *ManagedClusterReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + logger := r.Logger.With("ManagedCluster", req.NamespacedName) + logger.Info("Reconciling ManagedCluster") + + var managedCluster clusterv1.ManagedCluster + if err := r.Client.Get(ctx, req.NamespacedName, &managedCluster); err != nil { + if client.IgnoreNotFound(err) != nil { + logger.Error("Failed to get ManagedCluster", "error", err) + } + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + if err := r.ensureManagedClusterViews(ctx, managedCluster.Name); err != nil { + logger.Error("Failed to ensure ManagedClusterView", "error", err) + return ctrl.Result{}, err + } + + logger.Info("Successfully reconciled ManagedCluster", "name", managedCluster.Name) + + return ctrl.Result{}, nil +} + +func (r *ManagedClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { + r.Logger.Info("Setting up ManagedClusterReconciler with manager") + + return ctrl.NewControllerManagedBy(mgr). + For(&clusterv1.ManagedCluster{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Watches( + &viewv1beta1.ManagedClusterView{}, + handler.EnqueueRequestsFromMapFunc(r.managedClusterViewRequestMapper), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}, predicate.GenerationChangedPredicate{}), + ). + Complete(r) +} + +func (r *ManagedClusterReconciler) managedClusterViewRequestMapper(ctx context.Context, obj client.Object) []reconcile.Request { + mcv, ok := obj.(*viewv1beta1.ManagedClusterView) + + if !ok { + r.Logger.Error("Something unexpected occurred when casting obj into ManagedClusterView") + return []reconcile.Request{} + } + logger := r.Logger.With("ManagedClusterView", mcv) + mcName, ok := mcv.Labels[utils.MCVLabelKey] + + if !ok { + return []reconcile.Request{} + } + + logger.Info("ManagedClusterView of interest just got updated. Raising a reconcile request for ManagedCluster", "ManagedCluster", mcName) + + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: mcName, + }, + }, + } + +} +func (r *ManagedClusterReconciler) ensureManagedClusterViews(ctx context.Context, clusterName string) error { + namespace := "openshift-storage" + resourceType := "ConfigMap" + r.Logger.Info("Processing PeerRef", "ClusterName", clusterName) + mcv, err := utils.GetManagedClusterViewByLabel(r.Client, utils.MCVLabelKey, clusterName, clusterName) + if err != nil { + r.Logger.Info("ManagedClusterView does not exist, creating a new one") + mcv, err = utils.CreateOrUpdateManagedClusterView(ctx, r.Client, odfConfigMapName, namespace, resourceType, clusterName) + if err != nil { + r.Logger.Error("Failed to create or update ManagedClusterView", "error", err) + return err + } + r.Logger.Info("ManagedClusterView created or updated successfully", "ManagedClusterView", mcv.Name) + } else { + desiredSpec := viewv1beta1.ViewSpec{ + Scope: viewv1beta1.ViewScope{ + Name: odfConfigMapName, + Namespace: namespace, + Resource: resourceType, + }, + } + if mcv.Spec != desiredSpec { + r.Logger.Info("ManagedClusterView spec does not match, updating", "ClusterName", clusterName) + mcv, err = utils.CreateOrUpdateManagedClusterView(ctx, r.Client, odfConfigMapName, namespace, resourceType, clusterName) + if err != nil { + r.Logger.Error("Failed to update ManagedClusterView", "error", err) + return err + } + r.Logger.Info("ManagedClusterView updated successfully", "ManagedClusterView", mcv.Name) + } else { + r.Logger.Info("ManagedClusterView spec matches, no update needed", "ManagedClusterView", mcv.Name) + } + } + + return nil +} diff --git a/controllers/managedcluster_controller_test.go b/controllers/managedcluster_controller_test.go new file mode 100644 index 00000000..089f6905 --- /dev/null +++ b/controllers/managedcluster_controller_test.go @@ -0,0 +1,130 @@ +//go:build unit +// +build unit + +package controllers + +import ( + "context" + "testing" + + "github.com/red-hat-storage/odf-multicluster-orchestrator/controllers/utils" + viewv1beta1 "github.com/stolostron/multicloud-operators-foundation/pkg/apis/view/v1beta1" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + clusterv1 "open-cluster-management.io/api/cluster/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestManagedClusterReconcile(t *testing.T) { + scheme := runtime.NewScheme() + _ = clusterv1.AddToScheme(scheme) + _ = viewv1beta1.AddToScheme(scheme) + + client := fake.NewClientBuilder().WithScheme(scheme).Build() + logger := utils.GetLogger(utils.GetZapLogger(true)) + + reconciler := &ManagedClusterReconciler{ + Client: client, + Scheme: scheme, + Logger: logger, + } + + req := ctrl.Request{ + NamespacedName: types.NamespacedName{Name: "test-cluster"}, + } + + t.Run("ManagedCluster not found", func(t *testing.T) { + res, err := reconciler.Reconcile(context.TODO(), req) + assert.NoError(t, err) + assert.Equal(t, ctrl.Result{}, res) + }) + + t.Run("ManagedCluster found", func(t *testing.T) { + managedCluster := &clusterv1.ManagedCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + }, + } + _ = client.Create(context.TODO(), managedCluster) + + res, err := reconciler.Reconcile(context.TODO(), req) + assert.NoError(t, err) + assert.Equal(t, ctrl.Result{}, res) + }) +} + +func TestEnsureManagedClusterViews(t *testing.T) { + scheme := runtime.NewScheme() + _ = clusterv1.AddToScheme(scheme) + _ = viewv1beta1.AddToScheme(scheme) + + client := fake.NewClientBuilder().WithScheme(scheme).Build() + logger := utils.GetLogger(utils.GetZapLogger(true)) + + reconciler := &ManagedClusterReconciler{ + Client: client, + Scheme: scheme, + Logger: logger, + } + + t.Run("ManagedClusterView does not exist. It should create it", func(t *testing.T) { + err := reconciler.ensureManagedClusterViews(context.TODO(), "test-cluster") + assert.NoError(t, err) + + createdMCV := &viewv1beta1.ManagedClusterView{} + err = client.Get(context.TODO(), types.NamespacedName{Name: "test-cluster-mcv", Namespace: "test-cluster"}, createdMCV) + assert.NoError(t, err) + assert.Equal(t, "test-cluster-mcv", createdMCV.Name) + assert.Equal(t, "test-cluster", createdMCV.Namespace) + assert.Equal(t, "odf-info", createdMCV.Spec.Scope.Name) + assert.Equal(t, "openshift-storage", createdMCV.Spec.Scope.Namespace) + }) + + t.Run("ManagedClusterView exists but spec does not match. Desired spec should be reached", func(t *testing.T) { + existingMCV := &viewv1beta1.ManagedClusterView{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster-mcv", + Namespace: "test-cluster", + }, + Spec: viewv1beta1.ViewSpec{ + Scope: viewv1beta1.ViewScope{ + Name: "old-configmap", + Namespace: "old-namespace", + Resource: "ConfigMap", + }, + }, + } + _ = client.Create(context.TODO(), existingMCV) + + err := reconciler.ensureManagedClusterViews(context.TODO(), "test-cluster") + assert.NoError(t, err) + + updatedMCV := &viewv1beta1.ManagedClusterView{} + _ = client.Get(context.TODO(), types.NamespacedName{Name: "test-cluster-mcv", Namespace: "test-cluster"}, updatedMCV) + assert.Equal(t, "odf-info", updatedMCV.Spec.Scope.Name) + assert.Equal(t, "openshift-storage", updatedMCV.Spec.Scope.Namespace) + }) + + t.Run("ManagedClusterView exists and spec matches. Nothing should change. It should be error free", func(t *testing.T) { + existingMCV := &viewv1beta1.ManagedClusterView{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster-mcv", + Namespace: "test-cluster", + }, + Spec: viewv1beta1.ViewSpec{ + Scope: viewv1beta1.ViewScope{ + Name: "odf-info", + Namespace: "openshift-storage", + Resource: "ConfigMap", + }, + }, + } + _ = client.Create(context.TODO(), existingMCV) + + err := reconciler.ensureManagedClusterViews(context.TODO(), "test-cluster") + assert.NoError(t, err) + }) +} diff --git a/controllers/manager.go b/controllers/manager.go index 3a9c99bb..96157f6e 100644 --- a/controllers/manager.go +++ b/controllers/manager.go @@ -13,6 +13,7 @@ import ( "github.com/red-hat-storage/odf-multicluster-orchestrator/console" "github.com/red-hat-storage/odf-multicluster-orchestrator/controllers/utils" "github.com/spf13/cobra" + viewv1beta1 "github.com/stolostron/multicloud-operators-foundation/pkg/apis/view/v1beta1" "golang.org/x/sync/errgroup" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -43,6 +44,7 @@ func init() { utilruntime.Must(ramenv1alpha1.AddToScheme(mgrScheme)) utilruntime.Must(workv1.AddToScheme(mgrScheme)) + utilruntime.Must(viewv1beta1.AddToScheme(mgrScheme)) //+kubebuilder:scaffold:scheme } @@ -140,6 +142,15 @@ func (o *ManagerOptions) runManager() { os.Exit(1) } + if err = (&ManagedClusterReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Logger: logger.With("controller", "ManagedClusterReconciler"), + }).SetupWithManager(mgr); err != nil { + logger.Error("Failed to create ManagedCluster controller", "error", err) + os.Exit(1) + } + if err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error { err = console.InitConsole(ctx, mgr.GetClient(), o.MulticlusterConsolePort, namespace) if err != nil { diff --git a/controllers/mirrorpeer_controller.go b/controllers/mirrorpeer_controller.go index aa5a1f5f..f59a3123 100644 --- a/controllers/mirrorpeer_controller.go +++ b/controllers/mirrorpeer_controller.go @@ -70,6 +70,7 @@ const spokeClusterRoleBindingName = "spoke-clusterrole-bindings" //+kubebuilder:rbac:groups=addon.open-cluster-management.io,resources=managedclusteraddons;clustermanagementaddons,verbs=* //+kubebuilder:rbac:groups=addon.open-cluster-management.io,resources=managedclusteraddons/status,verbs=* //+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;create;update;watch +//+kubebuilder:rbac:groups=view.open-cluster-management.io,resources=managedclusterviews,verbs=* //+kubebuilder:rbac:groups=console.openshift.io,resources=consoleplugins,verbs=get;list;create;update;watch //+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;create;update;watch //+kubebuilder:rbac:groups=ramendr.openshift.io,resources=drclusters;drpolicies,verbs=get;list;create;update;watch diff --git a/controllers/utils/acm.go b/controllers/utils/acm.go new file mode 100644 index 00000000..4405831e --- /dev/null +++ b/controllers/utils/acm.go @@ -0,0 +1,72 @@ +package utils + +import ( + "context" + "fmt" + + viewv1beta1 "github.com/stolostron/multicloud-operators-foundation/pkg/apis/view/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const MCVLabelKey = "multicluster.odf.openshift.io/cluster" + +func CreateOrUpdateManagedClusterView(ctx context.Context, client ctrlClient.Client, resourceToFindName string, resourceToFindNamespace string, resourceToFindType string, clusterName string) (*viewv1beta1.ManagedClusterView, error) { + mcv := &viewv1beta1.ManagedClusterView{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-mcv", clusterName), + Namespace: clusterName, + }, + } + + _, err := controllerutil.CreateOrUpdate(ctx, client, mcv, func() error { + mcv.Spec = viewv1beta1.ViewSpec{ + Scope: viewv1beta1.ViewScope{ + Name: resourceToFindName, + Namespace: resourceToFindNamespace, + Resource: resourceToFindType, + }, + } + + if mcv.Labels == nil { + mcv.Labels = make(map[string]string) + } + + mcv.Labels[MCVLabelKey] = clusterName + + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to create or update ManagedClusterView: %w", err) + } + + return mcv, nil +} + +func GetManagedClusterViewByLabel(client ctrlClient.Client, labelKey, labelValue, namespace string) (*viewv1beta1.ManagedClusterView, error) { + mcvList := &viewv1beta1.ManagedClusterViewList{} + listOptions := &ctrlClient.ListOptions{ + Namespace: namespace, + LabelSelector: getLabelSelector(labelKey, labelValue), + } + + err := client.List(context.TODO(), mcvList, listOptions) + if err != nil { + return nil, fmt.Errorf("failed to list ManagedClusterViews: %w", err) + } + + if len(mcvList.Items) == 0 { + return nil, fmt.Errorf("no ManagedClusterView found with label %s=%s in namespace %s", labelKey, labelValue, namespace) + } + + return &mcvList.Items[0], nil +} + +func getLabelSelector(labelKey, labelValue string) labels.Selector { + return labels.SelectorFromSet(labels.Set{ + labelKey: labelValue, + }) +} diff --git a/controllers/utils/acm_test.go b/controllers/utils/acm_test.go new file mode 100644 index 00000000..2b2a74d9 --- /dev/null +++ b/controllers/utils/acm_test.go @@ -0,0 +1,61 @@ +package utils + +import ( + "context" + "testing" + + viewv1beta1 "github.com/stolostron/multicloud-operators-foundation/pkg/apis/view/v1beta1" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestCreateOrUpdateManagedClusterView(t *testing.T) { + s := runtime.NewScheme() + err := viewv1beta1.AddToScheme(s) + assert.NoError(t, err) + client := fake.NewClientBuilder().WithScheme(s).Build() + + t.Run("Success", func(t *testing.T) { + mcv, err := CreateOrUpdateManagedClusterView(context.TODO(), client, "example-configmap", "default", "ConfigMap", "managed-cluster-1") + assert.NoError(t, err) + assert.NotNil(t, mcv) + assert.Equal(t, "managed-cluster-1-mcv", mcv.Name) + assert.Equal(t, "example-configmap", mcv.Spec.Scope.Name) + assert.Equal(t, "default", mcv.Spec.Scope.Namespace) + assert.Equal(t, "ConfigMap", mcv.Spec.Scope.Resource) + assert.Equal(t, "managed-cluster-1", mcv.Labels[MCVLabelKey]) + }) +} + +func TestGetManagedClusterViewByLabel(t *testing.T) { + s := runtime.NewScheme() + err := viewv1beta1.AddToScheme(s) + assert.NoError(t, err) + + mcv := &viewv1beta1.ManagedClusterView{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-mcv", + Namespace: "default", + Labels: map[string]string{ + "managed-cluster": "test-cluster", + }, + }, + } + + client := fake.NewClientBuilder().WithScheme(s).WithObjects(mcv).Build() + + t.Run("Success", func(t *testing.T) { + result, err := GetManagedClusterViewByLabel(client, "managed-cluster", "test-cluster", "default") + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "example-mcv", result.Name) + }) + + t.Run("Failure due to non-existent label", func(t *testing.T) { + _, err := GetManagedClusterViewByLabel(client, "managed-cluster", "non-existent-cluster", "default") + assert.Error(t, err) + assert.Equal(t, "no ManagedClusterView found with label managed-cluster=non-existent-cluster in namespace default", err.Error()) + }) +} diff --git a/go.mod b/go.mod index 10f321e2..78d999df 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/red-hat-storage/ocs-operator/api/v4 v4.0.0-20240327160100-bbe9d9d49462 github.com/rook/rook/pkg/apis v0.0.0-20240513003450-39f88521f0fd github.com/spf13/cobra v1.8.0 + github.com/stolostron/multicloud-operators-foundation v0.0.0-20220824091202-e9cd9710d009 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 go.uber.org/zap/exp v0.2.0 diff --git a/go.sum b/go.sum index f5fccded..479458c4 100644 --- a/go.sum +++ b/go.sum @@ -761,6 +761,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stolostron/multicloud-operators-foundation v0.0.0-20220824091202-e9cd9710d009 h1:/KHe+raMpLL01sYy5adCs95esrfGcHs+IxsI4sd06K0= +github.com/stolostron/multicloud-operators-foundation v0.0.0-20220824091202-e9cd9710d009/go.mod h1:DhSK9KG5nkRnCHLb0QKqRIfnd1Tw+rLgwpCi8n7lIGY= github.com/stolostron/multicloud-operators-placementrule v1.2.4-1-20220311-8eedb3f.0.20230828200208-cd3c119a7fa0 h1:qL6eeBtdjLq7ktBBg8tB44b6jTKQjFy6bdl8EM+Kq6o= github.com/stolostron/multicloud-operators-placementrule v1.2.4-1-20220311-8eedb3f.0.20230828200208-cd3c119a7fa0/go.mod h1:uMTaz9cMLe5N+yJ/PpHPtSOdlBFB00WdxAW+K5TfkVw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=