images-dynamics-pi
1. Descripción general
Según el pom.xml, el proyecto se describe como "Load images to Dynamics". Es un microservicio batch (runner) que toma las imágenes de producto subidas a una carpeta compartida de Google Drive, las sube por SFTP tanto al ERP (Dynamics) como a un directorio de BI, y genera además una versión redimensionada y comprimida de cada imagen para un tercer destino.
El CLAUDE.md del repositorio describe una arquitectura distinta y ya no vigente (descarga de un CSV de Saleslayer vía HTTP y envío directo a un único FTP); esa lógica corresponde a ImagesDynamicsPiOldRunner, que está desactivada en el código actual (ver hallazgo en la sección 13).
2. Información técnica
| Campo | Valor |
|---|---|
artifactId | images-dynamics-pi |
groupId | com.hawkersco |
version | 1.0.25 |
| Java | 25 (maven.compiler.release=25) |
| Spring Boot | 4.0.6 |
| Tipo de artefacto | jar (ejecutable, Spring Boot batch/CLI) |
| Módulos | No aplica (proyecto de módulo único) |
3. Arquitectura y diseño
No es una API REST: es una aplicación Spring Boot CLI con dos CommandLineRunner activos, ejecutados en orden vía Ordered, más un tercer runner legado desactivado.
Paquetes principales:
com.hawkersco.imagesdynamicspi— clase principal (ImagesDynamicsPiApplication,@ConfigurationPropertiesScan) y los tres runners..config—GoogleDriveConfig(beanDriveautenticado con cuenta de servicio delegada).ImageProperties— record@ConfigurationProperties(prefijoimages.ftp), usado únicamente por el runner desactivado.
flowchart TD
A["1. ImagesDynamicsPiRunner"] -->|lista archivos no procesados| B[Google Drive<br/>carpeta 0AND09-6ovgdrUk9PVA]
B -->|descarga imagen| C[images/]
C -->|SFTP| D[ERP: /src/erp/products/]
C -->|SFTP| E[BI: /src/bi/]
A -->|marca appProperties.processed=true| B
F["2. ImagesResizePiRunner"] -->|lista archivos no redimensionados| B
F -->|descarga + Thumbnails resize a 180px| G[resize/]
G -->|SFTP| H[Resized: /src/resized/]
F -->|marca appProperties.resized=true| B
Ambos runners activos consultan la misma carpeta de Drive con flags de appProperties independientes (processed para el runner 1, resized para el runner 2), por lo que una misma imagen pasa por ambos flujos de forma independiente.
4. Dependencias principales
| Dependencia | Propósito |
|---|---|
spring-boot-starter | Núcleo de Spring Boot (sin web, es un runner CLI) |
com.google.apis:google-api-services-drive | Cliente de la API de Google Drive (listado/descarga/actualización de metadatos) |
com.google.apis:google-api-services-sheets | Declarada en el pom.xml; sin uso detectado en el código actual |
net.coobird:thumbnailator:0.4.21 | Redimensionado y compresión de imágenes (ImagesResizePiRunner) |
com.hawkersco:pi-function-commons:1.0.25-SNAPSHOT | DirectoryUtils, SftpUtils |
com.hawkersco:slack-client:1.0.25-SNAPSHOT | Usada solo por el runner desactivado ImagesDynamicsPiOldRunner (ver sección 13) |
spring-boot-starter-test (test) | JUnit 5 + Spring Test |
5. API / Endpoints
No aplica a este proyecto. Es un batch/runner sin capa REST.
6. Integraciones externas
| Sistema | Protocolo | Dirección | Detalle |
|---|---|---|---|
Google Drive (carpeta 0AND09-6ovgdrUk9PVA) | HTTP (API de Google Drive) | Entrante/Saliente | Descarga de imágenes pendientes y marcado de appProperties tras procesarlas |
Servidor SFTP (hawkers-images.hawkersco.net) | SFTP (SftpUtils) | Saliente | Subida a /src/erp/products/ (ERP), /src/bi/ (BI) y /src/resized/ (miniaturas) |
7. Configuración
En producción (application-pro.properties) las credenciales llegan por variables de entorno inyectadas como Secret de Kubernetes; en local (application.properties) el repositorio contiene actualmente valores reales hardcodeados (ver alerta de seguridad).
| Clave | Descripción | Ejemplo (producción) |
|---|---|---|
images.ftp.server / .user / .pass / .port | Conexión SFTP compartida por los tres runners | ${imagesFtpServer}, etc. |
images.ftp.dir-erp | Directorio remoto destino para el ERP | /src/erp/products/ |
images.ftp.dir-bi | Directorio remoto destino para BI | /src/bi/ |
images.ftp.dir-resized | Directorio remoto destino para las miniaturas | /src/resized/ |
slack.client.url / .auth.token / .channel.id | Configuración del cliente Slack (solo consumida por el runner desactivado) | ${slackClientUrl}, etc. |
La autenticación contra Google Drive no usa una propiedad de configuración: se resuelve en tiempo de ejecución mediante GoogleCredentials.getApplicationDefault() con delegación a la cuenta de servicio gcs-pi-kafka@pi-saldum.iam.gserviceaccount.com, tomando las credenciales del fichero montado en /etc/gcp/sa_credentials.json en el CronJob.
⚠️ Alerta de seguridad
El fichero src/main/resources/application.properties (perfil local) contiene actualmente credenciales reales en texto plano: contraseña del servidor SFTP de imágenes y token de bot de Slack (xoxb-...). Ninguno de estos valores se ha reproducido en este documento. Se recomienda:
- Rotar la contraseña SFTP y el token de Slack expuestos.
- Sustituir los valores hardcodeados de
application.propertiespor credenciales de un entorno de desarrollo aislado. - Revisar el historial de control de versiones, ya que estas credenciales pueden seguir expuestas en commits anteriores.
8. Persistencia
No aplica a este proyecto. No usa base de datos: el estado de "procesado"/"redimensionado" se guarda como appProperties en los propios metadatos de los ficheros de Google Drive, y los datos en tránsito son ficheros locales temporales (images/, resize/).
9. Procesos programados y mensajería
No hay @Scheduled ni listeners de colas: la periodicidad la impone el CronJob de Kubernetes (k8s/cronjob.yaml), que ejecuta el contenedor una vez al día a las 18:00 (schedule: "0 18 * * *", zona horaria Europe/Madrid, concurrencyPolicy: Forbid, activeDeadlineSeconds: 18000). Se ejecutan en orden los 2 runners activos:
| Orden | Runner | Función |
|---|---|---|
| 1 | ImagesDynamicsPiRunner | Descarga imágenes nuevas de Drive, las sube al ERP y a BI, marca processed=true |
| 2 | ImagesResizePiRunner | Descarga imágenes no redimensionadas de Drive, genera una miniatura de 180px de ancho ajustando iterativamente la calidad JPEG hasta no superar ~4519 bytes, la sube al destino resized, marca resized=true |
Ambos runners agrupan los ficheros por código de producto (extraído del nombre de fichero, cortando en el último -) y numeran secuencialmente las imágenes de un mismo producto al construir el nombre destino en el ERP ({código}_000_00{secuencia}.{extensión}), aunque solo ImagesDynamicsPiRunner usa esa numeración secuencial; ImagesResizePiRunner sube cada imagen con su nombre original.
10. Ejecución en local
Requisitos previos: JDK 25, Maven, credenciales de aplicación por defecto de Google con permiso de delegación sobre gcs-pi-kafka@pi-saldum.iam.gserviceaccount.com, y credenciales SFTP válidas.
# Compilar sin tests
mvn -B -DskipTests clean install
# Compilar con tests
mvn clean install
# Ejecutar tests
mvn test
# Ejecutar un test concreto
mvn test -Dtest=ImagesDynamicsPiApplicationTests
# Build Docker local
docker build -t images-dynamics-pi .
Al ser un CommandLineRunner, no expone Actuator/health: la forma de verificar la ejecución es revisar el log de consola o comprobar en Drive que los ficheros procesados llevan los appProperties processed/resized.
11. Despliegue
- Imagen: construida con
jib-maven-plugin(baseeclipse-temurin:25-jre,containerizingMode=packaged), publicada eneurope-west3-docker.pkg.dev/pi-saldum/pi-repo/images-dynamics-pi:<tag>. - Orquestación: Kubernetes
CronJob(k8s/cronjob.yaml) en el clúster GKEpi-cluster-hw(zonaeurope-west3-a, proyectopi-saldum), namespacepi, ejecutándose diariamente a las 18:00. - CI/CD (Jenkins): pipeline con 3 etapas —
Checkout→Build & Push(sustituyeapplication-pro.propertiesporapplication.propertiesantes demvn clean package jib:build) →Deploy to GKE. ElCLAUDE.mddescribe 5 etapas (Build → Test → Push → Deployment → Clean), más granular que elJenkinsfilereal de 3. - Las variables sensibles se inyectan en el pod mediante un
Secretde Kubernetes llamado igual que la app (images-dynamics-pi).
Job de Jenkins: https://jenkins-pi.hawkersco.net/job/images-dynamics-pi/
12. Manejo de errores y logging
No hay una estrategia de excepciones centralizada (no hay @ControllerAdvice, es un runner). Ambos runners activos capturan errores por fichero individual (descarga de Drive, subida SFTP, marcado de metadatos) y continúan con el siguiente archivo sin abortar todo el lote; ninguno de los dos notifica a Slack. ImagesDynamicsPiRunner implementa además una compensación explícita: si la subida a BI falla después de que la subida al ERP tuvo éxito, revierte (borra) el fichero ya subido al ERP para evitar un estado inconsistente entre ambos destinos. Logging mediante java.util.logging.Logger estándar (consola).
13. Notas y consideraciones
CLAUDE.mddescribe una arquitectura obsoleta: el documento describe el flujoSaleslayer CSV API → descarga de imágenes por URL → SFTP único, que corresponde íntegramente aImagesDynamicsPiOldRunner— clase presente en el código pero desactivada (@Componentcomentado). La arquitectura real y activa (ImagesDynamicsPiRunner+ImagesResizePiRunner, ambas basadas en una carpeta de Google Drive con flujo hacia tres destinos SFTP distintos: ERP, BI yresized) no se menciona en absoluto enCLAUDE.md. Es la discrepancia más relevante encontrada en este proyecto; se recomienda regenerar el documento.- Slack ya no se usa en el flujo activo:
slack-clienty la configuraciónslack.*solo son consumidos porImagesDynamicsPiOldRunner(desactivado). Los dos runners activos no notifican ningún fallo por Slack ni por otro canal externo al log del pod. - Lógica de extracción de código de producto duplicada:
extractProductCodeestá implementado de forma idéntica enImagesDynamicsPiRunneryImagesResizePiRunner; sería más mantenible extraerla a una clase de utilidad compartida. - Numeración secuencial no aplicada de forma uniforme:
ImagesDynamicsPiRunnerrenombra la imagen subida al ERP con una secuencia numérica por producto (_000_001,_000_002...), pero la copia subida a BI conserva el nombre original de Drive, yImagesResizePiRunnertampoco aplica ninguna numeración al subir al destinoresized— los tres destinos pueden acabar con convenciones de nombre distintas para el mismo fichero de origen. - Dependencia
google-api-services-sheetssin uso: declarada en elpom.xml, pero no se ha detectado ningún uso de la API de Sheets en el código fuente actual. - Ver alerta de seguridad en la sección 7 sobre credenciales reales expuestas en
application.properties.