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
5 changes: 4 additions & 1 deletion backend/druks/sandbox/templates.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
from pathlib import PurePosixPath

from druks.apps import loader
from druks.durable.activity import set_run_phase
Expand All @@ -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,
)


Expand Down
3 changes: 3 additions & 0 deletions backend/druks/setup_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"), ""),
Expand Down
37 changes: 34 additions & 3 deletions backend/tests/test_declared_sandboxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,23 @@ 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"
sandbox = shared

class Second:
kind = "notes.second"
sandbox = shared
sandbox = other

app = SimpleNamespace(workflows=lambda: [First, Second])
monkeypatch.setattr(templates, "loader", SimpleNamespace(iter_apps=lambda: [app]))
monkeypatch.setattr(Sandbox, "read_setup_script", lambda self: b"setup")

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():
Expand Down Expand Up @@ -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")
Expand Down
37 changes: 37 additions & 0 deletions backend/tests/test_setup_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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() == [
Expand Down Expand Up @@ -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