diff --git a/TESTING.md b/TESTING.md index 20975ff..59e4a8d 100644 --- a/TESTING.md +++ b/TESTING.md @@ -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 @@ -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 diff --git a/engine/engine_test.go b/engine/engine_test.go index e7bf659..367403e 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -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", @@ -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) - } } } diff --git a/engine/podman.go b/engine/podman.go index e621b6c..ba18fcf 100644 --- a/engine/podman.go +++ b/engine/podman.go @@ -3,7 +3,6 @@ package engine import ( "fmt" "io" - "net" "os" "os/exec" "strings" @@ -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 { @@ -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) } @@ -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" @@ -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. diff --git a/release-notes.d/windows-container-host-routing.md b/release-notes.d/windows-container-host-routing.md new file mode 100644 index 0000000..236c3fe --- /dev/null +++ b/release-notes.d/windows-container-host-routing.md @@ -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`. diff --git a/tests/e2e/guest.sh b/tests/e2e/guest.sh index 8aadd5a..fad9729 100755 --- a/tests/e2e/guest.sh +++ b/tests/e2e/guest.sh @@ -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" @@ -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 @@ -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}" \ diff --git a/tests/e2e/matrix.sh b/tests/e2e/matrix.sh index e8a39d9..6724d91 100755 --- a/tests/e2e/matrix.sh +++ b/tests/e2e/matrix.sh @@ -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")" @@ -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 @@ -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 diff --git a/tests/e2e/run-windows.sh b/tests/e2e/run-windows.sh index 4952dc4..b2654f3 100755 --- a/tests/e2e/run-windows.sh +++ b/tests/e2e/run-windows.sh @@ -8,12 +8,13 @@ source "${script_dir}/_common.sh" builder_image="${OMNIDECK_CLI_BUILDER_IMAGE:-omnideck-cli-builder:local}" assume_yes=0 keep_vm=0 +suite="${OMNIDECK_VM_E2E_SUITE:-product}" vm=windows original_args=("$@") usage() { cat <<'EOF' -Usage: ./tests/e2e/run-windows.sh [--yes] [--keep-vm] +Usage: ./tests/e2e/run-windows.sh [--suite product|onboarding] [--yes] [--keep-vm] Build the release-shaped Windows CLI ZIP, reset and boot only the disposable Windows lab guest, approve its real UAC prompt, exercise the required reboot, @@ -27,6 +28,10 @@ while (($#)); do assume_yes=1 shift ;; + --suite) + suite="${2:?--suite requires a value}" + shift 2 + ;; --keep-vm) keep_vm=1 shift @@ -42,24 +47,26 @@ while (($#)); do ;; esac done +case "$suite" in product) profile=product-ready ;; onboarding) profile=onboarding-clean ;; *) printf 'Unsupported suite: %s\n' "$suite" >&2; exit 2 ;; esac require_lab +baseline="$("${lab_dir}/lab.sh" profile "$profile" windows)" for dependency in docker curl ssh python3 openssl socat zip unzip; do command -v "${dependency}" >/dev/null 2>&1 || { printf '%s is required by the Windows VM E2E lane.\n' "${dependency}" >&2; exit 2; } done if [[ "${OMNIDECK_VM_LAB_LEASED:-}" != "1" ]]; then lease_run_id="$(date -u +%Y%m%dT%H%M%SZ)-$$" - "${lab_dir}/lab.sh" preflight cli release-clean --lanes windows >/dev/null + "${lab_dir}/lab.sh" preflight cli "$profile" --lanes windows >/dev/null source_state prepare_output_dir="${OMNIDECK_VM_E2E_OUTPUT_DIR:-$("${lab_dir}/lab.sh" artifact-path cli e2e "${lease_run_id}")}" mkdir -p "${prepare_output_dir}" "${lab_dir}/lab.sh" evidence-init "${prepare_output_dir}" cli e2e "${lease_run_id}" \ - "${source_short}" windows clean "phase=preparing" "sourceDirty=${source_dirty}" "sourceFingerprint=${source_fingerprint}" + "${source_short}" windows "$baseline" "phase=preparing" "testTier=${suite}" "sourceDirty=${source_dirty}" "sourceFingerprint=${source_fingerprint}" trap '"${lab_dir}/lab.sh" evidence-finish "${prepare_output_dir}" failed || true' EXIT prepare_cli_binaries windows "${lab_dir}/lab.sh" evidence-set "${prepare_output_dir}" "phase=prepared" "buildCacheKey=${cli_build_key}" - lease_args=(lease windows cli "${lease_run_id}" --cleanup-baseline clean) + lease_args=(lease windows cli "${lease_run_id}" --cleanup-baseline "$baseline") [[ "${keep_vm}" != "1" ]] || lease_args+=(--keep-state) lease_args+=(-- env OMNIDECK_CLI_BUILD_CACHE="${cli_build_cache}" OMNIDECK_CLI_BUILD_KEY="${cli_build_key}" \ OMNIDECK_VM_E2E_OUTPUT_DIR="${prepare_output_dir}" "$0" "${original_args[@]}") @@ -129,7 +136,7 @@ if [[ -f "${output_dir}/run.json" ]]; then "${lab_dir}/lab.sh" evidence-set "${output_dir}" "phase=executing" "expectedVersion=${expected_version}" "fixtureImage=${fixture_guest}" else "${lab_dir}/lab.sh" evidence-init "${output_dir}" cli e2e "${safe_run_id}" \ - "${source_commit}" windows clean "expectedVersion=${expected_version}" "fixtureImage=${fixture_guest}" \ + "${source_commit}" windows "$baseline" "expectedVersion=${expected_version}" "fixtureImage=${fixture_guest}" "testTier=${suite}" \ "sourceDirty=${source_dirty}" "sourceFingerprint=${source_fingerprint}" \ "buildCacheKey=${OMNIDECK_CLI_BUILD_KEY}" fi @@ -166,7 +173,7 @@ cleanup() { vm_started=0 fi if [[ "${initial_reset}" == "1" && "${keep_vm}" != "1" ]]; then - "${lab_dir}/lab.sh" reset windows clean || exit_code=1 + "${lab_dir}/lab.sh" reset windows "$baseline" || exit_code=1 elif [[ "${keep_vm}" == "1" ]]; then printf 'Windows guest kept stopped for debugging.\n' fi @@ -181,8 +188,8 @@ cleanup() { } trap cleanup EXIT -printf 'Resetting the leased Windows guest to its clean golden.\n' -"${lab_dir}/lab.sh" reset windows clean +printf 'Resetting the leased Windows guest to its %s baseline.\n' "$baseline" +"${lab_dir}/lab.sh" reset windows "$baseline" initial_reset=1 printf 'Using prepared CLI build cache: %s\n' "${OMNIDECK_CLI_BUILD_KEY}" @@ -223,23 +230,25 @@ curl --fail --silent --max-time 2 --cacert "${build_dir}/tls/registry.crt" \ --resolve "host.containers.internal:${tls_port}:127.0.0.1" \ "https://host.containers.internal:${tls_port}/v2/" >/dev/null -printf 'Starting and verifying the clean Windows guest.\n' +printf 'Starting and verifying the %s Windows guest.\n' "$suite" "${lab_dir}/lab.sh" start windows vm_started=1 "${lab_dir}/lab.sh" wait windows "${lab_dir}/lab.sh" verify windows | tee "${output_dir}/guest-verify-before.txt" -grep -Fq 'podman=absent' "${output_dir}/guest-verify-before.txt" - -"${lab_dir}/lab.sh" run windows "cmd.exe /d /c if not exist ${remote_root} mkdir ${remote_root}" +if [[ "$suite" == onboarding ]]; then grep -Fq 'podman=absent' "${output_dir}/guest-verify-before.txt"; else grep -Fq 'podman=' "${output_dir}/guest-verify-before.txt" && ! grep -Fq 'podman=absent' "${output_dir}/guest-verify-before.txt"; fi + +payload_dir="${build_dir}/payload" +mkdir -p "$payload_dir" +install -m 0644 "${build_dir}/omnideck-windows-amd64.zip" "${payload_dir}/omnideck-windows-amd64.zip" +install -m 0644 "${build_dir}/SHA256SUMS" "${payload_dir}/SHA256SUMS" +install -m 0755 "${build_dir}/releasecontract.exe" "${payload_dir}/releasecontract.exe" +install -m 0644 "${build_dir}/contracts.tar.gz" "${payload_dir}/contracts.tar.gz" +install -m 0644 "${script_dir}/windows_guest.ps1" "${payload_dir}/windows_guest.ps1" +install -m 0644 "${script_dir}/windows_registry.ps1" "${payload_dir}/windows_registry.ps1" +install -m 0644 "${repo_root}/tests/hardware/run.ps1" "${payload_dir}/hardware-run.ps1" +install -m 0644 "${build_dir}/tls/registry.crt" "${payload_dir}/registry.crt" +"${lab_dir}/lab.sh" stage windows "$payload_dir" "$remote_root" | tee "${output_dir}/payload-stage.txt" remote_staged=1 -"${lab_dir}/lab.sh" copy-to windows "${build_dir}/omnideck-windows-amd64.zip" "${remote_scp_root}/omnideck-windows-amd64.zip" -"${lab_dir}/lab.sh" copy-to windows "${build_dir}/SHA256SUMS" "${remote_scp_root}/SHA256SUMS" -"${lab_dir}/lab.sh" copy-to windows "${build_dir}/releasecontract.exe" "${remote_scp_root}/releasecontract.exe" -"${lab_dir}/lab.sh" copy-to windows "${build_dir}/contracts.tar.gz" "${remote_scp_root}/contracts.tar.gz" -"${lab_dir}/lab.sh" copy-to windows "${script_dir}/windows_guest.ps1" "${remote_scp_root}/windows_guest.ps1" -"${lab_dir}/lab.sh" copy-to windows "${script_dir}/windows_registry.ps1" "${remote_scp_root}/windows_registry.ps1" -"${lab_dir}/lab.sh" copy-to windows "${repo_root}/tests/hardware/run.ps1" "${remote_scp_root}/hardware-run.ps1" -"${lab_dir}/lab.sh" copy-to windows "${build_dir}/tls/registry.crt" "${remote_scp_root}/registry.crt" ssh_base_options=( -i "${key_file}" @@ -250,23 +259,24 @@ ssh_base_options=( -o ExitOnForwardFailure=yes -p "${ssh_port}" ) -ssh_terminal_options=( +ssh_forward_options=( "${ssh_base_options[@]}" - -tt -R "${reverse_port}:127.0.0.1:${tls_port}" ) +ssh_terminal_options=("${ssh_forward_options[@]}" -tt) json_command() { python3 -c 'import json, sys; print(json.dumps(sys.argv[1:]))' "$@" } -prepare_command="powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File ${remote_root}\\windows_guest.ps1 -Phase Prepare -WorkDir ${remote_root} -ExpectedVersion ${expected_version} -FixtureImage ${fixture_guest}" +prepare_command="powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File ${remote_root}\\windows_guest.ps1 -Phase Prepare -WorkDir ${remote_root} -ExpectedVersion ${expected_version} -FixtureImage ${fixture_guest} -TestTier ${suite}" bootstrap_remote="set \"OMNIDECK_CONFIG_DIR=${remote_root}\\config\"&& ${remote_root}\\bin\\omnideck.exe install --image ${fixture_guest}" install_remote="set \"OMNIDECK_CONFIG_DIR=${remote_root}\\config\"&& ${remote_root}\\bin\\omnideck.exe install --image ${fixture_guest}" ca_command="powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File ${remote_root}\\windows_registry.ps1 -CertificatePath ${remote_root}\\registry.crt -RegistryAuthority ${registry_authority}" -installed_command="wsl.exe --shutdown&& podman.exe machine start omnideck-runtime&& podman.exe start omnideck&& powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File ${remote_root}\\windows_guest.ps1 -Phase Installed -WorkDir ${remote_root} -ExpectedVersion ${expected_version} -FixtureImage ${fixture_guest}&& echo OMNIDECK_E2E_INSTALLED_PASSED" +product_setup_command="powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File ${remote_root}\\windows_guest.ps1 -Phase ProductSetup -WorkDir ${remote_root} -ExpectedVersion ${expected_version} -FixtureImage ${fixture_guest} -TestTier product -CertificatePath ${remote_root}\\registry.crt -RegistryAuthority ${registry_authority}" +installed_command="powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File ${remote_root}\\windows_guest.ps1 -Phase Installed -WorkDir ${remote_root} -ExpectedVersion ${expected_version} -FixtureImage ${fixture_guest} -TestTier ${suite}&& echo OMNIDECK_E2E_INSTALLED_PASSED" manage_remote="wsl.exe --shutdown&& podman.exe machine start omnideck-runtime&& podman.exe start omnideck&& set \"OMNIDECK_CONFIG_DIR=${remote_root}\\config\"&& ${remote_root}\\bin\\omnideck.exe tui" -final_command="wsl.exe --shutdown&& podman.exe machine start omnideck-runtime&& powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File ${remote_root}\\windows_guest.ps1 -Phase Final -WorkDir ${remote_root} -ExpectedVersion ${expected_version} -FixtureImage ${fixture_guest}&& echo OMNIDECK_E2E_FINAL_PASSED" +final_command="powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File ${remote_root}\\windows_guest.ps1 -Phase Final -WorkDir ${remote_root} -ExpectedVersion ${expected_version} -FixtureImage ${fixture_guest} -TestTier ${suite}&& echo OMNIDECK_E2E_FINAL_PASSED" set_registry_bridge() { local target_port="$1" @@ -288,6 +298,7 @@ set +e "${lab_dir}/lab.sh" run windows \ "netsh.exe advfirewall firewall add rule name=${firewall_rule} dir=in action=allow protocol=TCP localport=${bridge_port} profile=any" + if [[ "$suite" == onboarding ]]; then printf 'Driving Windows prerequisite setup and approving the real UAC prompt.\n' bootstrap_json="$(json_command ssh "${ssh_terminal_options[@]}" tester@127.0.0.1 "${bootstrap_remote}")" uac_hook_json="$(json_command "${script_dir}/windows_uac_hook.sh" "${lab_dir}" "${evidence_dir}")" @@ -327,12 +338,17 @@ set +e --install-timeout 2400 \ --command-json "${install_json}" \ --hook-command-json "${ca_hook_json}" + else + printf 'Creating the test instance on the certified product-ready runtime.\n' + ssh "${ssh_forward_options[@]}" tester@127.0.0.1 "${product_setup_command}" \ + | tee "${build_dir}/windows-product-setup.log" + fi printf 'Checking installed state and unattended JSON/update behavior.\n' # Windows OpenSSH places session children in a job that is torn down when # the session closes. Start the WSL-backed Podman machine inside every phase # that uses it, and attach the registry forward to that same live session. - ssh "${ssh_terminal_options[@]}" tester@127.0.0.1 "${installed_command}" \ + ssh "${ssh_forward_options[@]}" tester@127.0.0.1 "${installed_command}" \ | tee "${build_dir}/windows-installed-session.log" grep -Fq 'OMNIDECK_E2E_INSTALLED_PASSED' "${build_dir}/windows-installed-session.log" @@ -348,7 +364,7 @@ set +e --command-json "${manage_json}" printf 'Running the full unattended Windows CLI lifecycle.\n' - ssh "${ssh_terminal_options[@]}" tester@127.0.0.1 "${final_command}" \ + ssh "${ssh_forward_options[@]}" tester@127.0.0.1 "${final_command}" \ | tee "${build_dir}/windows-final-session.log" grep -Fq 'OMNIDECK_E2E_FINAL_PASSED' "${build_dir}/windows-final-session.log" ) diff --git a/tests/e2e/run.sh b/tests/e2e/run.sh index 2ee5f87..7553989 100755 --- a/tests/e2e/run.sh +++ b/tests/e2e/run.sh @@ -9,11 +9,12 @@ vm="${OMNIDECK_VM_E2E_VM:-appimage}" builder_image="${OMNIDECK_CLI_BUILDER_IMAGE:-omnideck-cli-builder:local}" assume_yes=0 keep_vm=0 +suite="${OMNIDECK_VM_E2E_SUITE:-product}" original_args=("$@") usage() { cat <<'EOF' -Usage: ./tests/e2e/run.sh [--vm appimage|deb|rpm|windows] [--yes] [--keep-vm] +Usage: ./tests/e2e/run.sh [--suite product|onboarding] [--vm appimage|deb|rpm|windows] [--yes] [--keep-vm] Build and install the current CLI in one clean disposable VM, then drive the guided install and management TUI through a real pseudo-terminal. @@ -31,6 +32,10 @@ while (($#)); do vm="${2:?--vm requires a value}" shift 2 ;; + --suite) + suite="${2:?--suite requires a value}" + shift 2 + ;; --yes) assume_yes=1 shift @@ -51,8 +56,14 @@ while (($#)); do esac done +case "$suite" in + product) profile=product-ready ;; + onboarding) profile=onboarding-clean ;; + *) printf 'Unsupported suite: %s\n' "$suite" >&2; exit 2 ;; +esac + if [[ "${vm}" == "windows" ]]; then - windows_args=() + windows_args=(--suite "$suite") [[ "${assume_yes}" == "1" ]] && windows_args+=(--yes) [[ "${keep_vm}" == "1" ]] && windows_args+=(--keep-vm) exec "${script_dir}/run-windows.sh" "${windows_args[@]}" @@ -69,22 +80,23 @@ case "${vm}" in esac require_lab +baseline="$("${lab_dir}/lab.sh" profile "$profile" "$vm")" command -v docker >/dev/null 2>&1 || { printf 'Docker is required for the pinned builder and fixture registry.\n' >&2; exit 2; } command -v curl >/dev/null 2>&1 || { printf 'curl is required to check the fixture registry.\n' >&2; exit 2; } command -v ssh >/dev/null 2>&1 || { printf 'ssh is required to run the guest through the lab connection.\n' >&2; exit 2; } if [[ "${OMNIDECK_VM_LAB_LEASED:-}" != "1" ]]; then lease_run_id="$(date -u +%Y%m%dT%H%M%SZ)-$$" - "${lab_dir}/lab.sh" preflight cli release-clean --lanes "${vm}" >/dev/null + "${lab_dir}/lab.sh" preflight cli "$profile" --lanes "${vm}" >/dev/null source_state prepare_output_dir="${OMNIDECK_VM_E2E_OUTPUT_DIR:-$("${lab_dir}/lab.sh" artifact-path cli e2e "${lease_run_id}")}" mkdir -p "${prepare_output_dir}" "${lab_dir}/lab.sh" evidence-init "${prepare_output_dir}" cli e2e "${lease_run_id}" \ - "${source_short}" "${vm}" clean "phase=preparing" "sourceDirty=${source_dirty}" "sourceFingerprint=${source_fingerprint}" + "${source_short}" "${vm}" "$baseline" "phase=preparing" "testTier=${suite}" "sourceDirty=${source_dirty}" "sourceFingerprint=${source_fingerprint}" trap '"${lab_dir}/lab.sh" evidence-finish "${prepare_output_dir}" failed || true' EXIT prepare_cli_binaries linux "${lab_dir}/lab.sh" evidence-set "${prepare_output_dir}" "phase=prepared" "buildCacheKey=${cli_build_key}" - lease_args=(lease "${vm}" cli "${lease_run_id}" --cleanup-baseline clean) + lease_args=(lease "${vm}" cli "${lease_run_id}" --cleanup-baseline "$baseline") [[ "${keep_vm}" != "1" ]] || lease_args+=(--keep-state) lease_args+=(-- env OMNIDECK_CLI_BUILD_CACHE="${cli_build_cache}" OMNIDECK_CLI_BUILD_KEY="${cli_build_key}" \ OMNIDECK_VM_E2E_OUTPUT_DIR="${prepare_output_dir}" "$0" "${original_args[@]}") @@ -145,7 +157,7 @@ if [[ -f "${output_dir}/run.json" ]]; then "${lab_dir}/lab.sh" evidence-set "${output_dir}" "phase=executing" "expectedVersion=${expected_version}" else "${lab_dir}/lab.sh" evidence-init "${output_dir}" cli e2e "${safe_run_id}" \ - "${source_commit}" "${vm}" clean "expectedVersion=${expected_version}" \ + "${source_commit}" "${vm}" "$baseline" "expectedVersion=${expected_version}" "testTier=${suite}" \ "sourceDirty=${source_dirty}" "sourceFingerprint=${source_fingerprint}" \ "buildCacheKey=${OMNIDECK_CLI_BUILD_KEY}" fi @@ -168,7 +180,7 @@ cleanup() { vm_started=0 fi if [[ "${keep_vm}" != "1" ]]; then - "${lab_dir}/lab.sh" reset "${vm}" clean || exit_code=1 + "${lab_dir}/lab.sh" reset "${vm}" "$baseline" || exit_code=1 else printf 'Guest kept stopped for debugging: %s\n' "${vm}" fi @@ -182,8 +194,8 @@ cleanup() { } trap cleanup EXIT -printf 'Resetting the leased %s guest to its clean golden.\n' "${vm}" -"${lab_dir}/lab.sh" reset "${vm}" clean +printf 'Resetting the leased %s guest to its %s baseline.\n' "${vm}" "$baseline" +"${lab_dir}/lab.sh" reset "${vm}" "$baseline" printf 'Using prepared CLI build cache: %s\n' "${OMNIDECK_CLI_BUILD_KEY}" @@ -203,24 +215,31 @@ docker tag "${fixture_local}" "${fixture_host}" docker push "${fixture_host}" >/dev/null fixture_guest="localhost:${reverse_port}/${fixture_repository}:${safe_run_id}" -printf 'Starting and verifying the clean %s guest.\n' "${vm}" +printf 'Starting and verifying the %s %s guest.\n' "$suite" "${vm}" "${lab_dir}/lab.sh" start "${vm}" vm_started=1 "${lab_dir}/lab.sh" wait "${vm}" "${lab_dir}/lab.sh" verify "${vm}" | tee "${output_dir}/guest-verify.txt" -grep -Fq 'podman=absent' "${output_dir}/guest-verify.txt" +if [[ "$suite" == onboarding ]]; then + grep -Fq 'podman=absent' "${output_dir}/guest-verify.txt" +else + grep -Fq 'podman=/' "${output_dir}/guest-verify.txt" + "${lab_dir}/lab.sh" run "${vm}" 'podman info >/dev/null' +fi "${lab_dir}/lab.sh" run "${vm}" "if ss -ltn | grep -q ':${reverse_port} '; then exit 1; fi" -"${lab_dir}/lab.sh" run "${vm}" "mkdir -p '${remote_root}/elevation-bin'" +payload_dir="${build_dir}/payload" +mkdir -p "${payload_dir}/elevation-bin" +install -m 0644 "${build_dir}/omnideck-linux-amd64.tar.gz" "${payload_dir}/omnideck-linux-amd64.tar.gz" +install -m 0644 "${build_dir}/SHA256SUMS" "${payload_dir}/SHA256SUMS" +install -m 0755 "${build_dir}/releasecontract" "${payload_dir}/releasecontract" +install -m 0644 "${build_dir}/contracts.tar.gz" "${payload_dir}/contracts.tar.gz" +install -m 0755 "${script_dir}/guest.sh" "${payload_dir}/guest.sh" +install -m 0755 "${script_dir}/terminal_driver.py" "${payload_dir}/terminal_driver.py" +install -m 0755 "${script_dir}/fixtures/sudo" "${payload_dir}/elevation-bin/sudo" +install -m 0755 "${repo_root}/tests/hardware/run.sh" "${payload_dir}/hardware-run.sh" +"${lab_dir}/lab.sh" stage "${vm}" "${payload_dir}" "${remote_root}" | tee "${output_dir}/payload-stage.txt" remote_staged=1 -"${lab_dir}/lab.sh" copy-to "${vm}" "${build_dir}/omnideck-linux-amd64.tar.gz" "${remote_root}/omnideck-linux-amd64.tar.gz" -"${lab_dir}/lab.sh" copy-to "${vm}" "${build_dir}/SHA256SUMS" "${remote_root}/SHA256SUMS" -"${lab_dir}/lab.sh" copy-to "${vm}" "${build_dir}/releasecontract" "${remote_root}/releasecontract" -"${lab_dir}/lab.sh" copy-to "${vm}" "${build_dir}/contracts.tar.gz" "${remote_root}/contracts.tar.gz" -"${lab_dir}/lab.sh" copy-to "${vm}" "${script_dir}/guest.sh" "${remote_root}/guest.sh" -"${lab_dir}/lab.sh" copy-to "${vm}" "${script_dir}/terminal_driver.py" "${remote_root}/terminal_driver.py" -"${lab_dir}/lab.sh" copy-to "${vm}" "${script_dir}/fixtures/sudo" "${remote_root}/elevation-bin/sudo" -"${lab_dir}/lab.sh" copy-to "${vm}" "${repo_root}/tests/hardware/run.sh" "${remote_root}/hardware-run.sh" ssh_options=( -i "${key_file}" @@ -234,7 +253,7 @@ ssh_options=( set +e ssh "${ssh_options[@]}" tester@127.0.0.1 \ - "chmod +x '${remote_root}/guest.sh' '${remote_root}/terminal_driver.py' '${remote_root}/releasecontract' '${remote_root}/hardware-run.sh' '${remote_root}/elevation-bin/sudo' && OMNIDECK_E2E_KEEP_GUEST_STATE='${keep_vm}' '${remote_root}/guest.sh' '${remote_root}' '${expected_version}' '${fixture_guest}'" + "OMNIDECK_E2E_KEEP_GUEST_STATE='${keep_vm}' '${remote_root}/guest.sh' '${remote_root}' '${expected_version}' '${fixture_guest}' '${suite}'" test_status=$? set -e diff --git a/tests/e2e/terminal_driver.py b/tests/e2e/terminal_driver.py index d49d44e..b24ae8c 100755 --- a/tests/e2e/terminal_driver.py +++ b/tests/e2e/terminal_driver.py @@ -242,7 +242,6 @@ def install_scenario(args: argparse.Namespace) -> None: [ "Preparing your environment", "Setting omnideck up on this computer. This usually takes a few minutes.", - "Getting your computer ready…", "Computer setup", "Application files", "Final checks", @@ -260,6 +259,13 @@ def install_scenario(args: argparse.Namespace) -> None: since=mark, checkpoint="runtime-permission", ) + terminal.expect_all( + ["Getting your computer ready…"], + timeout=args.install_timeout, + since=mark, + checkpoint="computer-ready", + fail_phrases=("The download didn’t finish", "Setup couldn’t finish"), + ) terminal.expect_all( [ "omnideck is ready", diff --git a/tests/e2e/test_lab_contract.py b/tests/e2e/test_lab_contract.py index 5a7bd4b..2f4528c 100644 --- a/tests/e2e/test_lab_contract.py +++ b/tests/e2e/test_lab_contract.py @@ -14,7 +14,7 @@ def test_linux_harness_uses_controller_contract(self) -> None: self.assertIn('evidence-finish', script) self.assertIn('prepare_cli_binaries linux', script) self.assertIn('artifact-path cli e2e', script) - self.assertIn('--cleanup-baseline clean', script) + self.assertIn('--cleanup-baseline "$baseline"', script) self.assertNotIn("omnideck-cli-vm-e2e", script) self.assertNotIn("discarded-before", script) @@ -26,14 +26,16 @@ def test_windows_harness_uses_same_controller_contract(self) -> None: self.assertIn('evidence-finish', script) self.assertIn('prepare_cli_binaries windows', script) self.assertIn('artifact-path cli e2e', script) - self.assertIn('--cleanup-baseline clean', script) + self.assertIn('--cleanup-baseline "$baseline"', script) self.assertNotIn("omnideck-cli-vm-e2e", script) self.assertNotIn("windows-tpm.*", script) def test_matrix_preflights_and_groups_lane_evidence(self) -> None: script = (ROOT / "tests/e2e/matrix.sh").read_text(encoding="utf-8") - self.assertIn("preflight cli release-clean", script) + self.assertIn('preflight cli "$profile"', script) self.assertIn("artifact-path cli matrix", script) + self.assertIn("interrupt_matrix 130 INT", script) + self.assertIn('wait "$active_lane_pid"', script) self.assertIn('OMNIDECK_VM_E2E_OUTPUT_DIR="$lane_dir"', script) self.assertIn("lane-status.tsv", script) diff --git a/tests/e2e/windows_guest.ps1 b/tests/e2e/windows_guest.ps1 index 0244832..31df950 100644 --- a/tests/e2e/windows_guest.ps1 +++ b/tests/e2e/windows_guest.ps1 @@ -1,13 +1,17 @@ param( [Parameter(Mandatory = $true)] - [ValidateSet("Prepare", "Installed", "Final")] + [ValidateSet("Prepare", "ProductSetup", "Installed", "Final")] [string]$Phase, [Parameter(Mandatory = $true)] [string]$WorkDir, [Parameter(Mandatory = $true)] [string]$ExpectedVersion, [Parameter(Mandatory = $true)] - [string]$FixtureImage + [string]$FixtureImage, + [ValidateSet("product", "onboarding")] + [string]$TestTier = "product", + [string]$CertificatePath = "", + [string]$RegistryAuthority = "" ) $ErrorActionPreference = "Stop" @@ -40,6 +44,33 @@ function Invoke-Cli([string[]]$Arguments) { } } +function Start-PodmanMachineReady { + $PreviousPreference = $ErrorActionPreference + $Ready = $false + try { + # Podman writes recoverable notices, including automatic SSH-port + # reassignment, to stderr. Judge native commands by their exit codes. + $ErrorActionPreference = "Continue" + for ($Attempt = 1; $Attempt -le 3; $Attempt++) { + & wsl.exe --shutdown *> $null + & podman.exe machine start omnideck-runtime 2>&1 | Write-Host + if ($LASTEXITCODE -eq 0) { + & podman.exe info *> $null + if ($LASTEXITCODE -eq 0) { + $Ready = $true + break + } + } + Start-Sleep -Seconds (3 * $Attempt) + } + } finally { + $ErrorActionPreference = $PreviousPreference + } + if (-not $Ready) { + throw "The omnideck-runtime Podman machine did not become ready after three attempts." + } +} + function Write-Inventory([string]$Suffix) { $Lines = [System.Collections.Generic.List[string]]::new() $Lines.Add("timestamp=$([DateTime]::UtcNow.ToString('o'))") @@ -115,10 +146,16 @@ try { switch ($Phase) { "Prepare" { $CurrentStep = "clean-host precondition" - Write-Inventory "before" - if (Get-Command podman.exe -ErrorAction SilentlyContinue) { + if ($TestTier -eq "onboarding" -and (Get-Command podman.exe -ErrorAction SilentlyContinue)) { throw "The Windows install scenario requires a clean guest with Podman absent." } + if ($TestTier -eq "product" -and -not (Get-Command podman.exe -ErrorAction SilentlyContinue)) { + throw "The Windows product scenario requires Podman in the certified baseline." + } + if ($TestTier -eq "product") { + Start-PodmanMachineReady + } + Write-Inventory "before" if (Test-Path (Join-Path $ConfigDir "instances\omnideck.yaml")) { throw "The isolated test configuration unexpectedly contains an existing instance." } @@ -156,7 +193,24 @@ try { ) } + "ProductSetup" { + if ($TestTier -ne "product") { throw "ProductSetup is only valid for the product tier." } + if (-not $CertificatePath -or -not $RegistryAuthority) { throw "ProductSetup requires the registry certificate and authority." } + $CurrentStep = "product baseline setup" + Start-PodmanMachineReady + Invoke-External "powershell.exe" @( + "-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", + "-File", (Join-Path $WorkDir "windows_registry.ps1"), + "-CertificatePath", $CertificatePath, + "-RegistryAuthority", $RegistryAuthority + ) + $env:OMNIDECK_CONFIG_DIR = $ConfigDir + Invoke-Cli @("install", "--plain", "--image", $FixtureImage) + } + "Installed" { + Start-PodmanMachineReady + Invoke-External "podman.exe" @("start", "omnideck") $env:OMNIDECK_CONFIG_DIR = $ConfigDir $CurrentStep = "installed behavior" (& podman.exe info 2>&1) | Set-Content -Encoding UTF8 -Path (Join-Path $ResultDir "podman-info.txt") @@ -200,6 +254,7 @@ try { } "Final" { + Start-PodmanMachineReady $env:OMNIDECK_CONFIG_DIR = $ConfigDir $CurrentStep = "removal cleanup contract" if (-not (Test-PodmanObjectAbsent @("container", "inspect", "omnideck"))) { throw "The TUI removal left the primary container behind." } diff --git a/tests/e2e/windows_registry.ps1 b/tests/e2e/windows_registry.ps1 index a6ec197..5f4b10e 100644 --- a/tests/e2e/windows_registry.ps1 +++ b/tests/e2e/windows_registry.ps1 @@ -3,28 +3,30 @@ param( [string]$CertificatePath, [Parameter(Mandatory = $true)] [string]$RegistryAuthority, - [int]$TimeoutSeconds = 300 + [int]$TimeoutSeconds = 90 ) $ErrorActionPreference = "Stop" $Distro = "podman-omnideck-runtime" $ResolvedCertificate = (Resolve-Path -Path $CertificatePath).Path -if ($ResolvedCertificate -notmatch '^([A-Za-z]):\\(.*)$') { - throw "The registry certificate must be on a Windows drive: $ResolvedCertificate" -} -$Drive = $Matches[1].ToLowerInvariant() -$Tail = $Matches[2] -replace '\\', '/' -$LinuxCertificate = "/mnt/$Drive/$Tail" +$EncodedCertificate = [Convert]::ToBase64String([IO.File]::ReadAllBytes($ResolvedCertificate)) +$LinuxCertificate = "/tmp/omnideck-e2e-registry.crt" $Deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) -Write-Host "Waiting for the Podman machine filesystem so the local fixture CA can be installed." +Write-Host "Waiting for the Podman machine so the local fixture CA can be installed." while ([DateTime]::UtcNow -lt $Deadline) { - & wsl.exe -d $Distro -u root -- sh -c "test -r '$LinuxCertificate'" *> $null + & wsl.exe -d $Distro -u root -- true *> $null if ($LASTEXITCODE -eq 0) { break } Start-Sleep -Seconds 1 } if ($LASTEXITCODE -ne 0) { - throw "The $Distro filesystem did not become available within $TimeoutSeconds seconds." + throw "The $Distro machine did not become available within $TimeoutSeconds seconds." +} + +$StageScript = "umask 077; printf '%s' '$EncodedCertificate' | base64 -d > '$LinuxCertificate'" +& wsl.exe -d $Distro -u root -- sh -c $StageScript +if ($LASTEXITCODE -ne 0) { + throw "Could not stage the local fixture registry CA in $Distro." } $InstallScript = @" @@ -40,17 +42,13 @@ if ($LASTEXITCODE -ne 0) { $Successes = 0 for ($Attempt = 1; $Attempt -le 240; $Attempt++) { - $Route = (& wsl.exe -d $Distro -u root -- ip -4 route show default 2>$null) -join " " - if ($Route -match 'default via ([0-9.]+)') { - $Gateway = $Matches[1] - $NetworkScript = "grep -v 'host\.containers\.internal' /etc/hosts > /tmp/omnideck-e2e-hosts; cat /tmp/omnideck-e2e-hosts > /etc/hosts; printf '%s host.containers.internal\n' '$Gateway' >> /etc/hosts; curl --fail --silent --max-time 2 --cacert '$LinuxCertificate' 'https://$RegistryAuthority/v2/' >/dev/null 2>&1" - & wsl.exe -d $Distro -u root -- sh -c $NetworkScript - if ($LASTEXITCODE -eq 0) { - $Successes++ - if ($Successes -ge 10) { break } - } else { - $Successes = 0 - } + $NetworkScript = "curl --fail --silent --max-time 2 --cacert '$LinuxCertificate' 'https://$RegistryAuthority/v2/' >/dev/null 2>&1" + & wsl.exe -d $Distro -u root -- sh -c $NetworkScript + if ($LASTEXITCODE -eq 0) { + $Successes++ + if ($Successes -ge 10) { break } + } else { + $Successes = 0 } Start-Sleep -Milliseconds 250 }