mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
fix(inventory): drop disable= toggles once their key leaves the list
Provisioning merges into host_vars and never deletes, so the false enabled/shared toggles that apply_services_disabled writes for a key stayed in the round inventory after a later run dropped that key from disable=. That covered the provider's own entry too: with seaweedfs once disabled, web-app-seaweedfs kept shared: false, the service registry offered no seaweedfs provider, container_networks attached no consumer, and BookWyrm ran USE_S3=true against http://seaweedfs:8333 without a route. reconcile_services_disabled_from_env records the keys of each disable run in the host_vars key _INFINITO_SERVICES_DISABLED. On the next run it deletes, in every application block, the enabled/shared toggles that are still exactly false for a recorded key the current disable no longer lists, drops a block left empty, and rewrites or removes the record. It runs right after ensure_host_vars_file, before credentials and the variant bake, so a variant that wants a toggle off writes it again. With an unchanged or empty disable it does nothing. An inventory written before this change has no record, so its older toggles are only reverted after one run with the old list. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1fb98b5b76
commit
0327114c44
3 files changed
+175
-1
No files matched your search
@@ -19,7 +19,10 @@ from .inventory_generator import generate_dynamic_inventory
|
||||
from .mirror_overrides import apply_mirror_overrides
|
||||
from .passwords import generate_random_password
|
||||
from .project import build_env_with_project_root, detect_project_root
|
||||
from .services_disabler import apply_services_disabled_from_env
|
||||
from .services_disabler import (
|
||||
apply_services_disabled_from_env,
|
||||
reconcile_services_disabled_from_env,
|
||||
)
|
||||
from .tor_node import apply_tor_node_onion
|
||||
from .users_generator import generate_user_passwords
|
||||
from .yaml_io import dump_yaml, load_yaml
|
||||
@@ -230,6 +233,9 @@ def main(argv: list[str] | None = None) -> int:
|
||||
host_vars_file=host_vars_file,
|
||||
host=args.host,
|
||||
)
|
||||
reconcile_services_disabled_from_env(
|
||||
host_vars_file=host_vars_file, roles_dir=roles_dir
|
||||
)
|
||||
|
||||
print(f"[INFO] Ensuring ansible_become_password for host '{args.host}'")
|
||||
ensure_become_password(
|
||||
|
||||
@@ -15,9 +15,13 @@ from utils.roles.applications.services.registry import (
|
||||
)
|
||||
from utils.roles.mapping import ROLE_FILE_META_SERVICES
|
||||
|
||||
from .ruamel_io import dump_document, load_document
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
DISABLED_SERVICES_RECORD = "_INFINITO_SERVICES_DISABLED"
|
||||
|
||||
|
||||
def _load_yaml_mapping_tolerant(path: Path) -> dict:
|
||||
"""Read a YAML file as a mapping; non-mapping or unparseable content
|
||||
@@ -262,6 +266,64 @@ def apply_services_disabled_from_env(
|
||||
)
|
||||
|
||||
|
||||
def _revert_disabled_toggles(applications: CommentedMap, services: list[str]) -> None:
|
||||
for app_id, app_data in applications.items():
|
||||
if not isinstance(app_data, CommentedMap):
|
||||
continue
|
||||
svc_map = app_data.get("services")
|
||||
if not isinstance(svc_map, CommentedMap):
|
||||
continue
|
||||
for svc_name in services:
|
||||
svc = svc_map.get(svc_name)
|
||||
if not isinstance(svc, CommentedMap):
|
||||
continue
|
||||
dropped = [flag for flag in ("enabled", "shared") if svc.get(flag) is False]
|
||||
if not dropped:
|
||||
continue
|
||||
for flag in dropped:
|
||||
del svc[flag]
|
||||
if not svc:
|
||||
del svc_map[svc_name]
|
||||
print(
|
||||
f"[INFO] disable: {app_id}.services.{svc_name} → "
|
||||
f"{', '.join(dropped)} back to the role default"
|
||||
)
|
||||
|
||||
|
||||
def reconcile_services_disabled_from_env(host_vars_file: Path, roles_dir: Path) -> None:
|
||||
"""Drop the false toggles of every key the previous `disable` run recorded
|
||||
and this run's `disable` no longer lists, then record this run's keys.
|
||||
|
||||
Runs before the variant bake, so a variant that wants a toggle off writes it
|
||||
again right after.
|
||||
|
||||
Args:
|
||||
host_vars_file: host_vars file of the inventory.
|
||||
roles_dir: roles tree the `disable` tokens resolve against.
|
||||
"""
|
||||
if not host_vars_file.exists():
|
||||
return
|
||||
current = expand_service_tokens(
|
||||
parse_services_disabled(os.environ.get("disable", "")), roles_dir
|
||||
)
|
||||
doc = load_document(host_vars_file)
|
||||
raw_record = doc.get(DISABLED_SERVICES_RECORD)
|
||||
recorded = [str(key) for key in raw_record] if isinstance(raw_record, list) else []
|
||||
if recorded == current:
|
||||
return
|
||||
|
||||
stale = [key for key in recorded if key not in current]
|
||||
applications = doc.get("applications")
|
||||
if stale and isinstance(applications, CommentedMap):
|
||||
_revert_disabled_toggles(applications, stale)
|
||||
|
||||
if current:
|
||||
doc[DISABLED_SERVICES_RECORD] = current
|
||||
else:
|
||||
del doc[DISABLED_SERVICES_RECORD]
|
||||
dump_document(host_vars_file, doc)
|
||||
|
||||
|
||||
def _inventory_application_ids(inventory_file: Path) -> set[str]:
|
||||
doc = _load_yaml_mapping_tolerant(inventory_file)
|
||||
all_section = doc.get("all") or {}
|
||||
|
||||
@@ -9,15 +9,18 @@ from tempfile import TemporaryDirectory
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from cli.administration.inventory.provision.services_disabler import (
|
||||
DISABLED_SERVICES_RECORD,
|
||||
apply_services_disabled,
|
||||
apply_services_disabled_from_env,
|
||||
assert_services_disabled_inventory_consistency_from_env,
|
||||
find_provider_roles,
|
||||
find_services_disabled_conflicts,
|
||||
parse_services_disabled,
|
||||
reconcile_services_disabled_from_env,
|
||||
remove_roles_from_inventory,
|
||||
)
|
||||
from utils.cache.yaml import dump_yaml
|
||||
from utils.roles.applications.services.registry import discover_role_services
|
||||
from utils.roles.mapping import ROLE_FILE_META_SERVICES, ROLE_FILE_VARS_MAIN
|
||||
|
||||
|
||||
@@ -544,5 +547,108 @@ class TestServicesDisabledConflicts(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestReconcileServicesDisabledFromEnv(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.root = Path(self.tmp.name)
|
||||
self.yaml_rt = YAML(typ="rt")
|
||||
self.yaml_rt.preserve_quotes = True
|
||||
self.host_vars = self.root / "host_vars.yml"
|
||||
self.roles_dir = self.root / "roles"
|
||||
self.roles_dir.mkdir()
|
||||
self.provider_entry = {"image": "seaweedfs", "enabled": True, "shared": True}
|
||||
self._make_role("web-app-seaweedfs", {"seaweedfs": self.provider_entry})
|
||||
self._make_role(
|
||||
"web-app-bookwyrm",
|
||||
{
|
||||
"seaweedfs": {
|
||||
"enabled": "{{ 'web-app-seaweedfs' in group_names }}",
|
||||
"shared": "{{ 'web-app-seaweedfs' in group_names }}",
|
||||
},
|
||||
"minio": {"enabled": False, "shared": False},
|
||||
},
|
||||
)
|
||||
self._make_role("web-app-minio", {"minio": {"enabled": True, "shared": True}})
|
||||
self._write({"applications": {}})
|
||||
|
||||
def _make_role(self, role_name: str, services: dict) -> None:
|
||||
role_dir = self.roles_dir / role_name
|
||||
(role_dir / "meta").mkdir(parents=True)
|
||||
(role_dir / "vars").mkdir(parents=True)
|
||||
dump_yaml(role_dir / ROLE_FILE_META_SERVICES, services)
|
||||
dump_yaml(role_dir / ROLE_FILE_VARS_MAIN, {"application_id": role_name})
|
||||
|
||||
def _write(self, data: dict) -> None:
|
||||
with self.host_vars.open("w") as f:
|
||||
self.yaml_rt.dump(data, f)
|
||||
|
||||
def _read(self) -> dict:
|
||||
with self.host_vars.open("r") as f:
|
||||
return self.yaml_rt.load(f)
|
||||
|
||||
def _provision(self, disable: str) -> None:
|
||||
with unittest.mock.patch.dict(os.environ, {"disable": disable}):
|
||||
reconcile_services_disabled_from_env(self.host_vars, self.roles_dir)
|
||||
apply_services_disabled_from_env(self.host_vars, roles_dir=self.roles_dir)
|
||||
|
||||
def _services(self, app_id: str) -> dict:
|
||||
return self._read()["applications"][app_id].get("services", {})
|
||||
|
||||
def test_a_provider_dropped_from_disable_is_a_registry_provider_again(self):
|
||||
self._provision("minio,seaweedfs")
|
||||
self._provision("minio")
|
||||
|
||||
override = self._services("web-app-seaweedfs").get("seaweedfs", {})
|
||||
self.assertNotIn("enabled", override)
|
||||
self.assertNotIn("shared", override)
|
||||
config = {"services": {"seaweedfs": {**self.provider_entry, **override}}}
|
||||
self.assertIn("seaweedfs", discover_role_services("web-app-seaweedfs", config))
|
||||
self.assertNotIn("seaweedfs", self._services("web-app-bookwyrm"))
|
||||
|
||||
def test_a_key_still_listed_stays_disabled(self):
|
||||
self._provision("minio,seaweedfs")
|
||||
self._provision("minio")
|
||||
|
||||
self.assertEqual(
|
||||
{"enabled": False, "shared": False},
|
||||
dict(self._services("web-app-minio")["minio"]),
|
||||
)
|
||||
self.assertEqual(["minio"], list(self._read()[DISABLED_SERVICES_RECORD]))
|
||||
|
||||
def test_a_toggle_written_true_after_the_disable_survives(self):
|
||||
self._provision("seaweedfs")
|
||||
hv = self._read()
|
||||
consumer = hv["applications"]["web-app-bookwyrm"]["services"]["seaweedfs"]
|
||||
consumer["enabled"] = True
|
||||
self._write(hv)
|
||||
|
||||
self._provision("")
|
||||
|
||||
self.assertEqual(
|
||||
{"enabled": True},
|
||||
dict(self._services("web-app-bookwyrm")["seaweedfs"]),
|
||||
)
|
||||
self.assertNotIn(DISABLED_SERVICES_RECORD, self._read())
|
||||
|
||||
def test_a_false_toggle_no_disable_run_recorded_is_left_alone(self):
|
||||
self._write(
|
||||
{
|
||||
"applications": {
|
||||
"web-app-bookwyrm": {
|
||||
"services": {"seaweedfs": {"enabled": False, "shared": False}}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
self._provision("")
|
||||
|
||||
self.assertEqual(
|
||||
{"enabled": False, "shared": False},
|
||||
dict(self._services("web-app-bookwyrm")["seaweedfs"]),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user