From 3ed8293a06079877e5dc1b596a39054c9d2d1b75 Mon Sep 17 00:00:00 2001
From: vbadrina <vbadrina@redhat.com>
Date: Wed, 17 Jul 2024 16:11:01 +0530
Subject: [PATCH] Add ManagedClusterView controller and create or update
 ClientInfo

- Added initialization for ManagedClusterViewReconciler in manager.go to setup the ManagedClusterView controller.
- Creates or updates configMap odf-client-info which maps client to it provider cluster
- Created comprehensive unit tests to cover the creation and update scenarios of the ConfigMap.

Signed-off-by: vbadrina <vbadrina@redhat.com>
---
 ...er-orchestrator.clusterserviceversion.yaml |   2 +-
 controllers/managedcluster_controller.go      |  12 +-
 controllers/managedclusterview_controller.go  | 199 ++++++++++++++++++
 .../managedclusterview_controller_test.go     | 159 ++++++++++++++
 controllers/manager.go                        |   8 +
 controllers/utils/managedcluster.go           |  36 ++++
 go.mod                                        |   2 +-
 7 files changed, 407 insertions(+), 11 deletions(-)
 create mode 100644 controllers/managedclusterview_controller.go
 create mode 100644 controllers/managedclusterview_controller_test.go
 create mode 100644 controllers/utils/managedcluster.go

diff --git a/bundle/manifests/odf-multicluster-orchestrator.clusterserviceversion.yaml b/bundle/manifests/odf-multicluster-orchestrator.clusterserviceversion.yaml
index 8e5746e2..5181eb92 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-07-12T13:14:27Z"
+    createdAt: "2024-07-16T05:37:17Z"
     olm.skipRange: ""
     operators.openshift.io/infrastructure-features: '["disconnected"]'
     operators.operatorframework.io/builder: operator-sdk-v1.34.1
diff --git a/controllers/managedcluster_controller.go b/controllers/managedcluster_controller.go
index 5f4626a7..7e15828a 100644
--- a/controllers/managedcluster_controller.go
+++ b/controllers/managedcluster_controller.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/red-hat-storage/odf-multicluster-orchestrator/controllers/utils"
 	viewv1beta1 "github.com/stolostron/multicloud-operators-foundation/pkg/apis/view/v1beta1"
+	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	clusterv1 "open-cluster-management.io/api/cluster/v1"
@@ -28,7 +29,6 @@ const (
 	OdfInfoClusterClaimNamespacedName = "odfinfo.odf.openshift.io"
 )
 
-// +kubebuilder:rbac:groups=view.open-cluster-management.io,resources=managedclusterviews,verbs=get;list;watch;create;update
 func (r *ManagedClusterReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
 	logger := r.Logger.With("ManagedCluster", req.NamespacedName)
 	logger.Info("Reconciling ManagedCluster")
@@ -46,7 +46,7 @@ func (r *ManagedClusterReconciler) Reconcile(ctx context.Context, req reconcile.
 		return ctrl.Result{}, err
 	}
 
-	logger.Info("Successfully reconciled ManagedCluster", "name", managedCluster.Name)
+	logger.Info("Successfully reconciled ManagedCluster")
 
 	return ctrl.Result{}, nil
 }
@@ -78,18 +78,12 @@ func (r *ManagedClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
 			}
 			return hasRequiredODFKey(obj)
 		},
-
-		DeleteFunc: func(e event.DeleteEvent) bool {
-			return false
-		},
-		GenericFunc: func(e event.GenericEvent) bool {
-			return false
-		},
 	}
 
 	return ctrl.NewControllerManagedBy(mgr).
 		For(&clusterv1.ManagedCluster{}, builder.WithPredicates(managedClusterPredicate, predicate.ResourceVersionChangedPredicate{})).
 		Owns(&viewv1beta1.ManagedClusterView{}).
+		Owns(&corev1.ConfigMap{}).
 		Complete(r)
 }
 
diff --git a/controllers/managedclusterview_controller.go b/controllers/managedclusterview_controller.go
new file mode 100644
index 00000000..aa46f07a
--- /dev/null
+++ b/controllers/managedclusterview_controller.go
@@ -0,0 +1,199 @@
+package controllers
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"log/slog"
+	"os"
+
+	ocsv1alpha1 "github.com/red-hat-storage/ocs-operator/api/v4/v1alpha1"
+	"github.com/red-hat-storage/odf-multicluster-orchestrator/controllers/utils"
+	viewv1beta1 "github.com/stolostron/multicloud-operators-foundation/pkg/apis/view/v1beta1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	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/controllerutil"
+	"sigs.k8s.io/controller-runtime/pkg/event"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+	"sigs.k8s.io/yaml"
+)
+
+type ManagedClusterViewReconciler struct {
+	Client client.Client
+	Logger *slog.Logger
+}
+
+const (
+	ODFInfoConfigMapName    = "odf-info"
+	ConfigMapResourceType   = "ConfigMap"
+	ClientInfoConfigMapName = "odf-client-info"
+)
+
+type ProviderInfo struct {
+	Version                    string               `json:"version"`
+	DeploymentType             string               `json:"deploymentType"`
+	StorageSystemName          string               `json:"storageSystemName"`
+	ProviderManagedClusterName string               `json:"providerManagedClusterName"`
+	NamespacedName             types.NamespacedName `json:"namespacedName"`
+	StorageProviderEndpoint    string               `json:"storageProviderEndpoint"`
+	CephClusterFSID            string               `json:"cephClusterFSID"`
+}
+
+type ClientInfo struct {
+	ClusterID                string       `json:"clusterId"`
+	Name                     string       `json:"name"`
+	ProviderInfo             ProviderInfo `json:"providerInfo,omitempty"`
+	ClientManagedClusterName string       `json:"clientManagedClusterName,omitempty"`
+}
+
+func (r *ManagedClusterViewReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	r.Logger.Info("Setting up ManagedClusterViewReconciler with manager")
+	managedClusterViewPredicate := predicate.Funcs{
+		UpdateFunc: func(e event.UpdateEvent) bool {
+			obj, ok := e.ObjectNew.(*viewv1beta1.ManagedClusterView)
+			if !ok {
+				return false
+			}
+			return hasODFInfoInScope(obj)
+		},
+		CreateFunc: func(e event.CreateEvent) bool {
+			obj, ok := e.Object.(*viewv1beta1.ManagedClusterView)
+			if !ok {
+				return false
+			}
+			return hasODFInfoInScope(obj)
+		},
+	}
+
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&viewv1beta1.ManagedClusterView{}, builder.WithPredicates(managedClusterViewPredicate, predicate.ResourceVersionChangedPredicate{})).
+		Complete(r)
+}
+
+func hasODFInfoInScope(mc *viewv1beta1.ManagedClusterView) bool {
+	if mc.Spec.Scope.Name == ODFInfoConfigMapName && mc.Spec.Scope.Kind == ConfigMapResourceType {
+		return true
+	}
+	return false
+}
+
+func (r *ManagedClusterViewReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
+	logger := r.Logger.With("ManagedClusterView", req.NamespacedName)
+	logger.Info("Reconciling ManagedClusterView")
+
+	var managedClusterView viewv1beta1.ManagedClusterView
+	if err := r.Client.Get(ctx, req.NamespacedName, &managedClusterView); err != nil {
+		if client.IgnoreNotFound(err) != nil {
+			logger.Error("Failed to get ManagedClusterView", "error", err)
+		}
+		return ctrl.Result{}, client.IgnoreNotFound(err)
+	}
+
+	if err := createOrUpdateConfigMap(ctx, r.Client, managedClusterView, r.Logger); err != nil {
+		logger.Error("Failed to create or update ConfigMap for ManagedClusterView", "error", err)
+		return ctrl.Result{}, err
+	}
+
+	logger.Info("Successfully reconciled ManagedClusterView")
+
+	return ctrl.Result{}, nil
+}
+
+func createOrUpdateConfigMap(ctx context.Context, c client.Client, managedClusterView viewv1beta1.ManagedClusterView, logger *slog.Logger) error {
+	logger = logger.With("ManagedClusterView", managedClusterView.Name, "Namespace", managedClusterView.Namespace)
+
+	var resultData map[string]string
+	err := json.Unmarshal(managedClusterView.Status.Result.Raw, &resultData)
+	if err != nil {
+		return fmt.Errorf("failed to unmarshal result data. %w", err)
+	}
+
+	clientInfoMap := make(map[string]ClientInfo)
+
+	for _, value := range resultData {
+		var odfInfo ocsv1alpha1.OdfInfoData
+		err := yaml.Unmarshal([]byte(value), &odfInfo)
+		if err != nil {
+			return fmt.Errorf("failed to unmarshal ODF info data. %w", err)
+		}
+
+		providerInfo := ProviderInfo{
+			Version:                    odfInfo.Version,
+			DeploymentType:             odfInfo.DeploymentType,
+			CephClusterFSID:            odfInfo.StorageCluster.CephClusterFSID,
+			StorageProviderEndpoint:    odfInfo.StorageCluster.StorageProviderEndpoint,
+			NamespacedName:             odfInfo.StorageCluster.NamespacedName,
+			StorageSystemName:          odfInfo.StorageSystemName,
+			ProviderManagedClusterName: managedClusterView.Namespace,
+		}
+
+		for _, client := range odfInfo.Clients {
+			managedCluster, err := utils.GetManagedClusterById(ctx, c, client.ClusterID)
+			if err != nil {
+				return err
+			}
+			clientInfo := ClientInfo{
+				ClusterID:                client.ClusterID,
+				Name:                     client.Name,
+				ProviderInfo:             providerInfo,
+				ClientManagedClusterName: managedCluster.Name,
+			}
+			clientInfoMap[fmt.Sprintf("%s/%s", managedCluster.Name, client.Name)] = clientInfo
+		}
+	}
+
+	operatorNamespace := os.Getenv("POD_NAMESPACE")
+	configMap := &corev1.ConfigMap{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      ClientInfoConfigMapName,
+			Namespace: operatorNamespace,
+		},
+	}
+	err = c.Get(ctx, types.NamespacedName{Name: ClientInfoConfigMapName, Namespace: operatorNamespace}, configMap)
+	if err != nil && !errors.IsNotFound(err) {
+		return fmt.Errorf("failed to get ConfigMap. %w", err)
+	}
+
+	if configMap.Data == nil {
+		configMap.Data = make(map[string]string)
+	}
+
+	op, err := controllerutil.CreateOrUpdate(ctx, c, configMap, func() error {
+		for clientKey, clientInfo := range clientInfoMap {
+			clientInfoJSON, err := json.Marshal(clientInfo)
+			if err != nil {
+				return fmt.Errorf("failed to marshal client info. %w", err)
+			}
+			configMap.Data[clientKey] = string(clientInfoJSON)
+		}
+
+		mcvOwnerRefs := managedClusterView.GetOwnerReferences()
+		for _, mcvOwnerRef := range mcvOwnerRefs {
+			exists := false
+			for _, existingOwnerRef := range configMap.OwnerReferences {
+				if existingOwnerRef.UID == mcvOwnerRef.UID {
+					exists = true
+					break
+				}
+			}
+			if !exists {
+				configMap.OwnerReferences = append(configMap.OwnerReferences, mcvOwnerRef)
+			}
+		}
+		return nil
+	})
+
+	if err != nil {
+		return fmt.Errorf("failed to create or update ConfigMap. %w", err)
+	}
+
+	logger.Info(fmt.Sprintf("ConfigMap %s in namespace %s has been %s", ClientInfoConfigMapName, operatorNamespace, op))
+
+	return nil
+}
diff --git a/controllers/managedclusterview_controller_test.go b/controllers/managedclusterview_controller_test.go
new file mode 100644
index 00000000..b4ef1a6c
--- /dev/null
+++ b/controllers/managedclusterview_controller_test.go
@@ -0,0 +1,159 @@
+//go:build unit
+// +build unit
+
+package controllers
+
+import (
+	"context"
+	"encoding/json"
+	"os"
+	"testing"
+
+	"github.com/google/uuid"
+	"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"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/kubernetes/scheme"
+	clusterv1 "open-cluster-management.io/api/cluster/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+)
+
+func TestCreateOrUpdateConfigMap(t *testing.T) {
+
+	s := scheme.Scheme
+	_ = viewv1beta1.AddToScheme(s)
+	_ = corev1.AddToScheme(s)
+	_ = clusterv1.AddToScheme(s)
+
+	c := fake.NewClientBuilder().WithScheme(s).Build()
+	os.Setenv("POD_NAMESPACE", "openshift-operators")
+	logger := utils.GetLogger(utils.GetZapLogger(true))
+
+	createManagedClusterView := func(name, namespace string, data map[string]string, ownerRefs []metav1.OwnerReference) *viewv1beta1.ManagedClusterView {
+		raw, _ := json.Marshal(data)
+		return &viewv1beta1.ManagedClusterView{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:            name,
+				Namespace:       namespace,
+				UID:             types.UID(uuid.New().String()),
+				OwnerReferences: ownerRefs,
+			},
+			Status: viewv1beta1.ViewStatus{
+				Result: runtime.RawExtension{Raw: raw},
+			},
+		}
+	}
+
+	createManagedCluster := func(name, clusterID string) *clusterv1.ManagedCluster {
+		return &clusterv1.ManagedCluster{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: name,
+				UID:  types.UID(uuid.New().String()),
+				Labels: map[string]string{
+					"clusterID": clusterID,
+				},
+			},
+		}
+	}
+
+	t.Run("Create ConfigMap with MCV in cluster1", func(t *testing.T) {
+		mc1 := createManagedCluster("cluster1-name", "cluster1")
+		mc2 := createManagedCluster("cluster2-name", "cluster2")
+		err := c.Create(context.TODO(), mc1)
+		assert.NoError(t, err)
+		err = c.Create(context.TODO(), mc2)
+		assert.NoError(t, err)
+
+		data := map[string]string{
+			"openshift-storage_ocs-storagecluster.config.yaml": `
+                version: "4.Y.Z"
+                deploymentType: "internal"
+                clients:
+                  - name: "client1"
+                    clusterId: "cluster1"
+                storageCluster:
+                  namespacedName:
+                    name: "ocs-storagecluster"
+                    namespace: "openshift-storage"
+                  storageProviderEndpoint: ""
+                  cephClusterFSID: "7a3d6b81-a55d-44fe-84d0-46c67cd395ca"
+                storageSystemName: "ocs-storagecluster-storagesystem"
+            `,
+		}
+		ownerRefs := []metav1.OwnerReference{
+			*metav1.NewControllerRef(mc1, clusterv1.SchemeGroupVersion.WithKind("ManagedCluster")),
+		}
+		mcv := createManagedClusterView("test-view", "cluster1", data, ownerRefs)
+
+		ctx := context.TODO()
+		err = c.Create(ctx, mcv)
+		assert.NoError(t, err)
+
+		err = createOrUpdateConfigMap(ctx, c, *mcv, logger)
+		assert.NoError(t, err)
+
+		cm := &corev1.ConfigMap{}
+		err = c.Get(ctx, types.NamespacedName{Name: ClientInfoConfigMapName, Namespace: os.Getenv("POD_NAMESPACE")}, cm)
+		assert.NoError(t, err)
+		assert.NotNil(t, cm)
+
+		expectedData := map[string]string{
+			"cluster1-name/client1": `{"clusterId":"cluster1","name":"client1","providerInfo":{"version":"4.Y.Z","deploymentType":"internal","storageSystemName":"ocs-storagecluster-storagesystem","providerManagedClusterName":"cluster1","namespacedName":{"Namespace":"openshift-storage","Name":"ocs-storagecluster"},"storageProviderEndpoint":"","cephClusterFSID":"7a3d6b81-a55d-44fe-84d0-46c67cd395ca"},"clientManagedClusterName":"cluster1-name"}`,
+		}
+
+		assert.Equal(t, expectedData, cm.Data)
+		assert.Equal(t, 1, len(cm.OwnerReferences))
+		assert.Equal(t, mc1.Name, cm.OwnerReferences[0].Name)
+		assert.Equal(t, "ManagedCluster", cm.OwnerReferences[0].Kind)
+		assert.Equal(t, clusterv1.GroupVersion.String(), cm.OwnerReferences[0].APIVersion)
+
+	})
+
+	t.Run("Update ConfigMap with MCV in cluster2", func(t *testing.T) {
+		mc2 := createManagedCluster("cluster2-name", "cluster2")
+		ctx := context.TODO()
+		data := map[string]string{
+			"openshift-storage_ocs-storagecluster.config.yaml": `
+                version: "4.Y.Z"
+                deploymentType: "internal"
+                clients:
+                  - name: "client2"
+                    clusterId: "cluster2"
+                storageCluster:
+                  namespacedName:
+                    name: "ocs-storagecluster"
+                    namespace: "openshift-storage"
+                  storageProviderEndpoint: ""
+                  cephClusterFSID: "8b3d6b81-b55d-55fe-94d0-56c67cd495ca"
+                storageSystemName: "ocs-storagecluster-storagesystem"
+            `,
+		}
+		ownerRefs := []metav1.OwnerReference{
+			*metav1.NewControllerRef(mc2, clusterv1.SchemeGroupVersion.WithKind("ManagedCluster")),
+		}
+		mcv := createManagedClusterView("new-view", "cluster2", data, ownerRefs)
+
+		err := c.Create(ctx, mcv)
+		assert.NoError(t, err)
+
+		err = createOrUpdateConfigMap(ctx, c, *mcv, logger)
+		assert.NoError(t, err)
+
+		cm := &corev1.ConfigMap{}
+		err = c.Get(ctx, types.NamespacedName{Name: ClientInfoConfigMapName, Namespace: os.Getenv("POD_NAMESPACE")}, cm)
+		assert.NoError(t, err)
+		assert.NotNil(t, cm)
+
+		expectedData := map[string]string{
+			"cluster1-name/client1": `{"clusterId":"cluster1","name":"client1","providerInfo":{"version":"4.Y.Z","deploymentType":"internal","storageSystemName":"ocs-storagecluster-storagesystem","providerManagedClusterName":"cluster1","namespacedName":{"Namespace":"openshift-storage","Name":"ocs-storagecluster"},"storageProviderEndpoint":"","cephClusterFSID":"7a3d6b81-a55d-44fe-84d0-46c67cd395ca"},"clientManagedClusterName":"cluster1-name"}`,
+			"cluster2-name/client2": `{"clusterId":"cluster2","name":"client2","providerInfo":{"version":"4.Y.Z","deploymentType":"internal","storageSystemName":"ocs-storagecluster-storagesystem","providerManagedClusterName":"cluster2","namespacedName":{"Namespace":"openshift-storage","Name":"ocs-storagecluster"},"storageProviderEndpoint":"","cephClusterFSID":"8b3d6b81-b55d-55fe-94d0-56c67cd495ca"},"clientManagedClusterName":"cluster2-name"}`,
+		}
+
+		assert.Equal(t, expectedData, cm.Data)
+		assert.Equal(t, 2, len(cm.OwnerReferences))
+	})
+}
diff --git a/controllers/manager.go b/controllers/manager.go
index 455f48aa..6dcf7a5a 100644
--- a/controllers/manager.go
+++ b/controllers/manager.go
@@ -150,6 +150,14 @@ func (o *ManagerOptions) runManager() {
 		os.Exit(1)
 	}
 
+	if err = (&ManagedClusterViewReconciler{
+		Client: mgr.GetClient(),
+		Logger: logger.With("controller", "ManagedClusterViewReconciler"),
+	}).SetupWithManager(mgr); err != nil {
+		logger.Error("Failed to create ManagedClusterView 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/utils/managedcluster.go b/controllers/utils/managedcluster.go
new file mode 100644
index 00000000..94157f04
--- /dev/null
+++ b/controllers/utils/managedcluster.go
@@ -0,0 +1,36 @@
+package utils
+
+import (
+	"context"
+	"fmt"
+
+	"k8s.io/apimachinery/pkg/labels"
+	clusterv1 "open-cluster-management.io/api/cluster/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+const clusterIDLabelKey = "clusterID"
+
+// GetManagedClusterById fetches a ManagedCluster by its cluster ID label
+func GetManagedClusterById(ctx context.Context, c client.Client, clusterId string) (*clusterv1.ManagedCluster, error) {
+	managedClusterList := &clusterv1.ManagedClusterList{}
+
+	labelSelector := labels.SelectorFromSet(labels.Set{
+		clusterIDLabelKey: clusterId,
+	})
+
+	listOptions := &client.ListOptions{
+		LabelSelector: labelSelector,
+	}
+	err := c.List(ctx, managedClusterList, listOptions)
+	if err != nil {
+		return nil, fmt.Errorf("failed to list managed clusters: %v", err)
+	}
+
+	if len(managedClusterList.Items) == 0 {
+		return nil, fmt.Errorf("managed cluster with ID %s not found", clusterId)
+	}
+
+	// Return the first matching ManagedCluster (there should only be one)
+	return &managedClusterList.Items[0], nil
+}
diff --git a/go.mod b/go.mod
index b2fe3795..da4c2997 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.22.0
 require (
 	github.com/csi-addons/kubernetes-csi-addons v0.8.0
 	github.com/go-logr/zapr v1.3.0
+	github.com/google/uuid v1.6.0
 	github.com/kube-object-storage/lib-bucket-provisioner v0.0.0-20221122204822-d1a8c34382f1
 	github.com/onsi/ginkgo v1.16.5
 	github.com/onsi/gomega v1.32.0
@@ -54,7 +55,6 @@ require (
 	github.com/google/gnostic-models v0.6.8 // indirect
 	github.com/google/go-cmp v0.6.0 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
-	github.com/google/uuid v1.6.0 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect