mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 11:49:28 +00:00
merge(plane-on-kubernetes): groups 1-2 -- cluster profile, no host literals, delivery through the S3 store
Kind proofs: records 01 and 02. Task 2.6 (receiver removal) is paused while the safety net and the decision engine come first. deployment.md conflicts resolved in the plain-language style; corrects the RuntimeClass claim (Substrate runs runsc inside its worker pod). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
+26
-2
@@ -65,11 +65,12 @@ from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
argv = sys.argv[1:]
|
||||
context = ""
|
||||
if argv[:1] == ["--context"]:
|
||||
argv = argv[2:]
|
||||
context, argv = argv[1], argv[2:]
|
||||
verb, actor, space = argv[0], argv[2], argv[argv.index("-a") + 1]
|
||||
with (HERE / "ax.log").open("a") as log:
|
||||
log.write(f"{time.monotonic():.4f} egress {verb} {actor}\n")
|
||||
log.write(f"{time.monotonic():.4f} egress {verb} {actor} {context}\n")
|
||||
record = HERE / "applied" / f"{actor}.json"
|
||||
alive = record.exists() and not json.loads(record.read_text()).get("gone")
|
||||
store = HERE / "policies" / f"{space}_{actor}.json"
|
||||
@@ -103,3 +104,26 @@ def kubectl_ate(tmp_path, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setenv("OUSAST_KUBECTL_ATE", str(path))
|
||||
monkeypatch.setenv("OUSAST_EGRESS_SETTLE", "0")
|
||||
return path
|
||||
|
||||
|
||||
PROFILE_CONTEXT = "kind-test" # the fakes must receive this, never a context a module built (plane-on-kubernetes 1.2)
|
||||
PROFILE_TOML = (
|
||||
'exec = "local"\nkube_context = "{context}"\nregistry = "registry.test:5000"\nimages = "images.json"\n'
|
||||
'memory = "file://{memory}"\natespace = "default"\n[pools.default]\nreplicas = 1\ncpu = "1"\nmemory = "1Gi"\n'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def plane_profile(tmp_path, monkeypatch: pytest.MonkeyPatch):
|
||||
"""A throwaway PlaneProfile under ``tmp_path`` (context ``kind-test``, a file:// memory store, a digest-pinned
|
||||
images file), selected through ``OUSAST_PLANE_PROFILE`` so every plane module loads it."""
|
||||
import json
|
||||
|
||||
root = tmp_path / "profile"
|
||||
root.mkdir()
|
||||
(root / "test.toml").write_text(PROFILE_TOML.format(context=PROFILE_CONTEXT, memory=tmp_path / "memory"), encoding="utf-8")
|
||||
(root / "images.json").write_text(json.dumps({"runner": "registry.test:5000/ousast-runner@sha256:" + "ab" * 32}), encoding="utf-8")
|
||||
monkeypatch.setenv("OUSAST_PLANE_PROFILE", str(root / "test.toml"))
|
||||
for key in ("OUSAST_KUBE_CONTEXT", "OUSAST_MEMORY", "OUSAST_REGISTRY", "OUSAST_IMAGES", "OUSAST_EXEC"):
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
return root / "test.toml"
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""An S3-shaped memory store for the delivery tests: objects in a dict, presigned URLs served by a local HTTP server
|
||||
that accepts PUT, GET and HEAD by key (``/<bucket>/<key>?X-Amz-Expires=N&X-Amz-Signature=fake``), the way a
|
||||
presigned S3 URL looks to the runner. Nothing here is imported by the product."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from collections.abc import Mapping
|
||||
from datetime import timedelta
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import unquote, urlsplit
|
||||
|
||||
from openultrasast.plane.memory import MemoryStore
|
||||
|
||||
|
||||
class HttpObjectStore(MemoryStore):
|
||||
"""``presign_*`` hand out URLs on a local server; ``requests`` records every HTTP call (method, key)."""
|
||||
|
||||
def __init__(self, bucket: str = "fake-bucket", host: str = "localhost") -> None:
|
||||
self.objects: dict[str, bytes] = {}
|
||||
self.bucket, self.host = bucket, host
|
||||
self.requests: list[tuple[str, str]] = []
|
||||
self.presigned: list[tuple[str, str, int]] = []
|
||||
self.lock = threading.Lock()
|
||||
self.server = ThreadingHTTPServer(("127.0.0.1", 0), _Handler)
|
||||
self.server.store = self # type: ignore[attr-defined]
|
||||
threading.Thread(target=self.server.serve_forever, daemon=True).start()
|
||||
|
||||
def close(self) -> None:
|
||||
self.server.shutdown()
|
||||
self.server.server_close()
|
||||
|
||||
def describe(self) -> str:
|
||||
return f"fake://{self.bucket}"
|
||||
|
||||
def _get(self, key: str) -> tuple[bytes, str | None] | None:
|
||||
with self.lock:
|
||||
data = self.objects.get(key)
|
||||
return None if data is None else (data, "v1")
|
||||
|
||||
def _put(self, key: str, data: bytes, labels: Mapping[str, str] | None = None) -> None:
|
||||
with self.lock:
|
||||
self.objects[key] = bytes(data)
|
||||
|
||||
def _delete(self, key: str) -> None:
|
||||
with self.lock:
|
||||
self.objects.pop(key, None)
|
||||
|
||||
def _keys(self, prefix: str) -> list[str]:
|
||||
with self.lock:
|
||||
return sorted(k for k in self.objects if k.startswith(prefix))
|
||||
|
||||
def _url(self, method: str, key: str, expires: timedelta) -> str:
|
||||
self.presigned.append((method, key, int(expires.total_seconds())))
|
||||
port = self.server.server_address[1]
|
||||
return f"http://{self.host}:{port}/{self.bucket}/{key}?X-Amz-Expires={int(expires.total_seconds())}&X-Amz-Signature=fake"
|
||||
|
||||
def presign_put(self, key: str, expires: timedelta = timedelta(hours=1)) -> str:
|
||||
return self._url("PUT", key, expires)
|
||||
|
||||
def presign_get(self, key: str, expires: timedelta = timedelta(hours=1)) -> str:
|
||||
return self._url("GET", key, expires)
|
||||
|
||||
|
||||
class _Handler(BaseHTTPRequestHandler):
|
||||
def _key(self) -> str | None:
|
||||
store: HttpObjectStore = self.server.store # type: ignore[attr-defined]
|
||||
path = unquote(urlsplit(self.path).path)
|
||||
prefix = f"/{store.bucket}/"
|
||||
return path[len(prefix) :] if path.startswith(prefix) else None
|
||||
|
||||
def do_PUT(self) -> None: # noqa: N802
|
||||
store: HttpObjectStore = self.server.store # type: ignore[attr-defined]
|
||||
key = self._key()
|
||||
body = self.rfile.read(int(self.headers.get("Content-Length") or 0))
|
||||
store.requests.append(("PUT", key or self.path))
|
||||
if key is None:
|
||||
return self._answer(404)
|
||||
store._put(key, body)
|
||||
self._answer(200)
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
store: HttpObjectStore = self.server.store # type: ignore[attr-defined]
|
||||
key = self._key()
|
||||
store.requests.append(("GET", key or self.path))
|
||||
got = store._get(key) if key else None
|
||||
if got is None:
|
||||
return self._answer(404)
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Length", str(len(got[0])))
|
||||
self.end_headers()
|
||||
self.wfile.write(got[0])
|
||||
|
||||
def do_HEAD(self) -> None: # noqa: N802
|
||||
store: HttpObjectStore = self.server.store # type: ignore[attr-defined]
|
||||
key = self._key()
|
||||
store.requests.append(("HEAD", key or self.path))
|
||||
got = store._get(key) if key else None
|
||||
self.send_response(404 if got is None else 200)
|
||||
self.send_header("Content-Length", "0" if got is None else str(len(got[0])))
|
||||
self.end_headers()
|
||||
|
||||
def _answer(self, code: int) -> None:
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, format: str, *args: object) -> None: # noqa: A002
|
||||
pass
|
||||
@@ -0,0 +1,164 @@
|
||||
"""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, declared, 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"
|
||||
delivery.reset("facts") # the next attempt must not complete on this attempt's tar
|
||||
assert delivery.delivered("facts") is False and "runs/r/facts/done.json" not in store.objects
|
||||
assert "runs/r/facts/facts.json" in store.objects, "a re-put output stays for its consumers until it is re-published"
|
||||
|
||||
|
||||
@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_declared_artifacts_are_outputs_plus_what_consumers_name(tmp_path: Path) -> None:
|
||||
"""A consumer may declare a producer's summary.json (every task writes one) without the producer listing it."""
|
||||
from openultrasast.plane.manifests import load_manifests
|
||||
|
||||
text = (
|
||||
"apiVersion: ax.io/v1alpha1\nkind: Task\nmetadata: {name: t}\nspec: {command: [repo-facts]}\n---\n"
|
||||
"apiVersion: openultrasast.io/v1alpha1\nkind: Run\nmetadata: {name: r}\nspec:\n tasks:\n"
|
||||
" - {name: facts, task: t, outputs: [facts.json]}\n"
|
||||
" - {name: verify, task: t, inputs: {facts: facts/facts.json, facts_summary: facts/summary.json}, outputs: [units.jsonl]}\n"
|
||||
" - {name: agree, task: t, inputs: {pass: verify/units.jsonl, pass_summary: verify/summary.json}}\n"
|
||||
)
|
||||
(tmp_path / "run.yaml").write_text(text, encoding="utf-8")
|
||||
run = next(iter(load_manifests([tmp_path / "run.yaml"]).runs.values()))
|
||||
assert declared(run) == {"facts": ["facts.json", "summary.json"], "verify": ["units.jsonl", "summary.json"], "agree": []}
|
||||
|
||||
|
||||
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"}
|
||||
+58
-17
@@ -1,8 +1,10 @@
|
||||
"""ai-service-plane task 0: the per-task egress policy, its kubectl-ate calls, and the runner's dialled delivery.
|
||||
|
||||
Agent Substrate's egress gateway denies by default; :func:`egress.policy_for` must grant exactly the receiver (HTTP
|
||||
by name), the bound Workspaces' Git hosts and the bound Model's API host (TLS passthrough), and nothing a gateway
|
||||
would reject or that would intercept TLS: no IP address, no wildcard, no ``https`` rule.
|
||||
Agent Substrate's egress gateway denies by default; :func:`egress.policy_for` must grant exactly the memory store's
|
||||
host (where the runner PUTs its tar and GETs its inputs), the bound Workspaces' Git hosts and the bound Model's API
|
||||
host, all as one TLS passthrough rule on 443 (plane-on-kubernetes 2.3), and nothing a gateway would reject or that
|
||||
would intercept TLS: no IP address, no wildcard, no ``https`` rule, no ``http`` rule (the receiver path, kept for
|
||||
group 2 only, is the one exception).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -70,24 +72,59 @@ def assert_tight(policy: dict) -> None:
|
||||
ipaddress.ip_address(host)
|
||||
|
||||
|
||||
def test_model_free_task_gets_only_the_receiver(manifests) -> None:
|
||||
policy = egress.policy_for(manifests.tasks["facts"], [], None, egress.RECEIVER_HOST)
|
||||
assert policy == {"rules": [{"http": {"hostnames": [egress.RECEIVER_HOST], "ports": {"numbers": [80]}}}]}
|
||||
STORE = "files.store.example"
|
||||
|
||||
|
||||
def test_model_free_task_gets_only_the_store(manifests) -> None:
|
||||
policy = egress.policy_for(manifests.tasks["facts"], [], None, STORE)
|
||||
assert policy == {"rules": [{"tlsPassthrough": {"hostnames": [STORE], "ports": {"numbers": [443]}}}]}
|
||||
assert_tight(policy)
|
||||
assert egress.policy_for(manifests.tasks["facts"], [], None, None) == {"rules": []}, "no store, no Git, no Model: nothing"
|
||||
|
||||
|
||||
def test_the_receiver_path_adds_the_one_http_rule_until_it_is_removed(manifests) -> None:
|
||||
policy = egress.policy_for(manifests.tasks["facts"], [], None, None, receiver_host=egress.RECEIVER_HOST)
|
||||
assert policy == {"rules": [{"http": {"hostnames": [egress.RECEIVER_HOST], "ports": {"numbers": [80]}}}]}
|
||||
assert egress.policy_for(manifests.tasks["facts"], [], None, None, receiver_host="127.0.0.1") == {"rules": []}
|
||||
|
||||
|
||||
def test_model_bound_task_with_git_workspaces(manifests) -> None:
|
||||
task = manifests.tasks["verify"]
|
||||
policy = egress.policy_for(task, [manifests.workspaces["cases"]], manifests.models["deepseek-flash"], egress.RECEIVER_HOST)
|
||||
policy = egress.policy_for(task, [manifests.workspaces["cases"]], manifests.models["deepseek-flash"], STORE)
|
||||
assert policy["rules"] == [
|
||||
{"http": {"hostnames": [egress.RECEIVER_HOST], "ports": {"numbers": [80]}}},
|
||||
{"tlsPassthrough": {"hostnames": ["api.deepseek.com", "github.com"], "ports": {"numbers": [443]}}},
|
||||
], "one github.com for two repos; the ssh and IP-address repos add nothing"
|
||||
{"tlsPassthrough": {"hostnames": ["api.deepseek.com", STORE, "github.com"], "ports": {"numbers": [443]}}},
|
||||
], "one rule: the store, one github.com for two repos, the Model host; the ssh and IP-address repos add nothing"
|
||||
assert_tight(policy)
|
||||
|
||||
|
||||
def test_an_ip_receiver_is_not_granted(manifests) -> None:
|
||||
assert egress.policy_for(manifests.tasks["facts"], [], None, "127.0.0.1") == {"rules": []}
|
||||
def test_an_ip_store_endpoint_is_refused_naming_the_variable(manifests) -> None:
|
||||
with pytest.raises(ValueError, match="S3_ENDPOINT must be a hostname"):
|
||||
egress.policy_for(manifests.tasks["facts"], [], None, "127.0.0.1")
|
||||
|
||||
|
||||
def test_an_http_git_repo_is_refused(tmp_path: Path) -> None:
|
||||
path = tmp_path / "m.yaml"
|
||||
path.write_text(MANIFESTS.replace("https://github.com/example-org/alpha", "http://mirror.example/alpha"), encoding="utf-8")
|
||||
loaded = load_manifests([path])
|
||||
with pytest.raises(ValueError, match="http Git repos .*mirror.example.* cannot be reached"):
|
||||
egress.policy_for(loaded.tasks["verify"], [loaded.workspaces["cases"]], None, STORE)
|
||||
|
||||
|
||||
def test_every_committed_template_renders_the_expected_policy() -> None:
|
||||
"""For each template under plane/tasks/: the store, the Model's hosts when bound, nothing else; one TLS rule."""
|
||||
from openultrasast.plane.generate import TEMPLATES
|
||||
|
||||
plane = Path("plane")
|
||||
loaded = load_manifests([plane / "tasks" / f"{name}.yaml" for name in TEMPLATES] + sorted((plane / "models").glob("*.yaml")))
|
||||
assert set(loaded.tasks) == set(TEMPLATES)
|
||||
for name, task in loaded.tasks.items():
|
||||
model = loaded.models.get(task.metadata.annotations.get("openultrasast.io/model") or "")
|
||||
policy = egress.policy_for(task, [], model, STORE)
|
||||
expected = sorted({STORE, *(model.egress_hosts if model else ())})
|
||||
assert policy == {"rules": [{"tlsPassthrough": {"hostnames": expected, "ports": {"numbers": [443]}}}]}, name
|
||||
assert_tight(policy)
|
||||
bound = {n for n, t in loaded.tasks.items() if "openultrasast.io/model" in t.metadata.annotations}
|
||||
assert bound == {"verify", "roles"}, "the model-bound templates, and only they, get the provider host"
|
||||
|
||||
|
||||
def test_deepseek_model_manifest_names_the_endpoint_host() -> None:
|
||||
@@ -121,7 +158,7 @@ def calls(tmp_path: Path) -> list[str]:
|
||||
return [" ".join(line.split()[2:4]) for line in (tmp_path / "ax.log").read_text().splitlines()]
|
||||
|
||||
|
||||
POLICY = {"rules": [{"http": {"hostnames": [egress.RECEIVER_HOST], "ports": {"numbers": [80]}}}]}
|
||||
POLICY = {"rules": [{"tlsPassthrough": {"hostnames": ["files.store.example"], "ports": {"numbers": [443]}}}]}
|
||||
|
||||
|
||||
def test_apply_creates_reads_back_and_delete_confirms_the_cascade(tmp_path: Path, kubectl_ate: Path) -> None:
|
||||
@@ -134,10 +171,11 @@ def test_apply_creates_reads_back_and_delete_confirms_the_cascade(tmp_path: Path
|
||||
actor(tmp_path, "t1", gone=True) # ax delete task: the actor, and with it the policy, is gone
|
||||
assert cli.delete("t1", "default") is None
|
||||
assert calls(tmp_path) == ["get t1", "create t1", "get t1", "get t1", "get t1", "get t1"]
|
||||
assert {line.split()[-1] for line in (tmp_path / "ax.log").read_text().splitlines()} == {"kind-test"}, "the given context"
|
||||
|
||||
|
||||
def test_apply_replaces_an_existing_policy(tmp_path: Path, kubectl_ate: Path) -> None:
|
||||
cli = egress.Egress(str(kubectl_ate))
|
||||
cli = egress.Egress(str(kubectl_ate), context="kind-test")
|
||||
actor(tmp_path, "t2")
|
||||
cli.apply("t2", "default", {"rules": []})
|
||||
cli.apply("t2", "default", POLICY)
|
||||
@@ -146,7 +184,7 @@ def test_apply_replaces_an_existing_policy(tmp_path: Path, kubectl_ate: Path) ->
|
||||
|
||||
|
||||
def test_apply_waits_for_the_actor_then_gives_up(tmp_path: Path, kubectl_ate: Path) -> None:
|
||||
cli = egress.Egress(str(kubectl_ate))
|
||||
cli = egress.Egress(str(kubectl_ate), context="kind-test")
|
||||
with pytest.raises(RuntimeError, match="not found"):
|
||||
cli.apply("missing", "default", POLICY, timeout=0.2)
|
||||
timer = threading.Timer(0.3, actor, args=(tmp_path, "late"))
|
||||
@@ -157,10 +195,13 @@ def test_apply_waits_for_the_actor_then_gives_up(tmp_path: Path, kubectl_ate: Pa
|
||||
|
||||
def test_receiver_address_modes(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("OUSAST_ARTIFACT_HOST", "127.0.0.1")
|
||||
assert egress.receiver_address(4242) == ("http://127.0.0.1:4242/", None)
|
||||
assert egress.receiver_address(4242, None) == ("http://127.0.0.1:4242/", None)
|
||||
monkeypatch.delenv("OUSAST_ARTIFACT_HOST")
|
||||
monkeypatch.setenv("OUSAST_ARTIFACT_DIAL", "172.19.0.1:80")
|
||||
assert egress.receiver_address(18090) == (f"http://{egress.RECEIVER_HOST}/", "172.19.0.1:80")
|
||||
assert egress.receiver_address(18090, "kind-test") == (f"http://{egress.RECEIVER_HOST}/", "172.19.0.1:80")
|
||||
monkeypatch.delenv("OUSAST_ARTIFACT_DIAL")
|
||||
with pytest.raises(RuntimeError, match="kube context"):
|
||||
egress.receiver_address(18090, None) # no context to look the Service up in: never a built one
|
||||
|
||||
|
||||
# --- the runner dials the gateway address with the receiver's name as Host ----------------------------------------
|
||||
|
||||
@@ -290,3 +290,42 @@ def test_case_record_carries_sites_in_set_and_cost_and_the_image_is_repinned(tmp
|
||||
(tmp_path / "runner-image").write_text("localhost:5001/ousast-runner:dev\n")
|
||||
with pytest.raises(ValueError, match="digest-pinned"):
|
||||
repin_templates(paths["plane"], tmp_path / "runner-image")
|
||||
|
||||
|
||||
def test_repin_reads_the_profiles_images_file(tmp_path: Path) -> None:
|
||||
"""plane-on-kubernetes 1.4: ``ops/ax/up.sh`` writes ``kind-images.json`` ({"runner": ...}); repin reads it, the
|
||||
older one-line pin still reads, and a file without a runner key or without a digest is refused by name."""
|
||||
paths = _fixture(tmp_path)
|
||||
digest = "localhost:5001/ousast-runner@sha256:" + "cd" * 32
|
||||
images = tmp_path / "kind-images.json"
|
||||
images.write_text(json.dumps({"runner": digest}) + "\n")
|
||||
assert repin_templates(paths["plane"], images) == digest
|
||||
assert {t.image for t in load_manifests(sorted((paths["plane"] / "tasks").glob("*.yaml"))).tasks.values()} == {digest}
|
||||
images.write_text(json.dumps({"engine": digest}))
|
||||
with pytest.raises(ValueError, match="no runner image"):
|
||||
repin_templates(paths["plane"], images)
|
||||
images.write_text(json.dumps({"runner": "localhost:5001/ousast-runner:dev"}))
|
||||
with pytest.raises(ValueError, match="not digest-pinned"):
|
||||
repin_templates(paths["plane"], images)
|
||||
|
||||
|
||||
def test_workspaces_go_to_the_profiles_atespace(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""plane-on-kubernetes 1.2: ``generate.ATESPACE`` is a default; ``increment`` takes the profile's atespace."""
|
||||
paths = _fixture(tmp_path)
|
||||
(tmp_path / "p.toml").write_text('exec = "local"\nkube_context = "c"\nregistry = "r"\nimages = "i.json"\natespace = "team-a"\n')
|
||||
monkeypatch.setenv("OUSAST_PLANE_PROFILE", str(tmp_path / "p.toml"))
|
||||
increment(paths["population"], paths["set"], paths["candidates"], paths["scans"], plane=paths["plane"], run_name="sp", command="t")
|
||||
manifests = load_manifests(sorted((paths["plane"] / "workspaces").glob("*.yaml")))
|
||||
assert manifests.workspaces and {w.metadata.atespace for w in manifests.workspaces.values()} == {"team-a"}
|
||||
increment(
|
||||
paths["population"],
|
||||
paths["set"],
|
||||
paths["candidates"],
|
||||
paths["scans"],
|
||||
plane=paths["plane"],
|
||||
run_name="sp",
|
||||
command="t",
|
||||
atespace="z",
|
||||
)
|
||||
manifests = load_manifests(sorted((paths["plane"] / "workspaces").glob("*.yaml")))
|
||||
assert {w.metadata.atespace for w in manifests.workspaces.values()} == {"z"}, "an explicit atespace wins over the profile"
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"""plane-on-kubernetes Req 1.3: no module, manifest or page assumes this host.
|
||||
|
||||
The scan covers ``src/``, ``plane/``, ``ops/`` (minus ``ops/ax/up.sh``, which creates the kind cluster and may name
|
||||
it, and ``ops/k8s/profiles/``, which *are* the configuration) and ``docs/`` for the kind context, the kind registry,
|
||||
the kind network's gateway, a loopback address with a port, and ``port-forward``. Allowed: ``port-forward`` and the
|
||||
loopback address inside ``router.py`` (the one module that opens the tunnel), and any line tagged ``ax-tunnel``
|
||||
(the tunnel is how both profiles reach ax and the router) or ``loopback`` (a process talking to itself). A
|
||||
digest-pinned ``image:`` line of a committed Task template under ``plane/tasks/`` is the kind profile's own value,
|
||||
written there by ``ousast plane`` repinning from ``ops/k8s/profiles/kind-images.json``; it is accepted only while it
|
||||
names that profile's registry, so the templates and the profile cannot drift apart. The patterns are built at runtime
|
||||
so this file does not match them (as ``test_removed_plane_references.py`` does).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from openultrasast.plane.profile import load_profile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCANNED = ("src", "plane", "ops", "docs")
|
||||
EXEMPT = (Path("ops/ax/up.sh"), Path("ops/k8s/profiles"))
|
||||
ROUTER = Path("src/openultrasast/plane/router.py")
|
||||
PATTERNS = {
|
||||
"kind context": re.compile("kind-" + "ousast"),
|
||||
"kind registry": re.compile("localhost:" + "5001"),
|
||||
"kind gateway": re.compile(r"172\.19\."),
|
||||
"loopback with port": re.compile(r"127\.0\.0\.1" + ":"),
|
||||
"port-forward": re.compile("port-" + "forward"),
|
||||
}
|
||||
ROUTER_ONLY = {"loopback with port", "port-forward"}
|
||||
TAGS = re.compile(r"ax-tunnel|loopback")
|
||||
_IMAGE_LINE = re.compile(r'^\s*image:\s*"?(?P<ref>\S+?)@sha256:[0-9a-f]{64}"?\s*(#.*)?$')
|
||||
|
||||
|
||||
def _files() -> list[Path]:
|
||||
out = []
|
||||
for top in SCANNED:
|
||||
for path in sorted((ROOT / top).rglob("*")):
|
||||
relative = path.relative_to(ROOT)
|
||||
if not path.is_file() or "__pycache__" in relative.parts or path.suffix in (".png", ".jpg", ".gz", ".zip"):
|
||||
continue
|
||||
if any(relative == e or e in relative.parents for e in EXEMPT):
|
||||
continue
|
||||
out.append(path)
|
||||
return out
|
||||
|
||||
|
||||
def _pinned_template_image(relative: Path, line: str, registry: str) -> bool:
|
||||
"""A committed template's digest-pinned image in the kind profile's registry: configuration, not a literal."""
|
||||
if relative.parts[:2] != ("plane", "tasks"):
|
||||
return False
|
||||
found = _IMAGE_LINE.match(line)
|
||||
return bool(found) and found.group("ref").startswith(registry + "/")
|
||||
|
||||
|
||||
def offenders(paths: list[Path], registry: str, root: Path = ROOT) -> list[str]:
|
||||
found = []
|
||||
for path in paths:
|
||||
relative = path.relative_to(root)
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except (UnicodeDecodeError, OSError):
|
||||
continue
|
||||
for number, line in enumerate(text.splitlines(), 1):
|
||||
for label, pattern in PATTERNS.items():
|
||||
if not pattern.search(line):
|
||||
continue
|
||||
if label in ROUTER_ONLY and relative == ROUTER:
|
||||
continue
|
||||
if TAGS.search(line) or _pinned_template_image(relative, line, registry):
|
||||
continue
|
||||
found.append(f"{relative}:{number}: [{label}] {line.strip()[:120]}")
|
||||
return found
|
||||
|
||||
|
||||
def test_no_host_literal_outside_the_profile_and_the_kind_bring_up() -> None:
|
||||
kind = load_profile("kind", environ={})
|
||||
found = offenders(_files(), kind.registry)
|
||||
assert found == [], "host-only literals (Req 1.3):\n" + "\n".join(found)
|
||||
|
||||
|
||||
def test_the_scan_sees_a_reinserted_literal(tmp_path: Path) -> None:
|
||||
"""The instrument is not vacuous: each pattern is caught, a tag or the router exempts only what it says."""
|
||||
kind = load_profile("kind", environ={})
|
||||
src = tmp_path / "src"
|
||||
src.mkdir()
|
||||
(src / "a.py").write_text(f'ctx = "{"kind-" + "ousast"}"\nreg = "{"localhost:" + "5001"}"\nip = "172.19.0.1"\n', encoding="utf-8")
|
||||
(src / "b.py").write_text('url = "http://127.0.0.1:8080" # loopback, this process\nargv = ["port-' + 'forward"] # ax-tunnel\n')
|
||||
(src / "c.py").write_text('argv = ["port-' + 'forward"]\n', encoding="utf-8")
|
||||
paths = sorted(src.glob("*.py"))
|
||||
found = [o.split(": ", 1)[1] for o in offenders(paths, kind.registry, root=tmp_path)]
|
||||
assert found == [
|
||||
"[kind context] ctx = " + '"kind-' + 'ousast"',
|
||||
"[kind registry] reg = " + '"localhost:' + '5001"',
|
||||
'[kind gateway] ip = "172.19.0.1"',
|
||||
"[port-forward] argv = " + '["port-' + 'forward"]',
|
||||
]
|
||||
|
||||
|
||||
def test_the_committed_templates_name_the_kind_profiles_registry() -> None:
|
||||
"""The one exemption is checked, not assumed: every template image is in the kind registry, by digest."""
|
||||
kind = load_profile("kind", environ={})
|
||||
images = []
|
||||
for path in sorted((ROOT / "plane/tasks").glob("*.yaml")):
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
found = _IMAGE_LINE.match(line)
|
||||
if found:
|
||||
images.append(found.group("ref"))
|
||||
assert found.group("ref").startswith(kind.registry + "/"), f"{path.name}: {line.strip()}"
|
||||
assert images, "the templates carry digest-pinned images"
|
||||
@@ -0,0 +1,158 @@
|
||||
"""plane-on-kubernetes task 1.1: the ``PlaneProfile`` (Req 1.1). Defaults, file-then-environment precedence, every
|
||||
rejection naming its field, both committed profiles loading, and no committed value that looks like a credential."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from openultrasast.plane import profile as profile_module
|
||||
from openultrasast.plane.profile import PlaneProfile, Pool, ProfileError, load_images, load_profile, profiles_dir
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DIGEST = "sha256:" + "ab" * 32
|
||||
MINIMAL = 'exec = "local"\nkube_context = "kind-test"\nregistry = "registry.test:5000"\nimages = "images.json"\nmemory = "s3://m"\n'
|
||||
|
||||
|
||||
def write_profile(tmp_path: Path, text: str = MINIMAL, name: str = "test") -> Path:
|
||||
path = tmp_path / f"{name}.toml"
|
||||
path.write_text(text, encoding="utf-8")
|
||||
(tmp_path / "images.json").write_text(json.dumps({"runner": f"registry.test:5000/ousast-runner@{DIGEST}"}), encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def test_defaults_and_the_images_path_resolve_against_the_profile_file(tmp_path: Path) -> None:
|
||||
loaded = load_profile(str(write_profile(tmp_path)), environ={})
|
||||
assert loaded.name == "test" and loaded.exec == "local" and loaded.kube_context == "kind-test"
|
||||
assert loaded.images == tmp_path / "images.json" and loaded.load_images() == {"runner": f"registry.test:5000/ousast-runner@{DIGEST}"}
|
||||
assert loaded.atespace == "default" and loaded.router_url == "" and loaded.pools == {}
|
||||
assert "(tunnel through the kube context)" in loaded.addresses()["router_url"]
|
||||
|
||||
|
||||
def test_the_environment_overrides_the_file_and_names_the_profile(tmp_path: Path) -> None:
|
||||
path = write_profile(tmp_path, MINIMAL + '[pools.default]\nreplicas = 2\ncpu = "1"\nmemory = "1536Mi"\n')
|
||||
env = {
|
||||
"OUSAST_PLANE_PROFILE": str(path),
|
||||
"OUSAST_KUBE_CONTEXT": "other-cluster",
|
||||
"OUSAST_MEMORY": "s3://elsewhere/prefix",
|
||||
"OUSAST_ROUTER_URL": "https://router.test",
|
||||
"OUSAST_POOL_DEFAULT_REPLICAS": "5",
|
||||
"OUSAST_POOL_DEFAULT_MEMORY": "2Gi",
|
||||
"OUSAST_EXEC": "", # an empty variable is not an override
|
||||
}
|
||||
loaded = load_profile(environ=env)
|
||||
assert (loaded.kube_context, loaded.memory, loaded.router_url, loaded.exec) == (
|
||||
"other-cluster",
|
||||
"s3://elsewhere/prefix",
|
||||
"https://router.test",
|
||||
"local",
|
||||
)
|
||||
assert loaded.pools == {"default": Pool(replicas=5, cpu="1", memory="2Gi")}
|
||||
assert load_profile(str(path), environ={}).kube_context == "kind-test", "the file is unchanged"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("text", "field"),
|
||||
[
|
||||
(MINIMAL + 'colour = "blue"\n', "unknown keys: colour"),
|
||||
(MINIMAL + 'aws_secret_key = "x"\n', "aws_secret_key looks like a credential"),
|
||||
(MINIMAL + 'api_token = "x"\n', "api_token looks like a credential"),
|
||||
(MINIMAL.replace('exec = "local"', 'exec = "cluster"'), "exec must be one of local, remote"),
|
||||
(MINIMAL + '[pools.default]\nreplicas = "two"\ncpu = "1"\nmemory = "1Gi"\n', "pools.default.replicas must be an integer"),
|
||||
(MINIMAL + '[pools.default]\nreplicas = 1\ncpu = "1"\n', "pools.default lacks memory"),
|
||||
(MINIMAL + '[pools.default]\nreplicas = 1\ncpu = "1"\nmemory = "1Gi"\nnodes = 3\n', "pools.default has unknown keys: nodes"),
|
||||
(MINIMAL.replace('kube_context = "kind-test"', "kube_context = 3"), "kube_context must be a string"),
|
||||
],
|
||||
)
|
||||
def test_each_rejection_names_the_field(tmp_path: Path, text: str, field: str) -> None:
|
||||
path = write_profile(tmp_path, text)
|
||||
with pytest.raises(ProfileError, match=re.escape(field)) as exc:
|
||||
load_profile(str(path), environ={})
|
||||
assert str(path) in str(exc.value)
|
||||
|
||||
|
||||
def test_a_credential_shaped_value_is_refused(tmp_path: Path) -> None:
|
||||
path = write_profile(tmp_path, MINIMAL.replace("s3://m", "sk-" + "a1b2c3d4" * 3))
|
||||
with pytest.raises(ProfileError, match="value of memory looks like a credential"):
|
||||
load_profile(str(path), environ={})
|
||||
|
||||
|
||||
def test_an_images_file_without_digest_pins_is_refused(tmp_path: Path) -> None:
|
||||
path = write_profile(tmp_path)
|
||||
(tmp_path / "images.json").write_text(json.dumps({"runner": "registry.test:5000/ousast-runner:dev"}), encoding="utf-8")
|
||||
with pytest.raises(ProfileError, match="runner is not digest-pinned"):
|
||||
load_profile(str(path), environ={})
|
||||
(tmp_path / "images.json").write_text(f"registry.test:5000/ousast-runner@{DIGEST}\n", encoding="utf-8")
|
||||
assert load_images(tmp_path / "images.json") == {"runner": f"registry.test:5000/ousast-runner@{DIGEST}"}, "the one-line form"
|
||||
(tmp_path / "images.json").unlink()
|
||||
loaded = load_profile(str(path), environ={}) # a file the release has not published yet: doctor's finding, not a load error
|
||||
with pytest.raises(ProfileError, match="images file"):
|
||||
loaded.load_images()
|
||||
|
||||
|
||||
def test_an_environment_override_of_the_wrong_shape_is_refused(tmp_path: Path) -> None:
|
||||
path = write_profile(tmp_path, MINIMAL + '[pools.default]\nreplicas = 1\ncpu = "1"\nmemory = "1Gi"\n')
|
||||
with pytest.raises(ProfileError, match="OUSAST_POOL_DEFAULT_REPLICAS must be an integer"):
|
||||
load_profile(str(path), environ={"OUSAST_POOL_DEFAULT_REPLICAS": "many"})
|
||||
with pytest.raises(ProfileError, match="names a pool the profile does not declare"):
|
||||
load_profile(str(path), environ={"OUSAST_POOL_ENGINE_REPLICAS": "1"})
|
||||
with pytest.raises(ProfileError, match="exec must be one of"):
|
||||
load_profile(str(path), environ={"OUSAST_EXEC": "both"})
|
||||
|
||||
|
||||
def test_a_missing_profile_names_its_path(tmp_path: Path) -> None:
|
||||
with pytest.raises(ProfileError, match="no-such.toml"):
|
||||
load_profile(str(tmp_path / "no-such.toml"), environ={})
|
||||
with pytest.raises(ProfileError, match="unknown"):
|
||||
load_profile("unknown", environ={"OUSAST_PLANE_PROFILES": str(tmp_path)})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["kind", "k3s"])
|
||||
def test_both_committed_profiles_load_with_the_designs_values(name: str) -> None:
|
||||
loaded = load_profile(name, environ={})
|
||||
assert loaded.path == profiles_dir() / f"{name}.toml" and profiles_dir() == ROOT / "ops/k8s/profiles"
|
||||
assert loaded.exec == {"kind": "local", "k3s": "remote"}[name]
|
||||
assert loaded.memory == "s3://sast-memory" and loaded.atespace == "default" and loaded.snapshots_bucket == "s3://ax-snapshots"
|
||||
assert set(loaded.pools) == {"default", "engine"} and loaded.pools["engine"].memory == "4Gi"
|
||||
assert loaded.router_url == "" and loaded.ax_server == "", "both profiles tunnel through the Kubernetes API"
|
||||
if name == "kind":
|
||||
assert loaded.image_pull_secret == "" and loaded.images.name == "kind-images.json" and loaded.images.is_file()
|
||||
assert set(loaded.load_images()) == {"runner"} and loaded.load_images()["runner"].startswith(loaded.registry + "/")
|
||||
else:
|
||||
assert loaded.image_pull_secret == "ghcr-pull" and loaded.registry.startswith("ghcr.io/") and loaded.images.name == "images.json"
|
||||
assert not loaded.images.is_file(), "the release asset is downloaded, never committed (design section 8)"
|
||||
|
||||
|
||||
def test_no_committed_profile_value_matches_a_credential_pattern() -> None:
|
||||
patterns = (
|
||||
re.compile(r"(?i)(api_key|secret_key|access_key|token|password)\s*="), # image_pull_secret names a Secret, holds none
|
||||
re.compile(r"sk-[A-Za-z0-9]{8,}"),
|
||||
re.compile(r"AKIA[0-9A-Z]{12,}"),
|
||||
)
|
||||
for path in sorted(profiles_dir().glob("*.toml")):
|
||||
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
||||
code = line.split("#", 1)[0]
|
||||
for pattern in patterns:
|
||||
assert not pattern.search(code), f"{path}:{number}: {line.strip()}"
|
||||
|
||||
|
||||
def test_print_for_shell_scripts(capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None:
|
||||
path = write_profile(tmp_path, MINIMAL + '[pools.default]\nreplicas = 3\ncpu = "1"\nmemory = "1Gi"\n')
|
||||
fields = ["registry", "kube_context", "images.runner", "pools.default.replicas"]
|
||||
assert profile_module.main(["--profile", str(path), "--print", *fields]) == 0
|
||||
assert capsys.readouterr().out.splitlines() == ["registry.test:5000", "kind-test", f"registry.test:5000/ousast-runner@{DIGEST}", "3"]
|
||||
assert profile_module.main(["--profile", str(path), "--print", "images"]) == 0, "the images file's path itself (up.sh writes it)"
|
||||
assert capsys.readouterr().out.strip() == str(tmp_path / "images.json")
|
||||
assert profile_module.main(["--profile", str(path), "--print", "images.engine"]) == 2
|
||||
assert "no image named 'engine'" in capsys.readouterr().err
|
||||
assert profile_module.main(["--profile", str(path), "--print", "nothing"]) == 2
|
||||
|
||||
|
||||
def test_profile_is_frozen_and_typed() -> None:
|
||||
loaded = load_profile("kind", environ={})
|
||||
assert isinstance(loaded, PlaneProfile)
|
||||
with pytest.raises(AttributeError):
|
||||
loaded.exec = "remote" # type: ignore[misc]
|
||||
+176
-27
@@ -4,9 +4,10 @@ The fake is a Python script the tests write into ``tmp_path`` that follows ax's
|
||||
internal/controller/reconciler.go, internal/server/server.go): ``apply`` creates the task ``Suspended``; ``resume``
|
||||
fails with DeadlineExceeded the first time (ax then reports ``Failed``, as while Agent Substrate builds a new
|
||||
image's golden snapshot) and succeeds after; ``get task`` answers ``Running`` forever -- ax has no Completed phase
|
||||
-- and on the scripted poll it POSTs a scripted tar to the ``OUSAST_ARTIFACT_URL`` in the applied Task's env, or
|
||||
turns ``Failed`` with a condition message, or vanishes. Every outcome comes from a scripted delivery, and a
|
||||
delivery happens only after the reconciler's start request reached the fake router (Req 4.5).
|
||||
-- and on the scripted poll it PUTs a scripted tar to the presigned ``delivery.put`` URL the start request carried
|
||||
(plane-on-kubernetes 2.4; or POSTs it to ``OUSAST_ARTIFACT_URL`` in receiver mode), or turns ``Failed`` with a
|
||||
condition message, or vanishes. Every outcome comes from a scripted delivery, and a delivery happens only after
|
||||
the reconciler's start request reached the fake router (Req 4.5). The store is ``plane_fake_store.HttpObjectStore``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -28,11 +29,13 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from plane_fake_store import HttpObjectStore
|
||||
|
||||
from openultrasast.plane import reconciler
|
||||
from openultrasast.plane.workspaces import workspaces
|
||||
|
||||
RECONCILER = Path("src/openultrasast/plane/reconciler.py")
|
||||
PROFILE_CONTEXT = "kind-test" # conftest's plane_profile fixture: the context every fake must be handed
|
||||
PIN = "0123456789abcdef0123456789abcdef01234567"
|
||||
SECRET = "sk-reconciler-test-7a2b-only-in-the-start-request"
|
||||
|
||||
@@ -145,11 +148,15 @@ elif argv[:2] == ["get", "task"]:
|
||||
started = (HERE / "started" / argv[2]).exists() # the runner runs its command only after the start request
|
||||
if rec["phase"] == "Running" and rec["polls"] >= script.get("polls", 1) and started and not rec.get("sent"):
|
||||
rec["sent"] = True
|
||||
body = json.loads((HERE / "started" / argv[2]).read_text() or "{{}}") # the start body the router saw
|
||||
delivery = body.get("delivery") or {{}}
|
||||
if script.get("deliver", True) and "phase" not in script and not script.get("vanish"):
|
||||
files = script.get("files", {{}})
|
||||
base = env["OUSAST_ARTIFACT_URL"].rstrip("/") + "/inputs/"
|
||||
base = env.get("OUSAST_ARTIFACT_URL", "").rstrip("/") + "/inputs/"
|
||||
for name, ref in json.loads(env.get("OUSAST_INPUTS", "{{}}")).items():
|
||||
fetch = urllib.request.Request(base + ref, headers={{"X-Ousast-Run": env["OUSAST_RUN"], "X-Ousast-Task": task}})
|
||||
presigned = (delivery.get("inputs") or {{}}).get(name)
|
||||
fetch = urllib.request.Request(presigned) if presigned else urllib.request.Request(
|
||||
base + ref, headers={{"X-Ousast-Run": env["OUSAST_RUN"], "X-Ousast-Task": task}})
|
||||
try:
|
||||
with urllib.request.urlopen(fetch) as resp:
|
||||
dest = HERE / "fetched" / task / ref
|
||||
@@ -170,7 +177,11 @@ elif argv[:2] == ["get", "task"]:
|
||||
tar.addfile(info, io.BytesIO(data))
|
||||
headers = {{"Content-Type": "application/x-tar", "X-Ousast-Run": env["OUSAST_RUN"]}}
|
||||
headers["X-Ousast-Task"] = script.get("as_task", task)
|
||||
req = urllib.request.Request(env["OUSAST_ARTIFACT_URL"], data=buf.getvalue(), method="POST", headers=headers)
|
||||
if delivery.get("put"): # the store path: PUT to the presigned URL, no receiver headers
|
||||
tar_headers = {{"Content-Type": "application/x-tar"}}
|
||||
req = urllib.request.Request(delivery["put"], data=buf.getvalue(), method="PUT", headers=tar_headers)
|
||||
else:
|
||||
req = urllib.request.Request(env["OUSAST_ARTIFACT_URL"], data=buf.getvalue(), method="POST", headers=headers)
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
(HERE / "ax.log").open("a").write(f"{{time.monotonic():.4f}} delivered {{task}} {{resp.status}}\\n")
|
||||
@@ -282,6 +293,7 @@ SCRIPT = {
|
||||
|
||||
class Fake:
|
||||
router: FakeRouter
|
||||
store: HttpObjectStore
|
||||
|
||||
def __init__(self, root: Path) -> None:
|
||||
self.root = root
|
||||
@@ -342,7 +354,8 @@ class _RouterHandler(BaseHTTPRequestHandler):
|
||||
self.server.starts.append({"at": time.monotonic(), "path": self.path, "target": target, "body": body, "code": code})
|
||||
if code == 202:
|
||||
(self.server.root / "started").mkdir(exist_ok=True)
|
||||
(self.server.root / "started" / name).touch()
|
||||
without_secrets = {k: v for k, v in body.items() if k != "credentials"} # the fake ax reads delivery from it
|
||||
(self.server.root / "started" / name).write_text(json.dumps(without_secrets))
|
||||
with (self.server.root / "ax.log").open("a") as log:
|
||||
log.write(f"{time.monotonic():.4f} started {name} {code}\n")
|
||||
self.send_response(code)
|
||||
@@ -355,7 +368,7 @@ class _RouterHandler(BaseHTTPRequestHandler):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, kubectl_ate: Path) -> Iterator[Fake]:
|
||||
def fake(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, kubectl_ate: Path, plane_profile: Path) -> Iterator[Fake]:
|
||||
monkeypatch.setenv("OUSAST_RESULTS", str(tmp_path / "results"))
|
||||
monkeypatch.setenv("OUSAST_ARTIFACT_HOST", "127.0.0.1")
|
||||
monkeypatch.setenv("OUSAST_POLL_SECONDS", "0.02")
|
||||
@@ -365,12 +378,15 @@ def fake(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, kubectl_ate: Path) ->
|
||||
fake = Fake(tmp_path)
|
||||
fake.script(SCRIPT)
|
||||
fake.router = FakeRouter(tmp_path)
|
||||
fake.store = HttpObjectStore() # the memory store the reconciler opens first: S3-shaped, presigned URLs served locally
|
||||
monkeypatch.setattr(reconciler, "open_store", lambda spec=None, environ=None: fake.store)
|
||||
monkeypatch.setenv("OUSAST_ROUTER_URL", f"http://127.0.0.1:{fake.router.server_address[1]}")
|
||||
try:
|
||||
yield fake
|
||||
finally:
|
||||
fake.router.shutdown()
|
||||
fake.router.server_close()
|
||||
fake.store.close()
|
||||
|
||||
|
||||
def write_run(tmp_path: Path, name: str, tasks: str = CHAIN) -> Path:
|
||||
@@ -404,13 +420,17 @@ def test_chain_runs_in_order_with_inputs_env_and_model(fake: Fake, tmp_path: Pat
|
||||
assert env["OUSAST_BUDGET_USD"] == "2.5" and env["OUSAST_BUDGET_CALLS"] == "40"
|
||||
assert env["OUSAST_INPUT_FACTS"] == "/workspace/.ousast-in/verify/repo-facts/facts.json"
|
||||
assert json.loads(env["OUSAST_INPUTS"]) == {"FACTS": "repo-facts/facts.json"}
|
||||
assert env["OUSAST_ARTIFACT_URL"].startswith("http://127.0.0.1:")
|
||||
assert "OUSAST_ARTIFACT_URL" not in env, "the store path renders no receiver address into the Task"
|
||||
assert env["OUSAST_MODEL"] == "deepseek-flash"
|
||||
assert json.loads(env["OUSAST_MODEL_PARAMS"]) == {"cache_hit_per_m": 0.07, "input_per_m": 0.27, "output_per_m": 1.1}
|
||||
kinds = [(d["kind"], d["metadata"]["name"]) for d in verify["docs"]]
|
||||
assert kinds == [("Model", "deepseek-flash"), ("Workspace", "case-pin"), ("Task", "chain-verify")], "no inputs Workspace"
|
||||
assert verify["docs"][-1]["spec"]["workspaces"] == [{"name": "case-pin", "path": "/workspace"}]
|
||||
assert [n for _, n in fake.events("fetched")] == ["verify FACTS 200", "agree PASS 200"], "inputs come over the receiver's GET"
|
||||
assert [n for _, n in fake.events("fetched")] == ["verify FACTS 200", "agree PASS 200"], "inputs come by their presigned GET"
|
||||
assert ("GET", "runs/chain/repo-facts/facts.json") in fake.store.requests and (
|
||||
"PUT",
|
||||
"runs/chain/verify/output.tar",
|
||||
) in fake.store.requests
|
||||
assert json.loads((tmp_path / "fetched" / "verify" / "repo-facts" / "facts.json").read_text()) == {"a.py": {"functions": ["f"]}}
|
||||
assert json.loads((tmp_path / "fetched" / "agree" / "verify" / "agreed.json").read_text()) == {"agreed": ["a.py:f"]}
|
||||
assert "annotations" not in verify["docs"][-1]["metadata"], "ax's ObjectMeta has no annotations"
|
||||
@@ -569,7 +589,15 @@ def test_start_is_sent_once_after_resume_with_the_models_credential(fake: Fake,
|
||||
assert [s["target"] for s in starts] == ["default/sig-repo-facts", "default/sig-verify", "default/sig-agree"], "once each"
|
||||
assert all(s["path"] == "/ousast/v1/start" and s["code"] == 202 for s in starts)
|
||||
by_task = {s["body"]["task"]: s for s in starts}
|
||||
assert by_task["sig-verify"]["body"] == {"run": "sig", "task": "sig-verify", "credentials": {"DEEPSEEK_API_KEY": SECRET}}
|
||||
body = by_task["sig-verify"]["body"]
|
||||
assert {k: body[k] for k in ("run", "task", "credentials")} == {
|
||||
"run": "sig",
|
||||
"task": "sig-verify",
|
||||
"credentials": {"DEEPSEEK_API_KEY": SECRET},
|
||||
}
|
||||
assert set(body["delivery"]) == {"put", "inputs"} and "/runs/sig/verify/output.tar?" in body["delivery"]["put"]
|
||||
assert set(body["delivery"]["inputs"]) == {"FACTS"} and "/runs/sig/repo-facts/facts.json?" in body["delivery"]["inputs"]["FACTS"]
|
||||
assert all("X-Amz" not in line for line in (tmp_path / "ax.log").read_text().splitlines()), "no presigned URL in the fake's log"
|
||||
assert by_task["sig-repo-facts"]["body"]["credentials"] == {} and by_task["sig-agree"]["body"]["credentials"] == {}, "no Model"
|
||||
accepted = {}
|
||||
for stamp, words in fake.log(): # the last resume of each task is the one ax accepted
|
||||
@@ -583,8 +611,10 @@ def test_start_is_sent_once_after_resume_with_the_models_credential(fake: Fake,
|
||||
assert verify_model["spec"]["secretKey"] == {"name": "deepseek", "key": "DEEPSEEK_API_KEY"}, "the manifest names the key"
|
||||
assert "DEEPSEEK_API_KEY" not in fake.applied("sig", "verify")["env"], "the rendered Task carries no credential"
|
||||
rendered = str(reconciler.run_dir("sig") / "verify" / "task.yaml")
|
||||
assert rendered in files_containing(tmp_path, "OUSAST_ARTIFACT_URL"), "the scan reads the rendered files it vouches for"
|
||||
assert rendered in files_containing(tmp_path, "OUSAST_INPUTS"), "the scan reads the rendered files it vouches for"
|
||||
assert files_containing(tmp_path, SECRET) == [], "no rendered task.yaml, state, log or applied record holds the value"
|
||||
leaked = [p for p in files_containing(tmp_path, "X-Amz-Signature") if "/started/" not in p] # the fake router's own records
|
||||
assert leaked == [], "nor a presigned URL in any product file (the fake store signs with that name)"
|
||||
|
||||
|
||||
def test_a_model_bound_task_without_its_secret_fails_before_ax_sees_it(fake: Fake, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@@ -631,7 +661,8 @@ def tar_of(files: dict[str, bytes]) -> bytes:
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def test_receiver_rejects_traversal_unknown_tasks_and_wrong_types(tmp_path: Path) -> None:
|
||||
def test_receiver_rejects_traversal_unknown_tasks_and_wrong_types(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("OUSAST_ARTIFACT_HOST", "127.0.0.1") # the direct test address: no Service lookup, no context needed
|
||||
receiver = reconciler.Receiver("r", tmp_path / "r", 0)
|
||||
receiver.running.add("t")
|
||||
headers = {"X-Ousast-Run": "r", "X-Ousast-Task": "t", "Content-Type": "application/x-tar"}
|
||||
@@ -647,6 +678,7 @@ def test_receiver_rejects_traversal_unknown_tasks_and_wrong_types(tmp_path: Path
|
||||
|
||||
|
||||
def test_delivery_for_a_task_that_is_not_running_is_rejected(fake: Fake, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("OUSAST_DELIVERY", "receiver") # the receiver path, kept for group 2 (task 2.6 removes this test)
|
||||
monkeypatch.setenv("OUSAST_TASK_TIMEOUT", "1") # the rejected delivery is no delivery: the task times out
|
||||
script = json.loads(json.dumps(SCRIPT))
|
||||
script["repo-facts"]["as_task"] = "agree" # the runner claims to be a task that has not started
|
||||
@@ -719,16 +751,31 @@ def test_status_of_an_unknown_run_says_so(fake: Fake) -> None:
|
||||
# --- doctor, workspaces, size ---------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_doctor_names_the_bring_up_script_when_nothing_is_reachable(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
monkeypatch.setenv("PATH", str(tmp_path)) # no kubectl, no curl
|
||||
def test_doctor_checks_the_profiles_cluster_and_reports_every_address(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, plane_profile: Path
|
||||
) -> None:
|
||||
"""Req 1.2: the configured context (never kind by name), the registry's runner reference, the memory store; the
|
||||
namespaces are not checked behind an unreachable context, so only that line fails for it."""
|
||||
monkeypatch.setenv("PATH", str(tmp_path)) # no kubectl, no docker
|
||||
from openultrasast.plane import doctor as doctor_module
|
||||
|
||||
monkeypatch.setattr(doctor_module, "_registry_has", lambda image: (False, f"registry.test:5000 unreachable for {image}"))
|
||||
checks = reconciler.doctor()
|
||||
assert [name for name, _, _ in checks] == [
|
||||
"kind cluster",
|
||||
"agent substrate (ate-system)",
|
||||
"ax controller (ax-system)",
|
||||
"runner image in kind registry",
|
||||
names = [name for name, _, _ in checks]
|
||||
assert names == [
|
||||
"profile test",
|
||||
"cluster kind-test",
|
||||
"runner image in registry.test:5000",
|
||||
f"memory store file://{tmp_path / 'memory'}",
|
||||
]
|
||||
assert all(not ok and "ops/ax/up.sh" in text for _, ok, text in checks)
|
||||
by_name = {name: (ok, text) for name, ok, text in checks}
|
||||
assert by_name["profile test"][0] and "kube_context=kind-test" in by_name["profile test"][1]
|
||||
for address in ("registry=registry.test:5000", "exec=local", "atespace=default", "router_url=(tunnel"):
|
||||
assert address in by_name["profile test"][1], address
|
||||
assert not by_name["cluster kind-test"][0] and "kind-test unreachable" in by_name["cluster kind-test"][1]
|
||||
assert not by_name["runner image in registry.test:5000"][0]
|
||||
assert by_name[f"memory store file://{tmp_path / 'memory'}"][0], "a file store opens; an S3 store is verified"
|
||||
assert not any("kind-" + "ousast" in text for _, _, text in checks), "nothing names the kind cluster"
|
||||
|
||||
|
||||
def test_workspaces_one_manifest_per_case_pin(tmp_path: Path) -> None:
|
||||
@@ -755,6 +802,23 @@ def test_workspaces_one_manifest_per_case_pin(tmp_path: Path) -> None:
|
||||
assert loaded["demo-sqli-benign-tip"].pins == {"repo": "d" * 40}
|
||||
|
||||
|
||||
def test_cli_doctor_takes_a_profile_and_fails_on_its_unreachable_context(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, plane_profile: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""plane-on-kubernetes 1.2: ``ousast plane doctor --profile`` drives the checks from that file, nothing built."""
|
||||
from openultrasast.cli import main
|
||||
from openultrasast.plane import doctor as doctor_module
|
||||
|
||||
monkeypatch.setenv("PATH", str(tmp_path))
|
||||
monkeypatch.setattr(doctor_module, "_registry_has", lambda image: (True, "present"))
|
||||
monkeypatch.delenv("OUSAST_PLANE_PROFILE")
|
||||
assert main(["plane", "doctor", "--profile", str(plane_profile)]) == 1
|
||||
out = capsys.readouterr().out
|
||||
assert "[FAIL] cluster kind-test:" in out and "[ok] profile test:" in out and "[ok] runner image in registry.test:5000" in out
|
||||
assert main(["plane", "doctor", "--profile", str(tmp_path / "missing.toml")]) == 2
|
||||
assert "missing.toml" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_rendered_documents_carry_only_ax_fields(tmp_path: Path) -> None:
|
||||
"""Req 1.1/1.4: whatever ``ax apply`` receives names only fields ax documents; annotations never leave."""
|
||||
(tmp_path / "repo-facts").mkdir()
|
||||
@@ -798,9 +862,13 @@ def test_mark_done_sets_the_state_under_the_run_lock(tmp_path: Path) -> None:
|
||||
assert not (base / "lock").exists(), "the lock is released"
|
||||
|
||||
|
||||
def test_reconciler_is_under_500_lines_and_holds_no_pipeline_logic() -> None:
|
||||
def test_reconciler_is_under_550_lines_and_holds_no_pipeline_logic() -> None:
|
||||
text = RECONCILER.read_text(encoding="utf-8")
|
||||
assert len(text.splitlines()) < 500, "Req 3.4: the reconciler stays under 500 lines"
|
||||
assert len(text.splitlines()) < 550, (
|
||||
"Req 3.4: the reconciler stays under 500 lines; plane-on-kubernetes 1.6/2.1 moved the bound to 550 while the "
|
||||
"store-delivery wiring (Delivery, state mirror and seed, the start-body delivery) sits beside the receiver path "
|
||||
"that task 2.6 removes -- 2.6 records the count and returns the bound to 500 if it allows"
|
||||
)
|
||||
for word in ("prompt(", "openai", "ChatClient", "score(", "system_prompt"):
|
||||
assert word not in text, f"Req 3.4: no pipeline logic in the reconciler ({word})"
|
||||
assert "Popen" not in text and "openultrasast.plane.tasks" not in text, "ax is the only executor"
|
||||
@@ -829,7 +897,7 @@ def test_doctor_treats_an_empty_namespace_as_not_ready(monkeypatch: pytest.Monke
|
||||
from openultrasast.plane import doctor
|
||||
|
||||
monkeypatch.setattr(doctor, "_sh", lambda *a: (True, "No resources found in ax-system namespace."))
|
||||
ok, text = doctor._pods_ready("ax-system", "kind-ousast")
|
||||
ok, text = doctor._pods_ready("ax-system", "kind-test")
|
||||
assert not ok and "no pods" in text
|
||||
monkeypatch.setattr(doctor, "_sh", lambda *a: (True, "a-1 1/1 Running 0 1m\nb-2 0/1 Completed 0 1m"))
|
||||
assert doctor._pods_ready("ns", "ctx") == (True, "2 pods ready")
|
||||
@@ -854,11 +922,13 @@ def test_egress_policy_between_apply_and_resume_and_checked_after_delete(fake: F
|
||||
order.append(first(prefix, order[-1] + 1))
|
||||
assert order == sorted(order)
|
||||
assert not (tmp_path / "policies" / "default_eg-verify.json").exists(), "the policy went with its actor"
|
||||
contexts = {line.split()[-1] for line in lines if line.startswith("egress ")}
|
||||
assert contexts == {PROFILE_CONTEXT}, "kubectl-ate received the profile's context, never a built one (1.2)"
|
||||
written = json.loads((tmp_path / "policies" / "default_eg-verify.written.json").read_text())
|
||||
tls = {"hostnames": ["api.deepseek.com", "example.invalid"], "ports": {"numbers": [443]}}
|
||||
assert written["rules"] == [{"tlsPassthrough": tls}], "the Model host and the Git host, nothing else"
|
||||
tls = {"hostnames": ["api.deepseek.com", "example.invalid", "localhost"], "ports": {"numbers": [443]}}
|
||||
assert written["rules"] == [{"tlsPassthrough": tls}], "the Model host, the Git host and the store host, nothing else"
|
||||
facts = json.loads((tmp_path / "policies" / "default_eg-repo-facts.written.json").read_text())
|
||||
assert facts["rules"] == [{"tlsPassthrough": {"hostnames": ["example.invalid"], "ports": {"numbers": [443]}}}]
|
||||
assert facts["rules"] == [{"tlsPassthrough": {"hostnames": ["example.invalid", "localhost"], "ports": {"numbers": [443]}}}]
|
||||
|
||||
|
||||
def test_a_refused_egress_policy_fails_the_task_before_resume(fake: Fake, tmp_path: Path, kubectl_ate: Path) -> None:
|
||||
@@ -943,3 +1013,82 @@ def test_ax_task_names_leave_room_for_the_template_suffix() -> None:
|
||||
assert other != long, "no collision between tasks sharing a long prefix"
|
||||
assert reconciler._ax_name("run", "facts", limit=reconciler.TASK_NAME_LIMIT) == "run-facts"
|
||||
assert re.fullmatch(r"[a-z0-9]([a-z0-9-]*[a-z0-9])?", long)
|
||||
|
||||
|
||||
# --- the store is the state of record (plane-on-kubernetes 2.1, design section 2) -----------------------------------
|
||||
|
||||
|
||||
def test_state_is_mirrored_to_the_store_and_seeds_a_rerun_from_an_empty_results_root(
|
||||
fake: Fake, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
assert reconciler.run(write_run(tmp_path, "mir"), ax=str(fake.ax)) == "done"
|
||||
mirrored = json.loads(fake.store.objects["runs/mir/state.json"].decode())
|
||||
assert mirrored == state_of("mir") and mirrored["status"] == "done", "mirrored after the last change"
|
||||
applies = len(fake.events("apply"))
|
||||
monkeypatch.setenv("OUSAST_RESULTS", str(tmp_path / "elsewhere")) # another machine: no local state at all
|
||||
assert reconciler.run(write_run(tmp_path, "mir"), ax=str(fake.ax)) == "done"
|
||||
assert statuses("mir") == {"repo-facts": "done", "verify": "done", "agree": "done"}, "seeded from the store"
|
||||
assert len(fake.events("apply")) == applies, "nothing was submitted again"
|
||||
|
||||
|
||||
def test_a_store_that_cannot_open_stops_the_run_before_ax_and_leaves_no_lock(
|
||||
fake: Fake, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
from openultrasast.plane import memory
|
||||
from openultrasast.plane.memory import MemoryStoreError
|
||||
|
||||
monkeypatch.setattr(reconciler, "open_store", memory.open_store) # the real one, not the fixture's fake
|
||||
monkeypatch.setenv("OUSAST_MEMORY", "s3://nowhere") # the profile's override; no S3_* in the test environment
|
||||
for key in ("S3_ENDPOINT", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"):
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
with pytest.raises(MemoryStoreError, match="S3_ENDPOINT"):
|
||||
reconciler.run(write_run(tmp_path, "nostore"), ax=str(fake.ax))
|
||||
assert fake.events("apply") == [] and not (reconciler.run_dir("nostore") / "lock").exists()
|
||||
|
||||
|
||||
# --- the store path end to end, and the receiver path kept for group 2 (plane-on-kubernetes 2.4) --------------------
|
||||
|
||||
|
||||
def test_the_store_holds_the_tar_the_declared_outputs_and_done_json(fake: Fake, tmp_path: Path) -> None:
|
||||
"""Completion is the tar in the store: downloaded, extracted under the run dir, declared outputs re-put as single
|
||||
objects (the consumer's GET names one artifact), done.json with the summary's status and the tar's size."""
|
||||
assert reconciler.run(write_run(tmp_path, "st"), ax=str(fake.ax)) == "done"
|
||||
keys = sorted(k for k in fake.store.objects if k.startswith("runs/st/"))
|
||||
assert keys == [
|
||||
"runs/st/agree/done.json",
|
||||
"runs/st/agree/output.tar",
|
||||
"runs/st/repo-facts/done.json",
|
||||
"runs/st/repo-facts/facts.json",
|
||||
"runs/st/repo-facts/output.tar",
|
||||
"runs/st/state.json",
|
||||
"runs/st/verify/agreed.json",
|
||||
"runs/st/verify/done.json",
|
||||
"runs/st/verify/output.tar",
|
||||
], "declared outputs only (summary.json is extracted locally, not declared by this Run)"
|
||||
done = json.loads(fake.store.objects["runs/st/repo-facts/done.json"])
|
||||
assert done["status"] == "done" and done["bytes"] == len(fake.store.objects["runs/st/repo-facts/output.tar"]) and done["files"] == 2
|
||||
assert json.loads((reconciler.run_dir("st") / "repo-facts" / "facts.json").read_text()) == {"a.py": {"functions": ["f"]}}
|
||||
assert fake.store.objects["runs/st/repo-facts/facts.json"] == (reconciler.run_dir("st") / "repo-facts" / "facts.json").read_bytes()
|
||||
|
||||
|
||||
def test_a_tar_the_store_path_cannot_accept_fails_the_task(fake: Fake, tmp_path: Path) -> None:
|
||||
script = json.loads(json.dumps(SCRIPT))
|
||||
script["repo-facts"]["files"] = {"../escape.json": "{}", "summary.json": FACTS_SUMMARY}
|
||||
fake.script(script)
|
||||
assert reconciler.run(write_run(tmp_path, "esc"), ax=str(fake.ax)) == "failed"
|
||||
assert "rejected member ../escape.json" in state_of("esc")["tasks"]["repo-facts"]["reason"]
|
||||
assert not (reconciler.run_dir("esc").parent / "escape.json").exists()
|
||||
|
||||
|
||||
def test_receiver_mode_still_runs_through_the_host_receiver(fake: Fake, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""OUSAST_DELIVERY=receiver (group 2 only): the old path, with its http rule and OUSAST_ARTIFACT_URL, until 2.6."""
|
||||
monkeypatch.setenv("OUSAST_DELIVERY", "receiver")
|
||||
assert reconciler.run(write_run(tmp_path, "rcv"), ax=str(fake.ax)) == "done"
|
||||
assert fake.applied("rcv", "verify")["env"]["OUSAST_ARTIFACT_URL"].startswith("http://127.0.0.1:")
|
||||
assert [s["body"].get("delivery") for s in fake.router.starts] == [None, None, None], "no presigned URLs in receiver mode"
|
||||
assert not any(k.startswith("runs/rcv/") and k.endswith("output.tar") for k in fake.store.objects), "nothing PUT to the store"
|
||||
assert "runs/rcv/state.json" in fake.store.objects, "the state is still mirrored"
|
||||
written = json.loads((tmp_path / "policies" / "default_rcv-repo-facts.written.json").read_text())
|
||||
assert written["rules"] == [{"tlsPassthrough": {"hostnames": ["example.invalid"], "ports": {"numbers": [443]}}}], (
|
||||
"an IP receiver: no http rule"
|
||||
)
|
||||
|
||||
@@ -93,17 +93,17 @@ def alive(pid: int) -> bool:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
for key in ("OUSAST_ROUTER_URL", "OUSAST_START_TIMEOUT", "KIND_CLUSTER_NAME", "FAKE_KUBECTL_MODE"):
|
||||
for key in ("OUSAST_ROUTER_URL", "OUSAST_START_TIMEOUT", "FAKE_KUBECTL_MODE"):
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
|
||||
|
||||
def test_port_forward_port_is_parsed_and_the_child_is_terminated_on_exit(tmp_path: Path, fake_router: FakeRouter) -> None:
|
||||
port = int(fake_router.server_address[1])
|
||||
kubectl = write_kubectl(tmp_path, port)
|
||||
with open_router(kubectl=str(kubectl)) as router:
|
||||
with open_router("kind-test", kubectl=str(kubectl)) as router: # the profile's context, as the reconciler passes it
|
||||
assert router.url == f"http://127.0.0.1:{port}"
|
||||
seen = json.loads((tmp_path / "kubectl.json").read_text())
|
||||
assert seen["argv"] == ["--context", "kind-ousast", "-n", "ate-system", "port-forward", "svc/atenet-router", ":80"]
|
||||
assert seen["argv"] == ["--context", "kind-test", "-n", "ate-system", "port-forward", "svc/atenet-router", ":80"]
|
||||
assert alive(seen["pid"])
|
||||
router.start_task("default", "run-t", "run", {})
|
||||
deadline = time.monotonic() + 5
|
||||
@@ -123,15 +123,23 @@ def test_a_failing_port_forward_raises_with_kubectls_output(tmp_path: Path, monk
|
||||
|
||||
def test_router_url_override_starts_no_port_forward(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, fake_router: FakeRouter) -> None:
|
||||
monkeypatch.setenv("OUSAST_ROUTER_URL", fake_router.url)
|
||||
with open_router(kubectl=str(tmp_path / "no-such-kubectl")) as router:
|
||||
with open_router("kind-test", kubectl=str(tmp_path / "no-such-kubectl")) as router:
|
||||
assert router.url == fake_router.url
|
||||
assert not (tmp_path / "kubectl.json").exists()
|
||||
monkeypatch.delenv("OUSAST_ROUTER_URL")
|
||||
with open_router("kind-test", url=fake_router.url, kubectl=str(tmp_path / "no-such-kubectl")) as router:
|
||||
assert router.url == fake_router.url, "the profile's router_url is the same override"
|
||||
|
||||
|
||||
def test_open_router_refuses_to_build_a_context(tmp_path: Path) -> None:
|
||||
with pytest.raises(StartError, match="kube context"), open_router("", kubectl=str(tmp_path / "no-such-kubectl")):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fake_router", [[(503, "no actor yet"), (502, "bad gateway"), (202, "started")]], indirect=True)
|
||||
def test_start_retries_503_and_502_then_succeeds(monkeypatch: pytest.MonkeyPatch, fake_router: FakeRouter) -> None:
|
||||
monkeypatch.setenv("OUSAST_ROUTER_URL", fake_router.url)
|
||||
with open_router() as router:
|
||||
with open_router("kind-test") as router:
|
||||
router.start_task("default", "chain-verify", "chain", {"DEEPSEEK_API_KEY": SECRET})
|
||||
assert len(fake_router.requests) == 3
|
||||
path, headers, body = fake_router.requests[-1]
|
||||
@@ -142,7 +150,7 @@ def test_start_retries_503_and_502_then_succeeds(monkeypatch: pytest.MonkeyPatch
|
||||
@pytest.mark.parametrize("fake_router", [[(409, f"this actor runs task 'x' (echo {SECRET})")]], indirect=True)
|
||||
def test_a_409_is_raised_at_once_with_the_response_text_redacted(monkeypatch: pytest.MonkeyPatch, fake_router: FakeRouter) -> None:
|
||||
monkeypatch.setenv("OUSAST_ROUTER_URL", fake_router.url)
|
||||
with open_router() as router, pytest.raises(StartError, match="HTTP 409.*runs task 'x'") as caught:
|
||||
with open_router("kind-test") as router, pytest.raises(StartError, match="HTTP 409.*runs task 'x'") as caught:
|
||||
router.start_task("default", "t", "r", {"DEEPSEEK_API_KEY": SECRET})
|
||||
assert len(fake_router.requests) == 1 and SECRET not in str(caught.value)
|
||||
assert router.starter("default", "t", "r", {})() is not None, "the starter returns the refusal instead of raising"
|
||||
@@ -153,7 +161,7 @@ def test_a_restart_after_re_resume_accepts_a_running_command(monkeypatch: pytest
|
||||
"""First start: 'already started' is a refusal. After a re-resume the actor may not have rebooted; then the
|
||||
runner's 'already started on this boot' means the command still runs, not a failure."""
|
||||
monkeypatch.setenv("OUSAST_ROUTER_URL", fake_router.url)
|
||||
with open_router() as router:
|
||||
with open_router("kind-test") as router:
|
||||
start = router.starter("default", "t", "r", {})
|
||||
assert start() is not None, "a first start that finds the command running is a refusal"
|
||||
assert start() is None, "a re-sent start after a re-resume is not"
|
||||
@@ -165,7 +173,7 @@ def test_a_restart_after_re_resume_accepts_a_running_command(monkeypatch: pytest
|
||||
def test_start_gives_up_after_the_start_timeout(monkeypatch: pytest.MonkeyPatch, fake_router: FakeRouter) -> None:
|
||||
monkeypatch.setenv("OUSAST_ROUTER_URL", fake_router.url)
|
||||
monkeypatch.setenv("OUSAST_START_TIMEOUT", "1")
|
||||
with open_router() as router, pytest.raises(StartError, match="OUSAST_START_TIMEOUT.*workspaces not materialised"):
|
||||
with open_router("kind-test") as router, pytest.raises(StartError, match="OUSAST_START_TIMEOUT.*workspaces not materialised"):
|
||||
router.start_task("default", "t", "r", {})
|
||||
assert len(fake_router.requests) >= 2
|
||||
|
||||
@@ -173,7 +181,7 @@ def test_start_gives_up_after_the_start_timeout(monkeypatch: pytest.MonkeyPatch,
|
||||
def test_connection_errors_are_retried_until_the_timeout(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("OUSAST_ROUTER_URL", "http://127.0.0.1:9")
|
||||
monkeypatch.setenv("OUSAST_START_TIMEOUT", "1")
|
||||
with open_router() as router, pytest.raises(StartError, match="within 1s"):
|
||||
with open_router("kind-test") as router, pytest.raises(StartError, match="within 1s"):
|
||||
router.start_task("default", "t", "r", {})
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from plane_fake_store import HttpObjectStore
|
||||
|
||||
from openultrasast.plane import egress, runner
|
||||
from openultrasast.plane.manifests import GitSource
|
||||
@@ -36,6 +37,7 @@ from openultrasast.plane.runner import (
|
||||
clone_destination,
|
||||
deliver,
|
||||
find_cached_checkout,
|
||||
parse_delivery,
|
||||
start_health_server,
|
||||
)
|
||||
|
||||
@@ -850,3 +852,121 @@ def test_fetch_dials_the_override_and_sends_the_url_host(tmp_path: Path) -> None
|
||||
server.server_close()
|
||||
assert size == 2 and (tmp_path / "x" / "a.json").read_bytes() == b"ok"
|
||||
assert seen[0]["path"] == "/inputs/p/a.json" and seen[0]["Host"] == "receiver.example" and seen[0]["X-Ousast-Run"] == "r"
|
||||
|
||||
|
||||
# --- store delivery through the start body (plane-on-kubernetes 2.2, design section 3) ---------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def object_store() -> Iterator[HttpObjectStore]:
|
||||
fake = HttpObjectStore()
|
||||
try:
|
||||
yield fake
|
||||
finally:
|
||||
fake.close()
|
||||
|
||||
|
||||
def test_the_start_body_may_carry_delivery_targets_and_refuses_unsafe_urls() -> None:
|
||||
"""``delivery {put, inputs}``: https to a hostname, never an address or http; the answer echoes no URL."""
|
||||
gate = StartGate()
|
||||
gate.arm("r", "t")
|
||||
bad = [
|
||||
({"put": "http://store.example/b/k?sig=1"}, "delivery.put must be an https URL"),
|
||||
({"put": "https://10.0.0.7/b/k"}, "delivery.put must name a host, not an address"),
|
||||
({"put": "https://user:[email protected]/k"}, "delivery.put must be an https URL"),
|
||||
({"inputs": {"FACTS": "ftp://store.example/k"}}, "delivery.inputs.FACTS must be an https URL"),
|
||||
({"inputs": {"lower": "https://store.example/k"}}, "delivery.inputs must map"),
|
||||
({"elsewhere": "x"}, "delivery must be an object"),
|
||||
("https://store.example/k", "delivery must be an object"),
|
||||
]
|
||||
for delivery, message in bad:
|
||||
status, text = gate.offer(json.dumps({"run": "r", "task": "t", "delivery": delivery}).encode())
|
||||
assert status == 400 and message in text, (delivery, status, text)
|
||||
assert "store.example" not in text and "10.0.0.7" not in text, "the answer never echoes a URL"
|
||||
body = {
|
||||
"run": "r",
|
||||
"task": "t",
|
||||
"delivery": {
|
||||
"put": "https://store.example/b/t/output.tar?X-Amz-Signature=s",
|
||||
"inputs": {"FACTS": "https://store.example/b/p/facts.json?X-Amz-Signature=s"},
|
||||
},
|
||||
}
|
||||
assert gate.offer(json.dumps(body).encode()) == (202, "started")
|
||||
start = gate.wait(threading.Event())
|
||||
assert start is not None and start.delivery is not None
|
||||
assert start.delivery.put == body["delivery"]["put"] and start.delivery.inputs == body["delivery"]["inputs"]
|
||||
assert "X-Amz" not in repr(start) and "store.example" not in repr(start), "a Start never prints a URL"
|
||||
assert parse_delivery(None) is None and parse_delivery({"put": "http://localhost:1/k"}, insecure=True) is not None
|
||||
with pytest.raises(ValueError, match="must be an https URL"):
|
||||
parse_delivery({"put": "http://localhost:1/k"})
|
||||
|
||||
|
||||
def test_output_is_put_to_the_presigned_url_and_inputs_are_fetched_from_theirs(
|
||||
env: dict[str, str], object_store: HttpObjectStore, tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""End to end against the S3-shaped fake: the start carries the URLs, the runner PUTs the tar and GETs the input;
|
||||
no receiver, no OUSAST_ARTIFACT_URL, no URL in any log line."""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
object_store._put("runs/run-1/repo-facts/facts.json", b'{"a.py": ["f"]}')
|
||||
put_url = object_store.presign_put("runs/run-1/stub/output.tar")
|
||||
facts_url = object_store.presign_get("runs/run-1/repo-facts/facts.json")
|
||||
thread, environ, codes = main_in_thread(
|
||||
tmp_path,
|
||||
AX_RUNNER_INSECURE_DELIVERY="1",
|
||||
OUSAST_TASK="stub",
|
||||
OUSAST_INPUTS=json.dumps({"FACTS": "repo-facts/facts.json"}),
|
||||
OUSAST_INPUT_FACTS=str(tmp_path / "ws" / "in" / "repo-facts" / "facts.json"),
|
||||
)
|
||||
body = {"run": "run-1", "task": "stub", "delivery": {"put": put_url, "inputs": {"FACTS": facts_url}}}
|
||||
assert post_start(environ["AX_RUNNER_BOUND_PORT"], body) == (202, "started")
|
||||
thread.join(timeout=30)
|
||||
assert codes == [0]
|
||||
assert (tmp_path / "ws" / "in" / "repo-facts" / "facts.json").read_bytes() == b'{"a.py": ["f"]}', "the input came by its URL"
|
||||
assert ("GET", "runs/run-1/repo-facts/facts.json") in object_store.requests and (
|
||||
"PUT",
|
||||
"runs/run-1/stub/output.tar",
|
||||
) in object_store.requests
|
||||
with tarfile.open(fileobj=io.BytesIO(object_store.objects["runs/run-1/stub/output.tar"]), mode="r:") as tar:
|
||||
names = sorted(tar.getnames())
|
||||
assert "facts.json" in names and "summary.json" in names, "the whole output directory, as a tar, in the store"
|
||||
marker = json.loads((tmp_path / "state" / "stub.done.json").read_text())
|
||||
assert marker["delivered"] is True and marker["status"] == "done"
|
||||
assert "X-Amz" not in caplog.text and "fake-bucket" not in caplog.text, "no presigned URL in any log line"
|
||||
assert files_containing(tmp_path, "X-Amz-Signature") == [], "nor in any file"
|
||||
|
||||
|
||||
def test_a_failed_put_is_retried_alone_on_the_next_start_with_a_fresh_url(
|
||||
env: dict[str, str], object_store: HttpObjectStore, tmp_path: Path
|
||||
) -> None:
|
||||
"""The marker and retry-only boot are unchanged: a PUT that fails (wrong bucket: 404) ends the task failed with
|
||||
the URL's host and path only; the next boot's start brings a working URL and only the delivery runs again."""
|
||||
counter = tmp_path / "runs.txt"
|
||||
wrong = object_store.presign_put("runs/run-1/stub/output.tar").replace("/fake-bucket/", "/no-such-bucket/")
|
||||
thread, environ, codes = main_in_thread(
|
||||
tmp_path, AX_RUNNER_INSECURE_DELIVERY="1", STUB_COUNTER=str(counter), OUSAST_DELIVERY_BACKOFF="0"
|
||||
)
|
||||
assert post_start(environ["AX_RUNNER_BOUND_PORT"], {"run": "run-1", "task": "stub", "delivery": {"put": wrong}})[0] == 202
|
||||
thread.join(timeout=30)
|
||||
assert codes == [EXIT_BY_STATUS["failed"]]
|
||||
marker = json.loads((tmp_path / "state" / "stub.done.json").read_text())
|
||||
assert marker["delivered"] is False and marker["status"] == "failed" and marker["exit"] == 0
|
||||
assert "HTTP 404" in marker["reason"] and "X-Amz" not in marker["reason"], "the reason names the status, not the signature"
|
||||
thread, environ, codes = main_in_thread(
|
||||
tmp_path, AX_RUNNER_INSECURE_DELIVERY="1", STUB_COUNTER=str(counter), OUSAST_DELIVERY_BACKOFF="0"
|
||||
)
|
||||
good = object_store.presign_put("runs/run-1/stub/output.tar")
|
||||
assert post_start(environ["AX_RUNNER_BOUND_PORT"], {"run": "run-1", "task": "stub", "delivery": {"put": good}})[0] == 202
|
||||
thread.join(timeout=30)
|
||||
assert codes == [0] and len(counter.read_text().splitlines()) == 1, "only the delivery is retried"
|
||||
assert "runs/run-1/stub/output.tar" in object_store.objects
|
||||
assert json.loads((tmp_path / "state" / "stub.done.json").read_text())["delivered"] is True
|
||||
|
||||
|
||||
def test_deliver_puts_and_names_only_the_safe_url_on_failure(object_store: HttpObjectStore) -> None:
|
||||
url = object_store.presign_put("k/output.tar")
|
||||
assert runner.deliver(url, b"tar", {"Content-Type": "application/x-tar"}, backoff=0, method="PUT") == 200
|
||||
assert object_store.objects["k/output.tar"] == b"tar"
|
||||
with pytest.raises(runner.DeliveryError) as caught:
|
||||
runner.deliver(url.replace("/fake-bucket/", "/other/"), b"x", {}, attempts=2, backoff=0, method="PUT")
|
||||
assert "HTTP 404" in str(caught.value) and "X-Amz" not in str(caught.value) and "/other/k/output.tar" in str(caught.value)
|
||||
assert runner.safe_url("https://h:1/p/q?X-Amz-Signature=s") == "https://h:1/p/q"
|
||||
|
||||
Reference in New Issue
Block a user