diff --git a/backend/druks/setup_env.py b/backend/druks/setup_env.py index 0454e98b..21c41d38 100644 --- a/backend/druks/setup_env.py +++ b/backend/druks/setup_env.py @@ -42,6 +42,10 @@ "REDIS_URL", "DRUKS_AUTH_HEADER", "DEFAULT_HOST_PROVIDER", + "REGISTRY_HOST", + "REGISTRY_USERNAME", + "REGISTRY_PASSWORD", + "TEMPLATE_REPOSITORY", "SERVICE_TOKENS", "DRUKS_AUTH_MODE", "DRUKS_AUTH_JWKS_URL", @@ -84,6 +88,10 @@ "service_url", "service_token", "image", + "registry_host", + "registry_username", + "registry_password", + "template_repository", "proxy_url", "issuer_url", "browser_login_proxy", @@ -200,6 +208,13 @@ def run_setup( service_url = "" service_token = "" image = "" +# Access to private sandbox images on one registry host, for example ghcr.io. +registry_host = "" +registry_username = "" +registry_password = "" +# The repository path on that host where drukbox publishes sandbox templates. +# The exe provider requires it. +template_repository = "" # The secrets proxy, at the address a sandbox dials. A sandbox sends its HTTPS # through it. The docker shape uses the Docker bridge gateway. A remote shape # names the address of this host that its sandboxes reach, for example the @@ -250,9 +265,6 @@ 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"), ""), @@ -424,6 +436,10 @@ def _render_env( ( ("DEFAULT_HOST_PROVIDER", provider), ("SERVICE_TOKENS", service_tokens), + ("REGISTRY_HOST", _get_string(config, ("sandbox", "registry_host"))), + ("REGISTRY_USERNAME", _get_string(config, ("sandbox", "registry_username"))), + ("REGISTRY_PASSWORD", _get_string(config, ("sandbox", "registry_password"))), + ("TEMPLATE_REPOSITORY", _get_string(config, ("sandbox", "template_repository"))), ("SECRETS_KEY", _get_string(config, ("secrets", "drukbox_secrets_key"))), ("SECRETS_PROXY_URL", proxy_url), # The proxy binds the address sandboxes dial and nothing else. diff --git a/backend/tests/test_setup_env.py b/backend/tests/test_setup_env.py index 7f3410b1..ab6a2fe0 100644 --- a/backend/tests/test_setup_env.py +++ b/backend/tests/test_setup_env.py @@ -61,9 +61,6 @@ def test_fresh_exe_render_matches_the_deployment_contract(tmp_path): assert config["paths"]["harness_config_root"] == values["DRUKS_HARNESS_CONFIG_ROOT"] 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["secrets"]["drukbox_secrets_key"]) == 44 assert len(config["sandbox"]["service_token"]) == 64 @@ -581,36 +578,31 @@ def test_a_copy_of_a_secrets_setting_is_a_named_gap(tmp_path, assignment, gap): assert "SECRETS_PROXY_URL" not in values -@pytest.mark.parametrize("repository", ["ghcr.io/acme/templates", "docker.io/acme/templates"]) -def test_exe_template_registry_uses_existing_provider_contract(tmp_path, repository): +@pytest.mark.parametrize("provider", ["docker", "exe"]) +def test_registry_settings_render_for_drukbox_on_every_provider(tmp_path, provider): env_path = tmp_path / ".env" printed = [] - assert ( - _run( - env_path, - print_fn=printed.append, - set_values=( - "sandbox.proxy_url=http://100.64.0.10:8880", - "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 + _run( + env_path, + provider=provider, + print_fn=printed.append, + set_values=( + "sandbox.registry_host=ghcr.io", + "sandbox.registry_username=builder", + "sandbox.registry_password=registry-token", + "sandbox.template_repository=acme/templates", + ), ) values = read_env(env_path) expected = { - "EXE_IMAGE_REGISTRY": repository, - "EXE_REGISTRY_USERNAME": "builder", - "EXE_REGISTRY_PASSWORD": "registry-token", + "REGISTRY_HOST": "ghcr.io", + "REGISTRY_USERNAME": "builder", + "REGISTRY_PASSWORD": "registry-token", + "TEMPLATE_REPOSITORY": "acme/templates", } 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 diff --git a/docs/configuration.md b/docs/configuration.md index b817fdba..a41cf572 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -40,7 +40,7 @@ host-run development template for that environment plane. | `[urls]` | Dashboard callback base URL and public webhook hostname | | `[secrets]` | Generated deployment secrets | | `[paths]` | Host data and harness configuration paths | -| `[sandbox]` | Drukbox provider, service URL and token, image override, and the proxy and issuer addresses | +| `[sandbox]` | Drukbox provider, service URL and token, image override, registry access, and the proxy and issuer addresses | | `[sandbox.]` | Provider environment passed through to the remote stack | | `[env]` | Additional deployment environment settings rendered verbatim | @@ -606,6 +606,8 @@ before provisioning a VM if its selected credential is missing. | `sandbox.service_token` | Drukbox API token | | `sandbox.timeout` | Control-plane request timeout. The default is 180 seconds | | `sandbox.image` | Optional provider image override | +| `sandbox.registry_host`, `sandbox.registry_username`, `sandbox.registry_password` | Access to private sandbox images on one registry host, for example `ghcr.io`. Set the three together. See [Drukbox](https://github.com/czpython/drukbox/blob/main/docs/deploy.md#private-image-registry) | +| `sandbox.template_repository` | The repository path on that host where Drukbox publishes sandbox templates. The exe provider requires it | | `sandbox.proxy_url` | The secrets proxy, at the address a sandbox dials. The docker shape sets `http://172.17.0.1:8880`. docker-sbx leaves it empty | | `sandbox.issuer_url` | The issuer base URL the secrets exchange dials. The default is `http://127.0.0.1:8001`. For a Drukbox on another server, set the address of the Druks host that Drukbox reaches. The installer then serves the issuer route there ([the issuer listener](deployment.md#the-issuer-listener)) | | `sandbox.browser_login_proxy` | Login-window egress proxy. An empty value keeps the box IP |