feat(plataforma): tenant proprio da Athletic Map

Namespace, banco, Keycloak e realm proprios, separados dos clientes e do tenant
de demonstracao. Leva 4 contextos (configuracao, cadastro, financeiro, captacao)
mais bff e SPA, e nao os 13 de um cliente: a Athletic Map nao e escolinha.

Ingress escrito do zero, sem o stub backend legado e sem catch-all /api. Duas
rotas publicas, ambas por desenho: /api/public/leads (o formulario da landing
page) e /api/public/webhooks/asaas (o aviso de pagamento, que chega da internet
sem token nosso).

Dominio em *.athleticmap.influxdigital.com.br, e nao no canonico athleticmap.com
do P0.9: aquele ainda nao tem registro A, e com desafio HTTP-01 nascer nele
daria certificado nao emitido e login quebrado.

Tres SealedSecret gerados para ESTE namespace, e a chave publica do backup no
lugar do placeholder. As 7 imagens locais exigidas estao no no, incluindo
servico-captacao:1.0 e atm-pg-backup:1.0, construidas hoje.

Preparado em P3.1 a P3.4. Validado com kubectl --dry-run: 32 documentos.
This commit is contained in:
Athletic Map Deploy
2026-09-17 17:58:21 +00:00
parent a93c2cb179
commit ea7da4364a
18 changed files with 1170 additions and 0 deletions
@@ -0,0 +1,68 @@
# Tenant `plataforma` da Athletic Map — isolamento do tenant (namespace + quota + limites + rede)
# Modelo: 1 namespace por cliente (ver arquitetura-multitenant-pods-por-cliente.md)
apiVersion: v1
kind: Namespace
metadata:
name: plataforma-prod
labels:
name: plataforma-prod
tenant: plataforma
athleticmap.io/tier: platform
---
# Cota de recursos do tenant — protege o cluster contra consumo excessivo de um cliente
apiVersion: v1
kind: ResourceQuota
metadata:
name: tenant-quota
namespace: plataforma-prod
spec:
hard:
requests.cpu: "2"
requests.memory: 2Gi
limits.cpu: "4"
limits.memory: 6Gi
pods: "20"
persistentvolumeclaims: "4"
---
# Defaults de recursos por container (para a cota ser satisfeita sem declarar em cada pod)
apiVersion: v1
kind: LimitRange
metadata:
name: defaults
namespace: plataforma-prod
spec:
limits:
- type: Container
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 100m
memory: 128Mi
---
# Isolamento de rede: nega tráfego cross-tenant; permite intra-namespace + ingress (Traefik) + egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-cross-tenant
namespace: plataforma-prod
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
ingress:
- from:
- podSelector: {} # mesma namespace
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system # Traefik (ingress)
egress:
- to: # intra-namespace (pods) + ClusterIPs (VIP de service, pre-DNAT)
- podSelector: {}
- ipBlock: { cidr: 10.43.0.0/16 }
- to: # DNS (CoreDNS em kube-system)
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 }
@@ -0,0 +1,36 @@
# ============================================================================
# Credencial do banco do tenant `plataforma`. Gerada em 2026-09-17 (P3.1).
#
# PODE SER COMMITADA. Um SealedSecret e cifrado com a chave publica do
# controlador DESTE cluster e, no escopo estrito (o padrao), so decifra com
# ESTE nome (`db-credentials`) neste namespace (`plataforma-prod`). Renomear o
# recurso ou copia-lo para outro namespace faz o controlador recusar — que e a
# razao de o do `piloto-prod` nao servir aqui.
#
# A SENHA NAO PASSOU POR MAQUINA DE NINGUEM: nasceu, selou e morreu dentro de um
# unico shell no servidor, e so este YAML cifrado voltou. Por isso nao existe
# lugar nenhum onde ela esteja anotada — e nem precisa: quem a le e o pod, a
# partir do Secret que o controlador cria.
#
# Consumida por: postgres (POSTGRES_PASSWORD), keycloak (KC_DB_PASSWORD) e os
# quatro servicos (SPRING_DATASOURCE_PASSWORD).
#
# PARA TROCAR: `scripts/gerar_credenciais_plataforma.sh db` e commitar. O
# Postgres ja existente NAO adota a senha nova sozinho — `POSTGRES_PASSWORD` so
# vale na inicializacao do volume. Depois do primeiro deploy, trocar exige
# tambem um `ALTER ROLE` no banco.
# ============================================================================
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
creationTimestamp: null
name: db-credentials
namespace: plataforma-prod
spec:
encryptedData:
password: AgCQo0CKgSuv6JiTU7GnxufPJhGTwNcNPuENhGy0D5Qk1g0/cYO2Pw2C6gjRc9VfqMOR0s2eJrIQ+x7xn42/jSV7Fh3GgFBaiI8+43nTJSr1IiUZThI2taLfKx5oC/Ai8Tww4Kclx8CvS1hyrxSNBWg6vMjvMYL2bTkLhvtBJzlUr1IJPQ3hSzv8zDfTe5Ybsu55t3mHJ6jWm57NacA3X3nfbitMNTQKmz95bq2gqiuMs2GUuB1XV9YPoMEUhlJCx1HDJLl+c3oEoqGcMwuAvlYctJC2XbMbqMgryIXqN5QfzMAqFcdHhI6m0pEDIa+Kl5ErABzTxmAlDFNZTCiUNKygK1uRnXJbybEhouA7vfbZQbT3fW+4ADkvn6/RcVupsQXsE3nl1oOLKGNn77W6xqEWsWd9NymJHqMIKd7YjmhFLbWf4oLivthwqJk45be1sy+jIXx4lzZ/T99qSx9NHK0ysFaEZYs2v4P8i2f34S0XdjaBzWgxksO19nTvZwJR8V2KZte93paeF4So+RwV0bpfISJrltAzFaqmHXR9V88S/XvbXDYM40tN6dCDvAAswe8tXbcYA9A+/e8xZqOEoiWzIRgTkDUmtEwdY2iO/Q9CCjmPClsOSLQPgEQAe6gM3tC1xntCVbiVl3OZq32tNi9bhzjClpUdcdsTfgXoWCSvDBDqurfXBnh8vr20ApN3H7J4CFUeSSCH3rVD99yIkzzC7YUHm7kssIc=
template:
metadata:
creationTimestamp: null
name: db-credentials
namespace: plataforma-prod
@@ -0,0 +1,37 @@
# ============================================================================
# Senha do admin do Keycloak do tenant `plataforma`. Gerada em 2026-09-17 (P3.1).
#
# PODE SER COMMITADA, pela mesma razao do 05: cifrada para este cluster e presa
# ao nome `keycloak-admin` no namespace `plataforma-prod`.
#
# A senha nasceu, selou e morreu num unico shell no servidor. Nao esta anotada
# em lugar nenhum.
#
# Consumida por: keycloak (KC_BOOTSTRAP_ADMIN_PASSWORD).
#
# ATENCAO — `KC_BOOTSTRAP_ADMIN_*` so cria o usuario na PRIMEIRA subida, com o
# banco do Keycloak vazio. Depois disso, trocar aqui nao troca a senha de quem ja
# existe: isso se faz pelo proprio Keycloak.
#
# PARA ENTRAR a primeira vez, o usuario e o que o `20-keycloak.yaml` define em
# `KC_BOOTSTRAP_ADMIN_USERNAME`. A senha voce recupera do cluster depois de
# aplicar, com `kubectl -n plataforma-prod get secret keycloak-admin`, e a
# primeira coisa a fazer e trocar por uma sua.
#
# PARA REGERAR (so antes da primeira subida):
# `scripts/gerar_credenciais_plataforma.sh keycloak`.
# ============================================================================
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
creationTimestamp: null
name: keycloak-admin
namespace: plataforma-prod
spec:
encryptedData:
password: AgCNIMheCsVg1efxr9P/NEImwaBC03RLQv4tNZdOnc2z5ledyQSWEl13RiYL/aJs0shUiSkKxGi0WOY4zCfpDMTvOVkW/CtcoK/FvXHQ3Qi6m22f0D3lQwWybpZb8S0tKIQv4T0AqXI6Rd/qYmCzxsaFsvUXYq74aGAxX4OiUAdP1UPBBg9kzcUmAUN1oJNbfGI+DdPA1NEx0GOmcMzKX1LDBNDKBLzaLHbCNJNEH2wioHp9N17/iimceg8sG7JO84a9YgDkDSsHd7MG+ilGjDbRSoVEY/4pjGlC7gmiEr65zvUkd2aEhUrKAlk5+8f4OSprmlLrNZDbGeNFj54QTBnuR0zHV6NrAKGTF9pb9pwvA5gK4S6AmDrG3pzBXHHfv6jqx9zXnnn3f7TnB3WVXPzEaz9Wh53d+6iys5pJykR7StLZzby5YP8qTNzazMnSALJdVsAXl9qgJx3fRMbG9zXLGGL1u+VJcyiFF7nRQl5EjheJocCHmClgUzdIOAEdsjvQ1O1DOJrAcICU0PRmqyJL02yUVbIA5NIQAP9leQQnNHcxY40UbNCXitaiakA6LFYivpvmJkRy1b9HGh4Xm5hAEvGPPLUXCcsFyoTBIJ6rULr6SSrBLQVK4s/Ufv1ohL1ievC92J1sCy9MpVA1kOSeOyD2vNwYCq2jRXhER5xoSLB1UdsvNqP9d5oQLU94K7W4sPQY9tD4hgKoOrynEDYvmvsJK9f/xjE=
template:
metadata:
creationTimestamp: null
name: keycloak-admin
namespace: plataforma-prod
+33
View File
@@ -0,0 +1,33 @@
# ============================================================================
# Credenciais do Asaas do tenant `plataforma`. Geradas em 2026-09-17 (P3.3).
#
# PODE SER COMMITADO: cifrado para ESTE cluster e preso ao nome
# `asaas-credentials` no namespace `plataforma-prod`.
#
# Sao DOIS segredos por desenho: `api-key` autoriza COBRAR e MOVIMENTAR
# DINHEIRO; `webhook-token` so autoriza alguem a nos AVISAR que algo aconteceu.
# `AsaasConfig` recusa subir o pod se forem iguais.
#
# O `webhook-token` tem de estar cadastrado no painel do Asaas, no campo "Token
# de autenticacao" do webhook. Se ele for regerado aqui sem ser atualizado la, os
# avisos passam a ser recusados — e 15 recusas seguidas param a fila da conta.
#
# PARA REGERAR: `scripts/gerar_credenciais_plataforma.sh asaas` (pede a chave de
# producao do painel). Passo a passo da ligacao em
# `docs/p33-ligar-o-asaas-de-producao.md`.
# ============================================================================
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
creationTimestamp: null
name: asaas-credentials
namespace: plataforma-prod
spec:
encryptedData:
api-key: AgA6oIPHMEJv5lT6RHldPfmgqw4rNOxq0dDSZ4Pj033HAGC+6mTo4nUQTrsIE3JyvjyDZdGC7lqNzG4URiNQA9AmwAFummr7jzqlsE4BBmzsemRPZWgPhKnsAfxe+wsiVhUlQ8a/h1GMwXPG/ASE/04eRZZ4nczY7sT61gS9TKj68p90a70jmto6Y2/8VZIVa8UrgdC8CSDVNzQhpCgUHyfWEq4+ofBoami+OAQxIC0tcYWJMrbaZm+qJmGrDibxL+a4qaJXpof4Q1r275M6Ih6Bq0iPjRoom9pkckzcwcamyGF8C0OvL2ToRb/JiSOcRbum+nQ0MeiTGPoOb99conhKTtQKoTpQ50J9K6/K3d6qaa773PswZUiRqTs0kfIP73U3fcr4UY0lckbDr3SbXLvW8IILxxWoHe6LUnmL1IopD2vI1UwzYNC+zofP3fzigmeDMLk0019DrZ+Pt5/FXAiJW0I5KZmqTjcQC6cBAFpmdRewvqdYSQ/FHPuLJV928eeBe5vSoMdpbJJydZFmzVYJT+xxZ87SPgBxaJd6Oif/1vJuXXApAQJ6n+n/0278uHGCsQzI8WfC/VFX+HU2Emfvxg+ab0kw9nG8YXXnTh5PbIJmFCZpg2B66IYjY6OF540sfQIPNPcLgOlPwrmlzbSFKvJpbjZYjesTCKwokwBXObvUF2Qg0gx0cJCcMWzNelcnTaSLBhQir5GOnCa2CPveMxaFafCw4/pc9cqEmKkqsuzYT2A/gzQUAcVJ8jhA7+0OjBQWq++2m24J6LSzCa5nLkyTCOFMIqP9JoAQYkJZxBN9UHvtSaeLB0QN9fvJiiFLpzygKAIDH9hyVjX67ZZe8vtTqAowp2m5t+3MncJ9+t+ImIFo+VU3lPzyvhX1gswGP3zuwVUzG9XYHc8zxWy9ebrx16d8
webhook-token: AgAPV9CV3uQ5r9ZHUPtWomaL07MnnjhrFolq4Onrya2zlAWlt7MV9++kz9/6x781eE9/G+X+r6M+HyWpP47Tpkxn6adWngyhW4qTPTcbdWQC4XeP/nF8XVB3rak1YKveMS3TiQ+1RA8j88YzzofiT5svOXUn7YK+vNBBsA0XxgEMerwzeYEbn0FEf0ZI/toAOnjJ7U72BCUALyt12q7mIZnI5XIdNNQlLQqRnbwaOTfb6RTo6fF8WT6qF5kAW1ontYaxTRxd4dUg5QKCOuWC1ycvlbX7lcjSRkWJojmKqK5fuoobds+/IRjj/WdV3KWMnojhif539SxHK1ThKphxc/bsquNKcWOfX09ISoh6dLGTxgiurbFnVerXGf4TbASd0xVH1DNtPm0QCq3rzsKxk/JxsO/w4ySSYltX0xWYJYy+bqDJdffvFi296I3sgINiseAT+HbcDpZAwZH0bAwb6nqIlq1YqUiXhd0NShFGGMqwUA7j5b3LC1iZYdqZ0b2YOydjhkkAy56NzFg/bKu2obNkcstftxfDBTAk2z7AZDUdoaZLw21AVLWb+IRbQUhIBoT/u9bQWvsMLC/uqyrz9kBVxDKGyFN94mrsgJgL+rQ1WjIwRevN1ZhrZdYodhwZ9E94Pj7iEdAXrCVKLdQlFzSvy/vRLRe6Bg5hxa0iMBTaXt9FmsQQ0O305nHR3IzxXPTsLBmUHUellYDnfYyACtYdYST40s7JJJ3GAc+xfmZL8g==
template:
metadata:
creationTimestamp: null
name: asaas-credentials
namespace: plataforma-prod
+133
View File
@@ -0,0 +1,133 @@
# PostgreSQL dedicado do tenant (banco da aplicação + banco do Keycloak)
apiVersion: v1
kind: ConfigMap
metadata:
name: pg-initdb
namespace: plataforma-prod
data:
01-keycloak.sql: |
CREATE DATABASE keycloak;
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
namespace: plataforma-prod
spec:
accessModes: [ReadWriteOnce]
storageClassName: local-path
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: plataforma-prod
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
# TLS ligado, e pg_hba SEM regra `host` em claro — ver o ConfigMap
# pg-hba-tls. Sem trocar `host` por `hostssl`, por o sslmode=require
# no cliente NAO impede outro cliente de conectar em claro: o servidor
# aceitaria os dois. O criterio "conexao sem TLS e recusada" e do lado
# do servidor, e so esta linha o cumpre.
args:
- -c
- ssl=on
- -c
- ssl_cert_file=/etc/postgres-tls/tls.crt
- -c
- ssl_key_file=/etc/postgres-tls/tls.key
- -c
- hba_file=/etc/postgres-hba/pg_hba.conf
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: athleticmap
- name: POSTGRES_USER
value: atm
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: tls
mountPath: /etc/postgres-tls
readOnly: true
- name: hba
mountPath: /etc/postgres-hba
readOnly: true
- name: data
mountPath: /var/lib/postgresql/data
- name: initdb
mountPath: /docker-entrypoint-initdb.d
readinessProbe:
exec:
command: ["pg_isready", "-U", "atm", "-d", "athleticmap"]
initialDelaySeconds: 10
periodSeconds: 5
# fsGroup 999 = gid do postgres na imagem debian. E o que faz o Secret
# montar como root:postgres, e sem isso o pod nao sobe.
securityContext:
fsGroup: 999
volumes:
- name: tls
secret:
secretName: postgres-tls
# 0640 + fsGroup 999 = root:postgres, que e a UNICA combinacao que
# funciona. Medido: root:root 0640 faz o Postgres morrer com
# "could not load private key file: Permission denied" — o processo
# roda como uid 999 e nao le arquivo do grupo root.
defaultMode: 0640
- name: hba
configMap:
name: pg-hba-tls
- name: data
persistentVolumeClaim:
claimName: postgres-data
- name: initdb
configMap:
name: pg-initdb
---
# pg_hba SEM nenhuma regra `host` em claro: quem chegar pela rede sem TLS leva
# "no pg_hba.conf entry ... no encryption" e nao conecta.
apiVersion: v1
kind: ConfigMap
metadata:
name: pg-hba-tls
namespace: plataforma-prod
data:
pg_hba.conf: |
# socket local: initdb, readinessProbe e administracao de dentro do pod
local all all trust
# rede: so com TLS
hostssl all all 0.0.0.0/0 scram-sha-256
hostssl all all ::/0 scram-sha-256
---
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: plataforma-prod
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
+86
View File
@@ -0,0 +1,86 @@
# Keycloak dedicado do tenant `plataforma` (IdP proprio da Athletic Map), persistindo no Postgres
apiVersion: apps/v1
kind: Deployment
metadata:
name: keycloak
namespace: plataforma-prod
spec:
replicas: 1
selector:
matchLabels:
app: keycloak
template:
metadata:
labels:
app: keycloak
spec:
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:26.0
args: ["start-dev", "--import-realm"]
env:
- name: KC_DB
value: postgres
- name: KC_DB_URL
value: "jdbc:postgresql://postgres:5432/keycloak?sslmode=require"
- name: KC_DB_USERNAME
value: atm
- name: KC_DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
- name: KC_BOOTSTRAP_ADMIN_USERNAME
value: admin
- name: KC_BOOTSTRAP_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-admin
key: password
- name: KC_HEALTH_ENABLED
value: "true"
- name: KC_HTTP_ENABLED
value: "true"
- name: KC_PROXY_HEADERS
value: xforwarded
- name: KC_HOSTNAME
value: "auth-plataforma.athleticmap.influxdigital.com.br"
- name: KC_HOSTNAME_STRICT
value: "true"
ports:
- containerPort: 8080
- containerPort: 9000
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
readinessProbe:
httpGet:
path: /health/ready
port: 9000
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 40
volumeMounts:
- name: realm-import
mountPath: /opt/keycloak/data/import
readOnly: true
volumes:
- name: realm-import
configMap:
name: kc-realm-import
---
apiVersion: v1
kind: Service
metadata:
name: keycloak
namespace: plataforma-prod
spec:
selector:
app: keycloak
ports:
- port: 8080
targetPort: 8080
+93
View File
@@ -0,0 +1,93 @@
# Ingress do tenant `plataforma`.
#
# ── Por que este arquivo foi escrito, e nao copiado ───────────────────────────
#
# O piloto guarda o Ingress dentro de `30-apps-stubs.yaml`, junto com o stub
# `backend`. Copiar aquele arquivo traria duas coisas que este tenant nao deve
# herdar:
#
# 1. O proprio stub `backend`, que P0.5 mediu nao atender trafego nenhum (11
# requisicoes externas em 50 dias, todas 401) e que esta em fila para ser
# aposentado. Tenant novo nao nasce com divida.
# 2. A rota catch-all `/api` -> `backend`. Aqui nao ha catch-all: o que nao
# casa com um prefixo conhecido devolve 404, que e a resposta honesta.
#
# Os nomes de Service tambem divergem de proposito: o piloto publica `frontend`,
# e aqui o Service se chama `frontend-spa` (veja `95-frontend-spa.yaml`). Copiar
# o do piloto daria um Ingress apontando para um Service inexistente.
#
# ── Por que o dominio e `.influxdigital.com.br`, e nao `.athleticmap.com` ─────
#
# O canonico decidido com o usuario em 2026-09-16 (P0.9) e
# `<tenant>.athleticmap.com`. Ele ainda NAO vale, e nascer nele hoje quebraria
# justamente o criterio deste card ("login funciona"):
#
# - `plataforma.athleticmap.com` e `auth-plataforma.athleticmap.com` nao tem
# registro A nenhum (conferido em 2026-09-17 por nslookup);
# - o apex `athleticmap.com` resolve para 72.61.42.87, que NAO e o cluster —
# o cluster e 187.77.37.184;
# - cert-manager usa desafio HTTP-01, entao sem o host resolvindo para o
# cluster o certificado nao e emitido, e sem certificado nao ha login.
#
# Ja `*.athleticmap.influxdigital.com.br` e wildcard para o cluster (conferido:
# ate um subdominio inventado resolve para 187.77.37.184). Entao este tenant
# sobe hoje e atravessa o corte de dominio junto com os demais, no passo 5 do
# `docs/p09-plano-de-corte-dominio.md` — e nao numa migracao propria.
#
# ── As rotas publicas ────────────────────────────────────────────────────────
#
# Duas, e ambas por desenho:
#
# `/api/public/leads` quem preenche o formulario da landing page
# ainda nao e cliente, e portanto nao tem token.
# `/api/public/webhooks/asaas` o aviso de pagamento chega dos servidores do
# provedor, que tambem nao tem token nosso; quem
# autentica e o cabecalho `asaas-access-token`.
#
# Nenhuma outra rota esta aberta, e nao ha catch-all — o que significa que uma
# rota publica NOVA em qualquer servico precisa ser acrescentada aqui a mao, ou
# devolve 404. O `scripts/conferir_rotas_publicas.py` existe para isso nao
# depender de alguem lembrar.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: plataforma
namespace: plataforma-prod
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.middlewares: plataforma-prod-redirect-https@kubernetescrd
spec:
ingressClassName: traefik
tls:
- hosts:
- plataforma.athleticmap.influxdigital.com.br
- auth-plataforma.athleticmap.influxdigital.com.br
secretName: plataforma-tls
rules:
- host: plataforma.athleticmap.influxdigital.com.br
http:
paths:
# PUBLICA — sem token, por desenho. Vem primeiro porque o Traefik
# ordena por tamanho do prefixo, mas deixar explicito evita que alguem
# reordene sem perceber.
- { path: /api/public/leads, pathType: Prefix, backend: { service: { name: servico-captacao, port: { number: 80 } } } }
# PUBLICA — o aviso de pagamento do Asaas, que chega da internet sem
# token nenhum (P3.3). NAO casa com /api/financeiro: o controlador
# atende em /api/public/webhooks/asaas, e como aqui nao ha catch-all,
# a ausencia desta linha daria 404 em todo aviso — a cobranca seria
# paga e nunca baixada. Quem autentica e o cabecalho
# `asaas-access-token`, conferido no WebhookAsaasController.
- { path: /api/public/webhooks/asaas, pathType: Prefix, backend: { service: { name: servico-financeiro, port: { number: 80 } } } }
# O resto exige token, como em qualquer tenant.
- { path: /api/configuracao, pathType: Prefix, backend: { service: { name: servico-configuracao, port: { number: 80 } } } }
- { path: /api/cadastro, pathType: Prefix, backend: { service: { name: servico-cadastro, port: { number: 80 } } } }
- { path: /api/financeiro, pathType: Prefix, backend: { service: { name: servico-financeiro, port: { number: 80 } } } }
- { path: /bff, pathType: Prefix, backend: { service: { name: servico-bff, port: { number: 80 } } } }
- { path: /, pathType: Prefix, backend: { service: { name: frontend-spa, port: { number: 80 } } } }
- host: auth-plataforma.athleticmap.influxdigital.com.br
http:
paths:
- { path: /, pathType: Prefix, backend: { service: { name: keycloak, port: { number: 8080 } } } }
@@ -0,0 +1,39 @@
apiVersion: v1
data:
athleticmap-realm.json: |
{
"realm": "athleticmap",
"enabled": true,
"displayName": "Athletic Map",
"loginWithEmailAllowed": true,
"roles": {
"realm": [
{ "name": "admin" },
{ "name": "atm_athlete" },
{ "name": "atm_trainer" },
{ "name": "atm_team_admin" },
{ "name": "atm_fed_admin" }
]
},
"clients": [
{
"clientId": "spa",
"name": "Athletic Map SPA",
"publicClient": true,
"protocol": "openid-connect",
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"redirectUris": ["https://plataforma.athleticmap.influxdigital.com.br/*"],
"webOrigins": ["https://plataforma.athleticmap.influxdigital.com.br"],
"attributes": {
"pkce.code.challenge.method": "S256",
"post.logout.redirect.uris": "https://plataforma.athleticmap.influxdigital.com.br/*"
}
}
]
}
kind: ConfigMap
metadata:
name: kc-realm-import
namespace: plataforma-prod
+10
View File
@@ -0,0 +1,10 @@
# Redirect HTTP->HTTPS (Traefik). Aplicado por-ingress (nao afeta o desafio ACME do cert-manager).
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-https
namespace: plataforma-prod
spec:
redirectScheme:
scheme: https
permanent: true
+83
View File
@@ -0,0 +1,83 @@
# Backup diario do Postgres do tenant `plataforma` (pg_dump cifrado com age, ver P2.4)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pg-backups
namespace: plataforma-prod
spec:
accessModes: [ReadWriteOnce]
storageClassName: local-path
resources:
requests:
storage: 5Gi
---
# ConfigMap com o DESTINATARIO age (chave PUBLICA).
#
# Publica pode ser versionada: ela so cifra. Quem tem a publica NAO abre o
# backup. A privada e gerada por scripts/backup/gerar_chave_backup.sh na maquina
# de quem restaura, e NUNCA chega neste repositorio nem no VPS.
#
# O valor abaixo e um marcador: substitua pelo destinatario real antes de
# aplicar. Com o marcador, o backup_tenant.sh recusa rodar em vez de gravar em
# claro — e o comportamento certo.
apiVersion: v1
kind: ConfigMap
metadata:
name: pg-backup-chave
namespace: plataforma-prod
data:
# Chave PUBLICA do age. Versionar isto e seguro e proposital: ela so serve
# para CIFRAR. Abrir um backup exige a privada, que por desenho nao esta
# neste repositorio nem no servidor — se estivesse no VPS, quem invadisse a
# maquina levaria a chave junto com o arquivo cifrado, e o backup cifrado
# nao teria servido para nada.
#
# Gerada em 2026-09-17 por `scripts/backup/gerar_chave_backup.sh`, na maquina
# do operador. ANOTE A DATA: rotacionar exige manter a chave antiga enquanto
# houver backup cifrado com ela dentro da retencao de 7 dias.
destinatario: "age1g37gc3gvq0zn7802pqh5dd0m3pkvc4z3g2hqk5w3dgvnaz3hwsrq7ps04y"
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: pg-backup
namespace: plataforma-prod
spec:
schedule: "0 2 * * *" # diariamente 02:00
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
# Imagem propria: postgres:16 + age. O dump precisa ser cifrado
# ANTES de sair do processo, entao as duas ferramentas tem de estar
# no mesmo container. Construida de scripts/backup/Containerfile.
- name: pg-backup
image: docker.io/library/atm-pg-backup:1.0
env:
- name: PGPASSWORD
valueFrom:
secretKeyRef: { name: db-credentials, key: password }
- name: CHAVE_PUBLICA
valueFrom:
configMapKeyRef: { name: pg-backup-chave, key: destinatario }
- { name: PGHOST, value: "postgres" }
- { name: PGUSER, value: "atm" }
- { name: DATABASES, value: "athleticmap keycloak" }
- { name: DESTINO, value: "/backups" }
- { name: RETENCAO_DIAS, value: "7" }
# COMANDO_ENVIO fica vazio ate haver credencial de armazenamento
# externo. Sem ele o backup fica cifrado, mas NO MESMO SERVIDOR —
# e o proprio script avisa isso no fim da execucao.
- { name: COMANDO_ENVIO, value: "" }
command: ["backup_tenant.sh"]
volumeMounts:
- { name: backups, mountPath: /backups }
volumes:
- name: backups
persistentVolumeClaim:
claimName: pg-backups
@@ -0,0 +1,59 @@
# Bounded context "configuracao" como app proprio (ADR-0002): Deployment/pod
# medivel = centro de custo. Schema proprio "configuracao" no Postgres do silo
# (criado pelo Flyway). Roteado em /api/configuracao no ingress do tenant.
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: servico-configuracao
namespace: plataforma-prod
labels:
app: servico-configuracao
athleticmap.io/contexto: configuracao
spec:
replicas: 1
selector: { matchLabels: { app: servico-configuracao } }
template:
metadata:
labels:
app: servico-configuracao
athleticmap.io/contexto: configuracao
spec:
containers:
- name: servico-configuracao
image: docker.io/library/servico-configuracao:1.0
imagePullPolicy: Never
ports: [{ containerPort: 8083 }]
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_JWK_SET_URI, value: "http://keycloak:8080/realms/athleticmap/protocol/openid-connect/certs" }
- { name: ATM_ISSUER, value: "https://auth-plataforma.athleticmap.influxdigital.com.br/realms/athleticmap" }
- { name: ATM_TENANT, value: "plataforma" }
resources:
requests: { cpu: 150m, memory: 320Mi }
limits: { cpu: "1", memory: 768Mi }
readinessProbe:
httpGet: { path: /api/configuracao/health, 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-configuracao
namespace: plataforma-prod
labels:
app: servico-configuracao
spec:
selector: { app: servico-configuracao }
ports: [{ name: http, port: 80, targetPort: 8083 }]
@@ -0,0 +1,81 @@
# Bounded context "financeiro" (2a onda): Cobranca + ApuracaoRateio (centros de custo).
# Schema proprio "financeiro" (Flyway). Roteado em /api/financeiro no ingress.
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: servico-financeiro
namespace: plataforma-prod
labels:
app: servico-financeiro
athleticmap.io/contexto: financeiro
spec:
replicas: 1
selector: { matchLabels: { app: servico-financeiro } }
template:
metadata:
labels:
app: servico-financeiro
athleticmap.io/contexto: financeiro
spec:
containers:
- name: servico-financeiro
image: docker.io/library/servico-financeiro:1.0
imagePullPolicy: Never
ports: [{ containerPort: 8083 }]
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_JWK_SET_URI, value: "http://keycloak:8080/realms/athleticmap/protocol/openid-connect/certs" }
- { name: ATM_ISSUER, value: "https://auth-plataforma.athleticmap.influxdigital.com.br/realms/athleticmap" }
- { name: ATM_TENANT, value: "plataforma" }
# ── Asaas de PRODUCAO (P3.3) ──────────────────────────────────
# Aqui e o tenant da plataforma: a conta Asaas e a da propria
# Athletic Map, e nao a de um cliente.
#
# O rotulo `ASAAS_AMBIENTE` e a URL tem de concordar. Se nao
# concordarem, `AsaasClient` RECUSA a emissao — de proposito:
# producao apontando para sandbox faria a cobranca "funcionar" sem
# ninguem pagar de verdade, que e o erro caro e silencioso.
- { name: ASAAS_BASE_URL, value: "https://api.asaas.com/v3" }
- { name: ASAAS_AMBIENTE, value: "producao" }
- { name: ASAAS_USER_AGENT, value: "AthleticMap/plataforma" }
# Os DOIS segredos sao separados, e nao por preciosismo: a API Key
# autoriza movimentar dinheiro; o token do webhook so autoriza
# alguem a nos avisar que algo aconteceu. Se forem iguais,
# `AsaasConfig` RECUSA subir o pod.
- name: ASAAS_API_KEY
valueFrom:
secretKeyRef: { name: asaas-credentials, key: api-key }
- name: ASAAS_WEBHOOK_TOKEN
valueFrom:
secretKeyRef: { name: asaas-credentials, key: webhook-token }
resources:
requests: { cpu: 150m, memory: 320Mi }
limits: { cpu: "1", memory: 768Mi }
readinessProbe:
httpGet: { path: /api/financeiro/health, 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-financeiro
namespace: plataforma-prod
labels:
app: servico-financeiro
spec:
selector: { app: servico-financeiro }
ports: [{ name: http, port: 80, targetPort: 8083 }]
@@ -0,0 +1,59 @@
# Bounded context "cadastro" como app proprio (ADR-0002/0003): Deployment/pod
# medivel = centro de custo. Schema proprio "cadastro" no Postgres do silo
# (criado pelo Flyway). Roteado em /api/cadastro no ingress do tenant.
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: servico-cadastro
namespace: plataforma-prod
labels:
app: servico-cadastro
athleticmap.io/contexto: cadastro
spec:
replicas: 1
selector: { matchLabels: { app: servico-cadastro } }
template:
metadata:
labels:
app: servico-cadastro
athleticmap.io/contexto: cadastro
spec:
containers:
- name: servico-cadastro
image: docker.io/library/servico-cadastro:1.0
imagePullPolicy: Never
ports: [{ containerPort: 8083 }]
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_JWK_SET_URI, value: "http://keycloak:8080/realms/athleticmap/protocol/openid-connect/certs" }
- { name: ATM_ISSUER, value: "https://auth-plataforma.athleticmap.influxdigital.com.br/realms/athleticmap" }
- { name: ATM_TENANT, value: "plataforma" }
resources:
requests: { cpu: 150m, memory: 320Mi }
limits: { cpu: "1", memory: 768Mi }
readinessProbe:
httpGet: { path: /api/cadastro/health, 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-cadastro
namespace: plataforma-prod
labels:
app: servico-cadastro
spec:
selector: { app: servico-cadastro }
ports: [{ name: http, port: 80, targetPort: 8083 }]
@@ -0,0 +1,81 @@
# 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:
containers:
- name: servico-captacao
image: docker.io/library/servico-captacao:1.0
imagePullPolicy: Never
ports: [{ containerPort: 8083 }]
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.influxdigital.com.br" }
- { name: CAPTACAO_RETENCAO_MESES, value: "24" }
- { 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: "" }
resources:
requests: { cpu: 150m, memory: 320Mi }
limits: { cpu: "1", memory: 768Mi }
readinessProbe:
httpGet: { path: /api/public/leads/health, 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 }]
+60
View File
@@ -0,0 +1,60 @@
# BFF (Backend-for-Frontend) — ponto unico da SPA; agrega configuracao + cadastro
# com token relay. Stateless (sem banco). Roteado em /bff no ingress do tenant.
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: servico-bff
namespace: plataforma-prod
labels:
app: servico-bff
athleticmap.io/contexto: bff
spec:
replicas: 1
selector: { matchLabels: { app: servico-bff } }
template:
metadata:
labels:
app: servico-bff
athleticmap.io/contexto: bff
spec:
containers:
- name: servico-bff
image: docker.io/library/servico-bff:1.5
imagePullPolicy: Never
ports: [{ containerPort: 8083 }]
env:
- { name: ATM_CONFIG_URL, value: "http://servico-configuracao" }
- { name: ATM_CADASTRO_URL, value: "http://servico-cadastro" }
- { name: ATM_ORGANIZACAO_URL, value: "http://servico-organizacao" }
- { name: ATM_PLANEJAMENTO_URL, value: "http://servico-planejamento" }
- { name: ATM_CAMPEONATO_URL, value: "http://servico-campeonato" }
- { name: ATM_SAUDE_URL, value: "http://servico-saude" }
- { name: ATM_FINANCEIRO_URL, value: "http://servico-financeiro" }
- { name: ATM_JWK_SET_URI, value: "http://keycloak:8080/realms/athleticmap/protocol/openid-connect/certs" }
- { name: ATM_ISSUER, value: "https://auth-plataforma.athleticmap.influxdigital.com.br/realms/athleticmap" }
- { name: ATM_TENANT, value: "plataforma" }
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { cpu: 500m, memory: 512Mi }
readinessProbe:
httpGet: { path: /bff/health, port: 8083 }
initialDelaySeconds: 20
periodSeconds: 10
failureThreshold: 30
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 8083 }
initialDelaySeconds: 50
periodSeconds: 20
failureThreshold: 6
---
apiVersion: v1
kind: Service
metadata:
name: servico-bff
namespace: plataforma-prod
labels:
app: servico-bff
spec:
selector: { app: servico-bff }
ports: [{ name: http, port: 80, targetPort: 8083 }]
+48
View File
@@ -0,0 +1,48 @@
# SPA Angular (shell OIDC + admin de catalogos + cadastro de Atleta) consumindo
# o BFF. Substitui o stub estatico nginx/configMap. Roteado em / no ingress.
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend-spa
namespace: plataforma-prod
labels:
app: frontend-spa
athleticmap.io/contexto: frontend
spec:
replicas: 1
selector: { matchLabels: { app: frontend-spa } }
template:
metadata:
labels:
app: frontend-spa
athleticmap.io/contexto: frontend
spec:
containers:
- name: frontend-spa
image: docker.io/library/frontend-spa:1.6
imagePullPolicy: Never
ports: [{ containerPort: 80 }]
env:
- { name: ATM_KC_URL, value: "https://auth-plataforma.athleticmap.influxdigital.com.br" }
- { name: ATM_REALM, value: "athleticmap" }
- { name: ATM_CLIENT, value: "spa" }
- { name: ATM_TENANT, value: "plataforma" }
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { cpu: 200m, memory: 128Mi }
readinessProbe:
httpGet: { path: /, port: 80 }
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: frontend-spa
namespace: plataforma-prod
labels:
app: frontend-spa
spec:
selector: { app: frontend-spa }
ports: [{ name: http, port: 80, targetPort: 80 }]
+102
View File
@@ -0,0 +1,102 @@
# Tenant `plataforma`
**Este não é um cliente.** É onde a Athletic Map opera a si mesma: o namespace,
o banco e o realm da própria empresa, separados dos clientes e separados também
do tenant de demonstração.
Gerado em 2026-09-17 pela tarefa **P3.1** (card 137).
## Estado: preparado, não aplicado
Nada aqui foi publicado. Os manifests estão prontos para o ArgoCD, mas **faltam
duas credenciais que só um humano pode gerar** — veja
[`05-sealed-db-credentials.yaml`](05-sealed-db-credentials.yaml) e
[`06-sealed-keycloak-admin.yaml`](06-sealed-keycloak-admin.yaml). No lugar dos
`SealedSecret` há um `ConfigMap` marcador, com nome impossível de confundir
(`CREDENCIAL-DO-BANCO-NAO-FOI-GERADA`) e com o comando `kubeseal` a rodar.
Enquanto os marcadores estiverem no lugar, o `postgres` e o `keycloak` não
sobem. É deliberado: melhor não subir do que subir com senha conhecida.
**Por que o assistente não gerou as credenciais:** `SealedSecret` é selado
**para um namespace**. O do `piloto-prod` não decifra em `plataforma-prod` — o
controlador recusa. E gerar segredo novo significaria escrevê-lo em algum lugar
antes de selar, o que o preâmbulo das tarefas proíbe.
## O que este tenant leva, e o que não leva
Um cliente roda 13 contextos. Aqui rodam 4, porque a Athletic Map não é uma
escolinha — não tem atleta, treino, campeonato nem prontuário.
| Leva | Para quê |
| --- | --- |
| `servico-configuracao` | catálogos e parâmetros |
| `servico-cadastro` | pessoas — os próprios funcionários |
| `servico-financeiro` | o financeiro da casa (veja a ressalva abaixo) |
| `servico-captacao` | **só existe aqui**: os leads da landing page |
| `bff` + `frontend-spa` | a porta de entrada |
| `postgres`, `keycloak`, `backup` | a infraestrutura do silo |
**Não leva**, de propósito:
- os 9 contextos restantes (agenda, saúde, pedagógico, nutrição, campeonato,
organização, planejamento, assistência social, administrativo) — sem uso aqui;
- o `30-apps-stubs.yaml` do piloto, que carrega o stub `backend` legado. P0.5
mediu: 11 requisições externas em 50 dias, todas 401. Tenant novo não nasce
com dívida, então o Ingress foi escrito do zero
([`30-ingress.yaml`](30-ingress.yaml)) e **sem o catch-all `/api`**.
## Domínio
Hoje: `plataforma.athleticmap.influxdigital.com.br` e
`auth-plataforma.athleticmap.influxdigital.com.br`.
Não é o canônico. O canônico decidido em 2026-09-16 é
`<tenant>.athleticmap.com` (P0.9), mas **o corte ainda não foi executado**.
Conferido em 2026-09-17: `plataforma.athleticmap.com` não tem registro A, e o
apex `athleticmap.com` resolve para 72.61.42.87, que não é o cluster
(187.77.37.184). Como o cert-manager usa desafio HTTP-01, nascer no `.com` hoje
daria certificado não emitido e login quebrado.
`*.athleticmap.influxdigital.com.br` é wildcard para o cluster, então este
tenant sobe já e **deve atravessar o corte junto com os demais**, no passo 5 do
plano.
> **Atenção — este tenant NÃO está no corte preparado.** O plano vive em
> `docs/p09-plano-de-corte-dominio.md`, **que não existe nesta branch**: está em
> `feat/dominio-app-athleticmap` (commit `f71480a`). E esse commit altera
> `acme`, `demo` e `piloto` — o `plataforma` ainda não existia quando foi
> escrito. Quem executar o P0.9 precisa **incluir esta pasta à mão**, senão ela
> fica sozinha no domínio do fornecedor.
Os hosts a trocar aqui estão em `30-ingress.yaml`,
`20-keycloak.yaml`, `35-realm-import-cm.yaml`, `realm/athleticmap-realm.json`,
`95-frontend-spa.yaml`, o `ATM_ISSUER` dos quatro serviços e o `CAPTACAO_ORIGEM`.
## Uma ressalva sobre "cobrar a si mesma"
O card 137 descreve este tenant como "onde a Athletic Map cobra a si mesma".
Isso **contradiz** `project-docs/Escolinhas esportivas/Produto e negócio/Planos
de assinatura.md:137`, que diz: *"Billing da assinatura é outro sistema. O `servico-financeiro` cobra o
aluno do cliente; cobrar o cliente exige gateway próprio (Asaas, Stripe, Iugu) e
ciclo de inadimplência/suspensão — não dá para reaproveitar."*
O tenant foi montado assim mesmo porque a infraestrutura é idêntica nas duas
leituras — namespace, banco, realm e login não mudam. O que muda é se o
`servico-financeiro` daqui serve para cobrar assinatura. **Isso está em aberto** e
é o que os cards 138 (P3.2) e 139 (P3.3, Asaas) decidem.
## Como verificar, quando for aplicado
O critério do card é *"login no tenant plataforma funciona e o financeiro abre
vazio"*. Depois de gerar as duas credenciais e deixar o ArgoCD sincronizar:
```sh
kubectl -n plataforma-prod get pods # postgres, keycloak e os 6 serviços prontos
kubectl -n plataforma-prod get ingress # certificado emitido, ADDRESS preenchido
curl -sI https://plataforma.athleticmap.influxdigital.com.br | head -1 # 200
```
O "financeiro vazio" é o esperado: banco novo, nenhuma cobrança. Se vier dado,
o tenant está apontando para o banco errado — pare e confira o
`05-sealed-db-credentials.yaml`.
@@ -0,0 +1,62 @@
{
"realm": "athleticmap",
"enabled": true,
"displayName": "Athletic Map",
"loginWithEmailAllowed": true,
"roles": {
"realm": [
{
"name": "admin"
},
{
"name": "atm_athlete"
},
{
"name": "atm_trainer"
},
{
"name": "atm_team_admin"
},
{
"name": "atm_fed_admin"
}
]
},
"clients": [
{
"clientId": "spa",
"name": "Athletic Map SPA",
"publicClient": true,
"protocol": "openid-connect",
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"redirectUris": [
"https://plataforma.athleticmap.influxdigital.com.br/*"
],
"webOrigins": [
"https://plataforma.athleticmap.influxdigital.com.br"
],
"attributes": {
"pkce.code.challenge.method": "S256",
"post.logout.redirect.uris": "https://plataforma.athleticmap.influxdigital.com.br/*"
},
"protocolMappers": [
{
"name": "funcionario_id",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"user.attribute": "funcionario_id",
"claim.name": "funcionario_id",
"jsonType.label": "String",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
}
]
}
]
}