Files
computer-playbook/scripts/tests/deploy/swarm/04_bootstrap_nodes.sh
T
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

56 lines
1.5 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
HOST_SRC="$(pwd)"
HOST_SUDO=""
if [ "$(id -u)" -ne 0 ]; then
HOST_SUDO="sudo -E"
fi
echo "==> Building .deb once on the act-runner"
DEB_PATH=$(PACKAGE_BUILD_ONLY=1 ${HOST_SUDO} bash "${HOST_SRC}/scripts/install/package.sh" |
tee /dev/stderr | tail -n1)
[ -f "${DEB_PATH}" ] || {
echo "FAILURE: builder did not produce a .deb (${DEB_PATH})"
exit 1
}
echo "==> built ${DEB_PATH}"
DEB_BASENAME="$(basename "${DEB_PATH}")"
provision_node() {
local node="$1"
echo "==> [${node}] copying repo + .deb + installing"
docker exec "${node}" mkdir -p /opt/infinito-nexus
tar -C "${HOST_SRC}" -cf - . | docker exec -i "${node}" tar -C /opt/infinito-nexus -xpf -
# docker cp to a tmpfs mount lands in the overlay shadow path (moby/22281).
docker exec -i "${node}" sh -c "cat > /tmp/${DEB_BASENAME}" <"${DEB_PATH}"
docker exec \
-e LANG=C.UTF-8 -e LC_ALL=C.UTF-8 \
-e PACKAGE_INSTALL_FROM="/tmp/${DEB_BASENAME}" \
"${node}" bash -c \
'cd /opt/infinito-nexus && bash scripts/install/package.sh'
}
declare -A PIDS
for node in "${MGR}" "${WRK1}" "${WRK2}" "${NFS_SERVER}"; do
provision_node "${node}" >"/tmp/bootstrap-${node}.log" 2>&1 &
PIDS["${node}"]=$!
done
rc=0
for node in "${!PIDS[@]}"; do
if ! wait "${PIDS[${node}]}"; then
echo "FAILURE: provision_node ${node} exited non-zero"
rc=1
fi
done
for node in "${MGR}" "${WRK1}" "${WRK2}" "${NFS_SERVER}"; do
echo "---- bootstrap log for ${node} ----"
cat "/tmp/bootstrap-${node}.log"
done
exit "${rc}"