diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 1ffe30d..f13785a 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,30 +1,30 @@
-version: 2
-
-updates:
- # Python dependencies
- - package-ecosystem: "pip"
- directory: "/"
- schedule:
- interval: "weekly"
- open-pull-requests-limit: 10
- ignore:
- - dependency-name: "setuptools"
- versions: ["*"]
- reviewers:
- - "GabrielPalmar"
-
- # Docker dependencies
- - package-ecosystem: "docker"
- directory: "/"
- schedule:
- interval: "weekly"
- reviewers:
- - "GabrielPalmar"
-
- # GitHub Actions
- - package-ecosystem: "github-actions"
- directory: "/"
- schedule:
- interval: "weekly"
- reviewers:
- - "GabrielPalmar"
+version: 2
+
+updates:
+ # Python dependencies
+ - package-ecosystem: "pip"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ ignore:
+ - dependency-name: "setuptools"
+ versions: ["*"]
+ reviewers:
+ - "GabrielPalmar"
+
+ # Docker dependencies
+ - package-ecosystem: "docker"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ reviewers:
+ - "GabrielPalmar"
+
+ # GitHub Actions
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ reviewers:
+ - "GabrielPalmar"
diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml
index 85f2ba2..9233274 100644
--- a/.github/workflows/build_push.yml
+++ b/.github/workflows/build_push.yml
@@ -1,73 +1,73 @@
-name: Build_Push
-
-on:
- push:
- branches:
- - main
-
-permissions:
- contents: read
- packages: write
-
-jobs:
- docker_build_test:
- runs-on: ubuntu-24.04
- name: Docker_build and Helm_build
- steps:
- - uses: actions/checkout@v5
-
- - name: Build the Docker image
- run: |
- docker build -t ghcr.io/gabrielpalmar/hivebox:$(cat version.txt) .
- docker tag ghcr.io/gabrielpalmar/hivebox:$(cat version.txt) ghcr.io/gabrielpalmar/hivebox:latest
-
- - name: Push Docker image to GHCR
- run: |
- echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- docker push ghcr.io/gabrielpalmar/hivebox:$(cat version.txt)
- docker push ghcr.io/gabrielpalmar/hivebox:latest
-
- # Get the SHA256 digest
- DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/gabrielpalmar/hivebox:$(cat version.txt) | cut -d'@' -f2)
- if [ -z "$DIGEST" ]; then
- echo "Failed to capture Docker digest"
- exit 1
- fi
- echo "Image digest: $DIGEST"
- echo "DOCKER_DIGEST=$DIGEST" >> $GITHUB_ENV
-
- - name: Install Helm
- run: |
- curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
- chmod 700 get_helm.sh
- ./get_helm.sh
- if ! command -v helm version &> /dev/null; then
- echo "Helm could not be installed"
- exit 1
- fi
-
- - name: Build Helm Chart
- run: |
- VERSION=$(cat version.txt)
- sed -i "s/^version:.*/version: $VERSION/" ./helm-chart/Chart.yaml
- sed -i "s/^appVersion:.*/appVersion: \"$VERSION\"/" ./helm-chart/Chart.yaml
- sed -i "s|hivebox: ghcr.io/gabrielpalmar/hivebox:.*|hivebox: ghcr.io/gabrielpalmar/hivebox:$VERSION@$DOCKER_DIGEST|" ./helm-chart/values.yaml
- helm package ./helm-chart
-
- - name: Push Helm Chart to GHCR
- run: |
- CHART_FILE="hivebox-$(cat version.txt).tgz"
- if [ ! -f "$CHART_FILE" ]; then
- echo "Helm chart $CHART_FILE not found"
- exit 1
- fi
- echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
- helm push $CHART_FILE oci://ghcr.io/gabrielpalmar/hivebox-helm-charts
-
- - name: Add job summary
- run: |
- VERSION=$(cat version.txt)
- echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
- echo "- Docker image: ghcr.io/gabrielpalmar/hivebox:$VERSION" >> $GITHUB_STEP_SUMMARY
- echo "- Image digest: $DOCKER_DIGEST" >> $GITHUB_STEP_SUMMARY
+name: Build_Push
+
+on:
+ push:
+ branches:
+ - main
+
+permissions:
+ contents: read
+ packages: write
+
+jobs:
+ docker_build_test:
+ runs-on: ubuntu-24.04
+ name: Docker_build and Helm_build
+ steps:
+ - uses: actions/checkout@v5
+
+ - name: Build the Docker image
+ run: |
+ docker build -t ghcr.io/gabrielpalmar/hivebox:$(cat version.txt) .
+ docker tag ghcr.io/gabrielpalmar/hivebox:$(cat version.txt) ghcr.io/gabrielpalmar/hivebox:latest
+
+ - name: Push Docker image to GHCR
+ run: |
+ echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
+ docker push ghcr.io/gabrielpalmar/hivebox:$(cat version.txt)
+ docker push ghcr.io/gabrielpalmar/hivebox:latest
+
+ # Get the SHA256 digest
+ DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/gabrielpalmar/hivebox:$(cat version.txt) | cut -d'@' -f2)
+ if [ -z "$DIGEST" ]; then
+ echo "Failed to capture Docker digest"
+ exit 1
+ fi
+ echo "Image digest: $DIGEST"
+ echo "DOCKER_DIGEST=$DIGEST" >> $GITHUB_ENV
+
+ - name: Install Helm
+ run: |
+ curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
+ chmod 700 get_helm.sh
+ ./get_helm.sh
+ if ! command -v helm version &> /dev/null; then
+ echo "Helm could not be installed"
+ exit 1
+ fi
+
+ - name: Build Helm Chart
+ run: |
+ VERSION=$(cat version.txt)
+ sed -i "s/^version:.*/version: $VERSION/" ./helm-chart/Chart.yaml
+ sed -i "s/^appVersion:.*/appVersion: \"$VERSION\"/" ./helm-chart/Chart.yaml
+ sed -i "s|hivebox: ghcr.io/gabrielpalmar/hivebox:.*|hivebox: ghcr.io/gabrielpalmar/hivebox:$VERSION@$DOCKER_DIGEST|" ./helm-chart/values.yaml
+ helm package ./helm-chart
+
+ - name: Push Helm Chart to GHCR
+ run: |
+ CHART_FILE="hivebox-$(cat version.txt).tgz"
+ if [ ! -f "$CHART_FILE" ]; then
+ echo "Helm chart $CHART_FILE not found"
+ exit 1
+ fi
+ echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
+ helm push $CHART_FILE oci://ghcr.io/gabrielpalmar/hivebox-helm-charts
+
+ - name: Add job summary
+ run: |
+ VERSION=$(cat version.txt)
+ echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
+ echo "- Docker image: ghcr.io/gabrielpalmar/hivebox:$VERSION" >> $GITHUB_STEP_SUMMARY
+ echo "- Image digest: $DOCKER_DIGEST" >> $GITHUB_STEP_SUMMARY
echo "- Helm chart: hivebox-$VERSION" >> $GITHUB_STEP_SUMMARY
\ No newline at end of file
diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml
index e6408a9..2db0c65 100644
--- a/.github/workflows/build_test.yml
+++ b/.github/workflows/build_test.yml
@@ -1,47 +1,47 @@
-name: Build_Test
-
-on:
- push:
- branches:
- - main
- pull_request:
- branches:
- - main
-
-permissions:
- contents: read
-
-jobs:
- docker_build_test:
- runs-on: ubuntu-24.04
- name: Docker_build
- steps:
- - uses: actions/checkout@v5
- - name: Build the Docker image
- run: docker build -t gabrielpalmar/hivebox:$(cat version.txt) .
-
- - name: Run Docker container
- run: docker run -d -p 5000:5000 gabrielpalmar/hivebox:$(cat version.txt)
-
- - name: Set up Python for testing
- uses: actions/setup-python@v6
- with:
- python-version: '3.12'
-
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install requests
- pip install vcrpy
-
- - name: Run tests
- run: |
- python tests/test_main.py
- TEST_EXIT_CODE=$?
- if [ $TEST_EXIT_CODE -ne 0 ]; then
- echo "Tests failed!"
- exit $TEST_EXIT_CODE
- fi
-
- - name: Stop Docker container
+name: Build_Test
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+permissions:
+ contents: read
+
+jobs:
+ docker_build_test:
+ runs-on: ubuntu-24.04
+ name: Docker_build
+ steps:
+ - uses: actions/checkout@v5
+ - name: Build the Docker image
+ run: docker build -t gabrielpalmar/hivebox:$(cat version.txt) .
+
+ - name: Run Docker container
+ run: docker run -d -p 5000:5000 gabrielpalmar/hivebox:$(cat version.txt)
+
+ - name: Set up Python for testing
+ uses: actions/setup-python@v6
+ with:
+ python-version: '3.12'
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install requests
+ pip install vcrpy
+
+ - name: Run tests
+ run: |
+ python tests/test_main.py
+ TEST_EXIT_CODE=$?
+ if [ $TEST_EXIT_CODE -ne 0 ]; then
+ echo "Tests failed!"
+ exit $TEST_EXIT_CODE
+ fi
+
+ - name: Stop Docker container
run: docker stop $(docker ps -q)
\ No newline at end of file
diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml
index 9a6de39..cfc28d1 100644
--- a/.github/workflows/hadolint.yml
+++ b/.github/workflows/hadolint.yml
@@ -1,21 +1,21 @@
-name: Hadolint
-
-on:
- push:
- branches:
- - main
- pull_request:
- branches:
- - main
-
-permissions:
- contents: read
-
-jobs:
- hadolint-linting:
- runs-on: ubuntu-24.04
- steps:
- - uses: actions/checkout@v5
- - uses: hadolint/hadolint-action@v3.3.0
- with:
+name: Hadolint
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+permissions:
+ contents: read
+
+jobs:
+ hadolint-linting:
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@v5
+ - uses: hadolint/hadolint-action@v3.3.0
+ with:
dockerfile: Dockerfile
\ No newline at end of file
diff --git a/.github/workflows/kube-linter.yml b/.github/workflows/kube-linter.yml
index 0fd3bcf..61ed874 100644
--- a/.github/workflows/kube-linter.yml
+++ b/.github/workflows/kube-linter.yml
@@ -1,24 +1,24 @@
-name: Kube-Linter
-
-on:
- push:
- branches:
- - main
- pull_request:
- branches:
- - main
-
-permissions:
- contents: read
-
-jobs:
- linting:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Scan repo with kube-linter
- uses: stackrox/kube-linter-action@v1.0.4
- with:
- directory: helm-chart
+name: Kube-Linter
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+permissions:
+ contents: read
+
+jobs:
+ linting:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Scan repo with kube-linter
+ uses: stackrox/kube-linter-action@v1.0.4
+ with:
+ directory: helm-chart
diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
index ca25b64..7c6fe5e 100644
--- a/.github/workflows/pylint.yml
+++ b/.github/workflows/pylint.yml
@@ -1,42 +1,42 @@
-name: Pylint
-
-on:
- push:
- branches:
- - main
- pull_request:
- branches:
- - main
-
-permissions:
- contents: read
-
-jobs:
- build:
- runs-on: ubuntu-latest
- strategy:
- matrix:
- python-version: ["3.12"]
- steps:
- - uses: actions/checkout@v5
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v6
- with:
- python-version: ${{ matrix.python-version }}
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install pylint
- pip install flask
- pip install requests
- pip install vcrpy
- pip install prometheus_client
- pip install redis
- pip install minio
- - name: Analysing the code with pylint
- run: |
- # Set PYTHONPATH so pylint can find the app module
- export PYTHONPATH="${PYTHONPATH}:$(pwd)"
- pylint app/ --ignore=__pycache__
- env:
+name: Pylint
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.12"]
+ steps:
+ - uses: actions/checkout@v5
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v6
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install pylint
+ pip install flask
+ pip install requests
+ pip install vcrpy
+ pip install prometheus_client
+ pip install redis
+ pip install minio
+ - name: Analysing the code with pylint
+ run: |
+ # Set PYTHONPATH so pylint can find the app module
+ export PYTHONPATH="${PYTHONPATH}:$(pwd)"
+ pylint app/ --ignore=__pycache__
+ env:
PYTHONPATH: ${{ github.workspace }}
\ No newline at end of file
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index cc040cb..76244e8 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -1,50 +1,50 @@
-name: SonarQube analysis
-
-on:
- workflow_dispatch: {}
-
-permissions:
- pull-requests: read
- contents: read
-
-jobs:
- Analysis:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v5
- with:
- fetch-depth: 0
-
- - name: Set up Python
- uses: actions/setup-python@v6
- with:
- python-version: '3.12'
-
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt
- pip install coverage pytest vcrpy
-
- - name: Run tests with coverage
- run: |
- export PYTHONPATH="${GITHUB_WORKSPACE}:${PYTHONPATH}"
- coverage run --source=app -m pytest tests/test_modules.py -v
- coverage xml -o coverage.xml
- coverage report --show-missing
- working-directory: ${{ github.workspace }}
-
- - name: Analyze with SonarQube
- uses: SonarSource/sonarqube-scan-action@master
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- with:
- args: >
- -Dsonar.projectKey=GabrielPalmar_HiveBox-Project
- -Dsonar.organization=gabrielpalmar
- -Dsonar.sources=app
- -Dsonar.tests=tests
- -Dsonar.python.coverage.reportPaths=coverage.xml
+name: SonarQube analysis
+
+on:
+ workflow_dispatch: {}
+
+permissions:
+ pull-requests: read
+ contents: read
+
+jobs:
+ Analysis:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: '3.12'
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+ pip install coverage pytest vcrpy
+
+ - name: Run tests with coverage
+ run: |
+ export PYTHONPATH="${GITHUB_WORKSPACE}:${PYTHONPATH}"
+ coverage run --source=app -m pytest tests/test_modules.py -v
+ coverage xml -o coverage.xml
+ coverage report --show-missing
+ working-directory: ${{ github.workspace }}
+
+ - name: Analyze with SonarQube
+ uses: SonarSource/sonarqube-scan-action@master
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ with:
+ args: >
+ -Dsonar.projectKey=GabrielPalmar_HiveBox-Project
+ -Dsonar.organization=gabrielpalmar
+ -Dsonar.sources=app
+ -Dsonar.tests=tests
+ -Dsonar.python.coverage.reportPaths=coverage.xml
-Dsonar.exclusions=**/fixtures/**,**/__pycache__/**,**/venv/**,**/.git/**,docker/**,k8s/**,scripts/**
\ No newline at end of file
diff --git a/.github/workflows/terrascan.yml b/.github/workflows/terrascan.yml
index a3acc0f..5b8f458 100644
--- a/.github/workflows/terrascan.yml
+++ b/.github/workflows/terrascan.yml
@@ -1,31 +1,31 @@
-name: Terrascan IaC scanner
-
-on:
- push:
- branches:
- - main
- pull_request:
- branches:
- - main
-
-permissions:
- pull-requests: read
- contents: read
-
-jobs:
- Analysis:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v5
-
- - name: Set up Terrascan
- uses: tenable/terrascan-action@v1.5.0
- with:
- iac_type: k8s
- iac_version: v1
- iac_dir: k8s
- policy_type: k8s
- verbose: true
- skip_rules: AC_K8S_0080
+name: Terrascan IaC scanner
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+permissions:
+ pull-requests: read
+ contents: read
+
+jobs:
+ Analysis:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+
+ - name: Set up Terrascan
+ uses: tenable/terrascan-action@v1.5.0
+ with:
+ iac_type: k8s
+ iac_version: v1
+ iac_dir: k8s
+ policy_type: k8s
+ verbose: true
+ skip_rules: AC_K8S_0080
only_warn: true
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 48cc42c..24fed09 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
-/env
-/.venv
-/__pycache__
-/out.txt
-/tests/__pycache__
-/.pytest_cache
+/env
+/.venv
+/__pycache__
+/out.txt
+/tests/__pycache__
+/.pytest_cache
/local
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 79c27cd..f2e35ed 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,27 +1,27 @@
-FROM python:3.13.7-alpine@sha256:9ba6d8cbebf0fb6546ae71f2a1c14f6ffd2fdab83af7fa5669734ef30ad48844
-
-RUN addgroup -S appgroup && adduser -S -G appgroup appuser
-
-WORKDIR /app
-
-COPY /app/ /app/app/
-COPY version.txt requirements.txt /app/
-
-RUN pip install --no-cache-dir -r /app/requirements.txt --require-hashes && \
- chown -R appuser:appgroup /app
-
-ENV FLASK_APP=app.main.py:app \
- PYTHONUNBUFFERED=1 \
- REDIS_PORT=6379 \
- REDIS_DB=0 \
- CACHE_TTL=300 \
- MINIO_PORT=9000 \
- MINIO_ACCESS_KEY=minioadmin \
- MINIO_SECRET_KEY=minioadmin \
- REDIS_HOST=redis \
- MINIO_HOST=minio
-
-USER appuser
-
-ENTRYPOINT [ "flask" ]
+FROM python:3.13.7-alpine@sha256:9ba6d8cbebf0fb6546ae71f2a1c14f6ffd2fdab83af7fa5669734ef30ad48844
+
+RUN addgroup -S appgroup && adduser -S -G appgroup appuser
+
+WORKDIR /app
+
+COPY /app/ /app/app/
+COPY version.txt requirements.txt /app/
+
+RUN pip install --no-cache-dir -r /app/requirements.txt --require-hashes && \
+ chown -R appuser:appgroup /app
+
+ENV FLASK_APP=app.main.py:app \
+ PYTHONUNBUFFERED=1 \
+ REDIS_PORT=6379 \
+ REDIS_DB=0 \
+ CACHE_TTL=300 \
+ MINIO_PORT=9000 \
+ MINIO_ACCESS_KEY=minioadmin \
+ MINIO_SECRET_KEY=minioadmin \
+ REDIS_HOST=redis \
+ MINIO_HOST=minio
+
+USER appuser
+
+ENTRYPOINT [ "flask" ]
CMD [ "run", "--host=0.0.0.0" ]
\ No newline at end of file
diff --git a/README.md b/README.md
index c556a8a..c1ba59f 100644
--- a/README.md
+++ b/README.md
@@ -1,35 +1,367 @@
-# HiveBox Project
-
-## Introduction
-
-DevOps project that includes most used technologies in the industry. Focusing in the CI and CD tools and integrating them simultaneously to achieve topics as the following:
-
-- Software Production.
-- Agile Planning.
-- QA and Quality Gates.
-- Code and Programming.
-- Operating System.
-- Docker Containers.
-- Kubernetes and Cloud.
-- Observability and Monitoring.
-- Continuous Integration/Delivery/Deployment.
-- Automation and Infrastructure as Code.
-
-For more information please refer to the Project webpage: [HiveBox](https://devopsroadmap.io/projects/hivebox/)
-
-## Technologies Used
-- Python (Flask, Prometheus, Redis, Requests)
-- Docker
-- Kubernetes (Minikube, Kind)
-- Dependabot
-- GitHub Actions:
- - SonarQube
- - Terrascan
- - Pylint
- - Hadolint
-
-## Development
-
-To adhere to industry standards, the Repository's Project section was used, leveraging the Kanban board to process tasks in an organized manner.
-
+# HiveBox Project
+
+
+
+
+
+## Overview
+
+HiveBox is a production-grade Python Flask application that aggregates real-time temperature data from global sensors using the [OpenSenseMap](https://opensensemap.org/) API. The project demonstrates end-to-end DevOps practices with containerization, orchestration, and cloud deployment.
+
+For more information please refer to the Project webpage: [HiveBox](https://devopsroadmap.io/projects/hivebox/)
+
+### Application Features
+
+- **Temperature Data API**: Fetches and processes data from thousands of global temperature sensors.
+- **Intelligent Caching**: Redis-based caching with 5-minute TTL to optimize API performance.
+- **Object Storage**: MinIO (S3-compatible) for persistent temperature data storage with automated CronJob uploads every 5 minutes.
+- **Observability**: Prometheus metrics exposure for monitoring and alerting.
+- **Health Probes**: Kubernetes-ready readiness and liveness endpoints with sensor availability checks.
+
+### Technology Stack
+
+- **[Application](./app/)**: Python with Flask framework.
+ - `main.py`: API endpoints (/version, /temperature, /metrics, /store, /readyz).
+ - `opensense.py`: OpenSenseMap API integration with streaming support.
+ - `storage.py`: MinIO client for object storage operations.
+ - `config.py`: Redis client configuration.
+ - `readiness.py`: Sophisticated health check logic.
+
+- **[Containerization](./Dockerfile)**: Security-hardened Alpine Linux images.
+ - Multi-stage Docker builds with Python 3.13.7-alpine base.
+ - Non-root user execution (UID 1000).
+ - Pinned dependencies with hash verification in [requirements.txt](./requirements.txt).
+ - Read-only filesystem with dropped capabilities.
+
+- **Orchestration**: Kubernetes 1.31 with local development support.
+ - **[Helm Charts](./helm-chart/)**: Package management with OCI registry support.
+ - **[Kustomize](./kustomize/)**: Environment-specific configurations (staging/prod).
+ - **Local Testing**: Minikube and Kind compatibility.
+
+- **[Infrastructure as Code](./terraform/)**: AWS EKS deployment with Terraform.
+ - VPC with public/private subnets across multiple availability zones.
+ - EKS cluster v1.31 with managed node groups.
+ - Modular design: VPC, EKS, IAM, Security Groups, Node Groups.
+ - Optional ElastiCache and S3 modules.
+
+- **CI/CD Pipeline**: GitHub Actions with comprehensive quality gates.
+ - **Build & Test**: Automated Docker builds with integration testing.
+ - **Code Quality**: SonarQube analysis with coverage reporting.
+ - **Linting**: Pylint (Python), Hadolint (Docker), Kube-Linter (Kubernetes).
+ - **Security Scanning**: Terrascan for IaC security analysis.
+ - **Artifact Publishing**: Automatic image and Helm chart publishing to GHCR.
+ - **Dependency Management**: Dependabot for automated security updates.
+
+### Architecture Components
+
+The application runs as a distributed system with three main services:
+
+1. **HiveBox App** (2 replicas): Flask application with anti-affinity rules for high availability.
+2. **Redis/Valkey** (1 replica): In-memory cache for API response optimization.
+3. **MinIO** (1 replica): Object storage for historical temperature data.
+4. **CronJob**: Periodic data storage trigger (every 5 minutes).
+
+## Architecture Diagram
+
+```mermaid
+graph TB
+ subgraph K8S["Kubernetes Cluster - EKS"]
+ INGRESS[Ingress nginx
hivebox.local]
+
+ SERVICE[Service: hivebox-service
ClusterIP:80 → Pod:5000]
+
+ subgraph PODS["HiveBox Application - Anti-Affinity"]
+ POD1[HiveBox Pod 1
CPU: 250m
Mem: 256Mi]
+ POD2[HiveBox Pod 2
CPU: 250m
Mem: 256Mi]
+ end
+
+ REDIS[Redis/Valkey
Cache TTL: 5min
CPU: 100m
Mem: 128Mi]
+
+ MINIO[MinIO S3 Storage
Bucket: hivebox
CPU: 100m
Mem: 128Mi]
+
+ CRON[CronJob
Schedule: */5 * * * *
curl /store
CPU: 10m
Mem: 16Mi]
+
+ INGRESS --> SERVICE
+ SERVICE --> POD1
+ SERVICE --> POD2
+
+ POD1 -.Anti-Affinity.-> POD2
+
+ POD1 --> REDIS
+ POD2 --> REDIS
+
+ POD1 --> MINIO
+ POD2 --> MINIO
+
+ CRON --> POD1
+ end
+```
+
+## API Endpoints
+
+| Endpoint | Method | Description | Response |
+|----------|--------|-------------|----------|
+| `/version` | GET | Returns current application version | `Current app version: 0.7.1` |
+| `/temperature` | GET | Fetches average global temperature from cached/live data | `Average temperature: XX.XX°C` + Pod IP |
+| `/metrics` | GET | Prometheus metrics in text exposition format | Prometheus metrics data |
+| `/store` | GET | Uploads current temperature data to MinIO S3 bucket | Storage confirmation message |
+| `/readyz` | GET | Kubernetes readiness probe - checks sensor availability & cache status | `{"status": "ready"}` (200) or `{"status": "not ready"}` (503) |
+
+### Readiness Probe Logic
+
+The `/readyz` endpoint implements sophisticated health checking:
+- **Sensor Check**: Validates >50% of sensors are reachable from OpenSenseMap API.
+- **Cache Check**: Verifies Redis cache freshness (5-minute TTL).
+- **Combined Logic**: Returns unhealthy (503) only when BOTH checks fail.
+- **Use Case**: Kubernetes uses this for traffic routing decisions.
+
+## Quick Start
+
+### Prerequisites
+
+- **Docker**: v20.10+.
+- **Kubernetes**: v1.31+ (Minikube or Kind for local development).
+- **Helm**: v3.0+.
+- **kubectl**: Configured and connected to cluster.
+- **(Optional) Terraform**: v1.0+ for AWS EKS deployment.
+
+### Local Development with Docker
+
+```bash
+# Clone the repository
+git clone https://github.com/GabrielPalmar/HiveBox-Project.git
+cd HiveBox-Project
+
+# Build the Docker image
+docker build -t hivebox:0.7.1 .
+
+# Run the application
+docker run -d -p 5000:5000 hivebox:0.7.1
+
+# Test the endpoints
+curl http://localhost:5000/version
+curl http://localhost:5000/temperature
+curl http://localhost:5000/metrics
+```
+
+### Kubernetes Deployment with Helm
+
+#### Using Helm from GHCR (Recommended)
+
+```bash
+# Install from GitHub Container Registry
+helm install hivebox oci://ghcr.io/gabrielpalmar/hivebox-helm-charts/hivebox --version 0.7.1
+
+# Access the application (if using Minikube)
+minikube service hivebox-service
+
+# Or via Ingress (add to /etc/hosts: hivebox.local)
+curl http://hivebox.local/temperature
+```
+
+#### Using Helm from Local Chart
+
+```bash
+# Install from local chart
+helm install hivebox ./helm-chart
+
+# Verify deployment
+kubectl get pods -l app=hivebox
+kubectl get svc hivebox-service
+
+# Port forward for local access
+kubectl port-forward svc/hivebox-service 8080:80
+
+# Test the application
+curl http://localhost:8080/temperature
+```
+
+#### Helm Configuration
+
+Key values in [helm-chart/values.yaml](./helm-chart/values.yaml):
+
+| Parameter | Default | Description |
+|-----------|---------|-------------|
+| `replicas.hivebox` | 2 | Number of HiveBox pod replicas |
+| `images.hivebox` | ghcr.io/gabrielpalmar/hivebox:latest | HiveBox container image with SHA digest |
+| `resources.hivebox.requests.cpu` | 250m | CPU request per pod |
+| `resources.hivebox.requests.memory` | 256Mi | Memory request per pod |
+| `ingress.enabled` | true | Enable/disable Ingress |
+| `ingress.host` | hivebox.local | Ingress hostname |
+
+### Kubernetes Deployment with Kustomize
+
+```bash
+# Deploy to staging environment
+kubectl apply -k kustomize/overlays/staging
+
+# Deploy to production environment
+kubectl apply -k kustomize/overlays/prod
+
+# Verify deployment
+kubectl get deployments,pods,services,ingress -n
+```
+
+### AWS EKS Deployment with Terraform
+
+For detailed infrastructure deployment instructions, see [terraform/README.md](./terraform/README.md).
+
+```bash
+cd terraform
+
+# Initialize Terraform
+terraform init
+
+# Review the execution plan
+terraform plan
+
+# Deploy infrastructure (EKS cluster, VPC, IAM, etc.)
+terraform apply
+
+# Configure kubectl
+aws eks update-kubeconfig --region --name
+
+# Deploy application using Helm
+helm install hivebox oci://ghcr.io/gabrielpalmar/hivebox-helm-charts/hivebox --version 0.7.1
+```
+
+## CI/CD Pipeline
+
+The project uses GitHub Actions for comprehensive CI/CD automation with multiple quality gates:
+
+### Workflows
+
+| Workflow | Trigger | Purpose |
+|----------|---------|---------|
+| [Build & Test](.github/workflows/build_test.yml) | Push/PR to `main` | Builds Docker image, runs container, executes integration tests |
+| [Build & Push](.github/workflows/build_push.yml) | Push to `main` | Builds and pushes Docker image + Helm chart to GHCR with SHA256 digest |
+| [Pylint](.github/workflows/pylint.yml) | Push/PR to `main` | Python code quality and standards enforcement |
+| [SonarQube](.github/workflows/sonarqube.yml) | Manual | Code quality analysis: coverage, vulnerabilities, code smells |
+| [Hadolint](.github/workflows/hadolint.yml) | Push/PR to `main` | Dockerfile best practices and security validation |
+| [Terrascan](.github/workflows/terrascan.yml) | Push/PR to `main` | Terraform IaC security scanning and compliance |
+| [Kube-Linter](.github/workflows/kube-linter.yml) | Push/PR to `main` | Kubernetes manifest validation for security and reliability |
+
+### Dependency Management
+
+**Dependabot** ([.github/dependabot.yml](.github/dependabot.yml)) automatically updates:
+- **Python dependencies**: Weekly (pip)
+- **Docker base images**: Weekly
+- **GitHub Actions**: Weekly
+
+### Quality Gates Summary
+
+```
+┌────────────────┐
+│ Code Push │
+└───────┬────────┘
+ │
+ ├─────► Pylint ────────┐
+ ├─────► Hadolint ──────┤
+ ├─────► Kube-Linter ───┤
+ ├─────► Terrascan ─────┤── All Pass? ──┐
+ ├─────► Build & Test ──┤ │
+ └─────► SonarQube ─────┘ │
+ ▼
+ ┌───────────────┐
+ │ Build & Push │
+ │ to GHCR │
+ └───────────────┘
+ │
+ ▼
+ ┌───────────────┐
+ │ Helm Chart │
+ │ Packaged │
+ └───────────────┘
+```
+
+## Configuration
+
+### Environment Variables
+
+Set in [Dockerfile](./Dockerfile) and configurable in Kubernetes:
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `FLASK_APP` | app.main:app | Flask application entry point |
+| `PYTHONUNBUFFERED` | 1 | Disable Python output buffering |
+| `REDIS_HOST` | redis | Redis service hostname |
+| `REDIS_PORT` | 6379 | Redis service port |
+| `REDIS_DB` | 0 | Redis database number |
+| `CACHE_TTL` | 300 | Cache time-to-live (5 minutes) |
+| `MINIO_HOST` | minio | MinIO service hostname |
+| `MINIO_PORT` | 9000 | MinIO service port |
+| `MINIO_ACCESS_KEY` | minioadmin | MinIO access credentials |
+| `MINIO_SECRET_KEY` | minioadmin | MinIO secret credentials |
+
+### Security Configuration
+
+All containers run with hardened security contexts:
+
+```yaml
+securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: true
+ runAsNonRoot: true
+ runAsUser: 1000
+ runAsGroup: 1000
+ capabilities:
+ drop: ["ALL"]
+ seccompProfile:
+ type: RuntimeDefault
+```
+
+## Testing
+
+### Run Integration Tests
+
+```bash
+# Install test dependencies
+pip install -r requirements.txt
+pip install vcrpy
+
+# Run integration tests with VCR cassettes
+python tests/test_main.py
+
+# Run unit tests
+python tests/test_modules.py
+```
+
+### Test Coverage
+
+- Integration tests: API endpoint validation with mocked responses.
+- Unit tests: Module-level functionality testing.
+- VCR cassettes: Record/replay HTTP interactions for reproducible tests.
+- CI pipeline: Automated testing on every PR/push.
+
+## Monitoring & Observability
+
+### Prometheus Metrics
+
+Access metrics at `/metrics` endpoint:
+
+```bash
+curl http://hivebox.local/metrics
+```
+
+**Available metrics**:
+- HTTP request counters.
+- Response time histograms.
+- Temperature data metrics.
+- Cache hit/miss ratios.
+
+### Health Checks
+
+**Liveness Probe**: `/version`
+- Simple endpoint to verify pod is alive.
+- Checks: 60s interval, 3 failures trigger restart.
+
+**Readiness Probe**: `/readyz`
+- Complex health check with sensor + cache validation.
+- Checks: 30s delay, 600s interval.
+- Removes pod from service if unhealthy.
+
+## Development
+
+To adhere to industry standards, the Repository's Project section was used, leveraging the Kanban board to process tasks in an organized manner.
+
[Kanban Board](https://github.com/users/GabrielPalmar/projects/1)
\ No newline at end of file
diff --git a/app/config.py b/app/config.py
index 5f97eda..b70d662 100644
--- a/app/config.py
+++ b/app/config.py
@@ -1,27 +1,27 @@
-'''Shared configuration module'''
-import os
-import redis
-
-# Redis configuration
-REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
-REDIS_PORT = int(os.environ.get('REDIS_PORT', 6379))
-REDIS_DB = int(os.environ.get('REDIS_DB', 0))
-CACHE_TTL = int(os.environ.get('CACHE_TTL', 300))
-
-def create_redis_client():
- '''Create and return Redis client with error handling'''
- try:
- redis_client = redis.StrictRedis(
- host=REDIS_HOST,
- port=REDIS_PORT,
- db=REDIS_DB,
- decode_responses=True,
- socket_connect_timeout=240,
- socket_timeout=240
- )
- redis_client.ping()
- print("Connected to Redis successfully!")
- return redis_client, True
- except (redis.ConnectionError, redis.TimeoutError) as e:
- print(f"Could not connect to Redis: {e}")
- return None, False
+'''Shared configuration module'''
+import os
+import redis
+
+# Redis configuration
+REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
+REDIS_PORT = int(os.environ.get('REDIS_PORT', 6379))
+REDIS_DB = int(os.environ.get('REDIS_DB', 0))
+CACHE_TTL = int(os.environ.get('CACHE_TTL', 300))
+
+def create_redis_client():
+ '''Create and return Redis client with error handling'''
+ try:
+ redis_client = redis.StrictRedis(
+ host=REDIS_HOST,
+ port=REDIS_PORT,
+ db=REDIS_DB,
+ decode_responses=True,
+ socket_connect_timeout=240,
+ socket_timeout=240
+ )
+ redis_client.ping()
+ print("Connected to Redis successfully!")
+ return redis_client, True
+ except (redis.ConnectionError, redis.TimeoutError) as e:
+ print(f"Could not connect to Redis: {e}")
+ return None, False
diff --git a/app/main.py b/app/main.py
index 7d514ff..28ad0c3 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,55 +1,55 @@
-'''Module containing the main function of the app.'''
-import os
-import socket
-from flask import Flask, Response
-from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
-from app import opensense
-from app import storage
-from app import readiness
-
-app = Flask(__name__)
-
-HOSTNAME = socket.gethostname()
-IPADDR = socket.gethostbyname(HOSTNAME)
-
-@app.route('/version')
-def print_version():
- '''Function printing the current version of the app.'''
- version_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'version.txt')
-
- with open(version_file, 'r', encoding="utf-8") as f:
- version = f.read()
-
- return f"Current app version: {version}\n"
-
-@app.route('/temperature')
-def get_temperature():
- '''Function to get the current temperature.'''
- result, _ = opensense.get_temperature()
- return result + f"From: {IPADDR}\n"
-
-@app.route('/metrics')
-def metrics():
- '''Function to return Prometheus metrics.'''
- return Response(generate_latest(), mimetype=CONTENT_TYPE_LATEST)
-
-@app.route('/store')
-def store():
- '''Function to store results in MinIO.'''
- return storage.store_temperature_data()
-
-@app.route('/readyz')
-def readyz():
- '''Readiness probe endpoint'''
- status_code = readiness.readiness_check()
-
- if status_code == 200:
- return {"status": "ready"}, 200
-
- return {
- "status": "not ready",
- "error": "More than 50% of sensors unreachable and cache expired"
- }, 503
-
-if __name__ == "__main__":
- app.run()
+'''Module containing the main function of the app.'''
+import os
+import socket
+from flask import Flask, Response
+from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
+from app import opensense
+from app import storage
+from app import readiness
+
+app = Flask(__name__)
+
+HOSTNAME = socket.gethostname()
+IPADDR = socket.gethostbyname(HOSTNAME)
+
+@app.route('/version')
+def print_version():
+ '''Function printing the current version of the app.'''
+ version_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'version.txt')
+
+ with open(version_file, 'r', encoding="utf-8") as f:
+ version = f.read()
+
+ return f"Current app version: {version}\n"
+
+@app.route('/temperature')
+def get_temperature():
+ '''Function to get the current temperature.'''
+ result, _ = opensense.get_temperature()
+ return result + f"From: {IPADDR}\n"
+
+@app.route('/metrics')
+def metrics():
+ '''Function to return Prometheus metrics.'''
+ return Response(generate_latest(), mimetype=CONTENT_TYPE_LATEST)
+
+@app.route('/store')
+def store():
+ '''Function to store results in MinIO.'''
+ return storage.store_temperature_data()
+
+@app.route('/readyz')
+def readyz():
+ '''Readiness probe endpoint'''
+ status_code = readiness.readiness_check()
+
+ if status_code == 200:
+ return {"status": "ready"}, 200
+
+ return {
+ "status": "not ready",
+ "error": "More than 50% of sensors unreachable and cache expired"
+ }, 503
+
+if __name__ == "__main__":
+ app.run()
diff --git a/app/opensense.py b/app/opensense.py
index fbf322c..930240f 100644
--- a/app/opensense.py
+++ b/app/opensense.py
@@ -1,172 +1,172 @@
-'''Module to get entries from OpenSenseMap API and get the average temperature'''
-# pylint: disable=too-many-locals,too-many-branches,too-many-statements
-from datetime import datetime, timezone, timedelta
-import json
-import requests
-import redis
-from app.config import create_redis_client, CACHE_TTL
-
-# Use shared Redis client
-redis_client, REDIS_AVAILABLE = create_redis_client()
-
-_sensor_stats = {"total_sensors": 0, "null_count": 0}
-
-def classify_temperature(average):
- '''Classify temperature based on ranges using dictionary approach'''
- # Define temperature ranges and their classifications
- temp_classifications = {
- "cold": (float('-inf'), 10, "Warning: Too cold"),
- "good": (10, 36, "Good"),
- "hot": (36, float('inf'), "Warning: Too hot")
- }
-
- # Find the appropriate classification
- for _, (min_temp, max_temp, status) in temp_classifications.items():
- if min_temp < average <= max_temp:
- return status
-
- return "Unknown" # Default case
-
-def _parse_partial_json_array(text: str):
- """Parse as many full objects as possible from a (possibly truncated) JSON array."""
- decoder = json.JSONDecoder()
- items = []
- i = text.find('[')
- if i == -1:
- return items
- i += 1 # past '['
- n = len(text)
- while i < n:
- while i < n and text[i].isspace():
- i += 1
- if i >= n or text[i] == ']':
- break
- try:
- obj, end = decoder.raw_decode(text, i)
- except json.JSONDecodeError:
- # truncated object at the end; stop with what we have
- break
- items.append(obj)
- i = end
- while i < n and text[i].isspace():
- i += 1
- if i < n and text[i] == ',':
- i += 1
- return items
-
-def get_temperature():
- '''Function to get the average temperature from OpenSenseMap API.'''
- if REDIS_AVAILABLE:
- try:
- cached_data = redis_client.get("temperature_data")
- if cached_data:
- print("Using cached data from Redis.")
- cached_result = cached_data
- default_stats = {"total_sensors": 0, "null_count": 0}
- return cached_result, default_stats
- except redis.RedisError as e:
- print(f"Redis error: {e}. Proceeding without cache.")
-
- print("Fetching new data from OpenSenseMap API...")
-
- # Ensuring that data is not older than 1 hour.
- time_iso = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat().replace("+00:00", "Z")
-
- params = {
- "date": time_iso,
- "format": "json"
- }
-
- # Streaming configuration
- max_mb = 0.5
- max_bytes = int(max_mb * 1024 * 1024)
-
- print('Getting data from OpenSenseMap API...')
-
- try:
- # Stream the response and count bytes
- response = requests.get(
- "https://api.opensensemap.org/boxes",
- params=params,
- stream=True,
- timeout=(180, 60)
- )
- response.raise_for_status()
-
- downloaded = 0
- chunks = []
- truncated = False
-
- for chunk in response.iter_content(chunk_size=64 * 1024): # 64 KB
- if not chunk:
- break
- chunks.append(chunk)
- downloaded += len(chunk)
- if downloaded >= max_bytes:
- print(f"Reached {max_mb} MB limit ({downloaded:,} bytes), stopping download")
- truncated = True
- response.close()
- break
-
- print(f'Bytes downloaded: {downloaded:,}')
- print('Data retrieved successfully!' + (" (partial)" if truncated else ""))
-
- # Build body and parse JSON
- body = b"".join(chunks)
- text = body.decode(response.encoding or "utf-8", errors="replace")
-
- try:
- data = json.loads(text)
- except json.JSONDecodeError:
- if not truncated:
- print("Warning: Unexpected JSON parse error. Trying partial parse.")
- data = _parse_partial_json_array(text)
- if not data:
- return "Error: Failed to parse JSON and no partial objects found\n", {
- "total_sensors": 0,
- "null_count": 0
- }
-
- except requests.Timeout:
- print("API request timed out")
- return "Error: API request timed out\n", {"total_sensors": 0, "null_count": 0}
- except requests.RequestException as e:
- print(f"API request failed: {e}")
- return f"Error: API request failed - {e}\n", {"total_sensors": 0, "null_count": 0}
-
- # Process the data (keeping the existing logic)
- _sensor_stats["total_sensors"] = sum(1 for d in data if isinstance(d, dict) and "sensors" in d)
- res = [d.get('sensors') for d in data if isinstance(d, dict) and 'sensors' in d]
-
- temp_list = []
- _sensor_stats["null_count"] = 0
-
- for sensor_list in res:
- for measure in sensor_list:
- if measure.get('unit') == "°C" and 'lastMeasurement' in measure:
- last = measure['lastMeasurement']
- if last is not None and isinstance(last, dict) and 'value' in last:
- try:
- temp_list.append(float(last['value']))
- except (TypeError, ValueError):
- _sensor_stats["null_count"] += 1
- else:
- _sensor_stats["null_count"] += 1
-
- average = sum(temp_list) / len(temp_list) if temp_list else 0.0
-
- if not temp_list:
- print("Warning: No valid temperature readings found")
-
- # Use the dictionary-based classification
- status = classify_temperature(average)
- result = f'Average temperature: {average:.2f} °C ({status})\n'
-
- if REDIS_AVAILABLE:
- try:
- redis_client.setex("temperature_data", CACHE_TTL, result)
- print("Data cached in Redis.")
- except redis.RedisError as e:
- print(f"Redis error while caching data: {e}")
-
- return result, _sensor_stats
+'''Module to get entries from OpenSenseMap API and get the average temperature'''
+# pylint: disable=too-many-locals,too-many-branches,too-many-statements
+from datetime import datetime, timezone, timedelta
+import json
+import requests
+import redis
+from app.config import create_redis_client, CACHE_TTL
+
+# Use shared Redis client
+redis_client, REDIS_AVAILABLE = create_redis_client()
+
+_sensor_stats = {"total_sensors": 0, "null_count": 0}
+
+def classify_temperature(average):
+ '''Classify temperature based on ranges using dictionary approach'''
+ # Define temperature ranges and their classifications
+ temp_classifications = {
+ "cold": (float('-inf'), 10, "Warning: Too cold"),
+ "good": (10, 36, "Good"),
+ "hot": (36, float('inf'), "Warning: Too hot")
+ }
+
+ # Find the appropriate classification
+ for _, (min_temp, max_temp, status) in temp_classifications.items():
+ if min_temp < average <= max_temp:
+ return status
+
+ return "Unknown" # Default case
+
+def _parse_partial_json_array(text: str):
+ """Parse as many full objects as possible from a (possibly truncated) JSON array."""
+ decoder = json.JSONDecoder()
+ items = []
+ i = text.find('[')
+ if i == -1:
+ return items
+ i += 1 # past '['
+ n = len(text)
+ while i < n:
+ while i < n and text[i].isspace():
+ i += 1
+ if i >= n or text[i] == ']':
+ break
+ try:
+ obj, end = decoder.raw_decode(text, i)
+ except json.JSONDecodeError:
+ # truncated object at the end; stop with what we have
+ break
+ items.append(obj)
+ i = end
+ while i < n and text[i].isspace():
+ i += 1
+ if i < n and text[i] == ',':
+ i += 1
+ return items
+
+def get_temperature():
+ '''Function to get the average temperature from OpenSenseMap API.'''
+ if REDIS_AVAILABLE:
+ try:
+ cached_data = redis_client.get("temperature_data")
+ if cached_data:
+ print("Using cached data from Redis.")
+ cached_result = cached_data
+ default_stats = {"total_sensors": 0, "null_count": 0}
+ return cached_result, default_stats
+ except redis.RedisError as e:
+ print(f"Redis error: {e}. Proceeding without cache.")
+
+ print("Fetching new data from OpenSenseMap API...")
+
+ # Ensuring that data is not older than 1 hour.
+ time_iso = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat().replace("+00:00", "Z")
+
+ params = {
+ "date": time_iso,
+ "format": "json"
+ }
+
+ # Streaming configuration
+ max_mb = 0.5
+ max_bytes = int(max_mb * 1024 * 1024)
+
+ print('Getting data from OpenSenseMap API...')
+
+ try:
+ # Stream the response and count bytes
+ response = requests.get(
+ "https://api.opensensemap.org/boxes",
+ params=params,
+ stream=True,
+ timeout=(180, 60)
+ )
+ response.raise_for_status()
+
+ downloaded = 0
+ chunks = []
+ truncated = False
+
+ for chunk in response.iter_content(chunk_size=64 * 1024): # 64 KB
+ if not chunk:
+ break
+ chunks.append(chunk)
+ downloaded += len(chunk)
+ if downloaded >= max_bytes:
+ print(f"Reached {max_mb} MB limit ({downloaded:,} bytes), stopping download")
+ truncated = True
+ response.close()
+ break
+
+ print(f'Bytes downloaded: {downloaded:,}')
+ print('Data retrieved successfully!' + (" (partial)" if truncated else ""))
+
+ # Build body and parse JSON
+ body = b"".join(chunks)
+ text = body.decode(response.encoding or "utf-8", errors="replace")
+
+ try:
+ data = json.loads(text)
+ except json.JSONDecodeError:
+ if not truncated:
+ print("Warning: Unexpected JSON parse error. Trying partial parse.")
+ data = _parse_partial_json_array(text)
+ if not data:
+ return "Error: Failed to parse JSON and no partial objects found\n", {
+ "total_sensors": 0,
+ "null_count": 0
+ }
+
+ except requests.Timeout:
+ print("API request timed out")
+ return "Error: API request timed out\n", {"total_sensors": 0, "null_count": 0}
+ except requests.RequestException as e:
+ print(f"API request failed: {e}")
+ return f"Error: API request failed - {e}\n", {"total_sensors": 0, "null_count": 0}
+
+ # Process the data (keeping the existing logic)
+ _sensor_stats["total_sensors"] = sum(1 for d in data if isinstance(d, dict) and "sensors" in d)
+ res = [d.get('sensors') for d in data if isinstance(d, dict) and 'sensors' in d]
+
+ temp_list = []
+ _sensor_stats["null_count"] = 0
+
+ for sensor_list in res:
+ for measure in sensor_list:
+ if measure.get('unit') == "°C" and 'lastMeasurement' in measure:
+ last = measure['lastMeasurement']
+ if last is not None and isinstance(last, dict) and 'value' in last:
+ try:
+ temp_list.append(float(last['value']))
+ except (TypeError, ValueError):
+ _sensor_stats["null_count"] += 1
+ else:
+ _sensor_stats["null_count"] += 1
+
+ average = sum(temp_list) / len(temp_list) if temp_list else 0.0
+
+ if not temp_list:
+ print("Warning: No valid temperature readings found")
+
+ # Use the dictionary-based classification
+ status = classify_temperature(average)
+ result = f'Average temperature: {average:.2f} °C ({status})\n'
+
+ if REDIS_AVAILABLE:
+ try:
+ redis_client.setex("temperature_data", CACHE_TTL, result)
+ print("Data cached in Redis.")
+ except redis.RedisError as e:
+ print(f"Redis error while caching data: {e}")
+
+ return result, _sensor_stats
diff --git a/app/readiness.py b/app/readiness.py
index d638608..2a6088f 100644
--- a/app/readiness.py
+++ b/app/readiness.py
@@ -1,66 +1,66 @@
-'''Module to check the readiness of the stored information'''
-import json
-import requests
-import redis
-from app.opensense import get_temperature
-from app.config import create_redis_client
-
-redis_client, REDIS_AVAILABLE = create_redis_client()
-
-def check_caching():
- '''Check if caching content is older than 5 minutes'''
- if not REDIS_AVAILABLE:
- return True
-
- try:
- cache_key = "temperature_data"
- ttl = redis_client.ttl(cache_key)
-
- if ttl in (-2, -1):
- return True
-
- return False
- except redis.RedisError as e:
- print(f"Redis error while checking cache: {e}")
- return True
-
-def reachable_boxes():
- '''Check if more than 50% of sensor boxes are reachable'''
- try:
- _, sensor_stats = get_temperature()
- total_boxes = sensor_stats.get('total_sensors', 0)
- unreachable = sensor_stats.get('null_count', 0)
-
- # No sensors configured => treat as healthy
- if total_boxes == 0:
- return 200
-
- percentage_unreachable = (unreachable / total_boxes) * 100
-
- # Fail only if strictly more than 50% are unreachable
- if percentage_unreachable > 50:
- return 400
- return 200
-
- except (json.JSONDecodeError, requests.exceptions.RequestException, redis.RedisError) as e:
- print(f"Error checking reachable boxes: {e}")
- return 200
- except (ValueError, TypeError, KeyError) as e:
- print(f"Data error checking reachable boxes: {e}")
- return 400
-
-def readiness_check():
- '''Combined readiness check for the /readyz endpoint'''
- try:
- boxes_status = reachable_boxes()
- cache_is_old = check_caching()
-
- # Only fail if BOTH conditions are bad
- if boxes_status == 400 and cache_is_old:
- return 503
-
- return 200
- except redis.RedisError as e:
- # If Redis is completely unavailable, still allow the service to be ready
- print(f"Redis error during readiness check: {e}")
- return 200
+'''Module to check the readiness of the stored information'''
+import json
+import requests
+import redis
+from app.opensense import get_temperature
+from app.config import create_redis_client
+
+redis_client, REDIS_AVAILABLE = create_redis_client()
+
+def check_caching():
+ '''Check if caching content is older than 5 minutes'''
+ if not REDIS_AVAILABLE:
+ return True
+
+ try:
+ cache_key = "temperature_data"
+ ttl = redis_client.ttl(cache_key)
+
+ if ttl in (-2, -1):
+ return True
+
+ return False
+ except redis.RedisError as e:
+ print(f"Redis error while checking cache: {e}")
+ return True
+
+def reachable_boxes():
+ '''Check if more than 50% of sensor boxes are reachable'''
+ try:
+ _, sensor_stats = get_temperature()
+ total_boxes = sensor_stats.get('total_sensors', 0)
+ unreachable = sensor_stats.get('null_count', 0)
+
+ # No sensors configured => treat as healthy
+ if total_boxes == 0:
+ return 200
+
+ percentage_unreachable = (unreachable / total_boxes) * 100
+
+ # Fail only if strictly more than 50% are unreachable
+ if percentage_unreachable > 50:
+ return 400
+ return 200
+
+ except (json.JSONDecodeError, requests.exceptions.RequestException, redis.RedisError) as e:
+ print(f"Error checking reachable boxes: {e}")
+ return 200
+ except (ValueError, TypeError, KeyError) as e:
+ print(f"Data error checking reachable boxes: {e}")
+ return 400
+
+def readiness_check():
+ '''Combined readiness check for the /readyz endpoint'''
+ try:
+ boxes_status = reachable_boxes()
+ cache_is_old = check_caching()
+
+ # Only fail if BOTH conditions are bad
+ if boxes_status == 400 and cache_is_old:
+ return 503
+
+ return 200
+ except redis.RedisError as e:
+ # If Redis is completely unavailable, still allow the service to be ready
+ print(f"Redis error during readiness check: {e}")
+ return 200
diff --git a/app/storage.py b/app/storage.py
index d93d60f..8708972 100644
--- a/app/storage.py
+++ b/app/storage.py
@@ -1,67 +1,67 @@
-'''This script uploads the output to a MinIO bucket.'''
-import os
-import io
-import datetime
-from minio import Minio
-from minio.error import S3Error, InvalidResponseError
-from app import opensense
-
-MINIO_HOST = os.getenv('MINIO_HOST', 'localhost')
-MINIO_PORT = int(os.environ.get('MINIO_PORT', 9000))
-MINIO_ACCESS_KEY = os.environ.get('MINIO_ACCESS_KEY', 'minioadmin')
-MINIO_SECRET_KEY = os.environ.get('MINIO_SECRET_KEY', 'minioadmin')
-
-def store_temperature_data():
- '''Function to upload temperature data to MinIO.'''
- try:
- client = Minio(f"{MINIO_HOST}:{MINIO_PORT}",
- access_key=MINIO_ACCESS_KEY,
- secret_key=MINIO_SECRET_KEY,
- secure=False
- )
-
- # Check if the MinIO server is reachable
- try:
- client.list_buckets()
- except ConnectionError as conn_exc:
- error_msg = f"Cannot connect to MinIO server: {conn_exc}"
- print(error_msg)
- return error_msg
-
- bucket_name = "temperature-data"
- destination_file = f"temperature_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S%f')}.txt"
-
- # Get the temperature data - unpack the tuple
- temperature_result, _ = opensense.get_temperature()
-
- text_bytes = temperature_result.encode('utf-8')
- text_stream = io.BytesIO(text_bytes)
-
- # Make the bucket if it doesn't exist.
- found = client.bucket_exists(bucket_name)
- if not found:
- client.make_bucket(bucket_name)
- print("Created bucket", bucket_name)
- else:
- print("Bucket", bucket_name, "already exists")
-
- # Upload the data
- client.put_object(
- bucket_name,
- destination_file,
- text_stream,
- length=len(text_bytes),
- content_type='text/plain'
- )
-
- return (f'Temperature data successfully uploaded as '
- f'{destination_file} to bucket {bucket_name}\n')
-
- except (S3Error, InvalidResponseError) as exc:
- error_msg = f"MinIO S3 error occurred: {exc}"
- print(error_msg)
- return error_msg
-
-if __name__ == "__main__":
- RESULT = store_temperature_data()
- print(RESULT)
+'''This script uploads the output to a MinIO bucket.'''
+import os
+import io
+import datetime
+from minio import Minio
+from minio.error import S3Error, InvalidResponseError
+from app import opensense
+
+MINIO_HOST = os.getenv('MINIO_HOST', 'localhost')
+MINIO_PORT = int(os.environ.get('MINIO_PORT', 9000))
+MINIO_ACCESS_KEY = os.environ.get('MINIO_ACCESS_KEY', 'minioadmin')
+MINIO_SECRET_KEY = os.environ.get('MINIO_SECRET_KEY', 'minioadmin')
+
+def store_temperature_data():
+ '''Function to upload temperature data to MinIO.'''
+ try:
+ client = Minio(f"{MINIO_HOST}:{MINIO_PORT}",
+ access_key=MINIO_ACCESS_KEY,
+ secret_key=MINIO_SECRET_KEY,
+ secure=False
+ )
+
+ # Check if the MinIO server is reachable
+ try:
+ client.list_buckets()
+ except ConnectionError as conn_exc:
+ error_msg = f"Cannot connect to MinIO server: {conn_exc}"
+ print(error_msg)
+ return error_msg
+
+ bucket_name = "temperature-data"
+ destination_file = f"temperature_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S%f')}.txt"
+
+ # Get the temperature data - unpack the tuple
+ temperature_result, _ = opensense.get_temperature()
+
+ text_bytes = temperature_result.encode('utf-8')
+ text_stream = io.BytesIO(text_bytes)
+
+ # Make the bucket if it doesn't exist.
+ found = client.bucket_exists(bucket_name)
+ if not found:
+ client.make_bucket(bucket_name)
+ print("Created bucket", bucket_name)
+ else:
+ print("Bucket", bucket_name, "already exists")
+
+ # Upload the data
+ client.put_object(
+ bucket_name,
+ destination_file,
+ text_stream,
+ length=len(text_bytes),
+ content_type='text/plain'
+ )
+
+ return (f'Temperature data successfully uploaded as '
+ f'{destination_file} to bucket {bucket_name}\n')
+
+ except (S3Error, InvalidResponseError) as exc:
+ error_msg = f"MinIO S3 error occurred: {exc}"
+ print(error_msg)
+ return error_msg
+
+if __name__ == "__main__":
+ RESULT = store_temperature_data()
+ print(RESULT)
diff --git a/helm-chart/Chart.yaml b/helm-chart/Chart.yaml
index 5bab78e..80a405e 100644
--- a/helm-chart/Chart.yaml
+++ b/helm-chart/Chart.yaml
@@ -1,6 +1,6 @@
-apiVersion: v2
-name: hivebox
-description: A Helm chart for HiveBox application
-type: application
-version: 0.1.0
-appVersion: "0.7.1"
+apiVersion: v2
+name: hivebox
+description: A Helm chart for HiveBox application
+type: application
+version: 0.1.0
+appVersion: "0.7.1"
diff --git a/helm-chart/NOTES.txt b/helm-chart/NOTES.txt
index 9bcfc68..f95cb6a 100644
--- a/helm-chart/NOTES.txt
+++ b/helm-chart/NOTES.txt
@@ -1,13 +1,13 @@
-# filepath: helm-chart/NOTES.txt
-1. Get the application URL by running these commands:
-{{- if contains "NodePort" .Values.service.type }}
- export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services hivebox-service)
- export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
- echo http://$NODE_IP:$NODE_PORT
-{{- else if contains "ClusterIP" .Values.service.type }}
- kubectl port-forward --namespace {{ .Release.Namespace }} svc/hivebox-service 8080:80
- echo "Visit http://127.0.0.1:8080 to use your application"
-{{- end }}
-
-2. Check the application status:
+# filepath: helm-chart/NOTES.txt
+1. Get the application URL by running these commands:
+{{- if contains "NodePort" .Values.service.type }}
+ export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services hivebox-service)
+ export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
+ echo http://$NODE_IP:$NODE_PORT
+{{- else if contains "ClusterIP" .Values.service.type }}
+ kubectl port-forward --namespace {{ .Release.Namespace }} svc/hivebox-service 8080:80
+ echo "Visit http://127.0.0.1:8080 to use your application"
+{{- end }}
+
+2. Check the application status:
kubectl get pods --namespace {{ .Release.Namespace }} -l "app=hivebox"
\ No newline at end of file
diff --git a/helm-chart/templates/_helpers.tpl b/helm-chart/templates/_helpers.tpl
index 2a8d0a5..d1020db 100644
--- a/helm-chart/templates/_helpers.tpl
+++ b/helm-chart/templates/_helpers.tpl
@@ -1,22 +1,22 @@
-{{/* Pod-level securityContext */}}
-{{- define "common.podSecurityContext" -}}
-{{- with .Values.podSecurityContext }}
-{{ toYaml . }}
-{{- end }}
-{{- end }}
-
-{{/* Container-level securityContext */}}
-{{- define "common.containerSecurityContext" -}}
-{{- with .Values.containerSecurityContext }}
-{{ toYaml . }}
-{{- end }}
-{{- end }}
-
-{{/* Resources per workload (hivebox, redis, minio, cronjob) */}}
-{{- define "common.resources" -}}
-{{- $vals := .Values -}}
-{{- $name := .name -}}
-{{- with (index $vals.resources $name) }}
-{{ toYaml . }}
-{{- end }}
-{{- end }}
+{{/* Pod-level securityContext */}}
+{{- define "common.podSecurityContext" -}}
+{{- with .Values.podSecurityContext }}
+{{ toYaml . }}
+{{- end }}
+{{- end }}
+
+{{/* Container-level securityContext */}}
+{{- define "common.containerSecurityContext" -}}
+{{- with .Values.containerSecurityContext }}
+{{ toYaml . }}
+{{- end }}
+{{- end }}
+
+{{/* Resources per workload (hivebox, redis, minio, cronjob) */}}
+{{- define "common.resources" -}}
+{{- $vals := .Values -}}
+{{- $name := .name -}}
+{{- with (index $vals.resources $name) }}
+{{ toYaml . }}
+{{- end }}
+{{- end }}
diff --git a/helm-chart/templates/cronjob.yaml b/helm-chart/templates/cronjob.yaml
index 769e044..0894aab 100644
--- a/helm-chart/templates/cronjob.yaml
+++ b/helm-chart/templates/cronjob.yaml
@@ -1,53 +1,53 @@
-apiVersion: batch/v1
-kind: CronJob
-metadata:
- name: temperature-storage-cronjob
- labels:
- app: hivebox-cronjob
-spec:
- schedule: "*/5 * * * *"
- concurrencyPolicy: Forbid
- jobTemplate:
- spec:
- template:
- spec:
- restartPolicy: OnFailure
- securityContext:
- {{- include "common.podSecurityContext" . | nindent 12 }}
- initContainers:
- - name: wait-for-start
- image: {{ .Values.images.cronjob }}
- command: ["/bin/sh", "-c"]
- args:
- - |
- set -eu
- while true; do
- if curl -sSf -m 3 http://hivebox-service/version >/dev/null; then
- echo "Hivebox service is up!"
- exit 0
- else
- echo "Waiting for Hivebox service to be available..."
- sleep 5
- fi
- done
- securityContext:
- {{- include "common.containerSecurityContext" . | nindent 16 }}
- resources:
- {{- include "common.resources" (dict "Values" .Values "name" "cronjob") | nindent 16 }}
- containers:
- - name: temperature-storage
- image: {{ .Values.images.cronjob }}
- command: ["curl"]
- args:
- - "-f"
- - "-s"
- - "-S"
- - "--max-time"
- - "60"
- - "http://hivebox-service/store"
- securityContext:
- {{- include "common.containerSecurityContext" . | nindent 16 }}
- resources:
- {{- include "common.resources" (dict "Values" .Values "name" "cronjob") | nindent 16 }}
- successfulJobsHistoryLimit: 3
- failedJobsHistoryLimit: 1
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+ name: temperature-storage-cronjob
+ labels:
+ app: hivebox-cronjob
+spec:
+ schedule: "*/5 * * * *"
+ concurrencyPolicy: Forbid
+ jobTemplate:
+ spec:
+ template:
+ spec:
+ restartPolicy: OnFailure
+ securityContext:
+ {{- include "common.podSecurityContext" . | nindent 12 }}
+ initContainers:
+ - name: wait-for-start
+ image: {{ .Values.images.cronjob }}
+ command: ["/bin/sh", "-c"]
+ args:
+ - |
+ set -eu
+ while true; do
+ if curl -sSf -m 3 http://hivebox-service/version >/dev/null; then
+ echo "Hivebox service is up!"
+ exit 0
+ else
+ echo "Waiting for Hivebox service to be available..."
+ sleep 5
+ fi
+ done
+ securityContext:
+ {{- include "common.containerSecurityContext" . | nindent 16 }}
+ resources:
+ {{- include "common.resources" (dict "Values" .Values "name" "cronjob") | nindent 16 }}
+ containers:
+ - name: temperature-storage
+ image: {{ .Values.images.cronjob }}
+ command: ["curl"]
+ args:
+ - "-f"
+ - "-s"
+ - "-S"
+ - "--max-time"
+ - "60"
+ - "http://hivebox-service/store"
+ securityContext:
+ {{- include "common.containerSecurityContext" . | nindent 16 }}
+ resources:
+ {{- include "common.resources" (dict "Values" .Values "name" "cronjob") | nindent 16 }}
+ successfulJobsHistoryLimit: 3
+ failedJobsHistoryLimit: 1
diff --git a/helm-chart/templates/deployment.yaml b/helm-chart/templates/deployment.yaml
index de4c439..a133bae 100644
--- a/helm-chart/templates/deployment.yaml
+++ b/helm-chart/templates/deployment.yaml
@@ -1,145 +1,145 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: hivebox
- labels:
- app: hivebox
-spec:
- replicas: {{ .Values.replicas.hivebox }}
- selector:
- matchLabels:
- app: hivebox
- template:
- metadata:
- labels:
- app: hivebox
- spec:
- {{- if gt (int .Values.replicas.hivebox) 1 }}
- affinity:
- podAntiAffinity:
- preferredDuringSchedulingIgnoredDuringExecution:
- - weight: 100
- podAffinityTerm:
- labelSelector:
- matchExpressions:
- - key: app
- operator: In
- values:
- - hivebox
- topologyKey: kubernetes.io/hostname
- {{- end }}
- securityContext:
- {{- include "common.podSecurityContext" . | nindent 8 }}
- containers:
- - name: hivebox
- image: {{ .Values.images.hivebox }}
- ports:
- - containerPort: 5000
- env:
- - name: REDIS_HOST
- value: {{ .Values.services.redis | quote }}
- - name: MINIO_HOST
- value: {{ .Values.services.minio | quote }}
- securityContext:
- {{- include "common.containerSecurityContext" . | nindent 12 }}
- resources:
- {{- include "common.resources" (dict "Values" .Values "name" "hivebox") | nindent 12 }}
- readinessProbe:
- httpGet:
- path: /readyz
- port: 5000
- initialDelaySeconds: 30
- timeoutSeconds: 480
- failureThreshold: 3
- periodSeconds: 600
- livenessProbe:
- httpGet:
- path: /version
- port: 5000
- timeoutSeconds: 3
- failureThreshold: 3
- periodSeconds: 60
- volumeMounts:
- - name: tmp-volume
- mountPath: /tmp
- volumes:
- - name: tmp-volume
- emptyDir: {}
-
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: redis
- labels:
- app: redis
-spec:
- replicas: {{ .Values.replicas.redis }}
- selector:
- matchLabels:
- app: redis
- template:
- metadata:
- labels:
- app: redis
- spec:
- securityContext:
- {{- include "common.podSecurityContext" . | nindent 10 }}
- containers:
- - name: valkey
- image: {{ .Values.images.redis }}
- ports:
- - containerPort: 6379
- command: ["valkey-server"]
- args: ["--save", "", "--appendonly", "no"]
- securityContext:
- {{- include "common.containerSecurityContext" . | nindent 12 }}
- resources:
- {{- include "common.resources" (dict "Values" .Values "name" "redis") | nindent 12 }}
- volumeMounts:
- - name: valkey-data
- mountPath: /data
- volumes:
- - name: valkey-data
- emptyDir: {}
-
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: minio
- labels:
- app: minio
-spec:
- replicas: {{ .Values.replicas.minio }}
- selector:
- matchLabels:
- app: minio
- template:
- metadata:
- labels:
- app: minio
- spec:
- securityContext:
- {{- include "common.podSecurityContext" . | nindent 10 }}
- containers:
- - name: minio
- image: {{ .Values.images.minio }}
- ports:
- - containerPort: 9000
- command: ["minio", "server", "/data"]
- env:
- - name: MINIO_ROOT_USER
- value: {{ .Values.minio.accessKey | quote }}
- - name: MINIO_ROOT_PASSWORD
- value: {{ .Values.minio.secretKey | quote }}
- securityContext:
- {{- include "common.containerSecurityContext" . | nindent 12 }}
- resources:
- {{- include "common.resources" (dict "Values" .Values "name" "minio") | nindent 12 }}
- volumeMounts:
- - name: minio-data
- mountPath: /data
- volumes:
- - name: minio-data
- emptyDir: {}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: hivebox
+ labels:
+ app: hivebox
+spec:
+ replicas: {{ .Values.replicas.hivebox }}
+ selector:
+ matchLabels:
+ app: hivebox
+ template:
+ metadata:
+ labels:
+ app: hivebox
+ spec:
+ {{- if gt (int .Values.replicas.hivebox) 1 }}
+ affinity:
+ podAntiAffinity:
+ preferredDuringSchedulingIgnoredDuringExecution:
+ - weight: 100
+ podAffinityTerm:
+ labelSelector:
+ matchExpressions:
+ - key: app
+ operator: In
+ values:
+ - hivebox
+ topologyKey: kubernetes.io/hostname
+ {{- end }}
+ securityContext:
+ {{- include "common.podSecurityContext" . | nindent 8 }}
+ containers:
+ - name: hivebox
+ image: {{ .Values.images.hivebox }}
+ ports:
+ - containerPort: 5000
+ env:
+ - name: REDIS_HOST
+ value: {{ .Values.services.redis | quote }}
+ - name: MINIO_HOST
+ value: {{ .Values.services.minio | quote }}
+ securityContext:
+ {{- include "common.containerSecurityContext" . | nindent 12 }}
+ resources:
+ {{- include "common.resources" (dict "Values" .Values "name" "hivebox") | nindent 12 }}
+ readinessProbe:
+ httpGet:
+ path: /readyz
+ port: 5000
+ initialDelaySeconds: 30
+ timeoutSeconds: 480
+ failureThreshold: 3
+ periodSeconds: 600
+ livenessProbe:
+ httpGet:
+ path: /version
+ port: 5000
+ timeoutSeconds: 3
+ failureThreshold: 3
+ periodSeconds: 60
+ volumeMounts:
+ - name: tmp-volume
+ mountPath: /tmp
+ volumes:
+ - name: tmp-volume
+ emptyDir: {}
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: redis
+ labels:
+ app: redis
+spec:
+ replicas: {{ .Values.replicas.redis }}
+ selector:
+ matchLabels:
+ app: redis
+ template:
+ metadata:
+ labels:
+ app: redis
+ spec:
+ securityContext:
+ {{- include "common.podSecurityContext" . | nindent 10 }}
+ containers:
+ - name: valkey
+ image: {{ .Values.images.redis }}
+ ports:
+ - containerPort: 6379
+ command: ["valkey-server"]
+ args: ["--save", "", "--appendonly", "no"]
+ securityContext:
+ {{- include "common.containerSecurityContext" . | nindent 12 }}
+ resources:
+ {{- include "common.resources" (dict "Values" .Values "name" "redis") | nindent 12 }}
+ volumeMounts:
+ - name: valkey-data
+ mountPath: /data
+ volumes:
+ - name: valkey-data
+ emptyDir: {}
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: minio
+ labels:
+ app: minio
+spec:
+ replicas: {{ .Values.replicas.minio }}
+ selector:
+ matchLabels:
+ app: minio
+ template:
+ metadata:
+ labels:
+ app: minio
+ spec:
+ securityContext:
+ {{- include "common.podSecurityContext" . | nindent 10 }}
+ containers:
+ - name: minio
+ image: {{ .Values.images.minio }}
+ ports:
+ - containerPort: 9000
+ command: ["minio", "server", "/data"]
+ env:
+ - name: MINIO_ROOT_USER
+ value: {{ .Values.minio.accessKey | quote }}
+ - name: MINIO_ROOT_PASSWORD
+ value: {{ .Values.minio.secretKey | quote }}
+ securityContext:
+ {{- include "common.containerSecurityContext" . | nindent 12 }}
+ resources:
+ {{- include "common.resources" (dict "Values" .Values "name" "minio") | nindent 12 }}
+ volumeMounts:
+ - name: minio-data
+ mountPath: /data
+ volumes:
+ - name: minio-data
+ emptyDir: {}
diff --git a/helm-chart/templates/ingress.yaml b/helm-chart/templates/ingress.yaml
index 7978576..eaaff86 100644
--- a/helm-chart/templates/ingress.yaml
+++ b/helm-chart/templates/ingress.yaml
@@ -1,21 +1,21 @@
-apiVersion: networking.k8s.io/v1
-kind: Ingress
-metadata:
- name: hivebox-ingress
- annotations:
- {{- if .Values.ingress.annotations }}
- {{- toYaml .Values.ingress.annotations | nindent 4 }}
- {{- end }}
-spec:
- ingressClassName: {{ .Values.ingress.className | default "nginx" }}
- rules:
- - host: {{ .Values.ingress.host | quote }}
- http:
- paths:
- - path: {{ .Values.ingress.path | default "/" }}
- pathType: Prefix
- backend:
- service:
- name: {{ .Values.ingress.serviceName | default "hivebox-service" }}
- port:
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: hivebox-ingress
+ annotations:
+ {{- if .Values.ingress.annotations }}
+ {{- toYaml .Values.ingress.annotations | nindent 4 }}
+ {{- end }}
+spec:
+ ingressClassName: {{ .Values.ingress.className | default "nginx" }}
+ rules:
+ - host: {{ .Values.ingress.host | quote }}
+ http:
+ paths:
+ - path: {{ .Values.ingress.path | default "/" }}
+ pathType: Prefix
+ backend:
+ service:
+ name: {{ .Values.ingress.serviceName | default "hivebox-service" }}
+ port:
number: {{ .Values.ingress.servicePort | default 80 }}
\ No newline at end of file
diff --git a/helm-chart/templates/service.yaml b/helm-chart/templates/service.yaml
index c17f307..a42b3f3 100644
--- a/helm-chart/templates/service.yaml
+++ b/helm-chart/templates/service.yaml
@@ -1,36 +1,36 @@
-apiVersion: v1
-kind: Service
-metadata:
- name: hivebox-service
- labels:
- app: hivebox
-spec:
- selector:
- app: hivebox
- ports:
- - port: 80
- targetPort: 5000
- protocol: TCP
- type: ClusterIP
----
-apiVersion: v1
-kind: Service
-metadata:
- name: redis-service
-spec:
- selector:
- app: redis
- ports:
- - port: 6379
- targetPort: 6379
----
-apiVersion: v1
-kind: Service
-metadata:
- name: minio-service
-spec:
- selector:
- app: minio
- ports:
- - port: 9000
+apiVersion: v1
+kind: Service
+metadata:
+ name: hivebox-service
+ labels:
+ app: hivebox
+spec:
+ selector:
+ app: hivebox
+ ports:
+ - port: 80
+ targetPort: 5000
+ protocol: TCP
+ type: ClusterIP
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: redis-service
+spec:
+ selector:
+ app: redis
+ ports:
+ - port: 6379
+ targetPort: 6379
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: minio-service
+spec:
+ selector:
+ app: minio
+ ports:
+ - port: 9000
targetPort: 9000
\ No newline at end of file
diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml
index 4f15594..9bf1671 100644
--- a/helm-chart/values.yaml
+++ b/helm-chart/values.yaml
@@ -1,64 +1,64 @@
-# Global, reused everywhere
-podSecurityContext:
- fsGroup: 1000
- seccompProfile:
- type: RuntimeDefault
-
-containerSecurityContext:
- allowPrivilegeEscalation: false
- readOnlyRootFilesystem: true
- runAsNonRoot: true
- runAsGroup: 1000
- runAsUser: 1000
- capabilities:
- drop: ["ALL"]
-
-images:
- hivebox: ghcr.io/gabrielpalmar/hivebox:latest@sha256:c731999c3fd9b757e2fd816e3c9dcf645dba56647d8a921cb567ece3cf378dc3
- redis: valkey/valkey:8-alpine3.22@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4
- minio: minio/minio:RELEASE.2025-07-23T15-54-02Z@sha256:d249d1fb6966de4d8ad26c04754b545205ff15a62e4fd19ebd0f26fa5baacbc0
- cronjob: curlimages/curl:8.15.0@sha256:4026b29997dc7c823b51c164b71e2b51e0fd95cce4601f78202c513d97da2922
-
-replicas:
- hivebox: 2
- redis: 1
- minio: 1
-
-resources:
- hivebox:
- limits: { memory: "512Mi", cpu: "500m" }
- requests: { memory: "256Mi", cpu: "250m" }
- redis:
- limits: { memory: "256Mi", cpu: "250m" }
- requests: { memory: "128Mi", cpu: "100m" }
- minio:
- limits: { memory: "256Mi", cpu: "250m" }
- requests: { memory: "128Mi", cpu: "100m" }
- cronjob:
- limits: { memory: "32Mi", cpu: "50m" }
- requests: { memory: "16Mi", cpu: "10m" }
-
-services:
- redis: redis-service
- minio: minio-service
-
-service:
- type: ClusterIP
- port: 80
- targetPort: 5000
-
-minio:
- accessKey: minioadmin
- secretKey: minioadmin
-
-ingress:
- enabled: true
- host: hivebox.local
- path: /
- className: nginx
- serviceName: hivebox-service
- servicePort: 80
- annotations:
- nginx.ingress.kubernetes.io/rewrite-target: /
- nginx.ingress.kubernetes.io/ssl-redirect: "false"
- nginx.ingress.kubernetes.io/proxy-body-size: "10m"
+# Global, reused everywhere
+podSecurityContext:
+ fsGroup: 1000
+ seccompProfile:
+ type: RuntimeDefault
+
+containerSecurityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: true
+ runAsNonRoot: true
+ runAsGroup: 1000
+ runAsUser: 1000
+ capabilities:
+ drop: ["ALL"]
+
+images:
+ hivebox: ghcr.io/gabrielpalmar/hivebox:latest@sha256:c731999c3fd9b757e2fd816e3c9dcf645dba56647d8a921cb567ece3cf378dc3
+ redis: valkey/valkey:8-alpine3.22@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4
+ minio: minio/minio:RELEASE.2025-07-23T15-54-02Z@sha256:d249d1fb6966de4d8ad26c04754b545205ff15a62e4fd19ebd0f26fa5baacbc0
+ cronjob: curlimages/curl:8.15.0@sha256:4026b29997dc7c823b51c164b71e2b51e0fd95cce4601f78202c513d97da2922
+
+replicas:
+ hivebox: 2
+ redis: 1
+ minio: 1
+
+resources:
+ hivebox:
+ limits: { memory: "512Mi", cpu: "500m" }
+ requests: { memory: "256Mi", cpu: "250m" }
+ redis:
+ limits: { memory: "256Mi", cpu: "250m" }
+ requests: { memory: "128Mi", cpu: "100m" }
+ minio:
+ limits: { memory: "256Mi", cpu: "250m" }
+ requests: { memory: "128Mi", cpu: "100m" }
+ cronjob:
+ limits: { memory: "32Mi", cpu: "50m" }
+ requests: { memory: "16Mi", cpu: "10m" }
+
+services:
+ redis: redis-service
+ minio: minio-service
+
+service:
+ type: ClusterIP
+ port: 80
+ targetPort: 5000
+
+minio:
+ accessKey: minioadmin
+ secretKey: minioadmin
+
+ingress:
+ enabled: true
+ host: hivebox.local
+ path: /
+ className: nginx
+ serviceName: hivebox-service
+ servicePort: 80
+ annotations:
+ nginx.ingress.kubernetes.io/rewrite-target: /
+ nginx.ingress.kubernetes.io/ssl-redirect: "false"
+ nginx.ingress.kubernetes.io/proxy-body-size: "10m"
diff --git a/kustomize/base/cronjob.yaml b/kustomize/base/cronjob.yaml
index dc05595..4e4ed5a 100644
--- a/kustomize/base/cronjob.yaml
+++ b/kustomize/base/cronjob.yaml
@@ -1,67 +1,67 @@
-apiVersion: batch/v1
-kind: CronJob
-metadata:
- name: temperature-storage-cronjob
- labels:
- app: hivebox-cronjob
-spec:
- schedule: "*/5 * * * *"
- concurrencyPolicy: Forbid
- jobTemplate:
- spec:
- template:
- spec:
- restartPolicy: OnFailure
- securityContext:
- fsGroup: 1000
- runAsNonRoot: true
- runAsUser: 1000
- runAsGroup: 1000
- initContainers:
- - name: wait-for-start
- image: curlimages/curl:8.15.0@sha256:4026b29997dc7c823b51c164b71e2b51e0fd95cce4601f78202c513d97da2922
- command: ["/bin/sh", "-c"]
- args:
- - |
- set -eu
- while true; do
- if curl -sSf -m 3 http://hivebox-service/version >/dev/null; then
- echo "Hivebox service is up!"
- exit 0
- else
- echo "Waiting for Hivebox service to be available..."
- sleep 5
- fi
- done
- securityContext:
- allowPrivilegeEscalation: false
- readOnlyRootFilesystem: true
- runAsNonRoot: true
- runAsGroup: 1000
- runAsUser: 1000
- capabilities:
- drop: ["ALL"]
- containers:
- - name: temperature-storage
- image: curlimages/curl:8.15.0@sha256:4026b29997dc7c823b51c164b71e2b51e0fd95cce4601f78202c513d97da2922
- command: ["curl"]
- args:
- - "-f"
- - "-s"
- - "-S"
- - "--max-time"
- - "60"
- - "http://hivebox-service/store"
- securityContext:
- allowPrivilegeEscalation: false
- readOnlyRootFilesystem: true
- runAsNonRoot: true
- runAsGroup: 1000
- runAsUser: 1000
- capabilities:
- drop: ["ALL"]
- resources:
- limits: { memory: "32Mi", cpu: "50m" }
- requests: { memory: "16Mi", cpu: "10m" }
- successfulJobsHistoryLimit: 3
- failedJobsHistoryLimit: 1
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+ name: temperature-storage-cronjob
+ labels:
+ app: hivebox-cronjob
+spec:
+ schedule: "*/5 * * * *"
+ concurrencyPolicy: Forbid
+ jobTemplate:
+ spec:
+ template:
+ spec:
+ restartPolicy: OnFailure
+ securityContext:
+ fsGroup: 1000
+ runAsNonRoot: true
+ runAsUser: 1000
+ runAsGroup: 1000
+ initContainers:
+ - name: wait-for-start
+ image: curlimages/curl:8.15.0@sha256:4026b29997dc7c823b51c164b71e2b51e0fd95cce4601f78202c513d97da2922
+ command: ["/bin/sh", "-c"]
+ args:
+ - |
+ set -eu
+ while true; do
+ if curl -sSf -m 3 http://hivebox-service/version >/dev/null; then
+ echo "Hivebox service is up!"
+ exit 0
+ else
+ echo "Waiting for Hivebox service to be available..."
+ sleep 5
+ fi
+ done
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: true
+ runAsNonRoot: true
+ runAsGroup: 1000
+ runAsUser: 1000
+ capabilities:
+ drop: ["ALL"]
+ containers:
+ - name: temperature-storage
+ image: curlimages/curl:8.15.0@sha256:4026b29997dc7c823b51c164b71e2b51e0fd95cce4601f78202c513d97da2922
+ command: ["curl"]
+ args:
+ - "-f"
+ - "-s"
+ - "-S"
+ - "--max-time"
+ - "60"
+ - "http://hivebox-service/store"
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: true
+ runAsNonRoot: true
+ runAsGroup: 1000
+ runAsUser: 1000
+ capabilities:
+ drop: ["ALL"]
+ resources:
+ limits: { memory: "32Mi", cpu: "50m" }
+ requests: { memory: "16Mi", cpu: "10m" }
+ successfulJobsHistoryLimit: 3
+ failedJobsHistoryLimit: 1
diff --git a/kustomize/base/deployment.yaml b/kustomize/base/deployment.yaml
index ad17429..a3a2db0 100644
--- a/kustomize/base/deployment.yaml
+++ b/kustomize/base/deployment.yaml
@@ -1,158 +1,158 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: hivebox
- labels:
- app: hivebox
-spec:
- replicas: 2
- selector:
- matchLabels:
- app: hivebox
- template:
- metadata:
- labels:
- app: hivebox
- spec:
- securityContext:
- fsGroup: 1000
- runAsNonRoot: true
- runAsUser: 1000
- runAsGroup: 1000
- containers:
- - name: hivebox
- image: ghcr.io/gabrielpalmar/hivebox:0.7.1@sha256:c731999c6fac6f2f17f746aea7fafe073cf608c49729eb1e189ecf3551c62646
- ports:
- - containerPort: 5000
- env:
- - name: REDIS_HOST
- value: redis-service
- - name: MINIO_HOST
- value: minio-service
- securityContext:
- allowPrivilegeEscalation: false
- readOnlyRootFilesystem: true
- capabilities:
- drop: ["ALL"]
- resources:
- limits: { memory: "512Mi", cpu: "500m" }
- requests: { memory: "256Mi", cpu: "250m" }
- readinessProbe:
- httpGet:
- path: /readyz
- port: 5000
- initialDelaySeconds: 30
- timeoutSeconds: 480
- failureThreshold: 3
- periodSeconds: 600
- livenessProbe:
- httpGet:
- path: /version
- port: 5000
- timeoutSeconds: 3
- failureThreshold: 3
- periodSeconds: 60
- volumeMounts:
- - name: tmp-volume
- mountPath: /tmp
- volumes:
- - name: tmp-volume
- emptyDir: {}
-
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: redis
- labels:
- app: redis
-spec:
- replicas: 1
- selector:
- matchLabels:
- app: redis
- template:
- metadata:
- labels:
- app: redis
- spec:
- securityContext:
- fsGroup: 1000
- runAsNonRoot: true
- runAsUser: 1000
- runAsGroup: 1000
- containers:
- - name: valkey
- image: valkey/valkey:8-alpine3.22@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4
- ports:
- - containerPort: 6379
- command: ["valkey-server"]
- args: ["--save", "", "--appendonly", "no"]
- securityContext:
- allowPrivilegeEscalation: false
- readOnlyRootFilesystem: true
- runAsNonRoot: true
- runAsGroup: 1000
- runAsUser: 1000
- capabilities:
- drop: ["ALL"]
- resources:
- limits: { memory: "256Mi", cpu: "250m" }
- requests: { memory: "128Mi", cpu: "100m" }
- volumeMounts:
- - name: valkey-data
- mountPath: /data
- volumes:
- - name: valkey-data
- emptyDir: {}
-
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: minio
- labels:
- app: minio
-spec:
- replicas: 1
- selector:
- matchLabels:
- app: minio
- template:
- metadata:
- labels:
- app: minio
- spec:
- securityContext:
- fsGroup: 1000
- runAsNonRoot: true
- runAsUser: 1000
- runAsGroup: 1000
- containers:
- - name: minio
- image: minio/minio:RELEASE.2025-07-23T15-54-02Z@sha256:d249d1fb6966de4d8ad26c04754b545205ff15a62e4fd19ebd0f26fa5baacbc0
- ports:
- - containerPort: 9000
- command: ["minio", "server", "/data"]
- env:
- - name: MINIO_ROOT_USER
- value: minioadmin
- - name: MINIO_ROOT_PASSWORD
- value: minioadmin
- securityContext:
- allowPrivilegeEscalation: false
- readOnlyRootFilesystem: true
- runAsNonRoot: true
- runAsGroup: 1000
- runAsUser: 1000
- capabilities:
- drop: ["ALL"]
- resources:
- limits: { memory: "256Mi", cpu: "250m" }
- requests: { memory: "128Mi", cpu: "100m" }
- volumeMounts:
- - name: minio-data
- mountPath: /data
- volumes:
- - name: minio-data
- emptyDir: {}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: hivebox
+ labels:
+ app: hivebox
+spec:
+ replicas: 2
+ selector:
+ matchLabels:
+ app: hivebox
+ template:
+ metadata:
+ labels:
+ app: hivebox
+ spec:
+ securityContext:
+ fsGroup: 1000
+ runAsNonRoot: true
+ runAsUser: 1000
+ runAsGroup: 1000
+ containers:
+ - name: hivebox
+ image: ghcr.io/gabrielpalmar/hivebox:0.7.1@sha256:c731999c6fac6f2f17f746aea7fafe073cf608c49729eb1e189ecf3551c62646
+ ports:
+ - containerPort: 5000
+ env:
+ - name: REDIS_HOST
+ value: redis-service
+ - name: MINIO_HOST
+ value: minio-service
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: true
+ capabilities:
+ drop: ["ALL"]
+ resources:
+ limits: { memory: "512Mi", cpu: "500m" }
+ requests: { memory: "256Mi", cpu: "250m" }
+ readinessProbe:
+ httpGet:
+ path: /readyz
+ port: 5000
+ initialDelaySeconds: 30
+ timeoutSeconds: 480
+ failureThreshold: 3
+ periodSeconds: 600
+ livenessProbe:
+ httpGet:
+ path: /version
+ port: 5000
+ timeoutSeconds: 3
+ failureThreshold: 3
+ periodSeconds: 60
+ volumeMounts:
+ - name: tmp-volume
+ mountPath: /tmp
+ volumes:
+ - name: tmp-volume
+ emptyDir: {}
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: redis
+ labels:
+ app: redis
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: redis
+ template:
+ metadata:
+ labels:
+ app: redis
+ spec:
+ securityContext:
+ fsGroup: 1000
+ runAsNonRoot: true
+ runAsUser: 1000
+ runAsGroup: 1000
+ containers:
+ - name: valkey
+ image: valkey/valkey:8-alpine3.22@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4
+ ports:
+ - containerPort: 6379
+ command: ["valkey-server"]
+ args: ["--save", "", "--appendonly", "no"]
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: true
+ runAsNonRoot: true
+ runAsGroup: 1000
+ runAsUser: 1000
+ capabilities:
+ drop: ["ALL"]
+ resources:
+ limits: { memory: "256Mi", cpu: "250m" }
+ requests: { memory: "128Mi", cpu: "100m" }
+ volumeMounts:
+ - name: valkey-data
+ mountPath: /data
+ volumes:
+ - name: valkey-data
+ emptyDir: {}
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: minio
+ labels:
+ app: minio
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: minio
+ template:
+ metadata:
+ labels:
+ app: minio
+ spec:
+ securityContext:
+ fsGroup: 1000
+ runAsNonRoot: true
+ runAsUser: 1000
+ runAsGroup: 1000
+ containers:
+ - name: minio
+ image: minio/minio:RELEASE.2025-07-23T15-54-02Z@sha256:d249d1fb6966de4d8ad26c04754b545205ff15a62e4fd19ebd0f26fa5baacbc0
+ ports:
+ - containerPort: 9000
+ command: ["minio", "server", "/data"]
+ env:
+ - name: MINIO_ROOT_USER
+ value: minioadmin
+ - name: MINIO_ROOT_PASSWORD
+ value: minioadmin
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: true
+ runAsNonRoot: true
+ runAsGroup: 1000
+ runAsUser: 1000
+ capabilities:
+ drop: ["ALL"]
+ resources:
+ limits: { memory: "256Mi", cpu: "250m" }
+ requests: { memory: "128Mi", cpu: "100m" }
+ volumeMounts:
+ - name: minio-data
+ mountPath: /data
+ volumes:
+ - name: minio-data
+ emptyDir: {}
diff --git a/kustomize/base/ingress.yaml b/kustomize/base/ingress.yaml
index 757ef7f..58b9f7d 100644
--- a/kustomize/base/ingress.yaml
+++ b/kustomize/base/ingress.yaml
@@ -1,21 +1,21 @@
-apiVersion: networking.k8s.io/v1
-kind: Ingress
-metadata:
- name: hivebox-ingress
- annotations:
- nginx.ingress.kubernetes.io/rewrite-target: /
- nginx.ingress.kubernetes.io/ssl-redirect: "false"
- nginx.ingress.kubernetes.io/proxy-body-size: "10m"
-spec:
- ingressClassName: nginx
- rules:
- - host: hivebox.local
- http:
- paths:
- - path: /
- pathType: Prefix
- backend:
- service:
- name: hivebox-service
- port:
- number: 80
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: hivebox-ingress
+ annotations:
+ nginx.ingress.kubernetes.io/rewrite-target: /
+ nginx.ingress.kubernetes.io/ssl-redirect: "false"
+ nginx.ingress.kubernetes.io/proxy-body-size: "10m"
+spec:
+ ingressClassName: nginx
+ rules:
+ - host: hivebox.local
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: hivebox-service
+ port:
+ number: 80
diff --git a/kustomize/base/kustomization.yaml b/kustomize/base/kustomization.yaml
index a64e1fd..571af57 100644
--- a/kustomize/base/kustomization.yaml
+++ b/kustomize/base/kustomization.yaml
@@ -1,11 +1,11 @@
-apiVersion: kustomize.config.k8s.io/v1beta1
-kind: Kustomization
-namespace: hivebox
-resources:
- - deployment.yaml
- - service.yaml
- - ingress.yaml
- - cronjob.yaml
-metadata:
- labels:
- app: hivebox
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: hivebox
+resources:
+ - deployment.yaml
+ - service.yaml
+ - ingress.yaml
+ - cronjob.yaml
+metadata:
+ labels:
+ app: hivebox
diff --git a/kustomize/base/service.yaml b/kustomize/base/service.yaml
index c17f307..a42b3f3 100644
--- a/kustomize/base/service.yaml
+++ b/kustomize/base/service.yaml
@@ -1,36 +1,36 @@
-apiVersion: v1
-kind: Service
-metadata:
- name: hivebox-service
- labels:
- app: hivebox
-spec:
- selector:
- app: hivebox
- ports:
- - port: 80
- targetPort: 5000
- protocol: TCP
- type: ClusterIP
----
-apiVersion: v1
-kind: Service
-metadata:
- name: redis-service
-spec:
- selector:
- app: redis
- ports:
- - port: 6379
- targetPort: 6379
----
-apiVersion: v1
-kind: Service
-metadata:
- name: minio-service
-spec:
- selector:
- app: minio
- ports:
- - port: 9000
+apiVersion: v1
+kind: Service
+metadata:
+ name: hivebox-service
+ labels:
+ app: hivebox
+spec:
+ selector:
+ app: hivebox
+ ports:
+ - port: 80
+ targetPort: 5000
+ protocol: TCP
+ type: ClusterIP
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: redis-service
+spec:
+ selector:
+ app: redis
+ ports:
+ - port: 6379
+ targetPort: 6379
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: minio-service
+spec:
+ selector:
+ app: minio
+ ports:
+ - port: 9000
targetPort: 9000
\ No newline at end of file
diff --git a/kustomize/overlays/prod/cronjob-patch.yaml b/kustomize/overlays/prod/cronjob-patch.yaml
index 8a65220..0e0d7ad 100644
--- a/kustomize/overlays/prod/cronjob-patch.yaml
+++ b/kustomize/overlays/prod/cronjob-patch.yaml
@@ -1,6 +1,6 @@
-apiVersion: batch/v1
-kind: CronJob
-metadata:
- name: temperature-storage-cronjob
-spec:
- schedule: "*/10 * * * *"
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+ name: temperature-storage-cronjob
+spec:
+ schedule: "*/10 * * * *"
diff --git a/kustomize/overlays/prod/deployment-patch.yaml b/kustomize/overlays/prod/deployment-patch.yaml
index 4208c17..0ce608f 100644
--- a/kustomize/overlays/prod/deployment-patch.yaml
+++ b/kustomize/overlays/prod/deployment-patch.yaml
@@ -1,11 +1,11 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: hivebox
-spec:
- replicas: 3
- template:
- spec:
- containers:
- - name: hivebox
- image: ghcr.io/gabrielpalmar/hivebox:0.7.1@sha256:c731999c6fac6f2f17f746aea7fafe073cf608c49729eb1e189ecf3551c62646
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: hivebox
+spec:
+ replicas: 3
+ template:
+ spec:
+ containers:
+ - name: hivebox
+ image: ghcr.io/gabrielpalmar/hivebox:0.7.1@sha256:c731999c6fac6f2f17f746aea7fafe073cf608c49729eb1e189ecf3551c62646
diff --git a/kustomize/overlays/prod/ingress-patch.yaml b/kustomize/overlays/prod/ingress-patch.yaml
index 5226119..ef9eb64 100644
--- a/kustomize/overlays/prod/ingress-patch.yaml
+++ b/kustomize/overlays/prod/ingress-patch.yaml
@@ -1,16 +1,16 @@
-apiVersion: networking.k8s.io/v1
-kind: Ingress
-metadata:
- name: hivebox-ingress
-spec:
- rules:
- - host: hivebox.example.com
- http:
- paths:
- - path: /
- pathType: Prefix
- backend:
- service:
- name: hivebox-service
- port:
- number: 80
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: hivebox-ingress
+spec:
+ rules:
+ - host: hivebox.example.com
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: hivebox-service
+ port:
+ number: 80
diff --git a/kustomize/overlays/prod/kustomization.yaml b/kustomize/overlays/prod/kustomization.yaml
index 33f48d5..0bfbd3f 100644
--- a/kustomize/overlays/prod/kustomization.yaml
+++ b/kustomize/overlays/prod/kustomization.yaml
@@ -1,11 +1,11 @@
-apiVersion: kustomize.config.k8s.io/v1beta1
-kind: Kustomization
-resources:
- - ../../base
-labels:
- - pairs:
- env: prod
-patches:
- - path: deployment-patch.yaml
- - path: ingress-patch.yaml
- - path: cronjob-patch.yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+ - ../../base
+labels:
+ - pairs:
+ env: prod
+patches:
+ - path: deployment-patch.yaml
+ - path: ingress-patch.yaml
+ - path: cronjob-patch.yaml
diff --git a/kustomize/overlays/staging/deployment-patch.yaml b/kustomize/overlays/staging/deployment-patch.yaml
index effb6af..8be5890 100644
--- a/kustomize/overlays/staging/deployment-patch.yaml
+++ b/kustomize/overlays/staging/deployment-patch.yaml
@@ -1,11 +1,11 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: hivebox
-spec:
- replicas: 1
- template:
- spec:
- containers:
- - name: hivebox
- image: ghcr.io/gabrielpalmar/hivebox:0.7.1@sha256:c731999c6fac6f2f17f746aea7fafe073cf608c49729eb1e189ecf3551c62646
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: hivebox
+spec:
+ replicas: 1
+ template:
+ spec:
+ containers:
+ - name: hivebox
+ image: ghcr.io/gabrielpalmar/hivebox:0.7.1@sha256:c731999c6fac6f2f17f746aea7fafe073cf608c49729eb1e189ecf3551c62646
diff --git a/kustomize/overlays/staging/ingress-patch.yaml b/kustomize/overlays/staging/ingress-patch.yaml
index 7d01cb9..46535ef 100644
--- a/kustomize/overlays/staging/ingress-patch.yaml
+++ b/kustomize/overlays/staging/ingress-patch.yaml
@@ -1,16 +1,16 @@
-apiVersion: networking.k8s.io/v1
-kind: Ingress
-metadata:
- name: hivebox-ingress
-spec:
- rules:
- - host: staging.hivebox.local
- http:
- paths:
- - path: /
- pathType: Prefix
- backend:
- service:
- name: hivebox-service
- port:
- number: 80
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: hivebox-ingress
+spec:
+ rules:
+ - host: staging.hivebox.local
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: hivebox-service
+ port:
+ number: 80
diff --git a/kustomize/overlays/staging/kustomization.yaml b/kustomize/overlays/staging/kustomization.yaml
index 1ec5d1b..9c6d752 100644
--- a/kustomize/overlays/staging/kustomization.yaml
+++ b/kustomize/overlays/staging/kustomization.yaml
@@ -1,10 +1,10 @@
-apiVersion: kustomize.config.k8s.io/v1beta1
-kind: Kustomization
-resources:
- - ../../base
-labels:
- - pairs:
- env: staging
-patches:
- - path: deployment-patch.yaml
- - path: ingress-patch.yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+ - ../../base
+labels:
+ - pairs:
+ env: staging
+patches:
+ - path: deployment-patch.yaml
+ - path: ingress-patch.yaml
diff --git a/pytest.ini b/pytest.ini
index d810fe3..6d744d1 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,7 +1,7 @@
-[tool:pytest]
-testpaths = tests
-pythonpath = .
-python_files = test_*.py
-python_classes = Test*
-python_functions = test_*
+[tool:pytest]
+testpaths = tests
+pythonpath = .
+python_files = test_*.py
+python_classes = Test*
+python_functions = test_*
addopts = --verbose --tb=short
\ No newline at end of file
diff --git a/requirements.in b/requirements.in
index 397163d..46e43c0 100644
--- a/requirements.in
+++ b/requirements.in
@@ -1,5 +1,5 @@
-Flask==3.1.2
-requests==2.32.5
-prometheus-client==0.23.1
-redis==6.4.0
+Flask==3.1.2
+requests==2.32.5
+prometheus-client==0.23.1
+redis==6.4.0
minio==7.2.16
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 7668db4..0242e3c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,374 +1,374 @@
-#
-# This file is autogenerated by pip-compile with Python 3.12
-# by the following command:
-#
-# pip-compile --generate-hashes --output-file=requirements.txt requirements.in
-#
-argon2-cffi==25.1.0 \
- --hash=sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1 \
- --hash=sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741
- # via minio
-argon2-cffi-bindings==25.1.0 \
- --hash=sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99 \
- --hash=sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6 \
- --hash=sha256:21378b40e1b8d1655dd5310c84a40fc19a9aa5e6366e835ceb8576bf0fea716d \
- --hash=sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44 \
- --hash=sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a \
- --hash=sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f \
- --hash=sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2 \
- --hash=sha256:5acb4e41090d53f17ca1110c3427f0a130f944b896fc8c83973219c97f57b690 \
- --hash=sha256:5d588dec224e2a83edbdc785a5e6f3c6cd736f46bfd4b441bbb5aa1f5085e584 \
- --hash=sha256:6dca33a9859abf613e22733131fc9194091c1fa7cb3e131c143056b4856aa47e \
- --hash=sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0 \
- --hash=sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f \
- --hash=sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623 \
- --hash=sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b \
- --hash=sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44 \
- --hash=sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98 \
- --hash=sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500 \
- --hash=sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94 \
- --hash=sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6 \
- --hash=sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d \
- --hash=sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85 \
- --hash=sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92 \
- --hash=sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d \
- --hash=sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a \
- --hash=sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520 \
- --hash=sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb
- # via argon2-cffi
-blinker==1.9.0 \
- --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \
- --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc
- # via flask
-certifi==2025.7.14 \
- --hash=sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2 \
- --hash=sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995
- # via
- # minio
- # requests
-cffi==1.17.1 \
- --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \
- --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \
- --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \
- --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \
- --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \
- --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \
- --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \
- --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \
- --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \
- --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \
- --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \
- --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \
- --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \
- --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \
- --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \
- --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \
- --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \
- --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \
- --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \
- --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \
- --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \
- --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \
- --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \
- --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \
- --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \
- --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \
- --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \
- --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \
- --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \
- --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \
- --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \
- --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \
- --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \
- --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \
- --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \
- --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \
- --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \
- --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \
- --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \
- --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \
- --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \
- --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \
- --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \
- --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \
- --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \
- --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \
- --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \
- --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \
- --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \
- --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \
- --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \
- --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \
- --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \
- --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \
- --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \
- --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \
- --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \
- --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \
- --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \
- --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \
- --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \
- --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \
- --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \
- --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \
- --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \
- --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \
- --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b
- # via argon2-cffi-bindings
-charset-normalizer==3.4.2 \
- --hash=sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4 \
- --hash=sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45 \
- --hash=sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7 \
- --hash=sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0 \
- --hash=sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7 \
- --hash=sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d \
- --hash=sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d \
- --hash=sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0 \
- --hash=sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184 \
- --hash=sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db \
- --hash=sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b \
- --hash=sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64 \
- --hash=sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b \
- --hash=sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8 \
- --hash=sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff \
- --hash=sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344 \
- --hash=sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58 \
- --hash=sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e \
- --hash=sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471 \
- --hash=sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148 \
- --hash=sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a \
- --hash=sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836 \
- --hash=sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e \
- --hash=sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63 \
- --hash=sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c \
- --hash=sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1 \
- --hash=sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01 \
- --hash=sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366 \
- --hash=sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58 \
- --hash=sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5 \
- --hash=sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c \
- --hash=sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2 \
- --hash=sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a \
- --hash=sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597 \
- --hash=sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b \
- --hash=sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5 \
- --hash=sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb \
- --hash=sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f \
- --hash=sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0 \
- --hash=sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941 \
- --hash=sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0 \
- --hash=sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86 \
- --hash=sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7 \
- --hash=sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7 \
- --hash=sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455 \
- --hash=sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6 \
- --hash=sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4 \
- --hash=sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0 \
- --hash=sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3 \
- --hash=sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1 \
- --hash=sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6 \
- --hash=sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981 \
- --hash=sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c \
- --hash=sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980 \
- --hash=sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645 \
- --hash=sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7 \
- --hash=sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12 \
- --hash=sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa \
- --hash=sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd \
- --hash=sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef \
- --hash=sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f \
- --hash=sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2 \
- --hash=sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d \
- --hash=sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5 \
- --hash=sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02 \
- --hash=sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3 \
- --hash=sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd \
- --hash=sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e \
- --hash=sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214 \
- --hash=sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd \
- --hash=sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a \
- --hash=sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c \
- --hash=sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681 \
- --hash=sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba \
- --hash=sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f \
- --hash=sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a \
- --hash=sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28 \
- --hash=sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691 \
- --hash=sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82 \
- --hash=sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a \
- --hash=sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027 \
- --hash=sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7 \
- --hash=sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518 \
- --hash=sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf \
- --hash=sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b \
- --hash=sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9 \
- --hash=sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544 \
- --hash=sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da \
- --hash=sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509 \
- --hash=sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f \
- --hash=sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a \
- --hash=sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f
- # via requests
-click==8.2.1 \
- --hash=sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202 \
- --hash=sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b
- # via flask
-flask==3.1.2 \
- --hash=sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87 \
- --hash=sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c
- # via -r requirements.in
-idna==3.10 \
- --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
- --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
- # via requests
-itsdangerous==2.2.0 \
- --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \
- --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173
- # via flask
-jinja2==3.1.6 \
- --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
- --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
- # via flask
-markupsafe==3.0.2 \
- --hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \
- --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \
- --hash=sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0 \
- --hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \
- --hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \
- --hash=sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13 \
- --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \
- --hash=sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca \
- --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \
- --hash=sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832 \
- --hash=sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0 \
- --hash=sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b \
- --hash=sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579 \
- --hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \
- --hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \
- --hash=sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff \
- --hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \
- --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \
- --hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \
- --hash=sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb \
- --hash=sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e \
- --hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \
- --hash=sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a \
- --hash=sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d \
- --hash=sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a \
- --hash=sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b \
- --hash=sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8 \
- --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \
- --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \
- --hash=sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144 \
- --hash=sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f \
- --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \
- --hash=sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d \
- --hash=sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93 \
- --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \
- --hash=sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158 \
- --hash=sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84 \
- --hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \
- --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \
- --hash=sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171 \
- --hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \
- --hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \
- --hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \
- --hash=sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d \
- --hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \
- --hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \
- --hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \
- --hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \
- --hash=sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29 \
- --hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \
- --hash=sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798 \
- --hash=sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c \
- --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \
- --hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \
- --hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \
- --hash=sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a \
- --hash=sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178 \
- --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \
- --hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \
- --hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \
- --hash=sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50
- # via
- # flask
- # jinja2
- # werkzeug
-minio==7.2.16 \
- --hash=sha256:81e365c8494d591d8204a63ee7596bfdf8a7d06ad1b1507d6b9c1664a95f299a \
- --hash=sha256:9288ab988ca57c181eb59a4c96187b293131418e28c164392186c2b89026b223
- # via -r requirements.in
-prometheus-client==0.23.1 \
- --hash=sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce \
- --hash=sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99
- # via -r requirements.in
-pycparser==2.22 \
- --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
- --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
- # via cffi
-pycryptodome==3.23.0 \
- --hash=sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4 \
- --hash=sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c \
- --hash=sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630 \
- --hash=sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f \
- --hash=sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27 \
- --hash=sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a \
- --hash=sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56 \
- --hash=sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef \
- --hash=sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5 \
- --hash=sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477 \
- --hash=sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886 \
- --hash=sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a \
- --hash=sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75 \
- --hash=sha256:63dad881b99ca653302b2c7191998dd677226222a3f2ea79999aa51ce695f720 \
- --hash=sha256:64093fc334c1eccfd3933c134c4457c34eaca235eeae49d69449dc4728079339 \
- --hash=sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625 \
- --hash=sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490 \
- --hash=sha256:6fe8258e2039eceb74dfec66b3672552b6b7d2c235b2dfecc05d16b8921649a8 \
- --hash=sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b \
- --hash=sha256:7ac1080a8da569bde76c0a104589c4f414b8ba296c0b3738cf39a466a9fb1818 \
- --hash=sha256:865d83c906b0fc6a59b510deceee656b6bc1c4fa0d82176e2b77e97a420a996a \
- --hash=sha256:89d4d56153efc4d81defe8b65fd0821ef8b2d5ddf8ed19df31ba2f00872b8002 \
- --hash=sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae \
- --hash=sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7 \
- --hash=sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d \
- --hash=sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265 \
- --hash=sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39 \
- --hash=sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566 \
- --hash=sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353 \
- --hash=sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b \
- --hash=sha256:b34e8e11d97889df57166eda1e1ddd7676da5fcd4d71a0062a760e75060514b4 \
- --hash=sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2 \
- --hash=sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575 \
- --hash=sha256:ce64e84a962b63a47a592690bdc16a7eaf709d2c2697ababf24a0def566899a6 \
- --hash=sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843 \
- --hash=sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4 \
- --hash=sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446 \
- --hash=sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379 \
- --hash=sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa \
- --hash=sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be \
- --hash=sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7
- # via minio
-redis==6.4.0 \
- --hash=sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010 \
- --hash=sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f
- # via -r requirements.in
-requests==2.32.5 \
- --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \
- --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf
- # via -r requirements.in
-typing-extensions==4.14.1 \
- --hash=sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36 \
- --hash=sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76
- # via minio
-urllib3==2.5.0 \
- --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \
- --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc
- # via
- # minio
- # requests
-werkzeug==3.1.3 \
- --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \
- --hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746
- # via flask
+#
+# This file is autogenerated by pip-compile with Python 3.12
+# by the following command:
+#
+# pip-compile --generate-hashes --output-file=requirements.txt requirements.in
+#
+argon2-cffi==25.1.0 \
+ --hash=sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1 \
+ --hash=sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741
+ # via minio
+argon2-cffi-bindings==25.1.0 \
+ --hash=sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99 \
+ --hash=sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6 \
+ --hash=sha256:21378b40e1b8d1655dd5310c84a40fc19a9aa5e6366e835ceb8576bf0fea716d \
+ --hash=sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44 \
+ --hash=sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a \
+ --hash=sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f \
+ --hash=sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2 \
+ --hash=sha256:5acb4e41090d53f17ca1110c3427f0a130f944b896fc8c83973219c97f57b690 \
+ --hash=sha256:5d588dec224e2a83edbdc785a5e6f3c6cd736f46bfd4b441bbb5aa1f5085e584 \
+ --hash=sha256:6dca33a9859abf613e22733131fc9194091c1fa7cb3e131c143056b4856aa47e \
+ --hash=sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0 \
+ --hash=sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f \
+ --hash=sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623 \
+ --hash=sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b \
+ --hash=sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44 \
+ --hash=sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98 \
+ --hash=sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500 \
+ --hash=sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94 \
+ --hash=sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6 \
+ --hash=sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d \
+ --hash=sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85 \
+ --hash=sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92 \
+ --hash=sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d \
+ --hash=sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a \
+ --hash=sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520 \
+ --hash=sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb
+ # via argon2-cffi
+blinker==1.9.0 \
+ --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \
+ --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc
+ # via flask
+certifi==2025.7.14 \
+ --hash=sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2 \
+ --hash=sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995
+ # via
+ # minio
+ # requests
+cffi==1.17.1 \
+ --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \
+ --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \
+ --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \
+ --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \
+ --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \
+ --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \
+ --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \
+ --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \
+ --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \
+ --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \
+ --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \
+ --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \
+ --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \
+ --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \
+ --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \
+ --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \
+ --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \
+ --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \
+ --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \
+ --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \
+ --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \
+ --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \
+ --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \
+ --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \
+ --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \
+ --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \
+ --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \
+ --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \
+ --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \
+ --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \
+ --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \
+ --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \
+ --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \
+ --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \
+ --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \
+ --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \
+ --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \
+ --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \
+ --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \
+ --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \
+ --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \
+ --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \
+ --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \
+ --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \
+ --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \
+ --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \
+ --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \
+ --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \
+ --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \
+ --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \
+ --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \
+ --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \
+ --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \
+ --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \
+ --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \
+ --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \
+ --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \
+ --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \
+ --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \
+ --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \
+ --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \
+ --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \
+ --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \
+ --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \
+ --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \
+ --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \
+ --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b
+ # via argon2-cffi-bindings
+charset-normalizer==3.4.2 \
+ --hash=sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4 \
+ --hash=sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45 \
+ --hash=sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7 \
+ --hash=sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0 \
+ --hash=sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7 \
+ --hash=sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d \
+ --hash=sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d \
+ --hash=sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0 \
+ --hash=sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184 \
+ --hash=sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db \
+ --hash=sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b \
+ --hash=sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64 \
+ --hash=sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b \
+ --hash=sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8 \
+ --hash=sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff \
+ --hash=sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344 \
+ --hash=sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58 \
+ --hash=sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e \
+ --hash=sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471 \
+ --hash=sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148 \
+ --hash=sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a \
+ --hash=sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836 \
+ --hash=sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e \
+ --hash=sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63 \
+ --hash=sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c \
+ --hash=sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1 \
+ --hash=sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01 \
+ --hash=sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366 \
+ --hash=sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58 \
+ --hash=sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5 \
+ --hash=sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c \
+ --hash=sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2 \
+ --hash=sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a \
+ --hash=sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597 \
+ --hash=sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b \
+ --hash=sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5 \
+ --hash=sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb \
+ --hash=sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f \
+ --hash=sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0 \
+ --hash=sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941 \
+ --hash=sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0 \
+ --hash=sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86 \
+ --hash=sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7 \
+ --hash=sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7 \
+ --hash=sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455 \
+ --hash=sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6 \
+ --hash=sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4 \
+ --hash=sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0 \
+ --hash=sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3 \
+ --hash=sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1 \
+ --hash=sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6 \
+ --hash=sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981 \
+ --hash=sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c \
+ --hash=sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980 \
+ --hash=sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645 \
+ --hash=sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7 \
+ --hash=sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12 \
+ --hash=sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa \
+ --hash=sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd \
+ --hash=sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef \
+ --hash=sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f \
+ --hash=sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2 \
+ --hash=sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d \
+ --hash=sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5 \
+ --hash=sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02 \
+ --hash=sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3 \
+ --hash=sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd \
+ --hash=sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e \
+ --hash=sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214 \
+ --hash=sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd \
+ --hash=sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a \
+ --hash=sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c \
+ --hash=sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681 \
+ --hash=sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba \
+ --hash=sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f \
+ --hash=sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a \
+ --hash=sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28 \
+ --hash=sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691 \
+ --hash=sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82 \
+ --hash=sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a \
+ --hash=sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027 \
+ --hash=sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7 \
+ --hash=sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518 \
+ --hash=sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf \
+ --hash=sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b \
+ --hash=sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9 \
+ --hash=sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544 \
+ --hash=sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da \
+ --hash=sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509 \
+ --hash=sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f \
+ --hash=sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a \
+ --hash=sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f
+ # via requests
+click==8.2.1 \
+ --hash=sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202 \
+ --hash=sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b
+ # via flask
+flask==3.1.2 \
+ --hash=sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87 \
+ --hash=sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c
+ # via -r requirements.in
+idna==3.10 \
+ --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
+ --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
+ # via requests
+itsdangerous==2.2.0 \
+ --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \
+ --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173
+ # via flask
+jinja2==3.1.6 \
+ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
+ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
+ # via flask
+markupsafe==3.0.2 \
+ --hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \
+ --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \
+ --hash=sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0 \
+ --hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \
+ --hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \
+ --hash=sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13 \
+ --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \
+ --hash=sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca \
+ --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \
+ --hash=sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832 \
+ --hash=sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0 \
+ --hash=sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b \
+ --hash=sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579 \
+ --hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \
+ --hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \
+ --hash=sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff \
+ --hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \
+ --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \
+ --hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \
+ --hash=sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb \
+ --hash=sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e \
+ --hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \
+ --hash=sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a \
+ --hash=sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d \
+ --hash=sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a \
+ --hash=sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b \
+ --hash=sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8 \
+ --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \
+ --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \
+ --hash=sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144 \
+ --hash=sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f \
+ --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \
+ --hash=sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d \
+ --hash=sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93 \
+ --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \
+ --hash=sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158 \
+ --hash=sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84 \
+ --hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \
+ --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \
+ --hash=sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171 \
+ --hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \
+ --hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \
+ --hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \
+ --hash=sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d \
+ --hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \
+ --hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \
+ --hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \
+ --hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \
+ --hash=sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29 \
+ --hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \
+ --hash=sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798 \
+ --hash=sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c \
+ --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \
+ --hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \
+ --hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \
+ --hash=sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a \
+ --hash=sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178 \
+ --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \
+ --hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \
+ --hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \
+ --hash=sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50
+ # via
+ # flask
+ # jinja2
+ # werkzeug
+minio==7.2.16 \
+ --hash=sha256:81e365c8494d591d8204a63ee7596bfdf8a7d06ad1b1507d6b9c1664a95f299a \
+ --hash=sha256:9288ab988ca57c181eb59a4c96187b293131418e28c164392186c2b89026b223
+ # via -r requirements.in
+prometheus-client==0.23.1 \
+ --hash=sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce \
+ --hash=sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99
+ # via -r requirements.in
+pycparser==2.22 \
+ --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
+ --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
+ # via cffi
+pycryptodome==3.23.0 \
+ --hash=sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4 \
+ --hash=sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c \
+ --hash=sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630 \
+ --hash=sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f \
+ --hash=sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27 \
+ --hash=sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a \
+ --hash=sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56 \
+ --hash=sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef \
+ --hash=sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5 \
+ --hash=sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477 \
+ --hash=sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886 \
+ --hash=sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a \
+ --hash=sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75 \
+ --hash=sha256:63dad881b99ca653302b2c7191998dd677226222a3f2ea79999aa51ce695f720 \
+ --hash=sha256:64093fc334c1eccfd3933c134c4457c34eaca235eeae49d69449dc4728079339 \
+ --hash=sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625 \
+ --hash=sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490 \
+ --hash=sha256:6fe8258e2039eceb74dfec66b3672552b6b7d2c235b2dfecc05d16b8921649a8 \
+ --hash=sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b \
+ --hash=sha256:7ac1080a8da569bde76c0a104589c4f414b8ba296c0b3738cf39a466a9fb1818 \
+ --hash=sha256:865d83c906b0fc6a59b510deceee656b6bc1c4fa0d82176e2b77e97a420a996a \
+ --hash=sha256:89d4d56153efc4d81defe8b65fd0821ef8b2d5ddf8ed19df31ba2f00872b8002 \
+ --hash=sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae \
+ --hash=sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7 \
+ --hash=sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d \
+ --hash=sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265 \
+ --hash=sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39 \
+ --hash=sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566 \
+ --hash=sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353 \
+ --hash=sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b \
+ --hash=sha256:b34e8e11d97889df57166eda1e1ddd7676da5fcd4d71a0062a760e75060514b4 \
+ --hash=sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2 \
+ --hash=sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575 \
+ --hash=sha256:ce64e84a962b63a47a592690bdc16a7eaf709d2c2697ababf24a0def566899a6 \
+ --hash=sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843 \
+ --hash=sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4 \
+ --hash=sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446 \
+ --hash=sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379 \
+ --hash=sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa \
+ --hash=sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be \
+ --hash=sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7
+ # via minio
+redis==6.4.0 \
+ --hash=sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010 \
+ --hash=sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f
+ # via -r requirements.in
+requests==2.32.5 \
+ --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \
+ --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf
+ # via -r requirements.in
+typing-extensions==4.14.1 \
+ --hash=sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36 \
+ --hash=sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76
+ # via minio
+urllib3==2.5.0 \
+ --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \
+ --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc
+ # via
+ # minio
+ # requests
+werkzeug==3.1.3 \
+ --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \
+ --hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746
+ # via flask
diff --git a/terraform/.gitignore b/terraform/.gitignore
new file mode 100644
index 0000000..73d14d6
--- /dev/null
+++ b/terraform/.gitignore
@@ -0,0 +1,27 @@
+# Local .terraform directories
+**/.terraform/*
+
+# .tfstate files
+*.tfstate
+*.tfstate.*
+
+# Crash log files
+crash.log
+crash.*.log
+
+# Exclude all .tfvars files, which are likely to contain sensitive data
+*.tfvars
+*.tfvars.json
+
+# Ignore override files as they are usually used to override resources locally
+override.tf
+override.tf.json
+*_override.tf
+*_override.tf.json
+
+# Ignore CLI configuration files
+.terraformrc
+terraform.rc
+
+# Ignore lock file (optional - some teams commit this)
+.terraform.lock.hcl
diff --git a/terraform/README.md b/terraform/README.md
new file mode 100644
index 0000000..8dcc82f
--- /dev/null
+++ b/terraform/README.md
@@ -0,0 +1,356 @@
+# HiveBox EKS Terraform Infrastructure
+
+This directory contains Terraform modules for deploying the HiveBox application infrastructure on AWS using Amazon EKS (Elastic Kubernetes Service).
+
+## Architecture Overview
+
+The infrastructure creates:
+
+- **VPC**: Multi-AZ VPC with public and private subnets, NAT Gateways, and Internet Gateway
+- **EKS Cluster**: Managed Kubernetes control plane with configurable logging and addons
+- **EKS Node Group**: Auto-scaling worker nodes with customizable instance types
+- **Security Groups**: Network security for cluster, nodes, and load balancer
+- **IAM Roles**: Proper IAM roles for cluster and nodes, with optional IRSA support
+
+**In-Cluster Services** (deployed via Helm/Kustomize):
+- **Valkey/Redis**: Runs as a deployment inside the cluster (redis-service)
+- **MinIO**: Runs as a deployment inside the cluster (minio-service)
+- **HiveBox Application**: Your Flask application with all components
+
+## Module Structure
+
+```
+terraform/
+├── main.tf # Root module configuration
+├── variables.tf # Input variables
+├── outputs.tf # Output values
+├── terraform.tfvars.example # Example configuration values
+├── README.md # This file
+└── modules/
+ ├── vpc/ # VPC and networking
+ ├── eks/ # EKS cluster
+ ├── node-group/ # EKS node group
+ ├── security-groups/ # Security groups
+ └── iam/ # IAM roles and policies
+```
+
+## Prerequisites
+
+Before deploying, ensure you have:
+
+1. **AWS CLI** configured with appropriate credentials
+ ```bash
+ aws configure --profile Gabriel-Admin
+ ```
+
+2. **Terraform** version >= 1.5.0
+ ```bash
+ terraform version
+ ```
+
+3. **kubectl** for Kubernetes management
+ ```bash
+ kubectl version --client
+ ```
+
+4. **Sufficient AWS permissions** to create VPC, EKS, IAM roles, and security groups
+
+## Quick Start
+
+### 1. Configure Variables
+
+```bash
+cd terraform
+
+# Copy example configuration
+cp terraform.tfvars.example terraform.tfvars
+
+# Edit configuration
+# Update these values in terraform.tfvars:
+# - cluster_public_access_cidrs: Restrict to your IP for security
+# - common_tags: Add your team/owner information
+```
+
+### 2. Deploy Infrastructure
+
+```bash
+# Initialize Terraform
+terraform init
+
+# Review planned changes
+terraform plan
+
+# Deploy infrastructure
+terraform apply
+# Type 'yes' when prompted
+```
+
+**Deployment time:** 15-20 minutes
+
+### 3. Configure kubectl
+
+After deployment completes:
+
+```bash
+# Use the output command
+terraform output -raw configure_kubectl | bash
+
+# Or manually
+aws eks update-kubeconfig \
+ --region us-east-2 \
+ --name hivebox-eks \
+ --profile Gabriel-Admin
+
+# Verify access
+kubectl get nodes
+```
+
+### 4. Deploy HiveBox Application
+
+The Terraform infrastructure only creates the EKS cluster. Your application components run inside Kubernetes.
+
+#### Using Helm
+
+```bash
+cd ../helm-chart
+
+helm upgrade --install hivebox . --namespace hivebox --create-namespace
+```
+
+#### Using Kustomize
+
+```bash
+cd ../kustomize
+
+# For production
+kubectl apply -k overlays/prod
+
+# For staging
+kubectl apply -k overlays/staging
+```
+
+#### Verify Deployment
+
+```bash
+# Check all resources
+kubectl get all -n hivebox
+
+# Check pods
+kubectl get pods -n hivebox
+
+# Expected pods:
+# - hivebox-app (2 replicas)
+# - redis/valkey (1 replica)
+# - minio (1 replica)
+
+# View logs
+kubectl logs -n hivebox -l app=hivebox --tail=50
+```
+
+## Architecture Details
+
+### Network Architecture
+
+```
+Internet
+ │
+ ▼
+┌─────────────────┐
+│ Internet Gateway│
+└────────┬────────┘
+ │
+ ▼
+┌──────────────────────────────────────┐
+│ Public Subnets (2 AZs) │
+│ - NAT Gateways │
+└────────┬─────────────────────────────┘
+ │
+ ▼
+┌──────────────────────────────────────┐
+│ Private Subnets (2 AZs) │
+│ - EKS Worker Nodes │
+│ - HiveBox Pods │
+│ - Redis/Valkey Pods │
+│ - MinIO Pods │
+└──────────────────────────────────────┘
+```
+
+### In-Cluster Services
+
+All application components run inside Kubernetes:
+
+| Service | Type | Port | Purpose |
+|---------|------|------|---------|
+| **HiveBox** | Deployment (2 replicas) | 5000 | Main Flask application |
+| **Valkey/Redis** | Deployment (1 replica) | 6379 | In-memory caching |
+| **MinIO** | Deployment (1 replica) | 9000 | Object storage |
+
+Communication between services uses Kubernetes ClusterIP services (DNS-based service discovery).
+
+## Important Configuration Variables
+
+### EKS Cluster
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `cluster_name` | `hivebox-eks` | Name of the EKS cluster |
+| `kubernetes_version` | `1.31` | Kubernetes version |
+| `cluster_endpoint_public_access` | `true` | Enable public API endpoint |
+| `cluster_public_access_cidrs` | `["0.0.0.0/0"]` | IPs allowed to access API |
+
+### Node Group
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `node_group_desired_size` | `2` | Desired number of nodes |
+| `node_group_min_size` | `1` | Minimum nodes |
+| `node_group_max_size` | `4` | Maximum nodes |
+| `node_group_instance_types` | `["t3.medium"]` | EC2 instance types |
+
+## Outputs
+
+After deployment, view outputs:
+
+```bash
+# View all outputs
+terraform output
+
+# Important outputs:
+terraform output cluster_name
+terraform output cluster_endpoint
+terraform output in_cluster_services
+```
+
+## Infrastructure Costs
+
+### Current Configuration (~$140/month)
+- **EKS Cluster**: $73/month (control plane)
+- **2x t3.medium instances**: ~$60/month
+- **2x NAT Gateways**: ~$65/month
+- **Data Transfer**: ~$5/month
+- **Total**: ~$203/month
+
+### Cost Optimization Tips
+
+For development/testing:
+
+```hcl
+# Use smaller instances
+node_group_instance_types = ["t3.small"]
+
+# Use SPOT instances (save 70%)
+node_group_capacity_type = "SPOT"
+
+# Reduce node count
+node_group_desired_size = 1
+node_group_min_size = 1
+```
+
+## Cleanup
+
+To destroy all resources:
+
+```bash
+cd terraform
+
+# IMPORTANT: Delete Kubernetes resources first
+kubectl delete namespace hivebox
+
+# Destroy Terraform infrastructure
+terraform destroy
+# Type 'yes' when prompted
+```
+
+**Warning:** This is irreversible. Ensure you have backups of any important data.
+
+## Security Best Practices
+
+1. **Restrict API Access**: Update `cluster_public_access_cidrs` to your IP
+ ```hcl
+ cluster_public_access_cidrs = ["YOUR_IP/32"]
+ ```
+
+2. **Use Private Subnets**: EKS nodes run in private subnets by default
+
+3. **Enable Logging**: All control plane logs are enabled for auditing
+
+4. **IAM Roles**: Least privilege IAM roles for cluster and nodes
+
+## Troubleshooting
+
+### Cannot create cluster
+
+**Error**: `Error creating EKS Cluster`
+
+**Solution**: Check AWS credentials and IAM permissions
+
+### Nodes not joining cluster
+
+**Solution**:
+1. Verify security groups allow communication
+2. Check IAM role has required policies
+3. Verify VPC CNI addon is installed
+
+```bash
+kubectl get pods -n kube-system | grep aws-node
+```
+
+### Application cannot connect to Redis/MinIO
+
+**Solution**: Ensure services are deployed and running
+
+```bash
+# Check services
+kubectl get svc -n hivebox
+
+# Should see:
+# - hivebox-service
+# - redis-service
+# - minio-service
+
+# Check pods
+kubectl get pods -n hivebox
+```
+
+## Maintenance
+
+### Updating Kubernetes Version
+
+1. Update `kubernetes_version` in `terraform.tfvars`
+2. Apply changes:
+ ```bash
+ terraform apply
+ ```
+
+**Note**: Upgrade one minor version at a time (1.30 → 1.31).
+
+### Scaling Nodes
+
+Update node count:
+
+```hcl
+node_group_desired_size = 3
+```
+
+Then apply:
+```bash
+terraform apply
+```
+
+## Additional Resources
+
+- [AWS EKS Documentation](https://docs.aws.amazon.com/eks/)
+- [Terraform AWS Provider](https://registry.terraform.io/providers/hashicorp/aws/latest/docs)
+- [Kubernetes Documentation](https://kubernetes.io/docs/)
+
+## Support
+
+For issues or questions:
+1. Check Terraform plan output
+2. Review CloudWatch logs
+3. Verify AWS service quotas
+4. Consult module documentation in `modules/*/`
+
+## License
+
+This infrastructure code is part of the HiveBox project.
diff --git a/terraform/main.tf b/terraform/main.tf
new file mode 100644
index 0000000..a019e63
--- /dev/null
+++ b/terraform/main.tf
@@ -0,0 +1,122 @@
+terraform {
+ required_version = ">= 1.5.0"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = "~> 6.0"
+ }
+ tls = {
+ source = "hashicorp/tls"
+ version = "~> 4.0"
+ }
+ }
+
+ # Optional: Configure S3 backend for state management
+ # Uncomment and configure this after creating the backend resources
+ # backend "s3" {
+ # bucket = "hivebox-terraform-state"
+ # key = "eks/terraform.tfstate"
+ # region = "us-east-2"
+ # encrypt = true
+ # dynamodb_table = "terraform-state-lock"
+ # }
+}
+
+# Configure the AWS Provider
+provider "aws" {
+ region = var.aws_region
+ profile = var.aws_profile
+
+ default_tags {
+ tags = var.common_tags
+ }
+}
+
+# Local variables
+locals {
+ cluster_name = var.cluster_name
+
+ common_tags = merge(
+ var.common_tags,
+ {
+ ManagedBy = "Terraform"
+ Project = "HiveBox"
+ }
+ )
+}
+
+# VPC Module
+module "vpc" {
+ source = "./modules/vpc"
+
+ cluster_name = local.cluster_name
+ vpc_cidr = var.vpc_cidr
+ availability_zones_count = var.availability_zones_count
+ tags = local.common_tags
+}
+
+# IAM Roles Module (must be created before EKS)
+module "iam" {
+ source = "./modules/iam"
+
+ cluster_name = local.cluster_name
+ oidc_provider_arn = "" # Will be populated after EKS cluster creation
+ namespace = var.kubernetes_namespace
+ service_account_name = var.kubernetes_service_account
+ create_irsa_role = var.create_irsa_role
+ tags = local.common_tags
+}
+
+# Security Groups Module
+module "security_groups" {
+ source = "./modules/security-groups"
+
+ cluster_name = local.cluster_name
+ vpc_id = module.vpc.vpc_id
+ tags = local.common_tags
+
+ depends_on = [module.vpc]
+}
+
+# EKS Cluster Module
+module "eks" {
+ source = "./modules/eks"
+
+ cluster_name = local.cluster_name
+ cluster_version = var.kubernetes_version
+ cluster_role_arn = module.iam.cluster_role_arn
+ private_subnet_ids = module.vpc.private_subnet_ids
+ public_subnet_ids = module.vpc.public_subnet_ids
+ cluster_security_group_id = module.security_groups.cluster_security_group_id
+ endpoint_private_access = var.cluster_endpoint_private_access
+ endpoint_public_access = var.cluster_endpoint_public_access
+ public_access_cidrs = var.cluster_public_access_cidrs
+ enabled_cluster_log_types = var.cluster_log_types
+ tags = local.common_tags
+
+ depends_on = [module.vpc, module.iam, module.security_groups]
+}
+
+# EKS Node Group Module
+module "node_group" {
+ source = "./modules/node-group"
+
+ cluster_name = module.eks.cluster_name
+ node_role_arn = module.iam.node_group_role_arn
+ subnet_ids = module.vpc.private_subnet_ids
+ kubernetes_version = var.kubernetes_version
+ desired_size = var.node_group_desired_size
+ max_size = var.node_group_max_size
+ min_size = var.node_group_min_size
+ instance_types = var.node_group_instance_types
+ capacity_type = var.node_group_capacity_type
+ disk_size = var.node_group_disk_size
+ tags = local.common_tags
+
+ depends_on = [module.eks]
+}
+
+# Note: MinIO and Valkey/Redis will be deployed as in-cluster Kubernetes resources
+# using your existing Helm charts or Kustomize manifests.
+# No AWS ElastiCache or S3 resources are created by this Terraform configuration.
diff --git a/terraform/modules/eks/main.tf b/terraform/modules/eks/main.tf
new file mode 100644
index 0000000..27668c7
--- /dev/null
+++ b/terraform/modules/eks/main.tf
@@ -0,0 +1,89 @@
+# EKS Cluster Module
+
+resource "aws_eks_cluster" "main" {
+ name = var.cluster_name
+ version = var.cluster_version
+ role_arn = var.cluster_role_arn
+
+ vpc_config {
+ subnet_ids = concat(var.private_subnet_ids, var.public_subnet_ids)
+ endpoint_private_access = var.endpoint_private_access
+ endpoint_public_access = var.endpoint_public_access
+ public_access_cidrs = var.public_access_cidrs
+ security_group_ids = [var.cluster_security_group_id]
+ }
+
+ enabled_cluster_log_types = var.enabled_cluster_log_types
+
+ encryption_config {
+ provider {
+ key_arn = var.kms_key_arn
+ }
+ resources = ["secrets"]
+ }
+
+ tags = var.tags
+
+ depends_on = [
+ var.cluster_role_arn
+ ]
+}
+
+# OIDC Provider for IRSA (IAM Roles for Service Accounts)
+data "tls_certificate" "cluster" {
+ url = aws_eks_cluster.main.identity[0].oidc[0].issuer
+}
+
+resource "aws_iam_openid_connect_provider" "cluster" {
+ client_id_list = ["sts.amazonaws.com"]
+ thumbprint_list = [data.tls_certificate.cluster.certificates[0].sha1_fingerprint]
+ url = aws_eks_cluster.main.identity[0].oidc[0].issuer
+
+ tags = var.tags
+}
+
+# EKS Add-ons
+resource "aws_eks_addon" "vpc_cni" {
+ cluster_name = aws_eks_cluster.main.name
+ addon_name = "vpc-cni"
+ addon_version = var.vpc_cni_version
+ resolve_conflicts_on_create = "OVERWRITE"
+ resolve_conflicts_on_update = "OVERWRITE"
+
+ tags = var.tags
+}
+
+resource "aws_eks_addon" "coredns" {
+ cluster_name = aws_eks_cluster.main.name
+ addon_name = "coredns"
+ addon_version = var.coredns_version
+ resolve_conflicts_on_create = "OVERWRITE"
+ resolve_conflicts_on_update = "OVERWRITE"
+
+ tags = var.tags
+
+ depends_on = [
+ aws_eks_addon.vpc_cni
+ ]
+}
+
+resource "aws_eks_addon" "kube_proxy" {
+ cluster_name = aws_eks_cluster.main.name
+ addon_name = "kube-proxy"
+ addon_version = var.kube_proxy_version
+ resolve_conflicts_on_create = "OVERWRITE"
+ resolve_conflicts_on_update = "OVERWRITE"
+
+ tags = var.tags
+}
+
+# EBS CSI Driver for persistent storage
+resource "aws_eks_addon" "ebs_csi_driver" {
+ cluster_name = aws_eks_cluster.main.name
+ addon_name = "aws-ebs-csi-driver"
+ addon_version = var.ebs_csi_driver_version
+ resolve_conflicts_on_create = "OVERWRITE"
+ resolve_conflicts_on_update = "OVERWRITE"
+
+ tags = var.tags
+}
diff --git a/terraform/modules/eks/outputs.tf b/terraform/modules/eks/outputs.tf
new file mode 100644
index 0000000..405444a
--- /dev/null
+++ b/terraform/modules/eks/outputs.tf
@@ -0,0 +1,45 @@
+output "cluster_id" {
+ description = "ID of the EKS cluster"
+ value = aws_eks_cluster.main.id
+}
+
+output "cluster_name" {
+ description = "Name of the EKS cluster"
+ value = aws_eks_cluster.main.name
+}
+
+output "cluster_arn" {
+ description = "ARN of the EKS cluster"
+ value = aws_eks_cluster.main.arn
+}
+
+output "cluster_endpoint" {
+ description = "Endpoint for EKS cluster API server"
+ value = aws_eks_cluster.main.endpoint
+}
+
+output "cluster_version" {
+ description = "Kubernetes version of the cluster"
+ value = aws_eks_cluster.main.version
+}
+
+output "cluster_certificate_authority_data" {
+ description = "Base64 encoded certificate data for cluster authentication"
+ value = aws_eks_cluster.main.certificate_authority[0].data
+ sensitive = true
+}
+
+output "cluster_oidc_issuer_url" {
+ description = "OIDC issuer URL for the cluster"
+ value = aws_eks_cluster.main.identity[0].oidc[0].issuer
+}
+
+output "oidc_provider_arn" {
+ description = "ARN of the OIDC provider for IRSA"
+ value = aws_iam_openid_connect_provider.cluster.arn
+}
+
+output "cluster_security_group_id" {
+ description = "Security group ID attached to the EKS cluster"
+ value = aws_eks_cluster.main.vpc_config[0].cluster_security_group_id
+}
diff --git a/terraform/modules/eks/variables.tf b/terraform/modules/eks/variables.tf
new file mode 100644
index 0000000..f578c31
--- /dev/null
+++ b/terraform/modules/eks/variables.tf
@@ -0,0 +1,90 @@
+variable "cluster_name" {
+ description = "Name of the EKS cluster"
+ type = string
+}
+
+variable "cluster_version" {
+ description = "Kubernetes version for EKS cluster"
+ type = string
+ default = "1.31"
+}
+
+variable "cluster_role_arn" {
+ description = "ARN of the IAM role for EKS cluster"
+ type = string
+}
+
+variable "private_subnet_ids" {
+ description = "List of private subnet IDs"
+ type = list(string)
+}
+
+variable "public_subnet_ids" {
+ description = "List of public subnet IDs"
+ type = list(string)
+}
+
+variable "cluster_security_group_id" {
+ description = "ID of the cluster security group"
+ type = string
+}
+
+variable "endpoint_private_access" {
+ description = "Enable private API server endpoint"
+ type = bool
+ default = true
+}
+
+variable "endpoint_public_access" {
+ description = "Enable public API server endpoint"
+ type = bool
+ default = true
+}
+
+variable "public_access_cidrs" {
+ description = "List of CIDR blocks that can access the public API server endpoint"
+ type = list(string)
+ default = ["0.0.0.0/0"]
+}
+
+variable "enabled_cluster_log_types" {
+ description = "List of control plane logging types to enable"
+ type = list(string)
+ default = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
+}
+
+variable "kms_key_arn" {
+ description = "ARN of KMS key for secrets encryption (optional)"
+ type = string
+ default = ""
+}
+
+variable "vpc_cni_version" {
+ description = "Version of VPC CNI addon"
+ type = string
+ default = null
+}
+
+variable "coredns_version" {
+ description = "Version of CoreDNS addon"
+ type = string
+ default = null
+}
+
+variable "kube_proxy_version" {
+ description = "Version of kube-proxy addon"
+ type = string
+ default = null
+}
+
+variable "ebs_csi_driver_version" {
+ description = "Version of EBS CSI driver addon"
+ type = string
+ default = null
+}
+
+variable "tags" {
+ description = "Tags to apply to all resources"
+ type = map(string)
+ default = {}
+}
diff --git a/terraform/modules/elasticache/main.tf b/terraform/modules/elasticache/main.tf
new file mode 100644
index 0000000..28ff48a
--- /dev/null
+++ b/terraform/modules/elasticache/main.tf
@@ -0,0 +1,177 @@
+# ElastiCache Redis Module
+# Replaces the in-cluster Valkey/Redis deployment
+
+resource "aws_elasticache_subnet_group" "redis" {
+ name = "${var.cluster_name}-redis-subnet-group"
+ subnet_ids = var.subnet_ids
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "${var.cluster_name}-redis-subnet-group"
+ }
+ )
+}
+
+resource "aws_elasticache_parameter_group" "redis" {
+ name = "${var.cluster_name}-redis-params"
+ family = var.parameter_group_family
+
+ # Custom parameters based on application requirements
+ dynamic "parameter" {
+ for_each = var.parameters
+ content {
+ name = parameter.value.name
+ value = parameter.value.value
+ }
+ }
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "${var.cluster_name}-redis-params"
+ }
+ )
+}
+
+# Redis Replication Group (supports both single node and cluster mode)
+resource "aws_elasticache_replication_group" "redis" {
+ replication_group_id = "${var.cluster_name}-redis"
+ description = "Redis cache for ${var.cluster_name} application"
+
+ engine = "redis"
+ engine_version = var.redis_version
+ node_type = var.node_type
+ num_cache_clusters = var.num_cache_nodes
+ port = 6379
+ parameter_group_name = aws_elasticache_parameter_group.redis.name
+ subnet_group_name = aws_elasticache_subnet_group.redis.name
+ security_group_ids = [var.security_group_id]
+
+ # Automatic failover must be enabled for multi-AZ
+ automatic_failover_enabled = var.num_cache_nodes > 1 ? true : false
+ multi_az_enabled = var.multi_az_enabled
+
+ # Backup and maintenance
+ snapshot_retention_limit = var.snapshot_retention_limit
+ snapshot_window = var.snapshot_window
+ maintenance_window = var.maintenance_window
+ auto_minor_version_upgrade = var.auto_minor_version_upgrade
+
+ # Encryption
+ at_rest_encryption_enabled = var.at_rest_encryption_enabled
+ transit_encryption_enabled = var.transit_encryption_enabled
+ auth_token = var.auth_token_enabled ? var.auth_token : null
+
+ # Logging
+ dynamic "log_delivery_configuration" {
+ for_each = var.enable_cloudwatch_logs ? [1] : []
+ content {
+ destination = aws_cloudwatch_log_group.redis[0].name
+ destination_type = "cloudwatch-logs"
+ log_format = "json"
+ log_type = "slow-log"
+ }
+ }
+
+ dynamic "log_delivery_configuration" {
+ for_each = var.enable_cloudwatch_logs ? [1] : []
+ content {
+ destination = aws_cloudwatch_log_group.redis[0].name
+ destination_type = "cloudwatch-logs"
+ log_format = "json"
+ log_type = "engine-log"
+ }
+ }
+
+ # Notification topic for events (optional)
+ notification_topic_arn = var.notification_topic_arn
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "${var.cluster_name}-redis"
+ }
+ )
+}
+
+# CloudWatch Log Group for Redis logs
+resource "aws_cloudwatch_log_group" "redis" {
+ count = var.enable_cloudwatch_logs ? 1 : 0
+ name = "/aws/elasticache/${var.cluster_name}-redis"
+ retention_in_days = var.log_retention_days
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "${var.cluster_name}-redis-logs"
+ }
+ )
+}
+
+# CloudWatch alarms for monitoring
+resource "aws_cloudwatch_metric_alarm" "redis_cpu" {
+ count = var.enable_alarms ? 1 : 0
+ alarm_name = "${var.cluster_name}-redis-cpu-utilization"
+ alarm_description = "Redis CPU utilization is too high"
+ comparison_operator = "GreaterThanThreshold"
+ evaluation_periods = 2
+ metric_name = "CPUUtilization"
+ namespace = "AWS/ElastiCache"
+ period = 300
+ statistic = "Average"
+ threshold = var.cpu_threshold
+ treat_missing_data = "notBreaching"
+
+ dimensions = {
+ ReplicationGroupId = aws_elasticache_replication_group.redis.id
+ }
+
+ alarm_actions = var.alarm_actions
+
+ tags = var.tags
+}
+
+resource "aws_cloudwatch_metric_alarm" "redis_memory" {
+ count = var.enable_alarms ? 1 : 0
+ alarm_name = "${var.cluster_name}-redis-memory-utilization"
+ alarm_description = "Redis memory utilization is too high"
+ comparison_operator = "GreaterThanThreshold"
+ evaluation_periods = 2
+ metric_name = "DatabaseMemoryUsagePercentage"
+ namespace = "AWS/ElastiCache"
+ period = 300
+ statistic = "Average"
+ threshold = var.memory_threshold
+ treat_missing_data = "notBreaching"
+
+ dimensions = {
+ ReplicationGroupId = aws_elasticache_replication_group.redis.id
+ }
+
+ alarm_actions = var.alarm_actions
+
+ tags = var.tags
+}
+
+resource "aws_cloudwatch_metric_alarm" "redis_evictions" {
+ count = var.enable_alarms ? 1 : 0
+ alarm_name = "${var.cluster_name}-redis-evictions"
+ alarm_description = "Redis evictions are too high"
+ comparison_operator = "GreaterThanThreshold"
+ evaluation_periods = 2
+ metric_name = "Evictions"
+ namespace = "AWS/ElastiCache"
+ period = 300
+ statistic = "Average"
+ threshold = var.evictions_threshold
+ treat_missing_data = "notBreaching"
+
+ dimensions = {
+ ReplicationGroupId = aws_elasticache_replication_group.redis.id
+ }
+
+ alarm_actions = var.alarm_actions
+
+ tags = var.tags
+}
diff --git a/terraform/modules/elasticache/outputs.tf b/terraform/modules/elasticache/outputs.tf
new file mode 100644
index 0000000..7a2ba6f
--- /dev/null
+++ b/terraform/modules/elasticache/outputs.tf
@@ -0,0 +1,34 @@
+output "redis_endpoint" {
+ description = "Primary endpoint for Redis cluster"
+ value = aws_elasticache_replication_group.redis.primary_endpoint_address
+}
+
+output "redis_port" {
+ description = "Port number for Redis"
+ value = aws_elasticache_replication_group.redis.port
+}
+
+output "redis_reader_endpoint" {
+ description = "Reader endpoint for Redis cluster (for read replicas)"
+ value = aws_elasticache_replication_group.redis.reader_endpoint_address
+}
+
+output "redis_configuration_endpoint" {
+ description = "Configuration endpoint for Redis cluster (cluster mode enabled)"
+ value = aws_elasticache_replication_group.redis.configuration_endpoint_address
+}
+
+output "redis_replication_group_id" {
+ description = "ID of the Redis replication group"
+ value = aws_elasticache_replication_group.redis.id
+}
+
+output "redis_replication_group_arn" {
+ description = "ARN of the Redis replication group"
+ value = aws_elasticache_replication_group.redis.arn
+}
+
+output "redis_member_clusters" {
+ description = "List of member cluster IDs"
+ value = aws_elasticache_replication_group.redis.member_clusters
+}
diff --git a/terraform/modules/elasticache/variables.tf b/terraform/modules/elasticache/variables.tf
new file mode 100644
index 0000000..69ab22d
--- /dev/null
+++ b/terraform/modules/elasticache/variables.tf
@@ -0,0 +1,161 @@
+variable "cluster_name" {
+ description = "Name of the EKS cluster"
+ type = string
+}
+
+variable "subnet_ids" {
+ description = "List of subnet IDs for ElastiCache"
+ type = list(string)
+}
+
+variable "security_group_id" {
+ description = "Security group ID for ElastiCache"
+ type = string
+}
+
+variable "redis_version" {
+ description = "Redis engine version"
+ type = string
+ default = "7.1"
+}
+
+variable "node_type" {
+ description = "Instance type for Redis nodes"
+ type = string
+ default = "cache.t3.micro"
+}
+
+variable "num_cache_nodes" {
+ description = "Number of cache nodes (1 for standalone, 2+ for replication)"
+ type = number
+ default = 2
+}
+
+variable "parameter_group_family" {
+ description = "Redis parameter group family"
+ type = string
+ default = "redis7"
+}
+
+variable "parameters" {
+ description = "List of Redis parameters to apply"
+ type = list(object({
+ name = string
+ value = string
+ }))
+ default = [
+ {
+ name = "maxmemory-policy"
+ value = "allkeys-lru"
+ }
+ ]
+}
+
+variable "multi_az_enabled" {
+ description = "Enable Multi-AZ for automatic failover"
+ type = bool
+ default = true
+}
+
+variable "snapshot_retention_limit" {
+ description = "Number of days to retain snapshots"
+ type = number
+ default = 5
+}
+
+variable "snapshot_window" {
+ description = "Daily time range for snapshots (UTC)"
+ type = string
+ default = "03:00-05:00"
+}
+
+variable "maintenance_window" {
+ description = "Weekly time range for maintenance (UTC)"
+ type = string
+ default = "sun:05:00-sun:07:00"
+}
+
+variable "auto_minor_version_upgrade" {
+ description = "Enable automatic minor version upgrades"
+ type = bool
+ default = true
+}
+
+variable "at_rest_encryption_enabled" {
+ description = "Enable encryption at rest"
+ type = bool
+ default = true
+}
+
+variable "transit_encryption_enabled" {
+ description = "Enable encryption in transit (TLS)"
+ type = bool
+ default = true
+}
+
+variable "auth_token_enabled" {
+ description = "Enable Redis AUTH token"
+ type = bool
+ default = false
+}
+
+variable "auth_token" {
+ description = "Redis AUTH token (required if auth_token_enabled is true)"
+ type = string
+ default = null
+ sensitive = true
+}
+
+variable "enable_cloudwatch_logs" {
+ description = "Enable CloudWatch logging"
+ type = bool
+ default = true
+}
+
+variable "log_retention_days" {
+ description = "CloudWatch log retention in days"
+ type = number
+ default = 7
+}
+
+variable "notification_topic_arn" {
+ description = "ARN of SNS topic for notifications"
+ type = string
+ default = ""
+}
+
+variable "enable_alarms" {
+ description = "Enable CloudWatch alarms"
+ type = bool
+ default = true
+}
+
+variable "cpu_threshold" {
+ description = "CPU utilization threshold for alarm"
+ type = number
+ default = 75
+}
+
+variable "memory_threshold" {
+ description = "Memory utilization threshold for alarm"
+ type = number
+ default = 80
+}
+
+variable "evictions_threshold" {
+ description = "Evictions threshold for alarm"
+ type = number
+ default = 1000
+}
+
+variable "alarm_actions" {
+ description = "List of ARNs to notify when alarm triggers"
+ type = list(string)
+ default = []
+}
+
+variable "tags" {
+ description = "Tags to apply to all resources"
+ type = map(string)
+ default = {}
+}
diff --git a/terraform/modules/iam/main.tf b/terraform/modules/iam/main.tf
new file mode 100644
index 0000000..14f2206
--- /dev/null
+++ b/terraform/modules/iam/main.tf
@@ -0,0 +1,95 @@
+# IAM Roles for EKS Cluster and Node Groups
+
+# EKS Cluster IAM Role
+resource "aws_iam_role" "cluster" {
+ name = "${var.cluster_name}-cluster-role"
+
+ assume_role_policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [{
+ Action = "sts:AssumeRole"
+ Effect = "Allow"
+ Principal = {
+ Service = "eks.amazonaws.com"
+ }
+ }]
+ })
+
+ tags = var.tags
+}
+
+# Attach required policies to EKS cluster role
+resource "aws_iam_role_policy_attachment" "cluster_AmazonEKSClusterPolicy" {
+ policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
+ role = aws_iam_role.cluster.name
+}
+
+resource "aws_iam_role_policy_attachment" "cluster_AmazonEKSVPCResourceController" {
+ policy_arn = "arn:aws:iam::aws:policy/AmazonEKSVPCResourceController"
+ role = aws_iam_role.cluster.name
+}
+
+# EKS Node Group IAM Role
+resource "aws_iam_role" "node_group" {
+ name = "${var.cluster_name}-node-group-role"
+
+ assume_role_policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [{
+ Action = "sts:AssumeRole"
+ Effect = "Allow"
+ Principal = {
+ Service = "ec2.amazonaws.com"
+ }
+ }]
+ })
+
+ tags = var.tags
+}
+
+# Attach required policies to node group role
+resource "aws_iam_role_policy_attachment" "node_group_AmazonEKSWorkerNodePolicy" {
+ policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
+ role = aws_iam_role.node_group.name
+}
+
+resource "aws_iam_role_policy_attachment" "node_group_AmazonEKS_CNI_Policy" {
+ policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
+ role = aws_iam_role.node_group.name
+}
+
+resource "aws_iam_role_policy_attachment" "node_group_AmazonEC2ContainerRegistryReadOnly" {
+ policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
+ role = aws_iam_role.node_group.name
+}
+
+# EBS CSI Driver policy (for persistent volumes)
+resource "aws_iam_role_policy_attachment" "node_group_AmazonEBSCSIDriverPolicy" {
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy"
+ role = aws_iam_role.node_group.name
+}
+
+# IRSA (IAM Roles for Service Accounts) Role for application pods (optional)
+resource "aws_iam_role" "pod_execution" {
+ count = var.create_irsa_role ? 1 : 0
+ name = "${var.cluster_name}-pod-execution-role"
+
+ assume_role_policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [{
+ Action = "sts:AssumeRoleWithWebIdentity"
+ Effect = "Allow"
+ Principal = {
+ Federated = var.oidc_provider_arn
+ }
+ Condition = {
+ StringEquals = {
+ "${replace(var.oidc_provider_arn, "/^(.*provider/)/", "")}:sub" = "system:serviceaccount:${var.namespace}:${var.service_account_name}"
+ "${replace(var.oidc_provider_arn, "/^(.*provider/)/", "")}:aud" = "sts.amazonaws.com"
+ }
+ }
+ }]
+ })
+
+ tags = var.tags
+}
diff --git a/terraform/modules/iam/outputs.tf b/terraform/modules/iam/outputs.tf
new file mode 100644
index 0000000..f627dc4
--- /dev/null
+++ b/terraform/modules/iam/outputs.tf
@@ -0,0 +1,24 @@
+output "cluster_role_arn" {
+ description = "ARN of the EKS cluster IAM role"
+ value = aws_iam_role.cluster.arn
+}
+
+output "cluster_role_name" {
+ description = "Name of the EKS cluster IAM role"
+ value = aws_iam_role.cluster.name
+}
+
+output "node_group_role_arn" {
+ description = "ARN of the EKS node group IAM role"
+ value = aws_iam_role.node_group.arn
+}
+
+output "node_group_role_name" {
+ description = "Name of the EKS node group IAM role"
+ value = aws_iam_role.node_group.name
+}
+
+output "pod_execution_role_arn" {
+ description = "ARN of the pod execution IAM role (IRSA - if created)"
+ value = var.create_irsa_role ? aws_iam_role.pod_execution[0].arn : null
+}
diff --git a/terraform/modules/iam/variables.tf b/terraform/modules/iam/variables.tf
new file mode 100644
index 0000000..1621166
--- /dev/null
+++ b/terraform/modules/iam/variables.tf
@@ -0,0 +1,34 @@
+variable "cluster_name" {
+ description = "Name of the EKS cluster"
+ type = string
+}
+
+variable "oidc_provider_arn" {
+ description = "ARN of the OIDC provider for IRSA"
+ type = string
+ default = ""
+}
+
+variable "namespace" {
+ description = "Kubernetes namespace for the application"
+ type = string
+ default = "hivebox"
+}
+
+variable "service_account_name" {
+ description = "Name of the Kubernetes service account"
+ type = string
+ default = "hivebox-sa"
+}
+
+variable "create_irsa_role" {
+ description = "Whether to create IRSA role for service accounts"
+ type = bool
+ default = false
+}
+
+variable "tags" {
+ description = "Tags to apply to all resources"
+ type = map(string)
+ default = {}
+}
diff --git a/terraform/modules/node-group/main.tf b/terraform/modules/node-group/main.tf
new file mode 100644
index 0000000..83ee6ae
--- /dev/null
+++ b/terraform/modules/node-group/main.tf
@@ -0,0 +1,113 @@
+# EKS Node Group Module
+
+resource "aws_eks_node_group" "main" {
+ cluster_name = var.cluster_name
+ node_group_name = "${var.cluster_name}-node-group"
+ node_role_arn = var.node_role_arn
+ subnet_ids = var.subnet_ids
+ version = var.kubernetes_version
+
+ scaling_config {
+ desired_size = var.desired_size
+ max_size = var.max_size
+ min_size = var.min_size
+ }
+
+ update_config {
+ max_unavailable_percentage = var.max_unavailable_percentage
+ }
+
+ instance_types = var.instance_types
+ capacity_type = var.capacity_type
+ disk_size = var.disk_size
+
+ # Remote access configuration (optional)
+ dynamic "remote_access" {
+ for_each = var.ec2_ssh_key != "" ? [1] : []
+ content {
+ ec2_ssh_key = var.ec2_ssh_key
+ source_security_group_ids = var.ssh_security_group_ids
+ }
+ }
+
+ # Launch template configuration
+ dynamic "launch_template" {
+ for_each = var.launch_template_id != "" ? [1] : []
+ content {
+ id = var.launch_template_id
+ version = var.launch_template_version
+ }
+ }
+
+ labels = merge(
+ var.labels,
+ {
+ "node-group" = "${var.cluster_name}-node-group"
+ }
+ )
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "${var.cluster_name}-node-group"
+ }
+ )
+
+ # Taint configuration for dedicated workloads (optional)
+ dynamic "taint" {
+ for_each = var.taints
+ content {
+ key = taint.value.key
+ value = taint.value.value
+ effect = taint.value.effect
+ }
+ }
+
+ lifecycle {
+ create_before_destroy = true
+ ignore_changes = [scaling_config[0].desired_size]
+ }
+
+ depends_on = [
+ var.node_role_arn
+ ]
+}
+
+# Auto Scaling Group Tags for Cluster Autoscaler
+resource "aws_autoscaling_group_tag" "cluster_autoscaler_enabled" {
+ for_each = toset(
+ data.aws_autoscaling_groups.node_group.names
+ )
+
+ autoscaling_group_name = each.value
+
+ tag {
+ key = "k8s.io/cluster-autoscaler/enabled"
+ value = "true"
+ propagate_at_launch = false
+ }
+}
+
+resource "aws_autoscaling_group_tag" "cluster_autoscaler_cluster_name" {
+ for_each = toset(
+ data.aws_autoscaling_groups.node_group.names
+ )
+
+ autoscaling_group_name = each.value
+
+ tag {
+ key = "k8s.io/cluster-autoscaler/${var.cluster_name}"
+ value = "owned"
+ propagate_at_launch = false
+ }
+}
+
+# Data source to get ASG names
+data "aws_autoscaling_groups" "node_group" {
+ filter {
+ name = "tag:eks:nodegroup-name"
+ values = [aws_eks_node_group.main.node_group_name]
+ }
+
+ depends_on = [aws_eks_node_group.main]
+}
diff --git a/terraform/modules/node-group/outputs.tf b/terraform/modules/node-group/outputs.tf
new file mode 100644
index 0000000..e680aad
--- /dev/null
+++ b/terraform/modules/node-group/outputs.tf
@@ -0,0 +1,24 @@
+output "node_group_id" {
+ description = "ID of the EKS node group"
+ value = aws_eks_node_group.main.id
+}
+
+output "node_group_arn" {
+ description = "ARN of the EKS node group"
+ value = aws_eks_node_group.main.arn
+}
+
+output "node_group_status" {
+ description = "Status of the EKS node group"
+ value = aws_eks_node_group.main.status
+}
+
+output "node_group_resources" {
+ description = "Resources associated with the node group"
+ value = aws_eks_node_group.main.resources
+}
+
+output "autoscaling_group_names" {
+ description = "Names of the Auto Scaling Groups"
+ value = data.aws_autoscaling_groups.node_group.names
+}
diff --git a/terraform/modules/node-group/variables.tf b/terraform/modules/node-group/variables.tf
new file mode 100644
index 0000000..2dafb4f
--- /dev/null
+++ b/terraform/modules/node-group/variables.tf
@@ -0,0 +1,108 @@
+variable "cluster_name" {
+ description = "Name of the EKS cluster"
+ type = string
+}
+
+variable "node_role_arn" {
+ description = "ARN of the IAM role for node group"
+ type = string
+}
+
+variable "subnet_ids" {
+ description = "List of subnet IDs for node group"
+ type = list(string)
+}
+
+variable "kubernetes_version" {
+ description = "Kubernetes version for node group"
+ type = string
+ default = null
+}
+
+variable "desired_size" {
+ description = "Desired number of worker nodes"
+ type = number
+ default = 2
+}
+
+variable "max_size" {
+ description = "Maximum number of worker nodes"
+ type = number
+ default = 4
+}
+
+variable "min_size" {
+ description = "Minimum number of worker nodes"
+ type = number
+ default = 1
+}
+
+variable "max_unavailable_percentage" {
+ description = "Maximum percentage of nodes unavailable during update"
+ type = number
+ default = 33
+}
+
+variable "instance_types" {
+ description = "List of instance types for node group"
+ type = list(string)
+ default = ["t3.medium"]
+}
+
+variable "capacity_type" {
+ description = "Type of capacity (ON_DEMAND or SPOT)"
+ type = string
+ default = "ON_DEMAND"
+}
+
+variable "disk_size" {
+ description = "Disk size in GiB for worker nodes"
+ type = number
+ default = 20
+}
+
+variable "ec2_ssh_key" {
+ description = "EC2 Key Pair name for SSH access to nodes"
+ type = string
+ default = ""
+}
+
+variable "ssh_security_group_ids" {
+ description = "List of security group IDs allowed to SSH to nodes"
+ type = list(string)
+ default = []
+}
+
+variable "launch_template_id" {
+ description = "ID of custom launch template (optional)"
+ type = string
+ default = ""
+}
+
+variable "launch_template_version" {
+ description = "Version of launch template to use"
+ type = string
+ default = "$Latest"
+}
+
+variable "labels" {
+ description = "Kubernetes labels to apply to nodes"
+ type = map(string)
+ default = {}
+}
+
+variable "taints" {
+ description = "List of Kubernetes taints to apply to nodes"
+ type = list(object({
+ key = string
+ value = string
+ effect = string
+ }))
+ default = []
+}
+
+variable "tags" {
+ description = "Tags to apply to all resources"
+ type = map(string)
+ default = {}
+}
diff --git a/terraform/modules/s3/main.tf b/terraform/modules/s3/main.tf
new file mode 100644
index 0000000..a60c366
--- /dev/null
+++ b/terraform/modules/s3/main.tf
@@ -0,0 +1,210 @@
+# S3 Module for Object Storage
+# Replaces MinIO for temperature data storage
+
+resource "aws_s3_bucket" "main" {
+ bucket = var.bucket_name
+ force_destroy = var.force_destroy
+
+ tags = merge(
+ var.tags,
+ {
+ Name = var.bucket_name
+ }
+ )
+}
+
+# Bucket versioning
+resource "aws_s3_bucket_versioning" "main" {
+ bucket = aws_s3_bucket.main.id
+
+ versioning_configuration {
+ status = var.versioning_enabled ? "Enabled" : "Disabled"
+ }
+}
+
+# Server-side encryption
+resource "aws_s3_bucket_server_side_encryption_configuration" "main" {
+ bucket = aws_s3_bucket.main.id
+
+ rule {
+ apply_server_side_encryption_by_default {
+ sse_algorithm = var.kms_key_id != "" ? "aws:kms" : "AES256"
+ kms_master_key_id = var.kms_key_id != "" ? var.kms_key_id : null
+ }
+ bucket_key_enabled = var.kms_key_id != "" ? true : false
+ }
+}
+
+# Block public access
+resource "aws_s3_bucket_public_access_block" "main" {
+ bucket = aws_s3_bucket.main.id
+
+ block_public_acls = var.block_public_access
+ block_public_policy = var.block_public_access
+ ignore_public_acls = var.block_public_access
+ restrict_public_buckets = var.block_public_access
+}
+
+# Lifecycle rules for data management
+resource "aws_s3_bucket_lifecycle_configuration" "main" {
+ count = var.enable_lifecycle_rules ? 1 : 0
+ bucket = aws_s3_bucket.main.id
+
+ rule {
+ id = "transition-to-ia"
+ status = "Enabled"
+
+ transition {
+ days = var.transition_to_ia_days
+ storage_class = "STANDARD_IA"
+ }
+
+ filter {
+ prefix = var.lifecycle_prefix
+ }
+ }
+
+ rule {
+ id = "transition-to-glacier"
+ status = "Enabled"
+
+ transition {
+ days = var.transition_to_glacier_days
+ storage_class = "GLACIER"
+ }
+
+ filter {
+ prefix = var.lifecycle_prefix
+ }
+ }
+
+ rule {
+ id = "expire-old-data"
+ status = var.enable_expiration ? "Enabled" : "Disabled"
+
+ expiration {
+ days = var.expiration_days
+ }
+
+ filter {
+ prefix = var.lifecycle_prefix
+ }
+ }
+
+ # Clean up incomplete multipart uploads
+ rule {
+ id = "abort-incomplete-multipart-upload"
+ status = "Enabled"
+
+ abort_incomplete_multipart_upload {
+ days_after_initiation = 7
+ }
+
+ filter {}
+ }
+}
+
+# CORS configuration (if needed for web access)
+resource "aws_s3_bucket_cors_configuration" "main" {
+ count = var.enable_cors ? 1 : 0
+ bucket = aws_s3_bucket.main.id
+
+ cors_rule {
+ allowed_headers = var.cors_allowed_headers
+ allowed_methods = var.cors_allowed_methods
+ allowed_origins = var.cors_allowed_origins
+ expose_headers = var.cors_expose_headers
+ max_age_seconds = var.cors_max_age_seconds
+ }
+}
+
+# Bucket policy for access control
+resource "aws_s3_bucket_policy" "main" {
+ bucket = aws_s3_bucket.main.id
+ policy = data.aws_iam_policy_document.bucket_policy.json
+}
+
+data "aws_iam_policy_document" "bucket_policy" {
+ # Allow SSL requests only
+ statement {
+ sid = "DenyInsecureTransport"
+ effect = "Deny"
+
+ principals {
+ type = "*"
+ identifiers = ["*"]
+ }
+
+ actions = [
+ "s3:*"
+ ]
+
+ resources = [
+ aws_s3_bucket.main.arn,
+ "${aws_s3_bucket.main.arn}/*"
+ ]
+
+ condition {
+ test = "Bool"
+ variable = "aws:SecureTransport"
+ values = ["false"]
+ }
+ }
+
+ # Additional custom policy statements
+ dynamic "statement" {
+ for_each = var.additional_policy_statements
+ content {
+ sid = statement.value.sid
+ effect = statement.value.effect
+ actions = statement.value.actions
+ resources = statement.value.resources
+
+ dynamic "principals" {
+ for_each = statement.value.principals
+ content {
+ type = principals.value.type
+ identifiers = principals.value.identifiers
+ }
+ }
+
+ dynamic "condition" {
+ for_each = lookup(statement.value, "conditions", [])
+ content {
+ test = condition.value.test
+ variable = condition.value.variable
+ values = condition.value.values
+ }
+ }
+ }
+ }
+}
+
+# Logging bucket (optional)
+resource "aws_s3_bucket" "logs" {
+ count = var.enable_logging ? 1 : 0
+ bucket = "${var.bucket_name}-logs"
+ force_destroy = var.force_destroy
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "${var.bucket_name}-logs"
+ }
+ )
+}
+
+resource "aws_s3_bucket_logging" "main" {
+ count = var.enable_logging ? 1 : 0
+ bucket = aws_s3_bucket.main.id
+
+ target_bucket = aws_s3_bucket.logs[0].id
+ target_prefix = "s3-access-logs/"
+}
+
+# CloudWatch metrics for monitoring
+resource "aws_s3_bucket_metric" "main" {
+ count = var.enable_metrics ? 1 : 0
+ bucket = aws_s3_bucket.main.id
+ name = "EntireBucket"
+}
diff --git a/terraform/modules/s3/outputs.tf b/terraform/modules/s3/outputs.tf
new file mode 100644
index 0000000..97bde69
--- /dev/null
+++ b/terraform/modules/s3/outputs.tf
@@ -0,0 +1,34 @@
+output "bucket_id" {
+ description = "ID of the S3 bucket"
+ value = aws_s3_bucket.main.id
+}
+
+output "bucket_arn" {
+ description = "ARN of the S3 bucket"
+ value = aws_s3_bucket.main.arn
+}
+
+output "bucket_domain_name" {
+ description = "Domain name of the S3 bucket"
+ value = aws_s3_bucket.main.bucket_domain_name
+}
+
+output "bucket_regional_domain_name" {
+ description = "Regional domain name of the S3 bucket"
+ value = aws_s3_bucket.main.bucket_regional_domain_name
+}
+
+output "bucket_region" {
+ description = "Region of the S3 bucket"
+ value = aws_s3_bucket.main.region
+}
+
+output "logs_bucket_id" {
+ description = "ID of the S3 logs bucket (if enabled)"
+ value = var.enable_logging ? aws_s3_bucket.logs[0].id : null
+}
+
+output "logs_bucket_arn" {
+ description = "ARN of the S3 logs bucket (if enabled)"
+ value = var.enable_logging ? aws_s3_bucket.logs[0].arn : null
+}
diff --git a/terraform/modules/s3/variables.tf b/terraform/modules/s3/variables.tf
new file mode 100644
index 0000000..0fcf669
--- /dev/null
+++ b/terraform/modules/s3/variables.tf
@@ -0,0 +1,138 @@
+variable "bucket_name" {
+ description = "Name of the S3 bucket"
+ type = string
+}
+
+variable "force_destroy" {
+ description = "Allow bucket to be destroyed even if it contains objects"
+ type = bool
+ default = false
+}
+
+variable "versioning_enabled" {
+ description = "Enable bucket versioning"
+ type = bool
+ default = true
+}
+
+variable "kms_key_id" {
+ description = "KMS key ID for encryption (optional, uses AES256 if not specified)"
+ type = string
+ default = ""
+}
+
+variable "block_public_access" {
+ description = "Block all public access to the bucket"
+ type = bool
+ default = true
+}
+
+variable "enable_lifecycle_rules" {
+ description = "Enable lifecycle rules for data management"
+ type = bool
+ default = true
+}
+
+variable "lifecycle_prefix" {
+ description = "Prefix for lifecycle rules"
+ type = string
+ default = ""
+}
+
+variable "transition_to_ia_days" {
+ description = "Days until transition to Infrequent Access storage"
+ type = number
+ default = 30
+}
+
+variable "transition_to_glacier_days" {
+ description = "Days until transition to Glacier storage"
+ type = number
+ default = 90
+}
+
+variable "enable_expiration" {
+ description = "Enable object expiration"
+ type = bool
+ default = false
+}
+
+variable "expiration_days" {
+ description = "Days until objects expire (deleted)"
+ type = number
+ default = 365
+}
+
+variable "enable_cors" {
+ description = "Enable CORS configuration"
+ type = bool
+ default = false
+}
+
+variable "cors_allowed_headers" {
+ description = "List of allowed headers for CORS"
+ type = list(string)
+ default = ["*"]
+}
+
+variable "cors_allowed_methods" {
+ description = "List of allowed methods for CORS"
+ type = list(string)
+ default = ["GET", "PUT", "POST", "DELETE"]
+}
+
+variable "cors_allowed_origins" {
+ description = "List of allowed origins for CORS"
+ type = list(string)
+ default = ["*"]
+}
+
+variable "cors_expose_headers" {
+ description = "List of headers to expose in CORS"
+ type = list(string)
+ default = ["ETag"]
+}
+
+variable "cors_max_age_seconds" {
+ description = "Max age for CORS preflight requests"
+ type = number
+ default = 3000
+}
+
+variable "additional_policy_statements" {
+ description = "Additional IAM policy statements for bucket policy"
+ type = list(object({
+ sid = string
+ effect = string
+ actions = list(string)
+ resources = list(string)
+ principals = list(object({
+ type = string
+ identifiers = list(string)
+ }))
+ conditions = optional(list(object({
+ test = string
+ variable = string
+ values = list(string)
+ })))
+ }))
+ default = []
+}
+
+variable "enable_logging" {
+ description = "Enable S3 access logging"
+ type = bool
+ default = false
+}
+
+variable "enable_metrics" {
+ description = "Enable CloudWatch metrics"
+ type = bool
+ default = true
+}
+
+variable "tags" {
+ description = "Tags to apply to all resources"
+ type = map(string)
+ default = {}
+}
diff --git a/terraform/modules/security-groups/main.tf b/terraform/modules/security-groups/main.tf
new file mode 100644
index 0000000..5ef4a4c
--- /dev/null
+++ b/terraform/modules/security-groups/main.tf
@@ -0,0 +1,128 @@
+# Security Groups for EKS and Application Load Balancer
+
+# EKS Cluster Security Group
+resource "aws_security_group" "cluster" {
+ name = "${var.cluster_name}-cluster-sg"
+ description = "Security group for EKS cluster"
+ vpc_id = var.vpc_id
+
+ egress {
+ from_port = 0
+ to_port = 0
+ protocol = "-1"
+ cidr_blocks = ["0.0.0.0/0"]
+ description = "Allow all outbound traffic"
+ }
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "${var.cluster_name}-cluster-sg"
+ }
+ )
+}
+
+# Node Group Security Group
+resource "aws_security_group" "node_group" {
+ name = "${var.cluster_name}-node-sg"
+ description = "Security group for EKS node group"
+ vpc_id = var.vpc_id
+
+ egress {
+ from_port = 0
+ to_port = 0
+ protocol = "-1"
+ cidr_blocks = ["0.0.0.0/0"]
+ description = "Allow all outbound traffic"
+ }
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "${var.cluster_name}-node-sg"
+ "kubernetes.io/cluster/${var.cluster_name}" = "owned"
+ }
+ )
+}
+
+# Allow nodes to communicate with each other
+resource "aws_security_group_rule" "node_to_node" {
+ type = "ingress"
+ from_port = 0
+ to_port = 65535
+ protocol = "-1"
+ security_group_id = aws_security_group.node_group.id
+ source_security_group_id = aws_security_group.node_group.id
+ description = "Allow nodes to communicate with each other"
+}
+
+# Allow worker nodes to receive communication from cluster control plane
+resource "aws_security_group_rule" "cluster_to_node" {
+ type = "ingress"
+ from_port = 1025
+ to_port = 65535
+ protocol = "tcp"
+ security_group_id = aws_security_group.node_group.id
+ source_security_group_id = aws_security_group.cluster.id
+ description = "Allow worker nodes to receive communication from cluster control plane"
+}
+
+# Allow cluster control plane to receive communication from worker nodes
+resource "aws_security_group_rule" "node_to_cluster" {
+ type = "ingress"
+ from_port = 443
+ to_port = 443
+ protocol = "tcp"
+ security_group_id = aws_security_group.cluster.id
+ source_security_group_id = aws_security_group.node_group.id
+ description = "Allow cluster control plane to receive communication from worker nodes"
+}
+
+# Application Load Balancer Security Group
+resource "aws_security_group" "alb" {
+ name = "${var.cluster_name}-alb-sg"
+ description = "Security group for Application Load Balancer"
+ vpc_id = var.vpc_id
+
+ ingress {
+ from_port = 80
+ to_port = 80
+ protocol = "tcp"
+ cidr_blocks = ["0.0.0.0/0"]
+ description = "Allow HTTP traffic from internet"
+ }
+
+ ingress {
+ from_port = 443
+ to_port = 443
+ protocol = "tcp"
+ cidr_blocks = ["0.0.0.0/0"]
+ description = "Allow HTTPS traffic from internet"
+ }
+
+ egress {
+ from_port = 0
+ to_port = 0
+ protocol = "-1"
+ cidr_blocks = ["0.0.0.0/0"]
+ description = "Allow all outbound traffic"
+ }
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "${var.cluster_name}-alb-sg"
+ }
+ )
+}
+
+# Allow ALB to communicate with nodes
+resource "aws_security_group_rule" "alb_to_nodes" {
+ type = "ingress"
+ from_port = 0
+ to_port = 65535
+ protocol = "tcp"
+ security_group_id = aws_security_group.node_group.id
+ source_security_group_id = aws_security_group.alb.id
+ description = "Allow ALB to communicate with nodes"
+}
diff --git a/terraform/modules/security-groups/outputs.tf b/terraform/modules/security-groups/outputs.tf
new file mode 100644
index 0000000..35c5f2b
--- /dev/null
+++ b/terraform/modules/security-groups/outputs.tf
@@ -0,0 +1,14 @@
+output "cluster_security_group_id" {
+ description = "ID of the EKS cluster security group"
+ value = aws_security_group.cluster.id
+}
+
+output "node_security_group_id" {
+ description = "ID of the EKS node group security group"
+ value = aws_security_group.node_group.id
+}
+
+output "alb_security_group_id" {
+ description = "ID of the Application Load Balancer security group"
+ value = aws_security_group.alb.id
+}
diff --git a/terraform/modules/security-groups/variables.tf b/terraform/modules/security-groups/variables.tf
new file mode 100644
index 0000000..ee1bc82
--- /dev/null
+++ b/terraform/modules/security-groups/variables.tf
@@ -0,0 +1,15 @@
+variable "cluster_name" {
+ description = "Name of the EKS cluster"
+ type = string
+}
+
+variable "vpc_id" {
+ description = "ID of the VPC"
+ type = string
+}
+
+variable "tags" {
+ description = "Tags to apply to all resources"
+ type = map(string)
+ default = {}
+}
diff --git a/terraform/modules/vpc/main.tf b/terraform/modules/vpc/main.tf
new file mode 100644
index 0000000..7924307
--- /dev/null
+++ b/terraform/modules/vpc/main.tf
@@ -0,0 +1,148 @@
+# VPC Module for EKS Cluster
+# Creates a VPC with public and private subnets across multiple AZs
+
+data "aws_availability_zones" "available" {
+ state = "available"
+}
+
+# VPC
+resource "aws_vpc" "main" {
+ cidr_block = var.vpc_cidr
+ enable_dns_hostnames = true
+ enable_dns_support = true
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "${var.cluster_name}-vpc"
+ "kubernetes.io/cluster/${var.cluster_name}" = "shared"
+ }
+ )
+}
+
+# Internet Gateway
+resource "aws_internet_gateway" "main" {
+ vpc_id = aws_vpc.main.id
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "${var.cluster_name}-igw"
+ }
+ )
+}
+
+# Public Subnets
+resource "aws_subnet" "public" {
+ count = var.availability_zones_count
+ vpc_id = aws_vpc.main.id
+ cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
+ availability_zone = data.aws_availability_zones.available.names[count.index]
+ map_public_ip_on_launch = true
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "${var.cluster_name}-public-${data.aws_availability_zones.available.names[count.index]}"
+ "kubernetes.io/cluster/${var.cluster_name}" = "shared"
+ "kubernetes.io/role/elb" = "1"
+ }
+ )
+}
+
+# Private Subnets
+resource "aws_subnet" "private" {
+ count = var.availability_zones_count
+ vpc_id = aws_vpc.main.id
+ cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + var.availability_zones_count)
+ availability_zone = data.aws_availability_zones.available.names[count.index]
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "${var.cluster_name}-private-${data.aws_availability_zones.available.names[count.index]}"
+ "kubernetes.io/cluster/${var.cluster_name}" = "shared"
+ "kubernetes.io/role/internal-elb" = "1"
+ }
+ )
+}
+
+# Elastic IPs for NAT Gateways
+resource "aws_eip" "nat" {
+ count = var.availability_zones_count
+ domain = "vpc"
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "${var.cluster_name}-nat-eip-${count.index + 1}"
+ }
+ )
+
+ depends_on = [aws_internet_gateway.main]
+}
+
+# NAT Gateways
+resource "aws_nat_gateway" "main" {
+ count = var.availability_zones_count
+ allocation_id = aws_eip.nat[count.index].id
+ subnet_id = aws_subnet.public[count.index].id
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "${var.cluster_name}-nat-${count.index + 1}"
+ }
+ )
+
+ depends_on = [aws_internet_gateway.main]
+}
+
+# Route Table for Public Subnets
+resource "aws_route_table" "public" {
+ vpc_id = aws_vpc.main.id
+
+ route {
+ cidr_block = "0.0.0.0/0"
+ gateway_id = aws_internet_gateway.main.id
+ }
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "${var.cluster_name}-public-rt"
+ }
+ )
+}
+
+# Route Table Associations for Public Subnets
+resource "aws_route_table_association" "public" {
+ count = var.availability_zones_count
+ subnet_id = aws_subnet.public[count.index].id
+ route_table_id = aws_route_table.public.id
+}
+
+# Route Tables for Private Subnets
+resource "aws_route_table" "private" {
+ count = var.availability_zones_count
+ vpc_id = aws_vpc.main.id
+
+ route {
+ cidr_block = "0.0.0.0/0"
+ nat_gateway_id = aws_nat_gateway.main[count.index].id
+ }
+
+ tags = merge(
+ var.tags,
+ {
+ Name = "${var.cluster_name}-private-rt-${count.index + 1}"
+ }
+ )
+}
+
+# Route Table Associations for Private Subnets
+resource "aws_route_table_association" "private" {
+ count = var.availability_zones_count
+ subnet_id = aws_subnet.private[count.index].id
+ route_table_id = aws_route_table.private[count.index].id
+}
diff --git a/terraform/modules/vpc/outputs.tf b/terraform/modules/vpc/outputs.tf
new file mode 100644
index 0000000..ea9543d
--- /dev/null
+++ b/terraform/modules/vpc/outputs.tf
@@ -0,0 +1,29 @@
+output "vpc_id" {
+ description = "ID of the VPC"
+ value = aws_vpc.main.id
+}
+
+output "vpc_cidr" {
+ description = "CIDR block of the VPC"
+ value = aws_vpc.main.cidr_block
+}
+
+output "public_subnet_ids" {
+ description = "IDs of the public subnets"
+ value = aws_subnet.public[*].id
+}
+
+output "private_subnet_ids" {
+ description = "IDs of the private subnets"
+ value = aws_subnet.private[*].id
+}
+
+output "nat_gateway_ids" {
+ description = "IDs of the NAT Gateways"
+ value = aws_nat_gateway.main[*].id
+}
+
+output "internet_gateway_id" {
+ description = "ID of the Internet Gateway"
+ value = aws_internet_gateway.main.id
+}
diff --git a/terraform/modules/vpc/variables.tf b/terraform/modules/vpc/variables.tf
new file mode 100644
index 0000000..48876d3
--- /dev/null
+++ b/terraform/modules/vpc/variables.tf
@@ -0,0 +1,22 @@
+variable "vpc_cidr" {
+ description = "CIDR block for VPC"
+ type = string
+ default = "10.0.0.0/16"
+}
+
+variable "cluster_name" {
+ description = "Name of the EKS cluster"
+ type = string
+}
+
+variable "availability_zones_count" {
+ description = "Number of availability zones to use"
+ type = number
+ default = 2
+}
+
+variable "tags" {
+ description = "Tags to apply to all resources"
+ type = map(string)
+ default = {}
+}
diff --git a/terraform/outputs.tf b/terraform/outputs.tf
new file mode 100644
index 0000000..744cb23
--- /dev/null
+++ b/terraform/outputs.tf
@@ -0,0 +1,119 @@
+# VPC Outputs
+output "vpc_id" {
+ description = "ID of the VPC"
+ value = module.vpc.vpc_id
+}
+
+output "vpc_cidr" {
+ description = "CIDR block of the VPC"
+ value = module.vpc.vpc_cidr
+}
+
+output "private_subnet_ids" {
+ description = "IDs of the private subnets"
+ value = module.vpc.private_subnet_ids
+}
+
+output "public_subnet_ids" {
+ description = "IDs of the public subnets"
+ value = module.vpc.public_subnet_ids
+}
+
+# EKS Cluster Outputs
+output "cluster_name" {
+ description = "Name of the EKS cluster"
+ value = module.eks.cluster_name
+}
+
+output "cluster_endpoint" {
+ description = "Endpoint for EKS cluster API server"
+ value = module.eks.cluster_endpoint
+}
+
+output "cluster_version" {
+ description = "Kubernetes version of the cluster"
+ value = module.eks.cluster_version
+}
+
+output "cluster_certificate_authority_data" {
+ description = "Base64 encoded certificate data for cluster authentication"
+ value = module.eks.cluster_certificate_authority_data
+ sensitive = true
+}
+
+output "cluster_oidc_issuer_url" {
+ description = "OIDC issuer URL for the cluster"
+ value = module.eks.cluster_oidc_issuer_url
+}
+
+output "oidc_provider_arn" {
+ description = "ARN of the OIDC provider for IRSA"
+ value = module.eks.oidc_provider_arn
+}
+
+# Node Group Outputs
+output "node_group_id" {
+ description = "ID of the EKS node group"
+ value = module.node_group.node_group_id
+}
+
+output "node_group_status" {
+ description = "Status of the EKS node group"
+ value = module.node_group.node_group_status
+}
+
+output "autoscaling_group_names" {
+ description = "Names of the Auto Scaling Groups"
+ value = module.node_group.autoscaling_group_names
+}
+
+# IAM Outputs
+output "cluster_role_arn" {
+ description = "ARN of the EKS cluster IAM role"
+ value = module.iam.cluster_role_arn
+}
+
+output "node_group_role_arn" {
+ description = "ARN of the EKS node group IAM role"
+ value = module.iam.node_group_role_arn
+}
+
+output "pod_execution_role_arn" {
+ description = "ARN of the pod execution IAM role (IRSA - if created)"
+ value = module.iam.pod_execution_role_arn
+}
+
+# Security Group Outputs
+output "cluster_security_group_id" {
+ description = "ID of the EKS cluster security group"
+ value = module.security_groups.cluster_security_group_id
+}
+
+output "node_security_group_id" {
+ description = "ID of the EKS node group security group"
+ value = module.security_groups.node_security_group_id
+}
+
+output "alb_security_group_id" {
+ description = "ID of the Application Load Balancer security group"
+ value = module.security_groups.alb_security_group_id
+}
+
+# Kubeconfig Command
+output "configure_kubectl" {
+ description = "Command to configure kubectl for the EKS cluster"
+ value = "aws eks update-kubeconfig --region ${var.aws_region} --name ${module.eks.cluster_name} --profile ${var.aws_profile}"
+}
+
+# In-Cluster Services Note
+output "in_cluster_services" {
+ description = "Services running inside the Kubernetes cluster"
+ value = {
+ note = "MinIO and Valkey/Redis run as in-cluster services. Deploy them using Helm or Kustomize."
+ redis_host = "redis-service (ClusterIP service in hivebox namespace)"
+ redis_port = 6379
+ minio_host = "minio-service (ClusterIP service in hivebox namespace)"
+ minio_port = 9000
+ namespace = var.kubernetes_namespace
+ }
+}
diff --git a/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example
new file mode 100644
index 0000000..4047507
--- /dev/null
+++ b/terraform/terraform.tfvars.example
@@ -0,0 +1,50 @@
+# AWS Provider Configuration
+# Copy this file to terraform.tfvars and customize the values
+# DO NOT commit terraform.tfvars to version control
+
+aws_region = "us-east-2"
+aws_profile = "Gabriel-Admin"
+
+# Common Tags
+common_tags = {
+ Environment = "production"
+ Project = "HiveBox"
+ Team = "DevOps"
+ Owner = "YourName"
+ ManagedBy = "Terraform"
+}
+
+# EKS Cluster Configuration
+cluster_name = "hivebox-eks"
+kubernetes_version = "1.31"
+cluster_endpoint_private_access = true
+cluster_endpoint_public_access = true
+
+# Restrict public access to specific IPs for better security
+# cluster_public_access_cidrs = ["YOUR_IP/32"]
+cluster_public_access_cidrs = ["0.0.0.0/0"]
+
+cluster_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
+
+# VPC Configuration
+vpc_cidr = "10.0.0.0/16"
+availability_zones_count = 2 # Use 2 AZs for high availability
+
+# Node Group Configuration
+node_group_desired_size = 2
+node_group_max_size = 4
+node_group_min_size = 1
+node_group_instance_types = ["t3.medium"] # Suitable for moderate workloads
+node_group_capacity_type = "ON_DEMAND" # or "SPOT" for cost savings
+node_group_disk_size = 20
+
+# Kubernetes Application Configuration
+kubernetes_namespace = "hivebox"
+kubernetes_service_account = "hivebox-sa"
+create_irsa_role = false # Set to true if you need IRSA for AWS service access
+
+# NOTE: MinIO and Valkey/Redis will run as in-cluster services
+# Deploy them using your existing Helm charts or Kustomize manifests
+# They will use:
+# - redis-service (ClusterIP) on port 6379
+# - minio-service (ClusterIP) on port 9000
diff --git a/terraform/variables.tf b/terraform/variables.tf
new file mode 100644
index 0000000..3ff3754
--- /dev/null
+++ b/terraform/variables.tf
@@ -0,0 +1,129 @@
+# AWS Provider Configuration
+variable "aws_region" {
+ description = "AWS region for resources"
+ type = string
+ default = "us-east-2"
+}
+
+variable "aws_profile" {
+ description = "AWS CLI profile to use"
+ type = string
+ default = "Gabriel-Admin"
+}
+
+# Common Tags
+variable "common_tags" {
+ description = "Common tags to apply to all resources"
+ type = map(string)
+ default = {
+ Environment = "production"
+ Project = "HiveBox"
+ ManagedBy = "Terraform"
+ }
+}
+
+# EKS Cluster Configuration
+variable "cluster_name" {
+ description = "Name of the EKS cluster"
+ type = string
+ default = "hivebox-eks"
+}
+
+variable "kubernetes_version" {
+ description = "Kubernetes version for EKS cluster"
+ type = string
+ default = "1.31"
+}
+
+variable "cluster_endpoint_private_access" {
+ description = "Enable private API server endpoint"
+ type = bool
+ default = true
+}
+
+variable "cluster_endpoint_public_access" {
+ description = "Enable public API server endpoint"
+ type = bool
+ default = true
+}
+
+variable "cluster_public_access_cidrs" {
+ description = "List of CIDR blocks that can access the public API server endpoint"
+ type = list(string)
+ default = ["0.0.0.0/0"]
+}
+
+variable "cluster_log_types" {
+ description = "List of control plane logging types to enable"
+ type = list(string)
+ default = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
+}
+
+# VPC Configuration
+variable "vpc_cidr" {
+ description = "CIDR block for VPC"
+ type = string
+ default = "10.0.0.0/16"
+}
+
+variable "availability_zones_count" {
+ description = "Number of availability zones to use"
+ type = number
+ default = 2
+}
+
+# Node Group Configuration
+variable "node_group_desired_size" {
+ description = "Desired number of worker nodes"
+ type = number
+ default = 2
+}
+
+variable "node_group_max_size" {
+ description = "Maximum number of worker nodes"
+ type = number
+ default = 4
+}
+
+variable "node_group_min_size" {
+ description = "Minimum number of worker nodes"
+ type = number
+ default = 1
+}
+
+variable "node_group_instance_types" {
+ description = "List of instance types for node group"
+ type = list(string)
+ default = ["t3.medium"]
+}
+
+variable "node_group_capacity_type" {
+ description = "Type of capacity (ON_DEMAND or SPOT)"
+ type = string
+ default = "ON_DEMAND"
+}
+
+variable "node_group_disk_size" {
+ description = "Disk size in GiB for worker nodes"
+ type = number
+ default = 20
+}
+
+# Kubernetes Application Configuration
+variable "kubernetes_namespace" {
+ description = "Kubernetes namespace for the HiveBox application"
+ type = string
+ default = "hivebox"
+}
+
+variable "kubernetes_service_account" {
+ description = "Name of the Kubernetes service account for IRSA"
+ type = string
+ default = "hivebox-sa"
+}
+
+variable "create_irsa_role" {
+ description = "Whether to create IRSA role for service accounts"
+ type = bool
+ default = false
+}
diff --git a/tests/fixtures/vcr_cassettes/metrics.yaml b/tests/fixtures/vcr_cassettes/metrics.yaml
index 65879d0..01c4d69 100644
--- a/tests/fixtures/vcr_cassettes/metrics.yaml
+++ b/tests/fixtures/vcr_cassettes/metrics.yaml
@@ -1,107 +1,107 @@
-interactions:
-- request:
- body: null
- headers:
- Accept:
- - '*/*'
- Accept-Encoding:
- - gzip, deflate
- Connection:
- - keep-alive
- User-Agent:
- - python-requests/2.31.0
- method: GET
- uri: http://127.0.0.1:5000/metrics
- response:
- body:
- string: '# HELP python_gc_objects_collected_total Objects collected during gc
-
- # TYPE python_gc_objects_collected_total counter
-
- python_gc_objects_collected_total{generation="0"} 352.0
-
- python_gc_objects_collected_total{generation="1"} 25.0
-
- python_gc_objects_collected_total{generation="2"} 0.0
-
- # HELP python_gc_objects_uncollectable_total Uncollectable objects found during
- GC
-
- # TYPE python_gc_objects_uncollectable_total counter
-
- python_gc_objects_uncollectable_total{generation="0"} 0.0
-
- python_gc_objects_uncollectable_total{generation="1"} 0.0
-
- python_gc_objects_uncollectable_total{generation="2"} 0.0
-
- # HELP python_gc_collections_total Number of times this generation was collected
-
- # TYPE python_gc_collections_total counter
-
- python_gc_collections_total{generation="0"} 30.0
-
- python_gc_collections_total{generation="1"} 2.0
-
- python_gc_collections_total{generation="2"} 0.0
-
- # HELP python_info Python platform information
-
- # TYPE python_info gauge
-
- python_info{implementation="CPython",major="3",minor="13",patchlevel="5",version="3.13.5"}
- 1.0
-
- # HELP process_virtual_memory_bytes Virtual memory size in bytes.
-
- # TYPE process_virtual_memory_bytes gauge
-
- process_virtual_memory_bytes 4.7783936e+07
-
- # HELP process_resident_memory_bytes Resident memory size in bytes.
-
- # TYPE process_resident_memory_bytes gauge
-
- process_resident_memory_bytes 4.12672e+07
-
- # HELP process_start_time_seconds Start time of the process since unix epoch
- in seconds.
-
- # TYPE process_start_time_seconds gauge
-
- process_start_time_seconds 1.75087194069e+09
-
- # HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.
-
- # TYPE process_cpu_seconds_total counter
-
- process_cpu_seconds_total 1.21
-
- # HELP process_open_fds Number of open file descriptors.
-
- # TYPE process_open_fds gauge
-
- process_open_fds 6.0
-
- # HELP process_max_fds Maximum number of open file descriptors.
-
- # TYPE process_max_fds gauge
-
- process_max_fds 1.048576e+06
-
- '
- headers:
- Connection:
- - close
- Content-Length:
- - '1891'
- Content-Type:
- - text/plain; version=0.0.4; charset=utf-8; charset=utf-8
- Date:
- - Wed, 25 Jun 2025 17:21:04 GMT
- Server:
- - Werkzeug/3.1.3 Python/3.13.5
- status:
- code: 200
- message: OK
-version: 1
+interactions:
+- request:
+ body: null
+ headers:
+ Accept:
+ - '*/*'
+ Accept-Encoding:
+ - gzip, deflate
+ Connection:
+ - keep-alive
+ User-Agent:
+ - python-requests/2.31.0
+ method: GET
+ uri: http://127.0.0.1:5000/metrics
+ response:
+ body:
+ string: '# HELP python_gc_objects_collected_total Objects collected during gc
+
+ # TYPE python_gc_objects_collected_total counter
+
+ python_gc_objects_collected_total{generation="0"} 352.0
+
+ python_gc_objects_collected_total{generation="1"} 25.0
+
+ python_gc_objects_collected_total{generation="2"} 0.0
+
+ # HELP python_gc_objects_uncollectable_total Uncollectable objects found during
+ GC
+
+ # TYPE python_gc_objects_uncollectable_total counter
+
+ python_gc_objects_uncollectable_total{generation="0"} 0.0
+
+ python_gc_objects_uncollectable_total{generation="1"} 0.0
+
+ python_gc_objects_uncollectable_total{generation="2"} 0.0
+
+ # HELP python_gc_collections_total Number of times this generation was collected
+
+ # TYPE python_gc_collections_total counter
+
+ python_gc_collections_total{generation="0"} 30.0
+
+ python_gc_collections_total{generation="1"} 2.0
+
+ python_gc_collections_total{generation="2"} 0.0
+
+ # HELP python_info Python platform information
+
+ # TYPE python_info gauge
+
+ python_info{implementation="CPython",major="3",minor="13",patchlevel="5",version="3.13.5"}
+ 1.0
+
+ # HELP process_virtual_memory_bytes Virtual memory size in bytes.
+
+ # TYPE process_virtual_memory_bytes gauge
+
+ process_virtual_memory_bytes 4.7783936e+07
+
+ # HELP process_resident_memory_bytes Resident memory size in bytes.
+
+ # TYPE process_resident_memory_bytes gauge
+
+ process_resident_memory_bytes 4.12672e+07
+
+ # HELP process_start_time_seconds Start time of the process since unix epoch
+ in seconds.
+
+ # TYPE process_start_time_seconds gauge
+
+ process_start_time_seconds 1.75087194069e+09
+
+ # HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.
+
+ # TYPE process_cpu_seconds_total counter
+
+ process_cpu_seconds_total 1.21
+
+ # HELP process_open_fds Number of open file descriptors.
+
+ # TYPE process_open_fds gauge
+
+ process_open_fds 6.0
+
+ # HELP process_max_fds Maximum number of open file descriptors.
+
+ # TYPE process_max_fds gauge
+
+ process_max_fds 1.048576e+06
+
+ '
+ headers:
+ Connection:
+ - close
+ Content-Length:
+ - '1891'
+ Content-Type:
+ - text/plain; version=0.0.4; charset=utf-8; charset=utf-8
+ Date:
+ - Wed, 25 Jun 2025 17:21:04 GMT
+ Server:
+ - Werkzeug/3.1.3 Python/3.13.5
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/tests/fixtures/vcr_cassettes/temperature.yaml b/tests/fixtures/vcr_cassettes/temperature.yaml
index e3ed554..b4329d4 100644
--- a/tests/fixtures/vcr_cassettes/temperature.yaml
+++ b/tests/fixtures/vcr_cassettes/temperature.yaml
@@ -1,32 +1,32 @@
-interactions:
-- request:
- body: null
- headers:
- Accept:
- - '*/*'
- Accept-Encoding:
- - gzip, deflate
- Connection:
- - keep-alive
- User-Agent:
- - python-requests/2.31.0
- method: GET
- uri: http://127.0.0.1:5000/temperature
- response:
- body:
- string: "Average temperature: 21.01 \xB0C (Good)\n"
- headers:
- Connection:
- - close
- Content-Length:
- - '38'
- Content-Type:
- - text/html; charset=utf-8
- Date:
- - Wed, 25 Jun 2025 17:21:04 GMT
- Server:
- - Werkzeug/3.1.3 Python/3.13.5
- status:
- code: 200
- message: OK
-version: 1
+interactions:
+- request:
+ body: null
+ headers:
+ Accept:
+ - '*/*'
+ Accept-Encoding:
+ - gzip, deflate
+ Connection:
+ - keep-alive
+ User-Agent:
+ - python-requests/2.31.0
+ method: GET
+ uri: http://127.0.0.1:5000/temperature
+ response:
+ body:
+ string: "Average temperature: 21.01 \xB0C (Good)\n"
+ headers:
+ Connection:
+ - close
+ Content-Length:
+ - '38'
+ Content-Type:
+ - text/html; charset=utf-8
+ Date:
+ - Wed, 25 Jun 2025 17:21:04 GMT
+ Server:
+ - Werkzeug/3.1.3 Python/3.13.5
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/tests/fixtures/vcr_cassettes/version.yaml b/tests/fixtures/vcr_cassettes/version.yaml
index 849ca34..7f67808 100644
--- a/tests/fixtures/vcr_cassettes/version.yaml
+++ b/tests/fixtures/vcr_cassettes/version.yaml
@@ -1,32 +1,32 @@
-interactions:
- - request:
- body: null
- headers:
- Accept:
- - "*/*"
- Accept-Encoding:
- - gzip, deflate
- Connection:
- - keep-alive
- User-Agent:
- - python-requests/2.31.0
- method: GET
- uri: http://127.0.0.1:5000/version
- response:
- body:
- string: "Current app version: 0.7.0"
- headers:
- Connection:
- - close
- Content-Length:
- - "27"
- Content-Type:
- - text/html; charset=utf-8
- Date:
- - Wed, 25 Jun 2025 17:19:51 GMT
- Server:
- - Werkzeug/3.1.3 Python/3.13.5
- status:
- code: 200
- message: OK
-version: 1
+interactions:
+ - request:
+ body: null
+ headers:
+ Accept:
+ - "*/*"
+ Accept-Encoding:
+ - gzip, deflate
+ Connection:
+ - keep-alive
+ User-Agent:
+ - python-requests/2.31.0
+ method: GET
+ uri: http://127.0.0.1:5000/version
+ response:
+ body:
+ string: "Current app version: 0.7.0"
+ headers:
+ Connection:
+ - close
+ Content-Length:
+ - "27"
+ Content-Type:
+ - text/html; charset=utf-8
+ Date:
+ - Wed, 25 Jun 2025 17:19:51 GMT
+ Server:
+ - Werkzeug/3.1.3 Python/3.13.5
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/tests/test_main.py b/tests/test_main.py
index ac133a7..aaf804a 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -1,86 +1,86 @@
-'''This module contains the test cases for the main module with VCR cassettes.'''
-import sys
-import re
-import os
-import requests
-import vcr
-
-API_HOST = os.environ.get('API_HOST', 'http://127.0.0.1:5000')
-
-my_vcr = vcr.VCR(
- cassette_library_dir='tests/fixtures/vcr_cassettes',
- record_mode='once',
- match_on=['uri', 'method'],
-)
-
-def make_request(url, expected_pattern):
- '''Reusable function to make requests and assert responses.'''
- try:
- response = requests.get(url, timeout=240)
- print(f"Response: {response.text}")
-
- # Check status code
- assert response.status_code == 200, "The request was not successful."
-
- # Check response content
- match = re.search(expected_pattern, response.text)
- assert match is not None, f"Pattern '{expected_pattern}' not found in response"
-
- return True, match
-
- except requests.exceptions.RequestException as e:
- print(f"❌ TEST FAILED: Network error: {str(e)}")
- return False, None
- except (AssertionError, TypeError, ValueError) as e:
- print(f"❌ TEST FAILED: {str(e)}")
- return False, None
-
-@my_vcr.use_cassette('version.yaml')
-def test_get_version():
- '''Function to test the get_version function.'''
- url = f"{API_HOST}/version"
- pattern = r"Current app version: (\d+\.\d+\.\d+)"
- version_success, match = make_request(url, pattern)
-
- if version_success and match:
- version = match.group(1)
- print(f"✅ Version test passed: {version}\n")
-
- return version_success
-
-@my_vcr.use_cassette('temperature.yaml')
-def test_get_temperature():
- '''Function to test the get_temperature function from the opensense module.'''
- url = f"{API_HOST}/temperature"
- pattern = r"Average temperature: (\d+\.\d+) °C"
- temperature_success, match = make_request(url, pattern)
-
- if temperature_success and match:
- temperature = float(match.group(1))
- print(f"✅ Temperature test passed: {temperature:.2f}°C\n")
-
- return temperature_success
-
-@my_vcr.use_cassette('metrics.yaml')
-def test_get_metrics():
- '''Function to test the metrics function.'''
- url = f"{API_HOST}/metrics"
- pattern = r"python_info"
- metrics_success, match = make_request(url, pattern)
-
- if metrics_success and match:
- print("✅ Metrics test passed: Prometheus metrics found\n")
-
- return metrics_success
-
-def run_all_tests():
- '''Run all tests and return overall success status.'''
- version_success = test_get_version()
- temperature_success = test_get_temperature()
- metrics_success = test_get_metrics()
-
- return version_success and temperature_success and metrics_success
-
-if __name__ == "__main__":
- SUCCESS = run_all_tests()
- sys.exit(0 if SUCCESS else 1)
+'''This module contains the test cases for the main module with VCR cassettes.'''
+import sys
+import re
+import os
+import requests
+import vcr
+
+API_HOST = os.environ.get('API_HOST', 'http://127.0.0.1:5000')
+
+my_vcr = vcr.VCR(
+ cassette_library_dir='tests/fixtures/vcr_cassettes',
+ record_mode='once',
+ match_on=['uri', 'method'],
+)
+
+def make_request(url, expected_pattern):
+ '''Reusable function to make requests and assert responses.'''
+ try:
+ response = requests.get(url, timeout=240)
+ print(f"Response: {response.text}")
+
+ # Check status code
+ assert response.status_code == 200, "The request was not successful."
+
+ # Check response content
+ match = re.search(expected_pattern, response.text)
+ assert match is not None, f"Pattern '{expected_pattern}' not found in response"
+
+ return True, match
+
+ except requests.exceptions.RequestException as e:
+ print(f"❌ TEST FAILED: Network error: {str(e)}")
+ return False, None
+ except (AssertionError, TypeError, ValueError) as e:
+ print(f"❌ TEST FAILED: {str(e)}")
+ return False, None
+
+@my_vcr.use_cassette('version.yaml')
+def test_get_version():
+ '''Function to test the get_version function.'''
+ url = f"{API_HOST}/version"
+ pattern = r"Current app version: (\d+\.\d+\.\d+)"
+ version_success, match = make_request(url, pattern)
+
+ if version_success and match:
+ version = match.group(1)
+ print(f"✅ Version test passed: {version}\n")
+
+ return version_success
+
+@my_vcr.use_cassette('temperature.yaml')
+def test_get_temperature():
+ '''Function to test the get_temperature function from the opensense module.'''
+ url = f"{API_HOST}/temperature"
+ pattern = r"Average temperature: (\d+\.\d+) °C"
+ temperature_success, match = make_request(url, pattern)
+
+ if temperature_success and match:
+ temperature = float(match.group(1))
+ print(f"✅ Temperature test passed: {temperature:.2f}°C\n")
+
+ return temperature_success
+
+@my_vcr.use_cassette('metrics.yaml')
+def test_get_metrics():
+ '''Function to test the metrics function.'''
+ url = f"{API_HOST}/metrics"
+ pattern = r"python_info"
+ metrics_success, match = make_request(url, pattern)
+
+ if metrics_success and match:
+ print("✅ Metrics test passed: Prometheus metrics found\n")
+
+ return metrics_success
+
+def run_all_tests():
+ '''Run all tests and return overall success status.'''
+ version_success = test_get_version()
+ temperature_success = test_get_temperature()
+ metrics_success = test_get_metrics()
+
+ return version_success and temperature_success and metrics_success
+
+if __name__ == "__main__":
+ SUCCESS = run_all_tests()
+ sys.exit(0 if SUCCESS else 1)
diff --git a/tests/test_modules.py b/tests/test_modules.py
index 9521b83..71332c6 100644
--- a/tests/test_modules.py
+++ b/tests/test_modules.py
@@ -1,404 +1,404 @@
-'''This module contains tests for the Flask and OpenSense modules.'''
-import re
-import unittest
-import unittest.mock as mock
-import requests # added
-import redis # added
-from minio.error import S3Error, InvalidResponseError
-from app.storage import store_temperature_data
-from app.main import app
-from app import opensense
-from app import readiness
-
-class TestFlaskApp(unittest.TestCase):
- """Test cases for Flask application endpoints"""
-
- def setUp(self):
- """Set up test client"""
- self.client = app.test_client()
- self.app_context = app.app_context()
- self.app_context.push()
-
- def tearDown(self):
- """Clean up after tests"""
- self.app_context.pop()
-
- def test_app_exists(self):
- """Test that the Flask app exists"""
- self.assertIsNotNone(app)
-
- def test_version_endpoint(self):
- """Test version endpoint returns 200"""
- response = self.client.get('/version')
- self.assertEqual(response.status_code, 200)
-
- def test_temperature_endpoint(self):
- """Test temperature endpoint returns 200 or 500"""
- with mock.patch('app.opensense.requests.get',
- return_value=MockOpenSenseResponse(20)):
- response = self.client.get('/temperature')
- self.assertIn(response.status_code, [200, 500])
-
- def test_metrics_endpoint(self):
- """Test metrics endpoint returns 200"""
- response = self.client.get('/metrics')
- self.assertEqual(response.status_code, 200)
-
- def test_store_endpoint_success(self):
- """Test store endpoint with successful storage"""
- with mock.patch('app.storage.store_temperature_data') as mock_store:
- mock_store.return_value = "Temperature data successfully uploaded"
-
- response = self.client.get('/store')
-
- self.assertEqual(response.status_code, 200)
- self.assertIn("successfully uploaded", response.get_data(as_text=True))
- mock_store.assert_called_once()
-
- def test_readyz_endpoint_ready(self):
- """Test /readyz endpoint when service is ready"""
- with mock.patch('app.readiness.readiness_check', return_value=200):
- response = self.client.get('/readyz')
- self.assertEqual(response.status_code, 200)
- data = response.get_json()
- self.assertEqual(data['status'], 'ready')
-
- def test_readyz_endpoint_not_ready(self):
- """Test /readyz endpoint when service is not ready"""
- with mock.patch('app.readiness.readiness_check', return_value=503):
- response = self.client.get('/readyz')
- self.assertEqual(response.status_code, 503)
- data = response.get_json()
- self.assertEqual(data['status'], 'not ready')
- self.assertIn('error', data)
-
-
-class MockOpenSenseResponse:
- """Mock response class to simulate OpenSenseMap API response."""
-
- def __init__(self, temp_value):
- self.text = "mock response text"
- self.temp_value = temp_value
-
- def json(self):
- """Return a mock JSON response."""
- return [{
- 'sensors': [
- {
- 'title': 'Temperatur',
- 'unit': '°C',
- 'lastMeasurement': {'value': str(self.temp_value)}
- }
- ]
- }]
-
-
-class TestOpenSense(unittest.TestCase):
- """Test cases for OpenSense module"""
-
- def test_get_temperature_returns_tuple(self):
- """Test that opensense.get_temperature returns a tuple with correct format"""
- with mock.patch('app.opensense.requests.get',
- return_value=MockOpenSenseResponse(20)):
- result, stats = opensense.get_temperature()
- self.assertIsInstance(result, str)
- self.assertIsInstance(stats, dict)
- self.assertIn('total_sensors', stats)
- self.assertIn('null_count', stats)
- self.assertIsNotNone(re.match(
- r"Average temperature: (\d+\.\d{2}) °C \((Warning: Too cold|Good|Warning: Too hot)\)",
- result
- ))
-
- def test_temperature_too_cold(self):
- """Test opensense.get_temperature for too cold condition (< 10°C)"""
- with mock.patch('app.opensense.requests.get',
- return_value=MockOpenSenseResponse(5)):
- result, _ = opensense.get_temperature()
- self.assertIn('Too cold', result)
-
- def test_temperature_good_range(self):
- """Test opensense.get_temperature for good temperature range (10-30°C)"""
- with mock.patch('app.opensense.requests.get',
- return_value=MockOpenSenseResponse(20)):
- result, _ = opensense.get_temperature()
- self.assertIn('Good', result)
-
- def test_temperature_too_hot(self):
- """Test opensense.get_temperature for too hot condition (> 30°C)"""
- with mock.patch('app.opensense.requests.get',
- return_value=MockOpenSenseResponse(40)):
- result, _ = opensense.get_temperature()
- self.assertIn('Too hot', result)
-
- def test_cache_hit(self):
- """Test that cached data is returned when available"""
- mock_redis_client = mock.MagicMock()
- mock_redis_client.get.return_value = "cached_result"
-
- with mock.patch('app.opensense.REDIS_AVAILABLE', True), \
- mock.patch('app.opensense.redis_client', mock_redis_client), \
- mock.patch('app.opensense.requests.get') as mock_requests:
-
- result, _ = opensense.get_temperature()
- self.assertEqual(result, "cached_result")
- mock_requests.assert_not_called()
- mock_redis_client.get.assert_called_once_with("temperature_data")
-
- def test_cache_miss_and_store(self):
- """Test that data is fetched and cached on cache miss"""
- mock_redis_client = mock.MagicMock()
- mock_redis_client.get.return_value = None
-
- with mock.patch('app.opensense.REDIS_AVAILABLE', True), \
- mock.patch('app.opensense.redis_client', mock_redis_client), \
- mock.patch('app.opensense.requests.get',
- return_value=MockOpenSenseResponse(25)):
-
- result, _ = opensense.get_temperature()
- self.assertIn('Average temperature', result)
- mock_redis_client.setex.assert_called()
- # Verify cache key and TTL
- call_args = mock_redis_client.setex.call_args
- self.assertEqual(call_args[0][0], "temperature_data")
- self.assertGreater(call_args[0][1], 0) # TTL should be positive
-
-
-class TestStorage(unittest.TestCase):
- """Test cases for storage functionality"""
-
- def setUp(self):
- """Set up common test data"""
- self.mock_temp_data = (
- "Average temperature: 22.5 °C (Good)\nFrom: test\n",
- {"total_sensors": 10, "null_count": 1}
- )
-
- def test_store_temperature_data_success(self):
- """Test successful temperature data storage"""
- with mock.patch('app.storage.Minio') as mock_minio_class:
- mock_client = mock.MagicMock()
- mock_minio_class.return_value = mock_client
- mock_client.bucket_exists.return_value = True
- mock_client.list_buckets.return_value = []
-
- with mock.patch('app.storage.opensense.get_temperature',
- return_value=self.mock_temp_data):
- result = store_temperature_data()
-
- self.assertIn("successfully uploaded", result)
- mock_client.put_object.assert_called_once()
-
- def test_store_temperature_data_create_bucket(self):
- """Test bucket creation when it doesn't exist"""
- with mock.patch('app.storage.Minio') as mock_minio_class:
- mock_client = mock.MagicMock()
- mock_minio_class.return_value = mock_client
- mock_client.bucket_exists.return_value = False
- mock_client.list_buckets.return_value = []
-
- with mock.patch('app.storage.opensense.get_temperature',
- return_value=self.mock_temp_data):
- result = store_temperature_data()
-
- mock_client.make_bucket.assert_called_once_with("temperature-data")
- self.assertIn("successfully uploaded", result)
-
- def test_store_temperature_data_s3_error(self):
- """Test S3Error exception handling"""
- with mock.patch('app.storage.Minio') as mock_minio_class:
- mock_client = mock.MagicMock()
- mock_minio_class.return_value = mock_client
- mock_client.list_buckets.return_value = []
- mock_client.bucket_exists.side_effect = S3Error(
- code="AccessDenied",
- message="Access denied",
- resource="/temperature-data",
- request_id="test-request-id",
- host_id="test-host-id",
- response=None
- )
-
- with mock.patch('app.storage.opensense.get_temperature',
- return_value=self.mock_temp_data):
- result = store_temperature_data()
-
- self.assertIn("MinIO S3 error occurred", result)
- self.assertIn("Access denied", result)
-
- def test_store_temperature_data_invalid_response(self):
- """Test InvalidResponseError exception handling"""
- with mock.patch('app.storage.Minio') as mock_minio_class:
- mock_client = mock.MagicMock()
- mock_minio_class.return_value = mock_client
- mock_client.list_buckets.return_value = []
- mock_client.bucket_exists.return_value = True
- mock_client.put_object.side_effect = InvalidResponseError(
- "Invalid response",
- content_type="application/json",
- body=b"{}"
- )
-
- with mock.patch('app.storage.opensense.get_temperature',
- return_value=self.mock_temp_data):
- result = store_temperature_data()
-
- self.assertIn("MinIO S3 error occurred", result)
- self.assertIn("Invalid response", result)
-
- def test_store_temperature_data_connection_error(self):
- """Test connection error handling"""
- with mock.patch('app.storage.Minio') as mock_minio_class:
- mock_client = mock.MagicMock()
- mock_minio_class.return_value = mock_client
- mock_client.list_buckets.side_effect = ConnectionError("Network unreachable")
- with mock.patch('app.storage.opensense.get_temperature',
- return_value=self.mock_temp_data):
- result = store_temperature_data()
-
- self.assertIn("Cannot connect to MinIO server", result)
- self.assertIn("Network unreachable", result)
-
-
-class TestReadiness(unittest.TestCase):
- """Test cases for readiness checks"""
-
- def test_check_caching_redis_unavailable(self):
- """Test check_caching when Redis is not available"""
- with mock.patch('app.readiness.REDIS_AVAILABLE', False):
- result = readiness.check_caching()
- self.assertTrue(result) # No Redis = cache is old
-
- def test_check_caching_key_not_exists(self):
- """Test check_caching when cache key doesn't exist"""
- mock_redis_client = mock.MagicMock()
- mock_redis_client.ttl.return_value = -2 # Key doesn't exist
-
- with mock.patch('app.readiness.REDIS_AVAILABLE', True), \
- mock.patch('app.readiness.redis_client', mock_redis_client):
- result = readiness.check_caching()
- self.assertTrue(result)
-
- def test_check_caching_key_no_expiry(self):
- """Test check_caching when cache key has no expiry"""
- mock_redis_client = mock.MagicMock()
- mock_redis_client.ttl.return_value = -1 # Key exists but no expiry
-
- with mock.patch('app.readiness.REDIS_AVAILABLE', True), \
- mock.patch('app.readiness.redis_client', mock_redis_client):
- result = readiness.check_caching()
- self.assertTrue(result)
-
- def test_check_caching_fresh_cache(self):
- """Test check_caching when cache is fresh"""
- mock_redis_client = mock.MagicMock()
- mock_redis_client.ttl.return_value = 150 # 2.5 minutes remaining
-
- with mock.patch('app.readiness.REDIS_AVAILABLE', True), \
- mock.patch('app.readiness.redis_client', mock_redis_client):
- result = readiness.check_caching()
- self.assertFalse(result) # Cache is fresh
-
- def test_check_caching_redis_error(self):
- """TTL raises RedisError -> treated as old cache (True)"""
- mock_redis_client = mock.MagicMock()
- mock_redis_client.ttl.side_effect = redis.RedisError("boom")
-
- with mock.patch('app.readiness.REDIS_AVAILABLE', True), \
- mock.patch('app.readiness.redis_client', mock_redis_client), \
- mock.patch('builtins.print'):
- result = readiness.check_caching()
- self.assertTrue(result)
-
- def test_reachable_boxes_healthy(self):
- """Test reachable_boxes when most sensors are working"""
- mock_stats = {"total_sensors": 100, "null_count": 10}
-
- with mock.patch('app.readiness.get_temperature',
- return_value=("temp_result", mock_stats)):
- result = readiness.reachable_boxes()
- self.assertEqual(result, 200)
-
- def test_reachable_boxes_unhealthy(self):
- """Test reachable_boxes when > 50% sensors are unreachable"""
- mock_stats = {"total_sensors": 100, "null_count": 51}
-
- with mock.patch('app.readiness.get_temperature',
- return_value=("temp_result", mock_stats)), \
- mock.patch('builtins.print'): # Suppress print output
- result = readiness.reachable_boxes()
- self.assertEqual(result, 400)
-
- def test_reachable_boxes_edge_cases(self):
- """Test reachable_boxes edge cases"""
- # No sensors
- with mock.patch('app.readiness.get_temperature',
- return_value=("temp", {"total_sensors": 0, "null_count": 0})):
- self.assertEqual(readiness.reachable_boxes(), 200)
-
- # Exactly 50% unreachable (should be OK)
- with mock.patch('app.readiness.get_temperature',
- return_value=("temp", {"total_sensors": 100, "null_count": 50})):
- self.assertEqual(readiness.reachable_boxes(), 200)
-
- def test_reachable_boxes_network_error(self):
- """requests exceptions -> treated as healthy (200)"""
- with mock.patch('app.readiness.get_temperature',
- side_effect=requests.exceptions.RequestException("net")), \
- mock.patch('builtins.print'):
- self.assertEqual(readiness.reachable_boxes(), 200)
-
- def test_reachable_boxes_redis_error(self):
- """Redis errors inside get_temperature -> treated as healthy (200)"""
- with mock.patch('app.readiness.get_temperature',
- side_effect=redis.RedisError("redis down")), \
- mock.patch('builtins.print'):
- self.assertEqual(readiness.reachable_boxes(), 200)
-
- def test_reachable_boxes_data_error(self):
- """Data parsing error -> returns 400"""
- # Cause a TypeError during percentage calculation
- bad_stats = {"total_sensors": 2, "null_count": "x"}
- with mock.patch('app.readiness.get_temperature',
- return_value=("temp", bad_stats)), \
- mock.patch('builtins.print'):
- self.assertEqual(readiness.reachable_boxes(), 400)
-
- def test_readiness_check_redis_error_top_level(self):
- """Top-level Redis error in readiness_check -> returns 200"""
- with mock.patch('app.readiness.check_caching',
- side_effect=redis.RedisError("ttl failed")), \
- mock.patch('builtins.print'):
- self.assertEqual(readiness.readiness_check(), 200)
-
- def test_readiness_check_all_good(self):
- """Test readiness_check when everything is healthy"""
- with mock.patch('app.readiness.check_caching', return_value=False), \
- mock.patch('app.readiness.reachable_boxes', return_value=200):
- result = readiness.readiness_check()
- self.assertEqual(result, 200)
-
- def test_readiness_check_both_bad(self):
- """Test readiness_check when both checks fail"""
- with mock.patch('app.readiness.check_caching', return_value=True), \
- mock.patch('app.readiness.reachable_boxes', return_value=400):
- result = readiness.readiness_check()
- self.assertEqual(result, 503)
-
- def test_readiness_check_partial_failure(self):
- """Test readiness_check when only one check fails"""
- # Only cache is old
- with mock.patch('app.readiness.check_caching', return_value=True), \
- mock.patch('app.readiness.reachable_boxes', return_value=200):
- result = readiness.readiness_check()
- self.assertEqual(result, 200)
-
- # Only sensors are unreachable
- with mock.patch('app.readiness.check_caching', return_value=False), \
- mock.patch('app.readiness.reachable_boxes', return_value=400):
- result = readiness.readiness_check()
- self.assertEqual(result, 200)
-
-
-if __name__ == '__main__':
- unittest.main()
+'''This module contains tests for the Flask and OpenSense modules.'''
+import re
+import unittest
+import unittest.mock as mock
+import requests # added
+import redis # added
+from minio.error import S3Error, InvalidResponseError
+from app.storage import store_temperature_data
+from app.main import app
+from app import opensense
+from app import readiness
+
+class TestFlaskApp(unittest.TestCase):
+ """Test cases for Flask application endpoints"""
+
+ def setUp(self):
+ """Set up test client"""
+ self.client = app.test_client()
+ self.app_context = app.app_context()
+ self.app_context.push()
+
+ def tearDown(self):
+ """Clean up after tests"""
+ self.app_context.pop()
+
+ def test_app_exists(self):
+ """Test that the Flask app exists"""
+ self.assertIsNotNone(app)
+
+ def test_version_endpoint(self):
+ """Test version endpoint returns 200"""
+ response = self.client.get('/version')
+ self.assertEqual(response.status_code, 200)
+
+ def test_temperature_endpoint(self):
+ """Test temperature endpoint returns 200 or 500"""
+ with mock.patch('app.opensense.requests.get',
+ return_value=MockOpenSenseResponse(20)):
+ response = self.client.get('/temperature')
+ self.assertIn(response.status_code, [200, 500])
+
+ def test_metrics_endpoint(self):
+ """Test metrics endpoint returns 200"""
+ response = self.client.get('/metrics')
+ self.assertEqual(response.status_code, 200)
+
+ def test_store_endpoint_success(self):
+ """Test store endpoint with successful storage"""
+ with mock.patch('app.storage.store_temperature_data') as mock_store:
+ mock_store.return_value = "Temperature data successfully uploaded"
+
+ response = self.client.get('/store')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("successfully uploaded", response.get_data(as_text=True))
+ mock_store.assert_called_once()
+
+ def test_readyz_endpoint_ready(self):
+ """Test /readyz endpoint when service is ready"""
+ with mock.patch('app.readiness.readiness_check', return_value=200):
+ response = self.client.get('/readyz')
+ self.assertEqual(response.status_code, 200)
+ data = response.get_json()
+ self.assertEqual(data['status'], 'ready')
+
+ def test_readyz_endpoint_not_ready(self):
+ """Test /readyz endpoint when service is not ready"""
+ with mock.patch('app.readiness.readiness_check', return_value=503):
+ response = self.client.get('/readyz')
+ self.assertEqual(response.status_code, 503)
+ data = response.get_json()
+ self.assertEqual(data['status'], 'not ready')
+ self.assertIn('error', data)
+
+
+class MockOpenSenseResponse:
+ """Mock response class to simulate OpenSenseMap API response."""
+
+ def __init__(self, temp_value):
+ self.text = "mock response text"
+ self.temp_value = temp_value
+
+ def json(self):
+ """Return a mock JSON response."""
+ return [{
+ 'sensors': [
+ {
+ 'title': 'Temperatur',
+ 'unit': '°C',
+ 'lastMeasurement': {'value': str(self.temp_value)}
+ }
+ ]
+ }]
+
+
+class TestOpenSense(unittest.TestCase):
+ """Test cases for OpenSense module"""
+
+ def test_get_temperature_returns_tuple(self):
+ """Test that opensense.get_temperature returns a tuple with correct format"""
+ with mock.patch('app.opensense.requests.get',
+ return_value=MockOpenSenseResponse(20)):
+ result, stats = opensense.get_temperature()
+ self.assertIsInstance(result, str)
+ self.assertIsInstance(stats, dict)
+ self.assertIn('total_sensors', stats)
+ self.assertIn('null_count', stats)
+ self.assertIsNotNone(re.match(
+ r"Average temperature: (\d+\.\d{2}) °C \((Warning: Too cold|Good|Warning: Too hot)\)",
+ result
+ ))
+
+ def test_temperature_too_cold(self):
+ """Test opensense.get_temperature for too cold condition (< 10°C)"""
+ with mock.patch('app.opensense.requests.get',
+ return_value=MockOpenSenseResponse(5)):
+ result, _ = opensense.get_temperature()
+ self.assertIn('Too cold', result)
+
+ def test_temperature_good_range(self):
+ """Test opensense.get_temperature for good temperature range (10-30°C)"""
+ with mock.patch('app.opensense.requests.get',
+ return_value=MockOpenSenseResponse(20)):
+ result, _ = opensense.get_temperature()
+ self.assertIn('Good', result)
+
+ def test_temperature_too_hot(self):
+ """Test opensense.get_temperature for too hot condition (> 30°C)"""
+ with mock.patch('app.opensense.requests.get',
+ return_value=MockOpenSenseResponse(40)):
+ result, _ = opensense.get_temperature()
+ self.assertIn('Too hot', result)
+
+ def test_cache_hit(self):
+ """Test that cached data is returned when available"""
+ mock_redis_client = mock.MagicMock()
+ mock_redis_client.get.return_value = "cached_result"
+
+ with mock.patch('app.opensense.REDIS_AVAILABLE', True), \
+ mock.patch('app.opensense.redis_client', mock_redis_client), \
+ mock.patch('app.opensense.requests.get') as mock_requests:
+
+ result, _ = opensense.get_temperature()
+ self.assertEqual(result, "cached_result")
+ mock_requests.assert_not_called()
+ mock_redis_client.get.assert_called_once_with("temperature_data")
+
+ def test_cache_miss_and_store(self):
+ """Test that data is fetched and cached on cache miss"""
+ mock_redis_client = mock.MagicMock()
+ mock_redis_client.get.return_value = None
+
+ with mock.patch('app.opensense.REDIS_AVAILABLE', True), \
+ mock.patch('app.opensense.redis_client', mock_redis_client), \
+ mock.patch('app.opensense.requests.get',
+ return_value=MockOpenSenseResponse(25)):
+
+ result, _ = opensense.get_temperature()
+ self.assertIn('Average temperature', result)
+ mock_redis_client.setex.assert_called()
+ # Verify cache key and TTL
+ call_args = mock_redis_client.setex.call_args
+ self.assertEqual(call_args[0][0], "temperature_data")
+ self.assertGreater(call_args[0][1], 0) # TTL should be positive
+
+
+class TestStorage(unittest.TestCase):
+ """Test cases for storage functionality"""
+
+ def setUp(self):
+ """Set up common test data"""
+ self.mock_temp_data = (
+ "Average temperature: 22.5 °C (Good)\nFrom: test\n",
+ {"total_sensors": 10, "null_count": 1}
+ )
+
+ def test_store_temperature_data_success(self):
+ """Test successful temperature data storage"""
+ with mock.patch('app.storage.Minio') as mock_minio_class:
+ mock_client = mock.MagicMock()
+ mock_minio_class.return_value = mock_client
+ mock_client.bucket_exists.return_value = True
+ mock_client.list_buckets.return_value = []
+
+ with mock.patch('app.storage.opensense.get_temperature',
+ return_value=self.mock_temp_data):
+ result = store_temperature_data()
+
+ self.assertIn("successfully uploaded", result)
+ mock_client.put_object.assert_called_once()
+
+ def test_store_temperature_data_create_bucket(self):
+ """Test bucket creation when it doesn't exist"""
+ with mock.patch('app.storage.Minio') as mock_minio_class:
+ mock_client = mock.MagicMock()
+ mock_minio_class.return_value = mock_client
+ mock_client.bucket_exists.return_value = False
+ mock_client.list_buckets.return_value = []
+
+ with mock.patch('app.storage.opensense.get_temperature',
+ return_value=self.mock_temp_data):
+ result = store_temperature_data()
+
+ mock_client.make_bucket.assert_called_once_with("temperature-data")
+ self.assertIn("successfully uploaded", result)
+
+ def test_store_temperature_data_s3_error(self):
+ """Test S3Error exception handling"""
+ with mock.patch('app.storage.Minio') as mock_minio_class:
+ mock_client = mock.MagicMock()
+ mock_minio_class.return_value = mock_client
+ mock_client.list_buckets.return_value = []
+ mock_client.bucket_exists.side_effect = S3Error(
+ code="AccessDenied",
+ message="Access denied",
+ resource="/temperature-data",
+ request_id="test-request-id",
+ host_id="test-host-id",
+ response=None
+ )
+
+ with mock.patch('app.storage.opensense.get_temperature',
+ return_value=self.mock_temp_data):
+ result = store_temperature_data()
+
+ self.assertIn("MinIO S3 error occurred", result)
+ self.assertIn("Access denied", result)
+
+ def test_store_temperature_data_invalid_response(self):
+ """Test InvalidResponseError exception handling"""
+ with mock.patch('app.storage.Minio') as mock_minio_class:
+ mock_client = mock.MagicMock()
+ mock_minio_class.return_value = mock_client
+ mock_client.list_buckets.return_value = []
+ mock_client.bucket_exists.return_value = True
+ mock_client.put_object.side_effect = InvalidResponseError(
+ "Invalid response",
+ content_type="application/json",
+ body=b"{}"
+ )
+
+ with mock.patch('app.storage.opensense.get_temperature',
+ return_value=self.mock_temp_data):
+ result = store_temperature_data()
+
+ self.assertIn("MinIO S3 error occurred", result)
+ self.assertIn("Invalid response", result)
+
+ def test_store_temperature_data_connection_error(self):
+ """Test connection error handling"""
+ with mock.patch('app.storage.Minio') as mock_minio_class:
+ mock_client = mock.MagicMock()
+ mock_minio_class.return_value = mock_client
+ mock_client.list_buckets.side_effect = ConnectionError("Network unreachable")
+ with mock.patch('app.storage.opensense.get_temperature',
+ return_value=self.mock_temp_data):
+ result = store_temperature_data()
+
+ self.assertIn("Cannot connect to MinIO server", result)
+ self.assertIn("Network unreachable", result)
+
+
+class TestReadiness(unittest.TestCase):
+ """Test cases for readiness checks"""
+
+ def test_check_caching_redis_unavailable(self):
+ """Test check_caching when Redis is not available"""
+ with mock.patch('app.readiness.REDIS_AVAILABLE', False):
+ result = readiness.check_caching()
+ self.assertTrue(result) # No Redis = cache is old
+
+ def test_check_caching_key_not_exists(self):
+ """Test check_caching when cache key doesn't exist"""
+ mock_redis_client = mock.MagicMock()
+ mock_redis_client.ttl.return_value = -2 # Key doesn't exist
+
+ with mock.patch('app.readiness.REDIS_AVAILABLE', True), \
+ mock.patch('app.readiness.redis_client', mock_redis_client):
+ result = readiness.check_caching()
+ self.assertTrue(result)
+
+ def test_check_caching_key_no_expiry(self):
+ """Test check_caching when cache key has no expiry"""
+ mock_redis_client = mock.MagicMock()
+ mock_redis_client.ttl.return_value = -1 # Key exists but no expiry
+
+ with mock.patch('app.readiness.REDIS_AVAILABLE', True), \
+ mock.patch('app.readiness.redis_client', mock_redis_client):
+ result = readiness.check_caching()
+ self.assertTrue(result)
+
+ def test_check_caching_fresh_cache(self):
+ """Test check_caching when cache is fresh"""
+ mock_redis_client = mock.MagicMock()
+ mock_redis_client.ttl.return_value = 150 # 2.5 minutes remaining
+
+ with mock.patch('app.readiness.REDIS_AVAILABLE', True), \
+ mock.patch('app.readiness.redis_client', mock_redis_client):
+ result = readiness.check_caching()
+ self.assertFalse(result) # Cache is fresh
+
+ def test_check_caching_redis_error(self):
+ """TTL raises RedisError -> treated as old cache (True)"""
+ mock_redis_client = mock.MagicMock()
+ mock_redis_client.ttl.side_effect = redis.RedisError("boom")
+
+ with mock.patch('app.readiness.REDIS_AVAILABLE', True), \
+ mock.patch('app.readiness.redis_client', mock_redis_client), \
+ mock.patch('builtins.print'):
+ result = readiness.check_caching()
+ self.assertTrue(result)
+
+ def test_reachable_boxes_healthy(self):
+ """Test reachable_boxes when most sensors are working"""
+ mock_stats = {"total_sensors": 100, "null_count": 10}
+
+ with mock.patch('app.readiness.get_temperature',
+ return_value=("temp_result", mock_stats)):
+ result = readiness.reachable_boxes()
+ self.assertEqual(result, 200)
+
+ def test_reachable_boxes_unhealthy(self):
+ """Test reachable_boxes when > 50% sensors are unreachable"""
+ mock_stats = {"total_sensors": 100, "null_count": 51}
+
+ with mock.patch('app.readiness.get_temperature',
+ return_value=("temp_result", mock_stats)), \
+ mock.patch('builtins.print'): # Suppress print output
+ result = readiness.reachable_boxes()
+ self.assertEqual(result, 400)
+
+ def test_reachable_boxes_edge_cases(self):
+ """Test reachable_boxes edge cases"""
+ # No sensors
+ with mock.patch('app.readiness.get_temperature',
+ return_value=("temp", {"total_sensors": 0, "null_count": 0})):
+ self.assertEqual(readiness.reachable_boxes(), 200)
+
+ # Exactly 50% unreachable (should be OK)
+ with mock.patch('app.readiness.get_temperature',
+ return_value=("temp", {"total_sensors": 100, "null_count": 50})):
+ self.assertEqual(readiness.reachable_boxes(), 200)
+
+ def test_reachable_boxes_network_error(self):
+ """requests exceptions -> treated as healthy (200)"""
+ with mock.patch('app.readiness.get_temperature',
+ side_effect=requests.exceptions.RequestException("net")), \
+ mock.patch('builtins.print'):
+ self.assertEqual(readiness.reachable_boxes(), 200)
+
+ def test_reachable_boxes_redis_error(self):
+ """Redis errors inside get_temperature -> treated as healthy (200)"""
+ with mock.patch('app.readiness.get_temperature',
+ side_effect=redis.RedisError("redis down")), \
+ mock.patch('builtins.print'):
+ self.assertEqual(readiness.reachable_boxes(), 200)
+
+ def test_reachable_boxes_data_error(self):
+ """Data parsing error -> returns 400"""
+ # Cause a TypeError during percentage calculation
+ bad_stats = {"total_sensors": 2, "null_count": "x"}
+ with mock.patch('app.readiness.get_temperature',
+ return_value=("temp", bad_stats)), \
+ mock.patch('builtins.print'):
+ self.assertEqual(readiness.reachable_boxes(), 400)
+
+ def test_readiness_check_redis_error_top_level(self):
+ """Top-level Redis error in readiness_check -> returns 200"""
+ with mock.patch('app.readiness.check_caching',
+ side_effect=redis.RedisError("ttl failed")), \
+ mock.patch('builtins.print'):
+ self.assertEqual(readiness.readiness_check(), 200)
+
+ def test_readiness_check_all_good(self):
+ """Test readiness_check when everything is healthy"""
+ with mock.patch('app.readiness.check_caching', return_value=False), \
+ mock.patch('app.readiness.reachable_boxes', return_value=200):
+ result = readiness.readiness_check()
+ self.assertEqual(result, 200)
+
+ def test_readiness_check_both_bad(self):
+ """Test readiness_check when both checks fail"""
+ with mock.patch('app.readiness.check_caching', return_value=True), \
+ mock.patch('app.readiness.reachable_boxes', return_value=400):
+ result = readiness.readiness_check()
+ self.assertEqual(result, 503)
+
+ def test_readiness_check_partial_failure(self):
+ """Test readiness_check when only one check fails"""
+ # Only cache is old
+ with mock.patch('app.readiness.check_caching', return_value=True), \
+ mock.patch('app.readiness.reachable_boxes', return_value=200):
+ result = readiness.readiness_check()
+ self.assertEqual(result, 200)
+
+ # Only sensors are unreachable
+ with mock.patch('app.readiness.check_caching', return_value=False), \
+ mock.patch('app.readiness.reachable_boxes', return_value=400):
+ result = readiness.readiness_check()
+ self.assertEqual(result, 200)
+
+
+if __name__ == '__main__':
+ unittest.main()