mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-08-12 15:16:52 +00:00
5e1864a0b0
Workflow changes: - Convert to matrix strategy testing PVE 8.4 and 9.1 sequentially - Each version gets unique VMID (99908/99909) and VM name - Pass PVETEST_PVE_VERSION env var for version-specific assertions - Version-specific artifact names for test results New integration tests: - User CRUD: create, list, update (Set-PveUser), remove - API Token CRUD: create, list, remove (on dedicated test user) - Role CRUD: create with privileges, list, remove - Permissions: list and set (Get/Set-PvePermission) - VM config: get and set config, resize disk - VM lifecycle: restart, explicit remove - Snapshots: full lifecycle (create, list, restore, remove) - Storage: list content (Get-PveStorageContent) - Tasks: list recent tasks (Get-PveTask) - Version assertion: verify PVE major version matches expected Coverage improved from 15 to ~30 cmdlets tested against live API. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
261 lines
10 KiB
YAML
261 lines
10 KiB
YAML
name: Integration Tests
|
|
|
|
# Real integration tests against a live Proxmox VE instance.
|
|
# By default, provisions throwaway nested PVE VMs via Terraform for both
|
|
# PVE 8 and PVE 9, runs tests against each, then destroys them.
|
|
# Can also run against a pre-existing PVE with skip_provision.
|
|
#
|
|
# Required repository secrets (for provisioning):
|
|
# PVE_ENDPOINT - Parent PVE API URL (e.g. https://pve.example.com:8006)
|
|
# PVE_API_TOKEN - Parent PVE API token (for Terraform + uploads)
|
|
# PVE_TARGET_NODE - Parent PVE node name
|
|
#
|
|
# Optional secrets (for skip_provision mode):
|
|
# PVETEST_HOST - Hostname or IP of a pre-existing nested PVE
|
|
# PVETEST_APITOKEN - API token for the pre-existing nested PVE
|
|
#
|
|
# WARNING: Integration tests create and destroy real resources (VMs, users,
|
|
# tokens, network config, etc.) on the target node. Never run against production.
|
|
|
|
on:
|
|
push:
|
|
branches: [ main ]
|
|
pull_request:
|
|
branches: [ main ]
|
|
workflow_dispatch:
|
|
inputs:
|
|
skip_provision:
|
|
description: 'Skip provisioning (use existing PVE from PVETEST_HOST secret)'
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
|
|
env:
|
|
INFRA_DIR: tests/infrastructure
|
|
SCRIPTS_DIR: tests/infrastructure/scripts
|
|
PVE_PASSWORD: "Testpass123!"
|
|
|
|
jobs:
|
|
integration:
|
|
runs-on: [self-hosted, proxmox, integration]
|
|
timeout-minutes: 45
|
|
strategy:
|
|
fail-fast: false
|
|
max-parallel: 1 # Single self-hosted runner — provision one at a time
|
|
matrix:
|
|
pve_version: ['9', '8']
|
|
include:
|
|
- pve_version: '9'
|
|
iso_base: /opt/pve-isos/proxmox-ve_9.1-1.iso
|
|
iso_filename: proxmox-ve_9.1-1-auto.iso
|
|
vm_id: 99909
|
|
vm_name: pve-test-pve9
|
|
- pve_version: '8'
|
|
iso_base: /opt/pve-isos/proxmox-ve_8.4-1.iso
|
|
iso_filename: proxmox-ve_8.4-1-auto.iso
|
|
vm_id: 99908
|
|
vm_name: pve-test-pve8
|
|
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
|
|
# ── Pre-flight cleanup ─────────────────────────────────────────────
|
|
|
|
- name: Pre-flight cleanup
|
|
if: inputs.skip_provision != true
|
|
shell: bash
|
|
env:
|
|
PVE_API_TOKEN: ${{ secrets.PVE_API_TOKEN }}
|
|
run: |
|
|
bash ${SCRIPTS_DIR}/preflight-cleanup.sh \
|
|
"${{ secrets.PVE_ENDPOINT }}" \
|
|
"${PVE_API_TOKEN}" \
|
|
${{ matrix.vm_id }} \
|
|
"${{ matrix.iso_filename }}" \
|
|
"${INFRA_DIR}"
|
|
|
|
# ── Provision nested PVE ───────────────────────────────────────────
|
|
|
|
- name: Generate answer file
|
|
if: inputs.skip_provision != true
|
|
shell: bash
|
|
run: |
|
|
sed "s/\${root_password}/${PVE_PASSWORD}/" \
|
|
${INFRA_DIR}/answer.toml.tftpl > ${RUNNER_TEMP}/answer.toml
|
|
echo "Generated answer file:"
|
|
cat ${RUNNER_TEMP}/answer.toml
|
|
|
|
- name: Prepare auto-install ISO
|
|
if: inputs.skip_provision != true
|
|
shell: bash
|
|
run: |
|
|
bash ${SCRIPTS_DIR}/prepare-auto-iso.sh \
|
|
"${{ matrix.iso_base }}" \
|
|
${RUNNER_TEMP}/answer.toml \
|
|
${SCRIPTS_DIR}/first-boot.sh \
|
|
"${{ runner.temp }}/${{ matrix.iso_filename }}"
|
|
|
|
- name: Terraform init
|
|
if: inputs.skip_provision != true
|
|
shell: bash
|
|
working-directory: ${{ env.INFRA_DIR }}
|
|
run: terraform init -input=false
|
|
|
|
- name: Terraform apply
|
|
if: inputs.skip_provision != true
|
|
id: terraform
|
|
shell: bash
|
|
working-directory: ${{ env.INFRA_DIR }}
|
|
env:
|
|
TF_VAR_proxmox_endpoint: ${{ secrets.PVE_ENDPOINT }}
|
|
TF_VAR_proxmox_api_token: ${{ secrets.PVE_API_TOKEN }}
|
|
TF_VAR_target_node: ${{ secrets.PVE_TARGET_NODE }}
|
|
TF_VAR_iso_local_path: ${{ runner.temp }}/${{ matrix.iso_filename }}
|
|
TF_VAR_test_vm_password: ${{ env.PVE_PASSWORD }}
|
|
TF_VAR_vm_id: ${{ matrix.vm_id }}
|
|
TF_VAR_vm_name: ${{ matrix.vm_name }}
|
|
run: |
|
|
terraform apply -auto-approve -input=false
|
|
echo "vm_id=$(terraform output -raw pve_test_vm_id)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Wait for install and create API token
|
|
if: inputs.skip_provision != true
|
|
id: provision
|
|
shell: bash
|
|
env:
|
|
PVE_API_TOKEN: ${{ secrets.PVE_API_TOKEN }}
|
|
run: |
|
|
OUTPUT=$(bash ${SCRIPTS_DIR}/create-api-token.sh \
|
|
"${{ secrets.PVE_ENDPOINT }}" \
|
|
"${PVE_API_TOKEN}" \
|
|
"${{ steps.terraform.outputs.vm_id }}" \
|
|
"${PVE_PASSWORD}" \
|
|
900)
|
|
VM_IP=$(echo "$OUTPUT" | grep "^IP=" | cut -d= -f2)
|
|
VM_TOKEN=$(echo "$OUTPUT" | grep "^TOKEN=" | cut -d= -f2-)
|
|
# Mask sensitive values before they appear in logs
|
|
echo "::add-mask::${VM_IP}"
|
|
echo "::add-mask::${VM_TOKEN}"
|
|
echo "Nested PVE ${{ matrix.pve_version }} ready at ${VM_IP}"
|
|
echo "host=${VM_IP}" >> "$GITHUB_OUTPUT"
|
|
echo "token=${VM_TOKEN}" >> "$GITHUB_OUTPUT"
|
|
|
|
# ── Resolve test target ────────────────────────────────────────────
|
|
|
|
- name: Set test target
|
|
id: target
|
|
shell: bash
|
|
run: |
|
|
if [ "${{ inputs.skip_provision }}" = "true" ]; then
|
|
echo "host=${{ secrets.PVETEST_HOST }}" >> "$GITHUB_OUTPUT"
|
|
echo "port=${{ secrets.PVETEST_PORT || '8006' }}" >> "$GITHUB_OUTPUT"
|
|
echo "token=${{ secrets.PVETEST_APITOKEN }}" >> "$GITHUB_OUTPUT"
|
|
echo "node=${{ secrets.PVETEST_NODE || 'pve' }}" >> "$GITHUB_OUTPUT"
|
|
echo "storage=${{ secrets.PVETEST_STORAGE || 'local' }}" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "host=${{ steps.provision.outputs.host }}" >> "$GITHUB_OUTPUT"
|
|
echo "port=8006" >> "$GITHUB_OUTPUT"
|
|
echo "token=${{ steps.provision.outputs.token }}" >> "$GITHUB_OUTPUT"
|
|
echo "node=pve" >> "$GITHUB_OUTPUT"
|
|
echo "storage=local" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Verify PVE API reachable
|
|
shell: bash
|
|
run: |
|
|
HOST="${{ steps.target.outputs.host }}"
|
|
PORT="${{ steps.target.outputs.port }}"
|
|
TOKEN="${{ steps.target.outputs.token }}"
|
|
echo "Testing connectivity to PVE ${{ matrix.pve_version }} API at ${HOST}:${PORT}..."
|
|
if curl -sk --connect-timeout 10 \
|
|
-H "Authorization: PVEAPIToken=${TOKEN}" \
|
|
"https://${HOST}:${PORT}/api2/json/nodes" | grep -q '"node"'; then
|
|
echo "PVE API is responsive"
|
|
else
|
|
echo "::error::Cannot reach PVE API at ${HOST}:${PORT}"
|
|
exit 1
|
|
fi
|
|
|
|
# ── Build and test ─────────────────────────────────────────────────
|
|
|
|
- name: Build module
|
|
run: dotnet publish src/PSProxmoxVE/PSProxmoxVE.csproj --configuration Release --framework netstandard2.0 --output ./publish/netstandard2.0
|
|
|
|
- name: Clean publish output for PS module loading
|
|
run: rm -f ./publish/netstandard2.0/*.deps.json
|
|
|
|
- name: Install Pester 5
|
|
shell: pwsh
|
|
run: Install-Module -Name Pester -MinimumVersion 5.0 -Force -Scope CurrentUser
|
|
|
|
- name: Copy module to module path
|
|
shell: pwsh
|
|
run: |
|
|
$modulePath = "$HOME/.local/share/powershell/Modules/PSProxmoxVE"
|
|
New-Item -ItemType Directory -Path $modulePath -Force | Out-Null
|
|
Copy-Item -Path ./publish/netstandard2.0/* -Destination $modulePath -Recurse -Force
|
|
|
|
- name: Create test ISO
|
|
shell: bash
|
|
run: dd if=/dev/urandom of=${RUNNER_TEMP}/pvetest.iso bs=1M count=1 2>/dev/null
|
|
|
|
- name: Run integration tests
|
|
shell: pwsh
|
|
env:
|
|
PVETEST_HOST: ${{ steps.target.outputs.host }}
|
|
PVETEST_PORT: ${{ steps.target.outputs.port }}
|
|
PVETEST_APITOKEN: ${{ steps.target.outputs.token }}
|
|
PVETEST_NODE: ${{ steps.target.outputs.node }}
|
|
PVETEST_STORAGE: ${{ steps.target.outputs.storage }}
|
|
PVETEST_ISO_PATH: ${{ runner.temp }}/pvetest.iso
|
|
PVETEST_PVE_VERSION: ${{ matrix.pve_version }}
|
|
run: |
|
|
Import-Module Pester -MinimumVersion 5.0
|
|
$config = New-PesterConfiguration
|
|
$config.Run.Path = "tests/PSProxmoxVE.Tests/Integration"
|
|
$config.Filter.Tag = "Integration"
|
|
$config.Output.Verbosity = "Detailed"
|
|
$config.TestResult.Enabled = $true
|
|
$config.TestResult.OutputFormat = "NUnitXml"
|
|
$config.TestResult.OutputPath = "TestResults/integration-results.xml"
|
|
Invoke-Pester -Configuration $config
|
|
|
|
- name: Upload test results
|
|
if: always()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: integration-test-results-pve${{ matrix.pve_version }}
|
|
path: TestResults/
|
|
|
|
# ── Teardown ───────────────────────────────────────────────────────
|
|
|
|
- name: Terraform destroy
|
|
if: always() && inputs.skip_provision != true
|
|
shell: bash
|
|
working-directory: ${{ env.INFRA_DIR }}
|
|
env:
|
|
TF_VAR_proxmox_endpoint: ${{ secrets.PVE_ENDPOINT }}
|
|
TF_VAR_proxmox_api_token: ${{ secrets.PVE_API_TOKEN }}
|
|
TF_VAR_target_node: ${{ secrets.PVE_TARGET_NODE }}
|
|
TF_VAR_iso_local_path: ${{ runner.temp }}/${{ matrix.iso_filename }}
|
|
TF_VAR_test_vm_password: ${{ env.PVE_PASSWORD }}
|
|
TF_VAR_vm_id: ${{ matrix.vm_id }}
|
|
TF_VAR_vm_name: ${{ matrix.vm_name }}
|
|
run: |
|
|
terraform init -input=false
|
|
terraform destroy -auto-approve -input=false || true
|
|
rm -f terraform.tfstate terraform.tfstate.backup .terraform.tfstate.lock.info
|
|
|
|
- name: Final cleanup
|
|
if: always() && inputs.skip_provision != true
|
|
shell: bash
|
|
env:
|
|
PVE_API_TOKEN: ${{ secrets.PVE_API_TOKEN }}
|
|
run: |
|
|
bash ${SCRIPTS_DIR}/preflight-cleanup.sh \
|
|
"${{ secrets.PVE_ENDPOINT }}" \
|
|
"${PVE_API_TOKEN}" \
|
|
${{ matrix.vm_id }} \
|
|
"${{ matrix.iso_filename }}" \
|
|
"${INFRA_DIR}" || true
|