Commit Graph
13 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5 e1d513a3c0 fix(swarm-matrix): bake a variant payload for every app the round deploys
The swarm matrix resolved variant payloads over the planner's round_include while provisioning the closure derive_includes returns. The two agree until a variant switches its couplings off: the planner then resolves no prerequisites, round_include collapses to the primary, and every deployed dependency silently keeps its base config. For svc-ai-litellm round 1 that meant web-app-keycloak ran with the base realm.totp_enabled true, so CONFIGURE_TOTP became a default required action and every SSO persona sat in a 300s predicate timeout until the job hit its wall clock at 20086s.

payload.py states the intent this restores: the role pulling a dependency in is what decides how lean the dependency runs. variant_scope keeps the decision next to the closure it filters and excludes the three lab infrastructure roles, whose payload would replace group-membership expressions such as {{ 'svc-swarm-node' in group_names }} with an unconditional true in every round.

Measured: round 1 goes from 1 to 29 payload apps and keycloak from absent to totp_enabled false, while the deployed closure stays 32. Repo-wide 62 of 280 rounds gain a real setting, dominated by postgres exposed in 48 and mariadb in 22; no round loses one, and no gained payload enables a service whose provider is not deployed. With disable set the scope shrinks instead, which is correct because a pruned provider is not deployed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 13:10:16 +02:00
kevinveenbirkenbachandClaude Opus 5 8ce66075b5 fix(swarm-test): label a worker kata-capable only when the app is sandboxed
Two Moodle#1 jobs of run 33445831564 -- tor/arch and clearnet/debian -- end with the
same line, 65 minutes apart:

    FAILURE: no running moodle task on a worker node

The deploy is clean; PLAY RECAP reports failed=0 on all four hosts. Only the
post-deploy drain drill fails, and it fails because nothing is left to drain.

The chain, each link measured rather than assumed:

  meta/variants.yml            variant 1 turns openwebui/hermes/openclaw/flowise on;
                               variant 0 turns all four off
  extend_inventory.py:88-89    svc-virt-kata in the closure put BOTH workers in the group
  svc-virt-kata/00_core.yml:61 labels every group member kata-capable=true
                               (2 label tasks in each moodle#1 log, 0 in moodle#0)
  deploy.yml.j2:18-23          non-kata services get node.labels.kata-capable != true
  05_seed_content.sh:12-22     filters running tasks with grep -vx "${MGR}"

With both workers labelled, moodle and nginx have no eligible node but the stack
host. `docker service ps` at failure shows all three moodle_nginx replicas Running
on mgr-01. The drill then looks for a worker task and finds none.

Controls in the same run: moodle#0 carries no kata group and 0 label tasks, and its
drill was reachable; Nextcloud#10 (green, no kata) logs
"initially scheduled on: ...wrk-01" -> "Rescheduled ... to ...mgr-01" -> drill complete.
Exactly three of the run's 18 swarm inventories carry the kata group, and all three
are red.

b20a70646f widened _WORKERS[-1] to *_WORKERS and said so in its own message: "whether
the stack host holds up under it is what the next swarm run measures". The stack host
held up. What broke is the drill's precondition.

The discriminator is the app's own declaration, and it separates cleanly -- executed,
not reasoned:

    web-app-moodle     kata.enabled=None    -> ('swarm-wrk-02',)
    web-app-nextcloud  kata.enabled=None    -> ('swarm-wrk-02',)
    web-app-openclaw   kata.enabled=True    -> ('swarm-wrk-01', 'swarm-wrk-02')
    web-app-hermes     kata.enabled=True    -> ('swarm-wrk-01', 'swarm-wrk-02')

_WORKERS[-1:] restores exactly the pre-b20a70646f tuple for the non-sandboxed case, so
that commit's drain fix stays intact for the rounds it was written for.
svc-virt-kata's own assert (00_core.yml:80-88, labelled_nodes | length > 0) holds with
one labelled worker.

applications_for_round() is a thin public wrapper over the existing
_applications_for_active_variants(get_merged_applications(...)); extend_inventory would
otherwise have to import a neighbour module's private helper.

Three tests cover the branch, which had none: grep -c "kata\|_WORKERS" on
test_extend_inventory.py returned 0 before this.

Not covered: the full chain -- label, placement, drill -- has not run. One
`make act-swarm apps=web-app-moodle variant=1` closes that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 10:06:53 +02:00
kevinveenbirkenbachandClaude Opus 5 e41508f48b fix(swarm-test): honour a role's pinned local database
force_shared_db flipped every DB service to shared with no exemption, so
web-app-magento's deliberate embedded MariaDB 11.4 - declared shared:
false with a comment warning that no bot may move the major underneath
it - became dead config and setup:install hit the central engine's 12.3.2,
outside Magento's supported range. The run log names the override:
'swarm force-shared: web-app-magento.services.mariadb -> shared=true'.

Exempts a DB service whose role meta declares a literal shared: false AND
whose role is manager-pinned. The second condition is not decoration:
utils/storage/nfs.py keeps a manager-pinned role's volumes node-local, and
InnoDB over NFS is why the harness force-shares in the first place. Of the
two roles with a literal local DB, magento qualifies and bigbluebutton
(unpinned) does not, so its behaviour is unchanged.

derive_includes._force_shared_db_view documents itself as mirroring
force_shared_db, so it takes the same exemption - without it the dep walk
would compute magento's include closure against the central engine while
the deploy runs a local sidecar.

The sidecar carried no deploy block at all, so an exempted service would
have been free-floating with one task per node over a single-writer
datadir. The block now comes from a container_deploy lookup shared with
sys-svc-container/templates/deploy.yml.j2, which replaces the replicas
ternary and the duplicated placement constraint there. Its unit test
caught a real defect while being written: with compose_mode_force=swarm
over DEPLOYMENT_MODE=compose the delegated compose_replicas returned
empty, emitting a deploy: key with no child.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 01:36:18 +02:00
kevinveenbirkenbachandClaude Opus 5 b44f30d72d fix(swarm-test): keep force-shared off disabled services
The swarm drill provisions a fresh inventory each round: _provision runs
the services disabler, which writes enabled: false, shared: false for
every service in `disable=`. _force_shared_db then ran four seconds
later and flipped shared back to true, because it only ever looked at
shared and never at enabled.

Its key set comes from every flagged top-level key of the svc-db-* roles,
so it covers svc-db-mariadb's own consumer entries tor and
container_backup, both of which sit on the disable list. The result was
enabled=False, shared=True, and the deploy aborted on the consistency
guard in services_disabler.

Guard the flip on enabled instead of narrowing the key set: the key set
would only drop today's two keys, while postgres, redis, memcached,
qdrant, typesense and ldap are all equally on the live disable list.
`is not False` keeps flipping entries that carry no enabled key at all.

derive_includes mirrors force_shared_db by contract, so it gets the same
condition; otherwise the dependency walk sees a different topology than
the deploy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 19:33:21 +02:00
kevinveenbirkenbachandClaude Opus 5 0bde90e47d fix(swarm-test): derive the DR drill backup providers from the closure
The drill configured svc-bkp-remote-2-local with a fixed manager plus NFS provider list for every app, while the manager only runs a backup repository when the app's include closure pulls svc-bkp-volume-2-local or svc-bkp-secrets-2-local in. For an app whose container_backup consumer is statically off, such as svc-dns-unbound, user-backup never ran on the manager, so the pull was rejected with 'Permission denied (publickey,password)' and the fail-if-any wrapper took the whole unit down at drill step 4/9. Four roles reach that step today: svc-dns-unbound, svc-db-redis, svc-registry-cache and svc-registry-docker.

backup_repos.py expresses the placement once and matrix derives the provider list from it per round. derive_includes accepts the round's variant map because INFINITO_APP_VARIANTS only reaches the subprocess callers. The extras file keeps the device paths the drill reads back and loses the provider list nothing read from it, and the drill banners stop claiming backup legs that were never installed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 11:29:47 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f4fa8adf16 feat(deploy): prune orphaned transitive deps on disable
disable= now drops providers left with no surviving parent, not only
the named provider roles. Compose (init) and swarm (derive_includes)
share one reachability cut: BFS from the surviving primaries over
dependency + service edges with disabled roles as cut-nodes. No-disable
deploys stay byte-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 20:05:59 +02:00
kevinveenbirkenbachandClaude Opus 4.8 8d2d8f4a7c feat(inventory): disable services by service key or provider app id
The disable env only matched service/entity keys (email, prometheus);
naming the provider role (web-app-mailu) silently did nothing. A shared
expand_service_tokens (registry SPOT) now normalizes either spelling to
the provided service key(s), and every disabler resolver plus the
conflict check normalize at entry, so compose and swarm accept both
forms.

derive_includes additionally prunes the disabled providers from the
swarm include closure. Without it the provision-time disabler removed
the provider role but extend_inventory re-added it via the same dep
walk, so the deploy tripped the 'provider role still active' conflict.
Live-verified on a swarm-dashboard run: disable='web-app-mailu,...'
expands to email->web-app-mailu and deploy round 1/1 proceeds with no
conflict.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 10:24:33 +02:00
kevinveenbirkenbachandClaude Fable 5 147403a939 refactor(test-swarm): compose node image + ansible provisioning + dir layout
Replace the per-node docker-run bring-up with a single node image
(compose/swarm/Dockerfile bakes python3/dnsmasq/openssh/rsync/cryptsetup)
declared in compose/swarm.yml, one compose up, and one ansible play
(compose/swarm/playbook.yml over ansible_connection: docker) that waits for
systemd, captures IPs, wires lab DNS and installs the .deb per node. Reshape
the harness into topology/ (naming SPOT), routine/ (01_bootstrap + the
sequenced 02-08 steps) and utils/ (clean/, collect/, shared helpers); the
workflow, matrix.py and Makefile follow the new paths. swarm init/join was
already ansible-owned (svc-swarm-node), corrected in the README diagram.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 03:15:52 +02:00
kevinveenbirkenbachandClaude Fable 5 6d7bcdf705 fix(swarm-test): mirror the force-shared DB view in the credential closure
derive_includes applies the same shared:true flip as force_shared_db, so
variant rounds that pin a consumer's DB to shared:false keep the central
provider (and its generated credentials) in the include set the deploy
will actually load. extend_inventory derives its manager-placed groups
from the same variant-aware closure instead of a base-config walk, which
re-added pruned provider groups without credentials. matrix.py passes the
round's variant map to the extend step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 17:05:55 +02:00
kevinveenbirkenbachandClaude Opus 4.8 2f9a54cea7 fix(deploy): honor variants in include resolution
Inventory include sets ignored the active variant: swarm
derive_includes walked the variant-free base config (dynamic service
flags count as enabled) and the compose CombinedResolver followed
run_after as an include edge. Providers a variant pins to false
(e.g. mobilizon variant 1 sso) were still deployed.

- derive_includes: swap every app listed in INFINITO_APP_VARIANTS to
  its variant config, index 0 included (nextcloud variant 0 disables
  11 partner services)
- CombinedResolver: follow_run_after flag; include resolution passes
  False so run_after stays a pure ordering hint
- drop dead deps.py chain (resolve_deploy_ids_for_*) and unused
  resolve_with_edges; rewiring them would resurrect variant-blind
  run_after-following includes
- inventory.md: document the real include pipeline and
  is_explicit_truth semantics

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 03:21:56 +02:00
kevinveenbirkenbach b5247eeafb fix(tests/swarm/derive_includes): include svc-registry-docker in swarm test inventory
The swarm test rig provisions its inventory via derive_includes.py
walking each APP_ID's transitive meta-deps. svc-registry-docker is
not a meta-dep of any web app (intentionally — it would otherwise
spin up an unused registry container in compose-mode), but the
sys-svc-compose handler "swarm: retag built images for local
registry and push" (handlers/main.yml:116) requires the registry
to be reachable at swarm-mgr-01:5000 whenever an app builds its
own image (web-svc-simpleicons, etc). Without the include the
handler crashed with "dial tcp 192.168.244.3:5000: connection
refused".

Add svc-registry-docker to _EXPLICIT_INCLUDES alongside
svc-swarm-manager and svc-storage-nfs-client. The role already
carries default_placement: manager in meta/services.yml, so the
constructor auto-places swarm-mgr-01 in the svc-registry-docker
group and tasks/groups/svc-registry-roles.yml deploys it ahead of
the first app stack. Verified with:
  APP_ID=web-svc-simpleicons python -m utils.tests.swarm.derive_includes
which now emits svc-registry-docker in the include list.
2026-06-09 20:23:54 +02:00
kevinveenbirkenbachandClaude Opus 4.7 23ec7a99fb refactor(roles): rename svc-{cache,docker}-* roles for prefix consistency
svc-cache-registry       -> svc-registry-cache
  svc-docker-registry      -> svc-registry-docker
  svc-docker-swarm         -> svc-swarm
  svc-docker-swarm-manager -> svc-swarm-manager

categories.yml: drop svc.docker, add svc.registry. Deliberately no svc.swarm
category so get_entity_name('svc-swarm') stays 'swarm' instead of collapsing
to '' (which would break discover_role_services).

Entity keys updated to match the new svc.registry prefix:
  services.cache-registry.* -> services.cache.*
  services.registry.*       -> services.docker.*

01_constructor: replace svc-docker bootstrap-loop entry with explicit
include_role of svc-swarm + bootstrap loop over [svc-registry, svc-storage].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 15:18:19 +02:00
kevinveenbirkenbachandClaude Opus 4.7 f67b913a4b feat(swarm): swarm-aware deploy CLI with inventory-derived dep closure
New ``cli.administration.deploy.swarm`` package wraps the strict
``cli.administration.deploy.dedicated`` runner with a layered closure
so swarm deploys never require operators (or test scripts) to
enumerate every infra role:

  effective --id = seed (operator --id, else inventory_role_groups)
                ∪ dep-walk(seed via applications_if_group_and_all_deps)
                ∪ default-placement safety net

The dep-walker is the same SPOT
(``utils.roles.applications.in_group_deps.applications_if_group_and_all_deps``)
the runtime constructor consumes via ``lookup('applications_current_play')``,
so the CLI-side closure and the in-play role set stay in sync.

The dedicated CLI keeps strict explicit-only semantics: ``main`` was
split into ``parse_args`` + ``run_from_args`` so the swarm wrapper
can share parser, mode wiring, validator and runner verbatim and
only mutate ``args.id``.

Meta-side: ``svc-db-postgres`` gains ``default_placement: manager``
(semantic symmetry with mariadb). ``default_placement`` is now a
PLACEMENT hint only ("if deployed, run on the manager"); deploy
decisions come from the dep-walk. Apps that declare ``mariadb:``
pull svc-db-mariadb; apps that declare ``postgres:`` pull
svc-db-postgres; apps that declare neither get no central DB.

Shared utils extracted so the four consumers stay aligned:
  * ``utils.inventory.groups.inventory_has_group``: YAML + INI static
    inventory group probe (extracted from ValidDeployId).
  * ``utils.roles.meta_lookup.{get_role_default_placement,
    iter_roles_with_default_placement}``: single source of truth for
    ``default_placement`` discovery (lookup plugin is now a thin wrapper).

Test pipeline (``scripts/tests/deploy/swarm/*``):
  * Heredoc-Python shells converted to proper modules under
    ``utils.tests.swarm.*`` and invoked via ``python -m``:
      - extend_inventory.py (was 08_extend_inventory.sh)
      - write_extras.py     (was 09_write_extras.sh dynamic half)
      - derive_includes.py  (replaces 06's hardcoded includes array)
      - write_hosts_entries.py (replaces 02's hardcoded /etc/hosts heredoc;
        derives every canonical + www-redirect domain from APP_ID via
        canonical_domains_map + generate_all_domains, so the test
        works for any app in the matrix without per-app maintenance)
  * 01_create_network.sh deleted (single docker command inlined in workflow).
  * 11_deploy.sh deleted (swarm CLI now called directly in workflow).
  * 09_write_extras.sh: static half moved to inventories/development/swarm.yml.
  * Remaining shell scripts renumbered 01-13 without gaps.
  * 08_wait_converge.sh: case block on DB_DEP replaced with single
    conditional (container name == DB_DEP).
  * 06_provision_inventory.sh: hardcoded includes array replaced with
    `mapfile -t includes < <(python -m utils.tests.swarm.derive_includes)`.
  * Workflow YAML step references updated to new script names + module
    invocations.

New tests (39 covering the closure surface):
  * tests/unit/cli/administration/deploy/swarm/test_closure.py (14)
  * tests/unit/utils/inventory/test_groups.py (11)
  * tests/unit/utils/roles/test_meta_lookup.py (+3 for default_placement)
  * tests/unit/utils/roles/validation/test_deploy_id.py (unchanged
    8 tests still pass after refactor onto the shared util).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 02:24:51 +02:00