From 69912e24d6187cb6f37a9efc15ce43ccf33ddd9e Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Thu, 31 Oct 2024 13:40:11 +0100 Subject: [PATCH] fixup Signed-off-by: Anatolii Bazko --- ...rkspace_cm_syncer.go => configmap2sync.go} | 31 ++++-- ..._syncer_test.go => configmap2sync_test.go} | 0 .../{workspace_pvc_syncer.go => pvc2sync.go} | 32 ++++-- ...ce_pvc_syncer_test.go => pvc2sync_test.go} | 0 ...kspace_secret_syncer.go => secret2sync.go} | 31 ++++-- ...ret_syncer_test.go => secret2sync_test.go} | 0 ...uctured_syncer.go => unstructured2sync.go} | 24 +++-- ...ncer_test.go => unstructured2sync_test.go} | 0 .../workspaces_config_controller.go | 88 +++++------------ .../workspaces_config_controller_test.go | 31 ------ .../workspaces_config_diff_helper.go | 99 +++++++++++++++++++ .../workspaces_config_diff_helper_test.go | 83 ++++++++++++++++ 12 files changed, 289 insertions(+), 130 deletions(-) rename controllers/usernamespace/{workspace_cm_syncer.go => configmap2sync.go} (65%) rename controllers/usernamespace/{workspace_cm_syncer_test.go => configmap2sync_test.go} (100%) rename controllers/usernamespace/{workspace_pvc_syncer.go => pvc2sync.go} (62%) rename controllers/usernamespace/{workspace_pvc_syncer_test.go => pvc2sync_test.go} (100%) rename controllers/usernamespace/{workspace_secret_syncer.go => secret2sync.go} (66%) rename controllers/usernamespace/{workspace_secret_syncer_test.go => secret2sync_test.go} (100%) rename controllers/usernamespace/{workspace_unstructured_syncer.go => unstructured2sync.go} (78%) rename controllers/usernamespace/{workspace_unstructured_syncer_test.go => unstructured2sync_test.go} (100%) create mode 100644 controllers/usernamespace/workspaces_config_diff_helper.go create mode 100644 controllers/usernamespace/workspaces_config_diff_helper_test.go diff --git a/controllers/usernamespace/workspace_cm_syncer.go b/controllers/usernamespace/configmap2sync.go similarity index 65% rename from controllers/usernamespace/workspace_cm_syncer.go rename to controllers/usernamespace/configmap2sync.go index 6bcbd577a..64f177545 100644 --- a/controllers/usernamespace/workspace_cm_syncer.go +++ b/controllers/usernamespace/configmap2sync.go @@ -15,6 +15,8 @@ package usernamespace import ( dwconstants "github.com/devfile/devworkspace-operator/pkg/constants" "github.com/eclipse-che/che-operator/pkg/common/utils" + "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/schema" @@ -25,24 +27,24 @@ var ( v1ConfigMapGKV = corev1.SchemeGroupVersion.WithKind("ConfigMap") ) -type cmWorkspaceSyncObject struct { - WorkspaceSyncObject +type configMap2Sync struct { + Object2Sync cm *corev1.ConfigMap } -func newCMWorkspaceSyncObject(cm *corev1.ConfigMap) *cmWorkspaceSyncObject { - return &cmWorkspaceSyncObject{cm: cm} +func newCM2Sync(cm *corev1.ConfigMap) *configMap2Sync { + return &configMap2Sync{cm: cm} } -func (p *cmWorkspaceSyncObject) getSrcObject() client.Object { +func (p *configMap2Sync) getSrcObject() client.Object { return p.cm } -func (p *cmWorkspaceSyncObject) getGKV() schema.GroupVersionKind { +func (p *configMap2Sync) getGKV() schema.GroupVersionKind { return v1ConfigMapGKV } -func (p *cmWorkspaceSyncObject) newDstObject() client.Object { +func (p *configMap2Sync) newDstObject() client.Object { dst := p.cm.DeepCopyObject() // We have to set the ObjectMeta fields explicitly, because // existed object contains unnecessary fields that we don't want to copy @@ -60,10 +62,21 @@ func (p *cmWorkspaceSyncObject) newDstObject() client.Object { return dst.(client.Object) } -func (p *cmWorkspaceSyncObject) getSrcObjectVersion() string { +func (p *configMap2Sync) getSrcObjectVersion() string { return p.cm.GetResourceVersion() } -func (p *cmWorkspaceSyncObject) hasROSpec() bool { +func (p *configMap2Sync) hasROSpec() bool { return false } + +func (p *configMap2Sync) isDiff(obj client.Object) bool { + return isLabelsOrAnnotationsDiff(p.cm, obj) || + cmp.Diff( + p.cm, + obj, + cmp.Options{ + cmpopts.IgnoreTypes(metav1.ObjectMeta{}), + cmpopts.IgnoreTypes(metav1.TypeMeta{}), + }) != "" +} diff --git a/controllers/usernamespace/workspace_cm_syncer_test.go b/controllers/usernamespace/configmap2sync_test.go similarity index 100% rename from controllers/usernamespace/workspace_cm_syncer_test.go rename to controllers/usernamespace/configmap2sync_test.go diff --git a/controllers/usernamespace/workspace_pvc_syncer.go b/controllers/usernamespace/pvc2sync.go similarity index 62% rename from controllers/usernamespace/workspace_pvc_syncer.go rename to controllers/usernamespace/pvc2sync.go index e87d1ba0e..58ae367b1 100644 --- a/controllers/usernamespace/workspace_pvc_syncer.go +++ b/controllers/usernamespace/pvc2sync.go @@ -13,6 +13,8 @@ package usernamespace import ( + "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/schema" @@ -23,26 +25,26 @@ var ( v1PvcGKV = corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim") ) -type pvcWorkspaceSyncObject struct { - WorkspaceSyncObject +type pvc2Sync struct { + Object2Sync pvc *corev1.PersistentVolumeClaim } -func newPvcWorkspaceSyncObject(pvc *corev1.PersistentVolumeClaim) *pvcWorkspaceSyncObject { - return &pvcWorkspaceSyncObject{ +func newPvc2Sync(pvc *corev1.PersistentVolumeClaim) *pvc2Sync { + return &pvc2Sync{ pvc: pvc, } } -func (p *pvcWorkspaceSyncObject) getGKV() schema.GroupVersionKind { +func (p *pvc2Sync) getGKV() schema.GroupVersionKind { return v1PvcGKV } -func (p *pvcWorkspaceSyncObject) getSrcObject() client.Object { +func (p *pvc2Sync) getSrcObject() client.Object { return p.pvc } -func (p *pvcWorkspaceSyncObject) newDstObject() client.Object { +func (p *pvc2Sync) newDstObject() client.Object { dst := p.pvc.DeepCopyObject() // We have to set the ObjectMeta fields explicitly, because // existed object contains unnecessary fields that we don't want to copy @@ -56,10 +58,22 @@ func (p *pvcWorkspaceSyncObject) newDstObject() client.Object { return dst.(client.Object) } -func (p *pvcWorkspaceSyncObject) getSrcObjectVersion() string { +func (p *pvc2Sync) getSrcObjectVersion() string { return p.pvc.GetResourceVersion() } -func (p *pvcWorkspaceSyncObject) hasROSpec() bool { +func (p *pvc2Sync) hasROSpec() bool { return true } + +func (p *pvc2Sync) isDiff(obj client.Object) bool { + return isLabelsOrAnnotationsDiff(p.pvc, obj) || + cmp.Diff( + p.pvc, + obj, + cmp.Options{ + cmpopts.IgnoreTypes(metav1.ObjectMeta{}), + cmpopts.IgnoreTypes(metav1.TypeMeta{}), + cmpopts.IgnoreTypes(corev1.PersistentVolumeClaimStatus{}), + }) != "" +} diff --git a/controllers/usernamespace/workspace_pvc_syncer_test.go b/controllers/usernamespace/pvc2sync_test.go similarity index 100% rename from controllers/usernamespace/workspace_pvc_syncer_test.go rename to controllers/usernamespace/pvc2sync_test.go diff --git a/controllers/usernamespace/workspace_secret_syncer.go b/controllers/usernamespace/secret2sync.go similarity index 66% rename from controllers/usernamespace/workspace_secret_syncer.go rename to controllers/usernamespace/secret2sync.go index 4e807c524..5b553651c 100644 --- a/controllers/usernamespace/workspace_secret_syncer.go +++ b/controllers/usernamespace/secret2sync.go @@ -15,6 +15,8 @@ package usernamespace import ( dwconstants "github.com/devfile/devworkspace-operator/pkg/constants" "github.com/eclipse-che/che-operator/pkg/common/utils" + "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/schema" @@ -25,26 +27,26 @@ var ( v1SecretGKV = corev1.SchemeGroupVersion.WithKind("Secret") ) -type secretWorkspaceSyncObject struct { - WorkspaceSyncObject +type secret2Sync struct { + Object2Sync secret *corev1.Secret } -func newSecretWorkspaceSyncObject(secret *corev1.Secret) *secretWorkspaceSyncObject { - return &secretWorkspaceSyncObject{ +func newSecret2Sync(secret *corev1.Secret) *secret2Sync { + return &secret2Sync{ secret: secret, } } -func (p *secretWorkspaceSyncObject) getGKV() schema.GroupVersionKind { +func (p *secret2Sync) getGKV() schema.GroupVersionKind { return v1SecretGKV } -func (p *secretWorkspaceSyncObject) getSrcObject() client.Object { +func (p *secret2Sync) getSrcObject() client.Object { return p.secret } -func (p *secretWorkspaceSyncObject) newDstObject() client.Object { +func (p *secret2Sync) newDstObject() client.Object { dst := p.secret.DeepCopyObject() // We have to set the ObjectMeta fields explicitly, because // existed object contains unnecessary fields that we don't want to copy @@ -62,10 +64,21 @@ func (p *secretWorkspaceSyncObject) newDstObject() client.Object { return dst.(client.Object) } -func (p *secretWorkspaceSyncObject) getSrcObjectVersion() string { +func (p *secret2Sync) getSrcObjectVersion() string { return p.secret.GetResourceVersion() } -func (p *secretWorkspaceSyncObject) hasROSpec() bool { +func (p *secret2Sync) hasROSpec() bool { return false } + +func (p *secret2Sync) isDiff(obj client.Object) bool { + return isLabelsOrAnnotationsDiff(p.secret, obj) || + cmp.Diff( + p.secret, + obj, + cmp.Options{ + cmpopts.IgnoreTypes(metav1.ObjectMeta{}), + cmpopts.IgnoreTypes(metav1.TypeMeta{}), + }) != "" +} diff --git a/controllers/usernamespace/workspace_secret_syncer_test.go b/controllers/usernamespace/secret2sync_test.go similarity index 100% rename from controllers/usernamespace/workspace_secret_syncer_test.go rename to controllers/usernamespace/secret2sync_test.go diff --git a/controllers/usernamespace/workspace_unstructured_syncer.go b/controllers/usernamespace/unstructured2sync.go similarity index 78% rename from controllers/usernamespace/workspace_unstructured_syncer.go rename to controllers/usernamespace/unstructured2sync.go index 827513aff..d3bef5a3e 100644 --- a/controllers/usernamespace/workspace_unstructured_syncer.go +++ b/controllers/usernamespace/unstructured2sync.go @@ -30,18 +30,18 @@ const ( PROJECT_NAME = "${PROJECT_NAME}" ) -type unstructuredSyncer struct { - WorkspaceSyncObject +type unstructured2Sync struct { + Object2Sync srcObj client.Object dstObj client.Object hash string } -func newUnstructuredSyncer( +func newUnstructured2Sync( raw []byte, userName string, - namespaceName string) (*unstructuredSyncer, error) { + namespaceName string) (*unstructured2Sync, error) { hash := utils.ComputeHash256(raw) @@ -56,22 +56,22 @@ func newUnstructuredSyncer( dstObj := srcObj.DeepCopyObject() - return &unstructuredSyncer{ + return &unstructured2Sync{ srcObj: srcObj, dstObj: dstObj.(client.Object), hash: hash, }, nil } -func (p *unstructuredSyncer) getSrcObject() client.Object { +func (p *unstructured2Sync) getSrcObject() client.Object { return p.srcObj } -func (p *unstructuredSyncer) getGKV() schema.GroupVersionKind { +func (p *unstructured2Sync) getGKV() schema.GroupVersionKind { return p.srcObj.GetObjectKind().GroupVersionKind() } -func (p *unstructuredSyncer) newDstObject() client.Object { +func (p *unstructured2Sync) newDstObject() client.Object { dstObj := p.dstObj.DeepCopyObject().(client.Object) switch dstObj.GetObjectKind().GroupVersionKind() { @@ -98,10 +98,14 @@ func (p *unstructuredSyncer) newDstObject() client.Object { return dstObj } -func (p *unstructuredSyncer) getSrcObjectVersion() string { +func (p *unstructured2Sync) getSrcObjectVersion() string { return p.hash } -func (p *unstructuredSyncer) hasROSpec() bool { +func (p *unstructured2Sync) hasROSpec() bool { return p.dstObj.GetObjectKind().GroupVersionKind() == v1PvcGKV } + +func (p *unstructured2Sync) isDiff(obj client.Object) bool { + return isLabelsOrAnnotationsDiff(p.srcObj, obj) || isUnstructuredDiff(p.srcObj, obj) +} diff --git a/controllers/usernamespace/workspace_unstructured_syncer_test.go b/controllers/usernamespace/unstructured2sync_test.go similarity index 100% rename from controllers/usernamespace/workspace_unstructured_syncer_test.go rename to controllers/usernamespace/unstructured2sync_test.go diff --git a/controllers/usernamespace/workspaces_config_controller.go b/controllers/usernamespace/workspaces_config_controller.go index 38e985716..2b9b2449c 100644 --- a/controllers/usernamespace/workspaces_config_controller.go +++ b/controllers/usernamespace/workspaces_config_controller.go @@ -20,11 +20,8 @@ import ( rbacv1 "k8s.io/api/rbac/v1" "github.com/devfile/devworkspace-operator/pkg/infrastructure" - "github.com/eclipse-che/che-operator/pkg/common/utils" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/eclipse-che/che-operator/pkg/common/constants" + "github.com/eclipse-che/che-operator/pkg/common/utils" "github.com/eclipse-che/che-operator/pkg/deploy" templatev1 "github.com/openshift/api/template/v1" corev1 "k8s.io/api/core/v1" @@ -51,19 +48,20 @@ type WorkspacesConfigReconciler struct { namespaceCache *namespaceCache } -type WorkspaceSyncObject interface { +type Object2Sync interface { getGKV() schema.GroupVersionKind hasROSpec() bool getSrcObject() client.Object getSrcObjectVersion() string newDstObject() client.Object + isDiff(obj client.Object) bool } type syncContext struct { dstNamespace string srcNamespace string ctx context.Context - wsSyncObject WorkspaceSyncObject + object2Sync Object2Sync syncConfig map[string]string } @@ -308,7 +306,7 @@ func (r *WorkspacesConfigReconciler) syncConfigMaps( &syncContext{ dstNamespace: dstNamespace, srcNamespace: srcNamespace, - wsSyncObject: newCMWorkspaceSyncObject(&cm), + object2Sync: newCM2Sync(&cm), syncConfig: syncConfig, ctx: ctx, }); err != nil { @@ -345,7 +343,7 @@ func (r *WorkspacesConfigReconciler) syncSecretes( &syncContext{ dstNamespace: dstNamespace, srcNamespace: srcNamespace, - wsSyncObject: newSecretWorkspaceSyncObject(&secret), + object2Sync: newSecret2Sync(&secret), syncConfig: syncConfig, ctx: ctx, }); err != nil { @@ -382,7 +380,7 @@ func (r *WorkspacesConfigReconciler) syncPVCs( &syncContext{ dstNamespace: dstNamespace, srcNamespace: srcNamespace, - wsSyncObject: newPvcWorkspaceSyncObject(&pvc), + object2Sync: newPvc2Sync(&pvc), syncConfig: syncConfig, ctx: ctx, }); err != nil { @@ -421,7 +419,7 @@ func (r *WorkspacesConfigReconciler) syncTemplates( for _, template := range templates.Items { for _, object := range template.Objects { - wsSyncObject, err := newUnstructuredSyncer(object.Raw, nsInfo.Username, dstNamespace) + object2Sync, err := newUnstructured2Sync(object.Raw, nsInfo.Username, dstNamespace) if err != nil { return err } @@ -430,14 +428,14 @@ func (r *WorkspacesConfigReconciler) syncTemplates( &syncContext{ dstNamespace: dstNamespace, srcNamespace: srcNamespace, - wsSyncObject: wsSyncObject, + object2Sync: object2Sync, syncConfig: syncConfig, ctx: ctx, }); err != nil { return err } - srcObjKey := buildKey(wsSyncObject.getGKV(), wsSyncObject.getSrcObject().GetName(), srcNamespace) + srcObjKey := buildKey(object2Sync.getGKV(), object2Sync.getSrcObject().GetName(), srcNamespace) syncedSrcObjKeys[srcObjKey] = true } } @@ -448,10 +446,10 @@ func (r *WorkspacesConfigReconciler) syncTemplates( // syncObject syncs object to a user destination namespace. // Returns error if sync failed in a destination namespace. func (r *WorkspacesConfigReconciler) syncObject(syncContext *syncContext) error { - dstObj := syncContext.wsSyncObject.newDstObject() + dstObj := syncContext.object2Sync.newDstObject() dstObj.SetNamespace(syncContext.dstNamespace) // ensure the name is the same as the source object - dstObj.SetName(syncContext.wsSyncObject.getSrcObject().GetName()) + dstObj.SetName(syncContext.object2Sync.getSrcObject().GetName()) // set mandatory labels dstObj.SetLabels(utils.MergeMaps( []map[string]string{ @@ -466,7 +464,7 @@ func (r *WorkspacesConfigReconciler) syncObject(syncContext *syncContext) error if err := r.syncObjectIfDiffers(syncContext, dstObj); err != nil { logger.Error(err, "Failed to sync object", "namespace", syncContext.dstNamespace, - "kind", gvk2PrintString(syncContext.wsSyncObject.getGKV()), + "kind", gvk2PrintString(syncContext.object2Sync.getGKV()), "name", dstObj.GetName()) return err } @@ -480,7 +478,7 @@ func (r *WorkspacesConfigReconciler) syncObjectIfDiffers( syncContext *syncContext, dstObj client.Object) error { - existedDstObj, err := r.scheme.New(syncContext.wsSyncObject.getGKV()) + existedDstObj, err := r.scheme.New(syncContext.object2Sync.getGKV()) if err != nil { return err } @@ -491,23 +489,23 @@ func (r *WorkspacesConfigReconciler) syncObjectIfDiffers( err = r.client.Get(syncContext.ctx, existedDstObjKey, existedDstObj.(client.Object)) if err == nil { - srcObj := syncContext.wsSyncObject.getSrcObject() + srcObj := syncContext.object2Sync.getSrcObject() - srcObjKey := buildKey(syncContext.wsSyncObject.getGKV(), srcObj.GetName(), syncContext.srcNamespace) - dstObjKey := buildKey(syncContext.wsSyncObject.getGKV(), dstObj.GetName(), syncContext.dstNamespace) + srcObjKey := buildKey(syncContext.object2Sync.getGKV(), srcObj.GetName(), syncContext.srcNamespace) + dstObjKey := buildKey(syncContext.object2Sync.getGKV(), dstObj.GetName(), syncContext.dstNamespace) - srcHasBeenChanged := syncContext.syncConfig[srcObjKey] != syncContext.wsSyncObject.getSrcObjectVersion() + srcHasBeenChanged := syncContext.syncConfig[srcObjKey] != syncContext.object2Sync.getSrcObjectVersion() dstHasBeenChanged := syncContext.syncConfig[dstObjKey] != existedDstObj.(client.Object).GetResourceVersion() if srcHasBeenChanged || dstHasBeenChanged { // destination object exists, and it differs from the source object, // so it will be updated - if syncContext.wsSyncObject.hasROSpec() { + if syncContext.object2Sync.hasROSpec() { // Skip updating objects with readonly spec. // Admin has to re-create them to update just update resource versions logger.Info("Object skipped since has readonly spec, re-create it to update", "namespace", dstObj.GetNamespace(), - "kind", gvk2PrintString(syncContext.wsSyncObject.getGKV()), + "kind", gvk2PrintString(syncContext.object2Sync.getGKV()), "name", dstObj.GetName()) r.doUpdateSyncConfig(syncContext, existedDstObj.(client.Object)) @@ -550,7 +548,7 @@ func (r *WorkspacesConfigReconciler) doCreateObject( } logger.Info("Object created", "namespace", dstObj.GetNamespace(), - "kind", gvk2PrintString(syncContext.wsSyncObject.getGKV()), + "kind", gvk2PrintString(syncContext.object2Sync.getGKV()), "name", dstObj.GetName()) return nil @@ -584,7 +582,7 @@ func (r *WorkspacesConfigReconciler) doUpdateObject( } logger.Info("Object updated", "namespace", dstObj.GetNamespace(), - "kind", gvk2PrintString(syncContext.wsSyncObject.getGKV()), + "kind", gvk2PrintString(syncContext.object2Sync.getGKV()), "name", dstObj.GetName()) return nil @@ -592,12 +590,12 @@ func (r *WorkspacesConfigReconciler) doUpdateObject( // doUpdateSyncConfig updates sync config with resource versions of synced objects. func (r *WorkspacesConfigReconciler) doUpdateSyncConfig(syncContext *syncContext, dstObj client.Object) { - srcObj := syncContext.wsSyncObject.getSrcObject() + srcObj := syncContext.object2Sync.getSrcObject() - srcObjKey := buildKey(syncContext.wsSyncObject.getGKV(), srcObj.GetName(), syncContext.srcNamespace) - dstObjKey := buildKey(syncContext.wsSyncObject.getGKV(), dstObj.GetName(), syncContext.dstNamespace) + srcObjKey := buildKey(syncContext.object2Sync.getGKV(), srcObj.GetName(), syncContext.srcNamespace) + dstObjKey := buildKey(syncContext.object2Sync.getGKV(), dstObj.GetName(), syncContext.dstNamespace) - syncContext.syncConfig[srcObjKey] = syncContext.wsSyncObject.getSrcObjectVersion() + syncContext.syncConfig[srcObjKey] = syncContext.object2Sync.getSrcObjectVersion() syncContext.syncConfig[dstObjKey] = dstObj.GetResourceVersion() } @@ -643,40 +641,6 @@ func (r *WorkspacesConfigReconciler) deleteIfObjectIsObsolete( return nil } -// isDiff checks if the given objects are different. -// The rules are following: -// - if labels of the source object are absent in the destination object, -// then the objects considered different -// - if annotations of the source object are absent in the destination object, -// then the objects considered different -// - if the rest fields of the objects are different, -// then the objects considered different -func isDiff(src client.Object, dst client.Object) bool { - if src.GetLabels() != nil { - for key, value := range src.GetLabels() { - if dst.GetLabels()[key] != value { - return true - } - } - } - - if src.GetAnnotations() != nil { - for key, value := range src.GetAnnotations() { - if dst.GetAnnotations()[key] != value { - return true - } - } - } - - return cmp.Diff( - src, - dst, - cmp.Options{ - cmpopts.IgnoreTypes(metav1.ObjectMeta{}), - cmpopts.IgnoreTypes(metav1.TypeMeta{}), - }) != "" -} - // getSyncConfig returns ConfigMap with synced objects resource versions. // Returns error if ConfigMap failed to be retrieved. func (r *WorkspacesConfigReconciler) getSyncConfig(ctx context.Context, namespace string) (*corev1.ConfigMap, error) { diff --git a/controllers/usernamespace/workspaces_config_controller_test.go b/controllers/usernamespace/workspaces_config_controller_test.go index b46d00ba4..c266a1f30 100644 --- a/controllers/usernamespace/workspaces_config_controller_test.go +++ b/controllers/usernamespace/workspaces_config_controller_test.go @@ -116,37 +116,6 @@ func TestGetEmptySyncConfig(t *testing.T) { assert.Equal(t, deploy.GetManagedByLabel(), cm.Labels[constants.KubernetesManagedByLabelKey]) } -func TestIsDiff(t *testing.T) { - src := &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - Kind: "ConfigMap", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "eclipse-che", - Labels: map[string]string{}, - Annotations: map[string]string{}, - }, - } - - dst := &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - Kind: "ConfigMap", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "eclipse-che", - Labels: map[string]string{"a": "b"}, - Annotations: map[string]string{"c": "d"}, - }, - } - - changed := isDiff(src, dst) - assert.False(t, changed) -} - func TestBuildKey(t *testing.T) { type testCase struct { name string diff --git a/controllers/usernamespace/workspaces_config_diff_helper.go b/controllers/usernamespace/workspaces_config_diff_helper.go new file mode 100644 index 000000000..80b98ad7e --- /dev/null +++ b/controllers/usernamespace/workspaces_config_diff_helper.go @@ -0,0 +1,99 @@ +// +// Copyright (c) 2019-2024 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 ( + "encoding/json" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// isDiff checks if the given objects are different. +// The rules are following: +// - if labels of the source object are absent in the destination object, +// then the objects considered different +// - if annotations of the source object are absent in the destination object, +// then the objects considered different +// - if the rest fields of the objects are different, +// then the objects considered different +func isDiff(src client.Object, dst client.Object) bool { + _, isSrcUnstructured := src.(*unstructured.Unstructured) + _, isDstUnstructured := dst.(*unstructured.Unstructured) + + if !isSrcUnstructured && !isDstUnstructured { + return isLabelsOrAnnotationsDiff(src, dst) || + cmp.Diff( + src, + dst, + cmp.Options{ + cmpopts.IgnoreTypes(metav1.ObjectMeta{}), + cmpopts.IgnoreTypes(metav1.TypeMeta{}), + }) != "" + } + + return isUnstructuredDiff(src, dst) +} + +func isUnstructuredDiff(src client.Object, dst client.Object) bool { + srcUnstructured := toUnstructured(src) + delete(srcUnstructured.Object, "metadata") + delete(srcUnstructured.Object, "status") + + dstUnstructured := toUnstructured(dst) + delete(dstUnstructured.Object, "metadata") + delete(dstUnstructured.Object, "status") + + return cmp.Diff(srcUnstructured, dstUnstructured) != "" +} + +func isLabelsOrAnnotationsDiff(src client.Object, dst client.Object) bool { + if src.GetLabels() != nil { + for key, value := range src.GetLabels() { + if dst.GetLabels()[key] != value { + return true + } + } + } + + if src.GetAnnotations() != nil { + for key, value := range src.GetAnnotations() { + if dst.GetAnnotations()[key] != value { + return true + } + } + } + + return false +} + +func toUnstructured(src client.Object) *unstructured.Unstructured { + data, err := json.Marshal(src) + if err != nil { + logger.Error(err, "Failed to marshal object") + return nil + } + + unstructuredObj := &unstructured.Unstructured{} + err = unstructuredObj.UnmarshalJSON(data) + if err != nil { + logger.Error(err, "Failed to unmarshal object") + return nil + } + + return unstructuredObj +} diff --git a/controllers/usernamespace/workspaces_config_diff_helper_test.go b/controllers/usernamespace/workspaces_config_diff_helper_test.go new file mode 100644 index 000000000..1d191e4d4 --- /dev/null +++ b/controllers/usernamespace/workspaces_config_diff_helper_test.go @@ -0,0 +1,83 @@ +// +// Copyright (c) 2019-2024 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 ( + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/yaml" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestIsDiff(t *testing.T) { + src := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "eclipse-che", + Labels: map[string]string{}, + Annotations: map[string]string{}, + }, + } + + dst := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "eclipse-che", + Labels: map[string]string{"a": "b"}, + Annotations: map[string]string{"c": "d"}, + }, + } + + changed := isDiff(src, dst) + assert.False(t, changed) +} + +func TestIsDiffUnstructured(t *testing.T) { + pvc := &corev1.PersistentVolumeClaim{ + TypeMeta: metav1.TypeMeta{ + Kind: "PersistentVolumeClaim", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "eclipse-che", + Labels: map[string]string{}, + Annotations: map[string]string{}, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + VolumeName: "test", + }, + } + + data, err := yaml.Marshal(pvc) + assert.NoError(t, err) + + unstructuredPvc := &unstructured.Unstructured{} + err = yaml.Unmarshal(data, unstructuredPvc) + assert.NoError(t, err) + + changed := isDiff(pvc, unstructuredPvc) + assert.False(t, changed) +}