Compare commits

..
11 Commits
Author SHA1 Message Date
alessandro barucci 34213918fe varie 2026-10-11 23:49:35 +02:00
alessandro 23f276417f Create IDP-AgentPlatform.pdf 2026-09-27 11:23:06 +02:00
alessandro ff808293aa ff 2026-09-27 11:02:10 +02:00
alessandro 9c0189e08e Merge branch 'main' of https://git.pigreco66.it/gitadmin/italiadatacenter 2026-09-21 10:00:14 +02:00
alessandro d190719c5e varie 2026-09-21 10:00:05 +02:00
alessandro barucci d41d7f40cc Create IDP-AgentPlatform-formattato.docx 2026-09-20 19:39:12 +02:00
alessandro barucci 3a0853dd4c Update IDP-AgentPlatform.docx 2026-09-20 19:34:03 +02:00
alessandro barucci 63649a642a varie 2026-09-20 19:26:05 +02:00
alessandro barucci 48f3f1ff22 varie 2026-09-16 09:25:31 +02:00
alessandro barucci f8ffa9e065 gitops 2026-09-12 19:21:41 +02:00
alessandro 58a443a076 varie 2026-09-11 15:01:40 +02:00
72 changed files with 5727 additions and 51 deletions
@@ -0,0 +1,21 @@
# Dockerfile di esempio — containerizza un flow esportato da Langflow
# come Python app standalone, da usare nel percorso BYO (Esempio B).
FROM python:3.11-slim
WORKDIR /app
# requirements.txt generato/estratto insieme all'export del flow
# (langflow, langgraph-checkpointer-kagent, e le dipendenze del tuo flow)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Codice esportato dal builder low-code + eventuali customizzazioni
# del developer (es. integrazione kagentCheckpointer se usi LangGraph)
COPY . .
# L'agente deve esporre un endpoint compatibile A2A sulla porta
# dichiarata in byo.deployment.port dell'Agent CR
EXPOSE 8080
CMD ["python", "agent_server.py"]
@@ -0,0 +1,154 @@
| | **Langflow** | **Kagent** |
| --------------- | -------------------------------------------- | --------------------------------------- |
| **Fase** | **Build time** | **Run time** |
| **Ruolo** | AI Agent Builder | AI Agent Runtime / Deployment |
| **Funzione** | Creare, configurare e testare agenti AI | Distribuire ed eseguire agenti AI |
| **Interfaccia** | Visuale / low-code | Kubernetes-native / YAML / CRD |
| **Deployment** | Può essere installato come servizio dell'IDP | È parte del runtime Kubernetes dell'IDP |
| **Output** | Flow/agent e relativo artefatto | Agent in esecuzione su Kubernetes |
Quindi:
### Langflow — Build Time
> **Software per progettare, costruire e testare AI Agent.**
Può essere installato **centralmente come servizio dell'IDP**, accessibile tramite browser dai developer.
Il developer può costruire, ad esempio:
```text
LLM
│
├── Knowledge Base
├── MCP Tool
├── API
└── Business Logic
│
▼
Agent
```
Il risultato può poi diventare un **artefatto software versionabile e deployabile**.
---
### Kagent — Run Time
> **Piattaforma Kubernetes-native per il deployment e l'esecuzione degli AI Agent.**
E qui la tua osservazione è importante: ci sono **due modalità**.
#### 1. Agent semplice definito direttamente
Per agenti relativamente semplici, la definizione può essere direttamente nella configurazione Kubernetes/Kagent:
```yaml
kind: Agent
metadata:
name: my-agent
spec:
model: ...
tools:
- ...
```
Quindi:
```text
YAML
↓
Kagent
↓
Agent Runtime
↓
Kubernetes
```
#### 2. Agent sviluppato con Langflow
Per agenti più complessi:
```text
Langflow
│
│ Build
▼
Agent Artifact
│
│ CI/CD
▼
Registry / Git
│
│ Deploy
▼
Kagent
│
▼
Kubernetes
│
▼
Running Agent
```
### La precisazione più importante
Direi però **"artefatti costruiti con Langflow"** piuttosto che:
> "Kagent esegue Langflow"
perché sono due cose diverse.
Kagent **non è il runtime di Langflow**. Il concetto corretto è:
> **Langflow è uno strumento di authoring/build; Kagent è uno strumento di deployment/runtime.**
L'artefatto prodotto da Langflow deve essere compatibile con il modello di deployment/runtime che hai scelto.
---
### Quindi il modello del tuo IDP diventa molto chiaro
```text
IDP
│
┌──────────┴──────────┐
│ │
BUILD TIME RUN TIME
│ │
┌─────────┐ ┌─────────┐
│Langflow │ │ Kagent │
│ │ │ │
│ Build │──────────►│ Deploy │
│ Test │ Artifact │ Execute │
└─────────┘ └────┬────┘
│
▼
Kubernetes
```
E aggiungerei un **terzo livello trasversale**:
```text
IDP GOVERNANCE
┌────────────────────────────┐
│ Security │
│ RBAC │
│ Secrets │
│ Network Policy │
│ Resource Quotas │
│ Approved Models │
│ Approved MCP │
│ Versioning │
│ CI/CD │
│ Observability │
│ Compliance │
└────────────────────────────┘
│
┌─────────┴─────────┐
▼ ▼
Langflow Kagent
Build Time Run Time
```
**Questa secondo me è la rappresentazione più corretta per il tuo progetto IDP:** Langflow e Kagent non sono concorrenti, ma **componenti di due fasi diverse del lifecycle dell'Agent**.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,7 @@
# crea secret con langflow-secrets-setup.sh
# Poi installa con i valori personalizzati
helm install langflow-ide langflow/langflow-ide \
-f langflow-values.yaml \
-n langflow-team-alpha --create-namespace
@@ -0,0 +1,61 @@
# HTTPRoute (Gateway API) per Langflow IDE
# Presuppone un Gateway nginx già esistente nel cluster (nginx Gateway
# Fabric), referenziato come parentRef qui sotto.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: langflow-team-alpha
namespace: langflow-team-alpha
spec:
parentRefs:
- name: nginx-gateway # nome del Gateway condiviso — verifica con:
namespace: nginx-gateway # kubectl get gateway -A
sectionName: https # nome del listener HTTPS sul Gateway
hostnames:
- "langflow-team-alpha.pigreco66.it"
rules:
# UI/editor visuale (frontend) — porta 8080 come da doc Langflow
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: langflow-ide-frontend # verifica nome esatto: kubectl get svc -n langflow-team-alpha
port: 8080
# API backend, se vuoi esporla separatamente (es. per invocazione
# programmatica di un flow senza passare dall'editor) — porta 7860
- matches:
- path:
type: PathPrefix
value: /api
backendRefs:
- name: langflow-ide-backend # verifica nome esatto: kubectl get svc -n langflow-team-alpha
port: 7860
---
# TLS: con Gateway API il certificato si referenzia sul Gateway stesso
# (non sull'HTTPRoute). Se il Gateway condiviso non ha già un listener
# per questo hostname, serve aggiungerlo lì, es.:
#
# apiVersion: gateway.networking.k8s.io/v1
# kind: Gateway
# metadata:
# name: nginx-gateway
# namespace: nginx-gateway
# spec:
# gatewayClassName: nginx
# listeners:
# - name: https
# protocol: HTTPS
# port: 443
# hostname: "*.pigreco66.it"
# tls:
# mode: Terminate
# certificateRefs:
# - name: wildcard-pigreco66-it-tls
# allowedRoutes:
# namespaces:
# from: All
@@ -0,0 +1,35 @@
# Creazione dei Secret richiesti da langflow-values.yaml
# Namespace di esempio: langflow-team-alpha (adatta al tuo tenant)
# 1. Secret connessione database Postgres
kubectl create secret generic langflow-db-secret \
-n langflow-team-alpha \
--from-literal=connection-string="postgresql://user:pass@host:5432/langflow"
# 2. Secret credenziali admin (superuser Langflow)
kubectl create secret generic langflow-admin-secret \
-n langflow-team-alpha \
--from-literal=username="admin" \
--from-literal=password="<password-sicura>"
# 3. Secret credenziali LLM — STESSO Secret già usato per kagent nel
# golden path di onboarding-team: se lo hai già creato per kagent
# in questo namespace, questo passaggio è già fatto, verifica solo
# che contenga le chiavi coi nomi attesi (vedi sotto).
#
# Se non esiste ancora, crealo così (aggiungi/rimuovi provider a
# seconda di cosa serve al team):
kubectl create secret generic llm-credentials \
-n langflow-team-alpha \
--from-literal=openai-api-key="sk-..." \
--from-literal=anthropic-api-key="sk-ant-..."
# Se il Secret esiste già (es. creato per kagent) e vuoi solo
# aggiungere/aggiornare una chiave senza ricrearlo da zero:
kubectl patch secret llm-credentials -n langflow-team-alpha \
--type=json \
-p='[{"op":"add","path":"/data/openai-api-key","value":"'$(echo -n "sk-..." | base64)'"}]'
# 4. Verifica che tutti i Secret siano presenti prima di installare/
# aggiornare la release Helm
kubectl get secrets -n langflow-team-alpha
@@ -0,0 +1,87 @@
# values.yaml — Langflow IDE, personalizzato per un namespace-tenant
# Uso: helm install langflow-ide langflow/langflow-ide -f values.yaml -n <namespace>
langflow:
backend:
image:
repository: langflowai/langflow
tag: "1.10.0" # fissa una versione esplicita, evita 'latest' in produzione
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "1"
memory: 2Gi
# Variabili LLM/DB — usa Secret, mai valori in chiaro qui
env:
- name: LANGFLOW_DATABASE_URL
valueFrom:
secretKeyRef:
name: langflow-db-secret
key: connection-string
- name: LANGFLOW_SUPERUSER
valueFrom:
secretKeyRef:
name: langflow-admin-secret
key: username
- name: LANGFLOW_SUPERUSER_PASSWORD
valueFrom:
secretKeyRef:
name: langflow-admin-secret
key: password
# Rimuove automaticamente eventuali API key salvate nei flow prima
# di persisterli a DB — best practice di sicurezza, seconda linea
# di difesa oltre alle Global Variable qui sotto
- name: LANGFLOW_REMOVE_API_KEYS
value: "true"
# --- Pre-caricamento credenziali LLM come Global Variable ---
# Riusa lo stesso Secret 'llm-credentials' già previsto per kagent
# in fase di onboarding del team (vedi golden path Backstage).
# I developer trovano le chiavi già pronte nel dropdown Global
# Variable, senza mai vederne il valore reale.
- name: LANGFLOW_STORE_ENVIRONMENT_VARIABLES
value: "true"
- name: LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT
value: "OPENAI_API_KEY,ANTHROPIC_API_KEY"
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: llm-credentials
key: openai-api-key
optional: true # non tutti i team useranno tutti i provider
- name: ANTHROPIC_API_KEY
valueFrom:
secretKeyRef:
name: llm-credentials
key: anthropic-api-key
optional: true
frontend:
image:
repository: langflowai/langflow
tag: "1.10.0"
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
# Ingress classico DISABILITATO — l'esposizione avviene via Gateway API
# (nginx Gateway Fabric), vedi manifest separato langflow-httproute.yaml
ingress:
enabled: false
# Il Service del frontend resta ClusterIP (default del chart), sarà
# l'HTTPRoute a instradare il traffico dal Gateway verso questo Service
# Persistenza dei flow salvati (altrimenti persi al riavvio del pod)
persistence:
enabled: true
size: 5Gi
storageClassName: standard # adatta alla tua StorageClass
@@ -0,0 +1,166 @@
# =====================================================================
# ESEMPIO A — Agent dichiarativo nativo (kagent esegue direttamente)
# Caso d'uso: flow Langflow/Flowise semplice — un LLM + un paio di tool,
# nessuna logica custom complessa. Tradotto 1:1 in prompt + tool MCP.
# =====================================================================
apiVersion: kagent.dev/v1alpha2
kind: Agent
metadata:
name: k8s-troubleshooter
namespace: team-alpha
spec:
description: Agente di troubleshooting Kubernetes
type: Declarative
declarative:
modelConfig: gpt4o-config
systemMessage: |
Sei un agente di troubleshooting per Kubernetes...
a2aConfig:
skills:
- id: diagnose-pod-issue
name: Diagnose Pod Issue
description: Investiga problemi su pod/servizi Kubernetes
inputModes: ["text"]
outputModes: ["text"]
tags: ["kubernetes"]
tools:
- type: McpServer
mcpServer:
name: kagent-tool-server
kind: RemoteMCPServer
toolNames:
- k8s_get_pods
- k8s_describe_pod
- k8s_get_logs
---
apiVersion: kagent.dev/v1alpha1
kind: ModelConfig
metadata:
name: gpt4o-config
namespace: team-alpha
spec:
provider: OpenAI
model: gpt-4o
apiKeySecretRef:
name: llm-credentials
key: openai-api-key
# invocazione
Le 4 modalità di invocazione
1. Dashboard kagent (per gli utenti umani, uso interattivo)
bash
kagent dashboard
Apre una UI web dove cerchi k8s-troubleshooter nella lista e chatti direttamente — il modo più immediato per un developer che vuole testare l'agente senza altri strumenti.
2. CLI kagent (per script, pipeline, debug da terminale)
bash
kagent invoke --agent k8s-troubleshooter -n team-alpha \
--task "Il pod payment-service-7d9f in default sta crashando, investiga"
3. Chiamata diretta via A2A REST (per integrazione da altri sistemi/servizi)
Ogni Agent espone un endpoint A2A standard, con un "agent card" che ne descrive le capability:
bash
kubectl port-forward -n kagent svc/kagent-service 8083:8083
# scopri le capability dell'agente
curl localhost:8083/api/a2a/team-alpha/k8s-troubleshooter/.well-known/agent.json
# invocalo
curl -X POST localhost:8083/api/a2a/team-alpha/k8s-troubleshooter/ \
-H "Content-Type: application/json" \
-d '{"task": "Perché il pod payment-service sta in CrashLoopBackOff?"}'
Questa è la via che useresti se, ad esempio, un tuo servizio interno (o un'automazione CI/CD) deve invocare l'agente programmaticamente.
4. Canali chat (Slack/Teams/Discord/Telegram) — via AgentHarness CRD
Se vuoi che i developer interroghino l'agente direttamente da Slack invece che aprire la dashboard, kagent ha una CRD dedicata (AgentHarness) che fa da ponte:
yaml
apiVersion: kagent.dev/v1alpha2
kind: AgentHarness
metadata:
name: k8s-troubleshooter-slack
namespace: kagent
spec:
backend: hermes
modelConfigRef: default-model-config
channels:
- name: platform
type: slack
slack:
botToken:
valueFrom: {type: Secret, name: slack-tokens, key: bot-token}
appToken:
valueFrom: {type: Secret, name: slack-tokens, key: app-token}
Poi in Slack: /mykagent perché il pod X sta crashando? — il bot inoltra la richiesta all'agente via A2A e posta la risposta nel canale. Interessante per un'IDP perché è il canale più naturale per i developer, senza dover imparare dashboard o CLI dedicate.
# =====================================================================
# ESEMPIO B — Agent BYO (Bring Your Own), container esportato da
# Langflow/Flowise dopo containerizzazione custom.
# Caso d'uso: flow complesso con logica di stato/loop che il developer
# ha personalizzato oltre quello che il builder low-code esporta,
# quindi ha bisogno di pieno controllo del codice.
# =====================================================================
apiVersion: kagent.dev/v1alpha1
kind: Agent
metadata:
name: currency-exchange-agent
namespace: team-alpha
spec:
type: BYO
byo:
deployment:
image: ghcr.io/team-alpha/langflow-currency-agent:latest
workingDir: /app
env:
- name: GOOGLE_API_KEY
valueFrom:
secretKeyRef:
name: llm-credentials
key: gemini-api-key
port: 8080 # porta su cui l'agente espone l'endpoint A2A
# Nota: kagent invoca questo agente via protocollo A2A, trattandolo
# come black box — nessuna decomposizione in prompt/tool separati,
# perché la logica vive interamente dentro l'immagine.
## Esempio Agent di Orchestrazione
apiVersion: kagent.dev/v1alpha2
kind: Agent
metadata:
name: incident-commander
namespace: team-alpha
spec:
type: Declarative
declarative:
systemMessage: |
Sei il coordinatore per la gestione incidenti. Quando ricevi una
segnalazione, decidi quali specialisti coinvolgere: se riguarda
pod/servizi K8s, delega al k8s-troubleshooter; se servono i log,
delega al log-analyzer. Puoi chiamare entrambi in sequenza se
il problema lo richiede.
modelConfig: gpt4o-config
tools:
- type: McpServer
mcpServer:
name: kagent-tool-server
toolNames: ["k8s_get_events"]
# Sotto-agenti esposti come "tool" — la scelta di chiamarli
# è dell'LLM di incident-commander, non di kagent
- type: Agent
agent:
name: k8s-troubleshooter
namespace: team-alpha
- type: Agent
agent:
name: log-analyzer
namespace: team-alpha
Binary file not shown.
@@ -0,0 +1,146 @@
# =====================================================================
# ToolServer — Gitea MCP (ufficiale), token con permessi SOLO lettura
# =====================================================================
apiVersion: kagent.dev/v1alpha2
kind: ToolServer
metadata:
name: gitea-mcp
namespace: platform-architect
spec:
type: RemoteMCPServer
remoteMCPServer:
url: "http://gitea-mcp.platform-tools.svc.cluster.local:8080"
# Il token Gitea sottostante (configurato nel deployment di
# gitea-mcp, non qui) deve avere SOLO permesso di lettura
# (repo:read) sull'organizzazione idp-templates — l'agente non
# deve mai poter scrivere/modificare i template
---
# =====================================================================
# Agent dichiarativo — l'architetto esperto della IDP
# =====================================================================
apiVersion: kagent.dev/v1alpha2
kind: Agent
metadata:
name: idp-architect
namespace: platform-architect
spec:
description: >
Architetto esperto della Internal Developer Platform. Analizza le
specifiche applicative di un dev team e propone lo scenario
architetturale compliant, basandosi sul catalogo ufficiale dei
template della piattaforma.
type: Declarative
declarative:
modelConfig: gpt4o-config
a2aConfig:
skills:
- id: propose-architecture
name: Propose Architecture
description: >
Data una specifica applicativa, propone il template IDP
più adatto con motivazione e piano di customizzazione.
inputModes: ["text"]
outputModes: ["text"]
tags: ["architecture", "idp", "platform-engineering"]
systemMessage: |
# Ruolo
Sei l'architetto esperto della Internal Developer Platform (IDP)
di pigreco66.it. Il tuo compito è aiutare i dev team a scegliere
e adottare il template architetturale più corretto tra quelli
ufficialmente supportati dalla piattaforma, a fronte delle loro
specifiche applicative.
Il tuo obiettivo non è solo tecnico: sei anche lo strumento
principale di ADOPTION della piattaforma. Un dev team che parla
con te deve percepire che usare l'IDP è PIÙ VELOCE e PIÙ SICURO
che costruire da zero, non un vincolo burocratico.
# Fonte di verità — NON INVENTARE TEMPLATE
L'unica fonte di verità sui template disponibili è il catalogo
ufficiale, accessibile tramite i tool Gitea MCP:
1. Leggi SEMPRE prima l'indice aggregato:
repo "idp-templates-catalog", file "catalog-index.yaml"
2. Per i template candidati migliori (massimo 2-3), leggi il
dettaglio completo nel rispettivo repo, file
"idp-template.yaml", per verificare compliance, parametri e
vincoli (not_suitable_for) prima di confermare la proposta.
Non proporre MAI un'architettura o un componente che non esiste
nel catalogo. Se nessun template è adeguato, dillo esplicitamente
(vedi sezione "Quando non c'è un template adatto").
# Processo da seguire
1. **Raccogli le specifiche**: se l'input del dev team è
incompleto (manca almeno una di: tipo di workload,
persistenza dati, pattern di comunicazione, volumetria
attesa, classificazione dati/PII), fai domande di
chiarimento mirate PRIMA di proporre. Non indovinare requisiti
critici per sicurezza/compliance.
2. **Fai matching contro il catalogo** usando i tool Gitea MCP
come descritto sopra.
3. **Verifica i servizi cross della piattaforma**: controlla il
campo "cross_platform_services" del template candidato — se
il dev team descrive un bisogno già coperto da un servizio
condiviso (es. "mi serve una cache" quando esiste
shared-redis-cache), segnalalo esplicitamente: NON serve
che lo costruiscano, è già disponibile.
4. **Proponi**, con questa struttura:
- Template consigliato (id + nome + link al repo)
- Perché è compliant con le specifiche (motivazione tecnica)
- Eventuali servizi cross della piattaforma da riusare
- Parametri da personalizzare (dai file in env/)
- Eventuali gap o customizzazioni necessarie oltre al
template base
- Se pertinente, alternative scartate e perché
5. Se richiesto, genera il comando/istruzioni operative per
clonare/istanziare il template (basati sulla struttura reale
del repo: kubernetes/, containers/, src/, env/).
# Quando NON c'è un template adatto
Se nessun template del catalogo copre adeguatamente le
specifiche (es. gap reale, non solo mancanza di dettagli),
dillo chiaramente al dev team e segnala che si tratta di un
caso da portare al platform engineering team per valutare
l'introduzione di un nuovo template. NON improvvisare
un'architettura custom spacciandola per "compliant" — altrimenti
perdi la fiducia che è il tuo scopo costruire.
# Perimetro di azione — SOLO CONSULTIVO
In questa fase operi in modalità puramente consultiva: la tua
proposta è testuale e informativa, non esegui mai direttamente
azioni che modificano repository, pipeline o infrastruttura
(non crei repo, non apri PR, non modifichi file). Se il dev team
ti chiede di "farlo per loro" (es. "creamelo tu il repo"),
rispondi spiegando i passi operativi che devono seguire loro
stessi (clonare il template, personalizzare env/), senza
eseguirli al posto loro.
# Tono
Sei un collega esperto, non un gatekeeper burocratico. Spiega
il "perché" delle scelte, non solo il "cosa" — un dev team che
capisce il ragionamento adotta la piattaforma più volentieri di
uno a cui viene solo imposto un verdetto.
tools:
- type: McpServer
mcpServer:
name: gitea-mcp
kind: RemoteMCPServer
toolNames:
- get_file_content
- get_dir_content
- list_repo_commits
@@ -0,0 +1,22 @@
apiVersion: kagent.dev/v1alpha2
kind: AgentHarness
metadata:
name: idp-architect-slack
namespace: platform-architect
spec:
backend: hermes
agentRef:
name: idp-architect
namespace: platform-architect
channels:
- name: idp-architect-chat
type: slack
slack:
botToken:
valueFrom: { type: Secret, name: slack-tokens, key: bot-token }
appToken:
valueFrom: { type: Secret, name: slack-tokens, key: app-token }
# Canale dedicato consigliato: #idp-architect-support
# così tutti i dev team sanno dove chiedere, e le conversazioni
# restano visibili/searchable per il resto del team (effetto
# knowledge-sharing, non solo 1:1 privato)
@@ -0,0 +1,15 @@
# idp-templates-catalog/catalog-index.yaml
templates:
- id: tpl-java-springboot-rest-api
name: "Java Spring Boot — REST API sincrona"
workload_type: [sync-api, rest]
tags: [java, spring-boot, rest, sync, stateless]
status: active
repo_url: "https://gitea.pigreco66.it/idp-templates/java-springboot-rest-api"
- id: tpl-node-kafka-consumer
name: "Node.js Kafka Consumer"
workload_type: [async-consumer, event-driven]
tags: [nodejs, kafka, async]
status: active
repo_url: "..."
# ... altri 18
@@ -0,0 +1,86 @@
# idp-template.yaml — da posizionare nella root di ogni repo Gitea
# di tipo "template". Questo è l'esempio compilato per un template
# "REST API sincrona Java/Spring Boot".
id: tpl-java-springboot-rest-api
name: "Java Spring Boot — REST API sincrona"
version: "2.1.0"
status: active # active | beta | deprecated
last_updated: "2026-09-01"
maintainer: "platform-engineering@pigreco66.it"
repo_url: "https://gitea.pigreco66.it/idp-templates/java-springboot-rest-api"
# --- Classificazione, usata dall'agente per il primo filtro ---
category: api-service
workload_type:
- sync-api
- rest
tech_stack:
language: Java 21
framework: Spring Boot 3.3
build_tool: Maven
tags: ["java", "spring-boot", "rest", "sync", "stateless"]
description: >
Template per esporre una REST API sincrona stateless. Include
healthcheck, metrics endpoint Prometheus-ready, struttura a layer
controller/service/repository, logging strutturato JSON.
# --- Cosa contiene esattamente la struttura del repo ---
components_included:
kubernetes:
- Deployment (con probe readiness/liveness preconfigurate)
- Service (ClusterIP)
- HorizontalPodAutoscaler
- NetworkPolicy (default-deny + regole minime)
containers:
- Dockerfile multi-stage (build + runtime minimale distroless)
src:
- hello-world Spring Boot funzionante, con un endpoint /health e
un endpoint di esempio /api/v1/ping
env:
- application-dev.properties
- application-staging.properties
- application-prod.properties
# --- Integrazione con servizi cross della piattaforma ---
# Fondamentale: dice all'agente COSA NON riproporre perché già
# disponibile centralmente (evita duplicazione di servizi)
cross_platform_services:
- service: shared-postgres
how: "via env PLATFORM_DB_CONNECTION_STRING, iniettata dalla pipeline"
- service: shared-redis-cache
how: "opzionale, abilitabile via env ENABLE_CACHE=true"
- service: centralized-logging
how: "automatico, nessuna configurazione richiesta"
# --- Vincoli di compliance soddisfatti dal template ---
compliance:
data_classification_supported: ["public", "internal", "confidential"]
# NON supporta dati PII senza customizzazioni aggiuntive
pii_ready: false
network_policy_baseline: "default-deny"
security_scanning: "incluso in pipeline (SAST + container scan)"
scaling:
default_replicas: 2
autoscaling: hpa
min_replicas: 2
max_replicas: 10
# --- Parametri personalizzabili (quelli nei file in env/) ---
parameters:
- name: PLATFORM_DB_CONNECTION_STRING
required: false
description: "Richiesta solo se il servizio necessita persistenza"
- name: ENABLE_CACHE
required: false
default: "false"
# --- Quando NON usare questo template (altrettanto utile del 'quando sì') ---
not_suitable_for:
- "Workload asincroni basati su eventi (usa tpl-java-kafka-consumer)"
- "Batch job schedulati (usa tpl-batch-job)"
- "Dati classificati come PII senza data masking aggiuntivo"
+45
View File
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="210mm" height="297mm">
<defs>
<linearGradient id="b">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="e" x1="49.136964" x2="747.09593" y1="467.2588" y2="467.2588" gradientTransform="scale(.90134 1.10946)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#729fcf"/>
<stop offset="1" stop-color="#204a87"/>
</linearGradient>
<linearGradient id="a">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#729fcf"/>
</linearGradient>
<linearGradient id="f">
<stop offset="0"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="c">
<stop offset="0" stop-color="#3977c3"/>
<stop offset="1" stop-color="#89aedc" stop-opacity="0"/>
</linearGradient>
<linearGradient id="d">
<stop offset="0" stop-color="#729fcf"/>
<stop offset=".31578946" stop-color="#babdb6"/>
<stop offset="1" stop-color="#3465a4"/>
</linearGradient>
<linearGradient xlink:href="#a" id="k" x1="24" x2="24" y1="16.525082" y2="13.284962" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#b" id="l" x1="16.07143" x2="15.785715" y1="19.5" y2="30" gradientTransform="translate(-152.46168 8.8865747) scale(21.1429)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#c" id="j" x1="17.160095" x2="37.969398" y1="14.004482" y2="56.575912" gradientTransform="translate(-149.44117 -46.991167) scale(21.1429)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#d" id="h" x1="9.3030529" x2="38.826412" y1="26.71875" y2="26.71875" gradientTransform="translate(-149.44117 -46.991167) scale(21.1429)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#e" id="i" x1="49.136964" x2="747.09593" y1="467.2588" y2="467.2588" gradientTransform="matrix(.90134 0 0 1.10946 .52026458 -.48501452)" gradientUnits="userSpaceOnUse"/>
<radialGradient xlink:href="#f" id="g" cx="23.857143" cy="40" r="17.142857" fx="23.857143" fy="40" gradientTransform="matrix(1 0 0 .5 0 20)" gradientUnits="userSpaceOnUse"/>
</defs>
<path fill="url(#g)" d="M41 40a17.142857 8.5714283 0 1 1-34.2857132 0A17.142857 8.5714283 0 1 1 41 40z" display="block" overflow="visible" style="marker:none" transform="matrix(22.3851 0 0 24.34096 -173.03587 -228.02997)"/>
<path fill="url(#h)" stroke="url(#i)" stroke-miterlimit="10" stroke-width="21.14290619" d="M356.66648 141.31284c-166.17052 0-301.285983 59.253-301.285983 132.14314v488.92965c0 72.89018 135.115463 132.14315 301.285983 132.14315 166.17053 0 306.66725-59.25297 306.66725-132.14315V273.45598c0-72.89014-140.49672-132.14314-306.66725-132.14314z" display="block" overflow="visible" style="marker:none"/>
<path fill="none" stroke="url(#j)" stroke-miterlimit="10" stroke-width="21.14290619" d="M356.84833 157.78994c-154.92885 0-280.904411 55.24468-280.904411 123.20413v467.81359c0 67.95922 125.975561 123.20393 280.904411 123.20393 154.93094 0 285.92181-55.24471 285.92181-123.20393V280.99407c0-67.95945-130.99087-123.20413-285.92181-123.20413z" display="block" opacity=".5443038" overflow="visible" style="marker:none"/>
<path fill="#3465a4" stroke="url(#k)" stroke-miterlimit="10" stroke-width="1.00492704" d="M36.857142 14.071428a12.857142 5.5 0 1 1-25.714284 0 12.857142 5.5 0 1 1 25.714284 0z" display="block" overflow="visible" style="marker:none" transform="matrix(21.93135 0 0 20.18342 -168.36368 -12.347239)"/>
<path fill="url(#l)" d="m139.00874 369.82596 7.5501 354.89887 96.65272 27.18366-3.0192-353.38841s43.7954 0 114.77637-4.53074c-110.27718-4.92776-236.44943-52.3219-279.389384-83.06136 29.958254 44.23476 63.429394 58.89798 63.429394 58.89798z" display="block" opacity=".49367083" overflow="visible" style="marker:none"/>
<path fill="#204a87" d="M114.15586 701.5918h16.09033v-63.74234l25.1253 55.573c2.84764 6.6832 6.93184 9.15937 14.8527 9.15937 7.92167 0 11.7583-2.47543 14.72914-9.15866l25.00197-55.57371v63.74234h16.21373v-63.74234c0-6.18856-2.47543-9.15937-7.67397-10.7683-12.25289-3.71313-20.42233-.4946-24.13546 7.79743l-24.63007 54.95407-23.7643-54.95407c-3.58957-8.29206-11.88261-11.51056-24.25961-7.79743-5.07443 1.6089-7.54976 4.5797-7.54976 10.7683v63.74234zm125.38108-51.86037h16.21373v35.0272c-.12343 1.98003.61887 6.43708 9.40633 6.56054 4.5797.12276 34.90377 0 35.15144 0v-41.83541h16.21454v57.30688c0 14.1095-17.45224 17.20454-25.62087 17.328h-50.99404v-10.7683h51.1175c10.39707-1.11347 9.15937-6.31203 9.15937-8.0452v-4.20854h-34.28487c-15.96684-.12343-26.2397-7.1786-26.3632-15.22377 0-.7431.37113-35.89373 0-36.14147l.00007.00007zm347.79917-40.59788c-9.90174-.24686-17.57571.74388-24.01204 3.46624-1.85657.7423-4.8274.74307-5.07511 3.09427.99081.9908 1.11427 2.59967 1.98084 3.96087 1.48621 2.47462 4.0843 5.8174 6.43627 7.5505 2.5989 1.98003 5.19857 3.96006 7.92093 5.6932 4.8274 2.97084 10.27364 4.70313 14.97681 7.67397 2.72394 1.73152 5.44549 3.96083 8.16863 5.8174 1.36197.98926 2.22773 2.5989 3.96083 3.21776v-.37116c-.86653-1.11424-1.11423-2.72314-1.98003-3.96084-1.23692-1.23773-2.4754-2.35197-3.71313-3.5897-3.58967-4.82663-8.04517-9.03513-12.87251-12.5006-3.96006-2.72313-12.62483-6.43627-14.23376-11.01597.00077 0-.12411-.12343-.24754-.24686 2.72314-.24787 5.94087-1.2377 8.54057-1.9808 4.20854-1.11427 8.04517-.86657 12.37714-1.98003 1.98003-.49541 3.96083-1.11427 5.94086-1.73314v-1.11346c-2.22692-2.22851-3.83659-5.19854-6.18856-7.30284-6.31203-5.44704-13.24374-10.76827-20.42234-15.22377-3.83737-2.47543-8.78824-4.0851-12.8725-6.1886-1.48463-.74387-3.96087-1.11423-4.82663-2.35197-2.22851-2.7231-3.46624-6.31203-5.07511-9.53056-3.58812-6.80821-7.05513-14.3572-10.1494-21.5358-2.22692-4.82741-3.58889-9.65484-6.31203-14.11024-12.74826-21.04113-26.6109-33.7895-47.89974-46.29087-4.5797-2.5989-10.02516-3.71314-15.8426-5.07433-3.09349-.12411-6.18856-.37114-9.28286-.49537-1.98004-.86657-3.96087-3.21777-5.69398-4.33204-7.05355-4.45623-25.24893-14.10949-30.4475-1.36116-3.34196 8.04517 4.95087 15.96606 7.79744 20.05114 2.10504 2.84659 4.8274 6.06433 6.3128 9.2829.86657 2.10268 1.1135 4.332 1.98003 6.55973 1.98084 5.44469 3.83663 11.51057 6.4363 16.5857 1.36194 2.5989 2.84657 5.32204 4.57971 7.6732.98999 1.36116 2.72232 1.9808 3.09426 4.20853-1.73233 2.47544-1.85656 6.18861-2.84737 9.28291-4.45546 13.98599-2.72233 31.31406 3.5897 41.5869 1.98003 3.09352 6.68394 9.9025 12.99604 7.30287 5.5697-2.22777 4.332-9.28291 5.94086-15.47151.37194-1.48617.12411-2.4754.86657-3.46543.00078.12343 0 .24787 0 .24787 1.7331 3.46543 3.46543 6.80747 5.0743 10.27283 3.8374 6.06437 10.52063 12.37717 16.0903 16.58574 2.97161 2.22696 5.32281 6.06436 9.03594 7.4263v-.37194h-.24787c-.7431-1.11427-1.85657-1.60886-2.8466-2.47543-2.22773-2.22773-4.70394-4.95087-6.43627-7.42627-5.19776-6.9317-9.77827-14.6049-13.86253-22.52657-1.98003-3.83659-3.71317-8.04516-5.32204-11.88176-.7431-1.48621-.7431-3.71314-1.9808-4.45547-1.85657 2.72237-4.57893 5.07434-5.9409 8.41634-2.35039 5.322-2.59886 11.88176-3.46543 18.68913-.49463.1241-.24787 0-.4954.24787-3.96006-.99081-5.32204-5.07433-6.80747-8.54057-3.71313-8.78743-4.332-22.89774-1.11346-33.04637.86656-2.59967 4.5797-10.7683 3.09426-13.24373-.74229-2.35197-3.2185-3.71314-4.5797-5.5697-1.60886-2.35197-3.34197-5.32203-4.45543-7.92167-2.97084-6.9309-4.45627-14.6049-7.674-21.53661-1.4854-3.2185-4.0843-6.55973-6.18857-9.52976-2.35119-3.342-4.95087-5.69397-6.80747-9.65484-.61806-1.36116-1.4854-3.58889-.4954-5.07433.24787-.99.74233-1.36116 1.7331-1.60886 1.6089-1.36194 6.18861.37116 7.79744 1.11349 4.5797 1.85576 8.41633 3.58967 12.2537 6.18857 1.73233 1.2377 3.5889 3.5897 5.81663 4.20853h2.59967c3.96087.8658 8.41633.24754 12.12947 1.3612 6.5605 2.10346 12.50059 5.19854 17.82343 8.54054 16.21454 10.27287 29.5809 24.87854 38.61691 42.33004 1.48543 2.84582 2.10349 5.44627 3.46546 8.41633 2.59967 6.0651 5.81741 12.2537 8.4163 18.19457 2.5989 5.81663 5.07433 11.75833 8.78747 16.58574 1.85734 2.59812 9.28286 3.96006 12.62483 5.32203 2.4754 1.11346 6.31281 2.10427 8.54054 3.46543 4.20776 2.59809 8.4163 5.56974 12.37713 8.4163 1.98084 1.48466 8.16863 4.57974 8.54057 7.05436h.00004z"/>
<path fill="#00546b" d="M461.08775 501.57571c-2.10427 0-3.58889.24854-5.07429.61964v.24787h.24786c.99081 1.98003 2.72233 3.342 3.96007 5.0743.9908 1.98003 1.85656 3.96086 2.84737 5.94167.12343-.12411.24686-.24787.24686-.24787 1.73391-1.23851 2.59967-3.21854 2.59967-6.18857-.7431-.86657-.86657-1.7331-1.4854-2.59967-.74233-1.23851-2.35197-1.85657-3.342-2.84737z"/>
<path fill="#f57900" d="M328.90008 701.5918h46.53854c5.4455 0 10.64406-1.11346 14.85179-3.0943 7.05595-3.21773 10.39711-7.54973 10.39711-13.2437v-11.88257c0-4.57893-3.83741-8.9109-11.51057-11.88177-3.96006-1.48463-8.9117-2.35119-13.7391-2.35119h-19.55496c-6.56051 0-9.65481-1.98081-10.52134-6.31281-.12411-.49463-.12411-.99003-.12411-1.48463v-7.30287c0-.37113 0-.86653.12411-1.36116.86656-3.34197 2.59889-4.20854 8.29286-4.8274h47.65281v-10.76831h-45.30164c-6.55974 0-10.02517.37117-13.1195 1.36197-9.53057 2.96926-13.73911 7.67317-13.73911 15.96603v9.40714c0 7.30203 8.16944 13.49063 22.03198 14.97603 1.48543.12343 3.0943.12343 4.70316.12343h16.70917c.61887 0 1.23773 0 1.73314.12411 5.07429.49459 7.30203 1.36116 8.78743 3.21773.99003.98922 1.23773 1.85656 1.23773 2.97083l.00078 9.40556c0 1.11424-.74311 2.60045-2.22851 3.83815-1.48466 1.23773-3.8374 2.1043-6.93174 2.22773-.61806 0-.99.12343-1.60886.12343l-44.68124-.00077v10.76907l.00007.00007zm172.53802-18.68916c0 11.01438 8.29287 17.20376 24.87773 18.44146 1.60886.1241 3.09426.24787 4.70316.24787h42.08389v-10.76908h-42.45431c-9.40633 0-12.99674-2.35042-12.99674-8.04439l.00077-55.69721h-16.21454v55.82148l.00004.00003zm-90.47782.49442v-38.36913c0-9.77747 6.9309-15.71914 20.42234-17.57567 1.48621-.1241 2.97083-.24686 4.332-.24686l30.69517-.00078c1.61044 0 2.97083.12411 4.5797.24754 13.4914 1.85576 20.29887 7.7982 20.29887 17.57567l.00081 38.36913c0 7.92097-2.84741 12.13028-9.53141 14.97684l15.8434 14.3572H478.912l-12.87251-11.6348-12.99603.74229h-17.32881c-2.84582 0-6.06514-.37036-9.40633-1.23692-10.27367-2.8466-15.34797-8.29287-15.34797-17.20457l-.00007-.00004zm17.45147-.86653c0 .49459.24753.9908.37194 1.60967.86576 4.45543 5.07429 6.9309 11.51056 6.9309h14.60487l-13.36717-12.13027h18.68997l11.75753 10.64487c2.22931-1.23851 3.58967-2.97007 4.08507-5.1978.12343-.49537.12343-1.11423.12343-1.60964v-36.7603c0-.49463 0-.99003-.12343-1.48463-.86653-4.20857-5.0751-6.56054-11.3871-6.56054h-24.38317c-7.1786 0-11.88257 3.0943-11.88257 8.04517v36.51264l.00007-.00007zm152.11655 19.18436h2.35197v-9.03513h3.0943v-1.85734h-8.6648v1.85734h3.21853v9.03513zm17.94771 0h2.22693v-10.8925h-3.3412l-2.72394 7.42627-2.96925-7.42627h-3.21851v10.8925h2.10427v-8.29286h.12343l3.09427 8.29286h1.60967l3.0943-8.29286v8.29286z"/>
<path fill="none" d="M-64.879845 115.85568h850v850h-850z"/>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

+536
View File
@@ -0,0 +1,536 @@
BLACKBOX EXPORTER - GUIDA OPERATIVA KUBERNETES
==============================================
Stack di partenza
-----------------
Nel cluster e' gia' installato kube-prometheus-stack nel namespace monitoring:
```text
helm list -A | grep prometheus
kube-prometheus-stack monitoring 4 2026-08-08 23:55:14.193903052 +0000 UTC deployed kube-prometheus-stack-88.1.5
```
Obiettivo
---------
Installare prometheus-blackbox-exporter per eseguire test HTTP/HTTPS su servizi Kubernetes interni, Ingress o URL esterni, esportando le metriche verso Prometheus tramite Prometheus Operator.
Metriche principali:
```text
probe_success
probe_duration_seconds
probe_http_status_code
probe_http_ssl
probe_dns_lookup_time_seconds
```
Legenda
-------
- COMANDO: istruzione da eseguire nel terminale.
- FILE: file da creare o aggiornare.
- NOTA: informazione da verificare prima di procedere.
- OUTPUT ATTESO: risultato indicativo del comando.
Prerequisiti
------------
- Accesso kubectl al cluster.
- Helm installato.
- Namespace monitoring presente o creabile.
- kube-prometheus-stack installato con Prometheus Operator.
- CRD Probe disponibile: probes.monitoring.coreos.com.
1. VERIFICA PROMETHEUS E PROMETHEUS OPERATOR
============================================
COMANDO - verifica release Helm Prometheus
```bash
helm list -A | grep prometheus
```
COMANDO - verifica pod Prometheus
```bash
kubectl get pods -A | grep prometheus
```
COMANDO - verifica CRD Probe
```bash
kubectl get crd probes.monitoring.coreos.com
```
COMANDO - verifica label delle risorse Prometheus
```bash
kubectl get prometheus -A --show-labels
```
COMANDO - controlla selector della risorsa Prometheus
```bash
kubectl get prometheus -n monitoring -o yaml
```
NOTA - label dei manifest
Dal comando `kubectl get prometheus -A --show-labels` risulta che Prometheus seleziona le risorse con la label `release=kube-prometheus-stack`.
Negli esempi sotto viene quindi usata questa label:
```yaml
release: kube-prometheus-stack
```
Se in futuro il tuo Prometheus usa un selector diverso, sostituisci questa label nei manifest Probe e PrometheusRule.
Controlla soprattutto questi campi nella risorsa Prometheus:
```yaml
spec:
probeSelector:
probeNamespaceSelector:
ruleSelector:
ruleNamespaceSelector:
```
2. AGGIUNTA REPOSITORY HELM
===========================
COMANDO - aggiungi repository prometheus-community
```bash
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
```
COMANDO - aggiorna indice chart Helm
```bash
helm repo update
```
3. CREA FILE VALUES HELM
========================
FILE - blackbox-values.yaml
Descrizione:
- configura i moduli HTTP usati da blackbox-exporter;
- espone blackbox-exporter come Service ClusterIP sulla porta 9115;
- lascia disabilitato ServiceMonitor perche' lo scraping dei target viene configurato tramite Probe.
COMANDO - crea blackbox-values.yaml
```bash
cat > blackbox-values.yaml <<'EOF'
fullnameOverride: blackbox-exporter
config:
modules:
http_2xx:
prober: http
timeout: 10s
http:
method: GET
preferred_ip_protocol: ip4
valid_http_versions:
- HTTP/1.1
- HTTP/2.0
valid_status_codes:
- 200
- 204
- 301
- 302
http_post_2xx:
prober: http
timeout: 10s
http:
method: POST
preferred_ip_protocol: ip4
valid_status_codes:
- 200
- 201
- 202
- 204
http_k8s_health:
prober: http
timeout: 10s
http:
method: GET
preferred_ip_protocol: ip4
fail_if_ssl: false
fail_if_not_ssl: false
valid_status_codes:
- 200
service:
type: ClusterIP
port: 9115
serviceMonitor:
enabled: false
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 256Mi
EOF
```
4. INSTALLA BLACKBOX EXPORTER
=============================
COMANDO - installa o aggiorna blackbox-exporter
```bash
helm upgrade --install blackbox-exporter prometheus-community/prometheus-blackbox-exporter \
--namespace monitoring \
--create-namespace \
-f blackbox-values.yaml
```
COMANDO - verifica pod blackbox-exporter
```bash
kubectl get pods -n monitoring | grep blackbox
```
COMANDO - verifica Service blackbox-exporter
```bash
kubectl get svc -n monitoring | grep blackbox
```
OUTPUT ATTESO
```text
blackbox-exporter ClusterIP <cluster-ip> <none> 9115/TCP
```
5. CREA PROBE HTTP PER SERVIZI KUBERNETES INTERNI
=================================================
FILE - blackbox-probes.yaml
Descrizione:
- testa endpoint HTTP interni tramite DNS Kubernetes;
- usa il modulo http_k8s_health definito in blackbox-values.yaml;
- invia le metriche a Prometheus tramite la risorsa Probe del Prometheus Operator.
Da personalizzare:
- my-service
- my-namespace
- porta del servizio
- path HTTP, ad esempio /health, /ready o /
- label release se diversa da prometheus
COMANDO - crea blackbox-probes.yaml
```bash
cat > blackbox-probes.yaml <<'EOF'
apiVersion: monitoring.coreos.com/v1
kind: Probe
metadata:
name: http-services-probe
namespace: monitoring
labels:
release: kube-prometheus-stack
spec:
jobName: http-services-probe
interval: 30s
scrapeTimeout: 15s
module: http_k8s_health
prober:
url: blackbox-exporter.monitoring.svc.cluster.local:9115
scheme: http
path: /probe
targets:
staticConfig:
static:
- http://frontend.idcidp-dev.svc.cluster.local
EOF
```
COMANDO - applica Probe servizi interni
```bash
kubectl apply -f blackbox-probes.yaml
```
COMANDO - verifica Probe
```bash
kubectl get probe -n monitoring
kubectl describe probe http-services-probe -n monitoring
```
NOTA - campi importanti della Probe
```yaml
spec:
module: http_k8s_health
prober:
url: blackbox-exporter.monitoring.svc.cluster.local:9115
targets:
staticConfig:
static:
- http://my-service.my-namespace.svc.cluster.local:8080/health
```
6. CREA PROBE HTTP/HTTPS PER INGRESS O URL ESTERNI
==================================================
FILE - blackbox-ingress-probes.yaml
Descrizione:
- testa URL esposti tramite Ingress, Gateway, reverse proxy o endpoint pubblici;
- usa il modulo generico http_2xx.
COMANDO - crea blackbox-ingress-probes.yaml
```bash
cat > blackbox-ingress-probes.yaml <<'EOF'
apiVersion: monitoring.coreos.com/v1
kind: Probe
metadata:
name: ingress-http-probe
namespace: monitoring
labels:
release: kube-prometheus-stack
spec:
jobName: ingress-http-probe
interval: 30s
scrapeTimeout: 15s
module: http_2xx
prober:
url: blackbox-exporter.monitoring.svc.cluster.local:9115
scheme: http
path: /probe
targets:
staticConfig:
static:
- https://app.example.com/health
- https://api.example.com/ready
EOF
```
COMANDO - applica Probe endpoint esterni
```bash
kubectl apply -f blackbox-ingress-probes.yaml
```
7. VERIFICA METRICHE IN PROMETHEUS
==================================
COMANDO - apri Prometheus in locale con port-forward
```bash
kubectl port-forward -n monitoring svc/kube-prometheus-stack-prometheus 9090:9090
```
Poi apri nel browser:
```text
http://localhost:9090
```
QUERY PROMQL - tutte le probe
```promql
probe_success
```
QUERY PROMQL - probe servizi interni
```promql
probe_success{job="http-services-probe"}
```
QUERY PROMQL - status code HTTP
```promql
probe_http_status_code{job="http-services-probe"}
```
QUERY PROMQL - durata probe
```promql
probe_duration_seconds{job="http-services-probe"}
```
QUERY PROMQL - target falliti
```promql
probe_success{job="http-services-probe"} == 0
```
Interpretazione:
- probe_success = 1: test riuscito.
- probe_success = 0: test fallito.
- probe_http_status_code: status HTTP ricevuto.
- probe_duration_seconds: durata della probe.
8. CREA ALERT PROMETHEUSRULE
============================
FILE - blackbox-http-alerts.yaml
Descrizione:
- genera alert quando un target HTTP non risponde correttamente;
- genera alert quando un target risponde troppo lentamente.
COMANDO - crea blackbox-http-alerts.yaml
```bash
cat > blackbox-http-alerts.yaml <<'EOF'
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: blackbox-http-alerts
namespace: monitoring
labels:
release: kube-prometheus-stack
spec:
groups:
- name: blackbox-http
rules:
- alert: BlackboxHttpProbeFailed
expr: probe_success == 0
for: 2m
labels:
severity: warning
annotations:
summary: "HTTP probe fallita"
description: "Il target {{ $labels.instance }} non risponde correttamente da almeno 2 minuti."
- alert: BlackboxHttpSlowResponse
expr: probe_duration_seconds > 2
for: 5m
labels:
severity: warning
annotations:
summary: "HTTP probe lenta"
description: "Il target {{ $labels.instance }} risponde in piu' di 2 secondi da almeno 5 minuti."
EOF
```
COMANDO - applica alert
```bash
kubectl apply -f blackbox-http-alerts.yaml
```
COMANDO - verifica PrometheusRule
```bash
kubectl get prometheusrule -n monitoring | grep blackbox
kubectl describe prometheusrule blackbox-http-alerts -n monitoring
```
9. TEST MANUALE BLACKBOX EXPORTER
=================================
COMANDO - port-forward blackbox-exporter
```bash
kubectl port-forward -n monitoring svc/blackbox-exporter 9115:9115
```
COMANDO - esegui probe manuale da un altro terminale
```bash
curl "http://localhost:9115/probe?target=http://my-service.my-namespace.svc.cluster.local:8080/health&module=http_k8s_health"
```
OUTPUT ATTESO - metriche indicative
```text
probe_success 1
probe_http_status_code 200
```
10. TROUBLESHOOTING
===================
CASO - Prometheus non vede la Probe
COMANDO
```bash
kubectl get probe -A
kubectl describe probe -n monitoring http-services-probe
kubectl get prometheus -n monitoring -o yaml
```
Controlla:
- spec.probeSelector
- spec.probeNamespaceSelector
- metadata.labels della Probe
CASO - blackbox-exporter non parte
COMANDO
```bash
kubectl get pods -n monitoring | grep blackbox
kubectl logs -n monitoring deploy/blackbox-exporter
```
CASO - servizio target non raggiungibile
COMANDO
```bash
kubectl get svc -n my-namespace
kubectl get endpoints -n my-namespace my-service
kubectl describe svc -n my-namespace my-service
```
CASO - DNS Kubernetes non risolve
COMANDO
```bash
kubectl run dns-test --rm -it --image=busybox:1.36 --restart=Never -- nslookup my-service.my-namespace.svc.cluster.local
```
CASO - endpoint HTTPS fallisce per certificati, redirect o header
Azioni consigliate:
- usare il modulo http_2xx per endpoint esterni generici;
- verificare se l'endpoint richiede SNI, autenticazione, header custom o path diverso;
- aggiungere un modulo dedicato in blackbox-values.yaml se serve una configurazione HTTP specifica.
11. ORDINE DI ESECUZIONE CONSIGLIATO
====================================
COMANDO - sequenza completa
```bash
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm upgrade --install blackbox-exporter prometheus-community/prometheus-blackbox-exporter \
--namespace monitoring \
--create-namespace \
-f blackbox-values.yaml
kubectl apply -f blackbox-probes.yaml
kubectl apply -f blackbox-ingress-probes.yaml
kubectl apply -f blackbox-http-alerts.yaml
```
FILE CONSIGLIATI DA VERSIONARE
```text
blackbox-values.yaml
blackbox-probes.yaml
blackbox-ingress-probes.yaml
blackbox-http-alerts.yaml
```
12. NOTE PER DEV, QA E PROD
===========================
Per separare gli ambienti e semplificare dashboard e alert, usa Probe distinte:
```text
dev-http-services-probe
qa-http-services-probe
prod-http-services-probe
```
QUERY PROMQL - ambiente dev
```promql
probe_success{job="dev-http-services-probe"}
```
QUERY PROMQL - target prod falliti
```promql
probe_success{job="prod-http-services-probe"} == 0
```
Per endpoint critici in produzione, valuta:
- interval piu' basso, ad esempio 15s o 30s;
- durata for degli alert tra 1m e 5m in base alla criticita';
- dashboard Grafana con stato, status code e latenza per target.
+100
View File
@@ -0,0 +1,100 @@
kubectl cnpg psql pg-devops -n devops
CREATE DATABASE giteadbtest;
CREATE USER giteatest WITH PASSWORD 'KAYQE1QA7uwUZ8uI';
GRANT ALL PRIVILEGES ON DATABASE giteadbtest TO giteatest;
ALTER DATABASE giteadbtest OWNER TO giteatest;
helm repo add gitea https://dl.gitea.io/charts/
helm repo update
kubectl create namespace gitea
cat <<EOF |cat >valuestest.yaml -
replicaCount: 1
image:
repository: gitea/gitea
tag: 1.25.4-rootless
pullPolicy: IfNotPresent
strategy:
type: Recreate
service:
http:
type: ClusterIP
port: 3000
ssh:
type: ClusterIP
port: 22
redis-cluster:
enabled: false
redis:
enabled: false
ingress:
enabled: false
persistence:
enabled: true
storageClass: csi-rbdfs-sc
size: 10Gi
postgresql:
enabled: false
postgresql-ha:
enabled: false
gitea:
admin:
username: gitadmin
password: KAYQE1QA7uwUZ8uI
email: gitadmin@italiadatacenter.com
config:
database:
DB_TYPE: postgres
HOST: pg-devops-rw.devops.svc:5432
NAME: giteadbtest
USER: giteatest
PASSWD: KAYQE1QA7uwUZ8uI
SSL_MODE: disable
server:
ROOT_URL: https://git2.pigreco66.it/
SSH_DOMAIN: git2.pigreco66.it
SSH_PORT: 22
security:
INSTALL_LOCK: true
EOF
helm upgrade --install gitea gitea-charts/gitea --namespace gitea -f values.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: gitea
namespace: gitea
spec:
hostnames:
- git.italiadatacenter.com
parentRefs:
- name: main-gateway
namespace: nginx-gateway
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: gitea-http
port: 3000
@@ -0,0 +1,24 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: namespace-b
namespace: argocd
spec:
project: gitops-3ns
source:
repoURL: "https://github.com/<tua-org>/gitops-repo.git"
targetRevision: main
path: gitops/clusters/idc/namespace-B/manifests
destination:
server: "https://kubernetes.default.svc"
namespace: namespace-B
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
+28 -3
View File
@@ -1,7 +1,32 @@
helm repo add rancher-stable https://releases.rancher.com/server-charts/stable
kubectl create namespace cattle-system
helm install rancher rancher-stable/rancher \
helm install rancher rancher-stable/rancher --namespace cattle-system --set hostname=idc.internal --set bootstrapPassword=.... --set ingress.tls.source=rancher
helm upgrade rancher rancher-stable/rancher \
--namespace cattle-system \
--set hostname=k8s.italiadatacenter.com \
--set bootstrapPassword=admin
--reuse-values \
--set hostname=idc.internal \
--set bootstrapPassword=KAYQE1QA7uwUZ8uI \
--set ingress.enabled=false \
--set ingress.tls.source=rancher
Poc-25_sts
POC-25_sts
vi sistemarancher.sh
# 1. recupera il ClusterIP del service "rancher" già creato dal chart
CLUSTERIP=$(kubectl get svc -n cattle-system rancher -o jsonpath='{.spec.clusterIP}')
# 2. patch del deployment per aggiungere hostAliases
kubectl patch deployment rancher -n cattle-system --type='json' \
-p="[{\"op\":\"add\",\"path\":\"/spec/template/spec/hostAliases\",\"value\":[{\"ip\":\"$CLUSTERIP\",\"hostnames\":[\"idc.internal\"]}]}]"
# 3. crea il service NodePort per l'accesso dal browser
kubectl expose deployment rancher -n cattle-system --type=NodePort --port=443 --target-port=443 --name=rancher-nodeport
# 4. riavvia e segui i log
kubectl rollout restart deployment rancher -n cattle-system
kubectl -n cattle-system logs -l app=rancher -f
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+136
View File
@@ -0,0 +1,136 @@
apiVersion: v1
kind: Secret
metadata:
name: opens3-console-secrets
namespace: minio
type: Opaque
stringData:
# Endpoint S3 del server MinIO a cui la console si collega.
# Cambialo con l'URL reale del tuo MinIO (Service interno o esterno).
CONSOLE_MINIO_SERVER: "http://minio.minio.svc.cluster.local:9000"
# Passphrase e salt usati da opens3/console per cifrare le sessioni
# (PBKDF2). Generane di robusti, es.:
# openssl rand -base64 32
CONSOLE_PBKDF_PASSPHRASE: "OHB576kn9SS4JdD7qG4+hyjRpa353HQqxPQ22z2Do0w="
CONSOLE_PBKDF_SALT: "OBHOWF2INklmrtmyI+qNEb3dbV0yz9ZAxiJJCcC6GYw="
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: opens3-console
namespace: minio
labels:
app.kubernetes.io/name: opens3-console
app.kubernetes.io/instance: opens3-console
spec:
replicas: 1
revisionHistoryLimit: 3
selector:
matchLabels:
app.kubernetes.io/name: opens3-console
app.kubernetes.io/instance: opens3-console
template:
metadata:
labels:
app.kubernetes.io/name: opens3-console
app.kubernetes.io/instance: opens3-console
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: console
image: opens3/console:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 9090
protocol: TCP
envFrom:
- secretRef:
name: opens3-console-secrets
# Se il MinIO target usa certificati self-signed, decommenta:
# env:
# - name: CONSOLE_MINIO_TLS_SKIP_VERIFICATION
# value: "on"
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 3
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 256Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
---
apiVersion: v1
kind: Service
metadata:
name: opens3-console
namespace: minio
labels:
app.kubernetes.io/name: opens3-console
app.kubernetes.io/instance: opens3-console
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: opens3-console
app.kubernetes.io/instance: opens3-console
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: httproute-minio-direct
spec:
hostnames:
- idcidp.pigreco66.it
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: main-gateway
namespace: nginx-gateway
rules:
- backendRefs:
- group: ""
kind: Service
name: opens3-console
port: 80
weight: 1
filters:
- responseHeaderModifier:
remove:
- X-Frame-Options
- Content-Security-Policy
set:
- name: X-Frame-Options
value: SAMEORIGIN
- name: Content-Security-Policy
value: frame-ancestors 'self'
type: ResponseHeaderModifier
matches:
- path:
type: PathPrefix
value: /minio-console
+870
View File
@@ -0,0 +1,870 @@
BLACKBOX EXPORTER - GUIDA OPERATIVA KUBERNETES
==============================================
Stack di partenza
-----------------
Nel cluster e' gia' installato kube-prometheus-stack nel namespace monitoring:
```text
helm list -A | grep prometheus
kube-prometheus-stack monitoring 4 2026-08-08 23:55:14.193903052 +0000 UTC deployed kube-prometheus-stack-88.1.5
```
Obiettivo
---------
Installare prometheus-blackbox-exporter per eseguire test HTTP/HTTPS su servizi Kubernetes interni, Ingress o URL esterni, esportando le metriche verso Prometheus tramite Prometheus Operator.
Metriche principali:
```text
probe_success
probe_duration_seconds
probe_http_status_code
probe_http_ssl
probe_dns_lookup_time_seconds
```
Legenda
-------
- COMANDO: istruzione da eseguire nel terminale.
- FILE: file da creare o aggiornare.
- NOTA: informazione da verificare prima di procedere.
- OUTPUT ATTESO: risultato indicativo del comando.
Prerequisiti
------------
- Accesso kubectl al cluster.
- Helm installato.
- Namespace monitoring presente o creabile.
- kube-prometheus-stack installato con Prometheus Operator.
- CRD Probe disponibile: probes.monitoring.coreos.com.
1. VERIFICA PROMETHEUS E PROMETHEUS OPERATOR
============================================
COMANDO - verifica release Helm Prometheus
```bash
helm list -A | grep prometheus
```
COMANDO - verifica pod Prometheus
```bash
kubectl get pods -A | grep prometheus
```
COMANDO - verifica CRD Probe
```bash
kubectl get crd probes.monitoring.coreos.com
```
COMANDO - verifica label delle risorse Prometheus
```bash
kubectl get prometheus -A --show-labels
```
COMANDO - controlla selector della risorsa Prometheus
```bash
kubectl get prometheus -n monitoring -o yaml
```
NOTA - label dei manifest
Dal comando `kubectl get prometheus -A --show-labels` risulta che Prometheus seleziona le risorse con la label `release=kube-prometheus-stack`.
Negli esempi sotto viene quindi usata questa label:
```yaml
release: kube-prometheus-stack
```
Se in futuro il tuo Prometheus usa un selector diverso, sostituisci questa label nei manifest Probe e PrometheusRule.
Controlla soprattutto questi campi nella risorsa Prometheus:
```yaml
spec:
probeSelector:
probeNamespaceSelector:
ruleSelector:
ruleNamespaceSelector:
```
2. AGGIUNTA REPOSITORY HELM
===========================
COMANDO - aggiungi repository prometheus-community
```bash
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
```
COMANDO - aggiorna indice chart Helm
```bash
helm repo update
```
3. CREA FILE VALUES HELM
========================
FILE - blackbox-values.yaml
Descrizione:
- configura i moduli HTTP usati da blackbox-exporter;
- espone blackbox-exporter come Service ClusterIP sulla porta 9115;
- lascia disabilitato ServiceMonitor perche' lo scraping dei target viene configurato tramite Probe.
COMANDO - crea blackbox-values.yaml
```bash
cat > blackbox-values.yaml <<'EOF'
fullnameOverride: blackbox-exporter
config:
modules:
http_2xx:
prober: http
timeout: 10s
http:
method: GET
preferred_ip_protocol: ip4
valid_http_versions:
- HTTP/1.1
- HTTP/2.0
valid_status_codes:
- 200
- 204
- 301
- 302
http_post_2xx:
prober: http
timeout: 10s
http:
method: POST
preferred_ip_protocol: ip4
valid_status_codes:
- 200
- 201
- 202
- 204
http_k8s_health:
prober: http
timeout: 10s
http:
method: GET
preferred_ip_protocol: ip4
fail_if_ssl: false
fail_if_not_ssl: false
valid_status_codes:
- 200
service:
type: ClusterIP
port: 9115
serviceMonitor:
enabled: false
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 256Mi
EOF
```
4. INSTALLA BLACKBOX EXPORTER
=============================
COMANDO - installa o aggiorna blackbox-exporter
```bash
helm upgrade --install blackbox-exporter prometheus-community/prometheus-blackbox-exporter \
--namespace monitoring \
--create-namespace \
-f blackbox-values.yaml
```
COMANDO - verifica pod blackbox-exporter
```bash
kubectl get pods -n monitoring | grep blackbox
```
COMANDO - verifica Service blackbox-exporter
```bash
kubectl get svc -n monitoring | grep blackbox
```
OUTPUT ATTESO
```text
blackbox-exporter ClusterIP <cluster-ip> <none> 9115/TCP
```
5. CREA PROBE HTTP PER SERVIZI KUBERNETES INTERNI
=================================================
FILE - blackbox-probes.yaml
Descrizione:
- testa endpoint HTTP interni tramite DNS Kubernetes;
- usa il modulo http_k8s_health definito in blackbox-values.yaml;
- invia le metriche a Prometheus tramite la risorsa Probe del Prometheus Operator.
Da personalizzare:
- my-service
- my-namespace
- porta del servizio
- path HTTP, ad esempio /health, /ready o /
- label release se diversa da prometheus
COMANDO - crea blackbox-probes.yaml
```bash
cat > blackbox-probes.yaml <<'EOF'
apiVersion: monitoring.coreos.com/v1
kind: Probe
metadata:
name: http-services-probe
namespace: monitoring
labels:
release: kube-prometheus-stack
spec:
jobName: http-services-probe
interval: 30s
scrapeTimeout: 15s
module: http_k8s_health
prober:
url: blackbox-exporter.monitoring.svc.cluster.local:9115
scheme: http
path: /probe
targets:
staticConfig:
static:
- http://my-service.my-namespace.svc.cluster.local:8080/health
- http://another-service.default.svc.cluster.local:80/
EOF
```
COMANDO - applica Probe servizi interni
```bash
kubectl apply -f blackbox-probes.yaml
```
COMANDO - verifica Probe
```bash
kubectl get probe -n monitoring
kubectl describe probe http-services-probe -n monitoring
```
NOTA - campi importanti della Probe
```yaml
spec:
module: http_k8s_health
prober:
url: blackbox-exporter.monitoring.svc.cluster.local:9115
targets:
staticConfig:
static:
- http://my-service.my-namespace.svc.cluster.local:8080/health
```
6. CREA PROBE HTTP/HTTPS PER INGRESS O URL ESTERNI
==================================================
FILE - blackbox-ingress-probes.yaml
Descrizione:
- testa URL esposti tramite Ingress, Gateway, reverse proxy o endpoint pubblici;
- usa il modulo generico http_2xx.
COMANDO - crea blackbox-ingress-probes.yaml
```bash
cat > blackbox-ingress-probes.yaml <<'EOF'
apiVersion: monitoring.coreos.com/v1
kind: Probe
metadata:
name: ingress-http-probe
namespace: monitoring
labels:
release: kube-prometheus-stack
spec:
jobName: ingress-http-probe
interval: 30s
scrapeTimeout: 15s
module: http_2xx
prober:
url: blackbox-exporter.monitoring.svc.cluster.local:9115
scheme: http
path: /probe
targets:
staticConfig:
static:
- https://app.example.com/health
- https://api.example.com/ready
EOF
```
COMANDO - applica Probe endpoint esterni
```bash
kubectl apply -f blackbox-ingress-probes.yaml
```
7. VERIFICA METRICHE IN PROMETHEUS
==================================
COMANDO - apri Prometheus in locale con port-forward
```bash
kubectl port-forward -n monitoring svc/kube-prometheus-stack-prometheus 9090:9090
```
Poi apri nel browser:
```text
http://localhost:9090
```
QUERY PROMQL - tutte le probe
```promql
probe_success
```
QUERY PROMQL - probe servizi interni
```promql
probe_success{job="http-services-probe"}
```
QUERY PROMQL - status code HTTP
```promql
probe_http_status_code{job="http-services-probe"}
```
QUERY PROMQL - durata probe
```promql
probe_duration_seconds{job="http-services-probe"}
```
QUERY PROMQL - target falliti
```promql
probe_success{job="http-services-probe"} == 0
```
Interpretazione:
- probe_success = 1: test riuscito.
- probe_success = 0: test fallito.
- probe_http_status_code: status HTTP ricevuto.
- probe_duration_seconds: durata della probe.
8. CREA DASHBOARD GRAFANA PER LE PROBE
======================================
Obiettivo:
- visualizzare lo stato delle probe HTTP;
- vedere quali target sono UP o DOWN;
- controllare status code e latenza per ogni endpoint;
- filtrare la dashboard per job e target.
COMANDO - apri Grafana in locale con port-forward
```bash
kubectl port-forward -n monitoring svc/kube-prometheus-stack-grafana 3000:80
```
Poi apri nel browser:
```text
http://localhost:3000
```
COMANDO - recupera password admin Grafana se non la conosci
```bash
kubectl get secret -n monitoring kube-prometheus-stack-grafana \
-o jsonpath="{.data.admin-password}" | base64 -d
```
Credenziali predefinite tipiche:
```text
utente: admin
password: valore recuperato dal secret
```
NOTA - datasource Prometheus
Con kube-prometheus-stack il datasource Prometheus di solito e' gia' configurato in Grafana.
Verifica da Grafana:
```text
Connections -> Data sources -> Prometheus
```
CREAZIONE DASHBOARD DA INTERFACCIA
1. Vai su Dashboards -> New -> New dashboard.
2. Clicca Add visualization.
3. Seleziona il datasource Prometheus.
4. Crea i pannelli usando le query sotto.
5. Salva la dashboard con nome, ad esempio:
```text
Blackbox HTTP Probes
```
VARIABILI CONSIGLIATE
Variabile job:
```text
Name: job
Type: Query
Data source: Prometheus
Query: label_values(probe_success, job)
Multi-value: enabled
Include All option: enabled
```
Variabile instance:
```text
Name: instance
Type: Query
Data source: Prometheus
Query: label_values(probe_success{job=~"$job"}, instance)
Multi-value: enabled
Include All option: enabled
```
PANNELLO - stato generale probe
Tipo pannello: Stat
Query:
```promql
min(probe_success{job=~"$job", instance=~"$instance"})
```
Configurazione consigliata:
```text
Unit: none
Thresholds:
0 = red
1 = green
Value mappings:
0 -> DOWN
1 -> UP
```
PANNELLO - stato per target
Tipo pannello: State timeline oppure Table
Query:
```promql
probe_success{job=~"$job", instance=~"$instance"}
```
Configurazione consigliata:
```text
Legend: {{ instance }}
Value mappings:
0 -> DOWN
1 -> UP
```
PANNELLO - target attualmente falliti
Tipo pannello: Table
Query:
```promql
probe_success{job=~"$job", instance=~"$instance"} == 0
```
Configurazione consigliata:
```text
Legend: {{ instance }}
Mostra colonne: instance, job, value
```
PANNELLO - durata probe per target
Tipo pannello: Time series
Query:
```promql
probe_duration_seconds{job=~"$job", instance=~"$instance"}
```
Configurazione consigliata:
```text
Unit: seconds
Legend: {{ instance }}
Thresholds:
1 = yellow
2 = red
```
PANNELLO - status code HTTP
Tipo pannello: Time series oppure Table
Query:
```promql
probe_http_status_code{job=~"$job", instance=~"$instance"}
```
Configurazione consigliata:
```text
Unit: none
Legend: {{ instance }}
```
PANNELLO - percentuale disponibilita' per target
Tipo pannello: Bar gauge oppure Table
Query:
```promql
avg_over_time(probe_success{job=~"$job", instance=~"$instance"}[24h]) * 100
```
Configurazione consigliata:
```text
Unit: percent
Min: 0
Max: 100
Legend: {{ instance }}
Thresholds:
95 = yellow
99 = green
```
PANNELLO - durata media nelle ultime 24 ore
Tipo pannello: Bar gauge oppure Table
Query:
```promql
avg_over_time(probe_duration_seconds{job=~"$job", instance=~"$instance"}[24h])
```
Configurazione consigliata:
```text
Unit: seconds
Legend: {{ instance }}
```
QUERY RAPIDE SENZA VARIABILI
Se vuoi creare una dashboard solo per la Probe di esempio dei servizi interni:
```promql
probe_success{job="http-services-probe"}
probe_http_status_code{job="http-services-probe"}
probe_duration_seconds{job="http-services-probe"}
probe_success{job="http-services-probe"} == 0
avg_over_time(probe_success{job="http-services-probe"}[24h]) * 100
```
Per la Probe di esempio degli Ingress o URL esterni:
```promql
probe_success{job="ingress-http-probe"}
probe_http_status_code{job="ingress-http-probe"}
probe_duration_seconds{job="ingress-http-probe"}
probe_success{job="ingress-http-probe"} == 0
avg_over_time(probe_success{job="ingress-http-probe"}[24h]) * 100
```
9. CREA ALERT PROMETHEUSRULE
============================
FILE - blackbox-http-alerts.yaml
Descrizione:
- genera alert quando un target HTTP non risponde correttamente;
- genera alert quando un target risponde troppo lentamente.
COMANDO - crea blackbox-http-alerts.yaml
```bash
cat > blackbox-http-alerts.yaml <<'EOF'
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: blackbox-http-alerts
namespace: monitoring
labels:
release: kube-prometheus-stack
spec:
groups:
- name: blackbox-http
rules:
- alert: BlackboxHttpProbeFailed
expr: probe_success == 0
for: 2m
labels:
severity: warning
annotations:
summary: "HTTP probe fallita"
description: "Il target {{ $labels.instance }} non risponde correttamente da almeno 2 minuti."
- alert: BlackboxHttpSlowResponse
expr: probe_duration_seconds > 2
for: 5m
labels:
severity: warning
annotations:
summary: "HTTP probe lenta"
description: "Il target {{ $labels.instance }} risponde in piu' di 2 secondi da almeno 5 minuti."
EOF
```
COMANDO - applica alert
```bash
kubectl apply -f blackbox-http-alerts.yaml
```
COMANDO - verifica PrometheusRule
```bash
kubectl get prometheusrule -n monitoring | grep blackbox
kubectl describe prometheusrule blackbox-http-alerts -n monitoring
```
10. CONFIGURA ALERTMANAGER - INVIO EMAIL SU FAIL DI UN SERVIZIO
================================================================
Obiettivo:
- inviare una email a un indirizzo specifico quando la probe di un
servizio specifico (serviceX) fallisce (alert BlackboxHttpProbeFailed).
FILE - blackbox-email-alert.yaml
Descrizione:
- crea un receiver email dedicato al servizio da monitorare;
- instrada verso quel receiver solo gli alert che riguardano il
target specifico, identificato tramite il label instance.
NOTA - credenziali SMTP
Se il server SMTP richiede autenticazione, crea prima un Secret con
la password:
COMANDO - crea secret con password SMTP
```bash
kubectl create secret generic alertmanager-smtp \
-n monitoring --from-literal=password='<SMTP_PASSWORD>'
```
COMANDO - crea blackbox-email-alert.yaml
```bash
cat > blackbox-email-alert.yaml <<'EOF'
apiVersion: monitoring.coreos.com/v1alpha1
kind: AlertmanagerConfig
metadata:
name: blackbox-email-routing
namespace: monitoring
labels:
release: kube-prometheus-stack
spec:
route:
groupBy: ["alertname", "instance"]
groupWait: 30s
groupInterval: 5m
repeatInterval: 1h
receiver: "email-default"
routes:
- matchers:
- name: alertname
value: BlackboxHttpProbeFailed
matchType: "="
- name: instance
value: "<SERVIZIO_X_URL_O_HOST>"
matchType: "="
receiver: "email-servizioX"
receivers:
- name: "email-default"
emailConfigs: []
- name: "email-servizioX"
emailConfigs:
- to: "<INDIRIZZO_X>"
from: "<SMTP_FROM>"
smarthost: "<SMTP_HOST>:<SMTP_PORT>"
authUsername: "<SMTP_USER>"
authPassword:
name: alertmanager-smtp
key: password
requireTLS: true
sendResolved: true
headers:
subject: "[ALERT] Servizio X non raggiungibile"
html: |
<p>Il servizio <b>{{ "{{" }} .CommonLabels.instance {{ "}}" }}</b> non risponde.</p>
<p>{{ "{{" }} .CommonAnnotations.description {{ "}}" }}</p>
EOF
```
Da personalizzare:
- <SERVIZIO_X_URL_O_HOST>: valore del label instance della probe da
monitorare (es. http://my-service.my-namespace.svc.cluster.local:8080/health);
- <INDIRIZZO_X>: indirizzo email destinatario;
- <SMTP_HOST>, <SMTP_PORT>: server SMTP (es. smtp.gmail.com:587);
- <SMTP_FROM>: mittente email;
- <SMTP_USER>: utente SMTP, se richiesta autenticazione.
COMANDO - applica la configurazione
```bash
kubectl apply -f blackbox-email-alert.yaml
```
COMANDO - verifica AlertmanagerConfig
```bash
kubectl get alertmanagerconfig -n monitoring
kubectl describe alertmanagerconfig blackbox-email-routing -n monitoring
```
NOTA - matcher su instance vs job
Se vuoi far scattare l'email per TUTTI i target di una Probe (job)
invece che per un singolo servizio, sostituisci il matcher su
`instance` con:
```yaml
- name: job
value: "http-services-probe"
matchType: "="
```
COMANDO - verifica alert in Alertmanager
```bash
kubectl port-forward -n monitoring svc/kube-prometheus-stack-alertmanager 9093
# -> http://localhost:9093, controlla che l'alert BlackboxHttpProbeFailed
# per il servizio X sia instradato sul receiver email-servizioX
```
11. TEST MANUALE BLACKBOX EXPORTER
==================================
COMANDO - port-forward blackbox-exporter
```bash
kubectl port-forward -n monitoring svc/blackbox-exporter 9115:9115
```
COMANDO - esegui probe manuale da un altro terminale
```bash
curl "http://localhost:9115/probe?target=http://my-service.my-namespace.svc.cluster.local:8080/health&module=http_k8s_health"
```
OUTPUT ATTESO - metriche indicative
```text
probe_success 1
probe_http_status_code 200
```
12. TROUBLESHOOTING
===================
CASO - Prometheus non vede la Probe
COMANDO
```bash
kubectl get probe -A
kubectl describe probe -n monitoring http-services-probe
kubectl get prometheus -n monitoring -o yaml
```
Controlla:
- spec.probeSelector
- spec.probeNamespaceSelector
- metadata.labels della Probe
CASO - blackbox-exporter non parte
COMANDO
```bash
kubectl get pods -n monitoring | grep blackbox
kubectl logs -n monitoring deploy/blackbox-exporter
```
CASO - servizio target non raggiungibile
COMANDO
```bash
kubectl get svc -n my-namespace
kubectl get endpoints -n my-namespace my-service
kubectl describe svc -n my-namespace my-service
```
CASO - DNS Kubernetes non risolve
COMANDO
```bash
kubectl run dns-test --rm -it --image=busybox:1.36 --restart=Never -- nslookup my-service.my-namespace.svc.cluster.local
```
CASO - endpoint HTTPS fallisce per certificati, redirect o header
Azioni consigliate:
- usare il modulo http_2xx per endpoint esterni generici;
- verificare se l'endpoint richiede SNI, autenticazione, header custom o path diverso;
- aggiungere un modulo dedicato in blackbox-values.yaml se serve una configurazione HTTP specifica.
13. ORDINE DI ESECUZIONE CONSIGLIATO
====================================
COMANDO - sequenza completa
```bash
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm upgrade --install blackbox-exporter prometheus-community/prometheus-blackbox-exporter \
--namespace monitoring \
--create-namespace \
-f blackbox-values.yaml
kubectl apply -f blackbox-probes.yaml
kubectl apply -f blackbox-ingress-probes.yaml
kubectl apply -f blackbox-http-alerts.yaml
```
FILE CONSIGLIATI DA VERSIONARE
```text
blackbox-values.yaml
blackbox-probes.yaml
blackbox-ingress-probes.yaml
blackbox-http-alerts.yaml
```
14. NOTE PER DEV, QA E PROD
===========================
Per separare gli ambienti e semplificare dashboard e alert, usa Probe distinte:
```text
dev-http-services-probe
qa-http-services-probe
prod-http-services-probe
```
QUERY PROMQL - ambiente dev
```promql
probe_success{job="dev-http-services-probe"}
```
QUERY PROMQL - target prod falliti
```promql
probe_success{job="prod-http-services-probe"} == 0
```
Per endpoint critici in produzione, valuta:
- interval piu' basso, ad esempio 15s o 30s;
- durata for degli alert tra 1m e 5m in base alla criticita';
- dashboard Grafana con stato, status code e latenza per target.
+52
View File
@@ -0,0 +1,52 @@
kubectl create secret generic alertmanager-smtp -n monitoring --from-literal=password='pX8XpxrNT8'
cat > blackbox-email-alert.yaml <<'EOF'
apiVersion: monitoring.coreos.com/v1alpha1
kind: AlertmanagerConfig
metadata:
name: blackbox-email-routing
namespace: monitoring
labels:
release: kube-prometheus-stack
spec:
route:
groupBy: ["alertname", "instance"]
groupWait: 30s
groupInterval: 5m
repeatInterval: 1h
receiver: "email-default"
routes:
- matchers:
- name: alertname
value: BlackboxHttpProbeFailed
matchType: "="
- name: instance
value: "https://idcidp.pigreco66.it"
matchType: "="
receiver: "email-servizio-idcidp"
receivers:
- name: "email-default"
emailConfigs: []
- name: "email-servizio-idcidp"
emailConfigs:
- to: "alessandro.barucci66@gmail.com"
from: "alert@italiadatacenter.com"
smarthost: "mail.tophost.it:587"
authUsername: "italiadatacenter.com62374"
authPassword:
name: alertmanager-smtp
key: password
requireTLS: true
sendResolved: true
headers:
- key: subject
value: "[ALERT] Servizio IDCIDP non raggiungibile"
html: |
<p>Il servizio <b>{{ .CommonLabels.instance }}</b> non risponde.</p>
<p>{{ .CommonAnnotations.description }}</p>
EOF
kubectl apply -f blackbox-email-alert.yaml
@@ -0,0 +1,553 @@
apiVersion: monitoring.coreos.com/v1
kind: Probe
metadata:
name: <namespace>-<env>-<endpoint>-probe
namespace: monitoring
labels:
release: kube-prometheus-stack
spec:
jobName: <namespace>-<env>-<endpoint>-probe
interval: 30s
scrapeTimeout: 15s
module: http_2xx
prober:
url: blackbox-exporter.monitoring.svc.cluster.local:9115
scheme: http
path: /probe
targets:
staticConfig:
static:
- https://<endpoint>
---
apiVersion: v1
kind: ConfigMap
metadata:
name: <namespace>-<env>-<endpoint>-dashboard
namespace: monitoring
labels:
grafana_dashboard: "1"
data:
<namespace>-<env>-<endpoint>-dashboard.json: |
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"links": [],
"panels": [
{
"type": "stat",
"title": "Services UP",
"id": 1,
"gridPos": {
"h": 5,
"w": 6,
"x": 0,
"y": 0
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "sum(probe_success{job=\"<namespace>-<env>-<endpoint>-probe\",instance=~\"$instance\"})"
}
],
"fieldConfig": {
"defaults": {
"unit": "short",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": null
},
{
"color": "green",
"value": 1
}
]
}
}
},
"options": {
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"orientation": "auto",
"textMode": "auto",
"colorMode": "value",
"graphMode": "none"
}
},
{
"type": "stat",
"title": "Services DOWN",
"id": 2,
"gridPos": {
"h": 5,
"w": 6,
"x": 6,
"y": 0
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "count(probe_success{job=\"<namespace>-<env>-<endpoint>-probe\",instance=~\"$instance\"}) - sum(probe_success{job=\"<namespace>-<env>-<endpoint>-probe\",instance=~\"$instance\"})"
}
],
"fieldConfig": {
"defaults": {
"unit": "short",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 1
}
]
}
}
},
"options": {
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"orientation": "auto",
"textMode": "auto",
"colorMode": "value",
"graphMode": "none"
}
},
{
"type": "stat",
"title": "Availability 24h",
"id": 3,
"gridPos": {
"h": 5,
"w": 6,
"x": 12,
"y": 0
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "avg(avg_over_time(probe_success{job=\"<namespace>-<env>-<endpoint>-probe\",instance=~\"$instance\"}[24h])) * 100"
}
],
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": null
},
{
"color": "orange",
"value": 99
},
{
"color": "green",
"value": 99.9
}
]
}
}
},
"options": {
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"orientation": "auto",
"textMode": "auto",
"colorMode": "value",
"graphMode": "area"
}
},
{
"type": "stat",
"title": "Avg Latency",
"id": 4,
"gridPos": {
"h": 5,
"w": 6,
"x": 18,
"y": 0
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "avg(probe_duration_seconds{job=\"<namespace>-<env>-<endpoint>-probe\",instance=~\"$instance\"}) * 1000"
}
],
"fieldConfig": {
"defaults": {
"unit": "ms",
"decimals": 0
}
},
"options": {
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"orientation": "auto",
"textMode": "auto",
"colorMode": "value",
"graphMode": "none"
}
},
{
"type": "table",
"title": "HTTP Services Status",
"id": 5,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 5
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "probe_success{job=\"<namespace>-<env>-<endpoint>-probe\",instance=~\"$instance\"}",
"instant": true,
"format": "table"
}
],
"fieldConfig": {
"defaults": {
"custom": {
"align": "auto",
"cellOptions": {
"type": "color-text"
}
}
}
},
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true
},
"renameByName": {
"Value": "Status",
"instance": "Service"
}
}
}
],
"options": {
"showHeader": true,
"cellHeight": "sm"
}
},
{
"type": "table",
"title": "HTTP Status Code",
"id": 6,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 5
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "probe_http_status_code{job=\"<namespace>-<env>-<endpoint>-probe\",instance=~\"$instance\"}",
"instant": true,
"format": "table"
}
],
"fieldConfig": {
"defaults": {
"unit": "short",
"custom": {
"align": "auto",
"cellOptions": {
"type": "color-text"
}
}
}
},
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true
},
"renameByName": {
"Value": "HTTP Status",
"instance": "Service"
}
}
}
],
"options": {
"showHeader": true,
"cellHeight": "sm"
}
},
{
"type": "timeseries",
"title": "HTTP Response Time",
"id": 7,
"gridPos": {
"h": 9,
"w": 24,
"x": 0,
"y": 13
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "probe_duration_seconds{job=\"<namespace>-<env>-<endpoint>-probe\",instance=~\"$instance\"} * 1000",
"legendFormat": "{{instance}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "ms",
"decimals": 0
}
},
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
}
},
{
"type": "table",
"title": "Currently DOWN",
"id": 8,
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 22
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "probe_success{job=\"<namespace>-<env>-<endpoint>-probe\",instance=~\"$instance\"} == 0",
"instant": true,
"format": "table"
}
],
"fieldConfig": {
"defaults": {
"custom": {
"cellOptions": {
"type": "color-text"
}
}
}
},
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true
},
"renameByName": {
"Value": "Status",
"instance": "Service"
}
}
}
],
"options": {
"showHeader": true,
"cellHeight": "sm"
}
},
{
"type": "timeseries",
"title": "Availability 24h",
"id": 9,
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 22
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "avg_over_time(probe_success{job=\"<namespace>-<env>-<endpoint>-probe\",instance=~\"$instance\"}[24h]) * 100",
"legendFormat": "{{instance}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"decimals": 2
}
},
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
}
},
{
"type": "timeseries",
"title": "P95 Latency - 1h",
"id": 10,
"gridPos": {
"h": 9,
"w": 24,
"x": 0,
"y": 31
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "quantile_over_time(0.95, probe_duration_seconds{job=\"<namespace>-<env>-<endpoint>-probe\",instance=~\"$instance\"}[1h]) * 1000",
"legendFormat": "{{instance}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "ms",
"decimals": 0
}
},
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
}
}
],
"refresh": "30s",
"schemaVersion": 41,
"tags": [
"blackbox",
"http",
"monitoring"
],
"templating": {
"list": [
{
"name": "instance",
"label": "Service",
"type": "query",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"definition": "label_values(probe_success{job=\"<namespace>-<env>-<endpoint>-probe\"}, instance)",
"query": {
"query": "label_values(probe_success{job=\"<namespace>-<env>-<endpoint>-probe\"}, instance)",
"refId": "StandardVariableQuery"
},
"includeAll": true,
"multi": true,
"allValue": ".*",
"refresh": 1,
"sort": 1
}
]
},
"time": {
"from": "now-24h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "<namespace>-<env>-<endpoint>-dashboard",
"uid": "<namespace>-<env>-<endpoint>-dashboard",
"version": 1,
"weekStart": ""
}
@@ -0,0 +1,553 @@
apiVersion: monitoring.coreos.com/v1
kind: Probe
metadata:
name: <my-http-services-probe>
namespace: monitoring
labels:
release: kube-prometheus-stack
spec:
jobName: <my-http-services-probe>
interval: 30s
scrapeTimeout: 15s
module: http_k8s_health
prober:
url: blackbox-exporter.monitoring.svc.cluster.local:9115
scheme: http
path: /probe
targets:
staticConfig:
static:
- http://<my-service>.<my-namespace>.svc.cluster.local:<my-port>/health
---
apiVersion: v1
kind: ConfigMap
metadata:
name: <my-http-services-dashboard>
namespace: monitoring
labels:
grafana_dashboard: "1"
data:
<my-http-services-dashboard>.json: |
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"links": [],
"panels": [
{
"type": "stat",
"title": "Services UP",
"id": 1,
"gridPos": {
"h": 5,
"w": 6,
"x": 0,
"y": 0
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "sum(probe_success{job=\"<my-http-services-probe>\",instance=~\"$instance\"})"
}
],
"fieldConfig": {
"defaults": {
"unit": "short",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": null
},
{
"color": "green",
"value": 1
}
]
}
}
},
"options": {
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"orientation": "auto",
"textMode": "auto",
"colorMode": "value",
"graphMode": "none"
}
},
{
"type": "stat",
"title": "Services DOWN",
"id": 2,
"gridPos": {
"h": 5,
"w": 6,
"x": 6,
"y": 0
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "count(probe_success{job=\"<my-http-services-probe>\",instance=~\"$instance\"}) - sum(probe_success{job=\"<my-http-services-probe>\",instance=~\"$instance\"})"
}
],
"fieldConfig": {
"defaults": {
"unit": "short",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 1
}
]
}
}
},
"options": {
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"orientation": "auto",
"textMode": "auto",
"colorMode": "value",
"graphMode": "none"
}
},
{
"type": "stat",
"title": "Availability 24h",
"id": 3,
"gridPos": {
"h": 5,
"w": 6,
"x": 12,
"y": 0
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "avg(avg_over_time(probe_success{job=\"<my-http-services-probe>\",instance=~\"$instance\"}[24h])) * 100"
}
],
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": null
},
{
"color": "orange",
"value": 99
},
{
"color": "green",
"value": 99.9
}
]
}
}
},
"options": {
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"orientation": "auto",
"textMode": "auto",
"colorMode": "value",
"graphMode": "area"
}
},
{
"type": "stat",
"title": "Avg Latency",
"id": 4,
"gridPos": {
"h": 5,
"w": 6,
"x": 18,
"y": 0
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "avg(probe_duration_seconds{job=\"<my-http-services-probe>\",instance=~\"$instance\"}) * 1000"
}
],
"fieldConfig": {
"defaults": {
"unit": "ms",
"decimals": 0
}
},
"options": {
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"orientation": "auto",
"textMode": "auto",
"colorMode": "value",
"graphMode": "none"
}
},
{
"type": "table",
"title": "HTTP Services Status",
"id": 5,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 5
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "probe_success{job=\"<my-http-services-probe>\",instance=~\"$instance\"}",
"instant": true,
"format": "table"
}
],
"fieldConfig": {
"defaults": {
"custom": {
"align": "auto",
"cellOptions": {
"type": "color-text"
}
}
}
},
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true
},
"renameByName": {
"Value": "Status",
"instance": "Service"
}
}
}
],
"options": {
"showHeader": true,
"cellHeight": "sm"
}
},
{
"type": "table",
"title": "HTTP Status Code",
"id": 6,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 5
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "probe_http_status_code{job=\"<my-http-services-probe>\",instance=~\"$instance\"}",
"instant": true,
"format": "table"
}
],
"fieldConfig": {
"defaults": {
"unit": "short",
"custom": {
"align": "auto",
"cellOptions": {
"type": "color-text"
}
}
}
},
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true
},
"renameByName": {
"Value": "HTTP Status",
"instance": "Service"
}
}
}
],
"options": {
"showHeader": true,
"cellHeight": "sm"
}
},
{
"type": "timeseries",
"title": "HTTP Response Time",
"id": 7,
"gridPos": {
"h": 9,
"w": 24,
"x": 0,
"y": 13
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "probe_duration_seconds{job=\"<my-http-services-probe>\",instance=~\"$instance\"} * 1000",
"legendFormat": "{{instance}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "ms",
"decimals": 0
}
},
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
}
},
{
"type": "table",
"title": "Currently DOWN",
"id": 8,
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 22
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "probe_success{job=\"<my-http-services-probe>\",instance=~\"$instance\"} == 0",
"instant": true,
"format": "table"
}
],
"fieldConfig": {
"defaults": {
"custom": {
"cellOptions": {
"type": "color-text"
}
}
}
},
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true
},
"renameByName": {
"Value": "Status",
"instance": "Service"
}
}
}
],
"options": {
"showHeader": true,
"cellHeight": "sm"
}
},
{
"type": "timeseries",
"title": "Availability 24h",
"id": 9,
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 22
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "avg_over_time(probe_success{job=\"<my-http-services-probe>\",instance=~\"$instance\"}[24h]) * 100",
"legendFormat": "{{instance}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"decimals": 2
}
},
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
}
},
{
"type": "timeseries",
"title": "P95 Latency - 1h",
"id": 10,
"gridPos": {
"h": 9,
"w": 24,
"x": 0,
"y": 31
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"targets": [
{
"refId": "A",
"expr": "quantile_over_time(0.95, probe_duration_seconds{job=\"<my-http-services-probe>\",instance=~\"$instance\"}[1h]) * 1000",
"legendFormat": "{{instance}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "ms",
"decimals": 0
}
},
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
}
}
],
"refresh": "30s",
"schemaVersion": 41,
"tags": [
"blackbox",
"http",
"monitoring"
],
"templating": {
"list": [
{
"name": "instance",
"label": "Service",
"type": "query",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"definition": "label_values(probe_success{job=\"<my-http-services-probe>\"}, instance)",
"query": {
"query": "label_values(probe_success{job=\"<my-http-services-probe>\"}, instance)",
"refId": "StandardVariableQuery"
},
"includeAll": true,
"multi": true,
"allValue": ".*",
"refresh": 1,
"sort": 1
}
]
},
"time": {
"from": "now-24h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "<my-http-services-dashboard>",
"uid": "<my-http-services-dashboard>",
"version": 1,
"weekStart": ""
}
+88
View File
@@ -0,0 +1,88 @@
# MySQL Database Operator
A small Kubernetes controller that provides a declarative `MySQLDatabase` CRD on top of an existing MySQL service, including Oracle MySQL Operator / InnoDB Cluster deployments.
## Scope
This MVP manages:
- `CREATE DATABASE IF NOT EXISTS`
- optional application user creation
- application credentials Secret generation
- database-scoped grants
- `Retain` or `Delete` deletion policy
- status/conditions
It does **not** create or manage the MySQL/InnoDBCluster itself. The Oracle MySQL Operator remains responsible for the MySQL infrastructure.
## Important production notes
1. `adminSecretRef` must contain `username` and `password`.
2. The controller currently connects to the host in `spec.connection.host`; if omitted it uses `spec.clusterRef.name`.
3. The generated application Secret contains `username`, `password`, and `database`.
4. The user is created as `'user'@'%'` because Kubernetes workloads normally connect over the cluster network. Tighten this later if your network model supports it.
5. The controller intentionally supports a small allow-list of privileges; do not accept arbitrary SQL from the CRD.
6. For production, add TLS support, NetworkPolicy, Secret rotation policy, metrics, admission validation, and tests before broad multi-team rollout.
## Build
```bash
go mod tidy
go build ./cmd
docker build -t harbor.example.com/platform/mysql-database-operator:0.1.0 .
docker push harbor.example.com/platform/mysql-database-operator:0.1.0
```
## Deploy on RKE2
```bash
kubectl create namespace mysql-database-operator-system
kubectl apply -f config/crd/mysqldatabase-crd.yaml
kubectl apply -f deploy/rbac.yaml
sed 's#ghcr.io/example/mysql-database-operator:0.1.0#harbor.example.com/platform/mysql-database-operator:0.1.0#' deploy/deployment.yaml | kubectl apply -f -
```
Or Helm:
```bash
helm upgrade --install mysql-database-operator ./helm/mysql-database-operator \
--namespace mysql-database-operator-system \
--create-namespace \
--set image.repository=harbor.example.com/platform/mysql-database-operator \
--set image.tag=0.1.0
```
Then create the admin Secret and database:
```bash
kubectl create namespace team-a
kubectl apply -f examples/admin-secret.yaml
kubectl apply -f examples/mysql-database.yaml
kubectl get mysqldatabase -n team-a
kubectl describe mysqldatabase orders -n team-a
kubectl get secret orders-db-credentials -n team-a
```
## Oracle MySQL Operator integration
Example architecture:
```text
InnoDBCluster mysql-prod
|
| Service mysql-prod:3306
v
MySQLDatabase orders
|
+-- CREATE DATABASE orders
+-- CREATE USER orders-app
+-- GRANT SELECT, INSERT, UPDATE, DELETE
+-- Secret orders-db-credentials
```
Use the actual Router/primary Service exposed by your Oracle MySQL Operator installation in `spec.connection.host` if it is not the same as `clusterRef.name`.
## Verification
The source tree was formatted with `gofmt`. Full `go test ./...` could not be executed in this environment because external Go module downloads are disabled; run `go mod tidy && go test ./...` in a network-enabled build environment before publishing the image.
+23
View File
@@ -0,0 +1,23 @@
bcdb6dd502904deb1982acff76dbcecef61fe64c1edce33dbc535fe714809d0f ./Dockerfile
811616c4bfe6df79795d4cfe845926b9e1af47501421d092f67bf511f4cdd673 ./README.md
ffe83a66f937391f9c6b79450f5161095bdba5c794838b2bc8965c715c5849ca ./api/v1alpha1/groupversion_info.go
12f4d95346545e4dd390dcd5718ae0e5104ffd3e6153b9ead6bfaf78e7293d09 ./api/v1alpha1/mysqldatabase_types.go
89e9b851001dcb3a3bf88de546fbde273d9cab9951b389d9a07bf106adc570f5 ./api/v1alpha1/zz_generated.deepcopy.go
08be703122880eb86372acdbe9d560280f9c2428cf2279cc05061bbf79cb8836 ./cmd/main.go
78813f202aa5a135a25f1c1524fdc8e09174f13f601985923a00cf6d9fcc6f28 ./config/crd/mysqldatabase-crd.yaml
c9f3d6c121d19bf780f6a35b69eb242202164c38fc201ad67f6789e363620bf4 ./controllers/mysqldatabase_controller.go
b164fdebc25270c033e6b5608a6b7eb005007757913ff3f7dea692f2ff306777 ./controllers/setup.go
75d21caeb9f41976855fa8421e07f31cee39877844ca30d9f0a3104863090458 ./deploy/deployment.yaml
18f1896539466ce1d0707ca42877d94e97b6e50af72924ecb236cbc276dd4690 ./deploy/namespace.yaml
0de7361b72bcd016c49d44e1bc1a292d6ceba4ab2836218732bd133380151472 ./deploy/rbac.yaml
f31d3c24dc3f666ddd62af0bc8dde2b2623edfa370099e4b759e3cfc87a40cf5 ./examples/admin-secret.yaml
9f954df207eebfa0f1551bdeb05ec97f33ea7184a8c69aad7526b25ffb98cb99 ./examples/mysql-database.yaml
a17f89658ffb2d16a6c64e6bae3a99f8b3fc5130a6c00fdeb6bbd231cde0488f ./go.mod
3266515bc31fd4465da3eacc34d98ddbb18e67444d855438c683a8875f7bce03 ./helm/mysql-database-operator/Chart.yaml
dcb57bf001e957e3abad642ca56c76ba2b65b6c6419dc9148b95be3293f29a4b ./helm/mysql-database-operator/templates/_helpers.tpl
78813f202aa5a135a25f1c1524fdc8e09174f13f601985923a00cf6d9fcc6f28 ./helm/mysql-database-operator/templates/crd.yaml
55159600ff11ee14ab542d6ad647129fb1321fc591e17693340d481af4467ab2 ./helm/mysql-database-operator/templates/deployment.yaml
dd8a29aa027dfc40a77f295fd4d70ab950c14bcc8a3c8bf7efc98f2ff77ee75d ./helm/mysql-database-operator/templates/namespace.yaml
e06e41e699589b14609bb5f32555c01851ba4a7e514d647f3191623832e09e01 ./helm/mysql-database-operator/templates/rbac.yaml
107ddf20ea52d35fbd2dfbbbcadc86e8848b6013566e4fd4229326d9087cce7e ./helm/mysql-database-operator/templates/serviceaccount.yaml
8d6267287c8b41e1c30b11d13c73273aaf48d2a69f717ea0de1f7dda72a26c78 ./helm/mysql-database-operator/values.yaml
@@ -0,0 +1,15 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var GroupVersion = schema.GroupVersion{Group: "platform.example.com", Version: "v1alpha1"}
var SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
var AddToScheme = SchemeBuilder.AddToScheme
func init() { SchemeBuilder.Register(&MySQLDatabase{}, &MySQLDatabaseList{}) }
var _ = metav1.Now
@@ -0,0 +1,68 @@
package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// MySQLDatabaseSpec defines the desired state of a logical MySQL database.
type MySQLDatabaseSpec struct {
ClusterRef ClusterReference `json:"clusterRef"`
DatabaseName string `json:"databaseName"`
AdminSecretRef SecretReference `json:"adminSecretRef"`
Connection ConnectionSpec `json:"connection,omitempty"`
User *UserSpec `json:"user,omitempty"`
Credentials *CredentialsSpec `json:"credentials,omitempty"`
Privileges []string `json:"privileges,omitempty"`
Charset string `json:"charset,omitempty"`
Collation string `json:"collation,omitempty"`
DeletionPolicy string `json:"deletionPolicy,omitempty"`
}
type ClusterReference struct {
Name string `json:"name"`
}
type SecretReference struct {
Name string `json:"name"`
}
type ConnectionSpec struct {
Host string `json:"host,omitempty"`
Port int32 `json:"port,omitempty"`
}
type UserSpec struct {
Name string `json:"name"`
Create bool `json:"create,omitempty"`
}
type CredentialsSpec struct {
SecretName string `json:"secretName"`
}
type MySQLDatabaseStatus struct {
Phase string `json:"phase,omitempty"`
Message string `json:"message,omitempty"`
DatabaseName string `json:"databaseName,omitempty"`
UserName string `json:"userName,omitempty"`
SecretName string `json:"secretName,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Database",type="string",JSONPath=".status.databaseName"
// +kubebuilder:printcolumn:name="User",type="string",JSONPath=".status.userName"
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
type MySQLDatabase struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec MySQLDatabaseSpec `json:"spec"`
Status MySQLDatabaseStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
type MySQLDatabaseList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []MySQLDatabase `json:"items"`
}
@@ -0,0 +1,37 @@
// Code generated by controller-gen style deepcopy implementation. DO NOT EDIT.
package v1alpha1
func (in *MySQLDatabase) DeepCopyInto(out *MySQLDatabase) {
*out = *in
out.ObjectMeta = *in.ObjectMeta.DeepCopy()
out.Status = in.Status
if in.Status.Conditions != nil {
out.Status.Conditions = make([]metav1.Condition, len(in.Status.Conditions))
copy(out.Status.Conditions, in.Status.Conditions)
}
}
func (in *MySQLDatabase) DeepCopy() *MySQLDatabase {
if in == nil {
return nil
}
out := new(MySQLDatabase)
in.DeepCopyInto(out)
return out
}
func (in *MySQLDatabaseList) DeepCopyInto(out *MySQLDatabaseList) {
*out = *in
if in.Items != nil {
out.Items = make([]MySQLDatabase, len(in.Items))
for i := range in.Items {
in.Items[i].DeepCopyInto(&out.Items[i])
}
}
}
func (in *MySQLDatabaseList) DeepCopy() *MySQLDatabaseList {
if in == nil {
return nil
}
out := new(MySQLDatabaseList)
in.DeepCopyInto(out)
return out
}
@@ -0,0 +1,45 @@
package main
import (
"flag"
"github.com/example/mysql-database-operator/api/v1alpha1"
"github.com/example/mysql-database-operator/controllers"
"k8s.io/apimachinery/pkg/runtime"
"os"
"sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/metrics/server"
)
func main() {
var metricsAddr, probeAddr string
var leader bool
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "metrics address")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "health probe address")
flag.BoolVar(&leader, "leader-elect", true, "enable leader election")
var zo zap.Options
zo.BindFlags(flag.CommandLine)
flag.Parse()
controller.SetLogger(zap.New(zap.UseFlagOptions(&zo)))
scheme := runtime.NewScheme()
_ = v1alpha1.AddToScheme(scheme)
mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{Scheme: scheme, Metrics: server.Options{BindAddress: metricsAddr}, HealthProbeBindAddress: probeAddr, LeaderElection: leader, LeaderElectionID: "mysql-database-operator"})
if err != nil {
os.Exit(1)
}
if err := (&controllers.MySQLDatabaseReconciler{Client: mgr.GetClient(), Scheme: scheme}).SetupWithManager(mgr); err != nil {
os.Exit(1)
}
if err := mgr.AddHealthzCheck("health", healthz.Ping); err != nil {
os.Exit(1)
}
if err := mgr.AddReadyzCheck("ready", healthz.Ping); err != nil {
os.Exit(1)
}
if err := mgr.Start(controller.SetupSignalHandler()); err != nil {
os.Exit(1)
}
}
@@ -0,0 +1,93 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: mysqldatabases.platform.example.com
spec:
group: platform.example.com
scope: Namespaced
names:
plural: mysqldatabases
singular: mysqldatabase
kind: MySQLDatabase
shortNames: [mysqldb]
versions:
- name: v1alpha1
served: true
storage: true
subresources:
status: {}
additionalPrinterColumns:
- name: Database
type: string
jsonPath: .status.databaseName
- name: User
type: string
jsonPath: .status.userName
- name: Phase
type: string
jsonPath: .status.phase
schema:
openAPIV3Schema:
type: object
required: [spec]
properties:
spec:
type: object
required: [clusterRef,databaseName,adminSecretRef]
properties:
clusterRef:
type: object
required: [name]
properties: {name: {type: string}}
databaseName:
type: string
minLength: 1
maxLength: 64
pattern: '^[A-Za-z0-9_$-]+$'
adminSecretRef:
type: object
required: [name]
properties: {name: {type: string}}
connection:
type: object
properties:
host: {type: string}
port: {type: integer, minimum: 1, maximum: 65535}
user:
type: object
properties:
name: {type: string, maxLength: 32}
create: {type: boolean}
credentials:
type: object
properties:
secretName: {type: string}
privileges:
type: array
items: {type: string}
charset: {type: string}
collation: {type: string}
deletionPolicy:
type: string
enum: [Retain, Delete]
default: Retain
status:
type: object
properties:
phase: {type: string}
message: {type: string}
databaseName: {type: string}
userName: {type: string}
secretName: {type: string}
observedGeneration: {type: integer, format: int64}
conditions:
type: array
items:
type: object
required: [type,status,reason,message,lastTransitionTime]
properties:
type: {type: string}
status: {type: string}
reason: {type: string}
message: {type: string}
lastTransitionTime: {type: string, format: date-time}
@@ -0,0 +1,242 @@
package controllers
import (
"context"
"crypto/rand"
"database/sql"
"fmt"
"math/big"
"strings"
"time"
platformv1alpha1 "github.com/example/mysql-database-operator/api/v1alpha1"
_ "github.com/go-sql-driver/mysql"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
const finalizer = "platform.example.com/mysqldatabase-finalizer"
type MySQLDatabaseReconciler struct{ client.Client }
func (r *MySQLDatabaseReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
logger := log.FromContext(ctx)
var obj platformv1alpha1.MySQLDatabase
if err := r.Get(ctx, req.NamespacedName, &obj); err != nil {
if apierrors.IsNotFound(err) {
return reconcile.Result{}, nil
}
return reconcile.Result{}, err
}
if obj.Spec.DatabaseName == "" {
return r.setError(ctx, &obj, "InvalidSpec", "databaseName is required")
}
if obj.Spec.AdminSecretRef.Name == "" {
return r.setError(ctx, &obj, "InvalidSpec", "adminSecretRef.name is required")
}
if !obj.DeletionTimestamp.IsZero() {
if !controllerutil.ContainsFinalizer(&obj, finalizer) {
return reconcile.Result{}, nil
}
if strings.EqualFold(obj.Spec.DeletionPolicy, "Delete") {
if err := r.withDB(ctx, &obj, func(db *sql.DB) error {
_, err := db.ExecContext(ctx, "DROP DATABASE IF EXISTS `"+quoteIdent(obj.Spec.DatabaseName)+"`")
return err
}); err != nil {
return reconcile.Result{RequeueAfter: 10 * time.Second}, err
}
}
controllerutil.RemoveFinalizer(&obj, finalizer)
return reconcile.Result{}, r.Update(ctx, &obj)
}
if !controllerutil.ContainsFinalizer(&obj, finalizer) {
controllerutil.AddFinalizer(&obj, finalizer)
return reconcile.Result{}, r.Update(ctx, &obj)
}
if err := r.withDB(ctx, &obj, func(db *sql.DB) error { return ensureDatabase(ctx, db, &obj) }); err != nil {
return r.setError(ctx, &obj, "ConnectionError", err.Error())
}
if obj.Spec.User != nil && obj.Spec.User.Create {
username := obj.Spec.User.Name
if username == "" {
return r.setError(ctx, &obj, "InvalidSpec", "user.name is required when user.create is true")
}
password, err := r.ensureCredentials(ctx, &obj, username)
if err != nil {
return r.setError(ctx, &obj, "CredentialsError", err.Error())
}
if err := r.withDB(ctx, &obj, func(db *sql.DB) error { return ensureUserAndGrants(ctx, db, &obj, password) }); err != nil {
return r.setError(ctx, &obj, "UserError", err.Error())
}
}
obj.Status.Phase = "Ready"
obj.Status.Message = "Database reconciled"
obj.Status.DatabaseName = obj.Spec.DatabaseName
if obj.Spec.User != nil {
obj.Status.UserName = obj.Spec.User.Name
}
if obj.Spec.Credentials != nil {
obj.Status.SecretName = obj.Spec.Credentials.SecretName
}
obj.Status.ObservedGeneration = obj.Generation
obj.Status.Conditions = []metav1.Condition{{Type: "Ready", Status: metav1.ConditionTrue, Reason: "Reconciled", Message: "MySQL database is ready", LastTransitionTime: metav1.Now()}}
if err := r.Status().Update(ctx, &obj); err != nil {
logger.Error(err, "status update failed")
return reconcile.Result{}, err
}
return reconcile.Result{RequeueAfter: 5 * time.Minute}, nil
}
func (r *MySQLDatabaseReconciler) setError(ctx context.Context, obj *platformv1alpha1.MySQLDatabase, reason, msg string) (reconcile.Result, error) {
obj.Status.Phase = "Error"
obj.Status.Message = msg
obj.Status.ObservedGeneration = obj.Generation
obj.Status.Conditions = []metav1.Condition{{Type: "Ready", Status: metav1.ConditionFalse, Reason: reason, Message: msg, LastTransitionTime: metav1.Now()}}
_ = r.Status().Update(ctx, obj)
return reconcile.Result{RequeueAfter: 15 * time.Second}, fmt.Errorf("%s: %s", reason, msg)
}
func (r *MySQLDatabaseReconciler) withDB(ctx context.Context, obj *platformv1alpha1.MySQLDatabase, fn func(*sql.DB) error) error {
var admin corev1.Secret
if err := r.Get(ctx, types.NamespacedName{Name: obj.Spec.AdminSecretRef.Name, Namespace: obj.Namespace}, &admin); err != nil {
return err
}
username, password := string(admin.Data["username"]), string(admin.Data["password"])
if username == "" || password == "" {
return fmt.Errorf("admin secret %q must contain username and password", obj.Spec.AdminSecretRef.Name)
}
host := obj.Spec.Connection.Host
if host == "" {
host = obj.Spec.ClusterRef.Name
}
port := obj.Spec.Connection.Port
if port == 0 {
port = 3306
}
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/?parseTime=true&timeout=5s&readTimeout=10s&writeTimeout=10s", username, password, host, port)
db, err := sql.Open("mysql", dsn)
if err != nil {
return err
}
defer db.Close()
pingCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
if err := db.PingContext(pingCtx); err != nil {
return err
}
return fn(db)
}
func ensureDatabase(ctx context.Context, db *sql.DB, obj *platformv1alpha1.MySQLDatabase) error {
q := "CREATE DATABASE IF NOT EXISTS `" + quoteIdent(obj.Spec.DatabaseName) + "`"
if obj.Spec.Charset != "" {
q += " CHARACTER SET " + safeSQLWord(obj.Spec.Charset)
}
if obj.Spec.Collation != "" {
q += " COLLATE " + safeSQLWord(obj.Spec.Collation)
}
_, err := db.ExecContext(ctx, q)
return err
}
func ensureUserAndGrants(ctx context.Context, db *sql.DB, obj *platformv1alpha1.MySQLDatabase, password string) error {
u := quoteIdent(obj.Spec.User.Name)
// User is intentionally scoped to localhost wildcard host. For networked K8s apps, '%' is needed.
if _, err := db.ExecContext(ctx, "CREATE USER IF NOT EXISTS '"+quoteString(obj.Spec.User.Name)+"'@'%' IDENTIFIED BY '"+quoteString(password)+"'"); err != nil {
return err
}
if _, err := db.ExecContext(ctx, "ALTER USER '"+quoteString(obj.Spec.User.Name)+"'@'%' IDENTIFIED BY '"+quoteString(password)+"'"); err != nil {
return err
}
grants := obj.Spec.Privileges
if len(grants) == 0 {
grants = []string{"SELECT", "INSERT", "UPDATE", "DELETE"}
}
for _, g := range grants {
if !isAllowedPrivilege(g) {
return fmt.Errorf("unsupported privilege %q", g)
}
}
grant := strings.Join(grants, ", ")
_, err := db.ExecContext(ctx, "GRANT "+grant+" ON `"+quoteIdent(obj.Spec.DatabaseName)+"`.* TO '"+quoteString(obj.Spec.User.Name)+"'@'%'")
_ = u
return err
}
func (r *MySQLDatabaseReconciler) ensureCredentials(ctx context.Context, obj *platformv1alpha1.MySQLDatabase, username string) (string, error) {
if obj.Spec.Credentials == nil || obj.Spec.Credentials.SecretName == "" {
return "", fmt.Errorf("credentials.secretName is required when user.create is true")
}
key := types.NamespacedName{Name: obj.Spec.Credentials.SecretName, Namespace: obj.Namespace}
var s corev1.Secret
err := r.Get(ctx, key, &s)
if err == nil {
p := string(s.Data["password"])
if p == "" {
return "", fmt.Errorf("credentials secret must contain password")
}
if string(s.Data["username"]) == "" {
s.Data["username"] = []byte(username)
if err := r.Update(ctx, &s); err != nil {
return "", err
}
}
return p, nil
}
if !apierrors.IsNotFound(err) {
return "", err
}
p, err := randomPassword(32)
if err != nil {
return "", err
}
s = corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: key.Name, Namespace: key.Namespace}, Type: corev1.SecretTypeOpaque, StringData: map[string]string{"username": username, "password": p, "database": obj.Spec.DatabaseName}}
if err := controllerutil.SetControllerReference(obj, &s, r.Scheme); err != nil {
return "", err
}
if err := r.Create(ctx, &s); err != nil {
return "", err
}
return p, nil
}
func randomPassword(n int) (string, error) {
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*_-"
b := make([]byte, n)
for i := range b {
x, e := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
if e != nil {
return "", e
}
b[i] = chars[x.Int64()]
}
return string(b), nil
}
func quoteIdent(s string) string { return strings.ReplaceAll(s, "`", "``") }
func quoteString(s string) string {
return strings.ReplaceAll(strings.ReplaceAll(s, "\\", "\\\\"), "'", "\\'")
}
func safeSQLWord(s string) string {
for _, r := range s {
if !(r == '_' || r == '-' || r == '.' || r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9') {
return "utf8mb4"
}
}
return s
}
func isAllowedPrivilege(s string) bool {
switch strings.ToUpper(strings.TrimSpace(s)) {
case "SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "ALTER", "INDEX", "DROP", "REFERENCES", "EXECUTE", "SHOW VIEW", "TRIGGER":
return true
default:
return false
}
}
@@ -0,0 +1,13 @@
package controllers
import (
platformv1alpha1 "github.com/example/mysql-database-operator/api/v1alpha1"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/manager"
)
func (r *MySQLDatabaseReconciler) SetupWithManager(mgr manager.Manager) error {
r.Client = mgr.GetClient()
return builder.ControllerManagedBy(mgr).For(&platformv1alpha1.MySQLDatabase{}).Owns(&corev1.Secret{}).Complete(r)
}
@@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: mysql-prod-admin
namespace: team-a
type: Opaque
stringData:
username: root
password: CHANGE-ME
@@ -0,0 +1,28 @@
apiVersion: platform.example.com/v1alpha1
kind: MySQLDatabase
metadata:
name: orders
namespace: team-a
spec:
clusterRef:
name: mysql-prod
# If omitted, the controller uses clusterRef.name as the Service DNS name.
connection:
host: mysql-prod
port: 3306
databaseName: orders
adminSecretRef:
name: mysql-prod-admin
user:
name: orders-app
create: true
credentials:
secretName: orders-db-credentials
privileges:
- SELECT
- INSERT
- UPDATE
- DELETE
charset: utf8mb4
collation: utf8mb4_0900_ai_ci
deletionPolicy: Retain
@@ -0,0 +1,6 @@
apiVersion: v2
name: mysql-database-operator
description: Declarative MySQL database/user controller for Kubernetes
version: 0.1.0
appVersion: "0.1.0"
type: application
@@ -0,0 +1,2 @@
{{- define "mysql-database-operator.name" -}}mysql-database-operator{{- end }}
{{- define "mysql-database-operator.fullname" -}}{{ include "mysql-database-operator.name" . }}{{- end }}
@@ -0,0 +1,93 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: mysqldatabases.platform.example.com
spec:
group: platform.example.com
scope: Namespaced
names:
plural: mysqldatabases
singular: mysqldatabase
kind: MySQLDatabase
shortNames: [mysqldb]
versions:
- name: v1alpha1
served: true
storage: true
subresources:
status: {}
additionalPrinterColumns:
- name: Database
type: string
jsonPath: .status.databaseName
- name: User
type: string
jsonPath: .status.userName
- name: Phase
type: string
jsonPath: .status.phase
schema:
openAPIV3Schema:
type: object
required: [spec]
properties:
spec:
type: object
required: [clusterRef,databaseName,adminSecretRef]
properties:
clusterRef:
type: object
required: [name]
properties: {name: {type: string}}
databaseName:
type: string
minLength: 1
maxLength: 64
pattern: '^[A-Za-z0-9_$-]+$'
adminSecretRef:
type: object
required: [name]
properties: {name: {type: string}}
connection:
type: object
properties:
host: {type: string}
port: {type: integer, minimum: 1, maximum: 65535}
user:
type: object
properties:
name: {type: string, maxLength: 32}
create: {type: boolean}
credentials:
type: object
properties:
secretName: {type: string}
privileges:
type: array
items: {type: string}
charset: {type: string}
collation: {type: string}
deletionPolicy:
type: string
enum: [Retain, Delete]
default: Retain
status:
type: object
properties:
phase: {type: string}
message: {type: string}
databaseName: {type: string}
userName: {type: string}
secretName: {type: string}
observedGeneration: {type: integer, format: int64}
conditions:
type: array
items:
type: object
required: [type,status,reason,message,lastTransitionTime]
properties:
type: {type: string}
status: {type: string}
reason: {type: string}
message: {type: string}
lastTransitionTime: {type: string, format: date-time}
@@ -0,0 +1,28 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-database-operator
namespace: mysql-database-operator-system
spec:
replicas: {{ .Values.replicaCount }}
selector: {matchLabels: {app: mysql-database-operator}}
template:
metadata: {labels: {app: mysql-database-operator}}
spec:
serviceAccountName: {{ .Values.serviceAccount.name }}
securityContext: {runAsNonRoot: true}
containers:
- name: manager
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args: ["--leader-elect={{ .Values.leaderElect }}"]
ports:
- {name: metrics, containerPort: 8080}
- {name: health, containerPort: 8081}
resources: {{- toYaml .Values.resources | nindent 10 }}
securityContext:
allowPrivilegeEscalation: false
capabilities: {drop: [ALL]}
seccompProfile: {type: RuntimeDefault}
readinessProbe: {httpGet: {path: /readyz, port: health}}
livenessProbe: {httpGet: {path: /healthz, port: health}}
@@ -0,0 +1,6 @@
{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: Namespace
metadata:
name: mysql-database-operator-system
{{- end }}
@@ -0,0 +1,33 @@
{{- if .Values.rbac.create }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata: {name: mysql-database-operator}
rules:
- apiGroups: [platform.example.com]
resources: [mysqldatabases]
verbs: [get,list,watch,create,update,patch,delete]
- apiGroups: [platform.example.com]
resources: [mysqldatabases/status]
verbs: [get,update,patch]
- apiGroups: [platform.example.com]
resources: [mysqldatabases/finalizers]
verbs: [update]
- apiGroups: [""]
resources: [secrets]
verbs: [get,list,watch,create,update,patch,delete]
- apiGroups: [""]
resources: [events]
verbs: [create,patch]
- apiGroups: [coordination.k8s.io]
resources: [leases]
verbs: [get,list,watch,create,update,patch,delete]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata: {name: mysql-database-operator}
roleRef: {apiGroup: rbac.authorization.k8s.io, kind: ClusterRole, name: mysql-database-operator}
subjects:
- kind: ServiceAccount
name: {{ .Values.serviceAccount.name }}
namespace: mysql-database-operator-system
{{- end }}
@@ -0,0 +1,5 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ .Values.serviceAccount.name }}
namespace: mysql-database-operator-system
@@ -0,0 +1,16 @@
image:
repository: ghcr.io/example/mysql-database-operator
tag: 0.1.0
pullPolicy: IfNotPresent
replicaCount: 1
leaderElect: true
resources:
requests: {cpu: 50m, memory: 64Mi}
limits: {cpu: 500m, memory: 256Mi}
serviceAccount:
create: true
name: mysql-database-operator
rbac:
create: true
crd:
install: true
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+7
View File
@@ -2,6 +2,13 @@
helm repo add sonarqube https://SonarSource.github.io/helm-chart-sonarqube
helm repo update
helm pull sonarqube/sonarqube --version 2026.1.0
helm registry login harbor.italiadatacenter.com -u admin
(pwd: KAYQE1QA7uwUZ8uI)
helm push sonarqube-2026.1.0.tgz oci://harbor.italiadatacenter.com/italiadatacenter
########### creazione ns e secret db ################
kubectl create namespace sonarqube
+85
View File
@@ -0,0 +1,85 @@
# gitops-repo - esempio completo (3 namespace, Fleet su RKE2)
Repo di esempio pronto da usare come base: GitOps limitato a
namespace-A, namespace-B, namespace-C, con Fleet come motore di sync.
## Struttura
```
.
├── bootstrap/ <- file legacy Argo CD (non usati con Fleet)
│ ├── 00-namespace.yaml
│ ├── 01-appproject.yaml
│ └── 02-root.yaml
│
└── clusters/
└── idc/ <- nome cluster/repository scope attuale
├── namespace-A/
│ ├── application.yaml <- da migrare a Fleet GitRepo
│ └── manifests/
│ └── deployment.yaml
├── namespace-B/
│ ├── application.yaml <- da migrare a Fleet GitRepo
│ └── manifests/
│ └── deployment.yaml
└── namespace-C/
├── application.yaml <- gia convertito a Fleet GitRepo
└── manifests/
└── deployment.yaml
```
## Cosa applichi a mano con Fleet
- Applichi a mano i manifest Fleet di tipo GitRepo (uno per namespace o uno aggregato).
- I file in clusters/idc/namespace-*/manifests vengono sincronizzati automaticamente da Fleet.
- I file in bootstrap sono legacy Argo CD: con Fleet installato, non sono necessari.
## Bootstrap Fleet - passo per passo
### 1. Verifica Fleet
```bash
kubectl -n cattle-fleet-system get pods
kubectl get crd gitrepos.fleet.cattle.io
```
### 2. Applica il GitRepo
Esempio con namespace-C:
```bash
kubectl apply -f clusters/idc/namespace-C/application.yaml
```
### 3. Verifica stato Fleet
```bash
kubectl -n fleet-local get gitrepo
kubectl -n cattle-fleet-system get bundles
kubectl -n cattle-fleet-system get bundledeployments
```
## Repo privato GitHub
Se il repo e privato, configura le credenziali per Fleet (Secret nel namespace Fleet usato, tipicamente fleet-local o fleet-default).
## Prima di usare questo repo
Sostituisci ovunque compare:
- <tua-org> -> org/utente reale del tuo repo Git
- i deployment di esempio -> i tuoi manifest reali
## Migrazione consigliata (A e B)
Attualmente solo namespace-C e gia in formato Fleet.
Per allineare tutto:
1. Converti clusters/idc/namespace-A/application.yaml in GitRepo Fleet.
2. Converti clusters/idc/namespace-B/application.yaml in GitRepo Fleet.
3. Applica i due manifest e verifica bundle/bundledeployments.
## Estendere a un quarto namespace in futuro
1. Crea clusters/idc/namespace-D/manifests con i tuoi YAML.
2. Crea clusters/idc/namespace-D/application.yaml in formato Fleet GitRepo.
3. Applica il manifest e verifica la generazione dei bundle.
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: argocd
labels:
app.kubernetes.io/name: argocd
+37
View File
@@ -0,0 +1,37 @@
# =====================================================================
# AppProject con perimetro ristretto a namespace-A, namespace-B,
# namespace-C. Qualunque Application che referenzi questo project e
# provi a scrivere in un namespace diverso da questi (+ argocd, per le
# risorse interne) viene rifiutata da Argo CD stesso.
# =====================================================================
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: gitops-3ns
namespace: argocd
spec:
description: "GitOps limitato a namespace-A, namespace-B, namespace-C"
sourceRepos:
- "https://github.com/<tua-org>/gitops-repo.git"
destinations:
- namespace: argocd
server: "https://kubernetes.default.svc"
- namespace: namespace-A
server: "https://kubernetes.default.svc"
- namespace: namespace-B
server: "https://kubernetes.default.svc"
- namespace: namespace-C
server: "https://kubernetes.default.svc"
clusterResourceWhitelist:
- group: ""
kind: Namespace
namespaceResourceWhitelist:
- group: "*"
kind: "*"
orphanedResources:
warn: true
+40
View File
@@ -0,0 +1,40 @@
# =====================================================================
# Root "app of apps": punta a gitops/clusters/idc/ nel repo. Grazie a
# directory.recurse=true, scopre automaticamente ogni application.yaml
# trovato dentro namespace-A/, namespace-B/, namespace-C/ e li applica
# come proprie Application figlie.
# =====================================================================
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root-3ns
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: gitops-3ns
source:
repoURL: "https://github.com/<tua-org>/gitops-repo.git"
targetRevision: main
path: gitops/clusters/idc
directory:
recurse: true
destination:
server: "https://kubernetes.default.svc"
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
retry:
limit: 5
backoff:
duration: 10s
factor: 2
maxDuration: 3m
@@ -0,0 +1,24 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: namespace-a
namespace: argocd
spec:
project: gitops-3ns
source:
repoURL: "https://github.com/<tua-org>/gitops-repo.git"
targetRevision: main
path: gitops/clusters/idc/namespace-A/manifests
destination:
server: "https://kubernetes.default.svc"
namespace: namespace-A
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
@@ -0,0 +1,37 @@
# =====================================================================
# Esempio di manifest reale — sostituiscilo con le tue risorse
# effettive. Ogni file .yaml in questa cartella viene applicato
# automaticamente da Argo CD (Application "namespace-a").
# =====================================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: esempio-app-a
namespace: namespace-A
spec:
replicas: 1
selector:
matchLabels:
app: esempio-app-a
template:
metadata:
labels:
app: esempio-app-a
spec:
containers:
- name: esempio-app-a
image: nginx:1.27
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: esempio-app-a
namespace: namespace-A
spec:
selector:
app: esempio-app-a
ports:
- port: 80
targetPort: 80
@@ -0,0 +1,24 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: namespace-b
namespace: argocd
spec:
project: gitops-3ns
source:
repoURL: "https://github.com/<tua-org>/gitops-repo.git"
targetRevision: main
path: gitops/clusters/idc/namespace-B/manifests
destination:
server: "https://kubernetes.default.svc"
namespace: namespace-B
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
@@ -0,0 +1,32 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: esempio-app-b
namespace: namespace-B
spec:
replicas: 1
selector:
matchLabels:
app: esempio-app-b
template:
metadata:
labels:
app: esempio-app-b
spec:
containers:
- name: esempio-app-b
image: nginx:1.27
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: esempio-app-b
namespace: namespace-B
spec:
selector:
app: esempio-app-b
ports:
- port: 80
targetPort: 80
@@ -0,0 +1,13 @@
apiVersion: fleet.cattle.io/v1alpha1
kind: GitRepo
metadata:
name: namespace-c
namespace: fleet-local
spec:
repo: "https://github.com/<tua-org>/gitops-repo.git"
branch: main
paths:
- gitops/clusters/idc/namespace-C/manifests
targets:
- clusterName: local
namespace: namespace-C
@@ -0,0 +1,32 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: esempio-app-c
namespace: namespace-C
spec:
replicas: 1
selector:
matchLabels:
app: esempio-app-c
template:
metadata:
labels:
app: esempio-app-c
spec:
containers:
- name: esempio-app-c
image: nginx:1.27
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: esempio-app-c
namespace: namespace-C
spec:
selector:
app: esempio-app-c
ports:
- port: 80
targetPort: 80
+256
View File
@@ -0,0 +1,256 @@
#!/usr/bin/env bash
# =====================================================================
# extract-manifests.sh
#
# Estrae tutte le risorse namespaced dai namespace indicati, le ripulisce
# con kubectl-neat (rimuove resourceVersion, uid, status, managedFields,
# creationTimestamp, ecc.) e le salva organizzate per namespace/kind,
# pronte per essere committate in un repo GitOps.
#
# Uso:
# ./extract-manifests.sh [opzioni] <namespace1> <namespace2> ...
# ./extract-manifests.sh [opzioni] --namespaces-file namespaces.txt
#
# Opzioni:
# -o, --output <dir> Directory di output (default: ./export)
# -s, --include-secrets Include anche i Secret (in CHIARO, base64 non
# cifrato — vedi warning qui sotto). Di default
# i Secret vengono SALTATI per sicurezza.
# -f, --namespaces-file File con un namespace per riga. Righe vuote e
# righe che iniziano con # vengono ignorate.
# -k, --kinds <lista> Lista custom di kind separati da virgola,
# al posto della discovery automatica
# (es. "deployment,service,httproute")
# -h, --help Mostra questo help
#
# Esempi:
# ./extract-manifests.sh monitoring minio idcidp-dev
# ./extract-manifests.sh -o ./gitops-repo/imported -s monitoring
# ./extract-manifests.sh -k "httproute,gateway" nginx-gateway
# ./extract-manifests.sh -f ./namespaces.txt -o ./gitops-repo/imported
#
# Prerequisiti:
# - kubectl configurato e puntato al cluster corretto
# - kubectl-neat installato (kubectl krew install neat)
# https://github.com/itaysk/kubectl-neat
# =====================================================================
set -euo pipefail
# ---------------------------------------------------------------------
# Default
# ---------------------------------------------------------------------
OUTPUT_DIR="./export"
INCLUDE_SECRETS="false"
CUSTOM_KINDS=""
NAMESPACES_FILE=""
# Kind che non ha senso portare in un repo GitOps: generati/gestiti
# automaticamente da controller, non sono mai "desired state" da
# dichiarare a mano.
EXCLUDED_KINDS="events pods replicasets endpoints endpointslices controllerrevisions"
usage() {
grep '^#' "$0" | sed -e 's/^#//' -e 's/^ //'
exit 0
}
load_namespaces_from_file() {
local file_path="$1"
if [[ ! -f "$file_path" ]]; then
echo "Errore: file namespace non trovato: $file_path" >&2
exit 1
fi
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%$'\r'}"
[[ -z "$line" ]] && continue
[[ "$line" =~ ^[[:space:]]*# ]] && continue
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
[[ -z "$line" ]] && continue
NAMESPACES+=("$line")
done < "$file_path"
}
# ---------------------------------------------------------------------
# Parsing argomenti
# ---------------------------------------------------------------------
NAMESPACES=()
while [[ $# -gt 0 ]]; do
case "$1" in
-o|--output)
OUTPUT_DIR="$2"
shift 2
;;
-s|--include-secrets)
INCLUDE_SECRETS="true"
shift
;;
-f|--namespaces-file)
NAMESPACES_FILE="$2"
shift 2
;;
-k|--kinds)
CUSTOM_KINDS="$2"
shift 2
;;
-h|--help)
usage
;;
-*)
echo "Opzione sconosciuta: $1" >&2
exit 1
;;
*)
NAMESPACES+=("$1")
shift
;;
esac
done
if [[ -n "$NAMESPACES_FILE" ]]; then
load_namespaces_from_file "$NAMESPACES_FILE"
fi
if [[ ${#NAMESPACES[@]} -eq 0 ]]; then
echo "Errore: specifica almeno un namespace o usa --namespaces-file." >&2
echo "Uso: $0 [opzioni] <namespace1> <namespace2> ..." >&2
echo " o: $0 [opzioni] --namespaces-file namespaces.txt" >&2
exit 1
fi
# ---------------------------------------------------------------------
# Prerequisiti
# ---------------------------------------------------------------------
if ! command -v kubectl >/dev/null 2>&1; then
echo "Errore: kubectl non trovato nel PATH." >&2
exit 1
fi
if ! kubectl neat --help >/dev/null 2>&1; then
echo "Errore: plugin kubectl-neat non trovato." >&2
echo "Installa con: kubectl krew install neat" >&2
echo "(krew: https://krew.sigs.k8s.io/docs/user-guide/setup/install/)" >&2
exit 1
fi
mkdir -p "$OUTPUT_DIR"
echo "==> Output directory: $OUTPUT_DIR"
echo "==> Namespace da processare: ${NAMESPACES[*]}"
echo "==> Include Secret: $INCLUDE_SECRETS"
echo ""
if [[ "$INCLUDE_SECRETS" == "true" ]]; then
echo "########################################################"
echo "# ATTENZIONE: i Secret verranno esportati in CHIARO #"
echo "# (base64, NON cifrato). Non committarli in Git così #"
echo "# come sono. Usa Sealed Secrets, SOPS o External Secrets #"
echo "# Operator prima di aggiungerli al repo. #"
echo "########################################################"
echo ""
fi
# ---------------------------------------------------------------------
# Determina i kind namespaced da processare
# ---------------------------------------------------------------------
get_kinds() {
if [[ -n "$CUSTOM_KINDS" ]]; then
echo "$CUSTOM_KINDS" | tr ',' '\n'
return
fi
# Discovery automatica: tutti i kind namespaced supportati dal cluster
# (copre anche le CRD installate, es. httproute, podmonitor, cluster
# CNPG, ecc.), escludendo quelli in EXCLUDED_KINDS.
kubectl api-resources --namespaced=true --verbs=list -o name 2>/dev/null \
| cut -d. -f1 \
| sort -u \
| while read -r kind; do
skip="false"
for excl in $EXCLUDED_KINDS; do
[[ "$kind" == "$excl" ]] && skip="true" && break
done
[[ "$skip" == "false" ]] && echo "$kind"
done
}
KINDS=$(get_kinds)
# ---------------------------------------------------------------------
# Estrazione
# ---------------------------------------------------------------------
TOTAL_EXPORTED=0
TOTAL_EMPTY=0
TOTAL_ERRORS=0
for ns in "${NAMESPACES[@]}"; do
echo "=== Namespace: $ns ==="
if ! kubectl get namespace "$ns" >/dev/null 2>&1; then
echo " ! Namespace '$ns' non trovato, salto." >&2
continue
fi
ns_dir="${OUTPUT_DIR}/${ns}"
mkdir -p "$ns_dir"
while IFS= read -r kind; do
[[ -z "$kind" ]] && continue
# Salta i secret a meno che non richiesti esplicitamente
if [[ "$kind" == "secrets" || "$kind" == "secret" ]] && [[ "$INCLUDE_SECRETS" != "true" ]]; then
continue
fi
# Conta quante risorse di questo kind esistono nel namespace, per
# evitare di scrivere file vuoti/inutili
count=$(kubectl get "$kind" -n "$ns" --no-headers 2>/dev/null | wc -l | tr -d ' ')
if [[ "$count" -eq 0 ]]; then
TOTAL_EMPTY=$((TOTAL_EMPTY + 1))
continue
fi
out_file="${ns_dir}/${kind}.yaml"
if kubectl get "$kind" -n "$ns" -o yaml 2>/dev/null | kubectl neat > "$out_file" 2>/dev/null; then
# kubectl neat su una List vuota/malformata può comunque produrre
# un file quasi-vuoto: verifichiamo che contenga davvero "kind:"
if grep -q "^kind:" "$out_file" 2>/dev/null || grep -q "^items:" "$out_file" 2>/dev/null; then
echo " + ${kind} (${count})"
TOTAL_EXPORTED=$((TOTAL_EXPORTED + 1))
else
rm -f "$out_file"
fi
else
echo " ! Errore esportando ${kind}" >&2
rm -f "$out_file"
TOTAL_ERRORS=$((TOTAL_ERRORS + 1))
fi
done <<< "$KINDS"
echo ""
done
echo "=== Riepilogo ==="
echo "Risorse esportate: $TOTAL_EXPORTED"
echo "Kind vuoti/saltati: $TOTAL_EMPTY"
echo "Errori: $TOTAL_ERRORS"
echo ""
echo "Output in: $OUTPUT_DIR"
echo ""
echo "Prossimi passi consigliati:"
echo " 1. Rivedi manualmente ogni file: alcuni campi (es. clusterIP,"
echo " nodePort, annotazioni iniettate da controller/webhook) vanno"
echo " rimossi a mano perché non fanno parte del 'desired state'."
echo " 2. Se hai esportato Secret, cifrali (Sealed Secrets / SOPS) prima"
echo " di committarli."
echo " 3. Riorganizza i file nella struttura del repo GitOps"
echo " (infrastructure/ vs apps/, vedi README del repo)."
+305
View File
@@ -0,0 +1,305 @@
#!/usr/bin/env bash
set -euo pipefail
OUTPUT_DIR="./generated-helmops"
API_VERSION="fleet.cattle.io/v1alpha1"
RESOURCE_KIND="HelmOp"
VALUES_ARGS=()
WORKSPACE_NAMESPACE="fleet-local"
usage() {
cat <<'EOF'
Usage:
./generate-fleet-helmops-from-helm.sh [options]
Description:
Legge le release presenti con `helm list -A`, recupera metadata e values
per ogni release e genera un file YAML per release in formato HelmOp-style.
Options:
-o, --output-dir <dir> Directory di output (default: ./generated-helmops)
-w, --workspace-namespace <ns>
Namespace dove creare la risorsa HelmOp
(default: fleet-local)
--api-version <value> apiVersion del manifest generato
(default: fleet.cattle.io/v1alpha1)
--kind <value> kind del manifest generato (default: HelmOp)
-a, --all-values Usa `helm get values -a -o yaml`
-h, --help Mostra questo help
Examples:
./generate-fleet-helmops-from-helm.sh
./generate-fleet-helmops-from-helm.sh -o ./clusters/idc/imported
./generate-fleet-helmops-from-helm.sh -w fleet-default
./generate-fleet-helmops-from-helm.sh --kind HelmChart --api-version helm.cattle.io/v1
EOF
}
require_cmd() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Errore: comando richiesto non trovato: $cmd" >&2
exit 1
fi
}
trim() {
local value="$1"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s' "$value"
}
safe_file_name() {
printf '%s' "$1" | tr '/\\:' '---'
}
extract_yaml_scalar() {
local key="$1"
local content="$2"
printf '%s\n' "$content" \
| sed -n "s/^[[:space:]]*${key}:[[:space:]]*//p" \
| head -n 1 \
| sed 's/^"//; s/"$//; s/^\x27//; s/\x27$//'
}
extract_first_source() {
local content="$1"
awk '
/^sources:/ { in_sources=1; next }
in_sources && /^[^[:space:]-]/ { exit }
in_sources && /^[[:space:]]*-[[:space:]]*/ {
sub(/^[[:space:]]*-[[:space:]]*/, "")
print
exit
}
' <<< "$content"
}
extract_chart_name_from_ref() {
local chart_ref="$1"
if [[ "$chart_ref" =~ ^(.+)-([0-9][A-Za-z0-9.+_-]*)$ ]]; then
printf '%s' "${BASH_REMATCH[1]}"
else
printf '%s' "$chart_ref"
fi
}
extract_chart_version_from_ref() {
local chart_ref="$1"
if [[ "$chart_ref" =~ ^(.+)-([0-9][A-Za-z0-9.+_-]*)$ ]]; then
printf '%s' "${BASH_REMATCH[2]}"
fi
}
find_repo_url_from_local_cache() {
local chart_name="$1"
local chart_version="$2"
local search_json=""
local repo_alias=""
[[ -z "$chart_name" ]] && return 0
search_json="$(helm search repo "$chart_name" --versions -o json 2>/dev/null || true)"
[[ -z "$search_json" || "$search_json" == "[]" ]] && return 0
if [[ -n "$chart_version" ]]; then
repo_alias="$(jq -r --arg chart "$chart_name" --arg version "$chart_version" '
map(select((.name | split("/") | last) == $chart and .version == $version))
| .[0].name // empty
' <<< "$search_json")"
fi
if [[ -z "$repo_alias" ]]; then
repo_alias="$(jq -r --arg chart "$chart_name" '
map(select((.name | split("/") | last) == $chart))
| .[0].name // empty
' <<< "$search_json")"
fi
[[ -z "$repo_alias" ]] && return 0
repo_alias="${repo_alias%%/*}"
helm repo list -o json 2>/dev/null \
| jq -r --arg alias "$repo_alias" 'map(select(.name == $alias)) | .[0].url // empty'
}
indent_file() {
local file_path="$1"
sed 's/^/ /' "$file_path"
}
while [[ $# -gt 0 ]]; do
case "$1" in
-o|--output-dir)
OUTPUT_DIR="$2"
shift 2
;;
-w|--workspace-namespace)
WORKSPACE_NAMESPACE="$2"
shift 2
;;
--api-version)
API_VERSION="$2"
shift 2
;;
--kind)
RESOURCE_KIND="$2"
shift 2
;;
-a|--all-values)
VALUES_ARGS=(-a)
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Opzione sconosciuta: $1" >&2
usage >&2
exit 1
;;
esac
done
require_cmd helm
require_cmd jq
mkdir -p "$OUTPUT_DIR"
releases_json="$(helm list -A -o json)"
release_count="$(jq 'length' <<< "$releases_json")"
if [[ "$release_count" -eq 0 ]]; then
echo "Nessuna release Helm trovata." >&2
exit 0
fi
echo "==> Release trovate: $release_count"
echo "==> Directory output: $OUTPUT_DIR"
generated_count=0
warning_count=0
while IFS= read -r release; do
name="$(jq -r '.name' <<< "$release")"
namespace="$(jq -r '.namespace' <<< "$release")"
chart_ref="$(jq -r '.chart // ""' <<< "$release")"
metadata_yaml="$(helm get metadata "$name" -n "$namespace" -o yaml 2>/dev/null || true)"
if [[ -z "$metadata_yaml" ]]; then
echo "! Impossibile leggere metadata per ${namespace}/${name}, salto." >&2
warning_count=$((warning_count + 1))
continue
fi
repo_url="$(trim "$(extract_yaml_scalar repo "$metadata_yaml")")"
if [[ -z "$repo_url" ]]; then
repo_url="$(trim "$(extract_yaml_scalar repository "$metadata_yaml")")"
fi
if [[ -z "$repo_url" ]]; then
repo_url="$(trim "$(extract_yaml_scalar repoURL "$metadata_yaml")")"
fi
if [[ -z "$repo_url" ]]; then
repo_url="$(trim "$(extract_first_source "$metadata_yaml")")"
fi
chart_name="$(trim "$(extract_yaml_scalar chart "$metadata_yaml")")"
chart_version="$(trim "$(extract_yaml_scalar version "$metadata_yaml")")"
if [[ -z "$chart_name" ]]; then
chart_name="$(extract_chart_name_from_ref "$chart_ref")"
fi
if [[ -z "$chart_version" ]]; then
chart_version="$(extract_chart_version_from_ref "$chart_ref")"
fi
if [[ -z "$repo_url" ]]; then
repo_url="$(trim "$(find_repo_url_from_local_cache "$chart_name" "$chart_version")")"
fi
if [[ -z "$repo_url" ]]; then
repo_url="REPO_URL_NOT_FOUND"
echo "! Repo URL non trovato in metadata o cache locale per ${namespace}/${name}" >&2
warning_count=$((warning_count + 1))
fi
if [[ -z "$chart_name" ]]; then
chart_name="CHART_NAME_NOT_FOUND"
echo "! Chart name non trovato per ${namespace}/${name}" >&2
warning_count=$((warning_count + 1))
fi
values_file="$(mktemp)"
if ! helm get values "$name" -n "$namespace" "${VALUES_ARGS[@]}" -o yaml > "$values_file" 2>/dev/null; then
printf '{}\n' > "$values_file"
echo "! Values non disponibili per ${namespace}/${name}, uso {}" >&2
warning_count=$((warning_count + 1))
fi
if [[ ! -s "$values_file" ]]; then
printf '{}\n' > "$values_file"
fi
values_compact="$(tr -d '[:space:]' < "$values_file")"
ns_dir="${OUTPUT_DIR}/${namespace}"
mkdir -p "$ns_dir"
safe_name="$(safe_file_name "$name")"
out_file="${ns_dir}/${safe_name}-helmop.yaml"
cat > "$out_file" <<EOF
apiVersion: ${API_VERSION}
kind: ${RESOURCE_KIND}
metadata:
name: ${safe_name}
namespace: ${WORKSPACE_NAMESPACE}
spec:
namespace: ${namespace}
helm:
releaseName: ${name}
repo: ${repo_url}
chart: ${chart_name}
EOF
if [[ -n "$chart_version" ]]; then
cat >> "$out_file" <<EOF
version: ${chart_version}
EOF
fi
if [[ "$values_compact" == "{}" ]]; then
cat >> "$out_file" <<EOF
values: {}
EOF
else
cat >> "$out_file" <<EOF
values:
$(indent_file "$values_file")
EOF
fi
rm -f "$values_file"
echo "+ Generato ${out_file} da ${namespace}/${name}"
generated_count=$((generated_count + 1))
done < <(jq -c '.[]' <<< "$releases_json")
echo ""
echo "=== Riepilogo ==="
echo "File generati: $generated_count"
echo "Warning: $warning_count"
echo "Output: $OUTPUT_DIR"
echo "Workspace namespace: $WORKSPACE_NAMESPACE"
echo ""
echo "Nota: il repo URL viene letto da 'helm get metadata' e, se assente,"
echo " viene cercato nella cache locale di 'helm repo list/search repo'."
echo "Se vedi REPO_URL_NOT_FOUND, completa il campo repo manualmente."
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
# Aggiunge un hostname a spec.template.spec.hostAliases del Deployment cert-manager.
#
# Uso:
# ./add-cert-manager-hostalias.sh <hostname>
#
# Esempio:
# ./add-cert-manager-hostalias.sh main-gateway-nginx.nginx-gateway.svc.cluster.local
# ./add-cert-manager-hostalias.sh api.internal
DEPLOYMENT_NAME="cert-manager"
DEPLOYMENT_NS="cert-manager"
TARGET_IP="10.43.37.118"
HOSTNAME_VAL="${1:-}"
if [[ -z "$HOSTNAME_VAL" ]]; then
echo "Uso: $0 <hostname>" >&2
exit 1
fi
if ! command -v kubectl >/dev/null 2>&1; then
echo "Errore: kubectl non trovato nel PATH" >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "Errore: jq non trovato nel PATH" >&2
echo "Installa jq e riprova." >&2
exit 1
fi
# Idempotenza: controlla solo il blocco hostAliases associato a TARGET_IP.
EXISTING_HOSTNAMES="$(kubectl get deployment "$DEPLOYMENT_NAME" -n "$DEPLOYMENT_NS" -o jsonpath="{.spec.template.spec.hostAliases[?(@.ip=='${TARGET_IP}')].hostnames[*]}" 2>/dev/null || true)"
if echo " $EXISTING_HOSTNAMES " | grep -Fq " $HOSTNAME_VAL "; then
echo "Hostname '$HOSTNAME_VAL' gia presente per IP ${TARGET_IP} in ${DEPLOYMENT_NS}/${DEPLOYMENT_NAME}. Nessuna modifica."
exit 0
fi
DEPLOY_JSON="$(kubectl get deployment "$DEPLOYMENT_NAME" -n "$DEPLOYMENT_NS" -o json)"
UPDATED_HOST_ALIASES="$({
echo "$DEPLOY_JSON" | jq -c --arg ip "$TARGET_IP" --arg hostname "$HOSTNAME_VAL" '
(.spec.template.spec.hostAliases //= [])
| if any(.spec.template.spec.hostAliases[]?; .ip == $ip) then
.spec.template.spec.hostAliases |= map(
if .ip == $ip then
if ((.hostnames // []) | index($hostname)) then
.
else
.hostnames = ((.hostnames // []) + [$hostname])
end
else
.
end
)
else
.spec.template.spec.hostAliases += [{"ip": $ip, "hostnames": [$hostname]}]
end
| .spec.template.spec.hostAliases
'
} )"
PATCH_PAYLOAD="$(jq -cn --argjson hostAliases "$UPDATED_HOST_ALIASES" '{spec:{template:{spec:{hostAliases:$hostAliases}}}}')"
kubectl patch deployment "$DEPLOYMENT_NAME" \
-n "$DEPLOYMENT_NS" \
--type=merge \
-p "$PATCH_PAYLOAD" >/dev/null
echo "Hostname aggiunto con successo."
echo " Deployment: ${DEPLOYMENT_NS}/${DEPLOYMENT_NAME}"
echo " IP : ${TARGET_IP}"
echo " Hostname : ${HOSTNAME_VAL}"
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
# Aggiunge un hostname a spec.template.spec.hostAliases del Deployment blackbox.
#
# Uso:
# ./add-hostalias.sh <hostname> <deployment_name> <deployment_namespace>
#
# Esempio:
# ./add-hostalias.sh idcidcp.pigreco66.it cert-manager cert-manager
#
DEPLOYMENT_NAME="${2:-}"
DEPLOYMENT_NS="${3:-}"
TARGET_IP="10.43.37.118"
HOSTNAME_VAL="${1:-}"
if [[ -z "$HOSTNAME_VAL" ]]; then
echo "Uso: $0 <hostname>" >&2
exit 1
fi
if ! command -v kubectl >/dev/null 2>&1; then
echo "Errore: kubectl non trovato nel PATH" >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "Errore: jq non trovato nel PATH" >&2
echo "Installa jq e riprova." >&2
exit 1
fi
# Idempotenza: controlla solo il blocco hostAliases associato a TARGET_IP.
EXISTING_HOSTNAMES="$(kubectl get deployment "$DEPLOYMENT_NAME" -n "$DEPLOYMENT_NS" -o jsonpath="{.spec.template.spec.hostAliases[?(@.ip=='${TARGET_IP}')].hostnames[*]}" 2>/dev/null || true)"
if echo " $EXISTING_HOSTNAMES " | grep -Fq " $HOSTNAME_VAL "; then
echo "Hostname '$HOSTNAME_VAL' gia presente per IP ${TARGET_IP} in ${DEPLOYMENT_NS}/${DEPLOYMENT_NAME}. Nessuna modifica."
exit 0
fi
DEPLOY_JSON="$(kubectl get deployment "$DEPLOYMENT_NAME" -n "$DEPLOYMENT_NS" -o json)"
UPDATED_HOST_ALIASES="$({
echo "$DEPLOY_JSON" | jq -c --arg ip "$TARGET_IP" --arg hostname "$HOSTNAME_VAL" '
(.spec.template.spec.hostAliases //= [])
| if any(.spec.template.spec.hostAliases[]?; .ip == $ip) then
.spec.template.spec.hostAliases |= map(
if .ip == $ip then
if ((.hostnames // []) | index($hostname)) then
.
else
.hostnames = ((.hostnames // []) + [$hostname])
end
else
.
end
)
else
.spec.template.spec.hostAliases += [{"ip": $ip, "hostnames": [$hostname]}]
end
| .spec.template.spec.hostAliases
'
} )"
PATCH_PAYLOAD="$(jq -cn --argjson hostAliases "$UPDATED_HOST_ALIASES" '{spec:{template:{spec:{hostAliases:$hostAliases}}}}')"
kubectl patch deployment "$DEPLOYMENT_NAME" \
-n "$DEPLOYMENT_NS" \
--type=merge \
-p "$PATCH_PAYLOAD" >/dev/null
echo "Hostname aggiunto con successo."
echo " Deployment: ${DEPLOYMENT_NS}/${DEPLOYMENT_NAME}"
echo " IP : ${TARGET_IP}"
echo " Hostname : ${HOSTNAME_VAL}"
+16
View File
@@ -35,6 +35,7 @@ fi
# Per calcolare il token usa host pulito (senza schema e path)
host_for_token="${endpoint#*://}"
host_for_token="${host_for_token%%/*}"
host_no_port="${host_for_token%%:*}"
token="${host_for_token%%.*}"
if [[ -z "$token" ]]; then
@@ -42,5 +43,20 @@ if [[ -z "$token" ]]; then
exit 1
fi
# Se l'hostname non e nel dominio *.italiadatacenter.com, aggiungilo anche a cert-manager hostAliases.
if [[ "$host_no_port" != *.italiadatacenter.com ]]; then
HOSTALIAS_SCRIPT="/root/work/pipeline/add-hostalias.sh"
if [[ -x "$HOSTALIAS_SCRIPT" ]]; then
"$HOSTALIAS_SCRIPT" "$host_no_port" "cert-manager" "cert-manager"
"$HOSTALIAS_SCRIPT" "$host_no_port" "blackbox-exporter" "monitoring"
elif [[ -f "$HOSTALIAS_SCRIPT" ]]; then
bash "$HOSTALIAS_SCRIPT" "$host_no_port" "cert-manager" "cert-manager"
bash "$HOSTALIAS_SCRIPT" "$host_no_port" "blackbox-exporter" "monitoring"
else
echo "Errore: script non trovato: $HOSTALIAS_SCRIPT" >&2
exit 1
fi
fi
# Output richiesto: <endpoint> https-<token> <token>-secret
/root/work/pipeline/add-listener.sh $endpoint https-$token $token-secret
+27 -1
View File
@@ -47,8 +47,10 @@ for dir in containers/*/; do
PLATFORM_CONF="$dir/platform.conf"
BUILD_PLATFORM=""
MULTISTAGE=""
if [ -f "$PLATFORM_CONF" ]; then
BUILD_PLATFORM=$(grep -E '^[[:space:]]*platform[[:space:]]*=' "$PLATFORM_CONF" | tail -n 1 | cut -d '=' -f 2- | tr -d '[:space:]')
MULTISTAGE=$(grep -E '^[[:space:]]*multistage[[:space:]]*=' "$PLATFORM_CONF" | tail -n 1 | cut -d '=' -f 2- | tr -d '[:space:]')
if [ -n "$BUILD_PLATFORM" ]; then
echo "platform.conf trovato in $dir: uso platform=$BUILD_PLATFORM"
else
@@ -57,15 +59,39 @@ for dir in containers/*/; do
fi
if [ -f "$DOCKERFILE" ]; then
if [ -n "$MULTISTAGE" ] && ! [[ "$MULTISTAGE" =~ ^[1-9][0-9]*$ ]]; then
echo "Valore multistage non valido in $PLATFORM_CONF: $MULTISTAGE"
exit 1
fi
BUILD_COUNT="${MULTISTAGE:-1}"
for ((target_index = 1; target_index <= BUILD_COUNT; target_index++)); do
BUILD_TARGET=""
if [ -n "$MULTISTAGE" ]; then
BUILD_TARGET=$(grep -E "^[[:space:]]*target${target_index}[[:space:]]*=" "$PLATFORM_CONF" | tail -n 1 | cut -d '=' -f 2- | tr -d '[:space:]')
if [ -z "$BUILD_TARGET" ]; then
echo "target${target_index} non valorizzato in $PLATFORM_CONF"
exit 1
fi
fi
echo "Build $target_index/$BUILD_COUNT${BUILD_TARGET:+ target=$BUILD_TARGET}"
BUILD_TARGET_ARGS=()
if [ -n "$BUILD_TARGET" ]; then
BUILD_TARGET_ARGS=(--target "$BUILD_TARGET")
fi
if [ -n "$BUILD_PLATFORM" ]; then
docker buildx build --platform "$BUILD_PLATFORM" -t "$IMAGE_TAG" -t "$IMAGE_TAG_LATEST" -f "$DOCKERFILE" "$dir" --push
docker buildx build --platform "$BUILD_PLATFORM" "${BUILD_TARGET_ARGS[@]}" -t "$IMAGE_TAG" -t "$IMAGE_TAG_LATEST" -f "$DOCKERFILE" "$dir" --push
else
docker buildx build \
--cache-from=type=registry,ref="${REGISTRY_URL}/${REPO_NAME}/${CONTAINER_NAME}:cache" \
--cache-to=type=registry,ref="${REGISTRY_URL}/${REPO_NAME}/${CONTAINER_NAME}:cache",mode=max \
"${BUILD_TARGET_ARGS[@]}" \
-t "$IMAGE_TAG" -t "$IMAGE_TAG_LATEST" \
-f "$DOCKERFILE" "$dir" --push
fi
done
echo "Build e push completate: $IMAGE_TAG"
# Cleanup immagini locali dopo il push (buildx --push potrebbe non averle in locale)
+29
View File
@@ -6,6 +6,19 @@ VALUES_DIR="env/$ENV"
PROPERTIES_FILE="properties.env"
YAML_DIR="kubernetes"
# Ricava il nome progetto dalle variabili esposte da Gitea/GitHub Actions.
# Fallback locale: nome della directory del repository.
PROJECT_NAME="${GITHUB_REPOSITORY_NAME:-${GITEA_REPOSITORY_NAME:-}}"
if [ -z "$PROJECT_NAME" ] && [ -n "${GITHUB_REPOSITORY:-}" ]; then
PROJECT_NAME="${GITHUB_REPOSITORY##*/}"
fi
if [ -z "$PROJECT_NAME" ] && [ -n "${GITEA_REPOSITORY:-}" ]; then
PROJECT_NAME="${GITEA_REPOSITORY##*/}"
fi
if [ -z "$PROJECT_NAME" ]; then
PROJECT_NAME=$(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
fi
# Estrai l'hash completo del commit e crea una variabile temporanea per la sostituzione
TAG=$(git rev-parse HEAD)
TMP_TAG_FILE=$(mktemp)
@@ -24,6 +37,9 @@ if [ ! -f "$PROPERTIES_FILE" ]; then
exit 1
fi
# Costruisce il nome del namespace come <project_name>-<env>
NAMESPACE="${PROJECT_NAME}-${ENV}"
# Crea una lista key=value temporanea partendo da tutti i file *.env in env/$ENV e aggiunge env dinamico
> "$TMP_VALUES_FILE"
for env_file in "$VALUES_DIR"/*.env; do
@@ -31,6 +47,19 @@ for env_file in "$VALUES_DIR"/*.env; do
printf '\n' >> "$TMP_VALUES_FILE"
done
printf '\nenv=%s\n' "$ENV" >> "$TMP_VALUES_FILE"
printf 'namespace=%s\n' "$NAMESPACE" >> "$TMP_VALUES_FILE"
printf 'project=%s\n' "$PROJECT_NAME" >> "$TMP_VALUES_FILE"
# Ricava endpoint se presente e genera le variabili derivate per le sostituzioni YAML.
ENDPOINT_VAL=$(grep -E '^[[:space:]]*endpoint[[:space:]]*=' "$TMP_VALUES_FILE" "$PROPERTIES_FILE" 2>/dev/null | tail -n 1 | sed -E 's/.*endpoint[[:space:]]*=[[:space:]]*//' | tr -d '\r\n[:space:]"' || true)
if [ -n "$ENDPOINT_VAL" ]; then
ENDPOINT_NODOT=$(echo "$ENDPOINT_VAL" | tr '.' '-')
ENDPOINT_NOSPACE=$(echo "$ENDPOINT_VAL" | tr -d '.')
printf 'endpoint-nodot=%s\n' "$ENDPOINT_NODOT" >> "$TMP_VALUES_FILE"
printf 'endpoint-nospace=%s\n' "$ENDPOINT_NOSPACE" >> "$TMP_VALUES_FILE"
fi
# Trova tutti i file .yaml nella directory kubernetes e sottodirectory
find "$YAML_DIR" -type f -name "*.yaml" | while read YAML_FILE; do
+11 -37
View File
@@ -7,53 +7,27 @@ ENVIRONMENT="${1:-dev}"
YAML_DIR="kubernetes"
# ---------------------------------------------------------------------------
# Cerca risorse postgresql.cnpg.io/v1 nei manifest e deploya una ConfigMap
# ---------------------------------------------------------------------------
CNPG_FILE=$(grep -rl "postgresql.cnpg.io/v1" "$YAML_DIR" 2>/dev/null | head -1 || true)
# Valore di alert_notification_email per l'ambiente (ultima occorrenza in env/<env>/*.env)
ALERT_EMAIL=$(cat "env/$ENVIRONMENT"/*.env 2>/dev/null | grep -E '^[[:space:]]*alert_notification_email[[:space:]]*=' | tail -n 1 | sed -E 's/^[^=]*=//' | tr -d '\r"'"'"'[:space:]' || true)
if [ -n "$CNPG_FILE" ]; then
echo "Trovata risorsa postgresql.cnpg.io/v1 in: $CNPG_FILE"
# Estrae metadata.name dal manifest CNPG preferendo yq, altrimenti awk
if command -v yq >/dev/null 2>&1; then
PG_NAME=$(yq eval 'select(.apiVersion == "postgresql.cnpg.io/v1") | .metadata.name' "$CNPG_FILE")
else
PG_NAME=$(awk '/postgresql\.cnpg\.io\/v1/{found=1} found && /^metadata:/{meta=1} meta && /^\s+name:/{print $2; exit}' "$CNPG_FILE")
fi
# Ricava il namespace dal Role namespace-deployer presente nel cluster
PG_NS=$(kubectl --kubeconfig=./kubeconfig get role namespace-deployer \
--no-headers \
-o custom-columns='NS:.metadata.namespace' 2>/dev/null | head -1 || true)
#PG_NS="${PG_NS:-default}"
if [ -z "$PG_NAME" ]; then
echo "⚠️ Impossibile estrarre metadata.name dal cluster CNPG, skip ConfigMap." >&2
else
echo " → cluster: $PG_NAME namespace: $PG_NS"
kubectl --kubeconfig=./kubeconfig apply -f - <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: service-config
namespace: ${PG_NS}
data:
tipodb: "postgres"
urldb: "${PG_NAME}.${PG_NS}.svc.cluster.local"
EOF
echo "ConfigMap db-config deployata in namespace ${PG_NS}."
fi
fi
# ---------------------------------------------------------------------------
# Deploy di tutti i manifest Kubernetes
# ---------------------------------------------------------------------------
find "$YAML_DIR" -type d | while read DIR; do
if ls "$DIR"/*.yaml 1> /dev/null 2>&1; then
if [ "$(basename "$DIR")" = "monitoring" ]; then
if [ -z "$ALERT_EMAIL" ]; then
echo "alert_notification_email non valorizzata per l'ambiente $ENVIRONMENT: deploy di $DIR saltato."
continue
fi
echo "Deploy delle risorse di monitoring nella directory $DIR con kubeconfig dedicato..."
kubectl --kubeconfig=/root/work/pipeline/kc apply -f "$DIR"
else
echo "Deploy delle risorse nella directory $DIR..."
kubectl --kubeconfig=./kubeconfig apply -f "$DIR"
fi
fi
done
echo "Deploy completato di tutte le directory YAML."
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
. ./.profile
cd /project/inbound_figc
rm -f *.csv
rm -f *.csv_int*
./getftp.sh
./loadpgare.sh csexc17f.csv ana
./loadpgare.sh cseri17f.csv imp
./loadpgare.sh cserc17f.csv cal
./loadpgare.sh csezc17f.csv cla
#./loadpgare.sh cseyc17f.csv
/usr/local/bin/kubectl cp cserc17f.csv_int_utf8_nobom.csv db/postgresql-1-postgresql-0:/etc/cserc17f_2load_utf8_nobom.csv
/usr/local/bin/kubectl cp cseri17f.csv_int_utf8_nobom.csv db/postgresql-1-postgresql-0:/etc/cseri17f_2load_utf8_nobom.csv
/usr/local/bin/kubectl cp csexc17f.csv_int_utf8_nobom.csv db/postgresql-1-postgresql-0:/etc/csexc17f_2load_utf8_nobom.csv
/usr/local/bin/kubectl cp csezc17f.csv_int_utf8_nobom.csv db/postgresql-1-postgresql-0:/etc/csezc17f_2load_utf8_nobom.csv
#k cp csezc17f.csv_int_utf8_nobom.csv db/postgresql-1-postgresql-0:/etc/csezc17f_2load_utf8_nobom.csv
name=$(date '+%Y-%m-%d')
tar -zcvf "$name.tar.gz" *.csv
mv *.gz ./arch
/usr/local/bin/kubectl cp pgareload.sql db/postgresql-1-postgresql-0:/etc/pgareload.sql
/usr/local/bin/kubectl exec -it postgresql-1-postgresql-0 -c postgresql-server -n db -- psql -U postgres -d pgare -f /etc/pgareload.sql
/usr/local/bin/kubectl cp loadclub.sql db/postgresql-1-postgresql-0:/etc/loadclub.sql
/usr/local/bin/kubectl exec -it postgresql-1-postgresql-0 -c postgresql-server -n db -- psql -U postgres -d segdigi -f /etc/loadclub.sql
sleep 120
/usr/local/bin/kubectl cp partite_ftp.sql db/postgresql-1-postgresql-0:/etc/partite_ftp.sql
/usr/local/bin/kubectl exec -it postgresql-1-postgresql-0 -c postgresql-server -n db -- psql -U postgres -d gare -f /etc/partite_ftp.sql
/usr/local/bin/kubectl cp campionati_ftp.sql db/postgresql-1-postgresql-0:/etc/campionati_ftp.sql
kubectl exec -it postgresql-1-postgresql-0 -c postgresql-server -n db -- psql -U postgres -d gare -f /etc/campionati_ftp.sql
/usr/local/bin/kubectl cp companies_ftp.sql db/postgresql-1-postgresql-0:/etc/companies_ftp.sql
/usr/local/bin/kubectl exec -it postgresql-1-postgresql-0 -c postgresql-server -n db -- psql -U postgres -d companies -f /etc/companies_ftp.sql