Compare commits

..

2 Commits

Author SHA1 Message Date
aagusfernandez02 71a6c49875 feat: new client, store and docs 2026-07-06 11:44:56 -03:00
aagusfernandez02 326872d471 refactor: deployment with cloudflare tunnel + nginx proxy manager 2026-07-06 11:43:03 -03:00
10 changed files with 646 additions and 100 deletions
+1
View File
@@ -0,0 +1 @@
CLOUDFLARE_TUNNEL_TOKEN=
+1
View File
@@ -0,0 +1 @@
.env
+150 -8
View File
@@ -14,15 +14,49 @@ Jenkins centralizado para el despliegue de software en terminales SCO (Self-Chec
├── deploy/ ├── deploy/
│ └── install.sh # Script desplegado en cada SCO │ └── install.sh # Script desplegado en cada SCO
└── pipelines/ └── pipelines/
── Jenkinsfile.ralph # Pipeline del cliente Ralph ── Jenkinsfile.ralph # Pipeline del cliente Ralph
└── Jenkinsfile.lab # Pipeline del entorno de laboratorio
``` ```
## Arquitectura ## Arquitectura
- **Jenkins master**: corre en Docker en un servidor (`172.19.13.204`). - **Jenkins master**: corre en Docker en un servidor (`172.19.13.204`). No expone puertos al host; solo es alcanzable dentro de la red interna Docker `proxy`.
- **Agentes (SCOs)**: cada terminal SCO conecta al master vía JNLP (puerto 50000). El proceso del agente corre como `root`. - **Nginx Proxy Manager (NPM)**: reverse proxy interno que resuelve el hostname `jenkins.laoficina1782.com` y hace forward hacia `jenkins-server:8080`. Corre en el mismo servidor, red `proxy`.
- **Cloudflare Tunnel (`cloudflared`)**: es el único punto de entrada desde internet. El contenedor `cloudflared` abre una conexión saliente hacia el edge de Cloudflare (autenticada con `TUNNEL_TOKEN`), por lo que **no hace falta abrir puertos entrantes en el firewall/router** del servidor. El tunnel está configurado del lado de Cloudflare para enrutar el hostname público hacia `http://npm:80` dentro de la red `proxy`.
- **Agentes (SCOs)**: cada terminal SCO conecta al master vía WebSocket (no JNLP), outbound hacia `wss://jenkins.laoficina1782.com/`. El proceso del agente corre como `root`.
- **Storage**: Minio almacena el software de cada cliente en buckets separados (ej: bucket `ralph`). - **Storage**: Minio almacena el software de cada cliente en buckets separados (ej: bucket `ralph`).
### Flujo de tráfico
```
Internet
Cloudflare Edge (DNS + proxy)
│ túnel saliente autenticado (TUNNEL_TOKEN)
cloudflared (contenedor, red "proxy", sin puertos publicados)
│ http://npm:80
NPM (nginx-proxy-manager) — vhost jenkins.laoficina1782.com
│ http://jenkins-server:8080
jenkins-server (sin puertos publicados al host)
```
Jenkins **no** expone puertos al host: NPM y `cloudflared` se comunican con él por nombre de contenedor dentro de la red Docker `proxy`. Como no hay puertos entrantes abiertos en el servidor, toda la superficie pública depende de Cloudflare (WAF, DNS, TLS terminado en el edge).
> ⚠️ NPM publica además `80`, `443` y `81` directamente al host (ver `docker-compose.yml`). Si el firewall del servidor permite esas conexiones, Jenkins/NPM también serían alcanzables sin pasar por Cloudflare. Para que el tunnel sea realmente el único punto de entrada, esos puertos deberían quedar bloqueados a nivel de firewall/red salvo acceso local.
### Variables de entorno
El tunnel necesita el token generado en el dashboard de Cloudflare (Zero Trust → Networks → Tunnels):
```sh
cp .env.example .env
# editar .env y setear CLOUDFLARE_TUNNEL_TOKEN=<token>
```
### Labels de agentes ### Labels de agentes
Formato: `sco <cliente> <tienda>` Formato: `sco <cliente> <tienda>`
@@ -39,7 +73,28 @@ Formato: `sco <cliente> <tienda>`
docker compose up -d --build docker compose up -d --build
``` ```
Jenkins queda disponible en `http://<server-ip>:8080` (admin / admin123). Jenkins queda disponible en `https://jenkins.laoficina1782.com` una vez configurados el proxy y el tunnel.
### Nginx Proxy Manager
Panel de administración: `http://172.19.13.204:81` (o localmente en el servidor).
Configuración del proxy host para Jenkins:
| Campo | Valor |
|-------|-------|
| Domain Names | `jenkins.laoficina1782.com` |
| Scheme | `http` |
| Forward Hostname | `jenkins-server` |
| Forward Port | `8080` |
| Websockets Support | ✅ activado |
### Cloudflare Tunnel
1. En el dashboard de Cloudflare (Zero Trust → Networks → Tunnels), crear un tunnel y copiar su token.
2. Configurar la ruta pública (Public Hostname) del tunnel: `jenkins.laoficina1782.com``http://npm:80`.
3. Setear el token en `.env` como `CLOUDFLARE_TUNNEL_TOKEN` (ver arriba).
4. Levantar el stack: el contenedor `cloudflared` se conecta solo, sin necesidad de abrir puertos en el servidor.
## Conectar un agente SCO ## Conectar un agente SCO
@@ -61,7 +116,8 @@ Jenkins queda disponible en `http://<server-ip>:8080` (admin / admin123).
[Service] [Service]
User=root User=root
ExecStart=/usr/bin/java -jar /opt/jenkins-agent/agent.jar \ ExecStart=/usr/bin/java -jar /opt/jenkins-agent/agent.jar \
-url http://<server-ip>:8080/ \ -url https://jenkins.laoficina1782.com/ \
-webSocket \
-secret <SECRET> \ -secret <SECRET> \
-name "<nombre-del-nodo>" \ -name "<nombre-del-nodo>" \
-workDir "/opt/jenkins-agent" -workDir "/opt/jenkins-agent"
@@ -80,10 +136,31 @@ Jenkins queda disponible en `http://<server-ip>:8080` (admin / admin123).
## Ejecutar un deploy ## Ejecutar un deploy
1. En Jenkins, abrir el job `deploy-ralph`. 1. En Jenkins, abrir el job `deploy-ralph` (o `deploy-lab` para el entorno de laboratorio).
2. Click en **Build with Parameters**. 2. Click en **Build with Parameters**.
3. Seleccionar la **tienda** destino en el dropdown (yabucoa / san-lorenzo / rio-grande). 3. Seleccionar la **tienda** destino en el dropdown (yabucoa / san-lorenzo / rio-grande para `deploy-ralph`; oficina para `deploy-lab`).
4. El pipeline despliega `install.sh ralph` en paralelo en todos los SCOs online de esa tienda. 4. Opcionalmente, completar **NODO** con el nombre exacto de un SCO (ej. `ralph-yabucoa-sco05`) para deployar solo en ese nodo. Si se deja vacío, despliega en paralelo en todos los SCOs online de la tienda seleccionada.
También se puede disparar desde consola con el Jenkins CLI o `curl`, ver [Ejecutar un job desde consola](#ejecutar-un-job-desde-consola).
### Ejecutar un job desde consola
```sh
curl -O https://jenkins.laoficina1782.com/jnlpJars/jenkins-cli.jar
java -jar jenkins-cli.jar -s https://jenkins.laoficina1782.com/ \
-auth TU_USUARIO:TU_API_TOKEN \
build deploy-lab -p TIENDA=oficina -s -v
# Deploy a un solo nodo de la tienda
java -jar jenkins-cli.jar -s https://jenkins.laoficina1782.com/ \
-auth TU_USUARIO:TU_API_TOKEN \
build deploy-lab -p TIENDA=oficina -p NODO=lab-oficina-sco-77 -s -v
```
- `-s` espera a que termine el build; `-v` muestra el log en consola.
- El API Token se genera en el usuario de Jenkins → Configure → API Token.
- Alternativa sin el CLI: `curl -X POST https://jenkins.laoficina1782.com/job/deploy-lab/buildWithParameters --user TU_USUARIO:TU_API_TOKEN --data-urlencode "TIENDA=oficina"`.
## Qué hace `install.sh` ## Qué hace `install.sh`
@@ -100,8 +177,73 @@ Recibe el nombre del cliente como argumento (`$1`):
9. Inicializa carpetas y archivos de base de datos del cpi-server. 9. Inicializa carpetas y archivos de base de datos del cpi-server.
10. Genera `engine.properties` para SymmetricDS (solo en primera instalación). 10. Genera `engine.properties` para SymmetricDS (solo en primera instalación).
## Scripts de alta (create-node.sh / create-client.sh)
`data/jenkins/scripts/` tiene dos scripts para no tener que editar `jenkins.yaml`, los Jenkinsfiles y `docker-compose.yml` a mano.
**Prerequisito**: [`yq`](https://github.com/mikefarah/yq) (la versión de Go, **no** la de Python — son incompatibles, ver más abajo) y que los scripts tengan permiso de ejecución:
```sh
chmod +x data/jenkins/scripts/*.sh
```
Ambos **solo editan archivos** — no hacen `docker compose up -d --build` ni commits. Después de correrlos, revisá con `git diff` y aplicá el rebuild vos mismo.
### create-node.sh — agregar un nodo a un cliente ya existente
```sh
data/jenkins/scripts/create-node.sh --cliente <cliente> --tienda <tienda> --numero <numero>
```
- Arma el nombre (`<cliente>-<tienda>-sco<numero>`, `numero` normalizado a 3 dígitos) y el label (`sco <cliente> <tienda>`) siguiendo la convención existente, y agrega el nodo al principio de `.jenkins.nodes` en `jenkins.yaml`.
- `cliente` y `tienda` deben ser minúsculas, números y guiones (`^[a-z0-9-]+$`) — nada de mayúsculas ni espacios.
- Valida que el cliente ya esté dado de alta (que exista `Jenkinsfile.<cliente>`) y que la tienda esté en su dropdown de `TIENDA`. Si falla cualquiera de las dos, corta sin tocar `jenkins.yaml`.
- Detecta nodos duplicados (mismo nombre final) y corta sin duplicar.
- `--skip-checks` salta las validaciones de cliente/tienda existentes — lo usa `create-client.sh` internamente al dar de alta un cliente nuevo (en ese momento el Jenkinsfile todavía no existe).
Ejemplo:
```sh
data/jenkins/scripts/create-node.sh --cliente ralph --tienda yabucoa --numero 8
# -> nodo "ralph-yabucoa-sco008", label "sco ralph yabucoa"
```
### create-client.sh — dar de alta un cliente nuevo completo
```sh
data/jenkins/scripts/create-client.sh --cliente <cliente> \
--tienda <nombre> [--tienda <nombre> ...] \
--nodo <tienda>:<numero> [--nodo <tienda>:<numero> ...]
```
- `--tienda` es repetible: declara cada tienda que va a aparecer en el dropdown del Jenkinsfile (se deduplica automáticamente si se repite).
- `--nodo` es repetible: uno por cada SCO a crear, en formato `<tienda>:<numero>`. Cada `tienda` referenciada en un `--nodo` tiene que haber sido declarada con algún `--tienda`.
- Corta antes de tocar nada si el cliente ya existe (ya hay `Jenkinsfile.<cliente>` o ya hay un job `deploy-<cliente>` en `jenkins.yaml`).
- Si falla la creación de algún nodo a mitad del loop (ej. `--nodo` repetido), revierte automáticamente los nodos ya creados en esa misma corrida antes de cortar — no deja nodos huérfanos.
- Al terminar, genera `data/jenkins/pipelines/Jenkinsfile.<cliente>` (mismo esqueleto que `Jenkinsfile.ralph`, con el parámetro `NODO` opcional), agrega el job `deploy-<cliente>` a `jenkins.yaml` y el volumen correspondiente a `docker-compose.yml`.
Ejemplo:
```sh
data/jenkins/scripts/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
```
### Troubleshooting
- **`usage: yq [-h] [--yaml-output] ...` / `yq: error: argument files: can't open '.jenkins...'`**: tenés instalado el `yq` de Python (kislyuk/yq), que usa sintaxis de `jq` y es incompatible. Desinstalalo (`pip uninstall yq`) e instalá el de Go:
```sh
wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/local/bin/yq
chmod +x /usr/local/bin/yq
hash -r # si ya habías corrido "yq" antes en esa misma terminal, bash puede tener cacheada la ruta vieja
yq --version # debe decir "yq (https://github.com/mikefarah/yq/) version v4..."
```
- **`Permission denied` al correr un script**: falta el bit de ejecución — `chmod +x data/jenkins/scripts/*.sh`.
- **`printf: NNN: invalid octal number`**: no debería pasar (ya está resuelto), pero si aparece es porque un `--numero` con cero a la izquierda se está interpretando como octal en vez de decimal.
## Agregar un nuevo cliente ## Agregar un nuevo cliente
Con los scripts (recomendado, ver arriba), o a mano siguiendo estos pasos:
1. Agregar los nodos del cliente en `data/jenkins/jenkins.yaml` con el label `sco <cliente> <tienda>`. 1. Agregar los nodos del cliente en `data/jenkins/jenkins.yaml` con el label `sco <cliente> <tienda>`.
2. Crear `data/jenkins/pipelines/Jenkinsfile.<cliente>` con el dropdown de tiendas. 2. Crear `data/jenkins/pipelines/Jenkinsfile.<cliente>` con el dropdown de tiendas.
3. Agregar el job en la sección `jobs` de `jenkins.yaml`. 3. Agregar el job en la sección `jobs` de `jenkins.yaml`.
+6 -71
View File
@@ -1,81 +1,16 @@
#!/bin/sh #!/bin/sh
### UPDATE SOFTWARE SERVER=minio-api.laoficina1782.com
SERVER=186.67.82.140
CUSTOMER=$1 CUSTOMER=$1
cat <<EOF > /tmp/rclone.conf cat <<CONF > /tmp/rclone.conf
[minio] [minio]
type = s3 type = s3
provider = Minio provider = Minio
access_key_id = minioadmin access_key_id = minioadmin
secret_access_key = LIGTNbUgQr secret_access_key = LIGTNbUgQr
endpoint = http://$SERVER:9000 endpoint = https://minio-api.laoficina1782.com
region = us-east-1 region = us-east-1
acl = private acl = private
EOF force_path_style = true
rclone --config /tmp/rclone.conf sync minio:$CUSTOMER /opt/sco/ --exclude "backend/docker/data/**" --progress --verbose CONF
find /opt/sco/ -type d -exec chmod 755 {} \; rclone --config /tmp/rclone.conf sync minio:$CUSTOMER /opt/sco/ --exclude "backend/docker/data/**" --s3-sign-accept-encoding=false --multi-thread-streams=0 --progress --verbose
find /opt/sco/ -type f -exec chmod 644 {} \;
chmod -R 755 /opt/sco/frontend/
### TMP FOLDER
cp /usr/lib/tmpfiles.d/fs-tmp.conf /etc/tmpfiles.d/
echo "D /tmp 1777 root root -" > /etc/tmpfiles.d/fs-tmp.conf
### SERVICE
find /opt/sco/resources/services/ -type f -exec sh -c 'cp "$1" "/etc/systemd/system/$(basename "$1")"' _ {} \;
find /opt/sco/resources/services/ -type f -exec sh -c 'systemctl enable "$(basename "$1")"' _ {} \;
systemctl daemon-reload
### AUTOSTART
rsync -a --delete /opt/sco/resources/autostart/ /home/sco/.config/autostart/
chown -R sco:users /home/sco/.config/autostart/*
chmod -R 644 /home/sco/.config/autostart/*
### CRONTAB
crontab -u root /opt/sco/resources/crontab/root.txt
crontab -u sco /opt/sco/resources/crontab/sco.txt
### PYTHON
find /opt/sco/resources/python/ -type d -iname "packages" | while read packages_dir; do
parent_dir=$(dirname "$packages_dir")
req_file="$parent_dir/requirements.txt"
if [ -f "$req_file" ]; then
pip install -q --no-index --find-links="$packages_dir" -r "$req_file"
fi
done
### DOCKER IMAGES
mkdir -p /tmp/images/
find /opt/sco/resources/images/ -type f -name '*.gz' -exec sh -c 'gunzip -c "$1" > /tmp/images/"$(basename "${1%.gz}")"' _ {} \;
find /tmp/images/ -type f -iname "*.tar" -exec docker load -i {} \;
### INIT CPI-SERVER DATABASES AND LOGS FOLDERS
mkdir -p /opt/sco/backend/docker/data/cpi-server/database
mkdir -p /opt/sco/backend/docker/data/cpi-server/logs/devices
mkdir -p /opt/sco/backend/docker/data/cpi-server/logs/service
touch /opt/sco/backend/docker/data/cpi-server/database/configuration.db
touch /opt/sco/backend/docker/data/cpi-server/database/connectivity.db
touch /opt/sco/backend/docker/data/cpi-server/database/Identity.db
touch /opt/sco/backend/docker/data/cpi-server/database/transaction.db
touch /opt/sco/backend/docker/data/cpi-server/database/trialdata.db
### INIT SYMETRIC CONFIGURATION FILE
mkdir -p /opt/sco/backend/docker/data/symmetric
if [ ! -f /opt/sco/backend/docker/data/symmetric/engine.properties ]; then
LOCAL_NODE=$(hostname)
LOCAL_IP=$(ip -4 addr show eth0 | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
cat <<EOF > /opt/sco/backend/docker/data/symmetric/engine.properties
engine.name=$LOCAL_NODE
group.id=fullmesh
external.id=$LOCAL_NODE
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://$LOCAL_IP:3306/sco
db.user=root
db.password=U2qs&U6LA9U2
registration.url=
sync.url=http://$LOCAL_IP:31415/sync/$LOCAL_NODE
start.pulled.job=true
job.purge.period.time.ms=7200000
EOF
fi
+67 -17
View File
@@ -1,5 +1,16 @@
jenkins: jenkins:
numExecutors: 2 numExecutors: 2
nodeMonitors:
- "architecture"
- "clock"
- diskSpace:
freeSpaceThreshold: "0"
freeSpaceWarningThreshold: "0"
- "swapSpace"
- tmpSpace:
freeSpaceThreshold: "0"
freeSpaceWarningThreshold: "0"
- "responseTime"
authorizationStrategy: authorizationStrategy:
loggedInUsersCanDoAnything: loggedInUsersCanDoAnything:
allowAnonymousRead: false allowAnonymousRead: false
@@ -10,7 +21,7 @@ jenkins:
- id: admin - id: admin
password: H0l4mund0! 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, # Nombre = identificador en Jenkins. Label "sco" agrupa todos los SCOs,
# luego cliente (ralph) y tienda (yabucoa / san-lorenzo / rio-grande). # luego cliente (ralph) y tienda (yabucoa / san-lorenzo / rio-grande).
nodes: nodes:
@@ -21,7 +32,8 @@ jenkins:
numExecutors: 1 numExecutors: 1
retentionStrategy: "always" retentionStrategy: "always"
launcher: launcher:
inbound: {} inbound:
webSocket: true
- permanent: - permanent:
name: "ralph-yabucoa-sco03" name: "ralph-yabucoa-sco03"
labelString: "sco ralph yabucoa" labelString: "sco ralph yabucoa"
@@ -29,7 +41,8 @@ jenkins:
numExecutors: 1 numExecutors: 1
retentionStrategy: "always" retentionStrategy: "always"
launcher: launcher:
inbound: {} inbound:
webSocket: true
- permanent: - permanent:
name: "ralph-yabucoa-sco04" name: "ralph-yabucoa-sco04"
labelString: "sco ralph yabucoa" labelString: "sco ralph yabucoa"
@@ -37,7 +50,8 @@ jenkins:
numExecutors: 1 numExecutors: 1
retentionStrategy: "always" retentionStrategy: "always"
launcher: launcher:
inbound: {} inbound:
webSocket: true
- permanent: - permanent:
name: "ralph-yabucoa-sco05" name: "ralph-yabucoa-sco05"
labelString: "sco ralph yabucoa" labelString: "sco ralph yabucoa"
@@ -45,7 +59,8 @@ jenkins:
numExecutors: 1 numExecutors: 1
retentionStrategy: "always" retentionStrategy: "always"
launcher: launcher:
inbound: {} inbound:
webSocket: true
- permanent: - permanent:
name: "ralph-yabucoa-sco06" name: "ralph-yabucoa-sco06"
labelString: "sco ralph yabucoa" labelString: "sco ralph yabucoa"
@@ -53,7 +68,8 @@ jenkins:
numExecutors: 1 numExecutors: 1
retentionStrategy: "always" retentionStrategy: "always"
launcher: launcher:
inbound: {} inbound:
webSocket: true
- permanent: - permanent:
name: "ralph-yabucoa-sco07" name: "ralph-yabucoa-sco07"
labelString: "sco ralph yabucoa" labelString: "sco ralph yabucoa"
@@ -61,7 +77,8 @@ jenkins:
numExecutors: 1 numExecutors: 1
retentionStrategy: "always" retentionStrategy: "always"
launcher: launcher:
inbound: {} inbound:
webSocket: true
- permanent: - permanent:
name: "ralph-san-lorenzo-sco03" name: "ralph-san-lorenzo-sco03"
labelString: "sco ralph san-lorenzo" labelString: "sco ralph san-lorenzo"
@@ -69,7 +86,8 @@ jenkins:
numExecutors: 1 numExecutors: 1
retentionStrategy: "always" retentionStrategy: "always"
launcher: launcher:
inbound: {} inbound:
webSocket: true
- permanent: - permanent:
name: "ralph-san-lorenzo-sco04" name: "ralph-san-lorenzo-sco04"
labelString: "sco ralph san-lorenzo" labelString: "sco ralph san-lorenzo"
@@ -77,7 +95,8 @@ jenkins:
numExecutors: 1 numExecutors: 1
retentionStrategy: "always" retentionStrategy: "always"
launcher: launcher:
inbound: {} inbound:
webSocket: true
- permanent: - permanent:
name: "ralph-san-lorenzo-sco05" name: "ralph-san-lorenzo-sco05"
labelString: "sco ralph san-lorenzo" labelString: "sco ralph san-lorenzo"
@@ -85,7 +104,8 @@ jenkins:
numExecutors: 1 numExecutors: 1
retentionStrategy: "always" retentionStrategy: "always"
launcher: launcher:
inbound: {} inbound:
webSocket: true
- permanent: - permanent:
name: "ralph-san-lorenzo-sco06" name: "ralph-san-lorenzo-sco06"
labelString: "sco ralph san-lorenzo" labelString: "sco ralph san-lorenzo"
@@ -93,7 +113,8 @@ jenkins:
numExecutors: 1 numExecutors: 1
retentionStrategy: "always" retentionStrategy: "always"
launcher: launcher:
inbound: {} inbound:
webSocket: true
- permanent: - permanent:
name: "ralph-rio-grande-sco01" name: "ralph-rio-grande-sco01"
labelString: "sco ralph rio-grande" labelString: "sco ralph rio-grande"
@@ -101,7 +122,8 @@ jenkins:
numExecutors: 1 numExecutors: 1
retentionStrategy: "always" retentionStrategy: "always"
launcher: launcher:
inbound: {} inbound:
webSocket: true
- permanent: - permanent:
name: "ralph-rio-grande-sco02" name: "ralph-rio-grande-sco02"
labelString: "sco ralph rio-grande" labelString: "sco ralph rio-grande"
@@ -109,7 +131,8 @@ jenkins:
numExecutors: 1 numExecutors: 1
retentionStrategy: "always" retentionStrategy: "always"
launcher: launcher:
inbound: {} inbound:
webSocket: true
- permanent: - permanent:
name: "ralph-rio-grande-sco03" name: "ralph-rio-grande-sco03"
labelString: "sco ralph rio-grande" labelString: "sco ralph rio-grande"
@@ -117,7 +140,8 @@ jenkins:
numExecutors: 1 numExecutors: 1
retentionStrategy: "always" retentionStrategy: "always"
launcher: launcher:
inbound: {} inbound:
webSocket: true
- permanent: - permanent:
name: "ralph-rio-grande-sco04" name: "ralph-rio-grande-sco04"
labelString: "sco ralph rio-grande" labelString: "sco ralph rio-grande"
@@ -125,7 +149,8 @@ jenkins:
numExecutors: 1 numExecutors: 1
retentionStrategy: "always" retentionStrategy: "always"
launcher: launcher:
inbound: {} inbound:
webSocket: true
- permanent: - permanent:
name: "ralph-rio-grande-sco05" name: "ralph-rio-grande-sco05"
labelString: "sco ralph rio-grande" labelString: "sco ralph rio-grande"
@@ -133,7 +158,8 @@ jenkins:
numExecutors: 1 numExecutors: 1
retentionStrategy: "always" retentionStrategy: "always"
launcher: launcher:
inbound: {} inbound:
webSocket: true
- permanent: - permanent:
name: "ralph-rio-grande-sco06" name: "ralph-rio-grande-sco06"
labelString: "sco ralph rio-grande" labelString: "sco ralph rio-grande"
@@ -141,7 +167,21 @@ jenkins:
numExecutors: 1 numExecutors: 1
retentionStrategy: "always" retentionStrategy: "always"
launcher: 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: security:
globalJobDslSecurityConfiguration: 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'], choices: ['yabucoa', 'san-lorenzo', 'rio-grande'],
description: 'Tienda destino' description: 'Tienda destino'
) )
string(
name: 'NODO',
defaultValue: '',
description: 'Nombre exacto del nodo a deployar (vacío = toda la tienda)'
)
} }
stages { stages {
@@ -24,6 +29,13 @@ pipeline {
script { script {
def labelExpr = "ralph && ${params.TIENDA}" def labelExpr = "ralph && ${params.TIENDA}"
def nodes = nodesByLabel(label: labelExpr, offline: false) 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()) { if (nodes.isEmpty()) {
error "No hay nodos online con el label: ${labelExpr}" 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
+51 -4
View File
@@ -1,4 +1,21 @@
services: services:
# minio:
# image: minio/minio:latest
# container_name: minio
# command: server /data --console-address ":9001"
# restart: unless-stopped
# environment:
# MINIO_ROOT_USER: minioadmin
# MINIO_ROOT_PASSWORD: LIGTNbUgQr
# ports:
# - "9000:9000" # API
# - "9001:9001" # Consola web
# volumes:
# - ./data/minio/data:/data
# #- ./data/certs:/root/.minio/certs:ro
# networks:
# - proxy
jenkins: jenkins:
build: build:
context: ./data/jenkins context: ./data/jenkins
@@ -7,14 +24,44 @@ services:
restart: unless-stopped restart: unless-stopped
environment: environment:
- JAVA_OPTS=-Djenkins.install.runSetupWizard=false - JAVA_OPTS=-Djenkins.install.runSetupWizard=false
ports: expose:
- "8080:8080" - "8080"
- "50000:50000"
volumes: volumes:
- jenkins_home:/var/jenkins_home - jenkins_home:/var/jenkins_home
- ./data/jenkins/jenkins.yaml:/usr/share/jenkins/ref/jenkins.yaml:ro - ./data/jenkins/jenkins.yaml:/usr/share/jenkins/ref/jenkins.yaml:ro
- ./data/jenkins/deploy:/opt/deploy:ro - ./data/jenkins/deploy:/opt/deploy:ro
- ./data/jenkins/pipelines/Jenkinsfile.ralph:/opt/jenkins/Jenkinsfile.ralph:ro - ./data/jenkins/pipelines/Jenkinsfile.ralph:/opt/jenkins/Jenkinsfile.ralph:ro
- ./data/jenkins/pipelines/Jenkinsfile.lab:/opt/jenkins/Jenkinsfile.lab:ro
networks:
- proxy
npm:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "81:81"
volumes:
- ./data/npm/data:/data
- ./data/npm/letsencrypt:/etc/letsencrypt
networks:
- proxy
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
networks:
- proxy
networks:
proxy:
driver: bridge
volumes: volumes:
jenkins_home: jenkins_home: