diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..55c0b92 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,73 @@ +# Continuous Deployment Workflow +# This workflow deploys your application to Kubernetes cluster +# Trigger: After successful CI build or manual dispatch + +name: CD - Deploy to Kubernetes + +on: + workflow_run: + workflows: ["CI - Build and Push"] + types: + - completed + branches: + - master + workflow_dispatch: + +env: + # Kubernetes configuration + KUBECONFIG_DATA: ${{ secrets.KUBECONFIG_DATA }} + KUBERNETES_URL: ${{ secrets.KUBERNETES_URL }} + KUBERNETES_NAMESPACE: ${{ secrets.KUBERNETES_NAMESPACE }} + KUBERNETES_INGRESS_HOST: ${{ secrets.KUBERNETES_INGRESS_HOST }} + KUBERNETES_DEPLOYMENT_REPLICAS: ${{ secrets.KUBERNETES_DEPLOYMENT_REPLICAS }} + + # Container registry configuration + CONTAINER_REGISTRY_URL: ${{ secrets.CONTAINER_REGISTRY_URL }} + CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} + CONTAINER_REGISTRY_NAMESPACE: ${{ secrets.CONTAINER_REGISTRY_NAMESPACE }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + CONTAINER_IMAGE_NAME: ${{ secrets.CONTAINER_IMAGE_NAME }} + CONTAINER_IMAGE_TAG: ${{ secrets.CONTAINER_IMAGE_TAG }} + FORCE_RESTART: ${{ secrets.KUBERNETES_FORCE_RESTART }} + + # Application configuration + DATABASE_DSN: ${{ secrets.DATABASE_DSN }} + +jobs: + deploy: + name: Deploy to Kubernetes + runs-on: ubuntu-latest + # Only run if CI workflow succeeded or manually dispatched + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Make CD script executable + run: chmod +x script/cd.sh + + - name: Deploy to Kubernetes + run: ./script/cd.sh deploy + + - name: Deployment Summary + if: success() + run: | + echo "### :white_check_mark: Deployment Successful!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Application:** \`${CONTAINER_IMAGE_NAME}\`" >> $GITHUB_STEP_SUMMARY + echo "**Namespace:** \`${KUBERNETES_NAMESPACE}\`" >> $GITHUB_STEP_SUMMARY + echo "**Image:** \`${CONTAINER_REGISTRY_URL}/${CONTAINER_REGISTRY_NAMESPACE}/${CONTAINER_IMAGE_NAME}:${CONTAINER_IMAGE_TAG}\`" >> $GITHUB_STEP_SUMMARY + echo "**Database:** Connected" >> $GITHUB_STEP_SUMMARY + echo "**URL:** http://${KUBERNETES_INGRESS_HOST}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Deployment Time:** $(date -u +'%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY + + - name: Deployment Failed + if: failure() + run: | + echo "### :x: Deployment Failed!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Please check the logs above for error details." >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..03c73d4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +# Continuous Integration Workflow +# This workflow builds and pushes Docker images to your private registry +# Trigger: Push to master branch or manual dispatch + +name: CI - Build and Push + +on: + push: + branches: + - master + workflow_dispatch: + +env: + CONTAINER_REGISTRY_URL: ${{ secrets.CONTAINER_REGISTRY_URL }} + CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} + CONTAINER_REGISTRY_NAMESPACE: ${{ secrets.CONTAINER_REGISTRY_NAMESPACE }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + CONTAINER_IMAGE_NAME: ${{ secrets.CONTAINER_IMAGE_NAME }} + CONTAINER_IMAGE_TAG: ${{ secrets.CONTAINER_IMAGE_TAG }} + +jobs: + build-and-push: + name: Build and Push Docker Image + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Make CI script executable + run: chmod +x script/ci.sh + + - name: Build Docker image + run: ./script/ci.sh build + + - name: Push Docker image + run: ./script/ci.sh push + + - name: Summary + run: | + echo "### :rocket: Build Complete!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Image:** \`${CONTAINER_REGISTRY_URL}/${CONTAINER_REGISTRY_NAMESPACE}/${CONTAINER_IMAGE_NAME}:${CONTAINER_IMAGE_TAG}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Registry:** ${CONTAINER_REGISTRY_URL}" >> $GITHUB_STEP_SUMMARY + echo "**Tag:** ${CONTAINER_IMAGE_TAG}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c26124d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# Build stage +FROM golang:1.24.4-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache git + +# Set working directory +WORKDIR /app + +# Copy go mod files from src directory +COPY src/go.mod src/go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy source code from src directory +COPY src/ . + +# Build the application +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o authenticator authenticator.go + +# Production stage +FROM alpine:latest + +# Install ca-certificates for HTTPS +RUN apk --no-cache add ca-certificates + +WORKDIR /root/ + +# Copy binary from builder +COPY --from=builder /app/authenticator . + +# Copy config file +COPY --from=builder /app/etc/authenticator.yaml ./etc/ + +# Expose port +EXPOSE 8888 + +# Run the application +CMD ["./authenticator", "-f", "etc/authenticator.yaml"] \ No newline at end of file diff --git a/script/cd.sh b/script/cd.sh new file mode 100644 index 0000000..e11c8bf --- /dev/null +++ b/script/cd.sh @@ -0,0 +1,173 @@ +#!/bin/bash + +# Continuous Deployment Script for Kubernetes +# This script deploys your application to a Kubernetes cluster + +# ============================================================================= +# Environment Variables (with default values) +# ============================================================================= + +# Kubernetes Configuration +KUBECONFIG_DATA="${KUBECONFIG_DATA:-}" +KUBERNETES_URL="${KUBERNETES_URL:-https://kubernetes.default.svc}" +KUBERNETES_NAMESPACE="${KUBERNETES_NAMESPACE:-default}" +KUBERNETES_INGRESS_HOST="${KUBERNETES_INGRESS_HOST:-auth-api.example.com}" +KUBERNETES_DEPLOYMENT_REPLICAS="${KUBERNETES_DEPLOYMENT_REPLICAS:-2}" + +# Container Registry +CONTAINER_REGISTRY_URL="${CONTAINER_REGISTRY_URL:-127.0.0.1}" +CONTAINER_REGISTRY_USERNAME="${CONTAINER_REGISTRY_USERNAME:-username}" +CONTAINER_REGISTRY_NAMESPACE="${CONTAINER_REGISTRY_NAMESPACE:-username}" +CONTAINER_REGISTRY_PASSWORD="${CONTAINER_REGISTRY_PASSWORD:-password}" +CONTAINER_IMAGE_NAME="${CONTAINER_IMAGE_NAME:-authenticator}" +CONTAINER_IMAGE_TAG="${CONTAINER_IMAGE_TAG:-latest}" + +# Application Configuration +DATABASE_DSN="${DATABASE_DSN:-postgres://postgres:password@localhost:5432/steam_union?sslmode=disable}" +FORCE_RESTART="${FORCE_RESTART:-true}" + +# ============================================================================= +# Functions +# ============================================================================= + +# Print help message +print_help() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Environment Variables:" + echo " KUBECONFIG_DATA Kubernetes config data (base64 encoded)" + echo " KUBERNETES_URL Kubernetes API URL (default: https://kubernetes.default.svc)" + echo " KUBERNETES_NAMESPACE Kubernetes namespace (default: default)" + echo " KUBERNETES_INGRESS_HOST Ingress host (default: auth-api.example.com)" + echo " KUBERNETES_DEPLOYMENT_REPLICAS Number of pod replicas (default: 2)" + echo " CONTAINER_REGISTRY_URL Container registry URL (default: 127.0.0.1)" + echo " CONTAINER_REGISTRY_USERNAME Registry username" + echo " CONTAINER_REGISTRY_PASSWORD Registry password" + echo " CONTAINER_IMAGE_NAME Image name (default: authenticator)" + echo " CONTAINER_IMAGE_TAG Image tag (default: latest)" + echo " DATABASE_DSN Database connection string" + echo " FORCE_RESTART Force rollout restart (default: true)" + echo "" + echo "Commands:" + echo " deploy Deploy application to Kubernetes" + echo " help Show this help message (default)" +} + +# Setup kubectl configuration +setup_kubectl() { + echo "Setting up kubectl configuration..." + + if [ -z "${KUBECONFIG_DATA}" ]; then + echo "✗ KUBECONFIG_DATA is not set" + return 1 + fi + + mkdir -p ~/.kube + echo "${KUBECONFIG_DATA}" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + + echo "✓ kubectl configured" + return 0 +} + +# Create namespace if it doesn't exist +create_namespace() { + echo "Checking namespace: ${KUBERNETES_NAMESPACE}" + + kubectl get namespace "${KUBERNETES_NAMESPACE}" &> /dev/null + + if [ $? -ne 0 ]; then + echo "Creating namespace: ${KUBERNETES_NAMESPACE}" + kubectl create namespace "${KUBERNETES_NAMESPACE}" + else + echo "✓ Namespace exists: ${KUBERNETES_NAMESPACE}" + fi +} + +# Create image pull secret +create_image_pull_secret() { + echo "Creating image pull secret..." + + kubectl create secret docker-registry regcred \ + --docker-server="${CONTAINER_REGISTRY_URL}" \ + --docker-username="${CONTAINER_REGISTRY_USERNAME}" \ + --docker-password="${CONTAINER_REGISTRY_PASSWORD}" \ + --namespace="${KUBERNETES_NAMESPACE}" \ + --dry-run=client -o yaml | kubectl apply -f - + + echo "✓ Image pull secret created/updated" +} + +# Deploy to Kubernetes +deploy_to_kubernetes() { + FULL_IMAGE_NAME="${CONTAINER_REGISTRY_URL}/${CONTAINER_REGISTRY_NAMESPACE}/${CONTAINER_IMAGE_NAME}:${CONTAINER_IMAGE_TAG}" + + echo "==========================================" + echo "Deploying to Kubernetes" + echo "==========================================" + echo "Namespace: ${KUBERNETES_NAMESPACE}" + echo "Image: ${FULL_IMAGE_NAME}" + echo "Host: ${KUBERNETES_INGRESS_HOST}" + echo "Database DSN: ${DATABASE_DSN}" + echo "" + + # Setup kubectl + setup_kubectl || return 1 + + # Create namespace + create_namespace || return 1 + + # Create image pull secret + create_image_pull_secret || return 1 + + # Apply Kubernetes manifests with variable substitution + echo "Applying Kubernetes manifests..." + + export FULL_IMAGE_NAME + export KUBERNETES_NAMESPACE + export KUBERNETES_INGRESS_HOST + export CONTAINER_IMAGE_NAME + export DATABASE_DSN + + for file in script/k8s/*.yaml; do + echo "Applying: $(basename $file)" + envsubst < "$file" | kubectl apply -f - + done + + echo "" + echo "✓ Deployment complete" + echo "" + echo "Waiting for rollout..." + + # Force restart if enabled + if [ "${FORCE_RESTART}" = "true" ]; then + echo "Forcing rollout restart..." + kubectl rollout restart deployment/${CONTAINER_IMAGE_NAME} -n ${KUBERNETES_NAMESPACE} + fi + + kubectl rollout status deployment/${CONTAINER_IMAGE_NAME} -n ${KUBERNETES_NAMESPACE} --timeout=300s + + if [ $? -eq 0 ]; then + echo "" + echo "✓ Application is ready" + echo "URL: http://${KUBERNETES_INGRESS_HOST}" + return 0 + else + echo "" + echo "✗ Rollout failed or timed out" + return 1 + fi +} + +# ============================================================================= +# Main Script +# ============================================================================= + +case "${1:-help}" in + deploy) + deploy_to_kubernetes + ;; + help|*) + print_help + ;; +esac \ No newline at end of file diff --git a/script/ci.sh b/script/ci.sh new file mode 100644 index 0000000..a773194 --- /dev/null +++ b/script/ci.sh @@ -0,0 +1,112 @@ +#!/bin/bash + +# CI/CD Script for Docker Build and Push +# This script builds a Docker image and pushes it to a private container registry + +# ============================================================================= +# Environment Variables (with default values) +# ============================================================================= +CONTAINER_REGISTRY_URL="${CONTAINER_REGISTRY_URL:-127.0.0.1}" +CONTAINER_REGISTRY_USERNAME="${CONTAINER_REGISTRY_USERNAME:-username}" +CONTAINER_REGISTRY_NAMESPACE="${CONTAINER_REGISTRY_NAMESPACE:-username}" +CONTAINER_REGISTRY_PASSWORD="${CONTAINER_REGISTRY_PASSWORD:-password}" +CONTAINER_IMAGE_NAME="${CONTAINER_IMAGE_NAME:-authenticator}" +CONTAINER_IMAGE_TAG="${CONTAINER_IMAGE_TAG:-latest}" + +# ============================================================================= +# Functions +# ============================================================================= + +# Print help message +print_help() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " build Build Docker image" + echo " push Push Docker image to registry" + echo " help Show this help message (default)" + echo "" + echo "Environment Variables:" + echo " CONTAINER_REGISTRY_URL Registry URL (default: 127.0.0.1)" + echo " CONTAINER_REGISTRY_USERNAME Registry username (default: username)" + echo " CONTAINER_REGISTRY_NAMESPACE Registry namespace (default: username)" + echo " CONTAINER_REGISTRY_PASSWORD Registry password (default: password)" + echo " CONTAINER_IMAGE_NAME Image name (default: authenticator)" + echo " CONTAINER_IMAGE_TAG Image tag (default: latest)" +} + +# Build Docker image +build_image() { + FULL_IMAGE_NAME="${CONTAINER_REGISTRY_URL}/${CONTAINER_REGISTRY_NAMESPACE}/${CONTAINER_IMAGE_NAME}:${CONTAINER_IMAGE_TAG}" + + echo "==========================================" + echo "Building Docker Image" + echo "==========================================" + echo "Image: ${FULL_IMAGE_NAME}" + echo "" + + docker build -t "${FULL_IMAGE_NAME}" . + + if [ $? -eq 0 ]; then + echo "" + echo "✓ Build successful: ${FULL_IMAGE_NAME}" + return 0 + else + echo "" + echo "✗ Build failed" + return 1 + fi +} + +# Push Docker image to registry +push_image() { + FULL_IMAGE_NAME="${CONTAINER_REGISTRY_URL}/${CONTAINER_REGISTRY_NAMESPACE}/${CONTAINER_IMAGE_NAME}:${CONTAINER_IMAGE_TAG}" + + echo "==========================================" + echo "Pushing Docker Image" + echo "==========================================" + echo "Registry: ${CONTAINER_REGISTRY_URL}" + echo "Image: ${FULL_IMAGE_NAME}" + echo "" + + # Login to registry + echo "${CONTAINER_REGISTRY_PASSWORD}" | docker login "${CONTAINER_REGISTRY_URL}" \ + --username "${CONTAINER_REGISTRY_USERNAME}" \ + --password-stdin + + if [ $? -ne 0 ]; then + echo "✗ Registry login failed" + return 1 + fi + + # Push image + docker push "${FULL_IMAGE_NAME}" + + if [ $? -eq 0 ]; then + echo "" + echo "✓ Push successful: ${FULL_IMAGE_NAME}" + docker logout "${CONTAINER_REGISTRY_URL}" + return 0 + else + echo "" + echo "✗ Push failed" + docker logout "${CONTAINER_REGISTRY_URL}" + return 1 + fi +} + +# ============================================================================= +# Main Script +# ============================================================================= + +case "${1:-help}" in + build) + build_image + ;; + push) + push_image + ;; + help|*) + print_help + ;; +esac \ No newline at end of file diff --git a/script/k8s/deployment.yaml b/script/k8s/deployment.yaml new file mode 100644 index 0000000..b58e12c --- /dev/null +++ b/script/k8s/deployment.yaml @@ -0,0 +1,64 @@ +# Kubernetes Deployment Configuration +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ${CONTAINER_IMAGE_NAME} + namespace: ${KUBERNETES_NAMESPACE} + labels: + app: ${CONTAINER_IMAGE_NAME} +spec: + replicas: ${KUBERNETES_DEPLOYMENT_REPLICAS} + + selector: + matchLabels: + app: ${CONTAINER_IMAGE_NAME} + + template: + metadata: + labels: + app: ${CONTAINER_IMAGE_NAME} + spec: + imagePullSecrets: + - name: regcred + + containers: + - name: ${CONTAINER_IMAGE_NAME} + image: ${FULL_IMAGE_NAME} + imagePullPolicy: Always + + ports: + - name: http + containerPort: 8888 + protocol: TCP + + readinessProbe: + httpGet: + path: /api/authenticator/ping + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + + livenessProbe: + httpGet: + path: /api/authenticator/ping + port: http + initialDelaySeconds: 15 + periodSeconds: 20 + timeoutSeconds: 5 + failureThreshold: 3 + + resources: + requests: + memory: "128Mi" + cpu: "200m" + limits: + memory: "256Mi" + cpu: "500m" + + env: + - name: TZ + value: "UTC" + - name: DATABASE_DSN + value: "${DATABASE_DSN}" \ No newline at end of file diff --git a/script/k8s/ingress.yaml b/script/k8s/ingress.yaml new file mode 100644 index 0000000..31d8390 --- /dev/null +++ b/script/k8s/ingress.yaml @@ -0,0 +1,25 @@ +# Kubernetes Ingress Configuration for Traefik +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ${CONTAINER_IMAGE_NAME} + namespace: ${KUBERNETES_NAMESPACE} + + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: web + + labels: + app: ${CONTAINER_IMAGE_NAME} + +spec: + rules: + - host: ${KUBERNETES_INGRESS_HOST} + http: + paths: + - path: /api/authenticator + pathType: Prefix + backend: + service: + name: ${CONTAINER_IMAGE_NAME} + port: + number: 8888 \ No newline at end of file diff --git a/script/k8s/namespace.yaml b/script/k8s/namespace.yaml new file mode 100644 index 0000000..fe47228 --- /dev/null +++ b/script/k8s/namespace.yaml @@ -0,0 +1,7 @@ +# Kubernetes Namespace Configuration +apiVersion: v1 +kind: Namespace +metadata: + name: ${KUBERNETES_NAMESPACE} + labels: + name: ${KUBERNETES_NAMESPACE} \ No newline at end of file diff --git a/script/k8s/service.yaml b/script/k8s/service.yaml new file mode 100644 index 0000000..37522aa --- /dev/null +++ b/script/k8s/service.yaml @@ -0,0 +1,19 @@ +# Kubernetes Service Configuration +apiVersion: v1 +kind: Service +metadata: + name: ${CONTAINER_IMAGE_NAME} + namespace: ${KUBERNETES_NAMESPACE} + labels: + app: ${CONTAINER_IMAGE_NAME} +spec: + type: ClusterIP + + selector: + app: ${CONTAINER_IMAGE_NAME} + + ports: + - name: http + port: 8888 + targetPort: http + protocol: TCP \ No newline at end of file