-
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.
feat: Add ManagedClusterView controller and create or update ClientIn…
…fo configmap - 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 <[email protected]>
- Loading branch information
Showing
6 changed files
with
354 additions
and
10 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,209 @@ | ||
package controllers | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"log/slog" | ||
"os" | ||
|
||
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" | ||
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" | ||
) | ||
|
||
// The temporary ODFInfoData struct starts here, Once it is exported in OCS, then it will be removed | ||
type connectedClient struct { | ||
Name string `yaml:"name"` | ||
ClusterID string `yaml:"clusterId"` | ||
} | ||
|
||
type infoStorageCluster struct { | ||
NamespacedName types.NamespacedName `yaml:"namespacedName"` | ||
StorageProviderEndpoint string `yaml:"storageProviderEndpoint"` | ||
CephClusterFSID string `yaml:"cephClusterFSID"` | ||
} | ||
|
||
type OdfInfoData struct { | ||
Version string `yaml:"version"` | ||
DeploymentType string `yaml:"deploymentType"` | ||
Clients []connectedClient `yaml:"clients"` | ||
StorageCluster infoStorageCluster `yaml:"storageCluster"` | ||
StorageSystemName string `yaml:"storageSystemName"` | ||
} | ||
|
||
// The temporary ODFInfoData struct ends here | ||
|
||
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"` | ||
} | ||
|
||
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{}, predicate.GenerationChangedPredicate{})). | ||
Owns(&corev1.ConfigMap{}). | ||
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", "name", managedClusterView.Name) | ||
|
||
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: %v", err) | ||
} | ||
|
||
clientInfoMap := make(map[string]ClientInfo) | ||
|
||
for _, value := range resultData { | ||
var odfInfo OdfInfoData | ||
err := yaml.Unmarshal([]byte(value), &odfInfo) | ||
if err != nil { | ||
return fmt.Errorf("failed to unmarshal ODF info data: %v", 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 { | ||
clientInfo := ClientInfo{ | ||
ClusterID: client.ClusterID, | ||
Name: client.Name, | ||
ProviderInfo: providerInfo, | ||
} | ||
clientInfoMap[client.Name] = clientInfo | ||
} | ||
} | ||
|
||
configMapData := make(map[string]string) | ||
for clientName, clientInfo := range clientInfoMap { | ||
clientInfoJSON, err := json.Marshal(clientInfo) | ||
if err != nil { | ||
return fmt.Errorf("failed to marshal client info: %v", err) | ||
} | ||
configMapData[clientName] = string(clientInfoJSON) | ||
} | ||
|
||
operatorNamespace := os.Getenv("POD_NAMESPACE") | ||
configMap := &corev1.ConfigMap{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: ClientInfoConfigMapName, | ||
Namespace: operatorNamespace, | ||
}, | ||
} | ||
|
||
op, err := controllerutil.CreateOrUpdate(ctx, c, configMap, func() error { | ||
configMap.Data = configMapData | ||
|
||
ownerExists := false | ||
for _, ownerRef := range configMap.OwnerReferences { | ||
if ownerRef.UID == managedClusterView.UID { | ||
ownerExists = true | ||
break | ||
} | ||
} | ||
|
||
if !ownerExists { | ||
ownerRef := *metav1.NewControllerRef(&managedClusterView, viewv1beta1.GroupVersion.WithKind("ManagedClusterView")) | ||
logger.Info("OwnerRef added", "UID", string(ownerRef.UID)) | ||
configMap.OwnerReferences = append(configMap.OwnerReferences, ownerRef) | ||
} | ||
|
||
return nil | ||
}) | ||
|
||
if err != nil { | ||
return fmt.Errorf("failed to create or update ConfigMap: %v", 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,135 @@ | ||
//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" | ||
"sigs.k8s.io/controller-runtime/pkg/client/fake" | ||
) | ||
|
||
func TestCreateOrUpdateConfigMap(t *testing.T) { | ||
|
||
s := scheme.Scheme | ||
_ = viewv1beta1.AddToScheme(s) | ||
_ = corev1.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) *viewv1beta1.ManagedClusterView { | ||
raw, _ := json.Marshal(data) | ||
return &viewv1beta1.ManagedClusterView{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: name, | ||
Namespace: namespace, | ||
UID: types.UID(uuid.New().String()), | ||
}, | ||
Status: viewv1beta1.ViewStatus{ | ||
Result: runtime.RawExtension{Raw: raw}, | ||
}, | ||
} | ||
} | ||
|
||
t.Run("Create ConfigMap", func(t *testing.T) { | ||
data := map[string]string{ | ||
"openshift-storage_ocs-storagecluster.config.yaml": ` | ||
version: "4.Y.Z" | ||
deploymentType: "internal" | ||
clients: | ||
- name: "client1" | ||
clusterId: "cluster1" | ||
- name: "client2" | ||
clusterId: "cluster2" | ||
storageCluster: | ||
namespacedName: | ||
name: "ocs-storagecluster" | ||
namespace: "openshift-storage" | ||
storageProviderEndpoint: "" | ||
cephClusterFSID: "7a3d6b81-a55d-44fe-84d0-46c67cd395ca" | ||
storageSystemName: "ocs-storagecluster-storagesystem" | ||
`, | ||
} | ||
mcv := createManagedClusterView("test-view", "local-cluster", data) | ||
|
||
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{ | ||
"client1": `{"clusterId":"cluster1","name":"client1","providerInfo":{"version":"4.Y.Z","deploymentType":"internal","storageSystemName":"ocs-storagecluster-storagesystem","providerManagedClusterName":"local-cluster","namespacedName":{"Namespace":"openshift-storage","Name":"ocs-storagecluster"},"storageProviderEndpoint":"","cephClusterFSID":"7a3d6b81-a55d-44fe-84d0-46c67cd395ca"}}`, | ||
"client2": `{"clusterId":"cluster2","name":"client2","providerInfo":{"version":"4.Y.Z","deploymentType":"internal","storageSystemName":"ocs-storagecluster-storagesystem","providerManagedClusterName":"local-cluster","namespacedName":{"Namespace":"openshift-storage","Name":"ocs-storagecluster"},"storageProviderEndpoint":"","cephClusterFSID":"7a3d6b81-a55d-44fe-84d0-46c67cd395ca"}}`, | ||
} | ||
|
||
assert.Equal(t, expectedData, cm.Data) | ||
assert.Equal(t, 1, len(cm.OwnerReferences)) | ||
assert.Equal(t, mcv.Name, cm.OwnerReferences[0].Name) | ||
assert.Equal(t, "ManagedClusterView", cm.OwnerReferences[0].Kind) | ||
assert.Equal(t, viewv1beta1.GroupVersion.String(), cm.OwnerReferences[0].APIVersion) | ||
|
||
}) | ||
|
||
t.Run("Update ConfigMap with additional owner reference", func(t *testing.T) { | ||
ctx := context.TODO() | ||
data := map[string]string{ | ||
"openshift-storage_ocs-storagecluster.config.yaml": ` | ||
version: "4.Y.Z" | ||
deploymentType: "internal" | ||
clients: | ||
- name: "client1" | ||
clusterId: "cluster1" | ||
- name: "client2" | ||
clusterId: "cluster2" | ||
storageCluster: | ||
namespacedName: | ||
name: "ocs-storagecluster" | ||
namespace: "openshift-storage" | ||
storageProviderEndpoint: "" | ||
cephClusterFSID: "7a3d6b81-a55d-44fe-84d0-46c67cd395ca" | ||
storageSystemName: "ocs-storagecluster-storagesystem" | ||
`, | ||
} | ||
mcv := createManagedClusterView("new-view", "local-cluster", data) | ||
|
||
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{ | ||
"client1": `{"clusterId":"cluster1","name":"client1","providerInfo":{"version":"4.Y.Z","deploymentType":"internal","storageSystemName":"ocs-storagecluster-storagesystem","providerManagedClusterName":"local-cluster","namespacedName":{"Namespace":"openshift-storage","Name":"ocs-storagecluster"},"storageProviderEndpoint":"","cephClusterFSID":"7a3d6b81-a55d-44fe-84d0-46c67cd395ca"}}`, | ||
"client2": `{"clusterId":"cluster2","name":"client2","providerInfo":{"version":"4.Y.Z","deploymentType":"internal","storageSystemName":"ocs-storagecluster-storagesystem","providerManagedClusterName":"local-cluster","namespacedName":{"Namespace":"openshift-storage","Name":"ocs-storagecluster"},"storageProviderEndpoint":"","cephClusterFSID":"7a3d6b81-a55d-44fe-84d0-46c67cd395ca"}}`, | ||
} | ||
|
||
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
Oops, something went wrong.