Compare commits

..

No commits in common. "main" and "fix/install-docker-kubectl" have entirely different histories.

15 changed files with 28 additions and 399 deletions

View File

@ -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=

View File

@ -14,17 +14,16 @@ jobs:
steps:
- 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
# Imagem default do runner (node:18-bullseye) não traz docker CLI nem kubectl —
# instalamos sob demanda. Eventualmente isso vai pra imagem custom do runner.
- name: Install docker CLI + kubectl
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
apt-get install -y -qq docker.io
KVER=$(curl -sL https://dl.k8s.io/release/stable.txt)
curl -sL "https://dl.k8s.io/release/${KVER}/bin/linux/amd64/kubectl" -o /usr/local/bin/kubectl
chmod +x /usr/local/bin/kubectl
docker --version && kubectl version --client=true
- name: Auth GCP
uses: google-github-actions/auth@v2
@ -36,56 +35,28 @@ jobs:
with:
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: |
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
docker build --platform=linux/amd64 -t "$IMG" .
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)
if: github.event_name == 'push'
env:
USE_GKE_GCLOUD_AUTH_PLUGIN: "True"
run: |
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
@ -93,7 +64,7 @@ jobs:
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}')
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
echo "Deployment $DEPLOYMENT não existe no ns $NS — pulei set image (provavelmente é o 1º deploy e o kubectl apply acabou de criar)"
fi

View File

@ -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

View File

@ -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 "

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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: ""

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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")