Compare commits
No commits in common. "main" and "fix/cloud-build" have entirely different histories.
main
...
fix/cloud-
@ -11,9 +11,8 @@ CLICKHOUSE_USER=wren_ia
|
||||
CLICKHOUSE_PASSWORD=
|
||||
CLICKHOUSE_SECURE=true
|
||||
|
||||
# CORS: origens permitidas por ambiente, separadas por vírgula.
|
||||
# Definir por ambiente (no k8s vem do secret/env do overlay — nunca hardcode de host aqui).
|
||||
VANNA_CORS_ORIGINS=
|
||||
# CORS (separar por vírgula)
|
||||
VANNA_CORS_ORIGINS=https://lab.clubpetro.com,https://homologation.clubpetro.com
|
||||
|
||||
# RLS defaults (apenas pra `python ask.py` na CLI; servidor web extrai de query string)
|
||||
RLS_PROGRAM_ID=
|
||||
|
||||
@ -38,40 +38,8 @@ jobs:
|
||||
|
||||
- name: Build & push (Cloud Build, sem Docker local)
|
||||
run: |
|
||||
set -e
|
||||
IMG="${IMAGE_BASE}/${{ gitea.event.repository.name }}:lab-${{ gitea.run_number }}"
|
||||
# --gcs-source-staging-dir: pula o auto-detect que precisa de
|
||||
# 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
|
||||
gcloud builds submit --tag "$IMG" --project=${{ secrets.GCP_PROJECT }} --timeout=30m
|
||||
echo "IMG=$IMG" >> $GITHUB_ENV
|
||||
|
||||
- name: Deploy hml2 (apenas em push pra master/main)
|
||||
@ -82,10 +50,9 @@ jobs:
|
||||
gcloud container clusters get-credentials ${{ secrets.GKE_CLUSTER }} --region ${{ secrets.GKE_REGION }} --project ${{ secrets.GCP_PROJECT }}
|
||||
NS=${{ secrets.K8S_NAMESPACE }}
|
||||
|
||||
# 1) Aplica manifests do LAB (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/lab ]; then
|
||||
kubectl apply -n "$NS" -f k8s/lab/
|
||||
# 1) Aplica manifests (idempotente — cria PVC/Service/Ingress/Deployment se faltarem)
|
||||
if [ -d k8s ]; then
|
||||
kubectl apply -n "$NS" -f k8s/
|
||||
fi
|
||||
|
||||
# 2) Atualiza image
|
||||
|
||||
14
Dockerfile
14
Dockerfile
@ -7,7 +7,7 @@ ARG VANNA_UPSTREAM_COMMIT=365d0617c1a4567ffee1b19b40c27feb4206bfcf
|
||||
# ============================================================================
|
||||
# Stage 1 — webcomponent (Node)
|
||||
# ============================================================================
|
||||
FROM node:20-bookworm-slim AS webcomponent
|
||||
FROM node:18-bookworm-slim AS webcomponent
|
||||
ARG VANNA_UPSTREAM_COMMIT
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@ -51,16 +51,8 @@ RUN pip install -r requirements.txt
|
||||
# Código do app
|
||||
COPY . .
|
||||
|
||||
# Gate E — usuário non-root. uid/gid 10001 batem com o securityContext do
|
||||
# k8s/hml/deployment.yaml (runAsUser/fsGroup). HOME=/home/app pro cache do
|
||||
# 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
|
||||
# data dirs
|
||||
RUN mkdir -p /app/chroma_db /app/data_storage
|
||||
|
||||
EXPOSE 8765
|
||||
|
||||
|
||||
2
agent.py
2
agent.py
@ -154,7 +154,7 @@ def build_agent(
|
||||
temperature = float(os.environ.get("OPENAI_TEMPERATURE", "1.0"))
|
||||
|
||||
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 "
|
||||
"respostas claras, direto ao ponto. Precisa de um relatório de faturamento, "
|
||||
"entender a performance da sua equipe ou aprofundar no comportamento de "
|
||||
|
||||
@ -16,28 +16,14 @@ spec:
|
||||
labels:
|
||||
app: vanna-clubpetro
|
||||
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:
|
||||
- name: vanna-clubpetro
|
||||
image: us-central1-docker.pkg.dev/corepetro/clubpetro-lab/vanna-clubpetro:lab-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://lab.clubpetro.com,https://homologation.clubpetro.com"
|
||||
envFrom:
|
||||
@ -71,7 +57,7 @@ spec:
|
||||
mountPath: /app/data_storage
|
||||
subPath: data_storage
|
||||
- name: data
|
||||
mountPath: /home/app/.cache/chroma
|
||||
mountPath: /root/.cache/chroma
|
||||
subPath: chroma-onnx-cache
|
||||
volumes:
|
||||
- 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):
|
||||
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")
|
||||
async def vanna_theme():
|
||||
path = os.path.join(here, "static", "vanna-theme.css")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user