Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,10 @@ because Silverblue includes Podman in its base deployment.

The repo-owned [VM end-to-end suite](tests/e2e/README.md) is the canonical
pre-release regression pass for mutable Linux and Windows 11 guests. It builds
a release-shaped archive from the current checkout, installs it in a clean
guest, drives guided setup and the management TUI through a real
a release-shaped archive from the current checkout and supports two tiers. The
default product tier starts from a certified warm runtime and focuses on
management and lifecycle behavior. The onboarding tier starts without mutable
runtime prerequisites and drives guided setup through a real
pseudo-terminal, and then runs the portable contract and unattended hardware
lifecycle against the same binary. It records exact visible wording at
semantic checkpoints plus JSON and JUnit evidence. It uses the tiny hardware
Expand All @@ -213,14 +215,19 @@ substitutes the fixture result for a production-image upgrade test.

```sh
export OMNIDECK_VM_LAB_DIR=/absolute/path/to/omnideck-release-lab
make vm-e2e-matrix YES=1 # Canonical complete, deterministic regression
make vm-e2e # Single Ubuntu lane; confirms reset interactively
make vm-e2e VM=windows # Single Windows UAC/reboot/Podman/TUI lane
./tests/e2e/matrix.sh --suite product --yes # Fast default product regression
./tests/e2e/matrix.sh --suite onboarding --yes # Prerequisite/UAC/reboot coverage
./tests/e2e/matrix.sh --suite all --yes # Complete release qualification
./tests/e2e/run.sh --suite product --vm appimage
./tests/e2e/run.sh --suite onboarding --vm windows --yes
```

The matrix is the canonical release-regression command. It preflights the
`release-clean` profile, prepares one content-addressed build before requesting
a guest, leases lanes in deterministic order, and restores every clean golden.
The matrix prepares one content-addressed build before requesting a guest,
leases lanes in deterministic order, transfers one verified payload bundle per
lane, and restores the selected certified baseline. Product uses
`product-ready`; onboarding uses `onboarding-clean`. Ctrl-C or termination
stops the active lane, waits for lease cleanup, records that lane as canceled,
and does not continue into another guest.
The Windows lane exercises the real UAC prompt, selects restart later, verifies
a complete controlled reboot, installs Podman, and continues setup. The Windows
restart-now RunOnce auto-reopen, macOS prompts, subjective visual checks, and
Expand Down
28 changes: 4 additions & 24 deletions engine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,16 +231,15 @@ func TestBuildPodmanRunArgsWindows(t *testing.T) {
Platform: "windows",
}

args := buildPodmanRunArgs(opts, "192.168.127.254")
args := buildPodmanRunArgs(opts)
assertContainsPrefix(t, args, "OLLAMA_HOST=http://host.containers.internal:11434")
assertContains(t, args, "--add-host")
assertContains(t, args, "host.containers.internal:192.168.127.254")
assertNotContains(t, args, "--add-host")
if got := defaultOllamaURL("podman", "windows"); got != "http://host.containers.internal:11434" {
t.Fatalf("Windows Podman Ollama URL = %q", got)
}
}

func TestBuildPodmanRunArgsDoesNotAddWindowsHostOverrideOnOtherPlatforms(t *testing.T) {
func TestBuildPodmanRunArgsDoesNotOverridePodmanMachineHostAlias(t *testing.T) {
for _, platform := range []string{"darwin", "linux"} {
opts := RunOptions{
Name: "omnideck",
Expand All @@ -250,27 +249,8 @@ func TestBuildPodmanRunArgsDoesNotAddWindowsHostOverrideOnOtherPlatforms(t *test
StateVolume: "omnideck-state",
Platform: platform,
}
args := buildPodmanRunArgs(opts, "192.168.127.254")
args := buildPodmanRunArgs(opts)
assertNotContains(t, args, "--add-host")
assertNotContains(t, args, "host.containers.internal:192.168.127.254")
}
}

func TestParseWindowsPodmanHostAddress(t *testing.T) {
tests := []struct {
output string
want string
}{
{"default via 192.168.127.1 dev podman-usermode\n", "192.168.127.1"},
{"192.168.127.254 STREAM host.containers.internal\n", "192.168.127.254"},
{"192.168.127.254 DGRAM\n192.168.127.254 RAW\n", "192.168.127.254"},
{"999.168.127.254 STREAM invalid\n", ""},
{"no address", ""},
}
for _, tt := range tests {
if got := parseWindowsPodmanHostAddress(tt.output); got != tt.want {
t.Errorf("parseWindowsPodmanHostAddress(%q) = %q, want %q", tt.output, got, tt.want)
}
}
}

Expand Down
56 changes: 2 additions & 54 deletions engine/podman.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package engine
import (
"fmt"
"io"
"net"
"os"
"os/exec"
"strings"
Expand Down Expand Up @@ -158,16 +157,7 @@ func newAnonymousRegistryAuthFile() (string, func(), error) {
}

func (e *PodmanEngine) RunContainer(opts RunOptions) error {
windowsHostAddress := ""
if opts.Platform == "windows" && usesPodmanHostAlias(opts.OllamaHost, opts.Platform) {
address, err := resolveWindowsPodmanHostAddress()
if err != nil {
return err
}
windowsHostAddress = address
}

args := buildPodmanRunArgs(opts, windowsHostAddress)
args := buildPodmanRunArgs(opts)
cmd := buildCmd("podman", args...)
out, err := cmd.CombinedOutput()
if err != nil {
Expand All @@ -176,44 +166,6 @@ func (e *PodmanEngine) RunContainer(opts RunOptions) error {
return nil
}

// resolveWindowsPodmanHostAddress asks the shared WSL-backed Podman machine
// for the default-route gateway it uses to reach Windows. A stock Podman 6 WSL
// machine does not define host.containers.internal, and rootless pasta assigns
// that name a container-local address, so Windows containers need this
// explicit gateway mapping.
func resolveWindowsPodmanHostAddress() (string, error) {
cmd := buildCmd(
"podman",
"machine", "ssh", OmnideckMachineName,
"ip", "-4", "route", "show", "default",
)
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf(
"Omnideck could not determine how the application container reaches Windows. Restart the %s Podman machine, then try again: %w",
OmnideckMachineName,
runtimeCommandError("Windows host address", err, out),
)
}
address := parseWindowsPodmanHostAddress(string(out))
if address == "" {
return "", fmt.Errorf(
"Omnideck could not determine how the application container reaches Windows. Restart the %s Podman machine, then try again",
OmnideckMachineName,
)
}
return address, nil
}

func parseWindowsPodmanHostAddress(output string) string {
for _, field := range strings.Fields(output) {
if address := net.ParseIP(field); address != nil && address.To4() != nil {
return address.String()
}
}
return ""
}

func (e *PodmanEngine) CheckOllamaConnection(name string) error {
return checkContainerOllama("podman", name)
}
Expand Down Expand Up @@ -279,7 +231,7 @@ func (e *PodmanEngine) ImageDigest(image string) string {

// buildPodmanRunArgs builds args for `podman run`. It deliberately avoids
// --replace so a name collision can never remove an unrelated container.
func buildPodmanRunArgs(opts RunOptions, windowsHostAddress ...string) []string {
func buildPodmanRunArgs(opts RunOptions) []string {
restart := opts.Restart
if restart == "" {
restart = "always"
Expand All @@ -300,10 +252,6 @@ func buildPodmanRunArgs(opts RunOptions, windowsHostAddress ...string) []string
if opts.Memory != "" {
args = append(args, "--memory="+opts.Memory)
}
if opts.Platform == "windows" && len(windowsHostAddress) > 0 && windowsHostAddress[0] != "" {
args = append(args, "--add-host", podmanHostAlias+":"+windowsHostAddress[0])
}

// The web UI is private to this computer. Desktop embeds this address and
// the CLI prints it for a local browser; neither use case should expose the
// agent on the LAN merely because Podman's default bind is 0.0.0.0.
Expand Down
7 changes: 7 additions & 0 deletions release-notes.d/windows-container-host-routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
type: fixed
area: runtime
---

Let Podman resolve its Windows container-to-host gateway so installed instances
remain reachable through `host.containers.internal`.
44 changes: 28 additions & 16 deletions tests/e2e/guest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ set -Eeuo pipefail
work_dir="${1:?guest work directory is required}"
expected_version="${2:?expected version is required}"
fixture_image="${3:?fixture image is required}"
test_tier="${4:?test tier is required}"
case "$test_tier" in product|onboarding) ;; *) printf 'Unknown test tier: %s\n' "$test_tier" >&2; exit 2 ;; esac
result_dir="${work_dir}/results"
archive="${work_dir}/omnideck-linux-amd64.tar.gz"
checksum_file="${work_dir}/SHA256SUMS"
Expand Down Expand Up @@ -83,10 +85,14 @@ trap write_evidence EXIT

current_step="clean-host precondition"
inventory before
if command -v podman >/dev/null 2>&1; then
if [[ "$test_tier" == onboarding ]] && command -v podman >/dev/null 2>&1; then
printf 'The install scenario requires a clean mutable guest with Podman absent.\n' >&2
exit 1
fi
if [[ "$test_tier" == product ]]; then
command -v podman >/dev/null 2>&1 || { printf 'The product scenario requires Podman in the certified baseline.\n' >&2; exit 1; }
podman info >/dev/null
fi
[[ ! -e "${config_dir}/instances/omnideck.yaml" ]] || {
printf 'The isolated test configuration unexpectedly contains an existing instance.\n' >&2
exit 1
Expand Down Expand Up @@ -121,25 +127,31 @@ location = "${fixture_image%%/*}"
insecure = true
EOF

current_step="guided install journey"
current_step="${test_tier} install journey"
# SSH does not have a graphical PolicyKit agent. Hide pkexec only for this
# disposable terminal journey so the CLI takes its documented sudo fallback;
# the trap restores the exact file before the guest is inventoried or reset.
pkexec_path="$(command -v pkexec || true)"
if [[ -n "${pkexec_path}" ]]; then
pkexec_backup="${pkexec_path}.omnideck-e2e-disabled"
[[ ! -e "${pkexec_backup}" ]]
sudo mv -- "${pkexec_path}" "${pkexec_backup}"
printf 'Temporarily hid %s to exercise terminal sudo fallback.\n' "${pkexec_path}" \
> "${result_dir}/terminal-elevation.txt"
if [[ "$test_tier" == onboarding ]]; then
pkexec_path="$(command -v pkexec || true)"
if [[ -n "${pkexec_path}" ]]; then
pkexec_backup="${pkexec_path}.omnideck-e2e-disabled"
[[ ! -e "${pkexec_backup}" ]]
sudo mv -- "${pkexec_path}" "${pkexec_backup}"
printf 'Temporarily hid %s to exercise terminal sudo fallback.\n' "${pkexec_path}" \
> "${result_dir}/terminal-elevation.txt"
fi
env PATH="${work_dir}/elevation-bin:${PATH}" python3 "${work_dir}/terminal_driver.py" install \
--binary "${binary}" \
--config-dir "${config_dir}" \
--registries-conf "${registries_conf}" \
--fixture-image "${fixture_image}" \
--artifact-dir "${result_dir}"
restore_pkexec
else
env OMNIDECK_CONFIG_DIR="${config_dir}" CONTAINERS_REGISTRIES_CONF="${registries_conf}" \
"${binary}" --no-color install --plain --name omnideck --image "${fixture_image}" \
| tee "${result_dir}/install-plain.txt"
fi
env PATH="${work_dir}/elevation-bin:${PATH}" python3 "${work_dir}/terminal_driver.py" install \
--binary "${binary}" \
--config-dir "${config_dir}" \
--registries-conf "${registries_conf}" \
--fixture-image "${fixture_image}" \
--artifact-dir "${result_dir}"
restore_pkexec

current_step="installed behavior"
env OMNIDECK_CONFIG_DIR="${config_dir}" CONTAINERS_REGISTRIES_CONF="${registries_conf}" \
Expand Down
50 changes: 41 additions & 9 deletions tests/e2e/matrix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,30 @@ repo_root="$(cd "${script_dir}/../.." && pwd)"
source "${script_dir}/_common.sh"
lanes_csv="${OMNIDECK_VM_E2E_LANES:-appimage,deb,rpm,windows}"
assume_yes=0
suite="${OMNIDECK_VM_E2E_SUITE:-product}"

while (($#)); do
case "$1" in
--lanes) lanes_csv="${2:?--lanes requires a value}"; shift 2 ;;
--suite) suite="${2:?--suite requires a value}"; shift 2 ;;
--yes) assume_yes=1; shift ;;
-h|--help) printf 'Usage: %s [--lanes appimage,deb,rpm,windows] [--yes]\n' "$0"; exit 0 ;;
-h|--help) printf 'Usage: %s [--suite product|onboarding|all] [--lanes appimage,deb,rpm,windows] [--yes]\n' "$0"; exit 0 ;;
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 2 ;;
esac
done

case "$suite" in product|onboarding|all) ;; *) printf 'Unsupported suite: %s\n' "$suite" >&2; exit 2 ;; esac

require_lab
IFS=',' read -r -a lanes <<<"$lanes_csv"
for lane in "${lanes[@]}"; do
case "$lane" in appimage|deb|rpm|windows) ;; *) printf 'Unsupported lane: %s\n' "$lane" >&2; exit 2 ;; esac
done
"${lab_dir}/lab.sh" preflight cli release-clean --lanes "$lanes_csv" >/dev/null
if [[ "$suite" == all ]]; then tiers=(product onboarding); else tiers=("$suite"); fi
for tier in "${tiers[@]}"; do
if [[ "$tier" == product ]]; then profile=product-ready; else profile=onboarding-clean; fi
"${lab_dir}/lab.sh" preflight cli "$profile" --lanes "$lanes_csv" >/dev/null
done

run_id="$(date -u +%Y%m%dT%H%M%SZ)-$$"
run_root="$("${lab_dir}/lab.sh" artifact-path cli matrix "$run_id")"
Expand All @@ -31,8 +39,10 @@ mkdir -p "${run_root}/lanes"
: > "$status_file"
source_state
"${lab_dir}/lab.sh" evidence-init "$run_root" cli matrix "$run_id" "$source_short" multi clean \
"phase=prepared" "lanes=${lanes_csv}" "sourceDirty=${source_dirty}" "sourceFingerprint=${source_fingerprint}"
"phase=prepared" "testTier=${suite}" "lanes=${lanes_csv}" "sourceDirty=${source_dirty}" "sourceFingerprint=${source_fingerprint}"
finalized=0
matrix_signal=0
active_lane_pid=""
finish_incomplete() {
local exit_status=$?
set +e
Expand All @@ -43,22 +53,44 @@ finish_incomplete() {
return "$exit_status"
}
trap finish_incomplete EXIT
interrupt_matrix() {
local status="$1" signal_name="$2"
matrix_signal="$status"
if [[ "$signal_name" == INT ]]; then signal_name=TERM; fi
if [[ "$active_lane_pid" =~ ^[0-9]+$ ]] && kill -0 "$active_lane_pid" 2>/dev/null; then
kill -s "$signal_name" "$active_lane_pid" 2>/dev/null || true
fi
}
trap 'interrupt_matrix 130 INT' INT
trap 'interrupt_matrix 143 TERM' TERM

status=0
for lane in "${lanes[@]}"; do
arguments=(--vm "$lane")
for tier in "${tiers[@]}"; do
for lane in "${lanes[@]}"; do
arguments=(--vm "$lane" --suite "$tier")
[[ "$assume_yes" == 0 ]] || arguments+=(--yes)
lane_status=0
lane_dir="${run_root}/lanes/${lane}"
lane_key="${tier}-${lane}"
lane_dir="${run_root}/lanes/${lane_key}"
mkdir -p "$lane_dir"
OMNIDECK_VM_E2E_OUTPUT_DIR="$lane_dir" \
"$script_dir/run.sh" "${arguments[@]}" > >(tee "${lane_dir}/host.log") 2>&1 || lane_status=$?
"$script_dir/run.sh" "${arguments[@]}" > >(tee "${lane_dir}/host.log") 2>&1 &
active_lane_pid=$!
wait "$active_lane_pid" || lane_status=$?
if [[ "$matrix_signal" != 0 ]]; then
wait "$active_lane_pid" 2>/dev/null || true
active_lane_pid=""
printf '%s\tcanceled\tlanes/%s\n' "$lane_key" "$lane_key" >> "$status_file"
exit "$matrix_signal"
fi
active_lane_pid=""
if [[ "$lane_status" == 0 ]]; then
printf '%s\tpassed\tlanes/%s\n' "$lane" "$lane" >> "$status_file"
printf '%s\tpassed\tlanes/%s\n' "$lane_key" "$lane_key" >> "$status_file"
else
printf '%s\tfailed\tlanes/%s\n' "$lane" "$lane" >> "$status_file"
printf '%s\tfailed\tlanes/%s\n' "$lane_key" "$lane_key" >> "$status_file"
status=1
fi
done
done
if [[ "$status" == 0 ]]; then
"${lab_dir}/lab.sh" evidence-finish "$run_root" passed
Expand Down
Loading