Skip to content

Commit c86e99f

Browse files
committed
fix: Recreate non cached object in a user namespace
Signed-off-by: Anatolii Bazko <abazko@redhat.com>
1 parent 8094056 commit c86e99f

File tree

9 files changed

+222
-243
lines changed

9 files changed

+222
-243
lines changed

controllers/usernamespace/configmap2sync_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright (c) 2019-2024 Red Hat, Inc.
2+
// Copyright (c) 2019-2025 Red Hat, Inc.
33
// This program and the accompanying materials are made
44
// available under the terms of the Eclipse Public License 2.0
55
// which is available at https://www.eclipse.org/legal/epl-2.0/
@@ -69,6 +69,7 @@ func TestSyncConfigMap(t *testing.T) {
6969
}})
7070

7171
workspaceConfigReconciler := NewWorkspacesConfigReconciler(
72+
deployContext.ClusterAPI.Client,
7273
deployContext.ClusterAPI.Client,
7374
deployContext.ClusterAPI.Scheme,
7475
&namespaceCache{
@@ -235,6 +236,7 @@ func TestSyncConfigMapShouldMergeLabelsAndAnnotationsOnUpdate(t *testing.T) {
235236
}})
236237

237238
workspaceConfigReconciler := NewWorkspacesConfigReconciler(
239+
deployContext.ClusterAPI.Client,
238240
deployContext.ClusterAPI.Client,
239241
deployContext.ClusterAPI.Scheme,
240242
&namespaceCache{
@@ -352,6 +354,7 @@ func TestSyncConfigMapShouldRespectDWOLabels(t *testing.T) {
352354
}})
353355

354356
workspaceConfigReconciler := NewWorkspacesConfigReconciler(
357+
deployContext.ClusterAPI.Client,
355358
deployContext.ClusterAPI.Client,
356359
deployContext.ClusterAPI.Scheme,
357360
&namespaceCache{
@@ -449,6 +452,7 @@ func TestSyncConfigMapShouldRemoveSomeLabels(t *testing.T) {
449452
}})
450453

451454
workspaceConfigReconciler := NewWorkspacesConfigReconciler(
455+
deployContext.ClusterAPI.Client,
452456
deployContext.ClusterAPI.Client,
453457
deployContext.ClusterAPI.Scheme,
454458
&namespaceCache{

controllers/usernamespace/pvc2sync_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright (c) 2019-2024 Red Hat, Inc.
2+
// Copyright (c) 2019-20255 Red Hat, Inc.
33
// This program and the accompanying materials are made
44
// available under the terms of the Eclipse Public License 2.0
55
// which is available at https://www.eclipse.org/legal/epl-2.0/
@@ -56,6 +56,7 @@ func TestSyncPVC(t *testing.T) {
5656
}})
5757

5858
workspaceConfigReconciler := NewWorkspacesConfigReconciler(
59+
deployContext.ClusterAPI.Client,
5960
deployContext.ClusterAPI.Client,
6061
deployContext.ClusterAPI.Scheme,
6162
&namespaceCache{

controllers/usernamespace/secret2sync_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright (c) 2019-2024 Red Hat, Inc.
2+
// Copyright (c) 2019-2025 Red Hat, Inc.
33
// This program and the accompanying materials are made
44
// available under the terms of the Eclipse Public License 2.0
55
// which is available at https://www.eclipse.org/legal/epl-2.0/
@@ -60,6 +60,7 @@ func TestSyncSecrets(t *testing.T) {
6060
}})
6161

6262
workspaceConfigReconciler := NewWorkspacesConfigReconciler(
63+
deployContext.ClusterAPI.Client,
6364
deployContext.ClusterAPI.Client,
6465
deployContext.ClusterAPI.Scheme,
6566
&namespaceCache{
@@ -235,6 +236,7 @@ func TestSyncSecretShouldMergeLabelsAndAnnotationsOnUpdate(t *testing.T) {
235236
}})
236237

237238
workspaceConfigReconciler := NewWorkspacesConfigReconciler(
239+
deployContext.ClusterAPI.Client,
238240
deployContext.ClusterAPI.Client,
239241
deployContext.ClusterAPI.Scheme,
240242
&namespaceCache{
@@ -341,6 +343,7 @@ func TestSyncSecretShouldRespectDWOLabels(t *testing.T) {
341343
}})
342344

343345
workspaceConfigReconciler := NewWorkspacesConfigReconciler(
346+
deployContext.ClusterAPI.Client,
344347
deployContext.ClusterAPI.Client,
345348
deployContext.ClusterAPI.Scheme,
346349
&namespaceCache{
@@ -439,6 +442,7 @@ func TestSyncSecretShouldRemoveSomeLabels(t *testing.T) {
439442
}})
440443

441444
workspaceConfigReconciler := NewWorkspacesConfigReconciler(
445+
deployContext.ClusterAPI.Client,
442446
deployContext.ClusterAPI.Client,
443447
deployContext.ClusterAPI.Scheme,
444448
&namespaceCache{

controllers/usernamespace/unstructured2sync_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright (c) 2019-2024 Red Hat, Inc.
2+
// Copyright (c) 2019-2025 Red Hat, Inc.
33
// This program and the accompanying materials are made
44
// available under the terms of the Eclipse Public License 2.0
55
// which is available at https://www.eclipse.org/legal/epl-2.0/
@@ -82,6 +82,7 @@ func TestSyncTemplateWithLimitRange(t *testing.T) {
8282
}})
8383

8484
workspaceConfigReconciler := NewWorkspacesConfigReconciler(
85+
deployContext.ClusterAPI.Client,
8586
deployContext.ClusterAPI.Client,
8687
deployContext.ClusterAPI.Scheme,
8788
&namespaceCache{

controllers/usernamespace/workspaces_config_controller.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright (c) 2019-2024 Red Hat, Inc.
2+
// Copyright (c) 2019-2025 Red Hat, Inc.
33
// This program and the accompanying materials are made
44
// available under the terms of the Eclipse Public License 2.0
55
// which is available at https://www.eclipse.org/legal/epl-2.0/
@@ -50,6 +50,7 @@ const (
5050
type WorkspacesConfigReconciler struct {
5151
scheme *runtime.Scheme
5252
client client.Client
53+
nonCachedClient client.Client
5354
namespaceCache *namespaceCache
5455
labelsToRemoveBeforeSync []*regexp.Regexp
5556
annotationsToRemoveBeforeSync []*regexp.Regexp
@@ -87,6 +88,7 @@ var (
8788

8889
func NewWorkspacesConfigReconciler(
8990
client client.Client,
91+
nonCachedClient client.Client,
9092
scheme *runtime.Scheme,
9193
namespaceCache *namespaceCache) *WorkspacesConfigReconciler {
9294

@@ -112,6 +114,7 @@ func NewWorkspacesConfigReconciler(
112114
return &WorkspacesConfigReconciler{
113115
scheme: scheme,
114116
client: client,
117+
nonCachedClient: nonCachedClient,
115118
namespaceCache: namespaceCache,
116119
labelsToRemoveBeforeSync: labelsToRemoveBeforeSync,
117120
annotationsToRemoveBeforeSync: annotationsToRemoveBeforeSync,
@@ -520,14 +523,35 @@ func (r *WorkspacesConfigReconciler) doCreateObject(
520523
syncContext *syncContext,
521524
dstObj client.Object) error {
522525

523-
if err := r.client.Create(syncContext.ctx, dstObj); err != nil {
524-
return err
526+
err := r.client.Create(syncContext.ctx, dstObj)
527+
if err != nil {
528+
if !errors.IsAlreadyExists(err) {
529+
return err
530+
}
531+
532+
// AlreadyExists Error might happen if object already exists and doesn't contain
533+
// `app.kubernetes.io/part-of=che.eclipse.org` label (is not cached)
534+
// 1. Delete the object from a destination namespace using non-cached client
535+
// 2. Create the object again using cached client
536+
if err = deploy.DeleteIgnoreIfNotFound(
537+
syncContext.ctx,
538+
r.nonCachedClient,
539+
types.NamespacedName{
540+
Name: dstObj.GetName(),
541+
Namespace: dstObj.GetNamespace(),
542+
},
543+
dstObj); err != nil {
544+
return err
545+
}
546+
547+
if err = r.client.Create(syncContext.ctx, dstObj); err != nil {
548+
return err
549+
}
525550
}
526551

527552
logger.Info("Object created", "namespace", dstObj.GetNamespace(),
528553
"kind", gvk2PrintString(syncContext.object2Sync.getGKV()),
529554
"name", dstObj.GetName())
530-
531555
return nil
532556
}
533557

controllers/usernamespace/workspaces_config_controller_test.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright (c) 2019-2024 Red Hat, Inc.
2+
// Copyright (c) 2019-2025 Red Hat, Inc.
33
// This program and the accompanying materials are made
44
// available under the terms of the Eclipse Public License 2.0
55
// which is available at https://www.eclipse.org/legal/epl-2.0/
@@ -31,6 +31,63 @@ import (
3131
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3232
)
3333

34+
func TestRecreateObjectIfAlreadyExists(t *testing.T) {
35+
// Actual object in a user namespace
36+
srcObject := &corev1.ConfigMap{
37+
TypeMeta: metav1.TypeMeta{
38+
Kind: "ConfigMap",
39+
APIVersion: "v1",
40+
},
41+
ObjectMeta: metav1.ObjectMeta{
42+
Name: "test",
43+
Namespace: "user-che",
44+
},
45+
Data: map[string]string{
46+
"key": "value",
47+
},
48+
}
49+
50+
// Expected object in a user namespace
51+
dstObject := &corev1.ConfigMap{
52+
TypeMeta: metav1.TypeMeta{
53+
Kind: "ConfigMap",
54+
APIVersion: "v1",
55+
},
56+
ObjectMeta: metav1.ObjectMeta{
57+
Name: "test",
58+
Namespace: "user-che",
59+
},
60+
Data: map[string]string{
61+
"new-key": "new-value",
62+
},
63+
}
64+
65+
ctx := test.GetDeployContext(nil, []runtime.Object{srcObject})
66+
67+
workspaceConfigReconciler := NewWorkspacesConfigReconciler(
68+
ctx.ClusterAPI.Client,
69+
ctx.ClusterAPI.Client,
70+
ctx.ClusterAPI.Scheme,
71+
NewNamespaceCache(ctx.ClusterAPI.NonCachingClient))
72+
73+
syncContext := &syncContext{
74+
dstNamespace: "user-che",
75+
srcNamespace: "eclipse-che",
76+
object2Sync: &configMap2Sync{cm: srcObject},
77+
syncConfig: map[string]string{},
78+
}
79+
80+
err := workspaceConfigReconciler.doCreateObject(syncContext, dstObject)
81+
assert.NoError(t, err)
82+
83+
cm := &corev1.ConfigMap{}
84+
exists, err := deploy.Get(ctx, types.NamespacedName{Namespace: "user-che", Name: "test"}, cm)
85+
assert.NoError(t, err)
86+
assert.True(t, exists)
87+
assert.Equal(t, 1, len(cm.Data))
88+
assert.Equal(t, "new-value", cm.Data["new-key"])
89+
}
90+
3491
func TestDeleteIfObjectIsObsolete(t *testing.T) {
3592
ctx := test.GetDeployContext(nil, []runtime.Object{
3693
&corev1.ConfigMap{
@@ -46,6 +103,7 @@ func TestDeleteIfObjectIsObsolete(t *testing.T) {
46103
})
47104

48105
workspaceConfigReconciler := NewWorkspacesConfigReconciler(
106+
ctx.ClusterAPI.Client,
49107
ctx.ClusterAPI.Client,
50108
ctx.ClusterAPI.Scheme,
51109
NewNamespaceCache(ctx.ClusterAPI.NonCachingClient))
@@ -103,6 +161,7 @@ func TestGetEmptySyncConfig(t *testing.T) {
103161
ctx := test.GetDeployContext(nil, []runtime.Object{})
104162

105163
workspaceConfigReconciler := NewWorkspacesConfigReconciler(
164+
ctx.ClusterAPI.Client,
106165
ctx.ClusterAPI.Client,
107166
ctx.ClusterAPI.Scheme,
108167
NewNamespaceCache(ctx.ClusterAPI.NonCachingClient))

main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
//
2-
// Copyright (c) 2019-2023 Red Hat, Inc.
1+
ma//
2+
// Copyright (c) 2019-2025 Red Hat, Inc.
33
// This program and the accompanying materials are made
44
// available under the terms of the Eclipse Public License 2.0
55
// which is available at https://www.eclipse.org/legal/epl-2.0/
@@ -290,7 +290,7 @@ func main() {
290290
os.Exit(1)
291291
}
292292

293-
workspacesConfigReconciler := usernamespace.NewWorkspacesConfigReconciler(mgr.GetClient(), mgr.GetScheme(), namespacechace)
293+
workspacesConfigReconciler := usernamespace.NewWorkspacesConfigReconciler(mgr.GetClient(), nonCachingClient, mgr.GetScheme(), namespacechace)
294294
if err = workspacesConfigReconciler.SetupWithManager(mgr); err != nil {
295295
setupLog.Error(err, "unable to set up controller", "controller", "WorkspacesConfigReconciler")
296296
os.Exit(1)

pkg/common/utils/sync.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
//
2+
// Copyright (c) 2019-2025 Red Hat, Inc.
3+
// This program and the accompanying materials are made
4+
// available under the terms of the Eclipse Public License 2.0
5+
// which is available at https://www.eclipse.org/legal/epl-2.0/
6+
//
7+
// SPDX-License-Identifier: EPL-2.0
8+
//
9+
// Contributors:
10+
// Red Hat, Inc. - initial API and implementation
11+
//
12+
13+
package utils
14+
15+
import (
16+
"context"
17+
"fmt"
18+
"reflect"
19+
20+
ctrl "sigs.k8s.io/controller-runtime"
21+
22+
"k8s.io/apimachinery/pkg/api/errors"
23+
"k8s.io/apimachinery/pkg/runtime"
24+
"sigs.k8s.io/controller-runtime/pkg/client"
25+
)
26+
27+
var (
28+
syncLog = ctrl.Log.WithName("sync")
29+
)
30+
31+
type Syncer interface {
32+
// Get gets object.
33+
// Returns true if object exists otherwise returns false.
34+
// Returns error if object cannot be retrieved otherwise returns nil.
35+
Get(key client.ObjectKey, objectMeta client.Object) (bool, error)
36+
// Delete does delete object by key.
37+
// Returns true if object deleted or not found otherwise returns false.
38+
// Returns error if object cannot be deleted otherwise returns nil.
39+
Delete(context context.Context, key client.ObjectKey, objectMeta client.Object) (bool, error)
40+
}
41+
42+
type ObjSyncer struct {
43+
syncer Syncer
44+
cli client.Client
45+
}
46+
47+
func (s ObjSyncer) Get(context context.Context, key client.ObjectKey, objectMeta client.Object) (bool, error) {
48+
return s.doGetIgnoreNotFound(context, key, objectMeta)
49+
}
50+
51+
func (s ObjSyncer) Delete(context context.Context, key client.ObjectKey, objectMeta client.Object) (bool, error) {
52+
return s.deleteByKeyIgnoreNotFound(context, key, objectMeta)
53+
}
54+
55+
// deleteByKeyIgnoreNotFound deletes object by key.
56+
// Returns true if object deleted or not found otherwise returns false.
57+
// Returns error if object cannot be deleted otherwise returns nil.
58+
func (s ObjSyncer) deleteByKeyIgnoreNotFound(context context.Context, key client.ObjectKey, objectMeta client.Object) (bool, error) {
59+
runtimeObject, ok := objectMeta.(runtime.Object)
60+
if !ok {
61+
return false, fmt.Errorf("object %T is not a runtime.Object", runtimeObject)
62+
}
63+
64+
actual := runtimeObject.DeepCopyObject().(client.Object)
65+
if exists, err := s.doGetIgnoreNotFound(context, key, actual); !exists {
66+
return true, nil
67+
} else if err != nil {
68+
return false, err
69+
}
70+
71+
return s.doDeleteIgnoreIfNotFound(context, actual)
72+
}
73+
74+
// doDeleteIgnoreIfNotFound deletes object.
75+
// Returns true if object deleted or not found otherwise returns false.
76+
// Returns error if object cannot be deleted otherwise returns nil.
77+
func (s ObjSyncer) doDeleteIgnoreIfNotFound(
78+
context context.Context,
79+
object client.Object,
80+
) (bool, error) {
81+
if err := s.cli.Delete(context, object); err == nil {
82+
if errors.IsNotFound(err) {
83+
syncLog.Info("Object not found", "namespace", object.GetNamespace(), "kind", GetObjectType(object), "name", object.GetName())
84+
} else {
85+
syncLog.Info("Object deleted", "namespace", object.GetNamespace(), "kind", GetObjectType(object), "name", object.GetName())
86+
}
87+
return true, nil
88+
} else {
89+
return false, err
90+
}
91+
}
92+
93+
// doGet gets object.
94+
// Returns true if object exists otherwise returns false.
95+
// Returns error if object cannot be retrieved otherwise returns nil.
96+
func (s ObjSyncer) doGetIgnoreNotFound(
97+
context context.Context,
98+
key client.ObjectKey,
99+
object client.Object,
100+
) (bool, error) {
101+
if err := s.cli.Get(context, key, object); err == nil {
102+
return true, nil
103+
} else if errors.IsNotFound(err) {
104+
return false, nil
105+
} else {
106+
return false, err
107+
}
108+
}
109+
110+
func GetObjectType(obj interface{}) string {
111+
objType := reflect.TypeOf(obj).String()
112+
if reflect.TypeOf(obj).Kind().String() == "ptr" {
113+
objType = objType[1:]
114+
}
115+
116+
return objType
117+
}

0 commit comments

Comments
 (0)