Skip to content

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

Merged
adamwg merged 1 commit into
crossplane:mainfrom
erikmiller-gusto:runtime-object-scheme
Aug 3, 2026
Merged

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

Conversation

@erikmiller-gusto

@erikmiller-gusto erikmiller-gusto commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Description of your changes

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 every command that generates schemas:
project build, project run, function generate, dependency add,
dependency update-cache, composition generate, composition render /
render, and operation render. The render commands also hand the same
generators to the dependency manager, so dependency schemas match the project's
own. dependency clean-cache is the one exception — it only removes generated
schemas, so it keeps the flag-off default in dependency.NewManager.

When the flag is off, no runtime.Object, DeepCopy or groupversion_info.go
code is emitted.

Dependencies

The generated models module 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 the models using the
shipped template go.mod/go.sum).

There is one go.mod/go.sum regardless of the flag. With the flag off nothing
imports apimachinery, but the requirement stays: an unused requirement is
harmless to go build, go mod tidy removes it for anyone who cares, and one
set of module files is one thing fewer to maintain and test.

Testing

  • Unit tests for the DeepCopy / runtime.Object generation (root vs non-root
    detection, scalar/struct/alias field handling), for the API group written into
    groupversion_info.go for built-in Kubernetes packages, and for the feature
    flag plumbing.

  • A default-off test confirming no runtime.Object artifacts are emitted.

  • Compile gates that materialize the generated module (the CRD path, the
    OpenAPI path, the flag-off path, and both features on) and go build /
    go test it — these
    catch DeepCopyInto codegen bugs that parse cleanly but don't type-check. One
    of them is 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.

    The compile gates shell out to the Go toolchain to resolve the generated
    module's dependencies, so they need network access and a writable module
    cache. The Nix sandbox our unit tests run in has neither, and the generated
    module pins the apimachinery version the function template uses rather than
    the one this repo depends on, so it can't resolve from gomod2nix either. They
    are therefore behind a compilegate build tag rather than a runtime skip,
    which would report as a test that ran:

    go test -tags compilegate ./internal/schemas/generator/...

    .golangci.yml lints the tagged file so it can't rot. Happy to add a
    network-enabled CI job that runs them as a follow-up if you'd like the gate
    enforced on every PR — didn't want to add a non-hermetic job unilaterally.

    Worth flagging: accessors_test.go from feat: add GetX/SetX accessors to generated Go models #160 solves the same problem with a
    runtime skip, so the package now has two conventions. Happy to converge on
    either — I left it alone rather than change merged code in a rebase.

Rebased onto main after #160

#160 landed the accessors feature and the generator Option plumbing this
change was written against, so the diff here is a good deal smaller than it was.
Four things the rebase decided, beyond the mechanical "both flags, not either":

  • generateK8sSharedSchemas, generateK8sPackageCode, generateModelsWithGVK
    and generateGVKGroupCode took an accessors bool. Rather than grow a second
    positional bool they now take the goGenerator, which already holds both
    flags.
  • Order matters, and it's now pinned by a test. Both features emit methods
    onto the same structs. runtime.Object runs first because addAccessors
    skips method names that already exist and applyRuntimeObjects doesn't — a
    field named objectKind yields a GetObjectKind accessor that collides with
    the one root types get from schema.ObjectKind. Reversed, the struct gets two
    and the module stops compiling.
  • Two new compile gates build the output with both features on; neither
    feature's own gate covered the combination.
  • The eight commits this branch carried were squashed into one. They were review
    iterations that introduced and then removed code, so replaying them meant
    resolving conflicts against states that no longer exist.

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.

I have:

On the two struck items: the crossplane config help text this PR extends is
rendered into the generated command reference, so crossplane/docs picks the new
key up without a separate change. And this is a new opt-in feature rather than a
fix, so it isn't a backport candidate.

Need help with this checklist? See the cheat sheet.

@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

📝 Walkthrough

Walkthrough

Generated Go models can now optionally implement Kubernetes runtime.Object, deep-copy methods, GVK methods, and package-level scheme registration. A new configuration flag controls the feature across CLI commands, CRD generation, and OpenAPI generation.

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
Adds and documents features.generateGoRuntimeObjects.
Command and generator option threading
cmd/crossplane/composition/generate.go, cmd/crossplane/dependency/*, cmd/crossplane/function/*, cmd/crossplane/project/*, cmd/crossplane/render/*
Passes the feature flag into schema generator configuration across CLI paths.
Configured Go generation and package artifacts
internal/schemas/generator/interface.go, internal/schemas/generator/go.go
Threads runtime-object settings through CRD and OpenAPI generation, updates generated module dependencies, and writes groupversion_info.go files.
AST runtime-object augmentation
internal/schemas/generator/runtimeobject.go
Generates deep-copy, runtime.Object, GVK, scheme registration, and required imports from Go ASTs.
Generation and compile validation
internal/schemas/generator/*test.go, .golangci.yml
Tests option propagation, generated artifacts, disabled behavior, method deduplication, compilation, and runtime-object behavior.

Estimated code review effort: 5 (Critical) | ~90+ minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI as Crossplane CLI
  participant Config as Config.Features
  participant Generator as Schema generator
  participant GoModels as Generated Go models
  participant Scheme as Generated scheme helpers
  CLI->>Config: set generateGoRuntimeObjects
  Config->>Generator: pass runtime-object option
  Generator->>GoModels: add DeepCopy and runtime.Object methods
  Generator->>Scheme: write GroupVersion and AddToScheme
Loading

Possibly related PRs

  • crossplane/cli#160: Adds the related Go schema generator option and accessor-generation pipeline.

Suggested reviewers: jcogilvie, tampakrap, jbw976

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title clearly describes the change but exceeds the 72-character limit at 83 characters. Shorten the title to 72 characters or fewer while preserving its focus on runtime.Object and AddToScheme generation.
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description explains the runtime.Object feature, configuration flag, implementation scope, dependencies, and tests.
Linked Issues check ✅ Passed The changes implement runtime.Object support and package-level AddToScheme helpers requested by issue #143.
Out of Scope Changes check ✅ Passed The code, documentation, configuration, and tests support the linked issue and the stated feature-flag implementation.
Breaking Changes ✅ Passed No files under apis/** changed. The cmd/** diff only adds an optional false-by-default config key/help and forwards it; no public fields or CLI flags were removed, renamed, or made required.
Feature Gate Requirement ✅ Passed The new runtime-object behavior is opt-in: config defaults false, the setter accepts features.generateGoRuntimeObjects, and all generator paths pass the flag to gated augmentation; no apis/** files...

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 Outdated
Comment thread internal/schemas/generator/runtimeobject.go Outdated
erikmiller-gusto added a commit to erikmiller-gusto/crossplane-cli that referenced this pull request Jul 21, 2026
Apply accessor generation after Go post-processing at the call sites via a
new applyAccessors helper, rather than folding it into generateGo. This
mirrors the runtime.Object generation approach in crossplane#162 so both features use
the same "post-process the generated code after the fact" pattern, and makes
generateGo's signature identical across both, keeping the two changes easy to
reconcile. Generating accessors after fixK8sTypeNames/removeSelfImports also
means they reference the final type names.

Skip unexported struct fields when emitting accessors: generated models don't
currently have any, but an accessor for one would be useless to external
consumers and could produce oddly-cased method names.

Reuse receiverTypeName from accessors.go in the tests instead of redefining an
equivalent renderRecv helper.

Signed-off-by: Erik Miller <erik.miller@gusto.com>

@adamwg adamwg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks good to me overall, and I like that #160 and this PR now work similarly.

One bug I noticed in testing: the core/v1 API group gets the wrong group name in groupversion_info.go:

var GroupVersion = schema.GroupVersion{Group: "core.k8s.io", Version: "v1"}

I think this one is a special case, since the Group should actually be empty. Other built-in groups I spot checked (apps/v1, networking.k8s.io/v1, batch/v1) look correct.

Comment thread internal/schemas/generator/go.go Outdated
// generateGoRuntimeObjects feature is enabled. It additionally requires
// k8s.io/apimachinery (used by the generated runtime.Object and AddToScheme
// code) and its transitive dependencies.
const goModContentsRuntimeObjects = `module dev.crossplane.io/models

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think there would be no harm in using this go.mod and go.sum unconditionally. There will be a couple of superfluous entries when runtime.Object generation is disabled, but go build is fine with that and go mod tidy will clean it up if anyone cares. One thing fewer to maintain and test if we just have one version.

@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: 1

🧹 Nitpick comments (1)
internal/schemas/generator/go.go (1)

636-675: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

User-facing messages in writeGroupVersionInfo.

Thanks for adding the scheme setup path. The wrapper import is already crossplane-runtime/pkg/errors, but the surrounding messages still describe generator internals; could the messages instead say what package/schema setup failed, such as “failed to write generated scheme registration for package ...”?

🤖 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/go.go` around lines 636 - 675, Update the error
messages in writeGroupVersionInfo to describe the user-facing failure in
generated scheme registration, including the relevant package or schema context
where available, instead of internal operations like stat, format, create, or
write. Preserve error wrapping and the existing behavior for each failure path.

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/go.go`:
- Around line 333-336: Update the k8sPkgMetaV1 handling in the package/group
generation flow to reuse getK8sPackageInfo(pkg) instead of assigning
meta.k8s.io, meta, and v1 directly. Preserve the canonical meta.core.k8s.io
mapping and apiGroup synthetic-group behavior used by the OpenAPI path.

---

Nitpick comments:
In `@internal/schemas/generator/go.go`:
- Around line 636-675: Update the error messages in writeGroupVersionInfo to
describe the user-facing failure in generated scheme registration, including the
relevant package or schema context where available, instead of internal
operations like stat, format, create, or write. Preserve error wrapping and the
existing behavior for each failure path.
🪄 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 Plus

Run ID: 507815b2-aef6-47c1-b768-2c7195b9ee23

📥 Commits

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

📒 Files selected for processing (16)
  • .golangci.yml
  • 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/interface_test.go
  • internal/schemas/generator/runtimeobject.go
  • internal/schemas/generator/runtimeobject_compilegate_test.go
  • internal/schemas/generator/runtimeobject_integration_test.go
  • internal/schemas/generator/runtimeobject_test.go
🚧 Files skipped from review as they are similar to previous changes (9)
  • cmd/crossplane/project/build.go
  • cmd/crossplane/config/help/config.md
  • cmd/crossplane/project/run.go
  • internal/config/config.go
  • cmd/crossplane/function/generate_test.go
  • cmd/crossplane/config/set.go
  • cmd/crossplane/main.go
  • internal/schemas/generator/interface.go
  • internal/schemas/generator/runtimeobject.go

Comment thread internal/schemas/generator/go.go Outdated

@adamwg adamwg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I gave the latest version of this a test and it is working 👍 I'm able to use the generated schemas with SDK helpers like resource.AsObject and comopsed.From, which lets me eliminate the manual JSON round-trip conversions in my example.

The only thing missing, as in #160, is threading the config through to the other locations where we generate schemas: crossplane dependency ... and the render commands.

@erikmiller-gusto

Copy link
Copy Markdown
Contributor Author

@adamwg threading of the config should now be done in both PRs. Would it be worth collapsing them together into a single PR so they don't end up with merge conflicts?

@adamwg

adamwg commented Aug 3, 2026

Copy link
Copy Markdown
Member

@adamwg threading of the config should now be done in both PRs. Would it be worth collapsing them together into a single PR so they don't end up with merge conflicts?

@erikmiller-gusto I've merged #160. This one will need a rebase, but I expect the merge conflicts will be relatively easy to resolve.

Fixes crossplane#143.

Behind a new opt-in feature flag, generated Go models gain controller-gen-style
DeepCopyInto/DeepCopy on every struct, and on root types (structs with
APIVersion + Kind + Metadata, i.e. the resource and its List) DeepCopyObject,
GetObjectKind/GroupVersionKind/SetGroupVersionKind reading and writing the typed
APIVersion/Kind fields, and an init registering the type with the package
SchemeBuilder. Each package containing root types gets a groupversion_info.go
defining GroupVersion, SchemeBuilder and AddToScheme, apimachinery-only with no
controller-runtime dependency. Users no longer have to set apiVersion/kind by
hand on composed resources.

Off by default:

    crossplane config set features.generateGoRuntimeObjects true

The flag is threaded from config through every command that generates schemas:
project build, project run, function generate, dependency add,
dependency update-cache, composition generate, composition render / render, and
operation render. The render commands hand the same generators to the dependency
manager, so dependency schemas match the project's own. dependency clean-cache
is the exception: it only removes generated schemas, so it keeps the flag-off
default in dependency.NewManager.

The generated models module requires k8s.io/apimachinery, pinned to the version
the Go function template uses so a function consuming the models via a replace
statement still resolves everything from the template's go.sum. There is one
go.mod and go.sum regardless of the flag: an unused requirement is harmless to
go build, and one set of module files is one thing fewer to maintain and test.

Built-in Kubernetes types register under their real API group. The group labels
the generator uses for those packages are synthetic and only drive the directory
layout, so a goPackage now carries the layout group and the API group
separately. Without that, core/v1 and meta/v1 registered under "core.k8s.io" and
"meta.core.k8s.io", GVKs that disagree with what each type's own
GroupVersionKind() reports.

Tests cover DeepCopy generation (root vs non-root detection, scalar, struct and
named-collection-alias fields), the API group written into groupversion_info.go,
the feature-flag plumbing, and a default-off case. Compile gates materialize the
generated module and build it, including a behavioral test for deep-copy
independence and AddToScheme GVK round-tripping. They shell out to the Go
toolchain, so they need network access and a writable module cache that the Nix
sandbox running our unit tests does not have; they sit behind a compilegate
build tag rather than a runtime skip, which would report as a test that ran:

    go test -tags compilegate ./internal/schemas/generator/...

.golangci.yml lints the tagged file so it cannot rot.

Signed-off-by: Erik Miller <erik.miller@gusto.com>

Rebased onto main after crossplane#160, which landed the Go model accessors and the
generator Option plumbing this change was written against. Four resolutions
worth calling out:

  - generateK8sSharedSchemas, generateK8sPackageCode, generateModelsWithGVK and
    generateGVKGroupCode took an `accessors bool`. Rather than grow a second
    positional bool they now take the goGenerator, which already holds both
    flags.

  - Both features emit methods onto the same structs, so the order matters.
    runtime.Object runs first because addAccessors skips method names that
    already exist and applyRuntimeObjects does not: a field named objectKind
    yields a GetObjectKind accessor that collides with the one root types get
    from schema.ObjectKind. Reversed, the struct gets two and the generated
    module stops compiling. TestRuntimeObjectsThenAccessorsDeduplicates pins it.

  - Two compile gates build the generated module with both features on, which
    neither feature's own gate covered.

  - TestAllLanguagesWithGoRuntimeObjects became TestAllLanguagesGoOptions and
    now asserts both flags reach the Go generator independently and together.

The eight commits this branch carried were squashed into one: they were review
iterations that introduced and then removed code, and replaying them over the
new base meant resolving conflicts against states that no longer exist.

Signed-off-by: Erik Miller <erik.miller@gusto.com>

@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.

🧹 Nitpick comments (1)
internal/schemas/generator/runtimeobject_compilegate_test.go (1)

239-246: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a reason field to this table-driven test.

Thanks for the clear built-in GVK checks! Every other table-driven test in this cohort (TestAllLanguagesGoOptions, TestAddRuntimeObjects) includes a reason field explaining what each case verifies. This cases map is missing one. Add a reason field for consistency and to help future readers understand why CoreV1, MetaV1, and Autoscaling are the representative cases chosen out of the 5 registered schemes.

As per path instructions, **/*_test.go: "Check for proper test case naming and reason fields."

♻️ Proposed fix
 	cases := map[string]struct {
+		reason string
 		obj  runtime.Object
 		want schema.GroupVersionKind
 	}{
-		"CoreV1":      {obj: &corev1.Pod{}, want: schema.GroupVersionKind{Version: "v1", Kind: "Pod"}},
-		"MetaV1":      {obj: &metav1.Status{}, want: schema.GroupVersionKind{Version: "v1", Kind: "Status"}},
-		"Autoscaling": {obj: &autoscalingv1.TokenRequest{}, want: schema.GroupVersionKind{Group: "autoscaling", Version: "v1", Kind: "TokenRequest"}},
+		"CoreV1":      {reason: "core group types report an empty group in their GVK", obj: &corev1.Pod{}, want: schema.GroupVersionKind{Version: "v1", Kind: "Pod"}},
+		"MetaV1":      {reason: "meta group types report an empty group in their GVK", obj: &metav1.Status{}, want: schema.GroupVersionKind{Version: "v1", Kind: "Status"}},
+		"Autoscaling": {reason: "non-core built-in groups report their real group name", obj: &autoscalingv1.TokenRequest{}, want: schema.GroupVersionKind{Group: "autoscaling", Version: "v1", Kind: "TokenRequest"}},
 	},
🤖 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_compilegate_test.go` around lines
239 - 246, Add a reason string field to the table-driven cases in the GVK test,
and populate it with concise explanations for the CoreV1, MetaV1, and
Autoscaling representative cases. Update the test assertions or failure
reporting to use each case’s reason consistently with TestAllLanguagesGoOptions
and TestAddRuntimeObjects.
🤖 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.

Nitpick comments:
In `@internal/schemas/generator/runtimeobject_compilegate_test.go`:
- Around line 239-246: Add a reason string field to the table-driven cases in
the GVK test, and populate it with concise explanations for the CoreV1, MetaV1,
and Autoscaling representative cases. Update the test assertions or failure
reporting to use each case’s reason consistently with TestAllLanguagesGoOptions
and TestAddRuntimeObjects.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c1903e34-daa0-400d-b032-afea82833873

📥 Commits

Reviewing files that changed from the base of the PR and between 9f91e88 and 0990a75.

📒 Files selected for processing (20)
  • .golangci.yml
  • cmd/crossplane/composition/generate.go
  • cmd/crossplane/config/help/config.md
  • cmd/crossplane/config/set.go
  • cmd/crossplane/dependency/add.go
  • cmd/crossplane/dependency/cache.go
  • cmd/crossplane/function/generate.go
  • cmd/crossplane/function/generate_test.go
  • cmd/crossplane/project/build.go
  • cmd/crossplane/project/run.go
  • cmd/crossplane/render/op/cmd.go
  • cmd/crossplane/render/xr/cmd.go
  • internal/config/config.go
  • internal/schemas/generator/go.go
  • internal/schemas/generator/interface.go
  • internal/schemas/generator/interface_test.go
  • internal/schemas/generator/runtimeobject.go
  • internal/schemas/generator/runtimeobject_compilegate_test.go
  • internal/schemas/generator/runtimeobject_integration_test.go
  • internal/schemas/generator/runtimeobject_test.go
🚧 Files skipped from review as they are similar to previous changes (10)
  • cmd/crossplane/config/set.go
  • .golangci.yml
  • internal/config/config.go
  • internal/schemas/generator/interface.go
  • cmd/crossplane/config/help/config.md
  • cmd/crossplane/function/generate_test.go
  • internal/schemas/generator/runtimeobject_integration_test.go
  • cmd/crossplane/function/generate.go
  • internal/schemas/generator/runtimeobject.go
  • internal/schemas/generator/go.go

@adamwg
adamwg merged commit 8df0f92 into crossplane:main Aug 3, 2026
10 checks passed
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

3 participants