diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index dcb95a82..2cdd4f40 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,35 +1,275 @@ -name: Docker Build & Scan +name: Docker Build, Scan & Publish on: push: - branches: [ main, dev ] + branches: [ main ] + tags: [ 'v*' ] pull_request: - branches: [ main, dev ] + branches: [ main ] workflow_dispatch: +env: + REGISTRY: ghcr.io + jobs: - docker: - name: Docker Build & Scan + build-base: + name: Build Base + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + outputs: + image-tag: ${{ steps.image-tag.outputs.tag }} + + steps: + - uses: actions/checkout@v6 + + - name: Set lowercase image prefix + run: echo "IMAGE_PREFIX=${GITHUB_REPOSITORY_OWNER,,}/integr8scode" >> $GITHUB_ENV + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/base + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=sha- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Determine image tag for dependent builds + id: image-tag + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo "tag=pr-${{ github.event.number }}" >> $GITHUB_OUTPUT + else + echo "tag=latest" >> $GITHUB_OUTPUT + fi + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./backend + file: ./backend/Dockerfile.base + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=base + cache-to: type=gha,mode=max,scope=base + + build-backend: + name: Build Backend + needs: build-base runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + outputs: + image-ref: ${{ steps.image-ref.outputs.ref }} + steps: - - uses: actions/checkout@v4 - - name: Build base image + - uses: actions/checkout@v6 + + - name: Set lowercase image prefix + run: echo "IMAGE_PREFIX=${GITHUB_REPOSITORY_OWNER,,}/integr8scode" >> $GITHUB_ENV + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/backend + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=sha- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Set image reference for scan + id: image-ref run: | - docker build -f ./backend/Dockerfile.base -t integr8scode-base:latest ./backend + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo "ref=${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/backend:pr-${{ github.event.number }}" >> $GITHUB_OUTPUT + else + echo "ref=${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/backend:latest" >> $GITHUB_OUTPUT + fi + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./backend + file: ./backend/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=backend + cache-to: type=gha,mode=max,scope=backend + build-contexts: | + base=docker-image://${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/base:${{ needs.build-base.outputs.image-tag }} + + build-frontend: + name: Build Frontend + needs: build-base + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + outputs: + image-ref: ${{ steps.image-ref.outputs.ref }} + + steps: + - uses: actions/checkout@v6 - - name: Build Docker image + - name: Set lowercase image prefix + run: echo "IMAGE_PREFIX=${GITHUB_REPOSITORY_OWNER,,}/integr8scode" >> $GITHUB_ENV + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/frontend + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=sha- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Set image reference for scan + id: image-ref run: | - DOCKER_BUILDKIT=1 docker build \ - --build-context base=docker-image://integr8scode-base:latest \ - -t integr8scode:test \ - ./backend + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo "ref=${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/frontend:pr-${{ github.event.number }}" >> $GITHUB_OUTPUT + else + echo "ref=${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/frontend:latest" >> $GITHUB_OUTPUT + fi + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./frontend + file: ./frontend/Dockerfile.prod + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=frontend + cache-to: type=gha,mode=max,scope=frontend + + scan-backend: + name: Scan Backend + needs: build-backend + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + + steps: + - uses: actions/checkout@v6 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.33.1 + with: + image-ref: ${{ needs.build-backend.outputs.image-ref }} + format: 'sarif' + output: 'trivy-backend-results.sarif' + ignore-unfixed: true + severity: 'CRITICAL,HIGH' + timeout: '5m0s' + trivyignores: 'backend/.trivyignore' + version: 'v0.68.2' + + - name: Upload Trivy scan results + if: always() + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: 'trivy-backend-results.sarif' + category: 'trivy-backend' + + scan-frontend: + name: Scan Frontend + needs: build-frontend + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + + steps: - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@master + uses: aquasecurity/trivy-action@0.33.1 with: - image-ref: 'integr8scode:test' - format: 'table' - exit-code: '1' + image-ref: ${{ needs.build-frontend.outputs.image-ref }} + format: 'sarif' + output: 'trivy-frontend-results.sarif' ignore-unfixed: true severity: 'CRITICAL,HIGH' timeout: '5m0s' - trivyignores: 'backend/.trivyignore' \ No newline at end of file + version: 'v0.68.2' + + - name: Upload Trivy scan results + if: always() + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: 'trivy-frontend-results.sarif' + category: 'trivy-frontend' + + summary: + name: Summary + if: github.event_name != 'pull_request' + needs: [build-base, build-backend, build-frontend, scan-backend, scan-frontend] + runs-on: ubuntu-latest + + steps: + - name: Set lowercase image prefix + run: echo "IMAGE_PREFIX=${GITHUB_REPOSITORY_OWNER,,}/integr8scode" >> $GITHUB_ENV + + - name: Generate summary + run: | + echo "## Docker Images Published" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Image | Pull Command |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------------|" >> $GITHUB_STEP_SUMMARY + echo "| Base | \`docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/base:latest\` |" >> $GITHUB_STEP_SUMMARY + echo "| Backend | \`docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/backend:latest\` |" >> $GITHUB_STEP_SUMMARY + echo "| Frontend | \`docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/frontend:latest\` |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Scan Results" >> $GITHUB_STEP_SUMMARY + echo "- Backend scan: ✅ Passed" >> $GITHUB_STEP_SUMMARY + echo "- Frontend scan: ✅ Passed" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a3b700a6..13f2b530 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up uv uses: astral-sh/setup-uv@v5 diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 1989c24b..786cdb3c 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -12,7 +12,7 @@ jobs: name: Mypy Type Checking runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up uv uses: astral-sh/setup-uv@v5 diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index a3e24156..b3db04a2 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -12,7 +12,7 @@ jobs: name: Ruff Linting runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up uv uses: astral-sh/setup-uv@v5 diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 75bbb671..73265ff7 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -12,7 +12,7 @@ jobs: name: Security Scanning runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up uv uses: astral-sh/setup-uv@v5 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 285e042b..56a5df4a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: name: Backend Tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Docker Buildx uses: docker/setup-buildx-action@v3 @@ -88,8 +88,12 @@ jobs: yq eval '.services.backend.environment += ["MONGO_ROOT_PASSWORD=rootpassword"]' -i docker-compose.ci.yaml # Disable OpenTelemetry SDK during tests to avoid exporter retries yq eval '.services.backend.environment += ["OTEL_SDK_DISABLED=true"]' -i docker-compose.ci.yaml - # Remove ./backend:/app volume mount - CI doesn't need hot-reload and it creates .venv permission conflicts + # Remove ./backend:/app volume mounts - CI doesn't need hot-reload and it creates .venv permission conflicts + # Backend has ./backend:/app, workers have ./backend:/app:ro yq eval '.services.backend.volumes = [.services.backend.volumes[] | select(. != "./backend:/app")]' -i docker-compose.ci.yaml + yq eval '.services."k8s-worker".volumes = [.services."k8s-worker".volumes[] | select(. != "./backend:/app:ro")]' -i docker-compose.ci.yaml + yq eval '.services."pod-monitor".volumes = [.services."pod-monitor".volumes[] | select(. != "./backend:/app:ro")]' -i docker-compose.ci.yaml + yq eval '.services."result-processor".volumes = [.services."result-processor".volumes[] | select(. != "./backend:/app:ro")]' -i docker-compose.ci.yaml # MongoDB service already has defaults in docker-compose.yaml (root/rootpassword) # No need to override them @@ -135,43 +139,16 @@ jobs: - name: Start services run: | - echo "Starting services (images already built with cache)..." - docker compose -f docker-compose.ci.yaml up -d --remove-orphans || \ - ( - echo "::error::Docker Compose failed to start. Dumping all logs..." - docker compose -f docker-compose.ci.yaml logs - exit 1 - ) - - echo "Services started. Waiting for stabilization..." - sleep 45 - - echo "Final status of all containers:" + echo "Starting services..." + docker compose -f docker-compose.ci.yaml up -d --remove-orphans docker compose -f docker-compose.ci.yaml ps - echo "Checking cert-generator logs:" - docker compose -f docker-compose.ci.yaml logs cert-generator || echo "cert-generator not found or exited" - - echo "Checking backend container status:" - docker compose -f docker-compose.ci.yaml ps backend - - echo "Checking backend logs:" - docker compose -f docker-compose.ci.yaml logs backend || echo "backend not found" - - # Explicitly check for containers that have exited - if docker compose -f docker-compose.ci.yaml ps | grep -q 'Exit'; then - echo "::error::One or more containers have exited unexpectedly. See logs above." - docker compose -f docker-compose.ci.yaml logs --no-color - exit 1 - fi - - name: Wait for backend to be healthy run: | - timeout 300 bash -c 'until curl -k https://127.0.0.1:443/api/v1/health -o /dev/null; do \ - echo "Retrying backend health check..."; \ - sleep 5; \ - done' - echo "Backend is healthy!" + echo "Waiting for backend (and its dependencies) to be healthy..." + curl --retry 60 --retry-delay 5 --retry-all-errors -ksf https://127.0.0.1:443/api/v1/health/live + echo "Backend is healthy!" + docker compose -f docker-compose.ci.yaml ps # Frontend is excluded in backend-only CI; skip UI readiness diff --git a/.gitignore b/.gitignore index 7af422d8..f93af750 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,7 @@ yarn-error.log* # Build outputs frontend/public/build/ + +# Helm +helm/*/charts/*.tgz +helm/*/Chart.lock diff --git a/backend/Dockerfile.base b/backend/Dockerfile.base index 11dca0a5..8515ceb1 100644 --- a/backend/Dockerfile.base +++ b/backend/Dockerfile.base @@ -12,8 +12,8 @@ RUN apt-get update && apt-get install -y \ liblzma-dev \ && rm -rf /var/lib/apt/lists/* -# Install uv -COPY --from=ghcr.io/astral-sh/uv:0.9.17 /uv /uvx /bin/ +# Install uv (using Docker Hub mirror - ghcr.io has rate limiting issues) +COPY --from=astral/uv:latest /uv /uvx /bin/ # Copy dependency files COPY pyproject.toml uv.lock ./ diff --git a/backend/app/services/coordinator/coordinator.py b/backend/app/services/coordinator/coordinator.py index ce5a7bbe..8005f168 100644 --- a/backend/app/services/coordinator/coordinator.py +++ b/backend/app/services/coordinator/coordinator.py @@ -124,9 +124,10 @@ async def start(self) -> None: await self.idempotency_manager.initialize() + settings = get_settings() consumer_config = ConsumerConfig( bootstrap_servers=self.kafka_servers, - group_id= f"{self.consumer_group}.{settings.KAFKA_GROUP_SUFFIX}", + group_id=f"{self.consumer_group}.{settings.KAFKA_GROUP_SUFFIX}", enable_auto_commit=False, session_timeout_ms=30000, # 30 seconds heartbeat_interval_ms=10000, # 10 seconds (must be < session_timeout / 3) diff --git a/backend/scripts/create_admin_user.py b/backend/scripts/create_admin_user.py deleted file mode 100755 index 88771f3a..00000000 --- a/backend/scripts/create_admin_user.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python3 -"""Create an admin user directly in MongoDB""" - -import asyncio -from datetime import datetime, timezone - -from bson import ObjectId -from motor.motor_asyncio import AsyncIOMotorClient -from passlib.context import CryptContext - -# Password hashing -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - - -async def create_admin_user() -> None: - # Connect to MongoDB - client: AsyncIOMotorClient = AsyncIOMotorClient( - "mongodb://root:rootpassword@mongo:27017/integr8scode?authSource=admin") - db = client.integr8scode - - # User details - username = "admin" - email = "admin@integr8scode.com" - password = "admin123" - - # Check if user already exists - existing = await db.users.find_one({"username": username}) - if existing: - print(f"User '{username}' already exists. Updating to admin role...") - result = await db.users.update_one( - {"username": username}, - {"$set": { - "role": "admin", - "is_superuser": True, - "is_active": True, - "updated_at": datetime.now(timezone.utc) - }} - ) - print(f"Updated: {result.modified_count} document(s)") - else: - # Create new admin user - hashed_password = pwd_context.hash(password) - user_doc = { - "_id": ObjectId(), - "user_id": str(ObjectId()), - "username": username, - "email": email, - "hashed_password": hashed_password, - "role": "admin", - "is_active": True, - "is_superuser": True, - "created_at": datetime.now(timezone.utc), - "updated_at": datetime.now(timezone.utc) - } - - insert_result = await db.users.insert_one(user_doc) - print(f"Created admin user '{username}' with ID: {insert_result.inserted_id}") - - print("\nAdmin credentials:") - print(f"Username: {username}") - print(f"Password: {password}") - print(f"Email: {email}") - - client.close() - - -if __name__ == "__main__": - asyncio.run(create_admin_user()) diff --git a/backend/scripts/create_topics.py b/backend/scripts/create_topics.py index bec95606..61f5c5f1 100755 --- a/backend/scripts/create_topics.py +++ b/backend/scripts/create_topics.py @@ -35,13 +35,15 @@ async def create_topics() -> None: # Get all required topics and their configs all_topics = get_all_topics() topic_configs = get_topic_configs() - logger.info(f"Total required topics: {len(all_topics)}") + topic_prefix = settings.KAFKA_TOPIC_PREFIX + logger.info(f"Total required topics: {len(all_topics)} (prefix: '{topic_prefix}')") # Create topics topics_to_create: List[NewTopic] = [] for topic in all_topics: - topic_name = topic.value + # Apply topic prefix for consistency with consumers/producers + topic_name = f"{topic_prefix}{topic.value}" if topic_name not in existing_topics: # Get config from topic_configs config = topic_configs.get(topic, { diff --git a/backend/scripts/seed_users.py b/backend/scripts/seed_users.py new file mode 100755 index 00000000..dcb86e2b --- /dev/null +++ b/backend/scripts/seed_users.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +""" +Seed default users in MongoDB (idempotent - safe to run multiple times). + +Creates: + 1. Default user (role=user) for testing/demo + 2. Admin user (role=admin, is_superuser=True) for administration + +Environment Variables: + MONGODB_URL: Connection string (default: mongodb://mongo:27017/integr8scode) + DEFAULT_USER_PASSWORD: Default user password (default: user123) + ADMIN_USER_PASSWORD: Admin user password (default: admin123) +""" + +import asyncio +import os +from datetime import datetime, timezone + +from bson import ObjectId +from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase +from passlib.context import CryptContext + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +async def upsert_user( + db: AsyncIOMotorDatabase, + username: str, + email: str, + password: str, + role: str, + is_superuser: bool, +) -> None: + """Create user or update if exists.""" + existing = await db.users.find_one({"username": username}) + + if existing: + print(f"User '{username}' exists - updating role={role}, is_superuser={is_superuser}") + await db.users.update_one( + {"username": username}, + {"$set": { + "role": role, + "is_superuser": is_superuser, + "is_active": True, + "updated_at": datetime.now(timezone.utc) + }} + ) + else: + print(f"Creating user '{username}' (role={role}, is_superuser={is_superuser})") + await db.users.insert_one({ + "_id": ObjectId(), + "user_id": str(ObjectId()), + "username": username, + "email": email, + "hashed_password": pwd_context.hash(password), + "role": role, + "is_active": True, + "is_superuser": is_superuser, + "created_at": datetime.now(timezone.utc), + "updated_at": datetime.now(timezone.utc) + }) + + +async def seed_users() -> None: + mongodb_url = os.getenv("MONGODB_URL", "mongodb://mongo:27017/integr8scode") + print("Connecting to MongoDB...") + + client: AsyncIOMotorClient = AsyncIOMotorClient(mongodb_url) + db_name = mongodb_url.rstrip("/").split("/")[-1].split("?")[0] or "integr8scode" + db = client[db_name] + + # Default user + await upsert_user( + db, + username="user", + email="user@integr8scode.com", + password=os.getenv("DEFAULT_USER_PASSWORD", "user123"), + role="user", + is_superuser=False + ) + + # Admin user + await upsert_user( + db, + username="admin", + email="admin@integr8scode.com", + password=os.getenv("ADMIN_USER_PASSWORD", "admin123"), + role="admin", + is_superuser=True + ) + + print("\n" + "=" * 50) + print("SEEDED USERS:") + print("=" * 50) + print("Default: user / user123 (or DEFAULT_USER_PASSWORD)") + print("Admin: admin / admin123 (or ADMIN_USER_PASSWORD)") + print("=" * 50) + + client.close() + + +if __name__ == "__main__": + asyncio.run(seed_users()) diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 00000000..23a4e0d1 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,369 @@ +#!/bin/bash +# ============================================================================= +# Integr8sCode Unified Deployment Script +# ============================================================================= +# +# Usage: +# ./deploy.sh dev # Start local development (docker-compose) +# ./deploy.sh dev --build # Rebuild and start local development +# ./deploy.sh down # Stop local development +# ./deploy.sh prod # Deploy to K8s (builds images locally) +# ./deploy.sh prod --prod # Deploy with production values (uses registry) +# ./deploy.sh prod --dry-run # Test Helm deployment without applying +# ./deploy.sh check # Run local quality checks (lint, type, security) +# ./deploy.sh test # Run full test suite locally +# ./deploy.sh logs [service] # View logs (dev mode) +# ./deploy.sh status # Show status of running services +# +# ============================================================================= + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Helm configuration +NAMESPACE="integr8scode" +RELEASE_NAME="integr8scode" +CHART_PATH="./helm/integr8scode" + +print_header() { + echo -e "${BLUE}" + echo "===========================================================================" + echo " $1" + echo "===========================================================================" + echo -e "${NC}" +} + +print_success() { echo -e "${GREEN}✓ $1${NC}"; } +print_warning() { echo -e "${YELLOW}⚠ $1${NC}"; } +print_error() { echo -e "${RED}✗ $1${NC}"; } +print_info() { echo -e "${BLUE}→ $1${NC}"; } + +show_help() { + echo "Integr8sCode Deployment Script" + echo "" + echo "Usage: ./deploy.sh [options]" + echo "" + echo "Commands:" + echo " dev [--build] Start local development environment (docker-compose)" + echo " down Stop local development environment" + echo " prod [options] Deploy to Kubernetes with Helm" + echo " check Run quality checks (ruff, mypy, bandit)" + echo " test Run full test suite with docker-compose" + echo " logs [service] View logs (defaults to all services)" + echo " status Show status of running services" + echo " help Show this help message" + echo "" + echo "Prod options:" + echo " --dry-run Validate templates without applying" + echo " --prod Use production values (ghcr.io images, no local build)" + echo " --local Force local build even with --prod values" + echo " --set key=value Override Helm values" + echo "" + echo "Examples:" + echo " ./deploy.sh dev # Start dev environment" + echo " ./deploy.sh dev --build # Rebuild and start" + echo " ./deploy.sh prod # Deploy with local images" + echo " ./deploy.sh prod --prod # Deploy with registry images (no build)" + echo " ./deploy.sh prod --prod --local # Deploy prod values but build locally" + echo " ./deploy.sh prod --set mongodb.auth.rootPassword=secret" + echo " ./deploy.sh logs backend # View backend logs" +} + +# ============================================================================= +# LOCAL DEVELOPMENT (docker-compose) +# ============================================================================= +cmd_dev() { + print_header "Starting Local Development Environment" + + local BUILD_FLAG="" + if [[ "$1" == "--build" ]]; then + BUILD_FLAG="--build" + print_info "Rebuilding images..." + fi + + docker compose up -d $BUILD_FLAG + + echo "" + print_success "Development environment started!" + echo "" + echo "Services:" + echo " Backend: https://localhost:443" + echo " Frontend: http://localhost:5001" + echo " Kafdrop: http://localhost:9000" + echo " Jaeger: http://localhost:16686" + echo " Grafana: http://localhost:3000" + echo "" + echo "Commands:" + echo " ./deploy.sh logs # View all logs" + echo " ./deploy.sh logs backend # View backend logs" + echo " ./deploy.sh down # Stop all services" +} + +cmd_down() { + print_header "Stopping Local Development Environment" + docker compose down + print_success "All services stopped" +} + +cmd_logs() { + local SERVICE="$1" + if [[ -n "$SERVICE" ]]; then + docker compose logs -f "$SERVICE" + else + docker compose logs -f + fi +} + +cmd_status() { + print_header "Service Status" + + echo "" + echo "Docker Compose Services:" + docker compose ps 2>/dev/null || echo " No docker-compose services running" + + echo "" + echo "Kubernetes Pods (if deployed):" + kubectl get pods -n "$NAMESPACE" 2>/dev/null || echo " No Kubernetes deployment found" +} + +# ============================================================================= +# QUALITY CHECKS +# ============================================================================= +cmd_check() { + print_header "Running Quality Checks" + + cd backend + + print_info "Running Ruff linting..." + if uv run ruff check . --config pyproject.toml; then + print_success "Ruff linting passed" + else + print_error "Ruff linting failed" + exit 1 + fi + + print_info "Running MyPy type checking..." + if uv run mypy --config-file pyproject.toml .; then + print_success "MyPy type checking passed" + else + print_error "MyPy type checking failed" + exit 1 + fi + + print_info "Running Bandit security scan..." + if uv run bandit -r . -x tests/ -ll; then + print_success "Security scan passed" + else + print_error "Security scan failed" + exit 1 + fi + + cd .. + print_success "All quality checks passed!" +} + +# ============================================================================= +# TEST SUITE +# ============================================================================= +cmd_test() { + print_header "Running Test Suite" + + print_info "Starting services..." + docker compose up -d --build + + print_info "Waiting for backend to be healthy..." + if ! curl --retry 60 --retry-delay 5 --retry-all-errors -ksfo /dev/null https://localhost:443/api/v1/health/live; then + print_error "Backend failed to become healthy" + docker compose logs + exit 1 + fi + print_success "Backend is healthy" + + print_info "Running tests..." + cd backend + if uv run pytest tests/integration tests/unit -v --cov=app --cov-report=term; then + print_success "All tests passed!" + TEST_RESULT=0 + else + print_error "Tests failed" + TEST_RESULT=1 + fi + cd .. + + print_info "Cleaning up..." + docker compose down + + exit $TEST_RESULT +} + +# ============================================================================= +# KUBERNETES DEPLOYMENT (Helm) +# ============================================================================= +build_and_import_images() { + print_info "Building Docker images..." + + docker build -t base:latest -f ./backend/Dockerfile.base ./backend + docker build -t integr8scode-backend:latest -f ./backend/Dockerfile ./backend + + if [[ -f ./frontend/Dockerfile.prod ]]; then + docker build -t integr8scode-frontend:latest -f ./frontend/Dockerfile.prod ./frontend + else + docker build -t integr8scode-frontend:latest -f ./frontend/Dockerfile ./frontend + fi + + print_success "Images built" + + if command -v k3s &> /dev/null; then + print_info "Importing images to K3s..." + docker save base:latest | sudo k3s ctr images import - + docker save integr8scode-backend:latest | sudo k3s ctr images import - + docker save integr8scode-frontend:latest | sudo k3s ctr images import - + print_success "Images imported to K3s" + else + print_warning "K3s not found - skipping image import" + fi +} + +cmd_prod() { + print_header "Deploying to Kubernetes" + + local DRY_RUN="" + local VALUES_FILE="values.yaml" + local EXTRA_ARGS="" + local USE_REGISTRY=false + local FORCE_LOCAL=false + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) + DRY_RUN="--dry-run" + print_warning "DRY RUN MODE - No changes will be applied" + ;; + --prod) + VALUES_FILE="values-prod.yaml" + USE_REGISTRY=true + print_info "Using production values (ghcr.io images)" + ;; + --local) + FORCE_LOCAL=true + print_info "Forcing local image build" + ;; + --set) + shift + EXTRA_ARGS="$EXTRA_ARGS --set $1" + ;; + *) + print_error "Unknown option: $1" + exit 1 + ;; + esac + shift + done + + deploy_helm "$VALUES_FILE" "$DRY_RUN" "$EXTRA_ARGS" "$USE_REGISTRY" "$FORCE_LOCAL" +} + +deploy_helm() { + local VALUES_FILE="$1" + local DRY_RUN="$2" + local EXTRA_ARGS="$3" + local USE_REGISTRY="$4" + local FORCE_LOCAL="$5" + + # Build images if: + # - Not dry-run AND + # - Not using registry OR force local build + if [[ -z "$DRY_RUN" ]]; then + if [[ "$USE_REGISTRY" != "true" ]] || [[ "$FORCE_LOCAL" == "true" ]]; then + build_and_import_images + else + print_info "Using pre-built images from ghcr.io (skipping local build)" + fi + fi + + print_info "Updating Helm dependencies..." + helm dependency update "$CHART_PATH" + + print_info "Creating namespace..." + if [[ -z "$DRY_RUN" ]]; then + kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - + fi + + print_info "Deploying with Helm..." + helm upgrade --install "$RELEASE_NAME" "$CHART_PATH" \ + --namespace "$NAMESPACE" \ + --values "$CHART_PATH/$VALUES_FILE" \ + --wait \ + --timeout 10m \ + $DRY_RUN \ + $EXTRA_ARGS + + if [[ -z "$DRY_RUN" ]]; then + echo "" + print_success "Deployment complete!" + echo "" + echo "Pods:" + kubectl get pods -n "$NAMESPACE" + echo "" + echo "Services:" + kubectl get services -n "$NAMESPACE" + echo "" + if [[ "$VALUES_FILE" == "values-prod.yaml" ]]; then + echo "Note: Passwords must be set via --set flags for production" + else + echo "Default credentials: user/user123, admin/admin123" + fi + echo "" + echo "Commands:" + echo " kubectl logs -n $NAMESPACE -l app.kubernetes.io/component=backend" + echo " kubectl port-forward -n $NAMESPACE svc/$RELEASE_NAME-backend 8443:443" + echo " helm uninstall $RELEASE_NAME -n $NAMESPACE" + fi +} + +# ============================================================================= +# MAIN +# ============================================================================= +case "${1:-help}" in + dev) + cmd_dev "$2" + ;; + down) + cmd_down + ;; + logs) + cmd_logs "$2" + ;; + status) + cmd_status + ;; + check) + cmd_check + ;; + test) + cmd_test + ;; + prod) + shift + cmd_prod "$@" + ;; + help|--help|-h) + show_help + ;; + *) + print_error "Unknown command: $1" + echo "" + show_help + exit 1 + ;; +esac diff --git a/docker-compose.yaml b/docker-compose.yaml index e3d31c59..89af7332 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -368,6 +368,27 @@ services: - app-network restart: "no" # Run once and exit + # Seed default users (runs once after mongo is ready) + user-seed: + build: + context: ./backend + dockerfile: Dockerfile + additional_contexts: + base: service:base + container_name: user-seed + depends_on: + base: + condition: service_completed_successfully + mongo: + condition: service_healthy + environment: + - MONGODB_URL=mongodb://${MONGO_ROOT_USER:-root}:${MONGO_ROOT_PASSWORD:-rootpassword}@mongo:27017/integr8scode?authSource=admin + - DEFAULT_USER_PASSWORD=${DEFAULT_USER_PASSWORD:-user123} + - ADMIN_USER_PASSWORD=${ADMIN_USER_PASSWORD:-admin123} + command: ["uv", "run", "python", "-m", "scripts.seed_users"] + networks: + - app-network + restart: "no" # Run once and exit # Event-driven workers coordinator: diff --git a/docs/operations/cicd.md b/docs/operations/cicd.md index 184c0a3f..7469f192 100644 --- a/docs/operations/cicd.md +++ b/docs/operations/cicd.md @@ -15,7 +15,18 @@ graph LR subgraph "Security" Bandit["Bandit SAST"] - Trivy["Trivy Container Scan"] + end + + subgraph "Docker Build & Scan" + Base["Build Base"] + Backend["Build Backend"] + Frontend["Build Frontend"] + ScanBE["Scan Backend"] + ScanFE["Scan Frontend"] + Base --> Backend + Base --> Frontend + Backend --> ScanBE + Frontend --> ScanFE end subgraph "Testing" @@ -30,7 +41,7 @@ graph LR Push["Push / PR"] --> Ruff Push --> MyPy Push --> Bandit - Push --> Trivy + Push --> Base Push --> Integration Push --> Docs Docs -->|main only| Pages @@ -55,24 +66,59 @@ caching to skip reinstallation when the lockfile hasn't changed. ## Security scanning -Security runs in two places. The security workflow uses [Bandit](https://bandit.readthedocs.io/) to perform static -analysis on Python source files, flagging issues like hardcoded credentials, SQL injection patterns, and unsafe -deserialization. It excludes the test directory and reports only medium-severity and above findings. +The security workflow uses [Bandit](https://bandit.readthedocs.io/) to perform static analysis on Python source files, +flagging issues like hardcoded credentials, SQL injection patterns, and unsafe deserialization. It excludes the test +directory and reports only medium-severity and above findings. Container-level vulnerability scanning with Trivy runs +as part of the Docker workflow (see below). + +## Docker build and scan + +The Docker workflow is structured as multiple jobs with dependencies, enabling parallel execution and early failure +detection. If any job fails, dependent jobs are skipped immediately. + +```mermaid +graph TD + A[build-base] --> B[build-backend] + A --> C[build-frontend] + B --> D[scan-backend] + C --> E[scan-frontend] + D --> F[summary] + E --> F + + style A fill:#e1f5fe + style B fill:#fff3e0 + style C fill:#fff3e0 + style D fill:#ffebee + style E fill:#ffebee + style F fill:#e8f5e9 +``` + +| Job | Depends On | Purpose | +|------------------|------------------|------------------------------------------------------| +| `build-base` | - | Build shared base image with Python and dependencies | +| `build-backend` | `build-base` | Build backend image using base as build context | +| `build-frontend` | `build-base` | Build frontend image (runs parallel with backend) | +| `scan-backend` | `build-backend` | Trivy vulnerability scan on backend image | +| `scan-frontend` | `build-frontend` | Trivy vulnerability scan on frontend image | +| `summary` | All scans | Generate summary (main branch only) | + +### Base image -The Docker workflow builds the backend image and scans it with [Trivy](https://trivy.dev/). Trivy checks the image -layers for known vulnerabilities in OS packages and Python dependencies, failing the build if it finds any critical or -high severity issues that have available fixes. This catches supply chain problems that static analysis would miss. +The base image (`Dockerfile.base`) contains Python, system dependencies, and all pip packages. It +uses [uv](https://docs.astral.sh/uv/) to install dependencies from the lockfile, ensuring reproducible builds. The base +includes gcc, curl, and compression libraries needed by some Python packages. Separating base from application means +dependency changes rebuild the base layer while code changes only rebuild the thin application layer. -## Docker build +### Build contexts -The Docker workflow builds images using a two-stage approach to optimize layer caching. First it builds a shared base -image (`Dockerfile.base`) containing Python, system dependencies, and all pip packages. Then it builds the main backend -image on top of that base, copying only the application code. This separation means dependency changes rebuild the base -layer while code changes only rebuild the thin application layer. +Backend and frontend builds reference the base image via Docker's `build-contexts` feature. The workflow passes the +appropriate tag (`pr-` for pull requests, `latest` for main branch) so each build uses the correct base. -The base image includes gcc, curl, and compression libraries needed by some Python packages. It -uses [uv](https://docs.astral.sh/uv/) to install dependencies from the lockfile, ensuring reproducible builds across -environments. The pinned uv version (currently 0.9.17) prevents unexpected behavior from upstream changes. +### Security scanning + +After each image builds, [Trivy](https://trivy.dev/) scans it for known vulnerabilities in OS packages and Python +dependencies. The scan fails if it finds any critical or high severity issues with available fixes. Results upload to +GitHub Security for tracking. The backend scan respects `.trivyignore` for acknowledged vulnerabilities. ## Integration tests @@ -112,9 +158,18 @@ It reads cache layers from previous runs and writes new layers back, so unchange scopes are branch-specific with a fallback to main, meaning feature branches benefit from the main branch cache even on their first run. -Once images are built, `docker compose up` starts the stack and waits for health checks. The backend needs MongoDB, -Redis, Kafka, Schema Registry, and the cert-generator to be ready before it can serve requests. After stabilization, the -workflow runs pytest against the integration and unit test suites with coverage reporting. Test isolation uses +Once images are built, `docker compose up -d` starts the stack. The workflow then uses curl's built-in retry mechanism +to wait for the backend health endpoint: + +```bash +curl --retry 60 --retry-delay 5 --retry-all-errors -ksf https://127.0.0.1:443/api/v1/health/live +``` + +This approach is cleaner than shell loops and more reliable than Docker Compose's `--wait` flag (which has issues with +init containers that exit after completion). The backend's `depends_on` configuration ensures MongoDB, Redis, Kafka, +and Schema Registry are healthy before backend starts, so waiting for backend health implicitly waits for all +dependencies. Once the health check passes, the workflow runs pytest against the integration and unit test suites with +coverage reporting. Test isolation uses per-worker database names and schema registry prefixes to avoid conflicts when pytest-xdist runs tests in parallel. Coverage reports go to [Codecov](https://codecov.io/) for tracking over time. The workflow always collects container @@ -157,9 +212,10 @@ uv run pytest tests/unit -v uv run pytest tests/integration tests/unit -v ``` -For the full integration test experience, start the stack with `docker compose up -d`, wait for services to stabilize, -then run pytest. The CI workflow's yq modifications aren't necessary locally since your environment likely has the -expected configuration already. +For the full integration test experience, start the stack with `docker compose up -d`, wait for the backend to be +healthy, then run pytest. Alternatively, use `./deploy.sh test` which handles startup, health checks, testing, and +cleanup automatically. The CI workflow's yq modifications aren't necessary locally since your environment +likely has the expected configuration already. ## Workflow files diff --git a/docs/operations/deployment.md b/docs/operations/deployment.md new file mode 100644 index 00000000..bc80665b --- /dev/null +++ b/docs/operations/deployment.md @@ -0,0 +1,383 @@ +# Deployment + +Integr8sCode supports two deployment modes: local development using Docker Compose and production deployment to +Kubernetes using Helm. Both modes share the same container images and configuration patterns, so what works locally +translates directly to production. + +## Deployment script + +The unified `deploy.sh` script in the repository root handles both modes. Running it without arguments shows available +commands. + +```bash +./deploy.sh dev # Start local development stack +./deploy.sh dev --build # Rebuild images and start +./deploy.sh down # Stop local stack +./deploy.sh check # Run quality checks (ruff, mypy, bandit) +./deploy.sh test # Run full test suite with coverage +./deploy.sh prod # Deploy to Kubernetes with Helm +./deploy.sh prod --dry-run # Validate Helm templates without applying +./deploy.sh status # Show running services +./deploy.sh logs backend # Tail logs for a specific service +``` + +The script abstracts away the differences between environments. For local development it orchestrates Docker Compose, +while for production it builds images, imports them to the container runtime, and runs Helm with appropriate values. + +## Local development + +Local development uses Docker Compose to spin up the entire stack on your machine. The compose file defines all services +with health checks and dependency ordering, so containers start in the correct sequence. + +```bash +./deploy.sh dev +``` + +This brings up MongoDB, Redis, Kafka with Zookeeper and Schema Registry, all seven workers, the backend API, and the +frontend. Two initialization containers run automatically: `kafka-init` creates required Kafka topics, and `user-seed` +populates the database with default user accounts. + +Once the stack is running, you can access the services at their default ports. + +| Service | URL | +|--------------------|------------------------| +| Frontend | https://localhost:5001 | +| Backend API | https://localhost:443 | +| Kafdrop (Kafka UI) | http://localhost:9000 | +| Jaeger (Tracing) | http://localhost:16686 | +| Grafana | http://localhost:3000 | + +The default credentials created by the seed job are `user` / `user123` for a regular account and `admin` / `admin123` +for an administrator. You can override these via environment variables if needed. + +```bash +DEFAULT_USER_PASSWORD=mypass ADMIN_USER_PASSWORD=myadmin ./deploy.sh dev +``` + +Hot reloading works for the backend since the source directory is mounted into the container. Changes to Python files +trigger Uvicorn to restart automatically. The frontend runs its own dev server with similar behavior. + +To stop everything and clean up volumes: + +```bash +./deploy.sh down +docker compose down -v # Also removes persistent volumes +``` + +### Running tests locally + +The `test` command runs the full integration and unit test suite: + +```bash +./deploy.sh test +``` + +This builds images, starts services, waits for the backend health endpoint using curl's built-in retry mechanism, runs +pytest with coverage reporting, then tears down the stack. The curl retry approach is cleaner than shell loops and +avoids issues with Docker Compose's `--wait` flag (which fails on init containers that exit after completion). Key services define healthchecks in `docker-compose.yaml`: + +| Service | Healthcheck | +|-----------------|-----------------------------------------------| +| MongoDB | `mongosh ping` | +| Redis | `redis-cli ping` | +| Backend | `curl /api/v1/health/live` | +| Kafka | `kafka-broker-api-versions` | +| Schema Registry | `curl /config` | +| Zookeeper | `echo ruok \| nc localhost 2181 \| grep imok` | + +Services without explicit healthchecks (workers, Grafana, Kafdrop) are considered "started" when their container is +running. The test suite doesn't require worker containers since tests instantiate worker classes directly. + +## Kubernetes deployment + +Production deployment targets Kubernetes using a Helm chart that packages all manifests and configuration. The chart +lives in `helm/integr8scode/` and includes templates for every component of the stack. + +### Prerequisites + +Before deploying, ensure you have Helm 3.x installed and kubectl configured to talk to your cluster. If you're using +K3s, the deploy script handles image import automatically. For other distributions, you'll need to push images to a +registry and update the image references in your values file. + +### Chart structure + +The Helm chart organizes templates by function. + +``` +helm/integr8scode/ +├── Chart.yaml # Chart metadata and dependencies +├── values.yaml # Default configuration +├── values-prod.yaml # Production overrides +├── templates/ +│ ├── _helpers.tpl # Template functions +│ ├── NOTES.txt # Post-install message +│ ├── namespace.yaml +│ ├── rbac/ # ServiceAccount, Role, RoleBinding +│ ├── secrets/ # Kubeconfig and Kafka JAAS +│ ├── configmaps/ # Environment variables +│ ├── infrastructure/ # Zookeeper, Kafka, Schema Registry, Jaeger +│ ├── app/ # Backend and Frontend deployments +│ ├── workers/ # All seven worker deployments +│ └── jobs/ # Kafka topic init and user seed +└── charts/ # Downloaded sub-charts (Redis, MongoDB) +``` + +The chart uses Bitnami sub-charts for Redis and MongoDB since they handle persistence, health checks, and configuration +well. Kafka uses custom templates instead of the Bitnami chart because Confluent images require a specific workaround +for Kubernetes environment variables. Kubernetes automatically creates environment variables like +`KAFKA_PORT=tcp://10.0.0.1:29092` for services, which conflicts with Confluent's expectation of a numeric port. The +templates include an `unset KAFKA_PORT` command in the container startup to avoid this collision. + +### Running a deployment + +The simplest deployment uses default values, which work for development and testing clusters. + +```bash +./deploy.sh prod +``` + +This builds the Docker images, imports them to K3s (if available), updates Helm dependencies to download the Redis and +MongoDB sub-charts, creates the namespace, and runs `helm upgrade --install`. The `--wait` flag ensures the command +blocks until all pods are ready. + +For production environments, pass additional flags to set secure passwords. + +```bash +./deploy.sh prod --prod \ + --set userSeed.defaultUserPassword=secure-user-pass \ + --set userSeed.adminUserPassword=secure-admin-pass \ + --set mongodb.auth.rootPassword=mongo-root-pass +``` + +The `--prod` flag tells the script to use `values-prod.yaml`, which increases replica counts, resource limits, and +enables MongoDB authentication. Without the password flags, the user seed job will fail since the production values +intentionally leave passwords empty to force explicit configuration. + +To validate templates without applying anything: + +```bash +./deploy.sh prod --dry-run +``` + +This renders the templates and prints what would be applied, useful for catching configuration errors before they hit +the cluster. + +### Configuration + +The `values.yaml` file contains all configurable options with comments explaining each setting. Key sections include +global settings, image references, resource limits, and infrastructure configuration. + +Environment variables shared across services live in the `env` section and get rendered into a ConfigMap. +Service-specific overrides go in their respective sections. For example, to increase backend replicas and memory: + +```yaml +backend: + replicas: 3 + resources: + limits: + memory: "2Gi" +``` + +Worker configuration follows a similar pattern. Each worker has its own section under `workers` where you can enable or +disable it, set replicas, and override the default command. + +```yaml +workers: + k8sWorker: + enabled: true + replicas: 2 + dlqProcessor: + enabled: false # Disable if not needed +``` + +The infrastructure section controls Confluent platform components. You can adjust heap sizes, resource limits, and +enable or disable optional services like Jaeger. + +```yaml +infrastructure: + kafka: + heapOpts: "-Xms1G -Xmx1G" + resources: + limits: + memory: "2Gi" + jaeger: + enabled: false # Disable tracing in resource-constrained environments +``` + +### Post-install jobs + +Two Helm hooks run after the main deployment completes. The kafka-init job waits for Kafka and Schema Registry to become +healthy, then creates all required topics using the `scripts/create_topics.py` module. Topics are created with the +prefix defined in settings (default `pref`) to avoid conflicts with Kubernetes-generated environment variables. + +The user-seed job waits for MongoDB, then runs `scripts/seed_users.py` to create the default and admin users. If users +already exist, the script updates their roles without creating duplicates, making it safe to run on upgrades. + +Both jobs have a hook-weight that controls ordering. Kafka init runs first (weight 5), followed by user seed (weight +10). The `before-hook-creation` delete policy ensures old jobs are cleaned up before new ones run, preventing conflicts +from previous releases. + +### Accessing deployed services + +After deployment, services are only accessible within the cluster by default. Use kubectl port-forward to access them +locally. + +```bash +kubectl port-forward -n integr8scode svc/integr8scode-backend 8443:443 +kubectl port-forward -n integr8scode svc/integr8scode-frontend 5001:5001 +``` + +For production exposure, enable the ingress section in your values file and configure it for your ingress controller. +The chart supports standard Kubernetes ingress annotations for TLS termination and path routing. + +### Monitoring the deployment + +Check pod status and logs using standard kubectl commands. + +```bash +kubectl get pods -n integr8scode +kubectl logs -n integr8scode -l app.kubernetes.io/component=backend +kubectl logs -n integr8scode -l app.kubernetes.io/component=coordinator +``` + +The deploy script's `status` command shows both Docker Compose and Kubernetes status in one view. + +```bash +./deploy.sh status +``` + +### Rollback and uninstall + +Helm maintains release history, so you can roll back to a previous version if something goes wrong. + +```bash +helm rollback integr8scode 1 -n integr8scode +``` + +To completely remove the deployment: + +```bash +helm uninstall integr8scode -n integr8scode +kubectl delete namespace integr8scode +``` + +This removes all resources created by the chart. Persistent volume claims for MongoDB and Redis may remain depending on +your storage class's reclaim policy. + +## Troubleshooting + +A few issues come up regularly during deployment. + +### Kafka topic errors + +If workers log errors about unknown topics, the kafka-init job may have failed or topics were created without the +expected prefix. Check the job logs and verify topics exist with the correct names. + +```bash +kubectl logs -n integr8scode job/integr8scode-kafka-init +kubectl exec -n integr8scode integr8scode-kafka-0 -- kafka-topics --list --bootstrap-server localhost:29092 +``` + +Topics should be prefixed (e.g., `prefexecution_events` not `execution_events`). If they're missing the prefix, the +`KAFKA_TOPIC_PREFIX` setting wasn't applied during topic creation. + +### Port conflicts with Confluent images + +Confluent containers may fail to start with errors about invalid port formats. This happens when Kubernetes environment +variables like `KAFKA_PORT=tcp://...` override the expected numeric values. The chart templates include +`unset KAFKA_PORT` and similar commands, but if you're customizing the deployment, ensure these remain in place. + +### Image pull failures + +If pods stay in ImagePullBackOff, the images aren't available to the cluster. For K3s, the deploy script imports images +automatically. For other distributions, use the pre-built images from GitHub Container Registry (see below) or push to +your own registry and update `values.yaml` with the correct repository and tag. + +```yaml +images: + backend: + repository: your-registry.com/integr8scode-backend + tag: v1.0.0 +global: + imagePullPolicy: Always +``` + +### MongoDB authentication + +When `mongodb.auth.enabled` is true (the default in values-prod.yaml), all connections must authenticate. The chart +constructs the MongoDB URL with credentials from the values file. If you're seeing authentication errors, verify the +password is set correctly and matches what MongoDB was initialized with. + +```bash +kubectl get secret -n integr8scode integr8scode-mongodb -o jsonpath='{.data.mongodb-root-password}' | base64 -d +``` + +### Resource constraints + +Workers may get OOMKilled or throttled if resource limits are too low for your workload. The default values are +conservative to work on small clusters. For production, increase limits based on observed usage. + +```yaml +workers: + common: + resources: + limits: + memory: "1Gi" + cpu: "1000m" +``` + +Monitor resource usage with kubectl top or your cluster's metrics solution to right-size the limits. + +## Pre-built images + +For production deployments, you can skip the local build step entirely by using pre-built images from GitHub Container +Registry. The CI pipeline automatically builds and pushes images on every merge to main. + +### Using registry images + +The `--prod` flag configures the deployment to pull images from `ghcr.io/hardmax71/integr8scode/`: + +```bash +./deploy.sh prod --prod \ + --set userSeed.defaultUserPassword=secure-pass \ + --set userSeed.adminUserPassword=secure-admin +``` + +This skips the local Docker build and tells Kubernetes to pull images from the registry. The `values-prod.yaml` file +sets `imagePullPolicy: IfNotPresent`, so images are cached locally after the first pull. + +### Available tags + +Each push to main produces multiple tags: + +| Tag | Description | +|---------------|------------------------------------| +| `latest` | Most recent build from main branch | +| `sha-abc1234` | Specific commit SHA | +| `v1.0.0` | Release version (from git tags) | + +For production, pin to a specific SHA or version rather than `latest`: + +```yaml +images: + backend: + repository: ghcr.io/hardmax71/integr8scode/backend + tag: sha-abc1234 +``` + +### Hybrid approach + +If you need production resource limits but want to build locally (for testing changes before pushing): + +```bash +./deploy.sh prod --prod --local +``` + +The `--local` flag forces a local build even when using `values-prod.yaml`. + +### CI/CD integration + +The GitHub Actions workflow in `.github/workflows/docker.yml` handles image building and publishing. On every push to +main, it builds the base, backend, and frontend images, scans them with Trivy for vulnerabilities, and pushes to +ghcr.io. +Pull requests build and scan but don't push, ensuring only tested code reaches the registry. diff --git a/docs/operations/nginx-configuration.md b/docs/operations/nginx-configuration.md index dd56b352..ad606668 100644 --- a/docs/operations/nginx-configuration.md +++ b/docs/operations/nginx-configuration.md @@ -1,15 +1,15 @@ # Nginx Configuration The frontend uses Nginx as a reverse proxy and static file server. This document explains the configuration in -`frontend/nginx.conf`. +`frontend/nginx.conf.template`. ## Architecture ```mermaid flowchart LR Browser --> Nginx - Nginx -->|/api/*| Backend[Backend :443] - Nginx -->|static files| Static[/usr/share/nginx/html] + Nginx -->|"/api/*"| Backend["Backend :443"] + Nginx -->|"static files"| Static["Static files"] ``` Nginx serves two purposes: @@ -192,16 +192,32 @@ and lets the Svelte router handle the path. ## Deployment -The nginx.conf is copied into the container during build: +The nginx configuration uses environment variable substitution via the official nginx Docker image's built-in `envsubst` +feature. The template file is processed at container startup, allowing the same image to work in different environments. ```dockerfile # frontend/Dockerfile.prod FROM nginx:alpine COPY --from=builder /app/public /usr/share/nginx/html -COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY nginx.conf.template /etc/nginx/templates/default.conf.template ``` -To apply changes: +The nginx image automatically processes files in `/etc/nginx/templates/*.template` and outputs the result to +`/etc/nginx/conf.d/` with the `.template` suffix removed. + +### Environment variables + +The template uses `${VARIABLE_NAME}` syntax for substitution. Currently, only `BACKEND_URL` is templated: + +| Variable | Purpose | Example | +|---------------|-----------------------------------|-----------------------| +| `BACKEND_URL` | Backend service URL for API proxy | `https://backend:443` | + +Set this via docker-compose environment section or Kubernetes deployment env vars. + +### Rebuilding + +To apply nginx configuration changes: ```bash docker build --no-cache -t integr8scode-frontend:latest \ diff --git a/docs/security/policies.md b/docs/security/policies.md index 30f78766..f77edd97 100644 --- a/docs/security/policies.md +++ b/docs/security/policies.md @@ -1,24 +1,53 @@ -# Network isolation +# Network Isolation -Executor pods run user code in a hardened environment: non-root user, no capabilities, read-only root filesystem, no service account token, and DNS disabled. Network isolation is enforced via a static CiliumNetworkPolicy that denies all egress traffic from executor pods. +Executor pods run user code in a hardened environment with strict security controls. The pod builder in `backend/app/services/k8s_worker/pod_builder.py` enforces these at pod creation time. -## Setup +## Container Security Context -The deny-all Cilium policy is defined in `backend/k8s/policies/executor-deny-all-cnp.yaml`. Apply it using the setup script: +Each executor container runs with: -```bash -./backend/scripts/setup_k8s.sh +- Non-root user (UID/GID 1000) +- Read-only root filesystem +- No privilege escalation allowed +- All Linux capabilities dropped +- RuntimeDefault seccomp profile + +The pod spec also sets `automount_service_account_token: false`, preventing containers from accessing the Kubernetes API. + +## Network Policy + +Network isolation uses a standard Kubernetes NetworkPolicy that denies all ingress and egress traffic from executor pods. The policy is applied during cluster setup. + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: executor-deny-all + namespace: integr8scode +spec: + podSelector: + matchLabels: + app: integr8s + component: executor + policyTypes: + - Ingress + - Egress ``` -This creates the namespace if needed and applies the CiliumNetworkPolicy there. Using the `default` namespace is forbidden — always run executor pods in a dedicated namespace. +This policy matches pods with labels `app=integr8s` and `component=executor`, which the pod builder applies to all executor pods. -## Pod labels +## Setup -The policy matches pods with these labels: +The network policy and RBAC resources are created by the setup script during initial deployment: -- `app=integr8s` -- `component=executor` +```bash +./cert-generator/setup-k8s.sh +``` + +This script creates the `integr8scode` namespace, a ServiceAccount with appropriate permissions, and the deny-all NetworkPolicy. For Helm deployments, only the ServiceAccount is managed by templates in the chart (`helm/integr8scode/templates/rbac/`). The namespace is created by `deploy.sh` before Helm runs, and the NetworkPolicy must be applied separately using the setup script or manually. ## Notes -Cilium must be installed with policy enforcement active. To allow in-cluster traffic later (for example, accessing internal services), modify the egress rules in the CNP to include `toEntities: ["cluster"]`. +The NetworkPolicy requires a CNI plugin that supports network policies (Calico, Cilium, Weave Net, etc). K3s includes Flannel by default, which does not enforce policies. For production, install a policy-capable CNI or use K3s with the `--flannel-backend=none` flag and a separate CNI. + +To allow specific egress traffic (for example, to internal services), create an additional NetworkPolicy with explicit egress rules rather than modifying the deny-all policy. diff --git a/frontend/Dockerfile.prod b/frontend/Dockerfile.prod index 54c1cc34..c5e21136 100644 --- a/frontend/Dockerfile.prod +++ b/frontend/Dockerfile.prod @@ -15,8 +15,14 @@ FROM nginx:alpine # Copy built static files COPY --from=builder /app/public /usr/share/nginx/html -# Copy nginx config -COPY nginx.conf /etc/nginx/conf.d/default.conf +# Copy nginx config template (envsubst runs at container startup) +# The nginx image automatically processes /etc/nginx/templates/*.template +# and outputs to /etc/nginx/conf.d/ with the .template suffix removed +COPY nginx.conf.template /etc/nginx/templates/default.conf.template + +# Create writable directories for nginx (required for read-only root filesystem) +RUN mkdir -p /var/cache/nginx /var/run && \ + chown -R nginx:nginx /var/cache/nginx /var/run EXPOSE 5001 diff --git a/frontend/nginx.conf b/frontend/nginx.conf.template similarity index 95% rename from frontend/nginx.conf rename to frontend/nginx.conf.template index e5717fb1..2359bf31 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf.template @@ -13,30 +13,30 @@ server { gzip_disable "msie6"; location /api/ { - proxy_pass https://backend:443; + proxy_pass ${BACKEND_URL}; proxy_ssl_verify off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - + # Forward cookies proxy_pass_request_headers on; proxy_set_header Cookie $http_cookie; - + # SSE specific settings for /api/v1/events/ location ~ ^/api/v1/events/ { - proxy_pass https://backend:443; + proxy_pass ${BACKEND_URL}; proxy_ssl_verify off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - + # Forward cookies proxy_pass_request_headers on; proxy_set_header Cookie $http_cookie; - + # SSE configuration proxy_set_header Connection ''; proxy_http_version 1.1; @@ -44,10 +44,10 @@ server { proxy_cache off; proxy_read_timeout 86400s; proxy_send_timeout 86400s; - + # Disable buffering for SSE proxy_set_header X-Accel-Buffering no; - + # CORS headers for SSE add_header Cache-Control 'no-cache'; add_header X-Accel-Buffering 'no'; diff --git a/helm/integr8scode/.helmignore b/helm/integr8scode/.helmignore new file mode 100644 index 00000000..b3e48029 --- /dev/null +++ b/helm/integr8scode/.helmignore @@ -0,0 +1,39 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ +# Helm +charts/*.tgz +# Testing +tests/ +# CI/CD +.github/ +.gitlab-ci.yml +.circleci/ +Jenkinsfile +# Documentation +*.md +!templates/NOTES.txt +# Development +Makefile +Dockerfile* +docker-compose* diff --git a/helm/integr8scode/Chart.yaml b/helm/integr8scode/Chart.yaml new file mode 100644 index 00000000..96125a88 --- /dev/null +++ b/helm/integr8scode/Chart.yaml @@ -0,0 +1,38 @@ +apiVersion: v2 +name: integr8scode +description: A Helm chart for Integr8sCode - Kubernetes-based code execution platform +type: application +version: 1.0.0 +appVersion: "1.0.0" + +keywords: + - kubernetes + - code-execution + - kafka + - event-driven + - microservices + +home: https://github.com/HardMax71/Integr8sCode +sources: + - https://github.com/HardMax71/Integr8sCode + +maintainers: + - name: Integr8sCode Team + +dependencies: + # Redis using Bitnami chart + - name: redis + version: "20.6.3" + repository: "https://charts.bitnami.com/bitnami" + condition: redis.enabled + + # MongoDB using Bitnami chart + - name: mongodb + version: "16.4.3" + repository: "https://charts.bitnami.com/bitnami" + condition: mongodb.enabled + +# NOTE: Kafka NOT using Bitnami sub-chart +# Reason: Confluent images have hardcoded port detection that conflicts +# with K8s auto-generated KAFKA_PORT env vars (tcp://IP:PORT format). +# Custom templates with `unset KAFKA_PORT` workaround are required. diff --git a/helm/integr8scode/charts/.gitkeep b/helm/integr8scode/charts/.gitkeep new file mode 100644 index 00000000..485d1a21 --- /dev/null +++ b/helm/integr8scode/charts/.gitkeep @@ -0,0 +1,2 @@ +# This directory is populated by `helm dependency update` +# Downloaded sub-charts (*.tgz) are gitignored diff --git a/helm/integr8scode/templates/NOTES.txt b/helm/integr8scode/templates/NOTES.txt new file mode 100644 index 00000000..dfba105a --- /dev/null +++ b/helm/integr8scode/templates/NOTES.txt @@ -0,0 +1,111 @@ +=========================================================================== + ___ _ ___ ____ _ +|_ _|_ __ | |_ ___ __ _ _ __ ( _ )___ / ___|___ __| | ___ + | || '_ \| __/ _ \/ _` | '__| / _ \___ \| | / _ \ / _` |/ _ \ + | || | | | || __/ (_| | | | (_) |__) | |__| (_) | (_| | __/ +|___|_| |_|\__\___|\__, |_| \___/____/ \____\___/ \__,_|\___| + |___/ +=========================================================================== + +Thank you for installing {{ .Chart.Name }}! + +Release: {{ .Release.Name }} +Namespace: {{ .Release.Namespace }} +Version: {{ .Chart.AppVersion }} + +=========================================================================== +DEPLOYMENT STATUS +=========================================================================== + +To check the status of your deployment: + + kubectl get pods -n {{ .Release.Namespace }} + +To view logs for a specific component: + + # Backend + kubectl logs -n {{ .Release.Namespace }} -l app.kubernetes.io/component=backend + + # Workers + kubectl logs -n {{ .Release.Namespace }} -l app.kubernetes.io/component=k8s-worker + kubectl logs -n {{ .Release.Namespace }} -l app.kubernetes.io/component=pod-monitor + kubectl logs -n {{ .Release.Namespace }} -l app.kubernetes.io/component=result-processor + +=========================================================================== +SERVICES +=========================================================================== + +Backend API: +{{- if contains "LoadBalancer" .Values.backend.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + Watch the status: + kubectl get svc -n {{ .Release.Namespace }} {{ include "integr8scode.fullname" . }}-backend -w +{{- else if contains "NodePort" .Values.backend.service.type }} + export NODE_PORT=$(kubectl get svc -n {{ .Release.Namespace }} {{ include "integr8scode.fullname" . }}-backend -o jsonpath="{.spec.ports[0].nodePort}") + export NODE_IP=$(kubectl get nodes -o jsonpath="{.items[0].status.addresses[0].address}") + echo "Backend: https://$NODE_IP:$NODE_PORT" +{{- else }} + kubectl port-forward -n {{ .Release.Namespace }} svc/{{ include "integr8scode.fullname" . }}-backend 8443:443 + echo "Backend: https://localhost:8443" +{{- end }} + +Frontend: + kubectl port-forward -n {{ .Release.Namespace }} svc/{{ include "integr8scode.fullname" . }}-frontend 5001:5001 + echo "Frontend: http://localhost:5001" + +{{- if .Values.infrastructure.jaeger.enabled }} + +Jaeger UI (Tracing): + kubectl port-forward -n {{ .Release.Namespace }} svc/{{ include "integr8scode.fullname" . }}-jaeger 16686:16686 + echo "Jaeger: http://localhost:16686" +{{- end }} + +=========================================================================== +KAFKA TOPICS +=========================================================================== + +{{- if .Values.kafkaInit.enabled }} + +Kafka topics should be automatically created by the post-install hook. +To verify topics were created: + + kubectl exec -n {{ .Release.Namespace }} {{ include "integr8scode.fullname" . }}-kafka-0 -- \ + kafka-topics --bootstrap-server localhost:29092 --list + +{{- else }} + +Kafka topic auto-creation is disabled. Please create topics manually: + + kubectl exec -n {{ .Release.Namespace }} {{ include "integr8scode.fullname" . }}-kafka-0 -- \ + kafka-topics --bootstrap-server localhost:29092 --create --topic execution_events --partitions 10 --replication-factor 1 + +{{- end }} + +=========================================================================== +TROUBLESHOOTING +=========================================================================== + +If pods are not starting: +1. Check events: kubectl get events -n {{ .Release.Namespace }} --sort-by='.lastTimestamp' +2. Check pod status: kubectl describe pod -n {{ .Release.Namespace }} +3. Check logs: kubectl logs -n {{ .Release.Namespace }} + +Common issues: +- Image not found: Ensure images are imported to K3s containerd +- RBAC errors: Check ServiceAccount and Role bindings +- Kafka connection: Wait for Kafka to be ready before workers start + +=========================================================================== +UPGRADE / ROLLBACK +=========================================================================== + +To upgrade: + helm upgrade {{ .Release.Name }} ./helm/integr8scode -n {{ .Release.Namespace }} + +To rollback: + helm rollback {{ .Release.Name }} 1 -n {{ .Release.Namespace }} + +To uninstall: + helm uninstall {{ .Release.Name }} -n {{ .Release.Namespace }} + +=========================================================================== diff --git a/helm/integr8scode/templates/_helpers.tpl b/helm/integr8scode/templates/_helpers.tpl new file mode 100644 index 00000000..e9c4122b --- /dev/null +++ b/helm/integr8scode/templates/_helpers.tpl @@ -0,0 +1,152 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "integr8scode.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "integr8scode.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "integr8scode.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "integr8scode.labels" -}} +helm.sh/chart: {{ include "integr8scode.chart" . }} +{{ include "integr8scode.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: integr8scode +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "integr8scode.selectorLabels" -}} +app.kubernetes.io/name: {{ include "integr8scode.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "integr8scode.serviceAccountName" -}} +{{- if .Values.rbac.create }} +{{- default (include "integr8scode.fullname" .) .Values.rbac.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.rbac.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Backend image reference +*/}} +{{- define "integr8scode.backendImage" -}} +{{- printf "%s:%s" .Values.images.backend.repository .Values.images.backend.tag }} +{{- end }} + +{{/* +Frontend image reference +*/}} +{{- define "integr8scode.frontendImage" -}} +{{- printf "%s:%s" .Values.images.frontend.repository .Values.images.frontend.tag }} +{{- end }} + +{{/* +Redis host - uses Bitnami sub-chart naming convention +*/}} +{{- define "integr8scode.redisHost" -}} +{{- if .Values.redis.enabled }} +{{- printf "%s-redis-master" .Release.Name }} +{{- else }} +{{- .Values.externalRedis.host }} +{{- end }} +{{- end }} + +{{/* +MongoDB host - uses Bitnami sub-chart naming convention +*/}} +{{- define "integr8scode.mongodbHost" -}} +{{- if .Values.mongodb.enabled }} +{{- printf "%s-mongodb" .Release.Name }} +{{- else }} +{{- .Values.externalMongodb.host }} +{{- end }} +{{- end }} + +{{/* +MongoDB URL - with URL-encoded credentials (RFC 3986) +*/}} +{{- define "integr8scode.mongodbUrl" -}} +{{- if .Values.mongodb.enabled }} +{{- if .Values.mongodb.auth.enabled }} +{{- printf "mongodb://%s:%s@%s:27017/integr8scode?authSource=admin" (.Values.mongodb.auth.rootUser | urlquery) (.Values.mongodb.auth.rootPassword | urlquery) (include "integr8scode.mongodbHost" .) }} +{{- else }} +{{- printf "mongodb://%s:27017/integr8scode" (include "integr8scode.mongodbHost" .) }} +{{- end }} +{{- else }} +{{- .Values.externalMongodb.url }} +{{- end }} +{{- end }} + +{{/* +Kafka bootstrap servers +*/}} +{{- define "integr8scode.kafkaBootstrapServers" -}} +{{- printf "%s-kafka:29092" .Release.Name }} +{{- end }} + +{{/* +Schema Registry URL +*/}} +{{- define "integr8scode.schemaRegistryUrl" -}} +{{- printf "http://%s-schema-registry:8081" .Release.Name }} +{{- end }} + +{{/* +Jaeger host +*/}} +{{- define "integr8scode.jaegerHost" -}} +{{- printf "%s-jaeger" .Release.Name }} +{{- end }} + +{{/* +Generate worker labels +*/}} +{{- define "integr8scode.workerLabels" -}} +{{ include "integr8scode.labels" .root }} +app.kubernetes.io/component: {{ .name }} +{{- end }} + +{{/* +Generate worker selector labels +*/}} +{{- define "integr8scode.workerSelectorLabels" -}} +app.kubernetes.io/name: {{ include "integr8scode.name" .root }} +app.kubernetes.io/instance: {{ .root.Release.Name }} +app.kubernetes.io/component: {{ .name }} +{{- end }} diff --git a/helm/integr8scode/templates/app/backend-deployment.yaml b/helm/integr8scode/templates/app/backend-deployment.yaml new file mode 100644 index 00000000..d45faceb --- /dev/null +++ b/helm/integr8scode/templates/app/backend-deployment.yaml @@ -0,0 +1,90 @@ +{{- if .Values.backend.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "integr8scode.fullname" . }}-backend + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: backend +spec: + replicas: {{ .Values.backend.replicas | default 1 }} + selector: + matchLabels: + app: {{ include "integr8scode.fullname" . }}-backend + template: + metadata: + labels: + app: {{ include "integr8scode.fullname" . }}-backend + {{- include "integr8scode.labels" . | nindent 8 }} + app.kubernetes.io/component: backend + annotations: + checksum/config: {{ include (print $.Template.BasePath "/secrets/env-secret.yaml") . | sha256sum }} + spec: + serviceAccountName: {{ include "integr8scode.serviceAccountName" . }} + containers: + - name: backend + image: {{ include "integr8scode.backendImage" . }} + imagePullPolicy: {{ .Values.global.imagePullPolicy | default "IfNotPresent" }} + ports: + - containerPort: 443 + name: https + - containerPort: 9090 + name: metrics + envFrom: + - secretRef: + name: {{ include "integr8scode.fullname" . }}-env + env: + - name: KUBECONFIG + value: "/app/kubeconfig.yaml" + {{- if .Values.backend.extraEnv }} + {{- if .Values.backend.extraEnv.SECRET_KEY }} + - name: SECRET_KEY + value: {{ .Values.backend.extraEnv.SECRET_KEY | quote }} + {{- end }} + {{- if .Values.backend.extraEnv.WEB_CONCURRENCY }} + - name: WEB_CONCURRENCY + value: {{ .Values.backend.extraEnv.WEB_CONCURRENCY | quote }} + {{- end }} + {{- end }} + resources: + {{- toYaml .Values.backend.resources | nindent 10 }} + volumeMounts: + - name: kubeconfig + mountPath: /app/kubeconfig.yaml + subPath: kubeconfig.yaml + readOnly: true + {{- if .Values.backend.ssl.enabled }} + - name: ssl-certs + mountPath: /app/certs + readOnly: true + {{- end }} + # Correct health probe path (fixes 405 error) + livenessProbe: + httpGet: + path: {{ .Values.backend.healthProbe.path | default "/api/v1/health/live" }} + port: 443 + scheme: {{ .Values.backend.healthProbe.scheme | default "HTTPS" }} + initialDelaySeconds: 30 + periodSeconds: 30 + failureThreshold: 3 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: {{ .Values.backend.healthProbe.readyPath | default "/api/v1/health/ready" }} + port: 443 + scheme: {{ .Values.backend.healthProbe.scheme | default "HTTPS" }} + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 3 + timeoutSeconds: 5 + volumes: + - name: kubeconfig + secret: + secretName: {{ .Values.kubeconfig.secretName }} + {{- if .Values.backend.ssl.enabled }} + - name: ssl-certs + secret: + secretName: {{ .Values.backend.ssl.secretName }} + {{- end }} +{{- end }} diff --git a/helm/integr8scode/templates/app/backend-service.yaml b/helm/integr8scode/templates/app/backend-service.yaml new file mode 100644 index 00000000..e6ac1789 --- /dev/null +++ b/helm/integr8scode/templates/app/backend-service.yaml @@ -0,0 +1,21 @@ +{{- if .Values.backend.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "integr8scode.fullname" . }}-backend + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: backend +spec: + type: {{ .Values.backend.service.type | default "ClusterIP" }} + selector: + app: {{ include "integr8scode.fullname" . }}-backend + ports: + - name: https + port: {{ .Values.backend.service.port | default 443 }} + targetPort: 443 + - name: metrics + port: 9090 + targetPort: 9090 +{{- end }} diff --git a/helm/integr8scode/templates/app/frontend-deployment.yaml b/helm/integr8scode/templates/app/frontend-deployment.yaml new file mode 100644 index 00000000..6ac1ebb9 --- /dev/null +++ b/helm/integr8scode/templates/app/frontend-deployment.yaml @@ -0,0 +1,51 @@ +{{- if .Values.frontend.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "integr8scode.fullname" . }}-frontend + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: frontend +spec: + replicas: {{ .Values.frontend.replicas | default 1 }} + selector: + matchLabels: + app: {{ include "integr8scode.fullname" . }}-frontend + template: + metadata: + labels: + app: {{ include "integr8scode.fullname" . }}-frontend + {{- include "integr8scode.labels" . | nindent 8 }} + app.kubernetes.io/component: frontend + spec: + automountServiceAccountToken: false + containers: + - name: frontend + image: {{ include "integr8scode.frontendImage" . }} + imagePullPolicy: {{ .Values.global.imagePullPolicy | default "IfNotPresent" }} + ports: + - containerPort: {{ .Values.frontend.containerPort | default 5001 }} + name: http + env: + # BACKEND_URL is used by nginx template (envsubst at container startup) + - name: BACKEND_URL + value: "https://{{ include "integr8scode.fullname" . }}-backend:443" + # VITE_BACKEND_URL is baked into JS at build time (not used in prod nginx image) + - name: VITE_BACKEND_URL + value: "https://{{ include "integr8scode.fullname" . }}-backend:443" + resources: + {{- toYaml .Values.frontend.resources | nindent 10 }} + livenessProbe: + httpGet: + path: / + port: {{ .Values.frontend.containerPort | default 5001 }} + initialDelaySeconds: 10 + periodSeconds: 30 + readinessProbe: + httpGet: + path: / + port: {{ .Values.frontend.containerPort | default 5001 }} + initialDelaySeconds: 5 + periodSeconds: 10 +{{- end }} diff --git a/helm/integr8scode/templates/app/frontend-service.yaml b/helm/integr8scode/templates/app/frontend-service.yaml new file mode 100644 index 00000000..83fb19df --- /dev/null +++ b/helm/integr8scode/templates/app/frontend-service.yaml @@ -0,0 +1,18 @@ +{{- if .Values.frontend.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "integr8scode.fullname" . }}-frontend + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: frontend +spec: + type: {{ .Values.frontend.service.type | default "ClusterIP" }} + selector: + app: {{ include "integr8scode.fullname" . }}-frontend + ports: + - name: http + port: {{ .Values.frontend.service.port | default 5001 }} + targetPort: {{ .Values.frontend.containerPort | default 5001 }} +{{- end }} diff --git a/helm/integr8scode/templates/configmaps/zookeeper-config.yaml b/helm/integr8scode/templates/configmaps/zookeeper-config.yaml new file mode 100644 index 00000000..ec63ab83 --- /dev/null +++ b/helm/integr8scode/templates/configmaps/zookeeper-config.yaml @@ -0,0 +1,28 @@ +{{- if .Values.infrastructure.zookeeper.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "integr8scode.fullname" . }}-zookeeper-config + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} +data: + zoo.cfg: | + tickTime=2000 + dataDir=/var/lib/zookeeper/data + dataLogDir=/var/lib/zookeeper/log + clientPort=2181 + serverCnxnFactory=org.apache.zookeeper.server.NettyServerCnxnFactory + authProvider.1=org.apache.zookeeper.server.auth.DigestAuthenticationProvider + autopurge.snapRetainCount=3 + autopurge.purgeInterval=24 + admin.enableServer=false + admin.serverPort=0 + 4lw.commands.whitelist=* + log4j.properties: | + log4j.rootLogger=WARN, CONSOLE + log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender + log4j.appender.CONSOLE.Threshold=WARN + log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout + log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n +{{- end }} diff --git a/helm/integr8scode/templates/infrastructure/jaeger.yaml b/helm/integr8scode/templates/infrastructure/jaeger.yaml new file mode 100644 index 00000000..49fccb18 --- /dev/null +++ b/helm/integr8scode/templates/infrastructure/jaeger.yaml @@ -0,0 +1,75 @@ +{{- if .Values.infrastructure.jaeger.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "integr8scode.fullname" . }}-jaeger + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: jaeger +spec: + replicas: 1 + selector: + matchLabels: + app: {{ include "integr8scode.fullname" . }}-jaeger + template: + metadata: + labels: + app: {{ include "integr8scode.fullname" . }}-jaeger + {{- include "integr8scode.labels" . | nindent 8 }} + app.kubernetes.io/component: jaeger + spec: + automountServiceAccountToken: false + containers: + - name: jaeger + image: {{ .Values.infrastructure.jaeger.image }} + imagePullPolicy: {{ .Values.global.imagePullPolicy | default "IfNotPresent" }} + ports: + - containerPort: 4317 + name: otlp-grpc + - containerPort: 6831 + name: thrift + protocol: UDP + - containerPort: 16686 + name: ui + env: + - name: COLLECTOR_OTLP_ENABLED + value: "true" + resources: + {{- toYaml .Values.infrastructure.jaeger.resources | nindent 10 }} + readinessProbe: + httpGet: + path: / + port: 16686 + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 16686 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "integr8scode.fullname" . }}-jaeger + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: jaeger +spec: + selector: + app: {{ include "integr8scode.fullname" . }}-jaeger + ports: + - name: otlp-grpc + port: 4317 + targetPort: 4317 + - name: thrift + port: 6831 + targetPort: 6831 + protocol: UDP + - name: ui + port: 16686 + targetPort: 16686 +{{- end }} diff --git a/helm/integr8scode/templates/infrastructure/kafka.yaml b/helm/integr8scode/templates/infrastructure/kafka.yaml new file mode 100644 index 00000000..b4142eba --- /dev/null +++ b/helm/integr8scode/templates/infrastructure/kafka.yaml @@ -0,0 +1,114 @@ +{{- if .Values.infrastructure.kafka.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "integr8scode.fullname" . }}-kafka + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: kafka +spec: + replicas: 1 + selector: + matchLabels: + app: {{ include "integr8scode.fullname" . }}-kafka + template: + metadata: + labels: + app: {{ include "integr8scode.fullname" . }}-kafka + {{- include "integr8scode.labels" . | nindent 8 }} + app.kubernetes.io/component: kafka + spec: + automountServiceAccountToken: false + containers: + - name: kafka + image: {{ .Values.infrastructure.kafka.image }} + imagePullPolicy: {{ .Values.global.imagePullPolicy | default "IfNotPresent" }} + # CRITICAL: Unset KAFKA_PORT to prevent Confluent image conflict + # K8s auto-generates KAFKA_PORT as tcp://IP:PORT format which + # conflicts with Confluent's internal port detection + command: + - bash + - -c + - | + unset KAFKA_PORT + /etc/confluent/docker/run + ports: + - containerPort: 29092 + name: broker + env: + - name: KAFKA_BROKER_ID + value: "1" + - name: KAFKA_ZOOKEEPER_CONNECT + value: "{{ include "integr8scode.fullname" . }}-zookeeper:2181" + - name: KAFKA_LISTENER_SECURITY_PROTOCOL_MAP + value: "PLAINTEXT:PLAINTEXT" + - name: KAFKA_LISTENERS + value: "PLAINTEXT://0.0.0.0:29092" + - name: KAFKA_ADVERTISED_LISTENERS + value: "PLAINTEXT://{{ include "integr8scode.fullname" . }}-kafka:29092" + - name: KAFKA_INTER_BROKER_LISTENER_NAME + value: "PLAINTEXT" + - name: KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR + value: "1" + - name: KAFKA_TRANSACTION_STATE_LOG_MIN_ISR + value: "1" + - name: KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR + value: "1" + - name: KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS + value: "0" + - name: KAFKA_AUTO_CREATE_TOPICS_ENABLE + value: {{ .Values.infrastructure.kafka.autoCreateTopics | default "true" | quote }} + - name: KAFKA_DELETE_TOPIC_ENABLE + value: "true" + - name: KAFKA_OPTS + value: "-Dzookeeper.sasl.client=true -Dzookeeper.sasl.clientconfig=Client -Djava.security.auth.login.config=/etc/kafka/secrets/kafka_jaas.conf" + - name: KAFKA_ZOOKEEPER_SET_ACL + value: "false" + - name: KAFKA_HEAP_OPTS + value: {{ .Values.infrastructure.kafka.heapOpts | default "-Xms256M -Xmx256M" | quote }} + resources: + {{- toYaml .Values.infrastructure.kafka.resources | nindent 10 }} + volumeMounts: + - name: jaas-config + mountPath: /etc/kafka/secrets + readOnly: true + readinessProbe: + exec: + command: + - sh + - -c + - "kafka-broker-api-versions --bootstrap-server localhost:29092" + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 10 + livenessProbe: + exec: + command: + - sh + - -c + - "kafka-broker-api-versions --bootstrap-server localhost:29092" + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 10 + volumes: + - name: jaas-config + secret: + secretName: {{ include "integr8scode.fullname" . }}-kafka-jaas +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "integr8scode.fullname" . }}-kafka + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: kafka +spec: + selector: + app: {{ include "integr8scode.fullname" . }}-kafka + ports: + - name: broker + port: 29092 + targetPort: 29092 +{{- end }} diff --git a/helm/integr8scode/templates/infrastructure/schema-registry.yaml b/helm/integr8scode/templates/infrastructure/schema-registry.yaml new file mode 100644 index 00000000..ee23cad0 --- /dev/null +++ b/helm/integr8scode/templates/infrastructure/schema-registry.yaml @@ -0,0 +1,78 @@ +{{- if .Values.infrastructure.schemaRegistry.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "integr8scode.fullname" . }}-schema-registry + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: schema-registry +spec: + replicas: 1 + selector: + matchLabels: + app: {{ include "integr8scode.fullname" . }}-schema-registry + template: + metadata: + labels: + app: {{ include "integr8scode.fullname" . }}-schema-registry + {{- include "integr8scode.labels" . | nindent 8 }} + app.kubernetes.io/component: schema-registry + spec: + automountServiceAccountToken: false + containers: + - name: schema-registry + image: {{ .Values.infrastructure.schemaRegistry.image }} + imagePullPolicy: {{ .Values.global.imagePullPolicy | default "IfNotPresent" }} + # CRITICAL: Unset SCHEMA_REGISTRY_PORT to prevent Confluent image conflict + command: + - bash + - -c + - | + unset SCHEMA_REGISTRY_PORT + /etc/confluent/docker/run + ports: + - containerPort: 8081 + name: http + env: + - name: SCHEMA_REGISTRY_HOST_NAME + value: "{{ include "integr8scode.fullname" . }}-schema-registry" + - name: SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS + value: "{{ include "integr8scode.fullname" . }}-kafka:29092" + - name: SCHEMA_REGISTRY_LISTENERS + value: "http://0.0.0.0:8081" + - name: SCHEMA_REGISTRY_HEAP_OPTS + value: {{ .Values.infrastructure.schemaRegistry.heapOpts | default "-Xms256M -Xmx256M" | quote }} + resources: + {{- toYaml .Values.infrastructure.schemaRegistry.resources | nindent 10 }} + readinessProbe: + httpGet: + path: /config + port: 8081 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 5 + livenessProbe: + httpGet: + path: /config + port: 8081 + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "integr8scode.fullname" . }}-schema-registry + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: schema-registry +spec: + selector: + app: {{ include "integr8scode.fullname" . }}-schema-registry + ports: + - name: http + port: 8081 + targetPort: 8081 +{{- end }} diff --git a/helm/integr8scode/templates/infrastructure/zookeeper.yaml b/helm/integr8scode/templates/infrastructure/zookeeper.yaml new file mode 100644 index 00000000..b8d6c06d --- /dev/null +++ b/helm/integr8scode/templates/infrastructure/zookeeper.yaml @@ -0,0 +1,99 @@ +{{- if .Values.infrastructure.zookeeper.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "integr8scode.fullname" . }}-zookeeper + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: zookeeper +spec: + replicas: 1 + selector: + matchLabels: + app: {{ include "integr8scode.fullname" . }}-zookeeper + template: + metadata: + labels: + app: {{ include "integr8scode.fullname" . }}-zookeeper + {{- include "integr8scode.labels" . | nindent 8 }} + app.kubernetes.io/component: zookeeper + spec: + automountServiceAccountToken: false + containers: + - name: zookeeper + image: {{ .Values.infrastructure.zookeeper.image }} + imagePullPolicy: {{ .Values.global.imagePullPolicy | default "IfNotPresent" }} + # CRITICAL: Unset ZOOKEEPER_PORT to prevent Confluent image conflict + # K8s auto-generates ZOOKEEPER_PORT as tcp://IP:PORT format + command: + - bash + - -c + - | + unset ZOOKEEPER_PORT + /etc/confluent/docker/run + ports: + - containerPort: 2181 + name: client + env: + - name: ZOOKEEPER_CLIENT_PORT + value: "2181" + - name: ZOOKEEPER_TICK_TIME + value: "2000" + - name: ZOOKEEPER_AUTH_PROVIDER_1 + value: "org.apache.zookeeper.server.auth.DigestAuthenticationProvider" + - name: ZOOKEEPER_SERVER_CNXN_FACTORY + value: "org.apache.zookeeper.server.NettyServerCnxnFactory" + - name: KAFKA_OPTS + value: "-Djava.security.auth.login.config=/etc/kafka/secrets/kafka_jaas.conf" + - name: ZOOKEEPER_4LW_COMMANDS_WHITELIST + value: "*" + - name: KAFKA_HEAP_OPTS + value: {{ .Values.infrastructure.zookeeper.heapOpts | default "-Xms256M -Xmx256M" | quote }} + - name: ZOOKEEPER_LOG4J_ROOT_LOGLEVEL + value: "WARN" + resources: + {{- toYaml .Values.infrastructure.zookeeper.resources | nindent 10 }} + volumeMounts: + - name: jaas-config + mountPath: /etc/kafka/secrets + readOnly: true + readinessProbe: + exec: + command: + - sh + - -c + - "echo ruok | nc localhost 2181 | grep imok" + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + livenessProbe: + exec: + command: + - sh + - -c + - "echo ruok | nc localhost 2181 | grep imok" + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + volumes: + - name: jaas-config + secret: + secretName: {{ include "integr8scode.fullname" . }}-kafka-jaas +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "integr8scode.fullname" . }}-zookeeper + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: zookeeper +spec: + selector: + app: {{ include "integr8scode.fullname" . }}-zookeeper + ports: + - name: client + port: 2181 + targetPort: 2181 +{{- end }} diff --git a/helm/integr8scode/templates/jobs/kafka-init.yaml b/helm/integr8scode/templates/jobs/kafka-init.yaml new file mode 100644 index 00000000..d267e66a --- /dev/null +++ b/helm/integr8scode/templates/jobs/kafka-init.yaml @@ -0,0 +1,73 @@ +{{- if .Values.kafkaInit.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "integr8scode.fullname" . }}-kafka-init + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: kafka-init + annotations: + # Helm hook: Run after install and upgrade, after all other resources are created + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-weight": "5" + # Delete previous job before creating new one to ensure idempotency + "helm.sh/hook-delete-policy": before-hook-creation +spec: + ttlSecondsAfterFinished: {{ .Values.kafkaInit.ttlSecondsAfterFinished | default 300 }} + backoffLimit: {{ .Values.kafkaInit.backoffLimit | default 3 }} + template: + metadata: + labels: + app: {{ include "integr8scode.fullname" . }}-kafka-init + {{- include "integr8scode.labels" . | nindent 8 }} + spec: + automountServiceAccountToken: false + restartPolicy: OnFailure + initContainers: + # Wait for Kafka to be ready before attempting topic creation + - name: wait-for-kafka + image: busybox:1.36 + command: + - sh + - -c + - | + echo "Waiting for Kafka to be ready at {{ include "integr8scode.fullname" . }}-kafka:29092..." + until nc -z {{ include "integr8scode.fullname" . }}-kafka 29092; do + echo "Kafka not ready, waiting 5 seconds..." + sleep 5 + done + echo "Kafka is ready!" + # Wait for Schema Registry to be ready + - name: wait-for-schema-registry + image: busybox:1.36 + command: + - sh + - -c + - | + echo "Waiting for Schema Registry to be ready..." + until wget -q -O- http://{{ include "integr8scode.fullname" . }}-schema-registry:8081/config > /dev/null 2>&1; do + echo "Schema Registry not ready, waiting 5 seconds..." + sleep 5 + done + echo "Schema Registry is ready!" + containers: + - name: kafka-init + image: {{ .Values.kafkaInit.image | default (include "integr8scode.backendImage" .) }} + imagePullPolicy: {{ .Values.global.imagePullPolicy | default "IfNotPresent" }} + command: + - uv + - run + - python + - -m + - scripts.create_topics + env: + - name: KAFKA_BOOTSTRAP_SERVERS + value: {{ include "integr8scode.kafkaBootstrapServers" . | quote }} + - name: SCHEMA_REGISTRY_URL + value: {{ include "integr8scode.schemaRegistryUrl" . | quote }} + - name: LOG_LEVEL + value: "INFO" + resources: + {{- toYaml .Values.kafkaInit.resources | nindent 10 }} +{{- end }} diff --git a/helm/integr8scode/templates/jobs/user-seed.yaml b/helm/integr8scode/templates/jobs/user-seed.yaml new file mode 100644 index 00000000..b6bdb8b6 --- /dev/null +++ b/helm/integr8scode/templates/jobs/user-seed.yaml @@ -0,0 +1,60 @@ +{{- if .Values.userSeed.enabled }} +{{/* Dev defaults provided - override via values-prod.yaml or --set for production */}} +{{- $defaultPwd := .Values.userSeed.defaultUserPassword | default "user123" }} +{{- $adminPwd := .Values.userSeed.adminUserPassword | default "admin123" }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "integr8scode.fullname" . }}-user-seed + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: user-seed + annotations: + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-weight": "10" + "helm.sh/hook-delete-policy": before-hook-creation +spec: + ttlSecondsAfterFinished: {{ .Values.userSeed.ttlSecondsAfterFinished | default 300 }} + backoffLimit: {{ .Values.userSeed.backoffLimit | default 3 }} + template: + metadata: + labels: + app: {{ include "integr8scode.fullname" . }}-user-seed + {{- include "integr8scode.labels" . | nindent 8 }} + spec: + automountServiceAccountToken: false + restartPolicy: OnFailure + initContainers: + - name: wait-for-mongodb + image: busybox:1.36 + command: + - sh + - -c + - | + echo "Waiting for MongoDB to be ready..." + until nc -z {{ include "integr8scode.fullname" . }}-mongodb 27017; do + echo "MongoDB not ready, waiting 5 seconds..." + sleep 5 + done + echo "MongoDB is ready!" + containers: + - name: user-seed + image: {{ .Values.userSeed.image | default (include "integr8scode.backendImage" .) }} + imagePullPolicy: {{ .Values.global.imagePullPolicy | default "IfNotPresent" }} + command: + - uv + - run + - python + - -m + - scripts.seed_users + env: + - name: MONGODB_URL + value: {{ include "integr8scode.mongodbUrl" . | quote }} + - name: DEFAULT_USER_PASSWORD + value: {{ $defaultPwd | quote }} + - name: ADMIN_USER_PASSWORD + value: {{ $adminPwd | quote }} + resources: + {{- toYaml .Values.userSeed.resources | nindent 10 }} +{{- end }} diff --git a/helm/integr8scode/templates/rbac/role.yaml b/helm/integr8scode/templates/rbac/role.yaml new file mode 100644 index 00000000..4b1e35d5 --- /dev/null +++ b/helm/integr8scode/templates/rbac/role.yaml @@ -0,0 +1,72 @@ +{{- if .Values.rbac.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "integr8scode.fullname" . }}-executor + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} +rules: + # Core API resources for pod management + - apiGroups: [""] + resources: + - pods + - pods/log + - pods/status + verbs: + - create + - get + - list + - watch + - delete + - update + - patch + # ConfigMaps for script storage + - apiGroups: [""] + resources: + - configmaps + verbs: + - create + - get + - list + - watch + - delete + - update + - patch + # Secrets for kubeconfig and other credentials + - apiGroups: [""] + resources: + - secrets + verbs: + - get + - list + - watch + # Services for pod networking (optional) + - apiGroups: [""] + resources: + - services + verbs: + - get + - list + - watch + # Apps API for DaemonSet management (runtime-image-pre-puller) + - apiGroups: ["apps"] + resources: + - daemonsets + verbs: + - create + - get + - list + - watch + - delete + - update + - patch + # Events for debugging + - apiGroups: [""] + resources: + - events + verbs: + - get + - list + - watch +{{- end }} diff --git a/helm/integr8scode/templates/rbac/rolebinding.yaml b/helm/integr8scode/templates/rbac/rolebinding.yaml new file mode 100644 index 00000000..66d22351 --- /dev/null +++ b/helm/integr8scode/templates/rbac/rolebinding.yaml @@ -0,0 +1,17 @@ +{{- if .Values.rbac.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "integr8scode.fullname" . }}-executor + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "integr8scode.fullname" . }}-executor +subjects: + - kind: ServiceAccount + name: {{ include "integr8scode.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/helm/integr8scode/templates/rbac/serviceaccount.yaml b/helm/integr8scode/templates/rbac/serviceaccount.yaml new file mode 100644 index 00000000..c0fed3bb --- /dev/null +++ b/helm/integr8scode/templates/rbac/serviceaccount.yaml @@ -0,0 +1,14 @@ +{{- if .Values.rbac.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "integr8scode.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + {{- with .Values.rbac.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: true +{{- end }} diff --git a/helm/integr8scode/templates/secrets/env-secret.yaml b/helm/integr8scode/templates/secrets/env-secret.yaml new file mode 100644 index 00000000..0176d3c8 --- /dev/null +++ b/helm/integr8scode/templates/secrets/env-secret.yaml @@ -0,0 +1,65 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "integr8scode.fullname" . }}-env + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} +type: Opaque +stringData: + # ========================================================================== + # CRITICAL: Explicit port settings to override K8s auto-generated env vars + # K8s auto-creates SERVICE_PORT variables in tcp://IP:PORT format which + # conflicts with application expectations of integer port numbers. + # ========================================================================== + REDIS_HOST: {{ include "integr8scode.redisHost" . | quote }} + REDIS_PORT: "6379" + REDIS_DB: {{ .Values.env.REDIS_DB | default "0" | quote }} + + # Kafka configuration + KAFKA_BOOTSTRAP_SERVERS: {{ include "integr8scode.kafkaBootstrapServers" . | quote }} + SCHEMA_REGISTRY_URL: {{ include "integr8scode.schemaRegistryUrl" . | quote }} + + # MongoDB (contains credentials - URL-encoded) + MONGODB_URL: {{ include "integr8scode.mongodbUrl" . | quote }} + + # Kubernetes + K8S_NAMESPACE: {{ .Release.Namespace | quote }} + + # Application settings + PROJECT_NAME: {{ .Values.env.PROJECT_NAME | default "integr8scode" | quote }} + API_V1_STR: {{ .Values.env.API_V1_STR | default "/api/v1" | quote }} + LOG_LEVEL: {{ .Values.env.LOG_LEVEL | default "INFO" | quote }} + SERVICE_NAME: {{ .Values.env.SERVICE_NAME | default "integr8scode-backend" | quote }} + SERVICE_VERSION: {{ .Values.env.SERVICE_VERSION | default .Chart.AppVersion | quote }} + + # Server configuration + SERVER_HOST: "0.0.0.0" + SERVER_PORT: "443" + + # Event streaming + ENABLE_EVENT_STREAMING: {{ .Values.env.ENABLE_EVENT_STREAMING | default "true" | quote }} + EVENT_RETENTION_DAYS: {{ .Values.env.EVENT_RETENTION_DAYS | default "30" | quote }} + + # Tracing + ENABLE_TRACING: {{ .Values.env.ENABLE_TRACING | default "false" | quote }} + {{- if .Values.infrastructure.jaeger.enabled }} + JAEGER_AGENT_HOST: {{ include "integr8scode.jaegerHost" . | quote }} + JAEGER_AGENT_PORT: "6831" + {{- end }} + TRACING_SAMPLING_RATE: {{ .Values.env.TRACING_SAMPLING_RATE | default "0.1" | quote }} + + # DLQ configuration + DLQ_RETRY_MAX_ATTEMPTS: {{ .Values.env.DLQ_RETRY_MAX_ATTEMPTS | default "5" | quote }} + DLQ_RETENTION_DAYS: {{ .Values.env.DLQ_RETENTION_DAYS | default "7" | quote }} + + # Rate limiting + RATE_LIMIT_ENABLED: {{ .Values.env.RATE_LIMIT_ENABLED | default "true" | quote }} + RATE_LIMIT_DEFAULT_REQUESTS: {{ .Values.env.RATE_LIMIT_DEFAULT_REQUESTS | default "100" | quote }} + RATE_LIMIT_DEFAULT_WINDOW: {{ .Values.env.RATE_LIMIT_DEFAULT_WINDOW | default "60" | quote }} + + # WebSocket / SSE + WEBSOCKET_PING_INTERVAL: {{ .Values.env.WEBSOCKET_PING_INTERVAL | default "30" | quote }} + WEBSOCKET_PING_TIMEOUT: {{ .Values.env.WEBSOCKET_PING_TIMEOUT | default "10" | quote }} + SSE_CONSUMER_POOL_SIZE: {{ .Values.env.SSE_CONSUMER_POOL_SIZE | default "10" | quote }} + SSE_HEARTBEAT_INTERVAL: {{ .Values.env.SSE_HEARTBEAT_INTERVAL | default "30" | quote }} diff --git a/helm/integr8scode/templates/secrets/kafka-jaas-secret.yaml b/helm/integr8scode/templates/secrets/kafka-jaas-secret.yaml new file mode 100644 index 00000000..0e36e82c --- /dev/null +++ b/helm/integr8scode/templates/secrets/kafka-jaas-secret.yaml @@ -0,0 +1,25 @@ +{{- if .Values.infrastructure.kafka.enabled }} +{{- $superPassword := .Values.infrastructure.kafka.jaas.superPassword | default "admin-secret" }} +{{- $kafkaPassword := .Values.infrastructure.kafka.jaas.kafkaPassword | default "kafka-secret" }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "integr8scode.fullname" . }}-kafka-jaas + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} +type: Opaque +stringData: + kafka_jaas.conf: | + Server { + org.apache.zookeeper.server.auth.DigestLoginModule required + user_super="{{ $superPassword }}" + user_kafka="{{ $kafkaPassword }}"; + }; + + Client { + org.apache.zookeeper.server.auth.DigestLoginModule required + username="kafka" + password="{{ $kafkaPassword }}"; + }; +{{- end }} diff --git a/helm/integr8scode/templates/secrets/kubeconfig-secret.yaml b/helm/integr8scode/templates/secrets/kubeconfig-secret.yaml new file mode 100644 index 00000000..24e76d74 --- /dev/null +++ b/helm/integr8scode/templates/secrets/kubeconfig-secret.yaml @@ -0,0 +1,40 @@ +{{- if .Values.kubeconfig.create }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.kubeconfig.secretName }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} +type: Opaque +stringData: + kubeconfig.yaml: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + {{- if .Values.kubeconfig.caCertificate }} + certificate-authority-data: {{ .Values.kubeconfig.caCertificate }} + {{- else }} + # For in-cluster access, use the mounted CA + certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt + {{- end }} + server: {{ .Values.kubeconfig.server | default "https://kubernetes.default.svc:443" }} + name: default-cluster + contexts: + - context: + cluster: default-cluster + namespace: {{ .Release.Namespace }} + user: default-user + name: default-context + current-context: default-context + users: + - name: default-user + user: + {{- if .Values.kubeconfig.token }} + token: {{ .Values.kubeconfig.token }} + {{- else }} + # For in-cluster access, use the mounted service account token + tokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + {{- end }} +{{- end }} diff --git a/helm/integr8scode/templates/workers/_worker.tpl b/helm/integr8scode/templates/workers/_worker.tpl new file mode 100644 index 00000000..480f4d19 --- /dev/null +++ b/helm/integr8scode/templates/workers/_worker.tpl @@ -0,0 +1,72 @@ +{{/* +Generate a worker deployment. +Usage: {{ include "integr8scode.worker" (dict "root" . "name" "k8s-worker" "config" .Values.workers.k8sWorker) }} +*/}} +{{- define "integr8scode.worker" -}} +{{- $root := .root -}} +{{- $name := .name -}} +{{- $config := .config -}} +{{- if $config.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "integr8scode.fullname" $root }}-{{ $name }} + namespace: {{ $root.Release.Namespace }} + labels: + {{- include "integr8scode.labels" $root | nindent 4 }} + app.kubernetes.io/component: {{ $name }} +spec: + replicas: {{ $config.replicas | default 1 }} + selector: + matchLabels: + app: {{ include "integr8scode.fullname" $root }}-{{ $name }} + template: + metadata: + labels: + app: {{ include "integr8scode.fullname" $root }}-{{ $name }} + {{- include "integr8scode.labels" $root | nindent 8 }} + app.kubernetes.io/component: {{ $name }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/secrets/env-secret.yaml") $root | sha256sum }} + spec: + serviceAccountName: {{ include "integr8scode.serviceAccountName" $root }} + containers: + - name: {{ $name }} + image: {{ include "integr8scode.backendImage" $root }} + imagePullPolicy: {{ $root.Values.global.imagePullPolicy | default "IfNotPresent" }} + command: + {{- toYaml $config.command | nindent 10 }} + envFrom: + - secretRef: + name: {{ include "integr8scode.fullname" $root }}-env + env: + - name: KAFKA_CONSUMER_GROUP_ID + value: {{ $config.consumerGroupId | quote }} + - name: TRACING_SERVICE_NAME + value: {{ $name | quote }} + {{- if $config.requiresKubeconfig }} + - name: KUBECONFIG + value: "/app/kubeconfig.yaml" + {{- end }} + resources: + {{- if $config.resources }} + {{- toYaml $config.resources | nindent 10 }} + {{- else }} + {{- toYaml $root.Values.workers.common.resources | nindent 10 }} + {{- end }} + {{- if $config.requiresKubeconfig }} + volumeMounts: + - name: kubeconfig + mountPath: /app/kubeconfig.yaml + subPath: kubeconfig.yaml + readOnly: true + {{- end }} + {{- if $config.requiresKubeconfig }} + volumes: + - name: kubeconfig + secret: + secretName: {{ $root.Values.kubeconfig.secretName }} + {{- end }} + restartPolicy: Always +{{- end }} +{{- end -}} diff --git a/helm/integr8scode/templates/workers/coordinator.yaml b/helm/integr8scode/templates/workers/coordinator.yaml new file mode 100644 index 00000000..1f00b403 --- /dev/null +++ b/helm/integr8scode/templates/workers/coordinator.yaml @@ -0,0 +1,46 @@ +{{- if .Values.workers.coordinator.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "integr8scode.fullname" . }}-coordinator + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: coordinator +spec: + replicas: {{ .Values.workers.coordinator.replicas | default 1 }} + selector: + matchLabels: + app: {{ include "integr8scode.fullname" . }}-coordinator + template: + metadata: + labels: + app: {{ include "integr8scode.fullname" . }}-coordinator + {{- include "integr8scode.labels" . | nindent 8 }} + app.kubernetes.io/component: coordinator + annotations: + checksum/config: {{ include (print $.Template.BasePath "/secrets/env-secret.yaml") . | sha256sum }} + spec: + serviceAccountName: {{ include "integr8scode.serviceAccountName" . }} + containers: + - name: coordinator + image: {{ include "integr8scode.backendImage" . }} + imagePullPolicy: {{ .Values.global.imagePullPolicy | default "IfNotPresent" }} + command: + {{- toYaml .Values.workers.coordinator.command | nindent 10 }} + envFrom: + - secretRef: + name: {{ include "integr8scode.fullname" . }}-env + env: + - name: KAFKA_CONSUMER_GROUP_ID + value: {{ .Values.workers.coordinator.consumerGroupId | quote }} + - name: TRACING_SERVICE_NAME + value: "execution-coordinator" + resources: + {{- if .Values.workers.coordinator.resources }} + {{- toYaml .Values.workers.coordinator.resources | nindent 10 }} + {{- else }} + {{- toYaml .Values.workers.common.resources | nindent 10 }} + {{- end }} + restartPolicy: Always +{{- end }} diff --git a/helm/integr8scode/templates/workers/dlq-processor.yaml b/helm/integr8scode/templates/workers/dlq-processor.yaml new file mode 100644 index 00000000..2c22d3dc --- /dev/null +++ b/helm/integr8scode/templates/workers/dlq-processor.yaml @@ -0,0 +1,46 @@ +{{- if .Values.workers.dlqProcessor.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "integr8scode.fullname" . }}-dlq-processor + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: dlq-processor +spec: + replicas: {{ .Values.workers.dlqProcessor.replicas | default 1 }} + selector: + matchLabels: + app: {{ include "integr8scode.fullname" . }}-dlq-processor + template: + metadata: + labels: + app: {{ include "integr8scode.fullname" . }}-dlq-processor + {{- include "integr8scode.labels" . | nindent 8 }} + app.kubernetes.io/component: dlq-processor + annotations: + checksum/config: {{ include (print $.Template.BasePath "/secrets/env-secret.yaml") . | sha256sum }} + spec: + serviceAccountName: {{ include "integr8scode.serviceAccountName" . }} + containers: + - name: dlq-processor + image: {{ include "integr8scode.backendImage" . }} + imagePullPolicy: {{ .Values.global.imagePullPolicy | default "IfNotPresent" }} + command: + {{- toYaml .Values.workers.dlqProcessor.command | nindent 10 }} + envFrom: + - secretRef: + name: {{ include "integr8scode.fullname" . }}-env + env: + - name: KAFKA_CONSUMER_GROUP_ID + value: {{ .Values.workers.dlqProcessor.consumerGroupId | quote }} + - name: TRACING_SERVICE_NAME + value: "dlq-processor" + resources: + {{- if .Values.workers.dlqProcessor.resources }} + {{- toYaml .Values.workers.dlqProcessor.resources | nindent 10 }} + {{- else }} + {{- toYaml .Values.workers.common.resources | nindent 10 }} + {{- end }} + restartPolicy: Always +{{- end }} diff --git a/helm/integr8scode/templates/workers/event-replay.yaml b/helm/integr8scode/templates/workers/event-replay.yaml new file mode 100644 index 00000000..da6139d5 --- /dev/null +++ b/helm/integr8scode/templates/workers/event-replay.yaml @@ -0,0 +1,46 @@ +{{- if .Values.workers.eventReplay.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "integr8scode.fullname" . }}-event-replay + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: event-replay +spec: + replicas: {{ .Values.workers.eventReplay.replicas | default 1 }} + selector: + matchLabels: + app: {{ include "integr8scode.fullname" . }}-event-replay + template: + metadata: + labels: + app: {{ include "integr8scode.fullname" . }}-event-replay + {{- include "integr8scode.labels" . | nindent 8 }} + app.kubernetes.io/component: event-replay + annotations: + checksum/config: {{ include (print $.Template.BasePath "/secrets/env-secret.yaml") . | sha256sum }} + spec: + serviceAccountName: {{ include "integr8scode.serviceAccountName" . }} + containers: + - name: event-replay + image: {{ include "integr8scode.backendImage" . }} + imagePullPolicy: {{ .Values.global.imagePullPolicy | default "IfNotPresent" }} + command: + {{- toYaml .Values.workers.eventReplay.command | nindent 10 }} + envFrom: + - secretRef: + name: {{ include "integr8scode.fullname" . }}-env + env: + - name: KAFKA_CONSUMER_GROUP_ID + value: {{ .Values.workers.eventReplay.consumerGroupId | quote }} + - name: TRACING_SERVICE_NAME + value: "event-replay" + resources: + {{- if .Values.workers.eventReplay.resources }} + {{- toYaml .Values.workers.eventReplay.resources | nindent 10 }} + {{- else }} + {{- toYaml .Values.workers.common.resources | nindent 10 }} + {{- end }} + restartPolicy: Always +{{- end }} diff --git a/helm/integr8scode/templates/workers/k8s-worker.yaml b/helm/integr8scode/templates/workers/k8s-worker.yaml new file mode 100644 index 00000000..73d8b026 --- /dev/null +++ b/helm/integr8scode/templates/workers/k8s-worker.yaml @@ -0,0 +1,57 @@ +{{- if .Values.workers.k8sWorker.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "integr8scode.fullname" . }}-k8s-worker + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: k8s-worker +spec: + replicas: {{ .Values.workers.k8sWorker.replicas | default 1 }} + selector: + matchLabels: + app: {{ include "integr8scode.fullname" . }}-k8s-worker + template: + metadata: + labels: + app: {{ include "integr8scode.fullname" . }}-k8s-worker + {{- include "integr8scode.labels" . | nindent 8 }} + app.kubernetes.io/component: k8s-worker + annotations: + checksum/config: {{ include (print $.Template.BasePath "/secrets/env-secret.yaml") . | sha256sum }} + spec: + serviceAccountName: {{ include "integr8scode.serviceAccountName" . }} + containers: + - name: k8s-worker + image: {{ include "integr8scode.backendImage" . }} + imagePullPolicy: {{ .Values.global.imagePullPolicy | default "IfNotPresent" }} + command: + {{- toYaml .Values.workers.k8sWorker.command | nindent 10 }} + envFrom: + - secretRef: + name: {{ include "integr8scode.fullname" . }}-env + env: + - name: KAFKA_CONSUMER_GROUP_ID + value: {{ .Values.workers.k8sWorker.consumerGroupId | quote }} + - name: TRACING_SERVICE_NAME + value: "k8s-worker" + - name: KUBECONFIG + value: "/app/kubeconfig.yaml" + resources: + {{- if .Values.workers.k8sWorker.resources }} + {{- toYaml .Values.workers.k8sWorker.resources | nindent 10 }} + {{- else }} + {{- toYaml .Values.workers.common.resources | nindent 10 }} + {{- end }} + volumeMounts: + - name: kubeconfig + mountPath: /app/kubeconfig.yaml + subPath: kubeconfig.yaml + readOnly: true + volumes: + - name: kubeconfig + secret: + secretName: {{ .Values.kubeconfig.secretName }} + restartPolicy: Always +{{- end }} diff --git a/helm/integr8scode/templates/workers/pod-monitor.yaml b/helm/integr8scode/templates/workers/pod-monitor.yaml new file mode 100644 index 00000000..f2ab0f3f --- /dev/null +++ b/helm/integr8scode/templates/workers/pod-monitor.yaml @@ -0,0 +1,57 @@ +{{- if .Values.workers.podMonitor.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "integr8scode.fullname" . }}-pod-monitor + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: pod-monitor +spec: + replicas: {{ .Values.workers.podMonitor.replicas | default 1 }} + selector: + matchLabels: + app: {{ include "integr8scode.fullname" . }}-pod-monitor + template: + metadata: + labels: + app: {{ include "integr8scode.fullname" . }}-pod-monitor + {{- include "integr8scode.labels" . | nindent 8 }} + app.kubernetes.io/component: pod-monitor + annotations: + checksum/config: {{ include (print $.Template.BasePath "/secrets/env-secret.yaml") . | sha256sum }} + spec: + serviceAccountName: {{ include "integr8scode.serviceAccountName" . }} + containers: + - name: pod-monitor + image: {{ include "integr8scode.backendImage" . }} + imagePullPolicy: {{ .Values.global.imagePullPolicy | default "IfNotPresent" }} + command: + {{- toYaml .Values.workers.podMonitor.command | nindent 10 }} + envFrom: + - secretRef: + name: {{ include "integr8scode.fullname" . }}-env + env: + - name: KAFKA_CONSUMER_GROUP_ID + value: {{ .Values.workers.podMonitor.consumerGroupId | quote }} + - name: TRACING_SERVICE_NAME + value: "pod-monitor" + - name: KUBECONFIG + value: "/app/kubeconfig.yaml" + resources: + {{- if .Values.workers.podMonitor.resources }} + {{- toYaml .Values.workers.podMonitor.resources | nindent 10 }} + {{- else }} + {{- toYaml .Values.workers.common.resources | nindent 10 }} + {{- end }} + volumeMounts: + - name: kubeconfig + mountPath: /app/kubeconfig.yaml + subPath: kubeconfig.yaml + readOnly: true + volumes: + - name: kubeconfig + secret: + secretName: {{ .Values.kubeconfig.secretName }} + restartPolicy: Always +{{- end }} diff --git a/helm/integr8scode/templates/workers/result-processor.yaml b/helm/integr8scode/templates/workers/result-processor.yaml new file mode 100644 index 00000000..7b3661dc --- /dev/null +++ b/helm/integr8scode/templates/workers/result-processor.yaml @@ -0,0 +1,46 @@ +{{- if .Values.workers.resultProcessor.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "integr8scode.fullname" . }}-result-processor + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: result-processor +spec: + replicas: {{ .Values.workers.resultProcessor.replicas | default 1 }} + selector: + matchLabels: + app: {{ include "integr8scode.fullname" . }}-result-processor + template: + metadata: + labels: + app: {{ include "integr8scode.fullname" . }}-result-processor + {{- include "integr8scode.labels" . | nindent 8 }} + app.kubernetes.io/component: result-processor + annotations: + checksum/config: {{ include (print $.Template.BasePath "/secrets/env-secret.yaml") . | sha256sum }} + spec: + serviceAccountName: {{ include "integr8scode.serviceAccountName" . }} + containers: + - name: result-processor + image: {{ include "integr8scode.backendImage" . }} + imagePullPolicy: {{ .Values.global.imagePullPolicy | default "IfNotPresent" }} + command: + {{- toYaml .Values.workers.resultProcessor.command | nindent 10 }} + envFrom: + - secretRef: + name: {{ include "integr8scode.fullname" . }}-env + env: + - name: KAFKA_CONSUMER_GROUP_ID + value: {{ .Values.workers.resultProcessor.consumerGroupId | quote }} + - name: TRACING_SERVICE_NAME + value: "result-processor" + resources: + {{- if .Values.workers.resultProcessor.resources }} + {{- toYaml .Values.workers.resultProcessor.resources | nindent 10 }} + {{- else }} + {{- toYaml .Values.workers.common.resources | nindent 10 }} + {{- end }} + restartPolicy: Always +{{- end }} diff --git a/helm/integr8scode/templates/workers/saga-orchestrator.yaml b/helm/integr8scode/templates/workers/saga-orchestrator.yaml new file mode 100644 index 00000000..7ff7e9f0 --- /dev/null +++ b/helm/integr8scode/templates/workers/saga-orchestrator.yaml @@ -0,0 +1,46 @@ +{{- if .Values.workers.sagaOrchestrator.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "integr8scode.fullname" . }}-saga-orchestrator + namespace: {{ .Release.Namespace }} + labels: + {{- include "integr8scode.labels" . | nindent 4 }} + app.kubernetes.io/component: saga-orchestrator +spec: + replicas: {{ .Values.workers.sagaOrchestrator.replicas | default 1 }} + selector: + matchLabels: + app: {{ include "integr8scode.fullname" . }}-saga-orchestrator + template: + metadata: + labels: + app: {{ include "integr8scode.fullname" . }}-saga-orchestrator + {{- include "integr8scode.labels" . | nindent 8 }} + app.kubernetes.io/component: saga-orchestrator + annotations: + checksum/config: {{ include (print $.Template.BasePath "/secrets/env-secret.yaml") . | sha256sum }} + spec: + serviceAccountName: {{ include "integr8scode.serviceAccountName" . }} + containers: + - name: saga-orchestrator + image: {{ include "integr8scode.backendImage" . }} + imagePullPolicy: {{ .Values.global.imagePullPolicy | default "IfNotPresent" }} + command: + {{- toYaml .Values.workers.sagaOrchestrator.command | nindent 10 }} + envFrom: + - secretRef: + name: {{ include "integr8scode.fullname" . }}-env + env: + - name: KAFKA_CONSUMER_GROUP_ID + value: {{ .Values.workers.sagaOrchestrator.consumerGroupId | quote }} + - name: TRACING_SERVICE_NAME + value: "saga-orchestrator" + resources: + {{- if .Values.workers.sagaOrchestrator.resources }} + {{- toYaml .Values.workers.sagaOrchestrator.resources | nindent 10 }} + {{- else }} + {{- toYaml .Values.workers.common.resources | nindent 10 }} + {{- end }} + restartPolicy: Always +{{- end }} diff --git a/helm/integr8scode/values-prod.yaml b/helm/integr8scode/values-prod.yaml new file mode 100644 index 00000000..77a9919c --- /dev/null +++ b/helm/integr8scode/values-prod.yaml @@ -0,0 +1,215 @@ +# ============================================================================= +# INTEGR8SCODE HELM CHART - PRODUCTION VALUES +# ============================================================================= +# Production-specific overrides for the Integr8sCode Helm chart. +# Usage: helm upgrade --install integr8scode ./helm/integr8scode -f values-prod.yaml + +# ============================================================================= +# IMAGE CONFIGURATION - USE REGISTRY IMAGES +# ============================================================================= +# Pre-built images from GitHub Container Registry (no local build required) +global: + imagePullPolicy: IfNotPresent + +images: + backend: + repository: ghcr.io/hardmax71/integr8scode/backend + tag: latest # Or pin to specific version: v1.0.0, sha-abc1234 + frontend: + repository: ghcr.io/hardmax71/integr8scode/frontend + tag: latest + base: + repository: ghcr.io/hardmax71/integr8scode/base + tag: latest + +# ============================================================================= +# ENVIRONMENT SETTINGS +# ============================================================================= +env: + LOG_LEVEL: "INFO" + ENABLE_TRACING: "true" + TRACING_SAMPLING_RATE: "0.1" + +# ============================================================================= +# BACKEND APPLICATION +# ============================================================================= +backend: + replicas: 2 + + resources: + requests: + memory: "1Gi" + cpu: "500m" + limits: + memory: "2Gi" + cpu: "2000m" + + extraEnv: + WEB_CONCURRENCY: "4" + # IMPORTANT: Set SECRET_KEY via --set or external secrets management + # SECRET_KEY: "" + +# ============================================================================= +# FRONTEND APPLICATION +# ============================================================================= +frontend: + replicas: 2 + + resources: + requests: + memory: "256Mi" + cpu: "200m" + limits: + memory: "512Mi" + cpu: "1000m" + +# ============================================================================= +# WORKERS - PRODUCTION SCALING +# ============================================================================= +workers: + common: + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "1000m" + + # Scale critical workers for production load + k8sWorker: + replicas: 2 + + podMonitor: + replicas: 1 # Single instance is usually sufficient + + resultProcessor: + replicas: 2 + + sagaOrchestrator: + replicas: 2 + + coordinator: + replicas: 2 + + eventReplay: + replicas: 1 # Low traffic service + + dlqProcessor: + replicas: 1 # Low traffic service + +# ============================================================================= +# INFRASTRUCTURE - PRODUCTION SETTINGS +# ============================================================================= +infrastructure: + zookeeper: + heapOpts: "-Xms512M -Xmx512M" + resources: + requests: + memory: "512Mi" + cpu: "200m" + limits: + memory: "1Gi" + cpu: "1000m" + + kafka: + heapOpts: "-Xms1G -Xmx1G" + # IMPORTANT: Set JAAS credentials via --set for production: + # --set infrastructure.kafka.jaas.superPassword= + # --set infrastructure.kafka.jaas.kafkaPassword= + jaas: + superPassword: "" # REQUIRED: Set via --set + kafkaPassword: "" # REQUIRED: Set via --set + resources: + requests: + memory: "1Gi" + cpu: "500m" + limits: + memory: "2Gi" + cpu: "2000m" + + schemaRegistry: + heapOpts: "-Xms512M -Xmx512M" + resources: + requests: + memory: "512Mi" + cpu: "200m" + limits: + memory: "1Gi" + cpu: "1000m" + + jaeger: + resources: + requests: + memory: "256Mi" + cpu: "200m" + limits: + memory: "1Gi" + cpu: "1000m" + +# ============================================================================= +# REDIS - PRODUCTION SETTINGS +# ============================================================================= +redis: + master: + resources: + requests: + memory: "256Mi" + cpu: "200m" + limits: + memory: "1Gi" + cpu: "1000m" + persistence: + enabled: true + size: 5Gi + +# ============================================================================= +# MONGODB - PRODUCTION SETTINGS +# ============================================================================= +mongodb: + auth: + enabled: true + rootUser: "root" + # IMPORTANT: Set password via --set mongodb.auth.rootPassword= + # rootPassword: "" + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "2Gi" + cpu: "2000m" + persistence: + enabled: true + size: 50Gi + +# ============================================================================= +# USER SEED - PRODUCTION SETTINGS +# ============================================================================= +userSeed: + enabled: true + # IMPORTANT: Set passwords via --set for production: + # --set userSeed.defaultUserPassword= + # --set userSeed.adminUserPassword= + defaultUserPassword: "" # REQUIRED: Set via --set + adminUserPassword: "" # REQUIRED: Set via --set + +# ============================================================================= +# INGRESS - PRODUCTION SETTINGS +# ============================================================================= +# Uncomment and configure for production ingress +# ingress: +# enabled: true +# className: "nginx" +# annotations: +# cert-manager.io/cluster-issuer: "letsencrypt-prod" +# nginx.ingress.kubernetes.io/ssl-redirect: "true" +# hosts: +# - host: integr8scode.example.com +# paths: +# - path: / +# pathType: Prefix +# tls: +# - secretName: integr8scode-tls +# hosts: +# - integr8scode.example.com diff --git a/helm/integr8scode/values.yaml b/helm/integr8scode/values.yaml new file mode 100644 index 00000000..40d105f5 --- /dev/null +++ b/helm/integr8scode/values.yaml @@ -0,0 +1,441 @@ +# ============================================================================= +# INTEGR8SCODE HELM CHART - DEFAULT VALUES +# ============================================================================= +# This file contains default configuration values for the Integr8sCode Helm chart. +# Override these values using --set flags or a custom values-prod.yaml file. + +# ============================================================================= +# GLOBAL SETTINGS +# ============================================================================= +global: + # Image pull policy: + # - "Never" for K3s with locally imported images (default for local dev) + # - "IfNotPresent" for registry-pulled images + # - "Always" for latest tags from registry + imagePullPolicy: Never + + # Container registry (leave empty for local images) + # Set to "ghcr.io/hardmax71/integr8scode" for GitHub Container Registry + imageRegistry: "" + +# Name overrides +nameOverride: "" +fullnameOverride: "" + +# ============================================================================= +# IMAGE CONFIGURATION +# ============================================================================= +images: + # For local development (imagePullPolicy: Never): + # repository: integr8scode-backend + # For registry (imagePullPolicy: IfNotPresent/Always): + # repository: ghcr.io/hardmax71/integr8scode/backend + backend: + repository: integr8scode-backend + tag: latest + frontend: + repository: integr8scode-frontend + tag: latest + # Base image (used by backend Dockerfile) + base: + repository: base + tag: latest + +# ============================================================================= +# RBAC CONFIGURATION +# ============================================================================= +rbac: + # Create ServiceAccount, Role, and RoleBinding + create: true + serviceAccount: + name: integr8scode-sa + annotations: {} + +# ============================================================================= +# KUBECONFIG SECRET +# ============================================================================= +kubeconfig: + # Create kubeconfig secret for workers that need K8s API access + create: true + secretName: kubeconfig-secret + # Server URL - use in-cluster service address + server: "https://kubernetes.default.svc:443" + # Optional: Base64-encoded CA certificate (leave empty for in-cluster token) + caCertificate: "" + # Optional: ServiceAccount token (leave empty for in-cluster token) + token: "" + +# ============================================================================= +# SHARED ENVIRONMENT VARIABLES +# ============================================================================= +env: + # Application settings + PROJECT_NAME: "integr8scode" + API_V1_STR: "/api/v1" + LOG_LEVEL: "INFO" + SERVICE_NAME: "integr8scode-backend" + SERVICE_VERSION: "1.0.0" + + # Redis (explicit to override K8s auto-generated vars) + REDIS_DB: "0" + + # Event streaming + ENABLE_EVENT_STREAMING: "true" + EVENT_RETENTION_DAYS: "30" + + # Tracing + ENABLE_TRACING: "false" + TRACING_SAMPLING_RATE: "0.1" + + # DLQ configuration + DLQ_RETRY_MAX_ATTEMPTS: "5" + DLQ_RETENTION_DAYS: "7" + + # Rate limiting + RATE_LIMIT_ENABLED: "true" + RATE_LIMIT_DEFAULT_REQUESTS: "100" + RATE_LIMIT_DEFAULT_WINDOW: "60" + + # WebSocket / SSE + WEBSOCKET_PING_INTERVAL: "30" + WEBSOCKET_PING_TIMEOUT: "10" + SSE_CONSUMER_POOL_SIZE: "10" + SSE_HEARTBEAT_INTERVAL: "30" + +# ============================================================================= +# BACKEND APPLICATION +# ============================================================================= +backend: + enabled: true + replicas: 1 + + resources: + requests: + memory: "512Mi" + cpu: "250m" + ephemeral-storage: "128Mi" + limits: + memory: "1Gi" + cpu: "1000m" + ephemeral-storage: "512Mi" + + service: + type: ClusterIP + port: 443 + + # Health probe configuration (fixes 405 error) + healthProbe: + path: /api/v1/health/live + readyPath: /api/v1/health/ready + scheme: HTTPS + + # SSL configuration + ssl: + enabled: false + secretName: backend-ssl + + # Additional environment variables + extraEnv: + WEB_CONCURRENCY: "4" + # SECRET_KEY: "" # Set in values-prod.yaml or via --set + +# ============================================================================= +# FRONTEND APPLICATION +# ============================================================================= +frontend: + enabled: true + replicas: 1 + containerPort: 5001 + + resources: + requests: + memory: "128Mi" + cpu: "100m" + ephemeral-storage: "64Mi" + limits: + memory: "256Mi" + cpu: "500m" + ephemeral-storage: "128Mi" + + service: + type: ClusterIP + port: 5001 + +# ============================================================================= +# WORKERS CONFIGURATION +# ============================================================================= +workers: + # Shared settings for all workers + common: + resources: + requests: + memory: "256Mi" + cpu: "100m" + ephemeral-storage: "128Mi" + limits: + memory: "512Mi" + cpu: "500m" + ephemeral-storage: "256Mi" + + # K8s Worker - Creates executor pods + k8sWorker: + enabled: true + replicas: 1 + requiresKubeconfig: true + command: + - uv + - run + - python + - workers/run_k8s_worker.py + consumerGroupId: "k8s-worker" + + # Pod Monitor - Watches pod lifecycle events + podMonitor: + enabled: true + replicas: 1 + requiresKubeconfig: true + command: + - uv + - run + - python + - workers/run_pod_monitor.py + consumerGroupId: "pod-monitor" + + # Result Processor - Stores execution results + resultProcessor: + enabled: true + replicas: 1 + requiresKubeconfig: false + command: + - uv + - run + - python + - workers/run_result_processor.py + consumerGroupId: "result-processor-group" + + # Saga Orchestrator - Manages distributed transactions + sagaOrchestrator: + enabled: true + replicas: 1 + requiresKubeconfig: false + command: + - uv + - run + - python + - workers/run_saga_orchestrator.py + consumerGroupId: "saga-orchestrator" + + # Coordinator - Schedules executions + coordinator: + enabled: true + replicas: 1 + requiresKubeconfig: false + command: + - uv + - run + - python + - -m + - workers.run_coordinator + consumerGroupId: "execution-coordinator" + + # Event Replay - Replays historical events + eventReplay: + enabled: true + replicas: 1 + requiresKubeconfig: false + command: + - uv + - run + - python + - workers/run_event_replay.py + consumerGroupId: "event-replay" + + # DLQ Processor - Handles failed messages + dlqProcessor: + enabled: true + replicas: 1 + requiresKubeconfig: false + command: + - uv + - run + - python + - workers/dlq_processor.py + consumerGroupId: "dlq-processor" + +# ============================================================================= +# INFRASTRUCTURE - CONFLUENT PLATFORM +# ============================================================================= +# NOTE: Custom templates are used instead of Bitnami sub-charts because +# Confluent images require `unset *_PORT` workaround for K8s compatibility + +infrastructure: + # Zookeeper + zookeeper: + enabled: true + image: confluentinc/cp-zookeeper:7.5.0 + heapOpts: "-Xms256M -Xmx256M" + resources: + requests: + memory: "256Mi" + cpu: "100m" + ephemeral-storage: "128Mi" + limits: + memory: "512Mi" + cpu: "500m" + ephemeral-storage: "256Mi" + + # Kafka + kafka: + enabled: true + image: confluentinc/cp-kafka:7.5.0 + heapOpts: "-Xms256M -Xmx256M" + autoCreateTopics: "true" + # JAAS authentication credentials for Zookeeper communication + # Override these in production via --set or values-prod.yaml + jaas: + superPassword: "admin-secret" + kafkaPassword: "kafka-secret" + resources: + requests: + memory: "384Mi" + cpu: "200m" + ephemeral-storage: "256Mi" + limits: + memory: "768Mi" + cpu: "1000m" + ephemeral-storage: "512Mi" + + # Schema Registry + schemaRegistry: + enabled: true + image: confluentinc/cp-schema-registry:7.5.0 + heapOpts: "-Xms256M -Xmx256M" + resources: + requests: + memory: "256Mi" + cpu: "100m" + ephemeral-storage: "128Mi" + limits: + memory: "512Mi" + cpu: "500m" + ephemeral-storage: "256Mi" + + # Jaeger (distributed tracing) + jaeger: + enabled: true + image: jaegertracing/all-in-one:1.52 + resources: + requests: + memory: "128Mi" + cpu: "100m" + ephemeral-storage: "128Mi" + limits: + memory: "512Mi" + cpu: "500m" + ephemeral-storage: "512Mi" + +# ============================================================================= +# REDIS (BITNAMI SUB-CHART) +# ============================================================================= +redis: + enabled: true + architecture: standalone + auth: + enabled: false + master: + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + persistence: + enabled: false # Disable for lightweight K3s deployment + +# ============================================================================= +# MONGODB (BITNAMI SUB-CHART) +# ============================================================================= +mongodb: + enabled: true + architecture: standalone + auth: + enabled: false # Disable auth for development; enable for production + rootUser: "root" + rootPassword: "" + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + persistence: + enabled: true + size: 8Gi + +# ============================================================================= +# KAFKA INIT JOB +# ============================================================================= +kafkaInit: + enabled: true + # Image to use (defaults to backend image which has create_topics.py) + image: "" # Leave empty to use backend image + backoffLimit: 3 + ttlSecondsAfterFinished: 300 + resources: + requests: + memory: "128Mi" + cpu: "50m" + ephemeral-storage: "64Mi" + limits: + memory: "256Mi" + cpu: "200m" + ephemeral-storage: "128Mi" + +# ============================================================================= +# USER SEED JOB +# ============================================================================= +userSeed: + enabled: true + # Image to use (defaults to backend image which has seed_users.py) + image: "" # Leave empty to use backend image + backoffLimit: 3 + ttlSecondsAfterFinished: 300 + # Default user credentials (override in values-prod.yaml for production) + defaultUserPassword: "user123" + adminUserPassword: "admin123" + resources: + requests: + memory: "128Mi" + cpu: "50m" + ephemeral-storage: "64Mi" + limits: + memory: "256Mi" + cpu: "200m" + ephemeral-storage: "128Mi" + +# ============================================================================= +# EXTERNAL SERVICES (when sub-charts are disabled) +# ============================================================================= +externalRedis: + host: "" + port: 6379 + +externalMongodb: + host: "" + url: "" + +# ============================================================================= +# INGRESS (OPTIONAL) +# ============================================================================= +ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: integr8scode.local + paths: + - path: / + pathType: Prefix + tls: [] diff --git a/mkdocs.yml b/mkdocs.yml index 07317992..d5b65ed9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -120,6 +120,7 @@ nav: - Schema Manager: components/schema-manager.md - Operations: + - Deployment: operations/deployment.md - CI/CD Pipeline: operations/cicd.md - Nginx Configuration: operations/nginx-configuration.md - Tracing: operations/tracing.md