Merge branch 'feature/updater' into feature/store-i18n

16 conflicts, resolved per hunk:

- sys-stk-front-proxy: tasks/main.yml keeps the network-sibling loop;
  the single stack_host_contents write of http.conf plus vhost and the
  front_proxy_vhost_changed gate move into tasks/vhost.yml, with the
  http.conf decided per sibling domain so an onion vhost gets no HTTPS
  redirect.
- sys-front-tls: 01_http.yml is gone as the updater intends; the
  cert-renew include in main.yml gates on the vhost domain's TLS.
- sys-svc-compose: the updater's resolve_host_cid without the remote
  echo and the prepared pending flag in up.yml, written with
  IS_SWARM_MODE and the repeated-when block grouping.
- web-app-nextcloud: the updater's post-upgrade gate and pending-only
  plugin configuration, which already carry the pinned-plugin handling.
- web-app-xwiki: one XWIKI_CORE_VERSION for the readiness probe and
  style.css.j2; OIDC/LDAP follow xwiki_extensions_ready at bootstrap,
  the S3 blob store stays off there as TestBlobstoreRenderOrder demands.
- roles_with_service: onion swap and dashboard card key combined, the
  canonical domain is the canonical URL's host in both branches.
- sys-ctl-hlth-csp: --clearnet-timeout and --concurrency together;
  run_checker forwards its options to build_docker_cmd, keeping the
  script under the 500-line limit.
- web-app-matrix: Element branding and web_client_location side by side.
- web-app-stalwart/README.md: the generated Cosmos section is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5.5 committed 2026-10-25 23:47:07 +01:00
commit 99123762df
72 files changed
+2844 -345

No files matched your search

@@ -125,6 +125,26 @@ class DummyTlsResolveLookup:
return [base_url]
class DummyCanonicalUrlLookup:
"""
Deterministic canonical_url stub.
Emulates: lookup('canonical_url', app_id, key) -> 'http(s)://<domains[app_id][key]>'
"""
def run(self, terms, variables=None, **kwargs):
variables = variables or {}
app_id, key = terms
app_domains = variables.get("domains", {}).get(app_id)
if not isinstance(app_domains, dict) or key not in app_domains:
raise AnsibleError(
f"dummy canonical_url: '{app_id}' has no canonical '{key}'"
)
scheme = (
"https" if _ansible_bool(variables.get("TLS_ENABLED", True)) else "http"
)
return [f"{scheme}://{app_domains[key]}"]
class TestDockerCardsLookup(unittest.TestCase):
def setUp(self):
self.test_roles_dir = tempfile.mkdtemp(prefix="test_roles_")
@@ -174,6 +194,8 @@ logo:
return _VarsLookup("domains")
if name == "tls":
return DummyTlsResolveLookup(templar)
if name == "canonical_url":
return DummyCanonicalUrlLookup()
raise AnsibleError(f"Unexpected lookup requested: {name}")
docker_cards_module.lookup_loader.get = _patched_get
@@ -304,6 +326,39 @@ logo:
self.assertEqual(result[0][0]["url"], "http://myportfolio.com")
def _card_target_variables(self, dashboard):
fake_variables = self._base_fake_variables()
fake_variables["domains"] = {
"portfolio": {
"www": "myportfolio.com",
"client": "client.myportfolio.com",
}
}
fake_variables["applications"]["portfolio"]["services"]["dashboard"] = dashboard
return fake_variables
def test_card_url_targets_the_card_key(self):
fake_variables = self._card_target_variables(
{"enabled": True, "card": "client"}
)
result = self._run_lookup(LookupModule(), fake_variables)
self.assertEqual(result[0][0]["url"], "https://client.myportfolio.com")
def test_card_url_without_card_key_targets_the_primary_domain(self):
fake_variables = self._card_target_variables({"enabled": True})
result = self._run_lookup(LookupModule(), fake_variables)
self.assertEqual(result[0][0]["url"], "https://myportfolio.com")
def test_unknown_card_key_fails_loudly(self):
fake_variables = self._card_target_variables({"enabled": True, "card": "nope"})
with self.assertRaisesRegex(AnsibleError, "nope"):
self._run_lookup(LookupModule(), fake_variables)
if __name__ == "__main__":
unittest.main()
@@ -72,13 +72,15 @@ class RolesWithServiceLookupTests(unittest.TestCase):
handover: frozenset = frozenset(),
**kwargs,
):
"""Run the lookup with the ``applications``, ``tls`` and
``network_handover`` sub-lookups patched to stay hermetic. The stubbed
tls answers ``[role_id, "url.base"]`` with the role's own configured
domain, which is what a clearnet deployment resolves; pass ``url_host``
(every role) or ``url_hosts`` (per role) to model a deployment whose URL
host differs, such as an onion one. A role in ``handover`` hands every
sibling outside its canonical network over."""
"""Run the lookup with the ``applications``, ``tls``,
``canonical_url`` and ``network_handover`` sub-lookups patched to stay
hermetic; any other sub-lookup raises ``KeyError``. The stubbed tls
answers ``[role_id, "url.base"]`` and the stubbed canonical_url
``[role_id, key]`` with the role's own configured domain, which is what
a clearnet deployment resolves; pass ``url_host`` (every role) or
``url_hosts`` (per role) to model a deployment whose URL host differs,
such as an onion one. A role in ``handover`` hands every sibling outside
its canonical network over."""
lookup = self._make_lookup(vars_ or {})
resolve = self.mod._resolve_canonical_domain
@@ -100,16 +102,31 @@ class RolesWithServiceLookupTests(unittest.TestCase):
role in handover and network_of(domain) != network_of(canonical)
]
named = self.mod._resolve_named_canonical_domain
class _StubCanonicalUrl:
def run(self, terms_, variables=None, **kwargs):
role, key = str(terms_[0]), str(terms_[1])
host = (
(url_hosts or {}).get(role)
or url_host
or named(role, applications.get(role, {}), key)
)
return [f"https://{host}"]
class _StubApplications:
def run(self, terms_, variables=None, **kwargs):
return [applications]
stubs = {
"applications": _StubApplications(),
"tls": _StubTls(),
"canonical_url": _StubCanonicalUrl(),
"network_handover": _StubHandover(),
}
def _dispatch(name, **_kwargs):
if name == "applications":
return _StubApplications()
if name == "network_handover":
return _StubHandover()
return _StubTls()
return stubs[name]
with patch.object(self.mod.lookup_loader, "get", side_effect=_dispatch):
return lookup.run(terms, variables=vars_ or {}, **kwargs)
@@ -683,6 +700,60 @@ class RolesWithServiceLookupTests(unittest.TestCase):
with self.assertRaises(AnsibleError):
self._network_ids({}, network="onion")
def _card_target_role(self, service: str, **block_extra) -> dict:
block = {"enabled": True, "shared": True}
block.update(block_extra)
return {
"web-app-chat": {
"services": {service: block},
"domains": {
"canonical": {
"synapse": "matrix.example.com",
"element": "element.example.com",
}
},
},
}
def test_dashboard_card_key_targets_that_domain(self):
applications = self._card_target_role("dashboard", card="element")
entry = self._run(["dashboard"], applications)[0][0]
self.assertEqual(
(entry["canonical_domain"], entry["canonical_url"]),
("element.example.com", "https://element.example.com"),
)
def test_dashboard_card_domain_follows_the_onion_url(self):
applications = self._card_target_role("dashboard", card="element")
entry = self._run(
["dashboard"], applications, url_hosts={"web-app-chat": f"element.{ONION}"}
)[0][0]
self.assertEqual(
(entry["canonical_domain"], entry["canonical_url"]),
(f"element.{ONION}", f"https://element.{ONION}"),
)
def test_dashboard_without_card_key_targets_the_primary_domain(self):
applications = self._card_target_role("dashboard")
entry = self._run(["dashboard"], applications)[0][0]
self.assertEqual(
(entry["canonical_domain"], entry["canonical_url"]),
("matrix.example.com", "https://matrix.example.com"),
)
def test_dashboard_unknown_card_key_fails_loudly(self):
applications = self._card_target_role("dashboard", card="nope")
with self.assertRaisesRegex(AnsibleError, "nope"):
self._run(["dashboard"], applications)
def test_other_services_ignore_a_card_key(self):
applications = self._card_target_role("prometheus", card="element")
entry = self._run(["prometheus"], applications)[0][0]
self.assertEqual(
(entry["canonical_domain"], entry["canonical_url"]),
("matrix.example.com", "https://matrix.example.com"),
)
if __name__ == "__main__":
unittest.main()