Skip to content

Commit 5a2eef0

Browse files
mbakalarskiclaude
andauthored
Publish the avd.netclab.dev API as its own Configuration package (#17)
A Function package cannot carry XRDs or Compositions. `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, because the XRs are its API. So the API this function serves was publishable from nowhere: not from here, and not from a consumer without copying it. It is now a second package, built from `apis/` with `apis/crossplane.yaml` as its metadata and depending on the function. Nothing moves out of this repo, so `kind-up.sh` and both e2e suites keep exercising the real API rather than a fixture -- which is the whole reason the API stays where the lab is. Three supporting changes: - The `Function` object is renamed to `netclab-function-avd`, because that is what Crossplane calls a dependency-installed function (`<org>-<name>`). The compositions and kind-up.sh follow it, so the dev cluster now uses the same name a real install does -- previously the e2e suite exercised a name no consumer would ever have. - The dev-only Function manifests move from `apis/function/` to `dev/`. They are not API, and with them under `apis/` the Configuration build fails outright ("no kind Function is registered"). Out of the package root, the build needs no --ignore, so a future directory under `apis/` cannot silently fall out of the package. - CI builds the Configuration on every PR, and the release publishes it to GHCR and Upbound under the same version as the function. It is deliberately not an `xpkg-*` artifact: the publish job globs those into one multi-platform index, and this package must not join it. Verified: both packages build, 31 offline tests pass, and the render manifest and compositions agree on the new function name. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent f95bd71 commit 5a2eef0

9 files changed

Lines changed: 228 additions & 11 deletions

File tree

.github/workflows/ci.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,38 @@ jobs:
3636

3737
- run: uv run pytest -q
3838

39+
# `xpkg build` is not a lint: it parses every YAML under --package-root and
40+
# rejects anything that is not a valid manifest of a kind the Configuration
41+
# scheme knows. So a green build here is a real statement about both XRDs,
42+
# both Compositions and all six examples.
43+
#
44+
# No --ignore: the dev-only Function manifests live in `dev/`, outside this
45+
# package root, precisely so this build needs no exclusions. Putting a
46+
# `Function` back under apis/ fails with "no kind Function is registered".
47+
configuration:
48+
name: configuration package
49+
runs-on: ubuntu-latest
50+
timeout-minutes: 10
51+
steps:
52+
- uses: actions/checkout@v7
53+
with:
54+
fetch-depth: 1
55+
56+
- name: Install crossplane CLI
57+
run: |
58+
set -euo pipefail
59+
# renovate: datasource=github-releases depName=crossplane/crossplane
60+
curl -sfL "https://cli.crossplane.io/install.sh" | XP_VERSION=v2.4.0 sh
61+
sudo mv ./crossplane /usr/local/bin/crossplane
62+
63+
- name: Build the Configuration package
64+
run: |
65+
set -euo pipefail
66+
crossplane xpkg build \
67+
--package-root=apis \
68+
--examples-root=examples/fabric \
69+
-o configuration-avd-ci.xpkg
70+
3971
# The offline suite runs on the runner's interpreter, so it says nothing about
4072
# the image the xpkg actually embeds. Without this job a Dockerfile, uv or
4173
# Python bump is only exercised by the release build -- i.e. after the tag,

.github/workflows/release.yml

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,108 @@ jobs:
143143
if-no-files-found: error
144144
retention-days: 7
145145

146+
# The API this function serves, published as its own package. A Function
147+
# package cannot carry XRDs or Compositions -- `xpkg build` rejects both with
148+
# "object is not a CRD" -- so the only way to ship the avd.netclab.dev API is
149+
# a Configuration, and it is released from here, beside the function it
150+
# depends on and under the same version.
151+
#
152+
# Deliberately NOT an `xpkg-*` artifact: the publish job below globs every
153+
# .xpkg it downloads into one multi-platform index, and this package must not
154+
# be folded into the function's.
155+
configuration:
156+
name: configuration package
157+
needs: test
158+
runs-on: ubuntu-latest
159+
timeout-minutes: 10
160+
steps:
161+
- uses: actions/checkout@v7
162+
with:
163+
fetch-depth: 1
164+
165+
- name: Install crossplane CLI
166+
run: |
167+
set -euo pipefail
168+
# renovate: datasource=github-releases depName=crossplane/crossplane
169+
curl -sfL "https://cli.crossplane.io/install.sh" | XP_VERSION=v2.4.0 sh
170+
sudo mv ./crossplane /usr/local/bin/crossplane
171+
172+
- name: Build the Configuration package
173+
run: |
174+
set -euo pipefail
175+
crossplane xpkg build \
176+
--package-root=apis \
177+
--examples-root=examples/fabric \
178+
-o "configuration-avd-${{ needs.test.outputs.version }}.xpkg"
179+
180+
- uses: actions/upload-artifact@v7
181+
with:
182+
name: configuration-xpkg
183+
path: ./*.xpkg
184+
if-no-files-found: error
185+
186+
publish-configuration:
187+
name: push configuration to GHCR + Upbound
188+
needs: [test, configuration]
189+
runs-on: ubuntu-latest
190+
timeout-minutes: 20
191+
permissions:
192+
contents: read
193+
packages: write
194+
env:
195+
VERSION: ${{ needs.test.outputs.version }}
196+
UP_TOKEN: ${{ secrets.UPBOUND_TOKEN }}
197+
steps:
198+
- uses: actions/download-artifact@v8
199+
with:
200+
name: configuration-xpkg
201+
202+
- name: Install crossplane CLI
203+
run: |
204+
set -euo pipefail
205+
# renovate: datasource=github-releases depName=crossplane/crossplane
206+
curl -sfL "https://cli.crossplane.io/install.sh" | XP_VERSION=v2.4.0 sh
207+
sudo mv ./crossplane /usr/local/bin/crossplane
208+
209+
- name: Login to GHCR
210+
uses: docker/login-action@v4
211+
with:
212+
registry: ghcr.io
213+
username: ${{ github.actor }}
214+
password: ${{ secrets.GITHUB_TOKEN }}
215+
216+
- name: Push to GHCR
217+
run: |
218+
set -euo pipefail
219+
# A Configuration is YAML with no runtime image, so there is nothing
220+
# to build per architecture and this is a single-file push.
221+
file="$(ls ./configuration-avd-*.xpkg)"
222+
crossplane xpkg push -f "${file}" \
223+
"ghcr.io/netclab/configuration-avd:${VERSION}"
224+
225+
- name: Install up CLI
226+
if: env.UP_TOKEN != ''
227+
run: |
228+
set -euo pipefail
229+
# renovate: datasource=github-releases depName=upbound/up
230+
curl -sL "https://cli.upbound.io" | VERSION=v0.51.1 sh
231+
sudo mv ./up /usr/local/bin/up
232+
233+
- name: Push to the Upbound Marketplace
234+
if: env.UP_TOKEN != ''
235+
run: |
236+
set -euo pipefail
237+
up login --token="${UP_TOKEN}"
238+
file="$(ls ./configuration-avd-*.xpkg)"
239+
up xpkg push -f "${file}" --create \
240+
"xpkg.upbound.io/netclab/configuration-avd:${VERSION}"
241+
242+
- name: Note a skipped Upbound push
243+
if: env.UP_TOKEN == ''
244+
run: |
245+
echo "::warning::UPBOUND_TOKEN is not set -- configuration pushed to" \
246+
"GHCR only."
247+
146248
publish:
147249
name: push to GHCR + Upbound
148250
needs: [test, build]

README.md

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ provider's poll interval, the same rhythm that paces the rest of the model.
7878
uv run avd-verify # pyavd vs AVD's own golden structured configs
7979
uv run avd-verify-xr # same, through the Fabric-XR fold
8080
crossplane render examples/fabric/single-dc-l3ls.yaml \
81-
apis/fabric/composition.yaml apis/function/function-render.yaml
81+
apis/fabric/composition.yaml dev/function-render.yaml
8282
```
8383

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

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

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

211215
The second one is what feeds
212216
[marketplace.upbound.io/functions/netclab](https://marketplace.upbound.io/functions/netclab):
@@ -232,6 +236,30 @@ spec:
232236
package: ghcr.io/netclab/function-avd:<version>
233237
```
234238
239+
### The Configuration package
240+
241+
A Function package **cannot carry XRDs or Compositions** — `crossplane xpkg build`
242+
rejects both with `object is not a CRD`, because a Function package takes only the CRDs
243+
describing its input, and this function has none (the XRs are its API). So
244+
`apis/fabric/` and `apis/device/` ship as their own Configuration, built from `apis/`
245+
with `apis/crossplane.yaml` as its metadata.
246+
247+
Installing it pulls the function in as a dependency, so this is the one line a consumer
248+
needs:
249+
250+
```yaml
251+
apiVersion: pkg.crossplane.io/v1
252+
kind: Configuration
253+
metadata:
254+
name: configuration-avd
255+
spec:
256+
package: ghcr.io/netclab/configuration-avd:<version>
257+
```
258+
259+
Crossplane names a dependency-installed function `<org>-<name>`, so the Function object
260+
lands as `netclab-function-avd` — which is exactly the name both Compositions reference
261+
and the name `kind-up.sh` installs under, so the dev cluster and a real install agree.
262+
235263
## Gotchas
236264

237265
The non-obvious things this repo encodes, each of which cost a debugging session:
@@ -325,7 +353,8 @@ The non-obvious things this repo encodes, each of which cost a debugging session
325353
| `function/ansible_inputs.py` | rebuild `all_inputs` from an Ansible example (inventory + group_vars merge) |
326354
| `function/verify_example.py`, `verify_xr.py` | golden-diff harnesses (`avd-verify`, `avd-verify-xr`) |
327355
| `apis/fabric/`, `apis/device/` | XRD + Composition for each layer |
328-
| `apis/function/` | `Function` manifests (cluster install, and local `crossplane render`) |
356+
| `apis/crossplane.yaml` | Configuration package metadata — `apis/` is that package's root |
357+
| `dev/` | `Function` manifests for local use (kind install, `crossplane render`); outside `apis/` so the Configuration build needs no exclusions |
329358
| `examples/fabric/` | example `Fabric` XRs (each reproduces golden) |
330359
| `examples/lab/` | kustomize overlay: the same fabric as run on the netclab lab |
331360
| `Dockerfile`, `package/crossplane.yaml` | function runtime image + package metadata |

apis/crossplane.yaml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Configuration package metadata. `apis/` is this package's root, so everything
2+
# beside this file -- the Fabric and Device XRDs and their Compositions -- is
3+
# what ships.
4+
#
5+
# Why a second package at all: a Function package cannot carry XRDs or
6+
# Compositions. `crossplane xpkg build` rejects both with "object is not a CRD";
7+
# a Function package takes only the CRDs describing its input, and this function
8+
# has none. So the API this function serves has to be published as a
9+
# Configuration, and this is the closest place to where it is developed and
10+
# proven against real devices.
11+
#
12+
# The dev-only `Function` manifests live in `dev/`, outside this root, so the
13+
# build needs no `--ignore` exclusions and adding a directory under `apis/`
14+
# cannot silently fall out of the package.
15+
apiVersion: meta.pkg.crossplane.io/v1
16+
kind: Configuration
17+
metadata:
18+
name: configuration-avd
19+
annotations:
20+
meta.crossplane.io/maintainer: netclab.dev
21+
meta.crossplane.io/source: github.com/netclab/function-avd
22+
meta.crossplane.io/license: Apache-2.0
23+
meta.crossplane.io/description: >-
24+
The avd.netclab.dev API: Fabric and Device XRDs and their Compositions,
25+
served by function-avd.
26+
meta.crossplane.io/readme: |
27+
The API served by
28+
[function-avd](https://marketplace.upbound.io/functions/netclab/function-avd):
29+
two composite kinds in the `avd.netclab.dev` group.
30+
31+
* **`Fabric`** — one AVD fabric. `spec.design` carries a fabric-wide
32+
`eos_designs` document; the function runs the fabric-wide pyavd pipeline
33+
and composes one `Device` per host.
34+
* **`Device`** — validates and renders its own structured config and EOS
35+
CLI, publishes them as a ConfigMap, and with `spec.push` set also
36+
composes a provider-http `Request` that holds the box's running config
37+
in sync over eAPI.
38+
39+
Installing this pulls the function in as a dependency. See the
40+
[repository](https://github.com/netclab/function-avd) for the XRD schemas,
41+
a runnable kind lab, and the design notes.
42+
spec:
43+
dependsOn:
44+
# Installed by Crossplane as the Function object `netclab-function-avd` --
45+
# `<org>-<name>` from the package path -- which is the name both
46+
# Compositions here reference, and the name `scripts/kind-up.sh` uses so the
47+
# dev cluster matches a real install.
48+
- apiVersion: pkg.crossplane.io/v1
49+
kind: Function
50+
package: xpkg.upbound.io/netclab/function-avd
51+
version: ">=v0.1.2"
52+
53+
crossplane:
54+
version: ">=v2.1.1"

apis/device/composition.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ spec:
1414
pipeline:
1515
- step: render-device
1616
functionRef:
17-
name: function-avd
17+
name: netclab-function-avd

apis/fabric/composition.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ spec:
1212
pipeline:
1313
- step: render-avd
1414
functionRef:
15-
name: function-avd
15+
name: netclab-function-avd
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
#
66
# uv run avd-function --insecure --debug # serves on :9443
77
# crossplane render examples/fabric/single-dc-l3ls.yaml \
8-
# apis/fabric/composition.yaml apis/function/function-render.yaml
8+
# apis/fabric/composition.yaml dev/function-render.yaml
99
#
10-
# For the real cluster install use apis/function/function.yaml instead (it
10+
# For the real cluster install use dev/function.yaml instead (it
1111
# references the packaged image in the registry).
1212
apiVersion: pkg.crossplane.io/v1
1313
kind: Function
1414
metadata:
15-
name: function-avd
15+
name: netclab-function-avd
1616
annotations:
1717
render.crossplane.io/runtime: Development
1818
spec:
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
apiVersion: pkg.crossplane.io/v1
22
kind: Function
33
metadata:
4-
name: function-avd
4+
name: netclab-function-avd
55
spec:
66
# Registry reachable by BOTH the Crossplane pod and the node's containerd
77
# (kind-network IP of the in-cluster registry; served over plain HTTP).

scripts/kind-up.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,12 @@ kubectl --context "$CTX" apply -f - <<EOF
103103
apiVersion: pkg.crossplane.io/v1
104104
kind: Function
105105
metadata:
106-
name: function-avd
106+
name: netclab-function-avd
107107
spec:
108108
package: ${REG_IP}:5000/netclab/function-avd:${TAG}
109109
packagePullPolicy: IfNotPresent
110110
EOF
111-
kubectl --context "$CTX" wait --for=condition=Healthy function.pkg.crossplane.io/function-avd --timeout=180s
111+
kubectl --context "$CTX" wait --for=condition=Healthy function.pkg.crossplane.io/netclab-function-avd --timeout=180s
112112

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

0 commit comments

Comments
 (0)