Skip to content

generator: make generated Go types implement runtime.Object and provide AddToScheme#162

Open
erikmiller-gusto wants to merge 1 commit into
crossplane:mainfrom
erikmiller-gusto:runtime-object-scheme
Open

generator: make generated Go types implement runtime.Object and provide AddToScheme#162
erikmiller-gusto wants to merge 1 commit into
crossplane:mainfrom
erikmiller-gusto:runtime-object-scheme

Conversation

@erikmiller-gusto

Copy link
Copy Markdown

Fixes #143.

What

Generates runtime.Object support for the Go schema models, behind a new opt-in
feature flag. When enabled, generated Go models can be registered in a
runtime.Scheme and used with k8s ecosystem libraries, so users no longer have
to set apiVersion/kind by hand on composed resources.

For every generated struct:

  • controller-gen-style DeepCopyInto / DeepCopy.

For root types (structs with APIVersion + Kind + Metadata, i.e. the
resource and its List):

  • DeepCopyObject() runtime.Object
  • GetObjectKind/GroupVersionKind/SetGroupVersionKind (the type implements
    schema.ObjectKind, reading/writing the typed APIVersion/Kind fields)
  • an init() registering the type with the package SchemeBuilder.

Per package containing root types, a groupversion_info.go defining
GroupVersion, SchemeBuilder and AddToScheme (apimachinery-only; no
controller-runtime dependency).

Feature gate

Off by default; enable with:

crossplane config set features.generateGoRuntimeObjects true

The flag is threaded from config through the project build, project run and
function generate commands. When off, generated output (including go.mod/
go.sum) is byte-for-byte unchanged.

Dependencies

When the flag is on, the generated models module additionally requires
k8s.io/apimachinery, pinned to v0.33.0 to match the function Go template —
so a generated function that consumes the models via replace still resolves
everything from the template's existing go.sum (verified locally by building a
function against flag-on models using the shipped template go.mod/go.sum).

Testing

  • Unit tests for the DeepCopy / runtime.Object generation (root vs non-root
    detection, scalar/struct/alias field handling).
  • Compile gates that materialize the generated module (both the CRD and
    OpenAPI generation paths) and go build / go test it — these catch
    DeepCopyInto codegen bugs that parse cleanly but don't type-check. They skip
    gracefully when modules can't be resolved offline.
  • A behavioral test asserting deep-copy independence for scalar,
    slice-of-struct and map fields, AddToScheme GVK round-tripping, and that
    SetGroupVersionKind writes the typed fields.
  • A default-off test confirming nothing is emitted and go.mod is unchanged
    when the flag is disabled.

Known limitation

DeepCopyInto decides struct-vs-scalar for cross-package types via AST
heuristics, with a small denylist of k8s alias types that have no DeepCopyInto
(Time, MicroTime, FieldsV1, RawExtension). That denylist was validated
against the test fixtures (incl. the large azure_linux_function_app CRD); a
real CRD referencing a k8s alias type not covered by the fixtures could emit a
DeepCopyInto call on a type that lacks it. The compile gate catches this for
anything in the fixtures; broadening detection (e.g. deriving the alias set
programmatically) is a possible follow-up.

@erikmiller-gusto erikmiller-gusto requested review from a team, jcogilvie and tampakrap as code owners June 26, 2026 21:22
@erikmiller-gusto erikmiller-gusto requested review from jbw976 and removed request for a team June 26, 2026 21:22
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@erikmiller-gusto, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 32 minutes and 52 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1cf5662f-c4da-4fc7-84eb-cfac448cedf3

📥 Commits

Reviewing files that changed from the base of the PR and between 75d816f and 56a98a2.

📒 Files selected for processing (13)
  • cmd/crossplane/config/help/config.md
  • cmd/crossplane/config/set.go
  • cmd/crossplane/function/generate.go
  • cmd/crossplane/function/generate_test.go
  • cmd/crossplane/main.go
  • cmd/crossplane/project/build.go
  • cmd/crossplane/project/run.go
  • internal/config/config.go
  • internal/schemas/generator/go.go
  • internal/schemas/generator/interface.go
  • internal/schemas/generator/runtimeobject.go
  • internal/schemas/generator/runtimeobject_integration_test.go
  • internal/schemas/generator/runtimeobject_test.go
📝 Walkthrough

Walkthrough

Adds an optional generateGoRuntimeObjects config flag, threads it through CLI command execution, and uses it to emit runtime.Object methods, AddToScheme helpers, groupversion_info.go, and validation tests for generated Go schemas.

Changes

Go runtime object generation

Layer / File(s) Summary
Config flag surface
internal/config/config.go, cmd/crossplane/config/set.go, cmd/crossplane/config/help/config.md, cmd/crossplane/main.go
Adds features.generateGoRuntimeObjects to the config schema and crossplane config set, documents the option, and binds loaded config into Kong context during parser setup.
Command config threading
internal/schemas/generator/interface.go, cmd/crossplane/project/build.go, cmd/crossplane/project/run.go, cmd/crossplane/function/generate.go, cmd/crossplane/function/generate_test.go
AllLanguages accepts generator options, project commands pass cfg.Features.GenerateGoRuntimeObjects into schema generation, and generateCmd.Run/its test accept an explicit config argument.
Go generator wiring
internal/schemas/generator/go.go
Adds runtime-object-aware generator state, selects the matching module templates, threads the flag through CRD/OpenAPI generation paths, and writes groupversion_info.go when generated code registers a scheme builder.
runtime.Object augmentation
internal/schemas/generator/runtimeobject.go
Adds AST-based code rewriting that emits DeepCopy* methods for structs, runtime.Object methods for root types, import injection, and SchemeBuilder.Register support when the feature is enabled.
Validation
internal/schemas/generator/runtimeobject_test.go, internal/schemas/generator/runtimeobject_integration_test.go
Adds unit and integration tests for generated methods, emitted artifacts, and compile/build behavior with the feature enabled and disabled.

Sequence Diagram(s)

sequenceDiagram
  participant main as cmd/crossplane/main.go
  participant run as cmd/crossplane/project/run.go
  participant iface as internal/schemas/generator/interface.go
  participant go as internal/schemas/generator/go.go
  participant aug as addRuntimeObjects

  main->>run: bind cfg into Kong context
  run->>iface: AllLanguages(WithGoRuntimeObjects(cfg.Features.GenerateGoRuntimeObjects))
  iface->>go: build goGenerator{runtimeObjects: enabled}
  go->>aug: rewrite generated Go source with runtime.Object support
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Possibly related PRs

  • crossplane/cli#24: Also changes generator selection plumbing in cmd/crossplane/project/build.go, cmd/crossplane/project/run.go, and internal/schemas/generator/interface.go.

Suggested reviewers

  • jcogilvie
  • tampakrap
  • jbw976
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title is descriptive, but it exceeds the 72-character limit required by the PR title rules. Shorten it to 72 characters or fewer while keeping the main change clear, such as mentioning runtime.Object support and AddToScheme.
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description directly explains the runtime.Object and AddToScheme feature and its feature-flag plumbing.
Linked Issues check ✅ Passed The changes implement runtime.Object support and package-level AddToScheme helpers, matching issue #143.
Out of Scope Changes check ✅ Passed The config, command wiring, docs, and tests all support the runtime.Object feature and do not appear unrelated.
Breaking Changes ✅ Passed PASS — The cmd changes only add an optional config key and thread config into unexported Run methods; no public cmd/apis fields or flags were removed, renamed, or made required.
Feature Gate Requirement ✅ Passed PASS: The new Go runtime-object generation is behind features.generateGoRuntimeObjects, plumbed through config, generator.WithGoRuntimeObjects, and build/run commands; default stays off.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
cmd/crossplane/function/generate_test.go (1)

291-328: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Please add a flag-enabled regression case here.

Thanks for updating the signature. This still passes only &config.Config{}, so it won't catch Run forgetting to thread GenerateGoRuntimeObjects into schema generation. Could you add a run case with Features.GenerateGoRuntimeObjects: true and assert the generated Go models include the runtime-object artifacts?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/function/generate_test.go` around lines 291 - 328, Add a new
Run regression case in TestRunErrors that exercises generateCmd.Run with
config.Config.Features.GenerateGoRuntimeObjects enabled, so the test verifies
schema generation still threads that flag through. Update the existing run-path
setup in generate_test.go to use a config with
Features.GenerateGoRuntimeObjects: true and assert the generated Go model
artifacts include the runtime-object outputs, using generateCmd.Run and the
related schema generation helpers as the main reference points.
cmd/crossplane/function/generate.go (1)

148-161: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Honor features.generateGoRuntimeObjects in schema generation.

Thanks for threading cfg into Run. Did you mean to mirror cmd/crossplane/project/build.go and cmd/crossplane/project/run.go here? Line 160 still builds the schema manager with generator.AllLanguages() only, so crossplane function generate ignores the new config key and keeps generating the default Go models even when users opt in.

Suggested fix
 	schemaMgr := manager.New(
 		c.schemasFS,
-		generator.Filter(generator.AllLanguages(), c.proj.Spec.Schemas.GetLanguages()),
+		generator.Filter(
+			generator.AllLanguages(generator.WithGoRuntimeObjects(cfg.Features.GenerateGoRuntimeObjects)),
+			c.proj.Spec.Schemas.GetLanguages(),
+		),
 		runner.NewRealSchemaRunner(runner.WithImageConfig(c.proj.Spec.ImageConfigs)),
 	)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/function/generate.go` around lines 148 - 161, The schema
generation path in generateCmd.Run is still hardcoded to
generator.AllLanguages(), so it ignores features.generateGoRuntimeObjects from
cfg and keeps producing the default Go models. Update the schema manager setup
in Run to mirror the config-aware filtering used in project build/run, using cfg
to decide whether Go runtime objects should be included or excluded. Keep the
change localized around generateCmd.Run, generator.Filter, and manager.New so
the selected schema languages honor the new feature flag.
🧹 Nitpick comments (1)
internal/schemas/generator/runtimeobject_test.go (1)

53-143: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Thanks for adding focused coverage — would you mind table-driving these cases?

They already follow the PascalCase/std-lib parts of the repo test conventions, but this path expects args/want-style tables with named cases. Folding the DeepCopy and root-type scenarios into one table would make the next generator edge case much easier to add without duplicating setup. As per path instructions, **/*_test.go: "Enforce table-driven test structure: PascalCase test names (no underscores), args/want pattern, use cmp.Diff with cmpopts.EquateErrors() for error testing. Check for proper test case naming and reason fields."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/schemas/generator/runtimeobject_test.go` around lines 53 - 143, The
two separate tests in addRuntimeObjects should be folded into a single
table-driven test using named cases, args/want-style inputs, and shared setup so
future generator edge cases are easier to add. Update
TestAddRuntimeObjectsDeepCopy and TestAddRuntimeObjectsRootType to use a table
with clear case names and expected method sets, while keeping the existing
assertions around addRuntimeObjects and roMethods; preserve the current coverage
for DeepCopy-only vs root-type method generation and make the structure match
the *_test.go table-driven convention.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/schemas/generator/runtimeobject_integration_test.go`:
- Around line 85-92: The integration test checks for k8s.io/apimachinery in
generated models/go.mod, but it currently skips when dependency resolution
fails, which can hide broken or drifting generated deps. Update the
runtimeobject integration test(s) to fail the test instead of calling t.Skipf
when go mod download or similar dependency resolution breaks, and use an
explicit opt-out for offline/local runs only if needed. Keep the assertion
around the generated go.mod content in the same test flow so compile-gate
regressions in the runtimeobject generator are caught by CI.

In `@internal/schemas/generator/runtimeobject.go`:
- Around line 149-172: Named map/slice aliases are being treated as scalars, so
DeepCopy emits shallow pointer copies for fields like *Labels and shares backing
storage. Update the type classification path in collectScalarTypes/classifyElem
to resolve local named aliases before falling back to scalar handling, so named
collection types are recognized as composite types and copied deeply. Then
adjust writeFieldCopy to use the deep-copy path for those aliases instead of
generating **out = **in.

---

Outside diff comments:
In `@cmd/crossplane/function/generate_test.go`:
- Around line 291-328: Add a new Run regression case in TestRunErrors that
exercises generateCmd.Run with config.Config.Features.GenerateGoRuntimeObjects
enabled, so the test verifies schema generation still threads that flag through.
Update the existing run-path setup in generate_test.go to use a config with
Features.GenerateGoRuntimeObjects: true and assert the generated Go model
artifacts include the runtime-object outputs, using generateCmd.Run and the
related schema generation helpers as the main reference points.

In `@cmd/crossplane/function/generate.go`:
- Around line 148-161: The schema generation path in generateCmd.Run is still
hardcoded to generator.AllLanguages(), so it ignores
features.generateGoRuntimeObjects from cfg and keeps producing the default Go
models. Update the schema manager setup in Run to mirror the config-aware
filtering used in project build/run, using cfg to decide whether Go runtime
objects should be included or excluded. Keep the change localized around
generateCmd.Run, generator.Filter, and manager.New so the selected schema
languages honor the new feature flag.

---

Nitpick comments:
In `@internal/schemas/generator/runtimeobject_test.go`:
- Around line 53-143: The two separate tests in addRuntimeObjects should be
folded into a single table-driven test using named cases, args/want-style
inputs, and shared setup so future generator edge cases are easier to add.
Update TestAddRuntimeObjectsDeepCopy and TestAddRuntimeObjectsRootType to use a
table with clear case names and expected method sets, while keeping the existing
assertions around addRuntimeObjects and roMethods; preserve the current coverage
for DeepCopy-only vs root-type method generation and make the structure match
the *_test.go table-driven convention.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 799a4495-015a-4313-a14e-5e2a177d5c30

📥 Commits

Reviewing files that changed from the base of the PR and between 86f5f7a and 75d816f.

📒 Files selected for processing (13)
  • cmd/crossplane/config/help/config.md
  • cmd/crossplane/config/set.go
  • cmd/crossplane/function/generate.go
  • cmd/crossplane/function/generate_test.go
  • cmd/crossplane/main.go
  • cmd/crossplane/project/build.go
  • cmd/crossplane/project/run.go
  • internal/config/config.go
  • internal/schemas/generator/go.go
  • internal/schemas/generator/interface.go
  • internal/schemas/generator/runtimeobject.go
  • internal/schemas/generator/runtimeobject_integration_test.go
  • internal/schemas/generator/runtimeobject_test.go

Comment thread internal/schemas/generator/runtimeobject_integration_test.go
Comment thread internal/schemas/generator/runtimeobject.go
…ne#143)

Add opt-in generation of controller-gen-style DeepCopy methods, runtime.Object
and schema.ObjectKind methods on root types, and a per-package
groupversion_info.go (GroupVersion/SchemeBuilder/AddToScheme), so generated Go
models can be registered in a runtime.Scheme and used with k8s ecosystem
libraries. This lets users register types instead of setting apiVersion/kind by
hand.

Gated behind a new features.generateGoRuntimeObjects config flag (off by
default), threaded from config through the project build/run and function
generate commands. When enabled, the generated models module additionally
depends on k8s.io/apimachinery (v0.33.0, matching the function go template so a
generated function that consumes the models still builds).

Root types are detected structurally (APIVersion+Kind+Metadata fields). The
runtime.Object pass runs after the k8s type-name fixups so it sees final type
names, and copies known stdlib-backed alias types (Time, MicroTime, FieldsV1,
RawExtension) by value rather than calling DeepCopyInto.

Verified by compile gates that build the generated module (CRD and OpenAPI
paths) plus a behavioral test asserting deep-copy independence for scalar,
slice-of-struct and map fields and AddToScheme GVK round-tripping.

Signed-off-by: Erik Miller <erik.miller@gusto.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make generated Go types implement runtime.Object and provide package-level AddToSchemes

1 participant