mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
fix/dev-python/python-upper-bound
87
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2d0016d27b |
fix(svc-net-tor): let a port probe attempt outlast Tor's SocksTimeout
The onion port probe gave every SOCKS connect 60s, half of Tor's
SocksTimeout (120s by default; torrc does not set it), although its own
docstring argued that abandoning an attempt before SocksTimeout is
destructive. Every attempt authenticates with fresh credentials, so Tor
builds a whole new rendezvous for it and keeps working on it until
SocksTimeout. The probe walked away first and called the port dead. The
other raw onion connects in the repository, the svc-db-* CLI tests and
test-e2e-playwright's wait_onion_reachable.sh, wait 120s per attempt.
Run 40128751852 shows how often an attempt needs more than that. Of 178
svc-net-tor CLI runs in its 89 compose onion jobs, three lost their
first attempt to the 60s timeout and connected on the second within
seconds (Shopware#1, Hugo#1, n8n#1; the CLI test took 77-79s against
the usual 1-16s), and Hugo#1's other run took 40s on a single attempt. When
the stall outlasts the 70s until the second attempt, the probe fails
while the HTTP probe reaches the same onion port seconds earlier:
OpenBao#1 (run 39166850273, job 118050491920, the original failure of
#1048) and Socialhome#1 (run 40088473634, job 121019758461). In the
Socialhome job tor.log holds no line for either attempt, because Tor
never reached its own verdict.
ONION_PORT_TIMEOUT is now 130: SocksTimeout plus room for Tor's
once-per-second expiry sweep, so an attempt ends with Tor's own answer
(reply 0x06, and a "Giving up" notice in tor.log, which the rescue
artifact captures) instead of the probe's. The second attempt now starts
at 140s and ends at 270s, so the probe rides out every stall the old
budget did, and any stall shorter than about 260s instead of about 120s.
A green run still costs one connect per port, and a refused or unmapped
port still fails at once. A unit test pins the window above Tor's
SocksTimeout and fails as soon as torrc sets its own.
That doubles what a dead port costs, and meta/tests.yml caps the whole
CLI test. The largest compose onion deploy in run 40128751852 probes 19
domains and 5 ports; with everything dead that is 19 x 350 + 5 x 260 +
10 = 7960s, beyond the 7200s that
|
||
|
|
3f8e3e3023 | feat(svc-net-tor): let a deployment put its hosts into meshes | ||
|
|
4f8c1f00f1 |
fix(svc-net-tor): fail the worker check when no torrc can be read
The check listed the torrcs without the limit through "grep -L ...; true". With no torrc under /opt/chutney/net/nodes, or with one that cannot be read, grep printed nothing, the trailing true turned its error into success, and the test reported that every node runs one worker. The check now counts the limit per file with "grep -cH" and reads the exit status. A status above 1 or an empty answer fails the test, and a file with a count of 0 is named as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
7968b47cf0 |
fix(svc-net-tor): give every chutney node one worker thread
Tor starts its worker threads by the number of CPU cores it detects, and the cpus limit of the container does not change that number. On a host with 20 cores every node of the private chutney network ran 21 threads and grew to about 0.6 GB. The seven relays and authorities filled the 2g limit and its swap within ten minutes, the kernel killed them one by one, and no circuit could be built any more. The entrypoint now writes NumCPUs 1 into the torrc of every chutney node before it starts the network. Measured on that host over ten minutes: the nodes run 3 threads and stay at 65 to 86 MB, the container at 0.47 GB without swap, where it had filled 2 GB plus 2 GB of swap before. The CLI test of the role fails when a chutney node runs without the limit. Closes #824 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
290d9e0d97 |
fix(svc-net-tor): hand every forwarded onion port to the port probe
test-e2e-cli loads test.env with `set -a && . test.env && set +a`. The unquoted `ONION_PORTS=25 80 110 ...` is not an assignment there but the command `80` with a one-word env prefix: bash prints `command not found`, leaves ONION_PORTS unset, and onion_ports.py reports "no onion-forwarded ports declared; nothing to probe". The port check passed vacuously in every tor job with more than one forwarded port (OpenResty, Listmonk, Pixelfed in run 35661520542) and only ran where a single port was declared. Pass the value through dotenv_quote, which quotes a space-separated value in both compose and swarm and keeps the env-file lint satisfied. A unit test renders the template in both modes and sources it the way test-e2e-cli does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
fc3c27246b |
fix(svc-net-tor): give every onion port probe its own circuit
Tor attaches a stream to an onion to the rendezvous circuit that matches its isolation and keeps it there until SocksTimeout; it never moves the stream to a fresh circuit. The probe connected without SOCKS authentication and gave up after 60s, so a dead rendezvous circuit would absorb both passes. That is the most likely reading of run 35661520542 (web-app-mastodon#1, public Tor), where <onion>:80 timed out twice right after curl had reached the same onion port. Authenticate every connect with fresh random username/password credentials. Tor accepts any and, under its default IsolateSOCKSAuth, builds a separate rendezvous circuit per credential set, so a retry can no longer land on the circuit that failed the attempt before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bc9a9344e9 | update(images): update Docker image versions (svc-net-tor) | ||
|
|
da665ab699 |
feat(services): give fifteen moving pins a name the registry can order
A pin like 'alpine', 'stable', 'lts' or 'buildx-stable-1' names no version, so nothing can compare it to anything: the bump job skips it and the pin drifts with whatever upstream last pushed under that name. Each of these fifteen now names its release, which is the precondition for the freshness pipeline to see it at all. Every conversion was placed against the tag it replaces, and none of them is a downgrade. Six resolve to a byte-identical digest and are pure relabelling (memcached, redis, the two debian bases, jenkins, xwiki); four are genuinely newer than what the moving tag carried, so those roles will deploy a later version than before (nginx moves past the 1.31.5 behind 'alpine', buildkit past the v0.32.2 behind 'buildx-stable-1', dockerize past a 'latest' older than v0.10.0); openresty and pretix keep their version and differ only by build. fider is the one that loses something: 'stable' carries a rebuild pushed twelve days after v0.36.0, which is still the newest versioned tag upstream publishes. An unnamed rebuild cannot be tracked, so the pin takes the named release and the updater can raise it when v0.37.0 lands. The flavor invariant keeps the families apart: jenkins stays on '-lts' and xwiki inside '-postgres-tomcat', so no bump can cross into a different runtime. svc-runner loses a 'nocheck: docker-version' that has become dead. The marker suppresses two checks at once, so while it stood buildkit was held out of the daily freshness warning as well; removing it puts the role back in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
08b4d0f2ff |
fix(svc-net-tor): render the onion forward flags before reading them
|
||
|
|
2f533eb279 |
refactor(svc-net-tor): declare the role's config in the role, and the onion's HTTP forward at all
group_vars/all/README.md forbids role-specific configuration in that directory, and 19_tor.yml held five variables no role but svc-net-tor ever read. They now live where the README points: the values an operator may override in meta/services.yml, where the inventory's applications block is deep-merged on top, and the derived ones in vars/main.yml.
The exception is the egress virtual range, which moves the other way into 08_networks.yml. It is not a property of the role but a claim on shared address space, and
|
||
|
|
e74a585402 |
fix(guide): ask the reader for the clearnet resolver a Tor node needs
svc-net-tor aborts at roles/svc-net-tor/tasks/router.yml:2 when networks.internet.dns is empty, and rightly so: dnsmasq runs with no-resolv, so an empty upstream leaves the node answering .onion and nothing else. That value is set in exactly two places, both under inventories/development/, and the documented production path reaches neither - the provisioner writes no networks key at all. A reader following the role README verbatim therefore cannot deploy a Tor node. CI has been proving this since 2026-08-31 without anyone seeing it: the README replay rides on the role's smallest variant and is skipped when the deploy is red, so the only row that ever reached it is one where svc-net-tor itself went green. That happened first on Ubuntu, where the local-service defect |
||
|
|
b12ff8d0c4 |
fix(svc-net-tor): tie the dnsmasq reload to the branch that writes its drop-in
The systemd drop-in is written inside a block gated on TOR_DNSMASQ_OWNS_LISTENER, which turns false as soon as a foreign file sits in /etc/dnsmasq.d. The start task read that block's register unconditionally, so the expression's correctness depended on ansible defining a register for a task it skipped. Short-circuiting on the same condition means the register is never reached when the block did not run, and the question stops mattering. The test asserted the literal declaration string, which exercises nothing. It now renders the expression read from the role with ansible's own bool filter and a changed test, in both states: owning the listener with a rewritten drop-in yields True, not owning it yields False without touching the register. The previous form raises UndefinedError under the second case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b5807e66ce |
fix(svc-net-tor): guard the node's resolver against everything it routes
dnsmasq binds the docker bridge address with the clearnet upstreams configured and the node's own .onion records loaded, and no rule covered port 53. Any source that could route a packet to that address got an open resolver plus the split-DNS records. |
||
|
|
abb6f37297 |
fix(svc-net-tor): drop the dnsmasq keyword the RedHat family already sets
EL10 and Fedora ship an active local-service=host in /etc/dnsmasq.conf. The drop-in repeated the keyword, and dnsmasq refuses a repeated ARG_ONE option: 'illegal repeated keyword at line N of /etc/dnsmasq.d//tor-onion.conf', then 'FAILED to start up'. Eight CI rows died at roles/svc-net-tor/tasks/router.yml:67, all of them on centos or fedora; arch and ubuntu start dnsmasq fine because their stock config leaves the keyword commented. The line never took effect on any distribution. dnsmasq resets OPT_LOCAL_SERVICE as soon as an access-control option is present, and listen-address fills exactly that list; the template emits listen-address=127.0.0.1 unconditionally in the same block. A query from a source outside every directly attached subnet is answered identically with and without the line. Port 53 therefore carries no source restriction. That is unchanged here and belongs in the nftables guard, which currently covers only the trans, DNS and SOCKS ports of tor itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
aa6bbe3b20 |
feat(lint): render the software identity from its declared variables
SOFTWARE_NAME, SOFTWARE_DOMAIN, SOFTWARE_URL and ORGANIZATION are declared once in group_vars/all/00_general.yml and derive from each other. Eleven templates wrote the value instead, so a rename would reach the four declarations and every consumer that reads them, and silently miss the literal. The lint reads the name from the declaration rather than carrying it, and searches case-insensitively: the domain is the name lowered, the URL is https:// plus the domain and the organization is the name again, so one literal finds the value of all four. A hit is reported with the variable it should have been, derived from the scheme in front of it and its case. The escape is nocheck: software-identity, same line or the one above. Every migrated template renders the same bytes as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
06cc485995 |
feat(svc-net-tor): reject a forged source on the guarded ports
The guard trusted an address. rp_filter ships at all=0 and default=2 on this host and the repository sets it nowhere, so loose mode accepts a packet whose source is routable via any interface at all. A datagram claiming a docker pool address and arriving on the uplink passed both the kernel check and the admitted set, which for udp needs one forged packet and no handshake. A strict reverse-path check on the guarded ports closes it. Measured in a namespace rig: a forged source routable via another interface reaches the input hook, because loose rp_filter lets it through, and the new rule counts it; a source routable nowhere never gets that far, the kernel drops it first. A loopback datagram to the same port does not match, so the host's own .onion resolution through 127.0.0.1 to the DNSPort is untouched. Scoped to the four guarded ports rather than the whole chain. Strict reverse-path filtering on everything traversing input and forward breaks asymmetric routing, and this role runs on hosts that terminate wireguard tunnels. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c89ca42c46 |
fix(svc-net-tor): bound the two ports that answered everyone
An audit of what the host actually filters found a single input chain in the whole ruleset, covering three tor ports. ss showed two more binds with nothing in front of them: the SOCKS port on 0.0.0.0, and dnsmasq on the bridge address. Each gets the layer it belongs in. SOCKS gets a packet filter rule against the same CIDRs SocksPolicy already accepts, so no client loses access while the filter closes if tor's own config ever stops enforcing. Reusing the narrower guard set was deliberately avoided: it excludes the LAN blocks SocksPolicy admits, so it would have changed the policy rather than hardened it. dnsmasq gets local-service, which answers only directly connected networks. A packet filter rule there would have sat on the container DNS path, which daemon.json points at that same bridge address, in exchange for closing an exposure that needs an extra route onto the bridge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bf32f57b85 |
feat(svc-net-firewall): own the packet filter as one nftables table per role
svc-net-tor rendered its own guard script, its own unit and its own handler, and hooked the rules into DOCKER-USER with an insert, because a rule appended to a chain docker owns lands behind the RETURN docker puts there. The script flushed its chain and refilled it rule by rule, so the boundary was gone for the duration of the refill. A role that needs packet filter rules now writes templates/nftables.conf.j2 declaring its own table and includes svc-net-firewall, which renders it to /etc/nftables.d/<role>.conf, rejects it with nft -c, loads it, reads the table back and refuses one that carries no chain. Deployed on its own the role installs nftables and the ruleset that loads every fragment at boot. That ruleset is a distribution decision, so its path follows os_family: RedHat reads /etc/sysconfig/nftables.conf, the others /etc/nftables.conf. The unit is only enabled, never started, because on Debian its ExecStop flushes the whole ruleset, docker's tables included. One table per role removes the shared path: a fragment replaces its own table and touches no other, and docker never touches either. nft -f on an existing table merges, so each fragment creates, deletes and redeclares its table in one transaction. The tests load every fragment twice into a throwaway network namespace and require the listing to match, because a merging fragment loads twice without error and shows up only as a duplicated rule. The tor guard now applies in both deploy modes. It was pinned to swarm while torrc binds TransPort and DNSPort on the bridge address in compose too, so on a single-host install those ports answered every on-link neighbour. The guard drops rather than admits, so nothing short-circuits another table, and ip saddr carries an implicit ipv4 dependency, which leaves the ipv6 exposure exactly where the iptables rules left it. svc-net-tor reads its own chain back and refuses a guard that drops nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
dd05caa34f |
feat(services): require an explicit onion decision per host-bound port
A port under ports.local or ports.public binds an address on the stack host, which is what a HiddenServicePort can forward to. Whether it should be forwarded has a cost in both directions: silently off leaves a service unreachable that the operator believes is on the onion, and silently on publishes a database, an admin interface or a SOCKS proxy to everyone holding the onion address. Neither belongs in a default, so the flag is now mandatory and every false carries the reason it is false. The first run produced 165 findings, of which 131 were one architectural fact repeated rather than 131 decisions. Four groups are therefore settled once in the lint instead: the proxy-fronted http, sso and websocket, because the node onion already forwards port 80 to the reverse proxy that routes every vhost, oauth2 hop and socket upgrade behind it; the implicit-TLS variants, because Tor is already the transport encryption and container_ports drops those listeners when TLS is off, which an onion deployment always is; the categories a hidden service cannot carry, listed once in plugins.lookup.tor_ports.UDP_ONLY_CATEGORIES, which the lint imports rather than restates; and any entity that already answers the question through exposed:. That leaves 23 real per-port decisions, each with a reason naming its own consumer. svc-net-tor keeps its own socks, trans and dns off the onion: a published SOCKS port is an open proxy, and SocksPolicy cannot refuse it because the forward arrives from the node's own address. Mailu turns smtp, submission, imap and pop3 on, which is what makes sending and receiving work over the onion, and leaves sieve off because ManageSieve manages filters rather than mail. Measured end to end through the merged applications view, across all 105 roles the deploy now forwards exactly 25, 110, 143 and 587. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
691911279f |
chore(tor): move the onion service from alpha to beta
lifecycle is a selection axis rather than a label: cli/meta/ci/validate.py hands --lifecycles to matrix.entries_of, so a run whose envelope admits beta but not alpha picks svc-net-tor up from now on. Recent sweeps ran with an empty envelope and are unaffected. The maturity call is the operator's; this commit only records it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ffb04da40a |
refactor(tor): keep the onion timeout multipliers with the flavor they key on
The per-flavor multipliers lived in test-e2e-playwright's vars, where only
that role could read them. The CSP health check needs the same number: it
probes onion vhosts through the Tor proxy, where the checker's navigation
budget has to grow by the same factor as a Playwright wait.
They now sit in svc-net-tor's meta/services.yml next to services.tor.flavor,
the key they are indexed by, and the Playwright var reads them through
lookup('config', 'svc-net-tor', 'services.tor.timeout_multipliers'). That
lookup resolves from the role defaults, so it answers even where svc-net-tor
is not part of the round, which is how the flavor was already read.
The flavor-coverage test moves to the tor role and reads the new location;
the values (chutney 2, public 5) and what the Playwright run computes from
them are unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
4f44eff128 |
fix(svc-net-tor): bind dnsmasq statically so a failed re-enumeration cannot drop every listener
Run 34421944328 Nextcloud#13 lost the dashboard's Matomo tracker. The dns53 sampler on mgr-01 shows no :53 socket from 01:39:52 to 01:48:44 while the dnsmasq PID stayed the same, six seconds after a short-lived container's veth on docker0. In that window the manager could not resolve its own onion vhosts, so sys-front-inj-matomo's site checks for web-svc-logout (01:45:35) and web-app-dashboard (01:47:18) never reached OpenResty, the injection was disabled as "API not ready", and the Playwright check found no matomo.js. The other four swarm managers of the run that carried the same listener kept it throughout. Under bind-dynamic, dnsmasq 2.91 re-enumerates interfaces on every netlink address event and then garbage-collects every listener it did not re-find (network.c enumerate_interfaces), also when the enumeration failed; release_listener has no other caller. |
||
|
|
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> |
||
|
|
4ac07edaea |
refactor(meta): move the info-media exemption to the file it speaks about
The marker names the reason a role has no upstream homepage or demo video, and
it sat in meta/main.yml because a comment-only info.yml parses to an empty
document, which the schema lint rejected outright. So the exemption lived one
file away from the two fields it explains, and a reader looking for the video
found neither the field nor the reason it was missing.
The schema lint now accepts an empty document exactly when it carries the
marker, and that is what allows the move. The media lint reads the marker from
info.yml rather than main.yml, and its failure message names that same file, so
the fix it suggests lands where the absent field would have gone.
A hundred and two roles follow. Every occurrence was a standalone comment on
the second line of main.yml, directly under the document start, so the move is
a line delete and an insert at the same offset. Seventy-three roles already had
an info.yml and gain the line; twenty-nine get one whose whole content is the
document start and the marker. All hundred and two reasons carry over verbatim,
including the thirty-five bespoke ones.
Nothing downstream sees a change. Of the four readers of meta/info.yml, three
coerce a null parse through `or {}` behind an isinstance guard, and
utils/cache/applications.py skips a falsy parse before assignment, so
applications.<role>.info stays absent for the twenty-nine exactly as it was
while the file did not exist.
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
|
||
|
|
bf20562cec |
style: apply make autoformat
Line-length reflow in three test modules and a stray blank line in zammad variants. In router.yml the formatter also moved the nsswitch lineinfile regex from single to double quotes with the backslashes doubled to match; single-quoted YAML already passes a backslash through literally, so the string reaching lineinfile is byte-identical and the rule still changes only NOTFOUND. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5b22d6cb6d |
fix(svc-net-tor): let a refused name reach the zone's resolver
The role sets up the .onion split-DNS and points /etc/resolv.conf at dnsmasq,
but resolv.conf only feeds the glibc `dns` module, and the image ships
hosts: files myhostname resolve [!UNAVAIL=return] dns
`[!UNAVAIL=return]` returns on every status except UNAVAIL, so once
systemd-resolved refuses a name, glibc never reaches dnsmasq. systemd-resolved
refuses .onion as a special-use domain, which makes the role's own split-DNS
unreachable to every NSS consumer on the host.
Measured in run 33785198875 job 100756597086: dnsmasq kept the same pid from
20:30 to the capture, logged nothing, and answered `dig @127.0.0.1` correctly
at 22:05:05, while getaddrinfo failed for 28 minutes. Fourteen .onion domains
returned EAI_NONAME in 4-5ms each, an authoritative answer rather than a
timeout, and the one clearnet domain in the same probe was fine. msmtp failed
26 times on the same suffix, a second consumer of the same path.
|
||
|
|
11c98825aa |
fix(svc-net-tor): bind the loopback listener statically when no bridge is expected
|
||
|
|
4986e80dd4 |
feat(readme): ask the reader for a domain the deployment can resolve
The Production block never set one. inventory provision writes only
ansible_connection for a local host, so DOMAIN_PRIMARY stayed at the
group_vars default of infinito.localhost and the deployment served a zone its
own resolver knows nothing about. CoreDNS templates answers for
INFINITO_DOMAIN_RE and nothing else, so every lookup of the deployment's own
name came back NXDOMAIN.
Mastodon is where that surfaced. tootctl accounts create validates the address
through EmailMxValidator, which resolves MX and then, for the domain itself,
A and AAAA. All six queries returned NXDOMAIN in run 33574056480, the
validator recorded email :unreachable, and the admin account was never created.
Only the guide rows were affected: the matrix rows write
DOMAIN_PRIMARY: "{{ lookup('env', 'INFINITO_DOMAIN') }}" into their host_vars
and resolve fine.
The template now carries DOMAIN=<your-domain> in both the host and the
container form and threads it into --vars, with the container form passing it
through the docker run boundary as well. The two guide runners substitute it
the way they already substitute the SSH key, so the replay deploys under
INFINITO_DOMAIN like every other CI deploy. compose_deploy.sh moves its env
load ahead of the substitutions to have the value; host_deploy.sh had no env
load at all.
A production reader is asked for a domain now instead of silently inheriting
infinito.localhost, which no real deployment wants either. The shipped default
keeps its value: sys-svc-nss documents that *.localhost resolves through the
myhostname NSS module without any zone, and host-side tasks lean on it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
5f7ff5a1c1 |
test(lint): let the host-fact rule see what its name promises
The rule read one of the two ways a fact is written, and it read only templates. Both gaps hid the same kind of value. Bracket subscripts were invisible: ansible_facts.default_ipv4 was matched, ansible_facts['default_ipv4'] was not. Both forms are now generated from one key list rather than spelled out. Role vars and defaults were out of scan. A fact assigned to a variable there reaches the template exactly as if it had been written in the template, and the only real violation in the tree was sitting in that blind spot, which is how it survived the rule that exists to catch it. The scan covers vars/main.yml and defaults/main.yml through the mapping constants, 839 files where it saw only templates before. Interface addresses were not a pattern at all, only the named default_ipv4. A resolver reading the docker bridge off ansible_facts['docker0'] cost thirteen jobs in one run and was removed by hand; the rule that should have caught it did not know the shape. Any ansible_facts[<iface>]['ipv4'] is now matched, and the legacy ansible_<iface>.ipv4 with it. What stays permitted is the distinction the rule turns on. A fact that ADDRESSES a host travels wrongly. A fact that describes its PLATFORM does not: os_family, architecture and processor_vcpus choose behaviour for the node the rendered file is consumed on, which is the node it was rendered for. All seven ansible_facts reads in the tree were read individually; five are of that second kind, and a rule needing a suppression on five correct lines out of seven is not a rule. svc-net-tor is the sixth and seventh. It reads both the bridge address and default_ipv4, and meta/services.yml pins the role to the manager, so the fact describes the node the hidden service forwards on. Declared rather than left looking like an oversight. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9e6f64dde2 |
docs(readme): regenerate after the tor onboarding
The READMEs are generated from the role declarations. Six role READMEs gain
svc-net-tor in their dependency graph and the top-level table gains the Tor
Onion Service column entries, following
|
||
|
|
575a5a87ac |
refactor(layout): sort shipped scripts by language and split the unit suite
Every script a role ships now lives under `files/<language>/`, and the unit
suite is split the same way: `tests/unit/python/` beside new sibling trees for
the other languages. `tests/lint/filesystem/test_role_files_layout.py` keeps it
that way; directories whose shape is dictated from outside carry a `.nocheck`
naming the constraint (Docker build contexts, WordPress mu-plugins, the Joomla
extension, Odoo addons, Playwright suites, the `files/test/test.sh` harness
that `test-e2e-cli` resolves generically).
This cannot be applied in halves. Moving a file and repointing what names it
are the same change: 14 references resolve only at deploy time, so the tree is
green either before or after, never between.
Those 14 were found by static resolution over every reference class, not by the
test suite -- a full `make quality-high` was green while all of them were
broken:
* 7 `src:` values built from a role variable holding a bare filename
(svc-bkp-remote-2-local, decidim, espocrm, friendica, joomla, pixelfed x2).
The variable doubles as the destination name inside the container, so the
language segment belongs at the call site, not in the variable.
* `sys-service/tasks/04_base.yml` resolved every consumer's systemd script
through `first_found(..., errors='strict')` over `files/script.{sh,py}`.
18 roles ship exactly that and have no template fallback: the play would
have died on each.
* `sys-front-inj-javascript` fell back to `<role>/files/javascript.js`; the
interface now reads `files/javascript/javascript.js`.
* `sys-front-inj-logout/templates/head_sub.j2` built the cache-busting mtime
path through `path_absolute(... 'files', ...)`.
* 4 `ansible.builtin.script:` invocations naming a bare command
(pull.py, prune.sh, set_postgres_superuser_password.sh, ganesha-serve.sh).
* `dev-python/files/shell/install.sh` pointed at a sibling role's script.
Depth-dependent paths inside the moved trees were bumped accordingly: 59
`parents[N]` computations, 12 relative climbs, and the per-role recover CLI,
whose path `cli/administration/recover/recoverers.py` still assembled from the
pre-move location.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
637470a419 |
refactor(roles): delete what cannot run and correct what is not true
Six findings from a full audit of the roles this branch touched. None was
behind a failing job; all of them lie about what the code does.
Two defaults that can never fire, and would do harm if they did.
vars/main.yml gave services.tor.flavor a 'public' fallback, but
meta/services.yml:28 always defines the key - so the fallback is
unreachable, and it defeats the assert at tasks/00_core.yml:4-9: remove
the key and the role silently deploys public instead of failing loudly.
templates/dnsmasq-tor-onion.conf.j2 guarded twice on
`TOR_DNSMASQ_OWNS_LISTENER | default(true)`, while tasks/router.yml:35-37
sets that fact unconditionally two tasks earlier; had it ever gone
missing, `true` would have reshaped a dnsmasq this role does not own.
Two tasks with no readers. sys-front-inj-logout/tasks/01_deploy.yml
stat'ed logout.js to build INJ_LOGOUT_JS_VERSION; neither that fact nor
INJ_LOGOUT_JS_STAT is referenced anywhere under roles/ or group_vars/.
Cache busting already runs independently through lookup('local_mtime_qs')
in templates/head_sub.j2:1.
One README claim inverted. svc-net-tor's feature list named
TOR_EGRESS_ENABLED and called it off by default. That variable exists only
in docs/requirements/031-*.md - the implementation settled on
TOR_EGRESS_ROUTER_ENABLED, which group_vars/all/19_tor.yml:4 sets to true.
The README stated the opposite of its own default under a name nothing
reads.
Three files nothing can load, in web-app-prometheus. tasks/reset.yml is a
single comment and is included nowhere; templates/compose.override.yml is
one byte, and sys-svc-compose/tasks/04_files.yml:52-54 searches only
templates/compose.override.yml.j2 and files/compose.override.yml;
templates/style.css is a single comment, and sys-front-inj-css/tasks/main.yml:10
searches only templates/style.css.j2 and files/style.css.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
bb0e098a9f |
fix(svc-net-tor): stop pinning the host to a single resolver
|
||
|
|
069df39027 |
fix(svc-net-tor): give the onion probe Tor's own patience
The probe abandoned each attempt at --max-time 30 while Tor's SocksTimeout
sits at its 120s default (no SocksTimeout anywhere in the repo; positive
control SocksPort hits templates/torrc.j2:1). It gave up at a quarter of
Tor's working window, and destructively: every retry opens a fresh SOCKS
connection, discarding the descriptor fetches and rendezvous retries Tor
was still working through.
Run 32284541284 job 96178512465 shows the cost. Ansible recorded start
07:22:17.260, end 07:26:33.366, delta 0:04:16.105994. The first domain
burned its deterministic 6x30 + 5x10 = 230s and failed; the second then
succeeded on its first attempt but needed ~23s of that 30s window - 77%
of the limit. The transient had cleared at t~233s. The first domain's
budget ran out three seconds short of it.
Two round numbers, one behaviour: 5 attempts at 60s is 340s of patience
against today's 230s, so the same transient now has 107s of margin
instead of -3s, and a 23s connect sits at 38% of the window instead of
77%. LAM#1 (job 96179137200) is the near-miss that corroborates it - four
dead attempts, then a ~4s success on the fifth.
meta/tests.yml is required, not optional. roles/test-e2e-cli/tasks/run_one.yml:74
runs the script under `timeout {{ _cli_timeout }}`, and with no meta/tests.yml
this role fell through to the 3600s default in roles/test-e2e-cli/defaults/main.yml:3.
Widening the window moves the all-domains-fail ceiling from 16 domains to
11, and a 13-domain role demonstrably exists (Jellyfin#0, four passes of
"13 probed, 0 failed"). 7200 restores the headroom.
The `|| true` goes with it: the script runs under `set -uo pipefail` with
no -e, so the command substitution's exit code was already discarded and
line 63 branches on ${code}. tests/lint/filesystem/shell/test_no_or_true.py
never saw it because its regex wants #, ; or EOL after `true` and a `)`
follows here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
c55f508e05 |
feat(readme): draw mandatory and disabled service relations
The cosmos generator linked a consumer to its provider only through an '<role>' in group_names reference or an explicit provides: name. A hard dependency has neither -- pinning the provider on unconditionally is exactly what removes the Jinja reference the generator was reading -- so web-svc-asset's bond to web-svc-file vanished from the diagram the moment it became binding. Match a service key against the provider's own primary key from the registry, in both directions. Services the role turns off drew a solid 1:1 edge, claiming a mandatory relationship for something that carries nothing at deploy time. They now render as a red 0..0 edge; edge merging gained a real precedence so a provider consumed through two keys stays live when only one is off. Named colour, not hex: a '#' opens an entity code in the mermaid lexer and turns the whole diagram into a parse error on GitHub. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b09f2cbd82 |
fix(svc-net-tor): tie the dnsmasq listener to ownership, not the deploy mode
Outside compose TOR_CONTAINER_DNS_HOST was empty, so the drop-in rendered no
listen-address and lookup('container_dns') handed containers the clearnet
resolver alone. A swarm stack host ran the egress router without a listener its
containers could reach, and .onion stayed unresolvable for every one of them
while compose resolved it fine. Ownership already governs the listener, the
upstream policy and the resolv.conf pin; the deploy mode only ever stood in for
it. With the mode clause gone both modes render the drop-in compose has been
running in production, and compose keeps its exact behaviour: where matched == 0
the old and the new expression are the same boolean.
In CI run 31927586060 the swarm node dnsmasq logged "ignoring query from
non-local network 172.18.0.16" seconds after a veth appeared and vanished on
docker0, and every container lookup timed out from then on. The health service
retried for 77 minutes against a resolver that no longer answered.
Measured on dnsmasq 2.93 before landing this: a listen-address disables
--local-service even when that flag comes from the init script; without a bind
directive dnsmasq takes the 0.0.0.0 and [::] wildcard pair, which would collide
with a systemd-resolved stub and occupy port 53 on every address; bind-dynamic
and bind-interfaces both hold their per-address listeners across 2000 veth
cycles; and the stock dnsmasq.conf on arch, fedora and centos carries no
bind-interfaces, so the drop-in's bind-dynamic cannot abort startup there.
Two expectations in tests/unit/plugins/lookup/test_container_dns.py still encode
the compose-only asymmetry and fail with this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
54aeba7e06 |
fix(svc-net-tor): name the clearnet upstream
The egress router scraped /etc/resolv.conf once and persisted whatever it found. Inside a container that is 127.0.0.11, docker's embedded resolver, which answered REFUSED to recursive queries in CI run 31927586060 and left the node with no clearnet DNS. The upstream now comes from networks.internet.dns, the same source lookup('container_dns') already gives the docker daemon, and an assert demands it before the role touches the host.
no-resolv, the clearnet server= lines and the /etc/resolv.conf pin now hang on TOR_DNSMASQ_OWNS_LISTENER. A dnsmasq the role does not own keeps its upstreams and the node keeps its resolver; only the onion routing is added there. Without that split a swarm node would end up with no-resolv, no upstream, and its resolver pinned at itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
76e76d4700 |
fix(svc-net-tor): order the test env keys alphabetically
The repository env lint requires ascending keys in every env file, and test.env.j2 listed TOR_SOCKS ahead of TOR_DNSMASQ_CONF, so make quality stopped there. Order carries no meaning in a dotenv template: Jinja resolves the values from the role vars, not from preceding lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6ba3b49ba9 |
test(svc-net-tor): make a failed probe say why the name did not resolve
Job 94849355639 of run 31823845576 failed on exactly one name: the bare apex
infinito.example, HTTP 000, "Could not resolve host", while
espo.crm.infinito.example answered 200 in the same second through the same
resolver. The probe printed dns= and stopped there, so the log named the
symptom and no artifact carried the cause. There is no dnsmasq log
(log-queries is off, the journal has zero dnsmasq lines), no CoreDNS log in
the compose artifact set, and no query output from the host at failure time.
Reproducing the resolver stack locally did not close it either. CoreDNS on the
CI Corefile, dnsmasq on the drop-in pulled byte-identical from that job's own
--diff at 20:58:35, and the full dnsmasq -> 127.0.0.11 -> CoreDNS chain with a
live forward path all answer the apex with 172.30.0.10. The configuration is
exonerated; what is missing is the transaction itself.
So the failure branch now prints the active resolver, any /etc/hosts match,
what the local dnsmasq answers, and what each upstream answers. The last two
are the split that matters: they separate "dnsmasq dropped it" from "upstream
dropped it", which is the fork the bench could not settle. Upstreams are read
from the drop-in directory rather than one file, so a shared dnsmasq carrying
foreign drop-ins is covered, and matching only bare-IP server= lines skips
server=/onion/127.0.0.1#9053.
TOR_DNSMASQ_CONF arrives through test.env instead of a hardcoded
/etc/dnsmasq.d, using the same :? idiom
|
||
|
|
ab72cff929 |
test(lint): require the onion in every variant, not just SSO ones
|
||
|
|
b2a0a317fa |
refactor(tor): loosen the onion bond to 0.5
Every variants.yml header states the rule the bond feeds: variant 0 carries each
tightly-coupled (bond>=1, same-host) service, while loosely-coupled (bond<1,
separate-host) partners are excluded from the closure and distributed across
later variants to keep every host under budget. tor was declared bond: 1 in all
101 consumers, so it had to sit in every variant-0 closure.
It does not belong there. The onion is a separate-host provider reached over the
node's hidden service, the same shape as the bridged partners that already carry
0.5, and
|
||
|
|
8d7fff4d2d |
test(svc-net-tor): let the probe demand the values its test.env already sets
roles/svc-net-tor/templates/test.env.j2 sets RETRIES=6 and SLEEP_SECONDS=10, and
test-e2e-cli/tasks/run_one.yml renders that file unconditionally at :66 before
sourcing it at :74. The ${RETRIES:-20} / ${SLEEP_SECONDS:-15} fallbacks could
therefore never be reached -- a second source of truth that additionally
documented the wrong numbers: the header promised "default 20 / default 15"
while every run used 6 and 10.
They now take the :? form the two variables above them already use, so
test.env.j2 is the only place the values live, and a missing one aborts loudly
instead of silently probing three times longer than intended. The header stops
claiming defaults.
The sibling probes in svc-db-mariadb, svc-db-openldap and svc-db-postgres keep
${RETRIES:-20}: their test.env.j2 does not set the pair, so there the fallback is
live, not dead. Converting them would mean adding the values to those templates
first, and is deliberately left out.
Measured: a missing RETRIES exits 1 with "line 24: RETRIES: pass RETRIES as env
(attempts per domain, from svc-net-tor test.env)"; a complete env leaves the
probe output unchanged. shellcheck is clean against the staged content, and the
comment, shell and cli-asset-pairing lints pass.
The message wording avoids an apostrophe on purpose: inside ${VAR:?...} bash
still treats one as a quote character, which splits the abort message across the
next source line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
64131a4c35 |
test(svc-net-tor): name the address a failed probe resolved to
HTTP 000 cannot tell a name that never resolved from a listener that never
answered. Both leave curl without a status and openresty without an access-log
entry, which is exactly the signature the apex infinito.example has produced in
every compose job of run 31543714113.
2a60ddb3c already stopped discarding curl's stderr, so the message now names the
layer -- (6) could not resolve, (7) failed to connect, (35) handshake. It never
names the address, and that is the half that decides between "the resolver
answered wrong" and "the resolver did not answer".
getent hosts goes through the same NSS path curl uses, so the verdict carries
what the node itself resolved:
[FAIL] localhost HTTP 000 dns=::1
[FAIL] no-such-host-xyz.invalid HTTP 000 dns=
Empty means resolution failed. An address together with a connect or TLS error
means the resolver answered and the far side did not. An address other than the
node's own means the resolver answered wrongly. Read next to curl's line, those
three branches separate.
Only the failure path changes; [OK] keeps its format, and getent's own noise
goes to /dev/null so a probe never fails on the diagnostic.
Measured: both branches above; getent and head present in all four node images
(fedora, arch, centos, debian) via docker run; shellcheck clean against the
staged content; tests/lint/repository/test_comments_valid.py,
tests/lint/filesystem/shell/ and test_cli_test_assets_paired.py pass.
This is not the fix for the apex failure. Nine mechanisms were eliminated by
measurement today -- CoreDNS, dnsmasq and Docker's 127.0.0.11 each resolve both
the apex and its sibling to 172.30.0.10; the vhost exists in http and https;
nginx validated six times; no server-name conflict, no proxy env, no conntrack
pressure. What remains needs the one fact the log has never carried, and this
line carries it.
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.
|