Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65ff6fcc34 | ||
|
|
ef9e6c62c2 | ||
|
|
4133396720 | ||
|
|
5fae1f31db | ||
|
|
4005d89c0c | ||
|
|
7a8c8537a5 | ||
|
|
714e2bc2ab | ||
|
|
1eabae6936 | ||
|
|
e419cb6307 | ||
|
|
e9998e3b48 | ||
|
|
e6fca752a2 | ||
|
|
b4f945c310 | ||
|
|
31a66884d6 | ||
|
|
5b24738dcf | ||
|
|
2953ec3202 | ||
|
|
06c5eb4391 | ||
|
|
3170a341d1 | ||
|
|
fd93359f2e | ||
|
|
25569eb29d | ||
|
|
01de97853d | ||
|
|
a3e121b468 | ||
|
|
023cee3447 |
+264
-1
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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" },
|
||||
]
|
||||
@@ -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), [])
|
||||
}))
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user