varie
This commit is contained in:
@@ -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"
|
||||||
@@ -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 |
@@ -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,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.
|
||||||
@@ -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 }}
|
||||||
+5
@@ -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
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -47,8 +47,10 @@ for dir in containers/*/; do
|
|||||||
|
|
||||||
PLATFORM_CONF="$dir/platform.conf"
|
PLATFORM_CONF="$dir/platform.conf"
|
||||||
BUILD_PLATFORM=""
|
BUILD_PLATFORM=""
|
||||||
|
MULTISTAGE=""
|
||||||
if [ -f "$PLATFORM_CONF" ]; then
|
if [ -f "$PLATFORM_CONF" ]; then
|
||||||
BUILD_PLATFORM=$(grep -E '^[[:space:]]*platform[[:space:]]*=' "$PLATFORM_CONF" | tail -n 1 | cut -d '=' -f 2- | tr -d '[:space:]')
|
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
|
if [ -n "$BUILD_PLATFORM" ]; then
|
||||||
echo "platform.conf trovato in $dir: uso platform=$BUILD_PLATFORM"
|
echo "platform.conf trovato in $dir: uso platform=$BUILD_PLATFORM"
|
||||||
else
|
else
|
||||||
@@ -57,15 +59,39 @@ for dir in containers/*/; do
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "$DOCKERFILE" ]; then
|
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
|
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
|
else
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--cache-from=type=registry,ref="${REGISTRY_URL}/${REPO_NAME}/${CONTAINER_NAME}:cache" \
|
--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 \
|
--cache-to=type=registry,ref="${REGISTRY_URL}/${REPO_NAME}/${CONTAINER_NAME}:cache",mode=max \
|
||||||
|
"${BUILD_TARGET_ARGS[@]}" \
|
||||||
-t "$IMAGE_TAG" -t "$IMAGE_TAG_LATEST" \
|
-t "$IMAGE_TAG" -t "$IMAGE_TAG_LATEST" \
|
||||||
-f "$DOCKERFILE" "$dir" --push
|
-f "$DOCKERFILE" "$dir" --push
|
||||||
fi
|
fi
|
||||||
|
done
|
||||||
echo "Build e push completate: $IMAGE_TAG"
|
echo "Build e push completate: $IMAGE_TAG"
|
||||||
|
|
||||||
# Cleanup immagini locali dopo il push (buildx --push potrebbe non averle in locale)
|
# Cleanup immagini locali dopo il push (buildx --push potrebbe non averle in locale)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ ENVIRONMENT="${1:-dev}"
|
|||||||
|
|
||||||
YAML_DIR="kubernetes"
|
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
|
find "$YAML_DIR" -type d | while read DIR; do
|
||||||
if ls "$DIR"/*.yaml 1> /dev/null 2>&1; then
|
if ls "$DIR"/*.yaml 1> /dev/null 2>&1; then
|
||||||
if [ "$(basename "$DIR")" = "monitoring" ]; 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..."
|
echo "Deploy delle risorse di monitoring nella directory $DIR con kubeconfig dedicato..."
|
||||||
kubectl --kubeconfig=/root/work/pipeline/kc apply -f "$DIR"
|
kubectl --kubeconfig=/root/work/pipeline/kc apply -f "$DIR"
|
||||||
else
|
else
|
||||||
|
|||||||
Reference in New Issue
Block a user