From 8a568932b4f98a9b51d9b4e2bc78e8668ec8253e Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Tue, 24 Mar 2026 14:45:19 -0700 Subject: [PATCH] feat(infra): add production-grade k3s Kubernetes setup for single VM Complete K8s deployment alternative to Docker Compose, targeting ~50 beta users on a Standard_D8s_v5 Azure VM (8 vCPU, 32 GB RAM). setup-k8s.sh (6 phases): 1. Pre-flight: verify docker phases 1-5 ran, disk/RAM checks 2. Install k3s: Docker runtime, NodePort range 1024-32767 3. Build images: docker compose build + tag as bytelyst/ 4. Config: namespaces, ConfigMap (3 copies), Secrets (JWT + blob keys), Ollama 5. Deploy: infra -> platform -> dashboards -> products (ordered) 6. Health check: 32 endpoints + kubectl pod status K8s manifests (18 files): - 4 namespaces (infra, platform, dashboards, products) - 6 infra (cosmos StatefulSet+PVC, azurite StatefulSet+PVC, mailpit, loki StatefulSet+PVC, grafana+PVC, ollama external) - 3 platform (Deployment+Service+NodePort each) - 2 dashboards (Deployment+Service+NodePort each) - 10 backends + 9 webs (all with readiness+liveness probes, resource limits, product-specific NEXT_PUBLIC_* env vars) Design decisions: - k3s --docker: reuses existing Docker images, no containerd import - Same ports as Docker Compose (NodePort with extended range) - ConfigMap replaces .env.ecosystem, copied to 3 app namespaces - Blob storage keys injected at deploy time via Secret (not in YAML) - Cross-namespace DNS: ..svc for service discovery - Ollama as Endpoints+Service pointing to host node IP - Resource limits: ~19 Gi total, fits in 32 GB with 13 GB headroom - Teardown: --teardown flag deletes namespaces, keeps k3s --- docs/devops/single_azure_vm/k8s/README.md | 360 ++++---- .../single_azure_vm/k8s/config/configmap.yaml | 77 ++ .../single_azure_vm/k8s/config/secrets.yaml | 14 + .../k8s/dashboards/admin-web.yaml | 80 ++ .../k8s/dashboards/tracker-web.yaml | 80 ++ .../single_azure_vm/k8s/infra/azurite.yaml | 80 ++ .../k8s/infra/cosmos-emulator.yaml | 93 +++ .../single_azure_vm/k8s/infra/grafana.yaml | 89 ++ .../single_azure_vm/k8s/infra/loki.yaml | 77 ++ .../single_azure_vm/k8s/infra/mailpit.yaml | 69 ++ .../k8s/infra/ollama-external.yaml | 26 + .../single_azure_vm/k8s/namespaces.yaml | 27 + .../k8s/platform/extraction-service.yaml | 85 ++ .../k8s/platform/mcp-server.yaml | 85 ++ .../k8s/platform/platform-service.yaml | 87 ++ .../k8s/products/backends.yaml | 778 ++++++++++++++++++ .../single_azure_vm/k8s/products/webs.yaml | 710 ++++++++++++++++ docs/devops/single_azure_vm/k8s/setup-k8s.sh | 580 +++++++++++++ 18 files changed, 3175 insertions(+), 222 deletions(-) create mode 100644 docs/devops/single_azure_vm/k8s/config/configmap.yaml create mode 100644 docs/devops/single_azure_vm/k8s/config/secrets.yaml create mode 100644 docs/devops/single_azure_vm/k8s/dashboards/admin-web.yaml create mode 100644 docs/devops/single_azure_vm/k8s/dashboards/tracker-web.yaml create mode 100644 docs/devops/single_azure_vm/k8s/infra/azurite.yaml create mode 100644 docs/devops/single_azure_vm/k8s/infra/cosmos-emulator.yaml create mode 100644 docs/devops/single_azure_vm/k8s/infra/grafana.yaml create mode 100644 docs/devops/single_azure_vm/k8s/infra/loki.yaml create mode 100644 docs/devops/single_azure_vm/k8s/infra/mailpit.yaml create mode 100644 docs/devops/single_azure_vm/k8s/infra/ollama-external.yaml create mode 100644 docs/devops/single_azure_vm/k8s/namespaces.yaml create mode 100644 docs/devops/single_azure_vm/k8s/platform/extraction-service.yaml create mode 100644 docs/devops/single_azure_vm/k8s/platform/mcp-server.yaml create mode 100644 docs/devops/single_azure_vm/k8s/platform/platform-service.yaml create mode 100644 docs/devops/single_azure_vm/k8s/products/backends.yaml create mode 100644 docs/devops/single_azure_vm/k8s/products/webs.yaml create mode 100755 docs/devops/single_azure_vm/k8s/setup-k8s.sh diff --git a/docs/devops/single_azure_vm/k8s/README.md b/docs/devops/single_azure_vm/k8s/README.md index cbee6174..83969490 100644 --- a/docs/devops/single_azure_vm/k8s/README.md +++ b/docs/devops/single_azure_vm/k8s/README.md @@ -1,18 +1,30 @@ # ByteLyst Single-VM Kubernetes Deployment (k3s) -> Deploy the ByteLyst ecosystem on Kubernetes using **k3s** — a lightweight, certified K8s distribution -> that runs on a single VM with ~512 MB overhead. - -**Status:** Planning — see design decisions below. +> Deploy the **entire ByteLyst ecosystem** (30 services, 10 products) on Kubernetes +> using **k3s** — a lightweight, CNCF-certified K8s distribution. +> Production-grade for ~50 beta users on a single Azure VM. --- +## Quick Start + +```bash +# Step 1: Run Docker setup phases 1-5 (system deps, Gitea, repos, packages) +cd /opt/bytelyst/learning_ai_common_plat/docs/devops/single_azure_vm +sudo ./docker/setup.sh --resume # Runs phases 1-5 (skip 6-8) + +# Step 2: Deploy to Kubernetes +sudo ./k8s/setup-k8s.sh # 6 phases: preflight → k3s → images → config → deploy → health + +# Step 3: Verify +/opt/bytelyst/check-health-k8s.sh # 32 health checks +kubectl get pods -A # All pods +``` + ## Prerequisites -Same VM as the Docker Compose approach: -- **Azure VM:** Ubuntu 24.04 LTS, **Standard_D8s_v5** (8 vCPU, 32 GB RAM) -- **Disk:** 128 GB+ -- **Docker images:** Built by `docker/setup.sh` phases 1-5 (reused, not rebuilt) +- **Azure VM:** Ubuntu 24.04 LTS, **Standard_D8s_v5** (8 vCPU, 32 GB RAM, 128 GB disk) +- **Docker setup phases 1-5 completed** (system deps, Gitea, repos, packages built + published) ## Why k3s? @@ -59,252 +71,156 @@ Ubuntu 24.04 VM └── Gitea (Docker container — :3300, used for build-time only) ``` -## Manifest Structure (planned) +## File Structure ``` k8s/ ├── README.md # This file -├── setup-k8s.sh # Bootstrap script (installs k3s, applies manifests) +├── setup-k8s.sh # Bootstrap script (6 phases) ├── namespaces.yaml # 4 namespaces ├── config/ │ ├── configmap.yaml # Shared env vars (replaces .env.ecosystem) -│ └── secrets.yaml # JWT_SECRET, COSMOS_KEY, etc. +│ └── secrets.yaml # JWT_SECRET template (generated at deploy) ├── infra/ -│ ├── cosmos-emulator.yaml # StatefulSet + Service + PVC -│ ├── azurite.yaml # StatefulSet + Service + PVC -│ ├── mailpit.yaml # Deployment + Service -│ ├── loki.yaml # StatefulSet + Service + PVC -│ └── grafana.yaml # Deployment + Service + PVC +│ ├── cosmos-emulator.yaml # StatefulSet + Service + PVC + NodePort +│ ├── azurite.yaml # StatefulSet + Service + PVC + NodePort +│ ├── mailpit.yaml # Deployment + Service + NodePort +│ ├── loki.yaml # StatefulSet + Service + PVC + NodePort +│ ├── grafana.yaml # Deployment + Service + PVC + NodePort +│ └── ollama-external.yaml # Service + Endpoints → host Ollama ├── platform/ -│ ├── platform-service.yaml # Deployment + Service -│ ├── extraction-service.yaml # Deployment + Service -│ └── mcp-server.yaml # Deployment + Service +│ ├── platform-service.yaml # Deployment + Service + NodePort (:4003) +│ ├── extraction-service.yaml # Deployment + Service + NodePort (:4005) +│ └── mcp-server.yaml # Deployment + Service + NodePort (:4007) ├── dashboards/ -│ ├── admin-web.yaml # Deployment + Service -│ └── tracker-web.yaml # Deployment + Service -├── products/ -│ ├── _backend-template.yaml # Helm-like template (for reference) -│ ├── peakpulse-backend.yaml -│ ├── chronomind-backend.yaml -│ ├── ... (8 more backends) -│ ├── lysnrai-dashboard.yaml -│ ├── chronomind-web.yaml -│ └── ... (7 more web apps) -└── ingress/ - └── ingress.yaml # Traefik IngressRoute rules +│ ├── admin-web.yaml # Deployment + Service + NodePort (:3001) +│ └── tracker-web.yaml # Deployment + Service + NodePort (:3003) +└── products/ + ├── backends.yaml # 10 backend Deployments + Services + NodePorts + └── webs.yaml # 9 web Deployments + Services + NodePorts ``` +## Setup Phases + +| Phase | Duration | What happens | +|-------|----------|--------------| +| 1. Pre-flight | ~10s | Verify Docker phases 1-5 completed, check disk/RAM | +| 2. Install k3s | ~2 min | k3s with Docker runtime, NodePort range 1024-32767 | +| 3. Build images | ~15 min | Docker compose build + tag as `bytelyst/:latest` | +| 4. Generate config | ~30s | Namespaces, ConfigMap (3 copies), Secrets (JWT), Ollama endpoint | +| 5. Deploy | ~5 min | Apply manifests: infra → platform → dashboards → products | +| 6. Health check | ~1 min | 32 endpoint checks + kubectl pod status | + ## Key Design Decisions -### 1. Image Source: Import from Docker +### k3s with Docker Runtime +k3s installed with `--docker` flag — reuses existing Docker daemon and images. +No `containerd` import step needed. Same images used by Docker Compose work directly. -k3s uses containerd, not Docker. We import the Docker-built images: +### 4-Namespace Isolation +- **bytelyst-infra** — Cosmos emulator, Azurite, Mailpit, Loki, Grafana +- **bytelyst-platform** — platform-service, extraction-service, mcp-server +- **bytelyst-dashboards** — admin-web, tracker-web +- **bytelyst-products** — 10 backends + 9 web apps -```bash -# Build images with Docker (phases 1-7 from docker/setup.sh) -docker save platform-service:latest | k3s ctr images import - +ConfigMap + Secrets are copied to all 3 app namespaces by the setup script. -# Or build directly with nerdctl (k3s-native) -nerdctl build -t platform-service:latest -f services/platform-service/Dockerfile . -``` +### Cross-Namespace DNS +K8s DNS: `..svc.cluster.local` +- Backends reach Cosmos: `cosmos-emulator.bytelyst-infra.svc:8081` +- Webs reach backends: `flowmonk-backend.bytelyst-products.svc:4017` +- Everything reaches platform: `platform-service.bytelyst-platform.svc:4003` -**Decision:** Import from Docker first (simpler), migrate to nerdctl later. +### Ollama as External Service +Ollama stays on the host (systemd). A headless Service + Endpoints in `bytelyst-infra` +points to the node's internal IP. Pods reach it as `ollama.bytelyst-infra.svc:11434`. +Setup script auto-detects the node IP. -### 2. Cosmos Emulator: StatefulSet with PVC +### NodePort for External Access +All services use the **same ports** as Docker Compose (e.g., `:4003`, `:3002`, `:3030`). +k3s is configured with `--kube-apiserver-arg=service-node-port-range=1024-32767`. -The Cosmos emulator needs persistent storage and specific env vars. -Use a `StatefulSet` (not Deployment) for stable network identity: - -```yaml -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: cosmos-emulator - namespace: bytelyst-infra -spec: - replicas: 1 - serviceName: cosmos-emulator - template: - spec: - containers: - - name: cosmos - image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest - ports: - - containerPort: 8081 - - containerPort: 1234 - env: - - name: AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE - value: "true" - - name: ENABLE_EXPLORER - value: "true" - resources: - limits: - memory: "3Gi" - cpu: "2" - volumeClaimTemplates: - - metadata: - name: cosmos-data - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 10Gi -``` - -### 3. Ollama: Host Network - -Ollama stays as a systemd service on the host. Pods reach it via `hostNetwork` -or a manually created Endpoints + Service pointing to the node IP: - -```yaml -apiVersion: v1 -kind: Service -metadata: - name: ollama - namespace: bytelyst-products -spec: - ports: - - port: 11434 ---- -apiVersion: v1 -kind: Endpoints -metadata: - name: ollama - namespace: bytelyst-products -subsets: -- addresses: - - ip: 172.17.0.1 # Host IP (node's internal IP) - ports: - - port: 11434 -``` - -### 4. ConfigMap replaces .env.ecosystem - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: bytelyst-config - namespace: bytelyst-platform -data: - COSMOS_ENDPOINT: "http://cosmos-emulator.bytelyst-infra.svc:8081" - COSMOS_DATABASE: "bytelyst" - DB_PROVIDER: "cosmos" - PLATFORM_SERVICE_URL: "http://platform-service.bytelyst-platform.svc:4003" - EXTRACTION_SERVICE_URL: "http://extraction-service.bytelyst-platform.svc:4005" -``` - -Note: K8s DNS uses `..svc` format for cross-namespace access. - -### 5. Secrets for sensitive values - -```yaml -apiVersion: v1 -kind: Secret -metadata: - name: bytelyst-secrets -type: Opaque -stringData: - JWT_SECRET: "" - COSMOS_KEY: "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" - AZURE_BLOB_ACCOUNT_KEY: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" -``` - -### 6. Health Checks → Readiness/Liveness Probes - -Every backend gets K8s-native probes: - -```yaml -readinessProbe: - httpGet: - path: /health - port: 4003 - initialDelaySeconds: 15 - periodSeconds: 10 -livenessProbe: - httpGet: - path: /health - port: 4003 - initialDelaySeconds: 30 - periodSeconds: 30 -``` - -### 7. Resource Limits +### Resource Limits (tuned for 32 GB VM, 50 beta users) | Service type | CPU request | CPU limit | Memory request | Memory limit | |-------------|------------|-----------|---------------|-------------| -| Backend | 100m | 500m | 256Mi | 512Mi | -| Web app | 100m | 500m | 256Mi | 512Mi | -| Platform service | 200m | 1000m | 384Mi | 768Mi | -| Cosmos emulator | 1000m | 2000m | 2Gi | 3Gi | -| Ollama | (host) | (host) | (host) | (host) | +| Backend (×10) | 100m | 500m | 256Mi | 512Mi | +| Web app (×9) | 100m | 500m | 256Mi | 512Mi | +| Platform (×3) | 200m | 1000m | 384Mi | 768Mi | +| Cosmos emulator | 500m | 2000m | 2Gi | 3Gi | +| Grafana | 100m | 500m | 128Mi | 256Mi | +| Mailpit / Loki | 50-100m | 500m | 64-128Mi | 512Mi | +| k3s overhead | — | — | — | ~512Mi | +| Ollama (host) | — | — | — | ~3Gi | +| **Total** | | | **~10 Gi** | **~19 Gi** | -## Implementation Phases +Fits comfortably in 32 GB with ~13 GB headroom. -### Phase A: Foundation (Day 1) -- [ ] Install k3s on VM -- [ ] Create 4 namespaces -- [ ] Deploy ConfigMap + Secrets -- [ ] Deploy cosmos-emulator + azurite (StatefulSets) -- [ ] Verify: `kubectl get pods -A` shows infra running +### Readiness + Liveness Probes +Every service gets both: +- **Readiness:** `GET /health` every 10s (traffic only when ready) +- **Liveness:** `GET /health` every 30s (auto-restart on failure) +- Backends: `initialDelaySeconds: 15`, Web apps: `initialDelaySeconds: 15` +- Cosmos emulator: `initialDelaySeconds: 60` (slow startup) -### Phase B: Platform (Day 1-2) -- [ ] Import platform-service Docker image -- [ ] Deploy platform-service (Deployment + Service) -- [ ] Verify: `kubectl exec` + `curl http://platform-service:4003/health` -- [ ] Deploy extraction-service + mcp-server -- [ ] Deploy admin-web + tracker-web - -### Phase C: Products (Day 2-3) -- [ ] Template: create one backend manifest, verify it works -- [ ] Replicate for all 10 backends -- [ ] Create web app manifests (9 services) -- [ ] Verify: all 30 services running - -### Phase D: Networking (Day 3) -- [ ] Set up Traefik IngressRoute for external access -- [ ] Configure NodePort services for direct port access -- [ ] Create Ollama external service endpoint -- [ ] Verify: health check script works against K8s services - -### Phase E: Operations (Day 4+) -- [ ] `kubectl scale deployment/flowmonk-backend --replicas=2` — test scaling -- [ ] `kubectl rollout restart deployment/platform-service` — test rolling update -- [ ] `kubectl top pods` — resource usage monitoring -- [ ] Set up HorizontalPodAutoscaler for one service -- [ ] Practice: `kubectl logs`, `kubectl exec`, `kubectl describe` - -## Useful Commands (cheat sheet) +## Operations Cheat Sheet ```bash -# Cluster status -kubectl get nodes -kubectl get pods -A # All namespaces -kubectl get pods -n bytelyst-products # Product namespace +# ── Cluster status ───────────────────────────────── +kubectl get nodes # Node health +kubectl get pods -A # All pods +kubectl top pods -A # Resource usage (CPU/memory) -# Deploy / update -kubectl apply -f k8s/ # Apply all manifests -kubectl apply -f k8s/products/ # Apply product manifests -kubectl rollout restart deployment/flowmonk-backend -n bytelyst-products +# ── Deploy / update ──────────────────────────────── +kubectl apply -f k8s/products/ # Re-apply product manifests +kubectl rollout restart deploy/flowmonk-backend -n bytelyst-products # Rolling restart -# Debugging -kubectl logs deployment/platform-service -n bytelyst-platform -f -kubectl describe pod -n bytelyst-platform -kubectl exec -it deployment/platform-service -n bytelyst-platform -- sh +# ── Scaling (for load testing) ───────────────────── +kubectl scale deploy/platform-service --replicas=2 -n bytelyst-platform +kubectl autoscale deploy/flowmonk-backend --min=1 --max=3 --cpu-percent=70 -n bytelyst-products -# Scaling -kubectl scale deployment/flowmonk-backend --replicas=2 -n bytelyst-products -kubectl autoscale deployment/flowmonk-backend --min=1 --max=3 --cpu-percent=70 +# ── Debugging ────────────────────────────────────── +kubectl logs deploy/platform-service -n bytelyst-platform -f # Stream logs +kubectl describe pod -n bytelyst-platform # Pod events +kubectl exec -it deploy/platform-service -n bytelyst-platform -- sh # Shell into pod -# Resource monitoring -kubectl top pods -n bytelyst-products -kubectl top nodes +# ── Teardown ─────────────────────────────────────── +sudo ./setup-k8s.sh --teardown # Delete all namespaces (keep k3s) +/usr/local/bin/k3s-uninstall.sh # Uninstall k3s completely ``` -## Migration from Docker Compose +## Port Map (same as Docker Compose) -Both approaches can coexist on the same VM: -1. `docker/setup.sh` builds images and publishes packages (phases 1-5) -2. `docker compose down` stops the compose stack -3. `setup-k8s.sh` imports images into k3s and applies manifests -4. Both share the same Gitea registry and Ollama instance +| Service | Port | Health check | +|---------|------|-------------| +| Gitea (npm) | 3300 | `http://localhost:3300/api/v1/version` | +| Ollama (LLM) | 11434 | `http://localhost:11434/api/version` | +| Cosmos Explorer | 1234 | `http://localhost:1234` | +| Azurite (Blob) | 10000 | `http://localhost:10000/devstoreaccount1?comp=list` | +| Mailpit UI | 8025 | `http://localhost:8025` | +| Loki | 3100 | `http://localhost:3100/ready` | +| Grafana | 3000 | `http://localhost:3000/api/health` | +| platform-service | 4003 | `/health` | +| extraction-service | 4005 | `/health` | +| mcp-server | 4007 | `/health` | +| admin-web | 3001 | `/` | +| tracker-web | 3003 | `/` | +| Backends | 4010-4019 | `/health` | +| Web apps | 3002, 3030, 3035, 3040, 3045, 3050, 3055, 3060, 3070 | `/` | + +## Switching Between Docker Compose and K8s + +Both approaches coexist on the same VM: + +```bash +# Docker → K8s +cd /opt/bytelyst/learning_ai_common_plat +docker compose -f docker-compose.ecosystem.yml down # Stop compose stack +sudo ../docs/devops/single_azure_vm/k8s/setup-k8s.sh # Deploy to k3s + +# K8s → Docker +sudo ./setup-k8s.sh --teardown # Remove k8s resources +sudo ../docker/setup.sh --phase=7 # Re-deploy via compose +``` + +Both share: Gitea registry (Docker container), Ollama (systemd), and built Docker images. diff --git a/docs/devops/single_azure_vm/k8s/config/configmap.yaml b/docs/devops/single_azure_vm/k8s/config/configmap.yaml new file mode 100644 index 00000000..7ed91134 --- /dev/null +++ b/docs/devops/single_azure_vm/k8s/config/configmap.yaml @@ -0,0 +1,77 @@ +# Shared configuration for all ByteLyst services. +# Generated by setup-k8s.sh phase 4 — this is the TEMPLATE. +# The setup script replaces JWT_SECRET with a random value at deploy time. +apiVersion: v1 +kind: ConfigMap +metadata: + name: bytelyst-config + namespace: bytelyst-platform + labels: + app.kubernetes.io/part-of: bytelyst +data: + # Cosmos DB Emulator + COSMOS_ENDPOINT: "http://cosmos-emulator.bytelyst-infra.svc:8081" + COSMOS_KEY: "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" + COSMOS_DATABASE: "bytelyst" + DB_PROVIDER: "cosmos" + + # Azure Blob Storage (Azurite) — keys in bytelyst-secrets + STORAGE_PROVIDER: "azure" + AZURE_BLOB_ACCOUNT_NAME: "devstoreaccount1" + AZURE_BLOB_PUBLIC_ENDPOINT: "http://localhost:10000/devstoreaccount1" + + # Email (Mailpit) + EMAIL_PROVIDER: "smtp" + EMAIL_FROM_ADDRESS: "noreply@bytelyst.local" + EMAIL_FROM_NAME: "ByteLyst" + SMTP_HOST: "mailpit.bytelyst-infra.svc" + SMTP_PORT: "1025" + SMTP_SECURE: "false" + SMTP_USER: "" + SMTP_PASSWORD: "" + + # Stripe (test placeholders) + STRIPE_SECRET_KEY: "sk_test_placeholder" + STRIPE_WEBHOOK_SECRET: "whsec_placeholder" + STRIPE_PRICE_PRO: "price_placeholder" + STRIPE_PRICE_ENTERPRISE: "price_placeholder" + + # Extraction Service + PYTHON_SIDECAR_URL: "http://localhost:4006" + DEFAULT_MODEL_ID: "gemini-2.5-flash" + GEMINI_API_KEY: "placeholder" + EXTRACTION_QUEUE_BACKEND: "file" + EXTRACTION_QUEUE_FILE: ".data/extraction-jobs.json" + + # Cross-service URLs (K8s DNS: ..svc) + PLATFORM_SERVICE_URL: "http://platform-service.bytelyst-platform.svc:4003" + EXTRACTION_SERVICE_URL: "http://extraction-service.bytelyst-platform.svc:4005" + MCP_SERVER_URL: "http://mcp-server.bytelyst-platform.svc:4007" + + # Telemetry + TELEMETRY_ENABLED: "true" + RATE_LIMIT_STORE_MODE: "datastore" + + # Event Bus + EVENT_BUS_BACKEND: "file" + EVENT_BUS_FILE: ".data/platform-events.json" + + # Field Encryption + FIELD_ENCRYPT_KEY_PROVIDER: "memory" + + # Product Identity + DEFAULT_PRODUCT_ID: "lysnrai" + + # Webhooks (disabled) + WEBHOOK_INVITATION_REDEEMED_URL: "" + WEBHOOK_REFERRAL_STATUS_URL: "" + WEBHOOK_WAITLIST_JOINED_URL: "" + + # Notifications (disabled) + TELEGRAM_BOT_TOKEN: "" + TELEGRAM_DEFAULT_CHAT_ID: "" + SLACK_WEBHOOK_URL: "" + SLACK_DEFAULT_CHANNEL: "" + + # Ollama (host service via ExternalName) + OLLAMA_URL: "http://ollama.bytelyst-infra.svc:11434" diff --git a/docs/devops/single_azure_vm/k8s/config/secrets.yaml b/docs/devops/single_azure_vm/k8s/config/secrets.yaml new file mode 100644 index 00000000..f9d8191e --- /dev/null +++ b/docs/devops/single_azure_vm/k8s/config/secrets.yaml @@ -0,0 +1,14 @@ +# Sensitive configuration — JWT_SECRET is replaced by setup-k8s.sh at deploy time. +# Cosmos and Azurite keys are well-known emulator keys (public, safe to commit). +apiVersion: v1 +kind: Secret +metadata: + name: bytelyst-secrets + namespace: bytelyst-platform + labels: + app.kubernetes.io/part-of: bytelyst +type: Opaque +stringData: + JWT_SECRET: "REPLACE_ME_AT_DEPLOY_TIME" + AZURE_BLOB_ACCOUNT_KEY: "REPLACE_ME_AT_DEPLOY_TIME" + AZURE_BLOB_CONNECTION_STRING: "REPLACE_ME_AT_DEPLOY_TIME" diff --git a/docs/devops/single_azure_vm/k8s/dashboards/admin-web.yaml b/docs/devops/single_azure_vm/k8s/dashboards/admin-web.yaml new file mode 100644 index 00000000..2d29b82d --- /dev/null +++ b/docs/devops/single_azure_vm/k8s/dashboards/admin-web.yaml @@ -0,0 +1,80 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: admin-web + namespace: bytelyst-dashboards + labels: + app: admin-web + tier: dashboard +spec: + replicas: 1 + selector: + matchLabels: + app: admin-web + template: + metadata: + labels: + app: admin-web + tier: dashboard + spec: + containers: + - name: admin-web + image: bytelyst/admin-web:latest + imagePullPolicy: Never + ports: + - containerPort: 3001 + name: http + env: + - name: NODE_ENV + value: "production" + - name: PORT + value: "3001" + - name: PLATFORM_SERVICE_URL + value: "http://platform-service.bytelyst-platform.svc:4003" + resources: + requests: + cpu: "100m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "512Mi" + readinessProbe: + httpGet: + path: / + port: 3001 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 3001 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: admin-web + namespace: bytelyst-dashboards +spec: + selector: + app: admin-web + ports: + - name: http + port: 3001 + targetPort: 3001 +--- +apiVersion: v1 +kind: Service +metadata: + name: admin-web-external + namespace: bytelyst-dashboards +spec: + type: NodePort + selector: + app: admin-web + ports: + - name: http + port: 3001 + targetPort: 3001 + nodePort: 3001 diff --git a/docs/devops/single_azure_vm/k8s/dashboards/tracker-web.yaml b/docs/devops/single_azure_vm/k8s/dashboards/tracker-web.yaml new file mode 100644 index 00000000..fdf4e85d --- /dev/null +++ b/docs/devops/single_azure_vm/k8s/dashboards/tracker-web.yaml @@ -0,0 +1,80 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tracker-web + namespace: bytelyst-dashboards + labels: + app: tracker-web + tier: dashboard +spec: + replicas: 1 + selector: + matchLabels: + app: tracker-web + template: + metadata: + labels: + app: tracker-web + tier: dashboard + spec: + containers: + - name: tracker-web + image: bytelyst/tracker-web:latest + imagePullPolicy: Never + ports: + - containerPort: 3003 + name: http + env: + - name: NODE_ENV + value: "production" + - name: PORT + value: "3003" + - name: PLATFORM_SERVICE_URL + value: "http://platform-service.bytelyst-platform.svc:4003" + resources: + requests: + cpu: "100m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "512Mi" + readinessProbe: + httpGet: + path: / + port: 3003 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 3003 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: tracker-web + namespace: bytelyst-dashboards +spec: + selector: + app: tracker-web + ports: + - name: http + port: 3003 + targetPort: 3003 +--- +apiVersion: v1 +kind: Service +metadata: + name: tracker-web-external + namespace: bytelyst-dashboards +spec: + type: NodePort + selector: + app: tracker-web + ports: + - name: http + port: 3003 + targetPort: 3003 + nodePort: 3003 diff --git a/docs/devops/single_azure_vm/k8s/infra/azurite.yaml b/docs/devops/single_azure_vm/k8s/infra/azurite.yaml new file mode 100644 index 00000000..d7d50def --- /dev/null +++ b/docs/devops/single_azure_vm/k8s/infra/azurite.yaml @@ -0,0 +1,80 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: azurite + namespace: bytelyst-infra + labels: + app: azurite +spec: + replicas: 1 + serviceName: azurite + selector: + matchLabels: + app: azurite + template: + metadata: + labels: + app: azurite + spec: + containers: + - name: azurite + image: mcr.microsoft.com/azure-storage/azurite:latest + command: ["azurite", "--blobHost", "0.0.0.0", "--queueHost", "0.0.0.0", "--tableHost", "0.0.0.0", "-l", "/data"] + ports: + - containerPort: 10000 + name: blob + - containerPort: 10001 + name: queue + - containerPort: 10002 + name: table + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "512Mi" + readinessProbe: + tcpSocket: + port: 10000 + initialDelaySeconds: 5 + periodSeconds: 10 + volumeMounts: + - name: azurite-data + mountPath: /data + volumeClaimTemplates: + - metadata: + name: azurite-data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 5Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: azurite + namespace: bytelyst-infra +spec: + selector: + app: azurite + ports: + - name: blob + port: 10000 + targetPort: 10000 +--- +apiVersion: v1 +kind: Service +metadata: + name: azurite-external + namespace: bytelyst-infra +spec: + type: NodePort + selector: + app: azurite + ports: + - name: blob + port: 10000 + targetPort: 10000 + nodePort: 10000 diff --git a/docs/devops/single_azure_vm/k8s/infra/cosmos-emulator.yaml b/docs/devops/single_azure_vm/k8s/infra/cosmos-emulator.yaml new file mode 100644 index 00000000..fc12da03 --- /dev/null +++ b/docs/devops/single_azure_vm/k8s/infra/cosmos-emulator.yaml @@ -0,0 +1,93 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: cosmos-emulator + namespace: bytelyst-infra + labels: + app: cosmos-emulator +spec: + replicas: 1 + serviceName: cosmos-emulator + selector: + matchLabels: + app: cosmos-emulator + template: + metadata: + labels: + app: cosmos-emulator + spec: + containers: + - name: cosmos + image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest + ports: + - containerPort: 8081 + name: api + - containerPort: 1234 + name: explorer + env: + - name: AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE + value: "true" + - name: ENABLE_EXPLORER + value: "true" + - name: AZURE_COSMOS_EMULATOR_IP_ADDRESS_OVERRIDE + value: "0.0.0.0" + - name: PROTOCOL + value: "http" + resources: + requests: + cpu: "500m" + memory: "2Gi" + limits: + cpu: "2000m" + memory: "3Gi" + readinessProbe: + httpGet: + path: /_explorer/emulator.pem + port: 8081 + scheme: HTTP + initialDelaySeconds: 60 + periodSeconds: 15 + timeoutSeconds: 5 + volumeMounts: + - name: cosmos-data + mountPath: /tmp/cosmos/appdata + volumeClaimTemplates: + - metadata: + name: cosmos-data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 10Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: cosmos-emulator + namespace: bytelyst-infra +spec: + selector: + app: cosmos-emulator + ports: + - name: api + port: 8081 + targetPort: 8081 + - name: explorer + port: 1234 + targetPort: 1234 +--- +# NodePort for external access to Cosmos Explorer +apiVersion: v1 +kind: Service +metadata: + name: cosmos-emulator-external + namespace: bytelyst-infra +spec: + type: NodePort + selector: + app: cosmos-emulator + ports: + - name: explorer + port: 1234 + targetPort: 1234 + nodePort: 1234 diff --git a/docs/devops/single_azure_vm/k8s/infra/grafana.yaml b/docs/devops/single_azure_vm/k8s/infra/grafana.yaml new file mode 100644 index 00000000..8e9048f4 --- /dev/null +++ b/docs/devops/single_azure_vm/k8s/infra/grafana.yaml @@ -0,0 +1,89 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: grafana + namespace: bytelyst-infra + labels: + app: grafana +spec: + replicas: 1 + selector: + matchLabels: + app: grafana + template: + metadata: + labels: + app: grafana + spec: + containers: + - name: grafana + image: grafana/grafana:10.4.0 + ports: + - containerPort: 3000 + name: http + env: + - name: GF_SECURITY_ADMIN_USER + value: "admin" + - name: GF_SECURITY_ADMIN_PASSWORD + value: "bytelyst" + - name: GF_USERS_ALLOW_SIGN_UP + value: "false" + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "256Mi" + readinessProbe: + httpGet: + path: /api/health + port: 3000 + initialDelaySeconds: 10 + periodSeconds: 10 + volumeMounts: + - name: grafana-data + mountPath: /var/lib/grafana + volumes: + - name: grafana-data + persistentVolumeClaim: + claimName: grafana-data +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: grafana-data + namespace: bytelyst-infra +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 2Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: grafana + namespace: bytelyst-infra +spec: + selector: + app: grafana + ports: + - name: http + port: 3000 + targetPort: 3000 +--- +apiVersion: v1 +kind: Service +metadata: + name: grafana-external + namespace: bytelyst-infra +spec: + type: NodePort + selector: + app: grafana + ports: + - name: http + port: 3000 + targetPort: 3000 + nodePort: 3000 diff --git a/docs/devops/single_azure_vm/k8s/infra/loki.yaml b/docs/devops/single_azure_vm/k8s/infra/loki.yaml new file mode 100644 index 00000000..f7338530 --- /dev/null +++ b/docs/devops/single_azure_vm/k8s/infra/loki.yaml @@ -0,0 +1,77 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: loki + namespace: bytelyst-infra + labels: + app: loki +spec: + replicas: 1 + serviceName: loki + selector: + matchLabels: + app: loki + template: + metadata: + labels: + app: loki + spec: + containers: + - name: loki + image: grafana/loki:2.9.0 + args: ["-config.file=/etc/loki/local-config.yaml"] + ports: + - containerPort: 3100 + name: http + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "512Mi" + readinessProbe: + httpGet: + path: /ready + port: 3100 + initialDelaySeconds: 15 + periodSeconds: 10 + volumeMounts: + - name: loki-data + mountPath: /loki + volumeClaimTemplates: + - metadata: + name: loki-data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 5Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: loki + namespace: bytelyst-infra +spec: + selector: + app: loki + ports: + - name: http + port: 3100 + targetPort: 3100 +--- +apiVersion: v1 +kind: Service +metadata: + name: loki-external + namespace: bytelyst-infra +spec: + type: NodePort + selector: + app: loki + ports: + - name: http + port: 3100 + targetPort: 3100 + nodePort: 3100 diff --git a/docs/devops/single_azure_vm/k8s/infra/mailpit.yaml b/docs/devops/single_azure_vm/k8s/infra/mailpit.yaml new file mode 100644 index 00000000..e4ff5f79 --- /dev/null +++ b/docs/devops/single_azure_vm/k8s/infra/mailpit.yaml @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mailpit + namespace: bytelyst-infra + labels: + app: mailpit +spec: + replicas: 1 + selector: + matchLabels: + app: mailpit + template: + metadata: + labels: + app: mailpit + spec: + containers: + - name: mailpit + image: axllent/mailpit:latest + ports: + - containerPort: 1025 + name: smtp + - containerPort: 8025 + name: ui + resources: + requests: + cpu: "50m" + memory: "64Mi" + limits: + cpu: "200m" + memory: "128Mi" + readinessProbe: + httpGet: + path: / + port: 8025 + initialDelaySeconds: 5 + periodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + name: mailpit + namespace: bytelyst-infra +spec: + selector: + app: mailpit + ports: + - name: smtp + port: 1025 + targetPort: 1025 + - name: ui + port: 8025 + targetPort: 8025 +--- +apiVersion: v1 +kind: Service +metadata: + name: mailpit-external + namespace: bytelyst-infra +spec: + type: NodePort + selector: + app: mailpit + ports: + - name: ui + port: 8025 + targetPort: 8025 + nodePort: 8025 diff --git a/docs/devops/single_azure_vm/k8s/infra/ollama-external.yaml b/docs/devops/single_azure_vm/k8s/infra/ollama-external.yaml new file mode 100644 index 00000000..dcd01acc --- /dev/null +++ b/docs/devops/single_azure_vm/k8s/infra/ollama-external.yaml @@ -0,0 +1,26 @@ +# Ollama runs on the host as a systemd service, not in K8s. +# This ExternalName service lets pods reach it via K8s DNS: +# ollama.bytelyst-infra.svc:11434 +# +# On the VM, the node's internal IP is used. The setup script +# patches the Endpoints IP at deploy time. +apiVersion: v1 +kind: Service +metadata: + name: ollama + namespace: bytelyst-infra +spec: + ports: + - port: 11434 + targetPort: 11434 +--- +apiVersion: v1 +kind: Endpoints +metadata: + name: ollama + namespace: bytelyst-infra +subsets: +- addresses: + - ip: NODE_IP_PLACEHOLDER + ports: + - port: 11434 diff --git a/docs/devops/single_azure_vm/k8s/namespaces.yaml b/docs/devops/single_azure_vm/k8s/namespaces.yaml new file mode 100644 index 00000000..1ac8964a --- /dev/null +++ b/docs/devops/single_azure_vm/k8s/namespaces.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: bytelyst-infra + labels: + app.kubernetes.io/part-of: bytelyst +--- +apiVersion: v1 +kind: Namespace +metadata: + name: bytelyst-platform + labels: + app.kubernetes.io/part-of: bytelyst +--- +apiVersion: v1 +kind: Namespace +metadata: + name: bytelyst-dashboards + labels: + app.kubernetes.io/part-of: bytelyst +--- +apiVersion: v1 +kind: Namespace +metadata: + name: bytelyst-products + labels: + app.kubernetes.io/part-of: bytelyst diff --git a/docs/devops/single_azure_vm/k8s/platform/extraction-service.yaml b/docs/devops/single_azure_vm/k8s/platform/extraction-service.yaml new file mode 100644 index 00000000..31f42f78 --- /dev/null +++ b/docs/devops/single_azure_vm/k8s/platform/extraction-service.yaml @@ -0,0 +1,85 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: extraction-service + namespace: bytelyst-platform + labels: + app: extraction-service + tier: platform +spec: + replicas: 1 + selector: + matchLabels: + app: extraction-service + template: + metadata: + labels: + app: extraction-service + tier: platform + spec: + containers: + - name: extraction-service + image: bytelyst/extraction-service:latest + imagePullPolicy: Never + ports: + - containerPort: 4005 + name: http + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "4005" + - name: NODE_ENV + value: "production" + envFrom: + - configMapRef: + name: bytelyst-config + - secretRef: + name: bytelyst-secrets + resources: + requests: + cpu: "200m" + memory: "384Mi" + limits: + cpu: "1000m" + memory: "768Mi" + readinessProbe: + httpGet: + path: /health + port: 4005 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 4005 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: extraction-service + namespace: bytelyst-platform +spec: + selector: + app: extraction-service + ports: + - name: http + port: 4005 + targetPort: 4005 +--- +apiVersion: v1 +kind: Service +metadata: + name: extraction-service-external + namespace: bytelyst-platform +spec: + type: NodePort + selector: + app: extraction-service + ports: + - name: http + port: 4005 + targetPort: 4005 + nodePort: 4005 diff --git a/docs/devops/single_azure_vm/k8s/platform/mcp-server.yaml b/docs/devops/single_azure_vm/k8s/platform/mcp-server.yaml new file mode 100644 index 00000000..d2d240b9 --- /dev/null +++ b/docs/devops/single_azure_vm/k8s/platform/mcp-server.yaml @@ -0,0 +1,85 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mcp-server + namespace: bytelyst-platform + labels: + app: mcp-server + tier: platform +spec: + replicas: 1 + selector: + matchLabels: + app: mcp-server + template: + metadata: + labels: + app: mcp-server + tier: platform + spec: + containers: + - name: mcp-server + image: bytelyst/mcp-server:latest + imagePullPolicy: Never + ports: + - containerPort: 4007 + name: http + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "4007" + - name: NODE_ENV + value: "production" + envFrom: + - configMapRef: + name: bytelyst-config + - secretRef: + name: bytelyst-secrets + resources: + requests: + cpu: "100m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "512Mi" + readinessProbe: + httpGet: + path: /health + port: 4007 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 4007 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: mcp-server + namespace: bytelyst-platform +spec: + selector: + app: mcp-server + ports: + - name: http + port: 4007 + targetPort: 4007 +--- +apiVersion: v1 +kind: Service +metadata: + name: mcp-server-external + namespace: bytelyst-platform +spec: + type: NodePort + selector: + app: mcp-server + ports: + - name: http + port: 4007 + targetPort: 4007 + nodePort: 4007 diff --git a/docs/devops/single_azure_vm/k8s/platform/platform-service.yaml b/docs/devops/single_azure_vm/k8s/platform/platform-service.yaml new file mode 100644 index 00000000..94904cf0 --- /dev/null +++ b/docs/devops/single_azure_vm/k8s/platform/platform-service.yaml @@ -0,0 +1,87 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: platform-service + namespace: bytelyst-platform + labels: + app: platform-service + tier: platform +spec: + replicas: 1 + selector: + matchLabels: + app: platform-service + template: + metadata: + labels: + app: platform-service + tier: platform + spec: + containers: + - name: platform-service + image: bytelyst/platform-service:latest + imagePullPolicy: Never + ports: + - containerPort: 4003 + name: http + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "4003" + - name: NODE_ENV + value: "production" + envFrom: + - configMapRef: + name: bytelyst-config + - secretRef: + name: bytelyst-secrets + resources: + requests: + cpu: "200m" + memory: "384Mi" + limits: + cpu: "1000m" + memory: "768Mi" + readinessProbe: + httpGet: + path: /health + port: 4003 + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 3 + livenessProbe: + httpGet: + path: /health + port: 4003 + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 +--- +apiVersion: v1 +kind: Service +metadata: + name: platform-service + namespace: bytelyst-platform +spec: + selector: + app: platform-service + ports: + - name: http + port: 4003 + targetPort: 4003 +--- +apiVersion: v1 +kind: Service +metadata: + name: platform-service-external + namespace: bytelyst-platform +spec: + type: NodePort + selector: + app: platform-service + ports: + - name: http + port: 4003 + targetPort: 4003 + nodePort: 4003 diff --git a/docs/devops/single_azure_vm/k8s/products/backends.yaml b/docs/devops/single_azure_vm/k8s/products/backends.yaml new file mode 100644 index 00000000..1f0e26bb --- /dev/null +++ b/docs/devops/single_azure_vm/k8s/products/backends.yaml @@ -0,0 +1,778 @@ +# All 10 product backends — Deployment + ClusterIP Service + NodePort Service +# Each backend: Fastify 5 + TypeScript, /health endpoint, shared ConfigMap + Secrets +# ═══════════════════════════════════════════════════════════════════════ + +# ── PeakPulse Backend (:4010) ───────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: peakpulse-backend + namespace: bytelyst-products + labels: &pp-labels + app: peakpulse-backend + tier: backend + product: peakpulse +spec: + replicas: 1 + selector: + matchLabels: + app: peakpulse-backend + template: + metadata: + labels: *pp-labels + spec: + containers: + - name: backend + image: bytelyst/peakpulse-backend:latest + imagePullPolicy: Never + ports: + - containerPort: 4010 + env: &backend-env-4010 + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "4010" + - name: NODE_ENV + value: "production" + envFrom: &backend-envfrom + - configMapRef: + name: bytelyst-config + - secretRef: + name: bytelyst-secrets + resources: &backend-resources + requests: + cpu: "100m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "512Mi" + readinessProbe: &probe-4010 + httpGet: + path: /health + port: 4010 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 4010 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: peakpulse-backend + namespace: bytelyst-products +spec: + selector: + app: peakpulse-backend + ports: + - port: 4010 + targetPort: 4010 +--- +apiVersion: v1 +kind: Service +metadata: + name: peakpulse-backend-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: peakpulse-backend + ports: + - port: 4010 + targetPort: 4010 + nodePort: 4010 + +--- +# ── ChronoMind Backend (:4011) ──────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: chronomind-backend + namespace: bytelyst-products + labels: &cm-labels + app: chronomind-backend + tier: backend + product: chronomind +spec: + replicas: 1 + selector: + matchLabels: + app: chronomind-backend + template: + metadata: + labels: *cm-labels + spec: + containers: + - name: backend + image: bytelyst/chronomind-backend:latest + imagePullPolicy: Never + ports: + - containerPort: 4011 + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "4011" + - name: NODE_ENV + value: "production" + envFrom: *backend-envfrom + resources: *backend-resources + readinessProbe: + httpGet: + path: /health + port: 4011 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 4011 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: chronomind-backend + namespace: bytelyst-products +spec: + selector: + app: chronomind-backend + ports: + - port: 4011 + targetPort: 4011 +--- +apiVersion: v1 +kind: Service +metadata: + name: chronomind-backend-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: chronomind-backend + ports: + - port: 4011 + targetPort: 4011 + nodePort: 4011 + +--- +# ── JarvisJr Backend (:4012) ────────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jarvisjr-backend + namespace: bytelyst-products + labels: + app: jarvisjr-backend + tier: backend + product: jarvisjr +spec: + replicas: 1 + selector: + matchLabels: + app: jarvisjr-backend + template: + metadata: + labels: + app: jarvisjr-backend + tier: backend + product: jarvisjr + spec: + containers: + - name: backend + image: bytelyst/jarvisjr-backend:latest + imagePullPolicy: Never + ports: + - containerPort: 4012 + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "4012" + - name: NODE_ENV + value: "production" + envFrom: *backend-envfrom + resources: *backend-resources + readinessProbe: + httpGet: + path: /health + port: 4012 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 4012 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: jarvisjr-backend + namespace: bytelyst-products +spec: + selector: + app: jarvisjr-backend + ports: + - port: 4012 + targetPort: 4012 +--- +apiVersion: v1 +kind: Service +metadata: + name: jarvisjr-backend-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: jarvisjr-backend + ports: + - port: 4012 + targetPort: 4012 + nodePort: 4012 + +--- +# ── NomGap Backend (:4013) ──────────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nomgap-backend + namespace: bytelyst-products + labels: + app: nomgap-backend + tier: backend + product: nomgap +spec: + replicas: 1 + selector: + matchLabels: + app: nomgap-backend + template: + metadata: + labels: + app: nomgap-backend + tier: backend + product: nomgap + spec: + containers: + - name: backend + image: bytelyst/nomgap-backend:latest + imagePullPolicy: Never + ports: + - containerPort: 4013 + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "4013" + - name: NODE_ENV + value: "production" + envFrom: *backend-envfrom + resources: *backend-resources + readinessProbe: + httpGet: + path: /health + port: 4013 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 4013 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: nomgap-backend + namespace: bytelyst-products +spec: + selector: + app: nomgap-backend + ports: + - port: 4013 + targetPort: 4013 +--- +apiVersion: v1 +kind: Service +metadata: + name: nomgap-backend-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: nomgap-backend + ports: + - port: 4013 + targetPort: 4013 + nodePort: 4013 + +--- +# ── MindLyst Backend (:4014) ────────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mindlyst-backend + namespace: bytelyst-products + labels: + app: mindlyst-backend + tier: backend + product: mindlyst +spec: + replicas: 1 + selector: + matchLabels: + app: mindlyst-backend + template: + metadata: + labels: + app: mindlyst-backend + tier: backend + product: mindlyst + spec: + containers: + - name: backend + image: bytelyst/mindlyst-backend:latest + imagePullPolicy: Never + ports: + - containerPort: 4014 + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "4014" + - name: NODE_ENV + value: "production" + envFrom: *backend-envfrom + resources: *backend-resources + readinessProbe: + httpGet: + path: /health + port: 4014 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 4014 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: mindlyst-backend + namespace: bytelyst-products +spec: + selector: + app: mindlyst-backend + ports: + - port: 4014 + targetPort: 4014 +--- +apiVersion: v1 +kind: Service +metadata: + name: mindlyst-backend-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: mindlyst-backend + ports: + - port: 4014 + targetPort: 4014 + nodePort: 4014 + +--- +# ── LysnrAI Backend (:4015) ────────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lysnrai-backend + namespace: bytelyst-products + labels: + app: lysnrai-backend + tier: backend + product: lysnrai +spec: + replicas: 1 + selector: + matchLabels: + app: lysnrai-backend + template: + metadata: + labels: + app: lysnrai-backend + tier: backend + product: lysnrai + spec: + containers: + - name: backend + image: bytelyst/lysnrai-backend:latest + imagePullPolicy: Never + ports: + - containerPort: 4015 + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "4015" + - name: NODE_ENV + value: "production" + envFrom: *backend-envfrom + resources: *backend-resources + readinessProbe: + httpGet: + path: /health + port: 4015 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 4015 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: lysnrai-backend + namespace: bytelyst-products +spec: + selector: + app: lysnrai-backend + ports: + - port: 4015 + targetPort: 4015 +--- +apiVersion: v1 +kind: Service +metadata: + name: lysnrai-backend-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: lysnrai-backend + ports: + - port: 4015 + targetPort: 4015 + nodePort: 4015 + +--- +# ── NoteLett Backend (:4016) ────────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: notelett-backend + namespace: bytelyst-products + labels: + app: notelett-backend + tier: backend + product: notelett +spec: + replicas: 1 + selector: + matchLabels: + app: notelett-backend + template: + metadata: + labels: + app: notelett-backend + tier: backend + product: notelett + spec: + containers: + - name: backend + image: bytelyst/notelett-backend:latest + imagePullPolicy: Never + ports: + - containerPort: 4016 + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "4016" + - name: NODE_ENV + value: "production" + envFrom: *backend-envfrom + resources: *backend-resources + readinessProbe: + httpGet: + path: /health + port: 4016 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 4016 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: notelett-backend + namespace: bytelyst-products +spec: + selector: + app: notelett-backend + ports: + - port: 4016 + targetPort: 4016 +--- +apiVersion: v1 +kind: Service +metadata: + name: notelett-backend-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: notelett-backend + ports: + - port: 4016 + targetPort: 4016 + nodePort: 4016 + +--- +# ── FlowMonk Backend (:4017) ───────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: flowmonk-backend + namespace: bytelyst-products + labels: + app: flowmonk-backend + tier: backend + product: flowmonk +spec: + replicas: 1 + selector: + matchLabels: + app: flowmonk-backend + template: + metadata: + labels: + app: flowmonk-backend + tier: backend + product: flowmonk + spec: + containers: + - name: backend + image: bytelyst/flowmonk-backend:latest + imagePullPolicy: Never + ports: + - containerPort: 4017 + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "4017" + - name: NODE_ENV + value: "production" + envFrom: *backend-envfrom + resources: *backend-resources + readinessProbe: + httpGet: + path: /health + port: 4017 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 4017 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: flowmonk-backend + namespace: bytelyst-products +spec: + selector: + app: flowmonk-backend + ports: + - port: 4017 + targetPort: 4017 +--- +apiVersion: v1 +kind: Service +metadata: + name: flowmonk-backend-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: flowmonk-backend + ports: + - port: 4017 + targetPort: 4017 + nodePort: 4017 + +--- +# ── ActionTrail Backend (:4018) ─────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: actiontrail-backend + namespace: bytelyst-products + labels: + app: actiontrail-backend + tier: backend + product: actiontrail +spec: + replicas: 1 + selector: + matchLabels: + app: actiontrail-backend + template: + metadata: + labels: + app: actiontrail-backend + tier: backend + product: actiontrail + spec: + containers: + - name: backend + image: bytelyst/actiontrail-backend:latest + imagePullPolicy: Never + ports: + - containerPort: 4018 + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "4018" + - name: NODE_ENV + value: "production" + envFrom: *backend-envfrom + resources: *backend-resources + readinessProbe: + httpGet: + path: /health + port: 4018 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 4018 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: actiontrail-backend + namespace: bytelyst-products +spec: + selector: + app: actiontrail-backend + ports: + - port: 4018 + targetPort: 4018 +--- +apiVersion: v1 +kind: Service +metadata: + name: actiontrail-backend-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: actiontrail-backend + ports: + - port: 4018 + targetPort: 4018 + nodePort: 4018 + +--- +# ── LocalMemGPT Backend (:4019) ────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: localmemgpt-backend + namespace: bytelyst-products + labels: + app: localmemgpt-backend + tier: backend + product: localmemgpt +spec: + replicas: 1 + selector: + matchLabels: + app: localmemgpt-backend + template: + metadata: + labels: + app: localmemgpt-backend + tier: backend + product: localmemgpt + spec: + containers: + - name: backend + image: bytelyst/localmemgpt-backend:latest + imagePullPolicy: Never + ports: + - containerPort: 4019 + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "4019" + - name: NODE_ENV + value: "production" + - name: OLLAMA_URL + value: "http://ollama.bytelyst-infra.svc:11434" + envFrom: *backend-envfrom + resources: *backend-resources + readinessProbe: + httpGet: + path: /health + port: 4019 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 4019 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: localmemgpt-backend + namespace: bytelyst-products +spec: + selector: + app: localmemgpt-backend + ports: + - port: 4019 + targetPort: 4019 +--- +apiVersion: v1 +kind: Service +metadata: + name: localmemgpt-backend-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: localmemgpt-backend + ports: + - port: 4019 + targetPort: 4019 + nodePort: 4019 diff --git a/docs/devops/single_azure_vm/k8s/products/webs.yaml b/docs/devops/single_azure_vm/k8s/products/webs.yaml new file mode 100644 index 00000000..a83a7f3d --- /dev/null +++ b/docs/devops/single_azure_vm/k8s/products/webs.yaml @@ -0,0 +1,710 @@ +# All 9 product web apps — Deployment + ClusterIP Service + NodePort Service +# Each web: Next.js 16, product-specific NEXT_PUBLIC_* env vars +# ═══════════════════════════════════════════════════════════════════════ + +# ── LysnrAI Dashboard (:3002) ───────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lysnrai-dashboard + namespace: bytelyst-products + labels: + app: lysnrai-dashboard + tier: web + product: lysnrai +spec: + replicas: 1 + selector: + matchLabels: + app: lysnrai-dashboard + template: + metadata: + labels: + app: lysnrai-dashboard + tier: web + product: lysnrai + spec: + containers: + - name: web + image: bytelyst/lysnrai-dashboard:latest + imagePullPolicy: Never + ports: + - containerPort: 3002 + env: + - name: NODE_ENV + value: "production" + - name: PORT + value: "3002" + - name: PLATFORM_SERVICE_URL + value: "http://platform-service.bytelyst-platform.svc:4003" + - name: ACTIONTRAIL_SERVICE_URL + value: "http://actiontrail-backend.bytelyst-products.svc:4018" + - name: NEXT_PUBLIC_PLATFORM_SERVICE_URL + value: "http://platform-service.bytelyst-platform.svc:4003" + - name: NEXT_PUBLIC_PRODUCT_ID + value: "lysnrai" + resources: &web-resources + requests: + cpu: "100m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "512Mi" + readinessProbe: + httpGet: + path: / + port: 3002 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 3002 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: lysnrai-dashboard + namespace: bytelyst-products +spec: + selector: + app: lysnrai-dashboard + ports: + - port: 3002 + targetPort: 3002 +--- +apiVersion: v1 +kind: Service +metadata: + name: lysnrai-dashboard-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: lysnrai-dashboard + ports: + - port: 3002 + targetPort: 3002 + nodePort: 3002 + +--- +# ── ChronoMind Web (:3030) ─────────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: chronomind-web + namespace: bytelyst-products + labels: + app: chronomind-web + tier: web + product: chronomind +spec: + replicas: 1 + selector: + matchLabels: + app: chronomind-web + template: + metadata: + labels: + app: chronomind-web + tier: web + product: chronomind + spec: + containers: + - name: web + image: bytelyst/chronomind-web:latest + imagePullPolicy: Never + ports: + - containerPort: 3030 + env: + - name: NODE_ENV + value: "production" + - name: PORT + value: "3030" + - name: NEXT_PUBLIC_BACKEND_URL + value: "http://chronomind-backend.bytelyst-products.svc:4011" + - name: NEXT_PUBLIC_PLATFORM_SERVICE_URL + value: "http://platform-service.bytelyst-platform.svc:4003" + resources: *web-resources + readinessProbe: + httpGet: + path: / + port: 3030 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 3030 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: chronomind-web + namespace: bytelyst-products +spec: + selector: + app: chronomind-web + ports: + - port: 3030 + targetPort: 3030 +--- +apiVersion: v1 +kind: Service +metadata: + name: chronomind-web-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: chronomind-web + ports: + - port: 3030 + targetPort: 3030 + nodePort: 3030 + +--- +# ── JarvisJr Web (:3035) ───────────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jarvisjr-web + namespace: bytelyst-products + labels: + app: jarvisjr-web + tier: web + product: jarvisjr +spec: + replicas: 1 + selector: + matchLabels: + app: jarvisjr-web + template: + metadata: + labels: + app: jarvisjr-web + tier: web + product: jarvisjr + spec: + containers: + - name: web + image: bytelyst/jarvisjr-web:latest + imagePullPolicy: Never + ports: + - containerPort: 3035 + env: + - name: NODE_ENV + value: "production" + - name: PORT + value: "3035" + - name: NEXT_PUBLIC_PLATFORM_SERVICE_URL + value: "http://platform-service.bytelyst-platform.svc:4003" + resources: *web-resources + readinessProbe: + httpGet: + path: / + port: 3035 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 3035 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: jarvisjr-web + namespace: bytelyst-products +spec: + selector: + app: jarvisjr-web + ports: + - port: 3035 + targetPort: 3035 +--- +apiVersion: v1 +kind: Service +metadata: + name: jarvisjr-web-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: jarvisjr-web + ports: + - port: 3035 + targetPort: 3035 + nodePort: 3035 + +--- +# ── FlowMonk Web (:3040) ───────────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: flowmonk-web + namespace: bytelyst-products + labels: + app: flowmonk-web + tier: web + product: flowmonk +spec: + replicas: 1 + selector: + matchLabels: + app: flowmonk-web + template: + metadata: + labels: + app: flowmonk-web + tier: web + product: flowmonk + spec: + containers: + - name: web + image: bytelyst/flowmonk-web:latest + imagePullPolicy: Never + ports: + - containerPort: 3040 + env: + - name: NODE_ENV + value: "production" + - name: PORT + value: "3040" + - name: NEXT_PUBLIC_API_URL + value: "http://flowmonk-backend.bytelyst-products.svc:4017" + - name: NEXT_PUBLIC_PLATFORM_URL + value: "http://platform-service.bytelyst-platform.svc:4003/api" + resources: *web-resources + readinessProbe: + httpGet: + path: / + port: 3040 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 3040 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: flowmonk-web + namespace: bytelyst-products +spec: + selector: + app: flowmonk-web + ports: + - port: 3040 + targetPort: 3040 +--- +apiVersion: v1 +kind: Service +metadata: + name: flowmonk-web-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: flowmonk-web + ports: + - port: 3040 + targetPort: 3040 + nodePort: 3040 + +--- +# ── NoteLett Web (:3045) ───────────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: notelett-web + namespace: bytelyst-products + labels: + app: notelett-web + tier: web + product: notelett +spec: + replicas: 1 + selector: + matchLabels: + app: notelett-web + template: + metadata: + labels: + app: notelett-web + tier: web + product: notelett + spec: + containers: + - name: web + image: bytelyst/notelett-web:latest + imagePullPolicy: Never + ports: + - containerPort: 3045 + env: + - name: NODE_ENV + value: "production" + - name: PORT + value: "3045" + - name: NEXT_PUBLIC_NOTES_API_URL + value: "http://notelett-backend.bytelyst-products.svc:4016/api" + - name: NEXT_PUBLIC_PLATFORM_SERVICE_URL + value: "http://platform-service.bytelyst-platform.svc:4003/api" + resources: *web-resources + readinessProbe: + httpGet: + path: / + port: 3045 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 3045 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: notelett-web + namespace: bytelyst-products +spec: + selector: + app: notelett-web + ports: + - port: 3045 + targetPort: 3045 +--- +apiVersion: v1 +kind: Service +metadata: + name: notelett-web-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: notelett-web + ports: + - port: 3045 + targetPort: 3045 + nodePort: 3045 + +--- +# ── MindLyst Web (:3050) ───────────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mindlyst-web + namespace: bytelyst-products + labels: + app: mindlyst-web + tier: web + product: mindlyst +spec: + replicas: 1 + selector: + matchLabels: + app: mindlyst-web + template: + metadata: + labels: + app: mindlyst-web + tier: web + product: mindlyst + spec: + containers: + - name: web + image: bytelyst/mindlyst-web:latest + imagePullPolicy: Never + ports: + - containerPort: 3050 + env: + - name: NODE_ENV + value: "production" + - name: PORT + value: "3050" + - name: NEXT_PUBLIC_PLATFORM_SERVICE_URL + value: "http://platform-service.bytelyst-platform.svc:4003" + resources: *web-resources + readinessProbe: + httpGet: + path: / + port: 3050 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 3050 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: mindlyst-web + namespace: bytelyst-products +spec: + selector: + app: mindlyst-web + ports: + - port: 3050 + targetPort: 3050 +--- +apiVersion: v1 +kind: Service +metadata: + name: mindlyst-web-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: mindlyst-web + ports: + - port: 3050 + targetPort: 3050 + nodePort: 3050 + +--- +# ── NomGap Web (:3055) ─────────────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nomgap-web + namespace: bytelyst-products + labels: + app: nomgap-web + tier: web + product: nomgap +spec: + replicas: 1 + selector: + matchLabels: + app: nomgap-web + template: + metadata: + labels: + app: nomgap-web + tier: web + product: nomgap + spec: + containers: + - name: web + image: bytelyst/nomgap-web:latest + imagePullPolicy: Never + ports: + - containerPort: 3055 + env: + - name: NODE_ENV + value: "production" + - name: PORT + value: "3055" + - name: NEXT_PUBLIC_NOMGAP_API_URL + value: "http://nomgap-backend.bytelyst-products.svc:4013/api" + - name: NEXT_PUBLIC_PLATFORM_SERVICE_URL + value: "http://platform-service.bytelyst-platform.svc:4003/api" + resources: *web-resources + readinessProbe: + httpGet: + path: / + port: 3055 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 3055 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: nomgap-web + namespace: bytelyst-products +spec: + selector: + app: nomgap-web + ports: + - port: 3055 + targetPort: 3055 +--- +apiVersion: v1 +kind: Service +metadata: + name: nomgap-web-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: nomgap-web + ports: + - port: 3055 + targetPort: 3055 + nodePort: 3055 + +--- +# ── ActionTrail Web (:3060) ────────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: actiontrail-web + namespace: bytelyst-products + labels: + app: actiontrail-web + tier: web + product: actiontrail +spec: + replicas: 1 + selector: + matchLabels: + app: actiontrail-web + template: + metadata: + labels: + app: actiontrail-web + tier: web + product: actiontrail + spec: + containers: + - name: web + image: bytelyst/actiontrail-web:latest + imagePullPolicy: Never + ports: + - containerPort: 3060 + env: + - name: NODE_ENV + value: "production" + - name: PORT + value: "3060" + - name: NEXT_PUBLIC_API_URL + value: "http://actiontrail-backend.bytelyst-products.svc:4018" + - name: NEXT_PUBLIC_PLATFORM_URL + value: "http://platform-service.bytelyst-platform.svc:4003" + resources: *web-resources + readinessProbe: + httpGet: + path: / + port: 3060 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 3060 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: actiontrail-web + namespace: bytelyst-products +spec: + selector: + app: actiontrail-web + ports: + - port: 3060 + targetPort: 3060 +--- +apiVersion: v1 +kind: Service +metadata: + name: actiontrail-web-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: actiontrail-web + ports: + - port: 3060 + targetPort: 3060 + nodePort: 3060 + +--- +# ── LocalMemGPT Web (:3070) ────────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: localmemgpt-web + namespace: bytelyst-products + labels: + app: localmemgpt-web + tier: web + product: localmemgpt +spec: + replicas: 1 + selector: + matchLabels: + app: localmemgpt-web + template: + metadata: + labels: + app: localmemgpt-web + tier: web + product: localmemgpt + spec: + containers: + - name: web + image: bytelyst/localmemgpt-web:latest + imagePullPolicy: Never + ports: + - containerPort: 3070 + env: + - name: NODE_ENV + value: "production" + - name: PORT + value: "3070" + - name: NEXT_PUBLIC_BACKEND_URL + value: "http://localmemgpt-backend.bytelyst-products.svc:4019" + - name: NEXT_PUBLIC_PLATFORM_URL + value: "http://platform-service.bytelyst-platform.svc:4003" + resources: *web-resources + readinessProbe: + httpGet: + path: / + port: 3070 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 3070 + initialDelaySeconds: 30 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: localmemgpt-web + namespace: bytelyst-products +spec: + selector: + app: localmemgpt-web + ports: + - port: 3070 + targetPort: 3070 +--- +apiVersion: v1 +kind: Service +metadata: + name: localmemgpt-web-ext + namespace: bytelyst-products +spec: + type: NodePort + selector: + app: localmemgpt-web + ports: + - port: 3070 + targetPort: 3070 + nodePort: 3070 diff --git a/docs/devops/single_azure_vm/k8s/setup-k8s.sh b/docs/devops/single_azure_vm/k8s/setup-k8s.sh new file mode 100755 index 00000000..7b5478a8 --- /dev/null +++ b/docs/devops/single_azure_vm/k8s/setup-k8s.sh @@ -0,0 +1,580 @@ +#!/usr/bin/env bash +# ═══════════════════════════════════════════════════════════════════════ +# ByteLyst Single-VM Kubernetes Setup (k3s) +# ═══════════════════════════════════════════════════════════════════════ +# Deploys the ByteLyst ecosystem on Kubernetes using k3s. +# +# PREREQUISITE: Run ../docker/setup.sh phases 1-5 first to install +# system deps, Gitea, clone repos, build + publish @bytelyst/* packages. +# This script handles k8s-specific deployment only. +# +# What this script does: +# Phase 1: Pre-flight checks (verify docker phases ran) +# Phase 2: Install k3s (lightweight K8s) +# Phase 3: Build Docker images + import into k3s containerd +# Phase 4: Generate K8s secrets (JWT, etc.) +# Phase 5: Apply K8s manifests (namespaces → config → infra → platform → products) +# Phase 6: Health check +# +# Usage: +# sudo ./setup-k8s.sh # Full install +# sudo ./setup-k8s.sh --phase=N # Run single phase (1-6) +# sudo ./setup-k8s.sh --status # Show phase status +# sudo ./setup-k8s.sh --reset # Clear markers, start fresh +# sudo ./setup-k8s.sh --teardown # Remove all K8s resources +# ═══════════════════════════════════════════════════════════════════════ +set -euo pipefail + +# ── Configuration ──────────────────────────────────────────────────── +INSTALL_DIR="/opt/bytelyst" +K8S_DIR="$(cd "$(dirname "$0")" && pwd)" +STATE_DIR="${INSTALL_DIR}/.setup-state-k8s" +COMPOSE_FILE="docker-compose.ecosystem.yml" + +# Well-known emulator keys (public, safe to embed) +COSMOS_EMULATOR_KEY="C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" + +# Image prefix for k3s imports +IMAGE_PREFIX="bytelyst" + +# Services that need Docker image builds (not pre-built infra) +BUILD_SERVICES=( + platform-service extraction-service mcp-server + admin-web tracker-web + peakpulse-backend chronomind-backend jarvisjr-backend nomgap-backend + mindlyst-backend lysnrai-backend notelett-backend flowmonk-backend + actiontrail-backend localmemgpt-backend + lysnrai-dashboard chronomind-web jarvisjr-web flowmonk-web notelett-web + mindlyst-web nomgap-web actiontrail-web localmemgpt-web +) + +# Namespaces that need the shared ConfigMap + Secrets +CONFIG_NAMESPACES=(bytelyst-platform bytelyst-dashboards bytelyst-products) + +# ── Helpers ────────────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' +log() { echo -e "${BLUE}[k8s]${NC} $*"; } +ok() { echo -e "${GREEN} ✓ $*${NC}"; } +warn() { echo -e "${YELLOW} ⚠ $*${NC}"; } +fail() { echo -e "${RED} ✗ $*${NC}" >&2; exit 1; } + +mkdir -p "$STATE_DIR" +mark_phase_done() { date -Iseconds > "${STATE_DIR}/phase${1}.done"; } +is_phase_done() { [ -f "${STATE_DIR}/phase${1}.done" ]; } +reset_markers() { rm -f "${STATE_DIR}"/phase*.done; log "Phase markers cleared."; } + +last_completed_phase() { + local last=0 + for i in 1 2 3 4 5 6; do + if is_phase_done "$i"; then last=$i; else break; fi + done + echo "$last" +} + +# ═══════════════════════════════════════════════════════════════════════ +# PHASE 1: Pre-flight Checks +# ═══════════════════════════════════════════════════════════════════════ +phase1_preflight() { + log "Phase 1: Pre-flight checks..." + + # Verify docker setup phases 1-5 ran + local docker_state="${INSTALL_DIR}/.setup-state" + for p in 1 2 3 4 5; do + if [ ! -f "${docker_state}/phase${p}.done" ]; then + fail "Docker phase ${p} not completed. Run first: sudo ../docker/setup.sh --resume" + fi + done + ok "Docker phases 1-5 completed" + + # Verify repos exist + local plat_dir="${INSTALL_DIR}/learning_ai_common_plat" + [ -d "$plat_dir" ] || fail "Missing ${plat_dir}. Run docker/setup.sh phases 1-5 first." + ok "Repos cloned" + + # Verify Docker is available (needed for image builds) + command -v docker &>/dev/null || fail "Docker not found. Run docker/setup.sh phase 1." + ok "Docker available" + + # Pre-flight: disk + memory + local disk_gb mem_gb + disk_gb=$(df -BG / | awk 'NR==2 {gsub(/G/,"",$4); print $4}') + mem_gb=$(free -g | awk '/^Mem:/ {print $2}') + log " Disk: ${disk_gb} GB free, RAM: ${mem_gb} GB total" + [ "${disk_gb:-0}" -ge 20 ] || warn "Low disk (${disk_gb} GB). Recommend 40+ GB free." + [ "${mem_gb:-0}" -ge 16 ] || warn "Low RAM (${mem_gb} GB). Recommend 32 GB." + + ok "Phase 1 complete. Pre-flight passed." +} + +# ═══════════════════════════════════════════════════════════════════════ +# PHASE 2: Install k3s +# ═══════════════════════════════════════════════════════════════════════ +phase2_k3s() { + log "Phase 2: Installing k3s..." + + if command -v kubectl &>/dev/null && kubectl cluster-info &>/dev/null 2>&1; then + ok "k3s already installed and running" + else + # Install k3s with: + # --docker: use Docker as container runtime (reuse existing images) + # --disable=traefik: we manage our own ingress + # --write-kubeconfig-mode=644: allow non-root kubectl + # --kube-apiserver-arg: extend NodePort range to use our service ports + log " Installing k3s (Docker runtime, extended NodePort range)..." + curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="\ + --docker \ + --disable=traefik \ + --write-kubeconfig-mode=644 \ + --kube-apiserver-arg=service-node-port-range=1024-32767" \ + sh - + + # Wait for k3s to be ready + log " Waiting for k3s node to be Ready..." + local retries=30 + while [ $retries -gt 0 ]; do + if kubectl get nodes 2>/dev/null | grep -q " Ready"; then + break + fi + sleep 5 + retries=$((retries - 1)) + done + + if [ $retries -eq 0 ]; then + fail "k3s node did not become Ready within 150 seconds." + fi + fi + + # Verify + kubectl get nodes + ok "Phase 2 complete. k3s is running." +} + +# ═══════════════════════════════════════════════════════════════════════ +# PHASE 3: Build Docker Images + Tag for k3s +# ═══════════════════════════════════════════════════════════════════════ +phase3_images() { + log "Phase 3: Building Docker images for k3s..." + + local plat_dir="${INSTALL_DIR}/learning_ai_common_plat" + + # Restore Gitea token for Docker builds + if [ -z "${GITEA_NPM_TOKEN:-}" ] && [ -f "${INSTALL_DIR}/.gitea_token" ]; then + GITEA_NPM_TOKEN=$(cat "${INSTALL_DIR}/.gitea_token") + export GITEA_NPM_TOKEN + fi + [ -n "${GITEA_NPM_TOKEN:-}" ] || fail "GITEA_NPM_TOKEN not set. Run docker/setup.sh phase 2." + + # Detect Docker host IP for Gitea access during builds + local docker_host_ip + docker_host_ip=$(ip -4 addr show docker0 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' || echo "172.17.0.1") + export GITEA_NPM_HOST="${docker_host_ip}" + export DOCKER_BUILDKIT=1 + export COMPOSE_DOCKER_CLI_BUILD=1 + + # Stop Ollama to free RAM during builds + if systemctl is-active --quiet ollama 2>/dev/null; then + log " Stopping Ollama to free RAM for builds..." + systemctl stop ollama 2>/dev/null || true + fi + + # Build images using docker compose (same Dockerfiles as docker approach) + local env_file="${plat_dir}/.env.ecosystem" + if [ ! -f "$env_file" ]; then + # Generate minimal env for compose config parsing + log " Generating temporary .env.ecosystem for compose builds..." + cat > "$env_file" <<-ENV + COSMOS_KEY=${COSMOS_EMULATOR_KEY} + JWT_SECRET=build-time-placeholder + COSMOS_ENDPOINT=http://localhost:8081 + COSMOS_DATABASE=bytelyst + DB_PROVIDER=cosmos + ENV + fi + + local build_ok=0 build_fail=0 + local total=${#BUILD_SERVICES[@]} + local idx=0 + mkdir -p "${STATE_DIR}/builds" + + for svc in "${BUILD_SERVICES[@]}"; do + idx=$((idx + 1)) + log " [${idx}/${total}] Building ${svc}..." + + local log_file="${STATE_DIR}/builds/${svc}.log" + if docker compose -f "${plat_dir}/${COMPOSE_FILE}" --env-file "$env_file" \ + build "$svc" > "$log_file" 2>&1; then + + # Tag for k3s: compose builds as -, we tag as bytelyst/ + local compose_image + compose_image=$(docker compose -f "${plat_dir}/${COMPOSE_FILE}" --env-file "$env_file" \ + images --format json 2>/dev/null | jq -r ".[] | select(.Service==\"${svc}\") | .Repository + \":\" + .Tag" 2>/dev/null || true) + + if [ -n "$compose_image" ] && [ "$compose_image" != ":" ]; then + docker tag "$compose_image" "${IMAGE_PREFIX}/${svc}:latest" 2>/dev/null || true + fi + + build_ok=$((build_ok + 1)) + ok " [${idx}/${total}] ${svc} — built" + else + build_fail=$((build_fail + 1)) + warn " [${idx}/${total}] ${svc} — FAILED (see ${log_file})" + fi + done + + # Restart Ollama + if command -v ollama &>/dev/null; then + log " Restarting Ollama..." + systemctl start ollama 2>/dev/null || nohup ollama serve > /var/log/ollama.log 2>&1 & + fi + + # Prune build cache + docker builder prune -f --filter "until=1h" > /dev/null 2>&1 || true + + log " Built: ${build_ok}, Failed: ${build_fail}" + [ "$build_fail" -eq 0 ] || warn " ${build_fail} images failed to build. Fix and re-run: sudo ./setup-k8s.sh --phase=3" + + ok "Phase 3 complete. ${build_ok}/${total} images ready." +} + +# ═══════════════════════════════════════════════════════════════════════ +# PHASE 4: Generate K8s Secrets + Patch ConfigMap +# ═══════════════════════════════════════════════════════════════════════ +phase4_config() { + log "Phase 4: Generating K8s configuration..." + + # Apply namespaces first + log " Creating namespaces..." + kubectl apply -f "${K8S_DIR}/namespaces.yaml" + + # Generate random JWT secret + local jwt_secret + jwt_secret=$(openssl rand -base64 32) + + # Patch the secrets template with real JWT secret + local secrets_file="${K8S_DIR}/config/secrets.yaml" + log " Generating secrets (JWT_SECRET)..." + + # Well-known Azurite emulator key (public, safe) + local azurite_key="Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" + local azurite_conn="DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=${azurite_key};BlobEndpoint=http://azurite.bytelyst-infra.svc:10000/devstoreaccount1;" + + # Apply secrets to all namespaces that need them + for ns in "${CONFIG_NAMESPACES[@]}"; do + kubectl create secret generic bytelyst-secrets \ + --namespace="$ns" \ + --from-literal=JWT_SECRET="$jwt_secret" \ + --from-literal=AZURE_BLOB_ACCOUNT_KEY="$azurite_key" \ + --from-literal=AZURE_BLOB_CONNECTION_STRING="$azurite_conn" \ + --dry-run=client -o yaml | kubectl apply -f - + done + ok "Secrets applied to ${#CONFIG_NAMESPACES[@]} namespaces" + + # Detect node IP for Ollama external service + local node_ip + node_ip=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}' 2>/dev/null || echo "172.17.0.1") + log " Node IP for Ollama: ${node_ip}" + + # Patch Ollama endpoint with actual node IP + sed "s/NODE_IP_PLACEHOLDER/${node_ip}/" "${K8S_DIR}/infra/ollama-external.yaml" \ + | kubectl apply -f - + ok "Ollama external service configured (${node_ip}:11434)" + + # Apply ConfigMap to all namespaces that need it + for ns in "${CONFIG_NAMESPACES[@]}"; do + sed "s/namespace: bytelyst-platform/namespace: ${ns}/" "${K8S_DIR}/config/configmap.yaml" \ + | kubectl apply -f - + done + ok "ConfigMap applied to ${#CONFIG_NAMESPACES[@]} namespaces" + + ok "Phase 4 complete." +} + +# ═══════════════════════════════════════════════════════════════════════ +# PHASE 5: Apply K8s Manifests (ordered deployment) +# ═══════════════════════════════════════════════════════════════════════ +phase5_deploy() { + log "Phase 5: Deploying services to k3s..." + + # ── 5a: Infrastructure ────────────────────────────────────────────── + log " Deploying infrastructure..." + for f in "${K8S_DIR}"/infra/*.yaml; do + local name + name=$(basename "$f" .yaml) + [ "$name" = "ollama-external" ] && continue # Already applied in phase 4 + log " Applying ${name}..." + kubectl apply -f "$f" + done + ok "Infrastructure deployed" + + # Wait for Cosmos emulator (everything depends on it) + log " Waiting for Cosmos emulator to be Ready (this can take 2-3 minutes)..." + kubectl wait --for=condition=Ready pod -l app=cosmos-emulator \ + -n bytelyst-infra --timeout=300s 2>/dev/null || warn "Cosmos emulator not ready yet" + + # ── 5b: Platform services ─────────────────────────────────────────── + log " Deploying platform services..." + kubectl apply -f "${K8S_DIR}/platform/" + ok "Platform services deployed" + + # Wait for platform-service + log " Waiting for platform-service..." + kubectl wait --for=condition=Ready pod -l app=platform-service \ + -n bytelyst-platform --timeout=120s 2>/dev/null || warn "platform-service not ready yet" + + # ── 5c: Dashboards ───────────────────────────────────────────────── + log " Deploying dashboards..." + kubectl apply -f "${K8S_DIR}/dashboards/" + ok "Dashboards deployed" + + # ── 5d: Product services ──────────────────────────────────────────── + log " Deploying product backends + web apps..." + kubectl apply -f "${K8S_DIR}/products/" + ok "Product services deployed" + + # ── Summary ───────────────────────────────────────────────────────── + log " Waiting 30s for services to stabilize..." + sleep 30 + + echo "" + log " Pod status across all namespaces:" + kubectl get pods -A -l app.kubernetes.io/part-of=bytelyst 2>/dev/null || \ + kubectl get pods -A 2>/dev/null | grep -E "bytelyst-" + echo "" + + ok "Phase 5 complete. All manifests applied." +} + +# ═══════════════════════════════════════════════════════════════════════ +# PHASE 6: Health Check +# ═══════════════════════════════════════════════════════════════════════ +phase6_verify() { + log "Phase 6: Verifying service health..." + + # Create reusable health check script + cat > "${INSTALL_DIR}/check-health-k8s.sh" <<'HEALTH' +#!/usr/bin/env bash +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' + +check() { + local name="$1" url="$2" + if curl -sf --connect-timeout 3 "$url" > /dev/null 2>&1; then + echo -e "${GREEN} ✓ ${name}${NC} ${url}" + else + echo -e "${RED} ✗ ${name}${NC} ${url}" + fi +} + +echo "" +echo "═══ K8s Cluster ═══" +echo -n " Nodes: "; kubectl get nodes --no-headers 2>/dev/null | wc -l | tr -d ' ' +echo -n " Pods: "; kubectl get pods -A --no-headers 2>/dev/null | wc -l | tr -d ' ' +echo -n " Ready: "; kubectl get pods -A --no-headers 2>/dev/null | grep -c "Running" || echo 0 + +echo "" +echo "═══ Infrastructure ═══" +check "Gitea (npm)" "http://localhost:3300/api/v1/version" +check "Ollama (LLM)" "http://localhost:11434/api/version" +check "Cosmos Explorer" "http://localhost:1234" +check "Azurite (Blob)" "http://localhost:10000/devstoreaccount1?comp=list" +check "Mailpit" "http://localhost:8025" +check "Loki" "http://localhost:3100/ready" +check "Grafana" "http://localhost:3000/api/health" + +echo "" +echo "═══ Platform Services ═══" +check "platform-service" "http://localhost:4003/health" +check "extraction-service" "http://localhost:4005/health" +check "mcp-server" "http://localhost:4007/health" + +echo "" +echo "═══ Dashboards ═══" +check "admin-web" "http://localhost:3001" +check "tracker-web" "http://localhost:3003" + +echo "" +echo "═══ Product Backends ═══" +check "peakpulse" "http://localhost:4010/health" +check "chronomind" "http://localhost:4011/health" +check "jarvisjr" "http://localhost:4012/health" +check "nomgap" "http://localhost:4013/health" +check "mindlyst" "http://localhost:4014/health" +check "lysnrai" "http://localhost:4015/health" +check "notelett" "http://localhost:4016/health" +check "flowmonk" "http://localhost:4017/health" +check "actiontrail" "http://localhost:4018/health" +check "localmemgpt" "http://localhost:4019/health" + +echo "" +echo "═══ Product Web Apps ═══" +check "lysnrai-dashboard" "http://localhost:3002" +check "chronomind-web" "http://localhost:3030" +check "jarvisjr-web" "http://localhost:3035" +check "flowmonk-web" "http://localhost:3040" +check "notelett-web" "http://localhost:3045" +check "mindlyst-web" "http://localhost:3050" +check "nomgap-web" "http://localhost:3055" +check "actiontrail-web" "http://localhost:3060" +check "localmemgpt-web" "http://localhost:3070" + +echo "" +echo "═══ K8s Quick Commands ═══" +echo " kubectl get pods -A # All pods" +echo " kubectl top pods -A # Resource usage" +echo " kubectl logs deploy/ -n -f # Stream logs" +echo " kubectl rollout restart deploy/ # Rolling restart" +echo "" +HEALTH + chmod +x "${INSTALL_DIR}/check-health-k8s.sh" + + # Run it + bash "${INSTALL_DIR}/check-health-k8s.sh" + + ok "Phase 6 complete." +} + +# ═══════════════════════════════════════════════════════════════════════ +# TEARDOWN: Remove all K8s resources +# ═══════════════════════════════════════════════════════════════════════ +teardown() { + log "Tearing down all ByteLyst K8s resources..." + for ns in bytelyst-products bytelyst-dashboards bytelyst-platform bytelyst-infra; do + log " Deleting namespace: ${ns}" + kubectl delete namespace "$ns" --ignore-not-found=true 2>/dev/null || true + done + reset_markers + ok "Teardown complete. k3s itself is still running (uninstall with: /usr/local/bin/k3s-uninstall.sh)" +} + +# ═══════════════════════════════════════════════════════════════════════ +# MAIN +# ═══════════════════════════════════════════════════════════════════════ +run_phase() { + local n="$1" + case "$n" in + 1) phase1_preflight ;; + 2) phase2_k3s ;; + 3) phase3_images ;; + 4) phase4_config ;; + 5) phase5_deploy ;; + 6) phase6_verify ;; + *) fail "Unknown phase: $n" ;; + esac + mark_phase_done "$n" +} + +usage() { + echo "Usage: sudo ./setup-k8s.sh [OPTIONS]" + echo "" + echo "Options:" + echo " --resume Auto-resume from last completed phase" + echo " --phase=N Run ONLY phase N (1-6)" + echo " --reset Clear phase markers" + echo " --status Show completed phases" + echo " --teardown Remove all K8s resources" + echo " -h, --help Show this help" + echo "" + echo "Phases:" + echo " 1 Pre-flight checks (verify docker phases ran)" + echo " 2 Install k3s" + echo " 3 Build Docker images + tag for k3s" + echo " 4 Generate K8s config (Secrets + ConfigMap)" + echo " 5 Apply manifests (infra → platform → dashboards → products)" + echo " 6 Health check" + echo "" + echo "PREREQUISITE: Run ../docker/setup.sh (phases 1-5) first." +} + +main() { + local mode="full" start_phase=1 only_phase=0 + + for arg in "$@"; do + case "$arg" in + --resume) + mode="resume" ;; + --phase=*) + mode="single" + only_phase="${arg#*=}" ;; + --reset) + reset_markers; exit 0 ;; + --status) + echo "K8s phase completion:" + for i in 1 2 3 4 5 6; do + if is_phase_done "$i"; then + echo " Phase $i: DONE ($(cat "${STATE_DIR}/phase${i}.done"))" + else + echo " Phase $i: pending" + fi + done + exit 0 ;; + --teardown) + teardown; exit 0 ;; + -h|--help) + usage; exit 0 ;; + *) + warn "Unknown option: $arg"; usage; exit 1 ;; + esac + done + + mkdir -p "$INSTALL_DIR" + exec > >(tee -a "${INSTALL_DIR}/setup-k8s.log") 2>&1 + + echo "" + echo "╔═══════════════════════════════════════════════════════════════╗" + echo "║ ByteLyst K8s Deployment (k3s on single VM) ║" + echo "║ 30 services · 4 namespaces · production-grade ║" + echo "╚═══════════════════════════════════════════════════════════════╝" + echo "" + log "Log file: ${INSTALL_DIR}/setup-k8s.log" + + [ "$(id -u)" -eq 0 ] || fail "This script must be run as root (sudo)." + + local start_time + start_time=$(date +%s) + + if [ "$mode" = "single" ]; then + log "Running ONLY phase ${only_phase}..." + run_phase "$only_phase" + ok "Phase ${only_phase} complete." + exit 0 + fi + + if [ "$mode" = "resume" ]; then + local last + last=$(last_completed_phase) + if [ "$last" -ge 6 ]; then + ok "All phases completed. Use --reset to start over." + exit 0 + fi + start_phase=$((last + 1)) + log "Resuming from phase ${start_phase}." + fi + + for n in 1 2 3 4 5 6; do + [ "$n" -ge "$start_phase" ] || continue + run_phase "$n" + done + + local elapsed=$(( $(date +%s) - start_time )) + local minutes=$(( elapsed / 60 )) + local seconds=$(( elapsed % 60 )) + + echo "" + echo "╔═══════════════════════════════════════════════════════════════╗" + echo "║ K8s deployment complete in ${minutes}m ${seconds}s ║" + echo "║ ║" + echo "║ Health check: /opt/bytelyst/check-health-k8s.sh ║" + echo "║ All pods: kubectl get pods -A ║" + echo "║ Pod resources: kubectl top pods -A ║" + echo "║ Stream logs: kubectl logs deploy/ -n -f ║" + echo "║ Scale up: kubectl scale deploy/ --replicas=2 ║" + echo "║ Teardown: sudo ./setup-k8s.sh --teardown ║" + echo "║ ║" + echo "║ Grafana: http://localhost:3000 (admin / bytelyst) ║" + echo "║ Mailpit: http://localhost:8025 ║" + echo "║ Gitea: http://localhost:3300 ║" + echo "║ Ollama: http://localhost:11434 ║" + echo "╚═══════════════════════════════════════════════════════════════╝" + echo "" +} + +main "$@"