#!/usr/bin/env bash # Da de alta un cliente nuevo: crea sus nodos, su Jenkinsfile, el job en # jenkins.yaml y el volumen en docker-compose.yml. # # Uso: # create-client.sh --cliente \ # --tienda [--tienda ...] \ # --nodo : [--nodo : ...] # # Ejemplo: # create-client.sh --cliente econo-mbs \ # --tienda yabucoa --tienda san-lorenzo \ # --nodo yabucoa:1 --nodo yabucoa:2 --nodo yabucoa:3 \ # --nodo san-lorenzo:3 --nodo san-lorenzo:4 set -euo pipefail usage() { cat < \\ --tienda [--tienda ...] \\ --nodo : [--nodo : ...] Ejemplo: $(basename "$0") --cliente econo-mbs \\ --tienda yabucoa --tienda san-lorenzo \\ --nodo yabucoa:1 --nodo yabucoa:2 --nodo yabucoa:3 \\ --nodo san-lorenzo:3 --nodo san-lorenzo:4 EOF } if [[ $# -eq 0 ]]; then usage exit 1 fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" JENKINS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" REPO_ROOT="$(cd "$JENKINS_DIR/../.." && pwd)" JENKINS_YAML="$JENKINS_DIR/jenkins.yaml" PIPELINES_DIR="$JENKINS_DIR/pipelines" DOCKER_COMPOSE="$REPO_ROOT/docker-compose.yml" cliente="" tiendas=() nodos=() while [[ $# -gt 0 ]]; do case "$1" in --cliente) cliente="$2"; shift 2 ;; --tienda) tiendas+=("$2"); shift 2 ;; --nodo) nodos+=("$2"); shift 2 ;; *) echo "Argumento desconocido: $1" >&2; exit 1 ;; esac done command -v yq >/dev/null 2>&1 || { echo "yq no está instalado. Instalalo (ej. 'apt install yq' o ver https://github.com/mikefarah/yq#install)." >&2 exit 1 } [[ -n "$cliente" ]] || { echo "Falta --cliente" >&2; exit 1; } [[ "$cliente" =~ ^[a-z0-9-]+$ ]] || { echo "cliente inválido: '$cliente' (solo minúsculas, números y guiones)" >&2; exit 1; } [[ ${#tiendas[@]} -gt 0 ]] || { echo "Falta al menos un --tienda" >&2; exit 1; } [[ ${#nodos[@]} -gt 0 ]] || { echo "Falta al menos un --nodo" >&2; exit 1; } for tienda in "${tiendas[@]}"; do [[ "$tienda" =~ ^[a-z0-9-]+$ ]] || { echo "tienda inválida: '$tienda' (solo minúsculas, números y guiones)" >&2; exit 1; } done declare -A tiendas_vistas=() tiendas_dedup=() for tienda in "${tiendas[@]}"; do [[ -n "${tiendas_vistas[$tienda]+x}" ]] && continue tiendas_vistas[$tienda]=1 tiendas_dedup+=("$tienda") done tiendas=("${tiendas_dedup[@]}") jenkinsfile="$PIPELINES_DIR/Jenkinsfile.${cliente}" [[ -f "$jenkinsfile" ]] && { echo "Ya existe $jenkinsfile" >&2; exit 1; } if yq eval '.jobs[].script' "$JENKINS_YAML" | grep -q "deploy-${cliente}"; then echo "Ya existe una entrada de job para '${cliente}' en $JENKINS_YAML" >&2 exit 1 fi # --nodo debe referenciar una tienda ya declarada con --tienda for nodo in "${nodos[@]}"; do nodo_tienda="${nodo%%:*}" nodo_numero="${nodo#*:}" [[ "$nodo_tienda" != "$nodo" && -n "$nodo_numero" ]] || { echo "Formato de --nodo inválido: '$nodo' (esperado :)" >&2 exit 1 } match=false for tienda in "${tiendas[@]}"; do [[ "$tienda" == "$nodo_tienda" ]] && { match=true; break; } done [[ "$match" == true ]] || { echo "El nodo '$nodo' referencia la tienda '$nodo_tienda', que no fue declarada con --tienda" >&2 exit 1 } done rollback_nodos() { local nombre for nombre in "${creados[@]}"; do echo " - revirtiendo nodo: $nombre" >&2 NODE_TO_DELETE="$nombre" yq eval -i 'del(.jenkins.nodes[] | select(.permanent.name == strenv(NODE_TO_DELETE)))' "$JENKINS_YAML" done } echo "Creando nodos..." creados=() for nodo in "${nodos[@]}"; do nodo_tienda="${nodo%%:*}" nodo_numero="${nodo#*:}" if ! "$SCRIPT_DIR/create-node.sh" --cliente "$cliente" --tienda "$nodo_tienda" --numero "$nodo_numero" --skip-checks; then echo "" >&2 echo "Falló la creación de '$nodo'. Revirtiendo los ${#creados[@]} nodo(s) ya creado(s) para '$cliente'..." >&2 rollback_nodos exit 1 fi nodo_numero_padded=$(printf '%03d' "$((10#$nodo_numero))") creados+=("${cliente}-${nodo_tienda}-sco${nodo_numero_padded}") done echo "" echo "Generado $jenkinsfile..." choices="" for tienda in "${tiendas[@]}"; do choices+="'${tienda}', " done choices="${choices%, }" cat > "$jenkinsfile" < ["Deploy \${nodeName}": { node(nodeName) { unstash 'scripts' sh "bash install.sh '${cliente}'" } }] } parallel deployStages } } } } } EOF echo "" echo "Agregado job 'deploy-${cliente}' a $JENKINS_YAML..." JOB_SCRIPT="pipelineJob('deploy-${cliente}') { description('Deploy de install.sh a los SCOs de ${cliente} en la tienda seleccionada.') definition { cps { script(new File('/opt/jenkins/Jenkinsfile.${cliente}').text) sandbox(true) } } } " JOB_SCRIPT="$JOB_SCRIPT" yq eval -i '.jobs += [{"script": strenv(JOB_SCRIPT)}]' "$JENKINS_YAML" yq eval -i '(.jobs[-1].script) style="literal"' "$JENKINS_YAML" echo "" echo "Agregado volumen a $DOCKER_COMPOSE..." VOLUME_LINE="./data/jenkins/pipelines/Jenkinsfile.${cliente}:/opt/jenkins/Jenkinsfile.${cliente}:ro" VOLUME_LINE="$VOLUME_LINE" yq eval -i '.services.jenkins.volumes += [strenv(VOLUME_LINE)]' "$DOCKER_COMPOSE" echo "" echo "Cliente '${cliente}' creado:" echo " - Nodos: ${nodos[*]}" echo " - Jenkinsfile: $jenkinsfile" echo " - Job: deploy-${cliente}" echo " - Volumen agregado en docker-compose.yml" echo "" echo "Aplicá con: docker compose up -d --build"