From 504f01e2d0391ea2d25ba3856bf6b6f7fce7c63c Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Wed, 24 Jan 2024 12:12:52 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20sync=20Secrets,=20ConfigMaps=20and=20Pe?= =?UTF-8?q?rsistentVolumesClaims=20to=20users=20n=E2=80=A6=20(#1799)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: sync Secrets, ConfigMaps and PersistentVolumesClaims to users namespaces Signed-off-by: Anatolii Bazko --- Makefile | 6 +- .../che-operator.clusterserviceversion.yaml | 4 +- controllers/che/checluster_controller.go | 30 +- controllers/che/cheobj_verifier.go | 15 +- controllers/che/cheobj_verifier_test.go | 4 +- controllers/usernamespace/namespacecache.go | 3 +- ...troller.go => usernamespace_controller.go} | 88 +-- ...st.go => usernamespace_controller_test.go} | 97 +--- .../usernamespace/usernamespace_utils.go | 44 ++ .../usernamespace/workspace_cm_syncer.go | 90 +++ .../usernamespace/workspace_cm_syncer_test.go | 312 +++++++++++ .../usernamespace/workspace_pvc_syncer.go | 61 +++ .../workspace_pvc_syncer_test.go | 127 +++++ .../usernamespace/workspace_secret_syncer.go | 90 +++ .../workspace_secret_syncer_test.go | 300 ++++++++++ .../workspaces_config_controller.go | 513 ++++++++++++++++++ main.go | 10 +- pkg/common/constants/constants.go | 1 + pkg/deploy/checluster.go | 23 + pkg/deploy/checluster_test.go | 45 ++ pkg/deploy/checluster_util.go | 44 -- pkg/deploy/checluster_util_test.go | 137 ----- pkg/deploy/pvc.go | 89 --- pkg/deploy/pvc_test.go | 48 -- pkg/deploy/sync.go | 95 +++- 25 files changed, 1743 insertions(+), 533 deletions(-) rename controllers/usernamespace/{controller.go => usernamespace_controller.go} (93%) rename controllers/usernamespace/{controller_test.go => usernamespace_controller_test.go} (87%) create mode 100644 controllers/usernamespace/usernamespace_utils.go create mode 100644 controllers/usernamespace/workspace_cm_syncer.go create mode 100644 controllers/usernamespace/workspace_cm_syncer_test.go create mode 100644 controllers/usernamespace/workspace_pvc_syncer.go create mode 100644 controllers/usernamespace/workspace_pvc_syncer_test.go create mode 100644 controllers/usernamespace/workspace_secret_syncer.go create mode 100644 controllers/usernamespace/workspace_secret_syncer_test.go create mode 100644 controllers/usernamespace/workspaces_config_controller.go delete mode 100644 pkg/deploy/checluster_util.go delete mode 100644 pkg/deploy/checluster_util_test.go delete mode 100644 pkg/deploy/pvc.go delete mode 100644 pkg/deploy/pvc_test.go diff --git a/Makefile b/Makefile index ebbd6539e..03d6e307a 100644 --- a/Makefile +++ b/Makefile @@ -336,7 +336,9 @@ genenerate-env: | select(.name=="che-operator") | .env[] | select(has("value")) - | "export \(.name)=\"\(.value)\""' \ + | "export \(.name)=\(.value)"' \ + | sed 's|"|\\"|g' \ + | sed -E 's|(.*)=(.*)|\1="\2"|g' \ > $(BASH_ENV_FILE) echo "export WATCH_NAMESPACE=$(ECLIPSE_CHE_NAMESPACE)" >> $(BASH_ENV_FILE) echo "[INFO] Created $(BASH_ENV_FILE)" @@ -348,6 +350,8 @@ genenerate-env: | .env[] | select(has("value")) | "\(.name)=\"\(.value)\""' \ + | sed 's|"|\\"|g' \ + | sed -E 's|(.*)=(.*)|\1="\2"|g' \ > $(VSCODE_ENV_FILE) echo "WATCH_NAMESPACE=$(ECLIPSE_CHE_NAMESPACE)" >> $(VSCODE_ENV_FILE) echo "[INFO] Created $(VSCODE_ENV_FILE)" diff --git a/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml b/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml index 44fdddd80..a7606ed1a 100644 --- a/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml +++ b/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml @@ -77,7 +77,7 @@ metadata: operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 repository: https://github.com/eclipse-che/che-operator support: Eclipse Foundation - name: eclipse-che.v7.81.0-829.next + name: eclipse-che.v7.81.0-830.next namespace: placeholder spec: apiservicedefinitions: {} @@ -1240,7 +1240,7 @@ spec: minKubeVersion: 1.19.0 provider: name: Eclipse Foundation - version: 7.81.0-829.next + version: 7.81.0-830.next webhookdefinitions: - admissionReviewVersions: - v1 diff --git a/controllers/che/checluster_controller.go b/controllers/che/checluster_controller.go index de8cec7f6..9374a460c 100644 --- a/controllers/che/checluster_controller.go +++ b/controllers/che/checluster_controller.go @@ -54,7 +54,6 @@ import ( chev2 "github.com/eclipse-che/che-operator/api/v2" networking "k8s.io/api/networking/v1" - "k8s.io/apimachinery/pkg/api/errors" ) // CheClusterReconciler reconciles a CheCluster object @@ -150,7 +149,7 @@ func (r *CheClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { } var toTrustedBundleConfigMapRequestMapper handler.MapFunc = func(obj client.Object) []ctrl.Request { - isTrusted, reconcileRequest := IsTrustedBundleConfigMap(r.nonCachedClient, r.namespace, obj) + isTrusted, reconcileRequest := IsTrustedBundleConfigMap(r.client, r.namespace, obj) if isTrusted { return []ctrl.Request{reconcileRequest} } @@ -158,7 +157,7 @@ func (r *CheClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { } var toEclipseCheRelatedObjRequestMapper handler.MapFunc = func(obj client.Object) []ctrl.Request { - isEclipseCheRelatedObj, reconcileRequest := IsEclipseCheRelatedObj(r.nonCachedClient, r.namespace, obj) + isEclipseCheRelatedObj, reconcileRequest := IsEclipseCheRelatedObj(r.client, r.namespace, obj) if isEclipseCheRelatedObj { return []ctrl.Request{reconcileRequest} } @@ -197,10 +196,6 @@ func (r *CheClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { IsController: true, OwnerType: &chev2.CheCluster{}, }). - Watches(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, &handler.EnqueueRequestForOwner{ - IsController: true, - OwnerType: &chev2.CheCluster{}, - }). Watches(&source.Kind{Type: &corev1.ConfigMap{}}, handler.EnqueueRequestsFromMapFunc(toTrustedBundleConfigMapRequestMapper), builder.WithPredicates(onAllExceptGenericEventsPredicate), @@ -251,16 +246,11 @@ func (r *CheClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) } // Fetch the CheCluster instance - checluster, err := r.GetCR(req) - - if err != nil { - if errors.IsNotFound(err) { - r.Log.Info("CheCluster Custom Resource not found.") - // Request object not found, could have been deleted after reconcile request. - // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. - // Return and don't requeue - return ctrl.Result{}, nil - } + checluster, err := deploy.FindCheClusterCRInNamespace(r.client, req.NamespacedName.Namespace) + if checluster == nil { + r.Log.Info("CheCluster Custom Resource not found.") + return ctrl.Result{}, nil + } else if err != nil { // Error reading the object - requeue the request. return ctrl.Result{}, err } @@ -305,9 +295,3 @@ func (r *CheClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{Requeue: !done}, nil } } - -func (r *CheClusterReconciler) GetCR(request ctrl.Request) (*chev2.CheCluster, error) { - checluster := &chev2.CheCluster{} - err := r.client.Get(context.TODO(), request.NamespacedName, checluster) - return checluster, err -} diff --git a/controllers/che/cheobj_verifier.go b/controllers/che/cheobj_verifier.go index 3ad146be6..f8a895f69 100644 --- a/controllers/che/cheobj_verifier.go +++ b/controllers/che/cheobj_verifier.go @@ -16,7 +16,6 @@ import ( "github.com/eclipse-che/che-operator/pkg/common/constants" "github.com/eclipse-che/che-operator/pkg/deploy" "github.com/eclipse-che/che-operator/pkg/deploy/tls" - "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -29,11 +28,8 @@ func IsTrustedBundleConfigMap(cl client.Client, watchNamespace string, obj clien return false, ctrl.Request{} } - checluster, num, _ := deploy.FindCheClusterCRInNamespace(cl, watchNamespace) - if num != 1 { - if num > 1 { - logrus.Warn("More than one checluster Custom Resource found.") - } + checluster, _ := deploy.FindCheClusterCRInNamespace(cl, watchNamespace) + if checluster == nil { return false, ctrl.Request{} } @@ -71,11 +67,8 @@ func IsEclipseCheRelatedObj(cl client.Client, watchNamespace string, obj client. return false, ctrl.Request{} } - checluster, num, _ := deploy.FindCheClusterCRInNamespace(cl, watchNamespace) - if num != 1 { - if num > 1 { - logrus.Warn("More than one checluster Custom Resource found.") - } + checluster, _ := deploy.FindCheClusterCRInNamespace(cl, watchNamespace) + if checluster == nil { return false, ctrl.Request{} } diff --git a/controllers/che/cheobj_verifier_test.go b/controllers/che/cheobj_verifier_test.go index 45b8cce0b..254189bd6 100644 --- a/controllers/che/cheobj_verifier_test.go +++ b/controllers/che/cheobj_verifier_test.go @@ -123,7 +123,7 @@ func TestIsTrustedBundleConfigMap(t *testing.T) { newTestObject.ObjectMeta.Labels = testCase.objLabels } - isEclipseCheObj, req := IsTrustedBundleConfigMap(deployContext.ClusterAPI.NonCachingClient, testCase.watchNamespace, newTestObject) + isEclipseCheObj, req := IsTrustedBundleConfigMap(deployContext.ClusterAPI.Client, testCase.watchNamespace, newTestObject) assert.Equal(t, testCase.expectedIsEclipseCheObj, isEclipseCheObj) if isEclipseCheObj { @@ -217,7 +217,7 @@ func TestIsEclipseCheRelatedObj(t *testing.T) { deployContext := test.GetDeployContext(nil, testCase.initObjects) testObject.ObjectMeta.Namespace = testCase.objNamespace - isEclipseCheObj, req := IsEclipseCheRelatedObj(deployContext.ClusterAPI.NonCachingClient, testCase.watchNamespace, testObject) + isEclipseCheObj, req := IsEclipseCheRelatedObj(deployContext.ClusterAPI.Client, testCase.watchNamespace, testObject) assert.Equal(t, testCase.expectedIsEclipseCheObj, isEclipseCheObj) if isEclipseCheObj { diff --git a/controllers/usernamespace/namespacecache.go b/controllers/usernamespace/namespacecache.go index 623d77842..c5d8c7a02 100644 --- a/controllers/usernamespace/namespacecache.go +++ b/controllers/usernamespace/namespacecache.go @@ -48,8 +48,9 @@ type namespaceInfo struct { CheCluster *types.NamespacedName } -func NewNamespaceCache() *namespaceCache { +func NewNamespaceCache(client client.Client) *namespaceCache { return &namespaceCache{ + client: client, knownNamespaces: map[string]namespaceInfo{}, lock: sync.Mutex{}, } diff --git a/controllers/usernamespace/controller.go b/controllers/usernamespace/usernamespace_controller.go similarity index 93% rename from controllers/usernamespace/controller.go rename to controllers/usernamespace/usernamespace_controller.go index 3114f9e1e..8d146bb73 100644 --- a/controllers/usernamespace/controller.go +++ b/controllers/usernamespace/usernamespace_controller.go @@ -29,7 +29,6 @@ import ( "github.com/devfile/devworkspace-operator/pkg/infrastructure" chev2 "github.com/eclipse-che/che-operator/api/v2" "github.com/eclipse-che/che-operator/controllers/che" - "github.com/eclipse-che/che-operator/controllers/devworkspace" "github.com/eclipse-che/che-operator/controllers/devworkspace/defaults" "github.com/eclipse-che/che-operator/pkg/deploy" projectv1 "github.com/openshift/api/project/v1" @@ -55,27 +54,28 @@ const ( ) type CheUserNamespaceReconciler struct { - client client.Client - scheme *runtime.Scheme - namespaceCache namespaceCache -} - -type eventRule struct { - check func(metav1.Object) bool - namespaces func(metav1.Object) []string + scheme *runtime.Scheme + client client.Client + nonCachedClient client.Client + namespaceCache *namespaceCache } var _ reconcile.Reconciler = (*CheUserNamespaceReconciler)(nil) -func NewReconciler() *CheUserNamespaceReconciler { - return &CheUserNamespaceReconciler{namespaceCache: *NewNamespaceCache()} +func NewCheUserNamespaceReconciler( + client client.Client, + noncachedClient client.Client, + scheme *runtime.Scheme, + namespaceCache *namespaceCache) *CheUserNamespaceReconciler { + + return &CheUserNamespaceReconciler{ + scheme: scheme, + client: client, + nonCachedClient: noncachedClient, + namespaceCache: namespaceCache} } func (r *CheUserNamespaceReconciler) SetupWithManager(mgr ctrl.Manager) error { - r.scheme = mgr.GetScheme() - r.client = mgr.GetClient() - r.namespaceCache.client = r.client - var obj client.Object if infrastructure.IsOpenShift() { obj = &projectv1.Project{} @@ -101,26 +101,6 @@ func (r *CheUserNamespaceReconciler) watchRulesForSecrets(ctx context.Context) h })) } -func asReconcileRequestsForNamespaces(obj metav1.Object, rules []eventRule) []reconcile.Request { - for _, r := range rules { - if r.check(obj) { - nss := r.namespaces(obj) - ret := make([]reconcile.Request, len(nss)) - for i, n := range nss { - ret[i] = reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: n, - }, - } - } - - return ret - } - } - - return []reconcile.Request{} -} - func (r *CheUserNamespaceReconciler) commonRules(ctx context.Context, namesInCheClusterNamespace ...string) []eventRule { return []eventRule{ { @@ -192,6 +172,10 @@ func (r *CheUserNamespaceReconciler) hasCheCluster(ctx context.Context, namespac } func (r *CheUserNamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + if req.Name == "" { + return ctrl.Result{}, nil + } + info, err := r.namespaceCache.ExamineNamespace(ctx, req.Name) if err != nil { logrus.Errorf("Failed to examine namespace %s for presence of Che user info labels: %v", req.Name, err) @@ -203,9 +187,10 @@ func (r *CheUserNamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Req return ctrl.Result{}, nil } - checluster := findManagingCheCluster(*info.CheCluster) - if checluster == nil { - return ctrl.Result{Requeue: true}, nil + checluster, err := deploy.FindCheClusterCRInNamespace(r.client, "") + if checluster == nil || err != nil { + // CheCluster is not found or error occurred, requeue the request + return ctrl.Result{}, err } // let's construct the deployContext to be able to use methods from v1 operator @@ -213,8 +198,7 @@ func (r *CheUserNamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Req CheCluster: checluster, ClusterAPI: chetypes.ClusterAPI{ Client: r.client, - NonCachingClient: r.client, - DiscoveryClient: nil, + NonCachingClient: r.nonCachedClient, Scheme: r.scheme, }, } @@ -257,30 +241,6 @@ func (r *CheUserNamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Req return ctrl.Result{}, nil } -func findManagingCheCluster(key types.NamespacedName) *chev2.CheCluster { - instances := devworkspace.GetCurrentCheClusterInstances() - if len(instances) == 0 { - return nil - } - - if len(instances) == 1 { - for k, v := range instances { - if key.Name == "" || (key.Name == k.Name && key.Namespace == k.Namespace) { - return &v - } - return nil - } - } - - ret, ok := instances[key] - - if ok { - return &ret - } else { - return nil - } -} - func (r *CheUserNamespaceReconciler) reconcileSelfSignedCert(ctx context.Context, deployContext *chetypes.DeployContext, targetNs string, checluster *chev2.CheCluster) error { if err := deleteLegacyObject("server-cert", &corev1.Secret{}, targetNs, checluster, deployContext); err != nil { return err diff --git a/controllers/usernamespace/controller_test.go b/controllers/usernamespace/usernamespace_controller_test.go similarity index 87% rename from controllers/usernamespace/controller_test.go rename to controllers/usernamespace/usernamespace_controller_test.go index 123aed4ea..a89782861 100644 --- a/controllers/usernamespace/controller_test.go +++ b/controllers/usernamespace/usernamespace_controller_test.go @@ -160,9 +160,10 @@ func setup(infraType devworkspaceinfra.Type, objs ...runtime.Object) (*runtime.S cl := fake.NewFakeClientWithScheme(scheme, objs...) r := &CheUserNamespaceReconciler{ - client: cl, - scheme: scheme, - namespaceCache: namespaceCache{ + client: cl, + nonCachedClient: cl, + scheme: scheme, + namespaceCache: &namespaceCache{ client: cl, knownNamespaces: map[string]namespaceInfo{}, lock: sync.Mutex{}, @@ -214,96 +215,6 @@ func TestSkipsUnlabeledNamespaces(t *testing.T) { }) } -func TestRequiresLabelsToMatchOneOfMultipleCheCluster(t *testing.T) { - test := func(t *testing.T, infraType devworkspaceinfra.Type, namespace metav1.Object) { - ctx := context.TODO() - scheme, cl, r := setup(infraType, namespace.(runtime.Object)) - setupCheCluster(t, ctx, cl, scheme, "che1", "che") - setupCheCluster(t, ctx, cl, scheme, "che2", "che") - - res, err := r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: namespace.GetName()}}) - assert.NoError(t, err, "Reconciliation should have succeeded.") - - assert.True(t, res.Requeue, "The reconciliation request should have been requeued.") - } - - t.Run("k8s", func(t *testing.T) { - test(t, devworkspaceinfra.Kubernetes, &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ns", - Labels: map[string]string{ - workspaceNamespaceOwnerUidLabel: "uid", - }, - }, - }) - }) - - t.Run("openshift", func(t *testing.T) { - test(t, devworkspaceinfra.OpenShiftv4, &projectv1.Project{ - ObjectMeta: metav1.ObjectMeta{ - Name: "prj", - Labels: map[string]string{ - workspaceNamespaceOwnerUidLabel: "uid", - }, - }, - }) - }) -} - -func TestMatchingCheClusterCanBeSelectedUsingLabels(t *testing.T) { - test := func(t *testing.T, infraType devworkspaceinfra.Type, namespace string, objs ...runtime.Object) { - ctx := context.TODO() - scheme, cl, r := setup(infraType, objs...) - setupCheCluster(t, ctx, cl, scheme, "che1", "che") - setupCheCluster(t, ctx, cl, scheme, "che2", "che") - - res, err := r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: namespace}}) - assert.NoError(t, err, "Reconciliation shouldn't have failed") - - assert.False(t, res.Requeue, "The reconciliation request should have succeeded but is requesting a requeue.") - } - - t.Run("k8s", func(t *testing.T) { - test(t, devworkspaceinfra.Kubernetes, - "ns", - &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ns", - Labels: map[string]string{ - workspaceNamespaceOwnerUidLabel: "uid", - cheNameLabel: "che", - cheNamespaceLabel: "che1", - }, - }, - }) - }) - - t.Run("openshift", func(t *testing.T) { - test(t, devworkspaceinfra.OpenShiftv4, - "ns", - &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ns", - Labels: map[string]string{ - workspaceNamespaceOwnerUidLabel: "uid", - cheNameLabel: "che", - cheNamespaceLabel: "che1", - }, - }, - }, - &projectv1.Project{ - ObjectMeta: metav1.ObjectMeta{ - Name: "prj", - Labels: map[string]string{ - workspaceNamespaceOwnerUidLabel: "uid", - cheNameLabel: "che", - cheNamespaceLabel: "che1", - }, - }, - }) - }) -} - func TestCreatesDataInNamespace(t *testing.T) { infrastructure.InitializeForTesting(infrastructure.Kubernetes) diff --git a/controllers/usernamespace/usernamespace_utils.go b/controllers/usernamespace/usernamespace_utils.go new file mode 100644 index 000000000..3a5b2c96d --- /dev/null +++ b/controllers/usernamespace/usernamespace_utils.go @@ -0,0 +1,44 @@ +// +// Copyright (c) 2019-2023 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package usernamespace + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type eventRule struct { + check func(metav1.Object) bool + namespaces func(metav1.Object) []string +} + +func asReconcileRequestsForNamespaces(obj metav1.Object, rules []eventRule) []reconcile.Request { + for _, r := range rules { + if r.check(obj) { + nss := r.namespaces(obj) + ret := make([]reconcile.Request, len(nss)) + for i, n := range nss { + ret[i] = reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: n, + }, + } + } + + return ret + } + } + + return []reconcile.Request{} +} diff --git a/controllers/usernamespace/workspace_cm_syncer.go b/controllers/usernamespace/workspace_cm_syncer.go new file mode 100644 index 000000000..16d9d2172 --- /dev/null +++ b/controllers/usernamespace/workspace_cm_syncer.go @@ -0,0 +1,90 @@ +// +// Copyright (c) 2019-2023 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package usernamespace + +import ( + dwconstants "github.com/devfile/devworkspace-operator/pkg/constants" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + v1ConfigMapGKV = corev1.SchemeGroupVersion.WithKind("ConfigMap") +) + +type configMapSyncer struct { + workspaceConfigSyncer +} + +func newConfigMapSyncer() *configMapSyncer { + return &configMapSyncer{} +} + +func (p *configMapSyncer) gkv() schema.GroupVersionKind { + return v1ConfigMapGKV +} + +func (p *configMapSyncer) newObjectFrom(src client.Object) client.Object { + dst := src.(runtime.Object).DeepCopyObject() + dst.(*corev1.ConfigMap).ObjectMeta = metav1.ObjectMeta{ + Name: src.GetName(), + Annotations: src.GetAnnotations(), + Labels: mergeWorkspaceConfigObjectLabels( + src.GetLabels(), + map[string]string{ + dwconstants.DevWorkspaceWatchConfigMapLabel: "true", + dwconstants.DevWorkspaceMountLabel: "true", + }, + ), + } + + return dst.(client.Object) +} + +func (p *configMapSyncer) isExistedObjChanged(newObj client.Object, existedObj client.Object) bool { + if newObj.GetLabels() != nil { + for key, value := range newObj.GetLabels() { + if existedObj.GetLabels()[key] != value { + return true + } + } + } + + if newObj.GetAnnotations() != nil { + for key, value := range newObj.GetAnnotations() { + if existedObj.GetAnnotations()[key] != value { + return true + } + } + } + + return cmp.Diff( + newObj, + existedObj, + cmp.Options{ + cmpopts.IgnoreFields(corev1.ConfigMap{}, "TypeMeta", "ObjectMeta"), + }) != "" +} + +func (p *configMapSyncer) getObjectList() client.ObjectList { + return &corev1.ConfigMapList{} +} + +func (p *configMapSyncer) hasReadOnlySpec() bool { + return false +} diff --git a/controllers/usernamespace/workspace_cm_syncer_test.go b/controllers/usernamespace/workspace_cm_syncer_test.go new file mode 100644 index 000000000..c1616626f --- /dev/null +++ b/controllers/usernamespace/workspace_cm_syncer_test.go @@ -0,0 +1,312 @@ +// +// Copyright (c) 2019-2023 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package usernamespace + +import ( + "context" + "testing" + + "github.com/eclipse-che/che-operator/pkg/common/constants" + "github.com/eclipse-che/che-operator/pkg/common/test" + "github.com/eclipse-che/che-operator/pkg/common/utils" + "github.com/eclipse-che/che-operator/pkg/deploy" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/pointer" + + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/types" +) + +const ( + eclipseCheNamespace = "eclipse-che" + userNamespace = "user-namespace" + objectName = "che-workspaces-config" +) + +var ( + objectKeyInUserNs = types.NamespacedName{Name: objectName, Namespace: userNamespace} + objectKeyInCheNs = types.NamespacedName{Name: objectName, Namespace: eclipseCheNamespace} +) + +func TestSyncConfigMap(t *testing.T) { + deployContext := test.GetDeployContext(nil, []runtime.Object{ + &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: objectName, + Namespace: "eclipse-che", + Labels: map[string]string{ + constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg, + constants.KubernetesComponentLabelKey: constants.WorkspacesConfig, + }, + Annotations: map[string]string{}, + }, + Data: map[string]string{ + "a": "b", + }, + Immutable: pointer.Bool(false), + }}) + + workspaceConfigReconciler := NewWorkspacesConfigReconciler( + deployContext.ClusterAPI.Client, + deployContext.ClusterAPI.NonCachingClient, + deployContext.ClusterAPI.Scheme, + NewNamespaceCache(deployContext.ClusterAPI.NonCachingClient)) + + // Sync ConfigMap + err := workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1ConfigMapGKV) + + // Check ConfigMap in a user namespace is created + cm := &corev1.ConfigMap{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, cm) + assert.Nil(t, err) + assert.Equal(t, "b", cm.Data["a"]) + assert.Equal(t, false, *cm.Immutable) + assert.Equal(t, constants.WorkspacesConfig, cm.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, cm.Labels[constants.KubernetesPartOfLabelKey]) + assert.Equal(t, "true", cm.Labels["controller.devfile.io/watch-configmap"]) + assert.Equal(t, "true", cm.Labels["controller.devfile.io/mount-to-devworkspace"]) + + // Update src ConfigMap + cm = &corev1.ConfigMap{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInCheNs, cm) + assert.Nil(t, err) + cm.Data["a"] = "c" + err = workspaceConfigReconciler.client.Update(context.TODO(), cm) + assert.Nil(t, err) + + // Sync ConfigMap + err = workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1ConfigMapGKV) + + // Check that destination ConfigMap is updated + cm = &corev1.ConfigMap{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, cm) + assert.Nil(t, err) + assert.Equal(t, "c", cm.Data["a"]) + assert.Equal(t, false, *cm.Immutable) + assert.Equal(t, constants.WorkspacesConfig, cm.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, cm.Labels[constants.KubernetesPartOfLabelKey]) + assert.Equal(t, "true", cm.Labels["controller.devfile.io/watch-configmap"]) + assert.Equal(t, "true", cm.Labels["controller.devfile.io/mount-to-devworkspace"]) + + // Update dst ConfigMap + cm = &corev1.ConfigMap{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, cm) + assert.Nil(t, err) + cm.Data["a"] = "new-c" + err = workspaceConfigReconciler.client.Update(context.TODO(), cm) + assert.Nil(t, err) + + // Sync ConfigMap + err = workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1ConfigMapGKV) + + // Check that destination ConfigMap is reverted + cm = &corev1.ConfigMap{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, cm) + assert.Nil(t, err) + assert.Equal(t, "c", cm.Data["a"]) + assert.Equal(t, false, *cm.Immutable) + assert.Equal(t, constants.WorkspacesConfig, cm.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, cm.Labels[constants.KubernetesPartOfLabelKey]) + assert.Equal(t, "true", cm.Labels["controller.devfile.io/watch-configmap"]) + assert.Equal(t, "true", cm.Labels["controller.devfile.io/mount-to-devworkspace"]) + + // Update dst ConfigMap in the way that it won't be reverted + cm = &corev1.ConfigMap{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, cm) + assert.Nil(t, err) + cm.Annotations = map[string]string{"new-annotation": "new-test"} + utils.AddMap(cm.Labels, map[string]string{"new-label": "new-test"}) + err = workspaceConfigReconciler.client.Update(context.TODO(), cm) + assert.Nil(t, err) + + // Sync ConfigMap + err = workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1ConfigMapGKV) + + // Check that destination ConfigMap is not reverted + cm = &corev1.ConfigMap{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, cm) + assert.Nil(t, err) + assert.Equal(t, "c", cm.Data["a"]) + assert.Equal(t, false, *cm.Immutable) + assert.Equal(t, constants.WorkspacesConfig, cm.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, cm.Labels[constants.KubernetesPartOfLabelKey]) + assert.Equal(t, "true", cm.Labels["controller.devfile.io/watch-configmap"]) + assert.Equal(t, "true", cm.Labels["controller.devfile.io/mount-to-devworkspace"]) + assert.Equal(t, "new-test", cm.Labels["new-label"]) + assert.Equal(t, "new-test", cm.Annotations["new-annotation"]) + + // Delete dst ConfigMap + err = deploy.DeleteIgnoreIfNotFound(context.TODO(), deployContext.ClusterAPI.Client, objectKeyInUserNs, &corev1.ConfigMap{}) + assert.Nil(t, err) + + // Sync ConfigMap + err = workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1ConfigMapGKV) + + // Check that destination ConfigMap is reverted + cm = &corev1.ConfigMap{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, cm) + assert.Nil(t, err) + assert.Equal(t, "c", cm.Data["a"]) + assert.Equal(t, false, *cm.Immutable) + assert.Equal(t, constants.WorkspacesConfig, cm.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, cm.Labels[constants.KubernetesPartOfLabelKey]) + assert.Equal(t, "true", cm.Labels["controller.devfile.io/watch-configmap"]) + assert.Equal(t, "true", cm.Labels["controller.devfile.io/mount-to-devworkspace"]) + + // Delete src ConfigMap + err = deploy.DeleteIgnoreIfNotFound(context.TODO(), deployContext.ClusterAPI.Client, objectKeyInCheNs, &corev1.ConfigMap{}) + assert.Nil(t, err) + + // Sync ConfigMap + err = workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 0, v1ConfigMapGKV) + + // Check that destination ConfigMap in a user namespace is deleted + cm = &corev1.ConfigMap{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, cm) + assert.NotNil(t, err) + assert.True(t, errors.IsNotFound(err)) +} + +func TestSyncConfigMapShouldMergeLabelsAndAnnotationsOnUpdate(t *testing.T) { + deployContext := test.GetDeployContext(nil, []runtime.Object{ + &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: objectName, + Namespace: "eclipse-che", + Labels: map[string]string{ + "label": "label-value", + constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg, + constants.KubernetesComponentLabelKey: constants.WorkspacesConfig, + }, + Annotations: map[string]string{ + "annotation": "annotation-value", + }, + }, + Data: map[string]string{ + "a": "b", + }, + }}) + + workspaceConfigReconciler := NewWorkspacesConfigReconciler( + deployContext.ClusterAPI.Client, + deployContext.ClusterAPI.NonCachingClient, + deployContext.ClusterAPI.Scheme, + NewNamespaceCache(deployContext.ClusterAPI.NonCachingClient)) + + // Sync ConfigMap + err := workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1ConfigMapGKV) + + // Check ConfigMap in a user namespace is created + cm := &corev1.ConfigMap{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, cm) + assert.Nil(t, err) + assert.Equal(t, constants.WorkspacesConfig, cm.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, cm.Labels[constants.KubernetesPartOfLabelKey]) + assert.Equal(t, "true", cm.Labels["controller.devfile.io/watch-configmap"]) + assert.Equal(t, "true", cm.Labels["controller.devfile.io/mount-to-devworkspace"]) + assert.Equal(t, "label-value", cm.Labels["label"]) + assert.Equal(t, "annotation-value", cm.Annotations["annotation"]) + + // Update labels and annotations on dst ConfigMap + cm = &corev1.ConfigMap{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, cm) + assert.Nil(t, err) + utils.AddMap(cm.Labels, map[string]string{"new-label": "new-label-value"}) + utils.AddMap(cm.Annotations, map[string]string{"new-annotation": "new-annotation-value"}) + err = workspaceConfigReconciler.client.Update(context.TODO(), cm) + assert.Nil(t, err) + + // Sync ConfigMap + err = workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1ConfigMapGKV) + + // Check that destination ConfigMap is not reverted + cm = &corev1.ConfigMap{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, cm) + assert.Nil(t, err) + assert.Equal(t, constants.WorkspacesConfig, cm.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, cm.Labels[constants.KubernetesPartOfLabelKey]) + assert.Equal(t, "true", cm.Labels["controller.devfile.io/watch-configmap"]) + assert.Equal(t, "true", cm.Labels["controller.devfile.io/mount-to-devworkspace"]) + assert.Equal(t, "label-value", cm.Labels["label"]) + assert.Equal(t, "new-label-value", cm.Labels["new-label"]) + assert.Equal(t, "annotation-value", cm.Annotations["annotation"]) + assert.Equal(t, "new-annotation-value", cm.Annotations["new-annotation"]) + + // Update src ConfigMap + cm = &corev1.ConfigMap{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInCheNs, cm) + assert.Nil(t, err) + cm.Data["a"] = "c" + utils.AddMap(cm.Labels, map[string]string{"label": "label-value-2"}) + utils.AddMap(cm.Annotations, map[string]string{"annotation": "annotation-value-2"}) + err = workspaceConfigReconciler.client.Update(context.TODO(), cm) + assert.Nil(t, err) + + // Sync ConfigMap + err = workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1ConfigMapGKV) + + // Check that destination ConfigMap is updated but old labels and annotations are preserved + cm = &corev1.ConfigMap{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, cm) + assert.Nil(t, err) + assert.Equal(t, "c", cm.Data["a"]) + assert.Equal(t, constants.WorkspacesConfig, cm.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, cm.Labels[constants.KubernetesPartOfLabelKey]) + assert.Equal(t, "true", cm.Labels["controller.devfile.io/watch-configmap"]) + assert.Equal(t, "true", cm.Labels["controller.devfile.io/mount-to-devworkspace"]) + assert.Equal(t, "label-value-2", cm.Labels["label"]) + assert.Equal(t, "new-label-value", cm.Labels["new-label"]) + assert.Equal(t, "annotation-value-2", cm.Annotations["annotation"]) + assert.Equal(t, "new-annotation-value", cm.Annotations["new-annotation"]) +} + +func assertSyncConfig(t *testing.T, workspaceConfigReconciler *WorkspacesConfigReconciler, expectedNumberOfRecords int, gkv schema.GroupVersionKind) { + cm, err := workspaceConfigReconciler.getSyncConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assert.Equal(t, expectedNumberOfRecords, len(cm.Data)) + if expectedNumberOfRecords == 2 { + assert.Contains(t, cm.Data, buildKey(gkv, objectName, userNamespace)) + assert.Contains(t, cm.Data, buildKey(gkv, objectName, eclipseCheNamespace)) + } +} diff --git a/controllers/usernamespace/workspace_pvc_syncer.go b/controllers/usernamespace/workspace_pvc_syncer.go new file mode 100644 index 000000000..d1d35dc3f --- /dev/null +++ b/controllers/usernamespace/workspace_pvc_syncer.go @@ -0,0 +1,61 @@ +// +// Copyright (c) 2019-2023 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package usernamespace + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + v1PvcGKV = corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim") +) + +type pvcSyncer struct { + workspaceConfigSyncer +} + +func newPvcSyncer() *pvcSyncer { + return &pvcSyncer{} +} + +func (p *pvcSyncer) gkv() schema.GroupVersionKind { + return v1PvcGKV +} + +func (p *pvcSyncer) newObjectFrom(src client.Object) client.Object { + dst := src.(runtime.Object).DeepCopyObject() + dst.(*corev1.PersistentVolumeClaim).ObjectMeta = metav1.ObjectMeta{ + Name: src.GetName(), + Annotations: src.GetAnnotations(), + Labels: src.GetLabels(), + } + dst.(*corev1.PersistentVolumeClaim).Status = corev1.PersistentVolumeClaimStatus{} + + return dst.(client.Object) +} + +func (p *pvcSyncer) isExistedObjChanged(newObj client.Object, existedObj client.Object) bool { + return false +} + +func (p *pvcSyncer) getObjectList() client.ObjectList { + return &corev1.PersistentVolumeClaimList{} +} + +func (p *pvcSyncer) hasReadOnlySpec() bool { + return true +} diff --git a/controllers/usernamespace/workspace_pvc_syncer_test.go b/controllers/usernamespace/workspace_pvc_syncer_test.go new file mode 100644 index 000000000..954802b32 --- /dev/null +++ b/controllers/usernamespace/workspace_pvc_syncer_test.go @@ -0,0 +1,127 @@ +// +// Copyright (c) 2019-2023 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package usernamespace + +import ( + "context" + "testing" + + "github.com/eclipse-che/che-operator/pkg/deploy" + "k8s.io/apimachinery/pkg/api/errors" + + "github.com/eclipse-che/che-operator/pkg/common/constants" + "github.com/eclipse-che/che-operator/pkg/common/test" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestSyncPVC(t *testing.T) { + deployContext := test.GetDeployContext(nil, []runtime.Object{ + &corev1.PersistentVolumeClaim{ + TypeMeta: metav1.TypeMeta{ + Kind: "PersistentVolumeClaim", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: objectName, + Namespace: "eclipse-che", + Labels: map[string]string{ + constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg, + constants.KubernetesComponentLabelKey: constants.WorkspacesConfig, + }, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("1Gi"), + }, + }, + }, + }}) + + workspaceConfigReconciler := NewWorkspacesConfigReconciler( + deployContext.ClusterAPI.Client, + deployContext.ClusterAPI.NonCachingClient, + deployContext.ClusterAPI.Scheme, + NewNamespaceCache(deployContext.ClusterAPI.NonCachingClient)) + + assertSyncConfig(t, workspaceConfigReconciler, 0, v1PvcGKV) + + // Sync PVC to a user namespace + err := workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1PvcGKV) + + // Check if PVC in a user namespace is created + pvc := &corev1.PersistentVolumeClaim{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, pvc) + assert.Nil(t, err) + assert.Equal(t, constants.WorkspacesConfig, pvc.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, pvc.Labels[constants.KubernetesPartOfLabelKey]) + assert.True(t, pvc.Spec.Resources.Requests[corev1.ResourceStorage].Equal(resource.MustParse("1Gi"))) + + // Update src PVC + pvc = &corev1.PersistentVolumeClaim{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInCheNs, pvc) + assert.Nil(t, err) + pvc.Spec.Resources.Requests[corev1.ResourceStorage] = resource.MustParse("2Gi") + err = workspaceConfigReconciler.client.Update(context.TODO(), pvc) + + // Sync PVC + err = workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1PvcGKV) + + // Check that destination PVC is not updated + pvc = &corev1.PersistentVolumeClaim{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, pvc) + assert.Nil(t, err) + assert.Equal(t, constants.WorkspacesConfig, pvc.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, pvc.Labels[constants.KubernetesPartOfLabelKey]) + assert.True(t, pvc.Spec.Resources.Requests[corev1.ResourceStorage].Equal(resource.MustParse("1Gi"))) + + // Delete dst PVC + err = deploy.DeleteIgnoreIfNotFound(context.TODO(), workspaceConfigReconciler.client, objectKeyInUserNs, &corev1.PersistentVolumeClaim{}) + assert.Nil(t, err) + + // Sync PVC + err = workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1PvcGKV) + + // Check if PVC in a user namespace is created again + pvc = &corev1.PersistentVolumeClaim{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, pvc) + assert.Nil(t, err) + assert.Equal(t, constants.WorkspacesConfig, pvc.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, pvc.Labels[constants.KubernetesPartOfLabelKey]) + assert.True(t, pvc.Spec.Resources.Requests[corev1.ResourceStorage].Equal(resource.MustParse("2Gi"))) + + // Delete src PVC + err = deploy.DeleteIgnoreIfNotFound(context.TODO(), workspaceConfigReconciler.client, objectKeyInCheNs, &corev1.PersistentVolumeClaim{}) + assert.Nil(t, err) + + // Sync PVC + err = workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 0, v1PvcGKV) + + // Check that destination PersistentVolumeClaim in a user namespace is deleted + pvc = &corev1.PersistentVolumeClaim{} + err = deployContext.ClusterAPI.Client.Get(context.TODO(), objectKeyInUserNs, pvc) + assert.NotNil(t, err) + assert.True(t, errors.IsNotFound(err)) +} diff --git a/controllers/usernamespace/workspace_secret_syncer.go b/controllers/usernamespace/workspace_secret_syncer.go new file mode 100644 index 000000000..d32d509dc --- /dev/null +++ b/controllers/usernamespace/workspace_secret_syncer.go @@ -0,0 +1,90 @@ +// +// Copyright (c) 2019-2023 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package usernamespace + +import ( + dwconstants "github.com/devfile/devworkspace-operator/pkg/constants" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + v1SecretGKV = corev1.SchemeGroupVersion.WithKind("Secret") +) + +type secretSyncer struct { + workspaceConfigSyncer +} + +func newSecretSyncer() *secretSyncer { + return &secretSyncer{} +} + +func (p *secretSyncer) gkv() schema.GroupVersionKind { + return v1SecretGKV +} + +func (p *secretSyncer) newObjectFrom(src client.Object) client.Object { + dst := src.(runtime.Object).DeepCopyObject() + dst.(*corev1.Secret).ObjectMeta = metav1.ObjectMeta{ + Name: src.GetName(), + Annotations: src.GetAnnotations(), + Labels: mergeWorkspaceConfigObjectLabels( + src.GetLabels(), + map[string]string{ + dwconstants.DevWorkspaceWatchSecretLabel: "true", + dwconstants.DevWorkspaceMountLabel: "true", + }, + ), + } + + return dst.(client.Object) +} + +func (p *secretSyncer) isExistedObjChanged(newObj client.Object, existedObj client.Object) bool { + if newObj.GetLabels() != nil { + for key, value := range newObj.GetLabels() { + if existedObj.GetLabels()[key] != value { + return true + } + } + } + + if newObj.GetAnnotations() != nil { + for key, value := range newObj.GetAnnotations() { + if existedObj.GetAnnotations()[key] != value { + return true + } + } + } + + return cmp.Diff( + newObj, + existedObj, + cmp.Options{ + cmpopts.IgnoreFields(corev1.Secret{}, "TypeMeta", "ObjectMeta"), + }) != "" +} + +func (p *secretSyncer) getObjectList() client.ObjectList { + return &corev1.SecretList{} +} + +func (p *secretSyncer) hasReadOnlySpec() bool { + return false +} diff --git a/controllers/usernamespace/workspace_secret_syncer_test.go b/controllers/usernamespace/workspace_secret_syncer_test.go new file mode 100644 index 000000000..cbe591a7b --- /dev/null +++ b/controllers/usernamespace/workspace_secret_syncer_test.go @@ -0,0 +1,300 @@ +// +// Copyright (c) 2019-2023 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package usernamespace + +import ( + "context" + "testing" + + "github.com/eclipse-che/che-operator/pkg/common/utils" + + "github.com/eclipse-che/che-operator/pkg/deploy" + "k8s.io/apimachinery/pkg/api/errors" + + "github.com/eclipse-che/che-operator/pkg/common/constants" + "github.com/eclipse-che/che-operator/pkg/common/test" + "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/utils/pointer" +) + +func TestSyncSecrets(t *testing.T) { + deployContext := test.GetDeployContext(nil, []runtime.Object{ + &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: objectName, + Namespace: "eclipse-che", + Labels: map[string]string{ + constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg, + constants.KubernetesComponentLabelKey: constants.WorkspacesConfig, + }, + }, + StringData: map[string]string{ + "a": "b", + }, + Data: map[string][]byte{ + "c": []byte("d"), + }, + Immutable: pointer.Bool(false), + }}) + + workspaceConfigReconciler := NewWorkspacesConfigReconciler( + deployContext.ClusterAPI.Client, + deployContext.ClusterAPI.NonCachingClient, + deployContext.ClusterAPI.Scheme, + NewNamespaceCache(deployContext.ClusterAPI.NonCachingClient)) + + // Sync Secret + err := workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1SecretGKV) + + // Check Secret in a user namespace is created + secret := &corev1.Secret{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, secret) + assert.Nil(t, err) + assert.Equal(t, "b", secret.StringData["a"]) + assert.Equal(t, []byte("d"), secret.Data["c"]) + assert.Equal(t, false, *secret.Immutable) + assert.Equal(t, constants.WorkspacesConfig, secret.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, secret.Labels[constants.KubernetesPartOfLabelKey]) + assert.Equal(t, "true", secret.Labels["controller.devfile.io/watch-secret"]) + assert.Equal(t, "true", secret.Labels["controller.devfile.io/mount-to-devworkspace"]) + + // Update src Secret + secret = &corev1.Secret{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInCheNs, secret) + assert.Nil(t, err) + secret.StringData["a"] = "c" + secret.Annotations = map[string]string{ + "test": "test", + } + err = workspaceConfigReconciler.client.Update(context.TODO(), secret) + assert.Nil(t, err) + + // Sync Secret + err = workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1SecretGKV) + + // Check that destination Secret is updated + secret = &corev1.Secret{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, secret) + assert.Nil(t, err) + assert.Equal(t, "c", secret.StringData["a"]) + assert.Equal(t, []byte("d"), secret.Data["c"]) + assert.Equal(t, false, *secret.Immutable) + assert.Equal(t, constants.WorkspacesConfig, secret.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, secret.Labels[constants.KubernetesPartOfLabelKey]) + assert.Equal(t, "true", secret.Labels["controller.devfile.io/watch-secret"]) + assert.Equal(t, "true", secret.Labels["controller.devfile.io/mount-to-devworkspace"]) + assert.Equal(t, "test", secret.Annotations["test"]) + + // Update dst Secret + secret = &corev1.Secret{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, secret) + assert.Nil(t, err) + secret.StringData["a"] = "new-c" + err = workspaceConfigReconciler.client.Update(context.TODO(), secret) + assert.Nil(t, err) + + // Sync Secret + err = workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1SecretGKV) + + // Check that destination Secret is reverted + secret = &corev1.Secret{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, secret) + assert.Nil(t, err) + assert.Equal(t, "c", secret.StringData["a"]) + assert.Equal(t, []byte("d"), secret.Data["c"]) + assert.Equal(t, false, *secret.Immutable) + assert.Equal(t, constants.WorkspacesConfig, secret.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, secret.Labels[constants.KubernetesPartOfLabelKey]) + assert.Equal(t, "true", secret.Labels["controller.devfile.io/watch-secret"]) + assert.Equal(t, "true", secret.Labels["controller.devfile.io/mount-to-devworkspace"]) + + // Update dst Secret in the way that it won't be reverted + secret = &corev1.Secret{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, secret) + assert.Nil(t, err) + utils.AddMap(secret.Annotations, map[string]string{"new-annotation": "new-test"}) + utils.AddMap(secret.Labels, map[string]string{"new-label": "new-test"}) + err = workspaceConfigReconciler.client.Update(context.TODO(), secret) + assert.Nil(t, err) + + // Sync Secret + err = workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1SecretGKV) + + // Check that destination Secret is not reverted + secret = &corev1.Secret{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, secret) + assert.Nil(t, err) + assert.Equal(t, "c", secret.StringData["a"]) + assert.Equal(t, []byte("d"), secret.Data["c"]) + assert.Equal(t, false, *secret.Immutable) + assert.Equal(t, constants.WorkspacesConfig, secret.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, secret.Labels[constants.KubernetesPartOfLabelKey]) + assert.Equal(t, "true", secret.Labels["controller.devfile.io/watch-secret"]) + assert.Equal(t, "true", secret.Labels["controller.devfile.io/mount-to-devworkspace"]) + assert.Equal(t, "new-test", secret.Labels["new-label"]) + assert.Equal(t, "new-test", secret.Annotations["new-annotation"]) + + // Delete dst Secret + err = deploy.DeleteIgnoreIfNotFound(context.TODO(), deployContext.ClusterAPI.Client, objectKeyInUserNs, &corev1.Secret{}) + assert.Nil(t, err) + + // Sync Secret + err = workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1SecretGKV) + + // Check that destination Secret is reverted + secret = &corev1.Secret{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, secret) + assert.Nil(t, err) + assert.Equal(t, "c", secret.StringData["a"]) + assert.Equal(t, []byte("d"), secret.Data["c"]) + assert.Equal(t, false, *secret.Immutable) + assert.Equal(t, constants.WorkspacesConfig, secret.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, secret.Labels[constants.KubernetesPartOfLabelKey]) + assert.Equal(t, "true", secret.Labels["controller.devfile.io/watch-secret"]) + assert.Equal(t, "true", secret.Labels["controller.devfile.io/mount-to-devworkspace"]) + + // Delete src Secret + err = deploy.DeleteIgnoreIfNotFound(context.TODO(), deployContext.ClusterAPI.Client, objectKeyInCheNs, &corev1.Secret{}) + assert.Nil(t, err) + + // Sync Secret + err = workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 0, v1SecretGKV) + + // Check that destination Secret in a user namespace is deleted + secret = &corev1.Secret{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, secret) + assert.NotNil(t, err) + assert.True(t, errors.IsNotFound(err)) +} + +func TestSyncSecretShouldMergeLabelsAndAnnotationsOnUpdate(t *testing.T) { + deployContext := test.GetDeployContext(nil, []runtime.Object{ + &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: objectName, + Namespace: "eclipse-che", + Labels: map[string]string{ + "label": "label-value", + constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg, + constants.KubernetesComponentLabelKey: constants.WorkspacesConfig, + }, + Annotations: map[string]string{ + "annotation": "annotation-value", + }, + }, + StringData: map[string]string{ + "a": "b", + }, + }}) + + workspaceConfigReconciler := NewWorkspacesConfigReconciler( + deployContext.ClusterAPI.Client, + deployContext.ClusterAPI.NonCachingClient, + deployContext.ClusterAPI.Scheme, + NewNamespaceCache(deployContext.ClusterAPI.NonCachingClient)) + + // Sync Secret + err := workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1SecretGKV) + + // Check Secret in a user namespace is created + secret := &corev1.Secret{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, secret) + assert.Nil(t, err) + assert.Equal(t, constants.WorkspacesConfig, secret.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, secret.Labels[constants.KubernetesPartOfLabelKey]) + assert.Equal(t, "true", secret.Labels["controller.devfile.io/watch-secret"]) + assert.Equal(t, "true", secret.Labels["controller.devfile.io/mount-to-devworkspace"]) + assert.Equal(t, "label-value", secret.Labels["label"]) + assert.Equal(t, "annotation-value", secret.Annotations["annotation"]) + + // Update labels and annotations on dst Secret + secret = &corev1.Secret{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, secret) + assert.Nil(t, err) + utils.AddMap(secret.Labels, map[string]string{"new-label": "new-label-value"}) + utils.AddMap(secret.Annotations, map[string]string{"new-annotation": "new-annotation-value"}) + err = workspaceConfigReconciler.client.Update(context.TODO(), secret) + assert.Nil(t, err) + + // Sync Secret + err = workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1SecretGKV) + + // Check that destination Secret is not reverted + secret = &corev1.Secret{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, secret) + assert.Nil(t, err) + assert.Equal(t, constants.WorkspacesConfig, secret.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, secret.Labels[constants.KubernetesPartOfLabelKey]) + assert.Equal(t, "true", secret.Labels["controller.devfile.io/watch-secret"]) + assert.Equal(t, "true", secret.Labels["controller.devfile.io/mount-to-devworkspace"]) + assert.Equal(t, "label-value", secret.Labels["label"]) + assert.Equal(t, "new-label-value", secret.Labels["new-label"]) + assert.Equal(t, "annotation-value", secret.Annotations["annotation"]) + assert.Equal(t, "new-annotation-value", secret.Annotations["new-annotation"]) + + // Update src Secret + secret = &corev1.Secret{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInCheNs, secret) + assert.Nil(t, err) + secret.StringData["a"] = "c" + utils.AddMap(secret.Labels, map[string]string{"label": "label-value-2"}) + utils.AddMap(secret.Annotations, map[string]string{"annotation": "annotation-value-2"}) + err = workspaceConfigReconciler.client.Update(context.TODO(), secret) + assert.Nil(t, err) + + // Sync Secret + err = workspaceConfigReconciler.syncWorkspacesConfig(context.TODO(), userNamespace) + assert.Nil(t, err) + assertSyncConfig(t, workspaceConfigReconciler, 2, v1SecretGKV) + + // Check that destination Secret is updated but old labels and annotations are preserved + secret = &corev1.Secret{} + err = workspaceConfigReconciler.client.Get(context.TODO(), objectKeyInUserNs, secret) + assert.Nil(t, err) + assert.Equal(t, "c", secret.StringData["a"]) + assert.Equal(t, constants.WorkspacesConfig, secret.Labels[constants.KubernetesComponentLabelKey]) + assert.Equal(t, constants.CheEclipseOrg, secret.Labels[constants.KubernetesPartOfLabelKey]) + assert.Equal(t, "true", secret.Labels["controller.devfile.io/watch-secret"]) + assert.Equal(t, "true", secret.Labels["controller.devfile.io/mount-to-devworkspace"]) + assert.Equal(t, "label-value-2", secret.Labels["label"]) + assert.Equal(t, "new-label-value", secret.Labels["new-label"]) + assert.Equal(t, "annotation-value-2", secret.Annotations["annotation"]) + assert.Equal(t, "new-annotation-value", secret.Annotations["new-annotation"]) +} diff --git a/controllers/usernamespace/workspaces_config_controller.go b/controllers/usernamespace/workspaces_config_controller.go new file mode 100644 index 000000000..dfdf1c345 --- /dev/null +++ b/controllers/usernamespace/workspaces_config_controller.go @@ -0,0 +1,513 @@ +// +// Copyright (c) 2019-2023 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package usernamespace + +import ( + "context" + "fmt" + "strings" + + "github.com/eclipse-che/che-operator/pkg/common/utils" + + "github.com/eclipse-che/che-operator/pkg/common/constants" + "github.com/eclipse-che/che-operator/pkg/deploy" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +const ( + syncedWorkspacesConfig = "sync-workspaces-config" +) + +type WorkspacesConfigReconciler struct { + scheme *runtime.Scheme + client client.Client + nonCachedClient client.Client + namespaceCache *namespaceCache +} + +// Interface for syncing workspace config objects. +type workspaceConfigSyncer interface { + gkv() schema.GroupVersionKind + isExistedObjChanged(newObj client.Object, existedObj client.Object) bool + hasReadOnlySpec() bool + getObjectList() client.ObjectList + newObjectFrom(src client.Object) client.Object +} + +type syncContext struct { + dstNamespace string + srcNamespace string + ctx context.Context + syncer workspaceConfigSyncer + syncConfig map[string]string +} + +var ( + log = ctrl.Log.WithName("workspaces-config") + + workspacesConfigLabels = map[string]string{ + constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg, + constants.KubernetesComponentLabelKey: constants.WorkspacesConfig, + } + workspacesConfigSelector = labels.SelectorFromSet(workspacesConfigLabels) +) + +func NewWorkspacesConfigReconciler( + client client.Client, + noncachedClient client.Client, + scheme *runtime.Scheme, + namespaceCache *namespaceCache) *WorkspacesConfigReconciler { + + return &WorkspacesConfigReconciler{ + scheme: scheme, + client: client, + nonCachedClient: noncachedClient, + namespaceCache: namespaceCache, + } +} + +func (r *WorkspacesConfigReconciler) SetupWithManager(mgr ctrl.Manager) error { + ctx := context.Background() + bld := ctrl.NewControllerManagedBy(mgr). + For(&corev1.Namespace{}). + Watches(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, r.watchRules(ctx)). + Watches(&source.Kind{Type: &corev1.Secret{}}, r.watchRules(ctx)). + Watches(&source.Kind{Type: &corev1.ConfigMap{}}, r.watchRules(ctx)) + + return bld.Complete(r) +} + +func (r *WorkspacesConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + if req.Name == "" { + return ctrl.Result{}, nil + } + + info, err := r.namespaceCache.ExamineNamespace(ctx, req.Name) + if err != nil { + log.Error(err, "Failed to examine namespace", "namespace", req.Name) + return ctrl.Result{}, err + } + + if info == nil || !info.IsWorkspaceNamespace { + // namespace is not a workspace namespace, nothing to do + return ctrl.Result{}, nil + } + + if err = r.syncWorkspacesConfig(ctx, req.Name); err != nil { + log.Error(err, "Failed to sync workspace configs", "namespace", req.Name) + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func (r *WorkspacesConfigReconciler) watchRules(ctx context.Context) handler.EventHandler { + return handler.EnqueueRequestsFromMapFunc( + func(obj client.Object) []reconcile.Request { + return asReconcileRequestsForNamespaces(obj, + []eventRule{ + { + // reconcile rule when workspace config is modified in a user namespace + // to revert the config + check: func(o metav1.Object) bool { + workspaceInfo, _ := r.namespaceCache.GetNamespaceInfo(ctx, o.GetNamespace()) + return isLabeledAsWorkspacesConfig(o) && + o.GetName() != syncedWorkspacesConfig && + workspaceInfo != nil && + workspaceInfo.IsWorkspaceNamespace + }, + namespaces: func(o metav1.Object) []string { return []string{o.GetNamespace()} }, + }, + { + // reconcile rule when workspace config is modified in a che namespace + // to update the config in all users` namespaces + check: func(o metav1.Object) bool { + cheCluster, _ := deploy.FindCheClusterCRInNamespace(r.client, o.GetNamespace()) + return isLabeledAsWorkspacesConfig(o) && cheCluster != nil + }, + namespaces: func(o metav1.Object) []string { return r.namespaceCache.GetAllKnownNamespaces() }, + }}) + }) +} + +func (r *WorkspacesConfigReconciler) syncWorkspacesConfig(ctx context.Context, targetNs string) error { + checluster, err := deploy.FindCheClusterCRInNamespace(r.client, "") + if checluster == nil { + return nil + } + + syncedConfig, err := r.getSyncConfig(ctx, targetNs) + if err != nil { + log.Error(err, "Failed to get workspace sync config", "namespace", targetNs) + return nil + } + + defer func() { + if syncedConfig != nil { + if syncedConfig.GetResourceVersion() == "" { + if err := r.client.Create(ctx, syncedConfig); err != nil { + log.Error(err, "Failed to workspace create sync config", "namespace", targetNs) + } + } else { + if err := r.client.Update(ctx, syncedConfig); err != nil { + log.Error(err, "Failed to update workspace sync config", "namespace", targetNs) + } + } + } + }() + + if err := r.syncObjects( + &syncContext{ + dstNamespace: targetNs, + srcNamespace: checluster.GetNamespace(), + syncer: newConfigMapSyncer(), + syncConfig: syncedConfig.Data, + ctx: ctx, + }); err != nil { + return err + } + + if err := r.syncObjects( + &syncContext{ + dstNamespace: targetNs, + srcNamespace: checluster.GetNamespace(), + syncer: newSecretSyncer(), + syncConfig: syncedConfig.Data, + ctx: ctx, + }); err != nil { + return err + } + + if err := r.syncObjects( + &syncContext{ + dstNamespace: targetNs, + srcNamespace: checluster.GetNamespace(), + syncer: newPvcSyncer(), + syncConfig: syncedConfig.Data, + ctx: ctx, + }); err != nil { + return err + } + + return nil +} + +// syncObjects syncs objects from che namespace to target namespace. +func (r *WorkspacesConfigReconciler) syncObjects(syncContext *syncContext) error { + srcObjsList := syncContext.syncer.getObjectList() + if err := r.readSrcObjsList(syncContext.ctx, syncContext.srcNamespace, srcObjsList); err != nil { + return err + } + + srcObjs, err := meta.ExtractList(srcObjsList) + if err != nil { + return err + } + + for _, srcObj := range srcObjs { + newObj := syncContext.syncer.newObjectFrom(srcObj.(client.Object)) + newObj.SetNamespace(syncContext.dstNamespace) + + if err := r.syncObjectToNamespace(syncContext, srcObj.(client.Object), newObj); err != nil { + log.Error(err, "Failed to sync object", + "namespace", syncContext.dstNamespace, + "kind", gvk2String(syncContext.syncer.gkv()), + "name", newObj.GetName()) + return err + } + } + + actualSyncedSrcObjKeys := make(map[string]bool) + for _, srcObj := range srcObjs { + // compute actual synced objects keys from che namespace + actualSyncedSrcObjKeys[getKey(srcObj.(client.Object))] = true + } + + for syncObjKey, _ := range syncContext.syncConfig { + if err := r.deleteObsoleteObjectFromNamespace(syncContext, actualSyncedSrcObjKeys, syncObjKey); err != nil { + log.Error(err, "Failed to delete obsolete object", + "namespace", syncContext.dstNamespace, + "kind", gvk2String(syncContext.syncer.gkv()), + "name", getNameElement(syncObjKey)) + return err + } + } + + return nil +} + +// deleteObsoleteObjectFromNamespace deletes objects that are not synced with source objects. +// Returns error if delete failed in a destination namespace. +func (r *WorkspacesConfigReconciler) deleteObsoleteObjectFromNamespace( + syncContext *syncContext, + actualSyncedSrcObjKeys map[string]bool, + syncObjKey string, +) error { + isObjectOfGivenKind := getGVKElement(syncObjKey) == gvk2Element(syncContext.syncer.gkv()) + isObjectFromSrcNamespace := getNamespaceElement(syncObjKey) == syncContext.srcNamespace + isNotSyncedInTargetNs := !actualSyncedSrcObjKeys[syncObjKey] + + if isObjectOfGivenKind && isObjectFromSrcNamespace && isNotSyncedInTargetNs { + blueprint, err := r.scheme.New(syncContext.syncer.gkv()) + if err != nil { + return err + } + + // then delete object from target namespace if it is not synced with source object + if err := deploy.DeleteIgnoreIfNotFound( + syncContext.ctx, + r.client, + types.NamespacedName{ + Name: getNameElement(syncObjKey), + Namespace: syncContext.dstNamespace, + }, + blueprint.(client.Object)); err != nil { + return err + } + + delete(syncContext.syncConfig, syncObjKey) + delete(syncContext.syncConfig, + buildKey( + syncContext.syncer.gkv(), + getNameElement(syncObjKey), + syncContext.dstNamespace), + ) + } + + return nil +} + +// syncObjectToNamespace syncs source object to destination object if they differ. +// Returns error if sync failed in a destination namespace. +func (r *WorkspacesConfigReconciler) syncObjectToNamespace( + syncContext *syncContext, + srcObj client.Object, + newObj client.Object) error { + + existedDstObj, err := r.scheme.New(syncContext.syncer.gkv()) + if err != nil { + return err + } + + err = r.client.Get( + syncContext.ctx, + types.NamespacedName{ + Name: newObj.GetName(), + Namespace: newObj.GetNamespace()}, + existedDstObj.(client.Object)) + if err == nil { + // destination object exists, update it if it differs from source object + srcHasBeenChanged := syncContext.syncConfig[getKey(srcObj)] != srcObj.GetResourceVersion() + dstHasBeenChanged := syncContext.syncConfig[getKey(existedDstObj.(client.Object))] != existedDstObj.(client.Object).GetResourceVersion() + + if srcHasBeenChanged || dstHasBeenChanged { + return r.doSyncObjectToNamespace(syncContext, srcObj, newObj, existedDstObj.(client.Object)) + } + } else if errors.IsNotFound(err) { + // destination object does not exist, so it will be created + return r.doSyncObjectToNamespace(syncContext, srcObj, newObj, nil) + } else { + return err + } + + return nil +} + +// doSyncObjectToNamespace syncs source object to destination object by updating or creating it. +// Returns error if sync failed in a destination namespace. +func (r *WorkspacesConfigReconciler) doSyncObjectToNamespace( + syncContext *syncContext, + srcObj client.Object, + newObj client.Object, + existedObj client.Object) error { + + if existedObj == nil { + if err := r.client.Create(syncContext.ctx, newObj); err != nil { + return err + } + + syncContext.syncConfig[getKey(srcObj)] = srcObj.GetResourceVersion() + syncContext.syncConfig[buildKey( + syncContext.syncer.gkv(), + newObj.GetName(), + newObj.GetNamespace())] = newObj.GetResourceVersion() + + log.Info("Object created", + "namespace", newObj.GetNamespace(), + "kind", gvk2String(syncContext.syncer.gkv()), + "name", newObj.GetName()) + return nil + } else { + if syncContext.syncer.hasReadOnlySpec() { + // skip updating objects with readonly spec + // admin has to re-create them to update + // just update resource versions + syncContext.syncConfig[getKey(srcObj)] = srcObj.GetResourceVersion() + syncContext.syncConfig[getKey(existedObj)] = existedObj.GetResourceVersion() + + log.Info("Object skipped since has readonly spec, re-create it to update", + "namespace", newObj.GetNamespace(), + "kind", gvk2String(syncContext.syncer.gkv()), + "name", newObj.GetName()) + return nil + } else { + if syncContext.syncer.isExistedObjChanged(newObj, existedObj) { + // preserve labels and annotations from existed object + newObj.SetLabels(preserveExistedMapValues(newObj.GetLabels(), existedObj.GetLabels())) + newObj.SetAnnotations(preserveExistedMapValues(newObj.GetAnnotations(), existedObj.GetAnnotations())) + + // set the correct resource version to update object + newObj.SetResourceVersion(existedObj.GetResourceVersion()) + if err := r.client.Update(syncContext.ctx, newObj); err != nil { + return err + } + + syncContext.syncConfig[getKey(srcObj)] = srcObj.GetResourceVersion() + syncContext.syncConfig[getKey(existedObj)] = newObj.GetResourceVersion() + + log.Info("Object updated", + "namespace", newObj.GetNamespace(), + "kind", gvk2String(syncContext.syncer.gkv()), + "name", newObj.GetName()) + return nil + } else { + // nothing to update objects are equal + // just update resource versions + syncContext.syncConfig[getKey(srcObj)] = srcObj.GetResourceVersion() + syncContext.syncConfig[getKey(existedObj)] = existedObj.GetResourceVersion() + return nil + } + } + } +} + +// getSyncConfig returns ConfigMap with synced objects resource versions. +// Returns error if ConfigMap failed to be retrieved. +func (r *WorkspacesConfigReconciler) getSyncConfig(ctx context.Context, targetNs string) (*corev1.ConfigMap, error) { + syncedConfig := &corev1.ConfigMap{} + err := r.client.Get( + ctx, + types.NamespacedName{ + Name: syncedWorkspacesConfig, + Namespace: targetNs, + }, + syncedConfig) + + if err != nil { + if errors.IsNotFound(err) { + syncedConfig = &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: syncedWorkspacesConfig, + Namespace: targetNs, + Labels: workspacesConfigLabels, + }, + Data: map[string]string{}, + } + } else { + return nil, err + } + } else if syncedConfig.Data == nil { + syncedConfig.Data = map[string]string{} + } + + return syncedConfig, nil +} + +func (r *WorkspacesConfigReconciler) readSrcObjsList(ctx context.Context, srcNamespace string, objList client.ObjectList) error { + return r.client.List( + ctx, + objList, + &client.ListOptions{ + Namespace: srcNamespace, + LabelSelector: workspacesConfigSelector, + }) +} + +func getKey(object client.Object) string { + return buildKey(object.GetObjectKind().GroupVersionKind(), object.GetName(), object.GetNamespace()) +} + +func buildKey(gvk schema.GroupVersionKind, name string, namespace string) string { + return fmt.Sprintf("%s.%s.%s", gvk2Element(gvk), name, namespace) +} + +func gvk2Element(gvk schema.GroupVersionKind) string { + if gvk.Group == "" { + return fmt.Sprintf("%s_%s", gvk.Version, gvk.Kind) + } + return fmt.Sprintf("%s_%s_%s", gvk.Group, gvk.Version, gvk.Kind) +} + +func gvk2String(gkv schema.GroupVersionKind) string { + return fmt.Sprintf("%s.%s", gkv.Version, gkv.Kind) +} + +func getGVKElement(key string) string { + splits := strings.Split(key, ".") + return splits[0] +} + +func getNameElement(key string) string { + splits := strings.Split(key, ".") + return splits[1] +} + +func getNamespaceElement(key string) string { + splits := strings.Split(key, ".") + return splits[2] +} + +func isLabeledAsWorkspacesConfig(obj metav1.Object) bool { + return obj.GetLabels()[constants.KubernetesComponentLabelKey] == constants.WorkspacesConfig && + obj.GetLabels()[constants.KubernetesPartOfLabelKey] == constants.CheEclipseOrg +} + +func mergeWorkspaceConfigObjectLabels(srcLabels map[string]string, additionalLabels map[string]string) map[string]string { + newLabels := utils.CloneMap(srcLabels) + for key, value := range additionalLabels { + newLabels[key] = value + } + + // default labels + for key, value := range deploy.GetLabels(constants.WorkspacesConfig) { + newLabels[key] = value + } + + return newLabels +} + +func preserveExistedMapValues(newObjMap map[string]string, existedObjMap map[string]string) map[string]string { + preservedMap := utils.CloneMap(newObjMap) + for key, value := range existedObjMap { + if _, ok := preservedMap[key]; !ok { + preservedMap[key] = value + } + } + return preservedMap +} diff --git a/main.go b/main.go index de943a3f8..55b180b8a 100644 --- a/main.go +++ b/main.go @@ -283,12 +283,20 @@ func main() { os.Exit(1) } - userNamespaceReconciler := usernamespace.NewReconciler() + namespacechace := usernamespace.NewNamespaceCache(nonCachingClient) + + userNamespaceReconciler := usernamespace.NewCheUserNamespaceReconciler(mgr.GetClient(), nonCachingClient, mgr.GetScheme(), namespacechace) if err = userNamespaceReconciler.SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to set up controller", "controller", "CheUserReconciler") os.Exit(1) } + workspacesConfigReconciler := usernamespace.NewWorkspacesConfigReconciler(mgr.GetClient(), nonCachingClient, mgr.GetScheme(), namespacechace) + if err = workspacesConfigReconciler.SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to set up controller", "controller", "WorkspacesConfigReconciler") + os.Exit(1) + } + terminationPeriod := int64(20) if !test.IsTestMode() { namespace, err := infrastructure.GetOperatorNamespace() diff --git a/pkg/common/constants/constants.go b/pkg/common/constants/constants.go index 53745118c..17854d7be 100644 --- a/pkg/common/constants/constants.go +++ b/pkg/common/constants/constants.go @@ -128,6 +128,7 @@ const ( // common CheFlavor = "che" CheEclipseOrg = "che.eclipse.org" + WorkspacesConfig = "workspaces-config" InstallOrUpdateFailed = "InstallOrUpdateFailed" FinalizerSuffix = "finalizers.che.eclipse.org" diff --git a/pkg/deploy/checluster.go b/pkg/deploy/checluster.go index 1f0d41464..21b3e95ee 100644 --- a/pkg/deploy/checluster.go +++ b/pkg/deploy/checluster.go @@ -14,6 +14,9 @@ package deploy import ( "context" + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/client" chev2 "github.com/eclipse-che/che-operator/api/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -67,3 +70,23 @@ func ReloadCheClusterCR(deployContext *chetypes.DeployContext) error { deployContext.CheCluster = cheCluster return nil } + +// FindCheClusterCRInNamespace returns CheCluster custom resource from given namespace. +// If namespace is empty then checluster will be found in any namespace. +// Only one instance of CheCluster custom resource is expected. +func FindCheClusterCRInNamespace(cl client.Client, namespace string) (*chev2.CheCluster, error) { + cheClusters := &chev2.CheClusterList{} + if err := cl.List(context.TODO(), cheClusters, &client.ListOptions{Namespace: namespace}); err != nil { + return nil, err + } + + num := len(cheClusters.Items) + switch num { + case 0: + return nil, nil + case 1: + return &cheClusters.Items[0], nil + default: + return nil, fmt.Errorf("expected one instance of CheCluster custom resources, but '%d' found", len(cheClusters.Items)) + } +} diff --git a/pkg/deploy/checluster_test.go b/pkg/deploy/checluster_test.go index f0f719978..7ea4767ba 100644 --- a/pkg/deploy/checluster_test.go +++ b/pkg/deploy/checluster_test.go @@ -15,8 +15,10 @@ package deploy import ( chev2 "github.com/eclipse-che/che-operator/api/v2" "github.com/eclipse-che/che-operator/pkg/common/chetypes" + "github.com/eclipse-che/che-operator/pkg/common/test" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -66,3 +68,46 @@ func TestReload(t *testing.T) { assert.Equal(t, "1", ctx.CheCluster.ObjectMeta.ResourceVersion) assert.Nil(t, ctx.CheCluster.Spec.Components.PluginRegistry.OpenVSXURL) } + +func TestFindCheCRinNamespace(t *testing.T) { + type testCase struct { + checluster *chev2.CheCluster + name string + namespace string + found bool + } + + testCases := []testCase{ + { + name: "case #1", + checluster: &chev2.CheCluster{ObjectMeta: metav1.ObjectMeta{Name: "eclipse-che", Namespace: "eclipse-che"}}, + namespace: "eclipse-che", + found: true, + }, + { + name: "case #2", + checluster: &chev2.CheCluster{ObjectMeta: metav1.ObjectMeta{Name: "eclipse-che", Namespace: "default"}}, + namespace: "eclipse-che", + found: false, + }, + { + name: "case #3", + checluster: &chev2.CheCluster{ObjectMeta: metav1.ObjectMeta{Name: "eclipse-che", Namespace: "eclipse-che"}}, + namespace: "", + found: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.GetDeployContext(testCase.checluster, []runtime.Object{}) + checluster, err := FindCheClusterCRInNamespace(deployContext.ClusterAPI.Client, testCase.namespace) + if testCase.found { + assert.NoError(t, err) + assert.Equal(t, testCase.checluster.Name, checluster.Name) + } else { + assert.Nil(t, checluster) + } + }) + } +} diff --git a/pkg/deploy/checluster_util.go b/pkg/deploy/checluster_util.go deleted file mode 100644 index 80f55f6fa..000000000 --- a/pkg/deploy/checluster_util.go +++ /dev/null @@ -1,44 +0,0 @@ -// -// Copyright (c) 2019-2023 Red Hat, Inc. -// This program and the accompanying materials are made -// available under the terms of the Eclipse Public License 2.0 -// which is available at https://www.eclipse.org/legal/epl-2.0/ -// -// SPDX-License-Identifier: EPL-2.0 -// -// Contributors: -// Red Hat, Inc. - initial API and implementation -// - -package deploy - -import ( - "context" - "fmt" - - chev2 "github.com/eclipse-che/che-operator/api/v2" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// Finds checluster custom resource in a given namespace. -// If namespace is empty then checluster will be found in any namespace. -func FindCheClusterCRInNamespace(cl client.Client, namespace string) (*chev2.CheCluster, int, error) { - cheClusters := &chev2.CheClusterList{} - listOptions := &client.ListOptions{Namespace: namespace} - if err := cl.List(context.TODO(), cheClusters, listOptions); err != nil { - return nil, -1, err - } - - if len(cheClusters.Items) != 1 { - return nil, len(cheClusters.Items), fmt.Errorf("Expected one instance of CheCluster custom resources, but '%d' found.", len(cheClusters.Items)) - } - - checluster := &chev2.CheCluster{} - namespacedName := types.NamespacedName{Namespace: cheClusters.Items[0].GetNamespace(), Name: cheClusters.Items[0].GetName()} - err := cl.Get(context.TODO(), namespacedName, checluster) - if err != nil { - return nil, -1, err - } - return checluster, 1, nil -} diff --git a/pkg/deploy/checluster_util_test.go b/pkg/deploy/checluster_util_test.go deleted file mode 100644 index 32345b8d8..000000000 --- a/pkg/deploy/checluster_util_test.go +++ /dev/null @@ -1,137 +0,0 @@ -// -// Copyright (c) 2019-2023 Red Hat, Inc. -// This program and the accompanying materials are made -// available under the terms of the Eclipse Public License 2.0 -// which is available at https://www.eclipse.org/legal/epl-2.0/ -// -// SPDX-License-Identifier: EPL-2.0 -// -// Contributors: -// Red Hat, Inc. - initial API and implementation -// - -package deploy - -import ( - "testing" - - chev2 "github.com/eclipse-che/che-operator/api/v2" - "github.com/stretchr/testify/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/kubernetes/scheme" - "sigs.k8s.io/controller-runtime/pkg/client/fake" -) - -func TestFindCheCRinNamespace(t *testing.T) { - type testCase struct { - name string - initObjects []runtime.Object - watchNamespace string - expectedNumber int - expectedNamespace string - expectedErr bool - } - - testCases := []testCase{ - { - name: "CR in 'eclipse-che' namespace", - initObjects: []runtime.Object{ - &chev2.CheCluster{ObjectMeta: metav1.ObjectMeta{Name: "eclipse-che", Namespace: "eclipse-che"}}, - }, - watchNamespace: "eclipse-che", - expectedNumber: 1, - expectedErr: false, - expectedNamespace: "eclipse-che", - }, - { - name: "CR in 'default' namespace", - initObjects: []runtime.Object{ - &chev2.CheCluster{ObjectMeta: metav1.ObjectMeta{Name: "eclipse-che", Namespace: "default"}}, - }, - watchNamespace: "eclipse-che", - expectedNumber: 0, - expectedErr: true, - }, - { - name: "several CR in 'eclipse-che' namespace", - initObjects: []runtime.Object{ - &chev2.CheCluster{ObjectMeta: metav1.ObjectMeta{Name: "eclipse-che", Namespace: "eclipse-che"}}, - &chev2.CheCluster{ObjectMeta: metav1.ObjectMeta{Name: "test-eclipse-che", Namespace: "eclipse-che"}}, - }, - watchNamespace: "eclipse-che", - expectedNumber: 2, - expectedErr: true, - }, - { - name: "several CR in different namespaces", - initObjects: []runtime.Object{ - &chev2.CheCluster{ObjectMeta: metav1.ObjectMeta{Name: "eclipse-che", Namespace: "eclipse-che"}}, - &chev2.CheCluster{ObjectMeta: metav1.ObjectMeta{Name: "eclipse-che", Namespace: "default"}}, - }, - watchNamespace: "eclipse-che", - expectedNumber: 1, - expectedErr: false, - expectedNamespace: "eclipse-che", - }, - { - name: "CR in 'eclipse-che' namespace, all-namespace mode", - initObjects: []runtime.Object{ - &chev2.CheCluster{ObjectMeta: metav1.ObjectMeta{Name: "eclipse-che", Namespace: "eclipse-che"}}, - }, - watchNamespace: "", - expectedNumber: 1, - expectedErr: false, - expectedNamespace: "eclipse-che", - }, - { - name: "CR in 'default' namespace, all-namespace mode", - initObjects: []runtime.Object{ - &chev2.CheCluster{ObjectMeta: metav1.ObjectMeta{Name: "eclipse-che", Namespace: "default"}}, - }, - watchNamespace: "", - expectedNumber: 1, - expectedErr: false, - expectedNamespace: "default", - }, - { - name: "several CR in 'eclipse-che' namespace, all-namespace mode", - initObjects: []runtime.Object{ - &chev2.CheCluster{ObjectMeta: metav1.ObjectMeta{Name: "eclipse-che", Namespace: "eclipse-che"}}, - &chev2.CheCluster{ObjectMeta: metav1.ObjectMeta{Name: "test-eclipse-che", Namespace: "eclipse-che"}}, - }, - watchNamespace: "", - expectedNumber: 2, - expectedErr: true, - }, - { - name: "several CR in different namespaces, all-namespace mode", - initObjects: []runtime.Object{ - &chev2.CheCluster{ObjectMeta: metav1.ObjectMeta{Name: "eclipse-che", Namespace: "eclipse-che"}}, - &chev2.CheCluster{ObjectMeta: metav1.ObjectMeta{Name: "eclipse-che", Namespace: "default"}}, - }, - watchNamespace: "", - expectedNumber: 2, - expectedErr: true, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - scheme := scheme.Scheme - chev2.SchemeBuilder.AddToScheme(scheme) - cli := fake.NewFakeClientWithScheme(scheme, testCase.initObjects...) - - checluster, num, err := FindCheClusterCRInNamespace(cli, testCase.watchNamespace) - assert.Equal(t, testCase.expectedNumber, num) - if testCase.expectedErr { - assert.NotNil(t, err) - } else { - assert.Nil(t, err) - } - if num == 1 { - assert.Equal(t, testCase.expectedNamespace, checluster.Namespace) - } - }) - } -} diff --git a/pkg/deploy/pvc.go b/pkg/deploy/pvc.go deleted file mode 100644 index d01dcd746..000000000 --- a/pkg/deploy/pvc.go +++ /dev/null @@ -1,89 +0,0 @@ -// -// Copyright (c) 2019-2023 Red Hat, Inc. -// This program and the accompanying materials are made -// available under the terms of the Eclipse Public License 2.0 -// which is available at https://www.eclipse.org/legal/epl-2.0/ -// -// SPDX-License-Identifier: EPL-2.0 -// -// Contributors: -// Red Hat, Inc. - initial API and implementation -// - -package deploy - -import ( - chev2 "github.com/eclipse-che/che-operator/api/v2" - "github.com/eclipse-che/che-operator/pkg/common/chetypes" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/pointer" -) - -var pvcDiffOpts = cmp.Options{ - cmpopts.IgnoreFields(corev1.PersistentVolumeClaim{}, "TypeMeta", "ObjectMeta", "Status"), - cmpopts.IgnoreFields(corev1.PersistentVolumeClaimSpec{}, "VolumeName", "StorageClassName", "VolumeMode", "Selector", "DataSource"), - cmpopts.IgnoreFields(corev1.ResourceRequirements{}, "Limits"), - cmp.Comparer(func(x, y resource.Quantity) bool { - return x.Cmp(y) == 0 - }), -} - -func SyncPVCToCluster( - deployContext *chetypes.DeployContext, - name string, - pvc *chev2.PVC, - component string) (bool, error) { - - pvcSpec := getPVCSpec(deployContext, name, pvc, component) - - actual := &corev1.PersistentVolumeClaim{} - exists, err := GetNamespacedObject(deployContext, name, actual) - if err != nil { - return false, err - } else if exists { - actual.Spec.Resources.Requests[corev1.ResourceName(corev1.ResourceStorage)] = resource.MustParse(pvc.ClaimSize) - return Sync(deployContext, actual, pvcDiffOpts) - } - - return Sync(deployContext, pvcSpec, pvcDiffOpts) -} - -func getPVCSpec( - deployContext *chetypes.DeployContext, - name string, - pvc *chev2.PVC, - component string) *corev1.PersistentVolumeClaim { - - labels := GetLabels(component) - accessModes := []corev1.PersistentVolumeAccessMode{ - corev1.ReadWriteOnce, - } - resources := corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceName(corev1.ResourceStorage): resource.MustParse(pvc.ClaimSize), - }} - pvcSpec := corev1.PersistentVolumeClaimSpec{ - AccessModes: accessModes, - Resources: resources, - } - if pvc.StorageClass != "" { - pvcSpec.StorageClassName = pointer.StringPtr(pvc.StorageClass) - } - - return &corev1.PersistentVolumeClaim{ - TypeMeta: metav1.TypeMeta{ - Kind: "PersistentVolumeClaim", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: deployContext.CheCluster.Namespace, - Labels: labels, - }, - Spec: pvcSpec, - } -} diff --git a/pkg/deploy/pvc_test.go b/pkg/deploy/pvc_test.go deleted file mode 100644 index 03246fe9e..000000000 --- a/pkg/deploy/pvc_test.go +++ /dev/null @@ -1,48 +0,0 @@ -// -// Copyright (c) 2019-2023 Red Hat, Inc. -// This program and the accompanying materials are made -// available under the terms of the Eclipse Public License 2.0 -// which is available at https://www.eclipse.org/legal/epl-2.0/ -// -// SPDX-License-Identifier: EPL-2.0 -// -// Contributors: -// Red Hat, Inc. - initial API and implementation -// - -package deploy - -import ( - "context" - - "testing" - - chev2 "github.com/eclipse-che/che-operator/api/v2" - "github.com/eclipse-che/che-operator/pkg/common/test" - "github.com/stretchr/testify/assert" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" -) - -func TestSyncPVCToCluster(t *testing.T) { - ctx := test.GetDeployContext(nil, []runtime.Object{}) - - done, err := SyncPVCToCluster(ctx, "test", &chev2.PVC{ClaimSize: "1Gi"}, "che") - assert.True(t, done) - assert.Nil(t, err) - - // sync a new pvc - _, err = SyncPVCToCluster(ctx, "test", &chev2.PVC{ClaimSize: "2Gi"}, "che") - assert.Nil(t, err) - - // sync pvc twice to be sure update done correctly - _, err = SyncPVCToCluster(ctx, "test", &chev2.PVC{ClaimSize: "2Gi"}, "che") - assert.Nil(t, err) - - actual := &corev1.PersistentVolumeClaim{} - err = ctx.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: "test", Namespace: "eclipse-che"}, actual) - assert.Nil(t, err) - assert.Equal(t, actual.Spec.Resources.Requests[corev1.ResourceStorage], resource.MustParse("2Gi")) -} diff --git a/pkg/deploy/sync.go b/pkg/deploy/sync.go index 934d8b390..2be298259 100644 --- a/pkg/deploy/sync.go +++ b/pkg/deploy/sync.go @@ -17,9 +17,10 @@ import ( "fmt" "reflect" + ctrl "sigs.k8s.io/controller-runtime" + "github.com/eclipse-che/che-operator/pkg/common/chetypes" "github.com/google/go-cmp/cmp" - "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -28,6 +29,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) +var ( + syncLog = ctrl.Log.WithName("sync") +) + // Sync syncs the blueprint to the cluster in a generic (as much as Go allows) manner. // Returns true if object is up-to-date otherwise returns false func Sync(deployContext *chetypes.DeployContext, blueprint client.Object, diffOpts ...cmp.Option) (bool, error) { @@ -148,13 +153,12 @@ func UpdateWithClient(client client.Client, deployContext *chetypes.DeployContex } if isUpdateUsingDeleteCreate(actual.GetObjectKind().GroupVersionKind().Kind) { - done, err := DeleteWithClient(client, actual) + done, err := doDeleteIgnoreIfNotFound(context.TODO(), client, actual) if !done { return false, err } return CreateWithClient(client, deployContext, blueprint, false) } else { - logrus.Infof("Updating existing object: %s, name: %s", GetObjectType(actualMeta), actualMeta.GetName()) err := setOwnerReferenceIfNeeded(deployContext, blueprint) if err != nil { return false, err @@ -163,6 +167,7 @@ func UpdateWithClient(client client.Client, deployContext *chetypes.DeployContex // to be able to update, we need to set the resource version of the object that we know of blueprint.(metav1.Object).SetResourceVersion(actualMeta.GetResourceVersion()) err = client.Update(context.TODO(), blueprint) + syncLog.Info("Object updated", "namespace", actual.GetNamespace(), "kind", GetObjectType(actual), "name", actual.GetName()) return false, err } } @@ -170,8 +175,6 @@ func UpdateWithClient(client client.Client, deployContext *chetypes.DeployContex } func CreateWithClient(client client.Client, deployContext *chetypes.DeployContext, blueprint client.Object, returnTrueIfAlreadyExists bool) (bool, error) { - logrus.Infof("Creating a new object: %s, name: %s", GetObjectType(blueprint), blueprint.GetName()) - err := setOwnerReferenceIfNeeded(deployContext, blueprint) if err != nil { return false, err @@ -179,6 +182,7 @@ func CreateWithClient(client client.Client, deployContext *chetypes.DeployContex err = client.Create(context.TODO(), blueprint) if err == nil { + syncLog.Info("Object created", "namespace", blueprint.GetNamespace(), "kind", GetObjectType(blueprint), "name", blueprint.GetName()) return true, nil } else if errors.IsAlreadyExists(err) { return returnTrueIfAlreadyExists, nil @@ -201,18 +205,7 @@ func DeleteByKeyWithClient(cli client.Client, key client.ObjectKey, objectMeta c return false, err } - return DeleteWithClient(cli, actual) -} - -func DeleteWithClient(client client.Client, actual client.Object) (bool, error) { - logrus.Infof("Deleting object: %s, name: %s", GetObjectType(actual), actual.GetName()) - - err := client.Delete(context.TODO(), actual) - if err == nil || errors.IsNotFound(err) { - return true, nil - } else { - return false, err - } + return doDeleteIgnoreIfNotFound(context.TODO(), cli, actual) } func GetWithClient(client client.Client, key client.ObjectKey, object client.Object) (bool, error) { @@ -259,3 +252,71 @@ func GetObjectType(obj interface{}) string { return objType } + +// DeleteIgnoreIfNotFound deletes object. +// Returns nil if object deleted or not found otherwise returns error. +func DeleteIgnoreIfNotFound(context context.Context, cli client.Client, key client.ObjectKey, blueprint client.Object) error { + runtimeObj, ok := blueprint.(runtime.Object) + if !ok { + return fmt.Errorf("object %T is not a runtime.Object. Cannot sync it", runtimeObj) + } + + actual := runtimeObj.DeepCopyObject().(client.Object) + + exists, err := doGet(context, cli, key, actual) + if exists { + _, err := doDeleteIgnoreIfNotFound(context, cli, actual) + return err + } + + return err +} + +// doCreate creates object. +// Returns true if object created otherwise returns false. +// Throws error if object cannot be created or already exists otherwise returns nil. +func doCreate(context context.Context, client client.Client, deployContext *chetypes.DeployContext, blueprint client.Object) (bool, error) { + err := setOwnerReferenceIfNeeded(deployContext, blueprint) + if err != nil { + return false, err + } + + err = client.Create(context, blueprint) + if err == nil { + syncLog.Info("Object created", "namespace", blueprint.GetNamespace(), "kind", GetObjectType(blueprint), "name", blueprint.GetName()) + return true, nil + } else { + return false, err + } +} + +// doDeleteIgnoreIfNotFound deletes object. +// Returns true if object deleted or not found otherwise returns false. +// Returns error if object cannot be deleted otherwise returns nil. +func doDeleteIgnoreIfNotFound(context context.Context, cli client.Client, actual client.Object) (bool, error) { + err := cli.Delete(context, actual) + if err == nil { + if errors.IsNotFound(err) { + syncLog.Info("Object not found", "namespace", actual.GetNamespace(), "kind", GetObjectType(actual), "name", actual.GetName()) + } else { + syncLog.Info("Object deleted", "namespace", actual.GetNamespace(), "kind", GetObjectType(actual), "name", actual.GetName()) + } + return true, nil + } else { + return false, err + } +} + +// doGet gets object. +// Returns true if object exists otherwise returns false. +// Returns error if object cannot be retrieved otherwise returns nil. +func doGet(context context.Context, cli client.Client, key client.ObjectKey, object client.Object) (bool, error) { + err := cli.Get(context, key, object) + if err == nil { + return true, nil + } else if errors.IsNotFound(err) { + return false, nil + } else { + return false, err + } +}