From 4ffaaaa96d08a916e6650726fe7a489d5dec38be Mon Sep 17 00:00:00 2001 From: danielvasic Date: Tue, 19 May 2026 10:00:21 +0200 Subject: [PATCH] init --- .dockerignore | 10 ++++ .gitignore | 6 +++ Dockerfile | 36 +++++++++++++ README.md | 39 ++++++++++++++ app/main.py | 125 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 36 +++++++++++++ requirements.txt | 5 ++ 7 files changed, 257 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 app/main.py create mode 100644 docker-compose.yml create mode 100644 requirements.txt diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..01f4081 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +__pycache__/ +*.pyc +*.pyo +.env +.venv +venv/ +.git/ +.gitignore +docker-compose.yml +README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3848e6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +*.pyo +.env +.venv +venv/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..35809eb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +# ── Stage 1: builder ───────────────────────────────────────────────────────── +FROM python:3.12-slim AS builder + +WORKDIR /build + +COPY requirements.txt . + +# Kompajliraj dependencies u wheel fajlove +RUN pip install --upgrade pip && \ + pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt + +# ── Stage 2: production ─────────────────────────────────────────────────────── +FROM python:3.12-slim + +# Ne koristiti root +RUN adduser --disabled-password --gecos "" appuser + +WORKDIR /app + +# Kopiraj samo wheel fajlove iz buildera +COPY --from=builder /wheels /wheels +RUN pip install --no-cache-dir --no-index --find-links=/wheels /wheels/* && \ + rm -rf /wheels + +# Kopiraj aplikaciju +COPY app/ ./app/ + +# Prebaci na non-root korisnika +USER appuser + +EXPOSE 8000 + +HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..78791aa --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# fastapi-todo-api + +Minimalan FastAPI REST API s PostgreSQL bazom — za studentske GitOps vježbe na FPMOZ. + +## Endpointi + +| Metoda | URL | Opis | +|--------|-----|------| +| GET | `/health` | Status aplikacije i baze | +| GET | `/todos` | Lista svih zapisa | +| GET | `/todos/{id}` | Jedan zapis | +| POST | `/todos` | Kreiraj zapis | +| PUT | `/todos/{id}` | Ažuriraj zapis | +| DELETE | `/todos/{id}` | Obriši zapis | + +Swagger dokumentacija: `http://localhost:8000/docs` + +## Lokalno pokretanje + +``` +docker compose up --build +``` + +## Environment varijable + +| Varijabla | Default | Opis | +|-----------|---------|------| +| `DATABASE_HOST` | `localhost` | Hostname baze | +| `DATABASE_PORT` | `5432` | Port baze | +| `POSTGRES_USER` | `fastapiuser` | Korisnik baze | +| `POSTGRES_PASSWORD` | `student123` | Lozinka baze | +| `POSTGRES_DB` | `fastapidb` | Naziv baze | + +## Build i push na Docker Hub + +``` +docker build -t vasic/fastapi-todo-api:1.0 . +docker push vasic/fastapi-todo-api:1.0 +``` diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..5f652e2 --- /dev/null +++ b/app/main.py @@ -0,0 +1,125 @@ +from fastapi import FastAPI, HTTPException, Depends +from sqlalchemy import create_engine, Column, Integer, String, Boolean, text +from sqlalchemy.orm import declarative_base, Session, sessionmaker +from pydantic import BaseModel +from typing import Optional +import os +import time +import logging + +logging.basicConfig(level=logging.INFO) +log = logging.getLogger(__name__) + +# ── Database connection ─────────────────────────────────────────────────────── +DB_HOST = os.getenv("DATABASE_HOST", "localhost") +DB_PORT = os.getenv("DATABASE_PORT", "5432") +DB_USER = os.getenv("POSTGRES_USER", "fastapiuser") +DB_PASS = os.getenv("POSTGRES_PASSWORD", "student123") +DB_NAME = os.getenv("POSTGRES_DB", "fastapidb") + +DATABASE_URL = f"postgresql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}" + +# Retry logic — baza mozda nije odmah dostupna kad Pod starta +engine = None +for attempt in range(10): + try: + engine = create_engine(DATABASE_URL) + with engine.connect() as conn: + conn.execute(text("SELECT 1")) + log.info(f"Baza dostupna (pokusaj {attempt + 1})") + break + except Exception as e: + log.warning(f"Baza nije dostupna, cekam 3s... (pokusaj {attempt + 1}): {e}") + time.sleep(3) + +if engine is None: + raise RuntimeError("Ne mogu se spojiti na bazu nakon 10 pokusaja") + +SessionLocal = sessionmaker(bind=engine) +Base = declarative_base() + +# ── Model ───────────────────────────────────────────────────────────────────── +class Todo(Base): + __tablename__ = "todos" + id = Column(Integer, primary_key=True, index=True) + title = Column(String(200), nullable=False) + done = Column(Boolean, default=False) + +Base.metadata.create_all(bind=engine) + +# ── Schemas ─────────────────────────────────────────────────────────────────── +class TodoCreate(BaseModel): + title: str + done: Optional[bool] = False + +class TodoUpdate(BaseModel): + title: Optional[str] = None + done: Optional[bool] = None + +class TodoOut(BaseModel): + id: int + title: str + done: bool + + model_config = {"from_attributes": True} + +# ── App ─────────────────────────────────────────────────────────────────────── +app = FastAPI(title="FastAPI Todo API", version="1.0.0") + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +# ── Routes ──────────────────────────────────────────────────────────────────── +@app.get("/health") +def health(): + try: + with engine.connect() as conn: + conn.execute(text("SELECT 1")) + return {"status": "ok", "database": "connected"} + except Exception as e: + raise HTTPException(status_code=503, detail=f"Baza nedostupna: {e}") + +@app.get("/todos", response_model=list[TodoOut]) +def get_todos(db: Session = Depends(get_db)): + return db.query(Todo).all() + +@app.get("/todos/{todo_id}", response_model=TodoOut) +def get_todo(todo_id: int, db: Session = Depends(get_db)): + todo = db.query(Todo).filter(Todo.id == todo_id).first() + if not todo: + raise HTTPException(status_code=404, detail="Todo nije pronaden") + return todo + +@app.post("/todos", response_model=TodoOut, status_code=201) +def create_todo(payload: TodoCreate, db: Session = Depends(get_db)): + todo = Todo(title=payload.title, done=payload.done) + db.add(todo) + db.commit() + db.refresh(todo) + return todo + +@app.put("/todos/{todo_id}", response_model=TodoOut) +def update_todo(todo_id: int, payload: TodoUpdate, db: Session = Depends(get_db)): + todo = db.query(Todo).filter(Todo.id == todo_id).first() + if not todo: + raise HTTPException(status_code=404, detail="Todo nije pronaden") + if payload.title is not None: + todo.title = payload.title + if payload.done is not None: + todo.done = payload.done + db.commit() + db.refresh(todo) + return todo + +@app.delete("/todos/{todo_id}") +def delete_todo(todo_id: int, db: Session = Depends(get_db)): + todo = db.query(Todo).filter(Todo.id == todo_id).first() + if not todo: + raise HTTPException(status_code=404, detail="Todo nije pronaden") + db.delete(todo) + db.commit() + return {"message": f"Todo {todo_id} obrisan"} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..15bbd32 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +services: + + db: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: fastapiuser + POSTGRES_PASSWORD: student123 + POSTGRES_DB: fastapidb + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD", "pg_isready", "-U", "fastapiuser", "-d", "fastapidb"] + interval: 5s + timeout: 5s + retries: 10 + + api: + build: . + restart: unless-stopped + ports: + - "8000:8000" + environment: + DATABASE_HOST: db + DATABASE_PORT: "5432" + POSTGRES_USER: fastapiuser + POSTGRES_PASSWORD: student123 + POSTGRES_DB: fastapidb + depends_on: + db: + condition: service_healthy + +volumes: + postgres_data: diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c686145 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.111.0 +uvicorn==0.30.1 +sqlalchemy==2.0.30 +psycopg2-binary==2.9.9 +pydantic==2.7.1