Conversation
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>
This was referenced May 22, 2026
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>
Collaborator
|
lgtm afaict! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
upCLI for building, testing, and pushing function packages since the project started. The tooling thatupprovides 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
upforcrossplane.upbound.yamlbecomescrossplane-project.yamlWe keepdocker-credential-upbecause we still need it forxpkg.upbound.ioregistry auth.Limiting the Crossplane CLI to schemas and packaging
The
crossplaneCLI (andup) 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.upbuilt 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 theupCLI) to drift.To that end I've opened crossplane/cli#24, and pinned Modelplane on that PR. The PR does two things:
crossplanenot to build function runtimes (i.e. function OCI images). You can instead use your own build tool to roll your own.crossplaneto only generate schemas for the languages you use.The result is that you can choose to use the
crossplaneCLI 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, sinceuv2nixseems to be the modern and minimal way to do Python builds with Nix.