Skip to content

Commit a981247

Browse files
yankawailexfrei
authored andcommitted
fix(registry): preserve write options in aggregated storage
Signed-off-by: Yan Bondarenko <[email protected]> Assisted-by: LLM
1 parent 88a7ea0 commit a981247

9 files changed

Lines changed: 773 additions & 32 deletions

File tree

‎pkg/registry/apps/application/rest.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
242242
klog.V(6).Infof("Creating HelmRelease %s in namespace %s", helmRelease.Name, app.Namespace)
243243

244244
// Create HelmRelease in Kubernetes
245-
err = r.c.Create(ctx, helmRelease, &client.CreateOptions{Raw: options})
245+
err = r.c.Create(ctx, helmRelease, registry.ClientCreateOptions(options))
246246
if err != nil {
247247
klog.Errorf("Failed to create HelmRelease %s: %v", helmRelease.Name, err)
248248
return nil, fmt.Errorf("failed to create HelmRelease: %v", err)
@@ -499,7 +499,7 @@ func (r *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObje
499499
klog.Errorf("Failed to get updated object: %v", err)
500500
return nil, false, err
501501
}
502-
createdObj, err := r.Create(ctx, obj, createValidation, &metav1.CreateOptions{})
502+
createdObj, err := r.Create(ctx, obj, createValidation, registry.CreateOptionsFromUpdate(options))
503503
if err != nil {
504504
klog.Errorf("Failed to create new Application: %v", err)
505505
return nil, false, err
@@ -606,7 +606,7 @@ func (r *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObje
606606
// real spec conflict here: refresh the resourceVersion from the live object
607607
// and retry.
608608
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
609-
updateErr := r.c.Update(ctx, helmRelease, &client.UpdateOptions{Raw: &metav1.UpdateOptions{}})
609+
updateErr := r.c.Update(ctx, helmRelease, registry.ClientUpdateOptions(options))
610610
if apierrors.IsConflict(updateErr) {
611611
cur := &helmv2.HelmRelease{}
612612
if getErr := r.c.Get(ctx, client.ObjectKey{Namespace: helmRelease.Namespace, Name: helmRelease.Name}, cur, &client.GetOptions{Raw: &metav1.GetOptions{}}); getErr != nil {
@@ -688,7 +688,7 @@ func (r *REST) Delete(ctx context.Context, name string, deleteValidation rest.Va
688688
klog.V(6).Infof("Deleting HelmRelease %s in namespace %s", helmReleaseName, namespace)
689689

690690
// Delete the HelmRelease corresponding to the Application
691-
err = r.c.Delete(ctx, helmRelease, &client.DeleteOptions{Raw: options})
691+
err = r.c.Delete(ctx, helmRelease, registry.ClientDeleteOptions(options))
692692
if err != nil {
693693
klog.Errorf("Failed to delete HelmRelease %s: %v", helmReleaseName, err)
694694
return nil, false, fmt.Errorf("failed to delete HelmRelease: %v", err)
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/*
2+
Copyright 2026 The Cozystack Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package application
18+
19+
import (
20+
"context"
21+
"reflect"
22+
"testing"
23+
24+
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
25+
helmv2 "github.com/fluxcd/helm-controller/api/v2"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/apimachinery/pkg/runtime"
28+
"k8s.io/apimachinery/pkg/types"
29+
"k8s.io/apiserver/pkg/endpoints/request"
30+
"k8s.io/apiserver/pkg/registry/rest"
31+
"sigs.k8s.io/controller-runtime/pkg/client"
32+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
33+
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
34+
35+
appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1"
36+
"github.com/cozystack/cozystack/pkg/config"
37+
)
38+
39+
const optionsTestNamespace = "tenant-options"
40+
41+
func newOptionsTestREST(t *testing.T, funcs interceptor.Funcs, objects ...client.Object) *REST {
42+
t.Helper()
43+
scheme := runtime.NewScheme()
44+
if err := cozyv1alpha1.AddToScheme(scheme); err != nil {
45+
t.Fatalf("register Cozystack API scheme: %v", err)
46+
}
47+
if err := helmv2.AddToScheme(scheme); err != nil {
48+
t.Fatalf("register HelmRelease scheme: %v", err)
49+
}
50+
c := fake.NewClientBuilder().
51+
WithScheme(scheme).
52+
WithObjects(objects...).
53+
WithInterceptorFuncs(funcs).
54+
Build()
55+
return NewREST(c, nil, &config.Resource{
56+
Application: config.ApplicationConfig{
57+
Kind: "PostgreSQL",
58+
Plural: "postgresqls",
59+
Singular: "postgresql",
60+
},
61+
Release: config.ReleaseConfig{Prefix: "postgresql-"},
62+
})
63+
}
64+
65+
func optionsTestApplication() *appsv1alpha1.Application {
66+
return &appsv1alpha1.Application{
67+
TypeMeta: metav1.TypeMeta{APIVersion: "apps.cozystack.io/v1alpha1", Kind: "PostgreSQL"},
68+
ObjectMeta: metav1.ObjectMeta{
69+
Name: "example",
70+
Namespace: optionsTestNamespace,
71+
},
72+
}
73+
}
74+
75+
func optionsTestHelmRelease() *helmv2.HelmRelease {
76+
return &helmv2.HelmRelease{ObjectMeta: metav1.ObjectMeta{
77+
Name: "postgresql-example",
78+
Namespace: optionsTestNamespace,
79+
Labels: map[string]string{
80+
ApplicationKindLabel: "PostgreSQL",
81+
ApplicationGroupLabel: appsv1alpha1.GroupName,
82+
ApplicationNameLabel: "example",
83+
},
84+
}}
85+
}
86+
87+
func captureCreateOptions(opts ...client.CreateOption) *metav1.CreateOptions {
88+
converted := (&client.CreateOptions{}).ApplyOptions(opts)
89+
return converted.AsCreateOptions().DeepCopy()
90+
}
91+
92+
func captureUpdateOptions(opts ...client.UpdateOption) *metav1.UpdateOptions {
93+
converted := (&client.UpdateOptions{}).ApplyOptions(opts)
94+
return converted.AsUpdateOptions().DeepCopy()
95+
}
96+
97+
func captureDeleteOptions(opts ...client.DeleteOption) *metav1.DeleteOptions {
98+
converted := &client.DeleteOptions{}
99+
for _, option := range opts {
100+
option.ApplyToDelete(converted)
101+
}
102+
return converted.AsDeleteOptions().DeepCopy()
103+
}
104+
105+
func TestCreatePropagatesWriteOptions(t *testing.T) {
106+
want := &metav1.CreateOptions{
107+
DryRun: []string{metav1.DryRunAll},
108+
FieldManager: "create-manager",
109+
FieldValidation: "Strict",
110+
}
111+
var got *metav1.CreateOptions
112+
r := newOptionsTestREST(t, interceptor.Funcs{
113+
Create: func(_ context.Context, _ client.WithWatch, _ client.Object, opts ...client.CreateOption) error {
114+
got = captureCreateOptions(opts...)
115+
return nil
116+
},
117+
})
118+
ctx := request.WithNamespace(context.Background(), optionsTestNamespace)
119+
if _, err := r.Create(ctx, optionsTestApplication(), nil, want); err != nil {
120+
t.Fatalf("Create returned error: %v", err)
121+
}
122+
if !reflect.DeepEqual(got, want) {
123+
t.Fatalf("Create options lost in controller-runtime conversion:\n got: %#v\nwant: %#v", got, want)
124+
}
125+
}
126+
127+
func TestUpdatePropagatesWriteOptions(t *testing.T) {
128+
want := &metav1.UpdateOptions{
129+
DryRun: []string{metav1.DryRunAll},
130+
FieldManager: "update-manager",
131+
FieldValidation: "Warn",
132+
}
133+
var got *metav1.UpdateOptions
134+
r := newOptionsTestREST(t, interceptor.Funcs{
135+
Update: func(_ context.Context, _ client.WithWatch, _ client.Object, opts ...client.UpdateOption) error {
136+
got = captureUpdateOptions(opts...)
137+
return nil
138+
},
139+
}, optionsTestHelmRelease())
140+
ctx := request.WithNamespace(context.Background(), optionsTestNamespace)
141+
if _, _, err := r.Update(ctx, "example", rest.DefaultUpdatedObjectInfo(optionsTestApplication()), nil, nil, false, want); err != nil {
142+
t.Fatalf("Update returned error: %v", err)
143+
}
144+
if !reflect.DeepEqual(got, want) {
145+
t.Fatalf("Update options lost in controller-runtime conversion:\n got: %#v\nwant: %#v", got, want)
146+
}
147+
}
148+
149+
func TestUpdateForceCreatePropagatesWriteOptions(t *testing.T) {
150+
update := &metav1.UpdateOptions{
151+
DryRun: []string{metav1.DryRunAll},
152+
FieldManager: "apply-manager",
153+
FieldValidation: "Strict",
154+
}
155+
want := &metav1.CreateOptions{
156+
DryRun: update.DryRun,
157+
FieldManager: update.FieldManager,
158+
FieldValidation: update.FieldValidation,
159+
}
160+
var got *metav1.CreateOptions
161+
r := newOptionsTestREST(t, interceptor.Funcs{
162+
Create: func(_ context.Context, _ client.WithWatch, _ client.Object, opts ...client.CreateOption) error {
163+
got = captureCreateOptions(opts...)
164+
return nil
165+
},
166+
})
167+
ctx := request.WithNamespace(context.Background(), optionsTestNamespace)
168+
if _, _, err := r.Update(ctx, "example", rest.DefaultUpdatedObjectInfo(optionsTestApplication()), nil, nil, true, update); err != nil {
169+
t.Fatalf("force-create Update returned error: %v", err)
170+
}
171+
if !reflect.DeepEqual(got, want) {
172+
t.Fatalf("force-create options lost in controller-runtime conversion:\n got: %#v\nwant: %#v", got, want)
173+
}
174+
}
175+
176+
func TestDeletePropagatesWriteOptions(t *testing.T) {
177+
grace := int64(3)
178+
propagation := metav1.DeletePropagationForeground
179+
uid := types.UID("expected-uid")
180+
rv := "17"
181+
want := &metav1.DeleteOptions{
182+
GracePeriodSeconds: &grace,
183+
Preconditions: &metav1.Preconditions{UID: &uid, ResourceVersion: &rv},
184+
PropagationPolicy: &propagation,
185+
DryRun: []string{metav1.DryRunAll},
186+
}
187+
var got *metav1.DeleteOptions
188+
r := newOptionsTestREST(t, interceptor.Funcs{
189+
Delete: func(_ context.Context, _ client.WithWatch, _ client.Object, opts ...client.DeleteOption) error {
190+
got = captureDeleteOptions(opts...)
191+
return nil
192+
},
193+
}, optionsTestHelmRelease())
194+
ctx := request.WithNamespace(context.Background(), optionsTestNamespace)
195+
if _, _, err := r.Delete(ctx, "example", nil, want); err != nil {
196+
t.Fatalf("Delete returned error: %v", err)
197+
}
198+
if !reflect.DeepEqual(got, want) {
199+
t.Fatalf("Delete options lost in controller-runtime conversion:\n got: %#v\nwant: %#v", got, want)
200+
}
201+
}

‎pkg/registry/core/tenantsecret/rest.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ func (r *REST) Create(
265265
}
266266

267267
sec := tenantToSecret(in, nil)
268-
err := r.c.Create(ctx, sec, &client.CreateOptions{Raw: opts})
268+
err := r.c.Create(ctx, sec, registry.ClientCreateOptions(opts))
269269
if err != nil {
270270
return nil, err
271271
}
@@ -412,7 +412,7 @@ func (r *REST) Update(
412412
return nil, false, err
413413
}
414414
}
415-
err := r.c.Create(ctx, newSec, &client.CreateOptions{Raw: &metav1.CreateOptions{}})
415+
err := r.c.Create(ctx, newSec, registry.ClientCreateOptionsFromUpdate(opts))
416416
return secretToTenant(newSec), true, err
417417
}
418418

@@ -423,7 +423,7 @@ func (r *REST) Update(
423423
}
424424

425425
newSec.ResourceVersion = cur.ResourceVersion
426-
err = r.c.Update(ctx, newSec, &client.UpdateOptions{Raw: opts})
426+
err = r.c.Update(ctx, newSec, registry.ClientUpdateOptions(opts))
427427
return secretToTenant(newSec), false, err
428428
}
429429

@@ -450,7 +450,7 @@ func (r *REST) Delete(
450450
}
451451
}
452452

453-
err = r.c.Delete(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}}, &client.DeleteOptions{Raw: opts})
453+
err = r.c.Delete(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}}, registry.ClientDeleteOptions(opts))
454454
return nil, err == nil, err
455455
}
456456

0 commit comments

Comments
 (0)