Files
PSProxmoxVE/tests/dev.ps1
T
Clint Branham f21c7f84b7 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>
2026-03-24 10:41:28 -05:00

252 lines
8.9 KiB
PowerShell

#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
}