Repository navigation
[apps] Refactor apiserver to use typed objects and fix UnstructuredList GVK - #1679
Conversation
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughReplaces unstructured REST handling with strongly-typed Application and TenantModule types; adds a go.mod replace directive for k8s.io/apimachinery; and expands e2e OpenAPI tests with top-level kind checks and a namespace create/delete test. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello Andrei Kvapil (@kvaps), I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly improves the robustness and maintainability of the apiserver by transitioning its REST handlers from using generic unstructured objects to specific typed objects. This change not only streamlines the code by removing complex runtime conversions but also addresses a critical bug in how Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request is a significant improvement, refactoring the apiserver REST handlers to use typed objects instead of unstructured.Unstructured. This greatly enhances type safety, readability, and maintainability by removing complex runtime conversions. The inclusion of the upstream fix for UnstructuredList GVK handling is also a crucial correction. My review includes a few suggestions to further improve the code, such as avoiding redundant code and a note on the dependency replacement for k8s.io/apimachinery. Overall, this is a high-quality contribution.
bd8f04c to
be1ff91
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pkg/registry/apps/application/rest.go (1)
1048-1090: Critical:runtime.RawExtension{Object: &app}uses pointer to loop variableIn both table builders, you’re taking the address of the range loop variable:
for _, app := range apps { row := metav1.TableRow{ Cells: ..., Object: runtime.RawExtension{Object: &app}, } table.Rows = append(table.Rows, row) }In Go,
appis a single variable reused for each iteration;&apphas the same address each time. AllRow.Objectentries will end up pointing to the lastappin the slice, which is incorrect and can confuse clients that rely on the embedded object.The same pattern appears in
buildTableFromTenantModulesinpkg/registry/core/tenantmodule/rest.go.Fix by taking the address of the slice element instead of the loop variable, e.g.:
-func (r *REST) buildTableFromApplications(apps []appsv1alpha1.Application) metav1.Table { +func (r *REST) buildTableFromApplications(apps []appsv1alpha1.Application) metav1.Table { @@ - for _, app := range apps { + for i := range apps { + app := &apps[i] row := metav1.TableRow{ - Cells: []interface{}{app.GetName(), getReadyStatus(app.Status.Conditions), computeAge(app.GetCreationTimestamp().Time, now), getVersion(app.Status.Version)}, - Object: runtime.RawExtension{Object: &app}, + Cells: []interface{}{app.GetName(), getReadyStatus(app.Status.Conditions), computeAge(app.GetCreationTimestamp().Time, now), getVersion(app.Status.Version)}, + Object: runtime.RawExtension{Object: app}, } table.Rows = append(table.Rows, row) }And similarly for
buildTableFromTenantModules:- for _, module := range modules { + for i := range modules { + module := &modules[i] row := metav1.TableRow{ - Cells: []interface{}{module.GetName(), getReadyStatus(module.Status.Conditions), computeAge(module.GetCreationTimestamp().Time, now), getVersion(module.Status.Version)}, - Object: runtime.RawExtension{Object: &module}, + Cells: []interface{}{module.GetName(), getReadyStatus(module.Status.Conditions), computeAge(module.GetCreationTimestamp().Time, now), getVersion(module.Status.Version)}, + Object: runtime.RawExtension{Object: module}, }This is a correctness issue that will affect all table output.
🧹 Nitpick comments (11)
pkg/registry/core/tenantmodule/rest.go (3)
149-157: Log message useskindwording but logsResourceThe log at Line 155 says “kind %s” but passes
r.gvr.Resource, which is the resource name, not the kind. This is harmless but slightly misleading when debugging.You could either change the format string to say “resource %s” or log
r.gvk.Kindinstead.
236-305: Prefer consistent TypeMeta construction and reuseNewListList currently:
- Builds
[]corev1alpha1.TenantModuleand sets each item’sTypeMetawith a hard-coded APIVersion string.- Constructs
TenantModuleListdirectly and setsTypeMeta.Kindtor.kindName + "List"with a hard-coded APIVersion.Functionally this works, but you can simplify and make it more robust against future group/version changes:
- Use
r.gvk.GroupVersion().String()instead of"core.cozystack.io/v1alpha1"when settingTypeMeta.APIVersion.- Consider creating the list via
r.NewList().(*corev1alpha1.TenantModuleList)so Kind/APIVersion always stay aligned with your REST storage config.Example:
- module.TypeMeta = metav1.TypeMeta{ - APIVersion: "core.cozystack.io/v1alpha1", - Kind: r.kindName, - } + module.TypeMeta = metav1.TypeMeta{ + APIVersion: r.gvk.GroupVersion().String(), + Kind: r.kindName, + } - moduleList := &corev1alpha1.TenantModuleList{} - moduleList.TypeMeta = metav1.TypeMeta{ - APIVersion: "core.cozystack.io/v1alpha1", - Kind: r.kindName + "List", - } + moduleList := r.NewList().(*corev1alpha1.TenantModuleList)
448-458: Watch path TypeMeta is consistent but could also user.gvkSetting
module.TypeMetain the watch code ensures events carry the correctapiVersion/kind, which is important for clients. For consistency withNew/NewListand future-proofing across version bumps, you might want to switchAPIVersionhere tor.gvk.GroupVersion().String()rather than a literal string.module.TypeMeta = metav1.TypeMeta{ - APIVersion: "core.cozystack.io/v1alpha1", + APIVersion: r.gvk.GroupVersion().String(), Kind: r.kindName, }pkg/registry/apps/application/rest.go (6)
142-188: Create: type assertion is fine, but considerapierrors.BadRequestand honoringcreateValidationThe explicit assertion to
*appsv1alpha1.Applicationis good, but two improvements are worth considering:
- For a wrong type, returning a generic
fmt.Errorfwill likely surface as a 500. For API correctness, it’s more appropriate to return a 4xx, e.g.:- app, ok := obj.(*appsv1alpha1.Application) - if !ok { - return nil, fmt.Errorf("expected *appsv1alpha1.Application object, got %T", obj) - } + app, ok := obj.(*appsv1alpha1.Application) + if !ok { + return nil, apierrors.NewBadRequest( + fmt.Sprintf("expected *appsv1alpha1.Application object, got %T", obj), + ) + }
- The
createValidationhook is currently unused. If callers rely on this (e.g., admission-style validation before persistence), you should invoke it before converting to HelmRelease:if createValidation != nil { if err := createValidation(ctx, app); err != nil { return nil, err } }Also, for consistency with
New/NewList, you may want to user.gvk.GroupVersion().String()instead of the literal"apps.cozystack.io/v1alpha1"when settingconvertedApp.TypeMeta.APIVersion.
231-239: Get: logging message uses “kind” but logsResourceSimilar to the TenantModule REST, the log line:
klog.V(6).Infof("Successfully retrieved and converted resource %s of kind %s", name, r.gvr.Resource)labels the second value as “kind” while passing
r.gvr.Resource. Either logr.gvk.Kindor change the message to “resource %s” for clarity.
321-385: List: typed items/list look good; tighten APIVersion handlingThe typed
itemsslice andApplicationListconstruction look correct and align with the move away fromunstructured. A couple of small consistency improvements:
- When setting
app.TypeMeta, prefer usingr.gvk.GroupVersion().String()rather than a hard-coded"apps.cozystack.io/v1alpha1"string.- You’re already using
r.NewList().(*appsv1alpha1.ApplicationList)which nicely centralizesTypeMetainitialization; that’s good and consistent withNewList().Example for APIVersion:
app.TypeMeta = metav1.TypeMeta{ - APIVersion: "apps.cozystack.io/v1alpha1", + APIVersion: r.gvk.GroupVersion().String(), Kind: r.kindName, }
428-501: Update: type assertion and return path are fine; align error type and APIVersionThe
newObjassertion to*appsv1alpha1.Applicationand the conversion/update flow look sound, but similar toCreate:
- Consider returning
apierrors.NewBadRequeston type mismatch instead offmt.Errorf, so the client sees a proper 4xx.- When setting
convertedApp.TypeMeta.APIVersion, user.gvk.GroupVersion().String()instead of a literal string, to avoid divergence if group/version changes.The final log (
"Returning updated Application object: %+v") is quite verbose at V(6); if these objects grow large, you may eventually want to trim it to key fields, but that’s optional.
694-704: Watch: TypeMeta assignment is correct; reuser.gvkfor APIVersionThe watch path correctly:
- Filters HelmReleases.
- Converts to
Application.- Sets
TypeMeta.- Wraps
&appin the event object.To stay consistent with
New/NewListand avoid hard-coded versions, I’d recommend:app.TypeMeta = metav1.TypeMeta{ - APIVersion: "apps.cozystack.io/v1alpha1", + APIVersion: r.gvk.GroupVersion().String(), Kind: r.kindName, }The
Object: &appusage here is fine becauseappis a fresh variable per event, not a loop variable reused across iterations.
727-733:helmReleaseNamehelper is now typed; consider extending if more callers appearThe helper now assumes a
*appsv1alpha1.Applicationand falls back to"<unknown>"for other types. This is reasonable for internal use; if you ever pass other runtime.Object implementations, you might want to either:
- Support additional known types explicitly, or
- Fail loudly (e.g., log and return empty) instead of returning a sentinel string.
For the current usage, this is acceptable.
hack/e2e-test-openapi.bats (1)
23-43: List-level kind assertions look correct and align with the GVK fixThe added checks for
.kind == "TenantList"and.kind == "IngressList"are a good regression guard for the UnstructuredList GVK behavior and complement the existing item-level kind checks. You are callingkubectl get --rawtwice per endpoint, which is fine; if you ever want to shave a bit of test time, you could capture the JSON once per resource and reuse it for both assertions.packages/system/cozystack-api/images/cozystack-api/Dockerfile (1)
6-8: Check whetherapimachinery/+patches/andpatchare still neededGiven that
go.modnow uses areplace k8s.io/apimachinery => github.com/cozystack/apimachinery ..., the Dockerfile’s comment about a “local replace” plus copyingapimachinery/andpatches/(and installingpatch) may be leftover from a previous approach.If the build no longer applies local patches or uses those directories, you can drop the
apk add patchand the extraCOPYlayers (and update the comment) to simplify the image and slightly speed up builds.Also applies to: 14-19
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (5)
go.mod(2 hunks)hack/e2e-test-openapi.bats(1 hunks)packages/system/cozystack-api/images/cozystack-api/Dockerfile(1 hunks)pkg/registry/apps/application/rest.go(11 hunks)pkg/registry/core/tenantmodule/rest.go(7 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
{go.mod,go.sum}
📄 CodeRabbit inference engine (AGENTS.md)
Do not manually modify
go.modandgo.sumfiles; usego getcommand instead
Files:
go.mod
**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
Use Controller-runtime patterns and kubebuilder style for Go code
Files:
pkg/registry/core/tenantmodule/rest.gopkg/registry/apps/application/rest.go
🧬 Code graph analysis (1)
pkg/registry/apps/application/rest.go (3)
pkg/apis/apps/v1alpha1/types.go (2)
Application(59-67)ApplicationList(27-32)pkg/generated/applyconfiguration/apps/v1alpha1/application.go (1)
Application(39-46)api/v1alpha1/groupversion_info.go (1)
GroupVersion(29-29)
🔇 Additional comments (4)
pkg/registry/core/tenantmodule/rest.go (1)
727-743:New/NewListimplementations look correct and align with typed storage
NewandNewListnow return typed*TenantModuleand*TenantModuleListwithTypeMetapopulated fromr.gvk, which matches how the appsApplicationREST storage is implemented. This should play nicely with API discovery and list-kind handling.pkg/registry/apps/application/rest.go (1)
1150-1165: New/NewList: good use ofr.gvkfor TypeMeta
NewandNewListnow return typedApplication/ApplicationListwithTypeMetaderived fromr.gvk, which is exactly what you want for discovery and list-kind behavior. This should work well with the upstream UnstructuredList/typed-object fixes you’re adopting.packages/system/cozystack-api/images/cozystack-api/Dockerfile (1)
1-2: Static binary onscratchis fine; confirm no dependence on OS CA bundle or other filesBuilding with
CGO_ENABLED=0and-extldflags=-staticand then running fromscratchis a solid minimal image pattern. Just make surecozystack-apidoesn’t rely on:
- System CA certificates (e.g.,
/etc/ssl/certs) for outbound HTTPS, or- Other OS files (zoneinfo, shells, etc.).
If it does, consider copying in just the required assets (e.g., CA bundle) or moving to a small distroless base.
Also applies to: 26-32
go.mod (1)
9-18: Fork module wiring is correctly configured and verifiedThe
k8s.io/apimachineryreplace directive togithub.com/cozystack/apimachineryis properly set up. Module verification passed,go mod tidycompleted without errors, and the fork resolution is correct (pseudo-versionv0.0.0-20251201201312-18e522a87614with valid module metadata). The promoted direct dependencies (logr,zapr,client_golang,zap) are also correctly resolved. No further action needed.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (6)
go.mod (1)
123-124: Ensure the replace directive is removed once upstream fix is released.This replace directive is a temporary workaround for the UnstructuredList GVK fix from kubernetes/kubernetes#135537. Document a plan to remove this once the upstream fix is released and consumed.
pkg/registry/apps/application/rest.go (2)
322-322: Pre-allocate slice capacity for performance.Initialize the
itemsslice with capacity equal tolen(hrList.Items)to avoid potential reallocations during append operations.Apply this diff:
- items := make([]appsv1alpha1.Application, 0) + items := make([]appsv1alpha1.Application, 0, len(hrList.Items))
180-184: Remove redundant TypeMeta assignment.This TypeMeta assignment is redundant as it's already set within
convertHelmReleaseToApplication(lines 921-924). This pattern appears in multiple methods (Create, Get, List, Update, Watch) and should be removed for cleaner code and to avoid potential inconsistencies.pkg/registry/core/tenantmodule/rest.go (3)
236-237: Preallocateitemsslice to avoid reallocationsYou can avoid potential reallocations in the hot List path by preallocating
itemswith the known upper bound fromhrList.Items(this matches earlier review feedback).- // Initialize TenantModule items array - items := make([]corev1alpha1.TenantModule, 0) + // Initialize TenantModule items array with capacity to avoid reallocations + items := make([]corev1alpha1.TenantModule, 0, len(hrList.Items))Also applies to: 291-291
252-256: Avoid redundantTypeMetaassignment after conversion
convertHelmReleaseToTenantModulealready initializesTypeMetawithAPIVersion: "core.cozystack.io/v1alpha1"andKind: r.kindName. Re‑assigning it here duplicates that responsibility and risks drift if the conversion helper is updated but this spot is not. Consider relying on the conversion function as the single source of truth and removing this block.
448-452: Reduce duplication ofTypeMetalogic and avoid reusing the same module pointer across eventsTwo points here:
The
TypeMetaassignment duplicates whatconvertHelmReleaseToTenantModulealready does, same as inList. It’s better to keep that centralized in the converter.
moduleis a loop‑scoped variable reused across iterations of the watch goroutine. Taking&moduleand sending it on the channel means all events share the same underlyingTenantModuleinstance, which can lead to subtle bugs if consumers keep references (they may observe the last value instead of the one at emission time). You can fix this by introducing a per‑iteration copy and taking its address:- // Explicitly set apiVersion and kind for TenantModule - module.TypeMeta = metav1.TypeMeta{ - APIVersion: "core.cozystack.io/v1alpha1", - Kind: r.kindName, - } - - // Create watch event with TenantModule object - moduleEvent := watch.Event{ - Type: event.Type, - Object: &module, - } + // Optionally rely on conversion for TypeMeta and avoid pointer reuse across events + m := module // create a per-event copy to take the address of + moduleEvent := watch.Event{ + Type: event.Type, + Object: &m, + }Also applies to: 457-457
🧹 Nitpick comments (1)
pkg/registry/core/tenantmodule/rest.go (1)
155-156: Clarify log message: use Kind instead of Resource nameThe log currently says
"kind %s"but logsr.gvr.Resource, which is the resource name (tenantmodules), not the kind (TenantModule). Prefer loggingr.gvk.Kind(orr.kindName) to avoid confusion when debugging.- klog.V(6).Infof("Successfully retrieved and converted resource %s of kind %s", name, r.gvr.Resource) + klog.V(6).Infof("Successfully retrieved and converted resource %s of kind %s", name, r.gvk.Kind)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (4)
go.mod(2 hunks)hack/e2e-test-openapi.bats(1 hunks)pkg/registry/apps/application/rest.go(11 hunks)pkg/registry/core/tenantmodule/rest.go(7 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
Use Controller-runtime patterns and kubebuilder style for Go code
Files:
pkg/registry/core/tenantmodule/rest.gopkg/registry/apps/application/rest.go
{go.mod,go.sum}
📄 CodeRabbit inference engine (AGENTS.md)
Do not manually modify
go.modandgo.sumfiles; usego getcommand instead
Files:
go.mod
🧬 Code graph analysis (2)
pkg/registry/core/tenantmodule/rest.go (4)
api/v1alpha1/groupversion_info.go (1)
GroupVersion(29-29)pkg/registry/apps/application/rest.go (1)
REST(77-86)pkg/registry/core/tenantsecret/rest.go (7)
REST(159-163)REST(181-181)REST(182-182)REST(183-185)REST(186-186)REST(190-190)REST(498-498)pkg/registry/core/tenantnamespace/rest.go (7)
REST(49-53)REST(74-74)REST(75-75)REST(76-78)REST(79-79)REST(83-83)REST(328-328)
pkg/registry/apps/application/rest.go (3)
pkg/apis/apps/v1alpha1/register.go (1)
Resource(48-50)api/v1alpha1/groupversion_info.go (1)
GroupVersion(29-29)pkg/registry/core/tenantmodule/rest.go (1)
REST(71-78)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Build
🔇 Additional comments (7)
hack/e2e-test-openapi.bats (2)
24-28: LGTM! List-level kind validation correctly implemented.This new validation appropriately tests the fix for UnstructuredList GVK handling, ensuring the top-level
.kindis "TenantList" as expected rather than potentially using an incorrect kind from typeToGVK registration.
34-38: LGTM! IngressList validation mirrors TenantList check.The validation correctly ensures the top-level
.kindis "IngressList", following the same pattern as the TenantList check and providing comprehensive coverage for the UnstructuredList GVK fix.go.mod (1)
9-17: LGTM! Correct promotion of dependencies.Promoting these logging and metrics dependencies from indirect to direct is appropriate given the refactoring to typed objects that now directly uses these packages in the REST handlers.
pkg/registry/apps/application/rest.go (1)
1150-1166: LGTM! Proper initialization in factory methods.The TypeMeta initialization in
New()andNewList()is correct and necessary. These factory methods are used by the REST framework to create properly initialized instances, unlike the redundant TypeMeta assignments in the CRUD methods.pkg/registry/core/tenantmodule/rest.go (3)
294-299: ListTypeMetainitialization looks consistentSetting
TenantModuleList.TypeMetafrom the REST’s GVK and usingr.kindName+"List"yieldsTenantModuleList, which matches the usual list-kind convention and aligns with howNewListis implemented.
725-733:New()implementation aligns with typed, kubebuilder-style REST storageReturning a typed
*corev1alpha1.TenantModulewithTypeMetainitialized fromr.gvkis consistent with controller-runtime / kubebuilder patterns and plays well with discovery and OpenAPI.
736-743:NewList()list-kind initialization looks goodUsing
TenantModuleListwithAPIVersionfromr.gvkandKind: r.kindName + "List"matches the REST GVK and the list-kind convention, and is consistent withNew().
be1ff91 to
52d6cf0
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pkg/registry/apps/application/rest.go (1)
1030-1036: Fix pointer-to-loop-variable bug in table rows.Same issue as in
tenantmodule/rest.go: the range variableappis reused across iterations, so&appcauses all rows to reference the same memory location.Apply this diff to fix the issue:
for _, app := range apps { + a := app row := metav1.TableRow{ Cells: []interface{}{app.GetName(), getReadyStatus(app.Status.Conditions), computeAge(app.GetCreationTimestamp().Time, now), getVersion(app.Status.Version)}, - Object: runtime.RawExtension{Object: &app}, + Object: runtime.RawExtension{Object: &a}, } table.Rows = append(table.Rows, row) }Alternatively, use index-based iteration:
- for _, app := range apps { + for i := range apps { row := metav1.TableRow{ - Cells: []interface{}{app.GetName(), getReadyStatus(app.Status.Conditions), computeAge(app.GetCreationTimestamp().Time, now), getVersion(app.Status.Version)}, - Object: runtime.RawExtension{Object: &app}, + Cells: []interface{}{apps[i].GetName(), getReadyStatus(apps[i].Status.Conditions), computeAge(apps[i].GetCreationTimestamp().Time, now), getVersion(apps[i].Status.Version)}, + Object: runtime.RawExtension{Object: &apps[i]}, } table.Rows = append(table.Rows, row) }
♻️ Duplicate comments (2)
go.mod (1)
122-124: Replace directive with future-dated pseudo-version.The replace directive uses a pseudo-version with a future date (
20251201...). This has already been flagged in a previous review. Ensure this temporary fork is tracked for removal once the upstream fix from kubernetes/kubernetes#135537 is released in a stable k8s.io/apimachinery version.Based on learnings,
go.modandgo.sumshould generally be modified usinggo getcommands rather than manual edits. However,replacedirectives are a valid exception since they cannot be added viago get.pkg/registry/core/tenantmodule/rest.go (1)
638-644: Fix pointer-to-loop-variable bug in table rows.The range variable
moduleis reused across iterations in Go, so&moduleinruntime.RawExtension{Object: &module}causes every row'sObjectto point to the same memory location (the last item). This was flagged in a previous review.Apply this diff to fix the issue:
for _, module := range modules { + m := module row := metav1.TableRow{ Cells: []interface{}{module.GetName(), getReadyStatus(module.Status.Conditions), computeAge(module.GetCreationTimestamp().Time, now), getVersion(module.Status.Version)}, - Object: runtime.RawExtension{Object: &module}, + Object: runtime.RawExtension{Object: &m}, } table.Rows = append(table.Rows, row) }Alternatively, use index-based iteration:
- for _, module := range modules { + for i := range modules { row := metav1.TableRow{ - Cells: []interface{}{module.GetName(), getReadyStatus(module.Status.Conditions), computeAge(module.GetCreationTimestamp().Time, now), getVersion(module.Status.Version)}, - Object: runtime.RawExtension{Object: &module}, + Cells: []interface{}{modules[i].GetName(), getReadyStatus(modules[i].Status.Conditions), computeAge(modules[i].GetCreationTimestamp().Time, now), getVersion(modules[i].Status.Version)}, + Object: runtime.RawExtension{Object: &modules[i]}, } table.Rows = append(table.Rows, row) }
🧹 Nitpick comments (2)
pkg/registry/core/tenantmodule/rest.go (1)
649-669: Potential issue: Address of parameter in single-item table.The function receives
moduleby value and takes its address forObject. While this works for a single row, it's inconsistent with best practices. Consider passing by pointer or documenting the intent.-func (r *REST) buildTableFromTenantModule(module corev1alpha1.TenantModule) metav1.Table { +func (r *REST) buildTableFromTenantModule(module *corev1alpha1.TenantModule) metav1.Table { table := metav1.Table{ ColumnDefinitions: []metav1.TableColumnDefinition{ {Name: "NAME", Type: "string", Description: "Name of the TenantModule", Priority: 0}, {Name: "READY", Type: "string", Description: "Ready status of the TenantModule", Priority: 0}, {Name: "AGE", Type: "string", Description: "Age of the TenantModule", Priority: 0}, {Name: "VERSION", Type: "string", Description: "Version of the TenantModule", Priority: 0}, }, Rows: []metav1.TableRow{}, } now := time.Now() row := metav1.TableRow{ Cells: []interface{}{module.GetName(), getReadyStatus(module.Status.Conditions), computeAge(module.GetCreationTimestamp().Time, now), getVersion(module.Status.Version)}, - Object: runtime.RawExtension{Object: &module}, + Object: runtime.RawExtension{Object: module}, } table.Rows = append(table.Rows, row) return table }Then update the caller at line 598:
- table = r.buildTableFromTenantModule(*obj) + table = r.buildTableFromTenantModule(obj)pkg/registry/apps/application/rest.go (1)
1041-1061: Consider passing by pointer for consistency.Similar to
tenantmodule/rest.go, the function receivesappby value and takes its address. Consider passing by pointer for consistency and to avoid the copy.-func (r *REST) buildTableFromApplication(app appsv1alpha1.Application) metav1.Table { +func (r *REST) buildTableFromApplication(app *appsv1alpha1.Application) metav1.Table { ... row := metav1.TableRow{ Cells: []interface{}{app.GetName(), getReadyStatus(app.Status.Conditions), computeAge(app.GetCreationTimestamp().Time, now), getVersion(app.Status.Version)}, - Object: runtime.RawExtension{Object: &app}, + Object: runtime.RawExtension{Object: app}, }Update the caller at line 990:
- table = r.buildTableFromApplication(*obj) + table = r.buildTableFromApplication(obj)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (4)
go.mod(2 hunks)hack/e2e-test-openapi.bats(1 hunks)pkg/registry/apps/application/rest.go(10 hunks)pkg/registry/core/tenantmodule/rest.go(6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- hack/e2e-test-openapi.bats
🧰 Additional context used
📓 Path-based instructions (2)
{go.mod,go.sum}
📄 CodeRabbit inference engine (AGENTS.md)
Do not manually modify
go.modandgo.sumfiles; usego getcommand instead
Files:
go.mod
**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
Use Controller-runtime patterns and kubebuilder style for Go code
Files:
pkg/registry/core/tenantmodule/rest.gopkg/registry/apps/application/rest.go
🧠 Learnings (1)
📚 Learning: 2025-11-27T11:21:45.808Z
Learnt from: CR
Repo: cozystack/cozystack PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-27T11:21:45.808Z
Learning: Applies to {go.mod,go.sum} : Do not manually modify `go.mod` and `go.sum` files; use `go get` command instead
Applied to files:
go.mod
🧬 Code graph analysis (1)
pkg/registry/apps/application/rest.go (2)
pkg/generated/applyconfiguration/apps/v1alpha1/application.go (1)
Application(39-46)api/v1alpha1/groupversion_info.go (1)
GroupVersion(29-29)
🔇 Additional comments (17)
go.mod (1)
9-10: LGTM: Dependencies promoted to direct.The promotion of
go-logr/logr,go-logr/zapr,prometheus/client_golang, andgo.uber.org/zapfrom indirect to direct dependencies aligns with the typed REST handling refactor.Also applies to: 14-14, 17-17
pkg/registry/core/tenantmodule/rest.go (6)
149-150: LGTM: Typed TenantModule returned from Get.The migration from unstructured to typed
TenantModuleis correctly implemented, and theTypeMetais set in the conversion function.
230-231: LGTM: Slice initialized with capacity.The items slice is now pre-allocated with capacity matching
len(hrList.Items), addressing the previous performance suggestion.
282-289: LGTM: TenantModuleList with proper TypeMeta.The
TenantModuleListis correctly initialized withAPIVersionandKind, ensuring proper GVK handling.
436-440: LGTM: Watch emits typed TenantModule.The watch correctly emits a pointer to the converted
module. Sincemoduleis a fresh value fromConvertHelmReleaseToTenantModuleeach iteration, taking its address is safe here.
594-599: LGTM: ConvertToTable handles typed objects.The switch now correctly handles
*corev1alpha1.TenantModuleListand*corev1alpha1.TenantModule, aligning with the typed object migration.
708-725: LGTM: New and NewList with proper TypeMeta.Both methods correctly initialize typed objects with
APIVersionandKindderived from the REST'sgvk, ensuring consistent GVK handling.pkg/registry/apps/application/rest.go (10)
144-147: LGTM: Create accepts typed Application.The type assertion to
*appsv1alpha1.Applicationis correct and provides a clear error message on type mismatch.
180-182: LGTM: Create returns typed Application.Returning a pointer to the converted
Applicationis correct.
225-227: LGTM: Get returns typed Application.The Get method correctly returns a pointer to the typed
Application.
309-310: LGTM: Slice initialized with capacity.The items slice is pre-allocated with capacity matching
len(hrList.Items), addressing previous performance feedback.
360-363: LGTM: List uses NewList for ApplicationList creation.Using
r.NewList()ensures consistent TypeMeta initialization across all code paths.
411-416: LGTM: Update type assertion.The type assertion to
*appsv1alpha1.Applicationis correct with clear error messaging.
474-477: LGTM: Update returns typed Application.Returning a pointer to the converted
Applicationis correct.
670-674: LGTM: Watch emits typed Application.The watch correctly emits a pointer to the converted
app. Sinceappis a fresh value fromConvertHelmReleaseToApplicationeach iteration, this is safe.
698-703: LGTM: helmReleaseName helper updated.The helper correctly handles the typed
*appsv1alpha1.Application.
1119-1136: LGTM: New and NewList with proper TypeMeta.Both methods correctly initialize typed objects with
APIVersionandKindderived from the REST'sgvk.
…st 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]>
52d6cf0 to
ca29fc8
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (5)
hack/e2e-test-openapi.bats (1)
46-53: Consider bounding namespace deletion timeThe create/delete namespace test is useful to catch stuck finalizers. To avoid a potentially hanging e2e run in pathological cases, consider adding an explicit timeout, e.g.
kubectl delete ns ... --wait=true --timeout=5m, so failures surface as test errors rather than indefinite hangs.pkg/registry/core/tenantmodule/rest.go (1)
149-151: Minor logging wording nit in GetThe log message says “of kind %s” but logs
r.gvr.Resource(e.g.tenantmodules). Consider changing the format string or argument to avoid “kind” vs “resource” confusion; the returned object itself looks correct.pkg/registry/apps/application/rest.go (3)
178-182: Create log message text is slightly misleadingIn Create, the log line says “Successfully retrieved and converted resource …” though this path is performing a create-then-convert. Consider rephrasing to “created and converted” (or similar) to avoid confusion when debugging.
218-227: Get returns typed Application with consistent TypeMetaReturning
&convertedAppafterConvertHelmReleaseToApplicationkeeps the REST Get path fully typed. As in the TenantModule REST, the log message mentions “kind” but logsr.gvr.Resource; consider aligning wording/argument, but behavior is otherwise correct.
472-477: Verbose logging of full updated object may be heavy
klog.V(6).Infof("Returning updated Application object: %+v", convertedApp)can produce very large logs if specs are big. Given V(6) is already quite verbose this is acceptable, but you might consider logging only metadata (name/namespace/resourceVersion) to keep debug output leaner.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (4)
go.mod(2 hunks)hack/e2e-test-openapi.bats(1 hunks)pkg/registry/apps/application/rest.go(12 hunks)pkg/registry/core/tenantmodule/rest.go(8 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- go.mod
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
Use Controller-runtime patterns and kubebuilder style for Go code
Files:
pkg/registry/apps/application/rest.gopkg/registry/core/tenantmodule/rest.go
🧬 Code graph analysis (2)
pkg/registry/apps/application/rest.go (2)
api/v1alpha1/groupversion_info.go (1)
GroupVersion(29-29)pkg/registry/registry.go (1)
REST(26-28)
pkg/registry/core/tenantmodule/rest.go (4)
api/v1alpha1/groupversion_info.go (1)
GroupVersion(29-29)pkg/registry/apps/application/rest.go (1)
REST(77-86)pkg/registry/core/tenantsecret/rest.go (7)
REST(159-163)REST(181-181)REST(182-182)REST(183-185)REST(186-186)REST(190-190)REST(498-498)pkg/registry/core/tenantnamespace/rest.go (7)
REST(49-53)REST(74-74)REST(75-75)REST(76-78)REST(79-79)REST(83-83)REST(328-328)
🔇 Additional comments (15)
hack/e2e-test-openapi.bats (1)
23-38: Kind checks for tenants/ingresses look goodValidating both the list
kind(TenantList/IngressList) and the first item kind strengthens the regression coverage around TypeMeta/Kind wiring. No issues from a test logic standpoint.pkg/registry/core/tenantmodule/rest.go (6)
230-293: List preallocation and TypeMeta wiring look correctPreallocating
itemswithlen(hrList.Items)and then populatingTenantModuleListwith explicitTypeMeta(core.cozystack.io/v1alpha1,TenantModuleList) is aligned with the typed REST approach and should satisfy list-kind OpenAPI expectations. No functional issues spotted.
436-440: Watch now emits typed TenantModule objectsEmitting
watch.Event{Object: &module}wheremoduleis a fully-populatedTenantModule(with TypeMeta set in the converter) is consistent with the new typed surface and avoids unstructured conversions. This looks correct.
593-599: ConvertToTable switch restricted to TenantModule typesHandling only
*corev1alpha1.TenantModuleListand*corev1alpha1.TenantModulehere simplifies the code and matches the move away fromunstructured.Unstructured. The error path for unsupported types is still appropriate.
625-648: Fixed table row object pointer construction for listsUsing
for i := range moduleswithmodule := &modules[i]and storingruntime.RawExtension{Object: module}ensures each row’sObjectpoints to the correct list element, avoiding the previous pointer-to-loop-variable pitfall.
650-671: Single-object table builder uses stable per-row pointerCopying the value into
m := moduleand taking&mgives each row its ownApplicationinstance, which is safe even if the caller later mutates the original argument. This matches the intent of the earlier pointer-safety fixes.
709-727: New/NewList correctly initialize TypeMetaInitializing
TypeMetainNewandNewListfromr.gvk.GroupVersion().String()andr.kindName/r.kindName+"List"ensures both singular and list objects have the right APIVersion/Kind when constructed by the apiserver, which is important for discovery and OpenAPI.pkg/registry/apps/application/rest.go (8)
141-147: Create now expects typed Application — assertion is appropriateAsserting
obj.(*appsv1alpha1.Application)at the REST boundary matches the new typed API surface and removes unstructured conversions. The error message is clear if the wrong type ever arrives.
309-367: List preallocation and list construction look solidPreallocating
items := make([]appsv1alpha1.Application, 0, len(hrList.Items))and buildingappList := r.NewList().(*appsv1alpha1.ApplicationList)ensures efficient slice growth and consistent TypeMeta (APIVersion/Kind) for the list object. This also dovetails with the UnstructuredList GVK fix.
410-416: Update type assertion matches Create/GetThe type assertion to
*appsv1alpha1.Applicationin Update keeps the API consistent across CRUD operations, and the error message includes the actual type, which is helpful for debugging mis-wired callers.
646-674: Watch now emits typed Application objectsCreating
watch.Event{Object: &app}aligns the watch stream with the typed REST surface. Givenappis a value produced per event and already has TypeMeta set in the converter, this is a clean replacement for the prior unstructured-based watch.
697-703: helmReleaseName helper now matches the typed APILimiting
helmReleaseNameto handle*appsv1alpha1.Applicationdirectly matches the new REST expectations. Returning"<unknown>"for other types is a reasonable fallback for logging-only usage.
1017-1040: Application list table builder uses safe per-element pointersThe
for i := range apps { app := &apps[i]; ... Object: runtime.RawExtension{Object: app} }pattern avoids the pointer-to-loop-variable issue and ensures each row points to the correct list element. Column definitions and cell content look correct.
1042-1063: Single-Application table builder uses independent copyCopying into
a := appand storing&ain theObjectfield avoids accidental aliasing if the original value is reused or mutated after building the table. This is consistent with the list builder’s pointer-safety approach.
1120-1138: New/NewList correctly set TypeMeta for ApplicationsInitializing
TypeMetafromr.gvk.GroupVersion().String()andr.kindName/r.kindName+"List"inNewandNewListensures server-created Application and ApplicationList objects carry accurate APIVersion/Kind, which is important for discovery and for the OpenAPI/List-kind tests you added.
|
I can confirm from my testing that this addresses my issue #1673 when using cozystack-api patched with this PR. |
Signed-off-by: Andrei Kvapil <[email protected]>
|
Successfully created backport PR for |
…st GVK (#1679) ## What this PR does This PR 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 ### Release note ```release-note [apps] Refactor apiserver to use typed objects and fix UnstructuredList GVK handling This change refactors the apiserver REST handlers to use typed Application objects instead of unstructured.Unstructured, eliminating runtime conversions and simplifying the codebase. Additionally, it fixes an issue where UnstructuredList objects were incorrectly 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 fix includes the upstream fix from kubernetes/kubernetes#135537. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Added a module replacement in dependency configuration to ensure reproducible builds. * **Refactor** * REST internals now use strongly-typed Application and TenantModule resources and return typed API objects with consistent metadata. * **Tests** * Strengthened end-to-end checks for resource kinds and added a create/delete namespace scenario. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…st GVK (#1679) ## What this PR does This PR 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 ### Release note ```release-note [apps] Refactor apiserver to use typed objects and fix UnstructuredList GVK handling This change refactors the apiserver REST handlers to use typed Application objects instead of unstructured.Unstructured, eliminating runtime conversions and simplifying the codebase. Additionally, it fixes an issue where UnstructuredList objects were incorrectly 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 fix includes the upstream fix from kubernetes/kubernetes#135537. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Added a module replacement in dependency configuration to ensure reproducible builds. * **Refactor** * REST internals now use strongly-typed Application and TenantModule resources and return typed API objects with consistent metadata. * **Tests** * Strengthened end-to-end checks for resource kinds and added a create/delete namespace scenario. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…st GVK (#1679) ## What this PR does This PR 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 ### Release note ```release-note [apps] Refactor apiserver to use typed objects and fix UnstructuredList GVK handling This change refactors the apiserver REST handlers to use typed Application objects instead of unstructured.Unstructured, eliminating runtime conversions and simplifying the codebase. Additionally, it fixes an issue where UnstructuredList objects were incorrectly 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 fix includes the upstream fix from kubernetes/kubernetes#135537. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Added a module replacement in dependency configuration to ensure reproducible builds. * **Refactor** * REST internals now use strongly-typed Application and TenantModule resources and return typed API objects with consistent metadata. * **Tests** * Strengthened end-to-end checks for resource kinds and added a create/delete namespace scenario. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
What this PR does
This PR refactors the apiserver REST handlers to use typed objects
(
appsv1alpha1.Application) instead ofunstructured.Unstructured, eliminatingthe need for runtime conversions and simplifying the codebase.
Additionally, it fixes an issue where
UnstructuredListobjects were usingthe first registered kind from
typeToGVKinstead of the kind from theobject'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
UnstructuredListsimilar to regularUnstructuredobjects, using GVK from the object field instead oftypeToGVK.Changes
rest.gohandlers to use typedApplicationobjectsunstructured.UnstructuredconversionsUnstructuredListGVK handlinggo.mod/go.sumRelease note
Summary by CodeRabbit
Chores
Refactor
Tests
✏️ Tip: You can customize this high-level summary in your review settings.