prole/tests/installer/test_cnpg_placement.py
chrisfu 59c8ea3f8f Add CNPG placement planner with persistence and integration tests
- Introduce `cnpg_placement.py` to handle round-robin node assignments for CloudNativePG clusters.
- Implement persistent placement plans with schema normalization and hashing.
- Add `load_cnpg_placement_plan` and `save_cnpg_placement_plan` for plan persistence.
- Integrate planner with installer to support node-based topology configuration.
- Update `actions.py` with placement planning logic, including rebalance support and node eligibility checks.
- Extend shell utilities (`init_cloudnative_pg.sh`) for placement-aware CNPG topology adjustments.
- Add comprehensive unit tests and integration tests for planner functionality, persistence, and shell environment exports.
2026-03-23 00:06:47 -07:00

108 lines
3.7 KiB
Python

from __future__ import annotations
from knoe.core.cnpg_placement import (
load_cnpg_placement_plan,
plan_cnpg_placement,
save_cnpg_placement_plan,
)
class TestCnpgPlacementPlanner:
def test_deterministic_eligible_node_ordering_and_round_robin(self):
plan = plan_cnpg_placement(
cluster_name="knoe-db",
desired_instances=5,
candidate_nodes=[" db-b ", "db-a", "db-b", "db-c"],
)
assert plan["eligible_nodes"] == ["db-a", "db-b", "db-c"]
assert plan["assignments"] == {
"0": "db-a",
"1": "db-b",
"2": "db-c",
"3": "db-a",
"4": "db-b",
}
def test_reuses_persisted_plan_when_still_valid(self):
prior = plan_cnpg_placement(
cluster_name="knoe-db",
desired_instances=3,
candidate_nodes=["db-a", "db-b"],
)
plan = plan_cnpg_placement(
cluster_name="knoe-db",
desired_instances=3,
candidate_nodes=["db-b", "db-a", "db-c"],
prior_plan=prior,
)
assert plan["metadata"]["reused"] is True
assert plan["metadata"]["regenerated"] is False
# New eligible node must not reshuffle existing ordinals.
assert plan["assignments"] == prior["assignments"]
def test_regenerates_when_assigned_node_becomes_ineligible(self):
prior = {
"cluster_name": "knoe-db",
"assignments": {"0": "db-a", "1": "db-b", "2": "db-a"},
}
plan = plan_cnpg_placement(
cluster_name="knoe-db",
desired_instances=3,
candidate_nodes=["db-a", "db-c"],
prior_plan=prior,
)
assert plan["metadata"]["regenerated"] is True
assert plan["metadata"]["reason"] == "assigned_node_no_longer_eligible"
assert set(plan["assignments"].values()) <= {"db-a", "db-c"}
def test_rebalance_regenerates_even_with_valid_prior(self):
prior = {
"cluster_name": "knoe-db",
"assignments": {"0": "db-a", "1": "db-a", "2": "db-a"},
}
plan = plan_cnpg_placement(
cluster_name="knoe-db",
desired_instances=3,
candidate_nodes=["db-a", "db-b", "db-c"],
prior_plan=prior,
rebalance=True,
)
assert plan["metadata"]["regenerated"] is True
assert plan["metadata"]["reason"] == "rebalance_requested"
assert plan["assignments"] == {"0": "db-a", "1": "db-b", "2": "db-c"}
def test_graceful_when_fewer_eligible_nodes_than_instances(self):
plan = plan_cnpg_placement(
cluster_name="knoe-db",
desired_instances=4,
candidate_nodes=["db-a"],
)
assert plan["assignments"] == {
"0": "db-a",
"1": "db-a",
"2": "db-a",
"3": "db-a",
}
class TestCnpgPlacementPersistence:
def test_persisted_plan_can_be_reloaded_and_reused(self, tmp_path):
plan_path = tmp_path / "cnpg-placement" / "knoe-system-knoe-db.json"
initial = plan_cnpg_placement(
cluster_name="knoe-db",
desired_instances=3,
candidate_nodes=["db-a", "db-b"],
)
save_cnpg_placement_plan(plan_path, initial)
prior = load_cnpg_placement_plan(plan_path)
assert prior is not None
reused = plan_cnpg_placement(
cluster_name="knoe-db",
desired_instances=3,
candidate_nodes=["db-a", "db-b", "db-c"],
prior_plan=prior,
)
assert reused["metadata"]["reused"] is True
assert reused["assignments"] == initial["assignments"]