Compare commits
No commits in common. "main" and "chore/initial-deploy" have entirely different histories.
main
...
chore/init
@ -11,9 +11,8 @@ CLICKHOUSE_USER=wren_ia
|
|||||||
CLICKHOUSE_PASSWORD=
|
CLICKHOUSE_PASSWORD=
|
||||||
CLICKHOUSE_SECURE=true
|
CLICKHOUSE_SECURE=true
|
||||||
|
|
||||||
# CORS: origens permitidas por ambiente, separadas por vírgula.
|
# CORS (separar por vírgula)
|
||||||
# Definir por ambiente (no k8s vem do secret/env do overlay — nunca hardcode de host aqui).
|
VANNA_CORS_ORIGINS=https://lab.clubpetro.com,https://homologation.clubpetro.com
|
||||||
VANNA_CORS_ORIGINS=
|
|
||||||
|
|
||||||
# RLS defaults (apenas pra `python ask.py` na CLI; servidor web extrai de query string)
|
# RLS defaults (apenas pra `python ask.py` na CLI; servidor web extrai de query string)
|
||||||
RLS_PROGRAM_ID=
|
RLS_PROGRAM_ID=
|
||||||
|
|||||||
@ -11,21 +11,10 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ hashFiles('Dockerfile') != '' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
# Instala kubectl + auth plugin (gcloud já vem via setup-gcloud).
|
|
||||||
# Build é feito remoto via Cloud Build (não precisa Docker no runner).
|
|
||||||
- name: Install kubectl + gke-auth-plugin
|
|
||||||
run: |
|
|
||||||
apt-get update -qq
|
|
||||||
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
|
||||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
|
||||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
|
||||||
apt-get update -qq
|
|
||||||
apt-get install -y -qq kubectl google-cloud-cli-gke-gcloud-auth-plugin
|
|
||||||
kubectl version --client=true
|
|
||||||
|
|
||||||
- name: Auth GCP
|
- name: Auth GCP
|
||||||
uses: google-github-actions/auth@v2
|
uses: google-github-actions/auth@v2
|
||||||
with:
|
with:
|
||||||
@ -36,56 +25,28 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
project_id: ${{ secrets.GCP_PROJECT }}
|
project_id: ${{ secrets.GCP_PROJECT }}
|
||||||
|
|
||||||
- name: Build & push (Cloud Build, sem Docker local)
|
- name: Configure Docker auth
|
||||||
|
run: gcloud auth configure-docker ${{ secrets.AR_LOCATION }}-docker.pkg.dev --quiet
|
||||||
|
|
||||||
|
- name: Build image
|
||||||
run: |
|
run: |
|
||||||
set -e
|
|
||||||
IMG="${IMAGE_BASE}/${{ gitea.event.repository.name }}:lab-${{ gitea.run_number }}"
|
IMG="${IMAGE_BASE}/${{ gitea.event.repository.name }}:lab-${{ gitea.run_number }}"
|
||||||
# --gcs-source-staging-dir: pula o auto-detect que precisa de
|
docker build --platform=linux/amd64 -t "$IMG" .
|
||||||
# storage.buckets.list (project-scope). gitea-cd só tem grant
|
|
||||||
# na bucket _cloudbuild, não no projeto inteiro.
|
|
||||||
# --async: gcloud retorna assim que o build é enfileirado. Sem
|
|
||||||
# isso, gcloud tenta streamar Cloud Logging, exige Viewer/Owner
|
|
||||||
# no projeto, e --suppress-logs nessa versão do CLI ainda dá
|
|
||||||
# exit != 0 quando não consegue ler. Polling abaixo é o trade.
|
|
||||||
BUILD_ID=$(gcloud builds submit \
|
|
||||||
--tag "$IMG" \
|
|
||||||
--project=${{ secrets.GCP_PROJECT }} \
|
|
||||||
--timeout=30m \
|
|
||||||
--gcs-source-staging-dir="gs://${{ secrets.GCP_PROJECT }}_cloudbuild/source" \
|
|
||||||
--async \
|
|
||||||
--format="value(id)")
|
|
||||||
echo "Cloud Build kicked off: $BUILD_ID"
|
|
||||||
# Poll até terminar. gcloud builds describe usa cloudbuild.builds.get
|
|
||||||
# (já incluído em roles/cloudbuild.builds.editor).
|
|
||||||
while true; do
|
|
||||||
STATUS=$(gcloud builds describe "$BUILD_ID" \
|
|
||||||
--project=${{ secrets.GCP_PROJECT }} \
|
|
||||||
--region=global \
|
|
||||||
--format="value(status)")
|
|
||||||
echo "[$(date -u +%H:%M:%S)] build $BUILD_ID: $STATUS"
|
|
||||||
case "$STATUS" in
|
|
||||||
SUCCESS) break ;;
|
|
||||||
FAILURE|INTERNAL_ERROR|TIMEOUT|CANCELLED|EXPIRED)
|
|
||||||
echo "::error::Cloud Build $STATUS — ver console: https://console.cloud.google.com/cloud-build/builds/$BUILD_ID?project=${{ secrets.GCP_PROJECT }}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
sleep 15
|
|
||||||
done
|
|
||||||
echo "IMG=$IMG" >> $GITHUB_ENV
|
echo "IMG=$IMG" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Push image (apenas em push pra master/main)
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
run: docker push "$IMG"
|
||||||
|
|
||||||
- name: Deploy hml2 (apenas em push pra master/main)
|
- name: Deploy hml2 (apenas em push pra master/main)
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
env:
|
|
||||||
USE_GKE_GCLOUD_AUTH_PLUGIN: "True"
|
|
||||||
run: |
|
run: |
|
||||||
gcloud container clusters get-credentials ${{ secrets.GKE_CLUSTER }} --region ${{ secrets.GKE_REGION }} --project ${{ secrets.GCP_PROJECT }}
|
gcloud container clusters get-credentials ${{ secrets.GKE_CLUSTER }} --region ${{ secrets.GKE_REGION }} --project ${{ secrets.GCP_PROJECT }}
|
||||||
NS=${{ secrets.K8S_NAMESPACE }}
|
NS=${{ secrets.K8S_NAMESPACE }}
|
||||||
|
|
||||||
# 1) Aplica manifests do LAB (idempotente — cria PVC/Service/Ingress/Deployment se faltarem).
|
# 1) Aplica manifests (idempotente — cria PVC/Service/Ingress/Deployment se faltarem)
|
||||||
# Overlay do lab vive em k8s/lab/; homolog usa k8s/hml/ (promoção via pipeline lab->homolog).
|
if [ -d k8s ]; then
|
||||||
if [ -d k8s/lab ]; then
|
kubectl apply -n "$NS" -f k8s/
|
||||||
kubectl apply -n "$NS" -f k8s/lab/
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2) Atualiza image
|
# 2) Atualiza image
|
||||||
@ -93,7 +54,7 @@ jobs:
|
|||||||
if kubectl get deployment "$DEPLOYMENT" -n "$NS" >/dev/null 2>&1; then
|
if kubectl get deployment "$DEPLOYMENT" -n "$NS" >/dev/null 2>&1; then
|
||||||
CONTAINER=$(kubectl get deployment "$DEPLOYMENT" -n "$NS" -o jsonpath='{.spec.template.spec.containers[0].name}')
|
CONTAINER=$(kubectl get deployment "$DEPLOYMENT" -n "$NS" -o jsonpath='{.spec.template.spec.containers[0].name}')
|
||||||
kubectl set image deployment/"$DEPLOYMENT" -n "$NS" "$CONTAINER=$IMG"
|
kubectl set image deployment/"$DEPLOYMENT" -n "$NS" "$CONTAINER=$IMG"
|
||||||
kubectl rollout status deployment/"$DEPLOYMENT" -n "$NS" --timeout=600s
|
kubectl rollout status deployment/"$DEPLOYMENT" -n "$NS" --timeout=300s
|
||||||
else
|
else
|
||||||
echo "Deployment $DEPLOYMENT não existe no ns $NS — pulei set image (provavelmente é o 1º deploy e o kubectl apply acabou de criar)"
|
echo "Deployment $DEPLOYMENT não existe no ns $NS — pulei set image (provavelmente é o 1º deploy e o kubectl apply acabou de criar)"
|
||||||
fi
|
fi
|
||||||
|
|||||||
14
Dockerfile
14
Dockerfile
@ -7,7 +7,7 @@ ARG VANNA_UPSTREAM_COMMIT=365d0617c1a4567ffee1b19b40c27feb4206bfcf
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Stage 1 — webcomponent (Node)
|
# Stage 1 — webcomponent (Node)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
FROM node:20-bookworm-slim AS webcomponent
|
FROM node:18-bookworm-slim AS webcomponent
|
||||||
ARG VANNA_UPSTREAM_COMMIT
|
ARG VANNA_UPSTREAM_COMMIT
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
|
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@ -51,16 +51,8 @@ RUN pip install -r requirements.txt
|
|||||||
# Código do app
|
# Código do app
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Gate E — usuário non-root. uid/gid 10001 batem com o securityContext do
|
# data dirs
|
||||||
# k8s/hml/deployment.yaml (runAsUser/fsGroup). HOME=/home/app pro cache do
|
RUN mkdir -p /app/chroma_db /app/data_storage
|
||||||
# ChromaDB (~/.cache/chroma) cair num diretório gravável pelo usuário.
|
|
||||||
ENV HOME=/home/app
|
|
||||||
RUN groupadd -g 10001 app \
|
|
||||||
&& useradd -u 10001 -g 10001 -m -d /home/app -s /usr/sbin/nologin app \
|
|
||||||
&& mkdir -p /app/chroma_db /app/data_storage /home/app/.cache/chroma \
|
|
||||||
&& chown -R 10001:10001 /app /home/app
|
|
||||||
|
|
||||||
USER 10001
|
|
||||||
|
|
||||||
EXPOSE 8765
|
EXPOSE 8765
|
||||||
|
|
||||||
|
|||||||
2
agent.py
2
agent.py
@ -154,7 +154,7 @@ def build_agent(
|
|||||||
temperature = float(os.environ.get("OPENAI_TEMPERATURE", "1.0"))
|
temperature = float(os.environ.get("OPENAI_TEMPERATURE", "1.0"))
|
||||||
|
|
||||||
welcome_message = (
|
welcome_message = (
|
||||||
"**👋 Olá! Aqui é a ClubPetro IA**\n\n"
|
"#### 👋 Olá! Aqui é a ClubPetro IA\n\n"
|
||||||
"Sua assistente de inteligência de dados. Eu transformo dados complexos em "
|
"Sua assistente de inteligência de dados. Eu transformo dados complexos em "
|
||||||
"respostas claras, direto ao ponto. Precisa de um relatório de faturamento, "
|
"respostas claras, direto ao ponto. Precisa de um relatório de faturamento, "
|
||||||
"entender a performance da sua equipe ou aprofundar no comportamento de "
|
"entender a performance da sua equipe ou aprofundar no comportamento de "
|
||||||
|
|||||||
@ -16,28 +16,14 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: vanna-clubpetro
|
app: vanna-clubpetro
|
||||||
spec:
|
spec:
|
||||||
# A imagem roda como non-root (uid/gid 10001). fsGroup dá posse dos
|
|
||||||
# volumes (PVC) ao grupo 10001 pra que o processo consiga escrever no
|
|
||||||
# store/cache — sem isso o ChromaDB/CSV falha com Permission denied.
|
|
||||||
securityContext:
|
|
||||||
runAsNonRoot: true
|
|
||||||
runAsUser: 10001
|
|
||||||
runAsGroup: 10001
|
|
||||||
fsGroup: 10001
|
|
||||||
containers:
|
containers:
|
||||||
- name: vanna-clubpetro
|
- name: vanna-clubpetro
|
||||||
image: us-central1-docker.pkg.dev/corepetro/clubpetro-lab/vanna-clubpetro:lab-latest
|
image: us-central1-docker.pkg.dev/corepetro/clubpetro-lab/vanna-clubpetro:lab-latest
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
securityContext:
|
|
||||||
allowPrivilegeEscalation: false
|
|
||||||
capabilities:
|
|
||||||
drop: ["ALL"]
|
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
containerPort: 8765
|
containerPort: 8765
|
||||||
env:
|
env:
|
||||||
- name: HOME
|
|
||||||
value: /home/app
|
|
||||||
- name: VANNA_CORS_ORIGINS
|
- name: VANNA_CORS_ORIGINS
|
||||||
value: "https://lab.clubpetro.com,https://homologation.clubpetro.com"
|
value: "https://lab.clubpetro.com,https://homologation.clubpetro.com"
|
||||||
envFrom:
|
envFrom:
|
||||||
@ -71,7 +57,7 @@ spec:
|
|||||||
mountPath: /app/data_storage
|
mountPath: /app/data_storage
|
||||||
subPath: data_storage
|
subPath: data_storage
|
||||||
- name: data
|
- name: data
|
||||||
mountPath: /home/app/.cache/chroma
|
mountPath: /root/.cache/chroma
|
||||||
subPath: chroma-onnx-cache
|
subPath: chroma-onnx-cache
|
||||||
volumes:
|
volumes:
|
||||||
- name: data
|
- name: data
|
||||||
@ -1,85 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: vanna-clubpetro-deployment
|
|
||||||
labels:
|
|
||||||
app: vanna-clubpetro
|
|
||||||
spec:
|
|
||||||
replicas: 1 # ChromaDB SQLite-based — múltiplas réplicas corrompem o vector store
|
|
||||||
strategy:
|
|
||||||
type: Recreate # com PVC ReadWriteOnce não dá pra ter 2 pods montando ao mesmo tempo
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: vanna-clubpetro
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: vanna-clubpetro
|
|
||||||
spec:
|
|
||||||
# Gate E: pod roda como non-root. fsGroup dá posse dos volumes (PVC) ao
|
|
||||||
# grupo 10001 pra que o processo non-root consiga escrever no store/cache.
|
|
||||||
securityContext:
|
|
||||||
runAsNonRoot: true
|
|
||||||
runAsUser: 10001
|
|
||||||
runAsGroup: 10001
|
|
||||||
fsGroup: 10001
|
|
||||||
containers:
|
|
||||||
- name: vanna-clubpetro
|
|
||||||
# A tag é sobrescrita pelo pipeline (kubectl set image). Placeholder:
|
|
||||||
image: us-central1-docker.pkg.dev/corepetro/clubpetro/vanna-clubpetro:hml-latest
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
securityContext:
|
|
||||||
allowPrivilegeEscalation: false
|
|
||||||
capabilities:
|
|
||||||
drop: ["ALL"]
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
containerPort: 8765
|
|
||||||
env:
|
|
||||||
- name: HOME
|
|
||||||
value: /home/app
|
|
||||||
- name: VANNA_CORS_ORIGINS
|
|
||||||
value: "https://homologation.clubpetro.com"
|
|
||||||
envFrom:
|
|
||||||
# Gate G: credenciais do ClickHouse num secret DEDICADO ao serviço.
|
|
||||||
- secretRef:
|
|
||||||
name: vanna-clubpetro-db
|
|
||||||
# Config de app (OpenAI, RLS defaults etc.) em secret separado.
|
|
||||||
- secretRef:
|
|
||||||
name: vanna-clubpetro-secret
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: "200m"
|
|
||||||
memory: "1Gi"
|
|
||||||
limits:
|
|
||||||
cpu: "1"
|
|
||||||
memory: "2Gi"
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /health
|
|
||||||
port: 8765
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 10
|
|
||||||
timeoutSeconds: 5
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /health
|
|
||||||
port: 8765
|
|
||||||
initialDelaySeconds: 60
|
|
||||||
periodSeconds: 30
|
|
||||||
timeoutSeconds: 5
|
|
||||||
failureThreshold: 3
|
|
||||||
volumeMounts:
|
|
||||||
- name: data
|
|
||||||
mountPath: /app/chroma_db
|
|
||||||
subPath: chroma_db
|
|
||||||
- name: data
|
|
||||||
mountPath: /app/data_storage
|
|
||||||
subPath: data_storage
|
|
||||||
- name: data
|
|
||||||
mountPath: /home/app/.cache/chroma
|
|
||||||
subPath: chroma-onnx-cache
|
|
||||||
volumes:
|
|
||||||
- name: data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: vanna-clubpetro-data
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: vanna-clubpetro
|
|
||||||
labels:
|
|
||||||
app: vanna-clubpetro
|
|
||||||
annotations:
|
|
||||||
kubernetes.io/ingress.class: nginx
|
|
||||||
nginx.ingress.kubernetes.io/use-regex: "true"
|
|
||||||
nginx.ingress.kubernetes.io/rewrite-target: /$2
|
|
||||||
# SSE/WebSocket precisam de timeout grande pra streaming não ser cortado
|
|
||||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
|
||||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
|
||||||
nginx.ingress.kubernetes.io/proxy-buffering: "off"
|
|
||||||
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
|
|
||||||
spec:
|
|
||||||
ingressClassName: nginx
|
|
||||||
rules:
|
|
||||||
- host: homologation.clubpetro.com
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- path: /api/vanna(/|$)(.*)
|
|
||||||
pathType: Prefix
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: vanna-clubpetro
|
|
||||||
port:
|
|
||||||
number: 80
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- homologation.clubpetro.com
|
|
||||||
secretName: homologation-tls
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
# Exemplo dos Secrets do homolog. NÃO versionar os valores reais — este arquivo
|
|
||||||
# é só documentação. Crie os secrets no cluster via kubectl (fora do git):
|
|
||||||
#
|
|
||||||
# # Gate G — credenciais de banco em secret DEDICADO ao serviço:
|
|
||||||
# kubectl -n <ns> create secret generic vanna-clubpetro-db \
|
|
||||||
# --from-literal=CLICKHOUSE_HOST=... \
|
|
||||||
# --from-literal=CLICKHOUSE_PORT=8443 \
|
|
||||||
# --from-literal=CLICKHOUSE_DATABASE=gold \
|
|
||||||
# --from-literal=CLICKHOUSE_USER=wren_ia \
|
|
||||||
# --from-literal=CLICKHOUSE_PASSWORD=... \
|
|
||||||
# --from-literal=CLICKHOUSE_SECURE=true
|
|
||||||
#
|
|
||||||
# # Config de app (não-banco): OpenAI + RLS defaults:
|
|
||||||
# kubectl -n <ns> create secret generic vanna-clubpetro-secret \
|
|
||||||
# --from-literal=OPENAI_API_KEY=... \
|
|
||||||
# --from-literal=OPENAI_MODEL=gpt-5 \
|
|
||||||
# --from-literal=OPENAI_TEMPERATURE=1.0 \
|
|
||||||
# --from-literal=RLS_PROGRAM_ID=... \
|
|
||||||
# --from-literal=RLS_STORE_ID=... \
|
|
||||||
# --from-literal=RLS_USER_ID=...
|
|
||||||
#
|
|
||||||
# As chaves acima são as mesmas do .env.example. O Deployment injeta ambos
|
|
||||||
# via envFrom.secretRef. Nunca colocar valor real em arquivo versionado.
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Secret
|
|
||||||
metadata:
|
|
||||||
name: vanna-clubpetro-db
|
|
||||||
labels:
|
|
||||||
app: vanna-clubpetro
|
|
||||||
type: Opaque
|
|
||||||
stringData:
|
|
||||||
CLICKHOUSE_HOST: ""
|
|
||||||
CLICKHOUSE_PORT: "8443"
|
|
||||||
CLICKHOUSE_DATABASE: "gold"
|
|
||||||
CLICKHOUSE_USER: "wren_ia"
|
|
||||||
CLICKHOUSE_PASSWORD: ""
|
|
||||||
CLICKHOUSE_SECURE: "true"
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Secret
|
|
||||||
metadata:
|
|
||||||
name: vanna-clubpetro-secret
|
|
||||||
labels:
|
|
||||||
app: vanna-clubpetro
|
|
||||||
type: Opaque
|
|
||||||
stringData:
|
|
||||||
OPENAI_API_KEY: ""
|
|
||||||
OPENAI_MODEL: "gpt-5"
|
|
||||||
OPENAI_TEMPERATURE: "1.0"
|
|
||||||
RLS_PROGRAM_ID: ""
|
|
||||||
RLS_STORE_ID: ""
|
|
||||||
RLS_USER_ID: ""
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: vanna-clubpetro-data
|
|
||||||
labels:
|
|
||||||
app: vanna-clubpetro
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 5Gi
|
|
||||||
storageClassName: standard-rwo
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: vanna-clubpetro
|
|
||||||
labels:
|
|
||||||
app: vanna-clubpetro
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
selector:
|
|
||||||
app: vanna-clubpetro
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 80
|
|
||||||
targetPort: 8765
|
|
||||||
protocol: TCP
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
# promotion-manifest.yaml
|
|
||||||
# Manifesto de promoção lab -> homolog (ClubPetro).
|
|
||||||
# A PR de promoção deve ser marcada com [lab] e conter este arquivo.
|
|
||||||
# O pipeline lê os campos abaixo pra rodar os gates 0 + A-F + G + admissao.
|
|
||||||
#
|
|
||||||
# OBS: o schema exato é definido pelo parser do pipeline (referências vivas:
|
|
||||||
# backend=external_apis, seed=action-plan). As chaves aqui são auto-descritivas;
|
|
||||||
# reconcilie com o parser se ele exigir nomes específicos.
|
|
||||||
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PromotionManifest
|
|
||||||
|
|
||||||
service:
|
|
||||||
name: vanna-clubpetro
|
|
||||||
kind: backend # serviço HTTP (não é MFE)
|
|
||||||
description: >
|
|
||||||
Deploy do Vanna 2.0 (text-to-SQL sobre LLM) que responde perguntas em
|
|
||||||
pt-BR consultando o ClickHouse Cloud (database gold) com RLS por tenant
|
|
||||||
(program_id + store_id).
|
|
||||||
owner: dados-plataforma # TODO: confirmar squad/owner
|
|
||||||
runtimePort: 8765
|
|
||||||
|
|
||||||
promotion:
|
|
||||||
from: lab
|
|
||||||
to: homolog
|
|
||||||
# Onde o pipeline re-materializa o código (branch limpa, sem histórico do lab).
|
|
||||||
kubernetesOverlay: k8s/hml
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Gate 0 — Stack
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
stack:
|
|
||||||
language: python
|
|
||||||
runtime: python-3.11
|
|
||||||
framework: vanna-2.0 + fastapi + uvicorn
|
|
||||||
packageManager: pip
|
|
||||||
# Stack padrão do core é NestJS>=10/TypeORM0.3 (yarn) OU MFE React17/MUI5.
|
|
||||||
# Python é NO-GO por padrão -> exceção formal abaixo (Gate 0).
|
|
||||||
stackException:
|
|
||||||
requested: true
|
|
||||||
reason: >
|
|
||||||
O serviço é um deploy da biblioteca Vanna 2.0, que é Python e não tem
|
|
||||||
equivalente em NestJS. O núcleo (agente LLM, memória vetorial ChromaDB,
|
|
||||||
runner RLS do ClickHouse e o web component <vanna-chat>) vem do upstream
|
|
||||||
vanna-ai/vanna. Reescrever em Node significaria reimplementar toda essa
|
|
||||||
cadeia — inviável e sem ganho. Mantém-se Python; todos os demais gates
|
|
||||||
aplicáveis são cumpridos (ver abaixo).
|
|
||||||
approvedBy: "" # TODO: preencher com o aprovador humano do stackException
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Gate B — Migrations
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
migrations:
|
|
||||||
applicable: false
|
|
||||||
reason: >
|
|
||||||
O serviço não possui schema relacional próprio. O ClickHouse Cloud
|
|
||||||
(database gold) é gerido fora do serviço; o "treino" (train.py) apenas
|
|
||||||
popula um vector store ChromaDB local, idempotente e reconstruível
|
|
||||||
(rm -rf chroma_db/ && python train.py). Não há migrations TypeORM.
|
|
||||||
Toda query ao ClickHouse é parametrizada via settings do clickhouse_connect
|
|
||||||
(RLS), nunca por interpolação de string — ver rls_runner.py.
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Gate D — Atalhos / segurança
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
security:
|
|
||||||
envVersioned: false # apenas .env.example versionado; .env no .gitignore
|
|
||||||
hardcodedSecrets: false # segredos vêm de k8s Secret (envFrom), nunca do git
|
|
||||||
hardcodedLabHost: false # overlay homolog usa homologation.clubpetro.com
|
|
||||||
npmrcVersioned: false # sem .npmrc no repo
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Gate G — Isolamento de credenciais de banco
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
database:
|
|
||||||
# Credenciais do ClickHouse em secret DEDICADO ao serviço (não genérico).
|
|
||||||
credentialsSecret: vanna-clubpetro-db
|
|
||||||
genericSecret: false # NÃO usa sqlhomologation / sqluserhomolgeneric etc.
|
|
||||||
engine: clickhouse-cloud
|
|
||||||
database: gold
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Admissão de capacidade
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
capacity:
|
|
||||||
replicas: 1 # ChromaDB é SQLite-based; réplicas > 1 corrompem o store
|
|
||||||
strategy: Recreate # PVC ReadWriteOnce — não dá 2 pods montando ao mesmo tempo
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: "200m"
|
|
||||||
memory: "1Gi"
|
|
||||||
limits:
|
|
||||||
cpu: "1"
|
|
||||||
memory: "2Gi"
|
|
||||||
persistentVolume:
|
|
||||||
size: 5Gi
|
|
||||||
accessMode: ReadWriteOnce # store vetorial + cache ONNX + CSVs
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Deltas (avisos — não bloqueiam)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
deltas:
|
|
||||||
A_architecture: >
|
|
||||||
Tenant (program_id/store_id) resolvido via RequestContext e validado por
|
|
||||||
regex ^[A-Za-z0-9_-]+$ (agent.py). O embed confiável passa os IDs por query
|
|
||||||
param do <vanna-chat> — não há token JWT nesta arquitetura de embed.
|
|
||||||
C_contract: >
|
|
||||||
OpenAPI auto-servido pelo FastAPI (/openapi.json, /docs). Rotas de chat já
|
|
||||||
versionadas: /api/vanna/v2/chat_sse|chat_websocket|chat_poll (upstream).
|
|
||||||
E_ops: >
|
|
||||||
Probes de liveness/readiness em GET /health; replicas: 1; Dockerfile
|
|
||||||
multi-stage rodando como usuário non-root (uid 10001).
|
|
||||||
F_ci: >
|
|
||||||
CI/CD via .gitea/workflows/cd.yml (build remoto no Cloud Build + rollout
|
|
||||||
no GKE). bitbucket-pipelines.yml N/A: o repo vive no Gitea self-hosted.
|
|
||||||
@ -42,12 +42,6 @@ def _build_app():
|
|||||||
if os.path.isdir(dist):
|
if os.path.isdir(dist):
|
||||||
fastapi_app.mount("/static", StaticFiles(directory=dist), name="static")
|
fastapi_app.mount("/static", StaticFiles(directory=dist), name="static")
|
||||||
|
|
||||||
@fastapi_app.get("/health")
|
|
||||||
async def health():
|
|
||||||
# Alvo das probes de liveness/readiness (Gate E). Leve, sem tocar
|
|
||||||
# ClickHouse/LLM — só sinaliza que o processo subiu e serve HTTP.
|
|
||||||
return {"status": "ok"}
|
|
||||||
|
|
||||||
@fastapi_app.get("/vanna-theme.css")
|
@fastapi_app.get("/vanna-theme.css")
|
||||||
async def vanna_theme():
|
async def vanna_theme():
|
||||||
path = os.path.join(here, "static", "vanna-theme.css")
|
path = os.path.join(here, "static", "vanna-theme.css")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user