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