1646 lines
47 KiB
Markdown
1646 lines
47 KiB
Markdown
|
|
# 🏗️ Diseño Detallado: KCL + REST API - Service Registry
|
||
|
|
|
||
|
|
**Fecha**: 2025-11-20
|
||
|
|
**Clasificación**: Arquitectura detallada y diseño técnico
|
||
|
|
**Estado**: Design Phase - Ready for Implementation
|
||
|
|
**Siguiendo**: ARCHITECTURE_REVISION.md (Option C: KCL + REST API)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📋 Tabla de Contenidos
|
||
|
|
|
||
|
|
1. [Visión General](#visión-general)
|
||
|
|
2. [Estructura de Definiciones KCL](#estructura-de-definiciones-kcl)
|
||
|
|
3. [API REST Detallada](#api-rest-detallada)
|
||
|
|
4. [Implementación Rust](#implementación-rust)
|
||
|
|
5. [Clientes Multi-Lenguaje](#clientes-multi-lenguaje)
|
||
|
|
6. [Flujos de Integración](#flujos-de-integración)
|
||
|
|
7. [Estrategia de Migración](#estrategia-de-migración)
|
||
|
|
8. [Operaciones y Mantenimiento](#operaciones-y-mantenimiento)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🎯 Visión General
|
||
|
|
|
||
|
|
### Principios de Diseño
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────┐
|
||
|
|
│ PRINCIPIOS CLAVE │
|
||
|
|
├─────────────────────────────────────────────────────────┤
|
||
|
|
│ 1. Source of Truth: KCL (en provisioning) │
|
||
|
|
│ 2. Transport: REST API (agnóstico del lenguaje) │
|
||
|
|
│ 3. Consumo: Cada lenguaje accede idiomatically │
|
||
|
|
│ 4. Cambios: Sin recompilación (solo cambiar KCL) │
|
||
|
|
│ 5. Escalabilidad: Soporta 50+ proyectos │
|
||
|
|
│ 6. Validación: Nativa en KCL + API validation │
|
||
|
|
└─────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
### Capas de Arquitectura
|
||
|
|
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────────┐
|
||
|
|
│ Layer 0: KCL Definition Layer │
|
||
|
|
│ (En provisioning, source of truth) │
|
||
|
|
│ - services.k (definiciones de servicios) │
|
||
|
|
│ - patterns.k (patrones de despliegue) │
|
||
|
|
│ - groups.k (agrupaciones de servicios) │
|
||
|
|
│ - policies.k (validaciones y reglas) │
|
||
|
|
└────────────────────────────────────────────┘
|
||
|
|
↓ (cargado y procesado)
|
||
|
|
┌────────────────────────────────────────────┐
|
||
|
|
│ Layer 1: KCL Runtime │
|
||
|
|
│ (En provisioning, valida y compila) │
|
||
|
|
│ - Compilación de KCL a tipos Rust │
|
||
|
|
│ - Validación de esquemas │
|
||
|
|
│ - Cálculo de dependencias │
|
||
|
|
│ - Generación de código (YAML/JSON/HCL) │
|
||
|
|
└────────────────────────────────────────────┘
|
||
|
|
↓ (expone como API)
|
||
|
|
┌────────────────────────────────────────────┐
|
||
|
|
│ Layer 2: REST API Gateway │
|
||
|
|
│ (En provisioning, servidor HTTP) │
|
||
|
|
│ - /api/v1/services │
|
||
|
|
│ - /api/v1/patterns │
|
||
|
|
│ - /api/v1/validate │
|
||
|
|
│ - /api/v1/generate │
|
||
|
|
│ - /api/v1/health │
|
||
|
|
└────────────────────────────────────────────┘
|
||
|
|
↙ ↓ ↘ ↙
|
||
|
|
┌──────────┬──────────┬──────────┬──────────┐
|
||
|
|
│ syntaxis │ Node.js │ Python │ Go │
|
||
|
|
│ (Rust) │ (HTTP) │ (HTTP) │ (HTTP) │
|
||
|
|
└──────────┴──────────┴──────────┴──────────┘
|
||
|
|
|
||
|
|
BENEFICIO: Cada consumidor usa su lenguaje,
|
||
|
|
todos consultan la misma verdad
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📦 Estructura de Definiciones KCL
|
||
|
|
|
||
|
|
### 1. Organización de Archivos KCL
|
||
|
|
|
||
|
|
```
|
||
|
|
provisioning/
|
||
|
|
├── services.k # Definiciones core de servicios
|
||
|
|
├── patterns.k # Patrones de despliegue
|
||
|
|
├── groups.k # Agrupaciones lógicas
|
||
|
|
├── policies.k # Reglas de validación
|
||
|
|
├── schemas/
|
||
|
|
│ ├── service_schema.k # Schema para servicios
|
||
|
|
│ ├── pattern_schema.k # Schema para patrones
|
||
|
|
│ └── dependency_schema.k # Schema para dependencias
|
||
|
|
└── examples/
|
||
|
|
├── api_service.k # Ejemplo: API service
|
||
|
|
├── database_service.k # Ejemplo: Database service
|
||
|
|
└── monitoring_service.k # Ejemplo: Monitoring service
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Definición de Servicio en KCL
|
||
|
|
|
||
|
|
```kcl
|
||
|
|
# services.k
|
||
|
|
|
||
|
|
# Schema de servicio
|
||
|
|
Service = {
|
||
|
|
id: str # Identificador único
|
||
|
|
name: str # Nombre legible
|
||
|
|
displayName: str # Nombre para mostrar
|
||
|
|
description: str # Descripción
|
||
|
|
version: str # Versión del servicio
|
||
|
|
image: str # Docker image
|
||
|
|
port: int # Puerto principal
|
||
|
|
protocol: str = "http" # Protocolo: http, grpc, tcp
|
||
|
|
replicas: int = 1 # Réplicas por defecto
|
||
|
|
|
||
|
|
# Configuración
|
||
|
|
env: {str: str} # Variables de entorno
|
||
|
|
resources: {
|
||
|
|
cpu: str # CPU request (e.g., "100m", "0.5")
|
||
|
|
memory: str # Memory request (e.g., "128Mi", "512Mi")
|
||
|
|
cpuLimit: str | None = None # CPU limit (opcional)
|
||
|
|
memoryLimit: str | None = None # Memory limit (opcional)
|
||
|
|
}
|
||
|
|
|
||
|
|
# Dependencias
|
||
|
|
depends_on: [str] # IDs de servicios dependientes
|
||
|
|
ports: [{
|
||
|
|
name: str
|
||
|
|
port: int
|
||
|
|
targetPort: int
|
||
|
|
protocol: str = "TCP"
|
||
|
|
}]
|
||
|
|
|
||
|
|
# Health check
|
||
|
|
healthCheck: {
|
||
|
|
enabled: bool = True
|
||
|
|
type: str = "http" # http, tcp, exec
|
||
|
|
path: str = "/health" # Para HTTP
|
||
|
|
interval: int = 10 # Segundos
|
||
|
|
timeout: int = 5 # Segundos
|
||
|
|
retries: int = 3
|
||
|
|
}
|
||
|
|
|
||
|
|
# Metadata
|
||
|
|
labels: {str: str} # Labels adicionales
|
||
|
|
annotations: {str: str} # Anotaciones
|
||
|
|
tags: [str] # Tags para búsqueda
|
||
|
|
}
|
||
|
|
|
||
|
|
# Definiciones de servicios individuales
|
||
|
|
services = [
|
||
|
|
{
|
||
|
|
id: "api-server"
|
||
|
|
name: "api-server"
|
||
|
|
displayName: "API Server"
|
||
|
|
description: "REST API principal"
|
||
|
|
version: "1.0.0"
|
||
|
|
image: "syntaxis:api-latest"
|
||
|
|
port: 3000
|
||
|
|
protocol: "http"
|
||
|
|
replicas: 2
|
||
|
|
|
||
|
|
env: {
|
||
|
|
"RUST_LOG": "info"
|
||
|
|
"DATABASE_URL": "postgresql://postgres:5432/syntaxis"
|
||
|
|
"ENVIRONMENT": "production"
|
||
|
|
}
|
||
|
|
|
||
|
|
resources: {
|
||
|
|
cpu: "100m"
|
||
|
|
memory: "256Mi"
|
||
|
|
cpuLimit: "500m"
|
||
|
|
memoryLimit: "512Mi"
|
||
|
|
}
|
||
|
|
|
||
|
|
depends_on: ["database", "cache"]
|
||
|
|
|
||
|
|
ports: [
|
||
|
|
{
|
||
|
|
name: "http"
|
||
|
|
port: 3000
|
||
|
|
targetPort: 3000
|
||
|
|
protocol: "TCP"
|
||
|
|
}
|
||
|
|
]
|
||
|
|
|
||
|
|
healthCheck: {
|
||
|
|
enabled: True
|
||
|
|
type: "http"
|
||
|
|
path: "/health"
|
||
|
|
interval: 10
|
||
|
|
timeout: 5
|
||
|
|
retries: 3
|
||
|
|
}
|
||
|
|
|
||
|
|
labels: {
|
||
|
|
"app": "syntaxis"
|
||
|
|
"component": "api"
|
||
|
|
"tier": "backend"
|
||
|
|
}
|
||
|
|
|
||
|
|
tags: ["core", "api", "backend"]
|
||
|
|
},
|
||
|
|
|
||
|
|
{
|
||
|
|
id: "database"
|
||
|
|
name: "postgres"
|
||
|
|
displayName: "PostgreSQL Database"
|
||
|
|
description: "Persistencia de datos principal"
|
||
|
|
version: "15.0"
|
||
|
|
image: "postgres:15-alpine"
|
||
|
|
port: 5432
|
||
|
|
protocol: "tcp"
|
||
|
|
replicas: 1
|
||
|
|
|
||
|
|
env: {
|
||
|
|
"POSTGRES_DB": "syntaxis"
|
||
|
|
"POSTGRES_USER": "postgres"
|
||
|
|
}
|
||
|
|
|
||
|
|
resources: {
|
||
|
|
cpu: "250m"
|
||
|
|
memory: "512Mi"
|
||
|
|
cpuLimit: "1000m"
|
||
|
|
memoryLimit: "2Gi"
|
||
|
|
}
|
||
|
|
|
||
|
|
depends_on: []
|
||
|
|
|
||
|
|
ports: [
|
||
|
|
{
|
||
|
|
name: "postgresql"
|
||
|
|
port: 5432
|
||
|
|
targetPort: 5432
|
||
|
|
protocol: "TCP"
|
||
|
|
}
|
||
|
|
]
|
||
|
|
|
||
|
|
healthCheck: {
|
||
|
|
enabled: True
|
||
|
|
type: "tcp"
|
||
|
|
interval: 10
|
||
|
|
timeout: 5
|
||
|
|
retries: 3
|
||
|
|
}
|
||
|
|
|
||
|
|
labels: {
|
||
|
|
"app": "syntaxis"
|
||
|
|
"component": "database"
|
||
|
|
"tier": "data"
|
||
|
|
}
|
||
|
|
|
||
|
|
tags: ["data", "persistent", "critical"]
|
||
|
|
}
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Definición de Patrones en KCL
|
||
|
|
|
||
|
|
```kcl
|
||
|
|
# patterns.k
|
||
|
|
|
||
|
|
Pattern = {
|
||
|
|
id: str # Identificador único
|
||
|
|
name: str # Nombre del patrón
|
||
|
|
displayName: str # Nombre legible
|
||
|
|
description: str # Descripción
|
||
|
|
tags: [str] # Tags para búsqueda
|
||
|
|
services: [str] # IDs de servicios incluidos
|
||
|
|
minReplicas: int = 1 # Réplicas mínimas
|
||
|
|
maxReplicas: int = 5 # Réplicas máximas
|
||
|
|
environment: str # Entorno: dev, staging, production
|
||
|
|
}
|
||
|
|
|
||
|
|
patterns = [
|
||
|
|
{
|
||
|
|
id: "local-dev"
|
||
|
|
name: "local-dev"
|
||
|
|
displayName: "Development Local"
|
||
|
|
description: "Stack completo para desarrollo local con Docker Compose"
|
||
|
|
tags: ["development", "local", "complete"]
|
||
|
|
services: ["api-server", "database", "cache"]
|
||
|
|
minReplicas: 1
|
||
|
|
maxReplicas: 1
|
||
|
|
environment: "development"
|
||
|
|
},
|
||
|
|
|
||
|
|
{
|
||
|
|
id: "staging"
|
||
|
|
name: "staging"
|
||
|
|
displayName: "Staging Environment"
|
||
|
|
description: "Stack para testing y validación pre-producción"
|
||
|
|
tags: ["staging", "testing", "complete"]
|
||
|
|
services: ["api-server", "database", "cache", "monitoring"]
|
||
|
|
minReplicas: 1
|
||
|
|
maxReplicas: 3
|
||
|
|
environment: "staging"
|
||
|
|
},
|
||
|
|
|
||
|
|
{
|
||
|
|
id: "production"
|
||
|
|
name: "production"
|
||
|
|
displayName: "Production Environment"
|
||
|
|
description: "Stack de producción con alta disponibilidad"
|
||
|
|
tags: ["production", "ha", "complete"]
|
||
|
|
services: ["api-server", "database", "cache", "monitoring", "backup"]
|
||
|
|
minReplicas: 2
|
||
|
|
maxReplicas: 10
|
||
|
|
environment: "production"
|
||
|
|
}
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. Definición de Grupos en KCL
|
||
|
|
|
||
|
|
```kcl
|
||
|
|
# groups.k
|
||
|
|
|
||
|
|
Group = {
|
||
|
|
id: str
|
||
|
|
name: str
|
||
|
|
displayName: str
|
||
|
|
description: str
|
||
|
|
services: [str] # IDs de servicios en el grupo
|
||
|
|
tier: str # Tier: frontend, backend, data, infra
|
||
|
|
}
|
||
|
|
|
||
|
|
groups = [
|
||
|
|
{
|
||
|
|
id: "backend-services"
|
||
|
|
name: "backend-services"
|
||
|
|
displayName: "Backend Services"
|
||
|
|
description: "Servicios de backend/API"
|
||
|
|
services: ["api-server", "api-gateway"]
|
||
|
|
tier: "backend"
|
||
|
|
},
|
||
|
|
|
||
|
|
{
|
||
|
|
id: "data-layer"
|
||
|
|
name: "data-layer"
|
||
|
|
displayName: "Data Layer"
|
||
|
|
description: "Servicios de persistencia de datos"
|
||
|
|
services: ["database", "cache", "backup"]
|
||
|
|
tier: "data"
|
||
|
|
},
|
||
|
|
|
||
|
|
{
|
||
|
|
id: "infrastructure"
|
||
|
|
name: "infrastructure"
|
||
|
|
displayName: "Infrastructure Services"
|
||
|
|
description: "Servicios de infraestructura"
|
||
|
|
services: ["monitoring", "logging", "messaging"]
|
||
|
|
tier: "infra"
|
||
|
|
}
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5. Políticas de Validación en KCL
|
||
|
|
|
||
|
|
```kcl
|
||
|
|
# policies.k
|
||
|
|
|
||
|
|
# Reglas de validación que se aplican automáticamente
|
||
|
|
|
||
|
|
# Validar que todos los servicios tienen health check
|
||
|
|
validate_health_checks = lambda services {
|
||
|
|
for s in services:
|
||
|
|
assert s.healthCheck.enabled, "Service {} must have health check enabled".format(s.id)
|
||
|
|
}
|
||
|
|
|
||
|
|
# Validar que las dependencias existen
|
||
|
|
validate_dependencies = lambda services {
|
||
|
|
service_ids = [s.id for s in services]
|
||
|
|
for s in services:
|
||
|
|
for dep_id in s.depends_on:
|
||
|
|
assert dep_id in service_ids, "Service {} depends on non-existent service {}".format(s.id, dep_id)
|
||
|
|
}
|
||
|
|
|
||
|
|
# Validar restricciones de recursos
|
||
|
|
validate_resources = lambda services {
|
||
|
|
for s in services:
|
||
|
|
# CPU debe estar en formato válido
|
||
|
|
assert s.resources.cpu, "Service {} must specify CPU request".format(s.id)
|
||
|
|
assert s.resources.memory, "Service {} must specify memory request".format(s.id)
|
||
|
|
}
|
||
|
|
|
||
|
|
# Validar que no hay ciclos de dependencias
|
||
|
|
validate_no_cycles = lambda services {
|
||
|
|
# Validar acíclico dirigido
|
||
|
|
def has_cycle(graph, node, visited, rec_stack):
|
||
|
|
visited[node] = True
|
||
|
|
rec_stack[node] = True
|
||
|
|
|
||
|
|
for neighbor in graph.get(node, []):
|
||
|
|
if not visited.get(neighbor):
|
||
|
|
if has_cycle(graph, neighbor, visited, rec_stack):
|
||
|
|
return True
|
||
|
|
elif rec_stack.get(neighbor):
|
||
|
|
return True
|
||
|
|
|
||
|
|
rec_stack[node] = False
|
||
|
|
return False
|
||
|
|
|
||
|
|
graph = {s.id: s.depends_on for s in services}
|
||
|
|
for service_id in graph:
|
||
|
|
if not has_cycle(graph, service_id, {}, {}):
|
||
|
|
continue
|
||
|
|
else:
|
||
|
|
assert False, "Cyclic dependency detected involving {}".format(service_id)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔌 API REST Detallada
|
||
|
|
|
||
|
|
### 1. Estructura de Respuesta Estándar
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// Todas las respuestas siguen este patrón
|
||
|
|
|
||
|
|
// Success response (HTTP 200)
|
||
|
|
{
|
||
|
|
"success": true,
|
||
|
|
"data": { /* contenido específico */ },
|
||
|
|
"meta": {
|
||
|
|
"version": "1.0",
|
||
|
|
"timestamp": "2025-11-20T10:30:00Z",
|
||
|
|
"request_id": "req_uuid_here"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Error response (HTTP 4xx/5xx)
|
||
|
|
{
|
||
|
|
"success": false,
|
||
|
|
"error": {
|
||
|
|
"code": "VALIDATION_ERROR",
|
||
|
|
"message": "Service dependency cycle detected",
|
||
|
|
"details": {
|
||
|
|
"field": "services[0].depends_on",
|
||
|
|
"value": ["api-server"],
|
||
|
|
"reason": "Creates cycle with service 'database'"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"meta": {
|
||
|
|
"version": "1.0",
|
||
|
|
"timestamp": "2025-11-20T10:30:00Z",
|
||
|
|
"request_id": "req_uuid_here"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Endpoints Principales
|
||
|
|
|
||
|
|
#### 2.1 GET /api/v1/health
|
||
|
|
Status del servicio API
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Request
|
||
|
|
GET /api/v1/health
|
||
|
|
|
||
|
|
# Response (200 OK)
|
||
|
|
{
|
||
|
|
"success": true,
|
||
|
|
"data": {
|
||
|
|
"status": "healthy",
|
||
|
|
"version": "1.0.0",
|
||
|
|
"uptime_seconds": 3600,
|
||
|
|
"kcl_compiler": "2.3.0",
|
||
|
|
"last_reload": "2025-11-20T09:00:00Z"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.2 GET /api/v1/services
|
||
|
|
Listar todos los servicios
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Request
|
||
|
|
GET /api/v1/services?tag=backend&limit=10&offset=0
|
||
|
|
|
||
|
|
# Response (200 OK)
|
||
|
|
{
|
||
|
|
"success": true,
|
||
|
|
"data": [
|
||
|
|
{
|
||
|
|
"id": "api-server",
|
||
|
|
"name": "api-server",
|
||
|
|
"displayName": "API Server",
|
||
|
|
"description": "REST API principal",
|
||
|
|
"version": "1.0.0",
|
||
|
|
"image": "syntaxis:api-latest",
|
||
|
|
"port": 3000,
|
||
|
|
"protocol": "http",
|
||
|
|
"tags": ["core", "api", "backend"],
|
||
|
|
"depends_on": ["database", "cache"]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"id": "database",
|
||
|
|
"name": "postgres",
|
||
|
|
"displayName": "PostgreSQL Database",
|
||
|
|
"description": "Persistencia de datos principal",
|
||
|
|
"version": "15.0",
|
||
|
|
"image": "postgres:15-alpine",
|
||
|
|
"port": 5432,
|
||
|
|
"protocol": "tcp",
|
||
|
|
"tags": ["data", "persistent", "critical"],
|
||
|
|
"depends_on": []
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"meta": {
|
||
|
|
"version": "1.0",
|
||
|
|
"total": 2,
|
||
|
|
"limit": 10,
|
||
|
|
"offset": 0,
|
||
|
|
"timestamp": "2025-11-20T10:30:00Z"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.3 GET /api/v1/services/:id
|
||
|
|
Obtener detalles de un servicio específico
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Request
|
||
|
|
GET /api/v1/services/api-server
|
||
|
|
|
||
|
|
# Response (200 OK)
|
||
|
|
{
|
||
|
|
"success": true,
|
||
|
|
"data": {
|
||
|
|
"id": "api-server",
|
||
|
|
"name": "api-server",
|
||
|
|
"displayName": "API Server",
|
||
|
|
"description": "REST API principal",
|
||
|
|
"version": "1.0.0",
|
||
|
|
"image": "syntaxis:api-latest",
|
||
|
|
"port": 3000,
|
||
|
|
"protocol": "http",
|
||
|
|
"replicas": 2,
|
||
|
|
|
||
|
|
"env": {
|
||
|
|
"RUST_LOG": "info",
|
||
|
|
"DATABASE_URL": "postgresql://postgres:5432/syntaxis",
|
||
|
|
"ENVIRONMENT": "production"
|
||
|
|
},
|
||
|
|
|
||
|
|
"resources": {
|
||
|
|
"cpu": "100m",
|
||
|
|
"memory": "256Mi",
|
||
|
|
"cpuLimit": "500m",
|
||
|
|
"memoryLimit": "512Mi"
|
||
|
|
},
|
||
|
|
|
||
|
|
"depends_on": ["database", "cache"],
|
||
|
|
|
||
|
|
"ports": [
|
||
|
|
{
|
||
|
|
"name": "http",
|
||
|
|
"port": 3000,
|
||
|
|
"targetPort": 3000,
|
||
|
|
"protocol": "TCP"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
|
||
|
|
"healthCheck": {
|
||
|
|
"enabled": true,
|
||
|
|
"type": "http",
|
||
|
|
"path": "/health",
|
||
|
|
"interval": 10,
|
||
|
|
"timeout": 5,
|
||
|
|
"retries": 3
|
||
|
|
},
|
||
|
|
|
||
|
|
"labels": {
|
||
|
|
"app": "syntaxis",
|
||
|
|
"component": "api",
|
||
|
|
"tier": "backend"
|
||
|
|
},
|
||
|
|
|
||
|
|
"tags": ["core", "api", "backend"]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.4 GET /api/v1/services/:id/dependents
|
||
|
|
Obtener servicios que dependen de uno específico
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Request
|
||
|
|
GET /api/v1/services/database/dependents
|
||
|
|
|
||
|
|
# Response (200 OK)
|
||
|
|
{
|
||
|
|
"success": true,
|
||
|
|
"data": [
|
||
|
|
{
|
||
|
|
"id": "api-server",
|
||
|
|
"displayName": "API Server",
|
||
|
|
"dependencyType": "required"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"id": "backup-service",
|
||
|
|
"displayName": "Backup Service",
|
||
|
|
"dependencyType": "optional"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"meta": {
|
||
|
|
"total": 2,
|
||
|
|
"timestamp": "2025-11-20T10:30:00Z"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.5 GET /api/v1/patterns
|
||
|
|
Listar patrones de despliegue
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Request
|
||
|
|
GET /api/v1/patterns?environment=production
|
||
|
|
|
||
|
|
# Response (200 OK)
|
||
|
|
{
|
||
|
|
"success": true,
|
||
|
|
"data": [
|
||
|
|
{
|
||
|
|
"id": "production",
|
||
|
|
"name": "production",
|
||
|
|
"displayName": "Production Environment",
|
||
|
|
"description": "Stack de producción con alta disponibilidad",
|
||
|
|
"environment": "production",
|
||
|
|
"services": ["api-server", "database", "cache", "monitoring", "backup"],
|
||
|
|
"minReplicas": 2,
|
||
|
|
"maxReplicas": 10,
|
||
|
|
"tags": ["production", "ha", "complete"]
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.6 GET /api/v1/patterns/:id/services
|
||
|
|
Obtener servicios de un patrón específico (con detalles)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Request
|
||
|
|
GET /api/v1/patterns/production/services
|
||
|
|
|
||
|
|
# Response (200 OK)
|
||
|
|
{
|
||
|
|
"success": true,
|
||
|
|
"data": [
|
||
|
|
{
|
||
|
|
"id": "api-server",
|
||
|
|
"displayName": "API Server",
|
||
|
|
"version": "1.0.0",
|
||
|
|
"replicas": 2,
|
||
|
|
"image": "syntaxis:api-latest"
|
||
|
|
// ... detalles completos del servicio
|
||
|
|
},
|
||
|
|
// ... más servicios
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.7 GET /api/v1/groups
|
||
|
|
Listar grupos de servicios
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Request
|
||
|
|
GET /api/v1/groups?tier=backend
|
||
|
|
|
||
|
|
# Response (200 OK)
|
||
|
|
{
|
||
|
|
"success": true,
|
||
|
|
"data": [
|
||
|
|
{
|
||
|
|
"id": "backend-services",
|
||
|
|
"name": "backend-services",
|
||
|
|
"displayName": "Backend Services",
|
||
|
|
"description": "Servicios de backend/API",
|
||
|
|
"tier": "backend",
|
||
|
|
"services": ["api-server", "api-gateway"]
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.8 POST /api/v1/validate
|
||
|
|
Validar cambios antes de deployment
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Request
|
||
|
|
POST /api/v1/validate
|
||
|
|
Content-Type: application/json
|
||
|
|
|
||
|
|
{
|
||
|
|
"services": [
|
||
|
|
{
|
||
|
|
"id": "new-service",
|
||
|
|
"name": "new-service",
|
||
|
|
"displayName": "New Service",
|
||
|
|
"version": "1.0.0",
|
||
|
|
"image": "new-service:latest",
|
||
|
|
"port": 3001,
|
||
|
|
"depends_on": ["database"]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"validate_against_pattern": "production"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Response (200 OK)
|
||
|
|
{
|
||
|
|
"success": true,
|
||
|
|
"data": {
|
||
|
|
"valid": true,
|
||
|
|
"warnings": [],
|
||
|
|
"errors": [],
|
||
|
|
"impact_analysis": {
|
||
|
|
"affected_services": ["api-server"],
|
||
|
|
"breaking_changes": false,
|
||
|
|
"deployment_risk": "low"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.9 POST /api/v1/generate
|
||
|
|
Generar código en múltiples formatos
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Request
|
||
|
|
POST /api/v1/generate
|
||
|
|
Content-Type: application/json
|
||
|
|
|
||
|
|
{
|
||
|
|
"pattern_id": "production",
|
||
|
|
"formats": ["kubernetes", "docker-compose", "terraform"],
|
||
|
|
"options": {
|
||
|
|
"namespace": "production",
|
||
|
|
"domain": "app.example.com",
|
||
|
|
"registry": "docker.io"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Response (200 OK)
|
||
|
|
{
|
||
|
|
"success": true,
|
||
|
|
"data": {
|
||
|
|
"kubernetes": {
|
||
|
|
"filename": "kubernetes-production.yaml",
|
||
|
|
"content": "apiVersion: v1\nkind: Service\n...",
|
||
|
|
"format": "yaml"
|
||
|
|
},
|
||
|
|
"docker-compose": {
|
||
|
|
"filename": "docker-compose-production.yml",
|
||
|
|
"content": "version: '3.8'\nservices:\n...",
|
||
|
|
"format": "yaml"
|
||
|
|
},
|
||
|
|
"terraform": {
|
||
|
|
"filename": "main.tf",
|
||
|
|
"content": "resource \"kubernetes_service\" \"api\" {\n...",
|
||
|
|
"format": "hcl"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.10 POST /api/v1/reload
|
||
|
|
Recargar definiciones KCL (requiere autenticación admin)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Request
|
||
|
|
POST /api/v1/reload
|
||
|
|
Authorization: Bearer admin_token
|
||
|
|
|
||
|
|
{}
|
||
|
|
|
||
|
|
# Response (200 OK)
|
||
|
|
{
|
||
|
|
"success": true,
|
||
|
|
"data": {
|
||
|
|
"reloaded_at": "2025-11-20T10:31:00Z",
|
||
|
|
"services_count": 12,
|
||
|
|
"patterns_count": 4,
|
||
|
|
"validation_passed": true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Parámetros de Query Comunes
|
||
|
|
|
||
|
|
```
|
||
|
|
?limit=10 # Límite de resultados (default: 20, max: 100)
|
||
|
|
?offset=0 # Offset para paginación (default: 0)
|
||
|
|
?tag=backend # Filtrar por tags (comma-separated)
|
||
|
|
?search=api # Búsqueda full-text en nombre/descripción
|
||
|
|
?sort=name # Campo para ordenar (name, version, id)
|
||
|
|
?order=asc # Orden (asc, desc)
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 💻 Implementación Rust
|
||
|
|
|
||
|
|
### 1. Estructura del Proyecto
|
||
|
|
|
||
|
|
```
|
||
|
|
provisioning/
|
||
|
|
├── Cargo.toml
|
||
|
|
├── src/
|
||
|
|
│ ├── main.rs # Entry point
|
||
|
|
│ ├── api/
|
||
|
|
│ │ ├── mod.rs # API module
|
||
|
|
│ │ ├── handlers.rs # Handler functions
|
||
|
|
│ │ ├── extractors.rs # Custom extractors
|
||
|
|
│ │ └── responses.rs # Response types
|
||
|
|
│ ├── kcl/
|
||
|
|
│ │ ├── mod.rs # KCL module
|
||
|
|
│ │ ├── loader.rs # Carga archivos KCL
|
||
|
|
│ │ └── validator.rs # Validaciones
|
||
|
|
│ ├── models/
|
||
|
|
│ │ ├── mod.rs
|
||
|
|
│ │ ├── service.rs # Service struct
|
||
|
|
│ │ ├── pattern.rs # Pattern struct
|
||
|
|
│ │ └── group.rs # Group struct
|
||
|
|
│ ├── generators/
|
||
|
|
│ │ ├── mod.rs
|
||
|
|
│ │ ├── kubernetes.rs # K8s generator
|
||
|
|
│ │ ├── docker.rs # Docker Compose generator
|
||
|
|
│ │ └── terraform.rs # Terraform generator
|
||
|
|
│ └── config.rs # Configuración
|
||
|
|
└── tests/
|
||
|
|
├── integration/
|
||
|
|
│ ├── health_test.rs
|
||
|
|
│ ├── services_test.rs
|
||
|
|
│ ├── patterns_test.rs
|
||
|
|
│ └── validation_test.rs
|
||
|
|
└── fixtures/
|
||
|
|
├── services.k
|
||
|
|
├── patterns.k
|
||
|
|
└── expected_outputs/
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Implementación de Main.rs
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use axum::{
|
||
|
|
extract::{Path, Query, State},
|
||
|
|
http::StatusCode,
|
||
|
|
response::IntoResponse,
|
||
|
|
routing::{get, post},
|
||
|
|
Json, Router,
|
||
|
|
};
|
||
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
use std::sync::Arc;
|
||
|
|
use tokio::sync::RwLock;
|
||
|
|
use tracing::{info, error};
|
||
|
|
|
||
|
|
mod api;
|
||
|
|
mod kcl;
|
||
|
|
mod models;
|
||
|
|
mod generators;
|
||
|
|
mod config;
|
||
|
|
|
||
|
|
use crate::models::{Service, Pattern, Group};
|
||
|
|
use crate::kcl::KclLoader;
|
||
|
|
|
||
|
|
#[derive(Clone)]
|
||
|
|
struct AppState {
|
||
|
|
kcl_loader: Arc<RwLock<KclLoader>>,
|
||
|
|
config: config::Config,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::main]
|
||
|
|
async fn main() {
|
||
|
|
// Initialize tracing
|
||
|
|
tracing_subscriber::fmt()
|
||
|
|
.with_max_level(tracing::Level::INFO)
|
||
|
|
.init();
|
||
|
|
|
||
|
|
// Load configuration
|
||
|
|
let config = config::Config::from_file("provisioning.toml")
|
||
|
|
.expect("Failed to load config");
|
||
|
|
|
||
|
|
info!("Loading KCL definitions from: {}", config.kcl_path);
|
||
|
|
|
||
|
|
// Load KCL definitions
|
||
|
|
let kcl_loader = KclLoader::new(&config.kcl_path)
|
||
|
|
.await
|
||
|
|
.expect("Failed to load KCL definitions");
|
||
|
|
|
||
|
|
let state = AppState {
|
||
|
|
kcl_loader: Arc::new(RwLock::new(kcl_loader)),
|
||
|
|
config,
|
||
|
|
};
|
||
|
|
|
||
|
|
// Build router
|
||
|
|
let app = Router::new()
|
||
|
|
// Health check
|
||
|
|
.route("/api/v1/health", get(api::handlers::health))
|
||
|
|
|
||
|
|
// Services endpoints
|
||
|
|
.route("/api/v1/services", get(api::handlers::list_services))
|
||
|
|
.route("/api/v1/services/:id", get(api::handlers::get_service))
|
||
|
|
.route("/api/v1/services/:id/dependents", get(api::handlers::get_dependents))
|
||
|
|
|
||
|
|
// Patterns endpoints
|
||
|
|
.route("/api/v1/patterns", get(api::handlers::list_patterns))
|
||
|
|
.route("/api/v1/patterns/:id/services", get(api::handlers::get_pattern_services))
|
||
|
|
|
||
|
|
// Groups endpoints
|
||
|
|
.route("/api/v1/groups", get(api::handlers::list_groups))
|
||
|
|
|
||
|
|
// Validation & Generation
|
||
|
|
.route("/api/v1/validate", post(api::handlers::validate))
|
||
|
|
.route("/api/v1/generate", post(api::handlers::generate))
|
||
|
|
|
||
|
|
// Admin endpoints
|
||
|
|
.route("/api/v1/reload", post(api::handlers::reload_kcl))
|
||
|
|
|
||
|
|
.with_state(state);
|
||
|
|
|
||
|
|
// Start server
|
||
|
|
let listener = tokio::net::TcpListener::bind(&state.config.server_addr)
|
||
|
|
.await
|
||
|
|
.expect("Failed to bind server");
|
||
|
|
|
||
|
|
info!("Server listening on {}", state.config.server_addr);
|
||
|
|
|
||
|
|
axum::serve(listener, app)
|
||
|
|
.await
|
||
|
|
.expect("Server error");
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Handler de Servicios
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// api/handlers.rs
|
||
|
|
|
||
|
|
use axum::extract::{Path, Query, State};
|
||
|
|
use axum::http::StatusCode;
|
||
|
|
use axum::Json;
|
||
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
|
||
|
|
#[derive(Serialize)]
|
||
|
|
pub struct ApiResponse<T: Serialize> {
|
||
|
|
success: bool,
|
||
|
|
data: T,
|
||
|
|
meta: ResponseMeta,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Serialize)]
|
||
|
|
pub struct ResponseMeta {
|
||
|
|
version: String,
|
||
|
|
timestamp: String,
|
||
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||
|
|
request_id: Option<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn list_services(
|
||
|
|
State(state): State<crate::AppState>,
|
||
|
|
Query(params): Query<ListServicesParams>,
|
||
|
|
) -> Json<ApiResponse<Vec<Service>>> {
|
||
|
|
let loader = state.kcl_loader.read().await;
|
||
|
|
let mut services = loader.get_services().clone();
|
||
|
|
|
||
|
|
// Aplicar filtros
|
||
|
|
if let Some(tag) = params.tag {
|
||
|
|
services.retain(|s| s.tags.iter().any(|t| t == &tag));
|
||
|
|
}
|
||
|
|
|
||
|
|
if let Some(search) = params.search {
|
||
|
|
services.retain(|s| {
|
||
|
|
s.display_name.contains(&search) || s.description.contains(&search)
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Aplicar paginación
|
||
|
|
let offset = params.offset.unwrap_or(0);
|
||
|
|
let limit = params.limit.unwrap_or(20).min(100);
|
||
|
|
let paginated = services
|
||
|
|
.into_iter()
|
||
|
|
.skip(offset)
|
||
|
|
.take(limit)
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
|
||
|
|
Json(ApiResponse {
|
||
|
|
success: true,
|
||
|
|
data: paginated,
|
||
|
|
meta: ResponseMeta {
|
||
|
|
version: "1.0".to_string(),
|
||
|
|
timestamp: chrono::Utc::now().to_rfc3339(),
|
||
|
|
request_id: None,
|
||
|
|
},
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn get_service(
|
||
|
|
State(state): State<crate::AppState>,
|
||
|
|
Path(id): Path<String>,
|
||
|
|
) -> Result<Json<ApiResponse<Service>>, (StatusCode, String)> {
|
||
|
|
let loader = state.kcl_loader.read().await;
|
||
|
|
|
||
|
|
match loader.get_service(&id) {
|
||
|
|
Some(service) => Ok(Json(ApiResponse {
|
||
|
|
success: true,
|
||
|
|
data: service.clone(),
|
||
|
|
meta: ResponseMeta {
|
||
|
|
version: "1.0".to_string(),
|
||
|
|
timestamp: chrono::Utc::now().to_rfc3339(),
|
||
|
|
request_id: None,
|
||
|
|
},
|
||
|
|
})),
|
||
|
|
None => Err((
|
||
|
|
StatusCode::NOT_FOUND,
|
||
|
|
format!("Service '{}' not found", id),
|
||
|
|
)),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn validate(
|
||
|
|
State(state): State<crate::AppState>,
|
||
|
|
Json(payload): Json<ValidatePayload>,
|
||
|
|
) -> Json<ApiResponse<ValidationResult>> {
|
||
|
|
let loader = state.kcl_loader.read().await;
|
||
|
|
let result = loader.validate(&payload.services, payload.validate_against_pattern.as_deref());
|
||
|
|
|
||
|
|
Json(ApiResponse {
|
||
|
|
success: true,
|
||
|
|
data: result,
|
||
|
|
meta: ResponseMeta {
|
||
|
|
version: "1.0".to_string(),
|
||
|
|
timestamp: chrono::Utc::now().to_rfc3339(),
|
||
|
|
request_id: None,
|
||
|
|
},
|
||
|
|
})
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🌐 Clientes Multi-Lenguaje
|
||
|
|
|
||
|
|
### 1. Cliente Rust (Integrado en syntaxis)
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// En syntaxis/core/crates/syntaxis-core/src/service_registry.rs
|
||
|
|
|
||
|
|
use reqwest::Client;
|
||
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
|
||
|
|
#[derive(Clone)]
|
||
|
|
pub struct ServiceRegistryClient {
|
||
|
|
client: Client,
|
||
|
|
base_url: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ServiceRegistryClient {
|
||
|
|
pub fn new(api_url: impl Into<String>) -> Self {
|
||
|
|
Self {
|
||
|
|
client: Client::new(),
|
||
|
|
base_url: api_url.into(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn get_services(&self) -> Result<Vec<Service>, Box<dyn std::error::Error>> {
|
||
|
|
let url = format!("{}/api/v1/services", self.base_url);
|
||
|
|
let response = self.client.get(&url).send().await?;
|
||
|
|
let data: ApiResponse<Vec<Service>> = response.json().await?;
|
||
|
|
Ok(data.data)
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn get_service(&self, id: &str) -> Result<Service, Box<dyn std::error::Error>> {
|
||
|
|
let url = format!("{}/api/v1/services/{}", self.base_url, id);
|
||
|
|
let response = self.client.get(&url).send().await?;
|
||
|
|
let data: ApiResponse<Service> = response.json().await?;
|
||
|
|
Ok(data.data)
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn get_pattern(&self, id: &str) -> Result<Pattern, Box<dyn std::error::Error>> {
|
||
|
|
let url = format!("{}/api/v1/patterns/{}", self.base_url, id);
|
||
|
|
let response = self.client.get(&url).send().await?;
|
||
|
|
let data: ApiResponse<Pattern> = response.json().await?;
|
||
|
|
Ok(data.data)
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn validate_services(
|
||
|
|
&self,
|
||
|
|
services: &[Service],
|
||
|
|
) -> Result<ValidationResult, Box<dyn std::error::Error>> {
|
||
|
|
let url = format!("{}/api/v1/validate", self.base_url);
|
||
|
|
let payload = ValidatePayload {
|
||
|
|
services: services.to_vec(),
|
||
|
|
validate_against_pattern: None,
|
||
|
|
};
|
||
|
|
let response = self.client.post(&url).json(&payload).send().await?;
|
||
|
|
let data: ApiResponse<ValidationResult> = response.json().await?;
|
||
|
|
Ok(data.data)
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn generate(
|
||
|
|
&self,
|
||
|
|
pattern_id: &str,
|
||
|
|
formats: &[&str],
|
||
|
|
) -> Result<GeneratedOutput, Box<dyn std::error::Error>> {
|
||
|
|
let url = format!("{}/api/v1/generate", self.base_url);
|
||
|
|
let payload = GeneratePayload {
|
||
|
|
pattern_id: pattern_id.to_string(),
|
||
|
|
formats: formats.iter().map(|s| s.to_string()).collect(),
|
||
|
|
options: Default::default(),
|
||
|
|
};
|
||
|
|
let response = self.client.post(&url).json(&payload).send().await?;
|
||
|
|
let data: ApiResponse<GeneratedOutput> = response.json().await?;
|
||
|
|
Ok(data.data)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Deserialize)]
|
||
|
|
struct ApiResponse<T> {
|
||
|
|
success: bool,
|
||
|
|
data: T,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Cliente JavaScript/Node.js
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// clients/javascript/service-registry.js
|
||
|
|
|
||
|
|
const fetch = require('node-fetch');
|
||
|
|
|
||
|
|
class ServiceRegistry {
|
||
|
|
constructor(apiUrl = 'http://127.0.0.1:8080') {
|
||
|
|
this.apiUrl = apiUrl;
|
||
|
|
this.timeout = 5000;
|
||
|
|
}
|
||
|
|
|
||
|
|
async _request(method, endpoint, body = null) {
|
||
|
|
const url = `${this.apiUrl}${endpoint}`;
|
||
|
|
const options = {
|
||
|
|
method,
|
||
|
|
timeout: this.timeout,
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
if (body) {
|
||
|
|
options.body = JSON.stringify(body);
|
||
|
|
}
|
||
|
|
|
||
|
|
const response = await fetch(url, options);
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
const error = await response.json();
|
||
|
|
throw new Error(`API Error: ${error.error.message}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
return data.data;
|
||
|
|
}
|
||
|
|
|
||
|
|
async getServices(filters = {}) {
|
||
|
|
const params = new URLSearchParams(filters);
|
||
|
|
const endpoint = `/api/v1/services?${params.toString()}`;
|
||
|
|
return this._request('GET', endpoint);
|
||
|
|
}
|
||
|
|
|
||
|
|
async getService(id) {
|
||
|
|
return this._request('GET', `/api/v1/services/${id}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
async getPatterns(filters = {}) {
|
||
|
|
const params = new URLSearchParams(filters);
|
||
|
|
const endpoint = `/api/v1/patterns?${params.toString()}`;
|
||
|
|
return this._request('GET', endpoint);
|
||
|
|
}
|
||
|
|
|
||
|
|
async getPatternServices(patternId) {
|
||
|
|
return this._request('GET', `/api/v1/patterns/${patternId}/services`);
|
||
|
|
}
|
||
|
|
|
||
|
|
async getGroups(filters = {}) {
|
||
|
|
const params = new URLSearchParams(filters);
|
||
|
|
const endpoint = `/api/v1/groups?${params.toString()}`;
|
||
|
|
return this._request('GET', endpoint);
|
||
|
|
}
|
||
|
|
|
||
|
|
async validate(services, patternId = null) {
|
||
|
|
const payload = {
|
||
|
|
services,
|
||
|
|
validate_against_pattern: patternId,
|
||
|
|
};
|
||
|
|
return this._request('POST', '/api/v1/validate', payload);
|
||
|
|
}
|
||
|
|
|
||
|
|
async generate(patternId, formats, options = {}) {
|
||
|
|
const payload = {
|
||
|
|
pattern_id: patternId,
|
||
|
|
formats,
|
||
|
|
options,
|
||
|
|
};
|
||
|
|
return this._request('POST', '/api/v1/generate', payload);
|
||
|
|
}
|
||
|
|
|
||
|
|
async health() {
|
||
|
|
return this._request('GET', '/api/v1/health');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = ServiceRegistry;
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Cliente Python
|
||
|
|
|
||
|
|
```python
|
||
|
|
# clients/python/service_registry.py
|
||
|
|
|
||
|
|
import requests
|
||
|
|
from typing import List, Dict, Optional, Any
|
||
|
|
from dataclasses import dataclass
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class ServiceRegistry:
|
||
|
|
api_url: str = "http://127.0.0.1:8080"
|
||
|
|
timeout: int = 5
|
||
|
|
|
||
|
|
def _request(self, method: str, endpoint: str, json: Optional[Dict] = None) -> Any:
|
||
|
|
url = f"{self.api_url}{endpoint}"
|
||
|
|
headers = {"Content-Type": "application/json"}
|
||
|
|
|
||
|
|
response = requests.request(
|
||
|
|
method=method,
|
||
|
|
url=url,
|
||
|
|
json=json,
|
||
|
|
headers=headers,
|
||
|
|
timeout=self.timeout,
|
||
|
|
)
|
||
|
|
|
||
|
|
if not response.ok:
|
||
|
|
error = response.json()
|
||
|
|
raise Exception(f"API Error: {error['error']['message']}")
|
||
|
|
|
||
|
|
data = response.json()
|
||
|
|
return data['data']
|
||
|
|
|
||
|
|
def get_services(self, tag: Optional[str] = None, search: Optional[str] = None) -> List[Dict]:
|
||
|
|
params = {}
|
||
|
|
if tag:
|
||
|
|
params['tag'] = tag
|
||
|
|
if search:
|
||
|
|
params['search'] = search
|
||
|
|
|
||
|
|
endpoint = "/api/v1/services"
|
||
|
|
if params:
|
||
|
|
query_string = "&".join(f"{k}={v}" for k, v in params.items())
|
||
|
|
endpoint = f"{endpoint}?{query_string}"
|
||
|
|
|
||
|
|
return self._request("GET", endpoint)
|
||
|
|
|
||
|
|
def get_service(self, service_id: str) -> Dict:
|
||
|
|
return self._request("GET", f"/api/v1/services/{service_id}")
|
||
|
|
|
||
|
|
def get_patterns(self, environment: Optional[str] = None) -> List[Dict]:
|
||
|
|
endpoint = "/api/v1/patterns"
|
||
|
|
if environment:
|
||
|
|
endpoint = f"{endpoint}?environment={environment}"
|
||
|
|
return self._request("GET", endpoint)
|
||
|
|
|
||
|
|
def get_pattern_services(self, pattern_id: str) -> List[Dict]:
|
||
|
|
return self._request("GET", f"/api/v1/patterns/{pattern_id}/services")
|
||
|
|
|
||
|
|
def get_groups(self) -> List[Dict]:
|
||
|
|
return self._request("GET", "/api/v1/groups")
|
||
|
|
|
||
|
|
def validate(self, services: List[Dict], pattern_id: Optional[str] = None) -> Dict:
|
||
|
|
payload = {
|
||
|
|
"services": services,
|
||
|
|
"validate_against_pattern": pattern_id,
|
||
|
|
}
|
||
|
|
return self._request("POST", "/api/v1/validate", json=payload)
|
||
|
|
|
||
|
|
def generate(
|
||
|
|
self,
|
||
|
|
pattern_id: str,
|
||
|
|
formats: List[str],
|
||
|
|
options: Optional[Dict] = None,
|
||
|
|
) -> Dict:
|
||
|
|
payload = {
|
||
|
|
"pattern_id": pattern_id,
|
||
|
|
"formats": formats,
|
||
|
|
"options": options or {},
|
||
|
|
}
|
||
|
|
return self._request("POST", "/api/v1/generate", json=payload)
|
||
|
|
|
||
|
|
def health(self) -> Dict:
|
||
|
|
return self._request("GET", "/api/v1/health")
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. Cliente Go
|
||
|
|
|
||
|
|
```go
|
||
|
|
// clients/go/service_registry.go
|
||
|
|
|
||
|
|
package serviceregistry
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"io/ioutil"
|
||
|
|
"net/http"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
type ServiceRegistry struct {
|
||
|
|
BaseURL string
|
||
|
|
Client *http.Client
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewServiceRegistry(apiURL string) *ServiceRegistry {
|
||
|
|
return &ServiceRegistry{
|
||
|
|
BaseURL: apiURL,
|
||
|
|
Client: &http.Client{
|
||
|
|
Timeout: 5 * time.Second,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (sr *ServiceRegistry) GetServices() ([]Service, error) {
|
||
|
|
endpoint := fmt.Sprintf("%s/api/v1/services", sr.BaseURL)
|
||
|
|
resp, err := sr.Client.Get(endpoint)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
var result struct {
|
||
|
|
Success bool `json:"success"`
|
||
|
|
Data []Service `json:"data"`
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
return result.Data, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (sr *ServiceRegistry) GetService(id string) (*Service, error) {
|
||
|
|
endpoint := fmt.Sprintf("%s/api/v1/services/%s", sr.BaseURL, id)
|
||
|
|
resp, err := sr.Client.Get(endpoint)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
var result struct {
|
||
|
|
Success bool `json:"success"`
|
||
|
|
Data Service `json:"data"`
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
return &result.Data, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (sr *ServiceRegistry) GetPatterns() ([]Pattern, error) {
|
||
|
|
endpoint := fmt.Sprintf("%s/api/v1/patterns", sr.BaseURL)
|
||
|
|
resp, err := sr.Client.Get(endpoint)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
var result struct {
|
||
|
|
Success bool `json:"success"`
|
||
|
|
Data []Pattern `json:"data"`
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
return result.Data, nil
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔗 Flujos de Integración
|
||
|
|
|
||
|
|
### 1. Flujo: syntaxis obtiene catálogo actualizado
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────┐
|
||
|
|
│ syntaxis (en cualquier momento, sin recompilación) │
|
||
|
|
└─────────────────────────────────────────────────────────┘
|
||
|
|
↓
|
||
|
|
GET /api/v1/services
|
||
|
|
↓
|
||
|
|
┌─────────────────────────────────────────────────────────┐
|
||
|
|
│ API Gateway (provisioning) │
|
||
|
|
│ ├─ Lee KCL definiciones en memoria │
|
||
|
|
│ ├─ Valida esquemas │
|
||
|
|
│ └─ Serializa a JSON │
|
||
|
|
└─────────────────────────────────────────────────────────┘
|
||
|
|
↓
|
||
|
|
JSON response con todos los servicios
|
||
|
|
↓
|
||
|
|
┌─────────────────────────────────────────────────────────┐
|
||
|
|
│ syntaxis - ServiceRegistryClient │
|
||
|
|
│ ├─ Parsea response │
|
||
|
|
│ ├─ Valida tipos Rust │
|
||
|
|
│ └─ Usa servicios (generación, validación, etc) │
|
||
|
|
└─────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
BENEFICIO: cambios en KCL reflejados automáticamente
|
||
|
|
SIN recompilación de syntaxis
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Flujo: Cambio en catalog KCL
|
||
|
|
|
||
|
|
```
|
||
|
|
1. DevOps modifica provisioning/services.k
|
||
|
|
└─ Agrega nuevo servicio o actualiza versión
|
||
|
|
|
||
|
|
2. CI/CD pipeline:
|
||
|
|
├─ Carga KCL
|
||
|
|
├─ Ejecuta validaciones (policies.k)
|
||
|
|
├─ Si valid: commit + push
|
||
|
|
└─ Si invalid: reject + notifica
|
||
|
|
|
||
|
|
3. provisioning/API server carga cambios:
|
||
|
|
├─ Detecta cambio en services.k
|
||
|
|
├─ Recompila KCL → tipos Rust internos
|
||
|
|
├─ Valida con policies.k
|
||
|
|
└─ Si valid: actualiza en memoria
|
||
|
|
|
||
|
|
4. Todos los consumidores obtienen nuevo catálogo:
|
||
|
|
├─ syntaxis: GET /api/v1/services
|
||
|
|
├─ Node.js app: GET /api/v1/services
|
||
|
|
├─ Python app: GET /api/v1/services
|
||
|
|
└─ Dashboard: GET /api/v1/services
|
||
|
|
|
||
|
|
BENEFICIO: cambio único, reflejado a todos
|
||
|
|
SIN recompilación de ningún consumidor
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Flujo: Generación de IaC para deployment
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────┐
|
||
|
|
│ DevOps quiere generar K8s manifest │
|
||
|
|
└─────────────────────────────────────┘
|
||
|
|
↓
|
||
|
|
POST /api/v1/generate
|
||
|
|
{
|
||
|
|
"pattern_id": "production",
|
||
|
|
"formats": ["kubernetes"],
|
||
|
|
"options": {
|
||
|
|
"namespace": "prod",
|
||
|
|
"registry": "docker.io"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
↓
|
||
|
|
┌─────────────────────────────────────┐
|
||
|
|
│ API Handler (provisioning) │
|
||
|
|
│ ├─ Carga patrón "production" │
|
||
|
|
│ ├─ Obtiene servicios del patrón │
|
||
|
|
│ ├─ Genera K8s manifests con Tera │
|
||
|
|
│ └─ Valida YAML con esquemas K8s │
|
||
|
|
└─────────────────────────────────────┘
|
||
|
|
↓
|
||
|
|
Response con YAML generado
|
||
|
|
↓
|
||
|
|
┌─────────────────────────────────────┐
|
||
|
|
│ DevOps │
|
||
|
|
│ ├─ Revisa YAML │
|
||
|
|
│ ├─ git commit + push │
|
||
|
|
│ └─ GitOps auto-deploys │
|
||
|
|
└─────────────────────────────────────┘
|
||
|
|
|
||
|
|
BENEFICIO: generación basada en KCL
|
||
|
|
SIN código Rust en el deployment
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔄 Estrategia de Migración
|
||
|
|
|
||
|
|
### Fase 0: Preparación (1 semana)
|
||
|
|
|
||
|
|
1. Documentar estado actual (TOML catalog en syntaxis)
|
||
|
|
2. Crear KCL equivalente en provisioning
|
||
|
|
3. Implementar API REST gateway en provisioning
|
||
|
|
4. Crear clientes multi-lenguaje
|
||
|
|
5. Escribir tests de equivalencia TOML → KCL
|
||
|
|
|
||
|
|
### Fase 1: Dual Mode (2 semanas)
|
||
|
|
|
||
|
|
```
|
||
|
|
┌──────────────────────────────────────┐
|
||
|
|
│ Ambos sistemas coexisten │
|
||
|
|
├──────────────────────────────────────┤
|
||
|
|
│ syntaxis puede usar: │
|
||
|
|
│ ├─ TOML directo (legacy) │
|
||
|
|
│ └─ API REST (nuevo) │
|
||
|
|
│ │
|
||
|
|
│ Validar que dan mismo resultado │
|
||
|
|
└──────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
**Actividades**:
|
||
|
|
- Executar tests comparativos
|
||
|
|
- Validar outputs generados son idénticos
|
||
|
|
- Medir performance (latencia API vs file read)
|
||
|
|
- Documentar diferencias
|
||
|
|
|
||
|
|
### Fase 2: API Primary (1 semana)
|
||
|
|
|
||
|
|
```
|
||
|
|
┌──────────────────────────────────────┐
|
||
|
|
│ syntaxis usa API por defecto │
|
||
|
|
├──────────────────────────────────────┤
|
||
|
|
│ ├─ Fallback a TOML si API no disponible
|
||
|
|
│ └─ Deprecate TOML para próximas versiones
|
||
|
|
└──────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
**Actividades**:
|
||
|
|
- Switch configuration en syntaxis
|
||
|
|
- Implementar reconnection logic
|
||
|
|
- Health check monitoring
|
||
|
|
- Setup alerting
|
||
|
|
|
||
|
|
### Fase 3: Otros Consumidores (2 semanas)
|
||
|
|
|
||
|
|
```
|
||
|
|
┌──────────────────────────────────────┐
|
||
|
|
│ Otros servicios integrados │
|
||
|
|
├──────────────────────────────────────┤
|
||
|
|
│ ├─ Node.js dashboard → API REST │
|
||
|
|
│ ├─ Python tools → API REST │
|
||
|
|
│ ├─ Go services → API REST │
|
||
|
|
│ └─ External tools → API REST │
|
||
|
|
└──────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
**Actividades**:
|
||
|
|
- Integrar clientes en cada servicio
|
||
|
|
- Test de carga (concurrent requests)
|
||
|
|
- Implementar caching en clientes
|
||
|
|
- Documentar integración
|
||
|
|
|
||
|
|
### Fase 4: Deprecación TOML (1 semana)
|
||
|
|
|
||
|
|
```
|
||
|
|
┌──────────────────────────────────────┐
|
||
|
|
│ TOML es legacy, usar KCL │
|
||
|
|
├──────────────────────────────────────┤
|
||
|
|
│ ├─ Remover código TOML de syntaxis │
|
||
|
|
│ ├─ Cleanup de archivos │
|
||
|
|
│ └─ Actualizar documentación │
|
||
|
|
└──────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ⚙️ Operaciones y Mantenimiento
|
||
|
|
|
||
|
|
### 1. Monitoreo
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// Métricas a monitorear
|
||
|
|
- API Response Time (p50, p95, p99)
|
||
|
|
- Request Rate (queries/sec)
|
||
|
|
- Error Rate (4xx, 5xx counts)
|
||
|
|
- KCL Compilation Time
|
||
|
|
- Uptime
|
||
|
|
- Cache Hit Ratio (si implementamos)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Backup y Recovery
|
||
|
|
|
||
|
|
```
|
||
|
|
Daily:
|
||
|
|
├─ Backup KCL definitions
|
||
|
|
├─ Backup API configuration
|
||
|
|
└─ Backup client libraries
|
||
|
|
|
||
|
|
Recovery:
|
||
|
|
├─ RTO: < 1 hour
|
||
|
|
├─ RPO: < 5 minutes
|
||
|
|
└─ Test quarterly
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Versionado
|
||
|
|
|
||
|
|
```
|
||
|
|
API:
|
||
|
|
├─ /api/v1/... (current)
|
||
|
|
├─ /api/v0/... (deprecated)
|
||
|
|
└─ Supported versions: last 2
|
||
|
|
|
||
|
|
KCL:
|
||
|
|
├─ services.k (version managed)
|
||
|
|
├─ patterns.k (version managed)
|
||
|
|
└─ Backward compatibility: 2 minor versions
|
||
|
|
|
||
|
|
Clients:
|
||
|
|
├─ Semantic versioning
|
||
|
|
├─ Compatibility matrix documented
|
||
|
|
└─ Auto-update via package managers
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📊 Resumen de Beneficios
|
||
|
|
|
||
|
|
| Aspecto | Antes (TOML) | Después (KCL + API) |
|
||
|
|
|---------|--------------|-------------------|
|
||
|
|
| **Source of Truth** | TOML en syntaxis | KCL en provisioning |
|
||
|
|
| **Recompilación** | Requerida para cambios | NO requerida |
|
||
|
|
| **Multi-lenguaje** | Requiere copiar archivos | REST API agnóstica |
|
||
|
|
| **Validación** | Manual en Rust | Automática en KCL |
|
||
|
|
| **Cambios** | Lento (recompile + deploy) | Rápido (solo KCL edit) |
|
||
|
|
| **Escalabilidad** | Difícil (múltiples copias) | Fácil (single source) |
|
||
|
|
| **Observabilidad** | Limitada | Completa (API logs) |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**Este documento proporciona el diseño detallado para implementar Option C (KCL + REST API). Los próximos pasos serían:**
|
||
|
|
|
||
|
|
1. **Validar con el equipo** que este diseño responde las 4 preguntas críticas
|
||
|
|
2. **Crear plan de implementación** basado en las 4 fases de migración
|
||
|
|
3. **Empezar con Fase 0** (preparación del equivalente KCL)
|
||
|
|
4. **Implementar API REST** en provisioning
|
||
|
|
5. **Migrar syntaxis** a usar API
|
||
|
|
6. **Onboard otros consumidores** (Node.js, Python, etc)
|