Skip to content

Migrate to the Crossplane CLI#83

Merged
negz merged 9 commits into
mainfrom
down
May 26, 2026
Merged

Migrate to the Crossplane CLI#83
negz merged 9 commits into
mainfrom
down

Conversation

@negz

@negz negz commented May 21, 2026

Copy link
Copy Markdown
Collaborator

Addresses #10 and #13.

There are two related things going on in this PR.

Moving to the Crossplane CLI

Modelplane has depended on the Upbound up CLI for building, testing, and pushing function packages since the project started. The tooling that up provides is being upstreamed into the Crossplane CLI, where the open-source community will align on tooling for control plane projects.

I want Modelplane to explore what using control plane projects looks like for a real, complex project and to feed that experience back into the Crossplane CLI.

This PR swaps up for crossplane. upbound.yaml becomes crossplane-project.yaml We keep docker-credential-up because we still need it for xpkg.upbound.io registry auth.

Limiting the Crossplane CLI to schemas and packaging

The crossplane CLI (and up) aim to handle the entire lifecycle of a control plane project. That includes building and testing function packages. This makes a lot of sense for folks starting out with control planes. We want them to be able to configure their control planes using Python, Go, KCL etc without needing to become experts in the the relevant language toolchain.

On the other hand, one of the biggest benefits of using a general purpose programming language like Python to configure your control plane is the extensive and mature ecosystem for things like testing and linting. With up, Modelplane was in a split brain situation. up built the Python code into function runtimes, but we still needed another build tool (in our case Nix) to run code quality checks like linters consistently across CI and dev environments. It was easy for our two build tools (Nix and the up CLI) to drift.

To that end I've opened crossplane/cli#24, and pinned Modelplane on that PR. The PR does two things:

  1. It lets you tell crossplane not to build function runtimes (i.e. function OCI images). You can instead use your own build tool to roll your own.
  2. It lets you tell crossplane to only generate schemas for the languages you use.

The result is that you can choose to use the crossplane CLI only for (Crossplane) package dependency management, schema generation, and Crossplane packaging - i.e. decorating a regular OCI image with Crossplane package metadata.

I've committed the generated Python models to the repo. This allows us to use them for reproducible builds, type checking, etc in CI without needing network to pull them on every build. (The tradeoff is they're a massive amount of code.)

I've also switched the Python build system from Hatch to uv, since uv2nix seems to be the modern and minimal way to do Python builds with Nix.

@negz negz changed the title Use the Crossplane CLI instead of the Upbound CLI Migrate to the Crossplane CLI May 22, 2026
@negz negz marked this pull request as ready for review May 22, 2026 21:37
Copilot AI review requested due to automatic review settings May 22, 2026 21:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

negz added 7 commits May 22, 2026 14:43
Modelplane depends on the Upbound up CLI for project tooling: building
function images, generating Pydantic models, running composition tests,
and pushing packages. The project tooling is being upstreamed into the
Crossplane CLI (crossplane/crossplane#6840),
which is now available at github.com/crossplane/cli. Modelplane is open
source and built on Crossplane — contributors should not need the
Upbound CLI.

This commit replaces the up CLI with the Crossplane CLI and restructures
the Python functions to match its expectations.

The Crossplane CLI's Python builder expects each function to be a
hatch-buildable package (pyproject.toml + function/ directory) rather
than a bare main.py with symlinks to shared code. This is a fundamental
change to how functions are structured, built, and tested.

Project manifest:
- upbound.yaml (meta.dev.upbound.io/v2alpha1) is replaced by
  crossplane-project.yaml (dev.crossplane.io/v1alpha1) with typed
  dependencies. function-auto-ready is dropped (unused).

Function structure:
- Each function is now a self-contained hatch package under
  functions/<name>/ with pyproject.toml, function/fn.py (FunctionRunner
  + Composer), function/main.py (CLI entrypoint), and
  function/_compat.py (SDK shims pending upstream).
- The lib/ shared library is eliminated. Helpers that belong in the SDK
  (set_conditions, update_status, child_name) are shimmed locally via
  _compat.py until crossplane/function-sdk-python#205
  ships. Everything else is inlined into the function that uses it.
- The lib/model symlinks are gone. Functions import generated models
  from the crossplane-models package at schemas/python/.

Tests:
- Composition tests using up's CompositionTest schema are replaced by
  unittest-based tests co-located with each function at
  functions/<name>/tests/test_fn.py. Tests call RunFunction directly
  and compare the full RunFunctionResponse via MessageToDict.

Nix:
- The up and docker-credential-up binaries are replaced by a crossplane
  binary extracted from the crossplane/cli flake. docker-credential-up
  is retained for xpkg.upbound.io registry authentication.
- nix run .#test-crossplane now builds the project, creates a venv with
  function dependencies, and runs unittest across all functions.
- nix run .#format is added for code formatting.

CI:
- The upbound/action-up login step is removed. Registry auth uses
  standard Docker credentials.

Addresses #13.

Signed-off-by: Nic Cope <nicc@rk0n.org>
Every function's pyproject.toml had the same template name ("function")
and description ("A Crossplane composition function."). This makes it
hard to tell functions apart in tooling output, error messages, and
dependency trees.

This commit gives each function a distinct name matching its directory
and a description summarizing what it composes.

Signed-off-by: Nic Cope <nicc@rk0n.org>
The schemas/python tree is generated from XRDs by the Crossplane CLI's
Python builder. Until now we relied on `crossplane project build` to
produce it, which meant nothing else could rely on the schemas being
present.

This commit tracks the generated schemas in git and re-includes
schemas/python/ in .gitignore. The schemas become a normal Python
package other tooling can depend on without first running the CLI. CI
can type-check the composition functions against their imported models,
and a Nix-based function image builder can take schemas/python as a
build input.

The schemas should be regenerated whenever an XRD changes.

Signed-off-by: Nic Cope <nicc@rk0n.org>
The Crossplane CLI handles simple projects end-to-end, but for a project
like Modelplane — nine composition functions, unit tests, linters, type
checking — it's not enough on its own. The Crossplane CLI can't run
tests, check types, or lint code. A project this size needs a real build
system layered on top.

This commit adopts uv as the Python workspace manager and Nix as the
build orchestrator, with uv2nix bridging the two. uv.lock is the single
source of truth for Python dependencies. Nix reads the lockfile via
uv2nix, builds per-function OCI image tarballs (including cross-arch),
and runs all checks in a sandbox. The Crossplane CLI assembles the final
project from pre-built tarballs via source: Tarball (crossplane/cli#24).

The principle is that the Crossplane CLI should integrate with language
ecosystems, not replace them. Each tool does what it's best at: uv
manages Python packages, Nix orchestrates builds and CI, the Crossplane
CLI packages the result.

nix flake check is the one-stop CI gate: Python lint (ruff), shell lint
(shellcheck, shfmt), Nix lint (statix, deadnix, nixfmt), and unit tests
for all nine functions. nix run .#fix auto-fixes everything those checks
verify. nix run .#generate regenerates Python schemas from XRDs and
dependency CRDs.

Depends on crossplane/cli#24.

Signed-off-by: Nic Cope <nicc@rk0n.org>
The previous restructuring commit on this branch replaced the up CLI's
test runner with nix flake check, dropped the lib/ shared library in
favor of per-function inlining, and started committing generated
schemas to git. CONTRIBUTING.md still described the old structure and
referenced non-existent files and commands.

This commit updates CONTRIBUTING.md to match what the tree actually
looks like: tests run via nix flake check (no test-crossplane app
exists), schemas/python/ is committed and regenerated via
nix run .#generate, the function layout puts Composer and compose()
in fn.py, and tests are unittest-based with no helpers module.

It also fixes a stale example in nix.sh's header comment.

Signed-off-by: Nic Cope <nicc@rk0n.org>
Each function and the schemas/python workspace member used hatchling as
its PEP 517 build backend, with hatch-version reading 0.0.0.dev0 from a
per-function __version__.py shim. The version was never surfaced
anywhere — the OCI images are tagged latest by Nix and the project tag
comes from git via the flake — so the dynamic-version machinery was
boilerplate without a purpose.

uv ships its own build backend (uv_build) that integrates with uv2nix
the same way hatchling does and matches our requirements: pure-Python
wheels, a non-default module layout (function/, not src/<name>/), and
inclusion of data files alongside the module. Switching drops 9
__version__.py shims and tightens each pyproject.toml.

uv_build follows uv's versioning policy, so the dev shell needs a
matching uv. This commit adds a nixpkgs-unstable input exposed as
pkgs.unstable and pulls uv from there to track recent uv_build
releases. The overlay is also a general escape hatch for future
packages we'd want newer than nixos-25.11 ships.

Two related additions:

- A uv-lock check fails nix flake check when uv.lock is out of sync
  with any pyproject.toml. Nothing previously caught a contributor
  editing a pyproject.toml without running uv lock; uv2nix would
  either build against the stale pin or fail with a less helpful
  error.

- nix run .#fix now runs uv lock so contributors don't need to
  remember a separate step after editing dependencies.

Signed-off-by: Nic Cope <nicc@rk0n.org>
pyright was installed in the dev shell and had a [tool.pyright] config
block in pyproject.toml, but nothing in the project actually ran it.
nix flake check, nix run .#fix, and the CI workflow all skip type
checking. The config was aspirational, not functional.

Drop both. A follow-up will reintroduce type checking with a checker
that's actually wired into CI.

Signed-off-by: Nic Cope <nicc@rk0n.org>
negz added 2 commits May 22, 2026 21:16
Signed-off-by: Nic Cope <nicc@rk0n.org>
We need it for fetching deps, not only pushing.

Signed-off-by: Nic Cope <nicc@rk0n.org>
@dennis-upbound

Copy link
Copy Markdown
Collaborator

lgtm afaict!

@negz negz merged commit 1247167 into main May 26, 2026
2 checks passed
@negz negz deleted the down branch June 16, 2026 16:56
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.

3 participants