mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-09-04 11:15:34 +00:00
Merge pull request #85 from GoodOlClint/ci/dynamic-node-selection
ci: provision picks the least-loaded node and guards host memory
This commit is contained in:
@@ -18,8 +18,12 @@ name: Integration Tests
|
|||||||
# PVE_API_TOKEN - Parent PVE API token (for Terraform + uploads)
|
# PVE_API_TOKEN - Parent PVE API token (for Terraform + uploads)
|
||||||
# PVE_TEST_PASSWORD - Root password for nested PVE instances (used for both provision and tests)
|
# PVE_TEST_PASSWORD - Root password for nested PVE instances (used for both provision and tests)
|
||||||
#
|
#
|
||||||
# Required repository variables:
|
# Optional repository variables:
|
||||||
# PVE_TARGET_NODE - Parent PVE node name (variable, not secret, to avoid log masking)
|
# PVE_TARGET_NODE - Parent PVE node name (variable, not secret, to avoid log
|
||||||
|
# masking). Unset or "auto": provisioning picks the online
|
||||||
|
# node with the most free memory and refuses to run when
|
||||||
|
# no node has enough headroom — the token's user needs
|
||||||
|
# PVEAuditor on /nodes for the memory stats.
|
||||||
#
|
#
|
||||||
# Optional secrets (for skip_provision mode):
|
# Optional secrets (for skip_provision mode):
|
||||||
# PVETEST_HOST - Hostname or IP of a pre-existing nested PVE
|
# PVETEST_HOST - Hostname or IP of a pre-existing nested PVE
|
||||||
@@ -56,7 +60,7 @@ env:
|
|||||||
CACHE_DIR: /opt/pve-integration
|
CACHE_DIR: /opt/pve-integration
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ── Build module artifact (GitHub-hosted) ────────────────────────────
|
# ── Build module artifact (GitHub-hosted) ────────────────────────
|
||||||
build:
|
build:
|
||||||
if: github.actor != 'dependabot[bot]'
|
if: github.actor != 'dependabot[bot]'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -184,7 +188,7 @@ jobs:
|
|||||||
name: integration-test-results-pve${{ matrix.pve_version }}
|
name: integration-test-results-pve${{ matrix.pve_version }}
|
||||||
path: TestResults/
|
path: TestResults/
|
||||||
|
|
||||||
# ── Cleanup: destroy all VMs (always runs) ──────────────────────────
|
# ── Cleanup: destroy all VMs (always runs) ──────────────────────
|
||||||
cleanup:
|
cleanup:
|
||||||
needs: [provision, test]
|
needs: [provision, test]
|
||||||
if: always() && needs.provision.result != 'skipped' && github.actor != 'dependabot[bot]'
|
if: always() && needs.provision.result != 'skipped' && github.actor != 'dependabot[bot]'
|
||||||
@@ -221,7 +225,7 @@ jobs:
|
|||||||
# cancelled runs, timeouts, or partial provisioning failures.
|
# cancelled runs, timeouts, or partial provisioning failures.
|
||||||
run: bash ${SCRIPTS_DIR}/run-integration.sh force-cleanup
|
run: bash ${SCRIPTS_DIR}/run-integration.sh force-cleanup
|
||||||
|
|
||||||
# ── Clean up old container images from GHCR ──────────────────────────
|
# ── Clean up old container images from GHCR ──────────────────────
|
||||||
cleanup-images:
|
cleanup-images:
|
||||||
needs: test
|
needs: test
|
||||||
if: always() && github.actor != 'dependabot[bot]'
|
if: always() && github.actor != 'dependabot[bot]'
|
||||||
|
|||||||
Executable → Regular
+70
-9
@@ -22,7 +22,6 @@
|
|||||||
# Required env vars (provision/cleanup):
|
# Required env vars (provision/cleanup):
|
||||||
# PVE_ENDPOINT Parent PVE API URL (e.g. https://pve.example.com:8006)
|
# PVE_ENDPOINT Parent PVE API URL (e.g. https://pve.example.com:8006)
|
||||||
# PVE_API_TOKEN Parent PVE API token
|
# PVE_API_TOKEN Parent PVE API token
|
||||||
# PVE_TARGET_NODE Parent PVE node name
|
|
||||||
# PVE_PASSWORD Root password for nested PVE instances
|
# PVE_PASSWORD Root password for nested PVE instances
|
||||||
#
|
#
|
||||||
# Required env vars (test with pre-existing PVE):
|
# Required env vars (test with pre-existing PVE):
|
||||||
@@ -31,6 +30,12 @@
|
|||||||
# Set SKIP_PROVISION=true
|
# Set SKIP_PROVISION=true
|
||||||
#
|
#
|
||||||
# Optional env vars:
|
# Optional env vars:
|
||||||
|
# PVE_TARGET_NODE Parent PVE node name; unset or "auto" picks the online
|
||||||
|
# node with the most free memory (token needs PVEAuditor
|
||||||
|
# on /nodes for the memory stats)
|
||||||
|
# PVE_VM_MEM_GB Memory per nested PVE VM in GiB, for the headroom check
|
||||||
|
# (default: 8, matches the Terraform default)
|
||||||
|
# PVE_MEM_HEADROOM_GB Free memory to leave the parent node (default: 8)
|
||||||
# CACHE_DIR ISO/image cache (default: /opt/pve-integration)
|
# CACHE_DIR ISO/image cache (default: /opt/pve-integration)
|
||||||
# WORK_DIR Temp dir for build artifacts (default: $CACHE_DIR/work)
|
# WORK_DIR Temp dir for build artifacts (default: $CACHE_DIR/work)
|
||||||
# CONFIG_FILE Test config JSON path (default: $WORK_DIR/config.json)
|
# CONFIG_FILE Test config JSON path (default: $WORK_DIR/config.json)
|
||||||
@@ -131,6 +136,56 @@ require_env() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Target node selection ───────────────────────────────────────────
|
||||||
|
# The chosen node is persisted to the shared mount: cleanup runs in a later
|
||||||
|
# job with its own environment and must aim at the node provision picked.
|
||||||
|
TARGET_NODE_FILE="$WORK_DIR/target-node"
|
||||||
|
|
||||||
|
# "node freeGiB" per online node; nodes without memory stats (token lacks
|
||||||
|
# Sys.Audit on /nodes) are omitted.
|
||||||
|
pve_free_nodes() {
|
||||||
|
curl -ksf -H "Authorization: PVEAPIToken=$PVE_API_TOKEN" \
|
||||||
|
"$PVE_ENDPOINT/api2/json/cluster/resources?type=node" |
|
||||||
|
jq -r '.data[]
|
||||||
|
| select(.status == "online" and .maxmem != null)
|
||||||
|
| "\(.node) \((.maxmem - .mem) / 1073741824 | floor)"'
|
||||||
|
}
|
||||||
|
|
||||||
|
# resolve_target_node <node-count>: sets PVE_TARGET_NODE (auto mode picks the
|
||||||
|
# online node with the most free memory) and refuses to provision onto a node
|
||||||
|
# without enough headroom — a polite CI failure beats OOM-killing a guest on
|
||||||
|
# the parent hypervisor.
|
||||||
|
resolve_target_node() {
|
||||||
|
local count="$1"
|
||||||
|
local need=$(( count * ${PVE_VM_MEM_GB:-8} + ${PVE_MEM_HEADROOM_GB:-8} ))
|
||||||
|
local nodes
|
||||||
|
nodes="$(pve_free_nodes || true)"
|
||||||
|
|
||||||
|
if [[ -z "${PVE_TARGET_NODE:-}" || "$PVE_TARGET_NODE" == "auto" ]]; then
|
||||||
|
if [[ -z "$nodes" ]]; then
|
||||||
|
ci_error "Node auto-selection needs memory stats from /cluster/resources — grant the token's user PVEAuditor on /nodes, or pin PVE_TARGET_NODE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
PVE_TARGET_NODE="$(sort -k2 -rn <<<"$nodes" | head -1 | cut -d' ' -f1)"
|
||||||
|
log "Auto-selected target node: $PVE_TARGET_NODE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local free
|
||||||
|
free="$(awk -v n="$PVE_TARGET_NODE" '$1 == n {print $2}' <<<"$nodes")"
|
||||||
|
if [[ -z "$free" ]]; then
|
||||||
|
log "WARNING: no memory stats for $PVE_TARGET_NODE (token lacks PVEAuditor on /nodes?) — skipping the headroom check"
|
||||||
|
elif (( free < need )); then
|
||||||
|
ci_error "Refusing to provision: $PVE_TARGET_NODE has ${free}GiB free, need ${need}GiB ($count VMs x ${PVE_VM_MEM_GB:-8}GiB + ${PVE_MEM_HEADROOM_GB:-8}GiB headroom)"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
log "Target node $PVE_TARGET_NODE: ${free}GiB free, need ${need}GiB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$WORK_DIR"
|
||||||
|
printf '%s' "$PVE_TARGET_NODE" > "$TARGET_NODE_FILE"
|
||||||
|
export PVE_TARGET_NODE
|
||||||
|
}
|
||||||
|
|
||||||
# ── Subcommands ─────────────────────────────────────────────────────
|
# ── Subcommands ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
cmd_provision() {
|
cmd_provision() {
|
||||||
@@ -151,8 +206,8 @@ cmd_provision() {
|
|||||||
log " Storage: Docker containers (iSCSI + NFS)"
|
log " Storage: Docker containers (iSCSI + NFS)"
|
||||||
require_env PVE_ENDPOINT
|
require_env PVE_ENDPOINT
|
||||||
require_env PVE_API_TOKEN
|
require_env PVE_API_TOKEN
|
||||||
require_env PVE_TARGET_NODE
|
|
||||||
require_env PVE_PASSWORD
|
require_env PVE_PASSWORD
|
||||||
|
resolve_target_node "$(wc -w <<<"$provision_nodes")"
|
||||||
|
|
||||||
ci_mask "$PVE_PASSWORD"
|
ci_mask "$PVE_PASSWORD"
|
||||||
mkdir -p "$WORK_DIR" "$CACHE_DIR"
|
mkdir -p "$WORK_DIR" "$CACHE_DIR"
|
||||||
@@ -233,8 +288,9 @@ cmd_provision() {
|
|||||||
log "Running Terraform init..."
|
log "Running Terraform init..."
|
||||||
(cd "$INFRA_DIR" && terraform init -input=false -reconfigure)
|
(cd "$INFRA_DIR" && terraform init -input=false -reconfigure)
|
||||||
|
|
||||||
# Always build tfvars for ALL versions to keep Terraform state consistent.
|
# tfvars must carry every version even on a subset run — a filtered var map
|
||||||
# When provisioning a subset, we use -target to limit the apply.
|
# makes terraform destroy the other versions' VMs still in state; subset
|
||||||
|
# applies are limited with -target only.
|
||||||
log "Building Terraform vars..."
|
log "Building Terraform vars..."
|
||||||
local tfvars="$WORK_DIR/instances.tfvars.json"
|
local tfvars="$WORK_DIR/instances.tfvars.json"
|
||||||
|
|
||||||
@@ -270,9 +326,6 @@ cmd_provision() {
|
|||||||
'{pve_instances: $pve_instances, pve_isos: $pve_isos}' > "$tfvars"
|
'{pve_instances: $pve_instances, pve_isos: $pve_isos}' > "$tfvars"
|
||||||
|
|
||||||
log "Running Terraform apply (PVE nodes)..."
|
log "Running Terraform apply (PVE nodes)..."
|
||||||
# Build -target flags when provisioning a subset of versions.
|
|
||||||
# This prevents Terraform from destroying VMs for other versions
|
|
||||||
# that exist in state but aren't in the filtered tfvars.
|
|
||||||
local tf_targets=""
|
local tf_targets=""
|
||||||
if [[ "$requested" != "all" ]]; then
|
if [[ "$requested" != "all" ]]; then
|
||||||
for v in $provision_versions; do
|
for v in $provision_versions; do
|
||||||
@@ -523,6 +576,9 @@ cmd_cleanup() {
|
|||||||
|
|
||||||
require_env PVE_ENDPOINT
|
require_env PVE_ENDPOINT
|
||||||
require_env PVE_API_TOKEN
|
require_env PVE_API_TOKEN
|
||||||
|
if [[ -z "${PVE_TARGET_NODE:-}" || "$PVE_TARGET_NODE" == "auto" ]]; then
|
||||||
|
PVE_TARGET_NODE="$(cat "$TARGET_NODE_FILE" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
require_env PVE_TARGET_NODE
|
require_env PVE_TARGET_NODE
|
||||||
|
|
||||||
# Discover Docker host IP for the docker_host_ip variable
|
# Discover Docker host IP for the docker_host_ip variable
|
||||||
@@ -591,7 +647,7 @@ cmd_cleanup() {
|
|||||||
|
|
||||||
# Clean up work directory when destroying all
|
# Clean up work directory when destroying all
|
||||||
if [[ "$requested" == "all" ]]; then
|
if [[ "$requested" == "all" ]]; then
|
||||||
rm -f "$CONFIG_FILE" "$WORK_DIR"/instances.tfvars.json
|
rm -f "$CONFIG_FILE" "$WORK_DIR"/instances.tfvars.json "$TARGET_NODE_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "Cleanup complete."
|
log "Cleanup complete."
|
||||||
@@ -609,6 +665,11 @@ cmd_force_cleanup() {
|
|||||||
|
|
||||||
log "Force cleanup — bypassing Terraform, using direct API calls..."
|
log "Force cleanup — bypassing Terraform, using direct API calls..."
|
||||||
|
|
||||||
|
if [[ -z "${PVE_TARGET_NODE:-}" || "$PVE_TARGET_NODE" == "auto" ]]; then
|
||||||
|
PVE_TARGET_NODE="$(cat "$TARGET_NODE_FILE" 2>/dev/null || true)"
|
||||||
|
[[ -n "$PVE_TARGET_NODE" ]] && export PVE_TARGET_NODE
|
||||||
|
fi
|
||||||
|
|
||||||
# Destroy VMs via the PVE API (works even with broken Terraform state)
|
# Destroy VMs via the PVE API (works even with broken Terraform state)
|
||||||
# Track which versions we've already cleaned up ISOs for (generic ISOs are shared)
|
# Track which versions we've already cleaned up ISOs for (generic ISOs are shared)
|
||||||
local cleaned_iso_versions=""
|
local cleaned_iso_versions=""
|
||||||
@@ -644,7 +705,7 @@ cmd_force_cleanup() {
|
|||||||
rm -rf "$INFRA_DIR/.terraform"
|
rm -rf "$INFRA_DIR/.terraform"
|
||||||
|
|
||||||
# Remove work artifacts
|
# Remove work artifacts
|
||||||
rm -f "$CONFIG_FILE" "$WORK_DIR"/instances.tfvars.json
|
rm -f "$CONFIG_FILE" "$WORK_DIR"/instances.tfvars.json "$TARGET_NODE_FILE"
|
||||||
|
|
||||||
log "Force cleanup complete. Next provision will start from scratch."
|
log "Force cleanup complete. Next provision will start from scratch."
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user