Files
2026-07-06 14:56:12 -03:00

224 lines
6.9 KiB
Bash

#!/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 <cliente> \
# --tienda <nombre> [--tienda <nombre> ...] \
# --nodo <tienda>:<numero> [--nodo <tienda>:<numero> ...]
#
# 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 <<EOF
Uso:
$(basename "$0") --cliente <cliente> \\
--tienda <nombre> [--tienda <nombre> ...] \\
--nodo <tienda>:<numero> [--nodo <tienda>:<numero> ...]
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 <tienda>:<numero>)" >&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" <<EOF
pipeline {
agent none
parameters {
choice(
name: 'TIENDA',
choices: [${choices}],
description: 'Tienda destino'
)
string(
name: 'NODO',
defaultValue: '',
description: 'Nombre exacto del nodo a deployar (vacío = toda la tienda)'
)
}
stages {
stage('Preparar scripts') {
agent { label 'built-in' }
steps {
sh 'cp /opt/deploy/install.sh .'
stash name: 'scripts', includes: 'install.sh'
}
}
stage('Deploy') {
agent { label 'built-in' }
steps {
script {
def labelExpr = "${cliente} \${params.TIENDA}"
def nodes = nodesByLabel(label: labelExpr, offline: false)
if (params.NODO?.trim()) {
def target = params.NODO.trim()
if (!nodes.contains(target)) {
error "El nodo '\${target}' no está online o no pertenece al label: \${labelExpr}"
}
nodes = [target]
}
if (nodes.isEmpty()) {
error "No hay nodos online con el label: \${labelExpr}"
}
def deployStages = nodes.collectEntries { nodeName ->
["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"