Files
PSProxmoxVE/tests/dev.ps1
T
Clint Branham 94bc1a9d73 fix: address Copilot review round 3
- Fix bare Skip-IfNoTarget calls in 13_Firewall and 14_Backup
  (missing if/return pattern caused tests to run when they should skip)
- Validate modifier-only switches in dev.ps1 (-Force/-Reprovision
  without an action switch now errors instead of defaulting to -Shell)
- Add force-cleanup to usage text in run-integration.sh
- Add --connect-timeout/--max-time to guest agent curl in wait-for-pve.sh

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 16:27:14 -05:00

292 lines
9.3 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.
Use switches to compose actions: -Provision -Integration -Cleanup can
be combined in a single invocation.
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 Shell
Open an interactive pwsh shell in the dev container.
.PARAMETER Build
Build the module inside the container.
.PARAMETER Test
Run unit tests (Pester, excluding Integration tag).
.PARAMETER Provision
Provision nested PVE VMs (x86 only). Required before -Integration
unless VMs are already running.
.PARAMETER Integration
Run integration tests against provisioned PVE VMs.
.PARAMETER Cleanup
Destroy provisioned VMs (x86 only).
.PARAMETER Stop
Stop all containers.
.PARAMETER Rebuild
Rebuild container images from scratch.
.PARAMETER Reprovision
When used with -Provision, taints the PVE VMs in Terraform state
before applying, forcing them to be destroyed and recreated.
Useful when the VMs are in a bad state (e.g. clustered, broken).
.PARAMETER Force
When used with -Cleanup, bypasses Terraform and destroys VMs
directly via the PVE API. Useful when Terraform state is corrupted
(e.g. after an interrupted provision). Also removes Terraform
state files so the next provision starts clean.
.PARAMETER Tests
Filter integration tests by area name. Comma-separated list of test
area names that match the numbered file prefixes. Examples:
-Tests Connection,Nodes # runs 00_Connection + 01_Nodes
-Tests VMs,Snapshots # runs 06_VMs + 07_Snapshots
-Tests Cluster # runs 16_Cluster
When omitted, all integration test files are run.
.PARAMETER Version
PVE version to test against (8, 9, or all). Default: all.
.PARAMETER DockerHost
SSH destination for a remote Docker host (e.g. 172.16.40.113).
.PARAMETER NoCleanup
When used with -Integration, skips cleanup after tests complete.
.EXAMPLE
./tests/dev.ps1 -Shell
# Opens a pwsh shell in the dev container
.EXAMPLE
./tests/dev.ps1 -Build -Test
# Builds the module and runs unit tests
.EXAMPLE
./tests/dev.ps1 -Provision -Integration -Cleanup -DockerHost 172.16.40.113
# Full lifecycle: provision, test, cleanup on remote host
.EXAMPLE
./tests/dev.ps1 -Integration -Tests Connection,VMs -Version 9 -DockerHost 172.16.40.113
# Run only Connection and VMs integration tests for PVE 9
.EXAMPLE
./tests/dev.ps1 -Provision -Integration -Tests Cluster,HA -Version 9 -Cleanup -DockerHost 172.16.40.113
# Provision, run cluster+HA tests for PVE 9, cleanup
#>
[CmdletBinding()]
param(
[switch] $Shell,
[switch] $Build,
[switch] $Test,
[switch] $Provision,
[switch] $Integration,
[switch] $Cleanup,
[switch] $Stop,
[switch] $Rebuild,
[switch] $Reprovision,
[switch] $Force,
[string[]] $Tests,
[Alias('PveVersion')]
[ValidateSet('8', '9', 'all')]
[string] $Version = 'all',
[string] $DockerHost,
[Alias('k')]
[switch] $NoCleanup
)
$ErrorActionPreference = 'Stop'
# If no action switches specified, default to -Shell
$anySwitchSet = $Shell -or $Build -or $Test -or $Provision -or $Integration -or $Cleanup -or $Stop -or $Rebuild
if (-not $anySwitchSet) {
if ($Force -or $Reprovision) {
throw "-Force and -Reprovision are modifiers — combine with an action switch (e.g. -Cleanup -Force, -Provision -Reprovision)."
}
$Shell = $true
}
# Resolve repo root (parent of tests/)
$RepoRoot = Split-Path -Parent $PSScriptRoot
Push-Location $RepoRoot
try {
# ── Remote Docker host support ────────────────────────────────────────
$RemoteRepoPath = $null
if ($DockerHost) {
$RemoteRepoPath = "/tmp/psproxmoxve-dev"
$env:DOCKER_HOST = "ssh://$DockerHost"
Write-Host "Syncing repo to ${DockerHost}:${RemoteRepoPath}..."
ssh $DockerHost "mkdir -p $RemoteRepoPath"
if ($LASTEXITCODE -ne 0) { throw "Failed to create remote directory" }
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"
}
$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'
@"
services:
dev:
volumes:
- ${RemoteRepoPath}:/repo
dev-infra:
volumes:
- ${RemoteRepoPath}:/repo
- /opt/pve-integration:/opt/pve-integration
"@ | 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 {
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)" }
}
# Build test filter argument for run-integration.sh
$TestFilter = ''
if ($Tests) {
$TestFilter = ($Tests -join ',')
}
# ── Execute actions in order ──────────────────────────────────────────
if ($Stop) {
docker compose @ComposeArgs --profile infra down
}
if ($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
}
if ($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
}
if ($Build) {
Start-DevContainer
Invoke-BuildModule $DevContainer
}
if ($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)" }
}
if ($Provision) {
Start-InfraContainer
if ($Reprovision) {
docker exec $InfraContainer bash $RunIntegration taint $Version
if ($LASTEXITCODE -ne 0) { throw "Taint failed (exit code $LASTEXITCODE)" }
}
docker exec $InfraContainer bash $RunIntegration provision $Version
if ($LASTEXITCODE -ne 0) { throw "Provisioning failed (exit code $LASTEXITCODE)" }
}
if ($Integration) {
Start-InfraContainer
# Verify environment is ready (config file exists from provisioning)
$configCheck = docker exec $InfraContainer bash -c 'test -f "${CONFIG_FILE:-/opt/pve-integration/work/config.json}" && echo OK || echo MISSING'
if ($configCheck.Trim() -eq 'MISSING' -and -not $Provision) {
throw "Integration environment not ready. Run with -Provision first, or use -Provision -Integration together."
}
docker exec $InfraContainer bash $RunIntegration test $Version $TestFilter
if ($LASTEXITCODE -ne 0) { throw "Integration tests failed (exit code $LASTEXITCODE)" }
}
if ($Cleanup) {
Start-InfraContainer
if ($Force) {
if ($Version -ne 'all') {
throw "-Force cannot be combined with -Version. Force cleanup destroys all resources and wipes Terraform state."
}
docker exec $InfraContainer bash $RunIntegration force-cleanup
} else {
docker exec $InfraContainer bash $RunIntegration cleanup $Version
}
if ($LASTEXITCODE -ne 0) { throw "Cleanup failed (exit code $LASTEXITCODE)" }
}
} finally {
if ($OverrideFile -and (Test-Path $OverrideFile)) {
Remove-Item $OverrideFile -Force -ErrorAction SilentlyContinue
}
if ($DockerHost) {
Remove-Item Env:\DOCKER_HOST -ErrorAction SilentlyContinue
}
Pop-Location
}