From 08782182358b6f1c45cdeedcbd177c76592fe1d2 Mon Sep 17 00:00:00 2001 From: JMPonce Date: Tue, 2 Jun 2026 19:51:02 +0200 Subject: [PATCH 1/4] feat(k8s): add raw Kubernetes manifests and Helm chart for GKE deployment - k8s/: API + UI Deployments, Services (ClusterIP + LoadBalancer), HPAs, ConfigMap, Secret example, optional Ingress with GKE ManagedCertificate, kustomization.yaml. Probes tuned for ~50s embedding-model cold start. - helm/pharmagraphrag/: production-grade chart with parameterized values, separate ConfigMap/Secret, podSecurityContext (non-root), HPAs, optional Ingress. NOTES.txt with post-install instructions. - .github/workflows/deploy-gke.yml: CD pipeline on v*-k8s tags, downloads ChromaDB from GCS, builds API+UI images to GCR, deploys via Helm upgrade. --- .github/workflows/deploy-gke.yml | 146 ++++++++++++++++++ helm/pharmagraphrag/.helmignore | 17 ++ helm/pharmagraphrag/Chart.yaml | 20 +++ helm/pharmagraphrag/README.md | 63 ++++++++ helm/pharmagraphrag/templates/NOTES.txt | 24 +++ helm/pharmagraphrag/templates/_helpers.tpl | 42 +++++ .../templates/api-deployment.yaml | 69 +++++++++ helm/pharmagraphrag/templates/api-hpa.yaml | 31 ++++ .../pharmagraphrag/templates/api-service.yaml | 18 +++ helm/pharmagraphrag/templates/configmap.yaml | 12 ++ helm/pharmagraphrag/templates/ingress.yaml | 47 ++++++ helm/pharmagraphrag/templates/secret.yaml | 13 ++ .../templates/ui-deployment.yaml | 47 ++++++ helm/pharmagraphrag/templates/ui-hpa.yaml | 23 +++ helm/pharmagraphrag/templates/ui-service.yaml | 22 +++ helm/pharmagraphrag/values.yaml | 130 ++++++++++++++++ k8s/README.md | 96 ++++++++++++ k8s/api-deployment.yaml | 67 ++++++++ k8s/api-hpa.yaml | 38 +++++ k8s/api-service.yaml | 18 +++ k8s/configmap.yaml | 19 +++ k8s/ingress.yaml | 51 ++++++ k8s/kustomization.yaml | 15 ++ k8s/namespace.yaml | 7 + k8s/secret.example.yaml | 24 +++ k8s/ui-deployment.yaml | 53 +++++++ k8s/ui-hpa.yaml | 19 +++ k8s/ui-service.yaml | 21 +++ 28 files changed, 1152 insertions(+) create mode 100644 .github/workflows/deploy-gke.yml create mode 100644 helm/pharmagraphrag/.helmignore create mode 100644 helm/pharmagraphrag/Chart.yaml create mode 100644 helm/pharmagraphrag/README.md create mode 100644 helm/pharmagraphrag/templates/NOTES.txt create mode 100644 helm/pharmagraphrag/templates/_helpers.tpl create mode 100644 helm/pharmagraphrag/templates/api-deployment.yaml create mode 100644 helm/pharmagraphrag/templates/api-hpa.yaml create mode 100644 helm/pharmagraphrag/templates/api-service.yaml create mode 100644 helm/pharmagraphrag/templates/configmap.yaml create mode 100644 helm/pharmagraphrag/templates/ingress.yaml create mode 100644 helm/pharmagraphrag/templates/secret.yaml create mode 100644 helm/pharmagraphrag/templates/ui-deployment.yaml create mode 100644 helm/pharmagraphrag/templates/ui-hpa.yaml create mode 100644 helm/pharmagraphrag/templates/ui-service.yaml create mode 100644 helm/pharmagraphrag/values.yaml create mode 100644 k8s/README.md create mode 100644 k8s/api-deployment.yaml create mode 100644 k8s/api-hpa.yaml create mode 100644 k8s/api-service.yaml create mode 100644 k8s/configmap.yaml create mode 100644 k8s/ingress.yaml create mode 100644 k8s/kustomization.yaml create mode 100644 k8s/namespace.yaml create mode 100644 k8s/secret.example.yaml create mode 100644 k8s/ui-deployment.yaml create mode 100644 k8s/ui-hpa.yaml create mode 100644 k8s/ui-service.yaml diff --git a/.github/workflows/deploy-gke.yml b/.github/workflows/deploy-gke.yml new file mode 100644 index 0000000..62e67be --- /dev/null +++ b/.github/workflows/deploy-gke.yml @@ -0,0 +1,146 @@ +# ============================================================ +# GitHub Actions: Deploy PharmaGraphRAG to GKE +# ============================================================ +# Triggered by tags matching v*-k8s (e.g. v1.5.0-k8s). +# Reuses CI workflow first, then builds API+UI images and +# deploys via Helm to a GKE Autopilot cluster. +# +# Required GitHub secrets: +# - GCP_SA_KEY: Service account JSON key (roles: container.developer, +# artifactregistry.writer, storage.admin) +# - GKE_CLUSTER_NAME: e.g. pharmagraphrag-autopilot +# - GKE_CLUSTER_LOCATION: e.g. us-central1 +# - GKE_PROJECT_ID: e.g. pharmagraphrag +# - PGRAG_NEO4J_URI +# - PGRAG_NEO4J_PASSWORD +# - PGRAG_GEMINI_API_KEY +# ============================================================ +name: Deploy to GKE + +on: + push: + tags: + - "v*-k8s" + workflow_dispatch: + inputs: + tag: + description: "Image tag to deploy (e.g. v1.5.0-k8s)" + required: true + default: "latest-k8s" + +env: + REGISTRY: gcr.io + PROJECT_ID: ${{ secrets.GKE_PROJECT_ID }} + API_IMAGE: pharmagraphrag-api + UI_IMAGE: pharmagraphrag-ui + CLUSTER_NAME: ${{ secrets.GKE_CLUSTER_NAME }} + CLUSTER_LOCATION: ${{ secrets.GKE_CLUSTER_LOCATION }} + NAMESPACE: pharmagraphrag + +jobs: + ci: + uses: ./.github/workflows/ci.yml + + build-and-deploy: + needs: ci + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Resolve image tag + id: tag + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "value=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT" + else + echo "value=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + fi + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SA_KEY }} + + - name: Set up gcloud CLI + uses: google-github-actions/setup-gcloud@v2 + with: + project_id: ${{ env.PROJECT_ID }} + + - name: Configure Docker for GCR + run: gcloud auth configure-docker gcr.io --quiet + + - name: Download ChromaDB snapshot from GCS + run: | + mkdir -p data/chroma + gcloud storage cp -r gs://pharmagraphrag-data/chroma/chroma/* data/chroma/ + + - name: Build API image + run: | + cp docker/Dockerfile.cloudrun.dockerignore .dockerignore + docker build -f docker/Dockerfile.cloudrun \ + -t ${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.API_IMAGE }}:${{ steps.tag.outputs.value }} \ + -t ${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.API_IMAGE }}:latest-k8s \ + . + + - name: Build UI image + run: | + docker build -f docker/Dockerfile.ui \ + -t ${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.UI_IMAGE }}:${{ steps.tag.outputs.value }} \ + -t ${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.UI_IMAGE }}:latest-k8s \ + . + + - name: Push images + run: | + docker push ${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.API_IMAGE }}:${{ steps.tag.outputs.value }} + docker push ${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.API_IMAGE }}:latest-k8s + docker push ${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.UI_IMAGE }}:${{ steps.tag.outputs.value }} + docker push ${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.UI_IMAGE }}:latest-k8s + + - name: Install gke-gcloud-auth-plugin + run: gcloud components install gke-gcloud-auth-plugin --quiet + + - name: Get GKE credentials + run: | + gcloud container clusters get-credentials ${{ env.CLUSTER_NAME }} \ + --region ${{ env.CLUSTER_LOCATION }} \ + --project ${{ env.PROJECT_ID }} + + - name: Create namespace if missing + run: | + kubectl get namespace ${{ env.NAMESPACE }} 2>/dev/null \ + || kubectl create namespace ${{ env.NAMESPACE }} + + - name: Apply secrets + run: | + kubectl -n ${{ env.NAMESPACE }} create secret generic pharmagraphrag-secrets \ + --from-literal=GEMINI_API_KEY='${{ secrets.PGRAG_GEMINI_API_KEY }}' \ + --from-literal=NEO4J_URI='${{ secrets.PGRAG_NEO4J_URI }}' \ + --from-literal=NEO4J_PASSWORD='${{ secrets.PGRAG_NEO4J_PASSWORD }}' \ + --dry-run=client -o yaml | kubectl apply -f - + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.14.0 + + - name: Deploy via Helm + run: | + helm upgrade --install pharmagraphrag helm/pharmagraphrag \ + --namespace ${{ env.NAMESPACE }} \ + --set image.registry=${{ env.REGISTRY }}/${{ env.PROJECT_ID }} \ + --set image.tag=${{ steps.tag.outputs.value }} \ + --set secrets.create=false \ + --set secrets.existingSecret=pharmagraphrag-secrets \ + --wait --timeout 10m + + - name: Show deployment status + run: | + kubectl -n ${{ env.NAMESPACE }} get pods,svc,hpa + echo "---" + echo "UI external IP (may take 1-2 min):" + kubectl -n ${{ env.NAMESPACE }} get svc pharmagraphrag-ui -o wide diff --git a/helm/pharmagraphrag/.helmignore b/helm/pharmagraphrag/.helmignore new file mode 100644 index 0000000..86bdce1 --- /dev/null +++ b/helm/pharmagraphrag/.helmignore @@ -0,0 +1,17 @@ +# Patterns to ignore when building Helm packages. +.DS_Store +.git/ +.gitignore +.bzr/ +.hg/ +.svn/ +*.swp +*.tmp +*.bak +*.orig +*~ +.project +.idea/ +*.tmproj +.vscode/ +README.md diff --git a/helm/pharmagraphrag/Chart.yaml b/helm/pharmagraphrag/Chart.yaml new file mode 100644 index 0000000..25fe224 --- /dev/null +++ b/helm/pharmagraphrag/Chart.yaml @@ -0,0 +1,20 @@ +apiVersion: v2 +name: pharmagraphrag +description: GraphRAG system for drug interactions & adverse events (FDA data + Neo4j + ChromaDB + Gemini/Ollama) +type: application +version: 0.1.0 +appVersion: "1.5.0" +keywords: + - graphrag + - rag + - neo4j + - chromadb + - llm + - pharmacovigilance +home: https://github.com/jmponcebe/PharmaGraphRAG +sources: + - https://github.com/jmponcebe/PharmaGraphRAG +maintainers: + - name: Jose María Ponce Bernabé + email: jmponcebe@gmail.com +icon: https://raw.githubusercontent.com/jmponcebe/PharmaGraphRAG/main/assets/screenshots/social-preview.png diff --git a/helm/pharmagraphrag/README.md b/helm/pharmagraphrag/README.md new file mode 100644 index 0000000..6c1b26e --- /dev/null +++ b/helm/pharmagraphrag/README.md @@ -0,0 +1,63 @@ +# PharmaGraphRAG Helm chart + +A Helm 3 chart that packages PharmaGraphRAG (API + UI) for Kubernetes. + +## TL;DR + +```bash +# Install / upgrade (uses an existing Secret by default) +helm upgrade --install pharmagraphrag ./helm/pharmagraphrag \ + --namespace pharmagraphrag --create-namespace \ + --set secrets.create=true \ + --set secrets.values.GEMINI_API_KEY="$GEMINI_API_KEY" \ + --set secrets.values.NEO4J_URI="$NEO4J_URI" \ + --set secrets.values.NEO4J_PASSWORD="$NEO4J_PASSWORD" + +# Uninstall (frees up GKE costs) +helm uninstall pharmagraphrag -n pharmagraphrag +``` + +## What it deploys + +- `Deployment` + `Service` + `HPA` for the FastAPI **API**. +- `Deployment` + `Service` (LoadBalancer by default) + `HPA` for the Streamlit **UI**. +- `ConfigMap` with non-secret config (LLM model, Neo4j user, Chroma path, inter-service URL). +- `Secret` with credentials (created by the chart in dev, or referenced from an existing one in prod). +- Optional `Ingress` + GKE `ManagedCertificate` for custom-domain HTTPS. +- Production-grade probes: startup probe sized for ~50s embedding-model cold start, plus liveness + readiness. + +## Key values + +| Key | Default | Notes | +|---|---|---| +| `image.registry` | `gcr.io/pharmagraphrag` | Change to your registry | +| `image.tag` | `latest` | Falls back to `Chart.appVersion` if empty | +| `api.replicaCount` / `ui.replicaCount` | `1` | Ignored when `autoscaling.enabled` | +| `api.autoscaling.{min,max}Replicas` | `1` / `3` | CPU 70%, memory 80% targets | +| `ui.autoscaling.{min,max}Replicas` | `1` / `2` | CPU 75% target | +| `ui.service.type` | `LoadBalancer` | Use `ClusterIP` if exposing via Ingress | +| `secrets.create` | `false` | `true` to let the chart create the Secret (dev only) | +| `secrets.existingSecret` | `pharmagraphrag-secrets` | Name when `create=false` | +| `ingress.enabled` | `false` | Set `true` + provide `ingress.host` to use GKE Ingress | + +## Render without installing + +```bash +helm template demo ./helm/pharmagraphrag --namespace pharmagraphrag +``` + +## Lint + +```bash +helm lint ./helm/pharmagraphrag +``` + +## Why a chart and not just raw manifests? + +Both are checked in: + +- [`k8s/`](../../k8s) — raw manifests, useful for understanding what gets created. +- [`helm/pharmagraphrag/`](.) — Helm chart, recommended for real deploys: parameterization, + upgrade/rollback, environment-specific values files, NOTES output, secret indirection. + +CI/CD ([`deploy-gke.yml`](../../.github/workflows/deploy-gke.yml)) uses the Helm chart. diff --git a/helm/pharmagraphrag/templates/NOTES.txt b/helm/pharmagraphrag/templates/NOTES.txt new file mode 100644 index 0000000..acd0216 --- /dev/null +++ b/helm/pharmagraphrag/templates/NOTES.txt @@ -0,0 +1,24 @@ +PharmaGraphRAG release "{{ .Release.Name }}" deployed to namespace "{{ .Release.Namespace }}". + +To check status: + kubectl -n {{ .Release.Namespace }} get pods,svc,hpa,ingress + +{{- if eq .Values.ui.service.type "LoadBalancer" }} + +The UI is exposed as a LoadBalancer Service. Wait ~1-2 min for the external IP to be assigned, then: + kubectl -n {{ .Release.Namespace }} get svc {{ .Release.Name }}-ui + +Once the EXTERNAL-IP shows a value (not ), open: http:// +{{- end }} + +{{- if .Values.ingress.enabled }} + +Ingress is enabled at host: {{ .Values.ingress.host }} +Point a DNS A record to the ingress IP, then access: https://{{ .Values.ingress.host }} +{{- end }} + +API is reachable inside the cluster at: + http://{{ .Release.Name }}-api.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.api.service.port }} + +To uninstall (and free up GKE costs): + helm uninstall {{ .Release.Name }} -n {{ .Release.Namespace }} diff --git a/helm/pharmagraphrag/templates/_helpers.tpl b/helm/pharmagraphrag/templates/_helpers.tpl new file mode 100644 index 0000000..e2ae4c8 --- /dev/null +++ b/helm/pharmagraphrag/templates/_helpers.tpl @@ -0,0 +1,42 @@ +{{/* +Common labels applied to every resource. +*/}} +{{- define "pharmagraphrag.labels" -}} +app.kubernetes.io/name: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }} +{{- end }} + +{{/* +Selector labels for a given component. +Usage: {{ include "pharmagraphrag.selectorLabels" (dict "Chart" .Chart "Release" .Release "component" "api") }} +*/}} +{{- define "pharmagraphrag.selectorLabels" -}} +app.kubernetes.io/name: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/component: {{ .component }} +{{- end }} + +{{/* +Component image string (registry/repository:tag). +Falls back to Chart.appVersion if no tag is set. +*/}} +{{- define "pharmagraphrag.image" -}} +{{- $componentTag := .componentImage.tag | default "" -}} +{{- $globalTag := .globalImage.tag | default "" -}} +{{- $tag := $componentTag | default $globalTag | default .chartAppVersion -}} +{{ .globalImage.registry }}/{{ .componentImage.repository }}:{{ $tag }} +{{- end }} + +{{/* +Resolve the secret name (existing or generated). +*/}} +{{- define "pharmagraphrag.secretName" -}} +{{- if .Values.secrets.create -}} +{{ .Release.Name }}-secrets +{{- else -}} +{{ .Values.secrets.existingSecret }} +{{- end -}} +{{- end }} diff --git a/helm/pharmagraphrag/templates/api-deployment.yaml b/helm/pharmagraphrag/templates/api-deployment.yaml new file mode 100644 index 0000000..6aafd0e --- /dev/null +++ b/helm/pharmagraphrag/templates/api-deployment.yaml @@ -0,0 +1,69 @@ +{{- if .Values.api.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-api + labels: + {{- include "pharmagraphrag.labels" . | nindent 4 }} + app.kubernetes.io/component: api +spec: + {{- if not .Values.api.autoscaling.enabled }} + replicas: {{ .Values.api.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "pharmagraphrag.selectorLabels" (dict "Chart" .Chart "Release" .Release "component" "api") | nindent 6 }} + template: + metadata: + labels: + {{- include "pharmagraphrag.selectorLabels" (dict "Chart" .Chart "Release" .Release "component" "api") | nindent 8 }} + spec: + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: api + image: {{ include "pharmagraphrag.image" (dict "globalImage" .Values.image "componentImage" .Values.api.image "chartAppVersion" .Chart.AppVersion) }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + ports: + - name: http + containerPort: 8000 + envFrom: + - configMapRef: + name: {{ .Release.Name }}-config + - secretRef: + name: {{ include "pharmagraphrag.secretName" . }} + env: + - name: PORT + value: "8000" + resources: + {{- toYaml .Values.api.resources | nindent 12 }} + startupProbe: + httpGet: + path: /health + port: http + {{- toYaml .Values.api.probes.startup | nindent 12 }} + readinessProbe: + httpGet: + path: /health + port: http + {{- toYaml .Values.api.probes.readiness | nindent 12 }} + livenessProbe: + httpGet: + path: /health + port: http + {{- toYaml .Values.api.probes.liveness | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/helm/pharmagraphrag/templates/api-hpa.yaml b/helm/pharmagraphrag/templates/api-hpa.yaml new file mode 100644 index 0000000..e0dab43 --- /dev/null +++ b/helm/pharmagraphrag/templates/api-hpa.yaml @@ -0,0 +1,31 @@ +{{- if and .Values.api.enabled .Values.api.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ .Release.Name }}-api + labels: + {{- include "pharmagraphrag.labels" . | nindent 4 }} + app.kubernetes.io/component: api +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ .Release.Name }}-api + minReplicas: {{ .Values.api.autoscaling.minReplicas }} + maxReplicas: {{ .Values.api.autoscaling.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.api.autoscaling.targetCPUUtilizationPercentage }} + {{- if .Values.api.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.api.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/pharmagraphrag/templates/api-service.yaml b/helm/pharmagraphrag/templates/api-service.yaml new file mode 100644 index 0000000..d1f6e7a --- /dev/null +++ b/helm/pharmagraphrag/templates/api-service.yaml @@ -0,0 +1,18 @@ +{{- if .Values.api.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-api + labels: + {{- include "pharmagraphrag.labels" . | nindent 4 }} + app.kubernetes.io/component: api +spec: + type: {{ .Values.api.service.type }} + ports: + - port: {{ .Values.api.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "pharmagraphrag.selectorLabels" (dict "Chart" .Chart "Release" .Release "component" "api") | nindent 4 }} +{{- end }} diff --git a/helm/pharmagraphrag/templates/configmap.yaml b/helm/pharmagraphrag/templates/configmap.yaml new file mode 100644 index 0000000..b924f95 --- /dev/null +++ b/helm/pharmagraphrag/templates/configmap.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-config + labels: + {{- include "pharmagraphrag.labels" . | nindent 4 }} +data: + {{- range $key, $value := .Values.config }} + {{ $key }}: {{ $value | quote }} + {{- end }} + # Inter-service DNS for the UI to reach the API + API_URL: "http://{{ .Release.Name }}-api:{{ .Values.api.service.port }}" diff --git a/helm/pharmagraphrag/templates/ingress.yaml b/helm/pharmagraphrag/templates/ingress.yaml new file mode 100644 index 0000000..77c151a --- /dev/null +++ b/helm/pharmagraphrag/templates/ingress.yaml @@ -0,0 +1,47 @@ +{{- if .Values.ingress.enabled }} +{{- if .Values.ingress.managedCertificate }} +apiVersion: networking.gke.io/v1 +kind: ManagedCertificate +metadata: + name: {{ .Release.Name }}-cert + labels: + {{- include "pharmagraphrag.labels" . | nindent 4 }} +spec: + domains: + - {{ .Values.ingress.host | quote }} +--- +{{- end }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ .Release.Name }} + labels: + {{- include "pharmagraphrag.labels" . | nindent 4 }} + annotations: + kubernetes.io/ingress.class: {{ .Values.ingress.className | quote }} + {{- if .Values.ingress.managedCertificate }} + networking.gke.io/managed-certificates: {{ .Release.Name }}-cert + {{- end }} + {{- if .Values.ingress.staticIPName }} + kubernetes.io/ingress.global-static-ip-name: {{ .Values.ingress.staticIPName | quote }} + {{- end }} +spec: + rules: + - host: {{ .Values.ingress.host | quote }} + http: + paths: + - path: /api + pathType: Prefix + backend: + service: + name: {{ .Release.Name }}-api + port: + number: {{ .Values.api.service.port }} + - path: / + pathType: Prefix + backend: + service: + name: {{ .Release.Name }}-ui + port: + number: {{ .Values.ui.service.port }} +{{- end }} diff --git a/helm/pharmagraphrag/templates/secret.yaml b/helm/pharmagraphrag/templates/secret.yaml new file mode 100644 index 0000000..94df98a --- /dev/null +++ b/helm/pharmagraphrag/templates/secret.yaml @@ -0,0 +1,13 @@ +{{- if .Values.secrets.create }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-secrets + labels: + {{- include "pharmagraphrag.labels" . | nindent 4 }} +type: Opaque +stringData: + {{- range $key, $value := .Values.secrets.values }} + {{ $key }}: {{ $value | quote }} + {{- end }} +{{- end }} diff --git a/helm/pharmagraphrag/templates/ui-deployment.yaml b/helm/pharmagraphrag/templates/ui-deployment.yaml new file mode 100644 index 0000000..81e8446 --- /dev/null +++ b/helm/pharmagraphrag/templates/ui-deployment.yaml @@ -0,0 +1,47 @@ +{{- if .Values.ui.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-ui + labels: + {{- include "pharmagraphrag.labels" . | nindent 4 }} + app.kubernetes.io/component: ui +spec: + {{- if not .Values.ui.autoscaling.enabled }} + replicas: {{ .Values.ui.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "pharmagraphrag.selectorLabels" (dict "Chart" .Chart "Release" .Release "component" "ui") | nindent 6 }} + template: + metadata: + labels: + {{- include "pharmagraphrag.selectorLabels" (dict "Chart" .Chart "Release" .Release "component" "ui") | nindent 8 }} + spec: + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: ui + image: {{ include "pharmagraphrag.image" (dict "globalImage" .Values.image "componentImage" .Values.ui.image "chartAppVersion" .Chart.AppVersion) }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + ports: + - name: http + containerPort: {{ .Values.ui.service.targetPort }} + envFrom: + - configMapRef: + name: {{ .Release.Name }}-config + resources: + {{- toYaml .Values.ui.resources | nindent 12 }} + readinessProbe: + httpGet: + path: /_stcore/health + port: http + {{- toYaml .Values.ui.probes.readiness | nindent 12 }} + livenessProbe: + httpGet: + path: /_stcore/health + port: http + {{- toYaml .Values.ui.probes.liveness | nindent 12 }} +{{- end }} diff --git a/helm/pharmagraphrag/templates/ui-hpa.yaml b/helm/pharmagraphrag/templates/ui-hpa.yaml new file mode 100644 index 0000000..a8e21a1 --- /dev/null +++ b/helm/pharmagraphrag/templates/ui-hpa.yaml @@ -0,0 +1,23 @@ +{{- if and .Values.ui.enabled .Values.ui.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ .Release.Name }}-ui + labels: + {{- include "pharmagraphrag.labels" . | nindent 4 }} + app.kubernetes.io/component: ui +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ .Release.Name }}-ui + minReplicas: {{ .Values.ui.autoscaling.minReplicas }} + maxReplicas: {{ .Values.ui.autoscaling.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.ui.autoscaling.targetCPUUtilizationPercentage }} +{{- end }} diff --git a/helm/pharmagraphrag/templates/ui-service.yaml b/helm/pharmagraphrag/templates/ui-service.yaml new file mode 100644 index 0000000..41bff96 --- /dev/null +++ b/helm/pharmagraphrag/templates/ui-service.yaml @@ -0,0 +1,22 @@ +{{- if .Values.ui.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-ui + labels: + {{- include "pharmagraphrag.labels" . | nindent 4 }} + app.kubernetes.io/component: ui + {{- if eq .Values.ui.service.type "LoadBalancer" }} + annotations: + cloud.google.com/load-balancer-type: "External" + {{- end }} +spec: + type: {{ .Values.ui.service.type }} + ports: + - port: {{ .Values.ui.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "pharmagraphrag.selectorLabels" (dict "Chart" .Chart "Release" .Release "component" "ui") | nindent 4 }} +{{- end }} diff --git a/helm/pharmagraphrag/values.yaml b/helm/pharmagraphrag/values.yaml new file mode 100644 index 0000000..1588393 --- /dev/null +++ b/helm/pharmagraphrag/values.yaml @@ -0,0 +1,130 @@ +# Default values for pharmagraphrag. +# This is a YAML-formatted file. + +# -- Global image registry shared by API + UI (override per component below if needed) +image: + registry: gcr.io/pharmagraphrag + pullPolicy: IfNotPresent + tag: latest + +# -- API component (FastAPI + ChromaDB embedded) +api: + enabled: true + image: + repository: pharmagraphrag-api + tag: "" # falls back to .Values.image.tag and Chart.appVersion + replicaCount: 1 + service: + type: ClusterIP + port: 8000 + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: 1000m + memory: 2Gi + autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 3 + targetCPUUtilizationPercentage: 70 + targetMemoryUtilizationPercentage: 80 + probes: + startup: + initialDelaySeconds: 10 + periodSeconds: 5 + failureThreshold: 24 + readiness: + initialDelaySeconds: 30 + periodSeconds: 10 + liveness: + initialDelaySeconds: 60 + periodSeconds: 30 + +# -- UI component (Streamlit) +ui: + enabled: true + image: + repository: pharmagraphrag-ui + tag: "" + replicaCount: 1 + service: + # LoadBalancer = direct external IP (recommended for demos without a domain) + # ClusterIP = internal-only (use Ingress for external access) + type: LoadBalancer + port: 80 + targetPort: 8501 + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: 500m + memory: 1Gi + autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 2 + targetCPUUtilizationPercentage: 75 + probes: + readiness: + initialDelaySeconds: 15 + periodSeconds: 10 + liveness: + initialDelaySeconds: 30 + periodSeconds: 30 + +# -- Non-secret application configuration (mounted as env vars via ConfigMap) +config: + LLM_PROVIDER: "gemini" + LLM_MODEL: "gemini-2.5-flash" + NEO4J_USER: "neo4j" + CHROMA_PERSIST_DIR: "/app/data/chroma" + API_HOST: "0.0.0.0" + API_PORT: "8000" + STREAMLIT_PORT: "8501" + LANGFUSE_ENABLED: "false" + LANGFUSE_HOST: "https://cloud.langfuse.com" + +# -- Secret values (NEVER commit real values here; override via --set or external secret manager) +secrets: + # Set to true to let Helm create the Secret (dev/demo only) + create: false + # If create=false, an existing Secret with this name must be provided out-of-band + existingSecret: pharmagraphrag-secrets + # Used only when create=true + values: + GEMINI_API_KEY: "" + NEO4J_URI: "" + NEO4J_PASSWORD: "" + LANGFUSE_PUBLIC_KEY: "" + LANGFUSE_SECRET_KEY: "" + +# -- Optional Ingress (GKE managed cert) +ingress: + enabled: false + className: "gce" + host: "" # e.g. pharmagraphrag.example.com + managedCertificate: false + staticIPName: "" # e.g. pharmagraphrag-ip (reserved beforehand with gcloud) + +# -- Pod security context (non-root) +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + +# -- Container security context +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: false # ChromaDB needs writes to /tmp internally + +# -- Node selection +nodeSelector: {} +tolerations: [] +affinity: {} diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 0000000..12ab703 --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,96 @@ +# Kubernetes manifests + +Production-grade Kubernetes manifests for PharmaGraphRAG. Two ways to deploy: + +1. **Raw manifests** (this folder) — apply directly with `kubectl apply -f k8s/`. +2. **Helm chart** ([../helm/pharmagraphrag/](../helm/pharmagraphrag/)) — recommended for real environments. + +## What's deployed + +| Resource | Purpose | +|---|---| +| `namespace.yaml` | Isolates all resources in `pharmagraphrag` namespace | +| `configmap.yaml` | Non-secret config (LLM model, Neo4j user, Chroma path, internal API URL) | +| `secret.example.yaml` | Template only — real Secret created via `kubectl create secret` | +| `api-deployment.yaml` | FastAPI Deployment (1 replica baseline, ChromaDB baked-in image) | +| `api-service.yaml` | ClusterIP exposing API on port 8000 | +| `api-hpa.yaml` | HPA: scale 1→3 on CPU 70% / memory 80% | +| `ui-deployment.yaml` | Streamlit Deployment | +| `ui-service.yaml` | LoadBalancer Service (external IP) for UI | +| `ui-hpa.yaml` | HPA: scale 1→2 on CPU 75% | +| `ingress.yaml` | Optional: GKE Ingress + managed cert (requires domain) | +| `kustomization.yaml` | Apply everything at once via Kustomize | + +## Quick deploy (GKE Autopilot) + +Assumes `gcloud`, `kubectl` and a GCP project with billing are set up. + +```bash +# 1. Create cluster (~5 min) +gcloud container clusters create-auto pharmagraphrag-autopilot \ + --region=us-central1 --project=pharmagraphrag + +# 2. Get credentials +gcloud container clusters get-credentials pharmagraphrag-autopilot \ + --region=us-central1 --project=pharmagraphrag + +# 3. Create namespace +kubectl apply -f k8s/namespace.yaml + +# 4. Create the real Secret (DO NOT use secret.example.yaml directly) +kubectl create secret generic pharmagraphrag-secrets \ + --namespace=pharmagraphrag \ + --from-literal=GEMINI_API_KEY="$GEMINI_API_KEY" \ + --from-literal=NEO4J_URI="$NEO4J_URI" \ + --from-literal=NEO4J_PASSWORD="$NEO4J_PASSWORD" + +# 5. Apply everything else +kubectl apply -k k8s/ + +# 6. Watch pods come up +kubectl -n pharmagraphrag get pods -w + +# 7. Get UI external IP (~1-2 min for LB provisioning) +kubectl -n pharmagraphrag get svc pharmagraphrag-ui +``` + +## Local validation with kind + +```bash +kind create cluster --name pgrag +kubectl apply -k k8s/ +# Port-forward instead of LoadBalancer: +kubectl -n pharmagraphrag port-forward svc/pharmagraphrag-ui 8501:80 +``` + +## Cost & cleanup + +GKE Autopilot bills per pod CPU/memory plus a small cluster management fee. +For a portfolio demo: + +- Run cluster ~2 hours → ~$1-2 total +- **Always destroy after screenshots** to avoid surprise bills: + +```bash +helm uninstall pharmagraphrag -n pharmagraphrag # if installed via Helm +kubectl delete -k k8s/ # if applied raw +gcloud container clusters delete pharmagraphrag-autopilot \ + --region=us-central1 --project=pharmagraphrag --quiet +``` + +The cluster is **on-demand by design**: manifests live in this repo, you can `helm install` +in 5 minutes whenever you need the live demo (e.g. before an interview screen-share). + +## Why this exists + +PharmaGraphRAG's primary cloud deployment is **Cloud Run** (lower cost for low-traffic +demos, scales to zero). The Kubernetes path was added as part of the +[portfolio upgrade roadmap](../README.md#deployment-options) to demonstrate +production-grade orchestration patterns: + +- Stateless API + UI Deployments with resource requests/limits +- Liveness, readiness and startup probes tuned for the embedding-model cold start +- HorizontalPodAutoscalers on CPU and memory +- ConfigMap + Secret separation +- Helm packaging with parameterized values +- CI/CD via GitHub Actions to GKE diff --git a/k8s/api-deployment.yaml b/k8s/api-deployment.yaml new file mode 100644 index 0000000..1cdbf65 --- /dev/null +++ b/k8s/api-deployment.yaml @@ -0,0 +1,67 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pharmagraphrag-api + namespace: pharmagraphrag + labels: + app.kubernetes.io/name: pharmagraphrag + app.kubernetes.io/component: api +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: pharmagraphrag + app.kubernetes.io/component: api + template: + metadata: + labels: + app.kubernetes.io/name: pharmagraphrag + app.kubernetes.io/component: api + spec: + containers: + - name: api + image: gcr.io/pharmagraphrag/pharmagraphrag-api:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8000 + envFrom: + - configMapRef: + name: pharmagraphrag-config + - secretRef: + name: pharmagraphrag-secrets + env: + - name: PORT + value: "8000" + resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "1000m" + memory: "2Gi" + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 + startupProbe: + httpGet: + path: /health + port: http + # Embedding model + ChromaDB load can take ~50s on cold start + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 5 + failureThreshold: 24 diff --git a/k8s/api-hpa.yaml b/k8s/api-hpa.yaml new file mode 100644 index 0000000..40bd948 --- /dev/null +++ b/k8s/api-hpa.yaml @@ -0,0 +1,38 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: pharmagraphrag-api + namespace: pharmagraphrag +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: pharmagraphrag-api + minReplicas: 1 + maxReplicas: 3 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 + behavior: + scaleDown: + stabilizationWindowSeconds: 300 + policies: + - type: Percent + value: 50 + periodSeconds: 60 + scaleUp: + stabilizationWindowSeconds: 60 + policies: + - type: Pods + value: 1 + periodSeconds: 60 diff --git a/k8s/api-service.yaml b/k8s/api-service.yaml new file mode 100644 index 0000000..4f89fea --- /dev/null +++ b/k8s/api-service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: pharmagraphrag-api + namespace: pharmagraphrag + labels: + app.kubernetes.io/name: pharmagraphrag + app.kubernetes.io/component: api +spec: + type: ClusterIP + ports: + - port: 8000 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: pharmagraphrag + app.kubernetes.io/component: api diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml new file mode 100644 index 0000000..560fbb7 --- /dev/null +++ b/k8s/configmap.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: pharmagraphrag-config + namespace: pharmagraphrag +data: + # Non-secret application configuration + LLM_PROVIDER: "gemini" + LLM_MODEL: "gemini-2.5-flash" + NEO4J_USER: "neo4j" + CHROMA_PERSIST_DIR: "/app/data/chroma" + API_HOST: "0.0.0.0" + API_PORT: "8000" + STREAMLIT_PORT: "8501" + # UI uses the internal service DNS to reach the API + API_URL: "http://pharmagraphrag-api.pharmagraphrag.svc.cluster.local:8000" + # Langfuse (opt-in) + LANGFUSE_ENABLED: "false" + LANGFUSE_HOST: "https://cloud.langfuse.com" diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 0000000..9ef6d3f --- /dev/null +++ b/k8s/ingress.yaml @@ -0,0 +1,51 @@ +# Optional: Ingress with GKE managed certificate (HTTPS) +# Requires: +# 1. A domain pointing to the Ingress IP via A record. +# 2. ManagedCertificate resource (GKE-specific). +# +# For quick demos without a domain, the LoadBalancer Service in +# ui-service.yaml already exposes the UI directly (no Ingress needed). +# +# Apply only if you have a domain. +apiVersion: networking.gke.io/v1 +kind: ManagedCertificate +metadata: + name: pharmagraphrag-cert + namespace: pharmagraphrag +spec: + domains: + - REPLACE_WITH_YOUR_DOMAIN.example.com +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: pharmagraphrag + namespace: pharmagraphrag + annotations: + kubernetes.io/ingress.class: "gce" + networking.gke.io/managed-certificates: pharmagraphrag-cert + kubernetes.io/ingress.global-static-ip-name: pharmagraphrag-ip +spec: + defaultBackend: + service: + name: pharmagraphrag-ui + port: + number: 80 + rules: + - host: REPLACE_WITH_YOUR_DOMAIN.example.com + http: + paths: + - path: /api + pathType: Prefix + backend: + service: + name: pharmagraphrag-api + port: + number: 8000 + - path: / + pathType: Prefix + backend: + service: + name: pharmagraphrag-ui + port: + number: 80 diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml new file mode 100644 index 0000000..88d3225 --- /dev/null +++ b/k8s/kustomization.yaml @@ -0,0 +1,15 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: pharmagraphrag +resources: + - namespace.yaml + - configmap.yaml + - api-deployment.yaml + - api-service.yaml + - api-hpa.yaml + - ui-deployment.yaml + - ui-service.yaml + - ui-hpa.yaml + # secret.example.yaml is intentionally excluded — create the real + # secret out-of-band with `kubectl create secret` (see secret.example.yaml). + # ingress.yaml is opt-in — uncomment if you have a domain. diff --git a/k8s/namespace.yaml b/k8s/namespace.yaml new file mode 100644 index 0000000..c0cacf0 --- /dev/null +++ b/k8s/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: pharmagraphrag + labels: + app.kubernetes.io/name: pharmagraphrag + app.kubernetes.io/managed-by: kubectl diff --git a/k8s/secret.example.yaml b/k8s/secret.example.yaml new file mode 100644 index 0000000..7e0c38d --- /dev/null +++ b/k8s/secret.example.yaml @@ -0,0 +1,24 @@ +# Example Secret manifest. DO NOT commit real values. +# +# Apply with real values via: +# kubectl create secret generic pharmagraphrag-secrets \ +# --namespace=pharmagraphrag \ +# --from-literal=GEMINI_API_KEY=$GEMINI_API_KEY \ +# --from-literal=NEO4J_URI=$NEO4J_URI \ +# --from-literal=NEO4J_PASSWORD=$NEO4J_PASSWORD \ +# --from-literal=LANGFUSE_PUBLIC_KEY=$LANGFUSE_PUBLIC_KEY \ +# --from-literal=LANGFUSE_SECRET_KEY=$LANGFUSE_SECRET_KEY +# +# Or use external-secrets-operator / GCP Secret Manager in production. +apiVersion: v1 +kind: Secret +metadata: + name: pharmagraphrag-secrets + namespace: pharmagraphrag +type: Opaque +stringData: + GEMINI_API_KEY: "REPLACE_ME" + NEO4J_URI: "neo4j+s://REPLACE_ME.databases.neo4j.io" + NEO4J_PASSWORD: "REPLACE_ME" + LANGFUSE_PUBLIC_KEY: "" + LANGFUSE_SECRET_KEY: "" diff --git a/k8s/ui-deployment.yaml b/k8s/ui-deployment.yaml new file mode 100644 index 0000000..6678577 --- /dev/null +++ b/k8s/ui-deployment.yaml @@ -0,0 +1,53 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pharmagraphrag-ui + namespace: pharmagraphrag + labels: + app.kubernetes.io/name: pharmagraphrag + app.kubernetes.io/component: ui +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: pharmagraphrag + app.kubernetes.io/component: ui + template: + metadata: + labels: + app.kubernetes.io/name: pharmagraphrag + app.kubernetes.io/component: ui + spec: + containers: + - name: ui + image: gcr.io/pharmagraphrag/pharmagraphrag-ui:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8501 + envFrom: + - configMapRef: + name: pharmagraphrag-config + resources: + requests: + cpu: "250m" + memory: "512Mi" + limits: + cpu: "500m" + memory: "1Gi" + readinessProbe: + httpGet: + path: /_stcore/health + port: http + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /_stcore/health + port: http + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 diff --git a/k8s/ui-hpa.yaml b/k8s/ui-hpa.yaml new file mode 100644 index 0000000..cc25119 --- /dev/null +++ b/k8s/ui-hpa.yaml @@ -0,0 +1,19 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: pharmagraphrag-ui + namespace: pharmagraphrag +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: pharmagraphrag-ui + minReplicas: 1 + maxReplicas: 2 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 75 diff --git a/k8s/ui-service.yaml b/k8s/ui-service.yaml new file mode 100644 index 0000000..b162126 --- /dev/null +++ b/k8s/ui-service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: pharmagraphrag-ui + namespace: pharmagraphrag + labels: + app.kubernetes.io/name: pharmagraphrag + app.kubernetes.io/component: ui + annotations: + # GKE: provision a Google Cloud Load Balancer for external access + cloud.google.com/load-balancer-type: "External" +spec: + type: LoadBalancer + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: pharmagraphrag + app.kubernetes.io/component: ui From 8a74585c22d221617bc898dde2644240515e8b33 Mon Sep 17 00:00:00 2001 From: JMPonce Date: Tue, 2 Jun 2026 19:51:07 +0200 Subject: [PATCH 2/4] ci: validate Helm chart and K8s manifests, docs sync - ci.yml: new helm-validate job runs helm lint + kubeconform on rendered chart and raw k8s/ manifests when helm/ or k8s/ files change. - README.md: add Deployment Options section comparing Cloud Run vs GKE+Helm. - .github/copilot-instructions.md: add K8s/Helm row to status table and document new deploy-gke.yml workflow. --- .github/copilot-instructions.md | 4 ++- .github/workflows/ci.yml | 43 +++++++++++++++++++++++++++++++++ README.md | 11 +++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 373bc9d..a71b502 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -24,7 +24,8 @@ All three development phases are finished. The system is fully operational end-t | Agent Mode | Complete | `src/pharmagraphrag/agent/` (LangGraph ReAct + multi-agent supervisor) | | Observability | Complete | `src/pharmagraphrag/observability.py` (Langfuse tracing) | | Docker Compose | Complete | `docker-compose.yml` + `docker/` | -| CI/CD | Complete | `.github/workflows/ci.yml` + `deploy.yml` | +| Kubernetes / Helm | Complete | `k8s/` (raw manifests) + `helm/pharmagraphrag/` (chart) | +| CI/CD | Complete | `.github/workflows/ci.yml` + `deploy.yml` + `deploy-gke.yml` | | Evaluation | Complete | `src/pharmagraphrag/evaluation/` (RAGAS metrics, agent eval, curated testset) | | Tests | 263 passing | `tests/` | | Cloud Deployment | Live | Streamlit Cloud + Cloud Run + Neo4j Aura | @@ -102,6 +103,7 @@ FDA FAERS (CSV) + DailyMed (API) - **API**: FastAPI >= 0.115 with Pydantic v2 - **UI**: Streamlit 1.54+ with streamlit-agraph, pyvis, plotly - **Containers**: Docker Compose (Neo4j + API + UI + optional Ollama) +- **Kubernetes**: Helm 3 chart (`helm/pharmagraphrag/`) + raw manifests (`k8s/`). HPA on CPU/memory, startup probes tuned for ~50s embedding-model cold start, LoadBalancer for UI, ClusterIP for API, optional GKE Ingress + managed cert. Designed for on-demand GKE Autopilot (destroy cluster after demos). - **CI/CD**: GitHub Actions (ci.yml: lint+test on push; deploy.yml: CD on v* tags via Cloud Build) - **Evaluation**: RAGAS 0.4.3 (Faithfulness, Relevancy, Precision, Recall, Correctness) + custom agent tool accuracy - **Testing**: pytest (261 tests passing) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aaa7056..0707e53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,7 @@ jobs: runs-on: ubuntu-latest outputs: docker: ${{ steps.filter.outputs.docker }} + helm: ${{ steps.filter.outputs.helm }} steps: - uses: actions/checkout@v4 - uses: dorny/paths-filter@v3 @@ -75,6 +76,48 @@ jobs: - 'uv.lock' - 'requirements.txt' - '.github/workflows/ci.yml' + helm: + - 'helm/**' + - 'k8s/**' + - '.github/workflows/ci.yml' + + # --------------------------------------------------------------- + # Job 3 — Validate Helm chart and K8s manifests + # --------------------------------------------------------------- + helm-validate: + runs-on: ubuntu-latest + needs: changes + if: needs.changes.outputs.helm == 'true' + steps: + - uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.16.2 + + - name: Helm lint + run: helm lint helm/pharmagraphrag + + - name: Helm template (default values) + run: helm template demo helm/pharmagraphrag --namespace pharmagraphrag > /tmp/rendered.yaml + + - name: Install kubeconform + run: | + curl -L -o kubeconform.tar.gz https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz + tar xf kubeconform.tar.gz + sudo mv kubeconform /usr/local/bin/ + + - name: Validate rendered manifests against K8s schema + run: kubeconform -strict -ignore-missing-schemas -schema-location default /tmp/rendered.yaml + + - name: Validate raw k8s/ manifests + run: | + # Skip the example secret file + for f in k8s/*.yaml; do + [[ "$f" == *secret.example.yaml ]] && continue + kubeconform -strict -ignore-missing-schemas -schema-location default "$f" + done # --------------------------------------------------------------- # Job 2 — Build Docker images (only when Docker-relevant files change) diff --git a/README.md b/README.md index 7db9de1..dc306a7 100644 --- a/README.md +++ b/README.md @@ -360,6 +360,17 @@ The project is **deployed and live** on a distributed cloud architecture: | API + Vector Store | [Google Cloud Run](https://cloud.google.com/run) | [pharmagraphrag-api-...run.app](https://pharmagraphrag-api-893694384146.us-central1.run.app/health) | | Knowledge Graph | [Neo4j Aura](https://neo4j.com/cloud/aura-free/) | Managed instance (11.9K nodes, 381K rels) | +### Deployment Options + +PharmaGraphRAG ships with two cloud deployment paths that you can pick from: + +| Path | Best for | Cost (idle) | Files | +| --- | --- | --- | --- | +| **Cloud Run + Streamlit Cloud** (default) | Low-traffic demos, scale-to-zero | $0 | [`cloudbuild.yaml`](cloudbuild.yaml), [`docker/Dockerfile.cloudrun`](docker/Dockerfile.cloudrun) | +| **GKE Autopilot + Helm** | Production-grade orchestration, HPA, rolling updates | ~$1-2/h while running | [`k8s/`](k8s/), [`helm/pharmagraphrag/`](helm/pharmagraphrag/), [`.github/workflows/deploy-gke.yml`](.github/workflows/deploy-gke.yml) | + +The Kubernetes path was added to the portfolio to demonstrate production patterns: parameterized Helm chart, HorizontalPodAutoscalers, ConfigMap/Secret separation, probes tuned for the embedding-model cold start, and an automated GitHub Actions workflow that builds images, pushes to GCR and rolls out via `helm upgrade`. The cluster is **provisioned on-demand**: manifests live permanently in the repo, and the cluster is destroyed after each demo to avoid GKE costs. See [`k8s/README.md`](k8s/README.md) for the deploy/destroy commands. +
📋 Reproducing the deployment From 12234d076312fb2b1bcf6a813b119112ad5b89e7 Mon Sep 17 00:00:00 2001 From: JMPonce Date: Tue, 2 Jun 2026 20:37:26 +0200 Subject: [PATCH 3/4] =?UTF-8?q?fix(k8s):=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20security=20contexts,=20ConfigMap=20keys,=20ingress?= =?UTF-8?q?=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Copilot review on PR #6: - k8s/api-deployment.yaml, ui-deployment.yaml: add pod + container securityContext (runAsNonRoot, drop ALL capabilities) to match Helm chart posture and pass Pod Security Admission 'restricted'. - k8s/configmap.yaml, helm/.../values.yaml: remove keys not read by Settings (LLM_MODEL, API_HOST, API_PORT, STREAMLIT_PORT) and rename LANGFUSE_HOST -> LANGFUSE_BASE_URL (matches Settings attribute). - helm/.../templates/configmap.yaml: only auto-inject API_URL when not already provided via .Values.config to avoid duplicate keys. - helm/.../templates/ingress.yaml: use 'required' to fail fast when ingress.enabled=true but ingress.host is empty. - helm/.../Chart.yaml: align appVersion (1.5.0 -> 0.1.0) with src/pharmagraphrag/__init__.py. --- data/evaluation/results/v2_full/agent_log.txt | Bin 0 -> 28452 bytes data/evaluation/results/v2_full/classic_log.txt | Bin 0 -> 18824 bytes data/evaluation/results/v2_full/multi_log.txt | Bin 0 -> 27210 bytes helm/pharmagraphrag/Chart.yaml | 2 +- helm/pharmagraphrag/templates/configmap.yaml | 4 +++- helm/pharmagraphrag/templates/ingress.yaml | 5 +++-- helm/pharmagraphrag/values.yaml | 9 +++------ k8s/api-deployment.yaml | 11 +++++++++++ k8s/configmap.yaml | 8 ++------ k8s/ui-deployment.yaml | 11 +++++++++++ pr6.json | Bin 0 -> 8592 bytes 11 files changed, 34 insertions(+), 16 deletions(-) create mode 100644 data/evaluation/results/v2_full/agent_log.txt create mode 100644 data/evaluation/results/v2_full/classic_log.txt create mode 100644 data/evaluation/results/v2_full/multi_log.txt create mode 100644 pr6.json diff --git a/data/evaluation/results/v2_full/agent_log.txt b/data/evaluation/results/v2_full/agent_log.txt new file mode 100644 index 0000000000000000000000000000000000000000..9d2d77e821ad4ca69aedd1c5135b32b9f42b3257 GIT binary patch literal 28452 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 D1r&&K literal 0 HcmV?d00001 diff --git a/data/evaluation/results/v2_full/multi_log.txt b/data/evaluation/results/v2_full/multi_log.txt new file mode 100644 index 0000000000000000000000000000000000000000..c30ccd3565d7949cec7e5f9e844942d88462a538 GIT binary patch literal 27210 zcmeI5?QR>#6^7?`f!+iE6iSgsDZfODg;A)Dof>sqS8+7?rBY)TXseA68xIbtsyx99juM@ZDj@+r=9$f!5&@Z=i^@gtB za9_KL&Kl{HGY_@rr|u_is21(HV|SrXj-2Tj?K{!dzK%Z9)`gC9>=)X<=gzhD$i3H| ziM9vsbG@2sb5E_M#k`x^Ps@36m&dwlLs$8izK6O7cc53l)%M8!)4kKRhT393E#^0$ zYJ1ba8tV67+@Ia=b)@H>`R@()D;+m^j`Yp-&)r8|=e_Sk!;N*-k=lB$ePj1p+nn`g zZjU$pZ;{hy*F8rDq33plHikBZ7KV{n^!*Qa-!EDgT7wLcF)croTt<@6Q%T`K+u@w) zZ{+`6$0kC{xqkCb@;lIbC@gU76OD5Ww>DmJJQ(imt^6849Z&zbJT3_8{E z;%vJIpYc93V~e@k*ButmRjzg}go1kRc&Kk?)O-JZD74R;kDF>2W4I?A5A{7z|IYN+ zzR-+(#`+9L`mU=MFyEhDox|9{H|(ExbsLYdu6LrA9Q*4vH0sFgM87HD;f-Y0QaiuX z9pQ?*mCr1f#b)h5eL~)gdQ02N{er=&GB7R#fep8z9)~%l;7YCfZ^QnRjqFT7qxJm)X-0zZeUEX|&wF=Z2A=SB9CPjxf&}{u<{xA6;s* z3qBqB-;FhgjTz|J%VeqBReJ)r>04-|B>mYs}u^_Q52N9V_&0v zAyxqufk(a<4$d_8aK=Pi@W`IdLK`&Ty&PeMZiMPDet`XdFsa+OSGYpztEVQhA~!wBe0)9gIVX;Xa$Fy z2tN~VrDG===NPI|It5DD!J{t(B+8_LlU)vCiRFSkyqiH`G& zhGJqwzu?~k&6Q*I7nVg&W3!ojEPg=?UwXZafBejSrE?B-w)s>rvBekQVmP{?Bk1nn zADDk^R{y5u2tJe?PM#Y-H%kM_rB95fiXjUY*hD z_|bvuxAY4BLtdk^BdLrfPpoBdPBezd(!@*M#)+O+$10z3vT-So)osQp9?#`&HY z+rYfQG!{DEj_*j@6D``hw#eq^23Mm?nnLMxteL-QNh~gqN5A4|pdYcQVO5(H@uKOd z&`9Ng=K zjc?5Fr>)uae6%>N;ifAD<%pYdUV0X{u2Ti?%D~usR^3-bGF#FAumJPi?B5Y5vHe zUnz7f6Hg7cNU%b%PN|#0ZiOkZQ_UU;k8!u7&_LLEDTy zVu-3!_3+kb>d4c9D$bhLD}^mvwQ^JP#p4ZL3^wX&Y`J(GmS|rYH}NSi5g&sp+rR(!dzIeZ;i&*oOV;vTm#xic= z@z{C&T8&qq!p$=A*lF7YP9Yk9e8nk@Js3EXULs;&uHLM2Nm>gLrwXw&Rs{y>Y!iNtZ| zHU=%~r-IEN)6(UN*We4V7X~&Tfe6E}hEIs?No6bfE%HWzadV0ywZu4Oti-byU?q=! zrLeM0_M+?)VwrJuVuluPwpapoUQxXp;tR78%Cs(KJsuXCRao>hMMgP4L7P(^OCJ47 zAtRojAY(r55l-7^(=|4QzgJl1d){WVDqi9XFb#18i#1plbTRvkYseRm+pGlQkN0EQ zUn)vUrO8CqoU?@cHOvT4Uo$t|7vI@z@M}p;H6eH z1Px`+zo6RpLhXlNtV;hAA7#d3ANwa3*cDdYA8Kjo4=>u1wgy=p&9getf$6wFOHtGM z=w5z2V(s4*(s(WbC}f4jN43P--ct|KG4%UxI~i14AAG5K`NGc>R{Bx-%aPD}a)@PkRM_ed8a|zJ!$lG9OJ&R!ny+LaSYcmCCts!akiyH`g$`% z`;z!g8RiRd|8K-@wodD|YqiYs0^P-vbml#aV*0$GNy0Nq@XT1v3|~z@!{LRv*q$In zwHf{0E$w-t-%X#vdP$J^NW4upfB1hbBa28y5qedFw`Ba>fLhw;Q!dnW3@KU# zio5bZQH%xdrt)lAX;LZ1>SprOwvvO{6P~I-9g{sjcJ@D!P#a{3DC$>d{|ggR|1T!A BAcOz_ literal 0 HcmV?d00001 diff --git a/helm/pharmagraphrag/Chart.yaml b/helm/pharmagraphrag/Chart.yaml index 25fe224..aa2acc0 100644 --- a/helm/pharmagraphrag/Chart.yaml +++ b/helm/pharmagraphrag/Chart.yaml @@ -3,7 +3,7 @@ name: pharmagraphrag description: GraphRAG system for drug interactions & adverse events (FDA data + Neo4j + ChromaDB + Gemini/Ollama) type: application version: 0.1.0 -appVersion: "1.5.0" +appVersion: "0.1.0" keywords: - graphrag - rag diff --git a/helm/pharmagraphrag/templates/configmap.yaml b/helm/pharmagraphrag/templates/configmap.yaml index b924f95..f47f640 100644 --- a/helm/pharmagraphrag/templates/configmap.yaml +++ b/helm/pharmagraphrag/templates/configmap.yaml @@ -8,5 +8,7 @@ data: {{- range $key, $value := .Values.config }} {{ $key }}: {{ $value | quote }} {{- end }} - # Inter-service DNS for the UI to reach the API + {{- if not (hasKey .Values.config "API_URL") }} + # Inter-service DNS for the UI to reach the API (auto-injected when not overridden) API_URL: "http://{{ .Release.Name }}-api:{{ .Values.api.service.port }}" + {{- end }} diff --git a/helm/pharmagraphrag/templates/ingress.yaml b/helm/pharmagraphrag/templates/ingress.yaml index 77c151a..6830909 100644 --- a/helm/pharmagraphrag/templates/ingress.yaml +++ b/helm/pharmagraphrag/templates/ingress.yaml @@ -1,4 +1,5 @@ {{- if .Values.ingress.enabled }} +{{- $host := required "ingress.host is required when ingress.enabled=true" .Values.ingress.host -}} {{- if .Values.ingress.managedCertificate }} apiVersion: networking.gke.io/v1 kind: ManagedCertificate @@ -8,7 +9,7 @@ metadata: {{- include "pharmagraphrag.labels" . | nindent 4 }} spec: domains: - - {{ .Values.ingress.host | quote }} + - {{ $host | quote }} --- {{- end }} apiVersion: networking.k8s.io/v1 @@ -27,7 +28,7 @@ metadata: {{- end }} spec: rules: - - host: {{ .Values.ingress.host | quote }} + - host: {{ $host | quote }} http: paths: - path: /api diff --git a/helm/pharmagraphrag/values.yaml b/helm/pharmagraphrag/values.yaml index 1588393..e1c7602 100644 --- a/helm/pharmagraphrag/values.yaml +++ b/helm/pharmagraphrag/values.yaml @@ -75,17 +75,14 @@ ui: initialDelaySeconds: 30 periodSeconds: 30 -# -- Non-secret application configuration (mounted as env vars via ConfigMap) +# -- Non-secret application configuration (mounted as env vars via ConfigMap). +# Keys must match attributes in src/pharmagraphrag/config.py:Settings. config: LLM_PROVIDER: "gemini" - LLM_MODEL: "gemini-2.5-flash" NEO4J_USER: "neo4j" CHROMA_PERSIST_DIR: "/app/data/chroma" - API_HOST: "0.0.0.0" - API_PORT: "8000" - STREAMLIT_PORT: "8501" LANGFUSE_ENABLED: "false" - LANGFUSE_HOST: "https://cloud.langfuse.com" + LANGFUSE_BASE_URL: "https://cloud.langfuse.com" # -- Secret values (NEVER commit real values here; override via --set or external secret manager) secrets: diff --git a/k8s/api-deployment.yaml b/k8s/api-deployment.yaml index 1cdbf65..c9de744 100644 --- a/k8s/api-deployment.yaml +++ b/k8s/api-deployment.yaml @@ -18,10 +18,21 @@ spec: app.kubernetes.io/name: pharmagraphrag app.kubernetes.io/component: api spec: + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 containers: - name: api image: gcr.io/pharmagraphrag/pharmagraphrag-api:latest imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: false ports: - name: http containerPort: 8000 diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml index 560fbb7..3962b89 100644 --- a/k8s/configmap.yaml +++ b/k8s/configmap.yaml @@ -4,16 +4,12 @@ metadata: name: pharmagraphrag-config namespace: pharmagraphrag data: - # Non-secret application configuration + # Non-secret application configuration (keys must match Settings in src/pharmagraphrag/config.py) LLM_PROVIDER: "gemini" - LLM_MODEL: "gemini-2.5-flash" NEO4J_USER: "neo4j" CHROMA_PERSIST_DIR: "/app/data/chroma" - API_HOST: "0.0.0.0" - API_PORT: "8000" - STREAMLIT_PORT: "8501" # UI uses the internal service DNS to reach the API API_URL: "http://pharmagraphrag-api.pharmagraphrag.svc.cluster.local:8000" # Langfuse (opt-in) LANGFUSE_ENABLED: "false" - LANGFUSE_HOST: "https://cloud.langfuse.com" + LANGFUSE_BASE_URL: "https://cloud.langfuse.com" diff --git a/k8s/ui-deployment.yaml b/k8s/ui-deployment.yaml index 6678577..07e2730 100644 --- a/k8s/ui-deployment.yaml +++ b/k8s/ui-deployment.yaml @@ -18,10 +18,21 @@ spec: app.kubernetes.io/name: pharmagraphrag app.kubernetes.io/component: ui spec: + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 containers: - name: ui image: gcr.io/pharmagraphrag/pharmagraphrag-ui:latest imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: false ports: - name: http containerPort: 8501 diff --git a/pr6.json b/pr6.json new file mode 100644 index 0000000000000000000000000000000000000000..92db911d0453f6af318b3374659f0f5632114ebf GIT binary patch literal 8592 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 H!x!OyOX>^2 literal 0 HcmV?d00001 From 85259640ff7c185627e3467571d76acb28dec756 Mon Sep 17 00:00:00 2001 From: JMPonce Date: Tue, 2 Jun 2026 20:37:36 +0200 Subject: [PATCH 4/4] chore: remove accidentally committed local files --- data/evaluation/results/v2_full/agent_log.txt | Bin 28452 -> 0 bytes data/evaluation/results/v2_full/classic_log.txt | Bin 18824 -> 0 bytes data/evaluation/results/v2_full/multi_log.txt | Bin 27210 -> 0 bytes pr6.json | Bin 8592 -> 0 bytes 4 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 data/evaluation/results/v2_full/agent_log.txt delete mode 100644 data/evaluation/results/v2_full/classic_log.txt delete mode 100644 data/evaluation/results/v2_full/multi_log.txt delete mode 100644 pr6.json diff --git a/data/evaluation/results/v2_full/agent_log.txt b/data/evaluation/results/v2_full/agent_log.txt deleted file mode 100644 index 9d2d77e821ad4ca69aedd1c5135b32b9f42b3257..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28452 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 D1r&&K diff --git a/data/evaluation/results/v2_full/multi_log.txt b/data/evaluation/results/v2_full/multi_log.txt deleted file mode 100644 index c30ccd3565d7949cec7e5f9e844942d88462a538..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27210 zcmeI5?QR>#6^7?`f!+iE6iSgsDZfODg;A)Dof>sqS8+7?rBY)TXseA68xIbtsyx99juM@ZDj@+r=9$f!5&@Z=i^@gtB za9_KL&Kl{HGY_@rr|u_is21(HV|SrXj-2Tj?K{!dzK%Z9)`gC9>=)X<=gzhD$i3H| ziM9vsbG@2sb5E_M#k`x^Ps@36m&dwlLs$8izK6O7cc53l)%M8!)4kKRhT393E#^0$ zYJ1ba8tV67+@Ia=b)@H>`R@()D;+m^j`Yp-&)r8|=e_Sk!;N*-k=lB$ePj1p+nn`g zZjU$pZ;{hy*F8rDq33plHikBZ7KV{n^!*Qa-!EDgT7wLcF)croTt<@6Q%T`K+u@w) zZ{+`6$0kC{xqkCb@;lIbC@gU76OD5Ww>DmJJQ(imt^6849Z&zbJT3_8{E z;%vJIpYc93V~e@k*ButmRjzg}go1kRc&Kk?)O-JZD74R;kDF>2W4I?A5A{7z|IYN+ zzR-+(#`+9L`mU=MFyEhDox|9{H|(ExbsLYdu6LrA9Q*4vH0sFgM87HD;f-Y0QaiuX z9pQ?*mCr1f#b)h5eL~)gdQ02N{er=&GB7R#fep8z9)~%l;7YCfZ^QnRjqFT7qxJm)X-0zZeUEX|&wF=Z2A=SB9CPjxf&}{u<{xA6;s* z3qBqB-;FhgjTz|J%VeqBReJ)r>04-|B>mYs}u^_Q52N9V_&0v zAyxqufk(a<4$d_8aK=Pi@W`IdLK`&Ty&PeMZiMPDet`XdFsa+OSGYpztEVQhA~!wBe0)9gIVX;Xa$Fy z2tN~VrDG===NPI|It5DD!J{t(B+8_LlU)vCiRFSkyqiH`G& zhGJqwzu?~k&6Q*I7nVg&W3!ojEPg=?UwXZafBejSrE?B-w)s>rvBekQVmP{?Bk1nn zADDk^R{y5u2tJe?PM#Y-H%kM_rB95fiXjUY*hD z_|bvuxAY4BLtdk^BdLrfPpoBdPBezd(!@*M#)+O+$10z3vT-So)osQp9?#`&HY z+rYfQG!{DEj_*j@6D``hw#eq^23Mm?nnLMxteL-QNh~gqN5A4|pdYcQVO5(H@uKOd z&`9Ng=K zjc?5Fr>)uae6%>N;ifAD<%pYdUV0X{u2Ti?%D~usR^3-bGF#FAumJPi?B5Y5vHe zUnz7f6Hg7cNU%b%PN|#0ZiOkZQ_UU;k8!u7&_LLEDTy zVu-3!_3+kb>d4c9D$bhLD}^mvwQ^JP#p4ZL3^wX&Y`J(GmS|rYH}NSi5g&sp+rR(!dzIeZ;i&*oOV;vTm#xic= z@z{C&T8&qq!p$=A*lF7YP9Yk9e8nk@Js3EXULs;&uHLM2Nm>gLrwXw&Rs{y>Y!iNtZ| zHU=%~r-IEN)6(UN*We4V7X~&Tfe6E}hEIs?No6bfE%HWzadV0ywZu4Oti-byU?q=! zrLeM0_M+?)VwrJuVuluPwpapoUQxXp;tR78%Cs(KJsuXCRao>hMMgP4L7P(^OCJ47 zAtRojAY(r55l-7^(=|4QzgJl1d){WVDqi9XFb#18i#1plbTRvkYseRm+pGlQkN0EQ zUn)vUrO8CqoU?@cHOvT4Uo$t|7vI@z@M}p;H6eH z1Px`+zo6RpLhXlNtV;hAA7#d3ANwa3*cDdYA8Kjo4=>u1wgy=p&9getf$6wFOHtGM z=w5z2V(s4*(s(WbC}f4jN43P--ct|KG4%UxI~i14AAG5K`NGc>R{Bx-%aPD}a)@PkRM_ed8a|zJ!$lG9OJ&R!ny+LaSYcmCCts!akiyH`g$`% z`;z!g8RiRd|8K-@wodD|YqiYs0^P-vbml#aV*0$GNy0Nq@XT1v3|~z@!{LRv*q$In zwHf{0E$w-t-%X#vdP$J^NW4upfB1hbBa28y5qedFw`Ba>fLhw;Q!dnW3@KU# zio5bZQH%xdrt)lAX;LZ1>SprOwvvO{6P~I-9g{sjcJ@D!P#a{3DC$>d{|ggR|1T!A BAcOz_ diff --git a/pr6.json b/pr6.json deleted file mode 100644 index 92db911d0453f6af318b3374659f0f5632114ebf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8592 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 H!x!OyOX>^2