Author SHA1 Message Date
arcodangeandClaude Opus 4.7 399cf38fb4 style: tofu fmt the two files my multi-env change reformatted
Helm Charts / Detect changed charts (pull_request) Successful in 39s
Helm Charts / Detect changed charts (push) Successful in 42s
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
Whitespace-only. `tofu fmt` realigned two spots that my Phase A edits
shifted:
- app_roles/main.tf: the REASSIGN revocation-statement trailing comment
  re-aligned after the GRANT line gained ${local.owner_role}
- variables.tf: the applications object keys re-aligned after adding the
  longer `envs` key

The two pre-existing unformatted files (factory_auth.tf, terraform.tfvars)
are left as-is — they were already unformatted on origin/main and are
outside this PR's scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-15 14:15:02 +02:00
arcodangeandClaude Opus 4.7 5de9793bdf modules: add env/envs parameter to app_roles + app_policy (multi-env)
Helm Charts / Detect changed charts (push) Successful in 1m18s
Helm Charts / Detect changed charts (pull_request) Successful in 38s
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
should be 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)
- main.tf: substitute local.name -> local.instance in all derived names
  (dynamic role name, k8s role name, SA bindings, token_policies)
- outputs.tf: add env + instance outputs; kvv2_path_prefix now 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
- main.tf: refactor kvv2 ops rules to dynamic blocks iterating local.instances
  preserving the original rule order (data, delete, undelete, destroy,
  metadata) so prod-only apps render a byte-identical policy document
- main.tf: allowed_parameter blocks for k8s role's bound_service_account_*
  and token_policies use comprehensions over local.instances
- main.tf: keep vault_policy.app (the env=prod runtime policy) at its
  original address; add vault_policy.app_non_prod via for_each over
  non_prod_instances for the other envs

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 through to app_policies

`tofu validate` passes. Every existing app's tofu plan should report no
changes because: (1) env="prod" defaults are used everywhere, (2) the
elision rule makes local.instance == local.name for prod, (3) dynamic
rule blocks preserve declaration order, (4) the new app_non_prod resource
is created via for_each over an empty set when no non-prod envs are
declared.

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 13:35:04 +02:00
9 changed files with 24 additions and 462 deletions
+1 -264
View File
@@ -69,26 +69,13 @@ 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: 60
failureThreshold: 10
## Use an alternate scheduler, e.g. "stork".
## ref: https://kubernetes.io/docs/tasks/administer-cluster/configure-multiple-schedulers/
@@ -716,14 +703,6 @@ 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
@@ -778,248 +757,6 @@ 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:
+1 -25
View File
@@ -74,36 +74,12 @@ 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
envs = each.value.envs
ops_policies = each.value.ops_policies
kv_read_paths = each.value.kv_read_paths
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
+6 -10
View File
@@ -8,14 +8,18 @@
locals {
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}"]
instances = [for e in local.envs : e == "prod" ? local.name : "${local.name}-${e}"]
# Non-prod instances only (for the per-env runtime policy iteration that doesn't touch the prod state address)
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) }
# Backwards-compat aliases kept for any caller that referenced these (unused outside the module).
bound_service_account_names = concat([var.name], var.service_account_names)
bound_service_account_namespaces = concat([var.name], var.service_account_namespaces)
}
data "vault_policy_document" "ops" {
@@ -178,14 +182,6 @@ 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
@@ -22,9 +22,4 @@ variable "service_account_namespaces" {
type = list(string)
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."
}
@@ -3,17 +3,17 @@ data "vault_auth_backend" "kubernetes" {
}
locals {
name = lower(var.name)
env = lower(var.env)
database = var.database == null ? local.instance : var.database
name = lower(var.name)
env = lower(var.env)
# 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.
# else → kebab-case "<name>-<env>" for K8s/Vault paths
instance = local.env == "prod" ? local.name : "${local.name}-${local.env}"
# 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"
database = var.database == null ? local.instance : var.database
bound_service_account_names = concat([local.instance], var.service_account_names)
bound_service_account_namespaces = concat([local.instance], var.service_account_namespaces)
+6 -12
View File
@@ -1,24 +1,18 @@
applications = [
{ name = "webapp" },
{
name = "erp"
envs = ["prod", "sandbox"]
kv_read_paths = ["kvv2/data/longhorn/gcs-backup"] # backup CronJob reads the shared GCS creds
},
{ name = "erp" },
{ name = "dance-lessons-coach" },
{
name = "cms"
ops_policies = ["factory__cf_r2_arcodange_tf"]
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" },
]
]
+1 -4
View File
@@ -12,15 +12,12 @@ variable "POSTGRES_CREDENTIALS_EDITOR_PASSWORD" {
variable "applications" {
type = set(object({
name = string
ops_policies = optional(list(string), [])
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
@@ -1,48 +0,0 @@
# 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
+3 -88
View File
@@ -612,11 +612,8 @@ prometheus: &prometheus_config
podLabels: {}
## Prometheus AlertManager configuration
## Lien Prometheus -> Alertmanager (service du sous-chart, ns tools).
alertmanagers:
- static_configs:
- targets:
- prometheus-alertmanager:9093
##
alertmanagers: []
## Use a StatefulSet if replicaCount needs to be greater than 1 (see below)
##
@@ -1118,59 +1115,7 @@ prometheus: &prometheus_config
serverFiles:
## Alerts configuration
## Ref: https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/
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."
alerting_rules.yml: {}
# groups:
# - name: Instances
# rules:
@@ -1236,36 +1181,6 @@ 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