Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
38 changes: 5 additions & 33 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ permissions:
contents: read

jobs:
lint:
check:
runs-on: ubuntu-24.04

steps:
Expand All @@ -26,50 +26,22 @@ jobs:
- name: Install Nix
uses: cachix/install-nix-action@v31

- name: Lint
run: >
nix build
.#checks.x86_64-linux.python
.#checks.x86_64-linux.shell-lint
.#checks.x86_64-linux.nix-lint
--print-build-logs

composition-tests:
runs-on: ubuntu-24.04

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Nix
uses: cachix/install-nix-action@v31

- name: Build and Test
run: nix run .#test-crossplane --print-build-logs
- name: Check
run: nix flake check --print-build-logs

push-package:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [lint, composition-tests]
needs: [check]
runs-on: ubuntu-24.04

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Needed for git rev-list --count.

- name: Install Nix
uses: cachix/install-nix-action@v31

# action-up handles robot token auth for both the API and the
# registry. docker/login-action is also needed because up project
# push uses Docker's credential store for OCI pushes.
- name: Login to Upbound
uses: upbound/action-up@v1
with:
api-token: ${{ secrets.UP_ROBOT_TOKEN }}
organization: modelplane

# The Crossplane CLI uses Docker's credential store for OCI pushes.
- name: Login to registry
uses: docker/login-action@v3
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.venv
.venv-*
_output
.up
result
Expand Down
105 changes: 54 additions & 51 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
Modelplane uses [Nix](https://nixos.org) for builds, checks, and the
development environment. If you have Nix installed, `nix develop` (or `direnv
allow` if you use [direnv](https://direnv.net/)) drops you into a shell with
everything you need: `up`, `kubectl`, `helm`, `kind`, Python, Go, Node.js,
linters, and formatters.
everything you need: `crossplane`, `kubectl`, `helm`, `kind`, Python, linters,
and formatters.

If you don't have Nix installed, [`nix.sh`](nix.sh) runs any Nix command inside
a Docker container. The first run downloads dependencies into a Docker volume.
Expand All @@ -22,74 +22,77 @@ nix develop

## Running checks

`nix flake check` runs all of the project's checks (tests, linters, formatters)
inside the Nix sandbox. Run `nix flake show` to see what's available.
`nix flake check` runs all of the project's checks — Python, shell, and Nix
linters and formatters, plus unit tests for every composition function —
inside the Nix sandbox. Run `nix flake show` to see what else is available.

```bash
nix flake check # or: ./nix.sh flake check
```

Composition tests run separately because they need Docker (the function-python
runtime runs in a container). These build the Crossplane project with `up project
build`, then run every test in `tests/`:

```bash
nix run .#test-crossplane # or: ./nix.sh run .#test-crossplane
```

Run both before opening a PR.
`nix run .#fix` auto-fixes most lint and formatting issues. Run it before
opening a PR.

## Working on composition functions

Modelplane is a [Crossplane](https://crossplane.io/) project. The core logic
lives in Python composition functions under `functions/`. See
[`skills/crossplane-python-functions/SKILL.md`](skills/crossplane-python-functions/SKILL.md)
for a detailed guide covering the function contract, build cycle, import paths,
resource composition, readiness tracking, gating, deletion ordering, and
debugging. There's also a complete
[example function](skills/crossplane-python-functions/references/example.py).

The short version: each function has a `main.py` that exports a `compose(req,
rsp)` function. It reads the XR from the request, composes resources into the
response, and tracks readiness. Functions use generated Pydantic models (under
`.up/`) for type-safe access to XR specs and status, and shared utilities from
`lib/`.

Build before you code. The Pydantic models are generated by `up project build`
(or `nix run .#build-crossplane`), so you need to build after changing an XRD
and before writing function code that imports the models.

The shared library in `lib/` provides helpers for common patterns: Helm release
builders, Kubernetes object wrappers, condition management, secret handling, and
serving profile matching. If you find yourself duplicating logic across
functions, it probably belongs in `lib/`.
for a detailed guide.

### Tests
Each function is a self-contained Python package, built as a hatch project and
managed in the workspace `uv.lock`:

Every function has at least one corresponding test in `tests/`. Tests are
Crossplane composition tests: they run a function against a mock XR and assert
the composed resources.
```
functions/<name>/
pyproject.toml # Hatch package metadata; declares SDK and models deps
function/
__init__.py
__version__.py
main.py # CLI entrypoint (boilerplate)
fn.py # FunctionRunner gRPC service and Composer logic
tests/
test_fn.py # unittest-based tests for fn.py
```

Each test directory has:
The `Composer.compose()` method in `fn.py` reads the XR from the request,
composes resources into the response, and tracks readiness. `FunctionRunner`
is the gRPC service that wires `Composer` to the SDK's runtime. Functions use
generated Pydantic models (in `schemas/python/`) for type-safe access to XR
specs and status.

- `main.py` defining the test (XR path, XRD path, composition path, extra
resources, and assertions)
- `xr.yaml` with the mock XR
Each function is self-contained — there is no shared library. Common patterns
like setting conditions, updating status, and building child resource names
are provided by the
[Crossplane Python Function SDK](https://github.com/crossplane/function-sdk-python).
Helpers specific to a single function live in that function's `function/`
package alongside `fn.py`.

The Pydantic models in `schemas/python/` are generated from the XRDs under
`apis/` and the project's dependency CRDs. They're committed to git so tests
and type checking don't need to run the Crossplane CLI first. Regenerate them
after changing an XRD or bumping a dependency:

```bash
nix run .#generate
```

### Tests

Tests use `model_to_dict()` for assertions (what the output should look like) and
`model_to_fixture()` for extra resources (what the function reads as input). Both
are in `lib/resource.py`.
Every function has tests under `functions/<name>/tests/test_fn.py`. Tests are
`unittest.IsolatedAsyncioTestCase` cases that build a typed `RunFunctionRequest`
from generated Pydantic models, call `FunctionRunner.RunFunction()`, and
compare the resulting `RunFunctionResponse` against an expected response via
`json_format.MessageToDict`.

To add a test, create a new directory under `tests/`, write an `xr.yaml`, and
write a `main.py` following the pattern in an existing test. Then run
`nix run .#test-crossplane` to verify it passes.
Add new cases to the function's existing `test_fn.py`. Run `nix flake check`
to verify they pass.

## Submitting changes

Sign off your commits using `git commit -s`. This adds a `Signed-off-by` line
certifying you have the right to submit the code under the project's license (the
[Developer Certificate of Origin](https://developercertificate.org/)).
certifying you have the right to submit the code under the project's license
(the [Developer Certificate of Origin](https://developercertificate.org/)).

Before opening a PR, run `nix flake check` and `nix run .#test-crossplane` and
make sure both pass. If you changed a composition function, make sure there's a
test covering the change.
Before opening a PR, run `nix flake check` and make sure it passes. If you
changed a composition function, make sure there's a test covering the change.
85 changes: 85 additions & 0 deletions crossplane-project.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
apiVersion: dev.crossplane.io/v1alpha1
kind: Project
metadata:
name: modelplane
spec:
repository: xpkg.upbound.io/modelplane/modelplane
source: github.com/modelplaneai/modelplane
license: Apache-2.0
description: An open-source AI inference platform built on Crossplane.
architectures: [amd64, arm64]
schemas:
languages: [python]
functions:
- source: Tarball
tarball:
name: compose-gke-cluster
pathPrefix: _output/functions/compose-gke-cluster
- source: Tarball
tarball:
name: compose-inference-class
pathPrefix: _output/functions/compose-inference-class
- source: Tarball
tarball:
name: compose-inference-cluster
pathPrefix: _output/functions/compose-inference-cluster
- source: Tarball
tarball:
name: compose-inference-gateway
pathPrefix: _output/functions/compose-inference-gateway
- source: Tarball
tarball:
name: compose-kserve-backend
pathPrefix: _output/functions/compose-kserve-backend
- source: Tarball
tarball:
name: compose-model-deployment
pathPrefix: _output/functions/compose-model-deployment
- source: Tarball
tarball:
name: compose-model-endpoint
pathPrefix: _output/functions/compose-model-endpoint
- source: Tarball
tarball:
name: compose-model-replica
pathPrefix: _output/functions/compose-model-replica
- source: Tarball
tarball:
name: compose-model-service
pathPrefix: _output/functions/compose-model-service
dependencies:
- type: crd
git:
repository: https://github.com/crossplane/crossplane
ref: release-2.2
path: cluster/crds
- type: xpkg
xpkg:
apiVersion: pkg.crossplane.io/v1
kind: Provider
package: xpkg.upbound.io/upbound/provider-gcp-container
version: v2.5.0
- type: xpkg
xpkg:
apiVersion: pkg.crossplane.io/v1
kind: Provider
package: xpkg.upbound.io/upbound/provider-gcp-compute
version: v2.5.0
- type: xpkg
xpkg:
apiVersion: pkg.crossplane.io/v1
kind: Provider
package: xpkg.upbound.io/upbound/provider-gcp-cloudplatform
version: v2.5.0
- type: xpkg
xpkg:
apiVersion: pkg.crossplane.io/v1
kind: Provider
package: xpkg.upbound.io/upbound/provider-helm
version: ">=v1.2.0"
- type: xpkg
xpkg:
apiVersion: pkg.crossplane.io/v1
kind: Provider
package: xpkg.upbound.io/upbound/provider-kubernetes
version: ">=v1.2.0"
20 changes: 10 additions & 10 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ You need the following tools installed:
- [kind](https://kind.sigs.k8s.io/)
- [kubectl](https://kubernetes.io/docs/tasks/tools/)
- [Helm](https://helm.sh/docs/intro/install/)
- The [Upbound CLI](https://docs.upbound.io/reference/cli/) (`up`), used to
create a pull secret for the Modelplane package registry.
- [Docker](https://www.docker.com/) (or a compatible credential helper) for
registry authentication.

You also need:

Expand Down Expand Up @@ -146,16 +146,16 @@ EOF

Modelplane is packaged as a Crossplane
[Configuration](https://docs.crossplane.io/latest/concepts/packages/#configuration-packages).
The package registry requires authentication. Create a pull secret using the
[Upbound CLI](https://docs.upbound.io/reference/cli/), then install the
Configuration. This pulls the providers and composition functions it depends on.

`up ctp pull-secret create` uses the credentials of the currently active `up`
profile. Make sure you're logged in as a user account with access to the
`modelplane` organization (run `up login` if not).
The package registry requires authentication. Create a pull secret, then install
the Configuration. This pulls the providers and composition functions it depends
on.

```bash
up ctp pull-secret create -n crossplane-system upbound-pull-secret --organization modelplane
kubectl create secret docker-registry upbound-pull-secret \
--docker-server=xpkg.upbound.io \
--docker-username='<robot-id>' \
--docker-password='<robot-token>' \
-n crossplane-system
```

```bash
Expand Down
12 changes: 6 additions & 6 deletions docs/pull-secret.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Pull secret for xpkg.upbound.io/modelplane/ packages.
#
# The package registry requires authentication. Generate this secret using the
# Upbound CLI:
# The package registry requires authentication. Create a pull secret:
#
# up ctp pull-secret create -n crossplane-system upbound-pull-secret --organization modelplane
#
# This creates a Kubernetes Secret with a 30-day session credential that grants
# read access to the modelplane organization's OCI packages.
# kubectl create secret docker-registry upbound-pull-secret \
# --docker-server=xpkg.upbound.io \
# --docker-username='<robot-id>' \
# --docker-password='<robot-token>' \
# -n crossplane-system
#
# Do not commit the generated secret to the repository.
Loading
Loading