Skip to content

Commit ca29fc8

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 ca29fc8

5 files changed

Lines changed: 95 additions & 218 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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,33 @@
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"
3242
exit 1
3343
fi
3444
}
45+
46+
@test "Create and delete namespace" {
47+
kubectl create ns cozy-test-create-and-delete-namespace --dry-run=client -o yaml | kubectl apply -f -
48+
if ! kubectl delete ns cozy-test-create-and-delete-namespace; then
49+
echo "Failed to delete namespace"
50+
kubectl describe ns cozy-test-create-and-delete-namespace
51+
exit 1
52+
fi
53+
}

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

Lines changed: 33 additions & 145 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,8 @@ 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)
194-
}
195-
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
180+
klog.V(6).Infof("Successfully retrieved and converted resource %s of type %s", convertedApp.GetName(), r.gvr.Resource)
181+
return &convertedApp, nil
198182
}
199183

200184
// Get retrieves an Application by converting the corresponding HelmRelease
@@ -238,25 +222,8 @@ func (r *REST) Get(ctx context.Context, name string, options *metav1.GetOptions)
238222
return nil, fmt.Errorf("conversion error: %v", err)
239223
}
240224

241-
// Explicitly set apiVersion and kind for Application
242-
convertedApp.TypeMeta = metav1.TypeMeta{
243-
APIVersion: "apps.cozystack.io/v1alpha1",
244-
Kind: r.kindName,
245-
}
246-
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
225+
klog.V(6).Infof("Successfully retrieved and converted resource %s of kind %s", name, r.gvr.Resource)
226+
return &convertedApp, nil
260227
}
261228

262229
// List retrieves a list of Applications by converting HelmReleases
@@ -339,8 +306,8 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption
339306
return nil, err
340307
}
341308

342-
// Initialize unstructured items array
343-
items := make([]unstructured.Unstructured, 0)
309+
// Initialize Application items array
310+
items := make([]appsv1alpha1.Application, 0, len(hrList.Items))
344311

345312
// Iterate over HelmReleases and convert to Applications
346313
for i := range hrList.Items {
@@ -387,17 +354,11 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption
387354
}
388355
}
389356

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})
357+
items = append(items, app)
397358
}
398359

399-
// Explicitly set apiVersion and kind in unstructured object
400-
appList := r.NewList().(*unstructured.UnstructuredList)
360+
// Create ApplicationList with proper kind
361+
appList := r.NewList().(*appsv1alpha1.ApplicationList)
401362
appList.SetResourceVersion(hrList.GetResourceVersion())
402363
appList.Items = items
403364

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

449410
// Assert the new object is of type Application
450-
us, ok := newObj.(*unstructured.Unstructured)
411+
app, ok := newObj.(*appsv1alpha1.Application)
451412
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")
413+
errMsg := fmt.Sprintf("expected *appsv1alpha1.Application object, got %T", newObj)
460414
klog.Errorf(errMsg)
461415
return nil, false, fmt.Errorf(errMsg)
462416
}
@@ -517,24 +471,9 @@ func (r *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObje
517471

518472
klog.V(6).Infof("Successfully updated and converted HelmRelease %s to Application", helmRelease.GetName())
519473

520-
// Explicitly set apiVersion and kind for Application
521-
convertedApp.TypeMeta = metav1.TypeMeta{
522-
APIVersion: "apps.cozystack.io/v1alpha1",
523-
Kind: r.kindName,
524-
}
525-
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)
474+
klog.V(6).Infof("Returning updated Application object: %+v", convertedApp)
534475

535-
klog.V(6).Infof("Returning patched Application object: %+v", unstructuredApp)
536-
537-
return obj, false, nil
476+
return &convertedApp, false, nil
538477
}
539478

540479
// Delete removes an Application by deleting the corresponding HelmRelease
@@ -728,19 +667,10 @@ func (r *REST) Watch(ctx context.Context, options *metainternalversion.ListOptio
728667
}
729668
}
730669

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
736-
}
737-
obj := &unstructured.Unstructured{Object: unstructuredApp}
738-
obj.SetGroupVersionKind(r.gvk)
739-
740670
// Create watch event with Application object
741671
appEvent := watch.Event{
742672
Type: event.Type,
743-
Object: obj,
673+
Object: &app,
744674
}
745675

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

767697
// Helper function to get HelmRelease name from object
768698
func helmReleaseName(obj runtime.Object) string {
769-
if u, ok := obj.(*unstructured.Unstructured); ok {
770-
return u.GetName()
699+
if app, ok := obj.(*appsv1alpha1.Application); ok {
700+
return app.GetName()
771701
}
772702
return "<unknown>"
773703
}
@@ -1059,56 +989,6 @@ func (r *REST) ConvertToTable(ctx context.Context, object runtime.Object, tableO
1059989
case *appsv1alpha1.Application:
1060990
table = r.buildTableFromApplication(*obj)
1061991
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()
1112992
default:
1113993
resource := schema.GroupResource{}
1114994
if info, ok := request.RequestInfoFrom(ctx); ok {
@@ -1147,10 +1027,11 @@ func (r *REST) buildTableFromApplications(apps []appsv1alpha1.Application) metav
11471027
}
11481028
now := time.Now()
11491029

1150-
for _, app := range apps {
1030+
for i := range apps {
1031+
app := &apps[i]
11511032
row := metav1.TableRow{
11521033
Cells: []interface{}{app.GetName(), getReadyStatus(app.Status.Conditions), computeAge(app.GetCreationTimestamp().Time, now), getVersion(app.Status.Version)},
1153-
Object: runtime.RawExtension{Object: &app},
1034+
Object: runtime.RawExtension{Object: app},
11541035
}
11551036
table.Rows = append(table.Rows, row)
11561037
}
@@ -1171,9 +1052,10 @@ func (r *REST) buildTableFromApplication(app appsv1alpha1.Application) metav1.Ta
11711052
}
11721053
now := time.Now()
11731054

1055+
a := app
11741056
row := metav1.TableRow{
11751057
Cells: []interface{}{app.GetName(), getReadyStatus(app.Status.Conditions), computeAge(app.GetCreationTimestamp().Time, now), getVersion(app.Status.Version)},
1176-
Object: runtime.RawExtension{Object: &app},
1058+
Object: runtime.RawExtension{Object: &a},
11771059
}
11781060
table.Rows = append(table.Rows, row)
11791061

@@ -1237,15 +1119,21 @@ func (r *REST) Destroy() {
12371119

12381120
// New creates a new instance of Application
12391121
func (r *REST) New() runtime.Object {
1240-
obj := &unstructured.Unstructured{}
1241-
obj.SetGroupVersionKind(r.gvk)
1122+
obj := &appsv1alpha1.Application{}
1123+
obj.TypeMeta = metav1.TypeMeta{
1124+
APIVersion: r.gvk.GroupVersion().String(),
1125+
Kind: r.kindName,
1126+
}
12421127
return obj
12431128
}
12441129

12451130
// NewList returns an empty list of Application objects
12461131
func (r *REST) NewList() runtime.Object {
1247-
obj := &unstructured.UnstructuredList{}
1248-
obj.SetGroupVersionKind(r.gvk.GroupVersion().WithKind(r.kindName + "List"))
1132+
obj := &appsv1alpha1.ApplicationList{}
1133+
obj.TypeMeta = metav1.TypeMeta{
1134+
APIVersion: r.gvk.GroupVersion().String(),
1135+
Kind: r.kindName + "List",
1136+
}
12491137
return obj
12501138
}
12511139

0 commit comments

Comments
 (0)