feat(ci): unify CI and local integration test infrastructure

- Replace tests/infrastructure/Dockerfile with tests/Dockerfile.test
  (single multi-stage Dockerfile for both CI and local dev)
- CI container-image job now builds from Dockerfile.test target dev-infra
- Add ARM support: PowerShell installed via dotnet tool on arm64,
  APT package on amd64
- Replace tests/dev.sh (bash) with tests/dev.ps1 (PowerShell) for
  cross-platform support (Windows, macOS, Linux)
- Add -DockerHost parameter for running x86 containers on a remote
  Docker host from ARM Macs (rsyncs repo, uses SSH Docker transport)
- Add -NoCleanup switch to keep nested PVE VMs after integration tests
- integration command now provisions nested PVE VMs instead of testing
  against a pre-existing PVE directly
- Share /opt/pve-isos host path between CI and local dev (was separate
  Docker named volume)
- Delete tools/Invoke-Tests.ps1 (unused, overlapped with run-integration.sh)
- Add .gitignore entries for Terraform state/artifacts
- Update all documentation references

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-24 10:41:28 -05:00
parent c1e1442155
commit f21c7f84b7
13 changed files with 304 additions and 617 deletions
+10 -14
View File
@@ -1,21 +1,17 @@
# PSProxmoxVE integration test configuration.
# Copy to .env.test and fill in values. This file is gitignored.
#
# These credentials point to the PARENT PVE host where nested test VMs
# will be provisioned. Integration tests run against the nested VMs,
# not against this host directly.
# ── For testing against a pre-existing PVE (./tests/dev.sh integration) ──
PVETEST_HOST=pve.example.com
PVETEST_PORT=8006
PVETEST_APITOKEN=user@realm!tokenid=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
PVETEST_NODE=pve1
PVETEST_STORAGE=local
PVETEST_PASSWORD=<your-test-password>
# ── Required: parent PVE for provisioning ─────────────────────────────
PVE_ENDPOINT=https://pve.example.com:8006
PVE_API_TOKEN=user@realm!tokenid=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
PVE_TARGET_NODE=pve1
PVE_PASSWORD=<root-password-for-nested-pve>
# ── For full provisioning (./tests/dev.sh provision, x86 only) ───────────
# PVE_ENDPOINT=https://pve.example.com:8006
# PVE_API_TOKEN=user@realm!tokenid=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# PVE_TARGET_NODE=pve1
# PVE_PASSWORD=<root-password-for-nested-pve>
# ── Optional overrides ──────────────────────────────────────────────────
# ── Optional overrides ────────────────────────────────────────────────
# CACHE_DIR=/opt/pve-isos
# WORK_DIR=/tmp/pve-integration
# PVE_VERSIONS=9 8
+20 -8
View File
@@ -16,16 +16,28 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0-noble AS dev
ENV DEBIAN_FRONTEND=noninteractive
# Install base packages
RUN apt-get update && apt-get install -y --no-install-recommends \
curl jq openssh-client ca-certificates apt-transport-https gnupg \
&& curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
| gpg --dearmor -o /usr/share/keyrings/microsoft-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] \
https://packages.microsoft.com/ubuntu/24.04/prod noble main" \
> /etc/apt/sources.list.d/microsoft-prod.list \
&& apt-get update && apt-get install -y --no-install-recommends powershell \
&& rm -rf /var/lib/apt/lists/* \
&& pwsh -NoProfile -Command \
&& rm -rf /var/lib/apt/lists/*
# Install PowerShell: APT package on amd64, dotnet global tool on arm64.
# The Microsoft APT repo does not publish arm64 packages for PowerShell.
RUN if [ "$(dpkg --print-architecture)" = "amd64" ]; then \
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
| gpg --dearmor -o /usr/share/keyrings/microsoft-archive-keyring.gpg \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] \
https://packages.microsoft.com/ubuntu/24.04/prod noble main" \
> /etc/apt/sources.list.d/microsoft-prod.list \
&& apt-get update && apt-get install -y --no-install-recommends powershell \
&& rm -rf /var/lib/apt/lists/*; \
else \
dotnet tool install --global PowerShell \
&& ln -s /root/.dotnet/tools/pwsh /usr/local/bin/pwsh; \
fi
# Install Pester and prepare module directory
RUN pwsh -NoProfile -Command \
'Set-PSRepository -Name PSGallery -InstallationPolicy Trusted; \
Install-Module -Name Pester -MinimumVersion 5.0 -Scope AllUsers -Force' \
&& mkdir -p /usr/local/share/powershell/Modules/PSProxmoxVE
@@ -24,8 +24,8 @@
To run:
Invoke-Pester -Path ./tests/PSProxmoxVE.Tests -Tag Integration
# or use the project's helper script:
./Invoke-Tests.ps1 -Tier Integration
# or use the dev container:
./tests/dev.ps1 integration
#>
BeforeAll {
@@ -119,10 +119,10 @@ env:
The integration suite is tagged `Integration`. Use the `-Tag` filter so that the unit
tests and integration tests can be run independently.
### Via the project helper script (recommended)
### Via the dev container (recommended)
```powershell
./Invoke-Tests.ps1 -Tier Integration
./tests/dev.ps1 integration
```
### Directly with Invoke-Pester
+251
View File
@@ -0,0 +1,251 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Helper script for the dev/test containers.
.DESCRIPTION
Manages Docker-based dev containers for building, testing, and running
integration tests for PSProxmoxVE. Works on Windows, macOS, and Linux.
For x86-only commands (integration, provision, cleanup), use -DockerHost
to run containers on a remote Docker host via SSH. The script syncs the
repo to the remote host automatically.
.PARAMETER Command
The action to perform:
shell - Open pwsh in the dev container (default)
build - Build the module inside the container
test - Run unit tests (ARM + x86)
integration - Provision nested PVE VMs, run integration tests, cleanup (x86 only)
provision - Provision nested PVE VMs only, no tests (x86 only)
cleanup - Destroy provisioned VMs (x86 only)
stop - Stop all containers
rebuild - Rebuild container image(s)
.PARAMETER PveVersion
PVE version to test against (8, 9, or all). Default: all.
Only used with 'integration' and 'provision' commands.
.PARAMETER NoCleanup
When used with 'integration', skips cleanup after tests complete.
Nested PVE VMs are left running for inspection or re-testing.
Run './tests/dev.ps1 cleanup' to destroy them later.
.PARAMETER DockerHost
SSH destination for a remote Docker host (e.g. user@runner-vm).
When set, the repo is rsynced to the remote host and all Docker
commands run against the remote daemon. Useful for running x86
containers from an ARM Mac.
Prerequisites on the remote host:
- Docker installed
- SSH user in the 'docker' group (sudo usermod -aG docker $USER)
- SSH key-based auth from the local machine
.EXAMPLE
./tests/dev.ps1
# Opens a pwsh shell in the dev container
.EXAMPLE
./tests/dev.ps1 test
# Builds the module and runs unit tests
.EXAMPLE
./tests/dev.ps1 integration -DockerHost user@runner-vm
# Syncs repo to runner-vm, provisions nested PVE, runs tests, cleans up
.EXAMPLE
./tests/dev.ps1 integration 9 -DockerHost user@runner-vm
# Same but only for PVE 9
#>
[CmdletBinding()]
param(
[Parameter(Position = 0)]
[ValidateSet('shell', 'build', 'test', 'integration', 'provision', 'cleanup', 'stop', 'rebuild')]
[string] $Command = 'shell',
[Parameter(Position = 1)]
[string] $PveVersion = 'all',
[Parameter()]
[string] $DockerHost,
[Parameter()]
[Alias('k')]
[switch] $NoCleanup
)
$ErrorActionPreference = 'Stop'
# Resolve repo root (parent of tests/)
$RepoRoot = Split-Path -Parent $PSScriptRoot
Push-Location $RepoRoot
try {
# ── Remote Docker host support ────────────────────────────────────────
# When -DockerHost is specified, we rsync the repo (including .env.test)
# to the remote host and set DOCKER_HOST so all docker/compose commands
# execute on the remote daemon. The compose file's volume mount (..:/repo)
# and build context reference the remote copy.
$RemoteRepoPath = $null
if ($DockerHost) {
$RemoteRepoPath = "/tmp/psproxmoxve-dev"
$env:DOCKER_HOST = "ssh://$DockerHost"
Write-Host "Syncing repo to ${DockerHost}:${RemoteRepoPath}..."
# Create remote directory
ssh $DockerHost "mkdir -p $RemoteRepoPath"
if ($LASTEXITCODE -ne 0) { throw "Failed to create remote directory" }
# Rsync repo to remote host, excluding build artifacts
rsync -az --delete `
--exclude 'bin/' `
--exclude 'obj/' `
--exclude 'publish/' `
--exclude 'TestResults/' `
--exclude '.terraform/' `
--exclude 'terraform.tfstate*' `
--include '.env.test' `
./ "${DockerHost}:${RemoteRepoPath}/"
if ($LASTEXITCODE -ne 0) { throw "Failed to sync repo to remote host" }
Write-Host "Using remote Docker host: $DockerHost"
}
# When running remotely, docker compose reads the compose file locally but
# volume mounts resolve on the remote host. We generate a temporary override
# file that remaps volume mounts to the rsynced remote path.
$ComposeFile = 'tests/docker-compose.test.yml'
$ComposeArgs = @('-f', $ComposeFile)
$OverrideFile = $null
if ($RemoteRepoPath) {
$OverrideFile = Join-Path ([System.IO.Path]::GetTempPath()) 'docker-compose.remote-override.yml'
# Override volume mounts to point at the rsynced remote repo path.
# env_file is NOT overridden — compose reads it locally and injects
# the values as container env vars, which is what we want.
@"
services:
dev:
volumes:
- ${RemoteRepoPath}:/repo
dev-infra:
volumes:
- ${RemoteRepoPath}:/repo
- /opt/pve-isos:/opt/pve-isos
"@ | Set-Content -Path $OverrideFile -Encoding utf8
$ComposeArgs = @('-f', $ComposeFile, '-f', $OverrideFile)
}
$DevContainer = 'psproxmoxve-dev'
$InfraContainer = 'psproxmoxve-dev-infra'
$RunIntegration = 'tests/infrastructure/scripts/run-integration.sh'
function Start-DevContainer {
# 'up -d' is idempotent: starts if stopped, recreates if config/env changed, no-ops if current
docker compose @ComposeArgs up -d dev
if ($LASTEXITCODE -ne 0) { throw 'Failed to start dev container' }
}
function Start-InfraContainer {
docker compose @ComposeArgs --profile infra up -d dev-infra
if ($LASTEXITCODE -ne 0) { throw 'Failed to start infra container (x86 only)' }
}
function Invoke-BuildModule {
param([string] $Container)
docker exec $Container bash -c @"
dotnet publish src/PSProxmoxVE/PSProxmoxVE.csproj -c Release -f netstandard2.0 -o /tmp/publish 2>&1 | tail -1 && \
cp -r /tmp/publish/* /usr/local/share/powershell/Modules/PSProxmoxVE/ && \
echo 'Module installed to /usr/local/share/powershell/Modules/PSProxmoxVE'
"@
if ($LASTEXITCODE -ne 0) { throw "Module build failed (exit code $LASTEXITCODE)" }
}
switch ($Command) {
'shell' {
if ($DockerHost) {
Write-Warning "Interactive shell over remote Docker is not supported. Use: ssh $DockerHost 'docker exec -it $DevContainer pwsh -NoProfile'"
return
}
Start-DevContainer
docker exec -it $DevContainer pwsh -NoProfile
}
'build' {
Start-DevContainer
Invoke-BuildModule $DevContainer
}
'test' {
Start-DevContainer
Invoke-BuildModule $DevContainer
docker exec $DevContainer pwsh -NoProfile -Command @'
$config = New-PesterConfiguration
$config.Run.Path = 'tests/PSProxmoxVE.Tests'
$config.Run.Exit = $true
$config.Filter.ExcludeTag = @('Integration')
$config.Output.Verbosity = 'Detailed'
Invoke-Pester -Configuration $config
'@
if ($LASTEXITCODE -ne 0) { throw "Unit tests failed (exit code $LASTEXITCODE)" }
}
'integration' {
# Full lifecycle: provision nested PVE VMs -> test -> cleanup (x86 only)
# Requires PVE_ENDPOINT, PVE_API_TOKEN, PVE_TARGET_NODE, PVE_PASSWORD in .env.test
# run-integration.sh handles module build if no pre-built artifact exists
Start-InfraContainer
if ($NoCleanup) {
# Provision and test separately — leave VMs running for inspection
docker exec $InfraContainer bash $RunIntegration provision
if ($LASTEXITCODE -ne 0) { throw "Provisioning failed (exit code $LASTEXITCODE)" }
docker exec $InfraContainer bash $RunIntegration test $PveVersion
if ($LASTEXITCODE -ne 0) { throw "Integration tests failed (exit code $LASTEXITCODE)" }
Write-Host 'VMs left running (-NoCleanup). Run ./tests/dev.ps1 cleanup to destroy them.'
} else {
docker exec $InfraContainer bash $RunIntegration all $PveVersion
if ($LASTEXITCODE -ne 0) { throw "Integration tests failed (exit code $LASTEXITCODE)" }
}
}
'provision' {
# Provision nested PVE VMs only, without running tests (x86 only)
# Useful for iterating: provision once, then shell in and run tests manually
Start-InfraContainer
docker exec $InfraContainer bash $RunIntegration provision
if ($LASTEXITCODE -ne 0) { throw "Provisioning failed (exit code $LASTEXITCODE)" }
}
'cleanup' {
# Destroy provisioned VMs
Start-InfraContainer
docker exec $InfraContainer bash $RunIntegration cleanup
if ($LASTEXITCODE -ne 0) { throw "Cleanup failed (exit code $LASTEXITCODE)" }
}
'stop' {
docker compose @ComposeArgs --profile infra down
}
'rebuild' {
docker compose @ComposeArgs --profile infra down
docker compose @ComposeArgs build --no-cache dev
docker compose @ComposeArgs --profile infra build --no-cache dev-infra
docker compose @ComposeArgs up -d dev
}
}
} finally {
if ($OverrideFile -and (Test-Path $OverrideFile)) {
Remove-Item $OverrideFile -Force -ErrorAction SilentlyContinue
}
if ($DockerHost) {
Remove-Item Env:\DOCKER_HOST -ErrorAction SilentlyContinue
}
Pop-Location
}
-117
View File
@@ -1,117 +0,0 @@
#!/usr/bin/env bash
# Helper script for the dev/test containers.
#
# Usage:
# ./tests/dev.sh # Start dev container and open pwsh shell
# ./tests/dev.sh build # Build the module inside the container
# ./tests/dev.sh test # Run unit tests
# ./tests/dev.sh integration # Run integration tests against existing PVE (needs .env.test)
# ./tests/dev.sh provision # Full CI flow: provision → test → cleanup (x86 only)
# ./tests/dev.sh cleanup # Destroy provisioned VMs (x86 only)
# ./tests/dev.sh stop # Stop all containers
# ./tests/dev.sh rebuild # Rebuild container image(s)
set -euo pipefail
cd "$(dirname "$0")/.."
COMPOSE="docker compose -f tests/docker-compose.test.yml"
DEV_CONTAINER="psproxmoxve-dev"
INFRA_CONTAINER="psproxmoxve-dev-infra"
MODULE_PATH="/usr/local/share/powershell/Modules/PSProxmoxVE"
RUN_INTEGRATION="tests/infrastructure/scripts/run-integration.sh"
ensure_dev() {
if ! docker inspect "$DEV_CONTAINER" --format '{{.State.Running}}' 2>/dev/null | grep -q true; then
echo "Starting dev container..."
$COMPOSE up -d dev
fi
}
ensure_infra() {
if ! docker inspect "$INFRA_CONTAINER" --format '{{.State.Running}}' 2>/dev/null | grep -q true; then
echo "Starting infra container (x86 only)..."
$COMPOSE --profile infra up -d dev-infra
fi
}
build_module() {
local container="$1"
docker exec "$container" bash -c "
dotnet publish src/PSProxmoxVE/PSProxmoxVE.csproj \
-c Release -f netstandard2.0 -o /tmp/publish 2>&1 | tail -1 && \
cp -r /tmp/publish/* $MODULE_PATH/ && \
echo "Module installed to $MODULE_PATH"
"
}
case "${1:-shell}" in
shell)
ensure_dev
docker exec -it "$DEV_CONTAINER" pwsh -NoProfile
;;
build)
ensure_dev
build_module "$DEV_CONTAINER"
;;
test)
ensure_dev
build_module "$DEV_CONTAINER"
docker exec "$DEV_CONTAINER" pwsh -NoProfile -Command "
\$config = New-PesterConfiguration
\$config.Run.Path = 'tests/PSProxmoxVE.Tests'
\$config.Run.Exit = \$true
\$config.Filter.ExcludeTag = @('Integration')
\$config.Output.Verbosity = 'Detailed'
Invoke-Pester -Configuration \$config
"
;;
integration)
# Run integration tests against a pre-existing PVE (set via .env.test)
ensure_dev
build_module "$DEV_CONTAINER"
docker exec "$DEV_CONTAINER" bash -c "
SKIP_PROVISION=true bash $RUN_INTEGRATION test ${2:-all}
"
;;
provision)
# Full CI lifecycle: provision → test → cleanup (x86 infra container)
ensure_infra
build_module "$INFRA_CONTAINER"
docker exec "$INFRA_CONTAINER" bash "$RUN_INTEGRATION" all "${2:-all}"
;;
cleanup)
# Destroy provisioned VMs
ensure_infra
docker exec "$INFRA_CONTAINER" bash "$RUN_INTEGRATION" cleanup
;;
stop)
$COMPOSE --profile infra down
;;
rebuild)
$COMPOSE --profile infra down
$COMPOSE build --no-cache dev
$COMPOSE --profile infra build --no-cache dev-infra
$COMPOSE up -d dev
;;
*)
echo "Usage: $0 {shell|build|test|integration|provision|cleanup|stop|rebuild}"
echo ""
echo " shell Open pwsh in the dev container"
echo " build Build the module"
echo " test Run unit tests"
echo " integration Run integration tests against existing PVE (.env.test)"
echo " provision Full CI flow: provision → test → cleanup (x86 only)"
echo " cleanup Destroy provisioned VMs (x86 only)"
echo " stop Stop all containers"
echo " rebuild Rebuild container images"
exit 1
;;
esac
+1 -4
View File
@@ -37,7 +37,7 @@ services:
profiles: [infra]
volumes:
- ..:/repo
- pve-isos:/opt/pve-isos
- /opt/pve-isos:/opt/pve-isos
working_dir: /repo
stdin_open: true
tty: true
@@ -45,6 +45,3 @@ services:
env_file:
- path: .env.test
required: false
volumes:
pve-isos:
-67
View File
@@ -1,67 +0,0 @@
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
# ── Base packages ──────────────────────────────────────────────────────
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
jq \
dosfstools \
mtools \
openssh-client \
sshpass \
apt-transport-https \
gnupg \
lsb-release \
ca-certificates \
python3 \
xorriso \
&& rm -rf /var/lib/apt/lists/*
# ── Microsoft repo (PowerShell) ───────────────────────────────────────
RUN curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
| gpg --dearmor -o /usr/share/keyrings/microsoft-archive-keyring.gpg && \
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] \
https://packages.microsoft.com/ubuntu/24.04/prod noble main" \
> /etc/apt/sources.list.d/microsoft-prod.list
# ── HashiCorp repo (Terraform) ────────────────────────────────────────
RUN curl -fsSL https://apt.releases.hashicorp.com/gpg \
| gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg && \
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com noble main" \
> /etc/apt/sources.list.d/hashicorp.list
# ── Proxmox repo (proxmox-auto-install-assistant) ─────────────────────
RUN curl -fsSL https://enterprise.proxmox.com/debian/proxmox-release-bookworm.gpg \
-o /usr/share/keyrings/proxmox-release-bookworm.gpg && \
echo "deb [signed-by=/usr/share/keyrings/proxmox-release-bookworm.gpg] \
http://download.proxmox.com/debian/pve bookworm pve-no-subscription" \
> /etc/apt/sources.list.d/proxmox-pve.list
# ── Install tooling ───────────────────────────────────────────────────
RUN apt-get update && apt-get install -y --no-install-recommends \
powershell \
terraform \
proxmox-auto-install-assistant \
qemu-utils \
&& rm -rf /var/lib/apt/lists/*
# ── Install Pester ────────────────────────────────────────────────────
RUN pwsh -NoProfile -Command \
'Set-PSRepository -Name PSGallery -InstallationPolicy Trusted; \
Install-Module -Name Pester -MinimumVersion 5.0 -Scope AllUsers -Force'
# ── Verify all tools are present ──────────────────────────────────────
RUN echo "=== Tool verification ===" && \
pwsh --version && \
terraform --version && \
proxmox-auto-install-assistant --version && \
sshpass -V | head -1 && \
xorriso --version 2>&1 | head -1 && \
qemu-img --version | head -1 && \
curl --version | head -1 && \
jq --version && \
python3 --version && \
ssh -V 2>&1 && \
echo "=== All tools verified ==="