-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
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
- 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>
Showing
7 changed files
with
406 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters