From 493bba2af8f4c3dd1a5dfb24aa7a6f9c51bfc551 Mon Sep 17 00:00:00 2001 From: Paulo Date: Sat, 5 Sep 2026 13:40:33 +0200 Subject: [PATCH] Configure exe sandbox templates with the existing Drukbox contract --- backend/druks/sandbox/templates.py | 5 +++- backend/druks/setup_env.py | 3 ++ backend/tests/test_declared_sandboxes.py | 37 ++++++++++++++++++++++-- backend/tests/test_setup_env.py | 37 ++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/backend/druks/sandbox/templates.py b/backend/druks/sandbox/templates.py index f2dbfe35..a884e700 100644 --- a/backend/druks/sandbox/templates.py +++ b/backend/druks/sandbox/templates.py @@ -1,4 +1,5 @@ import asyncio +from pathlib import PurePosixPath from druks.apps import loader from druks.durable.activity import set_run_phase @@ -23,10 +24,12 @@ def get_declared_sandboxes() -> dict[str, Sandbox]: async def prepare_sandbox_templates() -> None: base_image = load_settings().sandbox.image for sandbox in get_declared_sandboxes().values(): + app_name = loader.resolve_workflow_app(sandbox.module) + label = f"{app_name}-{PurePosixPath(sandbox.setup).stem}".replace("_", "-") await sandbox_client.create_template( setup_script=sandbox.read_setup_script().decode("utf-8"), base_image=base_image or None, - label=f"{loader.resolve_workflow_app(sandbox.module)}/{sandbox.setup}", + label=label, ) diff --git a/backend/druks/setup_env.py b/backend/druks/setup_env.py index ae623c53..2f962832 100644 --- a/backend/druks/setup_env.py +++ b/backend/druks/setup_env.py @@ -225,6 +225,9 @@ def _fresh_values(*, provider: str, home: str) -> tuple[tuple[tuple[str, ...], s (("sandbox", "service_url"), "http://127.0.0.1:8780"), (("sandbox", "service_token"), _hex_secret()), (("sandbox", "exe", "EXE_API_TOKEN"), ""), + (("sandbox", "exe", "EXE_IMAGE_REGISTRY"), ""), + (("sandbox", "exe", "EXE_REGISTRY_USERNAME"), ""), + (("sandbox", "exe", "EXE_REGISTRY_PASSWORD"), ""), (("sandbox", "exe", "TAILSCALE_TAILNET"), ""), (("sandbox", "exe", "TAILSCALE_OAUTH_CLIENT_ID"), ""), (("sandbox", "exe", "TAILSCALE_OAUTH_CLIENT_SECRET"), ""), diff --git a/backend/tests/test_declared_sandboxes.py b/backend/tests/test_declared_sandboxes.py index 7226bcdf..0e7a24eb 100644 --- a/backend/tests/test_declared_sandboxes.py +++ b/backend/tests/test_declared_sandboxes.py @@ -41,6 +41,7 @@ class BuildSite: def test_get_declared_sandboxes_deduplicates_by_content(monkeypatch): shared = Sandbox(setup="sandboxes/setup.sh") + other = Sandbox(setup="sandboxes/other.sh") class First: kind = "notes.first" @@ -48,7 +49,7 @@ class First: class Second: kind = "notes.second" - sandbox = shared + sandbox = other app = SimpleNamespace(workflows=lambda: [First, Second]) monkeypatch.setattr(templates, "loader", SimpleNamespace(iter_apps=lambda: [app])) @@ -56,7 +57,7 @@ class Second: declared = templates.get_declared_sandboxes() - assert declared == {hashlib.sha256(b"setup").hexdigest(): shared} + assert declared == {hashlib.sha256(b"setup").hexdigest(): other} def test_software_factory_maps_the_sandbox_building_phase(): @@ -95,10 +96,40 @@ async def test_prepare_sandbox_templates_requests_each_declaration(monkeypatch): create_template.assert_awaited_once_with( setup_script="setup", base_image="base", - label="notes/sandboxes/setup.sh", + label="notes-setup", ) +async def test_prepare_templates_labels_each_app_and_script(monkeypatch): + sandboxes = [Sandbox(setup="sandboxes/build.sh"), Sandbox(setup="sandboxes/preview.sh")] + for sandbox in sandboxes: + object.__setattr__(sandbox, "module", "site_builder.workflows") + create_template = AsyncMock() + monkeypatch.setattr(Sandbox, "read_setup_script", lambda self: self.setup.encode()) + monkeypatch.setattr( + templates, "load_settings", lambda: SimpleNamespace(sandbox=SimpleNamespace(image="")) + ) + monkeypatch.setattr( + templates, "loader", SimpleNamespace(resolve_workflow_app=lambda module: "site_builder") + ) + monkeypatch.setattr( + templates, + "get_declared_sandboxes", + lambda: {sandbox.setup_script_hash: sandbox for sandbox in sandboxes}, + ) + monkeypatch.setattr( + templates, "sandbox_client", SimpleNamespace(create_template=create_template) + ) + + await templates.prepare_sandbox_templates() + + assert [call.kwargs["label"] for call in create_template.await_args_list] == [ + "site-builder-build", + "site-builder-preview", + ] + assert all(call.kwargs["base_image"] is None for call in create_template.await_args_list) + + async def test_get_template_id_uses_available_template(monkeypatch): sandbox = Sandbox(setup="sandboxes/setup.sh") template = SimpleNamespace(id="template-1", status="available") diff --git a/backend/tests/test_setup_env.py b/backend/tests/test_setup_env.py index 320cc948..8ab3ae64 100644 --- a/backend/tests/test_setup_env.py +++ b/backend/tests/test_setup_env.py @@ -51,6 +51,9 @@ def test_fresh_exe_render_matches_the_deployment_contract(tmp_path): assert values["DRUKS_DATA_DIR"] == "/home/op/druks-data" assert "EXE_API_TOKEN" not in values assert "TAILSCALE_TAILNET" not in values + for key in ("EXE_IMAGE_REGISTRY", "EXE_REGISTRY_USERNAME", "EXE_REGISTRY_PASSWORD"): + assert config["sandbox"]["exe"][key] == "" + assert key not in values assert len(config["secrets"]["postgres_password"]) == 64 assert len(config["sandbox"]["service_token"]) == 64 assert (tmp_path / ".gitignore").read_text().splitlines() == [ @@ -431,3 +434,37 @@ def test_setup_toml_is_the_settings_source(tmp_path, monkeypatch): assert settings.sandbox.service_url == config["sandbox"]["service_url"] assert settings.sandbox.image == config["sandbox"]["image"] assert settings.sandbox.timeout == float(config["sandbox"]["timeout"]) + + +@pytest.mark.parametrize("repository", ["ghcr.io/acme/templates", "docker.io/acme/templates"]) +def test_exe_template_registry_uses_existing_provider_contract(tmp_path, repository): + env_path = tmp_path / ".env" + printed = [] + assert ( + _run( + env_path, + print_fn=printed.append, + set_values=( + "sandbox.exe.EXE_API_TOKEN=exe-token", + "sandbox.exe.TAILSCALE_TAILNET=tail.ts.net", + f"sandbox.exe.EXE_IMAGE_REGISTRY={repository}", + "sandbox.exe.EXE_REGISTRY_USERNAME=builder", + "sandbox.exe.EXE_REGISTRY_PASSWORD=registry-token", + ), + ) + == 0 + ) + values = read_env(env_path) + expected = { + "EXE_IMAGE_REGISTRY": repository, + "EXE_REGISTRY_USERNAME": "builder", + "EXE_REGISTRY_PASSWORD": "registry-token", + } + for key, value in expected.items(): + assert values[key] == value + assert env_path.read_text().count(f"{key}=") == 1 + assert "REGISTRY_HOST" not in values + assert "TEMPLATE_REPOSITORY" not in values + assert "registry-token" not in "\n".join(printed) + assert stat.S_IMODE(env_path.stat().st_mode) == 0o600 + assert stat.S_IMODE((tmp_path / "druks.toml").stat().st_mode) == 0o600