mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
fix/dev-python/python-upper-bound
11
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fdaa31d6cb |
feat(svc-net-tor): carry mail to a .onion in both deploy modes
Sending to a .onion needs two things of the node: a resolver that answers the suffix, and a route from the container to Tor's transparent port. Neither existed. Compose pointed Mailu at unbound, which knows nothing about .onion, and in swarm Tor's DNSPort and TransPort listen inside the container where the host's dnsmasq and nat rules cannot reach them, so the forwarding rules that were already there pointed at a closed port on every swarm node. Compose resolves through unbound, which now forwards the onion zone to Tor's DNSPort on the bridge it already owns. The forward alone would have answered SERVFAIL: the signed root proves that onion does not exist, so with auto-trust-anchor-file set every answer below it validates as bogus, and Tor's DNSPort can supply no DS. The zone is therefore declared insecure. Swarm resolves through the node dnsmasq instead, which every container already uses because container_dns puts the bridge resolver into daemon.json wherever svc-net-tor is deployed, and which now reaches Tor because its ports are published host-mode next to the SOCKS port that was published there already. The transport rule changed target rather than mechanism. REDIRECT rewrites the destination to the primary address of the interface the packet arrived on, which is a different project bridge for every role, so Tor would have had to listen on all of them. A DNAT naming the bridge keeps one listener, and in swarm the same address reaches the published port through the proxy docker runs there. That leaves the exposure. SocksPort has SocksPolicy; DNSPort and TransPort have no equivalent, and a swarm-published port carries no host address to bind to, so in swarm the packet filter is their only boundary. Compose needs none, because there Tor binds loopback and the bridge and nothing else. The swarm guard admits this deployment's own address space and drops the rest, and it is installed as a unit ordered after docker rather than as ansible-applied rules: nothing here persists netfilter state, so docker recreating its chains at boot would have left Tor back on the wildcard with the guard gone. It rebuilds its chain instead of appending to it, because the appending version could only ever be loosened. It hooks INPUT and DOCKER-USER both, since host-local traffic reaches a published port through docker's proxy and external traffic through FORWARD. The deploy now proves all of it back and fails when a rule is missing, the router no longer runs on swarm nodes that carry no Tor, and the CLI test probes the forwarded ports over SOCKS instead of assuming a HiddenServicePort line means a reachable service. Measured so far: Tor accepts the non-loopback binds and reports circuit-established, and the chain, its order and the DOCKER-USER hooks read back correctly from a deployed host. The swarm path is not yet exercised. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b036f60039 |
refactor(container_ports): express the remaining published ports through the lookup
The list form could publish a host:container pair and nothing else, so thirteen blocks stayed hand-written: the ones needing a protocol suffix, a port range, the swarm long syntax with mode host, or a container-port-only publish. Each of those duplicated numbers the role already declares in meta/services.yml, which is the drift the lookup exists to remove.
The lookup keeps the list form unchanged for its 93 existing call sites and gains a dict form: transport emits one item per protocol, container 'same' serves the services that cannot remap (STUN/TURN, media relays), mode 'host' emits the long block an edge role needs in swarm so the client address survives the hop, publish 'ephemeral' emits the container port alone, and key names the block for Discourse's launcher file, whose expose: list is what becomes docker run -p there. A declared {start, end} mapping renders as start-end on both sides, so a relay range needs no term of its own.
svc-prx-openresty had no ports block at all and bound 80 and 443 as literals in its template, while torrc.j2 carried a third literal 80 for the hidden service. None of the three referenced the others. openresty now declares the ports, the compose template reads them, and the onion's port 80 comes from ports.onion.http through TOR_ONION_EXTRA_PORTS.
Three blocks stay raw, each with the reason it cannot route through one call: keycloak's sso_proxy maps a published port owned by the consuming role to a container port owned by web-app-keycloak, and matrix's ansible flavor takes element on 8080 and federation on 8048 from the upstream playbook rather than from this repo's ports.internal.
The enforcing lint scanned only files with compose in the name and only double-quoted list items, so nine of the thirteen blocks were invisible to it. It now reads every role template that can carry a ports block, catches unquoted items, protocol suffixes, ranges, flow style and the long syntax, and tracks the block by the indentation of its key: keying on the next line closed the block on anything shaped like a key, which target, published, protocol and mode all are. Under expose: only a real mapping counts, because there the single-segment form documents a container port and publishes nothing.
Verified by rendering all 110 container_ports call sites through the real templar, the real lookup and the real merged applications view.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
28dd5462d7 |
fix(svc-net-tor): ask the running Tor for circuits instead of re-reading its config
The healthcheck ran `tor --verify-config`, which parses the torrc and never speaks to the daemon. Ansible renders that file from a template, so it is valid by construction and the probe could not fail once it existed: bootstrap progress, circuit establishment and descriptor publication were all invisible, and `retries` was a dead parameter. Swarm therefore reported the service healthy while Tor was still coming up, and the readiness that actually gates deploys had to be rebuilt beside it, in onion_self_reach.py and playwright's wait_onion_reachable.sh. The torrc already opens a cookie-authenticated control port, so the probe now authenticates there and asks GETINFO status/circuit-established. That is the question the container is supposed to answer. It needs no package the image lacks: bash carries /dev/tcp and od hex-encodes the cookie, both present in debian:trixie-slim. Measured against real Tor, the public flavor reports healthy 6.1s after start and chutney 54.6s, each confirmed by "Bootstrapped 100% (done)" in the container log. Chutney is the slower of the two because its entrypoint brings up a whole private network and waits for consensus before the node tor starts; its localhost advantage only pays off afterwards, at publish and rendezvous. Both fit ten minutes with margin to spare, so the flavor-dependent start window goes and every timing is now the platform default. Three nocheck markers go with it, including an interval of 60s whose own reason said the probe was "worth running rarely" while setting twice the default frequency. Two paths and one condition that had grown copies are pinned in vars/main.yml. The Dockerfile takes the script locations as build args rather than repeating them, reaching the entrypoint through a fixed symlink because ENTRYPOINT in exec form would not expand the arg and would silently keep pointing at the old path. One behaviour change worth stating plainly: a Tor that never establishes a circuit is now reported unhealthy after the start window, where before it was reported healthy forever. That is the point of the change, and it moves such a failure from the later Playwright run to the service itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
96408f23a0 |
perf(healthcheck): let the start window outlast a real boot
start_interval is the cadence while start_period runs, and only while it runs. Once the window closes the probe falls back to interval, so a service that becomes ready just after it pays a whole tick anyway and the cadence never reached it. Six of the seven flavors were shaped that way, Curl worst: a thirty second window against a one minute interval. Measured across 151 converge waits in four job logs, 63 sit exactly on a tick boundary, 36 in the thirty second step and 27 in the sixty second one. Attributed by application, web-app-seaweedfs is the largest at 1501s over sixteen waits with a median of 57.7s: it probes with curl and fell straight into the gap. web-app-zammad probes with connect, whose window is fifteen minutes, and its 154s median is real boot time with nothing to recover. So the window moves to ten minutes and the steady state interval to two, and the flavor overrides that sat below them go: Curl, Wget and Connect lose interval 1m, Http its twenty second window, Nc its ten. Connect keeps fifteen minutes. Neither number costs anything while a container works. start_period delays nothing, it only stops failures counting, and interval no longer touches convergence at all since that hangs on the window alone; it is steady state load, which drops from 120 probes an hour per service to 30. Forty six role files follow. Twenty seven declarations go that repeat the new default or undercut it, an undercut window being the defect itself. Eight windows longer than ten minutes keep their value and now say why. The literal blocks and the three Dockerfiles this repository builds spell every timing out, because outside the lookup an omission takes docker's value and not the platform's. A fourth lint rule keeps the shape: start_period must outlast one interval, checked against the effective values, so raising interval without the window cannot quietly bring the cliff back. Two faults in the lint surfaced while building it and are fixed here. It compared timings as strings, so 600s and 10m read as different values, and it matched the healthcheck key exactly, so a line carrying a trailing nocheck was skipped whole, which is how the two swarm-init partials went unexamined. One regression, bounded. A container that starts and never turns healthy is neither rejected nor errored, so stack_ready.sh only calls it fatal after three killed tasks, and each cycle now spans the longer window. That reaches twenty minutes rather than six, capped there by swarm_stack_converge_retries at 600 with a two second delay, and it lands only on runs that were failing anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
16be22007b |
fix(healthcheck): spell the timings out where the SPOT cannot reach
A healthcheck written outside the container_healthcheck lookup inherits nothing from utils/docker/healthcheck, and docker offers nowhere to put a replacement. daemon.json carries storage, logging, DNS, registries and runtimes, and no health key at all, so an omitted timing lands on docker's own value rather than the platform's. The two disagree where it matters most: docker starts a container with no start period, which leaves start_interval no window to act in. That is why the hand-written blocks never gained the cadence. The three images this repository builds itself now carry it. openldap and postgres also drop to the platform interval, and bluesky gains the start window it never had: svc-db-openldap/files/Dockerfile --interval=30s ... --start-interval=5s svc-db-postgres/templates/Dockerfile.j2 --interval=30s ... --start-interval=5s web-app-bluesky/.../Dockerfile --start-period=30s --start-interval=5s Six literal blocks move to the platform interval as well: magento, redis, elasticsearch, matrix and both sys-svc-rdbms partials. Since start_interval exists, interval no longer touches convergence and is steady state load alone. Sixteen values that stay where they are now name the mechanism that keeps them there. Two are worth reading. web-app-kix overrides start_period and nothing else on purpose, letting the image's own HEALTHCHECK supply test, interval, timeout and retries; filling those in would have overwritten what upstream tuned. The memcached and redis swarm-init partials probe every second and deliberately run with no start window, which is why start_interval is absent rather than forgotten. The rest name their probe: occ boots the whole Nextcloud stack, tor re-parses its torrc instead of asking a running service, opensearch is still forming a cluster, matrix waits on a file another step writes to a share. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d0a8608bfa |
perf(healthcheck): give every probe the start cadence 42ebd9ea71 added
|
||
|
|
acc9e25c0f |
refactor(volumes): one canonical mount surface, context-wired lookups
The mount pipeline was split across two layers: compose_volumes and
container_volumes lived as filter modules that ansible's filter loader
rejected on every start ("has no attribute 'FilterModule'", two warnings
per run), while the same-named lookups wrapped them thinly. The filter
bodies now live inside their lookups (the tor_socks precedent), the
filter files are gone, and the render tests moved next to the lookup
tests.
As lookups they see the templating context, so the application_id term
every call site passed back to its own role is gone too: both lookups
read application_id from context and fail loudly when it is missing.
107 templates and nfs_prep.yml drop the term; the calls collapse to
lookup('compose_volumes') and lookup('container_volumes', service_name).
The container_volumes call is now canonical - four leading spaces,
service_name in the service position, | indent(4) - enforced by a new
lint (test_container_volumes_call_shape). Normalizing the 85 literal
call sites surfaced that erpnext keyed its meta/volumes.yml mounts by
container name instead of compose service key, the only role to do so;
its meta now uses the service keys. wordpress's column-0 call, which
rendered its first line at top level, is indented like every other.
Template logic that only restated meta/volumes.yml is deleted: mailu fed
compose_volumes an extra_volumes dict whose eight names were looked up
from the very meta file compose_volumes reads itself; zammad's init and
railsserver extra mounts were the volumes.yml entries minus the two
services, which the meta now lists (the when-gated OIDC binds included).
Conditionality that meta cannot express stays in code, but in role-local
lookup plugins instead of template loops: matrix_bridge_mounts renders
the per-bridge registration mounts, nextcloud_extra_volumes the
feature-gated whiteboard/recording volumes (type: volume entries are
declared unconditionally by compose_volumes, so gating must happen
before it).
Parity is proven, not assumed: every mechanically rewritten template is
byte-identical to HEAD after the transformation regexes (95/95), and the
semantic roles render identically through the real functions against
HEAD's meta - zammad across six sso/tls polarities, all seven erpnext
services, mailu's top level. make test-lint passes complete.
Also: suitecrm's compose template pins container_extra_hosts with
host_alias (the SOCKS proxy URL host.docker.internal never resolved in
compose+tor), and the five disabled mautrix bridge addons use the
empty-default config lookup so templating the applications dict stops
emitting 125 errors per batch in every CI job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
29015311b6 |
feat(lint): derive container paths from meta/volumes.yml everywhere
meta/volumes.yml says where a volume lands inside its container. Wherever a
second file spelled the same path out, the two drifted independently: the mount
moves, the consumer keeps pointing at the old place, and nothing fails until the
process looks for content that is not there.
|
||
|
|
5399b48482 |
feat(svc-net-tor): add a chutney flavor for private-net test deployments
The test variant (meta/variants.yml, variant 0) switches the tor service to the chutney flavor: the image ships chutney and the entrypoint boots a private Tor network inside the container (all nodes on 127.0.0.1, so the design is identical under compose host-networking and swarm). The entrypoint bootstraps from /opt/chutney (chutney creates its net/ state CWD-relative), extracts the DirAuthority/ TestingTorNetwork lines into /run/tor/chutney-net.conf and fails fast when that file ends up empty, since the node would otherwise silently join the public network. torrc %includes the conf only for the chutney flavor. Descriptor publish and rendezvous now run at localhost speed: per-app onion e2e suites dropped from minutes to seconds in the compose validation. The test variant also raises the footprint to 2g/1.0cpu/512pids (measured: at 1g/0.25 the ~9 extra tor nodes pin the caps and desc publishing stalls); meta/services.yml stays static on the public flavor and the old production footprint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d88f272f9f |
Merge branch 'feature/swarm-nfs' into feature/svc-net-tor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e6538e4f61 |
feat(svc-net-tor): Tor onion-service provider (full onion node)
Turn a NAT/edge node into a full Tor v3 onion node: DOMAIN_PRIMARY becomes the node's .onion, so the whole stack (web, SSO, LDAP, CA) resolves onion domains and is reachable over Tor without a public IP, port forwarding, VPS or DynDNS. - utils/tor_onion.py: offline v3 key mint (address + HiddenServiceDir key files), verified against a known key->address test vector. - cli .../inventory/onion: `init-env` pre-mints the node onion, writes INFINITO_DOMAIN(+_RE) into .env and authoritative key files into .onion-identity/hs (idempotent). The onion is a deploy-time input; no in-deploy random minting (that would not match the pre-set domain). - roles/svc-net-tor: Tor sidecar (Debian-built, network_mode: host), installs the minted keys, torrc with one node HiddenService + a SOCKS proxy restricted to loopback/DiD ranges. files/test.sh probes every served onion domain over the Tor SOCKS. - tls: .onion -> TLS off (onion v3 crypto is the transport auth; LE cannot sign .onion) + app-level flavor override. - harness/DNS: coredns resolves the onion zone via INFINITO_DOMAIN; skip the clearnet DNS sanity check for onion; raise nginx server_names_hash_bucket_size to 128 for long onion server_names. - test-e2e-playwright: gated browser SOCKS proxy (PLAYWRIGHT_PROXY) + navigation/action timeouts so specs reach .onion over Tor and fail fast; the onion node points it at the Tor SOCKS. - docs/requirements/031: full design. Verified: dashboard + SSO (Keycloak) + 9 apps reachable over Tor (HTTP 200 through the node hidden service). make test green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |