Skip to main content

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

CampoValor
artifactIdimages-dynamics-pi
groupIdcom.hawkersco
version1.0.25
Java25 (maven.compiler.release=25)
Spring Boot4.0.6
Tipo de artefactojar (ejecutable, Spring Boot batch/CLI)
MódulosNo 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.
  • .configGoogleDriveConfig (bean Drive autenticado con cuenta de servicio delegada).
  • ImageProperties — record @ConfigurationProperties (prefijo images.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

DependenciaPropósito
spring-boot-starterNúcleo de Spring Boot (sin web, es un runner CLI)
com.google.apis:google-api-services-driveCliente de la API de Google Drive (listado/descarga/actualización de metadatos)
com.google.apis:google-api-services-sheetsDeclarada en el pom.xml; sin uso detectado en el código actual
net.coobird:thumbnailator:0.4.21Redimensionado y compresión de imágenes (ImagesResizePiRunner)
com.hawkersco:pi-function-commons:1.0.25-SNAPSHOTDirectoryUtils, SftpUtils
com.hawkersco:slack-client:1.0.25-SNAPSHOTUsada 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

SistemaProtocoloDirecciónDetalle
Google Drive (carpeta 0AND09-6ovgdrUk9PVA)HTTP (API de Google Drive)Entrante/SalienteDescarga de imágenes pendientes y marcado de appProperties tras procesarlas
Servidor SFTP (hawkers-images.hawkersco.net)SFTP (SftpUtils)SalienteSubida 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).

ClaveDescripciónEjemplo (producción)
images.ftp.server / .user / .pass / .portConexión SFTP compartida por los tres runners${imagesFtpServer}, etc.
images.ftp.dir-erpDirectorio remoto destino para el ERP/src/erp/products/
images.ftp.dir-biDirectorio remoto destino para BI/src/bi/
images.ftp.dir-resizedDirectorio remoto destino para las miniaturas/src/resized/
slack.client.url / .auth.token / .channel.idConfiguració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:

  1. Rotar la contraseña SFTP y el token de Slack expuestos.
  2. Sustituir los valores hardcodeados de application.properties por credenciales de un entorno de desarrollo aislado.
  3. 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:

OrdenRunnerFunción
1ImagesDynamicsPiRunnerDescarga imágenes nuevas de Drive, las sube al ERP y a BI, marca processed=true
2ImagesResizePiRunnerDescarga 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 (base eclipse-temurin:25-jre, containerizingMode=packaged), publicada en europe-west3-docker.pkg.dev/pi-saldum/pi-repo/images-dynamics-pi:<tag>.
  • Orquestación: Kubernetes CronJob (k8s/cronjob.yaml) en el clúster GKE pi-cluster-hw (zona europe-west3-a, proyecto pi-saldum), namespace pi, ejecutándose diariamente a las 18:00.
  • CI/CD (Jenkins): pipeline con 3 etapas — CheckoutBuild & Push (sustituye application-pro.properties por application.properties antes de mvn clean package jib:build) → Deploy to GKE. El CLAUDE.md describe 5 etapas (Build → Test → Push → Deployment → Clean), más granular que el Jenkinsfile real de 3.
  • Las variables sensibles se inyectan en el pod mediante un Secret de 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.md describe una arquitectura obsoleta: el documento describe el flujo Saleslayer CSV API → descarga de imágenes por URL → SFTP único, que corresponde íntegramente a ImagesDynamicsPiOldRunner — clase presente en el código pero desactivada (@Component comentado). La arquitectura real y activa (ImagesDynamicsPiRunner + ImagesResizePiRunner, ambas basadas en una carpeta de Google Drive con flujo hacia tres destinos SFTP distintos: ERP, BI y resized) no se menciona en absoluto en CLAUDE.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-client y la configuración slack.* solo son consumidos por ImagesDynamicsPiOldRunner (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: extractProductCode está implementado de forma idéntica en ImagesDynamicsPiRunner y ImagesResizePiRunner; sería más mantenible extraerla a una clase de utilidad compartida.
  • Numeración secuencial no aplicada de forma uniforme: ImagesDynamicsPiRunner renombra 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, y ImagesResizePiRunner tampoco aplica ninguna numeración al subir al destino resized — los tres destinos pueden acabar con convenciones de nombre distintas para el mismo fichero de origen.
  • Dependencia google-api-services-sheets sin uso: declarada en el pom.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.