Add TTL parser, expires_at on create, extend_ttl and expire/reap APIs, activity-core integration doc, repo classification, registry refresh, HTTP parity, and 69 tests.
170 lines
No EOL
5.4 KiB
Python
170 lines
No EOL
5.4 KiB
Python
"""Snapshot store and manager checkpoint tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from sandboxer.core.manager import SandboxManager
|
|
from sandboxer.lifecycle.store import SandboxStore
|
|
from sandboxer.models import (
|
|
ActorType,
|
|
Consumer,
|
|
Reachability,
|
|
SandboxCreateRequest,
|
|
SandboxState,
|
|
SandboxStatus,
|
|
SnapshotRecord,
|
|
)
|
|
from sandboxer.snapshots.store import SnapshotStore
|
|
|
|
|
|
class SnapshotBackend:
|
|
def supports_snapshots(self) -> bool:
|
|
return True
|
|
|
|
def provision(self, profile, inputs, host):
|
|
return {
|
|
"sandbox_id": "test1234",
|
|
"host": host,
|
|
"remote_dir": "/tmp/sandboxer/test1234",
|
|
"compose_project": "sbx-e2e-test1234",
|
|
"compose_file": "docker-compose.yml",
|
|
"ssh_user": "root",
|
|
}
|
|
|
|
def wait_ready(self, handle):
|
|
return {
|
|
"ssh": f"root@{handle['host']}",
|
|
"remote_dir": handle["remote_dir"],
|
|
"compose_project": handle["compose_project"],
|
|
"host": handle["host"],
|
|
}
|
|
|
|
def teardown(self, handle):
|
|
return {"compose_removed": "True", "remote_dir_removed": "True"}
|
|
|
|
def snapshot(self, handle):
|
|
return {
|
|
"snapshot_id": "snap12345678",
|
|
"artifact_path": "/tmp/sandboxer/snapshots/snap12345678.tar.gz",
|
|
"host": handle["host"],
|
|
"size_bytes": "4096",
|
|
}
|
|
|
|
def restore_from_snapshot(self, profile, snapshot_meta, inputs, host):
|
|
return {
|
|
"sandbox_id": "restored1",
|
|
"host": host,
|
|
"remote_dir": "/tmp/sandboxer/restored1",
|
|
"compose_project": "sbx-e2e-restored1",
|
|
"compose_file": "docker-compose.yml",
|
|
"ssh_user": "root",
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def store(tmp_path: Path) -> SandboxStore:
|
|
return SandboxStore(path=tmp_path / "sandboxes.json")
|
|
|
|
|
|
@pytest.fixture
|
|
def snapshots(tmp_path: Path) -> SnapshotStore:
|
|
return SnapshotStore(path=tmp_path / "snapshots.json")
|
|
|
|
|
|
def _ready_status(sandbox_id: str = "test1234") -> SandboxStatus:
|
|
now = datetime.now(UTC)
|
|
return SandboxStatus(
|
|
sandbox_id=sandbox_id,
|
|
profile_id="profile.compose-checkpoint",
|
|
extension_id="ext.compose-ssh",
|
|
state=SandboxState.READY,
|
|
consumer=Consumer(actor=ActorType.ADM, project="sand-boxer"),
|
|
host="coulombcore",
|
|
reachability=Reachability(
|
|
ssh="root@coulombcore",
|
|
remote_dir="/tmp/sandboxer/test1234",
|
|
compose_project="sbx-e2e-test1234",
|
|
host="coulombcore",
|
|
),
|
|
inputs={
|
|
"repo": "/tmp/repo",
|
|
"compose_file": "docker-compose.yml",
|
|
"ssh_user": "root",
|
|
},
|
|
created_at=now,
|
|
updated_at=now,
|
|
ready_at=now,
|
|
)
|
|
|
|
|
|
def test_snapshot_store_roundtrip(snapshots: SnapshotStore) -> None:
|
|
now = datetime.now(UTC)
|
|
record = SnapshotRecord(
|
|
snapshot_id="snap12345678",
|
|
sandbox_id="test1234",
|
|
profile_id="profile.compose-checkpoint",
|
|
extension_id="ext.compose-ssh",
|
|
host="coulombcore",
|
|
artifact_path="/tmp/snap.tar.gz",
|
|
created_at=now,
|
|
)
|
|
snapshots.save(record)
|
|
loaded = snapshots.get("snap12345678")
|
|
assert loaded is not None
|
|
assert loaded.sandbox_id == "test1234"
|
|
|
|
|
|
def test_manager_snapshot_and_restore(store: SandboxStore, snapshots: SnapshotStore) -> None:
|
|
store.save(_ready_status())
|
|
manager = SandboxManager(store=store, snapshots=snapshots)
|
|
backend = SnapshotBackend()
|
|
|
|
with (
|
|
patch("sandboxer.core.manager.resolve_backend", return_value=backend),
|
|
patch("sandboxer.core.manager.emit_lifecycle_event", return_value=None),
|
|
patch("sandboxer.core.manager.resolve_host", return_value="coulombcore"),
|
|
):
|
|
record = manager.snapshot("test1234", name="pre-test")
|
|
assert record.snapshot_id == "snap12345678"
|
|
assert record.name == "pre-test"
|
|
assert record.size_bytes == 4096
|
|
|
|
status = manager.restore("snap12345678")
|
|
assert status.state == SandboxState.READY
|
|
assert status.sandbox_id == "restored1"
|
|
assert status.inputs.get("restored_from") == "snap12345678"
|
|
|
|
|
|
def test_snapshot_requires_ready(store: SandboxStore, snapshots: SnapshotStore) -> None:
|
|
status = _ready_status()
|
|
status.state = SandboxState.PROVISIONING
|
|
store.save(status)
|
|
manager = SandboxManager(store=store, snapshots=snapshots)
|
|
|
|
with pytest.raises(RuntimeError, match="ready"):
|
|
manager.snapshot("test1234")
|
|
|
|
|
|
def test_create_snapshot_restore_flow(store: SandboxStore, snapshots: SnapshotStore) -> None:
|
|
manager = SandboxManager(store=store, snapshots=snapshots)
|
|
backend = SnapshotBackend()
|
|
request = SandboxCreateRequest(
|
|
profile="profile.compose-checkpoint",
|
|
inputs={"repo": "/tmp/repo"},
|
|
consumer=Consumer(actor=ActorType.ADM, project="sand-boxer"),
|
|
)
|
|
|
|
with (
|
|
patch("sandboxer.core.manager.resolve_backend", return_value=backend),
|
|
patch("sandboxer.core.manager.emit_lifecycle_event", return_value=None),
|
|
patch("sandboxer.core.manager.resolve_host", return_value="coulombcore"),
|
|
):
|
|
created = manager.create(request)
|
|
record = manager.snapshot(created.sandbox_id)
|
|
restored = manager.restore(record.snapshot_id)
|
|
assert restored.sandbox_id == "restored1" |