mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
feat(lookup): container_hostname — kernel-safe container hostname
`lookup('container_hostname', app)` returns the app's canonical primary
domain when it fits sethostname(2)'s 63-byte cap, else the short entity
name. Onion subdomains (sub.<56-char>.onion) overflow that limit and make
`docker run` fail with "sethostname: invalid argument", killing container
init on onion nodes. Replaces the seven roles that hostname'd their
container on the raw domain (bookwyrm, bridgy-fed, confluence, jira,
postmarks, pretix, xwiki) with the encapsulated lookup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
10a11fc570
commit
c8a4320478
9 files changed
+133
-7
No files matched your search
@@ -0,0 +1,63 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
from utils.cache.domains import get_merged_domains
|
||||
from utils.domains.primary_domain import get_domain
|
||||
from utils.roles.entity_name import get_entity_name
|
||||
|
||||
# sethostname(2) caps a hostname at 64 bytes; onion subdomains
|
||||
# (sub.<56-char-onion>.onion) regularly exceed that and make the container
|
||||
# fail to start with "sethostname: invalid argument".
|
||||
_HOST_NAME_MAX = 63
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
"""
|
||||
Usage:
|
||||
{{ lookup('container_hostname', application_id) }}
|
||||
|
||||
The Docker/OCI hostname for an application's container: its canonical
|
||||
primary domain when that fits the kernel's 63-byte limit, otherwise the
|
||||
short entity name (e.g. ``xwiki``). Centralises the domain-or-entity
|
||||
choice so onion deployments (long ``.onion`` subdomains that overflow
|
||||
sethostname(2)) never break container init.
|
||||
"""
|
||||
|
||||
def run(self, terms, variables: dict[str, Any] | None = None, **kwargs):
|
||||
if not terms or len(terms) != 1:
|
||||
raise AnsibleError(
|
||||
"lookup('container_hostname', application_id) expects exactly 1 term"
|
||||
)
|
||||
|
||||
application_id = terms[0]
|
||||
if not isinstance(application_id, str) or not application_id.strip():
|
||||
raise AnsibleError(
|
||||
"lookup('container_hostname'): application_id must be a "
|
||||
f"non-empty string, got {application_id!r}"
|
||||
)
|
||||
application_id = application_id.strip()
|
||||
|
||||
variables = variables or getattr(self._templar, "available_variables", {}) or {}
|
||||
|
||||
domains = get_merged_domains(
|
||||
variables=variables,
|
||||
roles_dir=kwargs.get("roles_dir"),
|
||||
templar=getattr(self, "_templar", None),
|
||||
)
|
||||
|
||||
try:
|
||||
domain = get_domain(domains, application_id)
|
||||
except Exception as e:
|
||||
raise AnsibleError(
|
||||
f"lookup('container_hostname'): failed to resolve domain for "
|
||||
f"'{application_id}': {e}"
|
||||
) from e
|
||||
|
||||
name = str(domain or "")
|
||||
if 0 < len(name) <= _HOST_NAME_MAX:
|
||||
return [name]
|
||||
return [get_entity_name(application_id)]
|
||||
@@ -5,7 +5,7 @@ entity_name: "{{ application_id | get_entity_name }}"
|
||||
|
||||
# Container
|
||||
container_port: 8000
|
||||
container_hostname: "{{ lookup('domain',application_id) }}"
|
||||
container_hostname: "{{ lookup('container_hostname', application_id) }}"
|
||||
|
||||
# BookWyrm
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ application_id: "web-app-bridgy-fed"
|
||||
container_port: 8080
|
||||
domain: "{{ container_hostname }}"
|
||||
http_port: "{{ lookup('config', application_id, 'services.bridgy-fed.ports.local.http') }}"
|
||||
container_hostname: "{{ lookup('domain',application_id) }}"
|
||||
container_hostname: "{{ lookup('container_hostname', application_id) }}"
|
||||
|
||||
# App container
|
||||
BRIDGY_CONTAINER: "{{ lookup('config', application_id, 'services.application.name') }}"
|
||||
|
||||
@@ -5,7 +5,7 @@ entity_name: "{{ application_id | get_entity_name }}"
|
||||
|
||||
# Container
|
||||
container_port: 8090
|
||||
container_hostname: "{{ lookup('domain',application_id) }}"
|
||||
container_hostname: "{{ lookup('container_hostname', application_id) }}"
|
||||
|
||||
# Confluence
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ entity_name: "{{ application_id | get_entity_name }}"
|
||||
|
||||
# Container
|
||||
container_port: 8080 # Standardport Jira
|
||||
container_hostname: "{{ lookup('domain',application_id) }}"
|
||||
container_hostname: "{{ lookup('container_hostname', application_id) }}"
|
||||
|
||||
# Jira
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ application_id: "web-app-postmarks"
|
||||
container_port: 3000
|
||||
domain: "{{ container_hostname }}"
|
||||
http_port: "{{ lookup('config', application_id, 'services.postmarks.ports.local.http') }}"
|
||||
container_hostname: "{{ lookup('domain', application_id) }}"
|
||||
container_hostname: "{{ lookup('container_hostname', application_id) }}"
|
||||
|
||||
# App container
|
||||
POSTMARKS_CONTAINER: "{{ lookup('config', application_id, 'services.application.name') }}"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# General
|
||||
application_id: "web-app-pretix"
|
||||
container_port: 80
|
||||
container_hostname: "{{ lookup('domain',application_id) }}"
|
||||
container_hostname: "{{ lookup('container_hostname', application_id) }}"
|
||||
|
||||
# Pretix
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ application_id: "web-app-xwiki"
|
||||
domain: "{{ lookup('domain',application_id) }}"
|
||||
|
||||
container_port: 8080
|
||||
container_hostname: "{{ domain }}"
|
||||
container_hostname: "{{ lookup('container_hostname', application_id) }}"
|
||||
|
||||
# XWiki
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
from plugins.lookup.container_hostname import LookupModule
|
||||
|
||||
|
||||
class TestContainerHostnameLookup(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.lookup = LookupModule()
|
||||
|
||||
def _domains_from_vars(*, variables=None, **_kwargs):
|
||||
return (variables or {}).get("domains", {})
|
||||
|
||||
self._patchers = [
|
||||
patch(
|
||||
"plugins.lookup.container_hostname.get_merged_domains",
|
||||
side_effect=_domains_from_vars,
|
||||
),
|
||||
patch(
|
||||
"plugins.lookup.container_hostname.get_domain",
|
||||
side_effect=lambda domains, app: domains[app],
|
||||
),
|
||||
patch(
|
||||
"plugins.lookup.container_hostname.get_entity_name",
|
||||
side_effect=lambda app: app.rsplit("-", 1)[-1],
|
||||
),
|
||||
]
|
||||
for p in self._patchers:
|
||||
p.start()
|
||||
self.addCleanup(lambda: [p.stop() for p in self._patchers])
|
||||
|
||||
def _run(self, app, domain):
|
||||
return self.lookup.run([app], variables={"domains": {app: domain}})[0]
|
||||
|
||||
def test_short_domain_is_kept(self):
|
||||
self.assertEqual(
|
||||
self._run("web-app-xwiki", "wiki.example.org"), "wiki.example.org"
|
||||
)
|
||||
|
||||
def test_long_onion_domain_falls_back_to_entity(self):
|
||||
onion = "x.wiki." + "a" * 56 + ".onion"
|
||||
self.assertEqual(self._run("web-app-xwiki", onion), "xwiki")
|
||||
|
||||
def test_63_char_domain_is_kept(self):
|
||||
name = "a" * 63
|
||||
self.assertEqual(self._run("web-app-xwiki", name), name)
|
||||
|
||||
def test_empty_domain_falls_back(self):
|
||||
self.assertEqual(self._run("web-app-xwiki", ""), "xwiki")
|
||||
|
||||
def test_bad_arity_raises(self):
|
||||
with self.assertRaises(AnsibleError):
|
||||
self.lookup.run([], variables={})
|
||||
|
||||
def test_empty_app_raises(self):
|
||||
with self.assertRaises(AnsibleError):
|
||||
self.lookup.run([" "], variables={})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user