diff --git a/add-on/Postgres_Restore.txt b/add-on/Postgres_Restore.txt new file mode 100644 index 0000000..1b40fe8 --- /dev/null +++ b/add-on/Postgres_Restore.txt @@ -0,0 +1,90 @@ + +kubectl get cluster.postgresql.cnpg.io -n +kubectl get backup -n + + + +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: -restored +spec: + instances: + + storage: + size: + storageClass: csi-rbdfs-sc + + walStorage: + storageClass: csi-rbdfs-sc + size: + + bootstrap: + recovery: + source: -origin + + backup: + name: sporteams-backup-20260718020000 # nome dell'oggetto Backup da kubectl get backup -n + recoveryTarget: # opzionale in assenza applica tutto il journal (wal) a partire dal backup + targetTime: "2026-07-18 15:30:00.000000+00" # timestamp in cui si ferma rebuild dal WAL + + externalClusters: + - name: -origin + barmanObjectStore: + endpointURL: http://minio.minio.svc.cluster.local:9000 + destinationPath: s3://cnpgbackup2/ + s3Credentials: + accessKeyId: + name: minio-backup-creds + key: ACCESS_KEY_ID + secretAccessKey: + name: minio-backup-creds + key: ACCESS_SECRET_KEY + wal: + compression: gzip + maxParallel: 2 + data: + compression: gzip + immediateCheckpoint: false + jobs: 2 + + # Dopo il restore, il cluster ripristinato può avere un backup indipendente: + backup: + barmanObjectStore: + endpointURL: http://minio.minio.svc.cluster.local:9000 + destinationPath: s3://cnpgbackup2/-restored + s3Credentials: + accessKeyId: + name: minio-backup-creds + key: ACCESS_KEY_ID + secretAccessKey: + name: minio-backup-creds + key: ACCESS_SECRET_KEY + wal: + compression: gzip + maxParallel: 2 + data: + compression: gzip + immediateCheckpoint: false + jobs: 2 + retentionPolicy: "30d" + + postgresql: + parameters: + max_connections: "300" + shared_buffers: "1GB" + + resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "2" + memory: "2Gi" + + +### Utility varie ########## +kubectl get cluster.postgresql.cnpg.io -n stsharedservices-dev +kubectl -n minio exec -it minio-0 -- sh +mc alias set local http://localhost:9000 minioadmin KAYQE1QA7uwUZ8uI +mc ls local cnpgbackup2 diff --git a/add-on/monitoring/files/01-kube-prometheus-stack-values.yaml b/add-on/monitoring/files/01-kube-prometheus-stack-values.yaml new file mode 100644 index 0000000..6525f70 --- /dev/null +++ b/add-on/monitoring/files/01-kube-prometheus-stack-values.yaml @@ -0,0 +1,173 @@ +# ===================================================================== +# values.yaml per kube-prometheus-stack su RKE2 (installazione diretta, +# senza Rancher Manager) +# +# Repo: https://prometheus-community.github.io/helm-charts +# Chart: kube-prometheus-stack +# +# Install: +# helm repo add prometheus-community https://prometheus-community.github.io/helm-charts +# helm repo update +# helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \ +# -n monitoring --create-namespace \ +# -f 01-kube-prometheus-stack-values.yaml +# ===================================================================== + +# --------------------------------------------------------------------- +# Prometheus +# --------------------------------------------------------------------- +prometheus: + prometheusSpec: + retention: 15d + retentionSize: "10GB" + + # Adatta alla storageClass disponibile nel tuo cluster RKE2 + # (es. longhorn, local-path-provisioner, ceph-rbd, ecc.) + storageSpec: + volumeClaimTemplate: + spec: + storageClassName: longhorn # <-- CAMBIA con la tua storageClass + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 50Gi + + resources: + requests: + cpu: 250m + memory: 1Gi + limits: + memory: 2Gi + + # Permette a Prometheus di scoprire ServiceMonitor/PodMonitor + # creati in QUALSIASI namespace (importante per uno stack "generico" + # dove le verticali applicative, es. CNPG, vivono in namespace propri) + serviceMonitorSelectorNilUsesHelmValues: false + podMonitorSelectorNilUsesHelmValues: false + ruleSelectorNilUsesHelmValues: false + + # Su RKE2 il control-plane espone metriche ma va abilitato esplicitamente + # lo scraping (vedi sezioni kubeEtcd/kubeControllerManager/kubeScheduler + # più sotto). Se il tuo cluster è "hardened" e non espone questi endpoint + # sui nodi worker, valuta di disabilitare le relative sezioni. + +# --------------------------------------------------------------------- +# Control plane RKE2 — di default questi componenti su RKE2 girano come +# pod statici raggiungibili sul nodo server; verificare le porte reali +# con `kubectl get pods -n kube-system` e `netstat` sul nodo se lo +# scraping non trova i target. +# --------------------------------------------------------------------- +kubeEtcd: + enabled: true + service: + port: 2381 + targetPort: 2381 + +kubeControllerManager: + enabled: true + service: + port: 10257 + targetPort: 10257 + # RKE2 espone il controller-manager in HTTPS con cert self-signed: + # potrebbe servire scrapeConfig con tlsConfig.insecureSkipVerify: true + # (vedi kube-prometheus-stack docs, sezione "kubeControllerManager"). + +kubeScheduler: + enabled: true + service: + port: 10259 + targetPort: 10259 + +# Traefik è deployato di default su RKE2 ed è considerato componente +# interno. Se lo usi come ingress controller, aggiungi un ServiceMonitor +# dedicato puntato al suo endpoint /metrics (di solito porta 9100). +kubeProxy: + enabled: true + +# --------------------------------------------------------------------- +# Alertmanager +# --------------------------------------------------------------------- +alertmanager: + alertmanagerSpec: + retention: 120h + storage: + volumeClaimTemplate: + spec: + storageClassName: longhorn # <-- CAMBIA con la tua storageClass + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 5Gi + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + memory: 256Mi + + # Config di base; per la config completa vedi il file + # 04-alertmanager-config.yaml (AlertmanagerConfig CRD, più gestibile + # in modo dichiarativo/GitOps rispetto a alertmanager.config qui). + +# --------------------------------------------------------------------- +# Grafana (incluso nel chart) +# --------------------------------------------------------------------- +grafana: + enabled: true + defaultDashboardsTimezone: Europe/Rome + + persistence: + enabled: true + storageClassName: longhorn # <-- CAMBIA con la tua storageClass + size: 5Gi + + # Cambia in produzione: usa un Secret invece di plaintext + adminPassword: "CHANGE_ME" + + # Loki come datasource aggiuntivo (vedi file 03-loki-alloy) + additionalDataSources: + - name: Loki + type: loki + access: proxy + url: http://loki.monitoring.svc.cluster.local:3100 + isDefault: false + + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + memory: 256Mi + + # Import automatico dashboard CNPG (ID 20417) via sidecar dashboards + # già incluso nel chart: basta creare un ConfigMap con label + # grafana_dashboard=1 (vedi file 02-cnpg-monitoring.yaml in fondo, + # sezione dashboard, oppure importa manualmente da Grafana UI). + +# --------------------------------------------------------------------- +# node-exporter +# --------------------------------------------------------------------- +nodeExporter: + enabled: true + +prometheus-node-exporter: + resources: + requests: + cpu: 50m + memory: 30Mi + limits: + memory: 50Mi + +# --------------------------------------------------------------------- +# kube-state-metrics +# --------------------------------------------------------------------- +kubeStateMetrics: + enabled: true + +kube-state-metrics: + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + memory: 128Mi diff --git a/add-on/monitoring/files/02-cnpg-monitoring.yaml b/add-on/monitoring/files/02-cnpg-monitoring.yaml new file mode 100644 index 0000000..96c147e --- /dev/null +++ b/add-on/monitoring/files/02-cnpg-monitoring.yaml @@ -0,0 +1,96 @@ +# ===================================================================== +# Monitoring per CloudNativePG (CNPG) +# +# Applica con: kubectl apply -f 02-cnpg-monitoring.yaml +# +# Prerequisiti: +# - CNPG operator già installato (namespace tipico: cnpg-system) +# - kube-prometheus-stack già installato nel namespace "monitoring" +# con podMonitorSelectorNilUsesHelmValues: false (vedi file 01), +# così Prometheus scopre PodMonitor in TUTTI i namespace. +# ===================================================================== + +# --------------------------------------------------------------------- +# 1) PodMonitor per l'operatore CNPG stesso +# (metriche interne: reconcile loop, controller-runtime, porta 8080) +# --------------------------------------------------------------------- +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: cnpg-operator + namespace: cnpg-system + labels: + release: kube-prometheus-stack +spec: + selector: + matchLabels: + app.kubernetes.io/name: cloudnative-pg + podMetricsEndpoints: + - port: metrics + interval: 30s + +--- +# --------------------------------------------------------------------- +# 2) PodMonitor generico per TUTTI i cluster Postgres gestiti da CNPG +# +# Nota: se preferisci il metodo "automatico" (enablePodMonitor: true +# nella risorsa Cluster), questo PodMonitor manuale NON serve per +# quel cluster specifico — ma il manuale è consigliato dalla doc +# ufficiale CNPG perché dà controllo esplicito sul lifecycle, +# indipendente dal singolo Cluster CR. +# +# Il selettore usa la label standard "cnpg.io/cluster" che CNPG +# applica automaticamente a tutti i pod di ogni cluster gestito, +# quindi UN SOLO PodMonitor copre TUTTI i cluster Postgres presenti +# nel namespace target (adatta matchExpressions se i tuoi cluster +# vivono in namespace diversi: serve un PodMonitor per namespace, +# oppure passa a un selector cross-namespace via namespaceSelector). +# --------------------------------------------------------------------- +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: cnpg-clusters + namespace: monitoring + labels: + release: kube-prometheus-stack +spec: + namespaceSelector: + any: true # scropa i pod CNPG in QUALSIASI namespace + selector: + matchExpressions: + - key: cnpg.io/cluster + operator: Exists + podMetricsEndpoints: + - port: metrics # exporter per-istanza, porta 9187 + interval: 30s + scrapeTimeout: 10s + path: /metrics + +--- +# --------------------------------------------------------------------- +# 3) Import automatico dashboard CNPG ufficiale (grafana.com ID 20417) +# tramite sidecar dashboards di Grafana (già incluso nel chart +# kube-prometheus-stack). Basta un ConfigMap con la label +# "grafana_dashboard: '1'" nel namespace di Grafana. +# +# In alternativa più semplice: Grafana UI -> Dashboards -> Import +# -> inserisci ID 20417 -> seleziona datasource Prometheus. +# --------------------------------------------------------------------- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cnpg-grafana-dashboard + namespace: monitoring + labels: + grafana_dashboard: "1" +data: + cnpg-dashboard.json: | + { + "annotations": {"list": []}, + "title": "CloudNativePG (import via ID 20417)", + "__inputs": [], + "__requires": [], + "schemaVersion": 39, + "panels": [], + "_comment": "Placeholder: sostituisci con l'export JSON completo scaricato da https://grafana.com/grafana/dashboards/20417 per avere la dashboard reale, oppure importa manualmente da Grafana UI usando l'ID." + } diff --git a/add-on/monitoring/files/03-alert-rules.yaml b/add-on/monitoring/files/03-alert-rules.yaml new file mode 100644 index 0000000..48b813a --- /dev/null +++ b/add-on/monitoring/files/03-alert-rules.yaml @@ -0,0 +1,205 @@ +# ===================================================================== +# PrometheusRule: alert infrastrutturali + CNPG +# Applica con: kubectl apply -f 03-alert-rules.yaml +# ===================================================================== + +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: infra-alerts + namespace: monitoring + labels: + release: kube-prometheus-stack +spec: + groups: + - name: node-health + rules: + - alert: NodeDown + expr: up{job="node-exporter"} == 0 + for: 5m + labels: + severity: critical + annotations: + summary: "Nodo {{ $labels.instance }} irraggiungibile" + description: "node-exporter non risponde da 5 minuti su {{ $labels.instance }}." + + - alert: NodeHighCPU + expr: 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85 + for: 10m + labels: + severity: warning + annotations: + summary: "CPU alta su {{ $labels.instance }}" + description: "Uso CPU sopra 85% da 10 minuti." + + - alert: NodeHighMemory + expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 90 + for: 10m + labels: + severity: warning + annotations: + summary: "Memoria alta su {{ $labels.instance }}" + description: "Uso memoria sopra 90% da 10 minuti." + + - alert: NodeDiskSpaceLow + expr: (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"}) * 100 < 15 + for: 10m + labels: + severity: warning + annotations: + summary: "Spazio disco basso su {{ $labels.instance }} ({{ $labels.mountpoint }})" + description: "Meno del 15% di spazio libero da 10 minuti." + + - alert: NodeDiskSpaceCritical + expr: (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"}) * 100 < 5 + for: 5m + labels: + severity: critical + annotations: + summary: "Spazio disco CRITICO su {{ $labels.instance }} ({{ $labels.mountpoint }})" + description: "Meno del 5% di spazio libero da 5 minuti." + + - name: kubernetes-health + rules: + - alert: KubePodCrashLooping + expr: increase(kube_pod_container_status_restarts_total[15m]) > 3 + for: 5m + labels: + severity: warning + annotations: + summary: "Pod {{ $labels.namespace }}/{{ $labels.pod }} in crash loop" + description: "Più di 3 restart negli ultimi 15 minuti." + + - alert: KubePodNotReady + expr: sum by (namespace, pod) (kube_pod_status_phase{phase=~"Pending|Unknown"}) > 0 + for: 15m + labels: + severity: warning + annotations: + summary: "Pod {{ $labels.namespace }}/{{ $labels.pod }} non Ready da 15 minuti" + + - alert: KubeDeploymentReplicasMismatch + expr: kube_deployment_spec_replicas != kube_deployment_status_replicas_available + for: 15m + labels: + severity: warning + annotations: + summary: "Deployment {{ $labels.namespace }}/{{ $labels.deployment }} con repliche non allineate" + + - alert: KubePersistentVolumeFillingUp + expr: (kubelet_volume_stats_available_bytes / kubelet_volume_stats_capacity_bytes) * 100 < 10 + for: 10m + labels: + severity: warning + annotations: + summary: "PVC {{ $labels.namespace }}/{{ $labels.persistentvolumeclaim }} quasi pieno" + description: "Meno del 10% di spazio libero sul volume." + + - alert: EtcdInsufficientMembers + expr: sum(up{job="kube-etcd"} == 1) < ((count(up{job="kube-etcd"}) + 1) / 2) + for: 5m + labels: + severity: critical + annotations: + summary: "Quorum etcd a rischio" + description: "Meno della maggioranza dei membri etcd è raggiungibile." + + - name: tls-certificates + rules: + # Richiede blackbox_exporter con probe HTTPS configurato sui tuoi + # endpoint esterni; vedi nota nel file README. + - alert: TLSCertExpiringSoon + expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 15 + for: 1h + labels: + severity: warning + annotations: + summary: "Certificato TLS per {{ $labels.instance }} in scadenza" + description: "Meno di 15 giorni alla scadenza del certificato." + + - alert: TLSCertExpired + expr: probe_ssl_earliest_cert_expiry - time() < 0 + for: 5m + labels: + severity: critical + annotations: + summary: "Certificato TLS per {{ $labels.instance }} SCADUTO" + +--- +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: cnpg-alerts + namespace: monitoring + labels: + release: kube-prometheus-stack +spec: + groups: + - name: cnpg-health + rules: + - alert: CNPGClusterNotHealthy + expr: cnpg_collector_up == 0 + for: 5m + labels: + severity: critical + annotations: + summary: "Cluster CNPG {{ $labels.namespace }}/{{ $labels.pod }} non raggiungibile" + description: "L'exporter CNPG non risponde da 5 minuti." + + - alert: CNPGInstanceInUnexpectedRecovery + expr: cnpg_pg_replication_in_recovery == 1 and on(pod) cnpg_pg_replication_is_wal_receiver_up == 0 + for: 5m + labels: + severity: warning + annotations: + summary: "Istanza {{ $labels.pod }} in recovery inatteso" + description: "Verifica se il ruolo primary/standby è quello atteso." + + - alert: CNPGReplicationLagHigh + expr: cnpg_pg_replication_lag > 300 + for: 5m + labels: + severity: warning + annotations: + summary: "Replication lag alto su {{ $labels.pod }}" + description: "Lag di replica superiore a 300 secondi." + + - alert: CNPGWALArchivingFailing + expr: increase(cnpg_pg_stat_archiver_failed_count[15m]) > 0 + for: 5m + labels: + severity: critical + annotations: + summary: "Archiviazione WAL fallita su {{ $labels.pod }}" + description: "Almeno un fallimento di archiviazione WAL negli ultimi 15 minuti — rischio per i backup e il point-in-time recovery." + + - alert: CNPGBackupFailed + expr: cnpg_collector_last_failed_backup_timestamp > cnpg_collector_last_available_backup_timestamp + for: 5m + labels: + severity: critical + annotations: + summary: "Ultimo backup fallito per {{ $labels.namespace }}/{{ $labels.pod }}" + + - alert: CNPGNoRecentBackup + expr: (time() - cnpg_collector_last_available_backup_timestamp) > 86400 + for: 10m + labels: + severity: warning + annotations: + summary: "Nessun backup riuscito nelle ultime 24h per {{ $labels.namespace }}/{{ $labels.pod }}" + + - alert: CNPGConnectionsNearLimit + expr: (cnpg_backends_total / cnpg_pg_settings_setting{name="max_connections"}) * 100 > 85 + for: 10m + labels: + severity: warning + annotations: + summary: "Connessioni Postgres vicine al limite su {{ $labels.pod }}" + description: "Oltre l'85% di max_connections in uso." + + # Nota: i nomi esatti delle metriche cnpg_* possono variare leggermente + # tra versioni dell'operatore CNPG. Verifica sempre su + # http://localhost:9090/graph con `{__name__=~"cnpg_.+"}` dopo il + # port-forward a Prometheus, e allinea le espressioni se necessario: + # kubectl port-forward -n monitoring svc/kube-prometheus-stack-prometheus 9090 diff --git a/add-on/monitoring/files/04-loki-alloy-values.yaml b/add-on/monitoring/files/04-loki-alloy-values.yaml new file mode 100644 index 0000000..9f7bee2 --- /dev/null +++ b/add-on/monitoring/files/04-loki-alloy-values.yaml @@ -0,0 +1,105 @@ +# ===================================================================== +# Loki + Grafana Alloy — log aggregation +# +# Loki: +# helm repo add grafana https://grafana.github.io/helm-charts +# helm repo update +# helm install loki grafana/loki -n monitoring -f loki-values.yaml +# +# Alloy (raccolta log, DaemonSet su ogni nodo RKE2): +# helm install alloy grafana/alloy -n monitoring -f alloy-values.yaml +# +# Questo file contiene ENTRAMBI i set di values, separati da "---"; +# salvali in due file distinti se preferisci due comandi helm separati. +# ===================================================================== + +# --------------------------------------------------------------------- +# loki-values.yaml (modalità single-binary, adatta a partire in piccolo; +# per HA/produzione a lungo termine valutare la modalità "simple scalable" +# con backend object storage S3-compatibile, es. MinIO) +# --------------------------------------------------------------------- +deploymentMode: SingleBinary + +loki: + auth_enabled: false + commonConfig: + replication_factor: 1 + storage: + type: filesystem + +singleBinary: + replicas: 1 + persistence: + enabled: true + storageClass: longhorn # <-- CAMBIA con la tua storageClass + size: 50Gi + resources: + requests: + cpu: 200m + memory: 512Mi + limits: + memory: 1Gi + +# Disabilita i componenti "scalable" non necessari in modalità SingleBinary +read: + replicas: 0 +write: + replicas: 0 +backend: + replicas: 0 + +gateway: + enabled: false + +test: + enabled: false + +--- +# --------------------------------------------------------------------- +# alloy-values.yaml (DaemonSet che raccoglie log da tutti i nodi/pod +# e li invia a Loki) +# --------------------------------------------------------------------- +alloy: + configMap: + content: | + discovery.kubernetes "pods" { + role = "pod" + } + + discovery.relabel "pods" { + targets = discovery.kubernetes.pods.targets + + rule { + source_labels = ["__meta_kubernetes_namespace"] + target_label = "namespace" + } + rule { + source_labels = ["__meta_kubernetes_pod_name"] + target_label = "pod" + } + rule { + source_labels = ["__meta_kubernetes_pod_container_name"] + target_label = "container" + } + } + + loki.source.kubernetes "pods" { + targets = discovery.relabel.pods.output + forward_to = [loki.write.default.receiver] + } + + loki.write "default" { + endpoint { + url = "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/push" + } + } + +controller: + type: daemonset + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + memory: 256Mi diff --git a/add-on/monitoring/files/05-alertmanager-config.yaml b/add-on/monitoring/files/05-alertmanager-config.yaml new file mode 100644 index 0000000..afa4f14 --- /dev/null +++ b/add-on/monitoring/files/05-alertmanager-config.yaml @@ -0,0 +1,62 @@ +# ===================================================================== +# AlertmanagerConfig — routing alert per severità +# Applica con: kubectl apply -f 05-alertmanager-config.yaml +# +# Richiede i Secret con le credenziali (webhook Slack, integration key +# PagerDuty) creati separatamente, es.: +# +# kubectl create secret generic alertmanager-slack \ +# -n monitoring --from-literal=url='https://hooks.slack.com/services/XXX/YYY/ZZZ' +# +# kubectl create secret generic alertmanager-pagerduty \ +# -n monitoring --from-literal=serviceKey='YOUR_PAGERDUTY_INTEGRATION_KEY' +# ===================================================================== + +apiVersion: monitoring.coreos.com/v1alpha1 +kind: AlertmanagerConfig +metadata: + name: routing-config + namespace: monitoring + labels: + # deve matchare alertmanagerConfigSelector del chart + # (di default kube-prometheus-stack seleziona per release label) + release: kube-prometheus-stack +spec: + route: + groupBy: ["alertname", "namespace"] + groupWait: 30s + groupInterval: 5m + repeatInterval: 4h + receiver: "slack-default" + routes: + - matchers: + - name: severity + value: critical + matchType: "=" + receiver: "pagerduty-critical" + continue: true # invia ANCHE a slack-default per visibilità + - matchers: + - name: severity + value: warning + matchType: "=" + receiver: "slack-default" + + receivers: + - name: "slack-default" + slackConfigs: + - apiURL: + name: alertmanager-slack + key: url + channel: "#alerts-k8s" + sendResolved: true + title: '{{ "{{" }} .CommonAnnotations.summary {{ "}}" }}' + text: '{{ "{{" }} .CommonAnnotations.description {{ "}}" }}' + + - name: "pagerduty-critical" + pagerdutyConfigs: + - serviceKey: + name: alertmanager-pagerduty + key: serviceKey + severity: "critical" + description: '{{ "{{" }} .CommonAnnotations.summary {{ "}}" }}' + sendResolved: true diff --git a/add-on/monitoring/files/README.md b/add-on/monitoring/files/README.md new file mode 100644 index 0000000..a76f8ae --- /dev/null +++ b/add-on/monitoring/files/README.md @@ -0,0 +1,121 @@ +# Stack di monitoring/alerting per RKE2 — CNPG come verticale iniziale + +Stack generico, installato senza passare da Rancher Manager: kube-prometheus-stack +(Prometheus Operator + Prometheus + Alertmanager + Grafana + node-exporter + +kube-state-metrics) + Loki/Alloy per i log + monitoring dedicato per CloudNativePG. + +## Ordine di installazione + +1. **Namespace + kube-prometheus-stack** + ```bash + kubectl create namespace monitoring + + helm repo add prometheus-community https://prometheus-community.github.io/helm-charts + helm repo add grafana https://grafana.github.io/helm-charts + helm repo update + + helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \ + -n monitoring \ + -f 01-kube-prometheus-stack-values.yaml + ``` + +kube-prometheus-stack has been installed. Check its status by running: + kubectl --namespace monitoring get pods -l "release=kube-prometheus-stack" + +Get Grafana 'admin' user password by running: + + kubectl --namespace monitoring get secrets kube-prometheus-stack-grafana -o jsonpath="{.data.admin-password}" | base64 -d ; echo + +Access Grafana local instance: + + export POD_NAME=$(kubectl --namespace monitoring get pod -l "app.kubernetes.io/name=grafana,app.kubernetes.io/instance=kube-prometheus-stack" -oname) + kubectl --namespace monitoring port-forward $POD_NAME 3000 + +Get your grafana admin user password by running: + + kubectl get secret --namespace monitoring -l app.kubernetes.io/component=admin-secret -o jsonpath="{.items[0].data.admin-password}" | base64 --decode ; echo + + +Visit https://github.com/prometheus-operator/kube-prometheus for instructions on how to create & configure Alertmanager and Prometheus instances using the Operator. + + kubectl run -it --rm debug --image=curlimages/curl -n monitoring --restart=Never -- curl -v http://kube-prometheus-stack-grafana.monitoring.svc.cluster.local:80/ + + + + +2. **Monitoring CNPG** (dopo che l'operatore CNPG è già installato) + ```bash + kubectl apply -f 02-cnpg-monitoring.yaml + ``` + +3. **Alert rules** (infra + CNPG) + ```bash + kubectl apply -f 03-alert-rules.yaml + ``` + +4. **Loki + Alloy** (log aggregation) — dividi il file in due se preferisci + comandi helm separati (`loki-values.yaml` / `alloy-values.yaml`) + ```bash + helm install loki grafana/loki -n monitoring -f 04-loki-alloy-values.yaml + helm install alloy grafana/alloy -n monitoring -f 04-loki-alloy-values.yaml + ``` + +5. **Alertmanager routing** — crea prima i Secret con le credenziali reali, + poi applica la config + ```bash + kubectl create secret generic alertmanager-slack \ + -n monitoring --from-literal=url='' + kubectl create secret generic alertmanager-pagerduty \ + -n monitoring --from-literal=serviceKey='' + + kubectl apply -f 05-alertmanager-config.yaml + ``` + +## Cose da personalizzare prima di applicare in un cluster reale + +- **storageClassName** in tutti i file (`longhorn` è solo un placeholder — + sostituisci con la storageClass effettivamente disponibile sul tuo RKE2, + verificabile con `kubectl get storageclass`). +- **adminPassword** di Grafana nel file 01 — spostalo in un Secret, + non lasciarlo in chiaro nei values in produzione. +- **Nomi metriche CNPG** — verifica che corrispondano alla versione esatta + dell'operatore CNPG installata (vedi nota in fondo al file 03). +- **Dashboard CNPG** — il ConfigMap nel file 02 è un placeholder; scarica + il JSON reale da https://grafana.com/grafana/dashboards/20417 e + sostituiscilo, oppure importa manualmente da Grafana UI (Dashboards → + Import → ID 20417). +- **Control-plane RKE2** (kubeEtcd/kubeControllerManager/kubeScheduler nel + file 01) — porte ed endpoint possono variare in base a come RKE2 è + configurato (hardening CIS, profili custom). Verifica con + `kubectl get pods -n kube-system` quali pod statici girano e su quali + porte, e correggi se necessario. +- **AlertmanagerConfig selector** — deve combaciare con + `alertmanagerConfigSelector` impostato dal chart (di default seleziona + per label `release: `); se hai chiamato la release + diversamente da `kube-prometheus-stack`, aggiorna la label. + +## Verifiche post-installazione + +```bash +# Prometheus targets (verifica che CNPG, node-exporter, ecc. siano UP) +kubectl port-forward -n monitoring svc/kube-prometheus-stack-prometheus 9090 +# -> http://localhost:9090/targets + +# Grafana +kubectl port-forward -n monitoring svc/kube-prometheus-stack-grafana 3000:80 +# -> http://localhost:3000 (utente: admin, password: quella impostata) + +# Alertmanager +kubectl port-forward -n monitoring svc/kube-prometheus-stack-alertmanager 9093 +# -> http://localhost:9093 +``` + +## Estensioni future (non incluse in questa prima fase) + +- **blackbox_exporter** — probe HTTP/TCP/TLS per endpoint applicativi + esterni e monitoraggio scadenza certificati (le regole `TLSCert*` nel + file 03 presuppongono questo exporter già installato). +- **Tempo + OpenTelemetry Collector** — tracing distribuito quando ci + saranno più verticali applicative da correlare. +- **Thanos/Mimir** — retention lunga e vista multi-cluster, se in futuro + servirà storicizzare oltre i 15gg locali o aggregare più cluster RKE2. diff --git a/add-on/monitoring/grafana-service.yaml b/add-on/monitoring/grafana-service.yaml new file mode 100644 index 0000000..f96a29f --- /dev/null +++ b/add-on/monitoring/grafana-service.yaml @@ -0,0 +1,38 @@ +apiVersion: v1 +kind: Service +metadata: + name: kube-prometheus-stack-grafana + namespace: monitoring + labels: + app.kubernetes.io/instance: kube-prometheus-stack + app.kubernetes.io/name: grafana +spec: + type: ClusterIP + selector: + app.kubernetes.io/instance: kube-prometheus-stack + app.kubernetes.io/name: grafana + ports: + - name: http + protocol: TCP + port: 80 + targetPort: grafana +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: grafana + namespace: monitoring +spec: + hostnames: + - grafana.internal + parentRefs: + - name: main-gateway + namespace: nginx-gateway + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: kube-prometheus-stack-grafana + port: 80 diff --git a/add-on/monitoring/grafana.yaml b/add-on/monitoring/grafana.yaml new file mode 100644 index 0000000..e69de29 diff --git a/exportManofest.sh b/exportManofest.sh new file mode 100644 index 0000000..fdb6e51 --- /dev/null +++ b/exportManofest.sh @@ -0,0 +1,438 @@ +#!/usr/bin/env bash +# +# export-k8s-manifests.sh +# +# Ricostruisce i manifest YAML dichiarativi delle risorse installate +# su un cluster Kubernetes, per una lista di namespace data in input. +# +# Rimuove i campi "runtime" (status, managedFields, resourceVersion, uid, +# creationTimestamp, ownerReferences generate da controller, ecc.) in modo +# da produrre YAML riapplicabili con `kubectl apply -f`. +# +# Uso: +# ./export-k8s-manifests.sh -n ns1,ns2,ns3 [-o output_dir] [-c kubeconfig] [-k context] +# ./export-k8s-manifests.sh -f namespaces.txt [-o output_dir] +# +# Dipendenze: +# - kubectl (obbligatorio) +# - yq (v4+, https://github.com/mikefarah/yq) — consigliato per pulizia campi +# Se assente, viene usato un fallback con python3. +# +set -euo pipefail + +# ------------------------------------------------------------------ +# Default +# ------------------------------------------------------------------ +OUTPUT_DIR="./k8s-export-$(date +%Y%m%d-%H%M%S)" +NAMESPACES=() +NAMESPACE_FILE="" +KUBECONFIG_OPT=() +CONTEXT_OPT=() +INCLUDE_SECRETS=false + +# Tipi di risorsa namespaced da esportare. +# Personalizza questa lista secondo le tue esigenze. +RESOURCE_TYPES=( + configmap + secret + service + serviceaccount + deployment + statefulset + daemonset + replicaset + job + cronjob + ingress + networkpolicy + poddisruptionbudget + horizontalpodautoscaler + persistentvolumeclaim + role + rolebinding + + # --- API Gateway (namespaced) ------------------------------------- + # Gateway API standard (gateway.networking.k8s.io) + gateway + httproute + grpcroute + tcproute + tlsroute + referencegrant + # Istio + virtualservice + destinationrule + # Traefik + ingressroute + middleware + # Kong + kongingress + kongplugin + kongconsumer + # APISIX + apisixroute + apisixpluginconfig + # NGINX Gateway Fabric — CRD di estensione (gateway.nginx.org) + # Le risorse Gateway API standard (gateway, httproute, gatewayclass, ecc.) + # sono già coperte sopra; queste sono le estensioni specifiche NGF per + # configurazioni non esprimibili nello standard Gateway API. + nginxproxy + clientsettingspolicy + observabilitypolicy + upstreamsettingspolicy + snippetsfilter + + # CRD comuni — decommenta/aggiungi secondo gli operator installati + cluster.postgresql.cnpg.io # CloudNativePG Cluster + scheduledbackup.postgresql.cnpg.io + certificate.cert-manager.io +) + +# Tipi di risorsa CLUSTER-SCOPED da esportare. +# Vengono filtrate per pertinenza ai namespace indicati dove possibile +# (es. ClusterRoleBinding con subject in uno dei namespace, PV con claim +# in uno dei namespace). Le risorse "globali" (StorageClass, GatewayClass, +# IngressClass, PriorityClass) vengono esportate sempre, essendo poche +# e prive di legame diretto con un namespace. +CLUSTER_RESOURCE_TYPES_FILTERED=( + clusterrolebinding + persistentvolume +) +CLUSTER_RESOURCE_TYPES_GLOBAL=( + storageclass + priorityclass + # API Gateway (cluster-scoped) + gatewayclass + ingressclass + # Da valutare se esportare sempre (potenzialmente numerose/rumorose): + # clusterrole + # customresourcedefinition +) + +# ------------------------------------------------------------------ +# Parsing argomenti +# ------------------------------------------------------------------ +usage() { + cat < Lista namespace separati da virgola + -f File con un namespace per riga (alternativo a -n) + -o Directory di output (default: ${OUTPUT_DIR}) + -c Path al kubeconfig da usare + -k Nome del context kubectl da usare + -s Includi anche i Secret (default: esclusi per sicurezza) + -h Mostra questo help +EOF + exit 1 +} + +while getopts "n:f:o:c:k:sh" opt; do + case "$opt" in + n) IFS=',' read -r -a NAMESPACES <<< "$OPTARG" ;; + f) NAMESPACE_FILE="$OPTARG" ;; + o) OUTPUT_DIR="$OPTARG" ;; + c) KUBECONFIG_OPT=(--kubeconfig "$OPTARG") ;; + k) CONTEXT_OPT=(--context "$OPTARG") ;; + s) INCLUDE_SECRETS=true ;; + h) usage ;; + *) usage ;; + esac +done + +if [[ -n "$NAMESPACE_FILE" ]]; then + while IFS= read -r line; do + [[ -z "$line" || "$line" =~ ^# ]] && continue + NAMESPACES+=("$line") + done < "$NAMESPACE_FILE" +fi + +if [[ ${#NAMESPACES[@]} -eq 0 ]]; then + echo "Errore: nessun namespace specificato (usa -n o -f)" >&2 + usage +fi + +if [[ "$INCLUDE_SECRETS" == false ]]; then + RESOURCE_TYPES=("${RESOURCE_TYPES[@]/secret}") +fi + +KCTL=(kubectl "${KUBECONFIG_OPT[@]}" "${CONTEXT_OPT[@]}") + +# ------------------------------------------------------------------ +# Verifica dipendenze +# ------------------------------------------------------------------ +command -v kubectl >/dev/null 2>&1 || { echo "Errore: kubectl non trovato" >&2; exit 1; } + +USE_YQ=false +if command -v yq >/dev/null 2>&1; then + USE_YQ=true +elif command -v python3 >/dev/null 2>&1; then + USE_YQ=false +else + echo "Errore: serve 'yq' oppure 'python3' per la pulizia dei manifest" >&2 + exit 1 +fi + +echo "Verifica connessione al cluster..." +"${KCTL[@]}" cluster-info >/dev/null 2>&1 || { echo "Errore: impossibile contattare il cluster" >&2; exit 1; } +CLUSTER_NAME=$("${KCTL[@]}" config current-context 2>/dev/null || echo "unknown-cluster") +echo "Cluster context: ${CLUSTER_NAME}" +echo "Output dir: ${OUTPUT_DIR}" +echo "Namespace da esportare: ${NAMESPACES[*]}" +echo "Secret inclusi: ${INCLUDE_SECRETS}" +echo + +mkdir -p "$OUTPUT_DIR" + +# Gli script Python vengono scritti su file temporanei perché in bash +# `python3 - <<'HEREDOC'` sovrascrive lo stdin del processo con l'heredoc, +# rendendo impossibile leggere i dati dalla pipeline (il fd 0 è già consumato +# prima che il codice Python arrivi a sys.stdin.read()). +_PY_CLEAN=$(mktemp) +_PY_FILTER=$(mktemp) +trap 'rm -f "$_PY_CLEAN" "$_PY_FILTER"' EXIT + +if [[ "$USE_YQ" == false ]]; then + cat > "$_PY_CLEAN" << 'PYCLEAN' +import sys, yaml +doc = yaml.safe_load(sys.stdin) +if doc is None: + sys.exit(0) +doc.pop("status", None) +md = doc.get("metadata", {}) +for f in ("uid", "resourceVersion", "generation", "creationTimestamp", + "selfLink", "managedFields", "ownerReferences"): + md.pop(f, None) +ann = md.get("annotations") +if ann: + ann.pop("kubectl.kubernetes.io/last-applied-configuration", None) + ann.pop("deployment.kubernetes.io/revision", None) + if not ann: + md.pop("annotations", None) +spec = doc.get("spec", {}) +spec.pop("clusterIP", None) +spec.pop("clusterIPs", None) +tmpl = spec.get("template", {}) +if isinstance(tmpl, dict): + tmpl.get("metadata", {}).pop("creationTimestamp", None) +print(yaml.dump(doc, sort_keys=False, default_flow_style=False), end="") +PYCLEAN + + cat > "$_PY_FILTER" << 'PYFILTER' +import json, os, sys +mode = sys.argv[1] +data = json.load(sys.stdin) +ns_set = set(os.environ.get("NS_CSV", "").split()) +for item in data.get("items", []): + if mode == "subjects": + subs = item.get("subjects") or [] + match = any(s.get("namespace") in ns_set for s in subs) + else: + claim = (item.get("spec") or {}).get("claimRef") or {} + match = claim.get("namespace") in ns_set + if match: + print(item["metadata"]["name"]) +PYFILTER +fi + +# ------------------------------------------------------------------ +# Funzione di pulizia dei campi runtime +# ------------------------------------------------------------------ +clean_manifest_yq() { + # Rimuove campi generati dal cluster, mantenendo lo spec dichiarativo + yq eval ' + del(.status) | + del(.metadata.uid) | + del(.metadata.resourceVersion) | + del(.metadata.generation) | + del(.metadata.creationTimestamp) | + del(.metadata.selfLink) | + del(.metadata.managedFields) | + del(.metadata.annotations."kubectl.kubernetes.io/last-applied-configuration") | + del(.metadata.annotations."deployment.kubernetes.io/revision") | + del(.metadata.ownerReferences) | + del(.spec.clusterIP) | + del(.spec.clusterIPs) | + del(.spec.template.metadata.creationTimestamp) + ' - +} + +clean_manifest_python() { + # Usa il file temporaneo creato all'avvio; l'heredoc inline non funziona + # in pipeline perché sovrascrive lo stdin del processo. + python3 "$_PY_CLEAN" +} + +clean_manifest() { + if [[ "$USE_YQ" == true ]]; then + clean_manifest_yq + else + clean_manifest_python + fi +} + +# ------------------------------------------------------------------ +# Export per namespace / kind / risorsa +# ------------------------------------------------------------------ +SUMMARY_FILE="${OUTPUT_DIR}/EXPORT_SUMMARY.md" +{ + echo "# Export manifest Kubernetes" + echo + echo "- Cluster context: \`${CLUSTER_NAME}\`" + echo "- Data export: $(date -u +'%Y-%m-%dT%H:%M:%SZ')" + echo "- Namespace: ${NAMESPACES[*]}" + echo + echo "| Namespace | Kind | Nome | File |" + echo "|---|---|---|---|" +} > "$SUMMARY_FILE" + +for ns in "${NAMESPACES[@]}"; do + echo "== Namespace: ${ns} ==" + + if ! "${KCTL[@]}" get namespace "$ns" >/dev/null 2>&1; then + echo " ATTENZIONE: namespace '${ns}' non trovato, salto." >&2 + continue + fi + + NS_DIR="${OUTPUT_DIR}/${ns}" + mkdir -p "$NS_DIR" + + for kind in "${RESOURCE_TYPES[@]}"; do + [[ -z "$kind" ]] && continue + + # Il controllo api-resources è omesso: kubectl get 2>/dev/null || true + # gestisce già i resource type non esistenti restituendo NAMES vuoto. + NAMES=$("${KCTL[@]}" -n "$ns" get "$kind" -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true) + [[ -z "$NAMES" ]] && continue + + KIND_DIR="${NS_DIR}/${kind}" + mkdir -p "$KIND_DIR" + + while IFS= read -r name; do + [[ -z "$name" ]] && continue + + # Salta le risorse generate automaticamente (es. default token secret, + # ReplicaSet gestiti da Deployment, Pod gestiti da controller superiori) + if [[ "$kind" == "secret" && "$name" =~ ^default-token- ]]; then + continue + fi + + OUT_FILE="${KIND_DIR}/${name}.yaml" + echo " -> ${kind}/${name}" + + if "${KCTL[@]}" -n "$ns" get "$kind" "$name" -o yaml 2>/dev/null | clean_manifest > "$OUT_FILE"; then + echo "| ${ns} | ${kind} | ${name} | \`${ns}/${kind}/${name}.yaml\` |" >> "$SUMMARY_FILE" + else + echo " ATTENZIONE: export fallito per ${kind}/${name}" >&2 + rm -f "$OUT_FILE" + fi + done <<< "$NAMES" + done +done + +# ------------------------------------------------------------------ +# Export risorse cluster-scoped +# ------------------------------------------------------------------ +echo +echo "== Risorse cluster-scoped ==" + +CLUSTER_DIR="${OUTPUT_DIR}/_cluster-scoped" + +# Cache unica delle risorse disponibili (evita N chiamate a api-resources) +_AVAIL_NAMES=$("${KCTL[@]}" api-resources --no-headers 2>/dev/null | awk '{print tolower($1)}' || true) +_AVAIL_KINDS=$("${KCTL[@]}" api-resources --no-headers 2>/dev/null | awk '{print tolower($NF)}' || true) + +resource_type_exists() { + local kind="$1" + # Controlla sia la colonna NAME (plurale) sia la colonna KIND (singolare) del + # risultato di api-resources per gestire plurali irregolari (es. ingress→ingresses). + echo "$_AVAIL_NAMES" | grep -qx "${kind}s" \ + || echo "$_AVAIL_KINDS" | grep -qx "$kind" +} + +export_cluster_resource() { + local kind="$1" + local name="$2" + local kind_dir="${CLUSTER_DIR}/${kind}" + mkdir -p "$kind_dir" + local out_file="${kind_dir}/${name}.yaml" + + echo " -> ${kind}/${name}" + if "${KCTL[@]}" get "$kind" "$name" -o yaml 2>/dev/null | clean_manifest > "$out_file"; then + echo "| (cluster) | ${kind} | ${name} | \`_cluster-scoped/${kind}/${name}.yaml\` |" >> "$SUMMARY_FILE" + else + echo " ATTENZIONE: export fallito per ${kind}/${name}" >&2 + rm -f "$out_file" + fi +} + +# --- Risorse globali: esportate sempre, senza filtro namespace --- +for kind in "${CLUSTER_RESOURCE_TYPES_GLOBAL[@]}"; do + [[ -z "$kind" ]] && continue + resource_type_exists "$kind" || continue + + NAMES=$("${KCTL[@]}" get "$kind" -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true) + [[ -z "$NAMES" ]] && continue + + while IFS= read -r name; do + [[ -z "$name" ]] && continue + export_cluster_resource "$kind" "$name" + done <<< "$NAMES" +done + +# Filtra gli item di un array JSON kubectl (-o json) tenendo solo quelli +# per cui `jq_path` (letto tramite kubectl jsonpath-like con yq/python) ha +# un valore presente nella lista NAMESPACES. Stampa i nomi corrispondenti. +# $1 = json completo (stdin) $2 = "subjects" | "claimref" +filter_by_namespace_field() { + local mode="$1" + if [[ "$USE_YQ" == true ]]; then + if [[ "$mode" == "subjects" ]]; then + yq eval '.items[] | select((.subjects // []) | any_c(.namespace == env(NS_MATCH))) | .metadata.name' - + else + yq eval '.items[] | select(.spec.claimRef.namespace == env(NS_MATCH)) | .metadata.name' - + fi + else + python3 "$_PY_FILTER" "$mode" + fi +} + +# --- ClusterRoleBinding: solo quelli con subject in uno dei namespace indicati --- +if resource_type_exists clusterrolebinding; then + MATCHED_CRB="" + for ns in "${NAMESPACES[@]}"; do + RESULT=$("${KCTL[@]}" get clusterrolebinding -o json 2>/dev/null \ + | NS_CSV="${NAMESPACES[*]}" NS_MATCH="$ns" filter_by_namespace_field subjects || true) + MATCHED_CRB="${MATCHED_CRB} +${RESULT}" + done + MATCHED_CRB=$(echo "$MATCHED_CRB" | sort -u | sed '/^$/d') + + while IFS= read -r name; do + [[ -z "$name" ]] && continue + export_cluster_resource clusterrolebinding "$name" + done <<< "$MATCHED_CRB" +fi + +# --- PersistentVolume: solo quelli con claimRef in uno dei namespace indicati --- +if resource_type_exists persistentvolume; then + MATCHED_PV="" + for ns in "${NAMESPACES[@]}"; do + RESULT=$("${KCTL[@]}" get persistentvolume -o json 2>/dev/null \ + | NS_CSV="${NAMESPACES[*]}" NS_MATCH="$ns" filter_by_namespace_field claimref || true) + MATCHED_PV="${MATCHED_PV} +${RESULT}" + done + MATCHED_PV=$(echo "$MATCHED_PV" | sort -u | sed '/^$/d') + + while IFS= read -r name; do + [[ -z "$name" ]] && continue + export_cluster_resource persistentvolume "$name" + done <<< "$MATCHED_PV" +fi + +echo +echo "Export completato in: ${OUTPUT_DIR}" +echo "Riepilogo: ${SUMMARY_FILE}" \ No newline at end of file diff --git a/pipeline/addlistener.sh b/pipeline/addlistener.sh index 0eeda21..3893daf 100644 --- a/pipeline/addlistener.sh +++ b/pipeline/addlistener.sh @@ -5,24 +5,26 @@ set -euo pipefail # env e' ignorato: endpoint viene sempre letto dalla chiave "endpoint". ENVIRONMENT="${1:-dev}" PROPERTIES_FILE="${2:-env/${ENVIRONMENT}/values.env}" +# Hardening per evitare errori con set -u in esecuzioni/sourcing anomali +: "${PROPERTIES_FILE:=env/${ENVIRONMENT}/values.env}" if [[ -n "$ENVIRONMENT" ]]; then echo "Ambiente richiesto: $ENVIRONMENT (chiave usata: endpoint)" fi -if [[ ! -f "$PROPERTIES_FILE" ]]; then - echo "Warning: file non trovato: $PROPERTIES_FILE" >&2 +if [[ ! -f "${PROPERTIES_FILE:-}" ]]; then + echo "Warning: file non trovato: ${PROPERTIES_FILE:-}" >&2 exit 0 fi # Estrae endpoint ignorando commenti e spazi, supportando anche endpoint = valore -endpoint_raw="$({ grep -E '^[[:space:]]*endpoint[[:space:]]*=' "$PROPERTIES_FILE" | tail -n1 || true; } | sed -E 's/^[[:space:]]*endpoint[[:space:]]*=[[:space:]]*//')" +endpoint_raw="$({ grep -E '^[[:space:]]*endpoint[[:space:]]*=' "${PROPERTIES_FILE}" | tail -n1 || true; } | sed -E 's/^[[:space:]]*endpoint[[:space:]]*=[[:space:]]*//')" # Rimuove eventuali virgolette e spazi ai bordi endpoint="$(echo "$endpoint_raw" | sed -E 's/^[[:space:]"\x27]+//; s/[[:space:]"\x27]+$//')" if [[ -z "$endpoint" ]]; then - echo "La chiave endpoint non e valorizzata in $PROPERTIES_FILE" >&2 + echo "La chiave endpoint non e valorizzata in ${PROPERTIES_FILE:-}" >&2 exit 0 fi