diff --git a/.commitlintrc.json b/.commitlintrc.json new file mode 100644 index 00000000..5ca7d2e5 --- /dev/null +++ b/.commitlintrc.json @@ -0,0 +1,7 @@ +{ + "extends": ["@commitlint/config-conventional"], + "rules": { + "header-max-length": [2, "always", 100], + "body-max-line-length": [1, "always", 120] + } +} diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml new file mode 100644 index 00000000..2a5766f9 --- /dev/null +++ b/.github/workflows/build-dev.yml @@ -0,0 +1,49 @@ +name: Build & Push Dev Images + +on: + push: + branches: [master, main] + +concurrency: + group: build-dev-${{ github.ref }} + cancel-in-progress: false + +permissions: + id-token: write + contents: read + +jobs: + build: + name: Build ${{ matrix.service }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + service: [frontend, auth, chat-api, inference] + steps: + - uses: actions/checkout@v4 + + - name: Configure AWS credentials (OIDC) + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ${{ vars.AWS_REGION || 'us-east-1' }} + + - name: Login to Amazon ECR + id: ecr-login + uses: aws-actions/amazon-ecr-login@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build & push image + uses: docker/build-push-action@v6 + with: + context: services/${{ matrix.service }} + file: services/${{ matrix.service }}/Dockerfile + push: true + tags: | + ${{ steps.ecr-login.outputs.registry }}/samosachaat/${{ matrix.service }}:dev-${{ github.sha }} + ${{ steps.ecr-login.outputs.registry }}/samosachaat/${{ matrix.service }}:dev-latest + cache-from: type=gha,scope=${{ matrix.service }} + cache-to: type=gha,mode=max,scope=${{ matrix.service }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..bb59dc61 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,185 @@ +name: CI + +on: + push: + branches: [master, main] + pull_request: + branches: [master, main] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: read + +jobs: + changes: + name: Detect changed paths + runs-on: ubuntu-latest + outputs: + frontend: ${{ steps.filter.outputs.frontend }} + auth: ${{ steps.filter.outputs.auth }} + chat-api: ${{ steps.filter.outputs.chat-api }} + inference: ${{ steps.filter.outputs.inference }} + terraform: ${{ steps.filter.outputs.terraform }} + steps: + - uses: actions/checkout@v4 + + - name: Filter paths + id: filter + uses: dorny/paths-filter@v3 + with: + filters: | + frontend: + - 'services/frontend/**' + auth: + - 'services/auth/**' + chat-api: + - 'services/chat-api/**' + inference: + - 'services/inference/**' + terraform: + - 'terraform/**' + + test-frontend: + name: Frontend — lint/type-check/test + needs: changes + if: needs.changes.outputs.frontend == 'true' + runs-on: ubuntu-latest + defaults: + run: + working-directory: services/frontend + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: services/frontend/package-lock.json + + - name: Install deps + run: npm ci + + - name: Lint + run: npm run lint + + - name: Type-check + run: npm run typecheck + + - name: Test + run: npm test --if-present + + test-auth: + name: Auth — pytest + needs: changes + if: needs.changes.outputs.auth == 'true' + runs-on: ubuntu-latest + defaults: + run: + working-directory: services/auth + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install uv + run: pip install uv==0.4.30 + + - name: Sync deps + run: uv sync + + - name: Run pytest + run: uv run pytest + + test-chat-api: + name: Chat-API — pytest (postgres service) + needs: changes + if: needs.changes.outputs.chat-api == 'true' + runs-on: ubuntu-latest + defaults: + run: + working-directory: services/chat-api + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: samosachaat_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + env: + DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/samosachaat_test + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install uv + run: pip install uv==0.4.30 + + - name: Sync deps + run: uv sync + + - name: Run pytest + run: uv run pytest + + test-inference: + name: Inference — pytest + needs: changes + if: needs.changes.outputs.inference == 'true' + runs-on: ubuntu-latest + defaults: + run: + working-directory: services/inference + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install uv + run: pip install uv==0.4.30 + + - name: Sync deps + run: uv sync + + - name: Run pytest + run: uv run pytest + + terraform-validate: + name: Terraform — validate + needs: changes + if: needs.changes.outputs.terraform == 'true' + runs-on: ubuntu-latest + defaults: + run: + working-directory: terraform + steps: + - uses: actions/checkout@v4 + + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: '1.9.8' + + - name: Terraform fmt check + run: terraform fmt -check -recursive + + - name: Terraform init (no backend) + run: terraform init -backend=false + + - name: Terraform validate + run: terraform validate diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 00000000..7dd2db0d --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,37 @@ +name: Nightly integration + +on: + schedule: + - cron: '0 6 * * *' + workflow_dispatch: + +permissions: + contents: read + +jobs: + compose-integration: + name: docker compose integration suite + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Run compose integration + run: | + set -euo pipefail + compose_files=(-f docker-compose.yml) + if [ -f docker-compose.test.yml ]; then + compose_files+=(-f docker-compose.test.yml) + fi + docker compose "${compose_files[@]}" up --build --abort-on-container-exit --exit-code-from tests + + - name: Compose logs on failure + if: failure() + run: docker compose logs --no-color + + - name: Tear down + if: always() + run: docker compose down -v --remove-orphans diff --git a/.github/workflows/promote-uat.yml b/.github/workflows/promote-uat.yml new file mode 100644 index 00000000..7d7f1f43 --- /dev/null +++ b/.github/workflows/promote-uat.yml @@ -0,0 +1,83 @@ +name: Promote to UAT + +on: + push: + tags: + - 'RC*' + +concurrency: + group: promote-uat + cancel-in-progress: false + +permissions: + id-token: write + contents: read + +env: + AWS_REGION: ${{ vars.AWS_REGION || 'us-east-1' }} + UAT_CLUSTER: samosachaat-uat + UAT_NAMESPACE: samosachaat-uat + SERVICES: frontend auth chat-api inference + +jobs: + promote: + name: Re-tag dev → uat and deploy + runs-on: ubuntu-latest + environment: uat + steps: + - uses: actions/checkout@v4 + + - name: Resolve tag + id: tag + run: echo "name=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + + - name: Configure AWS credentials (OIDC) + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + id: ecr-login + uses: aws-actions/amazon-ecr-login@v2 + + - name: Re-tag dev images as uat-${{ steps.tag.outputs.name }} + env: + REGISTRY: ${{ steps.ecr-login.outputs.registry }} + SRC_REF: dev-latest + DST_REF: uat-${{ steps.tag.outputs.name }} + run: | + set -euo pipefail + for svc in $SERVICES; do + repo="samosachaat/${svc}" + echo "Re-tagging $repo:$SRC_REF -> $repo:$DST_REF" + manifest=$(aws ecr batch-get-image \ + --repository-name "$repo" \ + --image-ids imageTag="$SRC_REF" \ + --query 'images[0].imageManifest' \ + --output text) + aws ecr put-image \ + --repository-name "$repo" \ + --image-tag "$DST_REF" \ + --image-manifest "$manifest" >/dev/null + done + + - name: Update kubeconfig + run: | + aws eks update-kubeconfig \ + --name "$UAT_CLUSTER" \ + --region "$AWS_REGION" + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: 'v3.16.2' + + - name: Helm upgrade (UAT) + run: | + helm upgrade --install samosachaat helm/samosachaat \ + -f helm/samosachaat/values-uat.yaml \ + --set global.imageTag=uat-${{ steps.tag.outputs.name }} \ + --namespace "$UAT_NAMESPACE" \ + --create-namespace \ + --wait --timeout 10m diff --git a/.github/workflows/release-prod.yml b/.github/workflows/release-prod.yml new file mode 100644 index 00000000..f1a72750 --- /dev/null +++ b/.github/workflows/release-prod.yml @@ -0,0 +1,119 @@ +name: Release to Prod (Blue/Green) + +on: + push: + tags: + - 'v*' + +concurrency: + group: release-prod + cancel-in-progress: false + +permissions: + id-token: write + contents: read + +env: + AWS_REGION: ${{ vars.AWS_REGION || 'us-east-1' }} + PROD_CLUSTER: samosachaat-prod + PROD_NAMESPACE: samosachaat-prod + SERVICES: frontend auth chat-api inference + +jobs: + release: + name: Blue/Green release ${{ github.ref_name }} + runs-on: ubuntu-latest + environment: production + steps: + - uses: actions/checkout@v4 + + - name: Resolve tag + id: tag + run: echo "name=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + + - name: Configure AWS credentials (OIDC) + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + id: ecr-login + uses: aws-actions/amazon-ecr-login@v2 + + - name: Promote uat images to prod tag + env: + REGISTRY: ${{ steps.ecr-login.outputs.registry }} + DST_REF: prod-${{ steps.tag.outputs.name }} + run: | + set -euo pipefail + for svc in $SERVICES; do + repo="samosachaat/${svc}" + src=$(aws ecr describe-images \ + --repository-name "$repo" \ + --query 'sort_by(imageDetails,&imagePushedAt)[?starts_with(imageTags[0], `uat-`)]|[-1].imageTags[0]' \ + --output text) + if [ -z "$src" ] || [ "$src" = "None" ]; then + echo "No uat-* image found for $repo" >&2 + exit 1 + fi + echo "Promoting $repo:$src -> $repo:$DST_REF" + manifest=$(aws ecr batch-get-image \ + --repository-name "$repo" \ + --image-ids imageTag="$src" \ + --query 'images[0].imageManifest' \ + --output text) + aws ecr put-image \ + --repository-name "$repo" \ + --image-tag "$DST_REF" \ + --image-manifest "$manifest" >/dev/null + done + + - name: Update kubeconfig + run: | + aws eks update-kubeconfig \ + --name "$PROD_CLUSTER" \ + --region "$AWS_REGION" + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: 'v3.16.2' + + - name: Deploy green slot + run: | + helm upgrade --install samosachaat-green helm/samosachaat \ + -f helm/samosachaat/values-prod.yaml \ + --set global.imageTag=prod-${{ steps.tag.outputs.name }} \ + --set deployment.slot=green \ + --set ingress.enabled=false \ + --namespace "$PROD_NAMESPACE" \ + --create-namespace \ + --wait --timeout 15m + + - name: Smoke test green + run: | + set -euo pipefail + kubectl -n "$PROD_NAMESPACE" rollout status deploy/frontend-green --timeout=5m + kubectl -n "$PROD_NAMESPACE" run smoke-${{ github.run_id }} \ + --rm -i --restart=Never \ + --image=curlimages/curl:8.10.1 \ + --command -- curl -fsS --max-time 10 \ + http://frontend-green.${PROD_NAMESPACE}.svc.cluster.local:3000/health + + - name: Swap ingress → green + run: | + helm upgrade --install samosachaat helm/samosachaat \ + -f helm/samosachaat/values-prod.yaml \ + --set global.imageTag=prod-${{ steps.tag.outputs.name }} \ + --set deployment.slot=green \ + --set ingress.enabled=true \ + --namespace "$PROD_NAMESPACE" \ + --wait --timeout 10m + + - name: Retain blue as rollback standby + run: | + echo "Blue slot retained for rollback. To roll back:" + echo " helm upgrade samosachaat helm/samosachaat \\" + echo " -f helm/samosachaat/values-prod.yaml \\" + echo " --set deployment.slot=blue --namespace $PROD_NAMESPACE" diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 00000000..80416c7b --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx --no-install commitlint --edit "$1" diff --git a/helm/samosachaat/Chart.yaml b/helm/samosachaat/Chart.yaml index 08a4ce2f..47f6c513 100644 --- a/helm/samosachaat/Chart.yaml +++ b/helm/samosachaat/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: samosachaat -description: Application chart scaffold for the samosaChaat platform. +description: Umbrella chart for the samosaChaat platform (frontend, auth, chat-api, inference). type: application version: 0.1.0 appVersion: "0.1.0" diff --git a/helm/samosachaat/templates/NOTES.txt b/helm/samosachaat/templates/NOTES.txt index 74a5ef53..63466537 100644 --- a/helm/samosachaat/templates/NOTES.txt +++ b/helm/samosachaat/templates/NOTES.txt @@ -1,4 +1,10 @@ -The samosaChaat application chart is scaffolded. +samosaChaat release {{ .Release.Name }} deployed to namespace {{ include "samosachaat.namespace" . }}. -Populate this chart with Deployments, Services, Ingress, Secrets, and ConfigMaps -as the platform services are implemented. +Image tag: {{ .Values.global.imageTag }} +Slot: {{ default "single" .Values.deployment.slot }} +Ingress host: {{ .Values.ingress.host }} + +Useful commands: + kubectl -n {{ include "samosachaat.namespace" . }} get pods + kubectl -n {{ include "samosachaat.namespace" . }} get ingress samosachaat + helm -n {{ include "samosachaat.namespace" . }} status {{ .Release.Name }} diff --git a/helm/samosachaat/templates/_helpers.tpl b/helm/samosachaat/templates/_helpers.tpl new file mode 100644 index 00000000..2b74692b --- /dev/null +++ b/helm/samosachaat/templates/_helpers.tpl @@ -0,0 +1,78 @@ +{{/* +Common helpers for the samosachaat umbrella chart. +*/}} + +{{/* Chart name truncated to 63 chars (k8s label limit). */}} +{{- define "samosachaat.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* Fully-qualified release name used as the chart-wide prefix. */}} +{{- define "samosachaat.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 -}} + +{{/* Common labels. */}} +{{- define "samosachaat.labels" -}} +app.kubernetes.io/name: {{ include "samosachaat.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: samosachaat +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end -}} + +{{/* +Compose a service-specific name. Honors .Values.deployment.slot so that a +"green" slot (during blue/green prod releases) produces e.g. "frontend-green". +Usage: {{ include "samosachaat.svcName" (dict "root" . "svc" "frontend") }} +*/}} +{{- define "samosachaat.svcName" -}} +{{- $root := .root -}} +{{- $svc := .svc -}} +{{- $slot := default "" $root.Values.deployment.slot -}} +{{- if $slot -}} +{{- printf "%s-%s" $svc $slot | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $svc | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{/* Per-service selector labels. */}} +{{- define "samosachaat.selectorLabels" -}} +{{- $root := .root -}} +{{- $svc := .svc -}} +app.kubernetes.io/name: {{ $svc }} +app.kubernetes.io/instance: {{ $root.Release.Name }} +app.kubernetes.io/component: {{ $svc }} +{{- with $root.Values.deployment.slot }} +app.kubernetes.io/slot: {{ . }} +{{- end }} +{{- end -}} + +{{/* Render a full image reference given a service's .image block. */}} +{{- define "samosachaat.image" -}} +{{- $root := .root -}} +{{- $svc := .svc -}} +{{- $registry := $root.Values.global.imageRegistry | default "" -}} +{{- $repo := $svc.image.repository -}} +{{- $tag := $root.Values.global.imageTag | default "dev-latest" -}} +{{- if $registry -}} +{{- printf "%s/%s:%s" $registry $repo $tag -}} +{{- else -}} +{{- printf "%s:%s" $repo $tag -}} +{{- end -}} +{{- end -}} + +{{/* Namespace that every resource should land in. */}} +{{- define "samosachaat.namespace" -}} +{{- default .Release.Namespace .Values.namespace.name -}} +{{- end -}} diff --git a/helm/samosachaat/templates/auth-deployment.yaml b/helm/samosachaat/templates/auth-deployment.yaml new file mode 100644 index 00000000..d7c8a786 --- /dev/null +++ b/helm/samosachaat/templates/auth-deployment.yaml @@ -0,0 +1,60 @@ +{{- if .Values.auth.enabled -}} +{{- $svcName := include "samosachaat.svcName" (dict "root" . "svc" "auth") -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $svcName }} + namespace: {{ include "samosachaat.namespace" . }} + labels: + {{- include "samosachaat.labels" . | nindent 4 }} + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "auth") | nindent 4 }} +spec: + replicas: {{ .Values.auth.replicaCount }} + selector: + matchLabels: + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "auth") | nindent 6 }} + template: + metadata: + labels: + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "auth") | nindent 8 }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: auth + image: {{ include "samosachaat.image" (dict "root" . "svc" .Values.auth) }} + imagePullPolicy: {{ .Values.global.imagePullPolicy }} + ports: + - name: http + containerPort: {{ .Values.auth.port }} + protocol: TCP + envFrom: + - configMapRef: + name: samosachaat-config + - secretRef: + name: samosachaat-secrets + optional: true + {{- with .Values.auth.env }} + env: + {{- range $k, $v := . }} + - name: {{ $k }} + value: {{ $v | quote }} + {{- end }} + {{- end }} + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + resources: + {{- toYaml .Values.auth.resources | nindent 12 }} +{{- end }} diff --git a/helm/samosachaat/templates/auth-service.yaml b/helm/samosachaat/templates/auth-service.yaml new file mode 100644 index 00000000..c16cec61 --- /dev/null +++ b/helm/samosachaat/templates/auth-service.yaml @@ -0,0 +1,20 @@ +{{- if .Values.auth.enabled -}} +{{- $svcName := include "samosachaat.svcName" (dict "root" . "svc" "auth") -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ $svcName }} + namespace: {{ include "samosachaat.namespace" . }} + labels: + {{- include "samosachaat.labels" . | nindent 4 }} + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "auth") | nindent 4 }} +spec: + type: ClusterIP + selector: + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "auth") | nindent 4 }} + ports: + - name: http + port: {{ .Values.auth.port }} + targetPort: http + protocol: TCP +{{- end }} diff --git a/helm/samosachaat/templates/chat-api-deployment.yaml b/helm/samosachaat/templates/chat-api-deployment.yaml new file mode 100644 index 00000000..82e2b940 --- /dev/null +++ b/helm/samosachaat/templates/chat-api-deployment.yaml @@ -0,0 +1,60 @@ +{{- if .Values.chatApi.enabled -}} +{{- $svcName := include "samosachaat.svcName" (dict "root" . "svc" "chat-api") -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $svcName }} + namespace: {{ include "samosachaat.namespace" . }} + labels: + {{- include "samosachaat.labels" . | nindent 4 }} + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "chat-api") | nindent 4 }} +spec: + replicas: {{ .Values.chatApi.replicaCount }} + selector: + matchLabels: + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "chat-api") | nindent 6 }} + template: + metadata: + labels: + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "chat-api") | nindent 8 }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: chat-api + image: {{ include "samosachaat.image" (dict "root" . "svc" .Values.chatApi) }} + imagePullPolicy: {{ .Values.global.imagePullPolicy }} + ports: + - name: http + containerPort: {{ .Values.chatApi.port }} + protocol: TCP + envFrom: + - configMapRef: + name: samosachaat-config + - secretRef: + name: samosachaat-secrets + optional: true + {{- with .Values.chatApi.env }} + env: + {{- range $k, $v := . }} + - name: {{ $k }} + value: {{ $v | quote }} + {{- end }} + {{- end }} + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + resources: + {{- toYaml .Values.chatApi.resources | nindent 12 }} +{{- end }} diff --git a/helm/samosachaat/templates/chat-api-service.yaml b/helm/samosachaat/templates/chat-api-service.yaml new file mode 100644 index 00000000..01a79d7f --- /dev/null +++ b/helm/samosachaat/templates/chat-api-service.yaml @@ -0,0 +1,20 @@ +{{- if .Values.chatApi.enabled -}} +{{- $svcName := include "samosachaat.svcName" (dict "root" . "svc" "chat-api") -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ $svcName }} + namespace: {{ include "samosachaat.namespace" . }} + labels: + {{- include "samosachaat.labels" . | nindent 4 }} + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "chat-api") | nindent 4 }} +spec: + type: ClusterIP + selector: + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "chat-api") | nindent 4 }} + ports: + - name: http + port: {{ .Values.chatApi.port }} + targetPort: http + protocol: TCP +{{- end }} diff --git a/helm/samosachaat/templates/configmap.yaml b/helm/samosachaat/templates/configmap.yaml new file mode 100644 index 00000000..4b799954 --- /dev/null +++ b/helm/samosachaat/templates/configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: samosachaat-config + namespace: {{ include "samosachaat.namespace" . }} + labels: + {{- include "samosachaat.labels" . | nindent 4 }} +data: + {{- range $k, $v := .Values.config }} + {{ $k }}: {{ $v | quote }} + {{- end }} diff --git a/helm/samosachaat/templates/db-migrate-job.yaml b/helm/samosachaat/templates/db-migrate-job.yaml new file mode 100644 index 00000000..bf7eb331 --- /dev/null +++ b/helm/samosachaat/templates/db-migrate-job.yaml @@ -0,0 +1,48 @@ +{{- if .Values.dbMigrate.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: samosachaat-db-migrate + namespace: {{ include "samosachaat.namespace" . }} + labels: + {{- include "samosachaat.labels" . | nindent 4 }} + app.kubernetes.io/component: db-migrate + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "-1" + "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation +spec: + backoffLimit: 2 + ttlSecondsAfterFinished: 300 + template: + metadata: + labels: + {{- include "samosachaat.labels" . | nindent 8 }} + app.kubernetes.io/component: db-migrate + spec: + restartPolicy: Never + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: db-migrate + image: {{ include "samosachaat.image" (dict "root" . "svc" .Values.dbMigrate) }} + imagePullPolicy: {{ .Values.global.imagePullPolicy }} + workingDir: {{ .Values.dbMigrate.workingDir }} + command: + {{- toYaml .Values.dbMigrate.command | nindent 12 }} + envFrom: + - configMapRef: + name: samosachaat-config + - secretRef: + name: samosachaat-secrets + optional: true + {{- with .Values.dbMigrate.env }} + env: + {{- range $k, $v := . }} + - name: {{ $k }} + value: {{ $v | quote }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/samosachaat/templates/frontend-deployment.yaml b/helm/samosachaat/templates/frontend-deployment.yaml new file mode 100644 index 00000000..20597989 --- /dev/null +++ b/helm/samosachaat/templates/frontend-deployment.yaml @@ -0,0 +1,60 @@ +{{- if .Values.frontend.enabled -}} +{{- $svcName := include "samosachaat.svcName" (dict "root" . "svc" "frontend") -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $svcName }} + namespace: {{ include "samosachaat.namespace" . }} + labels: + {{- include "samosachaat.labels" . | nindent 4 }} + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "frontend") | nindent 4 }} +spec: + replicas: {{ .Values.frontend.replicaCount }} + selector: + matchLabels: + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "frontend") | nindent 6 }} + template: + metadata: + labels: + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "frontend") | nindent 8 }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: frontend + image: {{ include "samosachaat.image" (dict "root" . "svc" .Values.frontend) }} + imagePullPolicy: {{ .Values.global.imagePullPolicy }} + ports: + - name: http + containerPort: {{ .Values.frontend.port }} + protocol: TCP + envFrom: + - configMapRef: + name: samosachaat-config + - secretRef: + name: samosachaat-secrets + optional: true + {{- with .Values.frontend.env }} + env: + {{- range $k, $v := . }} + - name: {{ $k }} + value: {{ $v | quote }} + {{- end }} + {{- end }} + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + resources: + {{- toYaml .Values.frontend.resources | nindent 12 }} +{{- end }} diff --git a/helm/samosachaat/templates/frontend-service.yaml b/helm/samosachaat/templates/frontend-service.yaml new file mode 100644 index 00000000..be37afbf --- /dev/null +++ b/helm/samosachaat/templates/frontend-service.yaml @@ -0,0 +1,20 @@ +{{- if .Values.frontend.enabled -}} +{{- $svcName := include "samosachaat.svcName" (dict "root" . "svc" "frontend") -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ $svcName }} + namespace: {{ include "samosachaat.namespace" . }} + labels: + {{- include "samosachaat.labels" . | nindent 4 }} + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "frontend") | nindent 4 }} +spec: + type: ClusterIP + selector: + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "frontend") | nindent 4 }} + ports: + - name: http + port: {{ .Values.frontend.port }} + targetPort: http + protocol: TCP +{{- end }} diff --git a/helm/samosachaat/templates/hpa.yaml b/helm/samosachaat/templates/hpa.yaml new file mode 100644 index 00000000..3845aec2 --- /dev/null +++ b/helm/samosachaat/templates/hpa.yaml @@ -0,0 +1,51 @@ +{{- if .Values.chatApi.hpa.enabled }} +{{- $svcName := include "samosachaat.svcName" (dict "root" . "svc" "chat-api") }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ $svcName }} + namespace: {{ include "samosachaat.namespace" . }} + labels: + {{- include "samosachaat.labels" . | nindent 4 }} + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "chat-api") | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ $svcName }} + minReplicas: {{ .Values.chatApi.hpa.minReplicas }} + maxReplicas: {{ .Values.chatApi.hpa.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.chatApi.hpa.targetCPUUtilizationPercentage }} +{{- end }} +{{- if .Values.inference.hpa.enabled }} +{{- $svcName := include "samosachaat.svcName" (dict "root" . "svc" "inference") }} +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ $svcName }} + namespace: {{ include "samosachaat.namespace" . }} + labels: + {{- include "samosachaat.labels" . | nindent 4 }} + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "inference") | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ $svcName }} + minReplicas: {{ .Values.inference.hpa.minReplicas }} + maxReplicas: {{ .Values.inference.hpa.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.inference.hpa.targetCPUUtilizationPercentage }} +{{- end }} diff --git a/helm/samosachaat/templates/inference-deployment.yaml b/helm/samosachaat/templates/inference-deployment.yaml new file mode 100644 index 00000000..de1ab239 --- /dev/null +++ b/helm/samosachaat/templates/inference-deployment.yaml @@ -0,0 +1,60 @@ +{{- if .Values.inference.enabled -}} +{{- $svcName := include "samosachaat.svcName" (dict "root" . "svc" "inference") -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $svcName }} + namespace: {{ include "samosachaat.namespace" . }} + labels: + {{- include "samosachaat.labels" . | nindent 4 }} + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "inference") | nindent 4 }} +spec: + replicas: {{ .Values.inference.replicaCount }} + selector: + matchLabels: + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "inference") | nindent 6 }} + template: + metadata: + labels: + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "inference") | nindent 8 }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: inference + image: {{ include "samosachaat.image" (dict "root" . "svc" .Values.inference) }} + imagePullPolicy: {{ .Values.global.imagePullPolicy }} + ports: + - name: http + containerPort: {{ .Values.inference.port }} + protocol: TCP + envFrom: + - configMapRef: + name: samosachaat-config + - secretRef: + name: samosachaat-secrets + optional: true + {{- with .Values.inference.env }} + env: + {{- range $k, $v := . }} + - name: {{ $k }} + value: {{ $v | quote }} + {{- end }} + {{- end }} + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + resources: + {{- toYaml .Values.inference.resources | nindent 12 }} +{{- end }} diff --git a/helm/samosachaat/templates/inference-service.yaml b/helm/samosachaat/templates/inference-service.yaml new file mode 100644 index 00000000..95b0095a --- /dev/null +++ b/helm/samosachaat/templates/inference-service.yaml @@ -0,0 +1,20 @@ +{{- if .Values.inference.enabled -}} +{{- $svcName := include "samosachaat.svcName" (dict "root" . "svc" "inference") -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ $svcName }} + namespace: {{ include "samosachaat.namespace" . }} + labels: + {{- include "samosachaat.labels" . | nindent 4 }} + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "inference") | nindent 4 }} +spec: + type: ClusterIP + selector: + {{- include "samosachaat.selectorLabels" (dict "root" . "svc" "inference") | nindent 4 }} + ports: + - name: http + port: {{ .Values.inference.port }} + targetPort: http + protocol: TCP +{{- end }} diff --git a/helm/samosachaat/templates/ingress.yaml b/helm/samosachaat/templates/ingress.yaml new file mode 100644 index 00000000..34c553a7 --- /dev/null +++ b/helm/samosachaat/templates/ingress.yaml @@ -0,0 +1,57 @@ +{{- if .Values.ingress.enabled -}} +{{- $frontendSvc := include "samosachaat.svcName" (dict "root" . "svc" "frontend") -}} +{{- $authSvc := include "samosachaat.svcName" (dict "root" . "svc" "auth") -}} +{{- $chatApiSvc := include "samosachaat.svcName" (dict "root" . "svc" "chat-api") -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: samosachaat + namespace: {{ include "samosachaat.namespace" . }} + labels: + {{- include "samosachaat.labels" . | nindent 4 }} + annotations: + kubernetes.io/ingress.class: alb + alb.ingress.kubernetes.io/scheme: internet-facing + alb.ingress.kubernetes.io/target-type: ip + alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]' + alb.ingress.kubernetes.io/ssl-redirect: "443" + {{- with .Values.ingress.acmCertArn }} + alb.ingress.kubernetes.io/certificate-arn: {{ . | quote }} + {{- end }} +spec: + rules: + - host: {{ .Values.ingress.host | quote }} + http: + paths: + - path: /api/auth + pathType: Prefix + backend: + service: + name: {{ $authSvc }} + port: + number: {{ .Values.auth.port }} + - path: /api + pathType: Prefix + backend: + service: + name: {{ $chatApiSvc }} + port: + number: {{ .Values.chatApi.port }} + - path: / + pathType: Prefix + backend: + service: + name: {{ $frontendSvc }} + port: + number: {{ .Values.frontend.port }} + - host: {{ .Values.ingress.grafanaHost | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ .Values.ingress.grafanaServiceName }} + port: + number: {{ .Values.ingress.grafanaServicePort }} +{{- end }} diff --git a/helm/samosachaat/templates/namespace.yaml b/helm/samosachaat/templates/namespace.yaml new file mode 100644 index 00000000..522727bc --- /dev/null +++ b/helm/samosachaat/templates/namespace.yaml @@ -0,0 +1,8 @@ +{{- if .Values.namespace.create }} +apiVersion: v1 +kind: Namespace +metadata: + name: {{ include "samosachaat.namespace" . }} + labels: + {{- include "samosachaat.labels" . | nindent 4 }} +{{- end }} diff --git a/helm/samosachaat/templates/pdb.yaml b/helm/samosachaat/templates/pdb.yaml new file mode 100644 index 00000000..61101168 --- /dev/null +++ b/helm/samosachaat/templates/pdb.yaml @@ -0,0 +1,19 @@ +{{- if .Values.pdb.enabled }} +{{- range $svc := list "frontend" "auth" "chat-api" "inference" }} +{{- $svcName := include "samosachaat.svcName" (dict "root" $ "svc" $svc) }} +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ $svcName }} + namespace: {{ include "samosachaat.namespace" $ }} + labels: + {{- include "samosachaat.labels" $ | nindent 4 }} + {{- include "samosachaat.selectorLabels" (dict "root" $ "svc" $svc) | nindent 4 }} +spec: + minAvailable: {{ $.Values.pdb.minAvailable }} + selector: + matchLabels: + {{- include "samosachaat.selectorLabels" (dict "root" $ "svc" $svc) | nindent 6 }} +{{- end }} +{{- end }} diff --git a/helm/samosachaat/templates/secrets.yaml b/helm/samosachaat/templates/secrets.yaml new file mode 100644 index 00000000..309f90fc --- /dev/null +++ b/helm/samosachaat/templates/secrets.yaml @@ -0,0 +1,16 @@ +{{- if .Values.secrets.create }} +apiVersion: v1 +kind: Secret +metadata: + name: samosachaat-secrets + namespace: {{ include "samosachaat.namespace" . }} + labels: + {{- include "samosachaat.labels" . | nindent 4 }} +type: Opaque +{{- with .Values.secrets.data }} +stringData: + {{- range $k, $v := . }} + {{ $k }}: {{ $v | quote }} + {{- end }} +{{- end }} +{{- end }} diff --git a/helm/samosachaat/values-dev.yaml b/helm/samosachaat/values-dev.yaml new file mode 100644 index 00000000..c45b3671 --- /dev/null +++ b/helm/samosachaat/values-dev.yaml @@ -0,0 +1,58 @@ +## Dev overrides — single-slot, 1 replica, small resources, no HPA. + +global: + imageTag: dev-latest + +namespace: + name: samosachaat-dev + +config: + ENVIRONMENT: "dev" + LOG_LEVEL: "debug" + +frontend: + replicaCount: 1 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + +auth: + replicaCount: 1 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + +chatApi: + replicaCount: 1 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + hpa: + enabled: false + +inference: + replicaCount: 1 + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: '1' + memory: 2Gi + hpa: + enabled: false + +pdb: + enabled: false diff --git a/helm/samosachaat/values-prod.yaml b/helm/samosachaat/values-prod.yaml new file mode 100644 index 00000000..3ca47162 --- /dev/null +++ b/helm/samosachaat/values-prod.yaml @@ -0,0 +1,62 @@ +## Production overrides — 3+ replicas, HPA enabled for chat-api/inference. + +namespace: + name: samosachaat-prod + +config: + ENVIRONMENT: "prod" + LOG_LEVEL: "info" + +frontend: + replicaCount: 3 + resources: + requests: + cpu: 200m + memory: 512Mi + limits: + cpu: '1' + memory: 1Gi + +auth: + replicaCount: 3 + resources: + requests: + cpu: 200m + memory: 512Mi + limits: + cpu: '1' + memory: 1Gi + +chatApi: + replicaCount: 3 + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: '2' + memory: 2Gi + hpa: + enabled: true + minReplicas: 3 + maxReplicas: 10 + targetCPUUtilizationPercentage: 70 + +inference: + replicaCount: 3 + resources: + requests: + cpu: '1' + memory: 2Gi + limits: + cpu: '4' + memory: 8Gi + hpa: + enabled: true + minReplicas: 3 + maxReplicas: 10 + targetCPUUtilizationPercentage: 70 + +pdb: + enabled: true + minAvailable: 1 diff --git a/helm/samosachaat/values-uat.yaml b/helm/samosachaat/values-uat.yaml new file mode 100644 index 00000000..95a8d57c --- /dev/null +++ b/helm/samosachaat/values-uat.yaml @@ -0,0 +1,56 @@ +## UAT overrides — 2 replicas each, medium resources. + +namespace: + name: samosachaat-uat + +config: + ENVIRONMENT: "uat" + LOG_LEVEL: "info" + +frontend: + replicaCount: 2 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + +auth: + replicaCount: 2 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + +chatApi: + replicaCount: 2 + resources: + requests: + cpu: 200m + memory: 512Mi + limits: + cpu: '1' + memory: 1Gi + hpa: + enabled: false + +inference: + replicaCount: 2 + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: '2' + memory: 4Gi + hpa: + enabled: false + +pdb: + enabled: true + minAvailable: 1 diff --git a/helm/samosachaat/values.yaml b/helm/samosachaat/values.yaml index 2f26a777..428fd26b 100644 --- a/helm/samosachaat/values.yaml +++ b/helm/samosachaat/values.yaml @@ -1,10 +1,131 @@ -image: - repository: ghcr.io/manmohan659/nanochat/frontend - tag: latest - pullPolicy: IfNotPresent +## Base values for the samosaChaat umbrella chart. +## Environment overrides live in values-{dev,uat,prod}.yaml. -replicaCount: 1 +global: + ## Set by CI (promote-uat / release-prod) or `helm install --set global.imageTag=...`. + imageTag: dev-latest + imagePullPolicy: Always + ## Optional ECR / registry prefix. e.g. 1234.dkr.ecr.us-east-1.amazonaws.com + imageRegistry: "" + imagePullSecrets: [] -service: - type: ClusterIP +namespace: + create: true + name: samosachaat + +## Shared env piped into every service. +config: + LOG_LEVEL: "info" + ENVIRONMENT: "dev" + +## Secret names/values. Values here are placeholders; real values should come +## from --set-file or an external secret manager. +secrets: + create: true + data: {} + +## ---------------- Deployment-wide knobs ---------------- +deployment: + ## blue / green slot name — empty string means single-slot (dev/uat). + slot: "" + +## ---------------- Per-service definitions ---------------- +frontend: + enabled: true + image: + repository: samosachaat/frontend + replicaCount: 1 port: 3000 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + env: + NEXT_PUBLIC_API_URL: "https://samosachaat.art/api" + hpa: + enabled: false + +auth: + enabled: true + image: + repository: samosachaat/auth + replicaCount: 1 + port: 8001 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + env: {} + hpa: + enabled: false + +chatApi: + enabled: true + image: + repository: samosachaat/chat-api + replicaCount: 1 + port: 8002 + resources: + requests: + cpu: 200m + memory: 512Mi + limits: + cpu: '1' + memory: 1Gi + env: {} + hpa: + enabled: false + minReplicas: 1 + maxReplicas: 3 + targetCPUUtilizationPercentage: 70 + +inference: + enabled: true + image: + repository: samosachaat/inference + replicaCount: 1 + port: 8003 + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: '2' + memory: 4Gi + env: {} + hpa: + enabled: false + minReplicas: 1 + maxReplicas: 3 + targetCPUUtilizationPercentage: 70 + +## ---------------- Ingress (AWS ALB) ---------------- +ingress: + enabled: true + host: samosachaat.art + grafanaHost: grafana.samosachaat.art + acmCertArn: "" + grafanaServiceName: grafana + grafanaServicePort: 3000 + +## ---------------- PodDisruptionBudgets ---------------- +pdb: + enabled: false + minAvailable: 1 + +## ---------------- DB migration Helm hook ---------------- +dbMigrate: + enabled: true + ## Uses the chat-api image to run alembic; alembic config ships with the auth + ## service but the image build for chat-api also includes migrations. + image: + repository: samosachaat/chat-api + command: ["alembic", "upgrade", "head"] + workingDir: /app + env: {} diff --git a/package.json b/package.json new file mode 100644 index 00000000..15afe121 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "samosachaat-monorepo", + "version": "0.1.0", + "private": true, + "description": "Root tooling for the samosaChaat monorepo (commitlint + husky).", + "scripts": { + "prepare": "husky install", + "commitlint": "commitlint --edit" + }, + "devDependencies": { + "@commitlint/cli": "^19.5.0", + "@commitlint/config-conventional": "^19.5.0", + "husky": "^9.1.6" + } +}