from __future__ import annotations import pytest from knoe.core.ops import storage as storage_ops def test_build_cluster_storage_paths_prefers_distinct_roots(monkeypatch): monkeypatch.setattr( storage_ops, "_is_non_root_mount", lambda path, **_kwargs: str(path) in {"/synology/d001", "/synology/d002"}, ) spec = storage_ops.ClusterStorageSpec( namespace="knoe-db", cluster_name="cluster-a", node_name="myrddin.knoe.org", synology_roots=("/synology/d001", "/synology/d002"), ) paths = storage_ops.build_cluster_storage_paths(spec) assert paths.data_root != paths.wal_root assert paths.data_path.endswith("/knoe/knoe-db/knoe-db/cluster-a/data") assert paths.wal_path.endswith("/knoe/knoe-db/knoe-db/cluster-a/wal") def test_build_cluster_storage_paths_falls_back_to_same_root(monkeypatch): monkeypatch.setattr( storage_ops, "_is_non_root_mount", lambda path, **_kwargs: str(path) == "/synology/d001", ) spec = storage_ops.ClusterStorageSpec( namespace="knoe-db", cluster_name="cluster-a", node_name="myrddin.knoe.org", synology_roots=("/synology/d001", "/synology/d002"), ) paths = storage_ops.build_cluster_storage_paths(spec) assert paths.data_root == "/synology/d001" assert paths.wal_root == "/synology/d001" assert paths.data_path != paths.wal_path def test_build_pv_labels_include_cluster_scope(): labels = storage_ops.build_pv_labels( namespace="knoe-db", cluster_name="cluster-a", role="data", volume="d001", ) assert labels["synology.storage/role"] == "data" assert labels["synology.storage/volume"] == "d001" assert labels["knoe.io/namespace"] == "knoe-db" assert labels["knoe.io/cluster"] == "cluster-a" assert labels["knoe.io/service"] == "knoe-db" def test_provision_cluster_storage_fails_on_existing_name_mismatch(monkeypatch): spec = storage_ops.ClusterStorageSpec( namespace="knoe-db", cluster_name="cluster-a", node_name="myrddin.knoe.org", ) paths = storage_ops.ClusterStoragePaths( data_root="/synology/d001", wal_root="/synology/d002", data_volume="d001", wal_volume="d002", data_path="/synology/d001/knoe/knoe-db/knoe-db/cluster-a/data", wal_path="/synology/d002/knoe/knoe-db/knoe-db/cluster-a/wal", ) data_name = storage_ops.build_pv_name("synology-iscsi", "knoe-db", "cluster-a", "data") monkeypatch.setattr(storage_ops, "build_cluster_storage_paths", lambda _spec: paths) monkeypatch.setattr(storage_ops, "ensure_host_path", lambda *args, **kwargs: None) existing = { data_name: { "metadata": { "name": data_name, "labels": storage_ops.build_pv_labels( namespace="knoe-db", cluster_name="cluster-a", role="data", volume="d001", ), }, "spec": { "storageClassName": "synology-iscsi", "local": {"path": "/synology/d001/other-path"}, "nodeAffinity": { "required": { "nodeSelectorTerms": [ { "matchExpressions": [ { "key": "kubernetes.io/hostname", "operator": "In", "values": ["myrddin.knoe.org"], } ] } ] } }, }, } } monkeypatch.setattr(storage_ops, "_collect_existing_pvs", lambda _spec: existing) with pytest.raises(storage_ops.StorageProvisioningError, match="unexpected path"): storage_ops.provision_cluster_storage(spec) def test_provision_cluster_storage_applies_and_returns_selectors(monkeypatch): spec = storage_ops.ClusterStorageSpec( namespace="knoe-db", cluster_name="cluster-a", node_name="myrddin.knoe.org", ) paths = storage_ops.ClusterStoragePaths( data_root="/synology/d001", wal_root="/synology/d002", data_volume="d001", wal_volume="d002", data_path="/synology/d001/knoe/knoe-db/knoe-db/cluster-a/data", wal_path="/synology/d002/knoe/knoe-db/knoe-db/cluster-a/wal", ) monkeypatch.setattr(storage_ops, "build_cluster_storage_paths", lambda _spec: paths) monkeypatch.setattr(storage_ops, "_ensure_node_host_path", lambda *args, **kwargs: None) monkeypatch.setattr(storage_ops, "ensure_host_path", lambda *args, **kwargs: None) monkeypatch.setattr(storage_ops, "_collect_existing_pvs", lambda _spec: {}) applied: list[dict] = [] def _capture_apply(_spec, manifest): applied.append(manifest) monkeypatch.setattr(storage_ops, "_kubectl_apply_manifest", _capture_apply) provisioned = storage_ops.provision_cluster_storage(spec) assert len(applied) == 2 assert provisioned.data_pv_name == "synology-iscsi-knoe-db-cluster-a-data" assert provisioned.wal_pv_name == "synology-iscsi-knoe-db-cluster-a-wal" assert provisioned.data_selector["knoe.io/namespace"] == "knoe-db" assert provisioned.data_selector["knoe.io/cluster"] == "cluster-a" assert provisioned.wal_selector["synology.storage/role"] == "wal" def test_provision_cluster_storage_fails_on_path_overlap(monkeypatch): spec = storage_ops.ClusterStorageSpec( namespace="knoe-db", cluster_name="cluster-a", node_name="myrddin.knoe.org", ) paths = storage_ops.ClusterStoragePaths( data_root="/synology/d001", wal_root="/synology/d002", data_volume="d001", wal_volume="d002", data_path="/synology/d001/knoe/knoe-db/knoe-db/cluster-a/data", wal_path="/synology/d002/knoe/knoe-db/knoe-db/cluster-a/wal", ) monkeypatch.setattr(storage_ops, "build_cluster_storage_paths", lambda _spec: paths) monkeypatch.setattr(storage_ops, "ensure_host_path", lambda *args, **kwargs: None) existing = { "some-other-pv": { "metadata": {"name": "some-other-pv", "labels": {}}, "spec": { "storageClassName": "synology-iscsi", "local": {"path": "/synology/d001/knoe/knoe-db/knoe-db/cluster-a"}, "nodeAffinity": {"required": {"nodeSelectorTerms": []}}, }, } } monkeypatch.setattr(storage_ops, "_collect_existing_pvs", lambda _spec: existing) with pytest.raises(storage_ops.StorageProvisioningError, match="overlap"): storage_ops.provision_cluster_storage(spec)