mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 11:49:28 +00:00
src/openultrasast/plane/delivery.py: Delivery(store, run, base) mints a presigned PUT for runs/<run>/<task>/output.tar and a GET per declared input (runs/<run>/<producer>/<artifact>), expiry OUSAST_TASK_TIMEOUT + 600 s; delivered() asks the store for the exact key; collect() downloads, extracts under the run directory with the receiver's member checks (no symlink, hard link or escape; nothing written on a rejection), re-puts the declared outputs as single objects and writes done.json; put_input() for inputs no task produces; save_state()/load_state() mirror state.json. The reconciler opens the store first (no fallback; the CLI reports a store that cannot open and submits nothing), mirrors state.json after every change and seeds a rerun with no local state from the store (the 2.1 bullet's state mirroring is what gives the new module its importer). The reconciler crossed 500 lines here (517) rather than at 2.4 as 1.6 projected: the bound moves to 550 with the reason in the assertion, as 1.6 decided. tests/plane_fake_store.py is the S3-shaped fake (objects in a dict, presigned URLs on a local server); tests/test_plane_delivery.py covers the body, expiry, polling, rejection, declared-only re-put, state seeding and that no URL reaches a log. Module-audit manifest regenerated (148/5, no orphan). Gates: pytest 2020 passed, 42 skipped, exit 0; ruff check 0; ruff format 0; mypy 0. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
146 lines
7.3 KiB
Python
146 lines
7.3 KiB
Python
"""plane-on-kubernetes task 2.1 (Req 3.1, 2.1): ``Delivery`` against an S3-shaped fake -- presigned URLs in the start
|
|
body, expiry, polling, extraction with the receiver's member checks, declared-only re-put, state seeding, and no URL
|
|
in any log line."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import json
|
|
import logging
|
|
import tarfile
|
|
import urllib.request
|
|
from collections.abc import Iterator
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from plane_fake_store import HttpObjectStore
|
|
|
|
from openultrasast.plane.delivery import GRACE_SECONDS, Delivery, DeliveryError, expiry, input_var
|
|
|
|
|
|
@pytest.fixture
|
|
def store() -> Iterator[HttpObjectStore]:
|
|
fake = HttpObjectStore()
|
|
try:
|
|
yield fake
|
|
finally:
|
|
fake.close()
|
|
|
|
|
|
def tar_of(files: dict[str, bytes], *, symlink: str | None = None) -> bytes:
|
|
buffer = io.BytesIO()
|
|
with tarfile.open(fileobj=buffer, mode="w") as tar:
|
|
for name, data in files.items():
|
|
info = tarfile.TarInfo(name)
|
|
info.size = len(data)
|
|
tar.addfile(info, io.BytesIO(data))
|
|
if symlink:
|
|
info = tarfile.TarInfo(symlink)
|
|
info.type = tarfile.SYMTYPE
|
|
info.linkname = "/etc/passwd"
|
|
tar.addfile(info)
|
|
return buffer.getvalue()
|
|
|
|
|
|
def put_via_url(url: str, data: bytes) -> int:
|
|
request = urllib.request.Request(url, data=data, method="PUT", headers={"Content-Type": "application/x-tar"})
|
|
with urllib.request.urlopen(request, timeout=10) as response: # noqa: S310 - the fake's local URL
|
|
return int(response.status)
|
|
|
|
|
|
def test_the_start_body_names_exactly_the_tar_and_the_declared_inputs(store: HttpObjectStore, tmp_path: Path, monkeypatch) -> None:
|
|
monkeypatch.setenv("OUSAST_TASK_TIMEOUT", "100")
|
|
delivery = Delivery(store, "run-1", tmp_path)
|
|
body = delivery.body("verify", {"facts": "repo-facts/facts.json", "pass a": "va/units.jsonl"})
|
|
assert set(body) == {"put", "inputs"}
|
|
assert body["put"].startswith(f"http://localhost:{store.server.server_address[1]}/fake-bucket/runs/run-1/verify/output.tar?")
|
|
assert set(body["inputs"]) == {"FACTS", "PASS_A"} and input_var("pass a") == "PASS_A"
|
|
assert "/runs/run-1/repo-facts/facts.json?" in body["inputs"]["FACTS"] and "/runs/run-1/va/units.jsonl?" in body["inputs"]["PASS_A"]
|
|
assert {m for m, _, _ in store.presigned} == {"PUT", "GET"}
|
|
assert {e for _, _, e in store.presigned} == {100 + GRACE_SECONDS}, "expiry = OUSAST_TASK_TIMEOUT + 600"
|
|
assert expiry({"OUSAST_TASK_TIMEOUT": "7200"}).total_seconds() == 7800 and expiry({}).total_seconds() == 7800
|
|
assert delivery.host == "localhost", "the hostname the egress policy must allow"
|
|
with pytest.raises(DeliveryError, match="not <producer>/<artifact>"):
|
|
delivery.input_urls({"x": "no-slash"})
|
|
|
|
|
|
def test_delivered_polls_the_store_and_collect_extracts_republishes_and_marks_done(
|
|
store: HttpObjectStore, tmp_path: Path, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
caplog.set_level(logging.DEBUG)
|
|
delivery = Delivery(store, "r", tmp_path / "r")
|
|
assert delivery.delivered("facts") is False
|
|
summary = {"status": "done", "units_done": 1, "units_total": 1}
|
|
tar = tar_of({"facts.json": b'{"a": 1}', "summary.json": json.dumps(summary).encode(), "scratch/notes.txt": b"not declared"})
|
|
assert put_via_url(delivery.put_url("facts"), tar) == 200, "the runner's PUT to the presigned URL"
|
|
assert delivery.delivered("facts") is True
|
|
got, size = delivery.collect("facts", ["facts.json", "summary.json", "missing.json"])
|
|
assert got == summary and size == len(tar)
|
|
assert (tmp_path / "r" / "facts" / "facts.json").read_bytes() == b'{"a": 1}'
|
|
assert (tmp_path / "r" / "facts" / "scratch" / "notes.txt").exists(), "extracted locally"
|
|
assert store.objects["runs/r/facts/facts.json"] == b'{"a": 1}' and "runs/r/facts/summary.json" in store.objects
|
|
assert "runs/r/facts/scratch/notes.txt" not in store.objects and "runs/r/facts/missing.json" not in store.objects, "declared only"
|
|
done = json.loads(store.objects["runs/r/facts/done.json"])
|
|
assert (
|
|
done["status"] == "done"
|
|
and done["bytes"] == len(tar)
|
|
and done["files"] == 3
|
|
and done["published"] == ["facts.json", "summary.json"]
|
|
)
|
|
# a consumer's presigned GET names one artifact and fetches it
|
|
urls = delivery.input_urls({"facts": "facts/facts.json"})
|
|
with urllib.request.urlopen(urls["FACTS"], timeout=10) as response: # noqa: S310
|
|
assert response.read() == b'{"a": 1}'
|
|
assert "X-Amz" not in caplog.text and "fake-bucket" not in caplog.text, "URLs never appear in logs"
|
|
|
|
|
|
@pytest.mark.parametrize("bad", ["../escape.json", "/abs.json"])
|
|
def test_a_tar_member_that_escapes_is_rejected_and_nothing_is_written(store: HttpObjectStore, tmp_path: Path, bad: str) -> None:
|
|
delivery = Delivery(store, "r", tmp_path / "r")
|
|
store._put(delivery.key("t", "output.tar"), tar_of({bad: b"{}", "summary.json": b"{}"}))
|
|
with pytest.raises(DeliveryError, match="rejected member"):
|
|
delivery.collect("t", ["summary.json"])
|
|
assert not (tmp_path / "r" / "t").exists() and not (tmp_path / "escape.json").exists()
|
|
assert "runs/r/t/done.json" not in store.objects and "runs/r/t/summary.json" not in store.objects
|
|
|
|
|
|
def test_a_symlink_member_and_a_non_tar_are_rejected(store: HttpObjectStore, tmp_path: Path) -> None:
|
|
delivery = Delivery(store, "r", tmp_path / "r")
|
|
store._put(delivery.key("t", "output.tar"), tar_of({"summary.json": b"{}"}, symlink="link"))
|
|
with pytest.raises(DeliveryError, match="rejected member link"):
|
|
delivery.collect("t", ["summary.json"])
|
|
store._put(delivery.key("t", "output.tar"), b"not a tar at all")
|
|
with pytest.raises(DeliveryError, match="bad tar"):
|
|
delivery.collect("t", ["summary.json"])
|
|
with pytest.raises(DeliveryError, match="not in the store"):
|
|
delivery.collect("never", [])
|
|
|
|
|
|
def test_state_is_mirrored_and_seeds_a_rerun_elsewhere(store: HttpObjectStore, tmp_path: Path) -> None:
|
|
first = Delivery(store, "r", tmp_path / "machine-a")
|
|
assert first.load_state() is None
|
|
first.save_state({"run": "r", "tasks": {"facts": {"status": "done"}}})
|
|
second = Delivery(store, "r", tmp_path / "machine-b") # another results root, the same store
|
|
assert second.load_state() == {"run": "r", "tasks": {"facts": {"status": "done"}}}
|
|
store._put(second.key("state.json"), b"not json")
|
|
assert second.load_state() is None
|
|
|
|
|
|
def test_put_input_stores_a_file_no_task_produces(store: HttpObjectStore, tmp_path: Path) -> None:
|
|
delivery = Delivery(store, "r", tmp_path / "r")
|
|
source = tmp_path / "candidates.json"
|
|
source.write_bytes(b"[1, 2, 3]")
|
|
url = delivery.put_input("candidates.json", source)
|
|
assert store.objects["runs/r/inputs/candidates.json"] == b"[1, 2, 3]"
|
|
with urllib.request.urlopen(url, timeout=10) as response: # noqa: S310
|
|
assert response.read() == b"[1, 2, 3]"
|
|
|
|
|
|
def test_publish_reputs_only_declared_files_under_the_task_directory(store: HttpObjectStore, tmp_path: Path) -> None:
|
|
delivery = Delivery(store, "r", tmp_path / "r")
|
|
(tmp_path / "r" / "seeded").mkdir(parents=True)
|
|
(tmp_path / "r" / "seeded" / "facts.json").write_bytes(b"{}")
|
|
(tmp_path / "r" / "secret.txt").write_bytes(b"outside")
|
|
assert delivery.publish("seeded", ["facts.json", "../secret.txt", "absent.json"]) == ["facts.json"]
|
|
assert set(store.objects) == {"runs/r/seeded/facts.json"}
|