From 748704317bb8d00fb43d00d5120fbb05435d6487 Mon Sep 17 00:00:00 2001 From: aagusfernandez02 Date: Wed, 1 Jul 2026 08:13:57 -0300 Subject: [PATCH] feat: jenkins server --- README.md | 225 +++++++++++++++++++++++ data/jenkins/Dockerfile | 17 ++ data/jenkins/deploy/install.sh | 81 ++++++++ data/jenkins/jenkins.yaml | 160 ++++++++++++++++ data/jenkins/pipelines/Jenkinsfile.ralph | 44 +++++ docker-compose.yml | 20 ++ 6 files changed, 547 insertions(+) create mode 100644 README.md create mode 100644 data/jenkins/Dockerfile create mode 100644 data/jenkins/deploy/install.sh create mode 100644 data/jenkins/jenkins.yaml create mode 100644 data/jenkins/pipelines/Jenkinsfile.ralph create mode 100644 docker-compose.yml diff --git a/README.md b/README.md new file mode 100644 index 0000000..555a364 --- /dev/null +++ b/README.md @@ -0,0 +1,225 @@ +# Jenkins SCO — Deploy Server + +Jenkins centralizado para el despliegue de software en terminales SCO (Self-Checkout) en producción. + +## Estructura del repositorio + +``` +. +├── docker-compose.yml +└── data/ + └── jenkins/ + ├── Dockerfile # Imagen Jenkins con plugins + ├── jenkins.yaml # Configuración JCasC (nodos, seguridad, jobs) + ├── deploy/ + │ └── install.sh # Script desplegado en cada SCO + └── pipelines/ + └── Jenkinsfile.ralph # Pipeline del cliente Ralph +``` + +## Arquitectura + +- **Jenkins master**: corre en Docker en el servidor Minio (`186.67.82.140`). +- **Agentes (SCOs)**: cada terminal SCO conecta al master vía JNLP (puerto 50000). El proceso del agente corre como `root`. +- **Storage**: Minio almacena el software de cada cliente en buckets separados (ej: bucket `ralph`). + +### Labels de agentes + +Formato: `sco ` + +| Cliente | Tienda | Agentes | +|---|---|---| +| ralph | yabucoa | sco02–sco07 | +| ralph | san-lorenzo | sco03–sco06 | +| ralph | rio-grande | sco01–sco06 | + +## Levantar el servidor + +```sh +docker compose up -d --build +``` + +Jenkins queda disponible en `http://:8080` (admin / H0l4mund0!). + +## Conectar un agente SCO + +1. Descargar el agente desde Jenkins: + ```sh + curl -sO http://:8080/jnlpJars/agent.jar + mv agent.jar /opt/jenkins-agent/agent.jar + ``` + +2. Obtener el secret del nodo: en Jenkins → *Manage Jenkins → Nodes → (nombre del nodo)*. + +3. Crear el servicio systemd en el SCO: + ```ini + # /etc/systemd/system/jenkins-agent.service + [Unit] + Description=Jenkins Agent + After=network.target + + [Service] + User=root + ExecStart=/usr/bin/java -jar /opt/jenkins-agent/agent.jar \ + -url http://:8080/ \ + -secret \ + -name "" \ + -workDir "/opt/jenkins-agent" + Restart=always + RestartSec=10 + + [Install] + WantedBy=multi-user.target + ``` + +4. Habilitar y arrancar: + ```sh + systemctl daemon-reload + systemctl enable --now jenkins-agent + ``` + +## Ejecutar un deploy + +1. En Jenkins, abrir el job `deploy-ralph`. +2. Click en **Build with Parameters**. +3. Seleccionar la **tienda** destino en el dropdown (yabucoa / san-lorenzo / rio-grande). +4. El pipeline despliega `install.sh ralph` en paralelo en todos los SCOs online de esa tienda. + +## Qué hace `install.sh` + +Recibe el nombre del cliente como argumento (`$1`): + +1. Genera config de rclone apuntando al Minio local. +2. Sincroniza el bucket `$CUSTOMER` desde Minio hacia `/opt/sco/` en el SCO (excluyendo datos persistentes del backend). +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). + +## Operaciones de mantenimiento + +### Agregar un nuevo cliente + +El deploy es idéntico para todos los clientes: el `install.sh` recibe el nombre del cliente como `$1`. +Solo cambia el nombre del job, el Jenkinsfile, y los nodos registrados. + +**1. Registrar los nodos en `data/jenkins/jenkins.yaml`** + +Bajo la clave `nodes`, añadir un bloque por cada SCO del cliente: + +```yaml +- permanent: + name: "nuevocliente-tienda1-sco01" + labelString: "sco nuevocliente tienda1" + remoteFS: "/opt/jenkins-agent" + numExecutors: 1 + retentionStrategy: "always" + launcher: + inbound: {} +``` + +Repetir para cada combinación tienda/SCO. Convención de nombres: +- Nombre del nodo: `--` (ej: `ralph-yabucoa-sco02`) +- Label: `sco ` (ej: `sco ralph yabucoa`) + +**2. Crear el Jenkinsfile en `data/jenkins/pipelines/Jenkinsfile.`** + +Copiar `Jenkinsfile.ralph` y ajustar dos cosas: + +```groovy +// En parameters: listar las tiendas del nuevo cliente +choices: ['tienda1', 'tienda2', 'tienda3'], + +// En el stage Deploy: cambiar el nombre del cliente (dos lugares) +def labelExpr = "nuevocliente && ${params.TIENDA}" +sh "bash install.sh 'nuevocliente'" +``` + +**3. Agregar el job en `data/jenkins/jenkins.yaml`** + +Al final de la sección `jobs`: + +```yaml +- script: | + pipelineJob('deploy-nuevocliente') { + description('Deploy de install.sh a los SCOs de NuevoCliente.') + definition { + cps { + script(new File('/opt/jenkins/Jenkinsfile.nuevocliente').text) + sandbox(true) + } + } + } +``` + +**4. Montar el Jenkinsfile en `docker-compose.yml`** + +```yaml +volumes: + - ./data/jenkins/pipelines/Jenkinsfile.nuevocliente:/opt/jenkins/Jenkinsfile.nuevocliente:ro +``` + +**5. Recrear el contenedor** + +```sh +docker compose up -d --build +``` + +En el servidor remoto: + +```sh +# Copiar los archivos modificados al servidor primero, luego: +docker compose up -d --build +``` + +--- + +### Agregar una tienda a un cliente existente + +**1. Añadir los nodos de la nueva tienda en `jenkins.yaml`** (un bloque por SCO, igual que arriba). + +**2. Agregar la tienda al dropdown del Jenkinsfile del cliente:** + +```groovy +choices: ['yabucoa', 'san-lorenzo', 'rio-grande', 'nueva-tienda'], +``` + +**3. Aplicar en el servidor:** + +```sh +docker compose restart jenkins +``` + +No hace falta rebuild completo si solo cambia `jenkins.yaml` o el Jenkinsfile — el contenedor los monta como volúmenes y JCasC los recarga. + +> Si agregaste nodos nuevos pero no reiniciaste Jenkins, también podés recargar desde la UI: *Manage Jenkins → Configuration as Code → Reload existing configuration*. + +--- + +### Agregar un SCO a una tienda existente + +Solo tocar `jenkins.yaml`: añadir el nodo nuevo bajo `nodes`. + +```yaml +- permanent: + name: "ralph-yabucoa-sco08" + labelString: "sco ralph yabucoa" + remoteFS: "/opt/jenkins-agent" + numExecutors: 1 + retentionStrategy: "always" + launcher: + inbound: {} +``` + +Luego recargar la configuración: + +```sh +docker compose restart jenkins +# o desde la UI: Manage Jenkins → Configuration as Code → Reload existing configuration +``` + +Una vez que Jenkins lo reconoce, conectar el agente en el SCO físico (ver sección "Conectar un agente SCO"). diff --git a/data/jenkins/Dockerfile b/data/jenkins/Dockerfile new file mode 100644 index 0000000..197b290 --- /dev/null +++ b/data/jenkins/Dockerfile @@ -0,0 +1,17 @@ +FROM jenkins/jenkins:2.541.3-jdk17 + +USER root +RUN apt-get update && apt-get install -y curl git && rm -rf /var/lib/apt/lists/* + +USER jenkins + +RUN jenkins-plugin-cli --plugins \ + configuration-as-code \ + credentials-binding \ + git \ + workflow-aggregator \ + job-dsl \ + pipeline-utility-steps + +COPY jenkins.yaml /usr/share/jenkins/ref/jenkins.yaml +ENV CASC_JENKINS_CONFIG=/usr/share/jenkins/ref/jenkins.yaml diff --git a/data/jenkins/deploy/install.sh b/data/jenkins/deploy/install.sh new file mode 100644 index 0000000..dfb6a5e --- /dev/null +++ b/data/jenkins/deploy/install.sh @@ -0,0 +1,81 @@ +#!/bin/sh + +### UPDATE SOFTWARE +SERVER=186.67.82.140 +CUSTOMER=$1 +cat < /tmp/rclone.conf +[minio] +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 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 < /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 \ No newline at end of file diff --git a/data/jenkins/jenkins.yaml b/data/jenkins/jenkins.yaml new file mode 100644 index 0000000..524c8a7 --- /dev/null +++ b/data/jenkins/jenkins.yaml @@ -0,0 +1,160 @@ +jenkins: + numExecutors: 2 + authorizationStrategy: + loggedInUsersCanDoAnything: + allowAnonymousRead: false + securityRealm: + local: + allowsSignup: false + users: + - id: admin + password: H0l4mund0! + + # Agentes permanentes (inbound/JNLP): cada SCO conecta outbound a Jenkins. + # Nombre = identificador en Jenkins. Label "sco" agrupa todos los SCOs, + # luego cliente (ralph) y tienda (yabucoa / san-lorenzo / rio-grande). + nodes: + - permanent: + name: "ralph-yabucoa-sco02" + labelString: "sco ralph yabucoa" + remoteFS: "/opt/jenkins-agent" + numExecutors: 1 + retentionStrategy: "always" + launcher: + inbound: {} + - permanent: + name: "ralph-yabucoa-sco03" + labelString: "sco ralph yabucoa" + remoteFS: "/opt/jenkins-agent" + numExecutors: 1 + retentionStrategy: "always" + launcher: + inbound: {} + - permanent: + name: "ralph-yabucoa-sco04" + labelString: "sco ralph yabucoa" + remoteFS: "/opt/jenkins-agent" + numExecutors: 1 + retentionStrategy: "always" + launcher: + inbound: {} + - permanent: + name: "ralph-yabucoa-sco05" + labelString: "sco ralph yabucoa" + remoteFS: "/opt/jenkins-agent" + numExecutors: 1 + retentionStrategy: "always" + launcher: + inbound: {} + - permanent: + name: "ralph-yabucoa-sco06" + labelString: "sco ralph yabucoa" + remoteFS: "/opt/jenkins-agent" + numExecutors: 1 + retentionStrategy: "always" + launcher: + inbound: {} + - permanent: + name: "ralph-yabucoa-sco07" + labelString: "sco ralph yabucoa" + remoteFS: "/opt/jenkins-agent" + numExecutors: 1 + retentionStrategy: "always" + launcher: + inbound: {} + - permanent: + name: "ralph-san-lorenzo-sco03" + labelString: "sco ralph san-lorenzo" + remoteFS: "/opt/jenkins-agent" + numExecutors: 1 + retentionStrategy: "always" + launcher: + inbound: {} + - permanent: + name: "ralph-san-lorenzo-sco04" + labelString: "sco ralph san-lorenzo" + remoteFS: "/opt/jenkins-agent" + numExecutors: 1 + retentionStrategy: "always" + launcher: + inbound: {} + - permanent: + name: "ralph-san-lorenzo-sco05" + labelString: "sco ralph san-lorenzo" + remoteFS: "/opt/jenkins-agent" + numExecutors: 1 + retentionStrategy: "always" + launcher: + inbound: {} + - permanent: + name: "ralph-san-lorenzo-sco06" + labelString: "sco ralph san-lorenzo" + remoteFS: "/opt/jenkins-agent" + numExecutors: 1 + retentionStrategy: "always" + launcher: + inbound: {} + - permanent: + name: "ralph-rio-grande-sco01" + labelString: "sco ralph rio-grande" + remoteFS: "/opt/jenkins-agent" + numExecutors: 1 + retentionStrategy: "always" + launcher: + inbound: {} + - permanent: + name: "ralph-rio-grande-sco02" + labelString: "sco ralph rio-grande" + remoteFS: "/opt/jenkins-agent" + numExecutors: 1 + retentionStrategy: "always" + launcher: + inbound: {} + - permanent: + name: "ralph-rio-grande-sco03" + labelString: "sco ralph rio-grande" + remoteFS: "/opt/jenkins-agent" + numExecutors: 1 + retentionStrategy: "always" + launcher: + inbound: {} + - permanent: + name: "ralph-rio-grande-sco04" + labelString: "sco ralph rio-grande" + remoteFS: "/opt/jenkins-agent" + numExecutors: 1 + retentionStrategy: "always" + launcher: + inbound: {} + - permanent: + name: "ralph-rio-grande-sco05" + labelString: "sco ralph rio-grande" + remoteFS: "/opt/jenkins-agent" + numExecutors: 1 + retentionStrategy: "always" + launcher: + inbound: {} + - permanent: + name: "ralph-rio-grande-sco06" + labelString: "sco ralph rio-grande" + remoteFS: "/opt/jenkins-agent" + numExecutors: 1 + retentionStrategy: "always" + launcher: + inbound: {} + +security: + globalJobDslSecurityConfiguration: + useScriptSecurity: false + +jobs: + - script: | + pipelineJob('deploy-ralph') { + description('Deploy de install.sh a los SCOs de Ralph en la tienda seleccionada.') + definition { + cps { + script(new File('/opt/jenkins/Jenkinsfile.ralph').text) + sandbox(true) + } + } + } diff --git a/data/jenkins/pipelines/Jenkinsfile.ralph b/data/jenkins/pipelines/Jenkinsfile.ralph new file mode 100644 index 0000000..85f17b5 --- /dev/null +++ b/data/jenkins/pipelines/Jenkinsfile.ralph @@ -0,0 +1,44 @@ +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 + } + } + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..89efe9a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +services: + jenkins: + build: + context: ./data/jenkins + dockerfile: Dockerfile + container_name: jenkins-server + restart: unless-stopped + environment: + - JAVA_OPTS=-Djenkins.install.runSetupWizard=false + ports: + - "8080:8080" + - "50000:50000" + volumes: + - jenkins_home:/var/jenkins_home + - ./data/jenkins/jenkins.yaml:/usr/share/jenkins/ref/jenkins.yaml:ro + - ./data/jenkins/deploy:/opt/deploy:ro + - ./data/jenkins/pipelines/Jenkinsfile.ralph:/opt/jenkins/Jenkinsfile.ralph:ro + +volumes: + jenkins_home: