From 29b3d051a6f0672a605571c29bab8cb74682bf19 Mon Sep 17 00:00:00 2001 From: Jona Aalten <159295595+jaalten@users.noreply.github.com> Date: Sat, 7 Mar 2026 16:29:39 +0100 Subject: [PATCH 1/4] add: Add pre-commit hooks --- .github/workflows/markdown2pdf.yml | 4 +- .github/workflows/tests_md-urls.yml | 2 +- .pre-commit-config.yaml | 28 +++ .secrets.baseline | 210 ++++++++++++++++++ README.md | 30 +-- app/main.py | 2 +- .../example-values.yaml | 2 - .../secret-example-values.yaml | 2 - 8 files changed, 257 insertions(+), 23 deletions(-) create mode 100644 .secrets.baseline diff --git a/.github/workflows/markdown2pdf.yml b/.github/workflows/markdown2pdf.yml index fcb4671..dd5035d 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 @@ -55,4 +55,4 @@ jobs: id: upload-readme-file with: name: 'README_WITH_LINKS.md' - path: README_WITH_LINKS.md \ No newline at end of file + path: README_WITH_LINKS.md diff --git a/.github/workflows/tests_md-urls.yml b/.github/workflows/tests_md-urls.yml index f61463f..402cf3e 100644 --- a/.github/workflows/tests_md-urls.yml +++ b/.github/workflows/tests_md-urls.yml @@ -16,4 +16,4 @@ jobs: with: file_types: .md,yaml,json retry_count: 3 - exclude_patterns: http://IP:NODE_PORT,http://localhost,http://xxxxxxxxx.compute-1.amazonaws.com,http://IP,http://minikube.test,http://prometheus.monitoring:9090 \ No newline at end of file + exclude_patterns: http://IP:NODE_PORT,http://localhost,http://xxxxxxxxx.compute-1.amazonaws.com,http://IP,http://minikube.test,http://prometheus.monitoring:9090 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d337d10..17d762a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,3 +3,31 @@ repos: rev: v5.0.0 hooks: - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-added-large-files + - id: check-yaml + exclude: ^helm/ + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.4 + hooks: + - id: ruff + args: [--fix] + + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 24.10.0 + hooks: + - id: black + + - repo: https://github.com/PyCQA/bandit + rev: 1.9.3 + hooks: + - id: bandit + args: ["-c", "pyproject.toml"] + exclude: ^tests/ + + - repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 + hooks: + - id: detect-secrets + args: ["--baseline", ".secrets.baseline"] diff --git a/.secrets.baseline b/.secrets.baseline new file mode 100644 index 0000000..11179d8 --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,210 @@ +{ + "version": "1.5.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "CloudantDetector" + }, + { + "name": "DiscordBotTokenDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "name": "GitLabTokenDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "IPPublicDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "OpenAIDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "PypiTokenDetector" + }, + { + "name": "SendGridDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TelegramBotTokenDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + } + ], + "results": { + "external-services-values/argo-cd-values.yaml": [ + { + "type": "Secret Keyword", + "filename": "external-services-values/argo-cd-values.yaml", + "hashed_secret": "9f7be9612bc530ef15cfc6ff8e07dad0a3278ea3", + "is_verified": false, + "line_number": 7 + } + ], + "external-services-values/monitoring-values.yaml": [ + { + "type": "Secret Keyword", + "filename": "external-services-values/monitoring-values.yaml", + "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", + "is_verified": false, + "line_number": 5 + } + ], + "helm/fastapi-gitops-starter/example-canary-with-analysis-values.yaml": [ + { + "type": "Secret Keyword", + "filename": "helm/fastapi-gitops-starter/example-canary-with-analysis-values.yaml", + "hashed_secret": "962e01ff6a135d92494aac7d57464e637cd88249", + "is_verified": false, + "line_number": 16 + } + ], + "helm/fastapi-gitops-starter/secret-example-canary-values.yaml": [ + { + "type": "Secret Keyword", + "filename": "helm/fastapi-gitops-starter/secret-example-canary-values.yaml", + "hashed_secret": "962e01ff6a135d92494aac7d57464e637cd88249", + "is_verified": false, + "line_number": 16 + }, + { + "type": "Base64 High Entropy String", + "filename": "helm/fastapi-gitops-starter/secret-example-canary-values.yaml", + "hashed_secret": "6e01f523d5b539497b7dd212e79e7182fa9a4cab", + "is_verified": false, + "line_number": 18 + }, + { + "type": "GitHub Token", + "filename": "helm/fastapi-gitops-starter/secret-example-canary-values.yaml", + "hashed_secret": "e175c6f5f2a92e8623bd9a4820edb4e8c1b0fd10", + "is_verified": false, + "line_number": 18 + } + ], + "helm/fastapi-gitops-starter/secret-example-values.yaml": [ + { + "type": "Secret Keyword", + "filename": "helm/fastapi-gitops-starter/secret-example-values.yaml", + "hashed_secret": "962e01ff6a135d92494aac7d57464e637cd88249", + "is_verified": false, + "line_number": 12 + }, + { + "type": "Base64 High Entropy String", + "filename": "helm/fastapi-gitops-starter/secret-example-values.yaml", + "hashed_secret": "6e01f523d5b539497b7dd212e79e7182fa9a4cab", + "is_verified": false, + "line_number": 14 + }, + { + "type": "GitHub Token", + "filename": "helm/fastapi-gitops-starter/secret-example-values.yaml", + "hashed_secret": "e175c6f5f2a92e8623bd9a4820edb4e8c1b0fd10", + "is_verified": false, + "line_number": 14 + } + ], + "helm/fastapi-gitops-starter/values.yaml": [ + { + "type": "Secret Keyword", + "filename": "helm/fastapi-gitops-starter/values.yaml", + "hashed_secret": "962e01ff6a135d92494aac7d57464e637cd88249", + "is_verified": false, + "line_number": 15 + } + ] + }, + "generated_at": "2026-03-07T15:20:08Z" +} diff --git a/README.md b/README.md index 79bb814..a289605 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` @@ -287,4 +287,4 @@ kubectl get hpa -n default -w ## 3.5 Questions 1. The auto-scaling did not work as expected. What could be the possible reasons? -2. How does Horizontal Pod Autoscaling (HPA) work in Kubernetes? \ No newline at end of file +2. How does Horizontal Pod Autoscaling (HPA) work in Kubernetes? diff --git a/app/main.py b/app/main.py index ae8adc6..0110ef9 100644 --- a/app/main.py +++ b/app/main.py @@ -50,4 +50,4 @@ async def get_item(item_id: int): if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=8000) + uvicorn.run(app, host="0.0.0.0", port=8000) # nosec B104 diff --git a/helm/fastapi-gitops-starter/example-values.yaml b/helm/fastapi-gitops-starter/example-values.yaml index 2a02efb..1b7fe00 100644 --- a/helm/fastapi-gitops-starter/example-values.yaml +++ b/helm/fastapi-gitops-starter/example-values.yaml @@ -28,5 +28,3 @@ autoscaling: minReplicas: 1 maxReplicas: 3 targetCPUUtilizationPercentage: 10 - - diff --git a/helm/fastapi-gitops-starter/secret-example-values.yaml b/helm/fastapi-gitops-starter/secret-example-values.yaml index 68c2a0e..a5d5e73 100644 --- a/helm/fastapi-gitops-starter/secret-example-values.yaml +++ b/helm/fastapi-gitops-starter/secret-example-values.yaml @@ -34,5 +34,3 @@ autoscaling: minReplicas: 1 maxReplicas: 3 targetCPUUtilizationPercentage: 10 - - From 289c3e7c8cde8d99c09e4bec5bb0ac1c25d3bb89 Mon Sep 17 00:00:00 2001 From: Jona Aalten <159295595+jaalten@users.noreply.github.com> Date: Sat, 7 Mar 2026 16:52:29 +0100 Subject: [PATCH 2/4] add: 3.2 new endpoint --- app/main.py | 11 +++++++++++ tests/test_main.py | 13 +++++++++++++ 2 files changed, 24 insertions(+) diff --git a/app/main.py b/app/main.py index 0110ef9..3314564 100644 --- a/app/main.py +++ b/app/main.py @@ -49,5 +49,16 @@ async def get_item(item_id: int): } +@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) # nosec B104 diff --git a/tests/test_main.py b/tests/test_main.py index db89e2f..55bed23 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -41,3 +41,16 @@ 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": "Test Item", "description": "A test item"} + ) + assert response.status_code == 200 + data = response.json() + assert data["id"] == 999 + assert data["name"] == "Test Item" + assert data["description"] == "A test item" + assert data["created"] is True From 8986ad6aef91322cd49e6f99039d4f86872c57e6 Mon Sep 17 00:00:00 2001 From: Jona Aalten <159295595+jaalten@users.noreply.github.com> Date: Sat, 7 Mar 2026 17:40:57 +0100 Subject: [PATCH 3/4] add: 3.3 ci pipeline --- .github/workflows/ci-cd.yml | 42 +++++++++++++++++++++++++++++++++++-- docker/Dockerfile | 4 ++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index a98b15e..2e1bc54 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -19,6 +19,17 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install Ruff + run: pip install ruff==0.8.4 + + - name: Run Ruff + run: ruff check app/ tests/ + test: name: Run Tests runs-on: ubuntu-latest @@ -28,15 +39,42 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run tests with coverage + run: pytest --cov=app --cov-report=term-missing --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: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + run: | + IMAGE=ghcr.io/${GITHUB_REPOSITORY,,} + docker build -f docker/Dockerfile -t $IMAGE:latest . + docker push $IMAGE:latest + if [ "${{ github.event_name }}" == "release" ]; then + VERSION=${{ github.event.release.tag_name }} + docker tag $IMAGE:latest $IMAGE:$VERSION + docker push $IMAGE:$VERSION + fi diff --git a/docker/Dockerfile b/docker/Dockerfile index 1ef04da..45e5b38 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,11 +5,11 @@ FROM python:3.11-slim WORKDIR /app # Install dependencies -COPY ../requirements.txt . +COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Copy application code -COPY ../app ./app/ +COPY app ./app/ # Expose port 8000 EXPOSE 8000 From 5efc4f57ea0cfe42d880c87320b1bcb77c2fe55f Mon Sep 17 00:00:00 2001 From: Jona Aalten <159295595+jaalten@users.noreply.github.com> Date: Sat, 7 Mar 2026 18:41:48 +0100 Subject: [PATCH 4/4] add: 3.4 custom-values.yaml --- custom-values.yaml | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 custom-values.yaml diff --git a/custom-values.yaml b/custom-values.yaml new file mode 100644 index 0000000..705c361 --- /dev/null +++ b/custom-values.yaml @@ -0,0 +1,42 @@ +# Custom Helm values for Exercise 3.4: Kubernetes deployment with HPA +# +# Build the image inside Minikube's Docker daemon first: +# eval $(minikube docker-env) +# docker build -f docker/Dockerfile -t fastapi-gitops-local:latest . +# +# Then deploy: +# helm install my-release ./helm/fastapi-gitops-starter -f custom-values.yaml + +image: + repository: fastapi-gitops-local + pullPolicy: Never # use the locally built image, don't pull from registry + tag: "latest" + +# No registry token needed for a local image +registry: + createImagePullSecret: false + +imagePullSecrets: [] + +ingress: + enabled: true + className: "nginx" + hosts: + - host: minikube.test + paths: + - path: / + pathType: Prefix + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + +autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 10