From d743b7b27b681b4f897a42a03774e8e155fb1fe3 Mon Sep 17 00:00:00 2001 From: vbadrina Date: Thu, 4 Jul 2024 16:59:50 +0530 Subject: [PATCH 1/2] feat: Implement reconciliation for ManagedCluster and create ManagedClusterView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added reconciliation logic for `ManagedCluster` - Added tests for `ManagedClusterReconciler` reconciliation logic and ensure ManagedClusterViews function. - Reconciliation for ManagedCluster creates ManagedClusterView which pulls the ‘odf-info’ configmap onto the hub. - Updated RBAC rules in `config/rbac/role.yaml` to include permissions for ManagedClusterView resources. (ManagedCluster RBAC already there) - Plumbing to make the controllers work like the items above - Updated go.mod and go.sum to include `github.com/stolostron/multicloud-operators-foundation`. - Fixes to functions, tests and adding new predicate Signed-off-by: vbadrina --- ...er-orchestrator.clusterserviceversion.yaml | 12 +- config/rbac/role.yaml | 10 + controllers/managedcluster_controller.go | 137 ++++++++++ controllers/managedcluster_controller_test.go | 249 ++++++++++++++++++ controllers/manager.go | 10 + controllers/mirrorpeer_controller.go | 1 + controllers/utils/managedclusterview.go | 69 +++++ controllers/utils/managedclusterview_test.go | 85 ++++++ go.mod | 1 + go.sum | 2 + 10 files changed, 575 insertions(+), 1 deletion(-) create mode 100644 controllers/managedcluster_controller.go create mode 100644 controllers/managedcluster_controller_test.go create mode 100644 controllers/utils/managedclusterview.go create mode 100644 controllers/utils/managedclusterview_test.go diff --git a/bundle/manifests/odf-multicluster-orchestrator.clusterserviceversion.yaml b/bundle/manifests/odf-multicluster-orchestrator.clusterserviceversion.yaml index 6e2b830c..8e5746e2 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-12T13:14:27Z" olm.skipRange: "" operators.openshift.io/infrastructure-features: '["disconnected"]' operators.operatorframework.io/builder: operator-sdk-v1.34.1 @@ -224,6 +224,16 @@ spec: - roles verbs: - '*' + - apiGroups: + - view.open-cluster-management.io + resources: + - managedclusterviews + verbs: + - create + - get + - list + - update + - watch - apiGroups: - work.open-cluster-management.io resources: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 12794311..efee82c3 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -161,6 +161,16 @@ rules: - roles verbs: - '*' +- apiGroups: + - view.open-cluster-management.io + resources: + - managedclusterviews + verbs: + - create + - get + - list + - update + - watch - 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..5f4626a7 --- /dev/null +++ b/controllers/managedcluster_controller.go @@ -0,0 +1,137 @@ +package controllers + +import ( + "context" + "fmt" + "log/slog" + "strings" + + "github.com/red-hat-storage/odf-multicluster-orchestrator/controllers/utils" + viewv1beta1 "github.com/stolostron/multicloud-operators-foundation/pkg/apis/view/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "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/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type ManagedClusterReconciler struct { + Client client.Client + Logger *slog.Logger +} + +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") + + 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.processManagedClusterViews(ctx, managedCluster); 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 hasRequiredODFKey(mc *clusterv1.ManagedCluster) bool { + claims := mc.Status.ClusterClaims + for _, claim := range claims { + if claim.Name == OdfInfoClusterClaimNamespacedName { + return true + } + } + return false + +} +func (r *ManagedClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { + r.Logger.Info("Setting up ManagedClusterReconciler with manager") + managedClusterPredicate := predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + obj, ok := e.ObjectNew.(*clusterv1.ManagedCluster) + if !ok { + return false + } + return hasRequiredODFKey(obj) + }, + CreateFunc: func(e event.CreateEvent) bool { + obj, ok := e.Object.(*clusterv1.ManagedCluster) + if !ok { + return false + } + 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{}). + Complete(r) +} + +func (r *ManagedClusterReconciler) processManagedClusterViews(ctx context.Context, managedCluster clusterv1.ManagedCluster) error { + resourceType := "ConfigMap" + odfInfoConfigMapNamespacedName, err := getNamespacedNameForClusterInfo(managedCluster) + if err != nil { + return fmt.Errorf("error while getting NamespacedName of the %s. %w", resourceType, err) + } + + enabled := true + disabled := false + mcvOwnerRef := &metav1.OwnerReference{ + APIVersion: managedCluster.APIVersion, + Kind: managedCluster.Kind, + UID: managedCluster.UID, + Name: managedCluster.Name, + Controller: &enabled, + BlockOwnerDeletion: &disabled, + } + + mcv, operationResult, err := utils.CreateOrUpdateManagedClusterView(ctx, r.Client, odfInfoConfigMapNamespacedName.Name, odfInfoConfigMapNamespacedName.Namespace, resourceType, managedCluster.Name, mcvOwnerRef) + if err != nil { + return fmt.Errorf("failed to create or update ManagedClusterView. %w", err) + + } + r.Logger.Info(fmt.Sprintf("ManagedClusterView was %s", operationResult), "ManagedClusterView", mcv.Name) + + return nil +} + +func getNamespacedNameForClusterInfo(managedCluster clusterv1.ManagedCluster) (types.NamespacedName, error) { + clusterClaims := managedCluster.Status.ClusterClaims + for _, claim := range clusterClaims { + if claim.Name == OdfInfoClusterClaimNamespacedName { + namespacedName := strings.Split(claim.Value, "/") + if len(namespacedName) != 2 { + return types.NamespacedName{}, fmt.Errorf("invalid format for namespaced name claim: expected 'namespace/name', got '%s'", claim.Value) + } + return types.NamespacedName{Namespace: namespacedName[0], Name: namespacedName[1]}, nil + } + } + + return types.NamespacedName{}, fmt.Errorf("cannot find ClusterClaim %q in ManagedCluster status", OdfInfoClusterClaimNamespacedName) +} diff --git a/controllers/managedcluster_controller_test.go b/controllers/managedcluster_controller_test.go new file mode 100644 index 00000000..cfed12ee --- /dev/null +++ b/controllers/managedcluster_controller_test.go @@ -0,0 +1,249 @@ +//go:build unit +// +build unit + +package controllers + +import ( + "context" + "reflect" + "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, + 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", + }, + Status: clusterv1.ManagedClusterStatus{ + ClusterClaims: []clusterv1.ManagedClusterClaim{ + { + Name: OdfInfoClusterClaimNamespacedName, + Value: "openshift-storage/odf-info", + }, + }, + }, + } + + _ = client.Create(context.TODO(), &managedCluster) + + res, err := reconciler.Reconcile(context.TODO(), req) + assert.NoError(t, err) + assert.Equal(t, ctrl.Result{}, res) + }) +} + +func TestProcessManagedClusterViews(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, + Logger: logger, + } + + t.Run("ManagedClusterView does not exist. It should create it", func(t *testing.T) { + managedCluster := clusterv1.ManagedCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + }, + Status: clusterv1.ManagedClusterStatus{ + ClusterClaims: []clusterv1.ManagedClusterClaim{ + { + Name: OdfInfoClusterClaimNamespacedName, + Value: "openshift-storage/odf-info", + }, + }, + }, + } + err := reconciler.processManagedClusterViews(context.TODO(), managedCluster) + assert.NoError(t, err) + + createdMCV := &viewv1beta1.ManagedClusterView{} + err = client.Get(context.TODO(), types.NamespacedName{Name: utils.GetManagedClusterViewName("test-cluster"), Namespace: "test-cluster"}, createdMCV) + assert.NoError(t, err) + assert.Equal(t, utils.GetManagedClusterViewName("test-cluster"), 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: utils.GetManagedClusterViewName("test-cluster"), + Namespace: "test-cluster", + }, + Spec: viewv1beta1.ViewSpec{ + Scope: viewv1beta1.ViewScope{ + Name: "old-configmap", + Namespace: "old-namespace", + Resource: "ConfigMap", + }, + }, + } + _ = client.Create(context.TODO(), existingMCV) + + managedCluster := clusterv1.ManagedCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + }, + Status: clusterv1.ManagedClusterStatus{ + ClusterClaims: []clusterv1.ManagedClusterClaim{ + { + Name: OdfInfoClusterClaimNamespacedName, + Value: "openshift-storage/odf-info", + }, + }, + }, + } + + err := reconciler.processManagedClusterViews(context.TODO(), managedCluster) + assert.NoError(t, err) + + updatedMCV := &viewv1beta1.ManagedClusterView{} + _ = client.Get(context.TODO(), types.NamespacedName{Name: utils.GetManagedClusterViewName("test-cluster"), 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: utils.GetManagedClusterViewName("test-cluster"), + Namespace: "test-cluster", + }, + Spec: viewv1beta1.ViewSpec{ + Scope: viewv1beta1.ViewScope{ + Name: "odf-info", + Namespace: "openshift-storage", + Resource: "ConfigMap", + }, + }, + } + _ = client.Create(context.TODO(), existingMCV) + + managedCluster := clusterv1.ManagedCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + }, + Status: clusterv1.ManagedClusterStatus{ + ClusterClaims: []clusterv1.ManagedClusterClaim{ + { + Name: OdfInfoClusterClaimNamespacedName, + Value: "openshift-storage/odf-info", + }, + }, + }, + } + err := reconciler.processManagedClusterViews(context.TODO(), managedCluster) + assert.NoError(t, err) + }) +} + +func Test_getNamespacedNameForClusterInfo(t *testing.T) { + type args struct { + managedCluster clusterv1.ManagedCluster + } + tests := []struct { + name string + args args + want types.NamespacedName + wantErr bool + }{ + { + name: "Valid Namespaced Name Claim", + args: args{ + managedCluster: clusterv1.ManagedCluster{ + Status: clusterv1.ManagedClusterStatus{ + ClusterClaims: []clusterv1.ManagedClusterClaim{ + { + Name: OdfInfoClusterClaimNamespacedName, + Value: "namespace/name", + }, + }, + }, + }, + }, + want: types.NamespacedName{Namespace: "namespace", Name: "name"}, + wantErr: false, + }, + { + name: "Missing Namespaced Name Claim", + args: args{ + managedCluster: clusterv1.ManagedCluster{ + Status: clusterv1.ManagedClusterStatus{ + ClusterClaims: []clusterv1.ManagedClusterClaim{}, + }, + }, + }, + want: types.NamespacedName{}, + wantErr: true, + }, + { + name: "Invalid Format for Namespaced Name Claim", + args: args{ + managedCluster: clusterv1.ManagedCluster{ + Status: clusterv1.ManagedClusterStatus{ + ClusterClaims: []clusterv1.ManagedClusterClaim{ + { + Name: OdfInfoClusterClaimNamespacedName, + Value: "invalidformat", + }, + }, + }, + }, + }, + want: types.NamespacedName{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getNamespacedNameForClusterInfo(tt.args.managedCluster) + if (err != nil) != tt.wantErr { + t.Errorf("getNamespacedNameForClusterInfo() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getNamespacedNameForClusterInfo() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/controllers/manager.go b/controllers/manager.go index 3a9c99bb..455f48aa 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,14 @@ func (o *ManagerOptions) runManager() { os.Exit(1) } + if err = (&ManagedClusterReconciler{ + Client: mgr.GetClient(), + 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..33cc2a9f 100644 --- a/controllers/mirrorpeer_controller.go +++ b/controllers/mirrorpeer_controller.go @@ -71,6 +71,7 @@ const spokeClusterRoleBindingName = "spoke-clusterrole-bindings" //+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=console.openshift.io,resources=consoleplugins,verbs=get;list;create;update;watch +//+kubebuilder:rbac:groups=view.open-cluster-management.io,resources=managedclusterviews,verbs=get;list;watch;create;update //+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 //+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings,verbs=get;list;create;update;delete;watch,resourceNames=spoke-clusterrole-bindings diff --git a/controllers/utils/managedclusterview.go b/controllers/utils/managedclusterview.go new file mode 100644 index 00000000..d766ecfa --- /dev/null +++ b/controllers/utils/managedclusterview.go @@ -0,0 +1,69 @@ +package utils + +import ( + "context" + "fmt" + + viewv1beta1 "github.com/stolostron/multicloud-operators-foundation/pkg/apis/view/v1beta1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const MCVLabelKey = "multicluster.odf.openshift.io/cluster" +const MCVNameTemplate = "odf-multicluster-mcv-%s" + +func GetManagedClusterViewName(clusterName string) string { + return fmt.Sprintf(MCVNameTemplate, clusterName) +} + +func CreateOrUpdateManagedClusterView(ctx context.Context, client ctrlClient.Client, resourceToFindName string, resourceToFindNamespace string, resourceToFindType string, clusterName string, ownerRef *metav1.OwnerReference) (*viewv1beta1.ManagedClusterView, controllerutil.OperationResult, error) { + mcv := &viewv1beta1.ManagedClusterView{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetManagedClusterViewName(clusterName), + Namespace: clusterName, + }, + } + + operationResult, 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[CreatedByLabelKey] = "odf-multicluster-managedcluster-controller" + + if ownerRef != nil { + mcv.OwnerReferences = []metav1.OwnerReference{*ownerRef} + } + + return nil + }) + + if err != nil { + return nil, controllerutil.OperationResultNone, err + } + + return mcv, operationResult, nil +} + +func GetManagedClusterView(client ctrlClient.Client, name, namespace string) (*viewv1beta1.ManagedClusterView, error) { + mcv := &viewv1beta1.ManagedClusterView{} + err := client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: namespace}, mcv) + if err != nil { + if errors.IsNotFound(err) { + return nil, fmt.Errorf("ManagedClusterView %s not found in namespace %s", name, namespace) + } + return nil, fmt.Errorf("failed to get ManagedClusterView %s in namespace %s. %w", name, namespace, err) + } + return mcv, nil +} diff --git a/controllers/utils/managedclusterview_test.go b/controllers/utils/managedclusterview_test.go new file mode 100644 index 00000000..2cc357c5 --- /dev/null +++ b/controllers/utils/managedclusterview_test.go @@ -0,0 +1,85 @@ +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", nil) + assert.NoError(t, err) + assert.NotNil(t, mcv) + assert.Equal(t, GetManagedClusterViewName("managed-cluster-1"), 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, "odf-multicluster-managedcluster-controller", mcv.Labels[CreatedByLabelKey]) + }) +} + +func TestGetManagedClusterView(t *testing.T) { + s := runtime.NewScheme() + err := viewv1beta1.AddToScheme(s) + assert.NoError(t, err) + + clusterView := &viewv1beta1.ManagedClusterView{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-mcv", + Namespace: "test-namespace", + }, + } + + client := fake.NewClientBuilder().WithScheme(s).WithObjects(clusterView).Build() + + tests := []struct { + name string + mcvName string + namespace string + wantErr bool + }{ + { + name: "existing ManagedClusterView", + mcvName: "test-mcv", + namespace: "test-namespace", + wantErr: false, + }, + { + name: "non-existing ManagedClusterView", + mcvName: "non-existing-mcv", + namespace: "test-namespace", + wantErr: true, + }, + { + name: "existing ManagedClusterView in different namespace", + mcvName: "test-mcv", + namespace: "different-namespace", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetManagedClusterView(client, tt.mcvName, tt.namespace) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotNil(t, got) + assert.Equal(t, tt.mcvName, got.Name) + assert.Equal(t, tt.namespace, got.Namespace) + } + }) + } +} diff --git a/go.mod b/go.mod index eec567e7..b2fe3795 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/red-hat-storage/ocs-operator/api/v4 v4.0.0-20240717095253-b12449490cc8 github.com/rook/rook/pkg/apis v0.0.0-20240701212738-8e25300ad55a 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 d8e9bd6f..f03357d4 100644 --- a/go.sum +++ b/go.sum @@ -785,6 +785,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= From 3ed8293a06079877e5dc1b596a39054c9d2d1b75 Mon Sep 17 00:00:00 2001 From: vbadrina Date: Wed, 17 Jul 2024 16:11:01 +0530 Subject: [PATCH 2/2] 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 --- ...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