Author SHA1 Message Date
arcodange 65ff6fcc34 feat: ajouter kadans à vault
Helm Charts / Detect changed charts (push) Successful in 1m2s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
2026-07-13 18:55:26 +02:00
arcodange ef9e6c62c2 Merge pull request 'feat(grafana): complète le dashboard de monitoring du pipeline prospection' (#11) from arcodange/prospection-grafana-dashboard into main
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Detect changed charts (push) Successful in 15s
Helm Charts / Application charts pgcat (push) Has been skipped
2026-07-10 08:48:42 +02:00
arcodangeandClaude Opus 4.8 4133396720 feat(grafana): complète le dashboard de monitoring du pipeline prospection
Helm Charts / Detect changed charts (push) Successful in 1m0s
Helm Charts / Detect changed charts (pull_request) Successful in 44s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Aligne le dashboard « Prospection — pipeline BI missions » sur le cahier
des charges de supervision :
- jauge 0–100 pour le meilleur score d'opportunité (au lieu d'une stat)
- nouvelle stat « Offres du brief » (prospection_brief_offres)
- bar charts « Durée par étape » et « Items par étape » sur le dernier run
  (prospection_step_duration_seconds / prospection_step_items)
- cadence quotidienne : refresh 30m et plage par défaut now-7d

Provisionné via le provider Grafana « prospection » (JSON inline dans
grafana/values.yaml), datasource Prometheus (${DS_PROMETHEUS}).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 08:47:58 +02:00
arcodange 5fae1f31db Merge pull request 'fix(alerting): VaultAuth Telegram exige vaultConnectionRef dans le ns tools' (#10) from arcodange/am-telegram-fix into main
Helm Charts / Detect changed charts (push) Successful in 18s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
2026-07-08 23:23:33 +02:00
arcodangeandClaude Opus 4.8 4005d89c0c fix(alerting): VaultAuth Telegram exige vaultConnectionRef dans le ns tools
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Detect changed charts (push) Successful in 16s
Helm Charts / Detect changed charts (pull_request) Successful in 15s
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
VSO refuse un VaultAuth sans vaultConnectionRef dans le ns tools
(« vaultConnectionRef must be set on resources in the "tools" namespace »),
contrairement au ns prospection qui hérite d'une connexion par défaut. On
référence la VaultConnection `default` déjà présente dans tools. Sans ça, le
Secret alertmanager-telegram n'est jamais synchronisé et le pod Alertmanager
reste bloqué sur le montage manquant.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 23:23:07 +02:00
arcodange 7a8c8537a5 Merge pull request 'feat(alerting): livraison Telegram des alertes via Alertmanager' (#9) from arcodange/am-telegram into main
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Detect changed charts (push) Successful in 17s
Helm Charts / Application charts pgcat (push) Has been skipped
2026-07-08 23:20:31 +02:00
arcodangeandClaude Opus 4.8 714e2bc2ab feat(alerting): livraison Telegram des alertes via Alertmanager
Helm Charts / Detect changed charts (push) Successful in 14s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Detect changed charts (pull_request) Successful in 14s
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Les règles d'alerte `prospection` s'évaluaient déjà dans Prometheus mais ne
notifiaient nulle part. On câble Alertmanager pour livrer les alertes sur le bot
Telegram de prospection, via `telegram_configs` natif d'Alertmanager.

- iac Vault : policy read-only `alertmanager-telegram` sur
  kvv2/data/prospection/telegram + rôle k8s `alertmanager` (SA
  prometheus-alertmanager, ns tools).
- VSO : VaultAuth + VaultStaticSecret resynchronisent kvv2/prospection/telegram
  vers un Secret `alertmanager-telegram` dans le ns tools (le Secret prospection
  est namespace-scoped, non réutilisable). rolloutRestartTargets sur le
  StatefulSet Alertmanager.
- prometheus values : lien server -> Alertmanager (prometheus-alertmanager:9093),
  route + receiver `telegram` (bot_token_file monté, chat_id inline, parse_mode
  HTML, send_resolved), et montage du Secret via extraSecretMounts.

Additif : ni grafana ni les règles d'alerte existantes ne sont touchés.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 23:20:00 +02:00
arcodange 1eabae6936 Merge pull request 'fix(grafana): startupProbe pour les migrations SQLite (CrashLoop au rollout)' (#8) from arcodange/grafana-startupprobe into main
Helm Charts / Detect changed charts (push) Successful in 17s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
2026-07-08 20:15:53 +02:00
arcodangeandClaude Opus 4.8 e419cb6307 fix(grafana): startupProbe + liveness grace pour les migrations SQLite
Helm Charts / Detect changed charts (push) Successful in 24s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Helm Charts / Detect changed charts (pull_request) Successful in 17s
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Grafana tourne en SQLite sur emptyDir → migration complète du schéma à chaque
démarrage de pod, > 160 s sur Raspberry Pi. La liveson (initialDelay 60 + 10×10s)
tuait Grafana en pleine migration → CrashLoop du nouveau pod à chaque rollout
(révélé par le rollout du dashboard prospection). Ajoute une startupProbe (~10 min)
et relève failureThreshold de la liveness (filet si le chart n'expose pas startupProbe).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 20:14:56 +02:00
arcodange e9998e3b48 Merge pull request 'feat(monitoring): dashboard Grafana + alertes prospection' (#7) from arcodange/prospection-monitoring into main
Helm Charts / Detect changed charts (push) Successful in 21s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
2026-07-08 20:02:46 +02:00
arcodangeandClaude Opus 4.8 e6fca752a2 feat(monitoring): dashboard Grafana + règles d'alerte prospection
Helm Charts / Detect changed charts (push) Successful in 22s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Helm Charts / Detect changed charts (pull_request) Successful in 21s
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Consomme les métriques poussées par le pipeline prospection au Pushgateway
(job=prospection, déjà scrapé). Additif — n'affecte aucun dashboard existant.

- grafana : provider + dashboard « Prospection — pipeline BI missions » inliné
  (grafana.dashboards.prospection, json) — vue d'ensemble (fraîcheur/statut/durée/
  erreurs/missions/score), collecte par étape (table + historique), modèle de
  données (opportunités A/B, offres/entités), livraison (brief/Telegram) + panneau
  Alertes actives. Inline plutôt que ConfigMap externe : grafana est déployé via un
  HelmChart CRD (tool lib), l'inline évite toute hypothèse de namespace.
- prometheus : groupe d'alertes `prospection` (serverFiles.alerting_rules.yml) —
  RunStale (>25h), RunFailed, StepError, NoOffers, BriefNotSent.

NB : la livraison des alertes (Alertmanager → Telegram) n'est pas câblée dans le
cluster (server.alertmanagers vide, aucun receiver) ; les règles restent visibles
dans Prometheus /alerts + le dashboard. Câblage delivery = décision séparée.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 19:52:29 +02:00
arcodange b4f945c310 Merge pull request 'feat(vault): onboard prospection (gitea_cicd role + ops policy)' (#6) from arcodange/onboard-prospection into main
Helm Charts / Detect changed charts (push) Successful in 46s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
2026-07-08 15:39:57 +02:00
arcodangeandClaude Opus 4.8 31a66884d6 feat(vault): onboard prospection (gitea_cicd role + ops policy)
Helm Charts / Detect changed charts (push) Successful in 5m32s
Helm Charts / Detect changed charts (pull_request) Successful in 1m2s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Ajoute prospection à la liste app_policies → crée le rôle JWT gitea_cicd_prospection
et la policy prospection-ops, requis par le workflow vault.yaml du repo prospection
(qui provisionne le rôle k8s-auth + la policy de lecture kvv2/prospection/*).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 15:37:44 +02:00
arcodange 5b24738dcf Merge pull request 'feat(vault): erp prod runtime may read the shared GCS backup creds (kv_read_paths)' (#5) from claude/erp-backup-vault-read into main
Helm Charts / Detect changed charts (push) Successful in 16s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
2026-06-30 17:40:31 +02:00
arcodangeandClaude Opus 4.7 2953ec3202 feat(vault): erp prod runtime may read the shared GCS backup creds (kv_read_paths)
Helm Charts / Detect changed charts (push) Successful in 21s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Helm Charts / Detect changed charts (pull_request) Successful in 14s
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Adds an optional kv_read_paths list to the app_policy module (default []) so an
app's env=prod runtime policy can read extra kvv2 data paths — e.g. a shared
backup-creds path owned by another app. Plumbed through the root applications
schema + module call (dynamic rule, read+list).

Set for erp: kv_read_paths = ["kvv2/data/longhorn/gcs-backup"], so the dedicated
Dolibarr backup CronJob (erp chart, gated) can read the existing GCS HMAC creds
via its own VaultStaticSecret instead of borrowing the Longhorn secret
cross-namespace or duplicating credentials.

No-op for every other app (default []). Only the `erp` runtime policy gains one
read+list rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-30 16:29:15 +02:00
arcodange 06c5eb4391 Merge pull request 'fix(vault): rename applications.policies → ops_policies (cms CI was silently missing its R2 policy)' (#4) from claude/fix-cms-ops-policies-key into main
Helm Charts / Detect changed charts (push) Failing after 10m13s
Helm Charts / Library charts tool (push) Has been cancelled
Helm Charts / Application charts pgcat (push) Has been cancelled
2026-06-28 19:21:54 +02:00
arcodangeandClaude Opus 4.7 3170a341d1 fix(vault): rename applications.policies field to ops_policies (cms CI was silently missing its R2 policy)
Helm Charts / Detect changed charts (pull_request) Successful in 19s
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Helm Charts / Detect changed charts (push) Failing after 13m59s
Helm Charts / Library charts tool (push) Has been cancelled
Helm Charts / Application charts pgcat (push) Has been cancelled
The `applications` object field was declared `policies` in variables.tf, but
the cms tfvars entry, the runbook (doc/runbooks/new-web-app/03-vault-platform.md),
the guidebook (vibe/guidebooks/tools/secrets-and-vso.md) and the module input
(modules/app_policy variable `ops_policies`) all use the name `ops_policies`.

Because Terraform silently drops unknown attributes when converting a value to
an object() type, cms's `ops_policies = ["factory__cf_r2_arcodange_tf"]` was
discarded and `each.value.policies` fell back to [] — so gitea_cicd_cms never
received the `factory__cf_r2_arcodange_tf` token policy (read on
kvv1/cloudflare/r2/arcodange-tf + kvv1/zoho/self_client, defined in
factory iac/cloudflare.tf). cms CI was missing its Cloudflare R2 Terraform-state
permissions.

Fix at the root: rename the schema field `policies` -> `ops_policies` (and its
single reference main.tf:82 `each.value.policies` -> `each.value.ops_policies`),
aligning the whole chain. This is lower-churn than renaming the tfvars key (the
chosen alternative would also have required fixing the runbook + guidebook, which
both already document `ops_policies`) and prevents the next app created from the
runbook from re-introducing the same silently-dropped key.

Behavioural change: gitea_cicd_cms gains `factory__cf_r2_arcodange_tf` in its
token_policies. No other app sets this field (all default []), so no other role
changes. Reviewer: confirm the R2 policy is the intended grant for cms CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-28 19:18:06 +02:00
arcodange fd93359f2e Merge pull request 'feat(multi-env): Phase D2 — Vault policies for erp-sandbox' (#3) from claude/phaseD-erp-sandbox-vault into main
Helm Charts / Detect changed charts (push) Successful in 1m8s
Helm Charts / Library charts tool (push) Failing after 11m12s
Helm Charts / Application charts pgcat (push) Has been cancelled
2026-06-28 17:19:20 +02:00
arcodangeandClaude Opus 4.7 25569eb29d feat(multi-env): Phase D2 — Vault policies for erp-sandbox
Helm Charts / Detect changed charts (pull_request) Failing after 11m29s
Helm Charts / Detect changed charts (push) Failing after 12m7s
Helm Charts / Library charts tool (push) Has been cancelled
Helm Charts / Application charts pgcat (push) Has been cancelled
Helm Charts / Library charts tool (pull_request) Has been cancelled
Helm Charts / Application charts pgcat (pull_request) Has been cancelled
ADR-0002 Phase D, Vault layer. `erp` gains `envs = ["prod", "sandbox"]`,
which flows into the app_policy module (main.tf:81 `envs = each.value.envs`).
For erp the module now resolves instances = ["erp", "erp-sandbox"], so the
apply:
  - ADDS  vault_policy.app_non_prod["erp-sandbox"] — the runtime policy
          named `erp-sandbox` (read kvv2/data/erp-sandbox/* +
          postgres/creds/erp-sandbox*), consumed by the sandbox pod's VSO.
  - UPDATES vault_policy.ops["erp"] in place — the `erp-ops` CI policy
          gains the erp-sandbox kvv2 data/delete/undelete/destroy/metadata
          rules + the erp-sandbox values in the k8s-role allowed_parameter
          lists, so CI can manage the sandbox instance. The glob rules
          (postgres/roles/erp*, kvv1/cloudflare/erp*, auth/kubernetes/role/erp*)
          already covered erp-sandbox, so they don't change.

No destroy/replace. prod `erp` runtime policy + every other app render
byte-identical (their envs still default to ["prod"]).

Diff kept to the single erp line — the pre-existing cms/crowdsec/plausible
alignment is left as-is on main (not reformatting unrelated entries).

D2 of Phase D. D1 (postgres DB+role) = factory#17 (merged). D3 (erp iac
creds + KV) and D4 (ArgoCD) follow.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-28 17:14:53 +02:00
arcodange 01de97853d Merge pull request 'modules: Phase A of multi-env — add env/envs parameter to app_roles and app_policy' (#2) from claude/multi-env-modules into main
Helm Charts / Detect changed charts (push) Successful in 51s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Reviewed-on: #2
2026-06-19 19:22:29 +02:00
arcodangeandClaude Opus 4.7 a3e121b468 modules: add env/envs parameter to app_roles + app_policy (multi-env)
Helm Charts / Detect changed charts (push) Successful in 23s
Helm Charts / Detect changed charts (pull_request) Successful in 22s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Phase A of the multi-environment evolution agreed in the erp repo design
thread. Both modules gain an optional env coordinate that defaults to
"prod"; by the elision rule, env=prod produces the existing single-env
derived names character-for-character, so every existing app's tofu plan
is a no-op.

app_roles (per-instance module — caller iterates over envs):
- variables.tf: add optional env = "prod"
- main.tf: compute local.instance via elision rule + local.owner_role
  (snake-case <name>_<env>_role for the Postgres owner). The name/env/
  database locals are grouped so fmt keeps the existing `name` alignment
  (no whitespace churn on unchanged keys).
- main.tf: substitute local.name -> local.instance / local.owner_role in
  the dynamic role name, k8s role name, SA bindings, token_policies
- outputs.tf: add env + instance outputs; kvv2_path_prefix derives from
  local.instance (== local.name when env=prod → backwards-compat)

app_policy (per-repo module — accepts list of envs):
- variables.tf: add optional envs = ["prod"]
- main.tf: compute local.instances + local.non_prod_instances; remove the
  now-dead bound_service_account_* alias locals (the allowed_parameter
  blocks build their values from per_instance_sa_* maps instead)
- main.tf: kvv2 ops rules become dynamic blocks iterating local.instances
  in the original order (data, delete, undelete, destroy, metadata), so a
  prod-only app renders a byte-identical policy document
- main.tf: allowed_parameter for bound_service_account_* + token_policies
  use comprehensions over local.instances (1-element → identical to old
  static values for prod-only apps)
- main.tf: keep vault_policy.app (env=prod runtime policy) at its original
  address; add vault_policy.app_non_prod via for_each over non_prod_instances
  (empty set for prod-only apps → no new resources)

Top-level wiring:
- iac/variables.tf: add envs = optional(list(string), ["prod"]) to the
  applications set(object) type
- iac/main.tf: pass envs = each.value.envs to app_policies

Verified: `tofu fmt -check` clean on all touched files, `tofu validate`
passes. Backwards-compat reasoning for the no-op plan is in the PR body.

Phase B (factory postgres iac + argocd + runbook docs) and Phase D
(erp iac/main.tf for_each + activate sandbox) follow in their own PRs.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-15 14:29:11 +02:00
arcodange 023cee3447 🤖 ci(vault): declare dance-lessons-coach JWT role + ops policy (#1)
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Helm Charts / Detect changed charts (push) Successful in 11s
Co-authored-by: Gabriel Radureau <[email protected]>
Co-committed-by: Gabriel Radureau <[email protected]>
2026-05-06 12:55:26 +02:00
11 changed files with 577 additions and 54 deletions
+264 -1
View File
@@ -69,13 +69,26 @@ grafana: &grafana_config
path: /api/health
port: 3000
# Base Grafana en SQLite sur emptyDir → migration complète du schéma à CHAQUE démarrage du
# pod, lente sur Raspberry Pi (> 160 s). Sans garde suffisante, la liveness tuait Grafana en
# pleine migration → CrashLoop au moindre rollout. startupProbe : ~10 min avant d'armer la
# liveness. failureThreshold de liveness relevé aussi (filet de sécurité si le chart n'expose
# pas startupProbe). Fix pérenne : DB persistante (PVC) ou externe (postgres) — hors scope ici.
startupProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 60
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 60
timeoutSeconds: 30
failureThreshold: 10
failureThreshold: 60
## Use an alternate scheduler, e.g. "stork".
## ref: https://kubernetes.io/docs/tasks/administer-cluster/configure-multiple-schedulers/
@@ -703,6 +716,14 @@ grafana: &grafana_config
editable: true
options:
path: /var/lib/grafana/dashboards/grafana-dashboards-kubernetes
- name: 'prospection'
orgId: 1
folder: 'Prospection'
type: file
disableDeletion: false
editable: true
options:
path: /var/lib/grafana/dashboards/prospection
# - name: 'default'
# orgId: 1
@@ -757,6 +778,248 @@ grafana: &grafana_config
k8s-views-pods:
url: https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-views-pods.json
token: ''
prospection:
prospection:
json: |
{
"__inputs": [],
"annotations": { "list": [] },
"editable": true,
"graphTooltip": 1,
"schemaVersion": 39,
"tags": ["prospection", "arcodange"],
"title": "Prospection — pipeline BI missions",
"uid": "prospection-pipeline",
"time": { "from": "now-7d", "to": "now" },
"refresh": "30m",
"templating": {
"list": [
{
"name": "DS_PROMETHEUS",
"label": "Datasource",
"type": "datasource",
"query": "prometheus",
"current": {},
"hide": 0,
"refresh": 1
}
]
},
"panels": [
{
"type": "row", "title": "Vue d'ensemble du dernier run",
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 1, "collapsed": false
},
{
"type": "stat", "title": "Dernier run réussi il y a", "id": 2,
"gridPos": { "h": 4, "w": 5, "x": 0, "y": 1 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [ { "refId": "A", "expr": "time() - prospection_run_timestamp_seconds", "instant": true, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } } ],
"fieldConfig": { "defaults": {
"unit": "s",
"thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "yellow", "value": 86400 }, { "color": "red", "value": 90000 } ] }
}, "overrides": [] },
"options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"] }, "textMode": "auto" }
},
{
"type": "stat", "title": "Statut du run", "id": 3,
"gridPos": { "h": 4, "w": 4, "x": 5, "y": 1 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [ { "refId": "A", "expr": "prospection_run_success", "instant": true, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } } ],
"fieldConfig": { "defaults": {
"mappings": [ { "type": "value", "options": { "0": { "text": "ÉCHEC", "color": "red", "index": 0 }, "1": { "text": "OK", "color": "green", "index": 1 } } } ],
"thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "green", "value": 1 } ] }
}, "overrides": [] },
"options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"] }, "textMode": "auto" }
},
{
"type": "stat", "title": "Durée du run", "id": 4,
"gridPos": { "h": 4, "w": 5, "x": 9, "y": 1 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [ { "refId": "A", "expr": "prospection_run_duration_seconds", "instant": true, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } } ],
"fieldConfig": { "defaults": { "unit": "s", "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "yellow", "value": 1200 }, { "color": "red", "value": 1700 } ] } }, "overrides": [] },
"options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }
},
{
"type": "stat", "title": "Étapes en erreur", "id": 5,
"gridPos": { "h": 4, "w": 4, "x": 14, "y": 1 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [ { "refId": "A", "expr": "prospection_run_errors_total", "instant": true, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } } ],
"fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] } }, "overrides": [] },
"options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"] } }
},
{
"type": "stat", "title": "Missions A qualifiées", "id": 6,
"gridPos": { "h": 4, "w": 3, "x": 18, "y": 1 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [ { "refId": "A", "expr": "prospection_opportunities_total{kind=\"A\"}", "instant": true, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } } ],
"fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "yellow", "value": 1 }, { "color": "green", "value": 10 } ] } }, "overrides": [] },
"options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }
},
{
"type": "gauge", "title": "Meilleur score", "id": 7,
"gridPos": { "h": 4, "w": 3, "x": 21, "y": 1 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [ { "refId": "A", "expr": "prospection_opportunity_top_score", "instant": true, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } } ],
"fieldConfig": { "defaults": { "min": 0, "max": 100, "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "yellow", "value": 65 }, { "color": "green", "value": 85 } ] } }, "overrides": [] },
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "showThresholdLabels": false, "showThresholdMarkers": true }
},
{
"type": "row", "title": "Collecte par étape",
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 }, "id": 8, "collapsed": false
},
{
"type": "table", "title": "Étapes — dernier run", "id": 9,
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 6 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{ "refId": "Statut", "expr": "prospection_step_status", "instant": true, "format": "table", "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } },
{ "refId": "Items", "expr": "prospection_step_items", "instant": true, "format": "table", "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } },
{ "refId": "Durée", "expr": "prospection_step_duration_seconds", "instant": true, "format": "table", "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } }
],
"transformations": [
{ "id": "merge", "options": {} },
{ "id": "organize", "options": {
"excludeByName": { "Time": true, "__name__": true, "job": true, "instance": true },
"renameByName": { "step": "Étape", "Value #Statut": "Statut", "Value #Items": "Items", "Value #Durée": "Durée (s)" },
"indexByName": { "step": 0, "Value #Statut": 1, "Value #Items": 2, "Value #Durée": 3 }
} }
],
"fieldConfig": { "defaults": { "custom": { "align": "auto" } }, "overrides": [
{ "matcher": { "id": "byName", "options": "Statut" }, "properties": [
{ "id": "mappings", "value": [ { "type": "value", "options": { "0": { "text": "⛔ erreur", "color": "red", "index": 0 }, "1": { "text": "✅ ok", "color": "green", "index": 1 }, "2": { "text": "⏭️ skip", "color": "blue", "index": 2 } } } ] },
{ "id": "custom.cellOptions", "value": { "type": "color-text" } }
] },
{ "matcher": { "id": "byName", "options": "Durée (s)" }, "properties": [ { "id": "unit", "value": "s" } ] }
] },
"options": { "showHeader": true, "sortBy": [ { "displayName": "Durée (s)", "desc": true } ] }
},
{
"type": "barchart", "title": "Durée par étape — dernier run", "id": 21,
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 6 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [ { "refId": "A", "expr": "prospection_step_duration_seconds", "instant": true, "format": "table", "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } } ],
"transformations": [ { "id": "organize", "options": { "excludeByName": { "Time": true, "__name__": true, "job": true, "instance": true }, "renameByName": { "step": "Étape", "Value": "Durée (s)" } } } ],
"fieldConfig": { "defaults": { "unit": "s", "color": { "mode": "continuous-GrYlRd" }, "custom": { "lineWidth": 1, "fillOpacity": 80 } }, "overrides": [] },
"options": { "orientation": "horizontal", "xField": "Étape", "showValue": "auto", "legend": { "showLegend": false }, "tooltip": { "mode": "single", "sort": "none" } }
},
{
"type": "barchart", "title": "Items par étape — dernier run", "id": 22,
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 15 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [ { "refId": "A", "expr": "prospection_step_items", "instant": true, "format": "table", "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } } ],
"transformations": [ { "id": "organize", "options": { "excludeByName": { "Time": true, "__name__": true, "job": true, "instance": true }, "renameByName": { "step": "Étape", "Value": "Items" } } } ],
"fieldConfig": { "defaults": { "unit": "short", "color": { "mode": "continuous-BlPu" }, "custom": { "lineWidth": 1, "fillOpacity": 80 } }, "overrides": [] },
"options": { "orientation": "horizontal", "xField": "Étape", "showValue": "auto", "legend": { "showLegend": false }, "tooltip": { "mode": "single", "sort": "none" } }
},
{
"type": "timeseries", "title": "Items collectés par étape (historique)", "id": 10,
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 15 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [ { "refId": "A", "expr": "prospection_step_items > 0", "legendFormat": "{{step}}", "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } } ],
"fieldConfig": { "defaults": { "custom": { "drawStyle": "bars", "fillOpacity": 40, "stacking": { "mode": "none" }, "lineWidth": 1 } }, "overrides": [] },
"options": { "legend": { "displayMode": "table", "placement": "right", "calcs": ["lastNotNull"] }, "tooltip": { "mode": "multi", "sort": "desc" } }
},
{
"type": "row", "title": "Modèle de données & scoring",
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 }, "id": 11, "collapsed": false
},
{
"type": "timeseries", "title": "Opportunités — missions A / cibles B", "id": 12,
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 24 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{ "refId": "A", "expr": "prospection_opportunities_total{kind=\"A\"}", "legendFormat": "Missions A", "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } },
{ "refId": "B", "expr": "prospection_opportunities_total{kind=\"B\"}", "legendFormat": "Cibles B", "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } }
],
"fieldConfig": { "defaults": { "custom": { "drawStyle": "line", "fillOpacity": 10, "lineWidth": 2, "pointSize": 5, "showPoints": "always" } }, "overrides": [] },
"options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }
},
{
"type": "timeseries", "title": "Offres & entités collectées (historique)", "id": 13,
"gridPos": { "h": 8, "w": 8, "x": 12, "y": 24 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{ "refId": "A", "expr": "prospection_offers_total", "legendFormat": "Offres (missions)", "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } },
{ "refId": "B", "expr": "prospection_entities_total", "legendFormat": "Entités (territoire)", "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } },
{ "refId": "C", "expr": "prospection_scores_total", "legendFormat": "Offres scorées", "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } }
],
"fieldConfig": { "defaults": { "custom": { "drawStyle": "line", "fillOpacity": 10, "lineWidth": 2, "showPoints": "auto" } }, "overrides": [] },
"options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }
},
{
"type": "bargauge", "title": "Modèle — dernier run", "id": 14,
"gridPos": { "h": 8, "w": 4, "x": 20, "y": 24 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{ "refId": "A", "expr": "prospection_signals_total", "legendFormat": "Signaux", "instant": true, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } },
{ "refId": "B", "expr": "prospection_entities_total", "legendFormat": "Entités", "instant": true, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } },
{ "refId": "C", "expr": "prospection_offers_total", "legendFormat": "Offres", "instant": true, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } },
{ "refId": "D", "expr": "prospection_scores_total", "legendFormat": "Scorées", "instant": true, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } }
],
"fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [ { "color": "blue", "value": null } ] } }, "overrides": [] },
"options": { "displayMode": "gradient", "orientation": "horizontal", "reduceOptions": { "calcs": ["lastNotNull"] }, "legend": { "showLegend": false } }
},
{
"type": "row", "title": "Livraison & alertes",
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 32 }, "id": 15, "collapsed": false
},
{
"type": "stat", "title": "Brief rendu", "id": 16,
"gridPos": { "h": 5, "w": 3, "x": 0, "y": 33 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [ { "refId": "A", "expr": "prospection_brief_rendered", "instant": true, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } } ],
"fieldConfig": { "defaults": { "mappings": [ { "type": "value", "options": { "0": { "text": "non", "color": "red", "index": 0 }, "1": { "text": "oui", "color": "green", "index": 1 } } } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "green", "value": 1 } ] } }, "overrides": [] },
"options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"] } }
},
{
"type": "stat", "title": "Poussé sur Telegram", "id": 17,
"gridPos": { "h": 5, "w": 4, "x": 3, "y": 33 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [ { "refId": "A", "expr": "prospection_brief_telegram_pushed", "instant": true, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } } ],
"fieldConfig": { "defaults": { "mappings": [ { "type": "value", "options": { "0": { "text": "non", "color": "red", "index": 0 }, "1": { "text": "oui", "color": "green", "index": 1 } } } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "green", "value": 1 } ] } }, "overrides": [] },
"options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"] } }
},
{
"type": "stat", "title": "Messages / mission", "id": 18,
"gridPos": { "h": 5, "w": 3, "x": 7, "y": 33 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [ { "refId": "A", "expr": "prospection_telegram_messages_sent", "instant": true, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } } ],
"fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [ { "color": "blue", "value": null } ] } }, "overrides": [] },
"options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }
},
{
"type": "stat", "title": "Offres du brief", "id": 23,
"gridPos": { "h": 5, "w": 3, "x": 10, "y": 33 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [ { "refId": "A", "expr": "prospection_brief_offres", "instant": true, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } } ],
"fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "green", "value": 1 } ] } }, "overrides": [] },
"options": { "colorMode": "value", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"] } }
},
{
"type": "stat", "title": "Vidéo brief", "id": 19,
"gridPos": { "h": 5, "w": 3, "x": 13, "y": 33 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [ { "refId": "A", "expr": "prospection_brief_video_kb * 1024", "instant": true, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } } ],
"fieldConfig": { "defaults": { "unit": "bytes", "thresholds": { "mode": "absolute", "steps": [ { "color": "blue", "value": null } ] } }, "overrides": [] },
"options": { "colorMode": "value", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"] } }
},
{
"type": "table", "title": "Alertes prospection actives", "id": 20,
"gridPos": { "h": 5, "w": 8, "x": 16, "y": 33 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [ { "refId": "A", "expr": "ALERTS{alertname=~\"Prospection.*\", alertstate=\"firing\"}", "instant": true, "format": "table", "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" } } ],
"transformations": [
{ "id": "organize", "options": {
"excludeByName": { "Time": true, "Value": true, "__name__": true, "job": true, "instance": true, "alertstate": true },
"renameByName": { "alertname": "Alerte", "severity": "Sévérité" }
} }
],
"fieldConfig": { "defaults": { "custom": { "align": "auto" } }, "overrides": [] },
"options": { "showHeader": true, "footer": { "show": false } }
}
]
}
# default:
# some-dashboard:
+32 -7
View File
@@ -74,12 +74,37 @@ resource "vault_kubernetes_auth_backend_role" "vso" {
alias_name_source = "serviceaccount_name"
}
# Alertmanager (ns tools) doit lire le token du bot Telegram de prospection
# pour livrer les alertes. Rôle k8s dédié + policy read-only sur kvv2/prospection/telegram.
data "vault_policy_document" "alertmanager_telegram" {
rule {
path = "kvv2/data/prospection/telegram"
capabilities = ["read"]
}
}
resource "vault_policy" "alertmanager_telegram" {
name = "alertmanager-telegram"
policy = data.vault_policy_document.alertmanager_telegram.hcl
}
resource "vault_kubernetes_auth_backend_role" "alertmanager" {
backend = vault_auth_backend.kubernetes.path
role_name = "alertmanager"
bound_service_account_names = ["prometheus-alertmanager"]
bound_service_account_namespaces = ["tools"]
token_ttl = 3600
token_policies = ["default", vault_policy.alertmanager_telegram.name]
audience = "vault"
alias_name_source = "serviceaccount_name"
}
module "app_policies" {
source = "./modules/app_policy"
for_each = { for app in var.applications : app.name => app }
name = each.value.name
ops_policies = each.value.policies
service_account_names = each.value.service_account_names
service_account_namespaces = each.value.service_account_namespaces
gitea_app_id = var.gitea_app_id
source = "./modules/app_policy"
for_each = { for app in var.applications : app.name => app }
name = each.value.name
envs = each.value.envs
ops_policies = each.value.ops_policies
kv_read_paths = each.value.kv_read_paths
service_account_names = each.value.service_account_names
service_account_namespaces = each.value.service_account_namespaces
gitea_app_id = var.gitea_app_id
}
+82 -25
View File
@@ -6,9 +6,16 @@
# - postgres role
locals {
name = lower(var.name)
bound_service_account_names = concat([var.name], var.service_account_names)
bound_service_account_namespaces = concat([var.name], var.service_account_namespaces)
name = lower(var.name)
envs = [for e in var.envs : lower(e)]
# Elision rule: env=prod → bare name; else <name>-<env>
instances = [for e in local.envs : e == "prod" ? local.name : "${local.name}-${e}"]
non_prod_instances = [for e in local.envs : "${local.name}-${e}" if e != "prod"]
# Per-instance SA name/namespace sets used by the CI policy's allowed_parameter blocks.
per_instance_sa_names = { for inst in local.instances : inst => concat([inst], var.service_account_names) }
per_instance_sa_namespaces = { for inst in local.instances : inst => concat([inst], var.service_account_namespaces) }
}
data "vault_policy_document" "ops" {
@@ -60,41 +67,61 @@ data "vault_policy_document" "ops" {
}
allowed_parameter {
key = "bound_service_account_names"
value = [jsonencode(local.bound_service_account_names)]
value = [for inst in local.instances : jsonencode(local.per_instance_sa_names[inst])]
}
allowed_parameter {
key = "bound_service_account_namespaces"
value = [jsonencode(local.bound_service_account_namespaces)]
value = [for inst in local.instances : jsonencode(local.per_instance_sa_namespaces[inst])]
}
allowed_parameter {
key = "token_policies"
value = [
jsonencode(["default", local.name]),
jsonencode([local.name, "default"])
]
value = flatten([
for inst in local.instances : [
jsonencode(["default", inst]),
jsonencode([inst, "default"])
]
])
}
}
# allow editing app secrets
rule {
path = "kvv2/data/${local.name}/*"
capabilities = ["create", "update", "read", "delete"]
# allow editing app secrets — one rule per (capability × instance) preserves the
# original rule order (data, delete, undelete, destroy, metadata) so prod-only apps
# render a byte-identical policy document (no Vault state diff). Multi-env apps add
# extra rules per non-prod instance.
dynamic "rule" {
for_each = local.instances
content {
path = "kvv2/data/${rule.value}/*"
capabilities = ["create", "update", "read", "delete"]
}
}
rule {
path = "kvv2/delete/${local.name}/*"
capabilities = ["update"]
dynamic "rule" {
for_each = local.instances
content {
path = "kvv2/delete/${rule.value}/*"
capabilities = ["update"]
}
}
rule {
path = "kvv2/undelete/${local.name}/*"
capabilities = ["update"]
dynamic "rule" {
for_each = local.instances
content {
path = "kvv2/undelete/${rule.value}/*"
capabilities = ["update"]
}
}
rule {
path = "kvv2/destroy/${local.name}/*"
capabilities = ["update"]
dynamic "rule" {
for_each = local.instances
content {
path = "kvv2/destroy/${rule.value}/*"
capabilities = ["update"]
}
}
rule {
path = "kvv2/metadata/${local.name}/*"
capabilities = ["read", "list", "delete"]
dynamic "rule" {
for_each = local.instances
content {
path = "kvv2/metadata/${rule.value}/*"
capabilities = ["read", "list", "delete"]
}
}
# allow edit vault role (risky ?)
}
@@ -139,6 +166,9 @@ resource "vault_jwt_auth_backend_role" "gitea_jwt_cicd" {
role_type = "jwt"
}
# Runtime policy for the env=prod instance — kept at its single-env address
# (data.vault_policy_document.app, vault_policy.app, name = local.name) so existing
# state isn't disturbed when this module is upgraded.
data "vault_policy_document" "app" {
rule {
path = "kvv2/data/${local.name}/*"
@@ -148,8 +178,35 @@ data "vault_policy_document" "app" {
path = "postgres/creds/${local.name}*"
capabilities = ["read"]
}
# Extra shared paths this app's prod runtime may read (e.g. backup creds).
dynamic "rule" {
for_each = var.kv_read_paths
content {
path = rule.value
capabilities = ["read", "list"]
}
}
}
resource "vault_policy" "app" {
name = local.name
policy = data.vault_policy_document.app.hcl
}
# Runtime policies for non-prod envs. Each one is named <name>-<env> and reads
# only its own kvv2 + postgres creds paths.
data "vault_policy_document" "app_non_prod" {
for_each = toset(local.non_prod_instances)
rule {
path = "kvv2/data/${each.key}/*"
capabilities = ["read", "list"]
}
rule {
path = "postgres/creds/${each.key}*"
capabilities = ["read"]
}
}
resource "vault_policy" "app_non_prod" {
for_each = toset(local.non_prod_instances)
name = each.key
policy = data.vault_policy_document.app_non_prod[each.key].hcl
}
@@ -1,6 +1,11 @@
variable "name" {
type = string
}
variable "envs" {
type = list(string)
default = ["prod"]
description = "List of environments this app deploys to. The CI policy + JWT role + identity group are created ONCE per repo regardless. One runtime policy is created per env; the env=prod runtime policy keeps its single-env address for backwards compatibility (no state move)."
}
variable "gitea_app_id" {
type = string
}
@@ -18,3 +23,8 @@ variable "service_account_namespaces" {
default = []
description = "var.name will always be included by default - whitelist service account namespaces that can take this policy"
}
variable "kv_read_paths" {
type = list(string)
default = []
description = "Extra kvv2 data paths the env=prod runtime policy may read (read,list) — e.g. a shared backup-creds path owned by another app (kvv2/data/longhorn/gcs-backup). Default none."
}
+16 -8
View File
@@ -4,10 +4,18 @@ data "vault_auth_backend" "kubernetes" {
locals {
name = lower(var.name)
database = var.database == null ? local.name : var.database
env = lower(var.env)
database = var.database == null ? local.instance : var.database
bound_service_account_names = concat([var.name], var.service_account_names)
bound_service_account_namespaces = concat([var.name], var.service_account_namespaces)
# Elision rule (factory runbook conventions.md):
# env == prod → identical to the single-env baseline (no suffix)
# else → kebab-case "<name>-<env>" for K8s/Vault paths.
# Postgres owner role stays snake-case for consistency with the existing "_role" suffix.
instance = local.env == "prod" ? local.name : "${local.name}-${local.env}"
owner_role = local.env == "prod" ? "${local.name}_role" : "${local.name}_${local.env}_role"
bound_service_account_names = concat([local.instance], var.service_account_names)
bound_service_account_namespaces = concat([local.instance], var.service_account_namespaces)
vault_mount_postgres = { path = "postgres" }
vault_mount_kvv2 = { path = "kvv2" }
@@ -20,14 +28,14 @@ moved {
resource "vault_database_secret_backend_role" "role" {
count = var.disable_database ? 0 : 1
backend = local.vault_mount_postgres.path
name = local.name
name = local.instance
db_name = "postgres"
creation_statements = [
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';",
"GRANT ${local.name}_role TO \"{{name}}\";",
"GRANT ${local.owner_role} TO \"{{name}}\";",
]
revocation_statements = [
"REASSIGN OWNED BY \"{{name}}\" TO ${local.name}_role;", # reassign must be executed in the database where the reassgined objects are - TODO (one connection per database/app)
"REASSIGN OWNED BY \"{{name}}\" TO ${local.owner_role};", # reassign must be executed in the database where the reassgined objects are - TODO (one connection per database/app)
"REVOKE ALL ON DATABASE ${local.database} FROM \"{{name}}\";", # should we drop the role ? -> YES after fixing reassign
]
renew_statements = []
@@ -36,11 +44,11 @@ resource "vault_database_secret_backend_role" "role" {
resource "vault_kubernetes_auth_backend_role" "role" {
backend = data.vault_auth_backend.kubernetes.path
role_name = local.name
role_name = local.instance
bound_service_account_names = local.bound_service_account_names
bound_service_account_namespaces = local.bound_service_account_namespaces
token_ttl = 3600
token_policies = ["default", local.name]
token_policies = ["default", local.instance]
audience = "vault"
alias_name_source = "serviceaccount_name"
}
@@ -1,6 +1,13 @@
output "name" {
value = local.name
}
output "env" {
value = local.env
}
output "instance" {
value = local.instance
description = "Derived id by the elision rule: equals name when env=prod, else <name>-<env>."
}
output "database" {
value = local.database
}
@@ -12,5 +19,6 @@ output "mount_paths" {
}
}
output "kvv2_path_prefix" {
value = format("%s/", local.name)
# Identical to format("%s/", local.name) when env=prod (backwards compat).
value = format("%s/", local.instance)
}
@@ -1,6 +1,11 @@
variable "name" {
type = string
}
variable "env" {
type = string
default = "prod"
description = "Deployment environment. By the elision rule (factory runbook conventions.md), env=prod produces names identical to the single-env baseline; non-prod values produce <name>-<env> kebab-case and <name>_<env>_role for the Postgres owner role."
}
variable "database" {
type = string
nullable = true
+12 -5
View File
@@ -1,17 +1,24 @@
applications = [
{ name = "webapp" },
{ name = "erp" },
{
name = "cms"
ops_policies = ["factory__cf_r2_arcodange_tf"]
name = "erp"
envs = ["prod", "sandbox"]
kv_read_paths = ["kvv2/data/longhorn/gcs-backup"] # backup CronJob reads the shared GCS creds
},
{ name = "dance-lessons-coach" },
{
name = "cms"
ops_policies = ["factory__cf_r2_arcodange_tf"]
service_account_names = ["cloudflared"]
},
{
name = "crowdsec"
name = "crowdsec"
service_account_namespaces = ["tools"]
},
{
name = "plausible"
name = "plausible"
service_account_namespaces = ["tools"]
},
{ name = "prospection" },
{ name = "kadans" },
]
+10 -3
View File
@@ -11,9 +11,16 @@ variable "POSTGRES_CREDENTIALS_EDITOR_PASSWORD" {
}
variable "applications" {
type = set(object({
name = string
policies = optional(list(string), [])
service_account_names = optional(list(string), [])
name = string
ops_policies = optional(list(string), [])
service_account_names = optional(list(string), [])
service_account_namespaces = optional(list(string), [])
# Multi-env extension: list of envs this app deploys to. Defaults to ["prod"] for
# every existing app — backwards compatible by the elision rule. Non-prod envs
# produce additional runtime policies named "<name>-<env>".
envs = optional(list(string), ["prod"])
# Extra kvv2 data paths the app's prod runtime policy may read (read,list) —
# e.g. a shared backup-creds path owned by another app. Default none.
kv_read_paths = optional(list(string), [])
}))
}
+48
View File
@@ -0,0 +1,48 @@
# Livraison des alertes Prometheus vers Telegram (bot prospection).
#
# Alertmanager tourne dans le namespace `tools`, mais le token du bot vit dans Vault
# (kvv2/prospection/telegram). Le Secret `prospection-telegram` synchronisé par VSO est
# namespace-scoped (prospection) et non réutilisable ici. On resynchronise donc le même
# chemin kvv2 vers un Secret `alertmanager-telegram` dans `tools`, via un VaultAuth dédié
# (rôle k8s `alertmanager`, provisionné par hashicorp-vault/iac).
#
# NB: ce chart prometheus est en mode `tool.kind: SubChart`, donc les templates
# helm-chart*.yaml ne rendent rien ; ce fichier, lui, est rendu tel quel et appliqué par
# ArgoCD (app `prometheus`, destination namespace `tools`).
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: alertmanager-telegram
namespace: tools
spec:
# Dans le ns tools, VSO exige un vaultConnectionRef explicite (contrairement au ns
# prospection qui hérite d'une connexion par défaut). On pointe la VaultConnection
# `default` déjà présente dans tools (http://hashicorp-vault.tools.svc:8200).
vaultConnectionRef: default
method: kubernetes
mount: kubernetes
kubernetes:
role: alertmanager
serviceAccount: prometheus-alertmanager
audiences:
- vault
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: alertmanager-telegram
namespace: tools
spec:
type: kv-v2
mount: kvv2
path: prospection/telegram
destination:
name: alertmanager-telegram
create: true
refreshAfter: 1h
vaultAuthRef: alertmanager-telegram
# Alertmanager lit le token depuis un fichier monté au démarrage et ne recharge pas à
# chaud un secret monté : on redémarre le StatefulSet quand le token change dans Vault.
rolloutRestartTargets:
- kind: StatefulSet
name: prometheus-alertmanager
+88 -3
View File
@@ -612,8 +612,11 @@ prometheus: &prometheus_config
podLabels: {}
## Prometheus AlertManager configuration
##
alertmanagers: []
## Lien Prometheus -> Alertmanager (service du sous-chart, ns tools).
alertmanagers:
- static_configs:
- targets:
- prometheus-alertmanager:9093
## Use a StatefulSet if replicaCount needs to be greater than 1 (see below)
##
@@ -1115,7 +1118,59 @@ prometheus: &prometheus_config
serverFiles:
## Alerts configuration
## Ref: https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/
alerting_rules.yml: {}
alerting_rules.yml:
groups:
# Pipeline prospection (métriques poussées au Pushgateway job=prospection en fin de
# run). NB : la LIVRAISON des alertes (Alertmanager → Telegram/…) n'est pas encore
# câblée dans ce cluster — ces règles s'évaluent et sont visibles dans Prometheus
# /alerts + le dashboard Grafana « Prospection » (panneau Alertes actives).
- name: prospection
rules:
- alert: ProspectionRunStale
expr: time() - prospection_run_timestamp_seconds > 90000 # > 25 h (cron quotidien)
for: 10m
labels:
severity: warning
app: prospection
annotations:
summary: "Prospection — aucun run réussi depuis plus de 25 h"
description: "Dernier run réussi il y a {{ $value | humanizeDuration }} ; le CronJob quotidien (~06:30 UTC) n'a pas abouti."
- alert: ProspectionRunFailed
expr: prospection_run_success == 0
for: 5m
labels:
severity: warning
app: prospection
annotations:
summary: "Prospection — le dernier run a échoué"
description: "prospection_run_success=0 : toutes les collectes ont échoué au dernier run."
- alert: ProspectionStepError
expr: prospection_step_status == 0
for: 5m
labels:
severity: info
app: prospection
annotations:
summary: "Prospection — étape {{ $labels.step }} en erreur"
description: "L'étape {{ $labels.step }} du pipeline a fini en erreur au dernier run."
- alert: ProspectionNoOffers
expr: prospection_offers_total == 0
for: 15m
labels:
severity: warning
app: prospection
annotations:
summary: "Prospection — 0 offre (mission) collectée"
description: "Aucune offre au dernier run : collecte France Travail / Free-Work potentiellement cassée."
- alert: ProspectionBriefNotSent
expr: prospection_brief_telegram_pushed == 0
for: 15m
labels:
severity: info
app: prospection
annotations:
summary: "Prospection — brief non poussé sur Telegram"
description: "Le brief vidéo n'a pas été diffusé sur Telegram au dernier run."
# groups:
# - name: Instances
# rules:
@@ -1181,6 +1236,36 @@ prometheus: &prometheus_config
##
enabled: true
## Configuration Alertmanager : livraison native Telegram (bot prospection).
## Le token est lu depuis le fichier monté via extraSecretMounts (Secret
## alertmanager-telegram, synchronisé par VSO — cf. templates/vault-telegram.yaml).
## chat_id est public (non sensible), donc inline.
config:
enabled: true
global: {}
templates:
- /etc/alertmanager/*.tmpl
route:
group_by: ["alertname", "app"]
group_wait: 30s
group_interval: 5m
repeat_interval: 3h
receiver: telegram
receivers:
- name: telegram
telegram_configs:
- bot_token_file: /etc/alertmanager/telegram/BOT_TOKEN
chat_id: 7497777082
parse_mode: HTML
send_resolved: true
## Montage du token du bot dans le pod Alertmanager (fichier BOT_TOKEN).
extraSecretMounts:
- name: telegram
mountPath: /etc/alertmanager/telegram
secretName: alertmanager-telegram
readOnly: true
persistence:
## If true, storage will create or use Persistence Volume
## If false, storage will use emptyDir