Merge _configs/ into config/ for single configuration directory. Update all path references. Changes: - Move _configs/* to config/ - Update .gitignore for new patterns - No code references to _configs/ found Impact: -1 root directory (layout_conventions.md compliance)
47 KiB
47 KiB
🏗️ 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
- Visión General
- Estructura de Definiciones KCL
- API REST Detallada
- Implementación Rust
- Clientes Multi-Lenguaje
- Flujos de Integración
- Estrategia de Migración
- 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
# 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
# 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
# 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
# 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
// 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
# 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
# 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
# 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
# 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
# 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)
# 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
# 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
# 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
# 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)
# 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
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
// 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)
// 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
// 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
# 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
// 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)
- Documentar estado actual (TOML catalog en syntaxis)
- Crear KCL equivalente en provisioning
- Implementar API REST gateway en provisioning
- Crear clientes multi-lenguaje
- 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
// 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:
- Validar con el equipo que este diseño responde las 4 preguntas críticas
- Crear plan de implementación basado en las 4 fases de migración
- Empezar con Fase 0 (preparación del equivalente KCL)
- Implementar API REST en provisioning
- Migrar syntaxis a usar API
- Onboard otros consumidores (Node.js, Python, etc)