Compare commits

..

15 Commits

Author SHA1 Message Date
aagusfernandez02 1335ea1ebc feat(install.sh): update MC download command to follow redirects 2026-07-20 10:45:18 -03:00
aagusfernandez02 91ff9dca9b feat(install.sh): enhance installation script with MC download, service setup, and database initialization 2026-07-20 10:04:39 -03:00
aagusfernandez02 fc489c2115 docs: update README for the single deploy-sco job scheme
Reflect the move from per-client Jenkinsfiles/labels to one parameterized
job (NODO + CLIENTE), the mc-based install.sh, and flag create-node.sh /
create-client.sh as obsolete until they're rewritten for this scheme.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 10:03:57 -03:00
aagusfernandez02 71fe54b3e5 feat(docker-compose.yml): update pipeline configuration to use Jenkinsfile.sco for client deployment and remove unused labels 2026-07-20 09:58:03 -03:00
aagusfernandez02 8ee3824dd7 feat(Jenkinsfile.sco): add new pipeline for client deployment with parameter validation 2026-07-20 09:51:28 -03:00
aagusfernandez02 1d7e14bb74 fix(Jenkinsfile.ralph): update deployment script argument to 'mbs-ralphs' 2026-07-20 09:23:56 -03:00
aagusfernandez02 2152259140 feat(install.sh): changes install.sh for not using rclone 2026-07-20 09:23:27 -03:00
aagusfernandez02 529ba5e123 feat(jenkins.yaml): add new permanent agents for laboratorio-girboy 2026-07-20 09:22:59 -03:00
aagusfernandez02 6a3d13cdfc feat(backup): add script for backing up jenkins_home 2026-07-17 15:52:13 -03:00
aagusfernandez02 a9f1d79453 feat(Dockerfile): update base image to jdk21 2026-07-17 15:52:00 -03:00
aagusfernandez02 dbc56955c9 fix(docker-compose): use bind mount for jenkins_home 2026-07-17 15:51:46 -03:00
aagusfernandez02 7d51e8d012 feat(create-client): docs 2026-07-06 14:56:12 -03:00
aagusfernandez02 8ad6f9e126 docs: update readme 2026-07-06 11:48:04 -03:00
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
12 changed files with 642 additions and 138 deletions
+1
View File
@@ -0,0 +1 @@
CLOUDFLARE_TUNNEL_TOKEN=
+1
View File
@@ -0,0 +1 @@
.env
+97 -33
View File
@@ -14,24 +14,47 @@ 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.sco # Pipeline único de deploy (NODO + CLIENTE)
``` ```
## 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`.
- **Storage**: Minio almacena el software de cada cliente en buckets separados (ej: bucket `ralph`). - **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. El nombre del bucket es el mismo valor que se selecciona en el parámetro `CLIENTE` del deploy (ej: bucket `mbs-ralphs`).
### Labels de agentes ### Flujo de tráfico
Formato: `sco <cliente> <tienda>` ```
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)
```
| Cliente | Tienda | Agentes | 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).
|---|---|---|
| ralph | yabucoa | sco02sco07 | > ⚠️ 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.
| ralph | san-lorenzo | sco03sco06 |
| ralph | rio-grande | sco01sco06 | ### 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>
```
## Levantar el servidor ## Levantar el servidor
@@ -39,7 +62,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 +105,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,30 +125,49 @@ Jenkins queda disponible en `http://<server-ip>:8080` (admin / admin123).
## Ejecutar un deploy ## Ejecutar un deploy
1. En Jenkins, abrir el job `deploy-ralph`. Hay un único job, `deploy-sco`, que despliega a **un nodo puntual** (no a una tienda entera).
1. En Jenkins, abrir el job `deploy-sco`.
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. Completar **NODO** con el nombre exacto del agente a deployar (ej. `ralph-yabucoa-sco05`).
4. El pipeline despliega `install.sh ralph` en paralelo en todos los SCOs online de esa tienda. 4. Seleccionar **CLIENTE** en el dropdown — es el nombre del bucket de Minio que se sincroniza en ese nodo.
Ambos parámetros son obligatorios: si `NODO` queda vacío o `CLIENTE` queda en `SELECCIONAR_CLIENTE` (el placeholder inicial), el build falla explícitamente en vez de deployar por accidente. Para desplegar a varios nodos (ej. toda una tienda) hay que correr el job una vez por nodo.
> Nota: el **primer build** de este job (o de cualquier job parametrizado nuevo) corre antes de que Jenkins conozca los parámetros declarados en el Jenkinsfile — es una limitación de cómo Jenkins descubre los parámetros la primera vez. El pipeline lo detecta (`params.CLIENTE == null`) y termina como `NOT_BUILT` sin deployar nada; alcanza con volver a correr el job con **Build with Parameters** normalmente.
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-sco -p NODO=ralph-yabucoa-sco05 -p CLIENTE=mbs-ralphs -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-sco/buildWithParameters --user TU_USUARIO:TU_API_TOKEN --data-urlencode "NODO=ralph-yabucoa-sco05" --data-urlencode "CLIENTE=mbs-ralphs"`.
- Si se dispara por CLI/`curl` **omitiendo** `-p CLIENTE=...`, Jenkins no manda un valor vacío: rellena el parámetro con su default, que es `SELECCIONAR_CLIENTE` (el placeholder es a propósito el primer valor de la lista, para que un `CLIENTE` faltante falle en vez de deployar silenciosamente al primer cliente real de la lista).
## Qué hace `install.sh` ## Qué hace `install.sh`
Recibe el nombre del cliente como argumento (`$1`): Recibe el nombre del cliente como argumento (`$1`), que es el mismo valor que se selecciona en el parámetro `CLIENTE` del deploy:
1. Genera config de rclone apuntando al Minio local. 1. Da de alta (o reutiliza) un alias `mc` apuntando a Minio a través del hostname público `https://minio-api.laoficina1782.com`.
2. Sincroniza el bucket `$CUSTOMER` desde Minio hacia `/opt/sco/` en el SCO (excluyendo datos persistentes del backend). 2. Mirrorea el bucket `$CUSTOMER` desde Minio hacia `/opt/sco/` en el SCO (excluyendo datos persistentes del backend), con `--remove --overwrite` para dejar el destino idéntico al bucket.
3. Ajusta permisos en `/opt/sco/`.
4. Instala servicios systemd desde `/opt/sco/resources/services/`.
5. Configura autostart del usuario `sco`.
6. Instala crontabs de `root` y `sco`.
7. Instala paquetes Python offline.
8. Carga imágenes Docker.
9. Inicializa carpetas y archivos de base de datos del cpi-server.
10. Genera `engine.properties` para SymmetricDS (solo en primera instalación).
## Agregar un nuevo cliente Los pasos de instalación local (permisos, servicios systemd, autostart, crontabs, paquetes Python, imágenes Docker, inicialización de bases de datos del cpi-server, `engine.properties` de SymmetricDS) ya no están en este script — quedaron fuera del alcance actual de `install.sh`, que hoy solo se ocupa de traer el software del cliente a `/opt/sco/`.
1. Agregar los nodos del cliente en `data/jenkins/jenkins.yaml` con el label `sco <cliente> <tienda>`. ## Agregar un nodo o un cliente nuevo
2. Crear `data/jenkins/pipelines/Jenkinsfile.<cliente>` con el dropdown de tiendas.
3. Agregar el job en la sección `jobs` de `jenkins.yaml`. Desde que el deploy pasó a un job único (`deploy-sco`, con `NODO` + `CLIENTE` como parámetros en vez de un Jenkinsfile/label por cliente), dar de alta algo nuevo es mucho más simple y **ya no requiere tocar `docker-compose.yml` ni crear Jenkinsfiles**:
4. Montar el nuevo Jenkinsfile en `docker-compose.yml`.
5. Recrear el contenedor: `docker compose up -d --build`. - **Agregar un nodo** (a un cliente existente o nuevo): agregar una entrada `permanent` en `.jenkins.nodes` de `data/jenkins/jenkins.yaml` con el nombre del agente (no hace falta `labelString`, el deploy targetea por nombre exacto de nodo). Después conectar el agente físico siguiendo [Conectar un agente SCO](#conectar-un-agente-sco).
- **Agregar un cliente**: agregar el nombre del bucket a la lista `choices` del parámetro `CLIENTE` en `data/jenkins/pipelines/Jenkinsfile.sco`.
- En ambos casos, aplicar el cambio con `docker compose up -d --build` (el contenedor de Jenkins re-lee `jenkins.yaml` vía JCasC).
> ⚠️ `data/jenkins/scripts/create-node.sh` y `create-client.sh` quedaron **obsoletos**: automatizan el esquema viejo (un Jenkinsfile y un label `sco <cliente> <tienda>` por cliente), que ya no existe. No usarlos hasta reescribirlos para el esquema de `deploy-sco`.
+1 -1
View File
@@ -1,4 +1,4 @@
FROM jenkins/jenkins:2.541.3-jdk17 FROM jenkins/jenkins:2.541.3-jdk21
USER root USER root
RUN apt-get update && apt-get install -y curl git && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y curl git && rm -rf /var/lib/apt/lists/*
+10 -13
View File
@@ -1,19 +1,16 @@
#!/bin/sh #!/bin/sh
### DOWNLOAD MC
if [ ! -f /usr/local/bin/mc ]; then
curl -L https://dl.min.io/client/mc/release/linux-amd64/mc -o /usr/local/bin/mc
chmod +x /usr/local/bin/mc
fi
### UPDATE SOFTWARE ### UPDATE SOFTWARE
SERVER=186.67.82.140 SERVER=minio-api.laoficina1782.com
CUSTOMER=$1 CUSTOMER=$1
cat <<EOF > /tmp/rclone.conf mc --config-dir /tmp/mc-config alias set minio https://minio-api.laoficina1782.com minioadmin LIGTNbUgQr
[minio] mc --config-dir /tmp/mc-config mirror --remove --overwrite --exclude "backend/docker/data/**" minio/$CUSTOMER /opt/sco/
type = s3
provider = Minio
access_key_id = minioadmin
secret_access_key = LIGTNbUgQr
endpoint = http://$SERVER:9000
region = us-east-1
acl = private
EOF
rclone --config /tmp/rclone.conf sync minio:$CUSTOMER /opt/sco/ --exclude "backend/docker/data/**" --progress --verbose
find /opt/sco/ -type d -exec chmod 755 {} \; find /opt/sco/ -type d -exec chmod 755 {} \;
find /opt/sco/ -type f -exec chmod 644 {} \; find /opt/sco/ -type f -exec chmod 644 {} \;
chmod -R 755 /opt/sco/frontend/ chmod -R 755 /opt/sco/frontend/
@@ -64,7 +61,7 @@ touch /opt/sco/backend/docker/data/cpi-server/database/trialdata.db
mkdir -p /opt/sco/backend/docker/data/symmetric mkdir -p /opt/sco/backend/docker/data/symmetric
if [ ! -f /opt/sco/backend/docker/data/symmetric/engine.properties ]; then if [ ! -f /opt/sco/backend/docker/data/symmetric/engine.properties ]; then
LOCAL_NODE=$(hostname) LOCAL_NODE=$(hostname)
LOCAL_IP=$(ip -4 addr show eth0 | grep -oP '(?<=inet\s)\d+(\.\d+){3}') LOCAL_IP=$(ip route get 1.1.1.1 | grep -oP '(?<=src\s)\d+(\.\d+){3}')
cat <<EOF > /opt/sco/backend/docker/data/symmetric/engine.properties cat <<EOF > /opt/sco/backend/docker/data/symmetric/engine.properties
engine.name=$LOCAL_NODE engine.name=$LOCAL_NODE
group.id=fullmesh group.id=fullmesh
+76 -40
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
@@ -9,151 +20,176 @@ jenkins:
users: users:
- id: admin - id: admin
password: H0l4mund0! password: H0l4mund0!
# Agentes permanentes (WebSocket): cada SCO conecta outbound a Jenkins via wss://.
# Agentes permanentes (inbound/JNLP): cada SCO conecta outbound a Jenkins. # El deploy (Jenkinsfile.sco) targetea por nombre exacto de nodo (parámetro NODO),
# Nombre = identificador en Jenkins. Label "sco" agrupa todos los SCOs, # no por label, por eso los nodos no llevan labelString.
# luego cliente (ralph) y tienda (yabucoa / san-lorenzo / rio-grande).
nodes: nodes:
- permanent: - permanent:
name: "ralph-yabucoa-sco02" name: "ralph-yabucoa-sco02"
labelString: "sco ralph yabucoa"
remoteFS: "/opt/jenkins-agent" remoteFS: "/opt/jenkins-agent"
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"
remoteFS: "/opt/jenkins-agent" remoteFS: "/opt/jenkins-agent"
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"
remoteFS: "/opt/jenkins-agent" remoteFS: "/opt/jenkins-agent"
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"
remoteFS: "/opt/jenkins-agent" remoteFS: "/opt/jenkins-agent"
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"
remoteFS: "/opt/jenkins-agent" remoteFS: "/opt/jenkins-agent"
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"
remoteFS: "/opt/jenkins-agent" remoteFS: "/opt/jenkins-agent"
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"
remoteFS: "/opt/jenkins-agent" remoteFS: "/opt/jenkins-agent"
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"
remoteFS: "/opt/jenkins-agent" remoteFS: "/opt/jenkins-agent"
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"
remoteFS: "/opt/jenkins-agent" remoteFS: "/opt/jenkins-agent"
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"
remoteFS: "/opt/jenkins-agent" remoteFS: "/opt/jenkins-agent"
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"
remoteFS: "/opt/jenkins-agent" remoteFS: "/opt/jenkins-agent"
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"
remoteFS: "/opt/jenkins-agent" remoteFS: "/opt/jenkins-agent"
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"
remoteFS: "/opt/jenkins-agent" remoteFS: "/opt/jenkins-agent"
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"
remoteFS: "/opt/jenkins-agent" remoteFS: "/opt/jenkins-agent"
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"
remoteFS: "/opt/jenkins-agent" remoteFS: "/opt/jenkins-agent"
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"
remoteFS: "/opt/jenkins-agent" remoteFS: "/opt/jenkins-agent"
numExecutors: 1 numExecutors: 1
retentionStrategy: "always" retentionStrategy: "always"
launcher: launcher:
inbound: {} inbound:
webSocket: true
- permanent:
name: "laboratorio-girboy-sco81"
remoteFS: "/opt/jenkins-agent"
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound:
webSocket: true
- permanent:
name: "laboratorio-girboy-sco82"
remoteFS: "/opt/jenkins-agent"
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound:
webSocket: true
- permanent:
name: "laboratorio-girboy-sco83"
remoteFS: "/opt/jenkins-agent"
numExecutors: 1
retentionStrategy: "always"
launcher:
inbound:
webSocket: true
unclassified:
location:
url: https://jenkins.laoficina1782.com/
security: security:
globalJobDslSecurityConfiguration: globalJobDslSecurityConfiguration:
useScriptSecurity: false useScriptSecurity: false
jobs: jobs:
- script: | - script: |
pipelineJob('deploy-ralph') { pipelineJob('deploy-sco') {
description('Deploy de install.sh a los SCOs de Ralph en la tienda seleccionada.') description('Deploy de install.sh a un nodo SCO puntual, para el cliente seleccionado.')
definition { definition {
cps { cps {
script(new File('/opt/jenkins/Jenkinsfile.ralph').text) script(new File('/opt/jenkins/Jenkinsfile.sco').text)
sandbox(true) sandbox(true)
} }
} }
-44
View File
@@ -1,44 +0,0 @@
pipeline {
agent none
parameters {
choice(
name: 'TIENDA',
choices: ['yabucoa', 'san-lorenzo', 'rio-grande'],
description: 'Tienda destino'
)
}
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 = "ralph && ${params.TIENDA}"
def nodes = nodesByLabel(label: labelExpr, offline: false)
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 'ralph'"
sh 'cat ~/sco-app/installed.txt'
}
}]
}
parallel deployStages
}
}
}
}
}
+70
View File
@@ -0,0 +1,70 @@
pipeline {
agent none
parameters {
string(
name: 'NODO',
defaultValue: '',
description: 'Nombre exacto del nodo a deployar'
)
choice(
name: 'CLIENTE',
choices: [
'SELECCIONAR_CLIENTE',
'cencosud-jumbo',
'censosud-staisabel',
'changomas-hiper',
'changomas-super',
'dibal',
'hisense',
'lsco',
'mbs-econo',
'mbs-famcoop',
'mbs-ralphs',
'mbs-supermax',
'modatelas',
'walmart-chile-acuenta',
'walmart-chile-lider',
'wm-mexico-bodega',
'wm-mexico-walmart'
],
description: 'Cliente para el cual se ejecuta el deploy'
)
}
stages {
stage('Registro de parámetros') {
agent none
when { expression { params.CLIENTE == null } }
steps {
echo 'Primer build: Jenkins está registrando los parámetros de este Jenkinsfile. No se ejecuta ningún deploy. Volvé a correr el job con "Build with Parameters".'
script { currentBuild.result = 'NOT_BUILT' }
}
}
stage('Preparar scripts') {
agent { label 'built-in' }
when { expression { params.CLIENTE != null } }
steps {
sh 'cp /opt/deploy/install.sh .'
stash name: 'scripts', includes: 'install.sh'
}
}
stage('Deploy') {
agent { label 'built-in' }
when { expression { params.CLIENTE != null } }
steps {
script {
if (!params.NODO?.trim()) {
error "El parámetro NODO es obligatorio"
}
if (params.CLIENTE == 'SELECCIONAR_CLIENTE') {
error "El parámetro CLIENTE es obligatorio"
}
def target = params.NODO.trim()
node(target) {
unstash 'scripts'
sh "bash install.sh '${params.CLIENTE.trim()}'"
}
}
}
}
}
}
+223
View File
@@ -0,0 +1,223 @@
#!/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"
+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
+50 -7
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,40 @@ 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 - ./data/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.sco:/opt/jenkins/Jenkinsfile.sco:ro
networks:
- proxy
volumes: npm:
jenkins_home: 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
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
set -euo pipefail
SRC_DIR=/opt/server/data
BACKUP_DIR=/opt/server/backups/jenkins_home
KEEP=4
mkdir -p "$BACKUP_DIR"
STAMP=$(date +%Y%m%d)
tar czf "$BACKUP_DIR/jenkins_home_$STAMP.tar.gz" -C "$SRC_DIR" jenkins_home
ls -1t "$BACKUP_DIR"/jenkins_home_*.tar.gz | tail -n +$((KEEP + 1)) | xargs -r rm --