From 0f80707bd237dffe02e96876b47d96b1e33a9d02 Mon Sep 17 00:00:00 2001 From: joesteel Date: Mon, 15 Sep 2025 18:16:35 +0100 Subject: [PATCH] adding helm charts --- common.sh | 13 +++++ .../my-stack/charts/fastapi/Chart.yaml | 6 +++ .../charts/fastapi/templates/deployment.yaml | 31 +++++++++++ .../charts/fastapi/templates/service.yaml | 12 +++++ minikube_deploy.sh | 9 ++-- source/app.py | 19 ++++++- source/dao/profile_dao.py | 26 ++++++++++ source/models/InsertProfile.py | 6 +++ source/models/__init__.py | 0 test/test_main.py | 51 ++++++++++++++++++- 10 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 helm_charts/my-stack/charts/fastapi/Chart.yaml create mode 100644 helm_charts/my-stack/charts/fastapi/templates/deployment.yaml create mode 100644 helm_charts/my-stack/charts/fastapi/templates/service.yaml create mode 100644 source/models/InsertProfile.py create mode 100644 source/models/__init__.py diff --git a/common.sh b/common.sh index 4f85bcc..ccf4fda 100755 --- a/common.sh +++ b/common.sh @@ -4,6 +4,19 @@ log() { echo -e "\033[0;32m$1\033[0m" } + +run_tests_locally(){ + log "====== installing dependencies locally ======" + pip install --upgrade pip + pip install --no-cache-dir -r requirements.txt + + log "✅ Running unit tests.... %f" + pytest +} + +source ../venv/bin/activate +set -e + NETWORK="dating-app-network" FAPI_LOCAL_PORT=8000 FAPI_KUBE_PORT=9000 diff --git a/helm_charts/my-stack/charts/fastapi/Chart.yaml b/helm_charts/my-stack/charts/fastapi/Chart.yaml new file mode 100644 index 0000000..9272574 --- /dev/null +++ b/helm_charts/my-stack/charts/fastapi/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: fastapi +description: FastAPI app +type: application +version: 0.1.0 +appVersion: "1.0.0" \ No newline at end of file diff --git a/helm_charts/my-stack/charts/fastapi/templates/deployment.yaml b/helm_charts/my-stack/charts/fastapi/templates/deployment.yaml new file mode 100644 index 0000000..29a16ff --- /dev/null +++ b/helm_charts/my-stack/charts/fastapi/templates/deployment.yaml @@ -0,0 +1,31 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fastapi-deployment +spec: + replicas: 1 + selector: + matchLabels: + app: fastapi + template: + metadata: + labels: + app: fastapi + spec: + containers: + - name: fastapi + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: Never + ports: + - containerPort: 8000 + env : + - name: DB_PASS + value: "pass123" + - name: DB_USER + value: "aiuser" + - name: DB_NAME + value: "profiledb" + - name: DB_HOST + value: "postgres-0.postgres.default.svc.cluster.local" + - name: DB_PORT + value: "5432" diff --git a/helm_charts/my-stack/charts/fastapi/templates/service.yaml b/helm_charts/my-stack/charts/fastapi/templates/service.yaml new file mode 100644 index 0000000..6adb250 --- /dev/null +++ b/helm_charts/my-stack/charts/fastapi/templates/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: fastapi-service +spec: + type: NodePort + selector: + app: fastapi + ports: + - port: 8000 + targetPort: 8000 + nodePort: 30080 \ No newline at end of file diff --git a/minikube_deploy.sh b/minikube_deploy.sh index f9a1ea6..95c3a7d 100755 --- a/minikube_deploy.sh +++ b/minikube_deploy.sh @@ -3,19 +3,18 @@ source ./common.sh RELEASE_TYPE=$1 if [ -z "$RELEASE_TYPE" ]; then - echo "No RELEASE_TYPE provided. Running in default mode." + echo "No RELEASE_TYPE provided. Shallow deploy in progress." RELEASE_TYPE="default" fi set -e -log "🚀 Starting minikube..." -minikube start +run_tests_locally log "🚀 getting k8ts ready..." eval "$(minikube docker-env)" docker build -t $FAPI_IMAGE_NAME . -minikube image load $FAPI_IMAGE_NAME +#minikube image load $FAPI_IMAGE_NAME if [ "$RELEASE_TYPE" == "ff" ]; then ( set +e @@ -23,7 +22,7 @@ if [ "$RELEASE_TYPE" == "ff" ]; then kubectl apply -f k8s/ ) else - kubectl rollout restart deployment fastapi + kubectl rollout restart deployment fastapi-deployment fi kubectl port-forward service/fastapi-service $FAPI_KUBE_PORT:$FAPI_LOCAL_PORT diff --git a/source/app.py b/source/app.py index ec3c38d..9e5608c 100644 --- a/source/app.py +++ b/source/app.py @@ -1,6 +1,7 @@ -from fastapi import FastAPI, Response, Depends +from fastapi import FastAPI, Response, Depends, HTTPException from source.dao.profile_dao import ProfileDao from source.models.profile import ProfileModel +from source.models.InsertProfile import InsertProfileModel import logging app = FastAPI() @@ -31,3 +32,19 @@ def read_root(): def get_random_profile(dao: ProfileDao = Depends(get_dao)): profile = dao.fetch_random_profile() return profile or {"error": "no profiles found"} + + +@app.get("/profiles/profile/{profile_id}", response_model=ProfileModel) +def get_random_profile(profile_id: int, dao: ProfileDao = Depends(get_dao)): + profile = dao.get_profile_by_id(profile_id) + if profile is None: + raise HTTPException(status_code=404, detail="Profile not found") + return profile + + +@app.post("/profiles", status_code=201, response_model=ProfileModel) +def create_profile(profile: InsertProfileModel, dao: ProfileDao = Depends(get_dao)): + profile = dao.insert_profile(profile.name, profile.description) + if profile is None: + raise HTTPException(status_code=404, detail="Profile not found") + return profile diff --git a/source/dao/profile_dao.py b/source/dao/profile_dao.py index ab09490..3346bd0 100644 --- a/source/dao/profile_dao.py +++ b/source/dao/profile_dao.py @@ -1,5 +1,6 @@ import logging import psycopg2 +from psycopg2.extras import RealDictCursor from source.configuration.profile_db_config import ProfileConfig logger = logging.getLogger(__name__) @@ -22,3 +23,28 @@ def fetch_random_profile(self): if row: return {"id": row[0], "name": row[1], "description": row[2]} return None + + def get_profile_by_id(self, profile_id: int): + conn = psycopg2.connect(self.db_url) + cur = conn.cursor() + cur.execute("SELECT id, name, description FROM profiles where id = %s", [profile_id]) + row = cur.fetchone() + cur.close() + conn.close() + if row: + return {"id": row[0], "name": row[1], "description": row[2]} + return None + + def insert_profile(self, name: str, description: str): + conn = psycopg2.connect(self.db_url) + cur = conn.cursor(cursor_factory=RealDictCursor) + cur.execute("INSERT INTO profiles (name, description) VALUES (%s, %s) RETURNING *", (name, description)) + inserted_profile = cur.fetchone() + if not inserted_profile: + conn.rollback() + raise Exception("Insert failed: no row returned") + conn.commit() + cur.close() + conn.close() + return inserted_profile + diff --git a/source/models/InsertProfile.py b/source/models/InsertProfile.py new file mode 100644 index 0000000..531a506 --- /dev/null +++ b/source/models/InsertProfile.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class InsertProfileModel(BaseModel): + name: str + description: str diff --git a/source/models/__init__.py b/source/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_main.py b/test/test_main.py index 48457f2..7914d22 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -1,13 +1,32 @@ from fastapi.testclient import TestClient from source.app import app, get_dao -fake_profile_json = {"id": 42, "name": "Stub", "description": "Fake profile"} +fake_id = 42 +fake_name = "stubbed name" +fake_description = "stubbed description" +fake_profile_json = {"id": fake_id, "name": fake_name, "description": fake_description} class StubProfileDAO: + last_inserted = {} + def fetch_random_profile(self): return fake_profile_json + def get_profile_by_id(self, profile_id: int): + return fake_profile_json + + def insert_profile(self, name: str, description: str): + fake_inserted_profile = { + "id": fake_id, + "name": name, + "description": description + } + self.last_inserted = fake_inserted_profile + return fake_profile_json + + + def test_root_custom_header(): client = TestClient(app) @@ -27,3 +46,33 @@ def test_random_profile(): app.dependency_overrides = {} return None + + +def test_profile_by_id(): + app.dependency_overrides[get_dao] = lambda: StubProfileDAO() + + client = TestClient(app) + response = client.get("/profiles/profile/1") + assert response.status_code == 200 + assert response.json() == fake_profile_json + + app.dependency_overrides = {} + return None + + +def test_insert_profile(): + stub_dao = StubProfileDAO() + app.dependency_overrides[get_dao] = lambda: stub_dao + + client = TestClient(app) + payload = {"name": fake_name, "description": fake_description} + + response = client.post('/profiles/', json=payload) + expected_profile = { + "id": fake_id, + "name": fake_name, + "description": fake_description + } + assert response.json() == expected_profile + assert stub_dao.last_inserted == expected_profile + return None