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
| Campo | Valor |
|---|---|
artifactId | feeds-update-gs |
groupId | com.hawkersco |
version | 1.0.25 |
| Java | 25 |
| Spring Boot | 4.0.6 |
| Tipo de artefacto | jar (ejecutable, servicio web de larga duración) |
| Módulos | No aplica (proyecto de módulo único) |
3. Arquitectura y diseño
A diferencia del resto de la familia de runners, esta aplicación sí 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@EnableSchedulingy@ConfigurationPropertiesScan) yFeedsUpdateGsController..jobs—FeedsUpdateGsJob, con la lógica principal de sincronización, anotada@Scheduled..config—BasicAuthSecurity(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
| Dependencia | Propósito |
|---|---|
spring-boot-starter-web | Exposición del endpoint REST |
spring-boot-starter-security | Autenticación HTTP Basic para el endpoint manual |
com.hawkersco:pi-function-commons:1.0.25-SNAPSHOT | Utilidades 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étodo | Ruta | Descripción | Autenticación |
|---|---|---|---|
GET | /upload-files | Dispara 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
| Sistema | Protocolo | Dirección | Detalle |
|---|---|---|---|
Servidor SFTP (sftp.hawkersco.com) | SFTP (SftpUtils) | Entrante/Saliente | Descarga de todos los ficheros de /src/feeds_gs y borrado posterior en origen |
Google Cloud Storage (pi-logistics-segment) | API de GCS (StorageUtils) | Saliente | Subida de los ficheros XML descargados, con alias adicionales para es.xml→true.xml y gb.xml→uk.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).
| Clave | Descripción | Ejemplo (producción) |
|---|---|---|
server.port | Puerto HTTP del servicio | 80 |
auth.user.name | Usuario de autenticación básica para /upload-files | ${authUserName} |
auth.user.password | Contraseña de autenticación básica | ${authUserPassword} |
ftp.production.server | Host del servidor SFTP | ${ftpServer} |
ftp.production.port | Puerto SFTP | ${ftpPort} |
ftp.production.user | Usuario SFTP | ${ftpUser} |
ftp.production.pass | Contraseña SFTP | ${ftpPass} |
ftp.production.dir | Declarada en ambos perfiles; sin uso en el código (ver sección 13) | /src/pending/ |
gcs.bucket.name | Nombre del bucket de GCS destino | pi-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:
- Rotar la contraseña de autenticación básica y la contraseña SFTP expuestas.
- 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: 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 localfeeds/→ descarga todos los ficheros de/src/feeds_gsen el SFTP → borra los ficheros del SFTP tras la descarga → si hay subdirectorios enfeeds/, sube cada fichero a GCS (con elcontent-typefijoapplication/xml), generando además una copia con nombre alternativo paraes.xml(→true.xml) ygb.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(baseeclipse-temurin:25-jre,containerizingMode=packaged), publicada eneurope-west3-docker.pkg.dev/pi-saldum/pi-repo/feeds-update-gs:<tag>. - Orquestación: Kubernetes
Deployment(k8s/deployment.yaml, noCronJob) en el clúster GKEpi-cluster-hw(zonaeurope-west3-a, proyectopi-saldum), namespacepi,replicas: 1. SinlivenessProbe/readinessProbeconfigurados. - CI/CD (Jenkins): pipeline con 2 etapas —
Checkout→Build & Push(sustituyeapplication-pro.propertiesporapplication.propertiesantes demvn clean package jib:build) →Deploy to GKE(aplica el manifiesto templado víasedy espera elrollout statusdelDeployment, a diferencia de los runnersCronJobque no pueden usarrollout status). ElCLAUDE.mddel repositorio menciona además etapas deKICS scanySonarQubey un scriptjenkins/scripts/deployment.sh/jenkins/deployment/app-deployment.yml, que no están presentes en elJenkinsfileni en la estructura de directorios actual del repositorio (ver sección 13). - Las variables sensibles se inyectan en el pod mediante un
Secretde 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. SiuploadToGcsfalla (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-domainconfigurado como público pero no implementado:BasicAuthSecuritydeclara.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
CustomFiltersin uso: existeconfig/CustomFilter.java, un filtroGenericFilterBeanque simplemente reenvía la petición sin hacer nada (filterChain.doFilter(...)), y no está registrado enBasicAuthSecurityni en ninguna otra configuración. Es código muerto. - Runner legado no documentado en
CLAUDE.md:FeedsUpdateGsRunnerimplementa el mismo flujo queFeedsUpdateGsJobcomoCommandLineRunnerde un solo disparo, pero está desactivado (@Componentcomentado) y no se menciona en elCLAUDE.mddel proyecto. Es la versión previa a la migración hacia el modelo@Scheduled+Deploymentcontinuo; 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.direstá declarada en ambos perfiles deapplication.properties, pero el recordFtpPropertiessolo exponeserver,port,userypass— el valor/src/pending/nunca se lee en el código. La ruta real de origen SFTP está hardcodeada enFeedsUpdateGsJobcomoSFTP_SOURCE_PATH = "/src/feeds_gs", un valor distinto al de la propiedad no usada. CLAUDE.mdcon información de despliegue desactualizada: mencionajenkins/scripts/deployment.shyjenkins/deployment/app-deployment.ymlpara el despliegue manual, así como etapas deKICS scan/SonarQubeen el pipeline; ninguno de estos ficheros ni etapas existen en el repositorio actual, cuyoJenkinsfilereal solo tieneCheckout → Build & Push → Deploy to GKE. El resto de la descripción arquitectónica deCLAUDE.md(flujo SFTP→GCS, aliases.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.