Skip to main content

feeds-update-gs

1. Descripción general

Según el pom.xml, el proyecto se describe como "Feeds Update GS". Es un microservicio que sincroniza ficheros de feed XML desde un servidor SFTP hacia un bucket de Google Cloud Storage (GCS), cada 30 minutos mediante una tarea programada. A diferencia de la mayoría de proyectos de la familia *-update-*/*-create-db (que son procesos batch de un solo disparo desplegados como CronJob), este proyecto se despliega como una aplicación web de larga duración (Deployment) que expone además un endpoint HTTP protegido por autenticación básica para disparar la sincronización manualmente.

2. Información técnica

CampoValor
artifactIdfeeds-update-gs
groupIdcom.hawkersco
version1.0.25
Java25
Spring Boot4.0.6
Tipo de artefactojar (ejecutable, servicio web de larga duración)
MódulosNo aplica (proyecto de módulo único)

3. Arquitectura y diseño

A diferencia del resto de la familia de runners, esta aplicación tiene capa web: expone un único endpoint REST protegido y ejecuta su lógica principal mediante una tarea @Scheduled.

Paquetes principales:

  • com.hawkersco.feedsupdategs — clase principal (FeedsUpdateGsApplication, con @EnableScheduling y @ConfigurationPropertiesScan) y FeedsUpdateGsController.
  • .jobsFeedsUpdateGsJob, con la lógica principal de sincronización, anotada @Scheduled.
  • .configBasicAuthSecurity (Spring Security), RestAuthenticationEntryPoint, PasswordEncoderConfig, GlobalExceptionHandler, y los records @ConfigurationProperties (FtpProperties, AuthUserProperties, GcsBucketProperties).
flowchart TD
A["@Scheduled cada 30 min<br/>FeedsUpdateGsJob.uploadFiles"] --> B[Borra/recrea dir local feeds/]
B --> C[Descarga todo de SFTP /src/feeds_gs]
C --> D[Borra los ficheros del SFTP]
D --> E[Sube XML a GCS pi-logistics-segment]

F["GET /upload-files<br/>(Basic Auth)"] -->|dispara manualmente| A
G["GET /check-domain<br/>(público, sin implementar)"] -.-> H[No hay controlador real]

FeedsUpdateGsRunner es una clase adicional presente en el código (com.hawkersco.feedsupdategs.FeedsUpdateGsRunner) que implementa el mismo flujo como CommandLineRunner, pero está desactivada (@Component comentado) — es la versión anterior del flujo, sustituida por FeedsUpdateGsJob + FeedsUpdateGsController. No está mencionada en el CLAUDE.md del proyecto.

4. Dependencias principales

DependenciaPropósito
spring-boot-starter-webExposición del endpoint REST
spring-boot-starter-securityAutenticación HTTP Basic para el endpoint manual
com.hawkersco:pi-function-commons:1.0.25-SNAPSHOTUtilidades comunes: SftpUtils, StorageUtils, DirectoryUtils
spring-boot-starter-test (test)JUnit 5 + Spring Test

La dependencia del cliente de Google Cloud Storage no se declara explícitamente en el pom.xml; llega de forma transitiva o está embebida en pi-function-commons (com.google.cloud.storage.* se usa directamente en FeedsUpdateGsJob).

5. API / Endpoints

MétodoRutaDescripciónAutenticación
GET/upload-filesDispara manualmente el mismo flujo que la tarea programada (SFTP → GCS)HTTP Basic (usuario feeds-update-user en local)

La configuración de seguridad (BasicAuthSecurity) declara /check-domain como ruta pública (permitAll()), pero no existe ningún controlador que implemente esa ruta en el código actual (ver sección 13).

Ejemplo de respuesta de /upload-files:

200 OK
Files uploaded

6. Integraciones externas

SistemaProtocoloDirecciónDetalle
Servidor SFTP (sftp.hawkersco.com)SFTP (SftpUtils)Entrante/SalienteDescarga de todos los ficheros de /src/feeds_gs y borrado posterior en origen
Google Cloud Storage (pi-logistics-segment)API de GCS (StorageUtils)SalienteSubida de los ficheros XML descargados, con alias adicionales para es.xmltrue.xml y gb.xmluk.xml

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)
server.portPuerto HTTP del servicio80
auth.user.nameUsuario de autenticación básica para /upload-files${authUserName}
auth.user.passwordContraseña de autenticación básica${authUserPassword}
ftp.production.serverHost del servidor SFTP${ftpServer}
ftp.production.portPuerto SFTP${ftpPort}
ftp.production.userUsuario SFTP${ftpUser}
ftp.production.passContraseña SFTP${ftpPass}
ftp.production.dirDeclarada en ambos perfiles; sin uso en el código (ver sección 13)/src/pending/
gcs.bucket.nameNombre del bucket de GCS destinopi-logistics-segment

Las credenciales de GCS se resuelven vía StorageOptions.getDefaultInstance(), que en el Deployment toma el fichero montado en /etc/gcp/sa_credentials.json (variable GOOGLE_APPLICATION_CREDENTIALS, procedente del Secret pi-saldum-gcp-credentials).

⚠️ Alerta de seguridad

El fichero src/main/resources/application.properties (perfil local) contiene actualmente credenciales reales en texto plano: contraseña de autenticación básica del propio servicio y contraseña del servidor SFTP. Ninguno de estos valores se ha reproducido en este documento. Se recomienda:

  1. Rotar la contraseña de autenticación básica y la contraseña SFTP expuestas.
  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: los datos que gestiona son ficheros XML en tránsito entre SFTP y GCS.

9. Procesos programados y mensajería

  • FeedsUpdateGsJob.uploadFiles()@Scheduled(cron = "0 */30 * * * *"), se ejecuta cada 30 minutos. Flujo: borra/recrea el directorio local feeds/ → descarga todos los ficheros de /src/feeds_gs en el SFTP → borra los ficheros del SFTP tras la descarga → si hay subdirectorios en feeds/, sube cada fichero a GCS (con el content-type fijo application/xml), generando además una copia con nombre alternativo para es.xml (→ true.xml) y gb.xml (→ uk.xml).
  • El mismo flujo puede dispararse manualmente vía GET /upload-files.

10. Ejecución en local

Requisitos previos: JDK 25, Maven, acceso al servidor SFTP y credenciales de aplicación por defecto de Google (GOOGLE_APPLICATION_CREDENTIALS) con permisos sobre el bucket pi-logistics-segment.

# Compilar sin tests
./mvnw clean install -DskipTests

# Compilar con tests
./mvnw clean install

# Ejecutar tests
./mvnw test

# Ejecutar un test concreto
./mvnw test -Dtest=FeedsUpdateGsApplicationTests

# Ejecutar la aplicación localmente
./mvnw spring-boot:run

No hay endpoint de Actuator/health configurado (no se declara spring-boot-starter-actuator en el pom.xml, ni el Deployment define livenessProbe/readinessProbe). La verificación de que el servicio está operativo se limita a comprobar que el puerto 80 responde y a revisar los logs de la tarea programada.

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/feeds-update-gs:<tag>.
  • Orquestación: Kubernetes Deployment (k8s/deployment.yaml, no CronJob) en el clúster GKE pi-cluster-hw (zona europe-west3-a, proyecto pi-saldum), namespace pi, replicas: 1. Sin livenessProbe/readinessProbe configurados.
  • CI/CD (Jenkins): pipeline con 2 etapas — CheckoutBuild & Push (sustituye application-pro.properties por application.properties antes de mvn clean package jib:build) → Deploy to GKE (aplica el manifiesto templado vía sed y espera el rollout status del Deployment, a diferencia de los runners CronJob que no pueden usar rollout status). El CLAUDE.md del repositorio menciona además etapas de KICS scan y SonarQube y un script jenkins/scripts/deployment.sh/jenkins/deployment/app-deployment.yml, que no están presentes en el Jenkinsfile ni en la estructura de directorios actual del repositorio (ver sección 13).
  • Las variables sensibles se inyectan en el pod mediante un Secret de Kubernetes llamado igual que la app (feeds-update-gs).

Job de Jenkins: https://jenkins-pi.hawkersco.net/job/feeds-update-gs/

12. Manejo de errores y logging

GlobalExceptionHandler (@RestControllerAdvice) centraliza el manejo de errores para las peticiones HTTP: captura IOException, JSchException, SftpException e IllegalArgumentException, registrando el error con Logger.severe y devolviendo 500 Internal Server Error con un mensaje genérico. Este manejador solo aplica al flujo disparado vía /upload-files; los fallos que ocurran durante la ejecución programada (@Scheduled) no pasan por GlobalExceptionHandler (los @ExceptionHandler de Spring MVC no se aplican a tareas programadas) y se propagan como una excepción no controlada en el hilo del scheduler, quedando solo en el log de la aplicación sin ninguna respuesta HTTP ni notificación externa. Autenticación fallida en /upload-files devuelve 401 Unauthorized vía RestAuthenticationEntryPoint.

13. Notas y consideraciones

  • Riesgo de pérdida de datos si falla la subida a GCS: FeedsUpdateGsJob.syncFromSftp() borra los ficheros del servidor SFTP (SftpUtils.deleteAllFilesFromDirectory) inmediatamente después de descargarlos, antes de haberlos subido con éxito a GCS. Si uploadToGcs falla (por ejemplo, por un problema transitorio de credenciales o de red hacia GCS), los ficheros ya han sido eliminados de su origen y solo existen en el directorio local efímero del pod; en el peor caso (reinicio del pod), el feed quedaría completamente perdido, sin ninguna copia de seguridad previa a la subida.
  • /check-domain configurado como público pero no implementado: BasicAuthSecurity declara .requestMatchers("/check-domain").permitAll(), pero no existe ningún controlador ni mapeo que implemente esa ruta en el código actual. Es configuración muerta o vestigio de un endpoint de verificación de dominio que se eliminó sin limpiar la regla de seguridad asociada.
  • Clase CustomFilter sin uso: existe config/CustomFilter.java, un filtro GenericFilterBean que simplemente reenvía la petición sin hacer nada (filterChain.doFilter(...)), y no está registrado en BasicAuthSecurity ni en ninguna otra configuración. Es código muerto.
  • Runner legado no documentado en CLAUDE.md: FeedsUpdateGsRunner implementa el mismo flujo que FeedsUpdateGsJob como CommandLineRunner de un solo disparo, pero está desactivado (@Component comentado) y no se menciona en el CLAUDE.md del proyecto. Es la versión previa a la migración hacia el modelo @Scheduled + Deployment continuo; se recomienda eliminarlo si no se prevé volver a usarlo, o documentarlo si se conserva como referencia.
  • Propiedad de configuración sin uso: ftp.production.dir está declarada en ambos perfiles de application.properties, pero el record FtpProperties solo expone server, port, user y pass — el valor /src/pending/ nunca se lee en el código. La ruta real de origen SFTP está hardcodeada en FeedsUpdateGsJob como SFTP_SOURCE_PATH = "/src/feeds_gs", un valor distinto al de la propiedad no usada.
  • CLAUDE.md con información de despliegue desactualizada: menciona jenkins/scripts/deployment.sh y jenkins/deployment/app-deployment.yml para el despliegue manual, así como etapas de KICS scan/SonarQube en el pipeline; ninguno de estos ficheros ni etapas existen en el repositorio actual, cuyo Jenkinsfile real solo tiene Checkout → Build & Push → Deploy to GKE. El resto de la descripción arquitectónica de CLAUDE.md (flujo SFTP→GCS, alias es.xml/gb.xml, seguridad Basic Auth) coincide con el código actual.
  • Ver alerta de seguridad en la sección 7 sobre credenciales reales expuestas en application.properties.