Files
athletic-map-deploy/tenants/plataforma/82-servico-captacao.yaml
T
deploy bb5e650d7a fix(plataforma): 8 tentativas por etapa, e nao 5
A etapa BANCO_CRIADO reinicia o PgBouncer e espera ele voltar. Com 5, o backoff
da 155s — menos do que o pod novo levou nas tres medicoes de hoje, e os dois
clientes pagos precisaram de retomada a mao. Com 8 da 515s, dentro da janela.
2026-09-24 22:57:38 +00:00

298 lines
16 KiB
YAML

# servico-captacao — recebe o formulario da landing page.
#
# SO EXISTE NESTE TENANT, e e por isso que o tenant `plataforma` precisa
# existir: lead e prospecto DA ATHLETIC MAP, e gravar isso no banco de um
# cliente seria o oposto do que a Fase 2 inteira fez. Ver P2.8.
#
# E o unico servico do repositorio com rota PUBLICA (sem token): quem
# preenche o formulario ainda nao e cliente. O que substitui a autenticacao
# esta no proprio servico — CORS restrito, rate limit por IP e consent
# obrigatorio.
# Bounded context "financeiro" (2a onda): Cobranca + ApuracaoRateio (centros de custo).
# Schema proprio "financeiro" (Flyway). Roteado em /api/public/leads no ingress.
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: servico-captacao
namespace: plataforma-prod
labels:
app: servico-captacao
athleticmap.io/contexto: financeiro
spec:
replicas: 1
selector: { matchLabels: { app: servico-captacao } }
template:
metadata:
labels:
app: servico-captacao
athleticmap.io/contexto: financeiro
spec:
# P5.6/P5.9: ServiceAccount DEDICADA (ver 16-rbac-provisionamento.yaml).
# Com a `default`, a permissao de ler e escalar Deployment dos tenants
# valeria para qualquer pod que subisse sem `serviceAccountName`.
serviceAccountName: atm-provisionamento
volumes:
- { name: gitops, emptyDir: {} }
# P5.3 — a copia de trabalho do GitOps, refeita a cada subida do pod.
#
# CLONE RASO E ANONIMO. O Gitea aceita leitura sem credencial neste
# repositorio (conferido em 2026-09-22), e leitura e tudo o que a etapa 1
# precisa: ela RENDERIZA o molde para `tenants/<nome>`. Empurrar e a etapa
# ARGO_SINCRONIZADO, que depende de credencial e continua bloqueada.
#
# `emptyDir`, e nao PVC: a copia e descartavel por desenho. Se o pod morrer
# no meio de um provisionamento, o que ficou pela metade some junto — e isso
# e melhor do que um clone velho e divergente sobrevivendo entre reinicios.
#
# A imagem e a do `act_runner`, que JA esta no no e tem git 2.45.2. Nao ha
# registry aqui (`imagePullPolicy: Never`), entao construir uma imagem so
# para ter git custaria mais do que reusar esta.
#
# O endereco e o INTERNO, o mesmo que o ApplicationSet do ArgoCD usa — o
# externo passa por Ingress e TLS, e nao ha motivo para sair do cluster
# para buscar o que esta dentro dele. Depende do `19-netpol-gitea.yaml`.
initContainers:
- name: clonar-gitops
image: docker.io/gitea/act_runner:0.2.11
imagePullPolicy: Never
command: ["/bin/sh", "-c"]
args:
- |
set -e
git clone --depth 1 http://gitea-http.gitea.svc:3000/atmadmin/athletic-map-deploy.git /gitops
test -d /gitops/moldes/consolidado || { echo "molde ausente no clone"; exit 1; }
# O container principal roda como o usuario app (uid 999), e o clone
# e feito como root. Sem esta linha a copia de trabalho fica
# so-leitura para ele, e a etapa MANIFESTOS_GERADOS falha ao escrever
# tenants/<nome> — medido em 2026-09-22.
#
# chown, e nao fsGroup: fsGroup ajusta o GRUPO e depende de o
# diretorio ter permissao de grupo, o que o git nao garante arquivo a
# arquivo. Entregar a posse e explicito e nao depende de umask.
chown -R 999:999 /gitops
echo "clone pronto: $(ls /gitops/moldes/consolidado | wc -l) arquivo(s) de molde"
volumeMounts:
- { name: gitops, mountPath: /gitops }
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 200m, memory: 128Mi }
containers:
- name: servico-captacao
image: docker.io/library/servico-captacao:2.9-corrigir-papel
imagePullPolicy: Never
ports: [{ containerPort: 8083 }]
volumeMounts:
- { name: gitops, mountPath: /gitops }
env:
- { name: SPRING_DATASOURCE_URL, value: "jdbc:postgresql://postgres:5432/athleticmap?sslmode=require" }
- { name: SPRING_DATASOURCE_USERNAME, value: "atm" }
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef: { name: db-credentials, key: password }
- { name: ATM_TENANT, value: "plataforma" }
# CORS: so a landing page. Formulario publico com CORS aberto
# vira endpoint de spam de qualquer site. Ver P2.8.
# A origem do navegador que faz o POST. A landing page e servida
# pelo proprio nginx da SPA (`frontend/nginx.conf`, `location =
# /lp-escolinhas`), que neste tenant e o Service `frontend-spa` —
# logo a origem e o host do tenant, e nao o apex de marketing.
# `@CrossOrigin` aqui aceita UMA origem: o placeholder vira string
# literal, entao virgula nao separa em duas. Se a LP for publicada
# noutro host, este valor muda junto ou o formulario toma CORS.
- { name: CAPTACAO_ORIGEM, value: "https://plataforma.athleticmap.com" }
- { name: CAPTACAO_RETENCAO_MESES, value: "24" }
# P5.1 — o token do webhook de ASSINATURA. Sem ele o endpoint
# RECUSA TUDO, de proposito, e o log diz exatamente isso. O mesmo
# valor precisa estar cadastrado no painel do Asaas, na conta da
# Athletic Map (nao na do cliente).
- name: PLATAFORMA_WEBHOOK_TOKEN
valueFrom:
secretKeyRef: { name: plataforma-webhook-token, key: token }
# P5.2 — o token de consulta do estado de um provisionamento. E
# segredo NOSSO, diferente do de cima: o Asaas nao tem o que ler
# aqui dentro. Interino, no cabecalho `x-atm-admin-token`; o lugar
# definitivo e uma tela com papel do Keycloak.
- name: PLATAFORMA_ADMIN_TOKEN
valueFrom:
secretKeyRef: { name: plataforma-admin-token, key: token }
# P5.3 — de onde vem o certificado PUBLICO do sealed-secrets.
#
# A URL, e nao um arquivo: o controlador ROTACIONA a chave a cada
# 30 dias (medido: 16/06, 16/07, 15/08, 14/09), e um certificado
# versionado nasce certo e fica velho em um mes, EM SILENCIO —
# selar com o antigo continua funcionando enquanto ele guardar as
# privadas anteriores. Lido a cada provisionamento, vem sempre o
# corrente.
#
# Depende do `18-netpol-sealed-secrets.yaml`: sem ele a conexao
# nao sai (o ipBlock da deny-cross-tenant nao vale depois do DNAT).
- { name: PROVISIONAMENTO_CERT_SELAGEM, value: "http://sealed-secrets-controller.kube-system:8080/v1/cert.pem" }
# P5.3 — a copia de trabalho do GitOps e o molde, no MESMO volume.
#
# O initContainer clona o repositorio em /gitops na subida; dentro
# dele, `moldes/consolidado` e o molde e `tenants/<nome>` e onde a
# etapa escreve. Um volume serve aos dois.
#
# Vazio falhava FECHADO, nomeando a propriedade — era o estado ate
# 2026-09-22. Escrever num diretorio temporario que ninguem commita
# daria a impressao de que a etapa funcionou.
- { name: PROVISIONAMENTO_GITOPS, value: "/gitops" }
# P5.3 — o repositorio remoto para onde o commit e EMPURRADO.
#
# O nome DE DENTRO do cluster, e nao o `git.<ip>.nip.io` que se usa
# de fora: o de fora sai do cluster e volta, e a NetworkPolicy de
# egress (19-netpol-gitea) permite o caminho interno, nao o externo.
#
# `gitea-http` e headless (ClusterIP None), entao o DNS devolve o IP
# do POD. Isso e o que faz a netpol por `podSelector` funcionar aqui
# — `ipBlock` com ClusterIP nao funciona atravessando namespace,
# porque o k3s avalia a politica DEPOIS do DNAT.
- { name: PROVISIONAMENTO_GITOPS_URL,
value: "http://gitea-http.gitea.svc:3000/atmadmin/athletic-map-deploy.git" }
# P5.3 — quem empurra. ESTA CREDENCIAL IMPLANTA EM PRODUCAO: o
# ApplicationSet tem gerador de diretorio sobre `tenants/*` com
# selfHeal, entao a pasta empurrada vira Application e e aplicada
# sozinha. Por isso e um usuario proprio do Gitea, com escrita so
# neste repositorio, e um TOKEN revogavel — nao a conta atmadmin.
- name: PROVISIONAMENTO_GITOPS_USUARIO
valueFrom:
secretKeyRef: { name: provisionamento-credentials, key: gitops-usuario }
- name: PROVISIONAMENTO_GITOPS_SENHA
valueFrom:
secretKeyRef: { name: provisionamento-credentials, key: gitops-senha }
# P5.4 — a Admin API do Keycloak, para criar o realm do tenant novo.
#
# `AdminDoKeycloak` pega token por `grant_type=password` no client
# `admin-cli` do realm `master`, entao isto e um usuario DE LA, e nao
# do realm `athleticmap`.
#
# Hoje pode ser o admin do master, que pode tudo. O certo e um
# usuario de servico com `create-realm` e nada mais — esta no diario.
- name: KEYCLOAK_ADMIN_USUARIO
valueFrom:
secretKeyRef: { name: provisionamento-credentials, key: keycloak-admin-usuario }
- name: KEYCLOAK_ADMIN_SENHA
valueFrom:
secretKeyRef: { name: provisionamento-credentials, key: keycloak-admin-senha }
- { name: PROVISIONAMENTO_MOLDE, value: "/gitops/moldes/consolidado" }
- { name: PROVISIONAMENTO_PLANO_PADRAO, value: "bronze" }
# ── PRECO DE TESTE, TEMPORARIO ─────────────────────────
#
# A venda de teste de 2026-09-24 exercita o caminho inteiro —
# link, pagamento, webhook, provisionamento — e nao faz sentido
# gastar R$ 170 para isso. Sobreposicao por ambiente, e nao
# mudanca no `application.yml`: assim nao ha nada a reverter no
# codigo, e tirar e remover este bloco.
#
# ENQUANTO ISTO ESTIVER AQUI, o plano bronze custa este valor.
# Nao venda bronze de verdade sem tirar.
- { name: PLANO_BRONZE_MENSAL, value: "5.00" }
# ── fim do preco de teste ──────────────────────────────
# P5.8 — a chave do Asaas, para CRIAR o link de assinatura.
#
# E o mesmo Secret que o `servico-financeiro` usa, e por isso
# nao ha credencial nova a selar: muda so quem a le. Os dois
# fluxos continuam distintos — o financeiro cobra o ATLETA, este
# cobra o TENANT.
#
# `optional: true` de proposito: um ambiente sem Asaas sobe do
# mesmo jeito, e a venda que precisar da chave recusa nomeando a
# propriedade. Derrubar o pod tiraria do ar o provisionamento e a
# captura de lead, que nao dependem disso.
- name: ASAAS_API_KEY
valueFrom:
secretKeyRef:
name: asaas-credentials
key: api-key
optional: true
- { name: CAPTACAO_LIMITE_POR_IP, value: "5" }
# Destino do aviso ao comercial. Vazio = lead e gravado e o
# aviso vira log sem PII. Preencher quando houver SMTP.
- { name: CAPTACAO_EMAIL_COMERCIAL, value: "" }
# ── SMTP ─────────────────────────────────
#
# HOSTINGER, e nao Google. O dominio `athleticmap.com` tem MX em
# mx1/mx2.hostinger.com e SPF `include:_spf.mail.hostinger.com`:
# mandar pelo Gmail falharia SPF justamente no e-mail que leva o
# link de senha de quem acabou de pagar. Medido em 2026-09-24 — a
# credencial e ACEITA em smtp.hostinger.com:587 e recusada (535)
# em smtp.gmail.com e em smtp.titan.email.
- { name: SMTP_HOST, value: "smtp.hostinger.com" }
- { name: SMTP_PORTA, value: "587" }
- { name: SMTP_USUARIO, value: "nao-responda@athleticmap.com" }
# `nao-responda@` de proposito: a senha dela vive no cluster, e uma
# caixa que nao guarda nada limita o estrago se o segredo vazar. Sem
# o RESPONDER_PARA, quem acabou de pagar e responde fala com o vazio.
- { name: SMTP_RESPONDER_PARA, value: "contato@athleticmap.com" }
- { name: SMTP_STARTTLS, value: "true" }
- { name: SMTP_AUTENTICAR, value: "true" }
# Vazia, o realm nasceria com um SMTP que so falha na hora de ENVIAR
# — depois de o cliente ter conta. A etapa recusa antes disso.
- name: SMTP_SENHA
valueFrom:
secretKeyRef: { name: smtp-credentials, key: senha }
# O molde do realm. Sem ele a etapa recusa nomeando a propriedade; o
# arquivo vive no mesmo clone do GitOps que o /gitops monta.
- { name: PROVISIONAMENTO_REALM_TEMPLATE, value: "/gitops/moldes/realm-do-tenant.json" }
# ── o banco do cliente novo (etapa BANCO_CRIADO) ──────────────
#
# DIRETO no pg-compartilhado, e nao pelo PgBouncer: o pool so serve
# databases que ja estao na lista dele, e aqui o database ainda NAO
# existe. E a netpol egress-banco-compartilhado ja permite os dois.
# 8, e nao as 5 do padrao.
#
# A etapa BANCO_CRIADO reinicia o PgBouncer e espera ele voltar servindo o
# tenant novo. Com 5 tentativas o backoff da 5+10+20+40+80 = 155s, e nas
# tres medicoes de 2026-09-24 o pod novo demorou mais que isso — os dois
# clientes pagos precisaram de uma retomada a mao CADA.
#
# Com 8 e o teto de 120s: 515s. A janela de 20 minutos da execucao continua
# cabendo, entao isto nao troca uma espera por um incidente silencioso.
- { name: PROVISIONAMENTO_TENTATIVAS, value: "8" }
- { name: PROVISIONAMENTO_PG_ADMIN_URL, value: "jdbc:postgresql://pg-compartilhado:5432/{database}" }
- { name: PROVISIONAMENTO_PG_ADMIN_USUARIO, value: "postgres" }
- name: PROVISIONAMENTO_PG_ADMIN_SENHA
valueFrom:
secretKeyRef: { name: pg-compartilhado-admin, key: password }
# As etapas de migracao e de catalogos, que conectam num banco que JA
# existe. Tambem direto: elas rodam com a conta de administracao, e
# passar pelo pool so acrescentaria uma peca ao caminho.
- { name: PROVISIONAMENTO_BANCO_URL, value: "jdbc:postgresql://pg-compartilhado:5432/athleticmap_{tenant}" }
- { name: PROVISIONAMENTO_BANCO_USUARIO, value: "postgres" }
- name: PROVISIONAMENTO_BANCO_SENHA
valueFrom:
secretKeyRef: { name: pg-compartilhado-admin, key: password }
resources:
requests: { cpu: 150m, memory: 320Mi }
limits: { cpu: "1", memory: 768Mi }
# `/actuator/health/readiness`, e nao um health de dominio.
#
# A primeira versao apontava para `/api/public/leads/health`, copiado
# do manifesto do financeiro por analogia. Esse caminho NAO EXISTE:
# o `LeadController` tem um unico `@PostMapping`, e por desenho — a
# superficie deste servico publico e exatamente um POST. O pod ficou
# 0/1 enquanto a aplicacao dizia "Started" no log, que e o sintoma
# mais confuso possivel.
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8083 }
initialDelaySeconds: 25
periodSeconds: 10
failureThreshold: 30
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 8083 }
initialDelaySeconds: 60
periodSeconds: 20
failureThreshold: 6
---
apiVersion: v1
kind: Service
metadata:
name: servico-captacao
namespace: plataforma-prod
labels:
app: servico-captacao
spec:
selector: { app: servico-captacao }
ports: [{ name: http, port: 80, targetPort: 8083 }]