syntaxis/docs/provision/kcl-rest-api-design.md
Jesús Pérez 9cef9b8d57 refactor: consolidate configuration directories
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)
2025-12-26 18:36:23 +00:00

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

  1. Visión General
  2. Estructura de Definiciones KCL
  3. API REST Detallada
  4. Implementación Rust
  5. Clientes Multi-Lenguaje
  6. Flujos de Integración
  7. Estrategia de Migración
  8. 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)

  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

// 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)