diff --git a/.github/workflows/deploy-prod.yaml b/.github/workflows/deploy-prod.yaml index 8004cac..3591be9 100644 --- a/.github/workflows/deploy-prod.yaml +++ b/.github/workflows/deploy-prod.yaml @@ -92,6 +92,13 @@ jobs: CSAS_CLIENT_ID: ${{ secrets.CSAS_CLIENT_ID }} CSAS_CLIENT_SECRET: ${{ secrets.CSAS_CLIENT_SECRET }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + SMTP_HOST: ${{ secrets.SMTP_HOST }} + SMTP_PORT: ${{ secrets.SMTP_PORT }} + SMTP_USERNAME: ${{ secrets.SMTP_USERNAME }} + SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }} + SMTP_USE_TLS: ${{ secrets.SMTP_USE_TLS }} + SMTP_USE_SSL: ${{ secrets.SMTP_USE_SSL }} + SMTP_FROM: ${{ secrets.SMTP_FROM }} run: | helm upgrade --install myapp ./7project/charts/myapp-chart \ -n prod --create-namespace \ @@ -111,4 +118,11 @@ jobs: --set-string oauth.csas.clientId="$CSAS_CLIENT_ID" \ --set-string oauth.csas.clientSecret="$CSAS_CLIENT_SECRET" \ --set-string sentry_dsn="$SENTRY_DSN" \ - --set-string database.encryptionSecret="${{ secrets.PROD_DB_ENCRYPTION_KEY }}" \ No newline at end of file + --set-string database.encryptionSecret="${{ secrets.PROD_DB_ENCRYPTION_KEY }}" \ + --set-string smtp.host="$SMTP_HOST" \ + --set smtp.port="$SMTP_PORT" \ + --set-string smtp.username="$SMTP_USERNAME" \ + --set-string smtp.password="$SMTP_PASSWORD" \ + --set-string smtp.tls="$SMTP_USE_TLS" \ + --set-string smtp.ssl="$SMTP_USE_SSL" \ + --set-string smtp.from="$SMTP_FROM" \ No newline at end of file diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 024be54..2c73b58 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -60,5 +60,7 @@ jobs: working-directory: ./7project/backend - name: Run tests with pytest + env: + PYTEST_RUN_CONFIG: "True" run: pytest working-directory: ./7project/backend \ No newline at end of file diff --git a/7project/backend/app/app.py b/7project/backend/app/app.py index eb3fa14..4ea49f1 100644 --- a/7project/backend/app/app.py +++ b/7project/backend/app/app.py @@ -1,10 +1,11 @@ +import json import logging import os import sys from datetime import datetime from pythonjsonlogger import jsonlogger -from fastapi import Depends, FastAPI +from fastapi import Depends, FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from prometheus_fastapi_instrumentator import Instrumentator, metrics from starlette.requests import Request @@ -59,7 +60,8 @@ fastApi.add_middleware( allow_headers=["*"], ) -if os.getenv("DISABLE_METRICS") != "1": + +if not os.getenv("PYTEST_RUN_CONFIG"): prometheus = Instrumentator().instrument(fastApi) # Register custom metrics prometheus.add(number_of_users()).add(number_of_transactions()) @@ -73,7 +75,6 @@ fastApi.include_router(auth_router) fastApi.include_router(categories_router) fastApi.include_router(transactions_router) - for h in list(logging.root.handlers): logging.root.removeHandler(h) @@ -86,7 +87,6 @@ _log_handler.setFormatter(_formatter) logging.root.setLevel(logging.INFO) logging.root.addHandler(_log_handler) - for _name in ("uvicorn", "uvicorn.error", "uvicorn.access"): _logger = logging.getLogger(_name) _logger.handlers = [_log_handler] @@ -169,16 +169,12 @@ async def authenticated_route(user: User = Depends(current_active_verified_user) return {"message": f"Hello {user.email}!"} -@fastApi.get("/debug/scrape/csas/all", tags=["debug"]) -async def debug_scrape_csas_all(): - logging.info("[Debug] Queueing CSAS scrape for all users via HTTP endpoint (Celery)") +@fastApi.get("/_cron", include_in_schema=False) +async def handle_cron(request: Request): + # endpoint accessed by Clodflare => return 404 + if request.headers.get("cf-connecting-ip"): + raise HTTPException(status_code=404) + + logging.info("[Cron] Triggering scheduled tasks via HTTP endpoint") task = load_all_transactions.delay() return {"status": "queued", "action": "csas_scrape_all", "task_id": getattr(task, 'id', None)} - - -@fastApi.post("/debug/scrape/csas/{user_id}", tags=["debug"]) -async def debug_scrape_csas_user(user_id: str, user: User = Depends(current_active_verified_user)): - logging.info("[Debug] Queueing CSAS scrape for single user via HTTP endpoint (Celery) | user_id=%s", user_id) - task = load_transactions.delay(user_id) - return {"status": "queued", "action": "csas_scrape_single", "user_id": user_id, - "task_id": getattr(task, 'id', None)} diff --git a/7project/backend/app/workers/celery_tasks.py b/7project/backend/app/workers/celery_tasks.py index 2c875d6..399f161 100644 --- a/7project/backend/app/workers/celery_tasks.py +++ b/7project/backend/app/workers/celery_tasks.py @@ -1,5 +1,8 @@ import logging import asyncio +import os +import smtplib +from email.message import EmailMessage from celery import shared_task @@ -84,8 +87,45 @@ def send_email(to: str, subject: str, body: str) -> None: logger.error("Email task missing fields. to=%r subject=%r body_len=%r", to, subject, len(body) if body else 0) return - # Placeholder for real email sending logic - logger.info("[Celery] Email sent | to=%s | subject=%s | body_len=%d", to, subject, len(body)) + host = os.getenv("SMTP_HOST") + if not host: + logger.error("SMTP_HOST is not configured; cannot send email") + return + + # Configuration + port = int(os.getenv("SMTP_PORT", "25")) + username = os.getenv("SMTP_USERNAME") + password = os.getenv("SMTP_PASSWORD") + use_tls = os.getenv("SMTP_USE_TLS", "0").lower() in {"1", "true", "yes"} + use_ssl = os.getenv("SMTP_USE_SSL", "0").lower() in {"1", "true", "yes"} + timeout = int(os.getenv("SMTP_TIMEOUT", "10")) + mail_from = os.getenv("SMTP_FROM") or username or "noreply@localhost" + + # Build message + msg = EmailMessage() + msg["To"] = to + msg["From"] = mail_from + msg["Subject"] = subject + msg.set_content(body) + + try: + if use_ssl: + with smtplib.SMTP_SSL(host=host, port=port, timeout=timeout) as smtp: + if username and password: + smtp.login(username, password) + smtp.send_message(msg) + else: + with smtplib.SMTP(host=host, port=port, timeout=timeout) as smtp: + # STARTTLS if requested + if use_tls: + smtp.starttls() + if username and password: + smtp.login(username, password) + smtp.send_message(msg) + logger.info("[Celery] Email sent | to=%s | subject=%s | body_len=%d", to, subject, len(body)) + except Exception: + logger.exception("Failed to send email via SMTP to=%s subject=%s host=%s port=%s tls=%s ssl=%s", to, subject, + host, port, use_tls, use_ssl) @shared_task(name="workers.load_transactions") diff --git a/7project/charts/myapp-chart/templates/cron.yaml b/7project/charts/myapp-chart/templates/cron.yaml new file mode 100644 index 0000000..67b0870 --- /dev/null +++ b/7project/charts/myapp-chart/templates/cron.yaml @@ -0,0 +1,25 @@ +{{- if .Values.cron.enabled }} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: cronjob +spec: + schedule: {{ .Values.cron.schedule | quote }} + concurrencyPolicy: {{ .Values.cron.concurrencyPolicy | quote }} + jobTemplate: + spec: + template: + spec: + containers: + - name: cronjob + image: curlimages/curl:latest + imagePullPolicy: IfNotPresent + args: + - -sS + - -o + - /dev/null + - -w + - "%{http_code}" + - {{ printf "%s://%s.%s.svc.cluster.local%s" .Values.cron.scheme .Values.app.name .Release.Namespace .Values.cron.endpoint | quote }} + restartPolicy: OnFailure +{{- end }} \ No newline at end of file diff --git a/7project/charts/myapp-chart/templates/prod.yaml b/7project/charts/myapp-chart/templates/prod.yaml index abb294a..01f76ef 100644 --- a/7project/charts/myapp-chart/templates/prod.yaml +++ b/7project/charts/myapp-chart/templates/prod.yaml @@ -19,3 +19,10 @@ stringData: RABBITMQ_USERNAME: {{ .Values.rabbitmq.username | quote }} SENTRY_DSN: {{ .Values.sentry_dsn | quote }} DB_ENCRYPTION_KEY: {{ required "Set .Values.database.encryptionSecret" .Values.database.encryptionSecret | quote }} + SMTP_HOST: {{ .Values.smtp.host | default "" | quote }} + SMTP_PORT: {{ .Values.smtp.port | default 587 | quote }} + SMTP_USERNAME: {{ .Values.smtp.username | default "" | quote }} + SMTP_PASSWORD: {{ .Values.smtp.password | default "" | quote }} + SMTP_USE_TLS: {{ .Values.smtp.tls | default false | quote }} + SMTP_USE_SSL: {{ .Values.smtp.ssl | default false | quote }} + SMTP_FROM: {{ .Values.smtp.from | default "" | quote }} diff --git a/7project/charts/myapp-chart/templates/worker-deployment.yaml b/7project/charts/myapp-chart/templates/worker-deployment.yaml index fbd5182..b6979c2 100644 --- a/7project/charts/myapp-chart/templates/worker-deployment.yaml +++ b/7project/charts/myapp-chart/templates/worker-deployment.yaml @@ -85,3 +85,38 @@ spec: secretKeyRef: name: prod key: DB_ENCRYPTION_KEY + - name: SMTP_HOST + valueFrom: + secretKeyRef: + name: prod + key: SMTP_HOST + - name: SMTP_PORT + valueFrom: + secretKeyRef: + name: prod + key: SMTP_PORT + - name: SMTP_USERNAME + valueFrom: + secretKeyRef: + name: prod + key: SMTP_USERNAME + - name: SMTP_PASSWORD + valueFrom: + secretKeyRef: + name: prod + key: SMTP_PASSWORD + - name: SMTP_USE_TLS + valueFrom: + secretKeyRef: + name: prod + key: SMTP_USE_TLS + - name: SMTP_USE_SSL + valueFrom: + secretKeyRef: + name: prod + key: SMTP_USE_SSL + - name: SMTP_FROM + valueFrom: + secretKeyRef: + name: prod + key: SMTP_FROM diff --git a/7project/charts/myapp-chart/values-prod.yaml b/7project/charts/myapp-chart/values-prod.yaml index b7100de..d0cd82c 100644 --- a/7project/charts/myapp-chart/values-prod.yaml +++ b/7project/charts/myapp-chart/values-prod.yaml @@ -5,3 +5,6 @@ app: worker: replicas: 3 + +cron: + enabled: true diff --git a/7project/charts/myapp-chart/values.yaml b/7project/charts/myapp-chart/values.yaml index d20fa70..cbffb51 100644 --- a/7project/charts/myapp-chart/values.yaml +++ b/7project/charts/myapp-chart/values.yaml @@ -35,6 +35,23 @@ worker: # Queue name for Celery worker and for CRD Queue mailQueueName: "mail_queue" +cron: + enabled: false + schedule: "*/5 * * * *" # every 5 minutes + scheme: "http" + endpoint: "/_cron" + concurrencyPolicy: "Forbid" + +smtp: + host: + port: 587 + username: "" + password: "" + tls: false + ssl: false + from: "" + + service: port: 80