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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,38 @@ jobs:

- run: uv run pytest -q

# `xpkg build` is not a lint: it parses every YAML under --package-root and
# rejects anything that is not a valid manifest of a kind the Configuration
# scheme knows. So a green build here is a real statement about both XRDs,
# both Compositions and all six examples.
#
# No --ignore: the dev-only Function manifests live in `dev/`, outside this
# package root, precisely so this build needs no exclusions. Putting a
# `Function` back under apis/ fails with "no kind Function is registered".
configuration:
name: configuration package
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 1

- name: Install crossplane CLI
run: |
set -euo pipefail
# renovate: datasource=github-releases depName=crossplane/crossplane
curl -sfL "https://cli.crossplane.io/install.sh" | XP_VERSION=v2.4.0 sh
sudo mv ./crossplane /usr/local/bin/crossplane

- name: Build the Configuration package
run: |
set -euo pipefail
crossplane xpkg build \
--package-root=apis \
--examples-root=examples/fabric \
-o configuration-avd-ci.xpkg

# The offline suite runs on the runner's interpreter, so it says nothing about
# the image the xpkg actually embeds. Without this job a Dockerfile, uv or
# Python bump is only exercised by the release build -- i.e. after the tag,
Expand Down
102 changes: 102 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,108 @@ jobs:
if-no-files-found: error
retention-days: 7

# The API this function serves, published as its own package. A Function
# package cannot carry XRDs or Compositions -- `xpkg build` rejects both with
# "object is not a CRD" -- so the only way to ship the avd.netclab.dev API is
# a Configuration, and it is released from here, beside the function it
# depends on and under the same version.
#
# Deliberately NOT an `xpkg-*` artifact: the publish job below globs every
# .xpkg it downloads into one multi-platform index, and this package must not
# be folded into the function's.
configuration:
name: configuration package
needs: test
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 1

- name: Install crossplane CLI
run: |
set -euo pipefail
# renovate: datasource=github-releases depName=crossplane/crossplane
curl -sfL "https://cli.crossplane.io/install.sh" | XP_VERSION=v2.4.0 sh
sudo mv ./crossplane /usr/local/bin/crossplane

- name: Build the Configuration package
run: |
set -euo pipefail
crossplane xpkg build \
--package-root=apis \
--examples-root=examples/fabric \
-o "configuration-avd-${{ needs.test.outputs.version }}.xpkg"

- uses: actions/upload-artifact@v7
with:
name: configuration-xpkg
path: ./*.xpkg
if-no-files-found: error

publish-configuration:
name: push configuration to GHCR + Upbound
needs: [test, configuration]
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
packages: write
env:
VERSION: ${{ needs.test.outputs.version }}
UP_TOKEN: ${{ secrets.UPBOUND_TOKEN }}
steps:
- uses: actions/download-artifact@v8
with:
name: configuration-xpkg

- name: Install crossplane CLI
run: |
set -euo pipefail
# renovate: datasource=github-releases depName=crossplane/crossplane
curl -sfL "https://cli.crossplane.io/install.sh" | XP_VERSION=v2.4.0 sh
sudo mv ./crossplane /usr/local/bin/crossplane

- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Push to GHCR
run: |
set -euo pipefail
# A Configuration is YAML with no runtime image, so there is nothing
# to build per architecture and this is a single-file push.
file="$(ls ./configuration-avd-*.xpkg)"
crossplane xpkg push -f "${file}" \
"ghcr.io/netclab/configuration-avd:${VERSION}"

- name: Install up CLI
if: env.UP_TOKEN != ''
run: |
set -euo pipefail
# renovate: datasource=github-releases depName=upbound/up
curl -sL "https://cli.upbound.io" | VERSION=v0.51.1 sh
sudo mv ./up /usr/local/bin/up

- name: Push to the Upbound Marketplace
if: env.UP_TOKEN != ''
run: |
set -euo pipefail
up login --token="${UP_TOKEN}"
file="$(ls ./configuration-avd-*.xpkg)"
up xpkg push -f "${file}" --create \
"xpkg.upbound.io/netclab/configuration-avd:${VERSION}"

- name: Note a skipped Upbound push
if: env.UP_TOKEN == ''
run: |
echo "::warning::UPBOUND_TOKEN is not set -- configuration pushed to" \
"GHCR only."

publish:
name: push to GHCR + Upbound
needs: [test, build]
Expand Down
35 changes: 32 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ provider's poll interval, the same rhythm that paces the rest of the model.
uv run avd-verify # pyavd vs AVD's own golden structured configs
uv run avd-verify-xr # same, through the Fabric-XR fold
crossplane render examples/fabric/single-dc-l3ls.yaml \
apis/fabric/composition.yaml apis/function/function-render.yaml
apis/fabric/composition.yaml dev/function-render.yaml
```

## How it works
Expand Down Expand Up @@ -201,12 +201,16 @@ either number. `workflow_dispatch` publishes too, taking no input — it release
version the checked-out tree declares.

The workflow reruns the offline suite, builds the runtime image for `linux/amd64` and
`linux/arm64`, embeds each into an xpkg, and pushes both as one multi-platform package:
`linux/arm64`, embeds each into an xpkg, and pushes both as one multi-platform package.
It also builds a **second package** — `configuration-avd`, the API this function serves
— and pushes it separately, under the same version:

| Destination | When |
|-------------|------|
| `ghcr.io/netclab/function-avd:<version>` | always — the workflow's `GITHUB_TOKEN` is enough |
| `ghcr.io/netclab/configuration-avd:<version>` | always |
| `xpkg.upbound.io/netclab/function-avd:<version>` | only when the `UPBOUND_TOKEN` secret is set |
| `xpkg.upbound.io/netclab/configuration-avd:<version>` | only when the `UPBOUND_TOKEN` secret is set |

The second one is what feeds
[marketplace.upbound.io/functions/netclab](https://marketplace.upbound.io/functions/netclab):
Expand All @@ -232,6 +236,30 @@ spec:
package: ghcr.io/netclab/function-avd:<version>
```

### The Configuration package

A Function package **cannot carry XRDs or Compositions** — `crossplane xpkg build`
rejects both with `object is not a CRD`, because a Function package takes only the CRDs
describing its input, and this function has none (the XRs are its API). So
`apis/fabric/` and `apis/device/` ship as their own Configuration, built from `apis/`
with `apis/crossplane.yaml` as its metadata.

Installing it pulls the function in as a dependency, so this is the one line a consumer
needs:

```yaml
apiVersion: pkg.crossplane.io/v1
kind: Configuration
metadata:
name: configuration-avd
spec:
package: ghcr.io/netclab/configuration-avd:<version>
```

Crossplane names a dependency-installed function `<org>-<name>`, so the Function object
lands as `netclab-function-avd` — which is exactly the name both Compositions reference
and the name `kind-up.sh` installs under, so the dev cluster and a real install agree.

## Gotchas

The non-obvious things this repo encodes, each of which cost a debugging session:
Expand Down Expand Up @@ -325,7 +353,8 @@ The non-obvious things this repo encodes, each of which cost a debugging session
| `function/ansible_inputs.py` | rebuild `all_inputs` from an Ansible example (inventory + group_vars merge) |
| `function/verify_example.py`, `verify_xr.py` | golden-diff harnesses (`avd-verify`, `avd-verify-xr`) |
| `apis/fabric/`, `apis/device/` | XRD + Composition for each layer |
| `apis/function/` | `Function` manifests (cluster install, and local `crossplane render`) |
| `apis/crossplane.yaml` | Configuration package metadata — `apis/` is that package's root |
| `dev/` | `Function` manifests for local use (kind install, `crossplane render`); outside `apis/` so the Configuration build needs no exclusions |
| `examples/fabric/` | example `Fabric` XRs (each reproduces golden) |
| `examples/lab/` | kustomize overlay: the same fabric as run on the netclab lab |
| `Dockerfile`, `package/crossplane.yaml` | function runtime image + package metadata |
Expand Down
54 changes: 54 additions & 0 deletions apis/crossplane.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Configuration package metadata. `apis/` is this package's root, so everything
# beside this file -- the Fabric and Device XRDs and their Compositions -- is
# what ships.
#
# Why a second package at all: a Function package cannot carry XRDs or
# Compositions. `crossplane xpkg build` rejects both with "object is not a CRD";
# a Function package takes only the CRDs describing its input, and this function
# has none. So the API this function serves has to be published as a
# Configuration, and this is the closest place to where it is developed and
# proven against real devices.
#
# The dev-only `Function` manifests live in `dev/`, outside this root, so the
# build needs no `--ignore` exclusions and adding a directory under `apis/`
# cannot silently fall out of the package.
apiVersion: meta.pkg.crossplane.io/v1
kind: Configuration
metadata:
name: configuration-avd
annotations:
meta.crossplane.io/maintainer: netclab.dev
meta.crossplane.io/source: github.com/netclab/function-avd
meta.crossplane.io/license: Apache-2.0
meta.crossplane.io/description: >-
The avd.netclab.dev API: Fabric and Device XRDs and their Compositions,
served by function-avd.
meta.crossplane.io/readme: |
The API served by
[function-avd](https://marketplace.upbound.io/functions/netclab/function-avd):
two composite kinds in the `avd.netclab.dev` group.

* **`Fabric`** — one AVD fabric. `spec.design` carries a fabric-wide
`eos_designs` document; the function runs the fabric-wide pyavd pipeline
and composes one `Device` per host.
* **`Device`** — validates and renders its own structured config and EOS
CLI, publishes them as a ConfigMap, and with `spec.push` set also
composes a provider-http `Request` that holds the box's running config
in sync over eAPI.

Installing this pulls the function in as a dependency. See the
[repository](https://github.com/netclab/function-avd) for the XRD schemas,
a runnable kind lab, and the design notes.
spec:
dependsOn:
# Installed by Crossplane as the Function object `netclab-function-avd` --
# `<org>-<name>` from the package path -- which is the name both
# Compositions here reference, and the name `scripts/kind-up.sh` uses so the
# dev cluster matches a real install.
- apiVersion: pkg.crossplane.io/v1
kind: Function
package: xpkg.upbound.io/netclab/function-avd
version: ">=v0.1.2"

crossplane:
version: ">=v2.1.1"
2 changes: 1 addition & 1 deletion apis/device/composition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ spec:
pipeline:
- step: render-device
functionRef:
name: function-avd
name: netclab-function-avd
2 changes: 1 addition & 1 deletion apis/fabric/composition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ spec:
pipeline:
- step: render-avd
functionRef:
name: function-avd
name: netclab-function-avd
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
#
# uv run avd-function --insecure --debug # serves on :9443
# crossplane render examples/fabric/single-dc-l3ls.yaml \
# apis/fabric/composition.yaml apis/function/function-render.yaml
# apis/fabric/composition.yaml dev/function-render.yaml
#
# For the real cluster install use apis/function/function.yaml instead (it
# For the real cluster install use dev/function.yaml instead (it
# references the packaged image in the registry).
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: function-avd
name: netclab-function-avd
annotations:
render.crossplane.io/runtime: Development
spec:
Expand Down
2 changes: 1 addition & 1 deletion apis/function/function.yaml → dev/function.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: function-avd
name: netclab-function-avd
spec:
# Registry reachable by BOTH the Crossplane pod and the node's containerd
# (kind-network IP of the in-cluster registry; served over plain HTTP).
Expand Down
4 changes: 2 additions & 2 deletions scripts/kind-up.sh
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,12 @@ kubectl --context "$CTX" apply -f - <<EOF
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: function-avd
name: netclab-function-avd
spec:
package: ${REG_IP}:5000/netclab/function-avd:${TAG}
packagePullPolicy: IfNotPresent
EOF
kubectl --context "$CTX" wait --for=condition=Healthy function.pkg.crossplane.io/function-avd --timeout=180s
kubectl --context "$CTX" wait --for=condition=Healthy function.pkg.crossplane.io/netclab-function-avd --timeout=180s

echo ">> install XRDs + Compositions (Fabric + Device)"
kubectl --context "$CTX" apply -f apis/fabric/xrd.yaml -f apis/device/xrd.yaml
Expand Down