Skip to content

Commit be1ff91

Browse files
committed
[apps] Refactor apiserver to use typed objects and fix UnstructuredList GVK
This commit refactors the apiserver REST handlers to use typed objects (appsv1alpha1.Application) instead of unstructured.Unstructured, eliminating the need for runtime conversions and simplifying the codebase. Additionally, it fixes an issue where UnstructuredList objects were using the first registered kind from typeToGVK instead of the kind from the object's field when multiple kinds are registered with the same Go type. This is a more comprehensive fix for the problem addressed in #1630, which was reverted in #1677. The fix includes the upstream fix from kubernetes/kubernetes#135537, which enables short-circuit path for UnstructuredList similar to regular Unstructured objects, using GVK from the object field instead of typeToGVK. Changes: - Refactored rest.go handlers to use typed Application objects - Removed unstructured.Unstructured conversions - Fixed UnstructuredList GVK handling - Updated dependencies in go.mod/go.sum - Added e2e test for OpenAPI validation - Updated Dockerfile Signed-off-by: Andrei Kvapil <[email protected]>
1 parent c7f478f commit be1ff91

5 files changed

Lines changed: 100 additions & 188 deletions

File tree

‎go.mod‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ go 1.23.0
66

77
require (
88
github.com/fluxcd/helm-controller/api v1.1.0
9+
github.com/go-logr/logr v1.4.2
10+
github.com/go-logr/zapr v1.3.0
911
github.com/google/gofuzz v1.2.0
1012
github.com/onsi/ginkgo/v2 v2.19.0
1113
github.com/onsi/gomega v1.33.1
14+
github.com/prometheus/client_golang v1.19.1
1215
github.com/spf13/cobra v1.8.1
1316
github.com/stretchr/testify v1.9.0
17+
go.uber.org/zap v1.27.0
1418
gopkg.in/yaml.v2 v2.4.0
1519
k8s.io/api v0.31.2
1620
k8s.io/apiextensions-apiserver v0.31.2
@@ -44,9 +48,7 @@ require (
4448
github.com/fluxcd/pkg/apis/meta v1.6.1 // indirect
4549
github.com/fsnotify/fsnotify v1.7.0 // indirect
4650
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
47-
github.com/go-logr/logr v1.4.2 // indirect
4851
github.com/go-logr/stdr v1.2.2 // indirect
49-
github.com/go-logr/zapr v1.3.0 // indirect
5052
github.com/go-openapi/jsonpointer v0.21.0 // indirect
5153
github.com/go-openapi/jsonreference v0.20.2 // indirect
5254
github.com/go-openapi/swag v0.23.0 // indirect
@@ -74,7 +76,6 @@ require (
7476
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
7577
github.com/pkg/errors v0.9.1 // indirect
7678
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
77-
github.com/prometheus/client_golang v1.19.1 // indirect
7879
github.com/prometheus/client_model v0.6.1 // indirect
7980
github.com/prometheus/common v0.55.0 // indirect
8081
github.com/prometheus/procfs v0.15.1 // indirect
@@ -94,7 +95,6 @@ require (
9495
go.opentelemetry.io/otel/trace v1.28.0 // indirect
9596
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
9697
go.uber.org/multierr v1.11.0 // indirect
97-
go.uber.org/zap v1.27.0 // indirect
9898
golang.org/x/crypto v0.31.0 // indirect
9999
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
100100
golang.org/x/net v0.33.0 // indirect
@@ -119,3 +119,6 @@ require (
119119
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
120120
sigs.k8s.io/yaml v1.4.0 // indirect
121121
)
122+
123+
// See: issues.k8s.io/135537
124+
replace k8s.io/apimachinery => github.com/cozystack/apimachinery v0.0.0-20251201201312-18e522a87614

‎go.sum‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr
1818
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
1919
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
2020
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
21+
github.com/cozystack/apimachinery v0.0.0-20251201201312-18e522a87614 h1:jH9elECUvhiIs3IMv3oS5k1JgCLVsSK6oU4dmq5gyW8=
22+
github.com/cozystack/apimachinery v0.0.0-20251201201312-18e522a87614/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
2123
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
2224
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
2325
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -291,8 +293,6 @@ k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0=
291293
k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk=
292294
k8s.io/apiextensions-apiserver v0.31.2 h1:W8EwUb8+WXBLu56ser5IudT2cOho0gAKeTOnywBLxd0=
293295
k8s.io/apiextensions-apiserver v0.31.2/go.mod h1:i+Geh+nGCJEGiCGR3MlBDkS7koHIIKWVfWeRFiOsUcM=
294-
k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw=
295-
k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
296296
k8s.io/apiserver v0.31.2 h1:VUzOEUGRCDi6kX1OyQ801m4A7AUPglpsmGvdsekmcI4=
297297
k8s.io/apiserver v0.31.2/go.mod h1:o3nKZR7lPlJqkU5I3Ove+Zx3JuoFjQobGX1Gctw6XuE=
298298
k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc=

‎hack/e2e-test-openapi.bats‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,21 @@
2121
}
2222

2323
@test "Test kinds" {
24+
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/tenants | jq -r '.kind')
25+
if [ "$val" != "TenantList" ]; then
26+
echo "Expected kind to be TenantList, got $val"
27+
exit 1
28+
fi
2429
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/tenants | jq -r '.items[0].kind')
2530
if [ "$val" != "Tenant" ]; then
2631
echo "Expected kind to be Tenant, got $val"
2732
exit 1
2833
fi
34+
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/ingresses | jq -r '.kind')
35+
if [ "$val" != "IngressList" ]; then
36+
echo "Expected kind to be IngressList, got $val"
37+
exit 1
38+
fi
2939
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/ingresses | jq -r '.items[0].kind')
3040
if [ "$val" != "Ingress" ]; then
3141
echo "Expected kind to be Ingress, got $val"

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

Lines changed: 42 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import (
2828
helmv2 "github.com/fluxcd/helm-controller/api/v2"
2929
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
3030
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31-
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3231
fields "k8s.io/apimachinery/pkg/fields"
3332
labels "k8s.io/apimachinery/pkg/labels"
3433
"k8s.io/apimachinery/pkg/runtime"
@@ -142,17 +141,9 @@ func (r *REST) GetSingularName() string {
142141
// Create handles the creation of a new Application by converting it to a HelmRelease
143142
func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
144143
// Assert the object is of type Application
145-
us, ok := obj.(*unstructured.Unstructured)
144+
app, ok := obj.(*appsv1alpha1.Application)
146145
if !ok {
147-
return nil, fmt.Errorf("expected unstructured.Unstructured object, got %T", obj)
148-
}
149-
150-
app := &appsv1alpha1.Application{}
151-
152-
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(us.Object, app); err != nil {
153-
errMsg := fmt.Sprintf("returned unstructured.Unstructured object was not an Application")
154-
klog.Errorf(errMsg)
155-
return nil, fmt.Errorf(errMsg)
146+
return nil, fmt.Errorf("expected *appsv1alpha1.Application object, got %T", obj)
156147
}
157148

158149
// Convert Application to HelmRelease
@@ -186,15 +177,14 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
186177

187178
klog.V(6).Infof("Successfully created and converted HelmRelease %s to Application", helmRelease.GetName())
188179

189-
// Convert Application to unstructured format
190-
unstructuredApp, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&convertedApp)
191-
if err != nil {
192-
klog.Errorf("Failed to convert Application to unstructured for resource %s: %v", convertedApp.GetName(), err)
193-
return nil, fmt.Errorf("failed to convert Application to unstructured: %v", err)
180+
// Explicitly set apiVersion and kind for Application
181+
convertedApp.TypeMeta = metav1.TypeMeta{
182+
APIVersion: "apps.cozystack.io/v1alpha1",
183+
Kind: r.kindName,
194184
}
195185

196-
klog.V(6).Infof("Successfully retrieved and converted resource %s of type %s to unstructured", convertedApp.GetName(), r.gvr.Resource)
197-
return &unstructured.Unstructured{Object: unstructuredApp}, nil
186+
klog.V(6).Infof("Successfully retrieved and converted resource %s of type %s", convertedApp.GetName(), r.gvr.Resource)
187+
return &convertedApp, nil
198188
}
199189

200190
// Get retrieves an Application by converting the corresponding HelmRelease
@@ -244,19 +234,8 @@ func (r *REST) Get(ctx context.Context, name string, options *metav1.GetOptions)
244234
Kind: r.kindName,
245235
}
246236

247-
// Convert Application to unstructured format
248-
unstructuredApp, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&convertedApp)
249-
if err != nil {
250-
klog.Errorf("Failed to convert Application to unstructured for resource %s: %v", name, err)
251-
return nil, fmt.Errorf("failed to convert Application to unstructured: %v", err)
252-
}
253-
254-
// Explicitly set apiVersion and kind in unstructured object
255-
unstructuredApp["apiVersion"] = "apps.cozystack.io/v1alpha1"
256-
unstructuredApp["kind"] = r.kindName
257-
258-
klog.V(6).Infof("Successfully retrieved and converted resource %s of kind %s to unstructured", name, r.gvr.Resource)
259-
return &unstructured.Unstructured{Object: unstructuredApp}, nil
237+
klog.V(6).Infof("Successfully retrieved and converted resource %s of kind %s", name, r.gvr.Resource)
238+
return &convertedApp, nil
260239
}
261240

262241
// List retrieves a list of Applications by converting HelmReleases
@@ -339,8 +318,8 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption
339318
return nil, err
340319
}
341320

342-
// Initialize unstructured items array
343-
items := make([]unstructured.Unstructured, 0)
321+
// Initialize Application items array
322+
items := make([]appsv1alpha1.Application, 0)
344323

345324
// Iterate over HelmReleases and convert to Applications
346325
for i := range hrList.Items {
@@ -354,6 +333,12 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption
354333
continue
355334
}
356335

336+
// Explicitly set apiVersion and kind for Application
337+
app.TypeMeta = metav1.TypeMeta{
338+
APIVersion: "apps.cozystack.io/v1alpha1",
339+
Kind: r.kindName,
340+
}
341+
357342
// If resourceName is set, check for match
358343
if resourceName != "" && app.Name != resourceName {
359344
continue
@@ -387,17 +372,11 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption
387372
}
388373
}
389374

390-
// Convert Application to unstructured
391-
unstructuredApp, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&app)
392-
if err != nil {
393-
klog.Errorf("Error converting Application %s to unstructured: %v", app.Name, err)
394-
continue
395-
}
396-
items = append(items, unstructured.Unstructured{Object: unstructuredApp})
375+
items = append(items, app)
397376
}
398377

399-
// Explicitly set apiVersion and kind in unstructured object
400-
appList := r.NewList().(*unstructured.UnstructuredList)
378+
// Create ApplicationList with proper kind
379+
appList := r.NewList().(*appsv1alpha1.ApplicationList)
401380
appList.SetResourceVersion(hrList.GetResourceVersion())
402381
appList.Items = items
403382

@@ -447,16 +426,9 @@ func (r *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObje
447426
}
448427

449428
// Assert the new object is of type Application
450-
us, ok := newObj.(*unstructured.Unstructured)
429+
app, ok := newObj.(*appsv1alpha1.Application)
451430
if !ok {
452-
errMsg := fmt.Sprintf("expected unstructured.Unstructured object, got %T", newObj)
453-
klog.Errorf(errMsg)
454-
return nil, false, fmt.Errorf(errMsg)
455-
}
456-
app := &appsv1alpha1.Application{}
457-
458-
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(us.Object, app); err != nil {
459-
errMsg := fmt.Sprintf("returned unstructured.Unstructured object was not an Application")
431+
errMsg := fmt.Sprintf("expected *appsv1alpha1.Application object, got %T", newObj)
460432
klog.Errorf(errMsg)
461433
return nil, false, fmt.Errorf(errMsg)
462434
}
@@ -523,18 +495,9 @@ func (r *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObje
523495
Kind: r.kindName,
524496
}
525497

526-
// Convert Application to unstructured format
527-
unstructuredApp, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&convertedApp)
528-
if err != nil {
529-
klog.Errorf("Failed to convert Application to unstructured for resource %s: %v", convertedApp.GetName(), err)
530-
return nil, false, fmt.Errorf("failed to convert Application to unstructured: %v", err)
531-
}
532-
obj := &unstructured.Unstructured{Object: unstructuredApp}
533-
obj.SetGroupVersionKind(r.gvk)
534-
535-
klog.V(6).Infof("Returning patched Application object: %+v", unstructuredApp)
498+
klog.V(6).Infof("Returning updated Application object: %+v", convertedApp)
536499

537-
return obj, false, nil
500+
return &convertedApp, false, nil
538501
}
539502

540503
// Delete removes an Application by deleting the corresponding HelmRelease
@@ -728,19 +691,16 @@ func (r *REST) Watch(ctx context.Context, options *metainternalversion.ListOptio
728691
}
729692
}
730693

731-
// Convert Application to unstructured
732-
unstructuredApp, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&app)
733-
if err != nil {
734-
klog.Errorf("Failed to convert Application to unstructured: %v", err)
735-
continue
694+
// Explicitly set apiVersion and kind for Application
695+
app.TypeMeta = metav1.TypeMeta{
696+
APIVersion: "apps.cozystack.io/v1alpha1",
697+
Kind: r.kindName,
736698
}
737-
obj := &unstructured.Unstructured{Object: unstructuredApp}
738-
obj.SetGroupVersionKind(r.gvk)
739699

740700
// Create watch event with Application object
741701
appEvent := watch.Event{
742702
Type: event.Type,
743-
Object: obj,
703+
Object: &app,
744704
}
745705

746706
// Send event to custom watcher
@@ -766,8 +726,8 @@ func (r *REST) Watch(ctx context.Context, options *metainternalversion.ListOptio
766726

767727
// Helper function to get HelmRelease name from object
768728
func helmReleaseName(obj runtime.Object) string {
769-
if u, ok := obj.(*unstructured.Unstructured); ok {
770-
return u.GetName()
729+
if app, ok := obj.(*appsv1alpha1.Application); ok {
730+
return app.GetName()
771731
}
772732
return "<unknown>"
773733
}
@@ -1059,56 +1019,6 @@ func (r *REST) ConvertToTable(ctx context.Context, object runtime.Object, tableO
10591019
case *appsv1alpha1.Application:
10601020
table = r.buildTableFromApplication(*obj)
10611021
table.ListMeta.ResourceVersion = obj.GetResourceVersion()
1062-
case *unstructured.UnstructuredList:
1063-
apps := make([]appsv1alpha1.Application, 0, len(obj.Items))
1064-
for _, u := range obj.Items {
1065-
var a appsv1alpha1.Application
1066-
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &a)
1067-
if err != nil {
1068-
klog.Errorf("Failed to convert Unstructured to Application: %v", err)
1069-
continue
1070-
}
1071-
apps = append(apps, a)
1072-
}
1073-
table = r.buildTableFromApplications(apps)
1074-
table.ListMeta.ResourceVersion = obj.GetResourceVersion()
1075-
case *unstructured.Unstructured:
1076-
var apps []appsv1alpha1.Application
1077-
for {
1078-
var items interface{}
1079-
var ok bool
1080-
var objects []unstructured.Unstructured
1081-
if items, ok = obj.Object["items"]; !ok {
1082-
break
1083-
}
1084-
if objects, ok = items.([]unstructured.Unstructured); !ok {
1085-
break
1086-
}
1087-
apps = make([]appsv1alpha1.Application, 0, len(objects))
1088-
var a appsv1alpha1.Application
1089-
for i := range objects {
1090-
err := runtime.DefaultUnstructuredConverter.FromUnstructured(objects[i].Object, &a)
1091-
if err != nil {
1092-
klog.Errorf("Failed to convert Unstructured to Application: %v", err)
1093-
continue
1094-
}
1095-
apps = append(apps, a)
1096-
}
1097-
break
1098-
}
1099-
if apps != nil {
1100-
table = r.buildTableFromApplications(apps)
1101-
table.ListMeta.ResourceVersion = obj.GetResourceVersion()
1102-
break
1103-
}
1104-
var app appsv1alpha1.Application
1105-
err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &app)
1106-
if err != nil {
1107-
klog.Errorf("Failed to convert Unstructured to Application: %v", err)
1108-
return nil, fmt.Errorf("failed to convert Unstructured to Application: %v", err)
1109-
}
1110-
table = r.buildTableFromApplication(app)
1111-
table.ListMeta.ResourceVersion = obj.GetResourceVersion()
11121022
default:
11131023
resource := schema.GroupResource{}
11141024
if info, ok := request.RequestInfoFrom(ctx); ok {
@@ -1237,15 +1147,21 @@ func (r *REST) Destroy() {
12371147

12381148
// New creates a new instance of Application
12391149
func (r *REST) New() runtime.Object {
1240-
obj := &unstructured.Unstructured{}
1241-
obj.SetGroupVersionKind(r.gvk)
1150+
obj := &appsv1alpha1.Application{}
1151+
obj.TypeMeta = metav1.TypeMeta{
1152+
APIVersion: r.gvk.GroupVersion().String(),
1153+
Kind: r.kindName,
1154+
}
12421155
return obj
12431156
}
12441157

12451158
// NewList returns an empty list of Application objects
12461159
func (r *REST) NewList() runtime.Object {
1247-
obj := &unstructured.UnstructuredList{}
1248-
obj.SetGroupVersionKind(r.gvk.GroupVersion().WithKind(r.kindName + "List"))
1160+
obj := &appsv1alpha1.ApplicationList{}
1161+
obj.TypeMeta = metav1.TypeMeta{
1162+
APIVersion: r.gvk.GroupVersion().String(),
1163+
Kind: r.kindName + "List",
1164+
}
12491165
return obj
12501166
}
12511167

0 commit comments

Comments
 (0)