@@ -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
162207if __name__ == "__main__":
163208 unittest.main()
0 commit comments