Skip to content

fix(spur-cloud-api): adapt GPU allocation to CDI-based device API (#63) #79

fix(spur-cloud-api): adapt GPU allocation to CDI-based device API (#63)

fix(spur-cloud-api): adapt GPU allocation to CDI-based device API (#63) #79

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-D warnings"
jobs:
deny:
runs-on: ubuntu-latest
env:
CARGO_DENY_VERSION: "0.19.6"
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install cargo-deny
run: |
curl -sL "https://github.com/EmbarkStudios/cargo-deny/releases/download/${CARGO_DENY_VERSION}/cargo-deny-${CARGO_DENY_VERSION}-x86_64-unknown-linux-musl.tar.gz" | tar xz -C /usr/local/bin --strip-components=1
- name: Check dependencies
run: cargo deny check
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install protobuf compiler
run: sudo apt-get install -y protobuf-compiler
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo registry and build
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Build backend
run: cargo build --all-targets
- name: Test backend
run: cargo test
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: Check Rust formatting
run: cargo fmt --all --check
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install protobuf compiler
run: sudo apt-get install -y protobuf-compiler
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Cache cargo registry and build
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-clippy-
- name: Clippy
run: cargo clippy --workspace --all-targets --locked -- -W clippy::all
frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Build frontend
working-directory: frontend
run: npm run build
integration:
name: Integration Test (MI300X)
runs-on: [self-hosted, mi300x]
needs: [build-and-test, fmt, clippy, frontend]
concurrency:
group: spur-cloud-${{ github.ref }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
- name: Build release binary
run: |
export PATH="$HOME/bin:$HOME/.cargo/bin:$PATH"
export PROTOC="$HOME/bin/protoc"
export PROTOC_INCLUDE="$HOME/protoc-install/include"
cargo build --release -j 4
- name: Start test services
run: |
export PATH="$HOME/bin:$HOME/.cargo/bin:$PATH"
# Start spurctld if not running
if ! pgrep -x spurctld >/dev/null; then
~/spur/etc/start-controller.sh 2>/dev/null || true
sleep 3
fi
# Start PostgreSQL test database
if ! pg_isready -q 2>/dev/null; then
echo "PostgreSQL not available, skipping integration test"
exit 0
fi
# Create test database if needed
psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='spur_cloud_test'" 2>/dev/null | grep -q 1 || \
psql -U postgres -c "CREATE DATABASE spur_cloud_test" 2>/dev/null || true
- name: Run API smoke test
run: |
export PATH="$HOME/bin:$HOME/.cargo/bin:$PATH"
# Skip if PostgreSQL is not available
if ! pg_isready -q 2>/dev/null; then
echo "SKIP: PostgreSQL not available on this runner"
echo "Build succeeded — integration test requires PostgreSQL"
exit 0
fi
# Generate test config
cat > /tmp/spur-cloud-test.toml << 'EOF'
public_url = "http://localhost:9090"
[server]
listen_addr = "127.0.0.1:9090"
backend = "native_host"
[native_host]
agent_port = 6828
[database]
url = "postgresql://postgres@localhost/spur_cloud_test"
[spur]
controller_addr = "http://127.0.0.1:6817"
[auth]
jwt_secret = "test-secret-for-ci-only-do-not-use-in-prod"
EOF
# Start the API server in background
timeout 30 ./target/release/spur-cloud-api --config /tmp/spur-cloud-test.toml &
API_PID=$!
sleep 3
# Health check
curl -sf http://localhost:9090/healthz || { echo "FAIL: healthz"; kill $API_PID 2>/dev/null; exit 1; }
echo "PASS: healthz"
# Register + login
REGISTER=$(curl -sf -X POST http://localhost:9090/api/auth/register \
-H 'Content-Type: application/json' \
-d '{"email":"ci@test.com","username":"citest","password":"testpassword123"}')
TOKEN=$(echo "$REGISTER" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])" 2>/dev/null)
[ -n "$TOKEN" ] && echo "PASS: register + login" || { echo "FAIL: register"; kill $API_PID 2>/dev/null; exit 1; }
# Issue #44: Test case-insensitive email login
LOGIN_UPPER=$(curl -s -w '\n%{http_code}' -X POST http://localhost:9090/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"CI@TEST.COM","password":"testpassword123"}')
LOGIN_UPPER_CODE=$(echo "$LOGIN_UPPER" | tail -1)
if [ "$LOGIN_UPPER_CODE" = "200" ]; then
echo "PASS: case-insensitive email login (CI@TEST.COM → ci@test.com)"
else
echo "FAIL: case-insensitive login returned $LOGIN_UPPER_CODE"; kill $API_PID 2>/dev/null; exit 1
fi
# Issue #44: Test wrong password returns specific error
WRONG_PW=$(curl -s -X POST http://localhost:9090/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"ci@test.com","password":"wrongpassword"}')
echo "$WRONG_PW" | grep -q "incorrect password" && echo "PASS: wrong password error message" || \
{ echo "FAIL: expected 'incorrect password', got: $WRONG_PW"; kill $API_PID 2>/dev/null; exit 1; }
# Issue #44: Test unknown email returns specific error
UNKNOWN_EMAIL=$(curl -s -X POST http://localhost:9090/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"nobody@test.com","password":"testpassword123"}')
echo "$UNKNOWN_EMAIL" | grep -q "no account found" && echo "PASS: unknown email error message" || \
{ echo "FAIL: expected 'no account found', got: $UNKNOWN_EMAIL"; kill $API_PID 2>/dev/null; exit 1; }
# Issue #36: Test GPU quota enforcement
# Register a second user, set quota, try to exceed it
REGISTER2=$(curl -sf -X POST http://localhost:9090/api/auth/register \
-H 'Content-Type: application/json' \
-d '{"email":"quota@test.com","username":"quotauser","password":"testpassword123"}')
TOKEN2=$(echo "$REGISTER2" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])" 2>/dev/null)
# Set quota to 2 GPUs via admin endpoint
curl -sf -X PUT "http://localhost:9090/api/admin/users/quota" \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d "{\"email\":\"quota@test.com\",\"max_gpus\":2}" >/dev/null 2>&1 && \
echo "PASS: set GPU quota" || echo "WARN: admin quota endpoint not available (spurctld may be down)"
# Auth providers
PROVIDERS=$(curl -sf http://localhost:9090/api/auth/providers)
echo "$PROVIDERS" | grep -q "local" && echo "PASS: providers" || echo "WARN: providers response unexpected"
# Issue #48: Test CPU-only session creation (gpu_count=0)
CPU_SESSION=$(curl -s -w '\n%{http_code}' -X POST http://localhost:9090/api/sessions \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"name":"cpu-test","container_image":"ubuntu:22.04","gpu_count":0,"gpu_type":"none"}')
CPU_CODE=$(echo "$CPU_SESSION" | tail -1)
if [ "$CPU_CODE" = "201" ]; then
echo "PASS: CPU-only session creation (gpu_count=0)"
else
echo "WARN: CPU-only session returned $CPU_CODE (spurctld may be down)"
fi
# Issue #48: Test cross-validation rejects gpu_count=0 with gpu_type set
INVALID=$(curl -s -w '\n%{http_code}' -X POST http://localhost:9090/api/sessions \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"name":"bad","container_image":"ubuntu:22.04","gpu_count":0,"gpu_type":"mi300x"}')
INVALID_CODE=$(echo "$INVALID" | tail -1)
if [ "$INVALID_CODE" = "400" ]; then
echo "PASS: cross-validation rejects gpu_count=0 + gpu_type=mi300x"
else
echo "FAIL: expected 400, got $INVALID_CODE"; kill $API_PID 2>/dev/null; exit 1
fi
# GPU capacity (requires spurctld)
GPU=$(curl -sf http://localhost:9090/api/gpus -H "Authorization: Bearer $TOKEN" 2>/dev/null)
echo "GPU capacity: $GPU"
echo "PASS: API smoke test complete"
kill $API_PID 2>/dev/null || true