feat: tarefas agendadas (resumo diario N16, eliminacao P6.3) no molde e nos 4 tenants; expurgo semanal P6.4 na plataforma; regra de alerta da nota fiscal N18

This commit is contained in:
deploy
2026-09-29 17:23:46 +00:00
parent ae615ad1b8
commit 15b3ae7e95
12 changed files with 1324 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
# =============================================================================
# N16 — o resumo AGREGADO do tenant, entregue a plataforma uma vez por dia, para o
# comprador ver todas as escolinhas dele lado a lado no /minha-conta.
#
# AGUARDANDO GRAVACAO no molde (`moldes/consolidado/` do GitOps) e nos tenants que
# ja existem. Nada aqui foi aplicado.
#
# O desenho: o TENANT calcula (runtime-operacao, GET /api/resumo-do-tenant, so
# contadores e somas) e o TENANT envia (este CronJob). A plataforma nao entra no
# namespace nem no banco do tenant para isto.
#
# 1. token do client `operacao` no realm do tenant (o mesmo das tarefas do
# financeiro: Secret `operacao-credentials`);
# 2. GET http://runtime-operacao/api/resumo-do-tenant (lista de 1 ou 2 meses);
# 3. POST https://plataforma.athleticmap.com/api/public/resumo-do-tenant/{{TENANT}}
# com o cabecalho `x-atm-resumo-token` — do Secret `resumo-credentials`, que
# so vale para ESTE tenant (HMAC do nome dele com a chave da plataforma).
#
# A saida para a plataforma e pela internet (443 publico), que a deny-cross-tenant
# ja libera: nao precisa de NetworkPolicy nova.
#
# `optional: true` no resumo-credentials: tenant sem o Secret (chave da plataforma
# ainda nao configurada) roda o job, que termina OK dizendo "sem token" — nao gera
# KubeJobFailed nem gasta a cota de e-mail dos alertas.
# =============================================================================
apiVersion: v1
kind: ConfigMap
metadata:
name: resumo-cron
namespace: {{TENANT}}-prod
data:
enviar.sh: |
#!/bin/sh
set -eu
: "${KEYCLOAK_URL:?falta KEYCLOAK_URL}"
: "${CLIENT_SECRET:?falta CLIENT_SECRET}"
: "${ISSUER_HOST:?falta ISSUER_HOST}"
: "${DESTINO:?falta DESTINO}"
if [ -z "${RESUMO_TOKEN:-}" ]; then
echo "sem o Secret resumo-credentials neste tenant: nada a enviar."
exit 0
fi
i=0
until curl -sS -o /dev/null -m 5 "${KEYCLOAK_URL}" 2>/dev/null; do
i=$((i+1))
if [ "$i" -ge 24 ]; then
echo "ERRO: ${KEYCLOAK_URL} inalcancavel depois de 2 min." >&2
exit 1
fi
sleep 5
done
RESP="$(printf 'grant_type=client_credentials&client_id=operacao&client_secret=%s' \
"${CLIENT_SECRET}" \
| curl -sS --fail-with-body -X POST "${KEYCLOAK_URL}" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'X-Forwarded-Proto: https' \
-H 'X-Forwarded-Port: 443' \
-H "X-Forwarded-Host: ${ISSUER_HOST}" -d @-)"
TOKEN="$(printf '%s' "${RESP}" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')"
if [ -z "${TOKEN}" ]; then
echo "ERRO: nao consegui token pelo client 'operacao'." >&2
exit 1
fi
# Os tokens vao por `-K -` (stdin), e nao na linha de comando: argumento de
# processo aparece em `ps` para quem estiver no no.
CODIGO="$(printf 'header = "Authorization: Bearer %s"\n' "${TOKEN}" \
| curl -sS -K - -o /tmp/resumo.json -w '%{http_code}' http://runtime-operacao/api/resumo-do-tenant)"
if [ "${CODIGO}" != "200" ]; then
echo "ERRO: o runtime-operacao respondeu ${CODIGO} ao resumo." >&2
exit 1
fi
echo "resumo calculado: $(wc -c < /tmp/resumo.json) bytes (so numeros)"
CODIGO="$(printf 'header = "x-atm-resumo-token: %s"\n' "${RESUMO_TOKEN}" \
| curl -sS -K - -X POST "${DESTINO}" -H 'Content-Type: application/json' \
--data-binary @/tmp/resumo.json -o /tmp/saida.json -w '%{http_code}')"
echo "plataforma: ${CODIGO}"
cat /tmp/saida.json; echo
case "${CODIGO}" in
2*) ;;
*) echo "ERRO: a plataforma respondeu ${CODIGO}." >&2; exit 1 ;;
esac
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: resumo-para-a-plataforma
namespace: {{TENANT}}-prod
spec:
# Madrugada, depois da virada do dia em Sao Paulo e fora do horario das
# mensalidades (dia 1, 06h).
schedule: "30 4 * * *"
timeZone: America/Sao_Paulo
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
activeDeadlineSeconds: 600
backoffLimit: 2
template:
spec:
restartPolicy: OnFailure
containers:
- name: enviar
image: docker.io/curlimages/curl:8.9.1
imagePullPolicy: Never
command: ["/bin/sh", "/script/enviar.sh"]
env:
- name: KEYCLOAK_URL
value: http://keycloak.plataforma-prod.svc:8080/realms/{{REALM}}/protocol/openid-connect/token
- name: ISSUER_HOST
value: auth.athleticmap.com
- name: DESTINO
value: https://plataforma.athleticmap.com/api/public/resumo-do-tenant/{{TENANT}}
- name: CLIENT_SECRET
valueFrom:
secretKeyRef:
name: operacao-credentials
key: client-secret
- name: RESUMO_TOKEN
valueFrom:
secretKeyRef:
name: resumo-credentials
key: token
optional: true
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 200m, memory: 64Mi }
volumeMounts:
- name: script
mountPath: /script
volumes:
- name: script
configMap:
name: resumo-cron
@@ -0,0 +1,75 @@
# =============================================================================
# P6.3 — a ELIMINACAO automatica (decisao 3 do dono, 2026-09-28): todo dia, o
# runtime-core elimina quem completou 5 anos do fim, anonimiza o fiscal e grava
# o registro. Nada fica elegivel antes de 2031 — ate la a rodada termina com
# zero eliminados, e e isso que prova que ela esta viva.
#
# PENDENTE DE GRAVACAO NO MOLDE (moldes/consolidado/) E NOS 4 TENANTS ANTIGOS
# (tenants/{acme,demo,escolinha,piloto}/). Este arquivo mora aqui ate o dono
# gravar — o mesmo caminho do 55-cronjobs-financeiro.yaml.
#
# Por que CronJob, e nao @Scheduled no runtime: com mais de uma replica, um
# agendador no processo rodaria a eliminacao duas vezes (o mesmo motivo do
# financeiro). Duas chamadas simultaneas nao duplicam nada — a pessoa e travada
# antes de sair —, mas uma so e o desenho certo.
#
# DEPENDE do ConfigMap `financeiro-cron` (55-cronjobs-financeiro.yaml): usa o
# mesmo `disparar.sh`, o mesmo client `operacao` e o mesmo Secret
# `operacao-credentials`. O runtime-core so aceita esta rota com token do client
# `operacao` (SegurancaDoRuntime) — nenhum usuario humano, nem admin.
#
# Titular que falha -> o endpoint devolve 500 -> o Job falha -> KubeJobFailed.
# Silencio aqui seria descobrir em 2031 que a rotina nunca rodou.
#
# ANTES DE APLICAR: o runtime-core do tenant tem de estar numa imagem com o
# modulo servico-retencao (senao a rota da 404 e o Job falha todo dia).
# Para conferir um tenant sem eliminar nada: `?simular=true` (mesmo caminho,
# transacao desfeita, contagens reais).
# =============================================================================
apiVersion: batch/v1
kind: CronJob
metadata:
name: retencao-eliminacao
namespace: {{TENANT}}-prod
spec:
# 04:40, longe do backup (02:30) e das tarefas do financeiro (:15 e :30).
schedule: "40 4 * * *"
timeZone: America/Sao_Paulo
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
activeDeadlineSeconds: 1800
backoffLimit: 1
template:
spec:
restartPolicy: OnFailure
containers:
- name: disparar
image: docker.io/curlimages/curl:8.9.1
imagePullPolicy: Never
command: ["/bin/sh", "/script/disparar.sh"]
env:
- name: KEYCLOAK_URL
value: http://keycloak.plataforma-prod.svc:8080/realms/{{REALM}}/protocol/openid-connect/token
- name: ISSUER_HOST
value: auth.athleticmap.com
- name: ALVO
value: http://runtime-core/api/retencao/eliminacao?limite=200
- name: CLIENT_SECRET
valueFrom:
secretKeyRef:
name: operacao-credentials
key: client-secret
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 200m, memory: 64Mi }
volumeMounts:
- name: script
mountPath: /script
volumes:
- name: script
configMap:
name: financeiro-cron
defaultMode: 0555
@@ -0,0 +1,141 @@
# =============================================================================
# N18 — Alerta de nota fiscal travada.
#
# DESTINO NO GITOPS: platform/monitoring/athleticmap-rules-nota-fiscal.yaml
# (repositorio athletic-map-deploy do servidor). O app `atm-platform` le
# `platform/` com recurse, entao basta o arquivo estar la — nao precisa mexer
# no `athleticmap-rules.yaml` nem no `application.yaml`.
#
# De onde vem: `MetricasDasNotasFiscais` e `ProcessarNotasPendentes`, no
# servico-financeiro (dentro do runtime-operacao nos tenants consolidados). As
# metricas `atm_*` ja estao na lista de keep dos tres ServiceMonitors
# (runtimes, servicos, tenants), entao chegam sem mudar a coleta.
#
# Procedimento manual: docs/runbook-nota-fiscal-travada.md (repo nova-atm).
#
# ── POR QUE ESTES LIMITES ───────────────────────────────────────────────────
#
# 6 HORAS para nota que devia estar andando (PENDENTE_EMISSAO / EMITINDO):
# - no normal, a varredura roda de hora em hora (:15) e o provedor emite em
# ate 15 min: uma nota passa menos de 1h30 pendente;
# - a NFS-e nacional cai e volta sozinha, em geral em minutos ou poucas
# horas, e o provedor tenta de novo sem ninguem pedir — alertar em 1-2h
# seria e-mail para algo que se resolve sozinho (e a caixa dos alertas e a
# mesma dos avisos aos clientes, com cota);
# - 6h sao seis rodadas da varredura: nao e mais soluco. E ainda da tempo de
# agir no mesmo dia, bem antes do fim da competencia.
# 24 HORAS vira critical (mesmo nome de alerta: o Alertmanager cala o warning
# quando o critical dispara — inhibit_rules com equal [namespace, alertname]).
# Um dia inteiro sem nota e pagante perguntando, e a virada de mes chegando.
#
# ERRO: 1 hora. Nota em ERRO NAO e reenviada sozinha — ninguem mexendo, ela
# fica la para sempre. O `for` so segura a rajada de uma rodada ruim, para o
# e-mail sair uma vez com o total, e nao nota a nota.
#
# PENDENTE_DADOS: 72 horas. Nao e instabilidade: e cadastro (configuracao
# fiscal ou dado do tomador faltando), resolvido pela escolinha ou pelo
# contador, em dia util. 72h atravessa um fim de semana sem alarme. O "mais
# brando" esta no PRAZO, e nao na severidade: `info` nao e usado na
# plataforma (scripts/test_regras_de_alerta.py so aceita warning/critical) e,
# no desenho do chart, info existe para ser calado pelo InfoInhibitor.
# =============================================================================
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: athleticmap-rules-nota-fiscal
namespace: monitoring
labels:
release: monitoring # o Prometheus so carrega regras com este label
spec:
groups:
- name: athleticmap.nota-fiscal
rules:
# `max by (namespace, situacao)`: com duas replicas do runtime, as duas
# publicam o mesmo numero (a conta vem do banco do tenant). O max junta.
- alert: AthleticMapNotaFiscalTravada
expr: |
max by (namespace, situacao) (
atm_nota_fiscal_pendente_mais_antiga_segundos{situacao=~"PENDENTE_EMISSAO|EMITINDO"}
) > 6 * 3600
for: 15m
labels:
severity: warning
annotations:
summary: "{{ $labels.namespace }}: nota fiscal parada em {{ $labels.situacao }} ha mais de 6h"
description: >-
A nota mais antiga em {{ $labels.situacao }} esta parada ha
{{ $value | humanizeDuration }}. EMITINDO = ja esta no provedor (Asaas)
esperando a prefeitura: quase sempre e a NFS-e nacional instavel —
confira o status da NFS-e nacional e o painel do Asaas (Notas fiscais)
antes de mexer; o provedor tenta de novo sozinho. PENDENTE_EMISSAO = a
varredura de hora em hora nao levou a nota: veja o CronJob
financeiro-notas (kubectl -n {{ $labels.namespace }} get jobs
-l job-name --sort-by=.metadata.creationTimestamp). NAO emita a nota
por fora sem cancelar a do provedor: sai em dobro. Procedimento:
docs/runbook-nota-fiscal-travada.md.
- alert: AthleticMapNotaFiscalTravada
expr: |
max by (namespace, situacao) (
atm_nota_fiscal_pendente_mais_antiga_segundos{situacao=~"PENDENTE_EMISSAO|EMITINDO"}
) > 24 * 3600
for: 15m
labels:
severity: critical
annotations:
summary: "{{ $labels.namespace }}: nota fiscal parada em {{ $labels.situacao }} ha mais de 24h"
description: >-
Um dia inteiro sem a nota sair ({{ $value | humanizeDuration }}). Se a
NFS-e nacional continua fora, avise a escolinha (a nota sai atrasada,
com a data do servico) e acompanhe; se voltou e a nota nao andou, use
"sincronizar" em cada nota e, se o provedor disser erro, "reabrir".
Emissao manual so seguindo o runbook, na ordem, para nao duplicar:
docs/runbook-nota-fiscal-travada.md.
- alert: AthleticMapNotaFiscalEmErro
expr: max by (namespace) (atm_nota_fiscal_pendentes{situacao="ERRO"}) > 0
for: 1h
labels:
severity: warning
annotations:
summary: "{{ $labels.namespace }}: {{ $value }} nota(s) fiscal(is) em ERRO"
description: >-
O provedor ou a prefeitura recusou, e nota em ERRO NAO e reenviada
sozinha. Veja o motivo de cada uma (GET /api/financeiro/notas?situacao=ERRO,
campo erroMensagem). Se foi a NFS-e nacional fora, espere ela voltar
e use POST /api/financeiro/notas/{id}/reabrir — a proxima varredura
manda de novo. Se foi dado errado, corrija antes de reabrir.
Procedimento: docs/runbook-nota-fiscal-travada.md.
- alert: AthleticMapNotaFiscalSemDadosFiscais
expr: |
max by (namespace) (
atm_nota_fiscal_pendente_mais_antiga_segundos{situacao="PENDENTE_DADOS"}
) > 72 * 3600
for: 1h
labels:
severity: warning
annotations:
summary: "{{ $labels.namespace }}: notas sem emitir por falta de dado fiscal ha mais de 3 dias"
description: >-
Nao e instabilidade: falta configuracao fiscal da escolinha (codigo de
servico municipal, aliquotas, emissao desligada) ou dado do tomador
(CPF/CNPJ, endereco). O motivo esta em erroMensagem ("faltam: ...") em
GET /api/financeiro/notas?situacao=PENDENTE_DADOS. Fale com a escolinha
ou o contador; depois de corrigir, "reabrir" cada nota.
# Silencio nao e "nenhuma nota parada": se a conta nao roda, as metricas
# acima ficam no ultimo valor (ou em zero, se nunca mediram). Com a
# validade de 2 min, ~30 tentativas por hora por replica.
- alert: AthleticMapNotaFiscalSemMedicao
expr: max by (namespace) (increase(atm_nota_fiscal_medicao_falhou_total[30m])) > 5
for: 30m
labels:
severity: warning
annotations:
summary: "{{ $labels.namespace }}: ha 1h nao se consegue contar as notas fiscais paradas"
description: >-
O alerta de nota travada esta cego neste tenant. Quase sempre e o banco
do financeiro fora ou sem conexao. Log: kubectl -n {{ $labels.namespace }}
logs deploy/runtime-operacao | grep 'nao consegui contar as pendentes'
(no escolinha-prod e no plataforma-prod o deploy e o servico-financeiro).
+141
View File
@@ -0,0 +1,141 @@
# =============================================================================
# N16 — o resumo AGREGADO do tenant, entregue a plataforma uma vez por dia, para o
# comprador ver todas as escolinhas dele lado a lado no /minha-conta.
#
# AGUARDANDO GRAVACAO no molde (`moldes/consolidado/` do GitOps) e nos tenants que
# ja existem. Nada aqui foi aplicado.
#
# O desenho: o TENANT calcula (runtime-operacao, GET /api/resumo-do-tenant, so
# contadores e somas) e o TENANT envia (este CronJob). A plataforma nao entra no
# namespace nem no banco do tenant para isto.
#
# 1. token do client `operacao` no realm do tenant (o mesmo das tarefas do
# financeiro: Secret `operacao-credentials`);
# 2. GET http://runtime-operacao/api/resumo-do-tenant (lista de 1 ou 2 meses);
# 3. POST https://plataforma.athleticmap.com/api/public/resumo-do-tenant/acme
# com o cabecalho `x-atm-resumo-token` — do Secret `resumo-credentials`, que
# so vale para ESTE tenant (HMAC do nome dele com a chave da plataforma).
#
# A saida para a plataforma e pela internet (443 publico), que a deny-cross-tenant
# ja libera: nao precisa de NetworkPolicy nova.
#
# `optional: true` no resumo-credentials: tenant sem o Secret (chave da plataforma
# ainda nao configurada) roda o job, que termina OK dizendo "sem token" — nao gera
# KubeJobFailed nem gasta a cota de e-mail dos alertas.
# =============================================================================
apiVersion: v1
kind: ConfigMap
metadata:
name: resumo-cron
namespace: acme-prod
data:
enviar.sh: |
#!/bin/sh
set -eu
: "${KEYCLOAK_URL:?falta KEYCLOAK_URL}"
: "${CLIENT_SECRET:?falta CLIENT_SECRET}"
: "${ISSUER_HOST:?falta ISSUER_HOST}"
: "${DESTINO:?falta DESTINO}"
if [ -z "${RESUMO_TOKEN:-}" ]; then
echo "sem o Secret resumo-credentials neste tenant: nada a enviar."
exit 0
fi
i=0
until curl -sS -o /dev/null -m 5 "${KEYCLOAK_URL}" 2>/dev/null; do
i=$((i+1))
if [ "$i" -ge 24 ]; then
echo "ERRO: ${KEYCLOAK_URL} inalcancavel depois de 2 min." >&2
exit 1
fi
sleep 5
done
RESP="$(printf 'grant_type=client_credentials&client_id=operacao&client_secret=%s' \
"${CLIENT_SECRET}" \
| curl -sS --fail-with-body -X POST "${KEYCLOAK_URL}" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'X-Forwarded-Proto: https' \
-H 'X-Forwarded-Port: 443' \
-H "X-Forwarded-Host: ${ISSUER_HOST}" -d @-)"
TOKEN="$(printf '%s' "${RESP}" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')"
if [ -z "${TOKEN}" ]; then
echo "ERRO: nao consegui token pelo client 'operacao'." >&2
exit 1
fi
# Os tokens vao por `-K -` (stdin), e nao na linha de comando: argumento de
# processo aparece em `ps` para quem estiver no no.
CODIGO="$(printf 'header = "Authorization: Bearer %s"\n' "${TOKEN}" \
| curl -sS -K - -o /tmp/resumo.json -w '%{http_code}' http://runtime-operacao/api/resumo-do-tenant)"
if [ "${CODIGO}" != "200" ]; then
echo "ERRO: o runtime-operacao respondeu ${CODIGO} ao resumo." >&2
exit 1
fi
echo "resumo calculado: $(wc -c < /tmp/resumo.json) bytes (so numeros)"
CODIGO="$(printf 'header = "x-atm-resumo-token: %s"\n' "${RESUMO_TOKEN}" \
| curl -sS -K - -X POST "${DESTINO}" -H 'Content-Type: application/json' \
--data-binary @/tmp/resumo.json -o /tmp/saida.json -w '%{http_code}')"
echo "plataforma: ${CODIGO}"
cat /tmp/saida.json; echo
case "${CODIGO}" in
2*) ;;
*) echo "ERRO: a plataforma respondeu ${CODIGO}." >&2; exit 1 ;;
esac
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: resumo-para-a-plataforma
namespace: acme-prod
spec:
# Madrugada, depois da virada do dia em Sao Paulo e fora do horario das
# mensalidades (dia 1, 06h).
schedule: "30 4 * * *"
timeZone: America/Sao_Paulo
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
activeDeadlineSeconds: 600
backoffLimit: 2
template:
spec:
restartPolicy: OnFailure
containers:
- name: enviar
image: docker.io/curlimages/curl:8.9.1
imagePullPolicy: Never
command: ["/bin/sh", "/script/enviar.sh"]
env:
- name: KEYCLOAK_URL
value: http://keycloak.plataforma-prod.svc:8080/realms/athleticmap-3/protocol/openid-connect/token
- name: ISSUER_HOST
value: auth.athleticmap.com
- name: DESTINO
value: https://plataforma.athleticmap.com/api/public/resumo-do-tenant/acme
- name: CLIENT_SECRET
valueFrom:
secretKeyRef:
name: operacao-credentials
key: client-secret
- name: RESUMO_TOKEN
valueFrom:
secretKeyRef:
name: resumo-credentials
key: token
optional: true
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 200m, memory: 64Mi }
volumeMounts:
- name: script
mountPath: /script
volumes:
- name: script
configMap:
name: resumo-cron
+75
View File
@@ -0,0 +1,75 @@
# =============================================================================
# P6.3 — a ELIMINACAO automatica (decisao 3 do dono, 2026-09-28): todo dia, o
# runtime-core elimina quem completou 5 anos do fim, anonimiza o fiscal e grava
# o registro. Nada fica elegivel antes de 2031 — ate la a rodada termina com
# zero eliminados, e e isso que prova que ela esta viva.
#
# PENDENTE DE GRAVACAO NO MOLDE (moldes/consolidado/) E NOS 4 TENANTS ANTIGOS
# (tenants/{acme,demo,escolinha,piloto}/). Este arquivo mora aqui ate o dono
# gravar — o mesmo caminho do 55-cronjobs-financeiro.yaml.
#
# Por que CronJob, e nao @Scheduled no runtime: com mais de uma replica, um
# agendador no processo rodaria a eliminacao duas vezes (o mesmo motivo do
# financeiro). Duas chamadas simultaneas nao duplicam nada — a pessoa e travada
# antes de sair —, mas uma so e o desenho certo.
#
# DEPENDE do ConfigMap `financeiro-cron` (55-cronjobs-financeiro.yaml): usa o
# mesmo `disparar.sh`, o mesmo client `operacao` e o mesmo Secret
# `operacao-credentials`. O runtime-core so aceita esta rota com token do client
# `operacao` (SegurancaDoRuntime) — nenhum usuario humano, nem admin.
#
# Titular que falha -> o endpoint devolve 500 -> o Job falha -> KubeJobFailed.
# Silencio aqui seria descobrir em 2031 que a rotina nunca rodou.
#
# ANTES DE APLICAR: o runtime-core do tenant tem de estar numa imagem com o
# modulo servico-retencao (senao a rota da 404 e o Job falha todo dia).
# Para conferir um tenant sem eliminar nada: `?simular=true` (mesmo caminho,
# transacao desfeita, contagens reais).
# =============================================================================
apiVersion: batch/v1
kind: CronJob
metadata:
name: retencao-eliminacao
namespace: acme-prod
spec:
# 04:40, longe do backup (02:30) e das tarefas do financeiro (:15 e :30).
schedule: "40 4 * * *"
timeZone: America/Sao_Paulo
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
activeDeadlineSeconds: 1800
backoffLimit: 1
template:
spec:
restartPolicy: OnFailure
containers:
- name: disparar
image: docker.io/curlimages/curl:8.9.1
imagePullPolicy: Never
command: ["/bin/sh", "/script/disparar.sh"]
env:
- name: KEYCLOAK_URL
value: http://keycloak.plataforma-prod.svc:8080/realms/athleticmap-3/protocol/openid-connect/token
- name: ISSUER_HOST
value: auth.athleticmap.com
- name: ALVO
value: http://runtime-core/api/retencao/eliminacao?limite=200
- name: CLIENT_SECRET
valueFrom:
secretKeyRef:
name: operacao-credentials
key: client-secret
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 200m, memory: 64Mi }
volumeMounts:
- name: script
mountPath: /script
volumes:
- name: script
configMap:
name: financeiro-cron
defaultMode: 0555
+141
View File
@@ -0,0 +1,141 @@
# =============================================================================
# N16 — o resumo AGREGADO do tenant, entregue a plataforma uma vez por dia, para o
# comprador ver todas as escolinhas dele lado a lado no /minha-conta.
#
# AGUARDANDO GRAVACAO no molde (`moldes/consolidado/` do GitOps) e nos tenants que
# ja existem. Nada aqui foi aplicado.
#
# O desenho: o TENANT calcula (runtime-operacao, GET /api/resumo-do-tenant, so
# contadores e somas) e o TENANT envia (este CronJob). A plataforma nao entra no
# namespace nem no banco do tenant para isto.
#
# 1. token do client `operacao` no realm do tenant (o mesmo das tarefas do
# financeiro: Secret `operacao-credentials`);
# 2. GET http://runtime-operacao/api/resumo-do-tenant (lista de 1 ou 2 meses);
# 3. POST https://plataforma.athleticmap.com/api/public/resumo-do-tenant/demo
# com o cabecalho `x-atm-resumo-token` — do Secret `resumo-credentials`, que
# so vale para ESTE tenant (HMAC do nome dele com a chave da plataforma).
#
# A saida para a plataforma e pela internet (443 publico), que a deny-cross-tenant
# ja libera: nao precisa de NetworkPolicy nova.
#
# `optional: true` no resumo-credentials: tenant sem o Secret (chave da plataforma
# ainda nao configurada) roda o job, que termina OK dizendo "sem token" — nao gera
# KubeJobFailed nem gasta a cota de e-mail dos alertas.
# =============================================================================
apiVersion: v1
kind: ConfigMap
metadata:
name: resumo-cron
namespace: demo-prod
data:
enviar.sh: |
#!/bin/sh
set -eu
: "${KEYCLOAK_URL:?falta KEYCLOAK_URL}"
: "${CLIENT_SECRET:?falta CLIENT_SECRET}"
: "${ISSUER_HOST:?falta ISSUER_HOST}"
: "${DESTINO:?falta DESTINO}"
if [ -z "${RESUMO_TOKEN:-}" ]; then
echo "sem o Secret resumo-credentials neste tenant: nada a enviar."
exit 0
fi
i=0
until curl -sS -o /dev/null -m 5 "${KEYCLOAK_URL}" 2>/dev/null; do
i=$((i+1))
if [ "$i" -ge 24 ]; then
echo "ERRO: ${KEYCLOAK_URL} inalcancavel depois de 2 min." >&2
exit 1
fi
sleep 5
done
RESP="$(printf 'grant_type=client_credentials&client_id=operacao&client_secret=%s' \
"${CLIENT_SECRET}" \
| curl -sS --fail-with-body -X POST "${KEYCLOAK_URL}" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'X-Forwarded-Proto: https' \
-H 'X-Forwarded-Port: 443' \
-H "X-Forwarded-Host: ${ISSUER_HOST}" -d @-)"
TOKEN="$(printf '%s' "${RESP}" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')"
if [ -z "${TOKEN}" ]; then
echo "ERRO: nao consegui token pelo client 'operacao'." >&2
exit 1
fi
# Os tokens vao por `-K -` (stdin), e nao na linha de comando: argumento de
# processo aparece em `ps` para quem estiver no no.
CODIGO="$(printf 'header = "Authorization: Bearer %s"\n' "${TOKEN}" \
| curl -sS -K - -o /tmp/resumo.json -w '%{http_code}' http://runtime-operacao/api/resumo-do-tenant)"
if [ "${CODIGO}" != "200" ]; then
echo "ERRO: o runtime-operacao respondeu ${CODIGO} ao resumo." >&2
exit 1
fi
echo "resumo calculado: $(wc -c < /tmp/resumo.json) bytes (so numeros)"
CODIGO="$(printf 'header = "x-atm-resumo-token: %s"\n' "${RESUMO_TOKEN}" \
| curl -sS -K - -X POST "${DESTINO}" -H 'Content-Type: application/json' \
--data-binary @/tmp/resumo.json -o /tmp/saida.json -w '%{http_code}')"
echo "plataforma: ${CODIGO}"
cat /tmp/saida.json; echo
case "${CODIGO}" in
2*) ;;
*) echo "ERRO: a plataforma respondeu ${CODIGO}." >&2; exit 1 ;;
esac
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: resumo-para-a-plataforma
namespace: demo-prod
spec:
# Madrugada, depois da virada do dia em Sao Paulo e fora do horario das
# mensalidades (dia 1, 06h).
schedule: "30 4 * * *"
timeZone: America/Sao_Paulo
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
activeDeadlineSeconds: 600
backoffLimit: 2
template:
spec:
restartPolicy: OnFailure
containers:
- name: enviar
image: docker.io/curlimages/curl:8.9.1
imagePullPolicy: Never
command: ["/bin/sh", "/script/enviar.sh"]
env:
- name: KEYCLOAK_URL
value: http://keycloak.plataforma-prod.svc:8080/realms/athleticmap-2/protocol/openid-connect/token
- name: ISSUER_HOST
value: auth.athleticmap.com
- name: DESTINO
value: https://plataforma.athleticmap.com/api/public/resumo-do-tenant/demo
- name: CLIENT_SECRET
valueFrom:
secretKeyRef:
name: operacao-credentials
key: client-secret
- name: RESUMO_TOKEN
valueFrom:
secretKeyRef:
name: resumo-credentials
key: token
optional: true
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 200m, memory: 64Mi }
volumeMounts:
- name: script
mountPath: /script
volumes:
- name: script
configMap:
name: resumo-cron
+75
View File
@@ -0,0 +1,75 @@
# =============================================================================
# P6.3 — a ELIMINACAO automatica (decisao 3 do dono, 2026-09-28): todo dia, o
# runtime-core elimina quem completou 5 anos do fim, anonimiza o fiscal e grava
# o registro. Nada fica elegivel antes de 2031 — ate la a rodada termina com
# zero eliminados, e e isso que prova que ela esta viva.
#
# PENDENTE DE GRAVACAO NO MOLDE (moldes/consolidado/) E NOS 4 TENANTS ANTIGOS
# (tenants/{acme,demo,escolinha,piloto}/). Este arquivo mora aqui ate o dono
# gravar — o mesmo caminho do 55-cronjobs-financeiro.yaml.
#
# Por que CronJob, e nao @Scheduled no runtime: com mais de uma replica, um
# agendador no processo rodaria a eliminacao duas vezes (o mesmo motivo do
# financeiro). Duas chamadas simultaneas nao duplicam nada — a pessoa e travada
# antes de sair —, mas uma so e o desenho certo.
#
# DEPENDE do ConfigMap `financeiro-cron` (55-cronjobs-financeiro.yaml): usa o
# mesmo `disparar.sh`, o mesmo client `operacao` e o mesmo Secret
# `operacao-credentials`. O runtime-core so aceita esta rota com token do client
# `operacao` (SegurancaDoRuntime) — nenhum usuario humano, nem admin.
#
# Titular que falha -> o endpoint devolve 500 -> o Job falha -> KubeJobFailed.
# Silencio aqui seria descobrir em 2031 que a rotina nunca rodou.
#
# ANTES DE APLICAR: o runtime-core do tenant tem de estar numa imagem com o
# modulo servico-retencao (senao a rota da 404 e o Job falha todo dia).
# Para conferir um tenant sem eliminar nada: `?simular=true` (mesmo caminho,
# transacao desfeita, contagens reais).
# =============================================================================
apiVersion: batch/v1
kind: CronJob
metadata:
name: retencao-eliminacao
namespace: demo-prod
spec:
# 04:40, longe do backup (02:30) e das tarefas do financeiro (:15 e :30).
schedule: "40 4 * * *"
timeZone: America/Sao_Paulo
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
activeDeadlineSeconds: 1800
backoffLimit: 1
template:
spec:
restartPolicy: OnFailure
containers:
- name: disparar
image: docker.io/curlimages/curl:8.9.1
imagePullPolicy: Never
command: ["/bin/sh", "/script/disparar.sh"]
env:
- name: KEYCLOAK_URL
value: http://keycloak.plataforma-prod.svc:8080/realms/athleticmap-2/protocol/openid-connect/token
- name: ISSUER_HOST
value: auth.athleticmap.com
- name: ALVO
value: http://runtime-core/api/retencao/eliminacao?limite=200
- name: CLIENT_SECRET
valueFrom:
secretKeyRef:
name: operacao-credentials
key: client-secret
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 200m, memory: 64Mi }
volumeMounts:
- name: script
mountPath: /script
volumes:
- name: script
configMap:
name: financeiro-cron
defaultMode: 0555
+141
View File
@@ -0,0 +1,141 @@
# =============================================================================
# N16 — o resumo AGREGADO do tenant, entregue a plataforma uma vez por dia, para o
# comprador ver todas as escolinhas dele lado a lado no /minha-conta.
#
# AGUARDANDO GRAVACAO no molde (`moldes/consolidado/` do GitOps) e nos tenants que
# ja existem. Nada aqui foi aplicado.
#
# O desenho: o TENANT calcula (runtime-operacao, GET /api/resumo-do-tenant, so
# contadores e somas) e o TENANT envia (este CronJob). A plataforma nao entra no
# namespace nem no banco do tenant para isto.
#
# 1. token do client `operacao` no realm do tenant (o mesmo das tarefas do
# financeiro: Secret `operacao-credentials`);
# 2. GET http://runtime-operacao/api/resumo-do-tenant (lista de 1 ou 2 meses);
# 3. POST https://plataforma.athleticmap.com/api/public/resumo-do-tenant/escolinha
# com o cabecalho `x-atm-resumo-token` — do Secret `resumo-credentials`, que
# so vale para ESTE tenant (HMAC do nome dele com a chave da plataforma).
#
# A saida para a plataforma e pela internet (443 publico), que a deny-cross-tenant
# ja libera: nao precisa de NetworkPolicy nova.
#
# `optional: true` no resumo-credentials: tenant sem o Secret (chave da plataforma
# ainda nao configurada) roda o job, que termina OK dizendo "sem token" — nao gera
# KubeJobFailed nem gasta a cota de e-mail dos alertas.
# =============================================================================
apiVersion: v1
kind: ConfigMap
metadata:
name: resumo-cron
namespace: escolinha-prod
data:
enviar.sh: |
#!/bin/sh
set -eu
: "${KEYCLOAK_URL:?falta KEYCLOAK_URL}"
: "${CLIENT_SECRET:?falta CLIENT_SECRET}"
: "${ISSUER_HOST:?falta ISSUER_HOST}"
: "${DESTINO:?falta DESTINO}"
if [ -z "${RESUMO_TOKEN:-}" ]; then
echo "sem o Secret resumo-credentials neste tenant: nada a enviar."
exit 0
fi
i=0
until curl -sS -o /dev/null -m 5 "${KEYCLOAK_URL}" 2>/dev/null; do
i=$((i+1))
if [ "$i" -ge 24 ]; then
echo "ERRO: ${KEYCLOAK_URL} inalcancavel depois de 2 min." >&2
exit 1
fi
sleep 5
done
RESP="$(printf 'grant_type=client_credentials&client_id=operacao&client_secret=%s' \
"${CLIENT_SECRET}" \
| curl -sS --fail-with-body -X POST "${KEYCLOAK_URL}" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'X-Forwarded-Proto: https' \
-H 'X-Forwarded-Port: 443' \
-H "X-Forwarded-Host: ${ISSUER_HOST}" -d @-)"
TOKEN="$(printf '%s' "${RESP}" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')"
if [ -z "${TOKEN}" ]; then
echo "ERRO: nao consegui token pelo client 'operacao'." >&2
exit 1
fi
# Os tokens vao por `-K -` (stdin), e nao na linha de comando: argumento de
# processo aparece em `ps` para quem estiver no no.
CODIGO="$(printf 'header = "Authorization: Bearer %s"\n' "${TOKEN}" \
| curl -sS -K - -o /tmp/resumo.json -w '%{http_code}' http://runtime-operacao/api/resumo-do-tenant)"
if [ "${CODIGO}" != "200" ]; then
echo "ERRO: o runtime-operacao respondeu ${CODIGO} ao resumo." >&2
exit 1
fi
echo "resumo calculado: $(wc -c < /tmp/resumo.json) bytes (so numeros)"
CODIGO="$(printf 'header = "x-atm-resumo-token: %s"\n' "${RESUMO_TOKEN}" \
| curl -sS -K - -X POST "${DESTINO}" -H 'Content-Type: application/json' \
--data-binary @/tmp/resumo.json -o /tmp/saida.json -w '%{http_code}')"
echo "plataforma: ${CODIGO}"
cat /tmp/saida.json; echo
case "${CODIGO}" in
2*) ;;
*) echo "ERRO: a plataforma respondeu ${CODIGO}." >&2; exit 1 ;;
esac
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: resumo-para-a-plataforma
namespace: escolinha-prod
spec:
# Madrugada, depois da virada do dia em Sao Paulo e fora do horario das
# mensalidades (dia 1, 06h).
schedule: "30 4 * * *"
timeZone: America/Sao_Paulo
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
activeDeadlineSeconds: 600
backoffLimit: 2
template:
spec:
restartPolicy: OnFailure
containers:
- name: enviar
image: docker.io/curlimages/curl:8.9.1
imagePullPolicy: Never
command: ["/bin/sh", "/script/enviar.sh"]
env:
- name: KEYCLOAK_URL
value: http://keycloak.plataforma-prod.svc:8080/realms/athleticmap-4/protocol/openid-connect/token
- name: ISSUER_HOST
value: auth.athleticmap.com
- name: DESTINO
value: https://plataforma.athleticmap.com/api/public/resumo-do-tenant/escolinha
- name: CLIENT_SECRET
valueFrom:
secretKeyRef:
name: operacao-credentials
key: client-secret
- name: RESUMO_TOKEN
valueFrom:
secretKeyRef:
name: resumo-credentials
key: token
optional: true
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 200m, memory: 64Mi }
volumeMounts:
- name: script
mountPath: /script
volumes:
- name: script
configMap:
name: resumo-cron
@@ -0,0 +1,75 @@
# =============================================================================
# P6.3 — a ELIMINACAO automatica (decisao 3 do dono, 2026-09-28): todo dia, o
# runtime-core elimina quem completou 5 anos do fim, anonimiza o fiscal e grava
# o registro. Nada fica elegivel antes de 2031 — ate la a rodada termina com
# zero eliminados, e e isso que prova que ela esta viva.
#
# PENDENTE DE GRAVACAO NO MOLDE (moldes/consolidado/) E NOS 4 TENANTS ANTIGOS
# (tenants/{acme,demo,escolinha,piloto}/). Este arquivo mora aqui ate o dono
# gravar — o mesmo caminho do 55-cronjobs-financeiro.yaml.
#
# Por que CronJob, e nao @Scheduled no runtime: com mais de uma replica, um
# agendador no processo rodaria a eliminacao duas vezes (o mesmo motivo do
# financeiro). Duas chamadas simultaneas nao duplicam nada — a pessoa e travada
# antes de sair —, mas uma so e o desenho certo.
#
# DEPENDE do ConfigMap `financeiro-cron` (55-cronjobs-financeiro.yaml): usa o
# mesmo `disparar.sh`, o mesmo client `operacao` e o mesmo Secret
# `operacao-credentials`. O runtime-core so aceita esta rota com token do client
# `operacao` (SegurancaDoRuntime) — nenhum usuario humano, nem admin.
#
# Titular que falha -> o endpoint devolve 500 -> o Job falha -> KubeJobFailed.
# Silencio aqui seria descobrir em 2031 que a rotina nunca rodou.
#
# ANTES DE APLICAR: o runtime-core do tenant tem de estar numa imagem com o
# modulo servico-retencao (senao a rota da 404 e o Job falha todo dia).
# Para conferir um tenant sem eliminar nada: `?simular=true` (mesmo caminho,
# transacao desfeita, contagens reais).
# =============================================================================
apiVersion: batch/v1
kind: CronJob
metadata:
name: retencao-eliminacao
namespace: escolinha-prod
spec:
# 04:40, longe do backup (02:30) e das tarefas do financeiro (:15 e :30).
schedule: "40 4 * * *"
timeZone: America/Sao_Paulo
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
activeDeadlineSeconds: 1800
backoffLimit: 1
template:
spec:
restartPolicy: OnFailure
containers:
- name: disparar
image: docker.io/curlimages/curl:8.9.1
imagePullPolicy: Never
command: ["/bin/sh", "/script/disparar.sh"]
env:
- name: KEYCLOAK_URL
value: http://keycloak.plataforma-prod.svc:8080/realms/athleticmap-4/protocol/openid-connect/token
- name: ISSUER_HOST
value: auth.athleticmap.com
- name: ALVO
value: http://runtime-core/api/retencao/eliminacao?limite=200
- name: CLIENT_SECRET
valueFrom:
secretKeyRef:
name: operacao-credentials
key: client-secret
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 200m, memory: 64Mi }
volumeMounts:
- name: script
mountPath: /script
volumes:
- name: script
configMap:
name: financeiro-cron
defaultMode: 0555
+141
View File
@@ -0,0 +1,141 @@
# =============================================================================
# N16 — o resumo AGREGADO do tenant, entregue a plataforma uma vez por dia, para o
# comprador ver todas as escolinhas dele lado a lado no /minha-conta.
#
# AGUARDANDO GRAVACAO no molde (`moldes/consolidado/` do GitOps) e nos tenants que
# ja existem. Nada aqui foi aplicado.
#
# O desenho: o TENANT calcula (runtime-operacao, GET /api/resumo-do-tenant, so
# contadores e somas) e o TENANT envia (este CronJob). A plataforma nao entra no
# namespace nem no banco do tenant para isto.
#
# 1. token do client `operacao` no realm do tenant (o mesmo das tarefas do
# financeiro: Secret `operacao-credentials`);
# 2. GET http://runtime-operacao/api/resumo-do-tenant (lista de 1 ou 2 meses);
# 3. POST https://plataforma.athleticmap.com/api/public/resumo-do-tenant/piloto
# com o cabecalho `x-atm-resumo-token` — do Secret `resumo-credentials`, que
# so vale para ESTE tenant (HMAC do nome dele com a chave da plataforma).
#
# A saida para a plataforma e pela internet (443 publico), que a deny-cross-tenant
# ja libera: nao precisa de NetworkPolicy nova.
#
# `optional: true` no resumo-credentials: tenant sem o Secret (chave da plataforma
# ainda nao configurada) roda o job, que termina OK dizendo "sem token" — nao gera
# KubeJobFailed nem gasta a cota de e-mail dos alertas.
# =============================================================================
apiVersion: v1
kind: ConfigMap
metadata:
name: resumo-cron
namespace: piloto-prod
data:
enviar.sh: |
#!/bin/sh
set -eu
: "${KEYCLOAK_URL:?falta KEYCLOAK_URL}"
: "${CLIENT_SECRET:?falta CLIENT_SECRET}"
: "${ISSUER_HOST:?falta ISSUER_HOST}"
: "${DESTINO:?falta DESTINO}"
if [ -z "${RESUMO_TOKEN:-}" ]; then
echo "sem o Secret resumo-credentials neste tenant: nada a enviar."
exit 0
fi
i=0
until curl -sS -o /dev/null -m 5 "${KEYCLOAK_URL}" 2>/dev/null; do
i=$((i+1))
if [ "$i" -ge 24 ]; then
echo "ERRO: ${KEYCLOAK_URL} inalcancavel depois de 2 min." >&2
exit 1
fi
sleep 5
done
RESP="$(printf 'grant_type=client_credentials&client_id=operacao&client_secret=%s' \
"${CLIENT_SECRET}" \
| curl -sS --fail-with-body -X POST "${KEYCLOAK_URL}" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'X-Forwarded-Proto: https' \
-H 'X-Forwarded-Port: 443' \
-H "X-Forwarded-Host: ${ISSUER_HOST}" -d @-)"
TOKEN="$(printf '%s' "${RESP}" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')"
if [ -z "${TOKEN}" ]; then
echo "ERRO: nao consegui token pelo client 'operacao'." >&2
exit 1
fi
# Os tokens vao por `-K -` (stdin), e nao na linha de comando: argumento de
# processo aparece em `ps` para quem estiver no no.
CODIGO="$(printf 'header = "Authorization: Bearer %s"\n' "${TOKEN}" \
| curl -sS -K - -o /tmp/resumo.json -w '%{http_code}' http://runtime-operacao/api/resumo-do-tenant)"
if [ "${CODIGO}" != "200" ]; then
echo "ERRO: o runtime-operacao respondeu ${CODIGO} ao resumo." >&2
exit 1
fi
echo "resumo calculado: $(wc -c < /tmp/resumo.json) bytes (so numeros)"
CODIGO="$(printf 'header = "x-atm-resumo-token: %s"\n' "${RESUMO_TOKEN}" \
| curl -sS -K - -X POST "${DESTINO}" -H 'Content-Type: application/json' \
--data-binary @/tmp/resumo.json -o /tmp/saida.json -w '%{http_code}')"
echo "plataforma: ${CODIGO}"
cat /tmp/saida.json; echo
case "${CODIGO}" in
2*) ;;
*) echo "ERRO: a plataforma respondeu ${CODIGO}." >&2; exit 1 ;;
esac
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: resumo-para-a-plataforma
namespace: piloto-prod
spec:
# Madrugada, depois da virada do dia em Sao Paulo e fora do horario das
# mensalidades (dia 1, 06h).
schedule: "30 4 * * *"
timeZone: America/Sao_Paulo
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
activeDeadlineSeconds: 600
backoffLimit: 2
template:
spec:
restartPolicy: OnFailure
containers:
- name: enviar
image: docker.io/curlimages/curl:8.9.1
imagePullPolicy: Never
command: ["/bin/sh", "/script/enviar.sh"]
env:
- name: KEYCLOAK_URL
value: http://keycloak.plataforma-prod.svc:8080/realms/athleticmap-1/protocol/openid-connect/token
- name: ISSUER_HOST
value: auth.athleticmap.com
- name: DESTINO
value: https://plataforma.athleticmap.com/api/public/resumo-do-tenant/piloto
- name: CLIENT_SECRET
valueFrom:
secretKeyRef:
name: operacao-credentials
key: client-secret
- name: RESUMO_TOKEN
valueFrom:
secretKeyRef:
name: resumo-credentials
key: token
optional: true
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 200m, memory: 64Mi }
volumeMounts:
- name: script
mountPath: /script
volumes:
- name: script
configMap:
name: resumo-cron
+75
View File
@@ -0,0 +1,75 @@
# =============================================================================
# P6.3 — a ELIMINACAO automatica (decisao 3 do dono, 2026-09-28): todo dia, o
# runtime-core elimina quem completou 5 anos do fim, anonimiza o fiscal e grava
# o registro. Nada fica elegivel antes de 2031 — ate la a rodada termina com
# zero eliminados, e e isso que prova que ela esta viva.
#
# PENDENTE DE GRAVACAO NO MOLDE (moldes/consolidado/) E NOS 4 TENANTS ANTIGOS
# (tenants/{acme,demo,escolinha,piloto}/). Este arquivo mora aqui ate o dono
# gravar — o mesmo caminho do 55-cronjobs-financeiro.yaml.
#
# Por que CronJob, e nao @Scheduled no runtime: com mais de uma replica, um
# agendador no processo rodaria a eliminacao duas vezes (o mesmo motivo do
# financeiro). Duas chamadas simultaneas nao duplicam nada — a pessoa e travada
# antes de sair —, mas uma so e o desenho certo.
#
# DEPENDE do ConfigMap `financeiro-cron` (55-cronjobs-financeiro.yaml): usa o
# mesmo `disparar.sh`, o mesmo client `operacao` e o mesmo Secret
# `operacao-credentials`. O runtime-core so aceita esta rota com token do client
# `operacao` (SegurancaDoRuntime) — nenhum usuario humano, nem admin.
#
# Titular que falha -> o endpoint devolve 500 -> o Job falha -> KubeJobFailed.
# Silencio aqui seria descobrir em 2031 que a rotina nunca rodou.
#
# ANTES DE APLICAR: o runtime-core do tenant tem de estar numa imagem com o
# modulo servico-retencao (senao a rota da 404 e o Job falha todo dia).
# Para conferir um tenant sem eliminar nada: `?simular=true` (mesmo caminho,
# transacao desfeita, contagens reais).
# =============================================================================
apiVersion: batch/v1
kind: CronJob
metadata:
name: retencao-eliminacao
namespace: piloto-prod
spec:
# 04:40, longe do backup (02:30) e das tarefas do financeiro (:15 e :30).
schedule: "40 4 * * *"
timeZone: America/Sao_Paulo
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
activeDeadlineSeconds: 1800
backoffLimit: 1
template:
spec:
restartPolicy: OnFailure
containers:
- name: disparar
image: docker.io/curlimages/curl:8.9.1
imagePullPolicy: Never
command: ["/bin/sh", "/script/disparar.sh"]
env:
- name: KEYCLOAK_URL
value: http://keycloak.plataforma-prod.svc:8080/realms/athleticmap-1/protocol/openid-connect/token
- name: ISSUER_HOST
value: auth.athleticmap.com
- name: ALVO
value: http://runtime-core/api/retencao/eliminacao?limite=200
- name: CLIENT_SECRET
valueFrom:
secretKeyRef:
name: operacao-credentials
key: client-secret
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 200m, memory: 64Mi }
volumeMounts:
- name: script
mountPath: /script
volumes:
- name: script
configMap:
name: financeiro-cron
defaultMode: 0555
@@ -0,0 +1,103 @@
# =============================================================================
# P6.4 — o EXPURGO das trilhas de auditoria com mais de 5 anos, pelo papel de
# banco proprio `expurgo_trilha` (membro de `atm_expurgo_trilha`). Nunca pela
# aplicacao: o usuario `r_<tenant>` e dono dos schemas, e os triggers (cadastro
# V020, financeiro V011) recusam o dono e o superusuario.
#
# PENDENTE DE GRAVACAO em tenants/plataforma/. Mora aqui ate o dono gravar.
#
# UM job para todos os tenants, no namespace da plataforma: e aqui que o
# Postgres compartilhado mora, e a credencial do papel de expurgo nunca entra
# num namespace de tenant (onde roda codigo de aplicacao). Descobre os bancos na
# hora, como o 51-backup-compartilhado: `athleticmap_%`.
#
# ── ANTES DE APLICAR ─────────────────────────────────────────────────────────
#
# 1. O DBA cria os papeis e da os GRANTs: scripts/banco/papel_expurgo_trilha.sql
# (fase `papeis` uma vez; fase `grants` em cada database). Senha com
# `\password expurgo_trilha`, na hora — nunca em arquivo.
# 2. O Secret `pg-expurgo-trilha` (chave `password`) e SELADO a partir dessa
# senha (kubeseal) e gravado como 58-sealed-pg-expurgo-trilha.yaml.
# 3. Tenant cujo banco ainda nao tem a V020/V011 aparece como "sem a funcao":
# nao e erro (nao ha o que expurgar com o trigger antigo), e a fase `grants`
# tem de rodar de novo depois que ele migrar.
# 4. Tenant antigo com Postgres PROPRIO (tenants/<t>/10-postgres.yaml), se
# algum ainda existir, NAO e coberto por este job: precisa do papel e de um
# job apontando para ele.
#
# Semanal: o prazo e de anos, e a primeira linha so vence em 2031.
# =============================================================================
apiVersion: v1
kind: ConfigMap
metadata:
name: expurgo-trilha
namespace: plataforma-prod
data:
expurgar.sh: |
#!/bin/sh
set -u
: "${PGPASSWORD:?falta PGPASSWORD (Secret pg-expurgo-trilha)}"
BANCOS="$(psql -d postgres -Atc "select datname from pg_database where datname like 'athleticmap\_%' order by 1")" \
|| { echo "ERRO: nao consegui listar os bancos." >&2; exit 1; }
[ -n "${BANCOS}" ] || { echo "ERRO: nenhum banco athleticmap_* encontrado." >&2; exit 1; }
FALHOU=0
for DB in ${BANCOS}; do
for SCHEMA in cadastro financeiro; do
TEM="$(psql -d "${DB}" -Atc "select to_regprocedure('${SCHEMA}.expurgar_trilhas_vencidas()') is not null" 2>&1)"
if [ "${TEM}" != "t" ]; then
echo "${DB} ${SCHEMA}: sem a funcao de expurgo (migracao pendente ou sem acesso): ${TEM}"
continue
fi
if SAIDA="$(psql -v ON_ERROR_STOP=1 -d "${DB}" -Atc "select tabela || '=' || apagados from ${SCHEMA}.expurgar_trilhas_vencidas()" 2>&1)"; then
echo "${DB} ${SCHEMA}: $(echo "${SAIDA}" | tr '\n' ' ')"
else
echo "ERRO ${DB} ${SCHEMA}: ${SAIDA}" >&2
FALHOU=1
fi
done
done
exit ${FALHOU}
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: expurgo-trilha
namespace: plataforma-prod
spec:
# Domingo, 05:10 — depois do backup (02:30) e da eliminacao dos tenants (04:40).
schedule: "10 5 * * 0"
timeZone: America/Sao_Paulo
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
activeDeadlineSeconds: 1800
backoffLimit: 1
template:
spec:
restartPolicy: OnFailure
containers:
- name: expurgar
image: postgres:16
imagePullPolicy: IfNotPresent
command: ["/bin/sh", "/script/expurgar.sh"]
env:
# Direto no Postgres, como o backup: o PgBouncer so conhece os
# papeis da aplicacao.
- { name: PGHOST, value: "pg-compartilhado" }
- { name: PGUSER, value: "expurgo_trilha" }
- name: PGPASSWORD
valueFrom:
secretKeyRef: { name: pg-expurgo-trilha, key: password }
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 200m, memory: 128Mi }
volumeMounts:
- name: script
mountPath: /script
volumes:
- name: script
configMap:
name: expurgo-trilha
defaultMode: 0555