mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-10-08 22:46:41 +00:00
fix(oci): prepare managed volumes across stack installs
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
"""Regression test for volume_preparations survival through the generic
|
||||
multi-LXC stack pipeline (apply_stack_support + service_template).
|
||||
|
||||
install_generic_stack.py::create_service() writes each service's own
|
||||
service['template'] to a per-service template.json and runs install_oci.sh
|
||||
against that file independently — the parent stack template's
|
||||
installer_profile is never read at install time. So the fix must land in
|
||||
each service's own template, not just the parent's top-level
|
||||
installer_profile (that alone would be a cosmetic data match with no real
|
||||
effect). This is the regression guard for the stack volume_preparations
|
||||
loss first found via read-only audit (2026-09-29): apply_stack_support()
|
||||
replaced installer_profile wholesale and silently dropped
|
||||
volume_preparations for every stack app (Blinko, Kimai, Linkwarden, Monica
|
||||
Official, Petio, Qui, RomM, Teable).
|
||||
"""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from proxmenux_oci.catalog import Catalog
|
||||
from proxmenux_oci.stack import build_stack, DefaultsUI
|
||||
|
||||
|
||||
class StackVolumePreparationsTests(unittest.TestCase):
|
||||
def test_stack_service_receives_only_its_own_volume_preparation(self):
|
||||
"""Blinko (2-service stack: blinko + blinko-postgres). The real
|
||||
build_stack() output — what create_service() actually writes to
|
||||
each service's template.json — must carry the preparation for
|
||||
/app/.blinko into the blinko service's own template, scoped to
|
||||
only_when_mount_type: managed-volume, and must NOT leak it (or any
|
||||
other service's paths) into blinko-postgres's template, which has
|
||||
no volume_preparations entry of its own in the source catalog."""
|
||||
template = Catalog(ROOT).compose("blinko")
|
||||
result = build_stack(template, DefaultsUI(), mode="simple")
|
||||
|
||||
services_by_name = {s["name"]: s for s in result["services"]}
|
||||
self.assertEqual(set(services_by_name), {"blinko", "blinko-postgres"})
|
||||
|
||||
blinko_preps = services_by_name["blinko"]["template"]["proxmox"] \
|
||||
.get("installer_profile", {}).get("volume_preparations", [])
|
||||
self.assertEqual(len(blinko_preps), 1)
|
||||
self.assertEqual(blinko_preps[0]["container_path"], "/app/.blinko")
|
||||
self.assertEqual(blinko_preps[0]["only_when_mount_type"], "managed-volume")
|
||||
|
||||
postgres_preps = services_by_name["blinko-postgres"]["template"]["proxmox"] \
|
||||
.get("installer_profile", {}).get("volume_preparations", [])
|
||||
self.assertEqual(postgres_preps, [])
|
||||
|
||||
def test_synthetic_dependency_volume_preparation_reaches_only_that_service(self):
|
||||
"""Blinko's postgres dependency service (blinko-postgres, image
|
||||
postgres:latest) has its own Compose-declared mount,
|
||||
/var/lib/postgresql/data, which build_stack() normalizes to
|
||||
/var/lib/postgresql for PostgreSQL 18+ (see the 'Official
|
||||
PostgreSQL 18+' comment in build_stack()) — this mount is NOT one
|
||||
of the DEPENDENCY_VOLUMES fallback entries, it comes straight from
|
||||
the source Compose file. A synthetic volume_preparations entry for
|
||||
the final, normalized mount target (/var/lib/postgresql) is
|
||||
injected on the parent template to verify it reaches only the
|
||||
postgres service's own template — not blinko's, and not lost. This
|
||||
test is only about correct metadata transfer through
|
||||
service_template()/build_stack(); it makes no claim about whether
|
||||
preparing a database data directory this way is runtime-appropriate."""
|
||||
template = Catalog(ROOT).compose("blinko")
|
||||
template["proxmox"]["installer_profile"].setdefault("volume_preparations", [])
|
||||
template["proxmox"]["installer_profile"]["volume_preparations"].append({
|
||||
"container_path": "/var/lib/postgresql",
|
||||
"remove_lost_found": True,
|
||||
"owner_strategy": "mapped-root",
|
||||
"only_when_mount_type": "managed-volume",
|
||||
})
|
||||
|
||||
result = build_stack(template, DefaultsUI(), mode="simple")
|
||||
services_by_name = {s["name"]: s for s in result["services"]}
|
||||
|
||||
postgres_preps = services_by_name["blinko-postgres"]["template"]["proxmox"] \
|
||||
.get("installer_profile", {}).get("volume_preparations", [])
|
||||
self.assertEqual(
|
||||
[p["container_path"] for p in postgres_preps],
|
||||
["/var/lib/postgresql"],
|
||||
"The synthetic preparation for the final, normalized postgres "
|
||||
"mount target must reach the postgres service's own template.",
|
||||
)
|
||||
self.assertEqual(
|
||||
postgres_preps[0]["container_path"],
|
||||
services_by_name["blinko-postgres"]["template"]["container_contract"]["volumes"][0]["container_path"],
|
||||
"The preparation's container_path must match the service's own "
|
||||
"final mount path exactly, not an intermediate/pre-rename form.",
|
||||
)
|
||||
|
||||
blinko_preps = services_by_name["blinko"]["template"]["proxmox"] \
|
||||
.get("installer_profile", {}).get("volume_preparations", [])
|
||||
self.assertEqual(
|
||||
[p["container_path"] for p in blinko_preps],
|
||||
["/app/.blinko"],
|
||||
"The synthetic postgres-only preparation must not leak into "
|
||||
"blinko's own template.",
|
||||
)
|
||||
|
||||
def test_synthetic_dependency_volume_preparation_keyed_to_the_pre_rename_path_is_also_normalized(self):
|
||||
"""The same postgres mount, but the synthetic preparation is keyed
|
||||
to the pre-rename Compose path (/var/lib/postgresql/data) instead
|
||||
of the final one. It must still reach blinko-postgres's own
|
||||
template, remapped to the same final container_path
|
||||
(/var/lib/postgresql) that build_stack() actually mounts — a
|
||||
preparation entry whose path no longer matches any real mount is
|
||||
dead configuration at install time. This test is only about
|
||||
correct metadata transfer; it makes no claim about runtime
|
||||
appropriateness."""
|
||||
template = Catalog(ROOT).compose("blinko")
|
||||
template["proxmox"]["installer_profile"].setdefault("volume_preparations", [])
|
||||
template["proxmox"]["installer_profile"]["volume_preparations"].append({
|
||||
"container_path": "/var/lib/postgresql/data",
|
||||
"remove_lost_found": True,
|
||||
"owner_strategy": "mapped-root",
|
||||
"only_when_mount_type": "managed-volume",
|
||||
})
|
||||
|
||||
result = build_stack(template, DefaultsUI(), mode="simple")
|
||||
services_by_name = {s["name"]: s for s in result["services"]}
|
||||
|
||||
postgres_preps = services_by_name["blinko-postgres"]["template"]["proxmox"] \
|
||||
.get("installer_profile", {}).get("volume_preparations", [])
|
||||
self.assertEqual(
|
||||
[p["container_path"] for p in postgres_preps],
|
||||
["/var/lib/postgresql"],
|
||||
"A preparation keyed to the pre-rename Compose path must be "
|
||||
"remapped to the same final path build_stack() actually mounts, "
|
||||
"not silently dropped and not left pointing at the stale path.",
|
||||
)
|
||||
self.assertEqual(
|
||||
postgres_preps[0]["container_path"],
|
||||
services_by_name["blinko-postgres"]["template"]["container_contract"]["volumes"][0]["container_path"],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user