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:
@@ -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).
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user