Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion controllers/usernamespace/configmap2sync_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (c) 2019-2024 Red Hat, Inc.
// Copyright (c) 2019-2025 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/
Expand Down Expand Up @@ -69,6 +69,7 @@ func TestSyncConfigMap(t *testing.T) {
}})

workspaceConfigReconciler := NewWorkspacesConfigReconciler(
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Scheme,
&namespaceCache{
Expand Down Expand Up @@ -235,6 +236,7 @@ func TestSyncConfigMapShouldMergeLabelsAndAnnotationsOnUpdate(t *testing.T) {
}})

workspaceConfigReconciler := NewWorkspacesConfigReconciler(
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Scheme,
&namespaceCache{
Expand Down Expand Up @@ -352,6 +354,7 @@ func TestSyncConfigMapShouldRespectDWOLabels(t *testing.T) {
}})

workspaceConfigReconciler := NewWorkspacesConfigReconciler(
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Scheme,
&namespaceCache{
Expand Down Expand Up @@ -449,6 +452,7 @@ func TestSyncConfigMapShouldRemoveSomeLabels(t *testing.T) {
}})

workspaceConfigReconciler := NewWorkspacesConfigReconciler(
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Scheme,
&namespaceCache{
Expand Down
3 changes: 2 additions & 1 deletion controllers/usernamespace/pvc2sync_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (c) 2019-2024 Red Hat, Inc.
// Copyright (c) 2019-20255 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/
Expand Down Expand Up @@ -56,6 +56,7 @@ func TestSyncPVC(t *testing.T) {
}})

workspaceConfigReconciler := NewWorkspacesConfigReconciler(
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Scheme,
&namespaceCache{
Expand Down
6 changes: 5 additions & 1 deletion controllers/usernamespace/secret2sync_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (c) 2019-2024 Red Hat, Inc.
// Copyright (c) 2019-2025 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/
Expand Down Expand Up @@ -60,6 +60,7 @@ func TestSyncSecrets(t *testing.T) {
}})

workspaceConfigReconciler := NewWorkspacesConfigReconciler(
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Scheme,
&namespaceCache{
Expand Down Expand Up @@ -235,6 +236,7 @@ func TestSyncSecretShouldMergeLabelsAndAnnotationsOnUpdate(t *testing.T) {
}})

workspaceConfigReconciler := NewWorkspacesConfigReconciler(
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Scheme,
&namespaceCache{
Expand Down Expand Up @@ -341,6 +343,7 @@ func TestSyncSecretShouldRespectDWOLabels(t *testing.T) {
}})

workspaceConfigReconciler := NewWorkspacesConfigReconciler(
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Scheme,
&namespaceCache{
Expand Down Expand Up @@ -439,6 +442,7 @@ func TestSyncSecretShouldRemoveSomeLabels(t *testing.T) {
}})

workspaceConfigReconciler := NewWorkspacesConfigReconciler(
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Scheme,
&namespaceCache{
Expand Down
3 changes: 2 additions & 1 deletion controllers/usernamespace/unstructured2sync_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (c) 2019-2024 Red Hat, Inc.
// Copyright (c) 2019-2025 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/
Expand Down Expand Up @@ -82,6 +82,7 @@ func TestSyncTemplateWithLimitRange(t *testing.T) {
}})

workspaceConfigReconciler := NewWorkspacesConfigReconciler(
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Client,
deployContext.ClusterAPI.Scheme,
&namespaceCache{
Expand Down
32 changes: 28 additions & 4 deletions controllers/usernamespace/workspaces_config_controller.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (c) 2019-2024 Red Hat, Inc.
// Copyright (c) 2019-2025 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/
Expand Down Expand Up @@ -50,6 +50,7 @@ const (
type WorkspacesConfigReconciler struct {
scheme *runtime.Scheme
client client.Client
nonCachedClient client.Client
namespaceCache *namespaceCache
labelsToRemoveBeforeSync []*regexp.Regexp
annotationsToRemoveBeforeSync []*regexp.Regexp
Expand Down Expand Up @@ -87,6 +88,7 @@ var (

func NewWorkspacesConfigReconciler(
client client.Client,
nonCachedClient client.Client,
scheme *runtime.Scheme,
namespaceCache *namespaceCache) *WorkspacesConfigReconciler {

Expand All @@ -112,6 +114,7 @@ func NewWorkspacesConfigReconciler(
return &WorkspacesConfigReconciler{
scheme: scheme,
client: client,
nonCachedClient: nonCachedClient,
namespaceCache: namespaceCache,
labelsToRemoveBeforeSync: labelsToRemoveBeforeSync,
annotationsToRemoveBeforeSync: annotationsToRemoveBeforeSync,
Expand Down Expand Up @@ -520,14 +523,35 @@ func (r *WorkspacesConfigReconciler) doCreateObject(
syncContext *syncContext,
dstObj client.Object) error {

if err := r.client.Create(syncContext.ctx, dstObj); err != nil {
return err
err := r.client.Create(syncContext.ctx, dstObj)
if err != nil {
if !errors.IsAlreadyExists(err) {
return err
}

// AlreadyExists Error might happen if object already exists and doesn't contain
// `app.kubernetes.io/part-of=che.eclipse.org` label (is not cached)
// 1. Delete the object from a destination namespace using non-cached client
// 2. Create the object again using cached client
if err = deploy.DeleteIgnoreIfNotFound(
syncContext.ctx,
r.nonCachedClient,
types.NamespacedName{
Name: dstObj.GetName(),
Namespace: dstObj.GetNamespace(),
},
dstObj); err != nil {
return err
}

if err = r.client.Create(syncContext.ctx, dstObj); err != nil {
return err
}
}

logger.Info("Object created", "namespace", dstObj.GetNamespace(),
"kind", gvk2PrintString(syncContext.object2Sync.getGKV()),
"name", dstObj.GetName())

return nil
}

Expand Down
61 changes: 60 additions & 1 deletion controllers/usernamespace/workspaces_config_controller_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (c) 2019-2024 Red Hat, Inc.
// Copyright (c) 2019-2025 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/
Expand Down Expand Up @@ -31,6 +31,63 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestRecreateObjectIfAlreadyExists(t *testing.T) {
// Actual object in a user namespace
srcObject := &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "user-che",
},
Data: map[string]string{
"key": "value",
},
}

// Expected object in a user namespace
dstObject := &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "user-che",
},
Data: map[string]string{
"new-key": "new-value",
},
}

ctx := test.GetDeployContext(nil, []runtime.Object{srcObject})

workspaceConfigReconciler := NewWorkspacesConfigReconciler(
ctx.ClusterAPI.Client,
ctx.ClusterAPI.Client,
ctx.ClusterAPI.Scheme,
NewNamespaceCache(ctx.ClusterAPI.NonCachingClient))

syncContext := &syncContext{
dstNamespace: "user-che",
srcNamespace: "eclipse-che",
object2Sync: &configMap2Sync{cm: srcObject},
syncConfig: map[string]string{},
}

err := workspaceConfigReconciler.doCreateObject(syncContext, dstObject)
assert.NoError(t, err)

cm := &corev1.ConfigMap{}
exists, err := deploy.Get(ctx, types.NamespacedName{Namespace: "user-che", Name: "test"}, cm)
assert.NoError(t, err)
assert.True(t, exists)
assert.Equal(t, 1, len(cm.Data))
assert.Equal(t, "new-value", cm.Data["new-key"])
}

func TestDeleteIfObjectIsObsolete(t *testing.T) {
ctx := test.GetDeployContext(nil, []runtime.Object{
&corev1.ConfigMap{
Expand All @@ -46,6 +103,7 @@ func TestDeleteIfObjectIsObsolete(t *testing.T) {
})

workspaceConfigReconciler := NewWorkspacesConfigReconciler(
ctx.ClusterAPI.Client,
ctx.ClusterAPI.Client,
ctx.ClusterAPI.Scheme,
NewNamespaceCache(ctx.ClusterAPI.NonCachingClient))
Expand Down Expand Up @@ -103,6 +161,7 @@ func TestGetEmptySyncConfig(t *testing.T) {
ctx := test.GetDeployContext(nil, []runtime.Object{})

workspaceConfigReconciler := NewWorkspacesConfigReconciler(
ctx.ClusterAPI.Client,
ctx.ClusterAPI.Client,
ctx.ClusterAPI.Scheme,
NewNamespaceCache(ctx.ClusterAPI.NonCachingClient))
Expand Down
6 changes: 3 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (c) 2019-2023 Red Hat, Inc.
ma//

Check failure on line 1 in main.go

View workflow job for this annotation

GitHub Actions / coverage-report

expected 'package', found ma

Check failure on line 1 in main.go

View workflow job for this annotation

GitHub Actions / resources-validation

expected 'IDENT', found newline

Check failure on line 1 in main.go

View workflow job for this annotation

GitHub Actions / resources-validation

expected 'package', found ma

Check failure on line 1 in main.go

View workflow job for this annotation

GitHub Actions / resources-validation

expected 'IDENT', found newline

Check failure on line 1 in main.go

View workflow job for this annotation

GitHub Actions / resources-validation

expected 'package', found ma

Check failure on line 1 in main.go

View workflow job for this annotation

GitHub Actions / resources-validation

expected 'IDENT', found newline

Check failure on line 1 in main.go

View workflow job for this annotation

GitHub Actions / resources-validation

expected 'package', found ma

Check failure on line 1 in main.go

View workflow job for this annotation

GitHub Actions / resources-validation

expected 'package', found ma

Check failure on line 1 in main.go

View workflow job for this annotation

GitHub Actions / unit-tests

expected 'package', found ma
// Copyright (c) 2019-2025 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/
Expand All @@ -10,7 +10,7 @@
// Red Hat, Inc. - initial API and implementation
//

package main

Check failure on line 13 in main.go

View workflow job for this annotation

GitHub Actions / resources-validation

expected ';', found 'package'

Check failure on line 13 in main.go

View workflow job for this annotation

GitHub Actions / resources-validation

expected ';', found 'package'

Check failure on line 13 in main.go

View workflow job for this annotation

GitHub Actions / resources-validation

expected ';', found 'package'

import (
"flag"
Expand Down Expand Up @@ -290,7 +290,7 @@
os.Exit(1)
}

workspacesConfigReconciler := usernamespace.NewWorkspacesConfigReconciler(mgr.GetClient(), mgr.GetScheme(), namespacechace)
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)
Expand Down
117 changes: 117 additions & 0 deletions pkg/common/utils/sync.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//
// Copyright (c) 2019-2025 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 utils

import (
"context"
"fmt"
"reflect"

ctrl "sigs.k8s.io/controller-runtime"

"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)

var (
syncLog = ctrl.Log.WithName("sync")
)

type Syncer interface {
// Get gets object.
// Returns true if object exists otherwise returns false.
// Returns error if object cannot be retrieved otherwise returns nil.
Get(key client.ObjectKey, objectMeta client.Object) (bool, error)
// Delete does delete object by key.
// Returns true if object deleted or not found otherwise returns false.
// Returns error if object cannot be deleted otherwise returns nil.
Delete(context context.Context, key client.ObjectKey, objectMeta client.Object) (bool, error)
}

type ObjSyncer struct {
syncer Syncer
cli client.Client
}

func (s ObjSyncer) Get(context context.Context, key client.ObjectKey, objectMeta client.Object) (bool, error) {
return s.doGetIgnoreNotFound(context, key, objectMeta)
}

func (s ObjSyncer) Delete(context context.Context, key client.ObjectKey, objectMeta client.Object) (bool, error) {
return s.deleteByKeyIgnoreNotFound(context, key, objectMeta)
}

// deleteByKeyIgnoreNotFound deletes object by key.
// Returns true if object deleted or not found otherwise returns false.
// Returns error if object cannot be deleted otherwise returns nil.
func (s ObjSyncer) deleteByKeyIgnoreNotFound(context context.Context, key client.ObjectKey, objectMeta client.Object) (bool, error) {
runtimeObject, ok := objectMeta.(runtime.Object)
if !ok {
return false, fmt.Errorf("object %T is not a runtime.Object", runtimeObject)
}

actual := runtimeObject.DeepCopyObject().(client.Object)
if exists, err := s.doGetIgnoreNotFound(context, key, actual); !exists {
return true, nil
} else if err != nil {
return false, err
}

return s.doDeleteIgnoreIfNotFound(context, actual)
}

// 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 (s ObjSyncer) doDeleteIgnoreIfNotFound(
context context.Context,
object client.Object,
) (bool, error) {
if err := s.cli.Delete(context, object); err == nil {
if errors.IsNotFound(err) {
syncLog.Info("Object not found", "namespace", object.GetNamespace(), "kind", GetObjectType(object), "name", object.GetName())
} else {
syncLog.Info("Object deleted", "namespace", object.GetNamespace(), "kind", GetObjectType(object), "name", object.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 (s ObjSyncer) doGetIgnoreNotFound(
context context.Context,
key client.ObjectKey,
object client.Object,
) (bool, error) {
if err := s.cli.Get(context, key, object); err == nil {
return true, nil
} else if errors.IsNotFound(err) {
return false, nil
} else {
return false, err
}
}

func GetObjectType(obj interface{}) string {
objType := reflect.TypeOf(obj).String()
if reflect.TypeOf(obj).Kind().String() == "ptr" {
objType = objType[1:]
}

return objType
}
Loading
Loading