diff --git a/.dockerignore b/.dockerignore index da93349..661867d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,6 +15,8 @@ coverage.xml .git .gitignore .pytest_cache +.ruff_cache +htmlcov/ .venv venv/ ENV/ diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index a98b15e..2636ba3 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -2,12 +2,12 @@ name: CI/CD Pipeline on: release: - types: [ published ] + types: [published] push: branches: - - '**' + - "**" tags-ignore: - - '**' + - "**" jobs: lint: @@ -18,6 +18,16 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + cache-dependency-path: requirements.txt + - name: Install dependencies + run: python -m pip install --upgrade pip && python -m pip install -r requirements.txt + - name: Run Ruff + run: ruff check . test: name: Run Tests @@ -27,16 +37,38 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + cache-dependency-path: requirements.txt + - name: Install dependencies + run: python -m pip install --upgrade pip && python -m pip install -r requirements.txt + - name: Run pytest with coverage threshold + run: pytest --cov-fail-under=80 build: name: Build Docker Image runs-on: ubuntu-latest needs: [lint, test] - if: github.event_name == 'push' || github.event_name == 'release' permissions: contents: read - actions: read packages: write steps: - name: Checkout code uses: actions/checkout@v4 + - name: Build Docker image + run: docker build -f docker/Dockerfile -t fastapi-gitops-starter:ci . + - name: Log in to GitHub Container Registry + if: github.event_name == 'release' + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin + - name: Tag and push release image + if: github.event_name == 'release' + run: | + IMAGE_NAME="ghcr.io/${GITHUB_REPOSITORY,,}" + VERSION_TAG="${{ github.event.release.tag_name }}" + docker tag fastapi-gitops-starter:ci "$IMAGE_NAME:$VERSION_TAG" + docker tag fastapi-gitops-starter:ci "$IMAGE_NAME:latest" + docker push "$IMAGE_NAME:$VERSION_TAG" + docker push "$IMAGE_NAME:latest" diff --git a/.github/workflows/markdown2pdf.yml b/.github/workflows/markdown2pdf.yml index fcb4671..4177b58 100644 --- a/.github/workflows/markdown2pdf.yml +++ b/.github/workflows/markdown2pdf.yml @@ -19,7 +19,7 @@ jobs: - name: Replace links run: | - cp README.md README_WITH_LINKS.md + cp README.md README_WITH_LINKS.md sed -i -e "s#\(^\!\[[^]]\+\](\)\(images/\)#\1$URL/\2#g" README_WITH_LINKS.md for file in sources/*; do sed -i -e "s#($file)#($URL/$file)#g" README_WITH_LINKS.md ; done diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d337d10..a443203 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,3 +3,22 @@ repos: rev: v5.0.0 hooks: - id: trailing-whitespace + - id: check-added-large-files + - id: check-yaml + exclude: ^helm/.*/templates/ + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.4 + hooks: + - id: ruff + args: [--fix] + files: ^(app|tests)/.*\.py$ + - repo: https://github.com/PyCQA/bandit + rev: 1.7.10 + hooks: + - id: bandit + args: [-q] + files: ^app/.*\.py$ + - repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 + hooks: + - id: detect-secrets diff --git a/2-GitOps with FastAPI.pdf b/2-GitOps with FastAPI.pdf new file mode 100644 index 0000000..4b59339 Binary files /dev/null and b/2-GitOps with FastAPI.pdf differ diff --git a/README.md b/README.md index 79bb814..057f54d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ GitOps with FastAPI ***University of Amsterdam*** -# 1. Introduction +# 1. Introduction In this tutorial, we use GitOps practices with FastAPI, including CI/CD pipelines, code quality tools, and automated testing. @@ -35,7 +35,7 @@ In this tutorial, we use GitOps practices with FastAPI, including CI/CD pipeline -# 2. Tutorial +# 2. Tutorial The steps of this tutorial are as follows: - [Building REST APIs with FastAPI](#21-setting-up-the-project) @@ -60,17 +60,17 @@ Prerequisites: ``` * Set Up the Python Environmentt: - + ```bash # Create a virtual environment python -m venv venv - + # Activate the virtual environment # On Linux/MacOS: source venv/bin/activate # On Windows: venv\Scripts\activate - + # Install dependencies pip install -r requirements.txt ``` @@ -110,7 +110,7 @@ Prerequisites: ```bash # Check for issues ruff check app/ tests/ - + # Fix auto-fixable issues ruff check app/ tests/ --fix ``` @@ -120,7 +120,7 @@ Prerequisites: ```bash # Check formatting black --check app/ tests/ - + # Format code black app/ tests/ ``` @@ -134,7 +134,7 @@ Pre-commit hooks automatically run checks before each commit to ensure consisten ```bash # Install pre-commit pip install pre-commit - + # Install the git hooks pre-commit install ``` @@ -142,11 +142,11 @@ Pre-commit hooks automatically run checks before each commit to ensure consisten * Using Pre-commit: Pre-commit will now run automatically on `git commit`. You can also run it manually: - + ```bash # Run on all files pre-commit run --all-files - + # Run on staged files pre-commit run ``` @@ -200,14 +200,14 @@ This repository includes a Helm chart for deploying the application to Kubernete - Kubernetes 1.19+ - Helm 3.0+ -* Install the Helm Chart: +* Install the Helm Chart: ```bash helm install my-release ./helm/fastapi-gitops-starter ``` -* Uninstall the Helm Chart: - +* Uninstall the Helm Chart: + ```bash helm uninstall my-release ``` @@ -239,7 +239,7 @@ including host and paths. * To make sure we do not commit secrets * To check code style - + ## 3.2 Add a New Endpoint 1. Open `app/main.py` diff --git a/app/main.py b/app/main.py index ae8adc6..fa3bc08 100644 --- a/app/main.py +++ b/app/main.py @@ -48,6 +48,16 @@ async def get_item(item_id: int): "description": f"This is item number {item_id}", } +@app.post("/api/items") +async def create_item(name: str, description: str): + """Create a new item.""" + return { + "id": 999, + "name": name, + "description": description, + "created": True + } + if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=8000) + uvicorn.run(app, host=os.getenv("UVICORN_HOST", "127.0.0.1"), port=8000) diff --git a/custom-values.yaml b/custom-values.yaml new file mode 100644 index 0000000..168765e --- /dev/null +++ b/custom-values.yaml @@ -0,0 +1,27 @@ +image: + repository: fastapi-gitops-starter + tag: "local" + pullPolicy: Never + +registry: + createImagePullSecret: false + +imagePullSecrets: [] + +ingress: + enabled: true + className: nginx + hosts: + - host: minikube.test + paths: + - path: /GitOps-Starter + pathType: Prefix + +app: + rootPath: "/GitOps-Starter" + +autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 3 + targetCPUUtilizationPercentage: 10 \ No newline at end of file diff --git a/external-services-values/argo-cd-values.yaml b/external-services-values/argo-cd-values.yaml index 6453246..a042c8f 100644 --- a/external-services-values/argo-cd-values.yaml +++ b/external-services-values/argo-cd-values.yaml @@ -3,8 +3,8 @@ global: configs: secret: - argocdServerAdminPassword: "$2a$10$f4euqaB7AZX41WFALuxIEu7FakyF03ir.zh48m0oVRi9B7zttHztq" - dex.config: | + argocdServerAdminPassword: "$2a$10$f4euqaB7AZX41WFALuxIEu7FakyF03ir.zh48m0oVRi9B7zttHztq" # pragma: allowlist secret + dex.config: | # pragma: allowlist secret connectors: - type: github id: github diff --git a/external-services-values/monitoring-values.yaml b/external-services-values/monitoring-values.yaml index d161e49..dbb43fb 100644 --- a/external-services-values/monitoring-values.yaml +++ b/external-services-values/monitoring-values.yaml @@ -2,7 +2,7 @@ grafana: enabled: true initChownData: enabled: false - adminPassword: password + adminPassword: password # pragma: allowlist secret grafana.ini: server: domain: naavre-dev.minikube.test diff --git a/helm/fastapi-gitops-starter/example-canary-with-analysis-values.yaml b/helm/fastapi-gitops-starter/example-canary-with-analysis-values.yaml index d5b2aba..2a92453 100644 --- a/helm/fastapi-gitops-starter/example-canary-with-analysis-values.yaml +++ b/helm/fastapi-gitops-starter/example-canary-with-analysis-values.yaml @@ -13,7 +13,7 @@ image: registry: createImagePullSecret: true - secretName: image-pull-secret + secretName: image-pull-secret # pragma: allowlist secret server: ghcr.io token: "" diff --git a/helm/fastapi-gitops-starter/secret-example-canary-values.yaml b/helm/fastapi-gitops-starter/secret-example-canary-values.yaml index 39aa040..3c1c989 100644 --- a/helm/fastapi-gitops-starter/secret-example-canary-values.yaml +++ b/helm/fastapi-gitops-starter/secret-example-canary-values.yaml @@ -13,9 +13,9 @@ image: registry: createImagePullSecret: true - secretName: image-pull-secret + secretName: image-pull-secret # pragma: allowlist secret server: ghcr.io - token: "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" + token: "" # Enable Argo Rollouts with Canary strategy and automated analysis rollout: diff --git a/helm/fastapi-gitops-starter/secret-example-values.yaml b/helm/fastapi-gitops-starter/secret-example-values.yaml index 68c2a0e..1cafc2e 100644 --- a/helm/fastapi-gitops-starter/secret-example-values.yaml +++ b/helm/fastapi-gitops-starter/secret-example-values.yaml @@ -9,9 +9,9 @@ image: registry: createImagePullSecret: true - secretName: image-pull-secret + secretName: image-pull-secret # pragma: allowlist secret server: ghcr.io - token: "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" + token: "" imagePullSecrets: - name: image-pull-secret diff --git a/helm/fastapi-gitops-starter/values.yaml b/helm/fastapi-gitops-starter/values.yaml index 20c08be..3f55dc1 100644 --- a/helm/fastapi-gitops-starter/values.yaml +++ b/helm/fastapi-gitops-starter/values.yaml @@ -12,7 +12,7 @@ image: registry: createImagePullSecret: true - secretName: image-pull-secret + secretName: image-pull-secret # pragma: allowlist secret server: ghcr.io token: "" diff --git a/tests/test_main.py b/tests/test_main.py index db89e2f..8c2fda6 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -41,3 +41,19 @@ def test_get_item(): assert data["id"] == 5 assert data["name"] == "Item 5" assert "item number 5" in data["description"] + +def test_create_item(): + """Test the create item endpoint.""" + response = client.post( + "/api/items", + params={ + "name": "New Item", + "description": "A newly created item" + } + ) + assert response.status_code == 200 + data = response.json() + assert data["id"] == 999 + assert data["name"] == "New Item" + assert data["description"] == "A newly created item" + assert data["created"] is True