Skip to content

Support pre-built function runtimes and per-language schema generation#24

Merged
adamwg merged 4 commits into
crossplane:mainfrom
negz:diy
Jun 5, 2026
Merged

Support pre-built function runtimes and per-language schema generation#24
adamwg merged 4 commits into
crossplane:mainfrom
negz:diy

Conversation

@negz
Copy link
Copy Markdown
Member

@negz negz commented May 21, 2026

Description of your changes

This PR bundles two small improvements I wanted while taking the new Crossplane CLI for a test drive on a complex project. Each is in its own commit.

1. Pre-built function runtime images (fixes #21)

Crossplane projects today discover embedded functions by convention: every subdirectory of paths.functions is treated as a function, and the CLI auto-detects the language and builds the runtime image. This works well for simple projects but blocks projects that have outgrown the built-in builders or that need to coordinate function builds with an existing build system (make, nix, Bazel, CI pipelines).

This PR adds an optional functions list to ProjectSpec. When the list is present it disables auto-discovery and is the sole source of truth for which functions to build. Each entry uses a source discriminator (Directory or Tarball):

spec:
  architectures: [amd64, arm64]
  functions:
    - source: Directory
      directory:
        name: function-a
    - source: Tarball
      tarball:
        name: function-b
        pathPrefix: build/function-b

Directory-source functions follow the existing build path. Tarball-source functions skip language detection and load one pre-built single-platform OCI image tarball per target architecture, following the naming convention <pathPrefix>-<arch>.tar or <pathPrefix>-<arch>.tar.gz (preferring the plain .tar when both exist). Per-architecture tarballs match what build tools naturally produce without bundling: docker save, Nix's dockerTools.buildImage, Bazel's oci_tarball, ko build --tarball, etc. all emit one single-platform tarball at a time. Packaging is inherently per-architecture too — each runtime image gets its own crossplane.yaml layer before they're tied together into a multi-arch package index — so the CLI would have to split a multi-arch input apart anyway. The gzipped variant is split into a separate commit; it's needed because Nix's image builders emit gzipped tarballs by default.

2. Per-language schema generation (fixes #29)

By default crossplane project build and crossplane dependency update-cache generate schemas for all four supported languages (Go, JSON, KCL, Python). For a project that only consumes one of them, every build generates language bindings the project never imports.

This commit adds an optional schemas block to ProjectSpec:

spec:
  schemas:
    languages: [python]

When languages is set, schema generation is restricted to the listed languages, both for the project's own XRDs and for its declared dependencies. The filter flows through project build/run and dependency update-cache/clean-cache. The block is nested rather than flat to leave room for future schema-related knobs.

Reviewers may want to focus on loadTarballRuntime and loadRuntimeImage in internal/project/build.go (the new tarball loading path) and on ProjectSchemas.Validate in apis/dev/v1alpha1/validate.go together with generator.Filter in internal/schemas/generator/interface.go (the schema language filter). The language identifiers are now defined as constants in the API package and consumed by the generators directly, so the two can't drift.

I have:

Comment thread internal/project/build.go Fixed
@negz negz force-pushed the diy branch 2 times, most recently from ca74952 to d261f8d Compare May 21, 2026 22:32
@negz negz changed the title Support pre-built function runtime images in projects Support pre-built function runtimes and per-language schema generation May 22, 2026
@negz negz marked this pull request as ready for review May 22, 2026 21:32
@negz negz requested review from a team, jcogilvie and tampakrap as code owners May 22, 2026 21:32
@negz negz requested review from jbw976 and removed request for a team May 22, 2026 21:32
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

Review Change Stack

Warning

Review limit reached

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

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

Your organization has run out of usage credits. Purchase more in the billing tab.

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

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

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: 00ab1505-3962-4bee-8754-a917f2d3e1b0

📥 Commits

Reviewing files that changed from the base of the PR and between fd39e0f and 0a780a5.

⛔ Files ignored due to path filters (1)
  • apis/dev/v1alpha1/zz_generated.deepcopy.go is excluded by !**/zz_generated*.go and included by **/*.go
📒 Files selected for processing (17)
  • apis/dev/v1alpha1/project_types.go
  • apis/dev/v1alpha1/validate.go
  • apis/dev/v1alpha1/validate_test.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
  • internal/project/build.go
  • internal/project/build_test.go
  • internal/schemas/generator/go.go
  • internal/schemas/generator/interface.go
  • internal/schemas/generator/interface_test.go
  • internal/schemas/generator/json.go
  • internal/schemas/generator/kcl.go
  • internal/schemas/generator/python.go
  • internal/schemas/manager/manager.go
📝 Walkthrough

Walkthrough

Adds ProjectSchemas and Function types (directory or tarball sources), validation for schemas and functions, resolves functions at build time, loads per-arch tarball runtimes (.tar/.tar.gz) with gzip support, centralizes schema language constants, and wires CLI commands to filter generators by project config.

Changes

Function sources and schema language configuration

Layer / File(s) Summary
Function and schema configuration types
apis/dev/v1alpha1/project_types.go
Adds function source discriminators (FunctionSourceDirectory, FunctionSourceTarball), schema language constants (SchemaLanguageGo, SchemaLanguageJSON, SchemaLanguageKCL, SchemaLanguagePython), SupportedSchemaLanguages(), ProjectSchemas with GetLanguages(), and Function/FunctionDirectory/FunctionTarball types plus Function.Name().
Configuration validation
apis/dev/v1alpha1/validate.go, apis/dev/v1alpha1/validate_test.go
Adds ProjectSchemas.Validate(), Function.Validate() and per-source validators; enforces nil vs explicit-empty semantics for schema languages, unique DNS-1123 subdomain function names, exactly-one source, and relative non-empty tarball PathPrefix.
Schema generator language centralization
internal/schemas/generator/interface.go, internal/schemas/generator/*, internal/schemas/generator/interface_test.go, internal/schemas/manager/manager.go
Replaces hard-coded generator language strings with dev API constants, adds Filter(all, langs) to select generators by configured languages, and adds tests asserting API alignment and filter behavior.
CLI command integration
cmd/crossplane/function/generate.go, cmd/crossplane/dependency/cache.go, cmd/crossplane/project/build.go, cmd/crossplane/project/run.go, cmd/crossplane/function/generate_test.go
Validates requested function languages against project schema config, and filters schema generators used by project build, project run, and dependency update-cache to the languages returned by ProjectSpec.Schemas.GetLanguages().
Function resolution and build orchestration
internal/project/build.go
Resolves functions up-front (explicit or auto-discover), dispatches directory functions to language builders, loads per-architecture tarball runtimes (.tar / .tar.gz) with gzip support and architecture validation, adjusts examples loading by source type, and updates concurrency/error handling.
Build tests and helpers
internal/project/build_test.go
Adds tests for resolveFunctions precedence, explicit-function builds, tarball-function image creation per-architecture, and tarball runtime loading formats/precedence; includes helpers to write and inspect runtime tarballs and extract image architectures.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • crossplane/crossplane#7251 — Implements pre-built function runtime images as tarball sources, matching the feature request to accept user-provided OCI runtime inputs.

Do you want a separate pass that flags specific lines for API stability/compatibility checks or that lists follow-up TODOs?

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: pre-built function runtimes and per-language schema generation, both core features introduced in the PR.
Description check ✅ Passed The description thoroughly explains both features, their motivation, implementation details, and includes links to the fixed issues.
Linked Issues check ✅ Passed All primary coding objectives from issues #21 and #29 are met: support for pre-built function runtimes (Directory/Tarball source discriminator, per-arch tarball loading), per-language schema generation (spec.schemas configuration, filtering through build/dependency commands), and validation logic.
Out of Scope Changes check ✅ Passed All code changes align with the two core features: function source configuration, schema language filtering, validation, and supporting generator/manager updates. No unrelated changes detected.
Breaking Changes ✅ Passed All changes are additions: new optional fields in ProjectSpec (Functions, Schemas), new types, constants, and methods. No removals, renames, required fields added, or behavior removed.
Feature Gate Requirement ✅ Passed Features are in v1alpha1 API (already experimental). Functions and Schemas fields are optional with backward compatibility: omitted fields preserve legacy behavior.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
internal/project/build.go (1)

607-642: 💤 Low value

Minor: Consider returning both errors from gzipReadCloser.Close().

Currently, if g.Reader.Close() succeeds but g.file.Close() fails, we return the file error. But if both fail, we only return the gzip error and lose the file error. This is probably fine in practice since file close errors are rare, but I wanted to mention it.

Would it be worth using errors.Join to return both errors when they both occur? That said, if you've considered this and decided the current behavior is sufficient, I'm happy to defer to your judgment.

🤖 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/project/build.go` around lines 607 - 642, The Close method on
gzipReadCloser currently returns only the first non-nil error (g.Reader.Close)
or the file error (g.file.Close), losing the other error if both fail; change
gzipReadCloser.Close to combine both errors when present (use errors.Join(gerr,
ferr) or equivalent) so callers receive both failures, keeping existing return
behavior when only one error exists; update the gzipReadCloser.Close
implementation to import and use errors.Join while preserving the current call
ordering and semantics used in gzipOpener and gzipReadCloser.
🤖 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 `@apis/dev/v1alpha1/project_types.go`:
- Around line 272-286: The Function.Name method should be nil-safe like
GetLanguages: add a nil-receiver guard at the start of Function.Name (check if f
== nil and return an empty string) so callers can safely call
(*Function)(nil).Name() without panicking; update the Function.Name method to
return "" immediately when f is nil and keep the existing switch logic
unchanged.

In `@apis/dev/v1alpha1/validate.go`:
- Around line 121-122: Update the user-facing validation messages that are
currently appended to errs for unsupported schema languages and invalid function
names: replace technical phrasing like "is not a supported schema language" with
clear, actionable text that states what the user tried to provide, what valid
options are, and exactly what to change and retry (e.g., "The schema language
'X' is not supported. Please choose one of [A,B,C] and update schemas.languages
to one of these values, then retry."). Apply the same style to the other related
error appends (the ones that reference schema languages, the variable supported,
lang, and the checks for invalid function names) so each error suggests the
corrective action and shows valid examples or accepted patterns.
- Around line 241-247: The validation error wrapping uses fmt.Errorf("...: %w",
err) for the Directory and Tarball cases; replace those with
crossplane-runtime's errors.Wrap to match project conventions: import
"github.com/crossplane/crossplane-runtime/pkg/errors" (or add to existing
imports) and change the two append lines to errs = append(errs, errors.Wrap(err,
"directory")) for f.Directory.Validate() and errs = append(errs,
errors.Wrap(err, "tarball")) for f.Tarball.Validate(), leaving the rest of the
switch logic unchanged.

---

Nitpick comments:
In `@internal/project/build.go`:
- Around line 607-642: The Close method on gzipReadCloser currently returns only
the first non-nil error (g.Reader.Close) or the file error (g.file.Close),
losing the other error if both fail; change gzipReadCloser.Close to combine both
errors when present (use errors.Join(gerr, ferr) or equivalent) so callers
receive both failures, keeping existing return behavior when only one error
exists; update the gzipReadCloser.Close implementation to import and use
errors.Join while preserving the current call ordering and semantics used in
gzipOpener and gzipReadCloser.
🪄 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: 6a7b1fe0-a0d4-41ce-9bbf-203928089248

📥 Commits

Reviewing files that changed from the base of the PR and between 5a1ea69 and 5208019.

⛔ Files ignored due to path filters (1)
  • apis/dev/v1alpha1/zz_generated.deepcopy.go is excluded by !**/zz_generated*.go and included by **/*.go
📒 Files selected for processing (15)
  • apis/dev/v1alpha1/project_types.go
  • apis/dev/v1alpha1/validate.go
  • apis/dev/v1alpha1/validate_test.go
  • cmd/crossplane/dependency/cache.go
  • cmd/crossplane/project/build.go
  • cmd/crossplane/project/run.go
  • internal/project/build.go
  • internal/project/build_test.go
  • internal/schemas/generator/go.go
  • internal/schemas/generator/interface.go
  • internal/schemas/generator/interface_test.go
  • internal/schemas/generator/json.go
  • internal/schemas/generator/kcl.go
  • internal/schemas/generator/python.go
  • internal/schemas/manager/manager.go

Comment thread apis/dev/v1alpha1/project_types.go
Comment thread apis/dev/v1alpha1/validate.go Outdated
Comment thread apis/dev/v1alpha1/validate.go
Copy link
Copy Markdown
Member

@adamwg adamwg left a comment

Choose a reason for hiding this comment

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

Couple of small comments, but I like both of these features overall.

I wonder if crossplane function generate should refuse to generate functions in languages that aren't specified in spec.languages. Feels like it would be surprising to generate a function and find you don't have any schemas to use in it.

Comment thread internal/project/build.go Outdated
Comment thread internal/project/build.go Outdated
Crossplane projects today discover embedded functions by convention:
every subdirectory of paths.functions is treated as a function, and the
CLI auto-detects the language and builds the runtime image. This works
well for simple projects but blocks projects that have outgrown the
built-in builders or that need to coordinate function builds with an
existing build system (make, nix, Bazel, CI pipelines).

Per crossplane#21, users want to supply
pre-built OCI runtime images alongside source-based functions, so the
CLI handles packaging while the user owns the build.

This commit adds an optional functions list to ProjectSpec. When the
list is present it disables auto-discovery and is the sole source of
truth for which functions to build. Each entry uses a Source
discriminator (Directory or Tarball) and a corresponding sub-field:

  spec:
    architectures: [amd64, arm64]
    functions:
      - source: Directory
        directory:
          name: function-a
      - source: Tarball
        tarball:
          name: function-b
          pathPrefix: build/function-b

Directory-source functions follow the existing build path. Tarball-
source functions skip language detection and load one pre-built
single-platform OCI image tarball per target architecture, following
the naming convention `<pathPrefix>-<arch>.tar`. So the example above
loads `build/function-b-amd64.tar` and `build/function-b-arm64.tar`.

Per-architecture tarballs match what build tools naturally produce
without bundling: `docker save`, Nix's dockerTools.buildImage,
Bazel's oci_tarball, `ko build --tarball`, etc. all emit one
single-platform tarball at a time. Packaging is inherently per-
architecture too — each runtime image gets its own crossplane.yaml
layer before they're tied together into a multi-arch package index —
so the CLI would have to split a multi-arch input apart anyway.

The CLI verifies that each tarball's image config records the
architecture its filename promises, and adds the package metadata
layer (crossplane.yaml) before assembling the multi-arch package
index. The on-disk output is identical to a CLI-built function.

When the functions list is omitted, the existing auto-discovery
behaviour is preserved unchanged.

Fixes crossplane#21.

Signed-off-by: Nic Cope <[email protected]>
@negz negz force-pushed the diy branch 2 times, most recently from fd39e0f to e46389d Compare June 5, 2026 00:03
negz added 3 commits June 4, 2026 17:13
Nix's dockerTools.buildImage produces gzipped tarballs by default. Some
other build tools (Bazel rules_oci's oci_load, certain ko invocations)
do the same. With only plain .tar accepted, users of these tools had to
add a decompress step to their build pipeline just to feed images to
the Crossplane CLI.

This commit teaches the function tarball loader to fall back to
`<pathPrefix>-<arch>.tar.gz` when `<pathPrefix>-<arch>.tar` is not
present, preferring the plain tar when both exist. The gzipped tarball
is streamed through gzip.NewReader into go-containerregistry's
tarball.Image; no temporary files are written.

Signed-off-by: Nic Cope <[email protected]>
By default crossplane project build and crossplane dependency
update-cache generate schemas for all four supported languages (Go,
JSON, KCL, Python). Per crossplane#29
this is wasteful for projects that only consume some of them: every
build generates language bindings the project never imports.

This commit adds an optional schemas block to ProjectSpec:

  spec:
    schemas:
      languages: [python]

When languages is set, schema generation is restricted to the listed
languages. The filter applies both to the project's own XRD schemas
and to its declared dependencies, and flows through project
build/run and dependency update-cache/clean-cache. When schemas is
omitted (the default), all languages are generated as before.

The schemas block is nested rather than flat to leave room for
future schema-related knobs (output paths, generator-specific
options) without scattering schema config across ProjectSpec.

The supported language identifiers are defined as constants
(SchemaLanguageGo, SchemaLanguageJSON, SchemaLanguageKCL,
SchemaLanguagePython) in the API package, with SupportedSchemaLanguages
returning the canonical set. The schema generator package consumes
these constants directly so the two cannot drift, and a test in the
generator package asserts that AllLanguages covers exactly the API's
declared set.

Fixes crossplane#29.

Signed-off-by: Nic Cope <[email protected]>
The function build path threaded two filesystems through its call
chain: the project root, and a separate afero.BasePathFs rooted at the
project's functions directory. Both pointed at the same underlying
tree, so most function operations were addressed relative to the
functions directory while runtime tarballs and the language builder's
ProjectFS were addressed relative to the project root. Carrying both
meant every function in the chain took two afero.Fs arguments, and the
per-function builder filesystem was a BasePathFs wrapped over another
BasePathFs.

This change drops the functions-rooted filesystem and addresses
everything relative to the project root, joining spec.paths.functions
inline where the functions directory was previously the root. The
build chain now takes a single afero.Fs, and the per-function builder
filesystem wraps the project filesystem once instead of twice.

Signed-off-by: Nic Cope <[email protected]>
@negz
Copy link
Copy Markdown
Member Author

negz commented Jun 5, 2026

@adamwg I think I've addressed everything now.

@negz negz requested a review from adamwg June 5, 2026 04:18
Copy link
Copy Markdown
Member

@adamwg adamwg left a comment

Choose a reason for hiding this comment

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

Thanks for the updates, this looks great.

@adamwg adamwg merged commit 5ed17c9 into crossplane:main Jun 5, 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.

Support generating schemas for specific languages Support pre-built function runtime images in control plane projects

3 participants