Files
OpenShell/python/openshell/rpm_gateway_config_migration_test.py
Drew Newberry 17ce738bfb fix(ci)!: remove gateway callback listener dependency (#3365)
* fix(ci): repair post-merge release canary

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(packaging): bootstrap canary runtime prerequisites

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* ci(canary): collect macOS VM diagnostics

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* ci(canary): pin libkrun-compatible macOS runner

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* ci(canary): limit macOS smoke test to package startup

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(compute)!: remove gateway callback listeners

Run Docker supervisors on host networking so they use the operator-configured primary gateway endpoint. Remove the unused compute-driver callback listener negotiation and listener-scoped routing machinery.

BREAKING CHANGE: The ComputeDriver API no longer exposes GetGatewayListenerRequirements or GatewayListenerRequirement. External drivers must regenerate bindings and connect supervisors to the configured primary gateway endpoint.

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(docker): use sandbox runtime image in launcher

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(podman): exercise production endpoint selection

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(e2e): route supervisors to reachable gateways

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(e2e): align Podman endpoint fixtures

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): preserve host aliases for supervisors

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(docker): align sandbox host gateway pin

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(e2e): address Docker fixtures by bridge IP

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(e2e): serialize sandbox lifecycle cases

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(e2e): host Docker TCP fixture with gateway

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(e2e): use loopback for host-network supervisor

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

---------

Signed-off-by: Drew Newberry <anewberry@nvidia.com>
2026-09-18 20:55:55 +00:00

223 lines
7.7 KiB
Python

# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import subprocess
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
MIGRATOR = REPO_ROOT / "deploy/rpm/migrate-gateway-config.sh"
CURRENT = REPO_ROOT / "deploy/rpm/gateway.toml.default"
LEGACY = REPO_ROOT / "deploy/rpm/gateway.toml.default.v1"
SHIPPED_V1_FIXTURE = b"""# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Default gateway configuration for RPM installs.
#
# This file is seeded to ~/.config/openshell/gateway.toml on first start
# of the openshell-gateway.service systemd user unit. Edit that copy to
# customize. This file is not read directly at runtime.
#
# Configuration precedence (highest to lowest):
# CLI flag > OPENSHELL_* env var > TOML file > built-in default
#
# To override settings without editing this file, set OPENSHELL_* variables
# in ~/.config/openshell/gateway.env or run:
# systemctl --user edit openshell-gateway
[openshell]
version = 1
[openshell.gateway]
# Keep the primary listener on the built-in 127.0.0.1:17670 default. The
# host-networked Podman supervisor uses this same loopback endpoint.
# Pin to the Podman compute driver. Without this, the gateway auto-detects
# in order: Kubernetes, Podman, Docker. Pinning prevents unexpected driver
# selection if Docker is also installed on the host.
compute_drivers = ["podman"]
"""
def run_migrator(
destination: Path, *, check: bool = True
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["sh", str(MIGRATOR), str(destination), str(CURRENT), str(LEGACY)],
check=check,
text=True,
capture_output=True,
)
def test_migrator_seeds_missing_config_and_is_idempotent(tmp_path: Path) -> None:
destination = tmp_path / "config/openshell/gateway.toml"
run_migrator(destination)
assert destination.read_bytes() == CURRENT.read_bytes()
run_migrator(destination)
assert destination.read_bytes() == CURRENT.read_bytes()
def test_migrator_replaces_only_exact_legacy_default(tmp_path: Path) -> None:
destination = tmp_path / "gateway.toml"
shipped_v1 = SHIPPED_V1_FIXTURE
assert b'compute_drivers = ["podman"]' in shipped_v1
assert LEGACY.read_bytes() == shipped_v1
destination.write_bytes(shipped_v1)
run_migrator(destination)
assert destination.read_bytes() == CURRENT.read_bytes()
assert destination.stat().st_mode & 0o777 == 0o644
def test_migrator_preserves_edited_legacy_and_current_configs(tmp_path: Path) -> None:
destination = tmp_path / "gateway.toml"
edited = LEGACY.read_text(encoding="utf-8") + "# operator edit\n"
destination.write_text(edited, encoding="utf-8")
run_migrator(destination)
assert destination.read_text(encoding="utf-8") == edited
destination.write_bytes(CURRENT.read_bytes())
run_migrator(destination)
assert destination.read_bytes() == CURRENT.read_bytes()
def test_migrator_refuses_symlink_destination(tmp_path: Path) -> None:
target = tmp_path / "target.toml"
target.write_text("operator-owned\n", encoding="utf-8")
destination = tmp_path / "gateway.toml"
destination.symlink_to(target)
result = run_migrator(destination, check=False)
assert result.returncode != 0
assert "non-regular gateway config" in result.stderr
assert target.read_text(encoding="utf-8") == "operator-owned\n"
def test_shipped_rpm_defaults_have_expected_schema_shapes() -> None:
import tomllib
current = tomllib.loads(CURRENT.read_text(encoding="utf-8"))
legacy = tomllib.loads(LEGACY.read_text(encoding="utf-8"))
assert current["openshell"]["version"] == 2
assert current["openshell"]["gateway"]["compute_driver"] == "podman"
assert "compute_drivers" not in current["openshell"]["gateway"]
assert current["openshell"]["drivers"]["podman"]["health_check_interval_secs"] == 10
assert LEGACY.read_bytes() == SHIPPED_V1_FIXTURE
assert legacy["openshell"]["version"] == 1
assert legacy["openshell"]["gateway"]["compute_drivers"] == ["podman"]
assert "compute_driver" not in legacy["openshell"]["gateway"]
def test_migrator_rejects_wrong_argument_count() -> None:
result = subprocess.run(
["sh", str(MIGRATOR)], text=True, capture_output=True, check=False
)
assert result.returncode == 2
assert result.stderr == (
f"usage: {MIGRATOR} DESTINATION CURRENT_DEFAULT LEGACY_DEFAULT\n"
)
def test_migrator_seeds_with_mode_0644(tmp_path: Path) -> None:
destination = tmp_path / "config/openshell/gateway.toml"
run_migrator(destination)
assert destination.read_bytes() == CURRENT.read_bytes()
assert destination.stat().st_mode & 0o777 == 0o644
def test_migrator_preserves_operator_content_and_mode(tmp_path: Path) -> None:
destination = tmp_path / "gateway.toml"
edited = LEGACY.read_text(encoding="utf-8") + "# operator edit\\n"
destination.write_text(edited, encoding="utf-8")
destination.chmod(0o600)
run_migrator(destination)
assert destination.read_text(encoding="utf-8") == edited
assert destination.stat().st_mode & 0o777 == 0o600
destination.write_bytes(CURRENT.read_bytes())
destination.chmod(0o640)
run_migrator(destination)
assert destination.read_bytes() == CURRENT.read_bytes()
assert destination.stat().st_mode & 0o777 == 0o640
def test_migrator_handles_paths_containing_spaces(tmp_path: Path) -> None:
package = tmp_path / "package defaults"
package.mkdir()
current = package / "current default.toml"
legacy = package / "legacy default.toml"
current.write_bytes(CURRENT.read_bytes())
legacy.write_bytes(LEGACY.read_bytes())
destination = tmp_path / "operator config/gateway config.toml"
subprocess.run(
["sh", str(MIGRATOR), str(destination), str(current), str(legacy)],
check=True,
)
assert destination.read_bytes() == current.read_bytes()
assert destination.stat().st_mode & 0o777 == 0o644
def test_migrator_rejects_missing_and_nonregular_sources(tmp_path: Path) -> None:
for source_name in ("current", "legacy"):
for source_kind in ("missing", "directory", "symlink"):
current = tmp_path / f"{source_name}-{source_kind}-current.toml"
legacy = tmp_path / f"{source_name}-{source_kind}-legacy.toml"
current.write_bytes(CURRENT.read_bytes())
legacy.write_bytes(LEGACY.read_bytes())
source = current if source_name == "current" else legacy
if source_kind == "missing":
source.unlink()
elif source_kind == "directory":
source.unlink()
source.mkdir()
else:
source.unlink()
source.symlink_to(CURRENT)
result = subprocess.run(
[
"sh",
str(MIGRATOR),
str(tmp_path / f"{source_name}-{source_kind}-destination.toml"),
str(current),
str(legacy),
],
check=False,
text=True,
capture_output=True,
)
assert result.returncode == 1
assert (
f"gateway config migration source is not a regular file: {source}"
in result.stderr
)
def test_migrator_refuses_directory_destination(tmp_path: Path) -> None:
destination = tmp_path / "gateway.toml"
destination.mkdir()
result = run_migrator(destination, check=False)
assert result.returncode == 1
assert (
f"refusing to replace non-regular gateway config: {destination}"
in result.stderr
)