diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index b9f5576..1ec9e50 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -15,21 +15,6 @@ jobs: name: Run Django Tests runs-on: ubuntu-latest - services: - postgres: - image: postgres:13 - env: - POSTGRES_DB: store - POSTGRES_USER: postgres - POSTGRES_PASSWORD: Admin - ports: - - 5432:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - steps: - name: Checkout Repository uses: actions/checkout@v3 @@ -50,7 +35,6 @@ jobs: echo "AT_USERNAME=$(echo '${{ secrets.AT_USERNAME }}' | base64 -d)" >> $GITHUB_ENV echo "AT_SHORT_CODE=$(echo '${{ secrets.AT_SHORT_CODE }}' | base64 -d)" >> $GITHUB_ENV echo "AT_API_KEY=$(echo '${{ secrets.AT_API_KEY }}' | base64 -d)" >> $GITHUB_ENV - - name: Run Tests env: @@ -68,7 +52,7 @@ jobs: [ -z "$DB_USER" ] && echo "DB_USER is not set" || echo "DB_USER is set" [ -z "$DB_HOST" ] && echo "DB_HOST is not set" || echo "DB_HOST is set" [ -z "$DB_PORT" ] && echo "DB_PORT is not set" || echo "DB_PORT is set" - pytest -v + pytest -v build-and-push: name: Build and Push Docker Image @@ -86,8 +70,8 @@ jobs: - name: Build and Push Docker Image run: | - docker build -t ${{ secrets.DOCKER_USERNAME }}/ecommerce-app:latest . - docker push ${{ secrets.DOCKER_USERNAME }}/ecommerce-app:latest + docker build -t ${{ secrets.DOCKER_USERNAME }}/e-commerce-service:latest . + docker push ${{ secrets.DOCKER_USERNAME }}/e-commerce-service:latest deploy: name: Deploy to Kubernetes @@ -103,8 +87,6 @@ jobs: with: version: 'latest' - - - name: Validate KUBECONFIG Secret run: | if [ -z "${{ secrets.KUBECONFIG }}" ]; then @@ -114,62 +96,17 @@ jobs: - name: Configure Kubeconfig run: | - echo "${{ secrets.KUBECONFIG }}" | base64 -d > kubeconfig.yaml || { echo "Error decoding kubeconfig"; exit 1; } + echo "${{ secrets.KUBECONFIG }}" | base64 -d > kubeconfig.yaml export KUBECONFIG=$(pwd)/kubeconfig.yaml echo "KUBECONFIG=$(pwd)/kubeconfig.yaml" >> $GITHUB_ENV - if [ ! -f kubeconfig.yaml ]; then - echo "Error: kubeconfig.yaml not created!" - exit 1 - fi - - name: Start Minikube - run: | - echo "Checking if Minikube is running..." - if ! minikube status; then - echo "Starting Minikube..." - minikube start --wait=all --driver=docker - echo "Minikube started successfully." - else - echo "Minikube is already running." - fi - - - name: Configure Minikube Without Certificate Check + - name: Deploy Postgres to Kubernetes run: | - kubectl config set-cluster minikube \ - --server=https://192.168.49.2:8443 \ - --insecure-skip-tls-verify=true - kubectl config set-context minikube --cluster=minikube --user=minikube - kubectl config use-context minikube - + kubectl apply -f k8s/postgres.yaml + kubectl rollout status statefulset/postgres --timeout=120s - - name: Test Minikube Connection - run: | - minikube status - minikube ip - kubectl cluster-info - minikube logs - kubectl get nodes - - - name: Clean Up Existing Deployment - run: | - kubectl scale deployment ecommerce-app --replicas=0 || true - kubectl wait --for=delete pod -l app=ecommerce-app --timeout=30s || true - - kubectl delete deployment ecommerce-app --ignore-not-found=true - kubectl wait --for=delete deployment/ecommerce-app --timeout=60s || true - kubectl delete pods -l app=ecommerce-app --force --grace-period=0 || true - - echo "Verifying all pods are gone..." - kubectl get pods -l app=ecommerce-app - - - name: Recreate Deployment + - name: Deploy Application to Kubernetes run: | kubectl apply -f k8s/deployment.yaml - kubectl set image deployment/ecommerce-app ecommerce-app=${{ secrets.DOCKER_USERNAME }}/ecommerce-app:latest - - kubectl rollout status deployment/ecommerce-app - echo "Checking deployment status..." - kubectl describe deployment ecommerce-app - - echo "Checking pod status..." - kubectl get pods -l app=ecommerce-app -o wide \ No newline at end of file + kubectl set image deployment/ecommerce-app ecommerce-app=${{ secrets.DOCKER_USERNAME }}/e-commerce-service:latest + kubectl rollout status deployment/ecommerce-app --timeout=120s diff --git a/Dockerfile b/Dockerfile index 9d478ba..019155f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,10 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh EXPOSE 8000 +ENTRYPOINT ["/entrypoint.sh"] CMD ["gunicorn", "--bind", "0.0.0.0:8000", "src.wsgi:application"] diff --git a/api/urls.py b/api/urls.py index 0b9476e..51d1ddf 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,6 +1,6 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .views import OrderViewSet, ProductViewSet, CategoryViewSet,CustomerViewSet +from .views import OrderViewSet, ProductViewSet, CategoryViewSet,CustomerViewSet, health_check router = DefaultRouter() router.register(r'products', ProductViewSet, basename='products') @@ -8,8 +8,10 @@ router.register(r'categories', CategoryViewSet, basename='categories') router.register(r'customers',CustomerViewSet, basename='customers') + app_name = 'api' urlpatterns = [ path('', include(router.urls)), + path('health/', health_check, name='health_check'), ] \ No newline at end of file diff --git a/api/views.py b/api/views.py index fa4f685..ba400fc 100644 --- a/api/views.py +++ b/api/views.py @@ -1,9 +1,12 @@ +from django.db import connections +from django.http import JsonResponse from django.shortcuts import render from rest_framework.decorators import action from rest_framework import viewsets, status from rest_framework.permissions import IsAuthenticated,AllowAny from rest_framework.response import Response from django.db.models import Avg +from django.db.utils import OperationalError from api.auth import Auth0Authentication from api.models.accounts import User @@ -74,4 +77,35 @@ def perform_create(self, serializer): class CategoryViewSet(viewsets.ModelViewSet): queryset = Category.objects.all() - serializer_class = CategorySerializer \ No newline at end of file + serializer_class = CategorySerializer + + +class HealthCheckView(viewsets.ViewSet): + """ + Health check endpoint to verify application and database connectivity + """ + def get(self, request): + try: + connections['default'].cursor() + except OperationalError: + return JsonResponse({'status': 'error', 'message': 'Database connection failed'}, status=500) + + return JsonResponse({ + 'status': 'healthy', + 'message': 'Application is running and database is accessible' + }) + +#convert this to class +def health_check(request): + """ + Health check endpoint to verify application and database connectivity + """ + try: + connections['default'].cursor() + except OperationalError: + return JsonResponse({'status': 'error', 'message': 'Database connection failed'}, status=500) + + return JsonResponse({ + 'status': 'healthy', + 'message': 'Application is running and database is accessible' + }) \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..4448ab9 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +echo "Waiting for database to be ready..." +while ! nc -z "$DATABASE_HOST" "$DATABASE_PORT"; do + sleep 2 +done + +echo "Database is up, starting the application..." +exec "$@" diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index 2e1f741..65868a2 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -14,9 +14,14 @@ spec: labels: app: ecommerce-app spec: + initContainers: + - name: wait-for-db + image: busybox + command: ['sh', '-c', 'until nc -z postgres-service 5432; do echo waiting for db; sleep 2; done;'] containers: - name: ecommerce-app - image: jwiki/e-commerce-service:latest + image: ${{ secrets.DOCKER_USERNAME }}/e-commerce-service:latest + imagePullPolicy: Always ports: - containerPort: 8000 envFrom: @@ -33,13 +38,16 @@ spec: cpu: "500m" readinessProbe: httpGet: - path: / + path: /api/v1/health/ port: 8000 - initialDelaySeconds: 10 - periodSeconds: 3 + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 3 + timeoutSeconds: 5 livenessProbe: httpGet: - path: / + path: /api/v1/health/ port: 8000 - initialDelaySeconds: 15 - periodSeconds: 20 + initialDelaySeconds: 45 + periodSeconds: 20 + failureThreshold: 5 \ No newline at end of file diff --git a/postgres.yaml b/k8s/postgres.yaml similarity index 78% rename from postgres.yaml rename to k8s/postgres.yaml index 7561e9e..9860daf 100644 --- a/postgres.yaml +++ b/k8s/postgres.yaml @@ -26,7 +26,7 @@ spec: spec: containers: - name: postgres - image: postgres:13 + image: postgres:14 ports: - containerPort: 5432 envFrom: @@ -35,6 +35,13 @@ spec: volumeMounts: - mountPath: /var/lib/postgresql/data name: postgres-storage + readinessProbe: + exec: + command: ["pg_isready", "-U", "postgres"] + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 volumes: - name: postgres-storage persistentVolumeClaim: diff --git a/src/settings.py b/src/settings.py index 3fb8417..7807e8b 100644 --- a/src/settings.py +++ b/src/settings.py @@ -29,7 +29,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['*'] # Application definition