From b69f367f034c168ab248c1e2983f0d2d3875bcf6 Mon Sep 17 00:00:00 2001 From: Diego Hernandez Date: Tue, 27 Oct 2026 17:28:28 -0300 Subject: [PATCH] feat(ci): pipeline CI/CD EP2 con Docker, tests de run_pipeline y escaneos de seguridad Agrega Dockerfile, .dockerignore, dependabot, scripts/run_pipeline.py con sus tests, y ajusta ci.yml, security.yml, train.yml y sonar. Quita la rubrica de EP1 del repo. --- .dockerignore | 18 +++++ .github/dependabot.yml | 32 +++++++++ .github/workflows/ci.yml | 35 +++++++++- .github/workflows/security.yml | 25 ++++--- .github/workflows/train.yml | 7 ++ Dockerfile | 27 ++++++++ EP1_DOY0101_Rubrica.md | 121 --------------------------------- README.md | 19 +++++- scripts/run_pipeline.py | 63 +++++++++++++++++ sonar-project.properties | 4 ++ tests/test_run_pipeline.py | 70 +++++++++++++++++++ 11 files changed, 289 insertions(+), 132 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/dependabot.yml create mode 100644 Dockerfile delete mode 100644 EP1_DOY0101_Rubrica.md create mode 100644 scripts/run_pipeline.py create mode 100644 tests/test_run_pipeline.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b6d5e73 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +.git +.github +.env +.venv +venv +__pycache__ +*.pyc +.pytest_cache +.ruff_cache +.coverage +coverage.xml +htmlcov +*.sarif +data +ml/artefactos +tests +Docs +README.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..72015e5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,32 @@ +version: 2 +updates: + # Dependencias Python (requirements.txt y requirements-dev.txt) + - package-ecosystem: pip + directory: / + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + labels: [dependencies, python] + commit-message: + prefix: "chore(deps)" + + # Imagen base del Dockerfile + - package-ecosystem: docker + directory: / + schedule: + interval: weekly + day: monday + labels: [dependencies, docker] + commit-message: + prefix: "chore(docker)" + + # Versiones de las actions usadas en los workflows + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + labels: [dependencies, ci] + commit-message: + prefix: "chore(ci)" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 227e593..267128e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,40 @@ jobs: run: ruff check ml tests scripts - name: Tests (pytest + cobertura) - run: pytest tests/ -v --cov=ml --cov-report=xml --cov-report=term + run: pytest tests/ -v --cov=ml --cov-report=xml --cov-report=term --cov-fail-under=60 - name: Health check (config) run: python -m ml.health --json --skip-s3 --skip-artifacts + + docker: + name: Imagen Docker (build, smoke test y publicacion) + runs-on: ubuntu-latest + needs: lint-test + permissions: + contents: read + packages: write + env: + IMAGE: ghcr.io/sebithaz-dev/model-training-s3 + + steps: + - name: Descargar codigo + uses: actions/checkout@v4 + + - name: Construir imagen (tag = SHA del commit) + run: | + docker build \ + --build-arg GIT_SHA=${{ github.sha }} \ + -t $IMAGE:${{ github.sha }} . + + - name: Smoke test del contenedor (health check de configuracion) + run: | + docker run --rm $IMAGE:${{ github.sha }} \ + python -m ml.health --json --skip-s3 --skip-artifacts + + - name: Publicar en GHCR (solo push a main) + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: | + echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + docker tag $IMAGE:${{ github.sha }} $IMAGE:latest + docker push $IMAGE:${{ github.sha }} + docker push $IMAGE:latest diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 9957bea..c1d7d8b 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -33,18 +33,15 @@ jobs: with: python-version: '3.11' + # Snyk necesita las dependencias instaladas para resolver el arbol completo + - name: Instalar dependencias + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + - name: Instalar Snyk CLI uses: snyk/actions/setup@v1 - - name: Snyk Open Source (runtime) - continue-on-error: true - run: > - snyk test --file=requirements.txt - --severity-threshold=high - --sarif-file-output=snyk-sca.sarif - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - - name: Snyk Open Source (dev) continue-on-error: true run: > @@ -60,6 +57,16 @@ jobs: env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + # Bloqueante: una vulnerabilidad high/critical en dependencias de + # runtime falla el workflow y, con la proteccion de main, bloquea el merge. + - name: Snyk Open Source (runtime) + run: > + snyk test --file=requirements.txt + --severity-threshold=high + --sarif-file-output=snyk-sca.sarif + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + - name: Subir SARIF a GitHub Security if: always() continue-on-error: true diff --git a/.github/workflows/train.yml b/.github/workflows/train.yml index bd9f9a3..bae56ef 100644 --- a/.github/workflows/train.yml +++ b/.github/workflows/train.yml @@ -6,6 +6,13 @@ on: - cron: '0 2 * * 0' workflow_dispatch: +permissions: + contents: read + +concurrency: + group: train + cancel-in-progress: false + jobs: train: runs-on: ubuntu-latest diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6daa68c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM python:3.11-slim + +ARG GIT_SHA=local +LABEL org.opencontainers.image.source="https://github.com/Sebithaz-dev/Model-Training-S3" \ + org.opencontainers.image.description="Job de entrenamiento del clasificador Citi Bike" \ + org.opencontainers.image.revision="${GIT_SHA}" + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 + +WORKDIR /app + +COPY requirements.txt . +RUN pip install -r requirements.txt + +COPY ml ./ml +COPY config ./config +COPY scripts ./scripts + +# Usuario sin privilegios: el job no necesita root +RUN useradd --create-home --uid 10001 trainer \ + && mkdir -p data ml/artefactos \ + && chown -R trainer:trainer /app +USER 10001 + +CMD ["python", "-m", "scripts.run_pipeline"] diff --git a/EP1_DOY0101_Rubrica.md b/EP1_DOY0101_Rubrica.md deleted file mode 100644 index 28f8d99..0000000 --- a/EP1_DOY0101_Rubrica.md +++ /dev/null @@ -1,121 +0,0 @@ -# Evaluación Parcial N°1 -## Encargo: Tu primer pipeline de despliegue -### Instrucciones y pauta de evaluación — Estudiante - -| Sigla | Nombre Asignatura | Tiempo Asignado | % Ponderación | -|---|---|---|---| -| DOY0101 | Ingeniería DevOps | 2 horas pedagógicas | 25% | - ---- - -## 1. Instrucciones generales - -**Descripción** - -- La Evaluación Parcial 1 corresponde a un encargo elaborado en **parejas** y consiste en la **creación de un repositorio Git** correspondiente a un microservicio previamente desarrollado, con el fin de preparar la base de trabajo para el pipeline DevOps que se construirá durante el semestre. -- Medirá los siguientes **Indicadores de Logro**: - - **IL1.1** Define estrategias de ramificación y control de versiones utilizando Git u otros sistemas de source control en escenarios colaborativos de desarrollo en la nube, para asegurar trazabilidad del código. - - **IL1.2** Configura flujos de trabajo DevOps que integren repositorios, automatización y colaboración en un entorno Cloud simulado, para cumplir con los estándares de CI/CD. - - **IL1.3** Especifica convenciones y buenas prácticas de uso de repositorios mediante documentación técnica en el marco de un proyecto de desarrollo ágil, para facilitar la colaboración y la calidad del código. -- El **tiempo asignado** para desarrollar esta evaluación como encargo es de **3 semanas** y se realiza **en parejas**. -- Iniciarás tu trabajo en el taller de proyectos (**TAITE 7**) con el/la docente, pero debes finalizar el encargo en tu tiempo de trabajo autónomo. - ---- - -## Ítem I: Instrucciones específicas de la Evaluación - -- Los/las estudiantes demuestran su dominio en el uso de sistemas de control de versiones y la implementación de estrategias de trabajo colaborativo en entornos DevOps. -- Se evaluará el diseño y la ejecución de un flujo de trabajo que integre **Git, GitHub y GitHub Actions** para control de versiones, automatización y colaboración efectiva. -- Se espera que los/las estudiantes elijan uno de sus **microservicios desarrollados previamente** en ramos anteriores y lo utilicen como base para este trabajo. - -### El encargo debe incluir los siguientes apartados: - -1. Crean un repositorio Git en GitHub con las siguientes ramas: `main`, `develop`, `feature/` y `hotfix/`. **(IE5)** -2. Implementan GitFlow o trunk-based development, justificando su elección en el README del repositorio. **(IE1)** -3. Simulan un desarrollo colaborativo integrando al menos 2 cambios tipo *feature* y 1 tipo *hotfix* mediante pull requests. **(IE2)** -4. Documentan en un archivo `README.md` o wiki las convenciones de commits, flujos de merge, naming de ramas y estrategias de revisión. **(IE5)** -5. Configuran al menos una acción básica de GitHub Actions que se ejecute con cada push a `develop` y pull request a `main`. **(IE3/IE4)** - -### Aspectos formales - -- El encargo se entregará mediante un **enlace en GitHub** del repositorio, el cual se envía a través de **AVA** y al **correo del docente**, en la fecha estipulada. - -### Materiales, herramientas o insumos requeridos - -- Materiales y contenidos académicos proporcionados durante el curso, incluyendo especificaciones técnicas, análisis previos y guías de referencia. - -### Indicaciones para el uso de Inteligencia Artificial (IA) - -- **Uso ético:** Los estudiantes pueden utilizar la IA como apoyo para mejorar redacción, buscar referencias o crear diagramas, pero todas las ideas, análisis y justificaciones técnicas deben ser propias del equipo. Se debe declarar en el informe qué herramientas de IA se usaron y cómo se aplicaron. -- **Prohibición en reflexiones críticas:** No se permite el uso de IA para redactar conclusiones, justificaciones técnicas o reflexiones individuales, ya que estas son esenciales para evaluar el aprendizaje y comprensión del equipo. -- **Validación de contenidos:** Todo contenido generado con IA debe ser revisado y validado por los estudiantes, asegurando que sea coherente con los requerimientos del proyecto. Se recomienda el uso de herramientas antiplagio para garantizar originalidad. -- **Reflexiones individuales obligatorias:** Cada integrante debe incluir en la sección de conclusiones una reflexión personal, redactada sin apoyo de IA, explicando su aprendizaje y contribución al proyecto. -- Todo uso de IA debe ser citado: https://bibliotecas.duoc.cl/ia - ---- - -## 2. Pauta de Evaluación - -### Escala general de niveles de logro - -| Categoría | % logro | Descripción | -|---|---|---| -| Muy buen desempeño | 100% | Demuestra un desempeño destacado, evidenciando el logro de todos los aspectos evaluados en el indicador. | -| Buen desempeño | 80% | Demuestra un alto desempeño del indicador, presentando pequeñas omisiones, dificultades y/o errores. | -| Desempeño aceptable | 60% | Demuestra un desempeño competente, evidenciando el logro de los elementos básicos del indicador, pero con omisiones, dificultades o errores. | -| Desempeño incipiente | 30% | Presenta importantes omisiones, dificultades o errores en el desempeño, que no permiten evidenciar los elementos básicos del logro del indicador, por lo que no puede ser considerado competente. | -| Desempeño no logrado | 0% | Presenta ausencia o incorrecto desempeño. | - -### Rúbrica detallada por indicador - -#### IE1. Identifica diferentes modelos de ramificación y su aplicabilidad en contextos colaborativos en la nube. *(Ponderación: 15%)* - -| Nivel | Descripción | -|---|---| -| Muy buen desempeño (100%) | Identifica con precisión diversos modelos de ramificación (Git Flow, GitHub Flow, trunk-based, entre otros), explicando claramente su aplicabilidad y ventajas en contextos colaborativos en la nube. | -| Buen desempeño (80%) | Identifica al menos dos modelos de ramificación y describe correctamente su aplicación en entornos colaborativos en la nube. | -| Desempeño aceptable (60%) | Identifica modelos de ramificación básicos y entrega una descripción general de su uso, con ejemplos parciales o poco específicos. | -| Desempeño incipiente (30%) | Identifica al menos un modelo de ramificación, pero con errores o sin vincularlo adecuadamente a contextos colaborativos. | -| Desempeño no logrado (0%) | No logra identificar modelos de ramificación ni su aplicabilidad en contextos colaborativos. | - -#### IE2. Simula un flujo de trabajo colaborativo donde aplica correctamente comandos de Git (clone, commit, push, pull, merge, etc.) documentando la trazabilidad del código fuente. *(Ponderación: 15%)* - -| Nivel | Descripción | -|---|---| -| Muy buen desempeño (100%) | Simula eficazmente un flujo colaborativo usando comandos Git adecuados, documentando con precisión cada etapa de trazabilidad del código. | -| Buen desempeño (80%) | Simula correctamente un flujo de trabajo colaborativo básico con comandos Git y presenta documentación general de la trazabilidad. | -| Desempeño aceptable (60%) | Simula parcialmente un flujo colaborativo, con algunos comandos mal aplicados o documentación limitada de los cambios. | -| Desempeño incipiente (30%) | Simula de forma incorrecta varios comandos Git y no documenta de manera adecuada la trazabilidad. | -| Desempeño no logrado (0%) | No realiza una simulación de flujo colaborativo o no emplea comandos Git pertinentes. | - -#### IE3. Implementa un flujo de trabajo DevOps básico que automatice la integración de cambios desde un repositorio Git en un entorno cloud simulado. *(Ponderación: 25%)* - -| Nivel | Descripción | -|---|---| -| Muy buen desempeño (100%) | Implementa un flujo de trabajo DevOps básico completamente funcional, automatizando de forma precisa la integración de cambios desde un repositorio Git en un entorno cloud simulado, sin errores y con documentación clara. | -| Buen desempeño (80%) | Implementa un flujo de trabajo DevOps básico que automatiza adecuadamente la integración de cambios desde un repositorio Git en un entorno cloud simulado, con mínimas observaciones. | -| Desempeño aceptable (60%) | Implementa un flujo de trabajo DevOps básico que automatiza parcialmente la integración de cambios desde un repositorio Git, aunque presenta fallos menores o falta de coherencia en el entorno cloud simulado. | -| Desempeño incipiente (30%) | Implementa un flujo de trabajo DevOps básico con errores importantes en la automatización o en la conexión entre el repositorio Git y el entorno cloud simulado. | -| Desempeño no logrado (0%) | No implementa un flujo de trabajo DevOps básico o la automatización de integración de cambios desde un repositorio Git no está presente. | - -#### IE4. Configura herramientas de automatización (GitHub Actions, GitLab CI o similares), explicando su rol en los procesos CI/CD. *(Ponderación: 25%)* - -| Nivel | Descripción | -|---|---| -| Muy buen desempeño (100%) | Configura herramientas de automatización de forma completa y funcional, y explica con claridad y precisión su rol dentro del proceso CI/CD, contextualizando su utilidad en un flujo de desarrollo real. | -| Buen desempeño (80%) | Configura herramientas de automatización correctamente, y explica de manera adecuada su rol en los procesos CI/CD, aunque con detalles menores por mejorar. | -| Desempeño aceptable (60%) | Configura herramientas de automatización con limitaciones o errores menores, y entrega una explicación básica pero comprensible de su función en CI/CD. | -| Desempeño incipiente (30%) | Configura herramientas de automatización de forma incompleta o incorrecta, y explica su rol en CI/CD de manera poco clara o con errores de concepto. | -| Desempeño no logrado (0%) | No configura herramientas de automatización, ni explica su función dentro de los procesos CI/CD. | - -#### IE5. Elabora una guía de buenas prácticas para el uso de repositorios en equipos DevOps, incluyendo naming de ramas, mensajes de commit, estructuras de carpetas y control de versiones. *(Ponderación: 20%)* - -| Nivel | Descripción | -|---|---| -| Muy buen desempeño (100%) | Elabora una guía de buenas prácticas completa, clara y aplicable, que incluye de forma precisa todos los aspectos solicitados: naming de ramas, mensajes de commit, estructuras de carpetas y control de versiones. | -| Buen desempeño (80%) | Elabora una guía de buenas prácticas bien estructurada, abarcando la mayoría de los aspectos solicitados con recomendaciones claras y funcionales. | -| Desempeño aceptable (60%) | Elabora una guía de buenas prácticas con algunos aspectos desarrollados de forma parcial o superficial, pero que ofrece lineamientos utilizables. | -| Desempeño incipiente (30%) | Elabora una guía de buenas prácticas incompleta, con recomendaciones generales o confusas, omitiendo varios elementos clave. | -| Desempeño no logrado (0%) | No elabora una guía de buenas prácticas, o el contenido presentado no es pertinente ni aplicable. | - -**Total: 100%** diff --git a/README.md b/README.md index a4b183c..dc81c66 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,8 @@ Las métricas se calculan sobre el test set (80/20 split estratificado) y se gua - **Triggers:** `pull_request` a `main` (gate de merge, obligatorio verde) y `push` a `main` (verificación post-merge del trunk). - **Pasos:** checkout → setup Python 3.11 (cache de pip) → instalar `requirements-dev.txt` → `ruff check` → `pytest` con cobertura → health check de configuración. - **Propósito:** garantizar que todo lo que llega a `main` pasa lint, tests y el health check básico. Bloquea el merge si falla. +- **Política de calidad:** cobertura mínima del 60 % sobre `ml/` (`--cov-fail-under=60`); bajo ese umbral el job falla. +- **Job `docker`** (`needs: lint-test`): construye la imagen con tag = SHA del commit, ejecuta un smoke test (`ml.health`) dentro del contenedor y, en push a `main`, la publica en GHCR como `ghcr.io/sebithaz-dev/model-training-s3:` y `:latest`. > **Nota sobre IE3/IE4:** el enunciado pide disparar la CI en "push a `develop` y pull request a `main`". Como este repositorio usa **TBD puro** (sin rama `develop` — ver [justificación](#estrategia-de-branching-trunk-based-development-tbd)), el trigger `push a develop` se sustituye por `push a main`, que cumple el mismo rol en TBD: verificar el trunk tras cada integración. @@ -246,7 +248,22 @@ Las métricas se calculan sobre el test set (80/20 split estratificado) y se gua - **SonarCloud:** ejecuta `pytest --cov=ml --cov-report=xml` y publica cobertura, bugs, vulnerabilidades y code smells. - **Secrets requeridos:** `SNYK_TOKEN`, `SONAR_TOKEN`. - **Variables:** `SONAR_ORGANIZATION` (default `sebithaz-dev`), `SONAR_PROJECT_KEY` (default `Sebithaz-dev_Model-Training-S3`). -- **Bloqueo:** Snyk es informativo al inicio (`continue-on-error`); SonarCloud publica el análisis sin esperar el quality gate. Cuando el equipo lo decida, se puede activar `sonar.qualitygate.wait=true`. +- **Bloqueo:** el escaneo de dependencias de runtime (`requirements.txt`) es **bloqueante**: una vulnerabilidad `high` o `critical` falla el workflow. Snyk sobre dependencias de desarrollo y Snyk Code quedan como **alertas** en GitHub Security. SonarCloud espera el resultado del **Quality Gate** (`sonar.qualitygate.wait=true`) y falla si no se cumple. Ambos checks se exigen en la protección de `main`. +- **Dependabot:** `.github/dependabot.yml` abre PRs semanales para dependencias `pip`, la imagen base de Docker y las GitHub Actions. + +### Contenedor del job de entrenamiento + +El `Dockerfile` empaqueta el job (`python:3.11-slim`, usuario sin privilegios `10001`, sin tests ni datos en la imagen gracias a `.dockerignore`). Su punto de entrada `scripts/run_pipeline.py` descarga el dataset curado, entrena y ejecuta el health check: + +- **Con credenciales AWS:** descarga con boto3, sube los artefactos a S3 y valida en modo `--strict`. +- **Sin credenciales:** descarga por la URL pública del bucket y deja los artefactos solo en el contenedor (modo demo). + +```bash +docker build -t model-training-s3 . +docker run --rm --env-file .env model-training-s3 +``` + +La orquestación junto al dashboard está en `docker-compose.yml` y `k8s/` del repo [Flask-Dashboard-S3](https://github.com/Sebithaz-dev/Flask-Dashboard-S3). --- diff --git a/scripts/run_pipeline.py b/scripts/run_pipeline.py new file mode 100644 index 0000000..f8e7b7d --- /dev/null +++ b/scripts/run_pipeline.py @@ -0,0 +1,63 @@ +"""Punto de entrada del contenedor de entrenamiento. + +Descarga el dataset curado desde S3, entrena el modelo y ejecuta el health check. + +- Con credenciales AWS (``AWS_ACCESS_KEY_ID``): descarga con boto3, sube los + artefactos a S3 y valida en modo estricto. +- Sin credenciales: descarga por la URL publica del bucket y deja los + artefactos solo en el contenedor (modo demo local). + +Uso: + python -m scripts.run_pipeline +""" +from __future__ import annotations + +import os +import sys +import urllib.request +from pathlib import Path + +from ml import health +from ml.train import entrenar + +DEFAULT_DATA_KEY = "curated/citibike_trips/citibike_curated.snappy.parquet" +DESTINO = Path("data/datos.parquet") + + +def _tiene_credenciales() -> bool: + return bool(os.getenv("AWS_ACCESS_KEY_ID")) + + +def descargar_datos(destino: Path = DESTINO) -> None: + bucket = os.environ["S3_BUCKET_NAME"] + key = os.getenv("S3_DATA_KEY", DEFAULT_DATA_KEY) + destino.parent.mkdir(parents=True, exist_ok=True) + + if _tiene_credenciales(): + from ml.s3_utils import download_from_s3 + + download_from_s3(bucket, key, str(destino)) + return + + region = os.getenv("AWS_REGION", "us-east-1") + url = f"https://{bucket}.s3.{region}.amazonaws.com/{key}" + print(f" Descargando {url}") + urllib.request.urlretrieve(url, destino) + + +def main() -> int: + descargar_datos() + + con_credenciales = _tiene_credenciales() + if not con_credenciales: + # Sin credenciales no se puede escribir en S3: artefactos solo locales. + os.environ.pop("S3_BUCKET_NAME", None) + + entrenar(str(DESTINO)) + + args = ["--json", "--strict"] if con_credenciales else ["--json", "--skip-s3"] + return health.main(args) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/sonar-project.properties b/sonar-project.properties index c3f007f..b5fbfaa 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -9,3 +9,7 @@ sonar.python.version=3.11 sonar.sourceEncoding=UTF-8 sonar.python.coverage.reportPaths=coverage.xml sonar.exclusions=ml/artefactos/**,data/**,Docs/**,**/__pycache__/** + +# El scan falla si no se cumple el Quality Gate (bloquea el PR) +sonar.qualitygate.wait=true +sonar.qualitygate.timeout=300 diff --git a/tests/test_run_pipeline.py b/tests/test_run_pipeline.py new file mode 100644 index 0000000..d94d5a6 --- /dev/null +++ b/tests/test_run_pipeline.py @@ -0,0 +1,70 @@ +"""Tests para scripts.run_pipeline (entrypoint del contenedor).""" +from pathlib import Path + +from scripts import run_pipeline + + +def test_descarga_publica_sin_credenciales(monkeypatch, tmp_path): + monkeypatch.delenv("AWS_ACCESS_KEY_ID", raising=False) + monkeypatch.setenv("S3_BUCKET_NAME", "bucket-demo") + monkeypatch.setenv("AWS_REGION", "us-east-1") + monkeypatch.setenv("S3_DATA_KEY", "curated/datos.parquet") + llamadas = [] + monkeypatch.setattr( + run_pipeline.urllib.request, + "urlretrieve", + lambda url, destino: llamadas.append((url, Path(destino))), + ) + + destino = tmp_path / "data" / "datos.parquet" + run_pipeline.descargar_datos(destino) + + assert llamadas == [ + ("https://bucket-demo.s3.us-east-1.amazonaws.com/curated/datos.parquet", destino) + ] + assert destino.parent.exists() + + +def test_descarga_con_credenciales_usa_boto3(monkeypatch, tmp_path): + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "test") + monkeypatch.setenv("S3_BUCKET_NAME", "bucket-demo") + monkeypatch.delenv("S3_DATA_KEY", raising=False) + llamadas = [] + monkeypatch.setattr( + "ml.s3_utils.download_from_s3", + lambda bucket, key, local: llamadas.append((bucket, key, local)), + ) + + destino = tmp_path / "datos.parquet" + run_pipeline.descargar_datos(destino) + + assert llamadas == [("bucket-demo", run_pipeline.DEFAULT_DATA_KEY, str(destino))] + + +def test_main_sin_credenciales_no_sube_a_s3(monkeypatch): + monkeypatch.delenv("AWS_ACCESS_KEY_ID", raising=False) + monkeypatch.setenv("S3_BUCKET_NAME", "bucket-demo") + eventos = {} + monkeypatch.setattr(run_pipeline, "descargar_datos", lambda: None) + + def fake_entrenar(ruta): + eventos["bucket_al_entrenar"] = run_pipeline.os.getenv("S3_BUCKET_NAME") + + monkeypatch.setattr(run_pipeline, "entrenar", fake_entrenar) + monkeypatch.setattr(run_pipeline.health, "main", lambda args: eventos.setdefault("args", args) and 0) + + assert run_pipeline.main() == 0 + assert eventos["bucket_al_entrenar"] is None + assert eventos["args"] == ["--json", "--skip-s3"] + + +def test_main_con_credenciales_valida_estricto(monkeypatch): + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "test") + monkeypatch.setenv("S3_BUCKET_NAME", "bucket-demo") + eventos = {} + monkeypatch.setattr(run_pipeline, "descargar_datos", lambda: None) + monkeypatch.setattr(run_pipeline, "entrenar", lambda ruta: None) + monkeypatch.setattr(run_pipeline.health, "main", lambda args: eventos.setdefault("args", args) and 0) + + assert run_pipeline.main() == 0 + assert eventos["args"] == ["--json", "--strict"]