feat: new client, store and docs

This commit is contained in:
aagusfernandez02
2026-07-06 11:44:56 -03:00
parent 326872d471
commit 71a6c49875
6 changed files with 587 additions and 25 deletions
+67 -17
View File
@@ -1,5 +1,16 @@
jenkins:
numExecutors: 2
nodeMonitors:
- "architecture"
- "clock"
- diskSpace:
freeSpaceThreshold: "0"
freeSpaceWarningThreshold: "0"
- "swapSpace"
- tmpSpace:
freeSpaceThreshold: "0"
freeSpaceWarningThreshold: "0"
- "responseTime"
authorizationStrategy:
loggedInUsersCanDoAnything:
allowAnonymousRead: false
@@ -10,7 +21,7 @@ jenkins:
- id: admin
password: H0l4mund0!
# Agentes permanentes (inbound/JNLP): cada SCO conecta outbound a Jenkins.
# Agentes permanentes (WebSocket): cada SCO conecta outbound a Jenkins via wss://.
# Nombre = identificador en Jenkins. Label "sco" agrupa todos los SCOs,
# luego cliente (ralph) y tienda (yabucoa / san-lorenzo / rio-grande).
nodes:
@@ -21,7 +32,8 @@ jenkins:
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound: {}
inbound:
webSocket: true
- permanent:
name: "ralph-yabucoa-sco03"
labelString: "sco ralph yabucoa"
@@ -29,7 +41,8 @@ jenkins:
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound: {}
inbound:
webSocket: true
- permanent:
name: "ralph-yabucoa-sco04"
labelString: "sco ralph yabucoa"
@@ -37,7 +50,8 @@ jenkins:
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound: {}
inbound:
webSocket: true
- permanent:
name: "ralph-yabucoa-sco05"
labelString: "sco ralph yabucoa"
@@ -45,7 +59,8 @@ jenkins:
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound: {}
inbound:
webSocket: true
- permanent:
name: "ralph-yabucoa-sco06"
labelString: "sco ralph yabucoa"
@@ -53,7 +68,8 @@ jenkins:
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound: {}
inbound:
webSocket: true
- permanent:
name: "ralph-yabucoa-sco07"
labelString: "sco ralph yabucoa"
@@ -61,7 +77,8 @@ jenkins:
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound: {}
inbound:
webSocket: true
- permanent:
name: "ralph-san-lorenzo-sco03"
labelString: "sco ralph san-lorenzo"
@@ -69,7 +86,8 @@ jenkins:
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound: {}
inbound:
webSocket: true
- permanent:
name: "ralph-san-lorenzo-sco04"
labelString: "sco ralph san-lorenzo"
@@ -77,7 +95,8 @@ jenkins:
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound: {}
inbound:
webSocket: true
- permanent:
name: "ralph-san-lorenzo-sco05"
labelString: "sco ralph san-lorenzo"
@@ -85,7 +104,8 @@ jenkins:
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound: {}
inbound:
webSocket: true
- permanent:
name: "ralph-san-lorenzo-sco06"
labelString: "sco ralph san-lorenzo"
@@ -93,7 +113,8 @@ jenkins:
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound: {}
inbound:
webSocket: true
- permanent:
name: "ralph-rio-grande-sco01"
labelString: "sco ralph rio-grande"
@@ -101,7 +122,8 @@ jenkins:
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound: {}
inbound:
webSocket: true
- permanent:
name: "ralph-rio-grande-sco02"
labelString: "sco ralph rio-grande"
@@ -109,7 +131,8 @@ jenkins:
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound: {}
inbound:
webSocket: true
- permanent:
name: "ralph-rio-grande-sco03"
labelString: "sco ralph rio-grande"
@@ -117,7 +140,8 @@ jenkins:
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound: {}
inbound:
webSocket: true
- permanent:
name: "ralph-rio-grande-sco04"
labelString: "sco ralph rio-grande"
@@ -125,7 +149,8 @@ jenkins:
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound: {}
inbound:
webSocket: true
- permanent:
name: "ralph-rio-grande-sco05"
labelString: "sco ralph rio-grande"
@@ -133,7 +158,8 @@ jenkins:
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound: {}
inbound:
webSocket: true
- permanent:
name: "ralph-rio-grande-sco06"
labelString: "sco ralph rio-grande"
@@ -141,7 +167,21 @@ jenkins:
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound: {}
inbound:
webSocket: true
- permanent:
name: "lab-oficina-sco-77"
labelString: "sco lab oficina"
remoteFS: "/opt/jenkins-agent"
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound:
webSocket: true
unclassified:
location:
url: https://jenkins.laoficina1782.com/
security:
globalJobDslSecurityConfiguration:
@@ -158,3 +198,13 @@ jobs:
}
}
}
- script: |
pipelineJob('deploy-lab') {
description('Deploy de install.sh a los SCOs de Deploy Lab en la tienda seleccionada.')
definition {
cps {
script(new File('/opt/jenkins/Jenkinsfile.lab').text)
sandbox(true)
}
}
}
+55
View File
@@ -0,0 +1,55 @@
pipeline {
agent none
parameters {
choice(
name: 'TIENDA',
choices: ['oficina'],
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 = "lab && ${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 'lab'"
}
}]
}
parallel deployStages
}
}
}
}
}
+12
View File
@@ -7,6 +7,11 @@ pipeline {
choices: ['yabucoa', 'san-lorenzo', 'rio-grande'],
description: 'Tienda destino'
)
string(
name: 'NODO',
defaultValue: '',
description: 'Nombre exacto del nodo a deployar (vacío = toda la tienda)'
)
}
stages {
@@ -24,6 +29,13 @@ pipeline {
script {
def labelExpr = "ralph && ${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}"
}
+203
View File
@@ -0,0 +1,203 @@
#!/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
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"
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
# Agrega un nodo permanente (SCO) a data/jenkins/jenkins.yaml.
#
# Uso:
# create-node.sh --cliente <cliente> --tienda <tienda> --numero <numero>
#
# Ejemplo:
# create-node.sh --cliente ralph --tienda yabucoa --numero 8
# -> nodo "ralph-yabucoa-sco008", label "sco ralph yabucoa"
#
# Por default valida que el cliente ya esté dado de alta (que exista
# Jenkinsfile.<cliente>) y que la tienda esté en su dropdown de TIENDA.
# --skip-checks salta ambas validaciones (lo usa create-client.sh al dar
# de alta un cliente nuevo, cuando el Jenkinsfile todavía no existe).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
JENKINS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
JENKINS_YAML="$JENKINS_DIR/jenkins.yaml"
PIPELINES_DIR="$JENKINS_DIR/pipelines"
cliente=""
tienda=""
numero=""
skip_checks=false
while [[ $# -gt 0 ]]; do
case "$1" in
--cliente) cliente="$2"; shift 2 ;;
--tienda) tienda="$2"; shift 2 ;;
--numero) numero="$2"; shift 2 ;;
--skip-checks) skip_checks=true; shift ;;
*) 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" && -n "$tienda" && -n "$numero" ]] || {
echo "Uso: $0 --cliente <cliente> --tienda <tienda> --numero <numero>" >&2
exit 1
}
[[ "$cliente" =~ ^[a-z0-9-]+$ ]] || { echo "cliente inválido: '$cliente' (solo minúsculas, números y guiones)" >&2; exit 1; }
[[ "$tienda" =~ ^[a-z0-9-]+$ ]] || { echo "tienda inválida: '$tienda' (solo minúsculas, números y guiones)" >&2; exit 1; }
[[ "$numero" =~ ^[0-9]+$ ]] || { echo "numero inválido: '$numero' (solo dígitos)" >&2; exit 1; }
numero_padded=$(printf '%03d' "$((10#$numero))")
node_name="${cliente}-${tienda}-sco${numero_padded}"
node_label="sco ${cliente} ${tienda}"
[[ -f "$JENKINS_YAML" ]] || { echo "No se encontró $JENKINS_YAML" >&2; exit 1; }
if [[ "$skip_checks" == false ]]; then
jenkinsfile="$PIPELINES_DIR/Jenkinsfile.${cliente}"
[[ -f "$jenkinsfile" ]] || {
echo "El cliente '$cliente' no está dado de alta (no existe $jenkinsfile)." >&2
echo "Usá create-client.sh para darlo de alta primero." >&2
exit 1
}
choices_line=$(grep -m1 "choices:" "$jenkinsfile" || true)
if [[ -n "$choices_line" ]] && ! grep -oE "'[a-z0-9-]+'" <<< "$choices_line" | tr -d "'" | grep -qx "$tienda"; then
echo "La tienda '$tienda' no está en el dropdown de $jenkinsfile" >&2
exit 1
fi
fi
if yq eval ".jenkins.nodes[].permanent.name" "$JENKINS_YAML" | grep -qx "$node_name"; then
echo "El nodo '$node_name' ya existe en $JENKINS_YAML" >&2
exit 1
fi
NODE_NAME="$node_name" NODE_LABEL="$node_label" yq eval -i '
.jenkins.nodes += [{
"permanent": {
"name": strenv(NODE_NAME),
"labelString": strenv(NODE_LABEL),
"remoteFS": "/opt/jenkins-agent",
"numExecutors": 1,
"retentionStrategy": "always",
"launcher": {
"inbound": {
"webSocket": true
}
}
}
}]
' "$JENKINS_YAML"
echo "Nodo creado: $node_name (label: $node_label)"
if [[ "$skip_checks" == false ]]; then
echo ""
echo "Revisá en jenkins.yaml > jenkins > nodes"
echo "Aplicá con: docker compose up -d --build"
fi