mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-10 04:36:40 +00:00
The entity name keys a role's instance directory, its compose project and its container name, and was derived only by stripping the longest matching category prefix. Two roles under different categories whose names share a suffix therefore collapse onto one entity and overwrite each other's rendered stack, which no provides: can repair. A literal entity_name in vars/main.yml now wins over the derivation. web-app-seaweedfs-console takes its shorter name back and declares the entity seaweedfs-console, which frees the seaweedfs key in its own services map for the engine consumer and removes the provides:, covers: and alias entries that stood in for it. A new lint pairs the declaration with the primary key it has to match, because a drift between them would drop the role from the provider set in silence. Compared across all 288 roles, exactly one entity changes, and it restores what main already keeps on disk, so no instance directory moves. The function and the filter lose their get_ prefix. Two lints read that prefix as the discriminator between the variable and the filter that fills it; both discriminate on the pipe now. Ansible's templar resolves a filter and a same-named variable independently, verified against ansible-core 2.20.2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
178 lines
5.8 KiB
Bash
Executable File
178 lines
5.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
_REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../../.." && pwd)"
|
|
# shellcheck source=scripts/meta/env/load.sh
|
|
source "${_REPO_ROOT}/scripts/meta/env/load.sh"
|
|
|
|
if [ -s "${SWARM_DRILL_ENV:-}" ]; then
|
|
set -a
|
|
# shellcheck source=/dev/null
|
|
source "${SWARM_DRILL_ENV}"
|
|
set +a
|
|
fi
|
|
|
|
: "${APP_ID:?APP_ID required}"
|
|
|
|
ENTITY="$(PYTHONPATH="${_REPO_ROOT}" "${PYTHON}" -c "from utils.roles.entity.name import entity_name; print(entity_name('${APP_ID}'))")"
|
|
|
|
STACK_NAME="${ENTITY}"
|
|
CUSTOM_IMAGE_REPO="${ENTITY}_custom"
|
|
|
|
ROLE_DIR="${_REPO_ROOT}/roles/${APP_ID}"
|
|
|
|
PRIMARY_SERVICE_KEY="$(PYTHONPATH="${_REPO_ROOT}" "${PYTHON}" -c "
|
|
import os, yaml
|
|
key = '${ENTITY}'
|
|
svc = os.path.join('${ROLE_DIR}', 'meta', 'services.yml')
|
|
if os.path.exists(svc):
|
|
data = yaml.safe_load(open(svc)) or {}
|
|
entity = data.get('${ENTITY}') if isinstance(data, dict) else None
|
|
if isinstance(entity, dict):
|
|
k = (entity.get('proxy') or {}).get('service_key')
|
|
if isinstance(k, str) and k:
|
|
key = k
|
|
print(key)
|
|
")"
|
|
SERVICE_NAME="${STACK_NAME}_${PRIMARY_SERVICE_KEY}"
|
|
|
|
DB_DEP=none
|
|
if [ -f "${ROLE_DIR}/meta/services.yml" ]; then
|
|
if grep -qE '^mariadb:' "${ROLE_DIR}/meta/services.yml"; then
|
|
DB_DEP=mariadb
|
|
elif grep -qE '^postgres:' "${ROLE_DIR}/meta/services.yml"; then
|
|
DB_DEP=postgres
|
|
fi
|
|
fi
|
|
|
|
DEFAULT_PLACEMENT_MANAGER=false
|
|
if [ -f "${ROLE_DIR}/meta/services.yml" ] &&
|
|
grep -qE "placement:[[:space:]]*manager\b" "${ROLE_DIR}/meta/services.yml"; then
|
|
DEFAULT_PLACEMENT_MANAGER=true
|
|
fi
|
|
|
|
PROBE_PORT="$(PYTHONPATH="${_REPO_ROOT}" "${PYTHON}" -c "
|
|
import os, yaml
|
|
port = 80
|
|
svc = os.path.join('${ROLE_DIR}', 'meta', 'services.yml')
|
|
if os.path.exists(svc):
|
|
data = yaml.safe_load(open(svc)) or {}
|
|
entity = data.get('${ENTITY}') if isinstance(data, dict) else None
|
|
if isinstance(entity, dict):
|
|
try:
|
|
port = entity['ports']['internal']['http'] or 80
|
|
except Exception:
|
|
port = 80
|
|
print(port)
|
|
")"
|
|
|
|
NFS_VOLUMES=""
|
|
if [ -f "${ROLE_DIR}/meta/volumes.yml" ]; then
|
|
NFS_VOLUMES="$(python3 -c "
|
|
import sys, yaml
|
|
with open('${ROLE_DIR}/meta/volumes.yml') as f:
|
|
data = yaml.safe_load(f) or {}
|
|
if isinstance(data, list):
|
|
entries = [e for e in data if isinstance(e, dict)]
|
|
elif isinstance(data, dict):
|
|
entries = [v for v in data.values() if isinstance(v, dict)]
|
|
else:
|
|
entries = []
|
|
for v in entries:
|
|
if v.get('type') != 'volume':
|
|
continue
|
|
if v.get('nfs') is False:
|
|
continue
|
|
docker_name = v.get('docker_name') or v.get('name')
|
|
if isinstance(docker_name, str):
|
|
print(docker_name)
|
|
")"
|
|
fi
|
|
|
|
PRIMARY_NFS_VOLUME="${NFS_VOLUMES%%$'\n'*}"
|
|
|
|
NFS_CHECK_MOUNTPOINT="/mnt/nfs-check"
|
|
|
|
export APP_ID ENTITY STACK_NAME SERVICE_NAME PRIMARY_SERVICE_KEY CUSTOM_IMAGE_REPO DB_DEP NFS_VOLUMES PRIMARY_NFS_VOLUME DEFAULT_PLACEMENT_MANAGER PROBE_PORT NFS_CHECK_MOUNTPOINT
|
|
|
|
# Exits the calling chaos step (09/10/11) with success when the role is
|
|
# manager-pinned (no worker task exists to drain).
|
|
skip_chaos_if_manager_pinned() {
|
|
if [ "${DEFAULT_PLACEMENT_MANAGER}" = true ]; then
|
|
echo "SKIP: ${ENTITY} declares placement: manager (single-node on the swarm manager) — drain-a-worker reschedule chaos does not apply"
|
|
exit 0
|
|
fi
|
|
}
|
|
|
|
has_swarm_service() {
|
|
local _names=""
|
|
[ -n "${MGR:-}" ] || return 1
|
|
if ! _names="$(docker exec "${MGR}" docker stack services \
|
|
--format '{{.Name}}' "${STACK_NAME}" 2>/dev/null)"; then
|
|
_names=""
|
|
fi
|
|
printf '%s\n' "${_names}" | grep -qxF "${SERVICE_NAME}"
|
|
}
|
|
|
|
# Prints the swarm service carrying APP_ID's database. A role that pins its own
|
|
# engine runs it as <entity>_database inside its own stack, a central provider
|
|
# as <dep>_<dep> in its own (plugins/lookup/database.py).
|
|
resolve_db_service() {
|
|
local _names=""
|
|
[ "${DB_DEP}" != "none" ] || return 0
|
|
[ -n "${MGR:-}" ] || return 0
|
|
if ! _names="$(timeout 30 docker exec "${MGR}" docker service ls --format '{{.Name}}' 2>/dev/null)"; then
|
|
return 0
|
|
fi
|
|
if printf '%s\n' "${_names}" | grep -qxF "${STACK_NAME}_database"; then
|
|
printf '%s\n' "${STACK_NAME}_database"
|
|
elif printf '%s\n' "${_names}" | grep -qxF "${DB_DEP}_${DB_DEP}"; then
|
|
printf '%s\n' "${DB_DEP}_${DB_DEP}"
|
|
fi
|
|
}
|
|
|
|
# Exits the calling gate (03/05/06/07) with success when the role deploys no
|
|
# swarm service to converge or drain.
|
|
skip_if_no_swarm_service() {
|
|
if ! has_swarm_service; then
|
|
echo "SKIP: ${ENTITY} (${APP_ID}) deployed no swarm service — converge/chaos gate does not apply"
|
|
exit 0
|
|
fi
|
|
}
|
|
|
|
# A stack whose sidecar is named after its app (<app>mcp) makes every
|
|
# `--filter name=<app>` a two-hit match, so both helpers below match exactly.
|
|
# Param: $1 node container, $2 swarm service name
|
|
service_container_id() {
|
|
docker exec "$1" docker ps \
|
|
--filter "label=com.docker.swarm.service.name=$2" \
|
|
--format '{{.ID}}' | head -1
|
|
}
|
|
|
|
# Param: $1 manager container, $2 swarm service name
|
|
service_replicas() {
|
|
docker exec "$1" docker service ls \
|
|
--filter "name=$2" --format '{{.Name}} {{.Replicas}}' |
|
|
awk -v svc="$2" '$1 == svc { print $2 }'
|
|
}
|
|
|
|
# Param: $1 node container, $2 app container id, $3 port
|
|
probe_app_reachable() {
|
|
docker exec "$1" docker exec "$2" sh -c \
|
|
"curl -sS http://localhost:$3/ || wget -qO- http://localhost:$3/ || nc -z localhost $3" >/dev/null 2>&1 ||
|
|
docker exec "$1" docker exec "$2" bash -c "exec 3<>/dev/tcp/localhost/$3" >/dev/null 2>&1 ||
|
|
docker exec "$1" sh -c "docker inspect -f '{{.State.Health.Status}}' $2 | grep -qx healthy" >/dev/null 2>&1
|
|
}
|
|
|
|
if [ "${SWARM_NFS_PILOT_VERBOSE:-0}" = "1" ]; then
|
|
echo "==> APP_ID=${APP_ID}"
|
|
echo " ENTITY=${ENTITY}"
|
|
echo " STACK_NAME=${STACK_NAME}"
|
|
echo " SERVICE_NAME=${SERVICE_NAME}"
|
|
echo " CUSTOM_IMAGE_REPO=${CUSTOM_IMAGE_REPO}"
|
|
echo " DB_DEP=${DB_DEP}"
|
|
echo " PRIMARY_NFS_VOLUME=${PRIMARY_NFS_VOLUME}"
|
|
echo " DEFAULT_PLACEMENT_MANAGER=${DEFAULT_PLACEMENT_MANAGER}"
|
|
echo " PROBE_PORT=${PROBE_PORT}"
|
|
fi
|