Complete infrastructure platform CLI: - Container runtime (systemd-nspawn) - VoltVisor VMs (Neutron Stardust / QEMU) - Stellarium CAS (content-addressed storage) - ORAS Registry - GitOps integration - Landlock LSM security - Compose orchestration - Mesh networking Copyright (c) Armored Gates LLC. All rights reserved. Licensed under AGPSL v5.0
305 lines
12 KiB
Bash
Executable File
305 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# Volt Hybrid Integration Tests — Container Mode Lifecycle
|
|
#
|
|
# Tests the full lifecycle of a standard container workload:
|
|
# 1. Create container from manifest/image
|
|
# 2. Start and verify running (process visible, network reachable)
|
|
# 3. Execute a command inside the container
|
|
# 4. Stop gracefully
|
|
# 5. Destroy and verify cleanup
|
|
# 6. CAS dedup: two containers from same image share objects
|
|
#
|
|
# Requires: root, systemd-nspawn, base image at /var/lib/volt/images/ubuntu_24.04
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
set -uo pipefail
|
|
source "$(dirname "$0")/test_helpers.sh"
|
|
|
|
# ── Prerequisites ─────────────────────────────────────────────────────────────
|
|
|
|
require_root
|
|
require_volt
|
|
require_nspawn
|
|
|
|
BASE_IMAGE="/var/lib/volt/images/ubuntu_24.04"
|
|
if ! require_image "$BASE_IMAGE"; then
|
|
echo "SKIP: No base image. Run: sudo debootstrap noble $BASE_IMAGE http://archive.ubuntu.com/ubuntu"
|
|
exit 0
|
|
fi
|
|
|
|
trap cleanup_all EXIT
|
|
|
|
echo "⚡ Volt Hybrid Integration Tests — Container Mode Lifecycle"
|
|
echo "════════════════════════════════════════════════════════════════"
|
|
|
|
# ── 1. Create container ──────────────────────────────────────────────────────
|
|
|
|
section "📦 1. Create Container"
|
|
|
|
CON1=$(test_name "lifecycle")
|
|
|
|
output=$(create_container "$CON1" "$BASE_IMAGE" 2>&1)
|
|
assert_ok "Create container '$CON1'" test $? -eq 0
|
|
|
|
# Verify rootfs directory was created
|
|
assert_dir_exists "Container rootfs exists" "/var/lib/volt/containers/$CON1"
|
|
|
|
# Verify systemd unit file was written
|
|
assert_file_exists "Unit file exists" "/etc/systemd/system/volt-hybrid@${CON1}.service"
|
|
|
|
# Verify .nspawn config was written
|
|
assert_file_exists "Nspawn config exists" "/etc/systemd/nspawn/${CON1}.nspawn"
|
|
|
|
# Verify the unit file references boot mode
|
|
if grep -q "\-\-boot" "/etc/systemd/system/volt-hybrid@${CON1}.service" 2>/dev/null; then
|
|
pass "Unit file uses --boot mode"
|
|
else
|
|
fail "Unit file uses --boot mode" "expected --boot in unit file"
|
|
fi
|
|
|
|
# ── 2. Start and verify running ─────────────────────────────────────────────
|
|
|
|
section "🚀 2. Start Container"
|
|
|
|
output=$(start_workload "$CON1" 2>&1)
|
|
assert_ok "Start container '$CON1'" test $? -eq 0
|
|
|
|
# Wait for the container to actually be running
|
|
if wait_running "$CON1" 30; then
|
|
pass "Container reached running state"
|
|
else
|
|
fail "Container reached running state" "timed out after 30s"
|
|
fi
|
|
|
|
# Verify the container is visible in machinectl list
|
|
if sudo machinectl list --no-legend --no-pager 2>/dev/null | grep -q "$CON1"; then
|
|
pass "Container visible in machinectl list"
|
|
else
|
|
fail "Container visible in machinectl list"
|
|
fi
|
|
|
|
# Verify leader PID exists
|
|
LEADER_PID=$(get_leader_pid "$CON1")
|
|
if [[ -n "$LEADER_PID" && "$LEADER_PID" != "0" ]]; then
|
|
pass "Leader PID is set (PID=$LEADER_PID)"
|
|
else
|
|
fail "Leader PID is set" "got: '$LEADER_PID'"
|
|
fi
|
|
|
|
# Verify the leader PID is an actual process on the host
|
|
if [[ -n "$LEADER_PID" ]] && [[ -d "/proc/$LEADER_PID" ]]; then
|
|
pass "Leader PID is a real process on host"
|
|
else
|
|
fail "Leader PID is a real process on host"
|
|
fi
|
|
|
|
# Check if the container has an IP address (network reachable)
|
|
sleep 2 # give the network a moment to come up
|
|
CON1_IP=$(get_container_ip "$CON1")
|
|
if [[ -n "$CON1_IP" ]]; then
|
|
pass "Container has IP address ($CON1_IP)"
|
|
|
|
# Try to ping the container from the host
|
|
if ping -c 1 -W 3 "$CON1_IP" &>/dev/null; then
|
|
pass "Container is network-reachable (ping)"
|
|
else
|
|
skip "Container is network-reachable (ping)" "bridge may not be configured"
|
|
fi
|
|
else
|
|
skip "Container has IP address" "no IP assigned (bridge may not exist)"
|
|
fi
|
|
|
|
# Verify container appears in volt container list
|
|
if sudo "$VOLT" container list --backend hybrid 2>/dev/null | grep -q "$CON1"; then
|
|
pass "Container visible in 'volt container list'"
|
|
else
|
|
# May also appear without --backend flag
|
|
if sudo "$VOLT" container list 2>/dev/null | grep -q "$CON1"; then
|
|
pass "Container visible in 'volt container list'"
|
|
else
|
|
fail "Container visible in 'volt container list'"
|
|
fi
|
|
fi
|
|
|
|
# ── 3. Exec command inside container ────────────────────────────────────────
|
|
|
|
section "🔧 3. Execute Command Inside Container"
|
|
|
|
# Simple command — check hostname
|
|
hostname_out=$(exec_in "$CON1" hostname 2>&1) || true
|
|
if [[ -n "$hostname_out" ]]; then
|
|
pass "exec hostname returns output ('$hostname_out')"
|
|
else
|
|
fail "exec hostname returns output" "empty output"
|
|
fi
|
|
|
|
# Check that /etc/os-release is readable
|
|
if exec_in "$CON1" cat /etc/os-release 2>/dev/null | grep -qi "ubuntu"; then
|
|
pass "exec cat /etc/os-release shows Ubuntu"
|
|
else
|
|
fail "exec cat /etc/os-release shows Ubuntu"
|
|
fi
|
|
|
|
# Create a test file and verify it persists
|
|
exec_in "$CON1" sh -c "echo 'volt-test-marker' > /tmp/test-exec-file" 2>/dev/null || true
|
|
if exec_in "$CON1" cat /tmp/test-exec-file 2>/dev/null | grep -q "volt-test-marker"; then
|
|
pass "exec can create and read files inside container"
|
|
else
|
|
fail "exec can create and read files inside container"
|
|
fi
|
|
|
|
# Verify environment variable is set
|
|
if exec_in "$CON1" env 2>/dev/null | grep -q "VOLT_CONTAINER=$CON1"; then
|
|
pass "VOLT_CONTAINER env var is set inside container"
|
|
else
|
|
skip "VOLT_CONTAINER env var is set inside container" "may not be injected yet"
|
|
fi
|
|
|
|
if exec_in "$CON1" env 2>/dev/null | grep -q "VOLT_RUNTIME=hybrid"; then
|
|
pass "VOLT_RUNTIME=hybrid env var is set"
|
|
else
|
|
skip "VOLT_RUNTIME=hybrid env var is set" "may not be injected yet"
|
|
fi
|
|
|
|
# ── 4. Stop gracefully ──────────────────────────────────────────────────────
|
|
|
|
section "⏹️ 4. Stop Container"
|
|
|
|
output=$(stop_workload "$CON1" 2>&1)
|
|
assert_ok "Stop container '$CON1'" test $? -eq 0
|
|
|
|
# Verify the container is no longer running
|
|
sleep 2
|
|
if ! sudo machinectl show "$CON1" --property=State 2>/dev/null | grep -q "running"; then
|
|
pass "Container is no longer running after stop"
|
|
else
|
|
fail "Container is no longer running after stop"
|
|
fi
|
|
|
|
# Verify the leader PID is gone
|
|
if [[ -n "$LEADER_PID" ]] && [[ ! -d "/proc/$LEADER_PID" ]]; then
|
|
pass "Leader PID ($LEADER_PID) is gone after stop"
|
|
else
|
|
if [[ -z "$LEADER_PID" ]]; then
|
|
skip "Leader PID is gone after stop" "no PID was recorded"
|
|
else
|
|
fail "Leader PID ($LEADER_PID) is gone after stop" "process still exists"
|
|
fi
|
|
fi
|
|
|
|
# Verify rootfs still exists (stop should not destroy data)
|
|
assert_dir_exists "Rootfs still exists after stop" "/var/lib/volt/containers/$CON1"
|
|
|
|
# ── 5. Destroy and verify cleanup ───────────────────────────────────────────
|
|
|
|
section "🗑️ 5. Destroy Container"
|
|
|
|
output=$(destroy_workload "$CON1" 2>&1)
|
|
assert_ok "Destroy container '$CON1'" test $? -eq 0
|
|
|
|
# Verify rootfs is gone
|
|
if [[ ! -d "/var/lib/volt/containers/$CON1" ]]; then
|
|
pass "Rootfs removed after destroy"
|
|
else
|
|
fail "Rootfs removed after destroy" "directory still exists"
|
|
fi
|
|
|
|
# Verify unit file is removed
|
|
if [[ ! -f "/etc/systemd/system/volt-hybrid@${CON1}.service" ]]; then
|
|
pass "Unit file removed after destroy"
|
|
else
|
|
fail "Unit file removed after destroy"
|
|
fi
|
|
|
|
# Verify .nspawn config is removed
|
|
if [[ ! -f "/etc/systemd/nspawn/${CON1}.nspawn" ]]; then
|
|
pass "Nspawn config removed after destroy"
|
|
else
|
|
fail "Nspawn config removed after destroy"
|
|
fi
|
|
|
|
# Verify container no longer appears in any listing
|
|
if ! sudo machinectl list --no-legend --no-pager 2>/dev/null | grep -q "$CON1"; then
|
|
pass "Container gone from machinectl list"
|
|
else
|
|
fail "Container gone from machinectl list"
|
|
fi
|
|
|
|
# Remove from cleanup list since we destroyed manually
|
|
CLEANUP_WORKLOADS=("${CLEANUP_WORKLOADS[@]/$CON1/}")
|
|
|
|
# ── 6. CAS Dedup — Two containers from same image ───────────────────────────
|
|
|
|
section "🔗 6. CAS Dedup Verification"
|
|
|
|
CON_A=$(test_name "dedup-a")
|
|
CON_B=$(test_name "dedup-b")
|
|
|
|
create_container "$CON_A" "$BASE_IMAGE" 2>&1 >/dev/null
|
|
assert_ok "Create first container for dedup test" test $? -eq 0
|
|
|
|
create_container "$CON_B" "$BASE_IMAGE" 2>&1 >/dev/null
|
|
assert_ok "Create second container for dedup test" test $? -eq 0
|
|
|
|
# Both should have rootfs directories
|
|
assert_dir_exists "Container A rootfs exists" "/var/lib/volt/containers/$CON_A"
|
|
assert_dir_exists "Container B rootfs exists" "/var/lib/volt/containers/$CON_B"
|
|
|
|
# If CAS is in use, check for shared objects in the CAS store
|
|
CAS_DIR="/var/lib/volt/cas/objects"
|
|
if [[ -d "$CAS_DIR" ]]; then
|
|
# Count objects — two identical images should share all CAS objects
|
|
CAS_COUNT=$(find "$CAS_DIR" -type f 2>/dev/null | wc -l)
|
|
if [[ $CAS_COUNT -gt 0 ]]; then
|
|
pass "CAS objects exist ($CAS_COUNT objects)"
|
|
|
|
# Check CAS refs for both containers
|
|
if [[ -d "/var/lib/volt/cas/refs" ]]; then
|
|
REFS_A=$(find /var/lib/volt/cas/refs -name "*$CON_A*" 2>/dev/null | wc -l)
|
|
REFS_B=$(find /var/lib/volt/cas/refs -name "*$CON_B*" 2>/dev/null | wc -l)
|
|
if [[ $REFS_A -gt 0 && $REFS_B -gt 0 ]]; then
|
|
pass "Both containers have CAS refs"
|
|
else
|
|
skip "Both containers have CAS refs" "CAS refs not found (may use direct copy)"
|
|
fi
|
|
else
|
|
skip "CAS refs directory check" "no refs dir"
|
|
fi
|
|
else
|
|
skip "CAS dedup objects" "CAS store empty — may use direct copy instead"
|
|
fi
|
|
else
|
|
skip "CAS dedup verification" "CAS not active (containers use direct rootfs copy)"
|
|
fi
|
|
|
|
# Verify both containers are independent (different rootfs paths)
|
|
if [[ "/var/lib/volt/containers/$CON_A" != "/var/lib/volt/containers/$CON_B" ]]; then
|
|
pass "Containers have independent rootfs paths"
|
|
else
|
|
fail "Containers have independent rootfs paths"
|
|
fi
|
|
|
|
# Verify the rootfs contents are identical (same image, same content)
|
|
# Compare a few key files
|
|
for f in "etc/os-release" "usr/bin/env"; do
|
|
if [[ -f "/var/lib/volt/containers/$CON_A/$f" ]] && [[ -f "/var/lib/volt/containers/$CON_B/$f" ]]; then
|
|
if diff -q "/var/lib/volt/containers/$CON_A/$f" "/var/lib/volt/containers/$CON_B/$f" &>/dev/null; then
|
|
pass "Identical content: $f"
|
|
else
|
|
fail "Identical content: $f" "files differ"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Cleanup dedup containers
|
|
destroy_workload "$CON_A"
|
|
destroy_workload "$CON_B"
|
|
CLEANUP_WORKLOADS=("${CLEANUP_WORKLOADS[@]/$CON_A/}")
|
|
CLEANUP_WORKLOADS=("${CLEANUP_WORKLOADS[@]/$CON_B/}")
|
|
|
|
# ── Results ──────────────────────────────────────────────────────────────────
|
|
|
|
print_results "Container Mode Lifecycle"
|
|
exit $?
|