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
61 changes: 61 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: E2E

# A real single-node cluster on a GitHub-hosted runner (nested KVM), built from
# this commit and created through the local action.
on:
schedule:
- cron: "17 3 * * *"
workflow_dispatch:

permissions:
contents: read

jobs:
cluster:
runs-on: ubuntu-latest
timeout-minutes: 75
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
- run: make build

- id: proxbase
uses: ./
with:
binary: bin/proxbase
name: e2e
nodes: "1"

- name: Health check
run: proxbase status e2e --check

- name: API call with the exported token
env:
ENDPOINT: ${{ steps.proxbase.outputs.endpoint }}
CA: ${{ steps.proxbase.outputs.ca-cert }}
TOKEN: ${{ steps.proxbase.outputs.api-token }}
ENV_FILE: ${{ steps.proxbase.outputs.env-file }}
run: |
curl -fsS --cacert "$CA" -H "Authorization: PVEAPIToken=$TOKEN" "${ENDPOINT}api2/json/version" | jq .
# the env file works too
# shellcheck source=/dev/null
. "$ENV_FILE"
curl -fsS --cacert "$PROXBASE_CA_CERT" -H "Authorization: PVEAPIToken=$PROXMOX_VE_API_TOKEN" "${PROXMOX_VE_ENDPOINT}api2/json/nodes" | jq -e '.data | length == 1'

- name: MCP server (status, exec, env, stop, snapshot, restore, start)
env:
PROXBASE_BIN: ${{ github.workspace }}/bin/proxbase
PROXBASE_E2E_CLUSTER: e2e
run: go test -tags e2e -run TestE2E -timeout 30m -v ./internal/mcpserver

- name: Logs
if: failure()
run: tail -n 100 ~/.local/share/proxbase/clusters/e2e/logs/*.log || true

- name: Destroy cluster
if: always()
run: proxbase destroy e2e --yes || true
6 changes: 4 additions & 2 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
name: Pages

# Publishes the JSON schema at https://proxbase.virtbase.com/schema/.
# Publishes the JSON schema at https://proxbase.virtbase.com/schema/ and the MCP
# registry entry at https://proxbase.virtbase.com/server.json.
on:
push:
branches: [main]
paths: [schema/**, .github/workflows/pages.yml]
paths: [schema/**, server.json, .github/workflows/pages.yml]
workflow_dispatch:

permissions: {}
Expand All @@ -30,6 +31,7 @@ jobs:
run: |
mkdir -p _site
cp -r schema _site/schema
cp server.json _site/server.json
echo proxbase.virtbase.com > _site/CNAME
printf '<!doctype html><meta charset="utf-8"><title>Proxbase</title><meta http-equiv="refresh" content="0; url=https://github.com/virtbase/proxbase">\n' > _site/index.html
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,21 @@ jobs:
- uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-checksums: dist/checksums.txt

# Moves the major tag (v0, v1, ...) used by `uses: virtbase/proxbase@v0`.
action-tag:
needs: [release-please, goreleaser]
if: ${{ !cancelled() && needs.goreleaser.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: write
env:
RELEASE_TAG: ${{ inputs.tag || needs.release-please.outputs.tag }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.RELEASE_TAG }}
- run: |
major="${RELEASE_TAG%%.*}"
git tag -f "$major" "$RELEASE_TAG^{commit}"
git push -f origin "refs/tags/$major"
1 change: 1 addition & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ dockers_v2:
org.opencontainers.image.version: "{{ .Version }}"
org.opencontainers.image.revision: "{{ .FullCommit }}"
org.opencontainers.image.licenses: Apache-2.0
io.modelcontextprotocol.server.name: io.github.virtbase/proxbase # MCP registry ownership, see server.json

docker_signs:
- cmd: cosign
Expand Down
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ pve3 running online 10.10.10.13 https://127.0.0.1:18003 127.0.0.1:18103
- **Fault injection:** power loss, hung nodes, pulled cables, network partitions,
latency and loss, to test HA, fencing and Ceph recovery.
- **Ready for automation:** API token and CA export, `proxbase env` for shells, JSON
and the bpg/proxmox Terraform provider, `-o json` for scripts.
and the bpg/proxmox Terraform provider, `-o json` and `--progress json` for scripts,
and a [GitHub Action](docs/ci.md) for CI.
- **For AI agents:** `proxbase mcp` is an [MCP server](docs/mcp.md): agents create
clusters, run commands on nodes, snapshot and inject faults through tools.
- **Fast:** `--golden` clones nodes from a cached base image: a 3-node cluster in
about 1.5 minutes, with a unique identity per node.
- **Declarative:** one YAML file with a JSON schema; every flag overrides a field.
Expand Down Expand Up @@ -77,6 +80,18 @@ proxbase snapshot restore lab base && proxbase start lab
proxbase env lab --format terraform # provider block for bpg/proxmox
proxbase fault partition lab pve3 # isolate a node, then: fault clear
proxbase status lab --check # exit code for CI and health checks
claude mcp add proxbase -- proxbase mcp # let an AI agent drive proxbase
```

In GitHub Actions:

```yaml
- id: pve
uses: virtbase/proxbase@v0 # KVM, QEMU, cache, create; outputs endpoint, api-token, env-file
- env: {PVE_ENV: "${{ steps.pve.outputs.env-file }}"}
run: . "$PVE_ENV" && go test ./... -tags integration
- if: always()
run: proxbase destroy ci --yes
```

A cluster file looks like this (everything has a default):
Expand All @@ -103,13 +118,14 @@ storage:
| [Configuration](docs/configuration.md) | Every field, default and limit |
| [Networking](docs/networking.md) · [Storage](docs/storage.md) · [Lifecycle](docs/lifecycle.md) | How it works and what to expect |
| [Fault injection](docs/fault-injection.md) · [Golden images](docs/golden-images.md) | Breaking the cluster on purpose, faster creates |
| [CI and automation](docs/ci.md) · [MCP server](docs/mcp.md) | GitHub Action, JSON progress, AI agents |
| [Docker](docs/docker.md) · [CLI reference](docs/cli.md) · [Troubleshooting](docs/troubleshooting.md) | |
| [Architecture](docs/architecture.md) | Packages, create flow, state on disk |
| [Examples](examples/) | Ready-to-use cluster files for common setups |

## Status and limits

Proxbase is young; the file format may change before v0.1.0. It is tested with
Proxbase is young; the file format may change before v1.0. It is tested with
Proxmox VE 9.2 on Debian 13 (QEMU 10.0, AMD). Clusters need a Linux host with KVM:
macOS builds are experimental and Docker Desktop has no `/dev/kvm`. Nodes are
reachable from the host through forwarded ports on `127.0.0.1`; host-reachable bridge
Expand Down
185 changes: 185 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
name: Proxbase
description: Create a Proxmox VE cluster in QEMU/KVM on the runner for integration tests
author: virtbase
branding:
icon: server
color: orange

inputs:
version:
description: proxbase release to install, e.g. v0.2.0, or "latest"
default: latest
binary:
description: Path to a proxbase binary to use instead of downloading a release
default: ""
name:
description: Cluster name
default: ci
config:
description: Cluster file (YAML); the inputs below override it
default: ""
nodes:
description: Number of nodes
default: "1"
memory:
description: Memory per node, e.g. 4G
default: ""
data-disks:
description: Data disks per node, e.g. 2x32G or none
default: ""
storage:
description: Storage on the data disks (zfs, ceph or none)
default: ""
pve-version:
description: Proxmox VE ISO version, e.g. 9.2
default: ""
golden:
description: Clone nodes from a cached base image (faster with cache)
default: "true"
cache:
description: Cache the ISO and base image with actions/cache
default: "true"
verify-signature:
description: Also verify the release signature with cosign (cosign must be installed)
default: "false"
github-token:
description: Token for the GitHub API (release lookup)
default: ${{ github.token }}

outputs:
cluster:
description: Cluster name
value: ${{ inputs.name }}
endpoint:
description: API endpoint, e.g. https://127.0.0.1:18001/
value: ${{ steps.env.outputs.endpoint }}
api-token:
description: API token (user@realm!id=secret)
value: ${{ steps.env.outputs.api-token }}
ca-cert:
description: Path of the cluster CA certificate
value: ${{ steps.env.outputs.ca-cert }}
ssh-key:
description: Path of the root SSH private key
value: ${{ steps.env.outputs.ssh-key }}
env-file:
description: Shell file exporting PROXMOX_VE_* and PROXBASE_* variables (source it)
value: ${{ steps.env.outputs.env-file }}

runs:
using: composite
steps:
- name: Enable KVM
shell: bash
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm.rules >/dev/null
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
test -w /dev/kvm || { echo "::error::/dev/kvm is not available on this runner"; exit 1; }

- name: Install QEMU
shell: bash
run: |
if ! command -v qemu-system-x86_64 >/dev/null || ! command -v qemu-img >/dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq --no-install-recommends qemu-system-x86 qemu-utils >/dev/null
fi

- name: Install proxbase
id: install
shell: bash
env:
VERSION: ${{ inputs.version }}
BINARY: ${{ inputs.binary }}
VERIFY: ${{ inputs.verify-signature }}
GH_TOKEN: ${{ inputs.github-token }}
run: |
set -euo pipefail
bindir="$RUNNER_TEMP/proxbase-bin"
mkdir -p "$bindir"
if [ -n "$BINARY" ]; then
install -m 0755 "$BINARY" "$bindir/proxbase"
else
if [ "$VERSION" = latest ]; then
VERSION=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" https://api.github.com/repos/virtbase/proxbase/releases/latest | jq -r .tag_name)
fi
arch=$(uname -m); case "$arch" in x86_64) arch=amd64 ;; aarch64) arch=arm64 ;; esac
base="https://github.com/virtbase/proxbase/releases/download/$VERSION"
archive="proxbase_${VERSION#v}_linux_${arch}.tar.gz"
cd "$RUNNER_TEMP"
curl -fsSLO "$base/$archive"
curl -fsSLO "$base/checksums.txt"
sha256sum --ignore-missing -c checksums.txt
if [ "$VERIFY" = true ]; then
curl -fsSLO "$base/checksums.txt.sigstore.json"
cosign verify-blob --bundle checksums.txt.sigstore.json \
--certificate-identity-regexp '^https://github.com/virtbase/proxbase/\.github/workflows/release\.yml@' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com checksums.txt
fi
tar -xzf "$archive" -C "$bindir" proxbase
fi
echo "$bindir" >> "$GITHUB_PATH"
"$bindir/proxbase" version
echo "version=$("$bindir/proxbase" version | cut -d' ' -f2)" >> "$GITHUB_OUTPUT"

- name: Restore ISO and base image cache
if: inputs.cache == 'true'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/proxbase
key: proxbase-${{ runner.os }}-${{ runner.arch }}-${{ steps.install.outputs.version }}-${{ inputs.pve-version }}
restore-keys: |
proxbase-${{ runner.os }}-${{ runner.arch }}-

- name: Create cluster
shell: bash
env:
NAME: ${{ inputs.name }}
CONFIG: ${{ inputs.config }}
NODES: ${{ inputs.nodes }}
MEMORY: ${{ inputs.memory }}
DATA_DISKS: ${{ inputs.data-disks }}
STORAGE: ${{ inputs.storage }}
PVE_VERSION: ${{ inputs.pve-version }}
GOLDEN: ${{ inputs.golden }}
run: |
set -euo pipefail
args=(create "$NAME")
[ -n "$CONFIG" ] && args+=(-f "$CONFIG")
[ -n "$NODES" ] && args+=(--nodes "$NODES")
[ -n "$MEMORY" ] && args+=(--memory "$MEMORY")
[ -n "$DATA_DISKS" ] && args+=(--data-disks "$DATA_DISKS")
[ -n "$STORAGE" ] && args+=(--storage "$STORAGE")
[ -n "$PVE_VERSION" ] && args+=(--pve-version "$PVE_VERSION")
[ "$GOLDEN" = true ] && args+=(--golden)
if ! proxbase "${args[@]}"; then
logs="${XDG_DATA_HOME:-$HOME/.local/share}/proxbase/clusters/$NAME/logs"
for f in "$logs"/*.log; do
[ -f "$f" ] || continue
echo "::group::$(basename "$f")"
tail -n 100 "$f"
echo "::endgroup::"
done
exit 1
fi

- name: Export credentials
id: env
shell: bash
env:
NAME: ${{ inputs.name }}
run: |
set -euo pipefail
dir="$RUNNER_TEMP/proxbase-$NAME"
proxbase env "$NAME" --export "$dir"
proxbase env "$NAME" --format json > "$dir/env.json"
echo "::add-mask::$(jq -r .tokenSecret "$dir/env.json")"
echo "::add-mask::$(jq -r .rootPassword "$dir/env.json")"
{
echo "endpoint=$(jq -r .endpoint "$dir/env.json")"
echo "api-token=$(jq -r .apiToken "$dir/env.json")"
echo "ca-cert=$dir/pve-root-ca.pem"
echo "ssh-key=$dir/id_ed25519"
echo "env-file=$dir/env.sh"
} >> "$GITHUB_OUTPUT"
rm "$dir/env.json"
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
| [Golden images](golden-images.md) | Faster creates from a cached base image |
| [Fault injection](fault-injection.md) | Power loss, hangs, pulled cables, partitions, latency and loss |
| [Docker](docker.md) | Running clusters in a container with Docker Compose |
| [CI and automation](ci.md) | GitHub Action, other CI systems, JSON progress |
| [MCP server](mcp.md) | Letting AI agents create, use and break clusters |
| [CLI reference](cli.md) | All commands and flags |
| [Troubleshooting](troubleshooting.md) | Logs, common errors and how to resume |
| [Architecture](architecture.md) | How Proxbase is built, with diagrams |
Expand Down
Loading
Loading