Assemble a fabric from input objects, and migrate them by asking Ansible #71
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
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| test: | |
| name: offline suite | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| # The AVD examples and their golden structured configs -- what the | |
| # suite diffs against -- live in the submodule, so without it every | |
| # fidelity test collects nothing. | |
| submodules: true | |
| fetch-depth: 1 | |
| # Full version, not @v9: setup-uv publishes floating major aliases only up | |
| # to v7, so @v9 does not resolve. Renovate tracks the exact tag instead. | |
| - uses: astral-sh/setup-uv@v9.0.0 | |
| with: | |
| enable-cache: true | |
| # --locked fails if uv.lock drifts from pyproject.toml, so a dependency | |
| # bump can't land without its lock. | |
| - run: uv sync --locked | |
| - 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 Function package's own build, minus the runtime image -- embedding | |
| # needs a built tarball, and nothing here depends on it to answer the | |
| # question this step asks: does `package/` plus the examples still parse? | |
| # | |
| # Added after the v0.1.4 release failed on exactly this. `--examples-root` | |
| # defaults to ./examples, so this build collected a helm values file that | |
| # had just landed under examples/ and rejected it -- a failure reachable | |
| # only from the release, i.e. after the tag. The configuration build above | |
| # could not have caught it: it names a narrower examples root, so the two | |
| # builds disagreed about what "the examples" are. | |
| - name: Build the Function package | |
| run: | | |
| set -euo pipefail | |
| crossplane xpkg build \ | |
| --package-root=package \ | |
| --examples-root=examples/fabric \ | |
| -o function-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, | |
| # when the cheap moment to react has passed. | |
| image: | |
| name: runtime image | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| # No submodules: .dockerignore keeps avd/ out of the image anyway. | |
| fetch-depth: 1 | |
| - uses: docker/setup-buildx-action@v4 | |
| # amd64 only. The release also builds arm64, but that leg is emulated and | |
| # slow, and a Dockerfile broken enough to matter breaks on both. | |
| - name: Build the runtime image | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| load: true | |
| tags: function-avd-runtime:ci | |
| # xpkg wants a plain image; kind-up.sh passes --provenance=false too. | |
| provenance: false | |
| # Its own cache scope: a PR build must not evict the release's, which | |
| # is the one that has to carry an emulated arm64 build. | |
| cache-from: type=gha,scope=ci-amd64 | |
| cache-to: type=gha,mode=max,scope=ci-amd64 | |
| # A build that succeeds is the weaker claim. An image can install cleanly | |
| # and still fail to import pyavd or to serve, and neither shows up until | |
| # something pulls the published package. | |
| - name: Smoke-test the image | |
| run: | | |
| set -euo pipefail | |
| docker run --rm -i --entrypoint /app/.venv/bin/python function-avd-runtime:ci - <<'PY' | |
| import sys, pyavd | |
| from function import fn | |
| print("python", sys.version.split()[0], "/ pyavd", pyavd.__version__) | |
| assert hasattr(fn, "FunctionRunner"), "function package did not import" | |
| # Exercise the schema machinery, not just the import: a bad interpreter | |
| # or a half-installed dependency surfaces here, not at import time. | |
| pyavd.validate_inputs({"hostname": "smoke", "fabric_name": "smoke"}) | |
| print("pyavd validate_inputs OK") | |
| PY | |
| # And that the entrypoint actually serves, which is all Crossplane | |
| # ever asks of this image. | |
| docker run -d --name smoke function-avd-runtime:ci --insecure >/dev/null | |
| for _ in $(seq 30); do | |
| if docker exec smoke /app/.venv/bin/python \ | |
| -c "import socket; socket.create_connection(('127.0.0.1', 9443), 1).close()" 2>/dev/null; then | |
| served=1; break | |
| fi | |
| sleep 1 | |
| done | |
| docker logs smoke | |
| docker rm -f smoke >/dev/null | |
| if [ "${served:-0}" != 1 ]; then | |
| echo "::error::the entrypoint never accepted a connection on 9443" | |
| exit 1 | |
| fi | |
| echo "entrypoint served on 9443" | |
| e2e: | |
| name: e2e on kind | |
| # Builds the function image and installs Crossplane, so it is far too heavy | |
| # to gate a PR: run it by hand from the Actions tab when the function, | |
| # the compositions, or the XRDs change. | |
| if: github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| submodules: true | |
| fetch-depth: 1 | |
| - uses: astral-sh/setup-uv@v9.0.0 | |
| with: | |
| enable-cache: true | |
| - run: uv sync --locked | |
| - name: Install kind + crossplane CLI | |
| run: | | |
| set -euo pipefail | |
| # Matches the version the e2e scenario was proven against locally. | |
| # renovate: datasource=github-releases depName=kubernetes-sigs/kind | |
| curl -sSLo ./kind https://kind.sigs.k8s.io/dl/v0.32.0/kind-linux-amd64 | |
| chmod +x ./kind | |
| sudo mv ./kind /usr/local/bin/kind | |
| # XP_VERSION pins the CLI: install.sh otherwise takes whatever is | |
| # current, so this job would drift onto a different CLI with no commit. | |
| # 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 | |
| # kind-up.sh needs helm; the runner image ships it, but don't rely on that. | |
| command -v helm >/dev/null || \ | |
| curl -sSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash | |
| kind --version | |
| # --client only: plain `crossplane version` talks to the cluster, which | |
| # does not exist yet, and would fail this step under `set -e`. | |
| crossplane version --client | |
| - name: Bring up the cluster | |
| run: scripts/kind-up.sh | |
| - name: Apply a fabric | |
| run: | | |
| kubectl --context kind-avd apply -f examples/fabric/single-dc-l3ls.yaml | |
| kubectl --context kind-avd -n default wait --for=condition=Ready \ | |
| fabric/single-dc-l3ls --timeout=300s | |
| # -s: let the [phase] timings through. pytest swallows stdout on a pass, | |
| # which is exactly the run where you want to see how much headroom is left. | |
| - run: uv run pytest -m e2e -v -s | |
| - name: Dump state on failure | |
| if: failure() | |
| run: | | |
| kubectl --context kind-avd -n default get fabric,device,cm -o wide || true | |
| kubectl --context kind-avd -n default describe device || true | |
| kubectl --context kind-avd -n crossplane-system logs -l app=crossplane --tail=200 || true |