Skip to content

Commit 9fb47a9

Browse files
committed
fix: enforce Watchtower scope and labels in compose generation for legacy user configs
1 parent 20d2e76 commit 9fb47a9

File tree

3 files changed

+75
-4
lines changed

3 files changed

+75
-4
lines changed

tests/test_generator.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,51 @@ def test_watchtower_service_omitted_when_disabled(self):
158158
doc = self._compose(watchtower_enabled=False)
159159
self.assertNotIn("watchtower", doc.get("services", {}))
160160

161+
def test_watchtower_scope_enforced_on_old_user_config(self):
162+
"""Scope env vars and label are injected even when old user-config lacks them."""
163+
import copy
164+
165+
# Simulate an older user-config whose watchtower service has no scope entries
166+
user_cfg = copy.deepcopy(_USER_CFG_BASE)
167+
user_cfg["compose_config_common"]["watchtower_service"]["proxy_disabled"] = {
168+
"container_name": "${DEVICE_NAME}_watchtower",
169+
"image": "nickfedor/watchtower:latest",
170+
# intentionally missing WATCHTOWER_SCOPE env and scope label
171+
"environment": ["WATCHTOWER_CLEANUP=true"],
172+
"labels": ["com.centurylinklabs.watchtower.enable=true"],
173+
"volumes": ["/var/run/docker.sock:/var/run/docker.sock"],
174+
"restart": "always",
175+
}
176+
177+
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".yaml") as f:
178+
compose_path = f.name
179+
try:
180+
assemble_docker_compose(
181+
_M4B_CFG, _APP_CFG, user_cfg, compose_path, is_main_instance=True
182+
)
183+
with open(compose_path) as f:
184+
doc = yaml.safe_load(f) or {}
185+
finally:
186+
if os.path.exists(compose_path):
187+
os.unlink(compose_path)
188+
189+
wt = doc.get("services", {}).get("watchtower", {})
190+
env = wt.get("environment", [])
191+
labels = wt.get("labels", [])
192+
193+
self.assertTrue(
194+
any("WATCHTOWER_SCOPE" in e for e in env),
195+
"WATCHTOWER_SCOPE must be enforced in watchtower environment",
196+
)
197+
self.assertTrue(
198+
any("WATCHTOWER_LABEL_ENABLE" in e for e in env),
199+
"WATCHTOWER_LABEL_ENABLE must be enforced in watchtower environment",
200+
)
201+
self.assertTrue(
202+
any("watchtower.scope" in lbl for lbl in labels),
203+
"scope label must be enforced on watchtower service",
204+
)
205+
161206

162207
if __name__ == "__main__":
163208
unittest.main()

utils/fn_setupApps.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -735,11 +735,17 @@ def setup_watchtower(user_config: dict[str, Any]) -> None:
735735
print("Keeping existing Watchtower settings.")
736736
logging.info("User chose to keep existing Watchtower settings.")
737737
return
738+
else:
739+
print("The M4B built-in Watchtower (auto-updater) is currently disabled.")
740+
if not ask_question_yn("Do you want to change the Watchtower settings?"):
741+
print("Keeping existing Watchtower settings (Watchtower remains disabled).")
742+
logging.info("User chose to keep existing Watchtower settings (disabled).")
743+
return
738744

739745
if ask_question_yn(
740746
"Do you want M4B to manage container auto-updates via its built-in Watchtower?\n"
741747
"(Disable only if another Watchtower instance is already running on this host)",
742-
default=True,
748+
default=current_enabled,
743749
):
744750
watchtower_config["enabled"] = True
745751
print("M4B built-in Watchtower enabled. Container images will be auto-updated.")

utils/generator.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import copy
12
import logging
23
import os
34
import re
@@ -305,9 +306,28 @@ def assemble_docker_compose(
305306
watchtower_service_key = (
306307
"proxy_enabled" if proxy_enabled else "proxy_disabled"
307308
)
308-
watchtower_service = compose_config_common["watchtower_service"][
309-
watchtower_service_key
310-
]
309+
watchtower_service = copy.deepcopy(
310+
compose_config_common["watchtower_service"][watchtower_service_key]
311+
)
312+
# Enforce scoping env vars at generation time so older user-config
313+
# files (missing WATCHTOWER_SCOPE / WATCHTOWER_LABEL_ENABLE) still
314+
# produce a correctly scoped Watchtower service.
315+
wt_env: list = watchtower_service.setdefault("environment", [])
316+
required_env = {
317+
"WATCHTOWER_SCOPE": "WATCHTOWER_SCOPE=${M4B_WATCHTOWER_SCOPE}",
318+
"WATCHTOWER_LABEL_ENABLE": "WATCHTOWER_LABEL_ENABLE=${M4B_WATCHTOWER_LABELS}",
319+
}
320+
existing_keys = {e.split("=")[0] for e in wt_env if isinstance(e, str)}
321+
for key, entry in required_env.items():
322+
if key not in existing_keys:
323+
wt_env.append(entry)
324+
logging.info(f"Enforced missing Watchtower env var: {entry}")
325+
# Enforce scope label so the service is self-managed within scope.
326+
wt_labels: list = watchtower_service.setdefault("labels", [])
327+
scope_label = "com.centurylinklabs.watchtower.scope=${M4B_WATCHTOWER_SCOPE}"
328+
if scope_label not in wt_labels:
329+
wt_labels.append(scope_label)
330+
logging.info("Enforced missing Watchtower scope label")
311331
services["watchtower"] = watchtower_service
312332
# Only add m4bwebdashboard if dashboard is enabled
313333
m4b_dashboard_config = user_config.get("m4b_dashboard", {})

0 commit comments

Comments
 (0)