Compare commits
7
Commits
d41d7f40cc
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bc033c329 | ||
|
|
1df884da84 | ||
|
|
34213918fe | ||
|
|
23f276417f | ||
|
|
ff808293aa | ||
|
|
9c0189e08e | ||
|
|
d190719c5e |
@@ -0,0 +1,154 @@
|
||||
| | **Langflow** | **Kagent** |
|
||||
| --------------- | -------------------------------------------- | --------------------------------------- |
|
||||
| **Fase** | **Build time** | **Run time** |
|
||||
| **Ruolo** | AI Agent Builder | AI Agent Runtime / Deployment |
|
||||
| **Funzione** | Creare, configurare e testare agenti AI | Distribuire ed eseguire agenti AI |
|
||||
| **Interfaccia** | Visuale / low-code | Kubernetes-native / YAML / CRD |
|
||||
| **Deployment** | Può essere installato come servizio dell'IDP | È parte del runtime Kubernetes dell'IDP |
|
||||
| **Output** | Flow/agent e relativo artefatto | Agent in esecuzione su Kubernetes |
|
||||
|
||||
Quindi:
|
||||
|
||||
### Langflow — Build Time
|
||||
|
||||
> **Software per progettare, costruire e testare AI Agent.**
|
||||
|
||||
Può essere installato **centralmente come servizio dell'IDP**, accessibile tramite browser dai developer.
|
||||
|
||||
Il developer può costruire, ad esempio:
|
||||
|
||||
```text
|
||||
LLM
|
||||
│
|
||||
├── Knowledge Base
|
||||
├── MCP Tool
|
||||
├── API
|
||||
└── Business Logic
|
||||
│
|
||||
▼
|
||||
Agent
|
||||
```
|
||||
|
||||
Il risultato può poi diventare un **artefatto software versionabile e deployabile**.
|
||||
|
||||
---
|
||||
|
||||
### Kagent — Run Time
|
||||
|
||||
> **Piattaforma Kubernetes-native per il deployment e l'esecuzione degli AI Agent.**
|
||||
|
||||
E qui la tua osservazione è importante: ci sono **due modalità**.
|
||||
|
||||
#### 1. Agent semplice definito direttamente
|
||||
|
||||
Per agenti relativamente semplici, la definizione può essere direttamente nella configurazione Kubernetes/Kagent:
|
||||
|
||||
```yaml
|
||||
kind: Agent
|
||||
metadata:
|
||||
name: my-agent
|
||||
spec:
|
||||
model: ...
|
||||
tools:
|
||||
- ...
|
||||
```
|
||||
|
||||
Quindi:
|
||||
|
||||
```text
|
||||
YAML
|
||||
↓
|
||||
Kagent
|
||||
↓
|
||||
Agent Runtime
|
||||
↓
|
||||
Kubernetes
|
||||
```
|
||||
|
||||
#### 2. Agent sviluppato con Langflow
|
||||
|
||||
Per agenti più complessi:
|
||||
|
||||
```text
|
||||
Langflow
|
||||
│
|
||||
│ Build
|
||||
▼
|
||||
Agent Artifact
|
||||
│
|
||||
│ CI/CD
|
||||
▼
|
||||
Registry / Git
|
||||
│
|
||||
│ Deploy
|
||||
▼
|
||||
Kagent
|
||||
│
|
||||
▼
|
||||
Kubernetes
|
||||
│
|
||||
▼
|
||||
Running Agent
|
||||
```
|
||||
|
||||
### La precisazione più importante
|
||||
|
||||
Direi però **"artefatti costruiti con Langflow"** piuttosto che:
|
||||
|
||||
> "Kagent esegue Langflow"
|
||||
|
||||
perché sono due cose diverse.
|
||||
|
||||
Kagent **non è il runtime di Langflow**. Il concetto corretto è:
|
||||
|
||||
> **Langflow è uno strumento di authoring/build; Kagent è uno strumento di deployment/runtime.**
|
||||
|
||||
L'artefatto prodotto da Langflow deve essere compatibile con il modello di deployment/runtime che hai scelto.
|
||||
|
||||
---
|
||||
|
||||
### Quindi il modello del tuo IDP diventa molto chiaro
|
||||
|
||||
```text
|
||||
IDP
|
||||
│
|
||||
┌──────────┴──────────┐
|
||||
│ │
|
||||
BUILD TIME RUN TIME
|
||||
│ │
|
||||
┌─────────┐ ┌─────────┐
|
||||
│Langflow │ │ Kagent │
|
||||
│ │ │ │
|
||||
│ Build │──────────►│ Deploy │
|
||||
│ Test │ Artifact │ Execute │
|
||||
└─────────┘ └────┬────┘
|
||||
│
|
||||
▼
|
||||
Kubernetes
|
||||
```
|
||||
|
||||
E aggiungerei un **terzo livello trasversale**:
|
||||
|
||||
```text
|
||||
IDP GOVERNANCE
|
||||
┌────────────────────────────┐
|
||||
│ Security │
|
||||
│ RBAC │
|
||||
│ Secrets │
|
||||
│ Network Policy │
|
||||
│ Resource Quotas │
|
||||
│ Approved Models │
|
||||
│ Approved MCP │
|
||||
│ Versioning │
|
||||
│ CI/CD │
|
||||
│ Observability │
|
||||
│ Compliance │
|
||||
└────────────────────────────┘
|
||||
│
|
||||
┌─────────┴─────────┐
|
||||
▼ ▼
|
||||
Langflow Kagent
|
||||
Build Time Run Time
|
||||
```
|
||||
|
||||
**Questa secondo me è la rappresentazione più corretta per il tuo progetto IDP:** Langflow e Kagent non sono concorrenti, ma **componenti di due fasi diverse del lifecycle dell'Agent**.
|
||||
Binary file not shown.
Binary file not shown.
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,6 @@
|
||||
STS Lab Srl — Spec-Driven Reader (skill per Claude Code)
|
||||
Copyright (c) 2026 STS Lab Srl. Tutti i diritti riservati.
|
||||
|
||||
Questa skill (SKILL.md, references/, scripts/) è di proprietà di STS Lab Srl.
|
||||
L'uso è riservato a STS Lab Srl, ai suoi collaboratori e ai clienti autorizzati.
|
||||
Non è consentita la ridistribuzione o la modifica senza autorizzazione scritta di STS Lab Srl.
|
||||
@@ -0,0 +1,248 @@
|
||||
---
|
||||
name: sts-spec-driven
|
||||
description: "STS Lab Srl — Spec-Driven Reader. Legge e SCRIVE la documentazione spec-driven di un progetto (cartella docs/ con PROJECT_MEMORY, REQUIREMENTS REQ-*, specifiche SPEC-*, task T-*, decisioni D-*, difetti DIF-*, TRACEABILITY, TODO, DECISIONS, CHANGELOG, DEVELOPMENT_LOG). Lettura: trasforma la documentazione in un brief di implementazione prima del codice e chiude il ciclo dopo. Scrittura: se il progetto non ha documentazione la crea da zero facendo scegliere fra layout strutturato (00-INDEX + 01-product…08-project) e piatto, con modelli, intervista e controllo di coerenza; aggiunge nuove spec, requisiti e decisioni a una documentazione esistente. Usala all'inizio di ogni sessione di sviluppo, quando l'utente cita un ID (REQ-, SPEC-, T-, D-, DIF-), chiede di implementare una spec o un task, chiede «leggi la documentazione», «da dove riprendo», «che stato ha il progetto», «crea la documentazione», «imposta lo spec driven», «scrivi la spec di …», «nuovo progetto», o quando si inizia a sviluppare in un progetto senza docs/."
|
||||
---
|
||||
|
||||
# Spec-Driven Reader — STS Lab Srl
|
||||
|
||||
> **STS Lab Srl** · Skill proprietaria per lo sviluppo software guidato da specifiche.
|
||||
> © 2026 STS Lab Srl — tutti i diritti riservati.
|
||||
|
||||
Nei progetti STS Lab la documentazione **precede** il codice e ne è il contratto:
|
||||
|
||||
```
|
||||
REQ-xxx cosa deve fare il prodotto (REQUIREMENTS)
|
||||
▼
|
||||
SPEC-xxx come lo fa: API, dati, regole, (03-specs/ o documenti di modulo)
|
||||
│ errori, criteri di accettazione
|
||||
▼
|
||||
T-xxx unità di lavoro implementabile (TODO)
|
||||
▼
|
||||
codice + test
|
||||
▼
|
||||
TRACEABILITY chiude il cerchio: ogni REQ ha spec, task e test che passano
|
||||
```
|
||||
|
||||
Ogni scelta strutturale è una decisione numerata `D-xxx`; ogni difetto noto è un `DIF-xx`.
|
||||
Questa skill serve a **scrivere** quella catena quando manca, a **leggerla bene** prima di
|
||||
toccare il codice, e a **richiuderla** dopo.
|
||||
|
||||
### Quale modalità
|
||||
|
||||
| Situazione | Modalità | Dove |
|
||||
|---|---|---|
|
||||
| Il progetto **non ha** documentazione spec-driven (o l'utente chiede di crearla) | **Scrittura** | §A qui sotto + [references/scrittura.md](references/scrittura.md) |
|
||||
| C'è solo lo scheletro, con molti `DA COMPILARE` | **Scrittura** (completamento) | idem, dallo step in corso |
|
||||
| Serve una spec, un requisito o una decisione **nuovi** | **Scrittura** (aggiunta) | [references/scrittura.md](references/scrittura.md) §6 |
|
||||
| La documentazione c'è e si deve sviluppare | **Lettura** | §1–§8 |
|
||||
|
||||
---
|
||||
|
||||
## 0. Strumento: `scripts/spec_index.py`
|
||||
|
||||
Libreria standard Python 3.8+. Usalo **prima** di aprire i file grandi: DECISIONS e
|
||||
DEVELOPMENT_LOG possono superare le 3–6 000 righe e non vanno mai letti interi.
|
||||
|
||||
```bash
|
||||
S=~/.claude/skills/sts-spec-driven/scripts/spec_index.py
|
||||
# lettura (non scrive nulla)
|
||||
python $S overview <docs> # layout, documenti chiave, stato delle spec, segnaposto
|
||||
python $S context <docs> SPEC-004 # definizione + ID collegati + TODO + TRACEABILITY
|
||||
python $S find <docs> D-014 # dove è definito e chi lo cita
|
||||
python $S check <docs> # coerenza: orfani, requisiti scoperti, derive di stato
|
||||
python $S next-id <docs> SPEC|D|DIF|T|REQ [--area AUT] [--spec SPEC-004] [--prefisso W]
|
||||
# scrittura (non sovrascrive mai un file esistente)
|
||||
python $S scaffold <docs> --layout strutturato|piatto --progetto "Nome"
|
||||
python $S new-spec <docs> --titolo "Titolo" [--prefisso W (solo layout piatto)]
|
||||
```
|
||||
|
||||
Su Windows da PowerShell il `~` non viene espanso: usa `$HOME\.claude\skills\sts-spec-driven\scripts\spec_index.py`
|
||||
(oppure lancialo dal tool Bash, dove `~` funziona).
|
||||
|
||||
- Per una SPEC, `context` stampa l'indice delle sezioni con i numeri di riga: leggi poi solo
|
||||
le sezioni che servono con `Read offset/limit`.
|
||||
- `check` esce con codice 1 se trova problemi; riporta all'utente cosa ha trovato.
|
||||
|
||||
Se Python non c'è, ripiega su `Grep` con il pattern
|
||||
`REQ-[A-Z]+-\d+|SPEC-\d{3}|T-[A-Z]{0,2}\d+|D-\d{3}|DIF-\d+` e copia a mano i modelli da
|
||||
`templates/`.
|
||||
|
||||
---
|
||||
|
||||
## A. Scrittura — creare la documentazione
|
||||
|
||||
Procedura completa in [references/scrittura.md](references/scrittura.md). In sintesi:
|
||||
|
||||
1. **Verifica** che non esista già (§1). Se esiste solo in parte, non ricrearla: completala.
|
||||
2. **Fai scegliere il layout all'utente** con `AskUserQuestion`, mettendo per primo con
|
||||
«(Consigliato)» quello adatto — *Strutturato* per progetti con più moduli, integrazioni,
|
||||
riscritture, committente esterno; *Piatto* per progetti piccoli, prototipi, un solo modulo.
|
||||
Chiedi anche dove crearla (default `docs/` alla radice del repository).
|
||||
3. **Crea lo scheletro** con `scaffold`. I modelli stanno in `templates/strutturato/`,
|
||||
`templates/piatto/`, `templates/spec.md`, `templates/modulo.md`.
|
||||
4. **Raccogli le informazioni**: prima leggi README, manifest, codice; poi intervista l'utente
|
||||
(al massimo 4 domande per giro, solo ciò che manca). Se c'è codice senza documenti,
|
||||
ricostruisci i requisiti dal codice come tipo **P** con *Origine* `file:funzione`.
|
||||
5. **Scrivi nell'ordine degli step** (visione → requisiti → architettura → spec → contratto e
|
||||
dati → devops → qualità e backlog), con un riepilogo e la conferma dell'utente a fine step.
|
||||
6. **Nuove spec** con `new-spec`: numerazione, blocco di task e lettera dei criteri sono
|
||||
automatici. Nascono sempre «da approvare».
|
||||
7. **Chiudi** con `check` e riporta l'esito.
|
||||
|
||||
Regole che non si derogano:
|
||||
|
||||
- **Non inventare**: ciò che non è noto resta `DA COMPILARE` o diventa una domanda.
|
||||
- **Mai approvare da solo**: una spec diventa *Approvata* solo dopo un sì esplicito
|
||||
dell'utente (procedura in scrittura.md §5).
|
||||
- **Mai sovrascrivere** documenti esistenti; i contenuti sparsi si spostano nei documenti
|
||||
giusti, con l'accordo dell'utente.
|
||||
|
||||
---
|
||||
|
||||
## 1. Trovare la documentazione
|
||||
|
||||
Cerca nell'ordine: `docs/`, `src/docs/`, `*/src/docs/`, `documentation/`. Una cartella è
|
||||
spec-driven se contiene almeno `PROJECT_MEMORY.md` o `00-INDEX.md` più uno fra `DECISIONS.md`,
|
||||
`TODO.md`, `REQUIREMENTS.md`. Leggi anche `CLAUDE.md`/`README.md` alla radice del repository:
|
||||
possono indicare la cartella o regole locali.
|
||||
|
||||
**Se non la trovi**, non scrivere codice: di' all'utente che il progetto non ha documentazione
|
||||
spec-driven e proponi di crearla (§A). Se l'utente preferisce procedere senza, rispetta la
|
||||
scelta e annotala nella risposta.
|
||||
|
||||
Due layout ricorrenti (dettagli in [references/layout.md](references/layout.md)):
|
||||
|
||||
| Layout | Riconoscimento | Esempio |
|
||||
|---|---|---|
|
||||
| **Strutturato** | cartelle `01-product/ … 08-project/`, `00-INDEX.md`, `03-specs/SPEC-xxx-*.md` | `KadeonCad_v2/docs` |
|
||||
| **Piatto** | tutti i `.md` in una cartella, specifiche come documenti di modulo (`CAD_MODULE.md`, `whatsapp/DESIGN.md`) | `Kadeon/kadeon/src/docs` |
|
||||
|
||||
---
|
||||
|
||||
## 2. Lettura di inizio sessione (sempre, prima del codice)
|
||||
|
||||
1. **`overview`** con lo script.
|
||||
2. **`PROJECT_MEMORY.md`** — per intero. Contiene ciò che non è deducibile dal codice e che, se
|
||||
dimenticato, fa ripetere errori: vincoli non negoziabili, azioni pendenti di sicurezza, «le
|
||||
cose da non dimenticare mai». Tienile presenti per tutta la sessione.
|
||||
3. **`00-INDEX.md`** (layout strutturato) — tabella degli step, stato dell'implementazione e i
|
||||
riquadri `> **Titolo (data):**` in testa: sono le novità più recenti, in ordine inverso.
|
||||
4. **Indice di `DECISIONS.md`** — solo la tabella iniziale; le singole decisioni si leggono
|
||||
quando servono.
|
||||
5. **`TODO.md`** — sezione «fuori sequenza / prima di tutto» e la fase corrente.
|
||||
|
||||
Non leggere CHANGELOG, DEVELOPMENT_LOG, openapi.yaml per intero: si interrogano per ID o data.
|
||||
|
||||
---
|
||||
|
||||
## 3. Prima di implementare: il cancello
|
||||
|
||||
Rispondi a queste domande **prima** di scrivere una riga. Se una risposta è «no», fermati e
|
||||
dillo all'utente.
|
||||
|
||||
| # | Domanda | Dove si verifica | Se «no» |
|
||||
|:-:|---|---|---|
|
||||
| 1 | Il lavoro richiesto corrisponde a un `T-xxx`? | TODO, sezione *Task* della spec | Proponi il task e la spec di appartenenza |
|
||||
| 2 | Il task appartiene a una `SPEC-xxx` in stato **Approvata**? | riga `Stato:` della spec, 00-INDEX | Non implementare: segnala lo stato (*da approvare*, 🟡) e chiedi |
|
||||
| 3 | Le dipendenze del task (colonna *Dipende da*) sono ✅? | TODO, tabella task della spec | Segnala l'ordine corretto |
|
||||
| 4 | Nessuna decisione `D-xxx` **accettata** lo vieta o lo vincola diversamente? | DECISIONS (per ID), PROJECT_MEMORY | Cita la decisione; una deviazione richiede una nuova `D-xxx` |
|
||||
| 5 | Se tocca un requisito di **parità** (tipo `P`) cambia il comportamento? | REQUIREMENTS, colonna *Tipo* | È una decisione di prodotto: serve una `D-xxx` |
|
||||
| 6 | Contratto API e schema dati sono già scritti? | `openapi.yaml` / API.md, DATABASE.md | Si scrive prima il contratto (contract-first) |
|
||||
|
||||
Eccezione legittima: richiesta esplicita dell'utente di procedere comunque. In quel caso
|
||||
procedi, ma registra la deviazione (vedi §6) — è ciò che i progetti chiamano «inversione»
|
||||
e che va lasciata scritta.
|
||||
|
||||
---
|
||||
|
||||
## 4. Leggere una spec
|
||||
|
||||
Struttura tipica di una `SPEC-xxx` (le sezioni possono variare):
|
||||
|
||||
1. Intestazione: `Stato`, versione, data, **requisiti coperti**, decisione di riferimento.
|
||||
2. Contesto e obiettivo — il *perché*; spesso contiene un dato misurato che giustifica la scelta.
|
||||
3. «Cosa cambia rispetto a ciò che è già scritto» — elenco dei punti fermi che la spec modifica.
|
||||
4. Regole / algoritmo in **forma normativa**: le parole in grassetto e i «**non**» sono vincoli.
|
||||
5. Contratto API: rotta, ingresso, uscita, **tabella degli esiti con codici HTTP**.
|
||||
6. Errori.
|
||||
7. **Criteri di accettazione** (`D1…D12`, `P1…P24`, …): ognuno diventa almeno un test.
|
||||
8. **Task** con dipendenze.
|
||||
|
||||
Regole di lettura:
|
||||
|
||||
- Leggi la spec **intera** quando la implementi; usa l'indice di `context` per le altre.
|
||||
- I paragrafi «è una decisione, non una dimenticanza» descrivono comportamenti voluti che
|
||||
sembrano bug: non «correggerli».
|
||||
- Sezioni con suffisso `bis`/`ter` (es. §5.3bis) sono aggiunte successive: leggile, spesso
|
||||
modificano la regola base.
|
||||
- Per ogni `REQ-` citato leggi la riga in REQUIREMENTS: colonna **Tipo** (P parità / C
|
||||
correzione / N nuovo) e **Origine** (per i `P` la fonte di verità è il codice della versione
|
||||
precedente indicato lì).
|
||||
- Per ogni `D-` citata leggi **solo** la sezione `## D-xxx` (Contesto → Decisione →
|
||||
Alternative → Conseguenze). Una decisione *Superata da D-yyy* non vale più: segui D-yyy.
|
||||
- Il glossario (`GLOSSARY.md`) fissa i termini: usa gli stessi nomi nel codice e nei messaggi.
|
||||
|
||||
---
|
||||
|
||||
## 5. Il brief di implementazione
|
||||
|
||||
Prima di scrivere codice, presenta all'utente un brief compatto con il modello in
|
||||
[references/brief-template.md](references/brief-template.md): obiettivo, stato della spec,
|
||||
requisiti, regole normative, contratto, criteri di accettazione → test, task e dipendenze,
|
||||
decisioni vincolanti, file di codice probabilmente toccati, **incoerenze trovate**.
|
||||
|
||||
Per un task piccolo il brief può stare in poche righe; non saltare mai le sezioni *criteri* e
|
||||
*incoerenze*.
|
||||
|
||||
---
|
||||
|
||||
## 6. Dopo l'implementazione: chiudere il ciclo
|
||||
|
||||
Aggiorna la documentazione **nella stessa modifica del codice**, seguendo
|
||||
[references/chiusura-ciclo.md](references/chiusura-ciclo.md). In sintesi:
|
||||
|
||||
| Documento | Cosa scrivere | Quando |
|
||||
|---|---|---|
|
||||
| TODO | stato del task (⬜ → 🟡 → ✅) | ✅ solo con test passanti |
|
||||
| TRACEABILITY | stato della riga REQ, criteri coperti | **quando il test passa, non quando il codice è scritto** |
|
||||
| spec | nuove sezioni `bis`, criteri e task aggiunti | se la regola cambia o si estende |
|
||||
| DECISIONS | nuova `D-xxx` (mai modificarne una vecchia: la si *supera*) | ogni scelta strutturale o deviazione |
|
||||
| CHANGELOG | cosa cambia **per chi usa il prodotto** | ogni modifica visibile |
|
||||
| DEVELOPMENT_LOG | cosa si è scoperto e **perché** si è deciso così | scoperte, tentativi respinti, misure |
|
||||
| PROJECT_MEMORY | solo ciò che, dimenticato, fa ripetere un errore | raramente |
|
||||
| 00-INDEX | riquadro `> **Titolo (data):**` in testa + date «Ultimo aggiornamento» | ogni consegna rilevante |
|
||||
|
||||
Riporta sempre il numero di test eseguiti e il loro esito reale (es. «backend 614 (+3)»).
|
||||
Non dichiarare ✅ ciò che non è stato verificato: scrivi 🟡 e cosa manca.
|
||||
|
||||
---
|
||||
|
||||
## 7. Incoerenze e fonti di verità
|
||||
|
||||
La documentazione viva va in deriva: indici non aggiornati, totali vecchi, spec marcata «da
|
||||
approvare» ma task già ✅, ID citati e mai definiti (l'`overview` li elenca). Quando le trovi:
|
||||
|
||||
1. **Non correggere in silenzio e non sceglierne una a caso.** Elencale nel brief.
|
||||
2. Applica la gerarchia delle fonti di verità dichiarata dal progetto. Default:
|
||||
- contratto API → `openapi.yaml` (non API.md, non il codice);
|
||||
- requisiti di parità → codice della versione precedente citato in *Origine*;
|
||||
- stato «verificato» → test **eseguiti** (non la TRACEABILITY);
|
||||
- decisioni → l'ultima `D-xxx` accettata che non è superata;
|
||||
- stato di una spec → la sua riga `Stato:`; in assenza, l'indice.
|
||||
3. Se l'incoerenza blocca il lavoro, chiedi all'utente; altrimenti prosegui e proponi la
|
||||
correzione documentale insieme alla consegna.
|
||||
|
||||
---
|
||||
|
||||
## 8. Cose da non fare
|
||||
|
||||
- Implementare da una spec *da approvare* o da un'idea non scritta, senza dirlo.
|
||||
- «Modernizzare» ciò che un requisito di parità o una decisione congela.
|
||||
- Rinumerare o riusare un ID: gli ID sono **immutabili**; quelli eliminati si barrano (`~~T-005~~`).
|
||||
- Cancellare una decisione o una voce di diario: si superano o si annotano.
|
||||
- Leggere per intero file da migliaia di righe quando basta una sezione.
|
||||
- Copiare segreti trovati nella documentazione in risposte, log o commit.
|
||||
|
||||
---
|
||||
|
||||
<sub>**STS Lab Srl** · Spec-Driven Reader v1.1 (lettura + scrittura) · 2026-10-16 · uso interno e per i clienti STS Lab Srl</sub>
|
||||
@@ -0,0 +1,42 @@
|
||||
# Modello — Brief di implementazione (STS Lab Srl)
|
||||
|
||||
Da presentare all'utente **prima** di scrivere codice. Ogni affermazione porta il riferimento
|
||||
`file:riga` o l'ID da cui viene. Togli le sezioni vuote, mai *Criteri* e *Incoerenze*.
|
||||
|
||||
```markdown
|
||||
## Brief · <T-xxx / SPEC-xxx> — <titolo breve>
|
||||
*STS Lab Srl · Spec-Driven Reader*
|
||||
|
||||
**Obiettivo** — una frase: cosa cambia per chi usa il prodotto.
|
||||
|
||||
**Cancello**
|
||||
| Verifica | Esito |
|
||||
|---|---|
|
||||
| Spec | SPEC-xxx · Stato: Approvata v1.0 (03-specs/SPEC-xxx.md:3) |
|
||||
| Task | T-xxx · ⬜ · dipende da T-yyy ✅, T-zzz 🟡 |
|
||||
| Decisioni vincolanti | D-0nn (sintesi in una riga) |
|
||||
| Parità | REQ-ABC-01 è **P**: comportamento da riprodurre identico |
|
||||
|
||||
**Requisiti coperti** — REQ-… con il testo essenziale e il tipo (P/C/N).
|
||||
|
||||
**Regole normative** — punti elenco presi dalla spec, con i «non» in evidenza.
|
||||
|
||||
**Contratto**
|
||||
- API: `METODO /percorso` → ingresso, uscita, esiti/codici (openapi.yaml:riga)
|
||||
- Dati: tabelle/colonne/vincoli toccati (DATABASE.md §n)
|
||||
- Stati: transizioni ammesse (DOMAIN_WORKFLOW.md §n)
|
||||
|
||||
**Criteri di accettazione → test**
|
||||
| Criterio | Test previsto | Stato attuale (TRACEABILITY) |
|
||||
|---|---|---|
|
||||
| D1 | … | ✅ / 🟡 / ⬜ |
|
||||
|
||||
**Piano** — passi in ordine, file di codice probabilmente toccati.
|
||||
|
||||
**Incoerenze trovate** — documento A dice X, documento B dice Y; fonte di verità scelta e perché.
|
||||
Se nessuna: «nessuna rilevata».
|
||||
|
||||
**Domande per l'utente** — solo quelle che bloccano.
|
||||
```
|
||||
|
||||
<sub>STS Lab Srl · Spec-Driven Reader</sub>
|
||||
@@ -0,0 +1,48 @@
|
||||
# Chiusura del ciclo — STS Lab Srl
|
||||
|
||||
Checklist da seguire a lavoro finito, **nella stessa modifica del codice**. Ogni voce è
|
||||
condizionale: aggiorna solo ciò che il lavoro ha davvero toccato.
|
||||
|
||||
## 1. Verifica prima di scrivere «fatto»
|
||||
|
||||
- [ ] I test dei criteri di accettazione del task esistono e sono stati **eseguiti**.
|
||||
- [ ] Annotato il conteggio reale della suite (es. «backend 614 (+3), tutti passanti»).
|
||||
- [ ] Se una verifica è stata fatta solo con test e non contro il sistema reale, scriverlo.
|
||||
- [ ] Se qualcosa non è verificabile ora (cluster, casella reale, fornitore esterno), aprire
|
||||
un task `T-xxx` per la verifica mancante invece di dichiarare ✅.
|
||||
|
||||
## 2. Documenti di tracciamento
|
||||
|
||||
- [ ] **TODO** — stato del task; nuovi task con l'ID successivo libero, la spec e lo stato.
|
||||
Data «Ultimo aggiornamento» (e «precedente», se il file la usa).
|
||||
- [ ] **TRACEABILITY** — righe REQ interessate: criteri coperti e stato. ✅ solo con test passanti.
|
||||
Aggiornare anche la riga di copertura dell'area.
|
||||
- [ ] **Spec** — se la regola è cambiata o estesa: nuova sezione `§n.bis`, nuovi criteri
|
||||
(lettera della spec + numero successivo), nuovi task nella tabella *Task*.
|
||||
- [ ] **REQUIREMENTS** — nuovo `REQ-<AREA>-<nn>` se nasce un comportamento nuovo; tipo N
|
||||
con la `D-xxx` d'origine; nuovo `DIF-nn` se è emerso un difetto.
|
||||
|
||||
## 3. Decisioni e memoria
|
||||
|
||||
- [ ] **DECISIONS** — una `D-xxx` nuova per ogni scelta strutturale, deviazione da una spec o
|
||||
cambio di un requisito di parità. Formato: Contesto → Decisione → Alternative valutate →
|
||||
Conseguenze. Aggiungerla anche alla tabella indice. Se sostituisce una decisione,
|
||||
marcare la vecchia «Superata da D-xxx» senza cancellarne il testo.
|
||||
- [ ] **DEVELOPMENT_LOG** — voce in testa (ordine cronologico inverso): `## AAAA-MM-GG (n) · titolo`.
|
||||
Cosa si è scoperto, cosa è stato tentato e respinto e **perché**, misure reali.
|
||||
- [ ] **PROJECT_MEMORY** — solo se è emerso un vincolo che, dimenticato, farebbe ripetere un errore.
|
||||
|
||||
## 4. Comunicazione
|
||||
|
||||
- [ ] **CHANGELOG** — sezione *[Non rilasciato]*; linguaggio per l'utente del prodotto
|
||||
(Aggiunto / Modificato / Corretto), non dettagli interni.
|
||||
- [ ] **00-INDEX** — riquadro in testa `> **Titolo (AAAA-MM-GG):** …` con gli ID coinvolti
|
||||
(`REQ-…`, `D-…`, `T-…`) e il conteggio dei test; aggiornare la data dell'indice e la
|
||||
riga della spec nella mappa se ne cambia lo stato o il numero di criteri.
|
||||
|
||||
## 5. Coerenza finale
|
||||
|
||||
- [ ] Rieseguire `spec_index.py overview <docs>`: nessun nuovo ID «citato ma mai definito».
|
||||
- [ ] Nessun segreto, password o chiave nei documenti aggiornati.
|
||||
|
||||
<sub>STS Lab Srl · Spec-Driven Reader</sub>
|
||||
@@ -0,0 +1,91 @@
|
||||
# Layout della documentazione — STS Lab Srl
|
||||
|
||||
Due forme ricorrenti nei progetti STS Lab Srl. La skill deve funzionare con entrambe e con
|
||||
varianti intermedie. Per un progetto nuovo l'utente sceglie quale creare: i modelli sono in
|
||||
`templates/strutturato/` e `templates/piatto/` (vedi [scrittura.md](scrittura.md)).
|
||||
|
||||
---
|
||||
|
||||
## A. Layout strutturato (es. `KadeonCad_v2/docs`)
|
||||
|
||||
```
|
||||
docs/
|
||||
├── 00-INDEX.md mappa, stato degli step, novità recenti in riquadri datati
|
||||
├── 01-product/ COSA e PER CHI
|
||||
│ ├── VISION.md perché esiste, perimetro, non-obiettivi
|
||||
│ ├── GLOSSARY.md lessico condiviso (usare gli stessi termini nel codice)
|
||||
│ ├── PERSONAS_CASI_USO.md attori e casi d'uso end-to-end
|
||||
│ └── REQUIREMENTS.md REQ-<AREA>-<nn> + DIF-<nn> (difetti noti)
|
||||
├── 02-architecture/ COME È FATTO
|
||||
│ ├── ARCHITECTURE.md · C4_MODEL.md · DOMAIN_WORKFLOW.md (macchina a stati, invarianti)
|
||||
│ ├── DATA_MODEL.md (entità, relazioni, invarianti) · SECURITY.md · STORAGE.md
|
||||
│ └── <componente>.md (AI_GATEWAY, MESH_ENGINE, …)
|
||||
├── 03-specs/ CONTRATTO PER L'IMPLEMENTAZIONE
|
||||
│ └── SPEC-<nnn>-<slug>.md
|
||||
├── 04-api/ CONTRATTO ESTERNO
|
||||
│ ├── openapi.yaml FONTE DI VERITÀ dell'API
|
||||
│ └── API.md · WEBHOOKS.md · INTEGRATION_*.md
|
||||
├── 05-data/ DATABASE.md (schema fisico) · MIGRATION_*.md · SEED_BOOTSTRAP.md
|
||||
├── 06-devops/ INFRASTRUCTURE · KUBERNETES · CI_CD · ENVIRONMENTS · OBSERVABILITY · RUNBOOK · BACKUP_DR
|
||||
├── 07-quality/ TEST_STRATEGY · ACCEPTANCE_CRITERIA · TRACEABILITY · liste di parità
|
||||
└── 08-project/ DECISIONS · ROADMAP · TODO · CHANGELOG · DEVELOPMENT_LOG · PROJECT_MEMORY
|
||||
```
|
||||
|
||||
**Dove cercare cosa**
|
||||
|
||||
| Domanda | Documento |
|
||||
|---|---|
|
||||
| Cosa deve fare il prodotto? | `01-product/REQUIREMENTS.md` |
|
||||
| Come deve farlo questa funzione? | `03-specs/SPEC-xxx` |
|
||||
| Che forma ha la rotta? | `04-api/openapi.yaml` (poi API.md per le convenzioni) |
|
||||
| Che colonne/vincoli ha la tabella? | `05-data/DATABASE.md`; entità logiche in `02-architecture/DATA_MODEL.md` |
|
||||
| Transizioni di stato ammesse? | `02-architecture/DOMAIN_WORKFLOW.md` |
|
||||
| Perché è così? | `08-project/DECISIONS.md` (`## D-xxx`), poi `DEVELOPMENT_LOG.md` |
|
||||
| È fatto? È verificato? | `08-project/TODO.md`, `07-quality/TRACEABILITY.md` |
|
||||
| Come si rilascia / variabili d'ambiente? | `06-devops/ENVIRONMENTS.md`, `KUBERNETES.md` |
|
||||
|
||||
**Legenda stati usata:** ⬜ da scrivere/fare · 🟡 in corso/parziale · ✅ completo/verificato ·
|
||||
🔒 congelato (modifica solo con nuova D-xxx) · ⏸ sospeso.
|
||||
|
||||
---
|
||||
|
||||
## B. Layout piatto (es. `Kadeon/kadeon/src/docs`)
|
||||
|
||||
```
|
||||
docs/
|
||||
├── PROJECT_MEMORY.md «leggere sempre prima di scrivere codice»; sintesi architettura,
|
||||
│ decisioni, componenti già creati, sessioni
|
||||
├── ARCHITECTURE.md · API.md · DATABASE.md · AI_GATEWAY.md · OUTPUT_SCHEMA.md
|
||||
├── <MODULO>.md proposta/specifica di un modulo (es. CAD_MODULE.md)
|
||||
├── <area>/DESIGN.md proposte con opzioni e «domande ancora aperte» (es. whatsapp/)
|
||||
├── <area>/openapi.yaml contratto del modulo
|
||||
├── DECISIONS.md D-xxx con Stato · Contesto · Opzioni · Scelta · Motivazione
|
||||
├── TODO.md T-<lettera><nn> raggruppati per tema, con Prio (P0–P3) e Stato
|
||||
├── CHANGELOG.md · DEVELOPMENT_LOG.md
|
||||
```
|
||||
|
||||
Differenze da tenere presenti:
|
||||
|
||||
- Non ci sono `REQ-`/`SPEC-`: il ruolo della spec lo svolge il **documento di modulo** o la
|
||||
sezione di PROJECT_MEMORY/ARCHITECTURE; il cancello §3 si applica a quello.
|
||||
- Gli ID dei task hanno un prefisso tematico (`T-S01` sicurezza, `T-Q5`, `T-H01`…).
|
||||
- Stati in inglese: `pending` · `in_progress` · `done` · `blocked`; decisioni `accepted` ·
|
||||
`proposed` · `superseded`.
|
||||
- Un `DESIGN.md` con «Domande ancora aperte» **non è approvato**: chiarire con l'utente.
|
||||
- PROJECT_MEMORY può essere lungo e datato: confronta la sua data con quelle di TODO e
|
||||
DECISIONS.
|
||||
|
||||
---
|
||||
|
||||
## C. Formati degli ID
|
||||
|
||||
| ID | Forma | Dove nasce | Note |
|
||||
|---|---|---|---|
|
||||
| Requisito | `REQ-<AREA>-<nn>` (AREA 2–5 lettere) | REQUIREMENTS | Tipo **P/C/N**; mai rinumerato |
|
||||
| Difetto | `DIF-<nn>` | REQUIREMENTS (sezione difetti) | ogni DIF ha un REQ di tipo C e un test |
|
||||
| Specifica | `SPEC-<nnn>` | file `03-specs/SPEC-nnn-slug.md` | riga `Stato:` in testa |
|
||||
| Task | `T-<nnn>` o `T-<lettera><nn>`, suffissi `b`, `c` | sezione *Task* della spec e TODO | barrato se chiuso senza lavoro |
|
||||
| Decisione | `D-<nnn>` | DECISIONS | *superata*, mai cancellata |
|
||||
| Criterio | lettera + numero (`D1`, `P12`, `M18`) | sezione *Criteri* della spec | ⚠ `D1` criterio ≠ `D-001` decisione |
|
||||
|
||||
<sub>STS Lab Srl · Spec-Driven Reader</sub>
|
||||
@@ -0,0 +1,181 @@
|
||||
# Scrivere la documentazione spec-driven — STS Lab Srl
|
||||
|
||||
Guida per creare la documentazione quando il progetto non ce l'ha, completarla quando è solo
|
||||
uno scheletro, o aggiungere una spec a un progetto che già la usa.
|
||||
|
||||
`S` = `python ~/.claude/skills/sts-spec-driven/scripts/spec_index.py`
|
||||
(da PowerShell: `python $HOME\.claude\skills\sts-spec-driven\scripts\spec_index.py`)
|
||||
|
||||
---
|
||||
|
||||
## 0. Scegliere il layout — lo decide l'utente
|
||||
|
||||
Chiedi sempre con `AskUserQuestion`, prima di creare file. Proponi come primo, con
|
||||
«(Consigliato)», il layout che si adatta al progetto:
|
||||
|
||||
| Layout | Quando consigliarlo | Cosa crea |
|
||||
|---|---|---|
|
||||
| **Strutturato** | più moduli o servizi, integrazioni con altri sistemi, riscrittura di un sistema esistente, più persone al lavoro, committente esterno, requisiti contrattuali | `00-INDEX` + `01-product … 08-project`, 29 documenti, spec in `03-specs/SPEC-nnn-*.md` |
|
||||
| **Piatto** | progetto piccolo o prototipo, un solo modulo, un solo sviluppatore, tempi brevi | 8 documenti nella stessa cartella, una spec per modulo `<NOME>_MODULE.md` |
|
||||
|
||||
Chiedi nella stessa domanda (o subito dopo) **dove** creare la cartella: default `docs/` nella
|
||||
radice del repository (decisione D-003 dei modelli). Se esiste già una cartella con documenti
|
||||
sparsi, non sovrascriverla: proponi di crearci dentro lo scheletro e di spostare i contenuti
|
||||
esistenti nei documenti giusti.
|
||||
|
||||
---
|
||||
|
||||
## 1. Creare lo scheletro
|
||||
|
||||
```bash
|
||||
S scaffold <repo>/docs --layout strutturato --progetto "Nome Progetto"
|
||||
S scaffold <repo>/docs --layout piatto --progetto "Nome Progetto"
|
||||
```
|
||||
|
||||
Lo script **non sovrascrive mai** un file esistente (li elenca come «già esistenti»). I
|
||||
documenti nascono con segnaposto `<!-- DA COMPILARE: … -->` che spiegano cosa scrivere, e con
|
||||
le decisioni iniziali già pronte: `D-001` stack (Proposta), `D-002` metodo spec-driven,
|
||||
`D-003` documentazione nel repository.
|
||||
|
||||
---
|
||||
|
||||
## 2. Raccogliere le informazioni
|
||||
|
||||
### 2.1 Prima guarda, poi chiedi
|
||||
|
||||
Prima di fare domande leggi ciò che esiste: `README`, `CLAUDE.md`, manifest delle dipendenze
|
||||
(`package.json`, `pyproject.toml`, `composer.json`, …), `docker-compose`, cartelle di codice,
|
||||
documenti del committente forniti dall'utente. Non chiedere ciò che puoi ricavare.
|
||||
|
||||
### 2.2 Intervista — al massimo 4 domande per giro, solo ciò che manca
|
||||
|
||||
| Tema | Domande |
|
||||
|---|---|
|
||||
| Problema | Chi usa il prodotto? Cosa fa oggi senza? Quanto costa il problema (tempo, errori)? |
|
||||
| Perimetro | Cosa deve esserci nel primo rilascio? Cosa è esplicitamente fuori? |
|
||||
| Vincoli | Stack imposto? Sistemi esistenti da integrare o da sostituire? Scadenze? |
|
||||
| Utenti | Quali ruoli? Chi può fare cosa? Multi-azienda (tenant)? |
|
||||
| Dati | Quali dati personali o sensibili? Per quanto si conservano? |
|
||||
| Esercizio | Dove gira (cloud, on-premise, Kubernetes)? Quali ambienti? |
|
||||
| Successo | Come si misura che il progetto ha funzionato? |
|
||||
|
||||
Le risposte vanno nei documenti, non solo nella conversazione.
|
||||
|
||||
### 2.3 Progetto con codice ma senza documentazione
|
||||
|
||||
Ricostruisci dal codice, come è stato fatto in KadeonCad_v2:
|
||||
|
||||
- ogni comportamento osservato diventa un `REQ` di tipo **P** (parità) con *Origine*
|
||||
`file:funzione`; il codice è la fonte di verità, non i commenti;
|
||||
- ogni difetto visto leggendo diventa un `DIF-nn` + un `REQ` di tipo **C**;
|
||||
- ogni scelta tecnica già fatta diventa una `D-xxx` *Accettata* con la data di oggi e
|
||||
«ricostruita dal codice» nel contesto;
|
||||
- le spec delle parti già scritte nascono comunque *da approvare*: l'utente conferma che il
|
||||
comportamento attuale è quello voluto.
|
||||
|
||||
---
|
||||
|
||||
## 3. Ordine di scrittura
|
||||
|
||||
Ogni step termina con un **riepilogo all'utente** e la sua conferma prima del successivo.
|
||||
A fine step aggiorna lo stato nella tabella degli step di `00-INDEX` (⬜ → 🟡 → ✅).
|
||||
|
||||
### Layout strutturato
|
||||
|
||||
| Step | Documenti | Esce quando |
|
||||
|:-:|---|---|
|
||||
| 0 | `00-INDEX` (cos'è, obiettivi, struttura del repo), `D-001` stack, `PROJECT_MEMORY` §1 | l'utente conferma obiettivi e stack |
|
||||
| 1 | `VISION` → `GLOSSARY` → `PERSONAS_CASI_USO` → `REQUIREMENTS` → `ARCHITECTURE`, `C4_MODEL`, `DOMAIN_WORKFLOW`, `DATA_MODEL`, `SECURITY` | requisiti rivisti dall'utente |
|
||||
| 2 | una `SPEC` per capability (`S new-spec`) | ogni REQ è coperto da almeno una spec |
|
||||
| 3 | `openapi.yaml` + `API.md`, `DATABASE.md`, `SEED_BOOTSTRAP.md` | ogni rotta delle spec è nel contratto |
|
||||
| 4 | `06-devops/*` | ambienti e pipeline descritti |
|
||||
| 5 | `TEST_STRATEGY`, `ACCEPTANCE_CRITERIA`, `TRACEABILITY`, `ROADMAP`, `TODO` | `S check` senza ID orfani né requisiti scoperti |
|
||||
|
||||
Per un progetto piccolo in layout strutturato gli step 4 e parte del 5 possono restare
|
||||
⬜ con una riga «rimandato a …» nell'indice: meglio dichiarato vuoto che riempito di generico.
|
||||
|
||||
### Layout piatto
|
||||
|
||||
1. `PROJECT_MEMORY` §1–2 e `ARCHITECTURE`.
|
||||
2. `DECISIONS` (`D-001` stack).
|
||||
3. Primo documento di modulo: `S new-spec <docs> --titolo "…" --prefisso X`.
|
||||
4. `API.md`, `DATABASE.md` per ciò che il modulo tocca.
|
||||
5. `TODO` con la lettera del modulo e i suoi task.
|
||||
|
||||
---
|
||||
|
||||
## 4. Regole di qualità
|
||||
|
||||
### Requisiti
|
||||
- Un comportamento per riga, osservabile e verificabile; «il sistema …».
|
||||
- Numeri espliciti al posto di aggettivi («entro 5 s», «≥ 200 pezzi», non «veloce», «grande»).
|
||||
- Niente soluzioni tecniche nel requisito (quelle vanno nella spec o in una `D-xxx`).
|
||||
- Ogni requisito ha Tipo (P/C/N) e Origine. Aree di 2–5 lettere maiuscole (`AUT`, `ING`, `NFR`).
|
||||
|
||||
### Specifiche
|
||||
- Forma **normativa**: «il sistema rifiuta…», «non ricalcola…». I «non» in grassetto.
|
||||
- Algoritmi in pseudocodice; combinazioni di casi in tabella.
|
||||
- Contratto API con la **tabella degli esiti** e i codici HTTP; poi subito in `openapi.yaml`.
|
||||
- Criteri di accettazione binari, nella forma «Dato …, quando …, allora …»; ogni requisito
|
||||
coperto ha almeno un criterio; i casi limite (valore 199/200, assente, altro tenant) sono
|
||||
criteri a sé.
|
||||
- Task piccoli (indicativamente ≤ 1 giorno), con dipendenze; l'ultimo è sempre «Test di
|
||||
accettazione <lettera>1–<lettera>n».
|
||||
- Numerazione: `SPEC-nnn` → task `T-nnn0…T-nnn9`, criteri con la lettera della spec
|
||||
(SPEC-001 → A, SPEC-002 → B, …). Usa `S next-id` per non sbagliare.
|
||||
- Documento di modulo (layout piatto): la sezione «Domande ancora aperte» deve essere vuota o
|
||||
«nessuna» prima dell'approvazione.
|
||||
|
||||
### Decisioni
|
||||
- Alternative **reali** valutate, con il motivo dello scarto.
|
||||
- Conseguenze comprese quelle negative (costi, limiti accettati).
|
||||
|
||||
### Sempre
|
||||
- **Non inventare.** Ciò che non sai resta `DA COMPILARE` o diventa una domanda all'utente;
|
||||
mai numeri, nomi di sistemi o vincoli plausibili ma non confermati.
|
||||
- **Mai approvare da solo.** Le spec nascono «da approvare»; diventano «Approvata» solo dopo
|
||||
un sì esplicito dell'utente (§5).
|
||||
- Italiano, frasi brevi, niente linguaggio promozionale. Gli stessi termini del `GLOSSARY`.
|
||||
- Nessun segreto nei documenti: solo il nome della variabile che lo contiene.
|
||||
|
||||
---
|
||||
|
||||
## 5. Approvare una spec (solo su richiesta dell'utente)
|
||||
|
||||
1. Riga di stato: `Stato: **Approvata** · Versione 1.0 · AAAA-MM-GG`.
|
||||
2. `00-INDEX` §03: riga della spec con numero di criteri, blocco di task, stato ✅.
|
||||
3. `TODO`: i task della spec nella fase giusta, stato ⬜.
|
||||
4. `TRACEABILITY`: una riga per requisito coperto (REQ → SPEC §n → T → criteri → ⬜).
|
||||
5. `DEVELOPMENT_LOG`: voce breve con cosa è stato deciso approvando.
|
||||
6. `S check <docs>`.
|
||||
|
||||
Una spec approvata si modifica solo con una nuova `D-xxx` o con una revisione tracciata
|
||||
(versione 1.1, sezione `§n.bis`).
|
||||
|
||||
---
|
||||
|
||||
## 6. Aggiungere una spec a un progetto che già esiste
|
||||
|
||||
```bash
|
||||
S next-id <docs> SPEC # numero libero
|
||||
S new-spec <docs> --titolo "Portale cliente" # crea SPEC-nnn-portale-cliente.md, da approvare
|
||||
S next-id <docs> REQ --area POR # se servono requisiti nuovi
|
||||
S next-id <docs> D # se la spec nasce da una decisione
|
||||
```
|
||||
|
||||
Poi: requisiti nuovi in `REQUIREMENTS` (tipo N, origine `D-xxx`), riga nella mappa di
|
||||
`00-INDEX`, riquadro «novità» in testa all'indice, e la procedura del §5 quando l'utente approva.
|
||||
|
||||
---
|
||||
|
||||
## 7. Verifica finale
|
||||
|
||||
```bash
|
||||
S check <docs>
|
||||
```
|
||||
|
||||
Prima di consegnare: nessun ID orfano, nessun requisito scoperto, nessun task delle spec
|
||||
assente dal TODO. I segnaposto «DA COMPILARE» rimasti devono corrispondere solo a parti
|
||||
dichiarate rimandate. Riporta all'utente l'esito del controllo, non solo «fatto».
|
||||
|
||||
<sub>STS Lab Srl · Spec-Driven Reader</sub>
|
||||
@@ -0,0 +1,669 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
STS Lab Srl — Spec-Driven Reader · spec_index.py
|
||||
Copyright (c) 2026 STS Lab Srl. Tutti i diritti riservati.
|
||||
|
||||
Strumento della skill «sts-spec-driven» per leggere e scrivere documentazione spec-driven.
|
||||
|
||||
Lettura (non scrive nulla):
|
||||
overview <docs> Che forma ha la documentazione? Quali spec, in che stato?
|
||||
find <docs> <ID> Dove è definito un ID e chi lo cita? (REQ-, SPEC-, T-, D-, DIF-)
|
||||
context <docs> <ID> Definizione, ID collegati a un passo, righe di TODO e TRACEABILITY.
|
||||
check <docs> Coerenza: ID orfani, spec senza criteri/task, task fuori dal TODO,
|
||||
spec non approvate con lavoro già fatto, segnaposto da compilare.
|
||||
next-id <docs> <TIPO> Prossimo ID libero: REQ (con --area), SPEC, D, DIF, T (con --spec
|
||||
o --prefisso).
|
||||
|
||||
Scrittura (non sovrascrive mai un file esistente):
|
||||
scaffold <docs> --layout strutturato|piatto --progetto NOME
|
||||
Crea lo scheletro della documentazione dai modelli.
|
||||
new-spec <docs> --titolo "…" Crea una nuova spec (strutturato: 03-specs/SPEC-nnn-slug.md;
|
||||
piatto: <NOME>_MODULE.md, serve --prefisso).
|
||||
|
||||
Solo libreria standard Python 3.8+.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import unicodedata
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
BRAND = "STS Lab Srl · Spec-Driven Reader"
|
||||
SKILL_DIR = Path(__file__).resolve().parent.parent
|
||||
TEMPLATES = SKILL_DIR / "templates"
|
||||
LAYOUTS = ("strutturato", "piatto")
|
||||
|
||||
ID_RE = re.compile(
|
||||
r"(?<![A-Za-z0-9_-])("
|
||||
r"REQ-[A-Z]{2,5}-\d{1,3}[a-z]?"
|
||||
r"|SPEC-\d{3}"
|
||||
r"|DIF-\d{1,3}"
|
||||
r"|D-\d{3}"
|
||||
r"|T-[A-Z]{0,2}\d{1,4}[a-z]?"
|
||||
r")(?![A-Za-z0-9_])"
|
||||
)
|
||||
|
||||
KEY_DOCS = [
|
||||
"00-INDEX.md", "INDEX.md", "README.md",
|
||||
"PROJECT_MEMORY.md", "REQUIREMENTS.md", "DECISIONS.md", "TODO.md",
|
||||
"TRACEABILITY.md", "ARCHITECTURE.md", "API.md", "openapi.yaml",
|
||||
"DATABASE.md", "DATA_MODEL.md", "CHANGELOG.md", "DEVELOPMENT_LOG.md", "ROADMAP.md",
|
||||
"GLOSSARY.md", "ACCEPTANCE_CRITERIA.md", "TEST_STRATEGY.md",
|
||||
]
|
||||
PLACEHOLDER = "DA COMPILARE"
|
||||
|
||||
|
||||
def out(text: str = "") -> None:
|
||||
print(text)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- caricamento
|
||||
|
||||
def load(docs: Path) -> dict[Path, list[str]]:
|
||||
files: dict[Path, list[str]] = {}
|
||||
for p in sorted(docs.rglob("*")):
|
||||
if p.is_file() and p.suffix.lower() in {".md", ".yaml", ".yml"}:
|
||||
try:
|
||||
files[p] = p.read_text(encoding="utf-8-sig", errors="replace").splitlines()
|
||||
except OSError:
|
||||
continue
|
||||
return files
|
||||
|
||||
|
||||
def strip_comments(lines: list[str]) -> list[str]:
|
||||
"""Svuota i commenti HTML (<!-- … -->, anche su più righe) mantenendo i numeri di riga:
|
||||
i segnaposto dei modelli non devono contare come ID."""
|
||||
res, inside = [], False
|
||||
for line in lines:
|
||||
buf, i = "", 0
|
||||
while i < len(line):
|
||||
if inside:
|
||||
j = line.find("-->", i)
|
||||
if j < 0:
|
||||
i = len(line)
|
||||
else:
|
||||
inside, i = False, j + 3
|
||||
else:
|
||||
j = line.find("<!--", i)
|
||||
if j < 0:
|
||||
buf += line[i:]
|
||||
i = len(line)
|
||||
else:
|
||||
buf += line[i:j]
|
||||
inside, i = True, j + 4
|
||||
res.append(buf)
|
||||
return res
|
||||
|
||||
|
||||
def rel(p: Path, docs: Path) -> str:
|
||||
return p.relative_to(docs).as_posix()
|
||||
|
||||
|
||||
def kind(id_: str) -> str:
|
||||
for prefix in ("REQ", "SPEC", "DIF"):
|
||||
if id_.startswith(prefix + "-"):
|
||||
return prefix
|
||||
return id_.split("-")[0]
|
||||
|
||||
|
||||
def is_definition(line: str, id_: str, path: Path) -> bool:
|
||||
"""Una riga *definisce* l'ID se è un titolo che inizia con l'ID o una riga di
|
||||
tabella il cui primo campo è l'ID (anche in **grassetto** o ~~barrato~~)."""
|
||||
s = line.strip()
|
||||
if s.startswith("#"):
|
||||
# solo titoli Markdown che *iniziano* con l'ID ("## D-014 — …", "# SPEC-004 — …")
|
||||
return path.suffix.lower() == ".md" and re.match(r"#+\s+\**" + re.escape(id_) + r"(?![0-9A-Za-z])", s) is not None
|
||||
if s.startswith("|"):
|
||||
first = s.strip("|").split("|")[0]
|
||||
first = re.sub(r"[*~`\[\]]", "", first).strip()
|
||||
# [D-001](#ancora) -> "D-001(#ancora)" : prendiamo il primo token
|
||||
first = re.split(r"[\s(]", first)[0] if first else first
|
||||
return first == id_
|
||||
return False
|
||||
|
||||
|
||||
def scan(files: dict[Path, list[str]], docs: Path):
|
||||
defs: dict[str, list[tuple[str, int, str]]] = defaultdict(list)
|
||||
refs: dict[str, list[tuple[str, int, str]]] = defaultdict(list)
|
||||
for p, raw in files.items():
|
||||
r = rel(p, docs)
|
||||
lines = strip_comments(raw) if p.suffix.lower() == ".md" else raw
|
||||
m = re.match(r"(SPEC-\d{3})", p.name)
|
||||
if m:
|
||||
defs[m.group(1)].append((r, 1, raw[0] if raw else p.name))
|
||||
for n, line in enumerate(lines, 1):
|
||||
for id_ in set(ID_RE.findall(line)):
|
||||
if is_definition(line, id_, p):
|
||||
defs[id_].append((r, n, line.strip()))
|
||||
else:
|
||||
refs[id_].append((r, n, line.strip()))
|
||||
return defs, refs
|
||||
|
||||
|
||||
def detect_layout(docs: Path, files: dict[Path, list[str]] | None = None) -> str:
|
||||
if any(d.is_dir() and re.match(r"\d{2}-", d.name) for d in docs.iterdir()):
|
||||
return "strutturato"
|
||||
if (docs / "03-specs").exists() or any(re.match(r"SPEC-\d{3}", p.name) for p in (files or {})):
|
||||
return "strutturato"
|
||||
return "piatto"
|
||||
|
||||
|
||||
def spec_status(lines: list[str]) -> str:
|
||||
for line in lines[:12]:
|
||||
m = re.match(r"\s*Stato\s*:\s*(.+)", line)
|
||||
if m:
|
||||
return re.sub(r"[*`]", "", m.group(1)).strip()
|
||||
return "— (riga «Stato:» assente)"
|
||||
|
||||
|
||||
def index_status(files: dict[Path, list[str]], spec_id: str) -> str:
|
||||
"""Ripiego: lo stato della spec letto dalla sua riga nell'indice (ultima cella)."""
|
||||
for p, lines in files.items():
|
||||
if p.name in ("00-INDEX.md", "INDEX.md", "README.md"):
|
||||
for line in strip_comments(lines):
|
||||
if line.lstrip().startswith("|") and spec_id in line:
|
||||
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
||||
if cells:
|
||||
return "dall'indice: " + re.sub(r"[*`]", "", cells[-1])
|
||||
return ""
|
||||
|
||||
|
||||
def status_flag(st: str) -> str:
|
||||
if re.search(r"da approvare|bozza|proposta|proposed|draft", st, re.I):
|
||||
return " <-- NON approvata: non implementare"
|
||||
if re.search(r"approvat|✅|accettat|accepted", st, re.I):
|
||||
return ""
|
||||
return " <-- stato incerto: verificare prima di implementare"
|
||||
|
||||
|
||||
def is_approved(st: str) -> bool:
|
||||
return status_flag(st) == ""
|
||||
|
||||
|
||||
def last_update(lines: list[str]) -> str:
|
||||
for line in lines[:12]:
|
||||
m = re.search(r"Ultimo aggiornamento\s*:\s*\**([0-9]{4}-[0-9]{2}-[0-9]{2})", line)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return ""
|
||||
|
||||
|
||||
def spec_docs(files: dict[Path, list[str]], layout: str) -> list[Path]:
|
||||
"""Spec del layout strutturato (SPEC-nnn-*.md) o documenti di modulo del layout piatto
|
||||
(qualunque .md non chiave con una riga «Stato:» in testa)."""
|
||||
res = []
|
||||
for p, lines in files.items():
|
||||
if p.suffix.lower() != ".md":
|
||||
continue
|
||||
if re.match(r"SPEC-\d{3}", p.name):
|
||||
res.append(p)
|
||||
elif layout == "piatto" and p.name not in KEY_DOCS and any(re.match(r"\s*Stato\s*:", l) for l in lines[:12]):
|
||||
res.append(p)
|
||||
return sorted(res)
|
||||
|
||||
|
||||
RANGE_RE = re.compile(r"REQ-([A-Z]{2,5})-(\d{1,3})`?\s*(?:…|\.\.\.|–)\s*`?(?:REQ-[A-Z]{2,5}-)?(\d{1,3})")
|
||||
SHORT_RE = re.compile(r"(?<![A-Za-z0-9-])([A-Z]{2,5})-(\d{2,3})(?![0-9])")
|
||||
|
||||
|
||||
def covered_reqs(spec_texts: list[str], trace_lines: list[str]) -> set[str]:
|
||||
"""Requisiti coperti: citati in una spec (anche come intervallo «REQ-X-01 … 05») o su una
|
||||
riga di TRACEABILITY che nomina una spec (anche in forma breve «PRI-04»)."""
|
||||
cov: set[str] = set()
|
||||
for t in spec_texts:
|
||||
cov |= {i for i in ID_RE.findall(t) if kind(i) == "REQ"}
|
||||
for area, a, b in RANGE_RE.findall(t):
|
||||
cov |= {"REQ-%s-%02d" % (area, k) for k in range(int(a), int(b) + 1)}
|
||||
for line in trace_lines:
|
||||
if "SPEC-" in line:
|
||||
cov |= {i for i in ID_RE.findall(line) if kind(i) == "REQ"}
|
||||
cov |= {"REQ-%s-%s" % (a, n) for a, n in SHORT_RE.findall(line)
|
||||
if a not in ("SPEC", "REQ", "DIF")}
|
||||
return cov
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- lettura
|
||||
|
||||
def cmd_overview(docs: Path) -> None:
|
||||
files = load(docs)
|
||||
defs, refs = scan(files, docs)
|
||||
out(f"== {BRAND} ==")
|
||||
out(f"Cartella : {docs}")
|
||||
out(f"Layout : {detect_layout(docs, files)}")
|
||||
out(f"File : {len(files)} (md/yaml)")
|
||||
out()
|
||||
out("-- Documenti chiave (ordine di lettura consigliato) --")
|
||||
by_name = defaultdict(list)
|
||||
for p in files:
|
||||
by_name[p.name].append(p)
|
||||
for name in KEY_DOCS:
|
||||
for p in by_name.get(name, []):
|
||||
lines = files[p]
|
||||
upd = last_update(lines)
|
||||
out(f" {rel(p, docs):45s} {len(lines):6d} righe {('agg. ' + upd) if upd else ''}")
|
||||
big = [(p, len(l)) for p, l in files.items() if len(l) > 800]
|
||||
if big:
|
||||
out()
|
||||
out("-- File grandi: NON leggerli interi, cerca la sezione per ID/data --")
|
||||
for p, n in sorted(big, key=lambda x: -x[1]):
|
||||
out(f" {rel(p, docs):45s} {n:6d} righe")
|
||||
specs = spec_docs(files, detect_layout(docs, files))
|
||||
if specs:
|
||||
out()
|
||||
out("-- Specifiche / documenti di modulo --")
|
||||
for p in specs:
|
||||
st = spec_status(files[p])
|
||||
if st.startswith("—"):
|
||||
st = index_status(files, p.stem[:8]) or st
|
||||
out(f" {rel(p, docs):45s} {st}{status_flag(st)}")
|
||||
todo = [(p, sum(PLACEHOLDER in l for l in ls)) for p, ls in files.items()]
|
||||
todo = [(p, n) for p, n in todo if n]
|
||||
if todo:
|
||||
out()
|
||||
out(f"-- Segnaposto «{PLACEHOLDER}» ancora presenti ({sum(n for _, n in todo)}) --")
|
||||
for p, n in sorted(todo, key=lambda x: rel(x[0], docs)):
|
||||
out(f" {rel(p, docs):45s} {n:4d}")
|
||||
out()
|
||||
out("-- ID trovati (definiti / citati) --")
|
||||
counts = defaultdict(lambda: [0, 0])
|
||||
for id_ in set(defs) | set(refs):
|
||||
k = kind(id_)
|
||||
counts[k][0] += 1 if id_ in defs else 0
|
||||
counts[k][1] += 1
|
||||
for k in ("REQ", "SPEC", "T", "D", "DIF"):
|
||||
if k in counts:
|
||||
out(f" {k:5s} {counts[k][0]:5d} definiti su {counts[k][1]:5d} distinti")
|
||||
orphans = sorted(i for i in refs if i not in defs and kind(i) in {"REQ", "SPEC", "D"})
|
||||
if orphans:
|
||||
out()
|
||||
out(f"-- Citati ma mai definiti ({len(orphans)}) — possibile deriva della documentazione --")
|
||||
out(" " + ", ".join(orphans[:60]) + (" …" if len(orphans) > 60 else ""))
|
||||
|
||||
|
||||
def cmd_find(docs: Path, id_: str, limit: int) -> None:
|
||||
files = load(docs)
|
||||
defs, refs = scan(files, docs)
|
||||
out(f"== {BRAND} · {id_} ==")
|
||||
if not defs.get(id_):
|
||||
out("Definizione: NON TROVATA (ID inesistente, rinumerato o definito con formato insolito)")
|
||||
else:
|
||||
out("Definizione:")
|
||||
for r, n, line in defs[id_]:
|
||||
out(f" {r}:{n} {line[:220]}")
|
||||
rs = refs.get(id_, [])
|
||||
out(f"Citazioni: {len(rs)}")
|
||||
by_file = defaultdict(list)
|
||||
for r, n, line in rs:
|
||||
by_file[r].append((n, line))
|
||||
for r, items in sorted(by_file.items(), key=lambda x: -len(x[1])):
|
||||
out(f" {r} ({len(items)})")
|
||||
for n, line in items[:limit]:
|
||||
out(f" :{n} {line[:180]}")
|
||||
if len(items) > limit:
|
||||
out(f" … altre {len(items) - limit}")
|
||||
|
||||
|
||||
def section(lines: list[str], start: int, max_lines: int = 60) -> list[str]:
|
||||
"""Dal titolo alla riga 'start' (1-based) fino al prossimo titolo di pari o superiore livello."""
|
||||
head = lines[start - 1]
|
||||
if not head.lstrip().startswith("#"):
|
||||
return [head]
|
||||
level = len(head) - len(head.lstrip("#"))
|
||||
body = [head]
|
||||
for line in lines[start:]:
|
||||
s = line.lstrip()
|
||||
if s.startswith("#") and (len(s) - len(s.lstrip("#"))) <= level:
|
||||
break
|
||||
body.append(line)
|
||||
if len(body) >= max_lines:
|
||||
body.append("… (sezione troncata: aprire il file per il resto)")
|
||||
break
|
||||
return body
|
||||
|
||||
|
||||
def cmd_context(docs: Path, id_: str) -> None:
|
||||
files = load(docs)
|
||||
defs, refs = scan(files, docs)
|
||||
by_rel = {rel(p, docs): l for p, l in files.items()}
|
||||
out(f"== {BRAND} · contesto di {id_} ==")
|
||||
|
||||
if id_.startswith("SPEC-"):
|
||||
spec_file = next((p for p in files if p.name.startswith(id_)), None)
|
||||
if spec_file is None:
|
||||
out("Spec non trovata.")
|
||||
return
|
||||
lines = files[spec_file]
|
||||
out(f"File : {rel(spec_file, docs)} ({len(lines)} righe)")
|
||||
out(f"Stato : {spec_status(lines)}")
|
||||
out()
|
||||
out("Indice delle sezioni:")
|
||||
for n, line in enumerate(lines, 1):
|
||||
if line.startswith("## ") or line.startswith("### "):
|
||||
out(f" :{n} {line}")
|
||||
linked = sorted({i for line in strip_comments(lines) for i in ID_RE.findall(line)} - {id_})
|
||||
else:
|
||||
linked = set()
|
||||
for r, n, line in defs.get(id_, []):
|
||||
out(f"Definizione: {r}:{n}")
|
||||
body = section(by_rel[r], n) if line.startswith("#") else [line]
|
||||
for b in body:
|
||||
out(" " + b)
|
||||
for b in strip_comments(body):
|
||||
linked |= set(ID_RE.findall(b))
|
||||
if not defs.get(id_):
|
||||
out("Definizione: NON TROVATA")
|
||||
for r, n, line in refs.get(id_, []):
|
||||
if "SPEC-" in Path(r).name:
|
||||
linked.add(Path(r).name[:8])
|
||||
linked = sorted(linked - {id_})
|
||||
|
||||
out()
|
||||
out("ID collegati (un passo):")
|
||||
groups = defaultdict(list)
|
||||
for i in linked:
|
||||
groups[kind(i)].append(i)
|
||||
for k in ("REQ", "SPEC", "T", "D", "DIF"):
|
||||
if groups.get(k):
|
||||
out(f" {k:5s} " + ", ".join(groups[k]))
|
||||
|
||||
out()
|
||||
out("Definizioni sintetiche dei collegati (righe di tabella / titoli):")
|
||||
for i in linked:
|
||||
for r, n, line in defs.get(i, [])[:1]:
|
||||
out(f" [{i}] {r}:{n} {line[:200]}")
|
||||
|
||||
for target in ("TODO.md", "TRACEABILITY.md"):
|
||||
hits = [(r, n, l) for r, n, l in refs.get(id_, []) + defs.get(id_, []) if Path(r).name == target]
|
||||
if hits:
|
||||
out()
|
||||
out(f"Righe in {target}:")
|
||||
for r, n, line in hits[:25]:
|
||||
out(f" {r}:{n} {line[:220]}")
|
||||
|
||||
|
||||
def cmd_check(docs: Path) -> int:
|
||||
files = load(docs)
|
||||
defs, refs = scan(files, docs)
|
||||
issues: list[tuple[str, str]] = []
|
||||
|
||||
for i in sorted(refs):
|
||||
if i not in defs and kind(i) in {"REQ", "SPEC", "D", "DIF"}:
|
||||
issues.append(("ID orfano", f"{i} è citato ma non definito (es. {refs[i][0][0]}:{refs[i][0][1]})"))
|
||||
|
||||
specs = spec_docs(files, detect_layout(docs, files))
|
||||
spec_text = {p: "\n".join(strip_comments(files[p])) for p in specs}
|
||||
for p, text in spec_text.items():
|
||||
r = rel(p, docs)
|
||||
if not re.search(r"^#{2,3}\s.*criteri", text, re.I | re.M):
|
||||
issues.append(("Spec incompleta", f"{r}: manca la sezione «Criteri di accettazione»"))
|
||||
if not re.search(r"^#{2,3}\s.*task", text, re.I | re.M):
|
||||
issues.append(("Spec incompleta", f"{r}: manca la sezione «Task»"))
|
||||
if re.search(r"^#{2,3}\s.*domande ancora aperte", text, re.I | re.M):
|
||||
tail = text.split("omande ancora aperte", 1)[1]
|
||||
body = [l for l in tail.splitlines()[1:] if l.strip() and not l.startswith("#")]
|
||||
if body and not re.search(r"nessuna", " ".join(body), re.I):
|
||||
issues.append(("Spec non approvabile", f"{r}: «Domande ancora aperte» non vuota"))
|
||||
|
||||
trace = [l for p, ls in files.items() if p.name == "TRACEABILITY.md" for l in strip_comments(ls)]
|
||||
req_in_specs = covered_reqs(list(spec_text.values()), trace)
|
||||
req_defined = {i for i in defs if kind(i) == "REQ"}
|
||||
for i in sorted(req_defined - req_in_specs):
|
||||
if specs:
|
||||
issues.append(("Requisito scoperto", f"{i} non è coperto da nessuna spec"))
|
||||
|
||||
todo_files = [p for p in files if p.name == "TODO.md"]
|
||||
todo_text = "\n".join("\n".join(strip_comments(files[p])) for p in todo_files)
|
||||
if todo_files:
|
||||
for p, text in spec_text.items():
|
||||
for t in sorted({i for i in ID_RE.findall(text) if kind(i) == "T"}):
|
||||
if any(d[0] == rel(p, docs) for d in defs.get(t, [])) and t not in todo_text:
|
||||
issues.append(("Task fuori dal TODO", f"{t} definito in {rel(p, docs)} ma assente dal TODO"))
|
||||
|
||||
for p in specs:
|
||||
st = spec_status(files[p])
|
||||
if st.startswith("—"):
|
||||
st = index_status(files, p.stem[:8]) or st
|
||||
if is_approved(st):
|
||||
continue
|
||||
tasks = {i for i in ID_RE.findall(spec_text[p]) if kind(i) == "T"}
|
||||
done = []
|
||||
for line in todo_text.splitlines():
|
||||
ids = set(ID_RE.findall(line))
|
||||
if ids & tasks and re.search(r"✅|\bdone\b", line):
|
||||
done.extend(sorted(ids & tasks))
|
||||
if done:
|
||||
issues.append(("Deriva di stato", f"{rel(p, docs)} è «{st}» ma ha task fatti: {', '.join(sorted(set(done))[:8])}"))
|
||||
|
||||
ph = [(rel(p, docs), sum(PLACEHOLDER in l for l in ls)) for p, ls in files.items()]
|
||||
for r, n in sorted(ph):
|
||||
if n:
|
||||
issues.append(("Da compilare", f"{r}: {n} segnaposto «{PLACEHOLDER}»"))
|
||||
|
||||
out(f"== {BRAND} · controllo di coerenza ==")
|
||||
out(f"Cartella: {docs}")
|
||||
if not issues:
|
||||
out("Nessun problema rilevato.")
|
||||
return 0
|
||||
by_kind = defaultdict(list)
|
||||
for k, msg in issues:
|
||||
by_kind[k].append(msg)
|
||||
for k in ("ID orfano", "Requisito scoperto", "Spec incompleta", "Spec non approvabile",
|
||||
"Task fuori dal TODO", "Deriva di stato", "Da compilare"):
|
||||
if by_kind.get(k):
|
||||
out()
|
||||
out(f"-- {k} ({len(by_kind[k])}) --")
|
||||
for msg in by_kind[k][:40]:
|
||||
out(" " + msg)
|
||||
if len(by_kind[k]) > 40:
|
||||
out(f" … altri {len(by_kind[k]) - 40}")
|
||||
return 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- numerazione
|
||||
|
||||
def _max_num(ids, pattern: str) -> int:
|
||||
nums = [int(m.group(1)) for i in ids for m in [re.fullmatch(pattern, i)] if m]
|
||||
return max(nums, default=0)
|
||||
|
||||
|
||||
def next_id(docs: Path, tipo: str, area: str | None = None, spec: str | None = None,
|
||||
prefisso: str | None = None) -> str:
|
||||
files = load(docs) if docs.exists() else {}
|
||||
defs, refs = scan(files, docs) if files else ({}, {})
|
||||
known = set(defs) | set(refs)
|
||||
for p in files:
|
||||
m = re.match(r"(SPEC-\d{3})", p.name)
|
||||
if m:
|
||||
known.add(m.group(1))
|
||||
tipo = tipo.upper()
|
||||
if tipo == "SPEC":
|
||||
return "SPEC-%03d" % (_max_num(known, r"SPEC-(\d{3})") + 1)
|
||||
if tipo == "D":
|
||||
return "D-%03d" % (_max_num(known, r"D-(\d{3})") + 1)
|
||||
if tipo == "DIF":
|
||||
return "DIF-%02d" % (_max_num(known, r"DIF-(\d{1,3})") + 1)
|
||||
if tipo == "REQ":
|
||||
if not area:
|
||||
raise SystemExit("next-id REQ richiede --area (es. --area AUT)")
|
||||
a = area.upper()
|
||||
return "REQ-%s-%02d" % (a, _max_num(known, r"REQ-" + a + r"-(\d{1,3})") + 1)
|
||||
if tipo == "T":
|
||||
if prefisso:
|
||||
pf = prefisso.upper()
|
||||
return "T-%s%02d" % (pf, _max_num(known, r"T-" + pf + r"(\d{1,4})") + 1)
|
||||
if spec:
|
||||
n = int(re.sub(r"\D", "", spec))
|
||||
block = [i for i in known if re.fullmatch(r"T-\d{3}", i) and int(i[2:]) // 10 == n]
|
||||
if not block:
|
||||
return f"T-{n * 10:03d}"
|
||||
last = max(int(i[2:]) for i in block)
|
||||
if last % 10 < 9:
|
||||
return f"T-{last + 1:03d}"
|
||||
sfx = [i for i in known if re.fullmatch("T-%03d[b-z]" % last, i)]
|
||||
return f"T-{last:03d}{chr(ord(max(sfx)[-1]) + 1) if sfx else 'b'}"
|
||||
return "T-%03d" % (_max_num(known, r"T-(\d{3})") + 1)
|
||||
raise SystemExit(f"Tipo sconosciuto: {tipo} (REQ, SPEC, T, D, DIF)")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- scrittura
|
||||
|
||||
def fill(text: str, values: dict[str, str]) -> str:
|
||||
for k, v in values.items():
|
||||
text = text.replace("{{" + k + "}}", v)
|
||||
return text
|
||||
|
||||
|
||||
def slugify(s: str) -> str:
|
||||
s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode()
|
||||
return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")
|
||||
|
||||
|
||||
def cmd_scaffold(docs: Path, layout: str, progetto: str, data: str) -> int:
|
||||
src = TEMPLATES / layout
|
||||
if not src.is_dir():
|
||||
raise SystemExit(f"Modelli non trovati: {src}")
|
||||
created, skipped = [], []
|
||||
for t in sorted(src.rglob("*")):
|
||||
if t.is_dir():
|
||||
continue
|
||||
dest = docs / t.relative_to(src)
|
||||
if dest.exists():
|
||||
skipped.append(dest)
|
||||
continue
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
text = t.read_text(encoding="utf-8")
|
||||
dest.write_text(fill(text, {"PROGETTO": progetto, "DATA": data}), encoding="utf-8")
|
||||
created.append(dest)
|
||||
if layout == "strutturato":
|
||||
(docs / "03-specs").mkdir(parents=True, exist_ok=True)
|
||||
out(f"== {BRAND} · scaffold «{layout}» ==")
|
||||
out(f"Cartella: {docs}")
|
||||
out(f"Creati : {len(created)}")
|
||||
for p in created:
|
||||
out(f" + {rel(p, docs)}")
|
||||
if skipped:
|
||||
out(f"Già esistenti, NON toccati: {len(skipped)}")
|
||||
for p in skipped:
|
||||
out(f" = {rel(p, docs)}")
|
||||
out()
|
||||
out("Prossimo passo: compilare i segnaposto «DA COMPILARE» seguendo references/scrittura.md,")
|
||||
out("poi `check` per verificare la coerenza.")
|
||||
return 0
|
||||
|
||||
|
||||
def criterion_letter(n: int) -> str:
|
||||
return chr(64 + n) if 1 <= n <= 26 else f"S{n}"
|
||||
|
||||
|
||||
def cmd_new_spec(docs: Path, titolo: str, data: str, slug: str | None, prefisso: str | None,
|
||||
layout: str | None) -> int:
|
||||
files = load(docs) if docs.exists() else {}
|
||||
layout = layout or detect_layout(docs, files)
|
||||
if layout == "strutturato":
|
||||
sid = next_id(docs, "SPEC")
|
||||
n = int(sid[5:])
|
||||
slug = slug or slugify(titolo)
|
||||
dest = docs / "03-specs" / f"{sid}-{slug}.md"
|
||||
values = {"SPEC_ID": sid, "TITOLO": titolo, "DATA": data,
|
||||
"LETTERA": criterion_letter(n),
|
||||
"T0": f"T-{n * 10:03d}", "T1": f"T-{n * 10 + 1:03d}"}
|
||||
tpl = TEMPLATES / "spec.md"
|
||||
else:
|
||||
if not prefisso:
|
||||
raise SystemExit("Nel layout piatto serve --prefisso (lettera dei task del modulo, es. W)")
|
||||
pf = prefisso.upper()
|
||||
used = {i for p, ls in files.items() for l in strip_comments(ls) for i in ID_RE.findall(l)
|
||||
if re.fullmatch(r"T-" + pf + r"\d+[a-z]?", i)}
|
||||
if used:
|
||||
raise SystemExit(f"Il prefisso T-{pf} è già usato ({', '.join(sorted(used)[:5])}): sceglierne un altro.")
|
||||
name = (slug or slugify(titolo)).upper().replace("-", "_")
|
||||
if not name.endswith("_MODULE"):
|
||||
name += "_MODULE"
|
||||
dest = docs / f"{name}.md"
|
||||
values = {"TITOLO": titolo, "DATA": data, "PREFISSO": pf}
|
||||
tpl = TEMPLATES / "modulo.md"
|
||||
if dest.exists():
|
||||
raise SystemExit(f"Esiste già: {dest} — non lo sovrascrivo.")
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
dest.write_text(fill(tpl.read_text(encoding="utf-8"), values), encoding="utf-8")
|
||||
out(f"== {BRAND} · nuova spec ==")
|
||||
out(f"Creata: {dest}")
|
||||
out("Stato : da approvare")
|
||||
if layout == "strutturato":
|
||||
out(f"Task : blocco {values['T0']}…T-{n * 10 + 9:03d} · criteri {values['LETTERA']}1…")
|
||||
out("Ricorda: riga nella mappa di 00-INDEX §03, task in TODO, righe in TRACEABILITY.")
|
||||
else:
|
||||
out("Ricorda: riga in PROJECT_MEMORY §4, lettera del modulo e task in TODO.")
|
||||
return 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- main
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
today = dt.date.today().isoformat()
|
||||
ap = argparse.ArgumentParser(description=f"{BRAND} — documentazione spec-driven")
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
o = sub.add_parser("overview")
|
||||
o.add_argument("docs")
|
||||
f = sub.add_parser("find")
|
||||
f.add_argument("docs")
|
||||
f.add_argument("id")
|
||||
f.add_argument("--limit", type=int, default=8)
|
||||
c = sub.add_parser("context")
|
||||
c.add_argument("docs")
|
||||
c.add_argument("id")
|
||||
k = sub.add_parser("check")
|
||||
k.add_argument("docs")
|
||||
n = sub.add_parser("next-id")
|
||||
n.add_argument("docs")
|
||||
n.add_argument("tipo", help="REQ | SPEC | T | D | DIF")
|
||||
n.add_argument("--area")
|
||||
n.add_argument("--spec")
|
||||
n.add_argument("--prefisso")
|
||||
s = sub.add_parser("scaffold")
|
||||
s.add_argument("docs")
|
||||
s.add_argument("--layout", choices=LAYOUTS, required=True)
|
||||
s.add_argument("--progetto", required=True)
|
||||
s.add_argument("--data", default=today)
|
||||
ns = sub.add_parser("new-spec")
|
||||
ns.add_argument("docs")
|
||||
ns.add_argument("--titolo", required=True)
|
||||
ns.add_argument("--slug")
|
||||
ns.add_argument("--prefisso", help="solo layout piatto: lettera dei task del modulo")
|
||||
ns.add_argument("--layout", choices=LAYOUTS)
|
||||
ns.add_argument("--data", default=today)
|
||||
a = ap.parse_args()
|
||||
docs = Path(a.docs).resolve()
|
||||
|
||||
if a.cmd == "scaffold":
|
||||
return cmd_scaffold(docs, a.layout, a.progetto, a.data)
|
||||
if a.cmd == "new-spec":
|
||||
return cmd_new_spec(docs, a.titolo, a.data, a.slug, a.prefisso, a.layout)
|
||||
if not docs.is_dir():
|
||||
print(f"Cartella non trovata: {docs}", file=sys.stderr)
|
||||
return 2
|
||||
if a.cmd == "overview":
|
||||
cmd_overview(docs)
|
||||
elif a.cmd == "find":
|
||||
cmd_find(docs, a.id, a.limit)
|
||||
elif a.cmd == "context":
|
||||
cmd_context(docs, a.id)
|
||||
elif a.cmd == "check":
|
||||
return cmd_check(docs)
|
||||
elif a.cmd == "next-id":
|
||||
out(next_id(docs, a.tipo, a.area, a.spec, a.prefisso))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,60 @@
|
||||
# {{TITOLO}}
|
||||
|
||||
Stato: **da approvare** · Versione 0.1 · {{DATA}} · Prefisso task: **T-{{PREFISSO}}**
|
||||
|
||||
**Decisioni di riferimento:** <!-- `D-…` o «nessuna» -->
|
||||
|
||||
---
|
||||
|
||||
## 1. Contesto e requisito
|
||||
|
||||
<!-- DA COMPILARE: perché serve, chi lo ha chiesto e quando, cosa deve ottenere l'utente. -->
|
||||
|
||||
## 2. Requisiti
|
||||
|
||||
| ID | Requisito | Origine |
|
||||
|---|---|---|
|
||||
| <!-- REQ-AREA-01 --> | <!-- «Il sistema …», limiti numerici espliciti --> | <!-- committente, data --> |
|
||||
|
||||
## 3. Opzioni valutate e raccomandazione
|
||||
|
||||
<!-- Solo se c'è una scelta da fare. Ogni opzione con pro, contro, costo. La scelta finale
|
||||
diventa una D-xxx in DECISIONS.md. -->
|
||||
|
||||
## 4. Regole
|
||||
|
||||
<!-- DA COMPILARE in forma normativa. -->
|
||||
|
||||
## 5. Architettura del modulo
|
||||
|
||||
<!-- DA COMPILARE: servizi, code, modelli dati, storage toccati. -->
|
||||
|
||||
## 6. Contratto API
|
||||
|
||||
| Metodo | Percorso | Ruolo minimo | Esiti |
|
||||
|---|---|---|---|
|
||||
| <!-- DA COMPILARE --> | | | 200 · 404 · 422 |
|
||||
|
||||
## 7. Errori e casi limite
|
||||
|
||||
| Caso | Comportamento |
|
||||
|---|---|
|
||||
| <!-- DA COMPILARE --> | |
|
||||
|
||||
## 8. Criteri di accettazione
|
||||
|
||||
| # | Criterio |
|
||||
|:-:|---|
|
||||
| {{PREFISSO}}1 | <!-- «Dato …, quando …, allora …» --> |
|
||||
|
||||
## 9. Task
|
||||
|
||||
| ID | Attività | Prio | Dipende da |
|
||||
|----|----------|------|------------|
|
||||
| T-{{PREFISSO}}01 | <!-- DA COMPILARE --> | P1 | — |
|
||||
|
||||
## 10. Domande ancora aperte
|
||||
|
||||
<!-- Finché questa sezione non è vuota il documento non può essere approvato. -->
|
||||
|
||||
- <!-- DA COMPILARE o «nessuna» -->
|
||||
@@ -0,0 +1,30 @@
|
||||
# API — {{PROGETTO}}
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
Il contratto si scrive **prima** del codice. Se esiste un `openapi.yaml`, quello è la fonte di verità.
|
||||
|
||||
## Convenzioni
|
||||
|
||||
| Aspetto | Regola |
|
||||
|---|---|
|
||||
| Prefisso | `/api/v1` |
|
||||
| Formato | JSON, UTF-8, date ISO 8601 UTC |
|
||||
| Autenticazione | <!-- DA COMPILARE --> |
|
||||
| Errori | `{ "error": { "code", "message", "details" } }` |
|
||||
|
||||
## Endpoint — Stato di servizio
|
||||
|
||||
### `GET /health` *(pubblico)*
|
||||
|
||||
Risponde `200 {"status": "ok"}`.
|
||||
|
||||
## Endpoint previsti
|
||||
|
||||
<!-- Un gruppo per modulo. Stato nel titolo: *(previsto)* · ✅ *implementato (T-xxx)*. -->
|
||||
|
||||
### <!-- Gruppo --> — *(previsto)*
|
||||
|
||||
| Metodo | Percorso | Ruolo minimo | Descrizione | Task |
|
||||
|---|---|---|---|---|
|
||||
| <!-- DA COMPILARE --> | | | | |
|
||||
@@ -0,0 +1,45 @@
|
||||
# ARCHITECTURE — {{PROGETTO}}
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
## Struttura del repository
|
||||
|
||||
```
|
||||
<!-- DA COMPILARE -->
|
||||
```
|
||||
|
||||
## 1. Vista d'insieme
|
||||
|
||||
```
|
||||
<!-- DA COMPILARE: schema ASCII dei servizi -->
|
||||
```
|
||||
|
||||
## 2. Servizi/componenti
|
||||
|
||||
| Componente | Tecnologia | Responsabilità |
|
||||
|---|---|---|
|
||||
| <!-- DA COMPILARE --> | | |
|
||||
|
||||
## 3. Flusso principale
|
||||
|
||||
<!-- DA COMPILARE: passo per passo, con code e dati scambiati. -->
|
||||
|
||||
## 4. Macchina a stati
|
||||
|
||||
<!-- DA COMPILARE: stati, transizioni ammesse (ogni altra è rifiutata), invarianti — o «non applicabile». -->
|
||||
|
||||
## 5. Code e lavori asincroni
|
||||
|
||||
<!-- DA COMPILARE o «non applicabile». -->
|
||||
|
||||
## 6. Storage
|
||||
|
||||
<!-- DA COMPILARE: database, file/oggetti, convenzioni di nome. -->
|
||||
|
||||
## 7. Sicurezza e multi-tenancy
|
||||
|
||||
<!-- DA COMPILARE: autenticazione, ruoli, isolamento, segreti (mai nel repository). -->
|
||||
|
||||
## 8. Aperto / da decidere
|
||||
|
||||
- <!-- DA COMPILARE -->
|
||||
@@ -0,0 +1,10 @@
|
||||
# CHANGELOG — {{PROGETTO}}
|
||||
|
||||
Formato: ogni voce riporta **data · componente · modifica · motivo**. Le voci più recenti in alto.
|
||||
|
||||
---
|
||||
|
||||
## {{DATA}} — Avvio della documentazione
|
||||
|
||||
Creata la documentazione di progetto con metodo spec-driven: PROJECT_MEMORY, ARCHITECTURE, API,
|
||||
DATABASE, DECISIONS (D-001…D-003), TODO.
|
||||
@@ -0,0 +1,18 @@
|
||||
# DATABASE — {{PROGETTO}}
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
Motore: <!-- DA COMPILARE --> · Migrazioni: <!-- DA COMPILARE: strumento e numerazione -->
|
||||
|
||||
Regole: date sempre UTC · una migrazione per modifica, mai riscrivere una migrazione applicata ·
|
||||
nessun file binario nel database.
|
||||
|
||||
## A. <!-- Gruppo di tabelle -->
|
||||
|
||||
### `<!-- tabella -->` *(migrazione `0001`)*
|
||||
|
||||
| Colonna | Tipo | Null | Note |
|
||||
|---|---|:-:|---|
|
||||
| `id` | | no | PK |
|
||||
|
||||
Indici: <!-- DA COMPILARE --> · Vincoli: <!-- DA COMPILARE -->
|
||||
@@ -0,0 +1,28 @@
|
||||
# DECISIONS — {{PROGETTO}} (Architecture Decision Records)
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
Formato: ogni decisione ha contesto, opzioni valutate, scelta e motivazione.
|
||||
Stato: `accepted` · `proposed` · `superseded by D-xxx` · `rejected`.
|
||||
Le decisioni non si cancellano: si superano.
|
||||
|
||||
---
|
||||
|
||||
## D-001 — Stack tecnologico
|
||||
- **Stato:** proposed
|
||||
- **Contesto:** <!-- DA COMPILARE -->
|
||||
- **Opzioni:** <!-- (a) …; (b) … -->
|
||||
- **Scelta:** <!-- DA COMPILARE -->
|
||||
- **Motivazione:** <!-- DA COMPILARE -->
|
||||
|
||||
## D-002 — Sviluppo spec-driven
|
||||
- **Stato:** accepted
|
||||
- **Contesto:** le regole di business scritte solo nel codice emergono dal debugging invece che dalla specifica.
|
||||
- **Opzioni:** (a) documentazione a valle del codice; (b) documento di modulo approvato prima del codice.
|
||||
- **Scelta:** (b). Nessun codice senza un documento di modulo in stato *Approvato*, con regole, contratto, criteri di accettazione e task.
|
||||
- **Motivazione:** le regole diventano esplicite e verificabili prima di costare un difetto.
|
||||
|
||||
## D-003 — La documentazione vive nel repository
|
||||
- **Stato:** accepted
|
||||
- **Scelta:** `docs/` nel repository del codice, aggiornata nella stessa modifica del codice che la riguarda.
|
||||
- **Motivazione:** documenti e codice non divergono; la revisione del codice è anche revisione dei documenti.
|
||||
@@ -0,0 +1,12 @@
|
||||
# DEVELOPMENT_LOG — {{PROGETTO}}
|
||||
|
||||
Diario operativo per sessione: cosa fatto, problemi, perché si è deciso così, prossime azioni.
|
||||
Le voci più recenti in alto. Le voci non si cancellano.
|
||||
|
||||
---
|
||||
|
||||
## {{DATA}} — Avvio della documentazione
|
||||
|
||||
Creato lo scheletro della documentazione spec-driven.
|
||||
|
||||
**Prossime azioni:** completare D-001 (stack), scrivere il primo documento di modulo.
|
||||
@@ -0,0 +1,68 @@
|
||||
# PROJECT_MEMORY — {{PROGETTO}}
|
||||
|
||||
> Memoria permanente del progetto. Leggere **sempre** questo file (e l'intera cartella `docs/`)
|
||||
> prima di scrivere codice. Aggiornarlo dopo ogni modifica significativa.
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}** · Sessione: **avvio della documentazione**
|
||||
|
||||
---
|
||||
|
||||
## 1. Cos'è {{PROGETTO}}
|
||||
|
||||
<!-- DA COMPILARE: cosa fa, per chi, flusso ad alto livello in 4-6 passi, valore. -->
|
||||
|
||||
---
|
||||
|
||||
## 2. Architettura attuale (sintesi)
|
||||
|
||||
> Dettaglio completo in [`ARCHITECTURE.md`](./ARCHITECTURE.md).
|
||||
|
||||
- <!-- DA COMPILARE: un punto per servizio/componente, con la tecnologia -->
|
||||
|
||||
---
|
||||
|
||||
## 3. Decisioni tecniche prese
|
||||
|
||||
> Registro completo con alternative e motivazioni in [`DECISIONS.md`](./DECISIONS.md).
|
||||
|
||||
- D-001 Stack tecnologico *(proposed)*
|
||||
- D-002 Sviluppo spec-driven: nessun codice senza un documento di modulo approvato
|
||||
- D-003 La documentazione vive nel repository e si aggiorna con il codice
|
||||
|
||||
---
|
||||
|
||||
## 4. Moduli e documenti di specifica
|
||||
|
||||
| Modulo | Documento | Stato |
|
||||
|---|---|---|
|
||||
| <!-- DA COMPILARE --> | <!-- [`NOME_MODULE.md`](./NOME_MODULE.md) --> | da approvare |
|
||||
|
||||
---
|
||||
|
||||
## 5. Componenti GIÀ creati
|
||||
|
||||
- [ ] <!-- DA COMPILARE -->
|
||||
|
||||
## 6. Componenti ANCORA da creare
|
||||
|
||||
- [ ] <!-- DA COMPILARE -->
|
||||
|
||||
---
|
||||
|
||||
## 7. Vincoli importanti
|
||||
|
||||
<!-- Ciò che, se dimenticato, fa ripetere un errore. Ogni vincolo con il suo perché. -->
|
||||
|
||||
- <!-- DA COMPILARE -->
|
||||
|
||||
---
|
||||
|
||||
## 8. Come continuare (per la prossima sessione)
|
||||
|
||||
1. Leggere questo file, poi [`TODO.md`](./TODO.md) (P0 aperti per primi).
|
||||
2. Per il task scelto, leggere il documento di modulo e le decisioni citate.
|
||||
3. A fine lavoro: TODO, CHANGELOG, DEVELOPMENT_LOG e questo file.
|
||||
|
||||
---
|
||||
|
||||
<sub>Documentazione redatta con il metodo spec-driven di STS Lab Srl.</sub>
|
||||
@@ -0,0 +1,25 @@
|
||||
# TODO — {{PROGETTO}}
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
Legenda stato: `pending` · `in_progress` · `done` · `blocked`
|
||||
Priorità: **P0** (fondante/bloccante) · **P1** (core) · **P2** (importante) · **P3** (successivo)
|
||||
|
||||
**Regola:** un task entra in lavorazione solo se il suo documento di modulo è approvato (D-002).
|
||||
`done` solo con i test del criterio passanti.
|
||||
|
||||
**Numerazione:** `T-<lettera del modulo><nn>` (es. `T-S<nn>` per la sicurezza, `T-W<nn>` per WhatsApp);
|
||||
la lettera si fissa quando nasce il modulo e si annota qui sotto.
|
||||
|
||||
| Lettera | Modulo |
|
||||
|:-:|---|
|
||||
| <!-- S --> | <!-- Sicurezza & API --> |
|
||||
|
||||
---
|
||||
|
||||
## Documentazione ({{DATA}})
|
||||
|
||||
| ID | Attività | Prio | Stato |
|
||||
|----|----------|------|-------|
|
||||
| T-D01 | Completare PROJECT_MEMORY, ARCHITECTURE, DECISIONS (D-001) | P0 | in_progress |
|
||||
| T-D02 | Primo documento di modulo approvato | P0 | pending |
|
||||
@@ -0,0 +1,83 @@
|
||||
# {{SPEC_ID}} — {{TITOLO}}
|
||||
|
||||
Stato: **da approvare** · Versione 0.1 · {{DATA}}
|
||||
|
||||
**Requisiti coperti:** <!-- DA COMPILARE: `REQ-…` -->
|
||||
**Decisioni di riferimento:** <!-- `D-…` o «nessuna» -->
|
||||
|
||||
---
|
||||
|
||||
## 1. Contesto e obiettivo
|
||||
|
||||
<!-- DA COMPILARE: perché questa capability esiste, cosa decide per l'utente. Un dato misurato
|
||||
vale più di un aggettivo. Se la spec è a sé per una ragione (regola delicata, rischio), dirlo. -->
|
||||
|
||||
---
|
||||
|
||||
## 2. Cosa cambia rispetto a ciò che è già scritto
|
||||
|
||||
<!-- Solo se la spec modifica requisiti, decisioni o altre spec. Altrimenti rimuovere la sezione. -->
|
||||
|
||||
| Dove | Prima | Dopo |
|
||||
|---|---|---|
|
||||
| | | |
|
||||
|
||||
---
|
||||
|
||||
## 3. Regole
|
||||
|
||||
<!-- DA COMPILARE in forma NORMATIVA: «il sistema …», «non …», limiti numerici espliciti.
|
||||
Pseudocodice per gli algoritmi. Una tabella per le combinazioni di casi. Quando un comportamento
|
||||
sembra un errore ma è voluto, dirlo: «è una decisione, non una dimenticanza». -->
|
||||
|
||||
---
|
||||
|
||||
## 4. Contratto API
|
||||
|
||||
### `METODO /api/v1/percorso`
|
||||
|
||||
**Ingresso:** <!-- campi e tipi -->
|
||||
|
||||
**Uscita:** <!-- campi e tipi -->
|
||||
|
||||
| Esito | Codice |
|
||||
|---|:-:|
|
||||
| <!-- Riuscito --> | 200 |
|
||||
| <!-- Non trovato o di altro tenant --> | 404 |
|
||||
| <!-- Validazione fallita --> | 422 |
|
||||
|
||||
Il dettaglio formale va in `04-api/openapi.yaml` **prima** del codice.
|
||||
|
||||
---
|
||||
|
||||
## 5. Dati
|
||||
|
||||
<!-- DA COMPILARE: entità, colonne, vincoli toccati — rimando a DATA_MODEL e DATABASE. -->
|
||||
|
||||
---
|
||||
|
||||
## 6. Errori e casi limite
|
||||
|
||||
| Caso | Comportamento |
|
||||
|---|---|
|
||||
| <!-- DA COMPILARE --> | |
|
||||
|
||||
---
|
||||
|
||||
## 7. Criteri di accettazione
|
||||
|
||||
Ogni criterio è verificabile da un test e ha un esito binario.
|
||||
|
||||
| # | Criterio |
|
||||
|:-:|---|
|
||||
| {{LETTERA}}1 | <!-- DA COMPILARE: «Dato …, quando …, allora …» --> |
|
||||
| {{LETTERA}}2 | |
|
||||
|
||||
---
|
||||
|
||||
## 8. Task
|
||||
|
||||
| ID | Task | Dipende da |
|
||||
|---|---|---|
|
||||
| **{{T0}}** | <!-- DA COMPILARE --> | — |
|
||||
| **{{T1}}** | Test di accettazione {{LETTERA}}1–{{LETTERA}}n | {{T0}} |
|
||||
@@ -0,0 +1,153 @@
|
||||
# {{PROGETTO}} — Indice della documentazione
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
| Step | Contenuto | Stato |
|
||||
|:-:|---|:-:|
|
||||
| 0 | Scheletro della documentazione · indice · decisioni iniziali | 🟡 |
|
||||
| 1 | `01-product/` + `02-architecture/` — visione, requisiti, architettura | ⬜ |
|
||||
| 2 | `03-specs/` — specifiche e criteri di accettazione | ⬜ |
|
||||
| 3 | `04-api/` + `05-data/` — contratto OpenAPI e schema dati | ⬜ |
|
||||
| 4 | `06-devops/` — ambienti, pipeline, infrastruttura | ⬜ |
|
||||
| 5 | `07-quality/` + `08-project/` — tracciabilità e backlog | ⬜ |
|
||||
| 6 | Implementazione — vedi [`08-project/ROADMAP.md`](08-project/ROADMAP.md) | ⬜ |
|
||||
|
||||
<!-- Novità: un riquadro per ogni consegna rilevante, il più recente in alto. Esempio:
|
||||
> **Titolo breve (AAAA-MM-GG):** cosa è cambiato e perché — `REQ-…`, `D-…`, `T-…`.
|
||||
> Verificato come (test, sistema reale) — backend N (+k).
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
## Cos'è questo progetto
|
||||
|
||||
<!-- DA COMPILARE: due o tre frasi. Che cosa fa il prodotto, per chi, su quale stack. -->
|
||||
|
||||
Obiettivi vincolanti del progetto:
|
||||
|
||||
1. <!-- DA COMPILARE: obiettivo misurabile -->
|
||||
2. <!-- DA COMPILARE -->
|
||||
3. **Spec-driven** — nessuna riga di codice senza una spec approvata e tracciata (D-002).
|
||||
|
||||
---
|
||||
|
||||
## Metodo: spec-driven development
|
||||
|
||||
Il flusso di lavoro è vincolato e va in un'unica direzione:
|
||||
|
||||
```
|
||||
REQ-xxx (01-product/REQUIREMENTS.md)
|
||||
│ cosa deve fare il prodotto
|
||||
▼
|
||||
SPEC-xxx (03-specs/)
|
||||
│ come lo fa: contratto API, dati, regole, errori, criteri di accettazione
|
||||
▼
|
||||
T-xxx (08-project/TODO.md)
|
||||
│ unità di lavoro implementabile
|
||||
▼
|
||||
codice + test
|
||||
│
|
||||
▼
|
||||
07-quality/TRACEABILITY.md ← chiude il cerchio: ogni REQ ha spec, task e test
|
||||
```
|
||||
|
||||
**Regola:** si implementa solo ciò che ha una `SPEC-xxx` in stato *Approvata*. Una spec non
|
||||
approvata non genera task; un task senza spec non entra in lavorazione. Ogni scelta strutturale
|
||||
diventa una decisione numerata `D-xxx` in [`08-project/DECISIONS.md`](08-project/DECISIONS.md).
|
||||
|
||||
---
|
||||
|
||||
## Mappa dei documenti
|
||||
|
||||
Legenda stato: ⬜ da scrivere · 🟡 in stesura · ✅ completo · 🔒 congelato (modifica solo via nuova D-xxx)
|
||||
|
||||
### 01 · Prodotto — *cosa* e *per chi*
|
||||
|
||||
| Documento | Contenuto | Stato |
|
||||
|---|---|---|
|
||||
| [`01-product/VISION.md`](01-product/VISION.md) | Perché esiste il prodotto, valore, perimetro, non-obiettivi | ⬜ |
|
||||
| [`01-product/GLOSSARY.md`](01-product/GLOSSARY.md) | Lessico condiviso | ⬜ |
|
||||
| [`01-product/PERSONAS_CASI_USO.md`](01-product/PERSONAS_CASI_USO.md) | Attori, persone, casi d'uso end-to-end | ⬜ |
|
||||
| [`01-product/REQUIREMENTS.md`](01-product/REQUIREMENTS.md) | Requisiti `REQ-*` e difetti noti `DIF-*` | ⬜ |
|
||||
|
||||
### 02 · Architettura — *come è fatto*
|
||||
|
||||
| Documento | Contenuto | Stato |
|
||||
|---|---|---|
|
||||
| [`02-architecture/ARCHITECTURE.md`](02-architecture/ARCHITECTURE.md) | Vista d'insieme, servizi, flusso principale | ⬜ |
|
||||
| [`02-architecture/C4_MODEL.md`](02-architecture/C4_MODEL.md) | Contesto / Contenitori / Componenti | ⬜ |
|
||||
| [`02-architecture/DOMAIN_WORKFLOW.md`](02-architecture/DOMAIN_WORKFLOW.md) | Stati, transizioni ammesse, invarianti | ⬜ |
|
||||
| [`02-architecture/DATA_MODEL.md`](02-architecture/DATA_MODEL.md) | Entità, relazioni, chiavi, cancellazioni, invarianti | ⬜ |
|
||||
| [`02-architecture/SECURITY.md`](02-architecture/SECURITY.md) | Autenticazione, autorizzazione, segreti, audit | ⬜ |
|
||||
|
||||
### 03 · Specifiche — *contratto per l'implementazione*
|
||||
|
||||
| Spec | Capability | Stato |
|
||||
|---|---|---|
|
||||
<!-- Una riga per spec, aggiunta quando la spec nasce:
|
||||
| [`SPEC-001-slug.md`](03-specs/SPEC-001-slug.md) | Capability · N criteri · T-010…019 | 🟡 da approvare |
|
||||
-->
|
||||
|
||||
### 04 · API — *contratto esterno*
|
||||
|
||||
| Documento | Contenuto | Stato |
|
||||
|---|---|---|
|
||||
| [`04-api/API.md`](04-api/API.md) | Catalogo endpoint, convenzioni, errori, paginazione | ⬜ |
|
||||
| [`04-api/openapi.yaml`](04-api/openapi.yaml) | Contratto OpenAPI 3.1 — **fonte di verità** | ⬜ |
|
||||
|
||||
### 05 · Dati
|
||||
|
||||
| Documento | Contenuto | Stato |
|
||||
|---|---|---|
|
||||
| [`05-data/DATABASE.md`](05-data/DATABASE.md) | Schema fisico: tabelle, enumerazioni, indici, vincoli, migrazioni | ⬜ |
|
||||
| [`05-data/SEED_BOOTSTRAP.md`](05-data/SEED_BOOTSTRAP.md) | Stato iniziale, bootstrap idempotente, credenziali | ⬜ |
|
||||
|
||||
### 06 · DevOps
|
||||
|
||||
| Documento | Contenuto | Stato |
|
||||
|---|---|---|
|
||||
| [`06-devops/INFRASTRUCTURE.md`](06-devops/INFRASTRUCTURE.md) | Topologia, servizi, risorse, rete, persistenza | ⬜ |
|
||||
| [`06-devops/ENVIRONMENTS.md`](06-devops/ENVIRONMENTS.md) | dev/qa/prod, variabili, promozione | ⬜ |
|
||||
| [`06-devops/CI_CD.md`](06-devops/CI_CD.md) | Pipeline, verifiche, rilascio, rientro | ⬜ |
|
||||
| [`06-devops/OBSERVABILITY.md`](06-devops/OBSERVABILITY.md) | Log, metriche, allarmi | ⬜ |
|
||||
| [`06-devops/RUNBOOK.md`](06-devops/RUNBOOK.md) | Procedure: sintomo → verifica → azione → conferma | ⬜ |
|
||||
| [`06-devops/BACKUP_DR.md`](06-devops/BACKUP_DR.md) | Backup, ripristino, prova periodica | ⬜ |
|
||||
|
||||
### 07 · Qualità
|
||||
|
||||
| Documento | Contenuto | Stato |
|
||||
|---|---|---|
|
||||
| [`07-quality/TEST_STRATEGY.md`](07-quality/TEST_STRATEGY.md) | Livelli di test, simulatori, criteri di uscita | ⬜ |
|
||||
| [`07-quality/ACCEPTANCE_CRITERIA.md`](07-quality/ACCEPTANCE_CRITERIA.md) | Criteri bloccanti di rilascio | ⬜ |
|
||||
| [`07-quality/TRACEABILITY.md`](07-quality/TRACEABILITY.md) | Matrice REQ → SPEC → T → test, senza righe scoperte | ⬜ |
|
||||
|
||||
### 08 · Progetto
|
||||
|
||||
| Documento | Contenuto | Stato |
|
||||
|---|---|---|
|
||||
| [`08-project/DECISIONS.md`](08-project/DECISIONS.md) | Decisioni architetturali `D-xxx` | 🟡 |
|
||||
| [`08-project/ROADMAP.md`](08-project/ROADMAP.md) | Fasi con criteri di uscita, dipendenze, rischi | ⬜ |
|
||||
| [`08-project/TODO.md`](08-project/TODO.md) | Task `T-xxx` per fase, tracciati sulle spec | ⬜ |
|
||||
| [`08-project/CHANGELOG.md`](08-project/CHANGELOG.md) | Cosa cambia per chi usa il prodotto | ⬜ |
|
||||
| [`08-project/DEVELOPMENT_LOG.md`](08-project/DEVELOPMENT_LOG.md) | Diario tecnico: cosa si è scoperto e perché | ⬜ |
|
||||
| [`08-project/PROJECT_MEMORY.md`](08-project/PROJECT_MEMORY.md) | **Da leggere a inizio di ogni sessione** | 🟡 |
|
||||
|
||||
---
|
||||
|
||||
## Struttura del repository
|
||||
|
||||
```
|
||||
<!-- DA COMPILARE: albero delle cartelle principali, una riga di descrizione ciascuna -->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Riferimenti esterni
|
||||
|
||||
| Risorsa | Percorso / URL |
|
||||
|---|---|
|
||||
| <!-- DA COMPILARE: repository, documenti del committente, sistemi collegati --> | |
|
||||
|
||||
---
|
||||
|
||||
<sub>Documentazione redatta con il metodo spec-driven di STS Lab Srl.</sub>
|
||||
@@ -0,0 +1,37 @@
|
||||
# GLOSSARY — Lessico di {{PROGETTO}}
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
Un termine, un significato. Gli stessi nomi si usano nei documenti, nel codice (classi,
|
||||
tabelle, rotte) e nell'interfaccia. Se un termine cambia, cambia qui per primo.
|
||||
|
||||
---
|
||||
|
||||
## 1. Termini di dominio
|
||||
|
||||
| Termine | Significato | Nel codice |
|
||||
|---|---|---|
|
||||
| <!-- DA COMPILARE --> | | `nome_tecnico` |
|
||||
|
||||
## 2. Termini di piattaforma
|
||||
|
||||
| Termine | Significato | Nel codice |
|
||||
|---|---|---|
|
||||
| <!-- DA COMPILARE: es. Tenant, Utente, Ruolo --> | | |
|
||||
|
||||
## 3. Convenzioni di identificazione nei documenti
|
||||
|
||||
| ID | Significato | Dove nasce |
|
||||
|---|---|---|
|
||||
| `REQ-<AREA>-<nn>` | Requisito | `01-product/REQUIREMENTS.md` |
|
||||
| `DIF-<nn>` | Difetto noto | `01-product/REQUIREMENTS.md` |
|
||||
| `SPEC-<nnn>` | Specifica | `03-specs/` |
|
||||
| `T-<nnn>` | Task | sezione *Task* della spec e `08-project/TODO.md` |
|
||||
| `D-<nnn>` | Decisione | `08-project/DECISIONS.md` |
|
||||
| `A1`, `B3`, … | Criterio di accettazione (lettera della spec + numero) | sezione *Criteri* della spec |
|
||||
|
||||
Gli ID sono **immutabili**: non si rinumerano e non si riusano. Un ID chiuso senza lavoro si barra (`~~T-xxx~~`).
|
||||
|
||||
## 4. Falsi amici
|
||||
|
||||
<!-- DA COMPILARE: parole che il committente usa con un significato diverso da quello tecnico. -->
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
# PERSONAS E CASI D'USO — {{PROGETTO}}
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
---
|
||||
|
||||
## 1. Attori
|
||||
|
||||
### 1.1 Attori umani
|
||||
|
||||
| Attore | Ruolo nel sistema | Permessi |
|
||||
|---|---|---|
|
||||
| <!-- DA COMPILARE --> | | |
|
||||
|
||||
### 1.2 Attori non umani
|
||||
|
||||
| Attore | Come interagisce |
|
||||
|---|---|
|
||||
| <!-- DA COMPILARE: sistemi esterni, job pianificati, integrazioni --> | |
|
||||
|
||||
## 2. Persone
|
||||
|
||||
### 2.1 <!-- Nome — Ruolo -->
|
||||
|
||||
<!-- DA COMPILARE: chi è, cosa fa ogni giorno, cosa lo frustra oggi, cosa considera un successo. -->
|
||||
|
||||
## 3. Casi d'uso
|
||||
|
||||
### UC-01 — <!-- titolo come risultato: «Una richiesta completa diventa una proposta» -->
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Attore** | |
|
||||
| **Precondizioni** | |
|
||||
| **Flusso principale** | 1. … 2. … 3. … |
|
||||
| **Flussi alternativi** | |
|
||||
| **Esito** | |
|
||||
| **Requisiti** | `REQ-…` |
|
||||
|
||||
## 4. Matrice attore × caso d'uso
|
||||
|
||||
| Caso d'uso | <!-- attore 1 --> | <!-- attore 2 --> |
|
||||
|---|:-:|:-:|
|
||||
| UC-01 | ● | |
|
||||
@@ -0,0 +1,62 @@
|
||||
# REQUIREMENTS — {{PROGETTO}}
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}** · Stato: **in stesura**
|
||||
|
||||
---
|
||||
|
||||
## Come leggere questo documento
|
||||
|
||||
Ogni requisito ha un identificativo `REQ-<AREA>-<nn>` immutabile, citato dalle specifiche
|
||||
(`03-specs/`), dai task (`08-project/TODO.md`) e dalla matrice di tracciabilità
|
||||
(`07-quality/TRACEABILITY.md`).
|
||||
|
||||
**Tipo:**
|
||||
|
||||
| Sigla | Significato |
|
||||
|:-:|---|
|
||||
| **P** | **Parità** — comportamento già presente nel sistema esistente, da riprodurre **identico**. Una differenza è una regressione. *(Solo per riscritture o evoluzioni.)* |
|
||||
| **C** | **Correzione** — comportamento esistente difettoso, da correggere. Rimanda a un `DIF-nn`. |
|
||||
| **N** | **Nuovo** — introdotto da questo progetto. |
|
||||
|
||||
**Origine:** dove il requisito è stato osservato o deciso: committente e data, `D-nnn`, oppure
|
||||
`file:funzione` del codice esistente per i requisiti di parità.
|
||||
|
||||
> **Regola.** Un requisito descrive un comportamento osservabile e verificabile, non una
|
||||
> soluzione tecnica. Modificare un requisito approvato è una decisione di prodotto e richiede
|
||||
> una `D-nnn`.
|
||||
|
||||
---
|
||||
|
||||
## Riepilogo
|
||||
|
||||
| Area | Codice | Requisiti | P | C | N |
|
||||
|---|:-:|:-:|:-:|:-:|:-:|
|
||||
| <!-- DA COMPILARE: es. Autenticazione e utenti --> | <!-- AUT --> | | | | |
|
||||
| Non funzionali | NFR | | | | |
|
||||
| **Totale** | | | | | |
|
||||
|
||||
---
|
||||
|
||||
## <!-- AREA --> · <!-- Nome dell'area -->
|
||||
|
||||
| ID | Requisito | Tipo | Origine |
|
||||
|---|---|:-:|---|
|
||||
| <!-- **REQ-AREA-01** --> | <!-- DA COMPILARE: «Il sistema …», condizioni in grassetto, limiti numerici espliciti --> | N | <!-- committente, AAAA-MM-GG --> |
|
||||
|
||||
---
|
||||
|
||||
## NFR · Non funzionali
|
||||
|
||||
| ID | Requisito | Tipo | Origine |
|
||||
|---|---|:-:|---|
|
||||
| <!-- **REQ-NFR-01** --> | <!-- DA COMPILARE: prestazioni, disponibilità, sicurezza, conservazione dei dati — sempre con un numero --> | N | |
|
||||
|
||||
---
|
||||
|
||||
## Difetti noti
|
||||
|
||||
Difetti del sistema esistente, o emersi durante il progetto, che un requisito di tipo **C** corregge.
|
||||
|
||||
| ID | Difetto | Effetto osservato | Requisito |
|
||||
|---|---|---|---|
|
||||
| <!-- **DIF-01** --> | | | `REQ-…` |
|
||||
@@ -0,0 +1,52 @@
|
||||
# VISION — {{PROGETTO}}
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}** · Stato: **in stesura**
|
||||
|
||||
---
|
||||
|
||||
## 1. La frase che guida il prodotto
|
||||
|
||||
> <!-- DA COMPILARE: una frase sola, quella con cui si decide cosa entra e cosa no. -->
|
||||
|
||||
## 2. Il problema
|
||||
|
||||
<!-- DA COMPILARE: chi ha il problema, come lo risolve oggi, quanto costa (tempo, errori, denaro).
|
||||
Preferire un dato misurato a un aggettivo. -->
|
||||
|
||||
## 3. Cosa fa il prodotto
|
||||
|
||||
<!-- DA COMPILARE: il flusso principale in 5-7 passi, dal punto di vista di chi lo usa. -->
|
||||
|
||||
## 4. Perché adesso
|
||||
|
||||
<!-- DA COMPILARE: cosa rende necessario il progetto ora (scadenza, cliente, limite del sistema attuale). -->
|
||||
|
||||
## 5. Cosa NON cambia
|
||||
|
||||
<!-- DA COMPILARE: vincoli fissi (sistemi esistenti, interfacce, abitudini degli utenti), oppure «non applicabile». -->
|
||||
|
||||
## 6. Perimetro
|
||||
|
||||
### Dentro il perimetro (primo rilascio)
|
||||
|
||||
- <!-- DA COMPILARE -->
|
||||
|
||||
### Fuori dal perimetro (esplicitamente)
|
||||
|
||||
- <!-- DA COMPILARE: ciò che qualcuno potrebbe aspettarsi e che NON si fa. Scriverlo evita discussioni dopo. -->
|
||||
|
||||
## 7. Come si misura il successo
|
||||
|
||||
| Indicatore | Valore attuale | Obiettivo |
|
||||
|---|:-:|:-:|
|
||||
| <!-- DA COMPILARE --> | | |
|
||||
|
||||
## 8. Rischi principali
|
||||
|
||||
| Rischio | Probabilità | Impatto | Contromisura |
|
||||
|---|:-:|:-:|---|
|
||||
| <!-- DA COMPILARE --> | | | |
|
||||
|
||||
## 9. Documenti collegati
|
||||
|
||||
[`REQUIREMENTS.md`](REQUIREMENTS.md) · [`PERSONAS_CASI_USO.md`](PERSONAS_CASI_USO.md) · [`../08-project/DECISIONS.md`](../08-project/DECISIONS.md)
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
# ARCHITECTURE — {{PROGETTO}}
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}** · Stato: **in stesura**
|
||||
|
||||
---
|
||||
|
||||
## 1. Vista d'insieme
|
||||
|
||||
```
|
||||
<!-- DA COMPILARE: schema ASCII dei servizi e delle frecce principali -->
|
||||
```
|
||||
|
||||
## 2. Servizi
|
||||
|
||||
| Servizio | Tecnologia | Responsabilità | Non fa |
|
||||
|---|---|---|---|
|
||||
| <!-- DA COMPILARE --> | | | |
|
||||
|
||||
<!-- Per ogni scelta non ovvia («perché due servizi e non uno») una sottosezione breve che
|
||||
rimanda alla D-xxx. -->
|
||||
|
||||
## 3. Flusso principale
|
||||
|
||||
<!-- DA COMPILARE: il caso d'uso UC-01 attraverso i servizi, passo per passo, con le code e i dati scambiati. -->
|
||||
|
||||
## 4. Code e lavori asincroni
|
||||
|
||||
| Coda / lavoro | Produttore | Consumatore | Tentativi | Cosa succede se fallisce |
|
||||
|---|---|---|:-:|---|
|
||||
| <!-- DA COMPILARE o «non applicabile» --> | | | | |
|
||||
|
||||
## 5. Configurazione
|
||||
|
||||
<!-- DA COMPILARE: cosa è variabile d'ambiente, cosa è configurazione a caldo, cosa è segreto. -->
|
||||
|
||||
## 6. Multi-tenancy e isolamento
|
||||
|
||||
<!-- DA COMPILARE o «non applicabile». -->
|
||||
|
||||
## 7. Integrazioni esterne
|
||||
|
||||
| Sistema | Direzione | Protocollo | Contratto |
|
||||
|---|---|---|---|
|
||||
| <!-- DA COMPILARE --> | | | |
|
||||
|
||||
## 8. Ambienti
|
||||
|
||||
Vedi [`../06-devops/ENVIRONMENTS.md`](../06-devops/ENVIRONMENTS.md).
|
||||
|
||||
## 9. Documenti collegati
|
||||
|
||||
[`C4_MODEL.md`](C4_MODEL.md) · [`DATA_MODEL.md`](DATA_MODEL.md) · [`DOMAIN_WORKFLOW.md`](DOMAIN_WORKFLOW.md) · [`SECURITY.md`](SECURITY.md)
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# C4 MODEL — Contesto, Contenitori, Componenti
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
---
|
||||
|
||||
## Livello 1 · Contesto
|
||||
|
||||
```
|
||||
<!-- DA COMPILARE: il sistema al centro, attori e sistemi esterni intorno -->
|
||||
```
|
||||
|
||||
| Attore / sistema esterno | Relazione con {{PROGETTO}} |
|
||||
|---|---|
|
||||
| <!-- DA COMPILARE --> | |
|
||||
|
||||
## Livello 2 · Contenitori
|
||||
|
||||
| Contenitore | Tecnologia | Parla con | Porta / protocollo |
|
||||
|---|---|---|---|
|
||||
| <!-- DA COMPILARE --> | | | |
|
||||
|
||||
## Livello 3 · Componenti
|
||||
|
||||
### 3.1 <!-- Contenitore -->
|
||||
|
||||
| Componente | Responsabilità | Spec |
|
||||
|---|---|---|
|
||||
| <!-- DA COMPILARE --> | | `SPEC-…` |
|
||||
|
||||
## 4. Corrispondenza fra i livelli e i requisiti
|
||||
|
||||
| Componente | Requisiti |
|
||||
|---|---|
|
||||
| <!-- DA COMPILARE --> | `REQ-…` |
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
# DATA MODEL — Entità, relazioni, invarianti
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
Modello **logico**. Lo schema fisico (tipi, indici, migrazioni) è in
|
||||
[`../05-data/DATABASE.md`](../05-data/DATABASE.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. Mappa delle entità
|
||||
|
||||
```
|
||||
<!-- DA COMPILARE: entità e cardinalità, es. Tenant 1──* Utente -->
|
||||
```
|
||||
|
||||
## 2. Entità
|
||||
|
||||
### 2.1 <!-- Entità -->
|
||||
|
||||
| Attributo | Significato | Obbligatorio | Note |
|
||||
|---|---|:-:|---|
|
||||
| <!-- DA COMPILARE --> | | | |
|
||||
|
||||
**Ciclo di vita:** <!-- chi la crea, chi la modifica, quando si cancella -->
|
||||
|
||||
## 3. Chiavi naturali e univocità
|
||||
|
||||
| Entità | Univoca per |
|
||||
|---|---|
|
||||
| <!-- DA COMPILARE --> | |
|
||||
|
||||
## 4. Cancellazioni
|
||||
|
||||
| Entità | Cancellazione | Effetto sui figli |
|
||||
|---|---|---|
|
||||
| <!-- DA COMPILARE: fisica, logica (soft delete), vietata --> | | |
|
||||
|
||||
## 5. Invarianti del modello
|
||||
|
||||
| # | Invariante |
|
||||
|:-:|---|
|
||||
| M1 | <!-- DA COMPILARE --> |
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# DOMAIN WORKFLOW — Stati, transizioni, invarianti
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
<!-- Se il dominio non ha un ciclo di vita con stati, scrivere «non applicabile» e il perché. -->
|
||||
|
||||
---
|
||||
|
||||
## 1. Stati
|
||||
|
||||
| Stato | Significato | Terminale |
|
||||
|---|---|:-:|
|
||||
| <!-- DA COMPILARE --> | | |
|
||||
|
||||
## 2. Macchina a stati
|
||||
|
||||
```
|
||||
<!-- DA COMPILARE: diagramma ASCII degli stati -->
|
||||
```
|
||||
|
||||
## 3. Transizioni ammesse
|
||||
|
||||
Qualsiasi transizione non elencata qui è **rifiutata**.
|
||||
|
||||
| # | Da | A | Attore | Condizione | Effetti collaterali |
|
||||
|:-:|---|---|---|---|---|
|
||||
| T1 | | | | | |
|
||||
|
||||
## 4. Invarianti
|
||||
|
||||
Condizioni che devono essere vere **sempre**, verificate da test.
|
||||
|
||||
| # | Invariante |
|
||||
|:-:|---|
|
||||
| I1 | <!-- DA COMPILARE --> |
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
# SECURITY — Modello di sicurezza
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
---
|
||||
|
||||
## 1. Perimetro
|
||||
|
||||
<!-- DA COMPILARE: cosa è esposto a Internet, cosa resta interno. -->
|
||||
|
||||
## 2. Autenticazione
|
||||
|
||||
<!-- DA COMPILARE: meccanismo, durata dei token, rotte pubbliche (elenco chiuso: aggiungerne una richiede una D-xxx). -->
|
||||
|
||||
## 3. Autorizzazione
|
||||
|
||||
| Ruolo | Può | Non può |
|
||||
|---|---|---|
|
||||
| <!-- DA COMPILARE --> | | |
|
||||
|
||||
Regola: nessuno crea un utente più potente di sé.
|
||||
|
||||
## 4. Segreti
|
||||
|
||||
- I segreti non entrano mai nel repository, nei log o nella documentazione.
|
||||
- <!-- DA COMPILARE: dove vivono (variabili d'ambiente, secret manager, cifrati a riposo). -->
|
||||
|
||||
## 5. Dati in ingresso e in uscita
|
||||
|
||||
<!-- DA COMPILARE: validazione, dimensioni massime, dati personali, cosa esce verso terzi. -->
|
||||
|
||||
## 6. Audit
|
||||
|
||||
<!-- DA COMPILARE: quali eventi si registrano, dove, per quanto tempo. -->
|
||||
|
||||
## 7. Minacce e contromisure
|
||||
|
||||
| Minaccia | Contromisura | Verificata da |
|
||||
|---|---|---|
|
||||
| <!-- DA COMPILARE --> | | test / `REQ-…` |
|
||||
|
||||
## 8. Cosa resta aperto
|
||||
|
||||
<!-- DA COMPILARE -->
|
||||
@@ -0,0 +1,59 @@
|
||||
# API — Catalogo e convenzioni
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
Il contratto è [`openapi.yaml`](openapi.yaml), **fonte di verità**: in caso di differenza fra
|
||||
questo documento, il codice e il contratto, vale il contratto. Si scrive il contratto **prima**
|
||||
del codice (contract-first).
|
||||
|
||||
---
|
||||
|
||||
## 1. Convenzioni
|
||||
|
||||
| Aspetto | Regola |
|
||||
|---|---|
|
||||
| Prefisso | `/api/v1` |
|
||||
| Formato | JSON, UTF-8, date ISO 8601 in UTC |
|
||||
| Nomi | <!-- DA COMPILARE: snake_case o camelCase, una sola scelta --> |
|
||||
| Autenticazione | <!-- DA COMPILARE --> |
|
||||
|
||||
## 2. Errori
|
||||
|
||||
Forma unica per tutti gli errori:
|
||||
|
||||
```json
|
||||
{ "error": { "code": "stringa_stabile", "message": "testo leggibile", "details": {} } }
|
||||
```
|
||||
|
||||
| Codice HTTP | Quando |
|
||||
|:-:|---|
|
||||
| 400 | Richiesta malformata |
|
||||
| 401 | Non autenticato |
|
||||
| 403 | Autenticato ma non autorizzato |
|
||||
| 404 | Risorsa inesistente **o di un altro tenant** (non si rivela l'esistenza) |
|
||||
| 409 | Conflitto (duplicato, stato non compatibile) |
|
||||
| 422 | Validazione fallita |
|
||||
|
||||
## 3. Paginazione
|
||||
|
||||
<!-- DA COMPILARE: parametri (page/size o cursore), limiti massimi, forma della risposta. -->
|
||||
|
||||
## 4. Catalogo
|
||||
|
||||
### <!-- Gruppo --> — `SPEC-…`
|
||||
|
||||
| Metodo | Percorso | Ruolo minimo | Descrizione |
|
||||
|---|---|---|---|
|
||||
| <!-- DA COMPILARE --> | | | |
|
||||
|
||||
## 5. API interne — non esposte
|
||||
|
||||
<!-- DA COMPILARE o «nessuna». -->
|
||||
|
||||
## 6. Idempotenza e concorrenza
|
||||
|
||||
<!-- DA COMPILARE: chiavi di idempotenza, controllo delle versioni, comportamento in caso di doppio invio. -->
|
||||
|
||||
## 7. Limiti
|
||||
|
||||
<!-- DA COMPILARE: rate limit, dimensioni massime dei corpi e dei file. -->
|
||||
@@ -0,0 +1,54 @@
|
||||
# Contratto pubblico di {{PROGETTO}} — FONTE DI VERITÀ (D-002).
|
||||
# Si modifica PRIMA del codice. Ogni operazione cita la spec che la definisce.
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: "{{PROGETTO}} API"
|
||||
version: 0.1.0
|
||||
description: |
|
||||
Contratto dell'API di {{PROGETTO}}.
|
||||
Documentazione redatta con il metodo spec-driven di STS Lab Srl.
|
||||
servers:
|
||||
- url: /api/v1
|
||||
tags:
|
||||
- name: health
|
||||
description: Stato del servizio
|
||||
paths:
|
||||
/health:
|
||||
get:
|
||||
tags: [health]
|
||||
summary: Stato del servizio
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
description: Servizio attivo
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
example: ok
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
schemas:
|
||||
Error:
|
||||
type: object
|
||||
required: [error]
|
||||
properties:
|
||||
error:
|
||||
type: object
|
||||
required: [code, message]
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
details:
|
||||
type: object
|
||||
security:
|
||||
- BearerAuth: []
|
||||
@@ -0,0 +1,47 @@
|
||||
# DATABASE — Schema fisico
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
Traduzione fisica di [`../02-architecture/DATA_MODEL.md`](../02-architecture/DATA_MODEL.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. Convenzioni
|
||||
|
||||
| Aspetto | Regola |
|
||||
|---|---|
|
||||
| Motore | <!-- DA COMPILARE: es. PostgreSQL 16 --> |
|
||||
| Nomi | tabelle al singolare, `snake_case` |
|
||||
| Chiavi primarie | <!-- DA COMPILARE: UUID / bigint --> |
|
||||
| Date | `timestamptz`, sempre UTC |
|
||||
| Multi-tenancy | <!-- DA COMPILARE: colonna `tenant_id` su ogni tabella di dominio, o «non applicabile» --> |
|
||||
|
||||
## 2. Tipi enumerati
|
||||
|
||||
| Tipo | Valori |
|
||||
|---|---|
|
||||
| <!-- DA COMPILARE --> | |
|
||||
|
||||
## 3. Tabelle
|
||||
|
||||
### `<!-- tabella -->`
|
||||
|
||||
| Colonna | Tipo | Null | Default | Note |
|
||||
|---|---|:-:|---|---|
|
||||
| `id` | | no | | PK |
|
||||
|
||||
Indici: <!-- DA COMPILARE --> · Vincoli: <!-- DA COMPILARE -->
|
||||
|
||||
## 4. Vincoli che riflettono requisiti
|
||||
|
||||
| Vincolo | Requisito |
|
||||
|---|---|
|
||||
| <!-- DA COMPILARE --> | `REQ-…` |
|
||||
|
||||
## 5. Migrazioni
|
||||
|
||||
<!-- DA COMPILARE: strumento (es. Alembic), regola «una migrazione per modifica, mai riscrivere una migrazione già applicata». -->
|
||||
|
||||
## 6. Dimensionamento
|
||||
|
||||
<!-- DA COMPILARE: volumi attesi per tabella a 1 e 3 anni. -->
|
||||
@@ -0,0 +1,30 @@
|
||||
# SEED E BOOTSTRAP — Stato iniziale del sistema
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
---
|
||||
|
||||
## 1. Principio
|
||||
|
||||
Il bootstrap è **idempotente**: eseguirlo due volte produce lo stesso stato di eseguirlo una.
|
||||
|
||||
## 2. Cosa viene creato
|
||||
|
||||
| Elemento | Quando | Origine dei valori |
|
||||
|---|---|---|
|
||||
| <!-- DA COMPILARE: utente amministratore iniziale, configurazione di base --> | primo avvio | variabili d'ambiente |
|
||||
|
||||
## 3. Credenziali iniziali
|
||||
|
||||
In collaudo e produzione l'avvio **fallisce** se le credenziali di bootstrap sono lasciate ai
|
||||
valori predefiniti.
|
||||
|
||||
## 4. Variabili di bootstrap
|
||||
|
||||
| Variabile | Obbligatoria | Significato |
|
||||
|---|:-:|---|
|
||||
| <!-- DA COMPILARE --> | | |
|
||||
|
||||
## 5. Dati di prova per lo sviluppo
|
||||
|
||||
<!-- DA COMPILARE: come si generano, e la regola che non arrivano mai in produzione. -->
|
||||
@@ -0,0 +1,34 @@
|
||||
# BACKUP E RIPRISTINO
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
---
|
||||
|
||||
## 1. Cosa va salvato
|
||||
|
||||
| Dato | Dove vive | Ricostruibile | Backup |
|
||||
|---|---|:-:|---|
|
||||
| <!-- DA COMPILARE --> | | | |
|
||||
|
||||
## 2. Obiettivi
|
||||
|
||||
| Obiettivo | Valore |
|
||||
|---|---|
|
||||
| RPO (dati che si accetta di perdere) | <!-- DA COMPILARE --> |
|
||||
| RTO (tempo per tornare operativi) | <!-- DA COMPILARE --> |
|
||||
|
||||
## 3. Procedura di ripristino
|
||||
|
||||
1. <!-- DA COMPILARE -->
|
||||
|
||||
## 4. Verifiche dopo un ripristino
|
||||
|
||||
- [ ] <!-- DA COMPILARE -->
|
||||
|
||||
## 5. Prova di ripristino
|
||||
|
||||
Un backup mai ripristinato non è un backup. Prova <!-- DA COMPILARE: frequenza -->, esito registrato in `DEVELOPMENT_LOG`.
|
||||
|
||||
## 6. Cosa non è coperto
|
||||
|
||||
<!-- DA COMPILARE -->
|
||||
@@ -0,0 +1,31 @@
|
||||
# CI/CD — Integrazione e rilascio
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
---
|
||||
|
||||
## 1. Flussi
|
||||
|
||||
| Flusso | Innesco | Ambiente di destinazione |
|
||||
|---|---|---|
|
||||
| <!-- DA COMPILARE --> | | |
|
||||
|
||||
## 2. Sequenza
|
||||
|
||||
1. <!-- DA COMPILARE: build, test, analisi, immagini, deploy -->
|
||||
|
||||
## 3. Verifiche in pipeline
|
||||
|
||||
| Verifica | Perché | Blocca il rilascio |
|
||||
|---|---|:-:|
|
||||
| Test automatici | criteri di accettazione delle spec | sì |
|
||||
| Ricerca di segreti | nessun segreto nel repository | sì |
|
||||
| Contratto contro implementazione | il codice rispetta `openapi.yaml` | sì |
|
||||
|
||||
## 4. Segreti della pipeline
|
||||
|
||||
<!-- DA COMPILARE -->
|
||||
|
||||
## 5. Rilascio in produzione e rientro
|
||||
|
||||
<!-- DA COMPILARE: chi approva, come si torna alla versione precedente, in quanto tempo. -->
|
||||
@@ -0,0 +1,31 @@
|
||||
# ENVIRONMENTS — Sviluppo, collaudo, produzione
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
---
|
||||
|
||||
## 1. Gli ambienti
|
||||
|
||||
| Ambiente | Scopo | Dati | Chi vi accede |
|
||||
|---|---|---|---|
|
||||
| dev | sviluppo | di prova | sviluppatori |
|
||||
| qa | collaudo | di prova, realistici | sviluppatori, committente |
|
||||
| prod | esercizio | reali | utenti |
|
||||
|
||||
## 2. Variabili di configurazione
|
||||
|
||||
| Variabile | Significato | dev | qa | prod | Segreta |
|
||||
|---|---|---|---|---|:-:|
|
||||
| <!-- DA COMPILARE --> | | | | | |
|
||||
|
||||
## 3. Ambiente locale di sviluppo
|
||||
|
||||
<!-- DA COMPILARE: comandi per avviare tutto in locale (es. docker compose up). -->
|
||||
|
||||
## 4. Promozione fra ambienti
|
||||
|
||||
<!-- DA COMPILARE: dev → qa → prod, chi approva, cosa si verifica a ogni passo. -->
|
||||
|
||||
## 5. Prima di dichiarare un ambiente pronto
|
||||
|
||||
- [ ] <!-- DA COMPILARE -->
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# INFRASTRUCTURE — Topologia e risorse
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
---
|
||||
|
||||
## 1. Topologia
|
||||
|
||||
```
|
||||
<!-- DA COMPILARE: nodi, servizi, rete -->
|
||||
```
|
||||
|
||||
## 2. Servizi e risorse
|
||||
|
||||
| Servizio | Immagine / runtime | Repliche | CPU | Memoria |
|
||||
|---|---|:-:|---|---|
|
||||
| <!-- DA COMPILARE --> | | | | |
|
||||
|
||||
## 3. Persistenza
|
||||
|
||||
| Volume / storage | Contenuto | Dimensione | Backup |
|
||||
|---|---|---|:-:|
|
||||
| <!-- DA COMPILARE --> | | | |
|
||||
|
||||
## 4. Rete, nomi e certificati
|
||||
|
||||
<!-- DA COMPILARE: domini, ingress, TLS, porte esposte. -->
|
||||
|
||||
## 5. Capacità e crescita
|
||||
|
||||
<!-- DA COMPILARE -->
|
||||
@@ -0,0 +1,37 @@
|
||||
# OBSERVABILITY — Log, metriche, allarmi
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
---
|
||||
|
||||
## 1. Il problema che risolve
|
||||
|
||||
<!-- DA COMPILARE: quali domande operative si devono poter rispondere senza aprire il database. -->
|
||||
|
||||
## 2. Log
|
||||
|
||||
| Aspetto | Regola |
|
||||
|---|---|
|
||||
| Formato | JSON strutturato |
|
||||
| Correlazione | un identificativo di richiesta propagato fra i servizi |
|
||||
| Mai nei log | password, token, segreti, dati personali non necessari |
|
||||
|
||||
## 3. Metriche
|
||||
|
||||
| Metrica | Tipo | Significato |
|
||||
|---|---|---|
|
||||
| <!-- DA COMPILARE --> | | |
|
||||
|
||||
## 4. Allarmi
|
||||
|
||||
| Allarme | Condizione | Gravità | Procedura |
|
||||
|---|---|:-:|---|
|
||||
| <!-- DA COMPILARE --> | | | [`RUNBOOK`](RUNBOOK.md) §… |
|
||||
|
||||
## 5. Salute
|
||||
|
||||
`GET /health` — <!-- DA COMPILARE: cosa verifica. -->
|
||||
|
||||
## 6. Conservazione
|
||||
|
||||
<!-- DA COMPILARE -->
|
||||
@@ -0,0 +1,25 @@
|
||||
# RUNBOOK — Procedure operative
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
Ogni procedura segue lo stesso schema: **sintomo → verifica → azione → conferma**.
|
||||
|
||||
---
|
||||
|
||||
## 1. <!-- Sintomo, come lo descriverebbe chi lo nota -->
|
||||
|
||||
| Passo | Cosa fare |
|
||||
|---|---|
|
||||
| **Verifica** | <!-- comando o schermata --> |
|
||||
| **Azione** | |
|
||||
| **Conferma** | |
|
||||
|
||||
## 2. Ripristino da backup
|
||||
|
||||
Vedi [`BACKUP_DR.md`](BACKUP_DR.md) §… .
|
||||
|
||||
## 3. Contatti e riferimenti
|
||||
|
||||
| Ruolo | Contatto |
|
||||
|---|---|
|
||||
| <!-- DA COMPILARE --> | |
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# ACCEPTANCE CRITERIA — Criteri di rilascio
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
---
|
||||
|
||||
## 1. Riepilogo per specifica
|
||||
|
||||
| Spec | Criteri | Verificati | Stato |
|
||||
|---|:-:|:-:|:-:|
|
||||
| <!-- SPEC-001 --> | | | ⬜ |
|
||||
|
||||
## 2. Criteri di rilascio — versione <!-- x.y.z -->
|
||||
|
||||
### 2.1 Bloccanti
|
||||
|
||||
| # | Criterio | Verificato da |
|
||||
|:-:|---|---|
|
||||
| R1 | Tutti i test passano in pipeline | CI |
|
||||
| R2 | Nessuna riga scoperta in `TRACEABILITY.md` | revisione |
|
||||
| R3 | <!-- DA COMPILARE --> | |
|
||||
|
||||
### 2.2 Attesi (non bloccanti)
|
||||
|
||||
| # | Criterio |
|
||||
|:-:|---|
|
||||
| <!-- DA COMPILARE --> | |
|
||||
|
||||
## 3. Verifica funzionale prima del rilascio
|
||||
|
||||
- [ ] <!-- DA COMPILARE: percorsi da provare a mano con il committente -->
|
||||
|
||||
## 4. Cosa non è criterio di rilascio
|
||||
|
||||
<!-- DA COMPILARE -->
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# TEST STRATEGY
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
---
|
||||
|
||||
## 1. Il criterio
|
||||
|
||||
Ogni criterio di accettazione di ogni spec ha **almeno un test eseguito**. Lo stato di un
|
||||
requisito in [`TRACEABILITY.md`](TRACEABILITY.md) diventa ✅ quando il test passa, non quando
|
||||
il codice è scritto. Ogni difetto `DIF-nn` ha un test che lo riproduce.
|
||||
|
||||
## 2. Livelli
|
||||
|
||||
| Livello | Cosa verifica | Strumento | Dove |
|
||||
|---|---|---|---|
|
||||
| Unitario | funzioni pure, regole di business | <!-- DA COMPILARE --> | |
|
||||
| Integrazione | servizi + database + code, con simulatori dei sistemi esterni | | |
|
||||
| Contratto | implementazione contro `openapi.yaml` | | |
|
||||
| End-to-end | casi d'uso dall'interfaccia | | |
|
||||
|
||||
## 3. Simulatori dei sistemi esterni
|
||||
|
||||
| Sistema | Simulatore | Comportamenti simulati (anche i guasti) |
|
||||
|---|---|---|
|
||||
| <!-- DA COMPILARE --> | | |
|
||||
|
||||
## 4. Nominare i test
|
||||
|
||||
Il nome del test cita il criterio: `test_D3_sufficiente_urgente_ricorrente_lotto_grande_priorita_1`.
|
||||
|
||||
## 5. Esecuzione
|
||||
|
||||
<!-- DA COMPILARE: comandi, durata attesa, numero di test (aggiornato in DEVELOPMENT_LOG). -->
|
||||
|
||||
## 6. Cosa non verifichiamo
|
||||
|
||||
<!-- DA COMPILARE, con il perché. -->
|
||||
@@ -0,0 +1,48 @@
|
||||
# TRACEABILITY — Matrice REQ → SPEC → T → test
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}** · Stato: **in stesura**
|
||||
|
||||
---
|
||||
|
||||
## 1. Regola
|
||||
|
||||
> **Nessuna riga vuota.** Ogni requisito ha una specifica, ogni specifica ha task, ogni task ha
|
||||
> test. Un requisito senza spec non è implementabile; un task senza spec non entra in lavorazione.
|
||||
|
||||
La colonna *Stato* si compila durante l'implementazione: ⬜ da fare · 🟡 in corso · ✅ verificato.
|
||||
Lo stato si aggiorna **quando il test passa, non quando il codice è scritto**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Copertura
|
||||
|
||||
| Area | REQ | Spec | Task | Criteri | Copertura |
|
||||
|---|:-:|---|---|:-:|:-:|
|
||||
| <!-- DA COMPILARE --> | | | | | ⬜ |
|
||||
| **Totale** | | | | | |
|
||||
|
||||
---
|
||||
|
||||
## 3. Matrice per requisito
|
||||
|
||||
### <!-- AREA --> · <!-- Nome -->
|
||||
|
||||
| REQ | Spec | Task | Criteri | Stato |
|
||||
|---|---|---|---|:-:|
|
||||
| <!-- AREA-01 --> | <!-- SPEC-001 §2 --> | <!-- T-010 --> | <!-- A1 --> | ⬜ |
|
||||
|
||||
---
|
||||
|
||||
## 4. Decisioni → specifiche
|
||||
|
||||
| Decisione | Dove si applica | Task |
|
||||
|---|---|---|
|
||||
| D-002 | metodo | — |
|
||||
|
||||
---
|
||||
|
||||
## 5. Difetti → test
|
||||
|
||||
| Difetto | Requisito | Test |
|
||||
|---|---|---|
|
||||
| <!-- DIF-01 --> | | |
|
||||
@@ -0,0 +1,14 @@
|
||||
# CHANGELOG
|
||||
|
||||
Formato: [Keep a Changelog](https://keepachangelog.com/it/1.1.0/) · Versionamento semantico.
|
||||
|
||||
Le voci descrivono ciò che cambia **per chi usa il prodotto**. Le decisioni tecniche stanno in
|
||||
[`DECISIONS.md`](DECISIONS.md), il diario di lavoro in [`DEVELOPMENT_LOG.md`](DEVELOPMENT_LOG.md).
|
||||
|
||||
---
|
||||
|
||||
## [Non rilasciato] — 0.1.0
|
||||
|
||||
### Aggiunto
|
||||
|
||||
- Documentazione di progetto con metodo spec-driven ({{DATA}}).
|
||||
@@ -0,0 +1,83 @@
|
||||
# DECISIONS — Registro delle decisioni architetturali (ADR)
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}** · Progetto: **{{PROGETTO}}**
|
||||
|
||||
Ogni decisione strutturale del progetto è registrata qui con un identificativo `D-xxx` immutabile.
|
||||
Le decisioni si **superano**, non si cancellano: una decisione sbagliata resta a registro con stato
|
||||
*Superata da D-yyy*, così la storia del progetto resta leggibile.
|
||||
|
||||
**Stati:** `Proposta` · `Accettata` · `Superata da D-xxx` · `Respinta`
|
||||
|
||||
**Formato:** Contesto → Decisione → Alternative valutate → Conseguenze.
|
||||
|
||||
---
|
||||
|
||||
## Indice
|
||||
|
||||
| ID | Titolo | Stato | Data |
|
||||
|---|---|---|---|
|
||||
| [D-001](#d-001--stack-tecnologico) | Stack tecnologico | Proposta | {{DATA}} |
|
||||
| [D-002](#d-002--sviluppo-spec-driven-vincolante) | Sviluppo spec-driven vincolante | Accettata | {{DATA}} |
|
||||
| [D-003](#d-003--la-documentazione-vive-nel-repository) | La documentazione vive nel repository | Accettata | {{DATA}} |
|
||||
|
||||
---
|
||||
|
||||
## D-001 — Stack tecnologico
|
||||
|
||||
**Stato:** Proposta · {{DATA}}
|
||||
|
||||
### Contesto
|
||||
<!-- DA COMPILARE: vincoli del committente, competenze, sistemi esistenti con cui integrarsi. -->
|
||||
|
||||
### Decisione
|
||||
<!-- DA COMPILARE: linguaggi, framework, database, code, storage, deploy. -->
|
||||
|
||||
### Alternative valutate
|
||||
- <!-- DA COMPILARE: alternativa — perché scartata. -->
|
||||
|
||||
### Conseguenze
|
||||
- <!-- DA COMPILARE -->
|
||||
|
||||
---
|
||||
|
||||
## D-002 — Sviluppo spec-driven vincolante
|
||||
|
||||
**Stato:** Accettata · {{DATA}}
|
||||
|
||||
### Contesto
|
||||
Le regole di business scritte solo nel codice emergono dal debugging invece che dalla
|
||||
specifica, e si scoprono quando un difetto le rivela.
|
||||
|
||||
### Decisione
|
||||
Nessuna implementazione senza una `SPEC-xxx` in stato *Approvata*. Ogni spec contiene: contesto,
|
||||
requisiti coperti (`REQ-xxx`), contratto API, modello dati, regole, casi di errore, criteri di
|
||||
accettazione, task derivati (`T-xxx`). `07-quality/TRACEABILITY.md` mantiene la matrice
|
||||
REQ → SPEC → T → test e non deve avere righe vuote. Il contratto API (`04-api/openapi.yaml`) è
|
||||
la fonte di verità e si scrive prima del codice.
|
||||
|
||||
### Alternative valutate
|
||||
- **Documentazione a valle del codice.** Produce documenti veri ma tardivi; le regole restano
|
||||
scoperte finché un bug non le rivela.
|
||||
|
||||
### Conseguenze
|
||||
- La fase di documentazione precede quella di codice; il codice inizia più tardi e procede più dritto.
|
||||
- Una spec approvata si modifica solo con una nuova `D-xxx` o una revisione tracciata.
|
||||
|
||||
---
|
||||
|
||||
## D-003 — La documentazione vive nel repository
|
||||
|
||||
**Stato:** Accettata · {{DATA}}
|
||||
|
||||
### Contesto
|
||||
Documentazione e codice divergono quando vivono in luoghi diversi.
|
||||
|
||||
### Decisione
|
||||
`docs/` sta nel repository del codice e si aggiorna **nella stessa modifica** del codice che la
|
||||
riguarda.
|
||||
|
||||
### Alternative valutate
|
||||
- **Wiki esterno.** Più comodo da modificare, ma non versionato con il codice.
|
||||
|
||||
### Conseguenze
|
||||
- Una revisione del codice è anche revisione della documentazione.
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# DEVELOPMENT LOG — Diario tecnico
|
||||
|
||||
Diario in ordine cronologico inverso. Registra **cosa si è scoperto e perché si è deciso così**,
|
||||
non l'elenco delle modifiche (quello è in [`CHANGELOG.md`](CHANGELOG.md)). Le voci non si
|
||||
cancellano: si annotano.
|
||||
|
||||
Formato del titolo: `## AAAA-MM-GG (n) · titolo che dice cosa si è capito`
|
||||
|
||||
---
|
||||
|
||||
## {{DATA}} · Nasce la documentazione
|
||||
|
||||
Creato lo scheletro della documentazione spec-driven. Decisioni iniziali: `D-001` (stack,
|
||||
proposta), `D-002` (metodo spec-driven), `D-003` (documentazione nel repository).
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
# PROJECT MEMORY — Memoria permanente
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
Da leggere **all'inizio di ogni sessione di lavoro** sul progetto. Contiene ciò che non è
|
||||
deducibile dal codice e che, se dimenticato, fa ripetere errori già commessi. Non è un
|
||||
riassunto della documentazione: ogni voce qui deve avere un perché.
|
||||
|
||||
---
|
||||
|
||||
## 1. In una frase
|
||||
|
||||
<!-- DA COMPILARE -->
|
||||
|
||||
---
|
||||
|
||||
## 2. Le cose da non dimenticare mai
|
||||
|
||||
<!-- Una sottosezione per vincolo. Ogni vincolo dice: la regola, perché esiste (meglio se con
|
||||
un fatto osservato e la data), la decisione D-xxx che la fissa. -->
|
||||
|
||||
### 2.1 <!-- vincolo -->
|
||||
|
||||
---
|
||||
|
||||
## 3. Azioni pendenti fuori sequenza
|
||||
|
||||
<!-- DA COMPILARE o «nessuna». Es. credenziali da ruotare. -->
|
||||
|
||||
---
|
||||
|
||||
## 4. Metodo
|
||||
|
||||
Spec-driven **vincolante** (D-002): `REQ → SPEC → T → codice + test → TRACEABILITY`.
|
||||
Si implementa solo da una spec *Approvata*.
|
||||
|
||||
---
|
||||
|
||||
## 5. Come riprendere il lavoro
|
||||
|
||||
1. Leggere questo file e [`../00-INDEX.md`](../00-INDEX.md) (riquadri delle novità in testa).
|
||||
2. Aprire [`TODO.md`](TODO.md): fuori sequenza, poi la fase corrente.
|
||||
3. Per il task scelto: la sua spec, i `REQ` e le `D-xxx` citate.
|
||||
@@ -0,0 +1,45 @@
|
||||
# ROADMAP
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
---
|
||||
|
||||
## 0. Fuori sequenza — da fare subito
|
||||
|
||||
| # | Azione | Perché ora |
|
||||
|:-:|---|---|
|
||||
| <!-- DA COMPILARE o «nessuna» --> | | |
|
||||
|
||||
## 1. Fasi
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ FASE 0 · Documentazione │
|
||||
│ visione · requisiti · architettura · spec · contratto · dati │
|
||||
│ ▸ criterio di uscita: spec della fase 1 approvate │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ FASE 1 · Fondamenta │
|
||||
│ <!-- DA COMPILARE --> │
|
||||
│ ▸ criterio di uscita: <!-- DA COMPILARE --> │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 2. Dettaglio
|
||||
|
||||
### Fase 1 · Fondamenta
|
||||
|
||||
| Spec | Task | Criterio di uscita |
|
||||
|---|---|---|
|
||||
| <!-- SPEC-001 --> | <!-- T-010…019 --> | |
|
||||
|
||||
## 3. Dipendenze critiche
|
||||
|
||||
<!-- DA COMPILARE -->
|
||||
|
||||
## 4. Rischi
|
||||
|
||||
| Rischio | Fase | Contromisura |
|
||||
|---|---|---|
|
||||
| <!-- DA COMPILARE --> | | |
|
||||
@@ -0,0 +1,39 @@
|
||||
# TODO — Backlog
|
||||
|
||||
Ultimo aggiornamento: **{{DATA}}**
|
||||
|
||||
Stato: ⬜ da fare · 🟡 in corso · ✅ fatto · ⏸ sospeso
|
||||
|
||||
**Regola:** un task entra in lavorazione solo se la sua specifica è approvata (D-002). Al termine
|
||||
si aggiorna [`TRACEABILITY.md`](../07-quality/TRACEABILITY.md) — quando il test passa, non quando
|
||||
il codice è scritto.
|
||||
|
||||
**Numerazione:** i task di `SPEC-nnn` usano il blocco `T-nnn0…T-nnn9` (la spec 4 usa i task da
|
||||
40 a 49); oltre il nono si aggiunge un suffisso (`b`, `c`). I task fuori sequenza usano i numeri
|
||||
da 1 a 9.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Fuori sequenza — prima di tutto
|
||||
|
||||
| ID | Task | Stato |
|
||||
|---|---|:-:|
|
||||
| <!-- T-001 --> | | ⬜ |
|
||||
|
||||
---
|
||||
|
||||
## Fase 0 · Documentazione
|
||||
|
||||
| ID | Task | Spec | Stato |
|
||||
|---|---|---|:-:|
|
||||
| — | Visione, glossario, casi d'uso, requisiti | — | 🟡 |
|
||||
| — | Architettura, modello dati, sicurezza | — | ⬜ |
|
||||
| — | Specifiche della fase 1 approvate | — | ⬜ |
|
||||
|
||||
---
|
||||
|
||||
## Fase 1 · Fondamenta
|
||||
|
||||
| ID | Task | Spec | Stato |
|
||||
|---|---|---|:-:|
|
||||
| <!-- T-010 --> | | <!-- SPEC-001 --> | ⬜ |
|
||||
Binary file not shown.
@@ -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 |
@@ -0,0 +1,536 @@
|
||||
BLACKBOX EXPORTER - GUIDA OPERATIVA KUBERNETES
|
||||
==============================================
|
||||
|
||||
Stack di partenza
|
||||
-----------------
|
||||
Nel cluster e' gia' installato kube-prometheus-stack nel namespace monitoring:
|
||||
|
||||
```text
|
||||
helm list -A | grep prometheus
|
||||
|
||||
kube-prometheus-stack monitoring 4 2026-08-08 23:55:14.193903052 +0000 UTC deployed kube-prometheus-stack-88.1.5
|
||||
```
|
||||
|
||||
Obiettivo
|
||||
---------
|
||||
Installare prometheus-blackbox-exporter per eseguire test HTTP/HTTPS su servizi Kubernetes interni, Ingress o URL esterni, esportando le metriche verso Prometheus tramite Prometheus Operator.
|
||||
|
||||
Metriche principali:
|
||||
|
||||
```text
|
||||
probe_success
|
||||
probe_duration_seconds
|
||||
probe_http_status_code
|
||||
probe_http_ssl
|
||||
probe_dns_lookup_time_seconds
|
||||
```
|
||||
|
||||
Legenda
|
||||
-------
|
||||
- COMANDO: istruzione da eseguire nel terminale.
|
||||
- FILE: file da creare o aggiornare.
|
||||
- NOTA: informazione da verificare prima di procedere.
|
||||
- OUTPUT ATTESO: risultato indicativo del comando.
|
||||
|
||||
Prerequisiti
|
||||
------------
|
||||
- Accesso kubectl al cluster.
|
||||
- Helm installato.
|
||||
- Namespace monitoring presente o creabile.
|
||||
- kube-prometheus-stack installato con Prometheus Operator.
|
||||
- CRD Probe disponibile: probes.monitoring.coreos.com.
|
||||
|
||||
|
||||
1. VERIFICA PROMETHEUS E PROMETHEUS OPERATOR
|
||||
============================================
|
||||
|
||||
COMANDO - verifica release Helm Prometheus
|
||||
```bash
|
||||
helm list -A | grep prometheus
|
||||
```
|
||||
|
||||
COMANDO - verifica pod Prometheus
|
||||
```bash
|
||||
kubectl get pods -A | grep prometheus
|
||||
```
|
||||
|
||||
COMANDO - verifica CRD Probe
|
||||
```bash
|
||||
kubectl get crd probes.monitoring.coreos.com
|
||||
```
|
||||
|
||||
COMANDO - verifica label delle risorse Prometheus
|
||||
```bash
|
||||
kubectl get prometheus -A --show-labels
|
||||
```
|
||||
|
||||
COMANDO - controlla selector della risorsa Prometheus
|
||||
```bash
|
||||
kubectl get prometheus -n monitoring -o yaml
|
||||
```
|
||||
|
||||
NOTA - label dei manifest
|
||||
Dal comando `kubectl get prometheus -A --show-labels` risulta che Prometheus seleziona le risorse con la label `release=kube-prometheus-stack`.
|
||||
Negli esempi sotto viene quindi usata questa label:
|
||||
|
||||
```yaml
|
||||
release: kube-prometheus-stack
|
||||
```
|
||||
|
||||
Se in futuro il tuo Prometheus usa un selector diverso, sostituisci questa label nei manifest Probe e PrometheusRule.
|
||||
Controlla soprattutto questi campi nella risorsa Prometheus:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
probeSelector:
|
||||
probeNamespaceSelector:
|
||||
ruleSelector:
|
||||
ruleNamespaceSelector:
|
||||
```
|
||||
|
||||
|
||||
2. AGGIUNTA REPOSITORY HELM
|
||||
===========================
|
||||
|
||||
COMANDO - aggiungi repository prometheus-community
|
||||
```bash
|
||||
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
|
||||
```
|
||||
|
||||
COMANDO - aggiorna indice chart Helm
|
||||
```bash
|
||||
helm repo update
|
||||
```
|
||||
|
||||
|
||||
3. CREA FILE VALUES HELM
|
||||
========================
|
||||
|
||||
FILE - blackbox-values.yaml
|
||||
|
||||
Descrizione:
|
||||
- configura i moduli HTTP usati da blackbox-exporter;
|
||||
- espone blackbox-exporter come Service ClusterIP sulla porta 9115;
|
||||
- lascia disabilitato ServiceMonitor perche' lo scraping dei target viene configurato tramite Probe.
|
||||
|
||||
COMANDO - crea blackbox-values.yaml
|
||||
```bash
|
||||
cat > blackbox-values.yaml <<'EOF'
|
||||
fullnameOverride: blackbox-exporter
|
||||
|
||||
config:
|
||||
modules:
|
||||
http_2xx:
|
||||
prober: http
|
||||
timeout: 10s
|
||||
http:
|
||||
method: GET
|
||||
preferred_ip_protocol: ip4
|
||||
valid_http_versions:
|
||||
- HTTP/1.1
|
||||
- HTTP/2.0
|
||||
valid_status_codes:
|
||||
- 200
|
||||
- 204
|
||||
- 301
|
||||
- 302
|
||||
|
||||
http_post_2xx:
|
||||
prober: http
|
||||
timeout: 10s
|
||||
http:
|
||||
method: POST
|
||||
preferred_ip_protocol: ip4
|
||||
valid_status_codes:
|
||||
- 200
|
||||
- 201
|
||||
- 202
|
||||
- 204
|
||||
|
||||
http_k8s_health:
|
||||
prober: http
|
||||
timeout: 10s
|
||||
http:
|
||||
method: GET
|
||||
preferred_ip_protocol: ip4
|
||||
fail_if_ssl: false
|
||||
fail_if_not_ssl: false
|
||||
valid_status_codes:
|
||||
- 200
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 9115
|
||||
|
||||
serviceMonitor:
|
||||
enabled: false
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
EOF
|
||||
```
|
||||
|
||||
|
||||
4. INSTALLA BLACKBOX EXPORTER
|
||||
=============================
|
||||
|
||||
COMANDO - installa o aggiorna blackbox-exporter
|
||||
```bash
|
||||
helm upgrade --install blackbox-exporter prometheus-community/prometheus-blackbox-exporter \
|
||||
--namespace monitoring \
|
||||
--create-namespace \
|
||||
-f blackbox-values.yaml
|
||||
```
|
||||
|
||||
COMANDO - verifica pod blackbox-exporter
|
||||
```bash
|
||||
kubectl get pods -n monitoring | grep blackbox
|
||||
```
|
||||
|
||||
COMANDO - verifica Service blackbox-exporter
|
||||
```bash
|
||||
kubectl get svc -n monitoring | grep blackbox
|
||||
```
|
||||
|
||||
OUTPUT ATTESO
|
||||
```text
|
||||
blackbox-exporter ClusterIP <cluster-ip> <none> 9115/TCP
|
||||
```
|
||||
|
||||
|
||||
5. CREA PROBE HTTP PER SERVIZI KUBERNETES INTERNI
|
||||
=================================================
|
||||
|
||||
FILE - blackbox-probes.yaml
|
||||
|
||||
Descrizione:
|
||||
- testa endpoint HTTP interni tramite DNS Kubernetes;
|
||||
- usa il modulo http_k8s_health definito in blackbox-values.yaml;
|
||||
- invia le metriche a Prometheus tramite la risorsa Probe del Prometheus Operator.
|
||||
|
||||
Da personalizzare:
|
||||
- my-service
|
||||
- my-namespace
|
||||
- porta del servizio
|
||||
- path HTTP, ad esempio /health, /ready o /
|
||||
- label release se diversa da prometheus
|
||||
|
||||
COMANDO - crea blackbox-probes.yaml
|
||||
```bash
|
||||
cat > blackbox-probes.yaml <<'EOF'
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: Probe
|
||||
metadata:
|
||||
name: http-services-probe
|
||||
namespace: monitoring
|
||||
labels:
|
||||
release: kube-prometheus-stack
|
||||
spec:
|
||||
jobName: http-services-probe
|
||||
interval: 30s
|
||||
scrapeTimeout: 15s
|
||||
module: http_k8s_health
|
||||
prober:
|
||||
url: blackbox-exporter.monitoring.svc.cluster.local:9115
|
||||
scheme: http
|
||||
path: /probe
|
||||
targets:
|
||||
staticConfig:
|
||||
static:
|
||||
- http://frontend.idcidp-dev.svc.cluster.local
|
||||
EOF
|
||||
```
|
||||
|
||||
COMANDO - applica Probe servizi interni
|
||||
```bash
|
||||
kubectl apply -f blackbox-probes.yaml
|
||||
```
|
||||
|
||||
COMANDO - verifica Probe
|
||||
```bash
|
||||
kubectl get probe -n monitoring
|
||||
kubectl describe probe http-services-probe -n monitoring
|
||||
```
|
||||
|
||||
NOTA - campi importanti della Probe
|
||||
```yaml
|
||||
spec:
|
||||
module: http_k8s_health
|
||||
prober:
|
||||
url: blackbox-exporter.monitoring.svc.cluster.local:9115
|
||||
targets:
|
||||
staticConfig:
|
||||
static:
|
||||
- http://my-service.my-namespace.svc.cluster.local:8080/health
|
||||
```
|
||||
|
||||
|
||||
6. CREA PROBE HTTP/HTTPS PER INGRESS O URL ESTERNI
|
||||
==================================================
|
||||
|
||||
FILE - blackbox-ingress-probes.yaml
|
||||
|
||||
Descrizione:
|
||||
- testa URL esposti tramite Ingress, Gateway, reverse proxy o endpoint pubblici;
|
||||
- usa il modulo generico http_2xx.
|
||||
|
||||
COMANDO - crea blackbox-ingress-probes.yaml
|
||||
```bash
|
||||
cat > blackbox-ingress-probes.yaml <<'EOF'
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: Probe
|
||||
metadata:
|
||||
name: ingress-http-probe
|
||||
namespace: monitoring
|
||||
labels:
|
||||
release: kube-prometheus-stack
|
||||
spec:
|
||||
jobName: ingress-http-probe
|
||||
interval: 30s
|
||||
scrapeTimeout: 15s
|
||||
module: http_2xx
|
||||
prober:
|
||||
url: blackbox-exporter.monitoring.svc.cluster.local:9115
|
||||
scheme: http
|
||||
path: /probe
|
||||
targets:
|
||||
staticConfig:
|
||||
static:
|
||||
- https://app.example.com/health
|
||||
- https://api.example.com/ready
|
||||
EOF
|
||||
```
|
||||
|
||||
COMANDO - applica Probe endpoint esterni
|
||||
```bash
|
||||
kubectl apply -f blackbox-ingress-probes.yaml
|
||||
```
|
||||
|
||||
|
||||
7. VERIFICA METRICHE IN PROMETHEUS
|
||||
==================================
|
||||
|
||||
COMANDO - apri Prometheus in locale con port-forward
|
||||
```bash
|
||||
kubectl port-forward -n monitoring svc/kube-prometheus-stack-prometheus 9090:9090
|
||||
```
|
||||
|
||||
Poi apri nel browser:
|
||||
|
||||
```text
|
||||
http://localhost:9090
|
||||
```
|
||||
|
||||
QUERY PROMQL - tutte le probe
|
||||
```promql
|
||||
probe_success
|
||||
```
|
||||
|
||||
QUERY PROMQL - probe servizi interni
|
||||
```promql
|
||||
probe_success{job="http-services-probe"}
|
||||
```
|
||||
|
||||
QUERY PROMQL - status code HTTP
|
||||
```promql
|
||||
probe_http_status_code{job="http-services-probe"}
|
||||
```
|
||||
|
||||
QUERY PROMQL - durata probe
|
||||
```promql
|
||||
probe_duration_seconds{job="http-services-probe"}
|
||||
```
|
||||
|
||||
QUERY PROMQL - target falliti
|
||||
```promql
|
||||
probe_success{job="http-services-probe"} == 0
|
||||
```
|
||||
|
||||
Interpretazione:
|
||||
- probe_success = 1: test riuscito.
|
||||
- probe_success = 0: test fallito.
|
||||
- probe_http_status_code: status HTTP ricevuto.
|
||||
- probe_duration_seconds: durata della probe.
|
||||
|
||||
|
||||
8. CREA ALERT PROMETHEUSRULE
|
||||
============================
|
||||
|
||||
FILE - blackbox-http-alerts.yaml
|
||||
|
||||
Descrizione:
|
||||
- genera alert quando un target HTTP non risponde correttamente;
|
||||
- genera alert quando un target risponde troppo lentamente.
|
||||
|
||||
COMANDO - crea blackbox-http-alerts.yaml
|
||||
```bash
|
||||
cat > blackbox-http-alerts.yaml <<'EOF'
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: PrometheusRule
|
||||
metadata:
|
||||
name: blackbox-http-alerts
|
||||
namespace: monitoring
|
||||
labels:
|
||||
release: kube-prometheus-stack
|
||||
spec:
|
||||
groups:
|
||||
- name: blackbox-http
|
||||
rules:
|
||||
- alert: BlackboxHttpProbeFailed
|
||||
expr: probe_success == 0
|
||||
for: 2m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "HTTP probe fallita"
|
||||
description: "Il target {{ $labels.instance }} non risponde correttamente da almeno 2 minuti."
|
||||
|
||||
- alert: BlackboxHttpSlowResponse
|
||||
expr: probe_duration_seconds > 2
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "HTTP probe lenta"
|
||||
description: "Il target {{ $labels.instance }} risponde in piu' di 2 secondi da almeno 5 minuti."
|
||||
EOF
|
||||
```
|
||||
|
||||
COMANDO - applica alert
|
||||
```bash
|
||||
kubectl apply -f blackbox-http-alerts.yaml
|
||||
```
|
||||
|
||||
COMANDO - verifica PrometheusRule
|
||||
```bash
|
||||
kubectl get prometheusrule -n monitoring | grep blackbox
|
||||
kubectl describe prometheusrule blackbox-http-alerts -n monitoring
|
||||
```
|
||||
|
||||
|
||||
9. TEST MANUALE BLACKBOX EXPORTER
|
||||
=================================
|
||||
|
||||
COMANDO - port-forward blackbox-exporter
|
||||
```bash
|
||||
kubectl port-forward -n monitoring svc/blackbox-exporter 9115:9115
|
||||
```
|
||||
|
||||
COMANDO - esegui probe manuale da un altro terminale
|
||||
```bash
|
||||
curl "http://localhost:9115/probe?target=http://my-service.my-namespace.svc.cluster.local:8080/health&module=http_k8s_health"
|
||||
```
|
||||
|
||||
OUTPUT ATTESO - metriche indicative
|
||||
```text
|
||||
probe_success 1
|
||||
probe_http_status_code 200
|
||||
```
|
||||
|
||||
|
||||
10. TROUBLESHOOTING
|
||||
===================
|
||||
|
||||
CASO - Prometheus non vede la Probe
|
||||
|
||||
COMANDO
|
||||
```bash
|
||||
kubectl get probe -A
|
||||
kubectl describe probe -n monitoring http-services-probe
|
||||
kubectl get prometheus -n monitoring -o yaml
|
||||
```
|
||||
|
||||
Controlla:
|
||||
- spec.probeSelector
|
||||
- spec.probeNamespaceSelector
|
||||
- metadata.labels della Probe
|
||||
|
||||
CASO - blackbox-exporter non parte
|
||||
|
||||
COMANDO
|
||||
```bash
|
||||
kubectl get pods -n monitoring | grep blackbox
|
||||
kubectl logs -n monitoring deploy/blackbox-exporter
|
||||
```
|
||||
|
||||
CASO - servizio target non raggiungibile
|
||||
|
||||
COMANDO
|
||||
```bash
|
||||
kubectl get svc -n my-namespace
|
||||
kubectl get endpoints -n my-namespace my-service
|
||||
kubectl describe svc -n my-namespace my-service
|
||||
```
|
||||
|
||||
CASO - DNS Kubernetes non risolve
|
||||
|
||||
COMANDO
|
||||
```bash
|
||||
kubectl run dns-test --rm -it --image=busybox:1.36 --restart=Never -- nslookup my-service.my-namespace.svc.cluster.local
|
||||
```
|
||||
|
||||
CASO - endpoint HTTPS fallisce per certificati, redirect o header
|
||||
|
||||
Azioni consigliate:
|
||||
- usare il modulo http_2xx per endpoint esterni generici;
|
||||
- verificare se l'endpoint richiede SNI, autenticazione, header custom o path diverso;
|
||||
- aggiungere un modulo dedicato in blackbox-values.yaml se serve una configurazione HTTP specifica.
|
||||
|
||||
|
||||
11. ORDINE DI ESECUZIONE CONSIGLIATO
|
||||
====================================
|
||||
|
||||
COMANDO - sequenza completa
|
||||
```bash
|
||||
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
|
||||
helm repo update
|
||||
|
||||
helm upgrade --install blackbox-exporter prometheus-community/prometheus-blackbox-exporter \
|
||||
--namespace monitoring \
|
||||
--create-namespace \
|
||||
-f blackbox-values.yaml
|
||||
|
||||
kubectl apply -f blackbox-probes.yaml
|
||||
kubectl apply -f blackbox-ingress-probes.yaml
|
||||
kubectl apply -f blackbox-http-alerts.yaml
|
||||
```
|
||||
|
||||
FILE CONSIGLIATI DA VERSIONARE
|
||||
```text
|
||||
blackbox-values.yaml
|
||||
blackbox-probes.yaml
|
||||
blackbox-ingress-probes.yaml
|
||||
blackbox-http-alerts.yaml
|
||||
```
|
||||
|
||||
|
||||
12. NOTE PER DEV, QA E PROD
|
||||
===========================
|
||||
|
||||
Per separare gli ambienti e semplificare dashboard e alert, usa Probe distinte:
|
||||
|
||||
```text
|
||||
dev-http-services-probe
|
||||
qa-http-services-probe
|
||||
prod-http-services-probe
|
||||
```
|
||||
|
||||
QUERY PROMQL - ambiente dev
|
||||
```promql
|
||||
probe_success{job="dev-http-services-probe"}
|
||||
```
|
||||
|
||||
QUERY PROMQL - target prod falliti
|
||||
```promql
|
||||
probe_success{job="prod-http-services-probe"} == 0
|
||||
```
|
||||
|
||||
Per endpoint critici in produzione, valuta:
|
||||
- interval piu' basso, ad esempio 15s o 30s;
|
||||
- durata for degli alert tra 1m e 5m in base alla criticita';
|
||||
- dashboard Grafana con stato, status code e latenza per target.
|
||||
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 |
+36
-10
@@ -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:]')
|
||||
BUILD_PLATFORM=$(grep -E '^[[:space:]]*platform[[:space:]]*=' "$PLATFORM_CONF" | tail -n 1 | cut -d '=' -f 2- | tr -d '[:space:]' || true)
|
||||
MULTISTAGE=$(grep -E '^[[:space:]]*multistage[[:space:]]*=' "$PLATFORM_CONF" | tail -n 1 | cut -d '=' -f 2- | tr -d '[:space:]' || true)
|
||||
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 "$BUILD_PLATFORM" ]; then
|
||||
docker buildx build --platform "$BUILD_PLATFORM" -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 \
|
||||
-t "$IMAGE_TAG" -t "$IMAGE_TAG_LATEST" \
|
||||
-f "$DOCKERFILE" "$dir" --push
|
||||
fi
|
||||
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:]' || true)
|
||||
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" "${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)
|
||||
|
||||
@@ -50,11 +50,15 @@ printf '\nenv=%s\n' "$ENV" >> "$TMP_VALUES_FILE"
|
||||
printf 'namespace=%s\n' "$NAMESPACE" >> "$TMP_VALUES_FILE"
|
||||
printf 'project=%s\n' "$PROJECT_NAME" >> "$TMP_VALUES_FILE"
|
||||
|
||||
# Ricava endpoint se presente e genera endpoint-nodot sostituendo '.' con '-'
|
||||
# Ricava endpoint se presente e genera le variabili derivate per le sostituzioni YAML.
|
||||
ENDPOINT_VAL=$(grep -E '^[[:space:]]*endpoint[[:space:]]*=' "$TMP_VALUES_FILE" "$PROPERTIES_FILE" 2>/dev/null | tail -n 1 | sed -E 's/.*endpoint[[:space:]]*=[[:space:]]*//' | tr -d '\r\n[:space:]"' || true)
|
||||
if [ -n "$ENDPOINT_VAL" ]; then
|
||||
ENDPOINT_NODOT=$(echo "$ENDPOINT_VAL" | tr '.' '-')
|
||||
ENDPOINT_NOSPACE=$(echo "$ENDPOINT_VAL" | tr -d '.')
|
||||
|
||||
printf 'endpoint-nodot=%s\n' "$ENDPOINT_NODOT" >> "$TMP_VALUES_FILE"
|
||||
printf 'endpoint-nospace=%s\n' "$ENDPOINT_NOSPACE" >> "$TMP_VALUES_FILE"
|
||||
|
||||
fi
|
||||
|
||||
# Trova tutti i file .yaml nella directory kubernetes e sottodirectory
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user