This commit is contained in:
alessandro barucci
2026-10-11 23:49:35 +02:00
parent 23f276417f
commit 34213918fe
32 changed files with 1411 additions and 9 deletions
@@ -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.
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+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
+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

+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)
+6
View File
@@ -7,6 +7,8 @@ ENVIRONMENT="${1:-dev}"
YAML_DIR="kubernetes"
# 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)
# ---------------------------------------------------------------------------
@@ -15,6 +17,10 @@ YAML_DIR="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