Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ManagedClusterView controller and create or update ClientInfo
Browse files Browse the repository at this point in the history
- 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>
vbnrh committed Jul 19, 2024
1 parent d743b7b commit ef0af41
Showing 7 changed files with 406 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -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
12 changes: 3 additions & 9 deletions controllers/managedcluster_controller.go
Original file line number Diff line number Diff line change
@@ -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)
}

199 changes: 199 additions & 0 deletions controllers/managedclusterview_controller.go
Original file line number Diff line number Diff line change
@@ -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
}
158 changes: 158 additions & 0 deletions controllers/managedclusterview_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
//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,
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))
})
}
8 changes: 8 additions & 0 deletions controllers/manager.go
Original file line number Diff line number Diff line change
@@ -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 {
36 changes: 36 additions & 0 deletions controllers/utils/managedcluster.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit ef0af41

Please sign in to comment.