prole/tests/installer/test_storage_ops.py
chrisfu b09319ffaf feat: pre-provision cluster-scoped CNPG Synology PVs
Add storage ops for deterministic namespace+cluster local PV paths, labels/selectors, host-path preparation, and idempotent reconciliation before CNPG cluster apply.

Wire selector injection and validation into CNPG deploy flow/script, and extend installer tests for provisioning orchestration and failure handling.

Co-authored-by: Junie <junie@jetbrains.com>
2026-03-23 00:32:35 -07:00

190 lines
6.7 KiB
Python

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.prole.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.prole.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["prole.io/namespace"] == "knoe-db"
assert labels["prole.io/cluster"] == "cluster-a"
assert labels["prole.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.prole.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.prole.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.prole.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)
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["prole.io/namespace"] == "knoe-db"
assert provisioned.data_selector["prole.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.prole.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)