mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
refactor(memory): S3-generic store on boto3; RustFS is the tested server (no MinIO naming)
The plane's memory store no longer names one vendor. `MinioStore`/`SdkClient`/ `minio_settings`/`MINIO_KEYS` become `S3Store`/`S3Client`/`s3_settings`/`S3_KEYS`; the URL scheme is `s3://<bucket>[/<prefix>]` (`s3://` alone uses `S3_BUCKET`); the settings are `S3_ENDPOINT` (a full URL), `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, optional `AWS_SESSION_TOKEN` and `S3_REGION`. The client is boto3 (extra `s3`, imported lazily): SigV4, path-style addressing, SDK checksum trailers off, paginated listing, versions and delete markers listed and deleted by version (`purge`, used by the contract tests' teardown so the bucket keeps nothing of a run). Behaviour is unchanged: the store never configures its bucket and verifies versioning, the `runs/` expiry rule, S3 Select and tag reads at startup; no local fallback; every row carries every queryable field with "" placeholders. The admin hints read `--endpoint-url "$S3_ENDPOINT"`. The fake client still reproduces RustFS's two Select failures (unbound leading-row field, null-typed column). Docs: rustfs.md stays the tested server's setup page (policy JSON unchanged, env names updated, any S3-compatible store with versioning, lifecycle, tags and Select works); memory.md, ops/ax/README.md, README.md, steering and the two current spec designs use the S3 naming. Retired specs and committed measurements are untouched. Real server (RustFS, OUSAST_MEMORY_TEST_S3=1): test_plane_memory + test_learn_compile 75 passed, 1 skipped. Store copy of the live local store to a temporary prefix: every count matched (776 row objects, 2982 rows, 22 facts, 788 excerpts, 2601 responses, 2 programs, 1793 index entries), 4190 versions purged afterwards. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
285b6a367f
commit
51d064e381
@@ -119,8 +119,8 @@ def test_rustfs_page_lists_the_complete_policy_and_no_secret() -> None:
|
||||
for needle in (
|
||||
'"Sid": "BucketSettingsReadOnly"', '"Sid": "ObjectsReadWrite"', '"arn:aws:s3:::sast-memory"', '"arn:aws:s3:::sast-memory/*"',
|
||||
"put-bucket-versioning", "put-bucket-lifecycle-configuration", "--endpoint-url https://files.because-security.com",
|
||||
"--bucket sast-memory", '"Prefix":"runs/"', '"Days":30', "OUSAST_MEMORY", "minio://sast-memory", "MINIO_ENDPOINT",
|
||||
"MINIO_ACCESS_KEY", "MINIO_SECRET_KEY", "MINIO_SECURE", "MINIO_REGION", "EvaluatorBindingDoesNotExist",
|
||||
"--bucket sast-memory", '"Prefix":"runs/"', '"Days":30', "OUSAST_MEMORY", "s3://sast-memory", "S3_ENDPOINT",
|
||||
"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "S3_REGION", "S3_BUCKET", "EvaluatorBindingDoesNotExist",
|
||||
): # fmt: skip
|
||||
assert needle in text, f"rustfs.md lacks {needle!r}"
|
||||
for shape in SECRET_SHAPES:
|
||||
|
||||
@@ -11,7 +11,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from learn_fixtures import ScriptedChat, by_id, corpus, default_answer, example
|
||||
from test_plane_memory import FakeObjects, _real_minio
|
||||
from test_plane_memory import FakeObjects, _real_s3
|
||||
|
||||
from openultrasast.learn import compile as compile_module
|
||||
from openultrasast.learn.compile import (
|
||||
@@ -34,7 +34,7 @@ from openultrasast.learn.examples import Example
|
||||
from openultrasast.learn.folds import Fold, FoldError
|
||||
from openultrasast.learn.program import Caller, Demo
|
||||
from openultrasast.plane.budget import MeteredClient
|
||||
from openultrasast.plane.memory import FileStore, MemoryStore, MinioStore
|
||||
from openultrasast.plane.memory import FileStore, MemoryStore, S3Store
|
||||
|
||||
PRICES = {"cache_hit_per_m": 0.014, "input_per_m": 0.44, "output_per_m": 1.32}
|
||||
MEMORY = corpus(groups=24, per_group=4)
|
||||
@@ -58,14 +58,14 @@ def answer(messages: Sequence[Mapping[str, object]], temperature: float | None)
|
||||
return json.dumps(data)
|
||||
|
||||
|
||||
@pytest.fixture(params=["file", "fake-minio", "minio"])
|
||||
@pytest.fixture(params=["file", "fake-s3-select", "s3"])
|
||||
def store(request: pytest.FixtureRequest, tmp_path: Path) -> Iterator[MemoryStore]:
|
||||
if request.param == "file":
|
||||
yield FileStore(tmp_path / "memory")
|
||||
elif request.param == "minio":
|
||||
yield from _real_minio()
|
||||
elif request.param == "s3":
|
||||
yield from _real_s3()
|
||||
else:
|
||||
yield MinioStore(FakeObjects("works"), "bucket", "p")
|
||||
yield S3Store(FakeObjects("works"), "bucket", "p")
|
||||
|
||||
|
||||
def _caller(chat: ScriptedChat, store: MemoryStore | None = None, **budget: object) -> Caller:
|
||||
|
||||
@@ -23,7 +23,7 @@ from openultrasast.learn.embeddings import (
|
||||
from openultrasast.learn.excerpt import excerpt_sha
|
||||
from openultrasast.plane.budget import BudgetExhausted, MeteredEmbeddingClient, UnmeteredUsage
|
||||
from openultrasast.plane.manifests import load_manifests
|
||||
from openultrasast.plane.memory import FileStore, MemoryStore, MinioStore
|
||||
from openultrasast.plane.memory import FileStore, MemoryStore, S3Store
|
||||
from openultrasast.provider.openrouter import parse_embedding_payload
|
||||
|
||||
PRICES = {"cache_hit_per_m": 0, "input_per_m": 0.02, "output_per_m": 0}
|
||||
@@ -43,9 +43,9 @@ class ScriptedEmbedder:
|
||||
return [[float(len(text) % 7 + i) for i in range(self.dims)] for text in inputs]
|
||||
|
||||
|
||||
@pytest.fixture(params=["file", "fake-minio"])
|
||||
@pytest.fixture(params=["file", "fake-s3-select"])
|
||||
def store(request: pytest.FixtureRequest, tmp_path: Path) -> Iterator[MemoryStore]:
|
||||
yield FileStore(tmp_path / "memory") if request.param == "file" else MinioStore(FakeObjects("works"), "bucket", "p")
|
||||
yield FileStore(tmp_path / "memory") if request.param == "file" else S3Store(FakeObjects("works"), "bucket", "p")
|
||||
|
||||
|
||||
def _excerpts(store: MemoryStore, n: int) -> list[str]:
|
||||
|
||||
@@ -24,7 +24,7 @@ from openultrasast.learn.examples import (
|
||||
)
|
||||
from openultrasast.learn.features import Part, quick_part, record
|
||||
from openultrasast.learn.labels import Source, Sources
|
||||
from openultrasast.plane.memory import FileStore, MemoryStore, MemoryStoreError, MinioStore
|
||||
from openultrasast.plane.memory import FileStore, MemoryStore, MemoryStoreError, S3Store
|
||||
|
||||
PIN = "0123456789abcdef0123456789abcdef01234567"
|
||||
SOURCES = Sources(
|
||||
@@ -39,9 +39,9 @@ CODE = {
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(params=["file", "fake-minio"])
|
||||
@pytest.fixture(params=["file", "fake-s3-select"])
|
||||
def store(request: pytest.FixtureRequest, tmp_path: Path) -> Iterator[MemoryStore]:
|
||||
yield FileStore(tmp_path / "memory") if request.param == "file" else MinioStore(FakeObjects("works"), "bucket", "p")
|
||||
yield FileStore(tmp_path / "memory") if request.param == "file" else S3Store(FakeObjects("works"), "bucket", "p")
|
||||
|
||||
|
||||
def label(**kw: Any) -> dict[str, Any]:
|
||||
|
||||
+76
-58
@@ -20,17 +20,17 @@ import pytest
|
||||
|
||||
from openultrasast.plane import memory, reconciler
|
||||
from openultrasast.plane.memory import (
|
||||
MINIO_KEYS,
|
||||
S3_KEYS,
|
||||
ExpiryRule,
|
||||
FileStore,
|
||||
MemoryStore,
|
||||
MemoryStoreError,
|
||||
MinioStore,
|
||||
SdkClient,
|
||||
S3Client,
|
||||
S3Store,
|
||||
memory_key,
|
||||
minio_settings,
|
||||
open_store,
|
||||
row_id,
|
||||
s3_settings,
|
||||
seed,
|
||||
where_sql,
|
||||
)
|
||||
@@ -40,10 +40,10 @@ PIN = "0123456789abcdef0123456789abcdef01234567"
|
||||
OTHER_PIN = "fedcba9876543210fedcba9876543210fedcba98"
|
||||
IMAGE = "sha256:" + "a" * 64
|
||||
FACTS = json.dumps({"callers": {"a.py::run": []}, "counts": {"files": 1}, "files": {"a.py": {"functions": ["run"]}}}).encode()
|
||||
MINIO_SKIP = (
|
||||
"MinIO/RustFS contract tests need OUSAST_MEMORY_TEST_MINIO=1 plus MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY "
|
||||
"and a bucket (OUSAST_MEMORY_TEST_BUCKET, else MINIO_BUCKET) exported (optional: MINIO_SECURE, MINIO_REGION) and the "
|
||||
"minio extra installed; the admin configures that bucket, the tests never do"
|
||||
S3_SKIP = (
|
||||
"real-server contract tests need OUSAST_MEMORY_TEST_S3=1 plus S3_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY "
|
||||
"and a bucket (OUSAST_MEMORY_TEST_BUCKET, else S3_BUCKET) exported (optional: S3_REGION, AWS_SESSION_TOKEN) and the "
|
||||
"s3 extra (boto3) installed; the admin configures that bucket, the tests never do"
|
||||
)
|
||||
|
||||
|
||||
@@ -112,13 +112,13 @@ class FakeObjects:
|
||||
leading = rows[: self.schema_rows]
|
||||
typeless = {k for r in leading for k, v in r.items() if v is None and all(x.get(k) is None for x in leading)}
|
||||
if any(r.get(k) is not None for r in rows[self.schema_rows :] for k in typeless): # null-typed, set later
|
||||
raise type("MinioException", (Exception,), {})("JSONParsingError: An error occurred while parsing the JSON file")
|
||||
raise type("SelectFailure", (Exception,), {})("JSONParsingError: An error occurred while parsing the JSON file")
|
||||
clauses = re.findall(r"s\.\"(\w+)\" = ('(?:[^']|'')*'|true|false|-?[\d.]+)", expression)
|
||||
want = {n: v[1:-1].replace("''", "'") if v.startswith("'") else json.loads(v) for n, v in clauses}
|
||||
return b"".join(json.dumps(r).encode() + b"\n" for r in rows if all(r.get(k) == v for k, v in want.items()))
|
||||
|
||||
def presign(self, method: str, key: str, expires: timedelta) -> str:
|
||||
return f"https://minio.test/{key}?method={method}&X-Amz-Expires={int(expires.total_seconds())}"
|
||||
return f"https://s3.test/{key}?method={method}&X-Amz-Expires={int(expires.total_seconds())}"
|
||||
|
||||
def versioning(self) -> str:
|
||||
if self.status == "denied":
|
||||
@@ -136,33 +136,32 @@ def fresh_verification(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(memory, "_VERIFIED", set())
|
||||
|
||||
|
||||
def _real_minio() -> Iterator[MemoryStore]:
|
||||
if os.environ.get("OUSAST_MEMORY_TEST_MINIO") != "1" or any(not os.environ.get(k) for k in MINIO_KEYS):
|
||||
pytest.skip(MINIO_SKIP)
|
||||
pytest.importorskip("minio", reason=MINIO_SKIP)
|
||||
bucket = os.environ.get("OUSAST_MEMORY_TEST_BUCKET") or os.environ.get("MINIO_BUCKET")
|
||||
def _real_s3() -> Iterator[MemoryStore]:
|
||||
if os.environ.get("OUSAST_MEMORY_TEST_S3") != "1" or any(not os.environ.get(k) for k in S3_KEYS):
|
||||
pytest.skip(S3_SKIP)
|
||||
pytest.importorskip("boto3", reason=S3_SKIP)
|
||||
bucket = os.environ.get("OUSAST_MEMORY_TEST_BUCKET") or os.environ.get("S3_BUCKET")
|
||||
if not bucket:
|
||||
pytest.skip(MINIO_SKIP)
|
||||
client = SdkClient(bucket=bucket, **minio_settings(os.environ))
|
||||
MinioStore(client, bucket) # verifies the admin's setup at the bucket root; the tests never configure the bucket
|
||||
pytest.skip(S3_SKIP)
|
||||
client = S3Client(bucket=bucket, **s3_settings(os.environ))
|
||||
S3Store(client, bucket) # verifies the admin's setup at the bucket root; the tests never configure the bucket
|
||||
# The contract rows go under a fresh prefix so every test starts empty; the admin's expiry rule covers the root's
|
||||
# runs/, not this prefix's, so the prefixed store counts as verified by the root's check above.
|
||||
prefix = f"contract-{uuid.uuid4().hex[:12]}"
|
||||
memory._VERIFIED.add(f"minio://{bucket}/{prefix}")
|
||||
store = MinioStore(client, bucket, prefix)
|
||||
memory._VERIFIED.add(f"s3://{bucket}/{prefix}")
|
||||
store = S3Store(client, bucket, prefix)
|
||||
yield store
|
||||
for key in client.keys(store.prefix + "/"):
|
||||
client.delete(key)
|
||||
client.purge(store.prefix + "/") # every version and delete marker: the bucket keeps nothing of the test
|
||||
|
||||
|
||||
@pytest.fixture(params=["file", "fake-minio", "minio"])
|
||||
@pytest.fixture(params=["file", "fake-s3-select", "s3"])
|
||||
def store(request: pytest.FixtureRequest, tmp_path: Path) -> Iterator[MemoryStore]:
|
||||
if request.param == "file":
|
||||
yield FileStore(tmp_path / "memory")
|
||||
elif request.param == "minio":
|
||||
yield from _real_minio()
|
||||
elif request.param == "s3":
|
||||
yield from _real_s3()
|
||||
else:
|
||||
yield MinioStore(FakeObjects(), "bucket", "p")
|
||||
yield S3Store(FakeObjects(), "bucket", "p")
|
||||
|
||||
|
||||
# --- the contract --------------------------------------------------------------------------------------------------
|
||||
@@ -269,7 +268,7 @@ def test_a_declared_field_absent_from_the_leading_rows_is_queryable(store: Memor
|
||||
|
||||
def test_the_fake_client_mimics_rustfs_and_the_normaliser_repairs_a_pre_rule_object() -> None:
|
||||
client = FakeObjects("works")
|
||||
store = MinioStore(client, "bucket")
|
||||
store = S3Store(client, "bucket")
|
||||
alerts, late = _late()
|
||||
key = f"repos/github.com__o__r/{PIN}.jsonl"
|
||||
legacy = sorted([*alerts, late], key=lambda r: str(r["id"])) # written before the rule: no queryable fields
|
||||
@@ -302,9 +301,9 @@ def test_a_where_on_an_undeclared_field_an_unknown_kind_or_null_is_refused_befor
|
||||
assert memory.QUERYABLE == ("candidate", "candidates_digest", "final", "profile")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", ["file", "fake-minio"])
|
||||
@pytest.mark.parametrize("backend", ["file", "fake-s3-select"])
|
||||
def test_the_normaliser_is_idempotent_and_keeps_every_value(backend: str, tmp_path: Path) -> None:
|
||||
store: MemoryStore = FileStore(tmp_path / "memory") if backend == "file" else MinioStore(FakeObjects(), "bucket", "p")
|
||||
store: MemoryStore = FileStore(tmp_path / "memory") if backend == "file" else S3Store(FakeObjects(), "bucket", "p")
|
||||
legacy = [verdict("a.py::run", "agreed"), row("unit_cost", "a:a.py", **{"pass": "a", "usd": 0.5, "final": None})]
|
||||
store._put(f"repos/github.com__o__r/{PIN}.jsonl", "".join(json.dumps(r) + "\n" for r in legacy).encode())
|
||||
current = memory.normalise_row(row("alert", "x", pin=OTHER_PIN))
|
||||
@@ -336,11 +335,11 @@ def test_the_memory_normalise_command_prints_its_counts(tmp_path: Path, capsys:
|
||||
assert "rewrote 0/1 objects; 0/1 rows filled 0 absent or null fields" in capsys.readouterr().out.splitlines()[-1]
|
||||
|
||||
|
||||
# --- MinIO specifics on the fake client ----------------------------------------------------------------------------
|
||||
# --- S3 specifics on the fake client ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _filled(client: FakeObjects) -> MinioStore:
|
||||
store = MinioStore(client, "bucket")
|
||||
def _filled(client: FakeObjects) -> S3Store:
|
||||
store = S3Store(client, "bucket")
|
||||
store.put_rows([verdict("a.py::run", "agreed"), verdict("a.py::x", "disputed"), row("unit_cost", "a:a.py", **{"pass": "a"})])
|
||||
return store
|
||||
|
||||
@@ -358,7 +357,7 @@ def test_select_is_pushed_down_when_the_probe_answers(caplog: pytest.LogCaptureF
|
||||
@pytest.mark.parametrize("mode", ["raises", "wrong"])
|
||||
def test_a_server_without_select_is_refused_at_open(mode: str) -> None:
|
||||
with pytest.raises(MemoryStoreError, match=r"S3 Select .* does not answer on _probe/select.jsonl .*no local fallback") as caught:
|
||||
MinioStore(FakeObjects(mode), "bucket")
|
||||
S3Store(FakeObjects(mode), "bucket")
|
||||
assert "versioning" not in str(caught.value) and "lifecycle" not in str(caught.value)
|
||||
|
||||
|
||||
@@ -383,20 +382,20 @@ def test_an_unbound_field_on_rustfs_raises_naming_the_schema_inference_never_ret
|
||||
|
||||
def test_verify_bucket_passes_on_the_admin_setup_and_writes_only_its_probe() -> None:
|
||||
client = FakeObjects()
|
||||
store = MinioStore(client, "bucket", "p")
|
||||
store = S3Store(client, "bucket", "p")
|
||||
assert sorted(client.objects) == ["p/_probe/select.jsonl"] and not hasattr(client, "enable_versioning")
|
||||
store.verify_bucket()
|
||||
MinioStore(client, "bucket", "p")
|
||||
S3Store(client, "bucket", "p")
|
||||
assert len(client.selects) == 2, "opening a verified store again does not re-verify in the same process"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", ["Suspended", "Off"])
|
||||
def test_verify_bucket_refuses_versioning_off_naming_the_admin_command(status: str) -> None:
|
||||
with pytest.raises(MemoryStoreError, match=rf"versioning is {status}, it must be Enabled") as caught:
|
||||
MinioStore(FakeObjects(versioning=status), "sast-memory")
|
||||
S3Store(FakeObjects(versioning=status), "sast-memory")
|
||||
message = str(caught.value)
|
||||
assert "never configures its bucket" in message and "lifecycle" not in message and "Select" not in message
|
||||
assert 'aws s3api put-bucket-versioning --endpoint-url "$MINIO_ENDPOINT" --bucket sast-memory' in message
|
||||
assert 'aws s3api put-bucket-versioning --endpoint-url "$S3_ENDPOINT" --bucket sast-memory' in message
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -412,25 +411,25 @@ def test_verify_bucket_refuses_versioning_off_naming_the_admin_command(status: s
|
||||
)
|
||||
def test_verify_bucket_refuses_a_missing_runs_expiry_rule(rules: list[ExpiryRule]) -> None:
|
||||
with pytest.raises(MemoryStoreError, match=r"no enabled lifecycle rule expires runs/") as caught:
|
||||
MinioStore(FakeObjects(rules=rules), "sast-memory")
|
||||
S3Store(FakeObjects(rules=rules), "sast-memory")
|
||||
assert "put-bucket-lifecycle-configuration" in str(caught.value)
|
||||
assert '"Filter":{"Prefix":"runs/"},"Expiration":{"Days":30}' in str(caught.value)
|
||||
|
||||
|
||||
def test_verify_bucket_names_the_prefixed_runs_and_refuses_a_rule_expiring_the_whole_store() -> None:
|
||||
with pytest.raises(MemoryStoreError, match=r"no enabled lifecycle rule expires mem/runs/"):
|
||||
MinioStore(FakeObjects(rules=[ExpiryRule("r", "runs/", 30, True)]), "bucket", "mem")
|
||||
S3Store(FakeObjects(rules=[ExpiryRule("r", "runs/", 30, True)]), "bucket", "mem")
|
||||
wide = [ExpiryRule("everything", "", 30, True), ExpiryRule("ousast-runs-expiry", "runs/", 30, True)]
|
||||
with pytest.raises(MemoryStoreError, match=r"lifecycle rule\(s\) everything expire the whole store"):
|
||||
MinioStore(FakeObjects(rules=wide), "bucket")
|
||||
MinioStore(FakeObjects(rules=[ExpiryRule("r", "mem/runs/", 14, True)]), "bucket", "mem")
|
||||
S3Store(FakeObjects(rules=wide), "bucket")
|
||||
S3Store(FakeObjects(rules=[ExpiryRule("r", "mem/runs/", 14, True)]), "bucket", "mem")
|
||||
|
||||
|
||||
def test_verify_bucket_names_every_missing_piece_and_the_permissions_to_read_them() -> None:
|
||||
client = FakeObjects("raises", versioning="denied", rules="denied")
|
||||
client.tags_denied = True
|
||||
with pytest.raises(MemoryStoreError) as caught:
|
||||
MinioStore(client, "sast-memory")
|
||||
S3Store(client, "sast-memory")
|
||||
message = str(caught.value)
|
||||
assert "versioning cannot be read (PermissionError: Access Denied)" in message and "s3:GetBucketVersioning" in message
|
||||
assert "lifecycle configuration cannot be read" in message and "s3:GetLifecycleConfiguration" in message
|
||||
@@ -440,7 +439,7 @@ def test_verify_bucket_names_every_missing_piece_and_the_permissions_to_read_the
|
||||
|
||||
def test_row_objects_carry_tags_and_a_kind_tag_skips_objects_without_that_kind() -> None:
|
||||
client = FakeObjects("works")
|
||||
store = MinioStore(client, "bucket")
|
||||
store = S3Store(client, "bucket")
|
||||
store.put_rows([verdict("a.py::run", "agreed"), row("unit_cost", "a:b.py", pin=OTHER_PIN, **{"pass": "a"})])
|
||||
tags = client.tags(f"repos/github.com__o__r/{PIN}.jsonl")
|
||||
assert tags == {"repo": "github.com/o/r", "pin": PIN, "kind": "verdict", "family": "injection", "run": "r1",
|
||||
@@ -469,32 +468,51 @@ def test_open_store_selects_the_backend(tmp_path: Path, monkeypatch: pytest.Monk
|
||||
assert isinstance(default, FileStore) and default.root == tmp_path / "plane" / "memory"
|
||||
chosen = open_store(environ={"OUSAST_MEMORY": f"file://{tmp_path}/m"})
|
||||
assert isinstance(chosen, FileStore) and chosen.root == tmp_path / "m"
|
||||
with pytest.raises(MemoryStoreError, match="file:///<path> or minio://<bucket>"):
|
||||
open_store("s3://bucket")
|
||||
with pytest.raises(MemoryStoreError, match="file:///<path> or s3://<bucket>"):
|
||||
open_store("gs://bucket")
|
||||
with pytest.raises(MemoryStoreError, match="names no bucket and S3_BUCKET is unset"):
|
||||
open_store(environ={"OUSAST_MEMORY": "s3://"})
|
||||
|
||||
|
||||
def test_minio_settings_name_missing_keys_never_values() -> None:
|
||||
with pytest.raises(MemoryStoreError, match="MINIO_ACCESS_KEY, MINIO_SECRET_KEY") as caught:
|
||||
open_store(environ={"OUSAST_MEMORY": "minio://bucket", "MINIO_ENDPOINT": "minio.example:9000"})
|
||||
assert "minio.example" not in str(caught.value)
|
||||
env = {"MINIO_ENDPOINT": "h:9000", "MINIO_ACCESS_KEY": "ak", "MINIO_SECRET_KEY": "sk-value", "MINIO_SECURE": "false"}
|
||||
assert minio_settings(env) == {"endpoint": "h:9000", "access_key": "ak", "secret_key": "sk-value", "secure": False}
|
||||
assert minio_settings({**env, "MINIO_SECURE": ""})["secure"] is True
|
||||
def test_s3_settings_name_missing_keys_never_values() -> None:
|
||||
with pytest.raises(MemoryStoreError, match="AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY") as caught:
|
||||
open_store(environ={"OUSAST_MEMORY": "s3://bucket", "S3_ENDPOINT": "https://files.example:9000"})
|
||||
assert "files.example" not in str(caught.value)
|
||||
env = {"S3_ENDPOINT": "https://h:9000", "AWS_ACCESS_KEY_ID": "ak", "AWS_SECRET_ACCESS_KEY": "sk-value"}
|
||||
assert s3_settings(env) == {"endpoint": "https://h:9000", "access_key": "ak", "secret_key": "sk-value"}
|
||||
assert s3_settings({**env, "S3_REGION": "eu-1", "AWS_SESSION_TOKEN": "tok"}) == {
|
||||
**s3_settings(env), "region": "eu-1", "session_token": "tok"
|
||||
} # fmt: skip
|
||||
with pytest.raises(MemoryStoreError, match=r"S3_ENDPOINT must be a URL with a scheme") as caught:
|
||||
s3_settings({**env, "S3_ENDPOINT": "h:9000"})
|
||||
assert "h:9000" not in str(caught.value)
|
||||
|
||||
|
||||
def test_minio_without_the_sdk_names_the_extra() -> None:
|
||||
def test_s3_without_boto3_names_the_extra() -> None:
|
||||
try:
|
||||
import minio # noqa: F401
|
||||
import boto3 # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
pytest.skip("the minio SDK is installed here")
|
||||
env = {"OUSAST_MEMORY": "minio://bucket", "MINIO_ENDPOINT": "h:9000", "MINIO_ACCESS_KEY": "ak", "MINIO_SECRET_KEY": "secret-xyz"}
|
||||
with pytest.raises(MemoryStoreError, match=r"install openultrasast\[minio\]") as caught:
|
||||
open_store(environ=env)
|
||||
pytest.skip("boto3 is installed here")
|
||||
env = {"OUSAST_MEMORY": "s3://bucket", "S3_ENDPOINT": "https://h:9000", "AWS_ACCESS_KEY_ID": "ak"}
|
||||
with pytest.raises(MemoryStoreError, match=r"install openultrasast\[s3\]") as caught:
|
||||
open_store(environ={**env, "AWS_SECRET_ACCESS_KEY": "secret-xyz"})
|
||||
assert "secret-xyz" not in str(caught.value)
|
||||
|
||||
|
||||
def test_the_boto3_client_is_built_for_path_style_sigv4_and_no_checksum_trailers() -> None:
|
||||
pytest.importorskip("boto3")
|
||||
client = S3Client("https://files.example", "ak", "secret-xyz", "sast-memory", region="eu-1")
|
||||
meta = client.sdk.meta
|
||||
assert (meta.endpoint_url, meta.region_name, meta.config.signature_version) == ("https://files.example", "eu-1", "s3v4")
|
||||
assert meta.config.s3 == {"addressing_style": "path"}
|
||||
assert getattr(meta.config, "request_checksum_calculation", "when_required") == "when_required"
|
||||
url = client.presign("PUT", "runs/r1/x/units.jsonl", timedelta(minutes=5))
|
||||
assert url.startswith("https://files.example/sast-memory/runs/r1/x/units.jsonl?") and "X-Amz-Expires=300" in url
|
||||
assert "secret-xyz" not in url
|
||||
|
||||
|
||||
# --- disk refusal (FileStore) --------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user