Compare commits
5 Commits
fc1c699795
...
2431636064
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2431636064 | ||
|
|
94742e9b9c | ||
|
|
49c09ff8fd | ||
|
|
d2a48fb549 | ||
|
|
93b0e5225c |
4
.gitignore
vendored
4
.gitignore
vendored
@ -111,3 +111,7 @@ Thumbs.db
|
||||
book-output/
|
||||
# Generated setup report
|
||||
SETUP_COMPLETE.md
|
||||
|
||||
# ML model caches (fastembed, huggingface, etc.)
|
||||
**/.fastembed_cache/
|
||||
**/.cache/huggingface/
|
||||
|
||||
@ -41,6 +41,18 @@ repos:
|
||||
# pass_filenames: false
|
||||
# stages: [pre-push]
|
||||
|
||||
# ============================================================================
|
||||
# SOLID Architecture Boundary Enforcement
|
||||
# ============================================================================
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: solid-boundary-check
|
||||
name: SOLID Architecture Boundaries
|
||||
entry: .pre-commit-hooks/solid-boundary-check.sh
|
||||
language: script
|
||||
pass_filenames: false
|
||||
stages: [pre-commit]
|
||||
|
||||
# ============================================================================
|
||||
# Nushell Hooks (ACTIVE)
|
||||
# ============================================================================
|
||||
|
||||
27
.pre-commit-hooks/solid-boundary-check.sh
Executable file
27
.pre-commit-hooks/solid-boundary-check.sh
Executable file
@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VIOLATIONS=$(git diff --cached --name-only --diff-filter=ACM |
|
||||
grep -E "\.(nu|rs)$" |
|
||||
grep -v "templates/" |
|
||||
grep -v "extensions/providers/" |
|
||||
grep -v "orchestrator/" |
|
||||
xargs grep -lE "^\^hcloud|^\^aws |^\^doctl|hcloud server" 2>/dev/null |
|
||||
grep -v "^$") || true
|
||||
|
||||
if [ -n "$VIOLATIONS" ]; then
|
||||
echo "SOLID VIOLATION: Provider API calls outside orchestrator:"
|
||||
echo "$VIOLATIONS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SSH_VIOLATIONS=$(git diff --cached --name-only --diff-filter=ACM |
|
||||
grep -E "\.(rs)$" |
|
||||
grep -E "control-center|vault-service" |
|
||||
xargs grep -lE "ssh2?::|russh::" 2>/dev/null) || true
|
||||
|
||||
if [ -n "$SSH_VIOLATIONS" ]; then
|
||||
echo "SOLID VIOLATION: SSH code outside orchestrator:"
|
||||
echo "$SSH_VIOLATIONS"
|
||||
exit 1
|
||||
fi
|
||||
@ -22,7 +22,7 @@ TypeDialog enables interactive form-based configuration from Nickel schemas.
|
||||
├── templates/ # Jinja2 templates for schema rendering
|
||||
│ └── service-form.template.j2
|
||||
├── schemas/ # Symlink to Nickel schemas
|
||||
│ └── platform/schemas/ → ../../../schemas/platform/schemas/
|
||||
│ └── platform/schemas/ → ../../../schemas/platform/
|
||||
└── constraints/ # Validation constraints
|
||||
└── constraints.toml # Shared validation rules
|
||||
```
|
||||
@ -97,7 +97,7 @@ typedialog --version
|
||||
|
||||
```toml
|
||||
# Batch generate all forms
|
||||
for schema in provisioning/schemas/platform/schemas/*.ncl; do
|
||||
for schema in provisioning/schemas/platform/*.ncl; do
|
||||
service=$(basename $schema .ncl)
|
||||
typedialog generate-form
|
||||
--schema $schema
|
||||
|
||||
14
Cargo.toml
14
Cargo.toml
@ -2,6 +2,8 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/platform-config",
|
||||
"crates/platform-nats",
|
||||
"crates/platform-db",
|
||||
"crates/service-clients",
|
||||
"crates/ai-service",
|
||||
"crates/extension-registry",
|
||||
@ -97,12 +99,18 @@ resolver = "2"
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "chrono", "uuid"] }
|
||||
surrealdb = { version = "2.6", features = ["kv-mem", "protocol-ws", "protocol-http"] }
|
||||
|
||||
# ============================================================================
|
||||
# MESSAGING (NATS)
|
||||
# ============================================================================
|
||||
async-nats = "0.40"
|
||||
|
||||
# ============================================================================
|
||||
# SECURITY AND CRYPTOGRAPHY
|
||||
# ============================================================================
|
||||
aes-gcm = "0.10"
|
||||
argon2 = "0.5"
|
||||
base64 = "0.22"
|
||||
git2 = { version = "0.20", default-features = false, features = ["https", "ssh"] }
|
||||
hmac = "0.12"
|
||||
jsonwebtoken = { version = "10.3", features = ["rust_crypto"] }
|
||||
rand = { version = "0.9", features = ["std_rng", "os_rng"] }
|
||||
@ -261,6 +269,8 @@ resolver = "2"
|
||||
# INTERNAL WORKSPACE CRATES (Local path dependencies)
|
||||
# ============================================================================
|
||||
platform-config = { path = "./crates/platform-config" }
|
||||
platform-nats = { path = "./crates/platform-nats" }
|
||||
platform-db = { path = "./crates/platform-db" }
|
||||
service-clients = { path = "./crates/service-clients" }
|
||||
rag = { path = "./crates/rag" }
|
||||
mcp-server = { path = "./crates/mcp-server" }
|
||||
@ -282,9 +292,9 @@ resolver = "2"
|
||||
stratum-llm = { path = "./stratumiops/crates/stratum-llm", features = ["anthropic", "openai", "ollama"] }
|
||||
|
||||
# ============================================================================
|
||||
# SECRETUMVAULT (Enterprise Secrets Management - optional)
|
||||
# SECRETUMVAULT (Enterprise Secrets Management - canonical source)
|
||||
# ============================================================================
|
||||
secretumvault = { path = "./secretumvault" }
|
||||
secretumvault = { path = "../../../Development/secretumvault", features = ["surrealdb-storage", "filesystem", "server", "cedar"] }
|
||||
|
||||
# ============================================================================
|
||||
# WASM/WEB-SPECIFIC DEPENDENCIES
|
||||
|
||||
109
config/README.md
109
config/README.md
@ -1,109 +0,0 @@
|
||||
# Platform Service Configuration Files
|
||||
|
||||
This directory contains **16 production-ready TOML configuration files** generated from Nickel schemas
|
||||
for all platform services across all deployment modes.
|
||||
|
||||
## Generated Files
|
||||
|
||||
**4 Services × 4 Deployment Modes = 16 Configuration Files**
|
||||
|
||||
```toml
|
||||
orchestrator.{solo,multiuser,cicd,enterprise}.toml (2.2 kB each)
|
||||
control-center.{solo,multiuser,cicd,enterprise}.toml (3.4 kB each)
|
||||
mcp-server.{solo,multiuser,cicd,enterprise}.toml (2.7 kB each)
|
||||
installer.{solo,multiuser,cicd,enterprise}.toml (2.5 kB each)
|
||||
```
|
||||
|
||||
**Total**: ~45 KB, all validated and ready for deployment
|
||||
|
||||
## Deployment Modes
|
||||
|
||||
| Mode | Resources | Database | Use Case | Load |
|
||||
| ------ | ----------- | ---------- | ---------- | ------ |
|
||||
| **solo** | 2 CPU, 4 GB | Embedded | Development | `ORCHESTRATOR_MODE=solo` |
|
||||
| **multiuser** | 4 CPU, 8 GB | PostgreSQL/SurrealDB | Team Staging | `ORCHESTRATOR_MODE=multiuser` |
|
||||
| **cicd** | 8 CPU, 16 GB | Ephemeral | CI/CD Pipelines | `ORCHESTRATOR_MODE=cicd` |
|
||||
| **enterprise** | 16+ CPU, 32+ GB | SurrealDB HA | Production | `ORCHESTRATOR_MODE=enterprise` |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Load a configuration mode
|
||||
|
||||
```toml
|
||||
# Solo mode (single developer)
|
||||
export ORCHESTRATOR_MODE=solo
|
||||
export CONTROL_CENTER_MODE=solo
|
||||
|
||||
# Multiuser mode (team development)
|
||||
export ORCHESTRATOR_MODE=multiuser
|
||||
export CONTROL_CENTER_MODE=multiuser
|
||||
|
||||
# Enterprise mode (production HA)
|
||||
export ORCHESTRATOR_MODE=enterprise
|
||||
export CONTROL_CENTER_MODE=enterprise
|
||||
```
|
||||
|
||||
### Override individual fields
|
||||
|
||||
```javascript
|
||||
export ORCHESTRATOR_SERVER_WORKERS=8
|
||||
export ORCHESTRATOR_SERVER_PORT=9090
|
||||
export CONTROL_CENTER_REQUIRE_MFA=true
|
||||
```
|
||||
|
||||
## Configuration Loading Hierarchy
|
||||
|
||||
Each service loads configuration with this priority:
|
||||
|
||||
1. **Explicit path** — `{SERVICE}_CONFIG` environment variable
|
||||
2. **Mode-specific** — `{SERVICE}_MODE` → `provisioning/platform/config/{service}.{mode}.toml`
|
||||
3. **Legacy** — `config.user.toml` (backward compatibility)
|
||||
4. **Defaults** — `config.defaults.toml` or built-in
|
||||
5. **Field overrides** — `{SERVICE}_*` environment variables
|
||||
|
||||
## Docker Compose Integration
|
||||
|
||||
```javascript
|
||||
export DEPLOYMENT_MODE=multiuser
|
||||
docker-compose -f provisioning/platform/infrastructure/docker/docker-compose.yml up
|
||||
```
|
||||
|
||||
## Kubernetes Integration
|
||||
|
||||
```yaml
|
||||
# Load enterprise mode configs into K8s
|
||||
kubectl create configmap orchestrator-config
|
||||
--from-file=provisioning/platform/config/orchestrator.enterprise.toml
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
Verify all configs parse correctly:
|
||||
|
||||
```toml
|
||||
for file in *.toml; do
|
||||
nu -c "open '$file'" && echo "✅ $file" || echo "❌ $file"
|
||||
done
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
- **orchestrator.*.toml** — Workflow engine configuration
|
||||
- **control-center.*.toml** — Policy/RBAC backend configuration
|
||||
- **mcp-server.*.toml** — MCP server configuration
|
||||
- **installer.*.toml** — Installation/bootstrap configuration
|
||||
|
||||
Each file contains service-specific settings for networking, storage, security, logging, and monitoring.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- **Configuration workflow**: `provisioning/.typedialog/provisioning/platform/configuration-workflow.md`
|
||||
- **Usage guide**: `provisioning/.typedialog/provisioning/platform/usage-guide.md`
|
||||
- **Schema definitions**: `provisioning/.typedialog/provisioning/platform/schemas/`
|
||||
- **Default values**: `provisioning/.typedialog/provisioning/platform/defaults/`
|
||||
|
||||
## Generated By
|
||||
|
||||
**Framework**: TypeDialog + Nickel Configuration System
|
||||
**Date**: 2026-01-05
|
||||
**Status**: ✅ Production Ready
|
||||
@ -1,32 +0,0 @@
|
||||
# CoreDNS Configuration for Provisioning Platform
|
||||
# Provides local DNS resolution for services
|
||||
|
||||
.:5353 {
|
||||
# Forward to upstream DNS
|
||||
forward . 8.8.8.8 8.8.4.4
|
||||
|
||||
# Logging
|
||||
log
|
||||
|
||||
# Error handling
|
||||
errors
|
||||
|
||||
# Cache
|
||||
cache 30
|
||||
}
|
||||
|
||||
provisioning.local:5353 {
|
||||
# Local zone file
|
||||
file /zones/provisioning.zone
|
||||
|
||||
# Logging
|
||||
log
|
||||
|
||||
# Error handling
|
||||
errors
|
||||
}
|
||||
|
||||
# Health check zone
|
||||
health.check:5353 {
|
||||
whoami
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
$ORIGIN provisioning.local.
|
||||
$TTL 3600
|
||||
|
||||
@ IN SOA ns.provisioning.local. admin.provisioning.local. (
|
||||
2024100601 ; Serial
|
||||
3600 ; Refresh
|
||||
1800 ; Retry
|
||||
604800 ; Expire
|
||||
86400 ; Minimum TTL
|
||||
)
|
||||
|
||||
@ IN NS ns.provisioning.local.
|
||||
|
||||
ns IN A 127.0.0.1
|
||||
orchestrator IN A 127.0.0.1
|
||||
control-center IN A 127.0.0.1
|
||||
gitea IN A 127.0.0.1
|
||||
oci-registry IN A 127.0.0.1
|
||||
mcp-server IN A 127.0.0.1
|
||||
api-gateway IN A 127.0.0.1
|
||||
|
||||
; Service discovery
|
||||
api IN CNAME orchestrator.provisioning.local.
|
||||
ui IN CNAME control-center.provisioning.local.
|
||||
git IN CNAME gitea.provisioning.local.
|
||||
registry IN CNAME oci-registry.provisioning.local.
|
||||
@ -1,201 +0,0 @@
|
||||
# Platform Configuration Examples
|
||||
|
||||
This directory contains example Nickel files demonstrating how to generate platform configurations for different deployment modes.
|
||||
|
||||
## File Structure
|
||||
|
||||
```bash
|
||||
examples/
|
||||
├── README.md # This file
|
||||
├── orchestrator.solo.example.ncl # Solo deployment (1 CPU, 1GB memory)
|
||||
├── orchestrator.multiuser.example.ncl # Multiuser deployment (2 CPU, 2GB memory, HA)
|
||||
├── orchestrator.enterprise.example.ncl # Enterprise deployment (4 CPU, 4GB memory, 3 replicas)
|
||||
└── control-center.solo.example.ncl # Control Center solo deployment
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
To generate actual TOML configuration from an example:
|
||||
|
||||
```toml
|
||||
# Export to TOML (placed in runtime/generated/)
|
||||
nickel export --format toml examples/orchestrator.solo.example.ncl > runtime/generated/orchestrator.solo.toml
|
||||
|
||||
# Export to JSON for inspection
|
||||
nickel export --format json examples/orchestrator.solo.example.ncl | jq .
|
||||
|
||||
# Type check example
|
||||
nickel typecheck examples/orchestrator.solo.example.ncl
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### 1. Schemas Reference
|
||||
All examples import from the schema library:
|
||||
- `provisioning/schemas/platform/schemas/orchestrator.ncl`
|
||||
- `provisioning/schemas/platform/defaults/orchestrator-defaults.ncl`
|
||||
|
||||
### 2. Mode-Based Composition
|
||||
Each example uses composition helpers to overlay mode-specific settings:
|
||||
|
||||
```javascript
|
||||
let helpers = import "../../schemas/platform/common/helpers.ncl" in
|
||||
let defaults = import "../../schemas/platform/defaults/orchestrator-defaults.ncl" in
|
||||
let mode = import "../../schemas/platform/defaults/deployment/solo-defaults.ncl" in
|
||||
|
||||
helpers.compose_config defaults mode {
|
||||
# User-specific overrides here
|
||||
}
|
||||
```
|
||||
|
||||
### 3. ConfigLoader Integration
|
||||
Generated TOML files are automatically loaded by Rust services:
|
||||
|
||||
```toml
|
||||
use platform_config::OrchestratorConfig;
|
||||
|
||||
let config = OrchestratorConfig::load().expect("Failed to load orchestrator config");
|
||||
println!("Orchestrator listening on port: {}", config.server.port);
|
||||
```
|
||||
|
||||
## Mode Reference
|
||||
|
||||
| Mode | CPU | Memory | Replicas | Use Case |
|
||||
| ------ | ----- | -------- | ---------- | ---------- |
|
||||
| **solo** | 1.0 | 1024M | 1 | Development, testing |
|
||||
| **multiuser** | 2.0 | 2048M | 2 | Staging, small production |
|
||||
| **enterprise** | 4.0 | 4096M | 3+ | Large production deployments |
|
||||
| **cicd** | 2.0 | 2048M | 1 | CI/CD pipelines |
|
||||
|
||||
## Workflow: Platform Configuration
|
||||
|
||||
1. **Choose deployment mode** → select example file (orchestrator.solo.example.ncl, etc.)
|
||||
2. **Customize if needed** → modify the example
|
||||
3. **Generate config** → `nickel export --format toml`
|
||||
4. **Place in runtime/generated/** → ConfigLoader picks it up automatically
|
||||
5. **Service reads config** → via platform-config crate
|
||||
|
||||
## Infrastructure Generation
|
||||
|
||||
These platform configuration examples work together with infrastructure schemas to create complete deployments.
|
||||
|
||||
### Complete Infrastructure Stack
|
||||
|
||||
Beyond platform configs, you can generate complete infrastructure from schemas:
|
||||
|
||||
**Infrastructure Examples**:
|
||||
- `provisioning/schemas/infrastructure/examples-solo-deployment.ncl` - Solo infrastructure
|
||||
- `provisioning/schemas/infrastructure/examples-enterprise-deployment.ncl` - Enterprise infrastructure
|
||||
|
||||
**What Gets Generated**:
|
||||
|
||||
```bash
|
||||
# Solo deployment infrastructure
|
||||
nickel export --format json provisioning/schemas/infrastructure/examples-solo-deployment.ncl
|
||||
|
||||
# Exports:
|
||||
# - docker_compose_services (5 services)
|
||||
# - nginx_config (load balancer setup)
|
||||
# - prometheus_config (4 scrape jobs)
|
||||
# - oci_registry_config (container registry)
|
||||
```
|
||||
|
||||
**Integration Pattern**:
|
||||
|
||||
```bash
|
||||
Platform Config (Orchestrator, Control Center, etc.)
|
||||
↓ ConfigLoader reads TOML
|
||||
↓ Services start with config
|
||||
|
||||
Infrastructure Config (Docker, Nginx, Prometheus, etc.)
|
||||
↓ nickel export → YAML/JSON
|
||||
↓ Deploy with Docker/Kubernetes/Nginx
|
||||
```
|
||||
|
||||
### Generation and Validation
|
||||
|
||||
**Generate all infrastructure configs**:
|
||||
|
||||
```toml
|
||||
provisioning/platform/scripts/generate-infrastructure-configs.nu --mode solo --format yaml
|
||||
provisioning/platform/scripts/generate-infrastructure-configs.nu --mode enterprise --format json
|
||||
```
|
||||
|
||||
**Validate generated configs**:
|
||||
|
||||
```toml
|
||||
provisioning/platform/scripts/validate-infrastructure.nu --config-dir /tmp/infra
|
||||
|
||||
# Output shows validation results for:
|
||||
# - Docker Compose (docker-compose config --quiet)
|
||||
# - Kubernetes (kubectl apply --dry-run=client)
|
||||
# - Nginx (nginx -t)
|
||||
# - Prometheus (promtool check config)
|
||||
```
|
||||
|
||||
**Interactive setup**:
|
||||
|
||||
```bash
|
||||
bash provisioning/platform/scripts/setup-with-forms.sh
|
||||
# Uses TypeDialog bash wrappers (TTY-safe) or basic Nushell prompts as fallback
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
If configuration fails to load:
|
||||
|
||||
```toml
|
||||
# Validate Nickel syntax
|
||||
nickel typecheck examples/orchestrator.solo.example.ncl
|
||||
|
||||
# Check TOML validity
|
||||
cargo test --package platform-config --test validation
|
||||
|
||||
# Verify path resolution
|
||||
provisioning validate-config --check-paths
|
||||
```
|
||||
|
||||
## Environment Variable Overrides
|
||||
|
||||
Even with TOML configs, environment variables take precedence:
|
||||
|
||||
```javascript
|
||||
export PROVISIONING_MODE=multiuser
|
||||
export ORCHESTRATOR_PORT=9000
|
||||
provisioning orchestrator start # Uses env overrides
|
||||
```
|
||||
|
||||
## Adding New Configurations
|
||||
|
||||
To add a new service configuration:
|
||||
|
||||
1. Create `service-name.mode.example.ncl` in this directory
|
||||
2. Import the service schema: `import "../../schemas/platform/schemas/service-name.ncl"`
|
||||
3. Compose using helpers: `helpers.compose_config defaults mode {}`
|
||||
4. Document in this README
|
||||
5. Test with: `nickel typecheck` and `nickel export --format json`
|
||||
|
||||
## Platform vs Infrastructure Configuration
|
||||
|
||||
**Platform Configuration** (this directory):
|
||||
- Service-specific settings (port, database host, logging level)
|
||||
- Loaded by ConfigLoader at service startup
|
||||
- Format: TOML files in `runtime/generated/`
|
||||
- Examples: orchestrator.solo.example.ncl, orchestrator.multiuser.example.ncl
|
||||
|
||||
**Infrastructure Configuration** (provisioning/schemas/infrastructure/):
|
||||
- Deployment-specific settings (replicas, resources, networking)
|
||||
- Generated and validated separately
|
||||
- Formats: YAML (Docker/Kubernetes), JSON (registries), conf (Nginx)
|
||||
- Examples: examples-solo-deployment.ncl, examples-enterprise-deployment.ncl
|
||||
|
||||
**Why Both?**:
|
||||
- Platform config: How should Orchestrator behave? (internal settings)
|
||||
- Infrastructure config: How should Orchestrator be deployed? (external deployment)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-01-06 (Updated with Infrastructure Integration Guide)
|
||||
**ConfigLoader Version**: 2.0.0
|
||||
**Nickel Version**: Latest
|
||||
**Infrastructure Integration**: Complete with schemas, examples, and validation scripts
|
||||
@ -1,151 +0,0 @@
|
||||
# Orchestrator Configuration Example - Enterprise Deployment Mode
|
||||
#
|
||||
# This example shows large-scale enterprise deployments with full HA,
|
||||
# 3 replicas, distributed storage, and comprehensive monitoring.
|
||||
#
|
||||
# Usage:
|
||||
# nickel export --format toml orchestrator.enterprise.example.ncl > orchestrator.enterprise.toml
|
||||
# nickel export --format json orchestrator.enterprise.example.ncl | jq
|
||||
|
||||
{
|
||||
workspace = {
|
||||
root_path = "/var/provisioning/workspace",
|
||||
data_path = "/mnt/provisioning/workspace/data",
|
||||
state_path = "/mnt/provisioning/workspace/state",
|
||||
cache_path = "/var/cache/provisioning",
|
||||
isolation_level = 'kubernetes,
|
||||
execution_mode = 'distributed,
|
||||
},
|
||||
|
||||
server = {
|
||||
address = "0.0.0.0",
|
||||
port = 8080,
|
||||
tls = true,
|
||||
tls_cert = "/etc/provisioning/certs/server.crt",
|
||||
tls_key = "/etc/provisioning/certs/server.key",
|
||||
tls_client_cert = "/etc/provisioning/certs/client-ca.crt",
|
||||
tls_require_client_cert = true,
|
||||
cors = {
|
||||
enabled = true,
|
||||
allowed_origins = [
|
||||
"https://control-center.production.svc:8081",
|
||||
"https://api.provisioning.example.com",
|
||||
],
|
||||
allowed_methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"],
|
||||
},
|
||||
rate_limiting = {
|
||||
enabled = true,
|
||||
requests_per_second = 5000,
|
||||
burst_size = 500,
|
||||
},
|
||||
request_timeout = 30000,
|
||||
keepalive_timeout = 75000,
|
||||
},
|
||||
|
||||
storage = {
|
||||
backend = 's3,
|
||||
s3 = {
|
||||
bucket = "provisioning-enterprise",
|
||||
region = "us-east-1",
|
||||
endpoint = "https://s3.us-east-1.amazonaws.com",
|
||||
},
|
||||
max_size = 1099511627776, # 1TB
|
||||
cache_enabled = true,
|
||||
cache_ttl = 14400, # 4 hours
|
||||
replication = {
|
||||
enabled = true,
|
||||
regions = ["us-west-2"],
|
||||
},
|
||||
},
|
||||
|
||||
queue = {
|
||||
max_concurrent_tasks = 100,
|
||||
retry_attempts = 7,
|
||||
retry_delay = 30000,
|
||||
retry_backoff = 'exponential,
|
||||
task_timeout = 14400000, # 4 hours
|
||||
persist = true,
|
||||
dead_letter_queue = {
|
||||
enabled = true,
|
||||
max_size = 100000,
|
||||
retention_days = 30,
|
||||
},
|
||||
priority_queue = true,
|
||||
metrics = true,
|
||||
distributed = true,
|
||||
redis = {
|
||||
cluster = "redis-provisioning",
|
||||
nodes = ["redis-1", "redis-2", "redis-3"],
|
||||
},
|
||||
},
|
||||
|
||||
database = {
|
||||
host = "postgres-primary.provisioning.svc",
|
||||
port = 5432,
|
||||
username = "provisioning",
|
||||
pool_size = 50,
|
||||
pool_idle_timeout = 900,
|
||||
connection_timeout = 30000,
|
||||
ssl = true,
|
||||
},
|
||||
|
||||
logging = {
|
||||
level = 'info,
|
||||
format = 'json,
|
||||
output = 'file,
|
||||
file = "/var/log/provisioning/orchestrator.log",
|
||||
max_size = 1073741824, # 1GB
|
||||
retention_days = 90,
|
||||
},
|
||||
|
||||
monitoring = {
|
||||
enabled = true,
|
||||
metrics_port = 9090,
|
||||
health_check_interval = 5,
|
||||
prometheus = {
|
||||
enabled = true,
|
||||
scrape_interval = "10s",
|
||||
remote_write = {
|
||||
url = "https://prometheus-remote.example.com/api/v1/write",
|
||||
queue_capacity = 10000,
|
||||
},
|
||||
},
|
||||
jaeger = {
|
||||
enabled = true,
|
||||
endpoint = "http://jaeger-collector.observability.svc:14268/api/traces",
|
||||
sample_rate = 0.1,
|
||||
},
|
||||
},
|
||||
|
||||
security = {
|
||||
enable_auth = true,
|
||||
auth_backend = 'local,
|
||||
token_expiry = 1800,
|
||||
enable_rbac = true,
|
||||
enable_audit_log = true,
|
||||
audit_log_path = "/var/log/provisioning/audit.log",
|
||||
},
|
||||
|
||||
mode = 'enterprise,
|
||||
|
||||
resources = {
|
||||
cpus = "4.0",
|
||||
memory = "4096M",
|
||||
disk = "1T",
|
||||
},
|
||||
|
||||
# Enterprise HA setup: 3 replicas with leader election
|
||||
replicas = 3,
|
||||
replica_sync = {
|
||||
enabled = true,
|
||||
sync_interval = 1000, # Faster sync for consistency
|
||||
quorum_required = true,
|
||||
},
|
||||
leader_election = {
|
||||
enabled = true,
|
||||
backend = 'etcd,
|
||||
etcd_endpoints = ["etcd-0.etcd", "etcd-1.etcd", "etcd-2.etcd"],
|
||||
lease_duration = 15,
|
||||
},
|
||||
|
||||
}
|
||||
@ -1,113 +0,0 @@
|
||||
# Orchestrator Configuration Example - Multiuser Deployment Mode
|
||||
#
|
||||
# This example shows multiuser deployments with HA setup (2 replicas)
|
||||
# and moderate resource allocation for staging/production.
|
||||
#
|
||||
# Usage:
|
||||
# nickel export --format toml orchestrator.multiuser.example.ncl > orchestrator.multiuser.toml
|
||||
# nickel export --format json orchestrator.multiuser.example.ncl | jq
|
||||
|
||||
{
|
||||
workspace = {
|
||||
root_path = "/var/provisioning/workspace",
|
||||
data_path = "/var/provisioning/workspace/data",
|
||||
state_path = "/var/provisioning/workspace/state",
|
||||
cache_path = "/var/provisioning/workspace/cache",
|
||||
isolation_level = 'container,
|
||||
execution_mode = 'distributed,
|
||||
},
|
||||
|
||||
server = {
|
||||
address = "0.0.0.0",
|
||||
port = 8080,
|
||||
tls = true,
|
||||
tls_cert = "/etc/provisioning/certs/server.crt",
|
||||
tls_key = "/etc/provisioning/certs/server.key",
|
||||
cors = {
|
||||
enabled = true,
|
||||
allowed_origins = ["https://control-center:8081"],
|
||||
allowed_methods = ["GET", "POST", "PUT", "DELETE", "PATCH"],
|
||||
},
|
||||
rate_limiting = {
|
||||
enabled = true,
|
||||
requests_per_second = 500,
|
||||
burst_size = 100,
|
||||
},
|
||||
},
|
||||
|
||||
storage = {
|
||||
backend = 's3,
|
||||
s3 = {
|
||||
bucket = "provisioning-storage",
|
||||
region = "us-east-1",
|
||||
endpoint = "https://s3.amazonaws.com",
|
||||
},
|
||||
max_size = 107374182400, # 100GB
|
||||
cache_enabled = true,
|
||||
cache_ttl = 7200, # 2 hours
|
||||
},
|
||||
|
||||
queue = {
|
||||
max_concurrent_tasks = 20,
|
||||
retry_attempts = 5,
|
||||
retry_delay = 10000,
|
||||
task_timeout = 7200000,
|
||||
persist = true,
|
||||
dead_letter_queue = {
|
||||
enabled = true,
|
||||
max_size = 10000,
|
||||
},
|
||||
priority_queue = true,
|
||||
metrics = true,
|
||||
},
|
||||
|
||||
database = {
|
||||
host = "postgres.provisioning.svc",
|
||||
port = 5432,
|
||||
username = "provisioning",
|
||||
pool_size = 20,
|
||||
connection_timeout = 15000,
|
||||
ssl = true,
|
||||
},
|
||||
|
||||
logging = {
|
||||
level = 'info,
|
||||
format = 'json,
|
||||
output = 'file,
|
||||
file = "/var/log/provisioning/orchestrator.log",
|
||||
max_size = 104857600, # 100MB
|
||||
retention_days = 30,
|
||||
},
|
||||
|
||||
monitoring = {
|
||||
enabled = true,
|
||||
metrics_port = 9090,
|
||||
health_check_interval = 10,
|
||||
prometheus = {
|
||||
enabled = true,
|
||||
scrape_interval = "15s",
|
||||
},
|
||||
},
|
||||
|
||||
security = {
|
||||
enable_auth = false,
|
||||
auth_backend = 'local,
|
||||
token_expiry = 3600,
|
||||
enable_rbac = false,
|
||||
},
|
||||
|
||||
mode = 'multiuser,
|
||||
|
||||
resources = {
|
||||
cpus = "2.0",
|
||||
memory = "2048M",
|
||||
disk = "100G",
|
||||
},
|
||||
|
||||
# Multiuser-specific: HA replicas
|
||||
replicas = 2,
|
||||
replica_sync = {
|
||||
enabled = true,
|
||||
sync_interval = 5000,
|
||||
},
|
||||
}
|
||||
@ -1,104 +0,0 @@
|
||||
# Orchestrator Configuration Example - Solo Deployment Mode
|
||||
#
|
||||
# This example shows how to configure the orchestrator for
|
||||
# solo (single-node) deployments with minimal resource allocation.
|
||||
#
|
||||
# Usage:
|
||||
# nickel export --format toml orchestrator.solo.example.ncl > orchestrator.solo.toml
|
||||
# nickel export --format json orchestrator.solo.example.ncl | jq
|
||||
#
|
||||
# This configuration will be loaded by ConfigLoader at runtime.
|
||||
|
||||
{
|
||||
# Workspace configuration for solo mode
|
||||
workspace = {
|
||||
root_path = "/var/provisioning/workspace",
|
||||
data_path = "/var/provisioning/workspace/data",
|
||||
state_path = "/var/provisioning/workspace/state",
|
||||
cache_path = "/var/provisioning/workspace/cache",
|
||||
isolation_level = 'process,
|
||||
execution_mode = 'local,
|
||||
},
|
||||
|
||||
# HTTP server settings - solo mode uses port 8080
|
||||
server = {
|
||||
address = "0.0.0.0",
|
||||
port = 8080,
|
||||
tls = false,
|
||||
cors = {
|
||||
enabled = true,
|
||||
allowed_origins = ["*"],
|
||||
allowed_methods = ["GET", "POST", "PUT", "DELETE"],
|
||||
},
|
||||
rate_limiting = {
|
||||
enabled = true,
|
||||
requests_per_second = 100,
|
||||
burst_size = 50,
|
||||
},
|
||||
},
|
||||
|
||||
# Storage configuration for solo mode (local filesystem)
|
||||
storage = {
|
||||
backend = 'filesystem,
|
||||
path = "/var/provisioning/storage",
|
||||
max_size = 10737418240, # 10GB
|
||||
cache_enabled = true,
|
||||
cache_ttl = 3600, # 1 hour
|
||||
},
|
||||
|
||||
# Queue configuration - conservative for solo
|
||||
queue = {
|
||||
max_concurrent_tasks = 5,
|
||||
retry_attempts = 3,
|
||||
retry_delay = 5000,
|
||||
task_timeout = 3600000,
|
||||
persist = true,
|
||||
dead_letter_queue = {
|
||||
enabled = true,
|
||||
max_size = 1000,
|
||||
},
|
||||
priority_queue = false,
|
||||
metrics = false,
|
||||
},
|
||||
|
||||
# Database configuration
|
||||
database = {
|
||||
host = "localhost",
|
||||
port = 5432,
|
||||
username = "provisioning",
|
||||
password = "changeme", # Should use secrets in production
|
||||
pool_size = 5,
|
||||
connection_timeout = 10000,
|
||||
},
|
||||
|
||||
# Logging configuration
|
||||
logging = {
|
||||
level = 'info,
|
||||
format = 'json,
|
||||
output = 'stdout,
|
||||
},
|
||||
|
||||
# Monitoring configuration
|
||||
monitoring = {
|
||||
enabled = true,
|
||||
metrics_port = 9090,
|
||||
health_check_interval = 30,
|
||||
},
|
||||
|
||||
# Security configuration
|
||||
security = {
|
||||
enable_auth = false, # Can be enabled later
|
||||
auth_backend = 'local,
|
||||
token_expiry = 86400,
|
||||
},
|
||||
|
||||
# Deployment mode identifier
|
||||
mode = 'solo,
|
||||
|
||||
# Resource limits
|
||||
resources = {
|
||||
cpus = "1.0",
|
||||
memory = "1024M",
|
||||
disk = "10G",
|
||||
},
|
||||
}
|
||||
78
config/external-services.ncl
Normal file
78
config/external-services.ncl
Normal file
@ -0,0 +1,78 @@
|
||||
# External Infrastructure Services Configuration
|
||||
# Defines the external services (databases, registries, CI/CD, etc.) that the platform integrates with
|
||||
# These services are NOT managed by provisioning, only monitored for health/status
|
||||
#
|
||||
# Schema validation: Loaded from provisioning/schemas/platform/external-services.ncl
|
||||
|
||||
let schema = import "schemas/platform/external-services.ncl" in
|
||||
|
||||
[
|
||||
# SecretumVault - Secrets management and encryption
|
||||
({
|
||||
name = "svault_server-vault",
|
||||
srvc = "vault",
|
||||
desc = "SecretumVault server for secrets management and encryption",
|
||||
url = "http://127.0.0.1:8082",
|
||||
port = 8082,
|
||||
required = true,
|
||||
dependencies = [],
|
||||
binary_path = "~/.local/bin/svault",
|
||||
startup_command = "svault server --config ~/.config/provisioning/secretumvault-dev.toml",
|
||||
health_check_timeout = 5,
|
||||
} | schema.ExternalService),
|
||||
|
||||
# SurrealDB - Multi-model database
|
||||
({
|
||||
name = "surrealdb-dbs",
|
||||
srvc = "dbs",
|
||||
desc = "SurrealDB multi-model database for data storage and queries",
|
||||
url = "http://127.0.0.1:8000",
|
||||
port = 8000,
|
||||
required = true,
|
||||
dependencies = [],
|
||||
} | schema.ExternalService),
|
||||
|
||||
# PostgreSQL - Database for Forgejo and Woodpecker
|
||||
({
|
||||
name = "postgresql-db",
|
||||
srvc = "postgres",
|
||||
desc = "PostgreSQL database for Forgejo and Woodpecker services",
|
||||
url = "postgresql://127.0.0.1:5432",
|
||||
port = 5432,
|
||||
required = false,
|
||||
dependencies = [],
|
||||
} | schema.ExternalService),
|
||||
|
||||
# Forgejo - Git server
|
||||
({
|
||||
name = "forgejo-git",
|
||||
srvc = "git",
|
||||
desc = "Forgejo Git server for version control and collaboration",
|
||||
url = "http://127.0.0.1:3000",
|
||||
port = 3000,
|
||||
required = false,
|
||||
dependencies = ["postgresql-db"],
|
||||
} | schema.ExternalService),
|
||||
|
||||
# Zot - OCI container registry
|
||||
({
|
||||
name = "zot-register",
|
||||
srvc = "register",
|
||||
desc = "Zot OCI-compliant container registry for container images",
|
||||
url = "http://127.0.0.1:5001",
|
||||
port = 5001,
|
||||
required = false,
|
||||
dependencies = [],
|
||||
} | schema.ExternalService),
|
||||
|
||||
# Woodpecker - CI/CD pipeline engine
|
||||
({
|
||||
name = "woodpecker-cdci",
|
||||
srvc = "cdci",
|
||||
desc = "Woodpecker CI/CD pipeline engine for automation and testing",
|
||||
url = "http://127.0.0.1:8180",
|
||||
port = 8180,
|
||||
required = false,
|
||||
dependencies = ["forgejo-git", "postgresql-db"],
|
||||
} | schema.ExternalService),
|
||||
]
|
||||
@ -1,19 +0,0 @@
|
||||
[ai_service.dag]
|
||||
max_concurrent_tasks = 20
|
||||
retry_attempts = 2
|
||||
task_timeout = 300000
|
||||
|
||||
[ai_service.mcp]
|
||||
enabled = true
|
||||
mcp_service_url = "http://mcp-cicd:8084"
|
||||
timeout = 30000
|
||||
|
||||
[ai_service.rag]
|
||||
enabled = false
|
||||
rag_service_url = "http://localhost:8083"
|
||||
timeout = 30000
|
||||
|
||||
[ai_service.server]
|
||||
host = "0.0.0.0"
|
||||
port = 8082
|
||||
workers = 8
|
||||
@ -1,22 +0,0 @@
|
||||
[ai_service.dag]
|
||||
max_concurrent_tasks = 50
|
||||
retry_attempts = 5
|
||||
task_timeout = 1200000
|
||||
|
||||
[ai_service.mcp]
|
||||
enabled = true
|
||||
mcp_service_url = "https://mcp.provisioning.prod:8084"
|
||||
timeout = 120000
|
||||
|
||||
[ai_service.monitoring]
|
||||
enabled = true
|
||||
|
||||
[ai_service.rag]
|
||||
enabled = true
|
||||
rag_service_url = "https://rag.provisioning.prod:8083"
|
||||
timeout = 120000
|
||||
|
||||
[ai_service.server]
|
||||
host = "0.0.0.0"
|
||||
port = 8082
|
||||
workers = 16
|
||||
@ -1,19 +0,0 @@
|
||||
[ai_service.dag]
|
||||
max_concurrent_tasks = 10
|
||||
retry_attempts = 5
|
||||
task_timeout = 600000
|
||||
|
||||
[ai_service.mcp]
|
||||
enabled = true
|
||||
mcp_service_url = "http://mcp-server:8084"
|
||||
timeout = 60000
|
||||
|
||||
[ai_service.rag]
|
||||
enabled = true
|
||||
rag_service_url = "http://rag:8083"
|
||||
timeout = 60000
|
||||
|
||||
[ai_service.server]
|
||||
host = "0.0.0.0"
|
||||
port = 8082
|
||||
workers = 4
|
||||
@ -1,19 +0,0 @@
|
||||
[ai_service.dag]
|
||||
max_concurrent_tasks = 3
|
||||
retry_attempts = 3
|
||||
task_timeout = 300000
|
||||
|
||||
[ai_service.mcp]
|
||||
enabled = false
|
||||
mcp_service_url = "http://localhost:8084"
|
||||
timeout = 30000
|
||||
|
||||
[ai_service.rag]
|
||||
enabled = true
|
||||
rag_service_url = "http://localhost:8083"
|
||||
timeout = 30000
|
||||
|
||||
[ai_service.server]
|
||||
host = "127.0.0.1"
|
||||
port = 8082
|
||||
workers = 2
|
||||
@ -1,193 +0,0 @@
|
||||
[control_center.audit]
|
||||
enabled = false
|
||||
redact_sensitive = true
|
||||
|
||||
[control_center.audit.storage]
|
||||
immutable = false
|
||||
retention_days = 90
|
||||
|
||||
[control_center.compliance]
|
||||
enabled = false
|
||||
encryption_required = false
|
||||
|
||||
[control_center.compliance.data_retention]
|
||||
audit_log_days = 2555
|
||||
policy_years = 7
|
||||
|
||||
[control_center.compliance.validation]
|
||||
enabled = false
|
||||
interval_hours = 24
|
||||
|
||||
[control_center.database]
|
||||
backend = "rocksdb"
|
||||
max_retries = "3"
|
||||
path = "/var/lib/provisioning/control-center/data"
|
||||
pool_size = 10
|
||||
retry = true
|
||||
timeout = 30
|
||||
|
||||
[control_center.integrations.ldap]
|
||||
enabled = false
|
||||
|
||||
[control_center.integrations.oauth2]
|
||||
enabled = false
|
||||
|
||||
[control_center.integrations.webhooks]
|
||||
enabled = false
|
||||
|
||||
[control_center.logging]
|
||||
format = "&"
|
||||
level = "&"
|
||||
outputs = ["stdout"]
|
||||
|
||||
[control_center.logging.fields]
|
||||
caller = false
|
||||
hostname = true
|
||||
pid = true
|
||||
service_name = true
|
||||
stack_trace = false
|
||||
timestamp = true
|
||||
|
||||
[control_center.logging.file]
|
||||
compress = false
|
||||
max_age = 30
|
||||
max_backups = 10
|
||||
max_size = 104857600
|
||||
path = "/var/log/provisioning/service.log"
|
||||
|
||||
[control_center.logging.performance]
|
||||
enabled = false
|
||||
memory_info = false
|
||||
slow_threshold = 1000
|
||||
|
||||
[control_center.logging.sampling]
|
||||
enabled = false
|
||||
initial = 100
|
||||
thereafter = 100
|
||||
|
||||
[control_center.logging.syslog]
|
||||
protocol = "udp"
|
||||
|
||||
[control_center.monitoring]
|
||||
enabled = false
|
||||
|
||||
[control_center.monitoring.alerting]
|
||||
enabled = false
|
||||
|
||||
[control_center.monitoring.health_check]
|
||||
enabled = false
|
||||
endpoint = "/health"
|
||||
healthy_threshold = 2
|
||||
interval = 30
|
||||
timeout = 5000
|
||||
type = "&"
|
||||
unhealthy_threshold = 3
|
||||
|
||||
[control_center.monitoring.metrics]
|
||||
buffer_size = 1000
|
||||
enabled = false
|
||||
interval = 60
|
||||
prometheus_path = "/metrics"
|
||||
retention_days = 30
|
||||
|
||||
[control_center.monitoring.resources]
|
||||
alert_threshold = 80
|
||||
cpu = false
|
||||
disk = false
|
||||
memory = false
|
||||
network = false
|
||||
|
||||
[control_center.monitoring.tracing]
|
||||
enabled = false
|
||||
sample_rate = 0.1
|
||||
|
||||
[control_center.policy]
|
||||
enabled = true
|
||||
|
||||
[control_center.policy.cache]
|
||||
enabled = true
|
||||
max_policies = 10000
|
||||
ttl = 3600
|
||||
|
||||
[control_center.policy.versioning]
|
||||
enabled = true
|
||||
max_versions = 20
|
||||
|
||||
[control_center.rbac]
|
||||
attribute_based = false
|
||||
default_role = "user"
|
||||
dynamic_roles = false
|
||||
enabled = true
|
||||
hierarchy = true
|
||||
|
||||
[control_center.rbac.roles]
|
||||
admin = true
|
||||
operator = true
|
||||
viewer = true
|
||||
|
||||
[control_center.security.cors]
|
||||
allow_credentials = false
|
||||
enabled = false
|
||||
|
||||
[control_center.security.jwt]
|
||||
algorithm = "HS256"
|
||||
audience = "provisioning"
|
||||
expiration = 3600
|
||||
issuer = "control-center"
|
||||
refresh_expiration = 86400
|
||||
secret = "change_me_in_production"
|
||||
|
||||
[control_center.security.mfa]
|
||||
lockout_duration = 15
|
||||
max_attempts = "5"
|
||||
methods = ["totp"]
|
||||
required = false
|
||||
|
||||
[control_center.security.rate_limiting]
|
||||
enabled = false
|
||||
max_requests = "1000"
|
||||
window_seconds = 60
|
||||
|
||||
[control_center.security.rbac]
|
||||
default_role = "user"
|
||||
enabled = true
|
||||
inheritance = true
|
||||
|
||||
[control_center.security.session]
|
||||
idle_timeout = 3600
|
||||
max_duration = 86400
|
||||
tracking = false
|
||||
|
||||
[control_center.security.tls]
|
||||
client_auth = false
|
||||
enabled = false
|
||||
|
||||
[control_center.server]
|
||||
graceful_shutdown = true
|
||||
host = "127.0.0.1"
|
||||
keep_alive = 75
|
||||
max_connections = 100
|
||||
port = 8080
|
||||
request_timeout = 30000
|
||||
shutdown_timeout = 30
|
||||
workers = 4
|
||||
|
||||
[control_center.users]
|
||||
audit_enabled = false
|
||||
enabled = true
|
||||
|
||||
[control_center.users.registration]
|
||||
auto_assign_role = "user"
|
||||
enabled = true
|
||||
requires_approval = false
|
||||
|
||||
[control_center.users.sessions]
|
||||
absolute_timeout = 86400
|
||||
idle_timeout = 3600
|
||||
max_active = 5
|
||||
|
||||
[control_center.workspace]
|
||||
enabled = true
|
||||
multi_workspace = false
|
||||
name = "default"
|
||||
path = "/var/lib/provisioning/control-center"
|
||||
@ -1,193 +0,0 @@
|
||||
[control_center.audit]
|
||||
enabled = false
|
||||
redact_sensitive = true
|
||||
|
||||
[control_center.audit.storage]
|
||||
immutable = false
|
||||
retention_days = 90
|
||||
|
||||
[control_center.compliance]
|
||||
enabled = false
|
||||
encryption_required = false
|
||||
|
||||
[control_center.compliance.data_retention]
|
||||
audit_log_days = 2555
|
||||
policy_years = 7
|
||||
|
||||
[control_center.compliance.validation]
|
||||
enabled = false
|
||||
interval_hours = 24
|
||||
|
||||
[control_center.database]
|
||||
backend = "rocksdb"
|
||||
max_retries = "3"
|
||||
path = "/var/lib/provisioning/control-center/data"
|
||||
pool_size = 10
|
||||
retry = true
|
||||
timeout = 30
|
||||
|
||||
[control_center.integrations.ldap]
|
||||
enabled = false
|
||||
|
||||
[control_center.integrations.oauth2]
|
||||
enabled = false
|
||||
|
||||
[control_center.integrations.webhooks]
|
||||
enabled = false
|
||||
|
||||
[control_center.logging]
|
||||
format = "&"
|
||||
level = "&"
|
||||
outputs = ["stdout"]
|
||||
|
||||
[control_center.logging.fields]
|
||||
caller = false
|
||||
hostname = true
|
||||
pid = true
|
||||
service_name = true
|
||||
stack_trace = false
|
||||
timestamp = true
|
||||
|
||||
[control_center.logging.file]
|
||||
compress = false
|
||||
max_age = 30
|
||||
max_backups = 10
|
||||
max_size = 104857600
|
||||
path = "/var/log/provisioning/service.log"
|
||||
|
||||
[control_center.logging.performance]
|
||||
enabled = false
|
||||
memory_info = false
|
||||
slow_threshold = 1000
|
||||
|
||||
[control_center.logging.sampling]
|
||||
enabled = false
|
||||
initial = 100
|
||||
thereafter = 100
|
||||
|
||||
[control_center.logging.syslog]
|
||||
protocol = "udp"
|
||||
|
||||
[control_center.monitoring]
|
||||
enabled = false
|
||||
|
||||
[control_center.monitoring.alerting]
|
||||
enabled = false
|
||||
|
||||
[control_center.monitoring.health_check]
|
||||
enabled = false
|
||||
endpoint = "/health"
|
||||
healthy_threshold = 2
|
||||
interval = 30
|
||||
timeout = 5000
|
||||
type = "&"
|
||||
unhealthy_threshold = 3
|
||||
|
||||
[control_center.monitoring.metrics]
|
||||
buffer_size = 1000
|
||||
enabled = false
|
||||
interval = 60
|
||||
prometheus_path = "/metrics"
|
||||
retention_days = 30
|
||||
|
||||
[control_center.monitoring.resources]
|
||||
alert_threshold = 80
|
||||
cpu = false
|
||||
disk = false
|
||||
memory = false
|
||||
network = false
|
||||
|
||||
[control_center.monitoring.tracing]
|
||||
enabled = false
|
||||
sample_rate = 0.1
|
||||
|
||||
[control_center.policy]
|
||||
enabled = true
|
||||
|
||||
[control_center.policy.cache]
|
||||
enabled = true
|
||||
max_policies = 10000
|
||||
ttl = 3600
|
||||
|
||||
[control_center.policy.versioning]
|
||||
enabled = true
|
||||
max_versions = 20
|
||||
|
||||
[control_center.rbac]
|
||||
attribute_based = false
|
||||
default_role = "user"
|
||||
dynamic_roles = false
|
||||
enabled = true
|
||||
hierarchy = true
|
||||
|
||||
[control_center.rbac.roles]
|
||||
admin = true
|
||||
operator = true
|
||||
viewer = true
|
||||
|
||||
[control_center.security.cors]
|
||||
allow_credentials = false
|
||||
enabled = false
|
||||
|
||||
[control_center.security.jwt]
|
||||
algorithm = "HS256"
|
||||
audience = "provisioning"
|
||||
expiration = 3600
|
||||
issuer = "control-center"
|
||||
refresh_expiration = 86400
|
||||
secret = "change_me_in_production"
|
||||
|
||||
[control_center.security.mfa]
|
||||
lockout_duration = 15
|
||||
max_attempts = "5"
|
||||
methods = ["totp"]
|
||||
required = false
|
||||
|
||||
[control_center.security.rate_limiting]
|
||||
enabled = false
|
||||
max_requests = "1000"
|
||||
window_seconds = 60
|
||||
|
||||
[control_center.security.rbac]
|
||||
default_role = "user"
|
||||
enabled = true
|
||||
inheritance = true
|
||||
|
||||
[control_center.security.session]
|
||||
idle_timeout = 3600
|
||||
max_duration = 86400
|
||||
tracking = false
|
||||
|
||||
[control_center.security.tls]
|
||||
client_auth = false
|
||||
enabled = false
|
||||
|
||||
[control_center.server]
|
||||
graceful_shutdown = true
|
||||
host = "127.0.0.1"
|
||||
keep_alive = 75
|
||||
max_connections = 100
|
||||
port = 8080
|
||||
request_timeout = 30000
|
||||
shutdown_timeout = 30
|
||||
workers = 4
|
||||
|
||||
[control_center.users]
|
||||
audit_enabled = false
|
||||
enabled = true
|
||||
|
||||
[control_center.users.registration]
|
||||
auto_assign_role = "user"
|
||||
enabled = true
|
||||
requires_approval = false
|
||||
|
||||
[control_center.users.sessions]
|
||||
absolute_timeout = 86400
|
||||
idle_timeout = 3600
|
||||
max_active = 5
|
||||
|
||||
[control_center.workspace]
|
||||
enabled = true
|
||||
multi_workspace = false
|
||||
name = "default"
|
||||
path = "/var/lib/provisioning/control-center"
|
||||
@ -1,193 +0,0 @@
|
||||
[control_center.audit]
|
||||
enabled = false
|
||||
redact_sensitive = true
|
||||
|
||||
[control_center.audit.storage]
|
||||
immutable = false
|
||||
retention_days = 90
|
||||
|
||||
[control_center.compliance]
|
||||
enabled = false
|
||||
encryption_required = false
|
||||
|
||||
[control_center.compliance.data_retention]
|
||||
audit_log_days = 2555
|
||||
policy_years = 7
|
||||
|
||||
[control_center.compliance.validation]
|
||||
enabled = false
|
||||
interval_hours = 24
|
||||
|
||||
[control_center.database]
|
||||
backend = "rocksdb"
|
||||
max_retries = "3"
|
||||
path = "/var/lib/provisioning/control-center/data"
|
||||
pool_size = 10
|
||||
retry = true
|
||||
timeout = 30
|
||||
|
||||
[control_center.integrations.ldap]
|
||||
enabled = false
|
||||
|
||||
[control_center.integrations.oauth2]
|
||||
enabled = false
|
||||
|
||||
[control_center.integrations.webhooks]
|
||||
enabled = false
|
||||
|
||||
[control_center.logging]
|
||||
format = "&"
|
||||
level = "&"
|
||||
outputs = ["stdout"]
|
||||
|
||||
[control_center.logging.fields]
|
||||
caller = false
|
||||
hostname = true
|
||||
pid = true
|
||||
service_name = true
|
||||
stack_trace = false
|
||||
timestamp = true
|
||||
|
||||
[control_center.logging.file]
|
||||
compress = false
|
||||
max_age = 30
|
||||
max_backups = 10
|
||||
max_size = 104857600
|
||||
path = "/var/log/provisioning/service.log"
|
||||
|
||||
[control_center.logging.performance]
|
||||
enabled = false
|
||||
memory_info = false
|
||||
slow_threshold = 1000
|
||||
|
||||
[control_center.logging.sampling]
|
||||
enabled = false
|
||||
initial = 100
|
||||
thereafter = 100
|
||||
|
||||
[control_center.logging.syslog]
|
||||
protocol = "udp"
|
||||
|
||||
[control_center.monitoring]
|
||||
enabled = false
|
||||
|
||||
[control_center.monitoring.alerting]
|
||||
enabled = false
|
||||
|
||||
[control_center.monitoring.health_check]
|
||||
enabled = false
|
||||
endpoint = "/health"
|
||||
healthy_threshold = 2
|
||||
interval = 30
|
||||
timeout = 5000
|
||||
type = "&"
|
||||
unhealthy_threshold = 3
|
||||
|
||||
[control_center.monitoring.metrics]
|
||||
buffer_size = 1000
|
||||
enabled = false
|
||||
interval = 60
|
||||
prometheus_path = "/metrics"
|
||||
retention_days = 30
|
||||
|
||||
[control_center.monitoring.resources]
|
||||
alert_threshold = 80
|
||||
cpu = false
|
||||
disk = false
|
||||
memory = false
|
||||
network = false
|
||||
|
||||
[control_center.monitoring.tracing]
|
||||
enabled = false
|
||||
sample_rate = 0.1
|
||||
|
||||
[control_center.policy]
|
||||
enabled = true
|
||||
|
||||
[control_center.policy.cache]
|
||||
enabled = true
|
||||
max_policies = 10000
|
||||
ttl = 3600
|
||||
|
||||
[control_center.policy.versioning]
|
||||
enabled = true
|
||||
max_versions = 20
|
||||
|
||||
[control_center.rbac]
|
||||
attribute_based = false
|
||||
default_role = "user"
|
||||
dynamic_roles = false
|
||||
enabled = true
|
||||
hierarchy = true
|
||||
|
||||
[control_center.rbac.roles]
|
||||
admin = true
|
||||
operator = true
|
||||
viewer = true
|
||||
|
||||
[control_center.security.cors]
|
||||
allow_credentials = false
|
||||
enabled = false
|
||||
|
||||
[control_center.security.jwt]
|
||||
algorithm = "HS256"
|
||||
audience = "provisioning"
|
||||
expiration = 3600
|
||||
issuer = "control-center"
|
||||
refresh_expiration = 86400
|
||||
secret = "change_me_in_production"
|
||||
|
||||
[control_center.security.mfa]
|
||||
lockout_duration = 15
|
||||
max_attempts = "5"
|
||||
methods = ["totp"]
|
||||
required = false
|
||||
|
||||
[control_center.security.rate_limiting]
|
||||
enabled = false
|
||||
max_requests = "1000"
|
||||
window_seconds = 60
|
||||
|
||||
[control_center.security.rbac]
|
||||
default_role = "user"
|
||||
enabled = true
|
||||
inheritance = true
|
||||
|
||||
[control_center.security.session]
|
||||
idle_timeout = 3600
|
||||
max_duration = 86400
|
||||
tracking = false
|
||||
|
||||
[control_center.security.tls]
|
||||
client_auth = false
|
||||
enabled = false
|
||||
|
||||
[control_center.server]
|
||||
graceful_shutdown = true
|
||||
host = "127.0.0.1"
|
||||
keep_alive = 75
|
||||
max_connections = 100
|
||||
port = 8080
|
||||
request_timeout = 30000
|
||||
shutdown_timeout = 30
|
||||
workers = 4
|
||||
|
||||
[control_center.users]
|
||||
audit_enabled = false
|
||||
enabled = true
|
||||
|
||||
[control_center.users.registration]
|
||||
auto_assign_role = "user"
|
||||
enabled = true
|
||||
requires_approval = false
|
||||
|
||||
[control_center.users.sessions]
|
||||
absolute_timeout = 86400
|
||||
idle_timeout = 3600
|
||||
max_active = 5
|
||||
|
||||
[control_center.workspace]
|
||||
enabled = true
|
||||
multi_workspace = false
|
||||
name = "default"
|
||||
path = "/var/lib/provisioning/control-center"
|
||||
@ -1,193 +0,0 @@
|
||||
[control_center.audit]
|
||||
enabled = false
|
||||
redact_sensitive = true
|
||||
|
||||
[control_center.audit.storage]
|
||||
immutable = false
|
||||
retention_days = 90
|
||||
|
||||
[control_center.compliance]
|
||||
enabled = false
|
||||
encryption_required = false
|
||||
|
||||
[control_center.compliance.data_retention]
|
||||
audit_log_days = 2555
|
||||
policy_years = 7
|
||||
|
||||
[control_center.compliance.validation]
|
||||
enabled = false
|
||||
interval_hours = 24
|
||||
|
||||
[control_center.database]
|
||||
backend = "rocksdb"
|
||||
max_retries = "3"
|
||||
path = "/var/lib/provisioning/control-center/data"
|
||||
pool_size = 10
|
||||
retry = true
|
||||
timeout = 30
|
||||
|
||||
[control_center.integrations.ldap]
|
||||
enabled = false
|
||||
|
||||
[control_center.integrations.oauth2]
|
||||
enabled = false
|
||||
|
||||
[control_center.integrations.webhooks]
|
||||
enabled = false
|
||||
|
||||
[control_center.logging]
|
||||
format = "&"
|
||||
level = "&"
|
||||
outputs = ["stdout"]
|
||||
|
||||
[control_center.logging.fields]
|
||||
caller = false
|
||||
hostname = true
|
||||
pid = true
|
||||
service_name = true
|
||||
stack_trace = false
|
||||
timestamp = true
|
||||
|
||||
[control_center.logging.file]
|
||||
compress = false
|
||||
max_age = 30
|
||||
max_backups = 10
|
||||
max_size = 104857600
|
||||
path = "/var/log/provisioning/service.log"
|
||||
|
||||
[control_center.logging.performance]
|
||||
enabled = false
|
||||
memory_info = false
|
||||
slow_threshold = 1000
|
||||
|
||||
[control_center.logging.sampling]
|
||||
enabled = false
|
||||
initial = 100
|
||||
thereafter = 100
|
||||
|
||||
[control_center.logging.syslog]
|
||||
protocol = "udp"
|
||||
|
||||
[control_center.monitoring]
|
||||
enabled = false
|
||||
|
||||
[control_center.monitoring.alerting]
|
||||
enabled = false
|
||||
|
||||
[control_center.monitoring.health_check]
|
||||
enabled = false
|
||||
endpoint = "/health"
|
||||
healthy_threshold = 2
|
||||
interval = 30
|
||||
timeout = 5000
|
||||
type = "&"
|
||||
unhealthy_threshold = 3
|
||||
|
||||
[control_center.monitoring.metrics]
|
||||
buffer_size = 1000
|
||||
enabled = false
|
||||
interval = 60
|
||||
prometheus_path = "/metrics"
|
||||
retention_days = 30
|
||||
|
||||
[control_center.monitoring.resources]
|
||||
alert_threshold = 80
|
||||
cpu = false
|
||||
disk = false
|
||||
memory = false
|
||||
network = false
|
||||
|
||||
[control_center.monitoring.tracing]
|
||||
enabled = false
|
||||
sample_rate = 0.1
|
||||
|
||||
[control_center.policy]
|
||||
enabled = true
|
||||
|
||||
[control_center.policy.cache]
|
||||
enabled = true
|
||||
max_policies = 10000
|
||||
ttl = 3600
|
||||
|
||||
[control_center.policy.versioning]
|
||||
enabled = true
|
||||
max_versions = 20
|
||||
|
||||
[control_center.rbac]
|
||||
attribute_based = false
|
||||
default_role = "user"
|
||||
dynamic_roles = false
|
||||
enabled = true
|
||||
hierarchy = true
|
||||
|
||||
[control_center.rbac.roles]
|
||||
admin = true
|
||||
operator = true
|
||||
viewer = true
|
||||
|
||||
[control_center.security.cors]
|
||||
allow_credentials = false
|
||||
enabled = false
|
||||
|
||||
[control_center.security.jwt]
|
||||
algorithm = "HS256"
|
||||
audience = "provisioning"
|
||||
expiration = 3600
|
||||
issuer = "control-center"
|
||||
refresh_expiration = 86400
|
||||
secret = "change_me_in_production"
|
||||
|
||||
[control_center.security.mfa]
|
||||
lockout_duration = 15
|
||||
max_attempts = "5"
|
||||
methods = ["totp"]
|
||||
required = false
|
||||
|
||||
[control_center.security.rate_limiting]
|
||||
enabled = false
|
||||
max_requests = "1000"
|
||||
window_seconds = 60
|
||||
|
||||
[control_center.security.rbac]
|
||||
default_role = "user"
|
||||
enabled = true
|
||||
inheritance = true
|
||||
|
||||
[control_center.security.session]
|
||||
idle_timeout = 3600
|
||||
max_duration = 86400
|
||||
tracking = false
|
||||
|
||||
[control_center.security.tls]
|
||||
client_auth = false
|
||||
enabled = false
|
||||
|
||||
[control_center.server]
|
||||
graceful_shutdown = true
|
||||
host = "127.0.0.1"
|
||||
keep_alive = 75
|
||||
max_connections = 100
|
||||
port = 8080
|
||||
request_timeout = 30000
|
||||
shutdown_timeout = 30
|
||||
workers = 4
|
||||
|
||||
[control_center.users]
|
||||
audit_enabled = false
|
||||
enabled = true
|
||||
|
||||
[control_center.users.registration]
|
||||
auto_assign_role = "user"
|
||||
enabled = true
|
||||
requires_approval = false
|
||||
|
||||
[control_center.users.sessions]
|
||||
absolute_timeout = 86400
|
||||
idle_timeout = 3600
|
||||
max_active = 5
|
||||
|
||||
[control_center.workspace]
|
||||
enabled = true
|
||||
multi_workspace = false
|
||||
name = "default"
|
||||
path = "/var/lib/provisioning/control-center"
|
||||
@ -1,23 +0,0 @@
|
||||
[registry.cache]
|
||||
capacity = 5000
|
||||
list_cache = false
|
||||
metadata_cache = true
|
||||
ttl = 600
|
||||
|
||||
[registry.gitea]
|
||||
enabled = false
|
||||
verify_ssl = false
|
||||
|
||||
[registry.oci]
|
||||
enabled = true
|
||||
namespace = "provisioning-cicd"
|
||||
registry = "registry.cicd:5000"
|
||||
timeout = 30000
|
||||
verify_ssl = false
|
||||
|
||||
[registry.server]
|
||||
compression = true
|
||||
cors_enabled = false
|
||||
host = "0.0.0.0"
|
||||
port = 8081
|
||||
workers = 8
|
||||
@ -1,30 +0,0 @@
|
||||
[registry.cache]
|
||||
capacity = 10000
|
||||
list_cache = true
|
||||
metadata_cache = true
|
||||
ttl = 1800
|
||||
|
||||
[registry.gitea]
|
||||
enabled = true
|
||||
org = "provisioning"
|
||||
timeout = 120000
|
||||
url = "https://gitea.provisioning.prod:443"
|
||||
verify_ssl = true
|
||||
|
||||
[registry.monitoring]
|
||||
enabled = true
|
||||
metrics_interval = 30
|
||||
|
||||
[registry.oci]
|
||||
enabled = true
|
||||
namespace = "provisioning"
|
||||
registry = "registry.provisioning.prod:5000"
|
||||
timeout = 120000
|
||||
verify_ssl = true
|
||||
|
||||
[registry.server]
|
||||
compression = true
|
||||
cors_enabled = true
|
||||
host = "0.0.0.0"
|
||||
port = 8081
|
||||
workers = 16
|
||||
@ -1,26 +0,0 @@
|
||||
[registry.cache]
|
||||
capacity = 1000
|
||||
list_cache = true
|
||||
metadata_cache = true
|
||||
ttl = 300
|
||||
|
||||
[registry.gitea]
|
||||
enabled = true
|
||||
org = "provisioning-team"
|
||||
timeout = 60000
|
||||
url = "http://gitea:3000"
|
||||
verify_ssl = false
|
||||
|
||||
[registry.oci]
|
||||
enabled = true
|
||||
namespace = "provisioning"
|
||||
registry = "registry.provisioning.local:5000"
|
||||
timeout = 60000
|
||||
verify_ssl = false
|
||||
|
||||
[registry.server]
|
||||
compression = true
|
||||
cors_enabled = true
|
||||
host = "0.0.0.0"
|
||||
port = 8081
|
||||
workers = 4
|
||||
@ -1,23 +0,0 @@
|
||||
[registry.cache]
|
||||
capacity = 100
|
||||
list_cache = true
|
||||
metadata_cache = true
|
||||
ttl = 60
|
||||
|
||||
[registry.gitea]
|
||||
enabled = true
|
||||
org = "provisioning-solo"
|
||||
timeout = 30000
|
||||
url = "http://localhost:3000"
|
||||
verify_ssl = false
|
||||
|
||||
[registry.oci]
|
||||
enabled = false
|
||||
verify_ssl = false
|
||||
|
||||
[registry.server]
|
||||
compression = true
|
||||
cors_enabled = false
|
||||
host = "127.0.0.1"
|
||||
port = 8081
|
||||
workers = 2
|
||||
@ -1,150 +0,0 @@
|
||||
[installer.database]
|
||||
auto_init = true
|
||||
backup_before_upgrade = true
|
||||
|
||||
[installer.database.migrations]
|
||||
enabled = true
|
||||
path = "/migrations"
|
||||
|
||||
[installer.high_availability]
|
||||
auto_healing = true
|
||||
enabled = false
|
||||
replicas = 1
|
||||
|
||||
[installer.high_availability.backup]
|
||||
enabled = false
|
||||
interval_hours = 24
|
||||
retention_days = 30
|
||||
|
||||
[installer.high_availability.health_checks]
|
||||
enabled = true
|
||||
interval_seconds = 30
|
||||
|
||||
[installer.installation]
|
||||
keep_artifacts = false
|
||||
parallel_services = 3
|
||||
rollback_on_failure = true
|
||||
timeout_minutes = 30
|
||||
|
||||
[installer.logging]
|
||||
format = "&"
|
||||
level = "&"
|
||||
outputs = ["stdout"]
|
||||
|
||||
[installer.logging.fields]
|
||||
caller = false
|
||||
hostname = true
|
||||
pid = true
|
||||
service_name = true
|
||||
stack_trace = false
|
||||
timestamp = true
|
||||
|
||||
[installer.logging.file]
|
||||
compress = false
|
||||
max_age = 30
|
||||
max_backups = 10
|
||||
max_size = 104857600
|
||||
path = "/var/log/provisioning/service.log"
|
||||
|
||||
[installer.logging.performance]
|
||||
enabled = false
|
||||
memory_info = false
|
||||
slow_threshold = 1000
|
||||
|
||||
[installer.logging.sampling]
|
||||
enabled = false
|
||||
initial = 100
|
||||
thereafter = 100
|
||||
|
||||
[installer.logging.syslog]
|
||||
protocol = "udp"
|
||||
|
||||
[installer.monitoring]
|
||||
enabled = false
|
||||
|
||||
[installer.monitoring.alerting]
|
||||
enabled = false
|
||||
|
||||
[installer.monitoring.health_check]
|
||||
enabled = false
|
||||
endpoint = "/health"
|
||||
healthy_threshold = 2
|
||||
interval = 30
|
||||
timeout = 5000
|
||||
type = "&"
|
||||
unhealthy_threshold = 3
|
||||
|
||||
[installer.monitoring.metrics]
|
||||
buffer_size = 1000
|
||||
enabled = false
|
||||
interval = 60
|
||||
prometheus_path = "/metrics"
|
||||
retention_days = 30
|
||||
|
||||
[installer.monitoring.resources]
|
||||
alert_threshold = 80
|
||||
cpu = false
|
||||
disk = false
|
||||
memory = false
|
||||
network = false
|
||||
|
||||
[installer.monitoring.tracing]
|
||||
enabled = false
|
||||
sample_rate = 0.1
|
||||
|
||||
[installer.networking.ingress]
|
||||
enabled = false
|
||||
tls = false
|
||||
|
||||
[installer.networking.load_balancer]
|
||||
enabled = false
|
||||
|
||||
[installer.networking.ports]
|
||||
control_center = 8080
|
||||
mcp_server = 3000
|
||||
orchestrator = 9090
|
||||
|
||||
[installer.post_install]
|
||||
enabled = false
|
||||
notify = false
|
||||
|
||||
[installer.post_install.verify]
|
||||
enabled = true
|
||||
timeout_minutes = 10
|
||||
|
||||
[installer.preflight]
|
||||
check_cpu = true
|
||||
check_dependencies = true
|
||||
check_disk_space = true
|
||||
check_memory = true
|
||||
check_network = true
|
||||
check_ports = true
|
||||
enabled = true
|
||||
min_cpu_cores = 2
|
||||
min_disk_gb = 50
|
||||
min_memory_gb = 4
|
||||
|
||||
[installer.services]
|
||||
control_center = true
|
||||
mcp_server = true
|
||||
orchestrator = true
|
||||
|
||||
[installer.storage]
|
||||
compression = false
|
||||
location = "/var/lib/provisioning"
|
||||
replication = false
|
||||
size_gb = 100
|
||||
|
||||
[installer.target]
|
||||
ssh_port = 22
|
||||
ssh_user = "root"
|
||||
target_type = "local"
|
||||
|
||||
[installer.upgrades]
|
||||
auto_upgrade = false
|
||||
|
||||
[installer.workspace]
|
||||
enabled = true
|
||||
multi_workspace = false
|
||||
name = "default"
|
||||
path = "/var/lib/provisioning/installer"
|
||||
@ -1,150 +0,0 @@
|
||||
[installer.database]
|
||||
auto_init = true
|
||||
backup_before_upgrade = true
|
||||
|
||||
[installer.database.migrations]
|
||||
enabled = true
|
||||
path = "/migrations"
|
||||
|
||||
[installer.high_availability]
|
||||
auto_healing = true
|
||||
enabled = false
|
||||
replicas = 1
|
||||
|
||||
[installer.high_availability.backup]
|
||||
enabled = false
|
||||
interval_hours = 24
|
||||
retention_days = 30
|
||||
|
||||
[installer.high_availability.health_checks]
|
||||
enabled = true
|
||||
interval_seconds = 30
|
||||
|
||||
[installer.installation]
|
||||
keep_artifacts = false
|
||||
parallel_services = 3
|
||||
rollback_on_failure = true
|
||||
timeout_minutes = 30
|
||||
|
||||
[installer.logging]
|
||||
format = "&"
|
||||
level = "&"
|
||||
outputs = ["stdout"]
|
||||
|
||||
[installer.logging.fields]
|
||||
caller = false
|
||||
hostname = true
|
||||
pid = true
|
||||
service_name = true
|
||||
stack_trace = false
|
||||
timestamp = true
|
||||
|
||||
[installer.logging.file]
|
||||
compress = false
|
||||
max_age = 30
|
||||
max_backups = 10
|
||||
max_size = 104857600
|
||||
path = "/var/log/provisioning/service.log"
|
||||
|
||||
[installer.logging.performance]
|
||||
enabled = false
|
||||
memory_info = false
|
||||
slow_threshold = 1000
|
||||
|
||||
[installer.logging.sampling]
|
||||
enabled = false
|
||||
initial = 100
|
||||
thereafter = 100
|
||||
|
||||
[installer.logging.syslog]
|
||||
protocol = "udp"
|
||||
|
||||
[installer.monitoring]
|
||||
enabled = false
|
||||
|
||||
[installer.monitoring.alerting]
|
||||
enabled = false
|
||||
|
||||
[installer.monitoring.health_check]
|
||||
enabled = false
|
||||
endpoint = "/health"
|
||||
healthy_threshold = 2
|
||||
interval = 30
|
||||
timeout = 5000
|
||||
type = "&"
|
||||
unhealthy_threshold = 3
|
||||
|
||||
[installer.monitoring.metrics]
|
||||
buffer_size = 1000
|
||||
enabled = false
|
||||
interval = 60
|
||||
prometheus_path = "/metrics"
|
||||
retention_days = 30
|
||||
|
||||
[installer.monitoring.resources]
|
||||
alert_threshold = 80
|
||||
cpu = false
|
||||
disk = false
|
||||
memory = false
|
||||
network = false
|
||||
|
||||
[installer.monitoring.tracing]
|
||||
enabled = false
|
||||
sample_rate = 0.1
|
||||
|
||||
[installer.networking.ingress]
|
||||
enabled = false
|
||||
tls = false
|
||||
|
||||
[installer.networking.load_balancer]
|
||||
enabled = false
|
||||
|
||||
[installer.networking.ports]
|
||||
control_center = 8080
|
||||
mcp_server = 3000
|
||||
orchestrator = 9090
|
||||
|
||||
[installer.post_install]
|
||||
enabled = false
|
||||
notify = false
|
||||
|
||||
[installer.post_install.verify]
|
||||
enabled = true
|
||||
timeout_minutes = 10
|
||||
|
||||
[installer.preflight]
|
||||
check_cpu = true
|
||||
check_dependencies = true
|
||||
check_disk_space = true
|
||||
check_memory = true
|
||||
check_network = true
|
||||
check_ports = true
|
||||
enabled = true
|
||||
min_cpu_cores = 2
|
||||
min_disk_gb = 50
|
||||
min_memory_gb = 4
|
||||
|
||||
[installer.services]
|
||||
control_center = true
|
||||
mcp_server = true
|
||||
orchestrator = true
|
||||
|
||||
[installer.storage]
|
||||
compression = false
|
||||
location = "/var/lib/provisioning"
|
||||
replication = false
|
||||
size_gb = 100
|
||||
|
||||
[installer.target]
|
||||
ssh_port = 22
|
||||
ssh_user = "root"
|
||||
target_type = "local"
|
||||
|
||||
[installer.upgrades]
|
||||
auto_upgrade = false
|
||||
|
||||
[installer.workspace]
|
||||
enabled = true
|
||||
multi_workspace = false
|
||||
name = "default"
|
||||
path = "/var/lib/provisioning/installer"
|
||||
@ -1,150 +0,0 @@
|
||||
[installer.database]
|
||||
auto_init = true
|
||||
backup_before_upgrade = true
|
||||
|
||||
[installer.database.migrations]
|
||||
enabled = true
|
||||
path = "/migrations"
|
||||
|
||||
[installer.high_availability]
|
||||
auto_healing = true
|
||||
enabled = false
|
||||
replicas = 1
|
||||
|
||||
[installer.high_availability.backup]
|
||||
enabled = false
|
||||
interval_hours = 24
|
||||
retention_days = 30
|
||||
|
||||
[installer.high_availability.health_checks]
|
||||
enabled = true
|
||||
interval_seconds = 30
|
||||
|
||||
[installer.installation]
|
||||
keep_artifacts = false
|
||||
parallel_services = 3
|
||||
rollback_on_failure = true
|
||||
timeout_minutes = 30
|
||||
|
||||
[installer.logging]
|
||||
format = "&"
|
||||
level = "&"
|
||||
outputs = ["stdout"]
|
||||
|
||||
[installer.logging.fields]
|
||||
caller = false
|
||||
hostname = true
|
||||
pid = true
|
||||
service_name = true
|
||||
stack_trace = false
|
||||
timestamp = true
|
||||
|
||||
[installer.logging.file]
|
||||
compress = false
|
||||
max_age = 30
|
||||
max_backups = 10
|
||||
max_size = 104857600
|
||||
path = "/var/log/provisioning/service.log"
|
||||
|
||||
[installer.logging.performance]
|
||||
enabled = false
|
||||
memory_info = false
|
||||
slow_threshold = 1000
|
||||
|
||||
[installer.logging.sampling]
|
||||
enabled = false
|
||||
initial = 100
|
||||
thereafter = 100
|
||||
|
||||
[installer.logging.syslog]
|
||||
protocol = "udp"
|
||||
|
||||
[installer.monitoring]
|
||||
enabled = false
|
||||
|
||||
[installer.monitoring.alerting]
|
||||
enabled = false
|
||||
|
||||
[installer.monitoring.health_check]
|
||||
enabled = false
|
||||
endpoint = "/health"
|
||||
healthy_threshold = 2
|
||||
interval = 30
|
||||
timeout = 5000
|
||||
type = "&"
|
||||
unhealthy_threshold = 3
|
||||
|
||||
[installer.monitoring.metrics]
|
||||
buffer_size = 1000
|
||||
enabled = false
|
||||
interval = 60
|
||||
prometheus_path = "/metrics"
|
||||
retention_days = 30
|
||||
|
||||
[installer.monitoring.resources]
|
||||
alert_threshold = 80
|
||||
cpu = false
|
||||
disk = false
|
||||
memory = false
|
||||
network = false
|
||||
|
||||
[installer.monitoring.tracing]
|
||||
enabled = false
|
||||
sample_rate = 0.1
|
||||
|
||||
[installer.networking.ingress]
|
||||
enabled = false
|
||||
tls = false
|
||||
|
||||
[installer.networking.load_balancer]
|
||||
enabled = false
|
||||
|
||||
[installer.networking.ports]
|
||||
control_center = 8080
|
||||
mcp_server = 3000
|
||||
orchestrator = 9090
|
||||
|
||||
[installer.post_install]
|
||||
enabled = false
|
||||
notify = false
|
||||
|
||||
[installer.post_install.verify]
|
||||
enabled = true
|
||||
timeout_minutes = 10
|
||||
|
||||
[installer.preflight]
|
||||
check_cpu = true
|
||||
check_dependencies = true
|
||||
check_disk_space = true
|
||||
check_memory = true
|
||||
check_network = true
|
||||
check_ports = true
|
||||
enabled = true
|
||||
min_cpu_cores = 2
|
||||
min_disk_gb = 50
|
||||
min_memory_gb = 4
|
||||
|
||||
[installer.services]
|
||||
control_center = true
|
||||
mcp_server = true
|
||||
orchestrator = true
|
||||
|
||||
[installer.storage]
|
||||
compression = false
|
||||
location = "/var/lib/provisioning"
|
||||
replication = false
|
||||
size_gb = 100
|
||||
|
||||
[installer.target]
|
||||
ssh_port = 22
|
||||
ssh_user = "root"
|
||||
target_type = "local"
|
||||
|
||||
[installer.upgrades]
|
||||
auto_upgrade = false
|
||||
|
||||
[installer.workspace]
|
||||
enabled = true
|
||||
multi_workspace = false
|
||||
name = "default"
|
||||
path = "/var/lib/provisioning/installer"
|
||||
@ -1,150 +0,0 @@
|
||||
[installer.database]
|
||||
auto_init = true
|
||||
backup_before_upgrade = true
|
||||
|
||||
[installer.database.migrations]
|
||||
enabled = true
|
||||
path = "/migrations"
|
||||
|
||||
[installer.high_availability]
|
||||
auto_healing = true
|
||||
enabled = false
|
||||
replicas = 1
|
||||
|
||||
[installer.high_availability.backup]
|
||||
enabled = false
|
||||
interval_hours = 24
|
||||
retention_days = 30
|
||||
|
||||
[installer.high_availability.health_checks]
|
||||
enabled = true
|
||||
interval_seconds = 30
|
||||
|
||||
[installer.installation]
|
||||
keep_artifacts = false
|
||||
parallel_services = 3
|
||||
rollback_on_failure = true
|
||||
timeout_minutes = 30
|
||||
|
||||
[installer.logging]
|
||||
format = "&"
|
||||
level = "&"
|
||||
outputs = ["stdout"]
|
||||
|
||||
[installer.logging.fields]
|
||||
caller = false
|
||||
hostname = true
|
||||
pid = true
|
||||
service_name = true
|
||||
stack_trace = false
|
||||
timestamp = true
|
||||
|
||||
[installer.logging.file]
|
||||
compress = false
|
||||
max_age = 30
|
||||
max_backups = 10
|
||||
max_size = 104857600
|
||||
path = "/var/log/provisioning/service.log"
|
||||
|
||||
[installer.logging.performance]
|
||||
enabled = false
|
||||
memory_info = false
|
||||
slow_threshold = 1000
|
||||
|
||||
[installer.logging.sampling]
|
||||
enabled = false
|
||||
initial = 100
|
||||
thereafter = 100
|
||||
|
||||
[installer.logging.syslog]
|
||||
protocol = "udp"
|
||||
|
||||
[installer.monitoring]
|
||||
enabled = false
|
||||
|
||||
[installer.monitoring.alerting]
|
||||
enabled = false
|
||||
|
||||
[installer.monitoring.health_check]
|
||||
enabled = false
|
||||
endpoint = "/health"
|
||||
healthy_threshold = 2
|
||||
interval = 30
|
||||
timeout = 5000
|
||||
type = "&"
|
||||
unhealthy_threshold = 3
|
||||
|
||||
[installer.monitoring.metrics]
|
||||
buffer_size = 1000
|
||||
enabled = false
|
||||
interval = 60
|
||||
prometheus_path = "/metrics"
|
||||
retention_days = 30
|
||||
|
||||
[installer.monitoring.resources]
|
||||
alert_threshold = 80
|
||||
cpu = false
|
||||
disk = false
|
||||
memory = false
|
||||
network = false
|
||||
|
||||
[installer.monitoring.tracing]
|
||||
enabled = false
|
||||
sample_rate = 0.1
|
||||
|
||||
[installer.networking.ingress]
|
||||
enabled = false
|
||||
tls = false
|
||||
|
||||
[installer.networking.load_balancer]
|
||||
enabled = false
|
||||
|
||||
[installer.networking.ports]
|
||||
control_center = 8080
|
||||
mcp_server = 3000
|
||||
orchestrator = 9090
|
||||
|
||||
[installer.post_install]
|
||||
enabled = false
|
||||
notify = false
|
||||
|
||||
[installer.post_install.verify]
|
||||
enabled = true
|
||||
timeout_minutes = 10
|
||||
|
||||
[installer.preflight]
|
||||
check_cpu = true
|
||||
check_dependencies = true
|
||||
check_disk_space = true
|
||||
check_memory = true
|
||||
check_network = true
|
||||
check_ports = true
|
||||
enabled = true
|
||||
min_cpu_cores = 2
|
||||
min_disk_gb = 50
|
||||
min_memory_gb = 4
|
||||
|
||||
[installer.services]
|
||||
control_center = true
|
||||
mcp_server = true
|
||||
orchestrator = true
|
||||
|
||||
[installer.storage]
|
||||
compression = false
|
||||
location = "/var/lib/provisioning"
|
||||
replication = false
|
||||
size_gb = 100
|
||||
|
||||
[installer.target]
|
||||
ssh_port = 22
|
||||
ssh_user = "root"
|
||||
target_type = "local"
|
||||
|
||||
[installer.upgrades]
|
||||
auto_upgrade = false
|
||||
|
||||
[installer.workspace]
|
||||
enabled = true
|
||||
multi_workspace = false
|
||||
name = "default"
|
||||
path = "/var/lib/provisioning/installer"
|
||||
@ -1,163 +0,0 @@
|
||||
[mcp_server.capabilities.prompts]
|
||||
enabled = true
|
||||
list_changed_callback = false
|
||||
|
||||
[mcp_server.capabilities.resources]
|
||||
enabled = true
|
||||
list_changed_callback = false
|
||||
subscribe = false
|
||||
|
||||
[mcp_server.capabilities.sampling]
|
||||
enabled = false
|
||||
|
||||
[mcp_server.capabilities.tools]
|
||||
enabled = true
|
||||
list_changed_callback = false
|
||||
|
||||
[mcp_server.control_center_integration]
|
||||
enabled = false
|
||||
enforce_rbac = true
|
||||
|
||||
[mcp_server.logging]
|
||||
format = "&"
|
||||
level = "&"
|
||||
outputs = ["stdout"]
|
||||
|
||||
[mcp_server.logging.fields]
|
||||
caller = false
|
||||
hostname = true
|
||||
pid = true
|
||||
service_name = true
|
||||
stack_trace = false
|
||||
timestamp = true
|
||||
|
||||
[mcp_server.logging.file]
|
||||
compress = false
|
||||
max_age = 30
|
||||
max_backups = 10
|
||||
max_size = 104857600
|
||||
path = "/var/log/provisioning/service.log"
|
||||
|
||||
[mcp_server.logging.performance]
|
||||
enabled = false
|
||||
memory_info = false
|
||||
slow_threshold = 1000
|
||||
|
||||
[mcp_server.logging.sampling]
|
||||
enabled = false
|
||||
initial = 100
|
||||
thereafter = 100
|
||||
|
||||
[mcp_server.logging.syslog]
|
||||
protocol = "udp"
|
||||
|
||||
[mcp_server.monitoring]
|
||||
enabled = false
|
||||
|
||||
[mcp_server.monitoring.alerting]
|
||||
enabled = false
|
||||
|
||||
[mcp_server.monitoring.health_check]
|
||||
enabled = false
|
||||
endpoint = "/health"
|
||||
healthy_threshold = 2
|
||||
interval = 30
|
||||
timeout = 5000
|
||||
type = "&"
|
||||
unhealthy_threshold = 3
|
||||
|
||||
[mcp_server.monitoring.metrics]
|
||||
buffer_size = 1000
|
||||
enabled = false
|
||||
interval = 60
|
||||
prometheus_path = "/metrics"
|
||||
retention_days = 30
|
||||
|
||||
[mcp_server.monitoring.resources]
|
||||
alert_threshold = 80
|
||||
cpu = false
|
||||
disk = false
|
||||
memory = false
|
||||
network = false
|
||||
|
||||
[mcp_server.monitoring.tracing]
|
||||
enabled = false
|
||||
sample_rate = 0.1
|
||||
|
||||
[mcp_server.orchestrator_integration]
|
||||
enabled = false
|
||||
|
||||
[mcp_server.performance]
|
||||
buffer_size = 1024
|
||||
compression = false
|
||||
pool_size = 10
|
||||
|
||||
[mcp_server.prompts]
|
||||
enabled = true
|
||||
max_templates = 100
|
||||
|
||||
[mcp_server.prompts.cache]
|
||||
enabled = true
|
||||
ttl = 3600
|
||||
|
||||
[mcp_server.prompts.versioning]
|
||||
enabled = false
|
||||
max_versions = 10
|
||||
|
||||
[mcp_server.protocol]
|
||||
version = "1.0"
|
||||
|
||||
[mcp_server.protocol.transport]
|
||||
endpoint = "http://localhost:3000"
|
||||
timeout = 30000
|
||||
|
||||
[mcp_server.resources]
|
||||
enabled = true
|
||||
max_size = 104857600
|
||||
|
||||
[mcp_server.resources.cache]
|
||||
enabled = true
|
||||
max_size_mb = 512
|
||||
ttl = 3600
|
||||
|
||||
[mcp_server.resources.validation]
|
||||
enabled = true
|
||||
max_depth = 10
|
||||
|
||||
[mcp_server.sampling]
|
||||
enabled = false
|
||||
max_tokens = 4096
|
||||
temperature = 0.7
|
||||
|
||||
[mcp_server.sampling.cache]
|
||||
enabled = true
|
||||
ttl = 3600
|
||||
|
||||
[mcp_server.server]
|
||||
graceful_shutdown = true
|
||||
host = "127.0.0.1"
|
||||
keep_alive = 75
|
||||
max_connections = 100
|
||||
port = 3000
|
||||
request_timeout = 30000
|
||||
shutdown_timeout = 30
|
||||
workers = 4
|
||||
|
||||
[mcp_server.tools]
|
||||
enabled = true
|
||||
max_concurrent = 5
|
||||
timeout = 30000
|
||||
|
||||
[mcp_server.tools.cache]
|
||||
enabled = true
|
||||
ttl = 3600
|
||||
|
||||
[mcp_server.tools.validation]
|
||||
enabled = true
|
||||
strict_mode = false
|
||||
|
||||
[mcp_server.workspace]
|
||||
enabled = true
|
||||
multi_workspace = false
|
||||
name = "default"
|
||||
path = "/var/lib/provisioning/mcp-server"
|
||||
@ -1,163 +0,0 @@
|
||||
[mcp_server.capabilities.prompts]
|
||||
enabled = true
|
||||
list_changed_callback = false
|
||||
|
||||
[mcp_server.capabilities.resources]
|
||||
enabled = true
|
||||
list_changed_callback = false
|
||||
subscribe = false
|
||||
|
||||
[mcp_server.capabilities.sampling]
|
||||
enabled = false
|
||||
|
||||
[mcp_server.capabilities.tools]
|
||||
enabled = true
|
||||
list_changed_callback = false
|
||||
|
||||
[mcp_server.control_center_integration]
|
||||
enabled = false
|
||||
enforce_rbac = true
|
||||
|
||||
[mcp_server.logging]
|
||||
format = "&"
|
||||
level = "&"
|
||||
outputs = ["stdout"]
|
||||
|
||||
[mcp_server.logging.fields]
|
||||
caller = false
|
||||
hostname = true
|
||||
pid = true
|
||||
service_name = true
|
||||
stack_trace = false
|
||||
timestamp = true
|
||||
|
||||
[mcp_server.logging.file]
|
||||
compress = false
|
||||
max_age = 30
|
||||
max_backups = 10
|
||||
max_size = 104857600
|
||||
path = "/var/log/provisioning/service.log"
|
||||
|
||||
[mcp_server.logging.performance]
|
||||
enabled = false
|
||||
memory_info = false
|
||||
slow_threshold = 1000
|
||||
|
||||
[mcp_server.logging.sampling]
|
||||
enabled = false
|
||||
initial = 100
|
||||
thereafter = 100
|
||||
|
||||
[mcp_server.logging.syslog]
|
||||
protocol = "udp"
|
||||
|
||||
[mcp_server.monitoring]
|
||||
enabled = false
|
||||
|
||||
[mcp_server.monitoring.alerting]
|
||||
enabled = false
|
||||
|
||||
[mcp_server.monitoring.health_check]
|
||||
enabled = false
|
||||
endpoint = "/health"
|
||||
healthy_threshold = 2
|
||||
interval = 30
|
||||
timeout = 5000
|
||||
type = "&"
|
||||
unhealthy_threshold = 3
|
||||
|
||||
[mcp_server.monitoring.metrics]
|
||||
buffer_size = 1000
|
||||
enabled = false
|
||||
interval = 60
|
||||
prometheus_path = "/metrics"
|
||||
retention_days = 30
|
||||
|
||||
[mcp_server.monitoring.resources]
|
||||
alert_threshold = 80
|
||||
cpu = false
|
||||
disk = false
|
||||
memory = false
|
||||
network = false
|
||||
|
||||
[mcp_server.monitoring.tracing]
|
||||
enabled = false
|
||||
sample_rate = 0.1
|
||||
|
||||
[mcp_server.orchestrator_integration]
|
||||
enabled = false
|
||||
|
||||
[mcp_server.performance]
|
||||
buffer_size = 1024
|
||||
compression = false
|
||||
pool_size = 10
|
||||
|
||||
[mcp_server.prompts]
|
||||
enabled = true
|
||||
max_templates = 100
|
||||
|
||||
[mcp_server.prompts.cache]
|
||||
enabled = true
|
||||
ttl = 3600
|
||||
|
||||
[mcp_server.prompts.versioning]
|
||||
enabled = false
|
||||
max_versions = 10
|
||||
|
||||
[mcp_server.protocol]
|
||||
version = "1.0"
|
||||
|
||||
[mcp_server.protocol.transport]
|
||||
endpoint = "http://localhost:3000"
|
||||
timeout = 30000
|
||||
|
||||
[mcp_server.resources]
|
||||
enabled = true
|
||||
max_size = 104857600
|
||||
|
||||
[mcp_server.resources.cache]
|
||||
enabled = true
|
||||
max_size_mb = 512
|
||||
ttl = 3600
|
||||
|
||||
[mcp_server.resources.validation]
|
||||
enabled = true
|
||||
max_depth = 10
|
||||
|
||||
[mcp_server.sampling]
|
||||
enabled = false
|
||||
max_tokens = 4096
|
||||
temperature = 0.7
|
||||
|
||||
[mcp_server.sampling.cache]
|
||||
enabled = true
|
||||
ttl = 3600
|
||||
|
||||
[mcp_server.server]
|
||||
graceful_shutdown = true
|
||||
host = "127.0.0.1"
|
||||
keep_alive = 75
|
||||
max_connections = 100
|
||||
port = 3000
|
||||
request_timeout = 30000
|
||||
shutdown_timeout = 30
|
||||
workers = 4
|
||||
|
||||
[mcp_server.tools]
|
||||
enabled = true
|
||||
max_concurrent = 5
|
||||
timeout = 30000
|
||||
|
||||
[mcp_server.tools.cache]
|
||||
enabled = true
|
||||
ttl = 3600
|
||||
|
||||
[mcp_server.tools.validation]
|
||||
enabled = true
|
||||
strict_mode = false
|
||||
|
||||
[mcp_server.workspace]
|
||||
enabled = true
|
||||
multi_workspace = false
|
||||
name = "default"
|
||||
path = "/var/lib/provisioning/mcp-server"
|
||||
@ -1,163 +0,0 @@
|
||||
[mcp_server.capabilities.prompts]
|
||||
enabled = true
|
||||
list_changed_callback = false
|
||||
|
||||
[mcp_server.capabilities.resources]
|
||||
enabled = true
|
||||
list_changed_callback = false
|
||||
subscribe = false
|
||||
|
||||
[mcp_server.capabilities.sampling]
|
||||
enabled = false
|
||||
|
||||
[mcp_server.capabilities.tools]
|
||||
enabled = true
|
||||
list_changed_callback = false
|
||||
|
||||
[mcp_server.control_center_integration]
|
||||
enabled = false
|
||||
enforce_rbac = true
|
||||
|
||||
[mcp_server.logging]
|
||||
format = "&"
|
||||
level = "&"
|
||||
outputs = ["stdout"]
|
||||
|
||||
[mcp_server.logging.fields]
|
||||
caller = false
|
||||
hostname = true
|
||||
pid = true
|
||||
service_name = true
|
||||
stack_trace = false
|
||||
timestamp = true
|
||||
|
||||
[mcp_server.logging.file]
|
||||
compress = false
|
||||
max_age = 30
|
||||
max_backups = 10
|
||||
max_size = 104857600
|
||||
path = "/var/log/provisioning/service.log"
|
||||
|
||||
[mcp_server.logging.performance]
|
||||
enabled = false
|
||||
memory_info = false
|
||||
slow_threshold = 1000
|
||||
|
||||
[mcp_server.logging.sampling]
|
||||
enabled = false
|
||||
initial = 100
|
||||
thereafter = 100
|
||||
|
||||
[mcp_server.logging.syslog]
|
||||
protocol = "udp"
|
||||
|
||||
[mcp_server.monitoring]
|
||||
enabled = false
|
||||
|
||||
[mcp_server.monitoring.alerting]
|
||||
enabled = false
|
||||
|
||||
[mcp_server.monitoring.health_check]
|
||||
enabled = false
|
||||
endpoint = "/health"
|
||||
healthy_threshold = 2
|
||||
interval = 30
|
||||
timeout = 5000
|
||||
type = "&"
|
||||
unhealthy_threshold = 3
|
||||
|
||||
[mcp_server.monitoring.metrics]
|
||||
buffer_size = 1000
|
||||
enabled = false
|
||||
interval = 60
|
||||
prometheus_path = "/metrics"
|
||||
retention_days = 30
|
||||
|
||||
[mcp_server.monitoring.resources]
|
||||
alert_threshold = 80
|
||||
cpu = false
|
||||
disk = false
|
||||
memory = false
|
||||
network = false
|
||||
|
||||
[mcp_server.monitoring.tracing]
|
||||
enabled = false
|
||||
sample_rate = 0.1
|
||||
|
||||
[mcp_server.orchestrator_integration]
|
||||
enabled = false
|
||||
|
||||
[mcp_server.performance]
|
||||
buffer_size = 1024
|
||||
compression = false
|
||||
pool_size = 10
|
||||
|
||||
[mcp_server.prompts]
|
||||
enabled = true
|
||||
max_templates = 100
|
||||
|
||||
[mcp_server.prompts.cache]
|
||||
enabled = true
|
||||
ttl = 3600
|
||||
|
||||
[mcp_server.prompts.versioning]
|
||||
enabled = false
|
||||
max_versions = 10
|
||||
|
||||
[mcp_server.protocol]
|
||||
version = "1.0"
|
||||
|
||||
[mcp_server.protocol.transport]
|
||||
endpoint = "http://localhost:3000"
|
||||
timeout = 30000
|
||||
|
||||
[mcp_server.resources]
|
||||
enabled = true
|
||||
max_size = 104857600
|
||||
|
||||
[mcp_server.resources.cache]
|
||||
enabled = true
|
||||
max_size_mb = 512
|
||||
ttl = 3600
|
||||
|
||||
[mcp_server.resources.validation]
|
||||
enabled = true
|
||||
max_depth = 10
|
||||
|
||||
[mcp_server.sampling]
|
||||
enabled = false
|
||||
max_tokens = 4096
|
||||
temperature = 0.7
|
||||
|
||||
[mcp_server.sampling.cache]
|
||||
enabled = true
|
||||
ttl = 3600
|
||||
|
||||
[mcp_server.server]
|
||||
graceful_shutdown = true
|
||||
host = "127.0.0.1"
|
||||
keep_alive = 75
|
||||
max_connections = 100
|
||||
port = 3000
|
||||
request_timeout = 30000
|
||||
shutdown_timeout = 30
|
||||
workers = 4
|
||||
|
||||
[mcp_server.tools]
|
||||
enabled = true
|
||||
max_concurrent = 5
|
||||
timeout = 30000
|
||||
|
||||
[mcp_server.tools.cache]
|
||||
enabled = true
|
||||
ttl = 3600
|
||||
|
||||
[mcp_server.tools.validation]
|
||||
enabled = true
|
||||
strict_mode = false
|
||||
|
||||
[mcp_server.workspace]
|
||||
enabled = true
|
||||
multi_workspace = false
|
||||
name = "default"
|
||||
path = "/var/lib/provisioning/mcp-server"
|
||||
@ -1,163 +0,0 @@
|
||||
[mcp_server.capabilities.prompts]
|
||||
enabled = true
|
||||
list_changed_callback = false
|
||||
|
||||
[mcp_server.capabilities.resources]
|
||||
enabled = true
|
||||
list_changed_callback = false
|
||||
subscribe = false
|
||||
|
||||
[mcp_server.capabilities.sampling]
|
||||
enabled = false
|
||||
|
||||
[mcp_server.capabilities.tools]
|
||||
enabled = true
|
||||
list_changed_callback = false
|
||||
|
||||
[mcp_server.control_center_integration]
|
||||
enabled = false
|
||||
enforce_rbac = true
|
||||
|
||||
[mcp_server.logging]
|
||||
format = "&"
|
||||
level = "&"
|
||||
outputs = ["stdout"]
|
||||
|
||||
[mcp_server.logging.fields]
|
||||
caller = false
|
||||
hostname = true
|
||||
pid = true
|
||||
service_name = true
|
||||
stack_trace = false
|
||||
timestamp = true
|
||||
|
||||
[mcp_server.logging.file]
|
||||
compress = false
|
||||
max_age = 30
|
||||
max_backups = 10
|
||||
max_size = 104857600
|
||||
path = "/var/log/provisioning/service.log"
|
||||
|
||||
[mcp_server.logging.performance]
|
||||
enabled = false
|
||||
memory_info = false
|
||||
slow_threshold = 1000
|
||||
|
||||
[mcp_server.logging.sampling]
|
||||
enabled = false
|
||||
initial = 100
|
||||
thereafter = 100
|
||||
|
||||
[mcp_server.logging.syslog]
|
||||
protocol = "udp"
|
||||
|
||||
[mcp_server.monitoring]
|
||||
enabled = false
|
||||
|
||||
[mcp_server.monitoring.alerting]
|
||||
enabled = false
|
||||
|
||||
[mcp_server.monitoring.health_check]
|
||||
enabled = false
|
||||
endpoint = "/health"
|
||||
healthy_threshold = 2
|
||||
interval = 30
|
||||
timeout = 5000
|
||||
type = "&"
|
||||
unhealthy_threshold = 3
|
||||
|
||||
[mcp_server.monitoring.metrics]
|
||||
buffer_size = 1000
|
||||
enabled = false
|
||||
interval = 60
|
||||
prometheus_path = "/metrics"
|
||||
retention_days = 30
|
||||
|
||||
[mcp_server.monitoring.resources]
|
||||
alert_threshold = 80
|
||||
cpu = false
|
||||
disk = false
|
||||
memory = false
|
||||
network = false
|
||||
|
||||
[mcp_server.monitoring.tracing]
|
||||
enabled = false
|
||||
sample_rate = 0.1
|
||||
|
||||
[mcp_server.orchestrator_integration]
|
||||
enabled = false
|
||||
|
||||
[mcp_server.performance]
|
||||
buffer_size = 1024
|
||||
compression = false
|
||||
pool_size = 10
|
||||
|
||||
[mcp_server.prompts]
|
||||
enabled = true
|
||||
max_templates = 100
|
||||
|
||||
[mcp_server.prompts.cache]
|
||||
enabled = true
|
||||
ttl = 3600
|
||||
|
||||
[mcp_server.prompts.versioning]
|
||||
enabled = false
|
||||
max_versions = 10
|
||||
|
||||
[mcp_server.protocol]
|
||||
version = "1.0"
|
||||
|
||||
[mcp_server.protocol.transport]
|
||||
endpoint = "http://localhost:3000"
|
||||
timeout = 30000
|
||||
|
||||
[mcp_server.resources]
|
||||
enabled = true
|
||||
max_size = 104857600
|
||||
|
||||
[mcp_server.resources.cache]
|
||||
enabled = true
|
||||
max_size_mb = 512
|
||||
ttl = 3600
|
||||
|
||||
[mcp_server.resources.validation]
|
||||
enabled = true
|
||||
max_depth = 10
|
||||
|
||||
[mcp_server.sampling]
|
||||
enabled = false
|
||||
max_tokens = 4096
|
||||
temperature = 0.7
|
||||
|
||||
[mcp_server.sampling.cache]
|
||||
enabled = true
|
||||
ttl = 3600
|
||||
|
||||
[mcp_server.server]
|
||||
graceful_shutdown = true
|
||||
host = "127.0.0.1"
|
||||
keep_alive = 75
|
||||
max_connections = 100
|
||||
port = 3000
|
||||
request_timeout = 30000
|
||||
shutdown_timeout = 30
|
||||
workers = 4
|
||||
|
||||
[mcp_server.tools]
|
||||
enabled = true
|
||||
max_concurrent = 5
|
||||
timeout = 30000
|
||||
|
||||
[mcp_server.tools.cache]
|
||||
enabled = true
|
||||
ttl = 3600
|
||||
|
||||
[mcp_server.tools.validation]
|
||||
enabled = true
|
||||
strict_mode = false
|
||||
|
||||
[mcp_server.workspace]
|
||||
enabled = true
|
||||
multi_workspace = false
|
||||
name = "default"
|
||||
path = "/var/lib/provisioning/mcp-server"
|
||||
@ -1,126 +0,0 @@
|
||||
[orchestrator.batch]
|
||||
metrics = false
|
||||
operation_timeout = 1800000
|
||||
parallel_limit = 5
|
||||
|
||||
[orchestrator.batch.checkpointing]
|
||||
enabled = true
|
||||
interval = 100
|
||||
max_checkpoints = 10
|
||||
|
||||
[orchestrator.batch.rollback]
|
||||
enabled = true
|
||||
max_rollback_depth = 5
|
||||
strategy = "checkpoint_based"
|
||||
|
||||
[orchestrator.extensions]
|
||||
auto_load = false
|
||||
discovery_interval = 300
|
||||
max_concurrent = 5
|
||||
sandbox = true
|
||||
timeout = 30000
|
||||
|
||||
[orchestrator.logging]
|
||||
format = "&"
|
||||
level = "&"
|
||||
outputs = ["stdout"]
|
||||
|
||||
[orchestrator.logging.fields]
|
||||
caller = false
|
||||
hostname = true
|
||||
pid = true
|
||||
service_name = true
|
||||
stack_trace = false
|
||||
timestamp = true
|
||||
|
||||
[orchestrator.logging.file]
|
||||
compress = false
|
||||
max_age = 30
|
||||
max_backups = 10
|
||||
max_size = 104857600
|
||||
path = "/var/log/provisioning/service.log"
|
||||
|
||||
[orchestrator.logging.performance]
|
||||
enabled = false
|
||||
memory_info = false
|
||||
slow_threshold = 1000
|
||||
|
||||
[orchestrator.logging.sampling]
|
||||
enabled = false
|
||||
initial = 100
|
||||
thereafter = 100
|
||||
|
||||
[orchestrator.logging.syslog]
|
||||
protocol = "udp"
|
||||
|
||||
[orchestrator.monitoring]
|
||||
enabled = false
|
||||
|
||||
[orchestrator.monitoring.alerting]
|
||||
enabled = false
|
||||
|
||||
[orchestrator.monitoring.health_check]
|
||||
enabled = false
|
||||
endpoint = "/health"
|
||||
healthy_threshold = 2
|
||||
interval = 30
|
||||
timeout = 5000
|
||||
type = "&"
|
||||
unhealthy_threshold = 3
|
||||
|
||||
[orchestrator.monitoring.metrics]
|
||||
buffer_size = 1000
|
||||
enabled = false
|
||||
interval = 60
|
||||
prometheus_path = "/metrics"
|
||||
retention_days = 30
|
||||
|
||||
[orchestrator.monitoring.resources]
|
||||
alert_threshold = 80
|
||||
cpu = false
|
||||
disk = false
|
||||
memory = false
|
||||
network = false
|
||||
|
||||
[orchestrator.monitoring.tracing]
|
||||
enabled = false
|
||||
sample_rate = 0.1
|
||||
|
||||
[orchestrator.queue]
|
||||
max_concurrent_tasks = 5
|
||||
metrics = false
|
||||
persist = true
|
||||
priority_queue = false
|
||||
retry_attempts = 3
|
||||
retry_delay = 5000
|
||||
task_timeout = 3600000
|
||||
|
||||
[orchestrator.queue.dead_letter_queue]
|
||||
enabled = true
|
||||
max_size = 1000
|
||||
|
||||
[orchestrator.server]
|
||||
graceful_shutdown = true
|
||||
host = "127.0.0.1"
|
||||
keep_alive = 75
|
||||
max_connections = 100
|
||||
port = 9090
|
||||
request_timeout = 30000
|
||||
shutdown_timeout = 30
|
||||
workers = 4
|
||||
|
||||
[orchestrator.storage]
|
||||
backend = "filesystem"
|
||||
path = "/var/lib/provisioning/orchestrator/data"
|
||||
|
||||
[orchestrator.storage.cache]
|
||||
enabled = true
|
||||
eviction_policy = "lru"
|
||||
ttl = 3600
|
||||
type = "in_memory"
|
||||
|
||||
[orchestrator.workspace]
|
||||
enabled = true
|
||||
multi_workspace = false
|
||||
name = "default"
|
||||
path = "/var/lib/provisioning/orchestrator"
|
||||
@ -1,126 +0,0 @@
|
||||
[orchestrator.batch]
|
||||
metrics = false
|
||||
operation_timeout = 1800000
|
||||
parallel_limit = 5
|
||||
|
||||
[orchestrator.batch.checkpointing]
|
||||
enabled = true
|
||||
interval = 100
|
||||
max_checkpoints = 10
|
||||
|
||||
[orchestrator.batch.rollback]
|
||||
enabled = true
|
||||
max_rollback_depth = 5
|
||||
strategy = "checkpoint_based"
|
||||
|
||||
[orchestrator.extensions]
|
||||
auto_load = false
|
||||
discovery_interval = 300
|
||||
max_concurrent = 5
|
||||
sandbox = true
|
||||
timeout = 30000
|
||||
|
||||
[orchestrator.logging]
|
||||
format = "&"
|
||||
level = "&"
|
||||
outputs = ["stdout"]
|
||||
|
||||
[orchestrator.logging.fields]
|
||||
caller = false
|
||||
hostname = true
|
||||
pid = true
|
||||
service_name = true
|
||||
stack_trace = false
|
||||
timestamp = true
|
||||
|
||||
[orchestrator.logging.file]
|
||||
compress = false
|
||||
max_age = 30
|
||||
max_backups = 10
|
||||
max_size = 104857600
|
||||
path = "/var/log/provisioning/service.log"
|
||||
|
||||
[orchestrator.logging.performance]
|
||||
enabled = false
|
||||
memory_info = false
|
||||
slow_threshold = 1000
|
||||
|
||||
[orchestrator.logging.sampling]
|
||||
enabled = false
|
||||
initial = 100
|
||||
thereafter = 100
|
||||
|
||||
[orchestrator.logging.syslog]
|
||||
protocol = "udp"
|
||||
|
||||
[orchestrator.monitoring]
|
||||
enabled = false
|
||||
|
||||
[orchestrator.monitoring.alerting]
|
||||
enabled = false
|
||||
|
||||
[orchestrator.monitoring.health_check]
|
||||
enabled = false
|
||||
endpoint = "/health"
|
||||
healthy_threshold = 2
|
||||
interval = 30
|
||||
timeout = 5000
|
||||
type = "&"
|
||||
unhealthy_threshold = 3
|
||||
|
||||
[orchestrator.monitoring.metrics]
|
||||
buffer_size = 1000
|
||||
enabled = false
|
||||
interval = 60
|
||||
prometheus_path = "/metrics"
|
||||
retention_days = 30
|
||||
|
||||
[orchestrator.monitoring.resources]
|
||||
alert_threshold = 80
|
||||
cpu = false
|
||||
disk = false
|
||||
memory = false
|
||||
network = false
|
||||
|
||||
[orchestrator.monitoring.tracing]
|
||||
enabled = false
|
||||
sample_rate = 0.1
|
||||
|
||||
[orchestrator.queue]
|
||||
max_concurrent_tasks = 5
|
||||
metrics = false
|
||||
persist = true
|
||||
priority_queue = false
|
||||
retry_attempts = 3
|
||||
retry_delay = 5000
|
||||
task_timeout = 3600000
|
||||
|
||||
[orchestrator.queue.dead_letter_queue]
|
||||
enabled = true
|
||||
max_size = 1000
|
||||
|
||||
[orchestrator.server]
|
||||
graceful_shutdown = true
|
||||
host = "127.0.0.1"
|
||||
keep_alive = 75
|
||||
max_connections = 100
|
||||
port = 9090
|
||||
request_timeout = 30000
|
||||
shutdown_timeout = 30
|
||||
workers = 4
|
||||
|
||||
[orchestrator.storage]
|
||||
backend = "filesystem"
|
||||
path = "/var/lib/provisioning/orchestrator/data"
|
||||
|
||||
[orchestrator.storage.cache]
|
||||
enabled = true
|
||||
eviction_policy = "lru"
|
||||
ttl = 3600
|
||||
type = "in_memory"
|
||||
|
||||
[orchestrator.workspace]
|
||||
enabled = true
|
||||
multi_workspace = false
|
||||
name = "default"
|
||||
path = "/var/lib/provisioning/orchestrator"
|
||||
@ -1,126 +0,0 @@
|
||||
[orchestrator.batch]
|
||||
metrics = false
|
||||
operation_timeout = 1800000
|
||||
parallel_limit = 5
|
||||
|
||||
[orchestrator.batch.checkpointing]
|
||||
enabled = true
|
||||
interval = 100
|
||||
max_checkpoints = 10
|
||||
|
||||
[orchestrator.batch.rollback]
|
||||
enabled = true
|
||||
max_rollback_depth = 5
|
||||
strategy = "checkpoint_based"
|
||||
|
||||
[orchestrator.extensions]
|
||||
auto_load = false
|
||||
discovery_interval = 300
|
||||
max_concurrent = 5
|
||||
sandbox = true
|
||||
timeout = 30000
|
||||
|
||||
[orchestrator.logging]
|
||||
format = "&"
|
||||
level = "&"
|
||||
outputs = ["stdout"]
|
||||
|
||||
[orchestrator.logging.fields]
|
||||
caller = false
|
||||
hostname = true
|
||||
pid = true
|
||||
service_name = true
|
||||
stack_trace = false
|
||||
timestamp = true
|
||||
|
||||
[orchestrator.logging.file]
|
||||
compress = false
|
||||
max_age = 30
|
||||
max_backups = 10
|
||||
max_size = 104857600
|
||||
path = "/var/log/provisioning/service.log"
|
||||
|
||||
[orchestrator.logging.performance]
|
||||
enabled = false
|
||||
memory_info = false
|
||||
slow_threshold = 1000
|
||||
|
||||
[orchestrator.logging.sampling]
|
||||
enabled = false
|
||||
initial = 100
|
||||
thereafter = 100
|
||||
|
||||
[orchestrator.logging.syslog]
|
||||
protocol = "udp"
|
||||
|
||||
[orchestrator.monitoring]
|
||||
enabled = false
|
||||
|
||||
[orchestrator.monitoring.alerting]
|
||||
enabled = false
|
||||
|
||||
[orchestrator.monitoring.health_check]
|
||||
enabled = false
|
||||
endpoint = "/health"
|
||||
healthy_threshold = 2
|
||||
interval = 30
|
||||
timeout = 5000
|
||||
type = "&"
|
||||
unhealthy_threshold = 3
|
||||
|
||||
[orchestrator.monitoring.metrics]
|
||||
buffer_size = 1000
|
||||
enabled = false
|
||||
interval = 60
|
||||
prometheus_path = "/metrics"
|
||||
retention_days = 30
|
||||
|
||||
[orchestrator.monitoring.resources]
|
||||
alert_threshold = 80
|
||||
cpu = false
|
||||
disk = false
|
||||
memory = false
|
||||
network = false
|
||||
|
||||
[orchestrator.monitoring.tracing]
|
||||
enabled = false
|
||||
sample_rate = 0.1
|
||||
|
||||
[orchestrator.queue]
|
||||
max_concurrent_tasks = 5
|
||||
metrics = false
|
||||
persist = true
|
||||
priority_queue = false
|
||||
retry_attempts = 3
|
||||
retry_delay = 5000
|
||||
task_timeout = 3600000
|
||||
|
||||
[orchestrator.queue.dead_letter_queue]
|
||||
enabled = true
|
||||
max_size = 1000
|
||||
|
||||
[orchestrator.server]
|
||||
graceful_shutdown = true
|
||||
host = "127.0.0.1"
|
||||
keep_alive = 75
|
||||
max_connections = 100
|
||||
port = 9090
|
||||
request_timeout = 30000
|
||||
shutdown_timeout = 30
|
||||
workers = 4
|
||||
|
||||
[orchestrator.storage]
|
||||
backend = "filesystem"
|
||||
path = "/var/lib/provisioning/orchestrator/data"
|
||||
|
||||
[orchestrator.storage.cache]
|
||||
enabled = true
|
||||
eviction_policy = "lru"
|
||||
ttl = 3600
|
||||
type = "in_memory"
|
||||
|
||||
[orchestrator.workspace]
|
||||
enabled = true
|
||||
multi_workspace = false
|
||||
name = "default"
|
||||
path = "/var/lib/provisioning/orchestrator"
|
||||
@ -1,126 +0,0 @@
|
||||
[orchestrator.batch]
|
||||
metrics = false
|
||||
operation_timeout = 1800000
|
||||
parallel_limit = 5
|
||||
|
||||
[orchestrator.batch.checkpointing]
|
||||
enabled = true
|
||||
interval = 100
|
||||
max_checkpoints = 10
|
||||
|
||||
[orchestrator.batch.rollback]
|
||||
enabled = true
|
||||
max_rollback_depth = 5
|
||||
strategy = "checkpoint_based"
|
||||
|
||||
[orchestrator.extensions]
|
||||
auto_load = false
|
||||
discovery_interval = 300
|
||||
max_concurrent = 5
|
||||
sandbox = true
|
||||
timeout = 30000
|
||||
|
||||
[orchestrator.logging]
|
||||
format = "&"
|
||||
level = "&"
|
||||
outputs = ["stdout"]
|
||||
|
||||
[orchestrator.logging.fields]
|
||||
caller = false
|
||||
hostname = true
|
||||
pid = true
|
||||
service_name = true
|
||||
stack_trace = false
|
||||
timestamp = true
|
||||
|
||||
[orchestrator.logging.file]
|
||||
compress = false
|
||||
max_age = 30
|
||||
max_backups = 10
|
||||
max_size = 104857600
|
||||
path = "/var/log/provisioning/service.log"
|
||||
|
||||
[orchestrator.logging.performance]
|
||||
enabled = false
|
||||
memory_info = false
|
||||
slow_threshold = 1000
|
||||
|
||||
[orchestrator.logging.sampling]
|
||||
enabled = false
|
||||
initial = 100
|
||||
thereafter = 100
|
||||
|
||||
[orchestrator.logging.syslog]
|
||||
protocol = "udp"
|
||||
|
||||
[orchestrator.monitoring]
|
||||
enabled = false
|
||||
|
||||
[orchestrator.monitoring.alerting]
|
||||
enabled = false
|
||||
|
||||
[orchestrator.monitoring.health_check]
|
||||
enabled = false
|
||||
endpoint = "/health"
|
||||
healthy_threshold = 2
|
||||
interval = 30
|
||||
timeout = 5000
|
||||
type = "&"
|
||||
unhealthy_threshold = 3
|
||||
|
||||
[orchestrator.monitoring.metrics]
|
||||
buffer_size = 1000
|
||||
enabled = false
|
||||
interval = 60
|
||||
prometheus_path = "/metrics"
|
||||
retention_days = 30
|
||||
|
||||
[orchestrator.monitoring.resources]
|
||||
alert_threshold = 80
|
||||
cpu = false
|
||||
disk = false
|
||||
memory = false
|
||||
network = false
|
||||
|
||||
[orchestrator.monitoring.tracing]
|
||||
enabled = false
|
||||
sample_rate = 0.1
|
||||
|
||||
[orchestrator.queue]
|
||||
max_concurrent_tasks = 5
|
||||
metrics = false
|
||||
persist = true
|
||||
priority_queue = false
|
||||
retry_attempts = 3
|
||||
retry_delay = 5000
|
||||
task_timeout = 3600000
|
||||
|
||||
[orchestrator.queue.dead_letter_queue]
|
||||
enabled = true
|
||||
max_size = 1000
|
||||
|
||||
[orchestrator.server]
|
||||
graceful_shutdown = true
|
||||
host = "127.0.0.1"
|
||||
keep_alive = 75
|
||||
max_connections = 100
|
||||
port = 9090
|
||||
request_timeout = 30000
|
||||
shutdown_timeout = 30
|
||||
workers = 4
|
||||
|
||||
[orchestrator.storage]
|
||||
backend = "filesystem"
|
||||
path = "/var/lib/provisioning/orchestrator/data"
|
||||
|
||||
[orchestrator.storage.cache]
|
||||
enabled = true
|
||||
eviction_policy = "lru"
|
||||
ttl = 3600
|
||||
type = "in_memory"
|
||||
|
||||
[orchestrator.workspace]
|
||||
enabled = true
|
||||
multi_workspace = false
|
||||
name = "default"
|
||||
path = "/var/lib/provisioning/orchestrator"
|
||||
@ -1,13 +0,0 @@
|
||||
[daemon.actions]
|
||||
auto_cleanup = true
|
||||
auto_update = false
|
||||
ephemeral_cleanup = true
|
||||
|
||||
[daemon.daemon]
|
||||
enabled = true
|
||||
max_workers = 8
|
||||
poll_interval = 10
|
||||
|
||||
[daemon.logging]
|
||||
file = "/tmp/provisioning-daemon-cicd.log"
|
||||
level = "warn"
|
||||
@ -1,18 +0,0 @@
|
||||
[daemon.actions]
|
||||
auto_cleanup = true
|
||||
auto_update = true
|
||||
health_checks = true
|
||||
workspace_sync = true
|
||||
|
||||
[daemon.daemon]
|
||||
enabled = true
|
||||
max_workers = 16
|
||||
poll_interval = 30
|
||||
|
||||
[daemon.logging]
|
||||
file = "/var/log/provisioning/daemon.log"
|
||||
level = "info"
|
||||
syslog = true
|
||||
|
||||
[daemon.monitoring]
|
||||
enabled = true
|
||||
@ -1,13 +0,0 @@
|
||||
[daemon.actions]
|
||||
auto_cleanup = true
|
||||
auto_update = false
|
||||
workspace_sync = true
|
||||
|
||||
[daemon.daemon]
|
||||
enabled = true
|
||||
max_workers = 4
|
||||
poll_interval = 30
|
||||
|
||||
[daemon.logging]
|
||||
file = "/var/log/provisioning/daemon.log"
|
||||
level = "info"
|
||||
@ -1,12 +0,0 @@
|
||||
[daemon.actions]
|
||||
auto_cleanup = false
|
||||
auto_update = false
|
||||
|
||||
[daemon.daemon]
|
||||
enabled = true
|
||||
max_workers = 2
|
||||
poll_interval = 60
|
||||
|
||||
[daemon.logging]
|
||||
file = "/tmp/provisioning-daemon-solo.log"
|
||||
level = "info"
|
||||
@ -1,2 +0,0 @@
|
||||
[rag.rag]
|
||||
enabled = false
|
||||
@ -1,39 +0,0 @@
|
||||
[rag.embeddings]
|
||||
batch_size = 200
|
||||
dimension = 3072
|
||||
model = "text-embedding-3-large"
|
||||
provider = "openai"
|
||||
|
||||
[rag.ingestion]
|
||||
auto_ingest = true
|
||||
chunk_size = 2048
|
||||
doc_types = ["md", "txt", "toml", "ncl", "rs", "nu", "yaml", "json"]
|
||||
overlap = 200
|
||||
watch_files = true
|
||||
|
||||
[rag.llm]
|
||||
max_tokens = 8192
|
||||
model = "claude-opus-4-5-20251101"
|
||||
provider = "anthropic"
|
||||
temperature = 0.5
|
||||
|
||||
[rag.monitoring]
|
||||
enabled = true
|
||||
|
||||
[rag.rag]
|
||||
enabled = true
|
||||
|
||||
[rag.retrieval]
|
||||
hybrid = true
|
||||
mmr_lambda = 0.5
|
||||
reranking = true
|
||||
similarity_threshold = 0.8
|
||||
top_k = 20
|
||||
|
||||
[rag.vector_db]
|
||||
database = "rag"
|
||||
db_type = "surrealdb"
|
||||
hnsw_ef_construction = 400
|
||||
hnsw_m = 32
|
||||
namespace = "provisioning-prod"
|
||||
url = "ws://surrealdb-cluster:8000"
|
||||
@ -1,35 +0,0 @@
|
||||
[rag.embeddings]
|
||||
batch_size = 100
|
||||
dimension = 1536
|
||||
model = "text-embedding-3-small"
|
||||
provider = "openai"
|
||||
|
||||
[rag.ingestion]
|
||||
auto_ingest = true
|
||||
chunk_size = 1024
|
||||
doc_types = ["md", "txt", "toml", "ncl", "rs", "nu"]
|
||||
overlap = 100
|
||||
watch_files = true
|
||||
|
||||
[rag.llm]
|
||||
max_tokens = 4096
|
||||
model = "claude-3-5-sonnet-20241022"
|
||||
provider = "anthropic"
|
||||
temperature = 0.7
|
||||
|
||||
[rag.rag]
|
||||
enabled = true
|
||||
|
||||
[rag.retrieval]
|
||||
hybrid = true
|
||||
reranking = true
|
||||
similarity_threshold = 0.75
|
||||
top_k = 10
|
||||
|
||||
[rag.vector_db]
|
||||
database = "rag"
|
||||
db_type = "surrealdb"
|
||||
hnsw_ef_construction = 200
|
||||
hnsw_m = 16
|
||||
namespace = "provisioning-team"
|
||||
url = "http://surrealdb:8000"
|
||||
@ -1,31 +0,0 @@
|
||||
[rag.embeddings]
|
||||
batch_size = 32
|
||||
dimension = 384
|
||||
model = "all-MiniLM-L6-v2"
|
||||
provider = "local"
|
||||
|
||||
[rag.ingestion]
|
||||
auto_ingest = true
|
||||
chunk_size = 512
|
||||
doc_types = ["md", "txt", "toml"]
|
||||
overlap = 50
|
||||
|
||||
[rag.llm]
|
||||
api_url = "http://localhost:11434"
|
||||
max_tokens = 2048
|
||||
model = "llama3.2"
|
||||
provider = "ollama"
|
||||
temperature = 0.7
|
||||
|
||||
[rag.rag]
|
||||
enabled = true
|
||||
|
||||
[rag.retrieval]
|
||||
hybrid = false
|
||||
reranking = false
|
||||
similarity_threshold = 0.7
|
||||
top_k = 5
|
||||
|
||||
[rag.vector_db]
|
||||
db_type = "memory"
|
||||
namespace = "provisioning-solo"
|
||||
@ -1,35 +0,0 @@
|
||||
[vault.ha]
|
||||
enabled = false
|
||||
mode = "raft"
|
||||
|
||||
[vault.logging]
|
||||
format = "json"
|
||||
level = "warn"
|
||||
|
||||
[vault.monitoring]
|
||||
enabled = false
|
||||
metrics_interval = 60
|
||||
|
||||
[vault.security]
|
||||
encryption_algorithm = "aes-256-gcm"
|
||||
key_rotation_days = 90
|
||||
|
||||
[vault.server]
|
||||
host = "0.0.0.0"
|
||||
keep_alive = 75
|
||||
max_connections = 200
|
||||
port = 8200
|
||||
workers = 8
|
||||
|
||||
[vault.storage]
|
||||
backend = "memory"
|
||||
encryption_key_path = "/tmp/provisioning-vault-cicd/master.key"
|
||||
path = "/tmp/provisioning-vault-cicd"
|
||||
|
||||
[vault.vault]
|
||||
deployment_mode = "Service"
|
||||
key_name = "provisioning-cicd"
|
||||
mount_point = "transit-cicd"
|
||||
server_url = "http://vault-cicd:8200"
|
||||
storage_backend = "memory"
|
||||
tls_verify = false
|
||||
@ -1,36 +0,0 @@
|
||||
[vault.ha]
|
||||
enabled = true
|
||||
mode = "raft"
|
||||
|
||||
[vault.logging]
|
||||
format = "json"
|
||||
level = "info"
|
||||
|
||||
[vault.monitoring]
|
||||
enabled = true
|
||||
metrics_interval = 30
|
||||
|
||||
[vault.security]
|
||||
encryption_algorithm = "aes-256-gcm"
|
||||
key_rotation_days = 30
|
||||
|
||||
[vault.server]
|
||||
host = "0.0.0.0"
|
||||
keep_alive = 75
|
||||
max_connections = 500
|
||||
port = 8200
|
||||
workers = 16
|
||||
|
||||
[vault.storage]
|
||||
backend = "etcd"
|
||||
encryption_key_path = "/var/lib/provisioning/vault/master.key"
|
||||
path = "/var/lib/provisioning/vault/data"
|
||||
|
||||
[vault.vault]
|
||||
deployment_mode = "Service"
|
||||
key_name = "provisioning-enterprise"
|
||||
mount_point = "transit"
|
||||
server_url = "https://vault-ha:8200"
|
||||
storage_backend = "etcd"
|
||||
tls_ca_cert = "/etc/vault/ca.crt"
|
||||
tls_verify = true
|
||||
@ -1,35 +0,0 @@
|
||||
[vault.ha]
|
||||
enabled = false
|
||||
mode = "raft"
|
||||
|
||||
[vault.logging]
|
||||
format = "json"
|
||||
level = "info"
|
||||
|
||||
[vault.monitoring]
|
||||
enabled = true
|
||||
metrics_interval = 60
|
||||
|
||||
[vault.security]
|
||||
encryption_algorithm = "aes-256-gcm"
|
||||
key_rotation_days = 90
|
||||
|
||||
[vault.server]
|
||||
host = "0.0.0.0"
|
||||
keep_alive = 75
|
||||
max_connections = 100
|
||||
port = 8200
|
||||
workers = 4
|
||||
|
||||
[vault.storage]
|
||||
backend = "surrealdb"
|
||||
encryption_key_path = "/var/lib/provisioning/vault/master.key"
|
||||
path = "/var/lib/provisioning/vault/data"
|
||||
|
||||
[vault.vault]
|
||||
deployment_mode = "Service"
|
||||
key_name = "provisioning-master"
|
||||
mount_point = "transit"
|
||||
server_url = "http://localhost:8200"
|
||||
storage_backend = "surrealdb"
|
||||
tls_verify = false
|
||||
@ -1,35 +0,0 @@
|
||||
[vault.ha]
|
||||
enabled = false
|
||||
mode = "raft"
|
||||
|
||||
[vault.logging]
|
||||
format = "json"
|
||||
level = "info"
|
||||
|
||||
[vault.monitoring]
|
||||
enabled = false
|
||||
metrics_interval = 60
|
||||
|
||||
[vault.security]
|
||||
encryption_algorithm = "aes-256-gcm"
|
||||
key_rotation_days = 90
|
||||
|
||||
[vault.server]
|
||||
host = "127.0.0.1"
|
||||
keep_alive = 75
|
||||
max_connections = 50
|
||||
port = 8200
|
||||
workers = 2
|
||||
|
||||
[vault.storage]
|
||||
backend = "filesystem"
|
||||
encryption_key_path = "/tmp/provisioning-vault-solo/master.key"
|
||||
path = "/tmp/provisioning-vault-solo/data"
|
||||
|
||||
[vault.vault]
|
||||
deployment_mode = "Embedded"
|
||||
key_name = "provisioning-master"
|
||||
mount_point = "transit"
|
||||
server_url = "http://localhost:8200"
|
||||
storage_backend = "filesystem"
|
||||
tls_verify = false
|
||||
@ -28,6 +28,9 @@ toml = { workspace = true }
|
||||
# Platform configuration
|
||||
platform-config = { workspace = true }
|
||||
|
||||
# Centralized observability (logging, metrics, health, tracing)
|
||||
observability = { workspace = true, features = ["logging", "metrics-prometheus", "health"] }
|
||||
|
||||
# Error handling
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
@ -1,240 +1,93 @@
|
||||
use std::env;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Context;
|
||||
use platform_config::ConfigLoader;
|
||||
/// AI Service configuration
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Main AI Service configuration
|
||||
/// AI Service configuration
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct AiServiceConfig {
|
||||
/// Server configuration
|
||||
#[serde(default)]
|
||||
pub server: ServerConfig,
|
||||
|
||||
/// RAG integration configuration
|
||||
#[serde(default)]
|
||||
pub rag: RagIntegrationConfig,
|
||||
|
||||
/// MCP integration configuration
|
||||
#[serde(default)]
|
||||
pub mcp: McpIntegrationConfig,
|
||||
|
||||
/// DAG execution configuration
|
||||
#[serde(default)]
|
||||
pub dag: DagConfig,
|
||||
pub ai_service: AiServiceSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct AiServiceSettings {
|
||||
pub server: ServerConfig,
|
||||
pub rag: RagConfig,
|
||||
pub mcp: McpConfig,
|
||||
pub dag: DagConfig,
|
||||
#[serde(default)]
|
||||
pub monitoring: Option<MonitoringConfig>,
|
||||
#[serde(default)]
|
||||
pub logging: Option<LoggingConfig>,
|
||||
#[serde(default)]
|
||||
pub build: Option<DockerBuildConfig>,
|
||||
}
|
||||
|
||||
/// Server configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServerConfig {
|
||||
/// Server bind address
|
||||
#[serde(default = "default_host")]
|
||||
pub host: String,
|
||||
|
||||
/// Server port
|
||||
#[serde(default = "default_server_port")]
|
||||
pub port: u16,
|
||||
|
||||
/// Number of worker threads
|
||||
#[serde(default = "default_workers")]
|
||||
pub workers: usize,
|
||||
|
||||
/// TCP keep-alive timeout (seconds)
|
||||
#[serde(default = "default_keep_alive")]
|
||||
pub keep_alive: u64,
|
||||
|
||||
/// Request timeout (milliseconds)
|
||||
#[serde(default = "default_request_timeout")]
|
||||
pub request_timeout: u64,
|
||||
}
|
||||
|
||||
/// RAG integration configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RagIntegrationConfig {
|
||||
/// Enable RAG integration
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
|
||||
/// RAG service URL
|
||||
#[serde(default = "default_rag_url")]
|
||||
pub rag_service_url: String,
|
||||
|
||||
/// Request timeout (milliseconds)
|
||||
#[serde(default = "default_rag_timeout")]
|
||||
pub timeout: u64,
|
||||
|
||||
/// Max retries for failed requests
|
||||
#[serde(default = "default_max_retries")]
|
||||
pub max_retries: u32,
|
||||
|
||||
/// Enable response caching
|
||||
#[serde(default = "default_true")]
|
||||
pub cache_enabled: bool,
|
||||
}
|
||||
|
||||
/// MCP integration configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpIntegrationConfig {
|
||||
/// Enable MCP integration
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
|
||||
/// MCP service URL
|
||||
#[serde(default = "default_mcp_url")]
|
||||
pub mcp_service_url: String,
|
||||
|
||||
/// Request timeout (milliseconds)
|
||||
#[serde(default = "default_mcp_timeout")]
|
||||
pub timeout: u64,
|
||||
|
||||
/// Max retries for failed requests
|
||||
#[serde(default = "default_max_retries")]
|
||||
pub max_retries: u32,
|
||||
|
||||
/// MCP protocol version
|
||||
#[serde(default = "default_protocol_version")]
|
||||
pub protocol_version: String,
|
||||
}
|
||||
|
||||
/// DAG execution configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DagConfig {
|
||||
/// Maximum concurrent tasks
|
||||
#[serde(default = "default_max_concurrent_tasks")]
|
||||
pub max_concurrent_tasks: usize,
|
||||
|
||||
/// Task timeout (milliseconds)
|
||||
#[serde(default = "default_task_timeout")]
|
||||
pub task_timeout: u64,
|
||||
|
||||
/// Number of retry attempts
|
||||
#[serde(default = "default_dag_retry_attempts")]
|
||||
pub retry_attempts: u32,
|
||||
|
||||
/// Delay between retries (milliseconds)
|
||||
#[serde(default = "default_retry_delay")]
|
||||
pub retry_delay: u64,
|
||||
|
||||
/// Task queue size
|
||||
#[serde(default = "default_queue_size")]
|
||||
pub queue_size: usize,
|
||||
}
|
||||
|
||||
// Default value functions
|
||||
fn default_host() -> String {
|
||||
"127.0.0.1".to_string()
|
||||
}
|
||||
|
||||
fn default_server_port() -> u16 {
|
||||
8082
|
||||
}
|
||||
|
||||
fn default_workers() -> usize {
|
||||
4
|
||||
}
|
||||
|
||||
fn default_keep_alive() -> u64 {
|
||||
75
|
||||
}
|
||||
|
||||
fn default_request_timeout() -> u64 {
|
||||
30000
|
||||
}
|
||||
|
||||
fn default_rag_url() -> String {
|
||||
"http://localhost:8083".to_string()
|
||||
}
|
||||
|
||||
fn default_rag_timeout() -> u64 {
|
||||
30000
|
||||
}
|
||||
|
||||
fn default_mcp_url() -> String {
|
||||
"http://localhost:8084".to_string()
|
||||
}
|
||||
|
||||
fn default_mcp_timeout() -> u64 {
|
||||
30000
|
||||
}
|
||||
|
||||
fn default_max_retries() -> u32 {
|
||||
3
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_protocol_version() -> String {
|
||||
"1.0".to_string()
|
||||
}
|
||||
|
||||
fn default_max_concurrent_tasks() -> usize {
|
||||
10
|
||||
}
|
||||
|
||||
fn default_task_timeout() -> u64 {
|
||||
600000
|
||||
}
|
||||
|
||||
fn default_dag_retry_attempts() -> u32 {
|
||||
3
|
||||
}
|
||||
|
||||
fn default_retry_delay() -> u64 {
|
||||
1000
|
||||
}
|
||||
|
||||
fn default_queue_size() -> usize {
|
||||
1000
|
||||
pub workers: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for ServerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
host: default_host(),
|
||||
port: default_server_port(),
|
||||
workers: default_workers(),
|
||||
keep_alive: default_keep_alive(),
|
||||
request_timeout: default_request_timeout(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 8082,
|
||||
workers: Some(4),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RagIntegrationConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
rag_service_url: default_rag_url(),
|
||||
timeout: default_rag_timeout(),
|
||||
max_retries: default_max_retries(),
|
||||
cache_enabled: default_true(),
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct RagConfig {
|
||||
pub enabled: bool,
|
||||
pub rag_service_url: Option<String>,
|
||||
pub timeout: Option<u64>,
|
||||
}
|
||||
|
||||
impl Default for McpIntegrationConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
mcp_service_url: default_mcp_url(),
|
||||
timeout: default_mcp_timeout(),
|
||||
max_retries: default_max_retries(),
|
||||
protocol_version: default_protocol_version(),
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct McpConfig {
|
||||
pub enabled: bool,
|
||||
pub mcp_service_url: Option<String>,
|
||||
pub timeout: Option<u64>,
|
||||
}
|
||||
|
||||
impl Default for DagConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_concurrent_tasks: default_max_concurrent_tasks(),
|
||||
task_timeout: default_task_timeout(),
|
||||
retry_attempts: default_dag_retry_attempts(),
|
||||
retry_delay: default_retry_delay(),
|
||||
queue_size: default_queue_size(),
|
||||
}
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct DagConfig {
|
||||
pub max_concurrent_tasks: Option<usize>,
|
||||
pub task_timeout: Option<u64>,
|
||||
pub retry_attempts: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MonitoringConfig {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LoggingConfig {
|
||||
pub level: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DockerBuildConfig {
|
||||
pub base_image: String,
|
||||
#[serde(default)]
|
||||
pub build_args: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl AiServiceConfig {
|
||||
pub fn load() -> anyhow::Result<Self> {
|
||||
let config_json = platform_config::load_service_config_from_ncl("ai-service")
|
||||
.context("Failed to load ai-service configuration from Nickel")?;
|
||||
|
||||
serde_json::from_value(config_json)
|
||||
.context("Failed to deserialize ai-service configuration")
|
||||
}
|
||||
}
|
||||
|
||||
@ -245,20 +98,23 @@ impl ConfigLoader for AiServiceConfig {
|
||||
|
||||
fn load_from_hierarchy() -> std::result::Result<Self, Box<dyn std::error::Error + Send + Sync>>
|
||||
{
|
||||
let service = Self::service_name();
|
||||
|
||||
if let Some(path) = platform_config::resolve_config_path(service) {
|
||||
if let Some(path) = platform_config::resolve_config_path(Self::service_name()) {
|
||||
return Self::from_path(&path);
|
||||
}
|
||||
|
||||
// Fallback to defaults
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
fn apply_env_overrides(
|
||||
&mut self,
|
||||
) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
Self::apply_env_overrides_internal(self);
|
||||
if let Ok(host) = std::env::var("AI_SERVICE_SERVER_HOST") {
|
||||
self.ai_service.server.host = host;
|
||||
}
|
||||
if let Ok(port) = std::env::var("AI_SERVICE_SERVER_PORT") {
|
||||
if let Ok(p) = port.parse() {
|
||||
self.ai_service.server.port = p;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -266,16 +122,11 @@ impl ConfigLoader for AiServiceConfig {
|
||||
path: P,
|
||||
) -> std::result::Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let path = path.as_ref();
|
||||
let json_value = platform_config::format::load_config(path).map_err(|e| {
|
||||
let err: Box<dyn std::error::Error + Send + Sync> = Box::new(e);
|
||||
err
|
||||
})?;
|
||||
let json_value = platform_config::format::load_config(path)
|
||||
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
|
||||
|
||||
serde_json::from_value(json_value).map_err(|e| {
|
||||
let err_msg = format!(
|
||||
"Failed to deserialize AI service config from {:?}: {}",
|
||||
path, e
|
||||
);
|
||||
let err_msg = format!("Failed to deserialize ai-service config: {}", e);
|
||||
Box::new(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
err_msg,
|
||||
@ -284,87 +135,6 @@ impl ConfigLoader for AiServiceConfig {
|
||||
}
|
||||
}
|
||||
|
||||
impl AiServiceConfig {
|
||||
/// Load configuration from hierarchical sources with mode support
|
||||
///
|
||||
/// Priority order:
|
||||
/// 1. AI_SERVICE_CONFIG environment variable (explicit path)
|
||||
/// 2. AI_SERVICE_MODE environment variable (mode-specific file)
|
||||
/// 3. Default configuration
|
||||
///
|
||||
/// After loading, applies environment variable overrides.
|
||||
pub fn load_from_hierarchy() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
<Self as ConfigLoader>::load_from_hierarchy().map_err(|_e| {
|
||||
Box::new(std::io::Error::other("Failed to load AI service config"))
|
||||
as Box<dyn std::error::Error>
|
||||
})
|
||||
}
|
||||
|
||||
/// Internal: Apply environment variable overrides (mutable reference)
|
||||
///
|
||||
/// Overrides take precedence over loaded config values.
|
||||
/// Pattern: AI_SERVICE_{SECTION}_{KEY}
|
||||
fn apply_env_overrides_internal(config: &mut Self) {
|
||||
// Server overrides
|
||||
if let Ok(val) = env::var("AI_SERVICE_SERVER_HOST") {
|
||||
config.server.host = val;
|
||||
}
|
||||
if let Ok(val) = env::var("AI_SERVICE_SERVER_PORT") {
|
||||
if let Ok(port) = val.parse() {
|
||||
config.server.port = port;
|
||||
}
|
||||
}
|
||||
if let Ok(val) = env::var("AI_SERVICE_SERVER_WORKERS") {
|
||||
if let Ok(workers) = val.parse() {
|
||||
config.server.workers = workers;
|
||||
}
|
||||
}
|
||||
|
||||
// RAG integration overrides
|
||||
if let Ok(val) = env::var("AI_SERVICE_RAG_ENABLED") {
|
||||
config.rag.enabled = val.parse().unwrap_or(config.rag.enabled);
|
||||
}
|
||||
if let Ok(val) = env::var("AI_SERVICE_RAG_URL") {
|
||||
config.rag.rag_service_url = val;
|
||||
}
|
||||
if let Ok(val) = env::var("AI_SERVICE_RAG_TIMEOUT") {
|
||||
if let Ok(timeout) = val.parse() {
|
||||
config.rag.timeout = timeout;
|
||||
}
|
||||
}
|
||||
|
||||
// MCP integration overrides
|
||||
if let Ok(val) = env::var("AI_SERVICE_MCP_ENABLED") {
|
||||
config.mcp.enabled = val.parse().unwrap_or(config.mcp.enabled);
|
||||
}
|
||||
if let Ok(val) = env::var("AI_SERVICE_MCP_URL") {
|
||||
config.mcp.mcp_service_url = val;
|
||||
}
|
||||
if let Ok(val) = env::var("AI_SERVICE_MCP_TIMEOUT") {
|
||||
if let Ok(timeout) = val.parse() {
|
||||
config.mcp.timeout = timeout;
|
||||
}
|
||||
}
|
||||
|
||||
// DAG overrides
|
||||
if let Ok(val) = env::var("AI_SERVICE_DAG_MAX_CONCURRENT_TASKS") {
|
||||
if let Ok(tasks) = val.parse() {
|
||||
config.dag.max_concurrent_tasks = tasks;
|
||||
}
|
||||
}
|
||||
if let Ok(val) = env::var("AI_SERVICE_DAG_TASK_TIMEOUT") {
|
||||
if let Ok(timeout) = val.parse() {
|
||||
config.dag.task_timeout = timeout;
|
||||
}
|
||||
}
|
||||
if let Ok(val) = env::var("AI_SERVICE_DAG_RETRY_ATTEMPTS") {
|
||||
if let Ok(retries) = val.parse() {
|
||||
config.dag.retry_attempts = retries;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@ -372,26 +142,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_default_config() {
|
||||
let config = AiServiceConfig::default();
|
||||
assert_eq!(config.server.port, 8082);
|
||||
assert_eq!(config.server.workers, 4);
|
||||
assert!(!config.rag.enabled);
|
||||
assert!(!config.mcp.enabled);
|
||||
assert_eq!(config.dag.max_concurrent_tasks, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_server_config_defaults() {
|
||||
let server = ServerConfig::default();
|
||||
assert_eq!(server.host, "127.0.0.1");
|
||||
assert_eq!(server.port, 8082);
|
||||
assert_eq!(server.workers, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dag_config_defaults() {
|
||||
let dag = DagConfig::default();
|
||||
assert_eq!(dag.max_concurrent_tasks, 10);
|
||||
assert_eq!(dag.task_timeout, 600000);
|
||||
assert_eq!(dag.retry_attempts, 3);
|
||||
assert_eq!(config.ai_service.server.port, 8082);
|
||||
assert!(!config.ai_service.rag.enabled);
|
||||
assert!(!config.ai_service.mcp.enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,6 @@ use std::sync::Arc;
|
||||
use ai_service::{handlers, AiService, DEFAULT_PORT};
|
||||
use axum::Router;
|
||||
use clap::Parser;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "ai-service")]
|
||||
@ -36,15 +35,28 @@ struct Args {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// Initialize tracing
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::new(
|
||||
std::env::var("RUST_LOG").unwrap_or_else(|_| "ai_service=info,axum=debug".to_string()),
|
||||
))
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
// Parse CLI arguments FIRST (so --help works before any other processing)
|
||||
let args = Args::parse();
|
||||
|
||||
// Initialize centralized observability (logging, metrics, health checks)
|
||||
let _guard = observability::init_from_env("ai-service", env!("CARGO_PKG_VERSION"))?;
|
||||
|
||||
// Check if ai-service is enabled in deployment-mode.ncl
|
||||
if let Ok(deployment) = platform_config::load_deployment_mode() {
|
||||
if let Ok(enabled) = deployment.is_service_enabled("ai_service") {
|
||||
if !enabled {
|
||||
tracing::warn!("⚠ AI Service is DISABLED in deployment-mode.ncl");
|
||||
std::process::exit(1);
|
||||
}
|
||||
tracing::info!("✓ AI Service is ENABLED in deployment-mode.ncl");
|
||||
}
|
||||
}
|
||||
|
||||
// Try to load ai-service.ncl
|
||||
if let Ok(config) = platform_config::load_service_config_from_ncl("ai-service") {
|
||||
tracing::info!("✓ Loaded ai-service configuration from NCL");
|
||||
tracing::debug!("Config: {:?}", config);
|
||||
}
|
||||
let addr: SocketAddr = format!("{}:{}", args.host, args.port).parse()?;
|
||||
|
||||
// Create service
|
||||
|
||||
@ -100,7 +100,10 @@ async fn test_explicit_tool_call_rag_ask() {
|
||||
args: json!({"question": "What is Nushell?"}),
|
||||
};
|
||||
|
||||
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed");
|
||||
let response = service
|
||||
.call_mcp_tool(req)
|
||||
.await
|
||||
.expect("MCP tool call failed");
|
||||
assert_eq!(response.result["status"], "success");
|
||||
assert_eq!(response.result["tool"], "rag_ask_question");
|
||||
}
|
||||
@ -116,7 +119,10 @@ async fn test_explicit_tool_call_guidance_status() {
|
||||
args: json!({}),
|
||||
};
|
||||
|
||||
let response = service.call_mcp_tool(req).await.expect("guidance_check_system_status failed");
|
||||
let response = service
|
||||
.call_mcp_tool(req)
|
||||
.await
|
||||
.expect("guidance_check_system_status failed");
|
||||
assert_eq!(response.result["status"], "healthy");
|
||||
assert_eq!(response.result["tool"], "guidance_check_system_status");
|
||||
}
|
||||
@ -131,7 +137,10 @@ async fn test_explicit_tool_call_settings() {
|
||||
args: json!({}),
|
||||
};
|
||||
|
||||
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed");
|
||||
let response = service
|
||||
.call_mcp_tool(req)
|
||||
.await
|
||||
.expect("MCP tool call failed");
|
||||
assert_eq!(response.result["status"], "success");
|
||||
// Verify real SettingsTools data is returned (not empty placeholder)
|
||||
assert!(
|
||||
@ -152,7 +161,10 @@ async fn test_settings_tools_platform_recommendations() {
|
||||
args: json!({}),
|
||||
};
|
||||
|
||||
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed");
|
||||
let response = service
|
||||
.call_mcp_tool(req)
|
||||
.await
|
||||
.expect("MCP tool call failed");
|
||||
assert_eq!(response.result["status"], "success");
|
||||
// Should have real recommendations array from SettingsTools platform detection
|
||||
assert!(response.result.get("recommendations").is_some());
|
||||
@ -168,7 +180,10 @@ async fn test_settings_tools_mode_defaults() {
|
||||
args: json!({"mode": "solo"}),
|
||||
};
|
||||
|
||||
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed");
|
||||
let response = service
|
||||
.call_mcp_tool(req)
|
||||
.await
|
||||
.expect("MCP tool call failed");
|
||||
assert_eq!(response.result["status"], "success");
|
||||
// Verify real mode defaults (resource requirements)
|
||||
assert!(response.result.get("min_cpu_cores").is_some());
|
||||
@ -185,7 +200,10 @@ async fn test_explicit_tool_call_iac() {
|
||||
args: json!({"path": "/tmp/infra"}),
|
||||
};
|
||||
|
||||
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed");
|
||||
let response = service
|
||||
.call_mcp_tool(req)
|
||||
.await
|
||||
.expect("MCP tool call failed");
|
||||
assert_eq!(response.result["status"], "success");
|
||||
// Verify real technology detection (returns technologies array)
|
||||
assert!(response.result.get("technologies").is_some());
|
||||
@ -202,7 +220,10 @@ async fn test_iac_detect_technologies_real() {
|
||||
args: json!({"path": "../../provisioning"}),
|
||||
};
|
||||
|
||||
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed");
|
||||
let response = service
|
||||
.call_mcp_tool(req)
|
||||
.await
|
||||
.expect("MCP tool call failed");
|
||||
assert_eq!(response.result["status"], "success");
|
||||
|
||||
// Should detect technologies as an array
|
||||
@ -221,7 +242,10 @@ async fn test_iac_analyze_completeness() {
|
||||
args: json!({"path": "/tmp/test-infra"}),
|
||||
};
|
||||
|
||||
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed");
|
||||
let response = service
|
||||
.call_mcp_tool(req)
|
||||
.await
|
||||
.expect("MCP tool call failed");
|
||||
assert_eq!(response.result["status"], "success");
|
||||
// Verify real analysis data
|
||||
assert!(response.result.get("complete").is_some());
|
||||
@ -365,7 +389,10 @@ async fn test_tool_execution_with_required_args() {
|
||||
args: json!({"query": "kubernetes"}),
|
||||
};
|
||||
|
||||
let response = service.call_mcp_tool(req).await.expect("MCP tool call failed");
|
||||
let response = service
|
||||
.call_mcp_tool(req)
|
||||
.await
|
||||
.expect("MCP tool call failed");
|
||||
assert_eq!(response.result["status"], "success");
|
||||
}
|
||||
|
||||
|
||||
@ -35,7 +35,7 @@ pub struct WorkflowTask {
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Server creation workflow request
|
||||
/// Server creation workflow request - Complete auditable unit
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateServerWorkflow {
|
||||
pub infra: String,
|
||||
@ -43,6 +43,23 @@ pub struct CreateServerWorkflow {
|
||||
pub servers: Vec<String>,
|
||||
pub check_mode: bool,
|
||||
pub wait: bool,
|
||||
// Template and execution context
|
||||
#[serde(default)]
|
||||
pub template_path: Option<String>, // Path to template used: /provisioning/extensions/providers/.../hetzner_servers.j2
|
||||
#[serde(default)]
|
||||
pub template_vars_compressed: Option<String>, // Gzip+Base64 encoded template variables
|
||||
// Generated script (compressed for transmission)
|
||||
#[serde(default)]
|
||||
pub script_compressed: Option<String>, // Gzip+Base64 encoded script
|
||||
#[serde(default)]
|
||||
pub script_encoding: Option<String>, // Encoding type: "gzip+base64"
|
||||
// Compression metrics
|
||||
#[serde(default)]
|
||||
pub compression_ratio: Option<f32>, // Overall compression ratio
|
||||
#[serde(default)]
|
||||
pub original_size: Option<u64>, // Original script size
|
||||
#[serde(default)]
|
||||
pub compressed_size: Option<u64>, // Compressed size
|
||||
}
|
||||
|
||||
/// Task service workflow request
|
||||
|
||||
@ -32,10 +32,16 @@ uuid = { workspace = true }
|
||||
sqlx = { workspace = true }
|
||||
surrealdb = { workspace = true }
|
||||
|
||||
# Platform shared crates (optional NATS bridge)
|
||||
platform-nats = { workspace = true, optional = true }
|
||||
|
||||
# Configuration and CLI
|
||||
clap = { workspace = true }
|
||||
config = { workspace = true }
|
||||
|
||||
# Centralized observability (logging, metrics, health, tracing)
|
||||
observability = { workspace = true, features = ["logging", "metrics-prometheus", "health"] }
|
||||
|
||||
# Error handling
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
@ -153,8 +159,11 @@ compliance = ["core"]
|
||||
# Modules: anomaly (detection)
|
||||
experimental = ["core"]
|
||||
|
||||
# NATS event bus integration
|
||||
nats = ["dep:platform-nats"]
|
||||
|
||||
# Default: All features enabled
|
||||
default = ["core", "kms", "audit", "mfa", "compliance", "experimental"]
|
||||
default = ["core", "kms", "audit", "mfa", "compliance", "experimental", "nats"]
|
||||
|
||||
# Full: All features enabled (development and testing)
|
||||
all = ["core", "kms", "audit", "mfa", "compliance", "experimental"]
|
||||
|
||||
@ -56,19 +56,20 @@ pub trait AppStateBuilder: Send + Sync {
|
||||
/// Default AppState builder - uses standard initialization
|
||||
pub struct DefaultAppStateBuilder {
|
||||
config: Config,
|
||||
solo_mode: bool,
|
||||
}
|
||||
|
||||
impl DefaultAppStateBuilder {
|
||||
/// Create a new default builder with configuration
|
||||
pub fn new(config: Config) -> Self {
|
||||
Self { config }
|
||||
pub fn new(config: Config, solo_mode: bool) -> Self {
|
||||
Self { config, solo_mode }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AppStateBuilder for DefaultAppStateBuilder {
|
||||
async fn build(&self) -> Result<AppState> {
|
||||
AppState::new(self.config.clone()).await
|
||||
AppState::new(self.config.clone(), self.solo_mode).await
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
@ -76,18 +77,11 @@ impl AppStateBuilder for DefaultAppStateBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Factory function for creating AppState with standard configuration
|
||||
/// Factory function for creating AppState with standard configuration.
|
||||
///
|
||||
/// This is the primary API for new code. Replaces direct `AppState::new()`
|
||||
/// calls.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// let app_state = create_app_state(config).await?;
|
||||
/// ```
|
||||
pub async fn create_app_state(config: Config) -> Result<AppState> {
|
||||
let builder = DefaultAppStateBuilder::new(config);
|
||||
/// `solo_mode = true` disables JWT auth and injects a local-admin context.
|
||||
pub async fn create_app_state(config: Config, solo_mode: bool) -> Result<AppState> {
|
||||
let builder = DefaultAppStateBuilder::new(config, solo_mode);
|
||||
builder.build().await
|
||||
}
|
||||
|
||||
|
||||
@ -473,8 +473,7 @@ mod tests {
|
||||
// Generate fresh RSA keys for testing
|
||||
use crate::services::jwt::generate_rsa_key_pair;
|
||||
|
||||
let keys = generate_rsa_key_pair()
|
||||
.expect("Failed to generate test RSA keys");
|
||||
let keys = generate_rsa_key_pair().expect("Failed to generate test RSA keys");
|
||||
|
||||
(
|
||||
keys.private_key_pem.into_bytes(),
|
||||
|
||||
@ -157,9 +157,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_password_strength_fair() {
|
||||
let service = PasswordService::new();
|
||||
// Fair: 8-9 chars with 0-2 complexity types (lowercase, uppercase, digit, special)
|
||||
// Fair: 8-9 chars with 0-2 complexity types (lowercase, uppercase, digit,
|
||||
// special)
|
||||
assert_eq!(
|
||||
service.evaluate_strength("password1"), // 9 chars, 2 types: lowercase + digit
|
||||
service.evaluate_strength("password1"), // 9 chars, 2 types: lowercase + digit
|
||||
PasswordStrength::Fair
|
||||
);
|
||||
}
|
||||
|
||||
@ -215,10 +215,10 @@ impl Default for ControlCenterConfig {
|
||||
impl ControlCenterConfig {
|
||||
/// Load configuration with hierarchical fallback logic:
|
||||
/// 1. Environment variable CONTROL_CENTER_CONFIG (explicit config path)
|
||||
/// 2. Mode-specific config:
|
||||
/// provisioning/platform/config/control-center.{mode}.toml
|
||||
/// 3. System defaults: config.defaults.toml
|
||||
/// 2. CLI config resolution via platform_config::ConfigResolver
|
||||
/// 3. Built-in defaults
|
||||
///
|
||||
/// Supports both .ncl (Nickel) and .toml formats.
|
||||
/// Then environment variables (CONTROL_CENTER_*) override specific fields.
|
||||
pub fn load() -> Result<Self> {
|
||||
let mut config = Self::load_from_hierarchy()?;
|
||||
@ -233,22 +233,20 @@ impl ControlCenterConfig {
|
||||
return Self::from_file(&config_path);
|
||||
}
|
||||
|
||||
// Priority 2: Mode-specific config (provisioning/platform/config/)
|
||||
if let Ok(mode) = std::env::var("CONTROL_CENTER_MODE") {
|
||||
let mode_config_path =
|
||||
format!("provisioning/platform/config/control-center.{}.toml", mode);
|
||||
if Path::new(&mode_config_path).exists() {
|
||||
return Self::from_file(&mode_config_path);
|
||||
}
|
||||
// Priority 2: Use platform_config resolver (supports NCL and TOML)
|
||||
let resolver = platform_config::ConfigResolver::new()
|
||||
.with_cli_config_dir(
|
||||
std::env::var("PROVISIONING_CONFIG_DIR")
|
||||
.ok()
|
||||
.map(PathBuf::from),
|
||||
)
|
||||
.with_cli_mode(std::env::var("CONTROL_CENTER_MODE").ok());
|
||||
|
||||
if let Some(path) = resolver.resolve("control-center") {
|
||||
return Self::from_file(&path);
|
||||
}
|
||||
|
||||
// Priority 3: System defaults
|
||||
let defaults_path = Path::new("config/control-center.defaults.toml");
|
||||
if defaults_path.exists() {
|
||||
return Self::from_file(defaults_path);
|
||||
}
|
||||
|
||||
// Priority 4: Built-in defaults if file doesn't exist
|
||||
// Priority 3: Built-in defaults if file doesn't exist
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
@ -314,20 +312,22 @@ impl ControlCenterConfig {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load configuration from file with environment variable interpolation
|
||||
/// Load configuration from file (.ncl or .toml) with environment variable
|
||||
/// interpolation
|
||||
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
let content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
|
||||
let path_ref = path.as_ref();
|
||||
|
||||
// Use platform_config to load NCL or TOML
|
||||
let json_value = platform_config::format::load_config(path_ref).map_err(|e| {
|
||||
ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
|
||||
format!("Failed to read config file {:?}: {}", path.as_ref(), e),
|
||||
format!("Failed to load config from {:?}: {}", path_ref, e),
|
||||
))
|
||||
})?;
|
||||
|
||||
// Interpolate environment variables
|
||||
let interpolated = Self::interpolate_env_vars(&content)?;
|
||||
|
||||
let config: Self = toml::from_str(&interpolated).map_err(|e| {
|
||||
// Deserialize from JSON value
|
||||
let config: Self = serde_json::from_value(json_value).map_err(|e| {
|
||||
ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
|
||||
format!("Failed to parse config: {}", e),
|
||||
format!("Failed to deserialize config: {}", e),
|
||||
))
|
||||
})?;
|
||||
|
||||
|
||||
@ -3,6 +3,8 @@ pub mod deployment_events;
|
||||
pub mod iac_deployment;
|
||||
pub mod iac_detection;
|
||||
pub mod iac_rules;
|
||||
#[cfg(feature = "nats")]
|
||||
pub mod nats_bridge;
|
||||
pub mod permission;
|
||||
pub mod role;
|
||||
pub mod secrets;
|
||||
|
||||
77
crates/control-center/src/handlers/nats_bridge.rs
Normal file
77
crates/control-center/src/handlers/nats_bridge.rs
Normal file
@ -0,0 +1,77 @@
|
||||
//! NATS→WebSocket bridge for real-time task status updates.
|
||||
//!
|
||||
//! Subscribes to `provisioning.tasks.*.status` via JetStream durable consumer
|
||||
//! and re-broadcasts each event to all connected WebSocket clients, eliminating
|
||||
//! polling between control-center and orchestrator.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::StreamExt;
|
||||
use platform_nats::NatsBridge;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{error, info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::handlers::websocket::{WebSocketEvent, WebSocketManager};
|
||||
|
||||
const STREAM_NAME: &str = "TASKS";
|
||||
const CONSUMER_NAME: &str = "cc-task-status-bridge";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct TaskStatusPayload {
|
||||
pub task_id: String,
|
||||
pub status: String,
|
||||
pub progress: Option<u32>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
/// Spawn the NATS→WebSocket bridge as a background tokio task.
|
||||
pub fn spawn_nats_bridge(nats: Arc<NatsBridge>, ws_manager: Arc<WebSocketManager>) {
|
||||
tokio::spawn(async move {
|
||||
run_bridge(nats, ws_manager).await;
|
||||
});
|
||||
}
|
||||
|
||||
async fn run_bridge(nats: Arc<NatsBridge>, ws_manager: Arc<WebSocketManager>) {
|
||||
let mut messages = match nats.subscribe_pull(STREAM_NAME, CONSUMER_NAME).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("NATS bridge: subscribe failed — {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!("NATS→WebSocket bridge running on stream {STREAM_NAME}");
|
||||
|
||||
while let Some(msg_result) = messages.next().await {
|
||||
match msg_result {
|
||||
Ok(msg) => {
|
||||
match serde_json::from_slice::<TaskStatusPayload>(&msg.payload) {
|
||||
Ok(payload) => {
|
||||
let event = WebSocketEvent {
|
||||
event_type: "task_status_update".to_string(),
|
||||
data: serde_json::json!({
|
||||
"task_id": payload.task_id,
|
||||
"status": payload.status,
|
||||
"progress": payload.progress,
|
||||
"message": payload.message,
|
||||
}),
|
||||
timestamp: chrono::Utc::now(),
|
||||
target_user: None,
|
||||
};
|
||||
ws_manager.broadcast_event(event).await;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("NATS bridge: deserialize failed — {e}");
|
||||
}
|
||||
}
|
||||
if let Err(e) = msg.ack().await {
|
||||
warn!("NATS bridge: ack failed — {e}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("NATS bridge: message error — {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -117,11 +117,17 @@ pub struct AppState {
|
||||
pub monitoring_service: Arc<MonitoringService>,
|
||||
pub orchestrator_client: Arc<OrchestratorClient>,
|
||||
pub config: Config,
|
||||
/// When true, auth middleware is replaced by a no-op that injects LocalUser
|
||||
/// context.
|
||||
pub solo_mode: bool,
|
||||
/// NATS bridge for task status subscription (optional feature)
|
||||
#[cfg(feature = "nats")]
|
||||
pub nats: Option<Arc<platform_nats::NatsBridge>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
/// Create a new application state instance
|
||||
pub async fn new(config: Config) -> Result<Self> {
|
||||
pub async fn new(config: Config, solo_mode: bool) -> Result<Self> {
|
||||
// Initialize database service
|
||||
let database_service = Arc::new(DatabaseService::new(config.database.clone()).await?);
|
||||
|
||||
@ -248,6 +254,22 @@ impl AppState {
|
||||
monitoring_service,
|
||||
orchestrator_client,
|
||||
config,
|
||||
solo_mode,
|
||||
#[cfg(feature = "nats")]
|
||||
nats: {
|
||||
use platform_nats::{NatsBridge, NatsConfig};
|
||||
match NatsBridge::connect(&NatsConfig::default()).await {
|
||||
Ok(bridge) => {
|
||||
let bridge = std::sync::Arc::new(bridge);
|
||||
tracing::info!("Connected to NATS");
|
||||
Some(bridge)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("NATS connection failed (bridge disabled): {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ use control_center::handlers::{
|
||||
websocket::websocket_handler,
|
||||
};
|
||||
use control_center::middleware::{
|
||||
auth::auth_middleware,
|
||||
auth::{auth_middleware, solo_auth_middleware},
|
||||
cors::create_cors_from_env,
|
||||
rate_limit::{RateLimitConfig, RateLimitLayer},
|
||||
};
|
||||
@ -40,25 +40,35 @@ use hyper::http::StatusCode;
|
||||
use tokio::signal;
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::{compression::CompressionLayer, timeout::TimeoutLayer, trace::TraceLayer};
|
||||
use tracing::{error, info};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "control-center")]
|
||||
#[command(about = "Control Center - JWT Authentication & User Management Service")]
|
||||
#[command(version = env!("CARGO_PKG_VERSION"))]
|
||||
#[command(after_help = "CONFIGURATION HIERARCHY (highest to lowest priority):\n 1. CLI: -c/--config <path> (explicit file)\n 2. CLI: --config-dir <dir> --mode <mode> (directory + mode)\n 3. CLI: --config-dir <dir> (searches for control-center.ncl|toml|json)\n 4. CLI: --mode <mode> (searches in provisioning/platform/config/)\n 5. ENV: CONTROL_CENTER_CONFIG (explicit file)\n 6. ENV: PROVISIONING_CONFIG_DIR (searches for control-center.ncl|toml|json)\n 7. ENV: CONTROL_CENTER_MODE (mode-based in default path)\n 8. Built-in defaults")]
|
||||
#[command(
|
||||
after_help = "CONFIGURATION HIERARCHY (highest to lowest priority):\n 1. CLI: -c/--config \
|
||||
<path> (explicit file)\n 2. CLI: --config-dir <dir> --mode <mode> (directory + \
|
||||
mode)\n 3. CLI: --config-dir <dir> (searches for \
|
||||
control-center.ncl|toml|json)\n 4. CLI: --mode <mode> (searches in \
|
||||
provisioning/platform/config/)\n 5. ENV: CONTROL_CENTER_CONFIG (explicit \
|
||||
file)\n 6. ENV: PROVISIONING_CONFIG_DIR (searches for \
|
||||
control-center.ncl|toml|json)\n 7. ENV: CONTROL_CENTER_MODE (mode-based in \
|
||||
default path)\n 8. Built-in defaults"
|
||||
)]
|
||||
struct Cli {
|
||||
/// Configuration file path (highest priority)
|
||||
///
|
||||
/// Accepts absolute or relative path. Supports .ncl, .toml, and .json formats.
|
||||
/// Accepts absolute or relative path. Supports .ncl, .toml, and .json
|
||||
/// formats.
|
||||
#[arg(short = 'c', long, env = "CONTROL_CENTER_CONFIG")]
|
||||
config: Option<PathBuf>,
|
||||
|
||||
/// Configuration directory (searches for control-center.ncl|toml|json)
|
||||
///
|
||||
/// Searches for configuration files in order of preference: .ncl > .toml > .json
|
||||
/// Can also search for mode-specific files: control-center.{mode}.{ncl|toml|json}
|
||||
/// Searches for configuration files in order of preference: .ncl > .toml >
|
||||
/// .json Can also search for mode-specific files:
|
||||
/// control-center.{mode}.{ncl|toml|json}
|
||||
#[arg(long, env = "PROVISIONING_CONFIG_DIR")]
|
||||
config_dir: Option<PathBuf>,
|
||||
|
||||
@ -98,14 +108,26 @@ async fn main() -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Initialize logging
|
||||
let log_level = if cli.debug { "debug" } else { "info" };
|
||||
let filter = EnvFilter::new(format!("control_center={},tower_http=info", log_level));
|
||||
// Initialize centralized observability (logging, metrics, health checks)
|
||||
let _guard = observability::init_from_env("control-center", env!("CARGO_PKG_VERSION"))
|
||||
.map_err(|e| control_center::ControlCenterError::from(anyhow::anyhow!(e)))?;
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_target(false)
|
||||
.init();
|
||||
// Check if control-center is enabled in deployment-mode.ncl
|
||||
if let Ok(deployment) = platform_config::load_deployment_mode() {
|
||||
if let Ok(enabled) = deployment.is_service_enabled("control-center") {
|
||||
if !enabled {
|
||||
warn!("⚠ Control Center is DISABLED in deployment-mode.ncl");
|
||||
std::process::exit(1);
|
||||
}
|
||||
info!("✓ Control Center is ENABLED in deployment-mode.ncl");
|
||||
}
|
||||
}
|
||||
|
||||
// Try to load control-center.ncl
|
||||
if let Ok(config) = platform_config::load_service_config_from_ncl("control-center") {
|
||||
info!("✓ Loaded control-center configuration from NCL");
|
||||
tracing::debug!("Config: {:?}", config);
|
||||
}
|
||||
|
||||
// Resolve config file path using new resolver
|
||||
let resolver = platform_config::ConfigResolver::new()
|
||||
@ -135,7 +157,11 @@ async fn main() -> Result<()> {
|
||||
);
|
||||
|
||||
// Initialize application state
|
||||
let app_state = Arc::new(AppState::new(config.clone()).await?);
|
||||
let solo_mode = cli.mode.as_deref() == Some("solo");
|
||||
if solo_mode {
|
||||
warn!("⚠ Solo mode: JWT authentication is DISABLED — local operator access only");
|
||||
}
|
||||
let app_state = Arc::new(AppState::new(config.clone(), solo_mode).await?);
|
||||
|
||||
// Health check
|
||||
if let Err(e) = app_state.health_check().await {
|
||||
@ -256,12 +282,18 @@ async fn create_router(app_state: Arc<AppState>) -> Result<Router> {
|
||||
.route("/secrets/monitoring/alerts", get(get_alert_summary))
|
||||
.route("/secrets/monitoring/expiring", get(get_expiring_secrets))
|
||||
// WebSocket route
|
||||
.route("/ws", get(websocket_handler))
|
||||
// Apply authentication middleware to all protected routes
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
.route("/ws", get(websocket_handler));
|
||||
|
||||
// In solo mode skip JWT validation and inject a fixed local-admin context
|
||||
// instead.
|
||||
let protected_routes = if app_state.solo_mode {
|
||||
protected_routes.route_layer(middleware::from_fn(solo_auth_middleware))
|
||||
} else {
|
||||
protected_routes.route_layer(middleware::from_fn_with_state(
|
||||
app_state.jwt_service.clone(),
|
||||
auth_middleware,
|
||||
));
|
||||
))
|
||||
};
|
||||
|
||||
// Combine all routes
|
||||
let app = Router::new()
|
||||
@ -307,6 +339,15 @@ async fn start_background_tasks(app_state: Arc<AppState>) {
|
||||
}
|
||||
});
|
||||
|
||||
// NATS → WebSocket bridge: forward task status events to all connected WS
|
||||
// clients
|
||||
#[cfg(feature = "nats")]
|
||||
if let Some(nats) = &app_state.nats {
|
||||
use control_center::handlers::nats_bridge::spawn_nats_bridge;
|
||||
spawn_nats_bridge(Arc::clone(nats), Arc::clone(&app_state.websocket_manager));
|
||||
info!("NATS→WebSocket bridge started");
|
||||
}
|
||||
|
||||
info!("Background tasks started");
|
||||
}
|
||||
|
||||
|
||||
@ -75,6 +75,28 @@ pub async fn auth_middleware(
|
||||
}
|
||||
}
|
||||
|
||||
/// Solo mode authentication bypass middleware.
|
||||
///
|
||||
/// Injects a fixed local-admin `UserContext` for every request without
|
||||
/// validating any JWT. Applied only when the service starts with `--mode solo`.
|
||||
/// All handlers receive the same context — roles = ["admin"], mfa_verified =
|
||||
/// true.
|
||||
pub async fn solo_auth_middleware(
|
||||
mut request: Request,
|
||||
next: Next,
|
||||
) -> std::result::Result<Response, StatusCode> {
|
||||
let user_context = UserContext {
|
||||
user_id: Uuid::nil(),
|
||||
session_id: Uuid::nil(),
|
||||
roles: vec!["admin".to_string()],
|
||||
mfa_verified: true,
|
||||
ip_address: Some("127.0.0.1".to_string()),
|
||||
approval_id: None,
|
||||
};
|
||||
request.extensions_mut().insert(user_context);
|
||||
Ok(next.run(request).await)
|
||||
}
|
||||
|
||||
/// Optional authentication middleware that allows unauthenticated requests
|
||||
pub async fn optional_auth_middleware(
|
||||
State(jwt_service): State<Arc<JwtService>>,
|
||||
@ -373,7 +395,7 @@ mod tests {
|
||||
async fn test_auth_header_parsing() {
|
||||
let jwt_service = Arc::new(
|
||||
JwtService::new(create_test_jwt_config())
|
||||
.expect("Failed to create JWT service for test")
|
||||
.expect("Failed to create JWT service for test"),
|
||||
);
|
||||
|
||||
let user_id = Uuid::new_v4();
|
||||
|
||||
@ -187,9 +187,9 @@ impl RefreshTokenClaims {
|
||||
|
||||
/// Generate RSA key pair for JWT signing (RS256)
|
||||
pub fn generate_rsa_key_pair() -> Result<RsaKeys> {
|
||||
use rsa::pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding};
|
||||
use rsa::rand_core::OsRng;
|
||||
use rsa::{RsaPrivateKey, RsaPublicKey};
|
||||
use rsa::pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding};
|
||||
|
||||
// Generate 2048-bit RSA key pair with OS randomness for cryptographic security
|
||||
let private_key =
|
||||
@ -253,8 +253,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_jwt_token_generation_and_verification() {
|
||||
let config = create_test_jwt_config();
|
||||
let jwt_service = JwtService::new(config)
|
||||
.expect("Failed to create JWT service for test");
|
||||
let jwt_service = JwtService::new(config).expect("Failed to create JWT service for test");
|
||||
|
||||
let user_id = Uuid::new_v4();
|
||||
let session_id = Uuid::new_v4();
|
||||
|
||||
@ -378,7 +378,8 @@ fn test_invalid_signature_detection() {
|
||||
.expect("Failed to generate token pair");
|
||||
|
||||
// Service 2 with different public key tries to validate
|
||||
// This should fail because the token was signed with key1 but we're validating with key2
|
||||
// This should fail because the token was signed with key1 but we're validating
|
||||
// with key2
|
||||
let jwt_service2 = JwtService::new(
|
||||
&private_key1,
|
||||
&public_key2, // Different public key!
|
||||
|
||||
@ -28,6 +28,9 @@ toml = { workspace = true }
|
||||
# Platform configuration
|
||||
platform-config = { workspace = true }
|
||||
|
||||
# Centralized observability (logging, metrics, health, tracing)
|
||||
observability = { workspace = true, features = ["logging", "metrics-prometheus", "health"] }
|
||||
|
||||
# Error handling
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
@ -1,150 +1,96 @@
|
||||
//! Provisioning Daemon configuration wrapper
|
||||
//!
|
||||
//! This module wraps the external daemon library's configuration system
|
||||
//! with support for hierarchical loading and environment variable overrides.
|
||||
//! Loads configuration from provisioning-daemon.ncl using platform-config crate
|
||||
//! This module handles loading the service operational config (port, polling,
|
||||
//! workers) and converting it to the format needed by daemon-cli.
|
||||
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use platform_config::ConfigLoader;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Wrapper for external daemon configuration
|
||||
///
|
||||
/// Provides hierarchical configuration loading and environment variable
|
||||
/// overrides for the provisioning-daemon service.
|
||||
/// Service operational configuration loaded from provisioning-daemon.ncl
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProvisioningDaemonConfigWrapper {
|
||||
/// Configuration path used for loading
|
||||
#[serde(skip)]
|
||||
pub config_path: PathBuf,
|
||||
pub struct ProvisioningDaemonConfig {
|
||||
pub server: ServerConfig,
|
||||
#[serde(default)]
|
||||
pub daemon: DaemonConfig,
|
||||
#[serde(default)]
|
||||
pub logging: LoggingConfig,
|
||||
#[serde(default)]
|
||||
pub actions: ActionsConfig,
|
||||
}
|
||||
|
||||
impl ProvisioningDaemonConfigWrapper {
|
||||
/// Load configuration from hierarchical sources with mode support
|
||||
///
|
||||
/// Priority order:
|
||||
/// 1. DAEMON_CONFIG environment variable (explicit path)
|
||||
/// 2. DAEMON_MODE environment variable (mode-specific file)
|
||||
/// 3. Default configuration path
|
||||
///
|
||||
/// This method always succeeds - it resolves a path regardless of whether
|
||||
/// the file exists. The external daemon library handles file validation.
|
||||
pub fn load_from_hierarchy() -> Self {
|
||||
let config_path = Self::resolve_config_path();
|
||||
Self { config_path }
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServerConfig {
|
||||
#[serde(default = "default_host")]
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DaemonConfig {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
#[serde(default = "default_poll_interval")]
|
||||
pub poll_interval: u64,
|
||||
#[serde(default = "default_max_workers")]
|
||||
pub max_workers: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LoggingConfig {
|
||||
#[serde(default = "default_log_level")]
|
||||
pub level: String,
|
||||
pub file: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct ActionsConfig {
|
||||
#[serde(default)]
|
||||
pub auto_cleanup: bool,
|
||||
#[serde(default)]
|
||||
pub auto_update: bool,
|
||||
}
|
||||
|
||||
fn default_host() -> String {
|
||||
"0.0.0.0".to_string()
|
||||
}
|
||||
|
||||
fn default_poll_interval() -> u64 {
|
||||
60
|
||||
}
|
||||
|
||||
fn default_max_workers() -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn default_log_level() -> String {
|
||||
"info".to_string()
|
||||
}
|
||||
|
||||
impl ProvisioningDaemonConfig {
|
||||
/// Load configuration from provisioning-daemon.ncl via platform-config
|
||||
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let config_json = platform_config::load_service_config_from_ncl("provisioning-daemon")?;
|
||||
|
||||
// The Nickel file returns { provisioning_daemon: { ... } }
|
||||
// Extract the inner config object
|
||||
let config_value = if let Some(inner) = config_json.get("provisioning_daemon") {
|
||||
inner.clone()
|
||||
} else {
|
||||
config_json
|
||||
};
|
||||
|
||||
let config: ProvisioningDaemonConfig = serde_json::from_value(config_value)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Resolve the configuration path from environment variables or defaults
|
||||
fn resolve_config_path() -> PathBuf {
|
||||
if let Ok(path) = env::var("DAEMON_CONFIG") {
|
||||
// Explicit config path provided
|
||||
return PathBuf::from(path);
|
||||
}
|
||||
|
||||
if let Ok(mode) = env::var("DAEMON_MODE") {
|
||||
// Mode-specific config file
|
||||
return PathBuf::from(format!(
|
||||
"provisioning/platform/config/provisioning-daemon.{}.toml",
|
||||
mode
|
||||
));
|
||||
}
|
||||
|
||||
// Default fallback - use a basic default config
|
||||
// In production, this would point to a system config location
|
||||
PathBuf::from("provisioning/platform/config/provisioning-daemon.solo.toml")
|
||||
}
|
||||
|
||||
/// Get the resolved configuration path
|
||||
pub fn path(&self) -> &PathBuf {
|
||||
&self.config_path
|
||||
}
|
||||
|
||||
/// Load configuration using the external daemon library's loader
|
||||
///
|
||||
/// This delegates to the external library's configuration loading
|
||||
/// mechanism. The external library handles the actual parsing and
|
||||
/// validation.
|
||||
/// Get the bind address for the HTTP server
|
||||
#[allow(dead_code)]
|
||||
pub fn load_with_external(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// This would integrate with the external daemon_cli library
|
||||
// Example: daemon_cli::DaemonConfig::load(&self.config_path)?;
|
||||
// For now, this is a placeholder that demonstrates the pattern
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply environment variable overrides
|
||||
///
|
||||
/// Environment variables follow the pattern: DAEMON_{SECTION}_{KEY}
|
||||
///
|
||||
/// Supported overrides:
|
||||
/// - DAEMON_POLL_INTERVAL (seconds)
|
||||
/// - DAEMON_MAX_WORKERS (count)
|
||||
/// - DAEMON_LOGGING_LEVEL (debug/info/warn/error)
|
||||
/// - DAEMON_AUTO_CLEANUP (true/false)
|
||||
#[allow(dead_code)]
|
||||
pub fn apply_env_overrides(&mut self) {
|
||||
// Environment variable overrides are handled by the external library
|
||||
// or by pre-setting them before calling the external loader.
|
||||
//
|
||||
// The pattern is:
|
||||
// 1. Load the config file (via external library)
|
||||
// 2. Parse environment variable overrides
|
||||
// 3. Apply overrides on top of loaded config
|
||||
//
|
||||
// Since the external library may not support this pattern directly,
|
||||
// this wrapper allows the calling code to:
|
||||
// - Check env vars before calling the loader
|
||||
// - Inject config path via environment
|
||||
// - Handle overrides at the application level
|
||||
}
|
||||
|
||||
/// Get the configuration path as a string
|
||||
pub fn path_str(&self) -> String {
|
||||
self.config_path.to_string_lossy().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProvisioningDaemonConfigWrapper {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config_path: PathBuf::from(
|
||||
"provisioning/platform/config/provisioning-daemon.solo.toml",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigLoader for ProvisioningDaemonConfigWrapper {
|
||||
fn service_name() -> &'static str {
|
||||
"provisioning-daemon"
|
||||
}
|
||||
|
||||
fn load_from_hierarchy() -> std::result::Result<Self, Box<dyn std::error::Error + Send + Sync>>
|
||||
{
|
||||
let service = Self::service_name();
|
||||
|
||||
if let Some(path) = platform_config::resolve_config_path(service) {
|
||||
return Self::from_path(&path);
|
||||
}
|
||||
|
||||
// Fallback to defaults
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
fn apply_env_overrides(
|
||||
&mut self,
|
||||
) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// No-op for wrapper - env overrides handled by external daemon library
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn from_path<P: AsRef<Path>>(
|
||||
path: P,
|
||||
) -> std::result::Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Ok(Self {
|
||||
config_path: path.as_ref().to_path_buf(),
|
||||
})
|
||||
pub fn bind_addr(&self) -> SocketAddr {
|
||||
format!("{}:{}", self.server.host, self.server.port)
|
||||
.parse()
|
||||
.unwrap_or_else(|_| ([127, 0, 0, 1], 9090).into())
|
||||
}
|
||||
}
|
||||
|
||||
@ -153,22 +99,63 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_config_path() {
|
||||
let config = ProvisioningDaemonConfigWrapper::default();
|
||||
assert!(config.path_str().contains("provisioning-daemon.solo.toml"));
|
||||
fn test_config_defaults() {
|
||||
let config = ProvisioningDaemonConfig {
|
||||
server: ServerConfig {
|
||||
host: "0.0.0.0".to_string(),
|
||||
port: 9095,
|
||||
},
|
||||
daemon: DaemonConfig {
|
||||
enabled: true,
|
||||
poll_interval: 60,
|
||||
max_workers: 2,
|
||||
},
|
||||
logging: LoggingConfig {
|
||||
level: "info".to_string(),
|
||||
file: None,
|
||||
},
|
||||
actions: ActionsConfig {
|
||||
auto_cleanup: false,
|
||||
auto_update: false,
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(config.server.port, 9095);
|
||||
assert!(config.daemon.enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_path_method() {
|
||||
let config = ProvisioningDaemonConfigWrapper::default();
|
||||
let path = config.path();
|
||||
assert!(path.to_string_lossy().contains("provisioning-daemon"));
|
||||
}
|
||||
fn test_bind_addr() {
|
||||
let config = ProvisioningDaemonConfig {
|
||||
server: ServerConfig {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 9095,
|
||||
},
|
||||
daemon: Default::default(),
|
||||
logging: Default::default(),
|
||||
actions: Default::default(),
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_resolve_config_path_default() {
|
||||
// Without env vars, should return default
|
||||
let wrapper = ProvisioningDaemonConfigWrapper::load_from_hierarchy();
|
||||
assert!(wrapper.path_str().contains("provisioning-daemon"));
|
||||
let addr = config.bind_addr();
|
||||
assert_eq!(addr.port(), 9095);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DaemonConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
poll_interval: default_poll_interval(),
|
||||
max_workers: default_max_workers(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LoggingConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
level: default_log_level(),
|
||||
file: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,6 @@ use std::sync::Arc;
|
||||
|
||||
use axum::Router;
|
||||
use clap::Parser;
|
||||
use config::ProvisioningDaemonConfigWrapper;
|
||||
use daemon_cli::{
|
||||
api::api_routes,
|
||||
core::{DaemonConfig, HierarchicalCache, Result},
|
||||
@ -22,7 +21,6 @@ use daemon_cli::{
|
||||
AppState,
|
||||
};
|
||||
use tokio::net::TcpListener;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
/// Provisioning daemon - Nushell execution and configuration rendering service
|
||||
#[derive(Parser, Debug)]
|
||||
@ -59,38 +57,87 @@ struct Args {
|
||||
async fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
// Initialize logging
|
||||
let log_level = if args.verbose { "debug" } else { "info" };
|
||||
let filter = EnvFilter::new(format!(
|
||||
"provisioning_daemon={},tower_http=info,daemon_cli=info",
|
||||
log_level
|
||||
));
|
||||
// Initialize centralized observability (logging, metrics, health checks)
|
||||
let _guard = observability::init_from_env("daemon", env!("CARGO_PKG_VERSION"))
|
||||
.map_err(|e| daemon_cli::core::DaemonError::http_server_error(e.to_string()))?;
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_target(false)
|
||||
.init();
|
||||
// Check if daemon is enabled in deployment-mode.ncl
|
||||
if let Ok(deployment) = platform_config::load_deployment_mode() {
|
||||
if let Ok(enabled) = deployment.is_service_enabled("provisioning_daemon") {
|
||||
if !enabled {
|
||||
tracing::warn!("⚠ Provisioning Daemon is DISABLED in deployment-mode.ncl");
|
||||
std::process::exit(1);
|
||||
}
|
||||
tracing::info!("✓ Provisioning Daemon is ENABLED in deployment-mode.ncl");
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"Starting Provisioning Daemon v{}",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
|
||||
// Load configuration with hierarchical support
|
||||
let config_wrapper = if let Some(explicit_path) = args.config {
|
||||
// Use explicit path from CLI argument
|
||||
ProvisioningDaemonConfigWrapper {
|
||||
config_path: explicit_path,
|
||||
// Load provisioning-daemon configuration from Nickel via platform-config
|
||||
tracing::info!("Loading provisioning-daemon configuration from provisioning-daemon.ncl");
|
||||
let provisioning_config = match config::ProvisioningDaemonConfig::load() {
|
||||
Ok(cfg) => {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
|
||||
#[cfg(target_os = "macos")]
|
||||
let config_path = format!(
|
||||
"{}/Library/Application \
|
||||
Support/provisioning/platform/config/provisioning-daemon.ncl",
|
||||
home
|
||||
);
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let config_path = format!(
|
||||
"{}/.config/provisioning/platform/config/provisioning-daemon.ncl",
|
||||
home
|
||||
);
|
||||
|
||||
tracing::info!("✓ Loaded configuration from: {}", config_path);
|
||||
tracing::info!(" Server: {}:{}", cfg.server.host, cfg.server.port);
|
||||
tracing::info!(
|
||||
" Daemon: enabled={}, poll_interval={}s, max_workers={}",
|
||||
cfg.daemon.enabled,
|
||||
cfg.daemon.poll_interval,
|
||||
cfg.daemon.max_workers
|
||||
);
|
||||
cfg
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("✗ Failed to load provisioning-daemon.ncl: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else {
|
||||
// Use hierarchical loading (env vars or defaults)
|
||||
ProvisioningDaemonConfigWrapper::load_from_hierarchy()
|
||||
};
|
||||
|
||||
tracing::debug!("Loading configuration from: {}", config_wrapper.path_str());
|
||||
// Load daemon-cli configuration from TOML (if provided) or use defaults
|
||||
// The daemon-cli library needs its own infrastructure config separate from the
|
||||
// service config
|
||||
let mut config = if let Some(explicit_path) = args.config {
|
||||
// Use explicit path from CLI argument
|
||||
DaemonConfig::load(Some(explicit_path))?
|
||||
} else {
|
||||
// Load from config_dir if provided, otherwise use default
|
||||
if let Some(config_dir) = args.config_dir {
|
||||
let config_path = config_dir.join("provisioning-daemon-cli.toml");
|
||||
if config_path.exists() {
|
||||
DaemonConfig::load(Some(config_path))?
|
||||
} else {
|
||||
// Use defaults if no daemon-cli config found
|
||||
DaemonConfig::default()
|
||||
}
|
||||
} else {
|
||||
DaemonConfig::default()
|
||||
}
|
||||
};
|
||||
|
||||
// Load configuration using external daemon library
|
||||
let config = DaemonConfig::load(Some(config_wrapper.path().clone()))?;
|
||||
// Override the daemon-cli bind port with the port from provisioning-daemon.ncl
|
||||
// This ensures consistency: the service config (provisioning-daemon.ncl)
|
||||
// controls the port
|
||||
config.server.bind = format!(
|
||||
"{}:{}",
|
||||
provisioning_config.server.host, provisioning_config.server.port
|
||||
);
|
||||
|
||||
// Handle special commands
|
||||
if args.validate_config {
|
||||
@ -106,7 +153,12 @@ async fn main() -> Result<()> {
|
||||
|
||||
// Validate configuration
|
||||
config.validate()?;
|
||||
tracing::info!("Configuration validated: bind={}", config.server.bind);
|
||||
tracing::info!("✓ Daemon-cli configuration validated");
|
||||
tracing::info!(
|
||||
" Bind address: {} (from provisioning-daemon.ncl)",
|
||||
config.server.bind
|
||||
);
|
||||
tracing::info!(" Executor: {}", config.server.executor_strategy);
|
||||
|
||||
// Create cache system
|
||||
let cache = HierarchicalCache::new()?;
|
||||
@ -141,14 +193,14 @@ async fn main() -> Result<()> {
|
||||
// Create router
|
||||
let app = Router::new().nest("/api/v1", api_routes(state.clone()));
|
||||
|
||||
// Start server
|
||||
// Start server using the configured bind address
|
||||
let addr = config.bind_addr()?;
|
||||
let listener = TcpListener::bind(addr).await?;
|
||||
|
||||
tracing::info!("✓ Provisioning daemon listening on http://{}", addr);
|
||||
tracing::info!("API documentation: http://{}/api/v1/health", addr);
|
||||
tracing::info!("Config rendering: http://{}/api/v1/render", addr);
|
||||
tracing::info!("i18n translation: http://{}/api/v1/translate", addr);
|
||||
tracing::info!(" API documentation: http://{}/api/v1/health", addr);
|
||||
tracing::info!(" Config rendering: http://{}/api/v1/render", addr);
|
||||
tracing::info!(" i18n translation: http://{}/api/v1/translate", addr);
|
||||
|
||||
// Run server
|
||||
axum::serve(listener, app).await?;
|
||||
|
||||
@ -27,6 +27,9 @@ serde_json = { workspace = true }
|
||||
# Platform configuration
|
||||
platform-config = { workspace = true }
|
||||
|
||||
# Centralized observability (logging, metrics, health, tracing)
|
||||
observability = { workspace = true, features = ["logging", "metrics-prometheus", "health"] }
|
||||
|
||||
# Error handling
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
@ -61,9 +64,16 @@ lru = { workspace = true }
|
||||
# Parking lot for synchronization
|
||||
parking_lot = { workspace = true }
|
||||
|
||||
# Platform NATS bridge (optional)
|
||||
platform-nats = { workspace = true, optional = true }
|
||||
|
||||
# TOML parsing
|
||||
toml = { workspace = true }
|
||||
|
||||
[features]
|
||||
nats = ["dep:platform-nats"]
|
||||
default = []
|
||||
|
||||
[dev-dependencies]
|
||||
http-body-util = { workspace = true }
|
||||
hyper = { workspace = true }
|
||||
|
||||
@ -45,7 +45,7 @@ RUN cargo install cargo-chef --version 0.1.67
|
||||
COPY --from=planner /workspace/recipe.json recipe.json
|
||||
|
||||
# Build dependencies - This layer will be cached
|
||||
RUN cargo chef cook --release --recipe-path recipe.json
|
||||
RUN cargo chef cook --release --recipe-path recipe.json
|
||||
|
||||
# ============================================================================
|
||||
# Stage 3: BUILDER - Build source code
|
||||
@ -76,7 +76,7 @@ COPY stratumiops ./stratumiops
|
||||
|
||||
# Build release binary with parallelism
|
||||
ENV CARGO_BUILD_JOBS=4
|
||||
RUN cargo build --release --package extension-registry
|
||||
RUN cargo build --release --package extension-registry
|
||||
|
||||
# ============================================================================
|
||||
# Stage 4: RUNTIME - Minimal runtime image
|
||||
|
||||
@ -11,6 +11,8 @@ use tracing::{debug, error, info};
|
||||
use crate::cache::ExtensionCache;
|
||||
use crate::client::{ClientFactory, DistributionClient, SourceClient};
|
||||
use crate::error::{RegistryError, Result};
|
||||
#[cfg(feature = "nats")]
|
||||
use crate::events::{spawn_cache_invalidator, EventPublisher};
|
||||
use crate::models::*;
|
||||
|
||||
/// Application state
|
||||
@ -20,13 +22,19 @@ pub struct AppState {
|
||||
pub distribution_clients: Vec<Arc<dyn DistributionClient>>,
|
||||
pub cache: Arc<ExtensionCache>,
|
||||
pub start_time: Instant,
|
||||
#[cfg(feature = "nats")]
|
||||
pub events: Option<Arc<EventPublisher>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(config: crate::config::Config) -> Result<Self> {
|
||||
let (source_clients, distribution_clients) = ClientFactory::create_from_config(&config)?;
|
||||
/// Create application state.
|
||||
///
|
||||
/// `vault_url` enables vault:// token resolution (e.g. `Some("http://127.0.0.1:9094")`).
|
||||
/// Pass `None` to use filesystem-only token resolution.
|
||||
pub async fn new(config: crate::config::Config, vault_url: Option<&str>) -> Result<Self> {
|
||||
let (source_clients, distribution_clients) =
|
||||
ClientFactory::create_from_config(&config, vault_url).await?;
|
||||
|
||||
// Initialize cache
|
||||
let cache = Arc::new(ExtensionCache::new(
|
||||
config.cache.capacity,
|
||||
config.cache.ttl_seconds,
|
||||
@ -39,8 +47,19 @@ impl AppState {
|
||||
distribution_clients,
|
||||
cache,
|
||||
start_time: Instant::now(),
|
||||
#[cfg(feature = "nats")]
|
||||
events: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Attach a connected NatsBridge and spawn the cache invalidator
|
||||
/// subscriber.
|
||||
#[cfg(feature = "nats")]
|
||||
pub fn with_nats(mut self, bridge: Arc<platform_nats::NatsBridge>) -> Self {
|
||||
spawn_cache_invalidator(Arc::clone(&bridge), Arc::clone(&self.cache));
|
||||
self.events = Some(Arc::new(EventPublisher::new(bridge)));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// List all extensions
|
||||
@ -254,6 +273,12 @@ pub async fn download_extension(
|
||||
.download_extension(extension_type, &name, &version)
|
||||
.await
|
||||
{
|
||||
#[cfg(feature = "nats")]
|
||||
if let Some(events) = &state.events {
|
||||
let events = Arc::clone(events);
|
||||
let (ty, n, v) = (extension_type, name.clone(), version.clone());
|
||||
tokio::spawn(async move { events.publish_installed(ty, &n, &v).await });
|
||||
}
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
[(header::CONTENT_TYPE, "application/octet-stream")],
|
||||
@ -269,6 +294,12 @@ pub async fn download_extension(
|
||||
.download_extension(extension_type, &name, &version)
|
||||
.await
|
||||
{
|
||||
#[cfg(feature = "nats")]
|
||||
if let Some(events) = &state.events {
|
||||
let events = Arc::clone(events);
|
||||
let (ty, n, v) = (extension_type, name.clone(), version.clone());
|
||||
tokio::spawn(async move { events.publish_installed(ty, &n, &v).await });
|
||||
}
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
[(header::CONTENT_TYPE, "application/octet-stream")],
|
||||
|
||||
@ -2,9 +2,10 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use tracing::info;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use super::traits::{DistributionClient, SourceClient};
|
||||
use super::vault_resolver::VaultResolver;
|
||||
use super::{ForgejoClient, GitHubClient};
|
||||
use crate::config::Config;
|
||||
use crate::error::{RegistryError, Result};
|
||||
@ -15,46 +16,64 @@ use crate::oci::OciClient as OciClientImpl;
|
||||
pub struct ClientFactory;
|
||||
|
||||
impl ClientFactory {
|
||||
/// Create all configured clients from configuration
|
||||
pub fn create_from_config(
|
||||
/// Create all configured clients from configuration, resolving any vault://
|
||||
/// token references.
|
||||
///
|
||||
/// When `vault_url` is `Some`, any `token_path` starting with `vault://` is
|
||||
/// resolved via the vault-service HTTP API with a 5-minute in-memory
|
||||
/// cache. When `vault_url` is `None`, vault:// paths cause an error.
|
||||
pub async fn create_from_config(
|
||||
config: &Config,
|
||||
vault_url: Option<&str>,
|
||||
) -> Result<(Vec<Arc<dyn SourceClient>>, Vec<Arc<dyn DistributionClient>>)> {
|
||||
let resolver = vault_url.map(|u| VaultResolver::new(u.to_string()));
|
||||
|
||||
let mut source_clients: Vec<Arc<dyn SourceClient>> = Vec::new();
|
||||
let mut distribution_clients: Vec<Arc<dyn DistributionClient>> = Vec::new();
|
||||
|
||||
// Create Gitea clients (source-based)
|
||||
if let Some(ref gitea_config) = config.gitea {
|
||||
let client = GiteaClientImpl::new(gitea_config)?;
|
||||
let wrapped = Arc::new(client) as Arc<dyn SourceClient>;
|
||||
source_clients.push(wrapped);
|
||||
for gitea_config in &config.sources.gitea {
|
||||
let token = resolve_token(&gitea_config.token_path, resolver.as_ref()).await?;
|
||||
let client = GiteaClientImpl::new(gitea_config, token)?;
|
||||
source_clients.push(Arc::new(client) as Arc<dyn SourceClient>);
|
||||
info!("Registered Gitea client: {}", gitea_config.url);
|
||||
}
|
||||
|
||||
// Create Forgejo clients (source-based)
|
||||
if let Some(ref forgejo_config) = config.gitea {
|
||||
let client = ForgejoClient::new(forgejo_config)?;
|
||||
let wrapped = Arc::new(client) as Arc<dyn SourceClient>;
|
||||
source_clients.push(wrapped);
|
||||
for forgejo_config in &config.sources.forgejo {
|
||||
let token = resolve_token(&forgejo_config.token_path, resolver.as_ref()).await?;
|
||||
let client = ForgejoClient::new(forgejo_config, token)?;
|
||||
source_clients.push(Arc::new(client) as Arc<dyn SourceClient>);
|
||||
info!("Registered Forgejo client: {}", forgejo_config.url);
|
||||
}
|
||||
|
||||
// Create GitHub clients (source-based)
|
||||
if let Some(ref github_config) = config.gitea {
|
||||
let client = GitHubClient::new(github_config)?;
|
||||
let wrapped = Arc::new(client) as Arc<dyn SourceClient>;
|
||||
source_clients.push(wrapped);
|
||||
for github_config in &config.sources.github {
|
||||
// GitHub tokens are optional; a missing file is treated as unauthenticated
|
||||
// access
|
||||
let token = resolve_token_optional(&github_config.token_path, resolver.as_ref()).await;
|
||||
if token.is_none() {
|
||||
warn!(
|
||||
"GitHub client '{}' has no token — API rate limits apply",
|
||||
github_config.organization
|
||||
);
|
||||
}
|
||||
let client = GitHubClient::new(github_config, token)?;
|
||||
source_clients.push(Arc::new(client) as Arc<dyn SourceClient>);
|
||||
info!("Registered GitHub client: {}", github_config.organization);
|
||||
}
|
||||
|
||||
// Create OCI clients (distribution-based)
|
||||
if let Some(ref oci_config) = config.oci {
|
||||
let client = OciClientImpl::new(oci_config)?;
|
||||
let wrapped = Arc::new(client) as Arc<dyn DistributionClient>;
|
||||
distribution_clients.push(wrapped);
|
||||
for oci_config in &config.distributions.oci {
|
||||
let auth_token = match &oci_config.auth_token_path {
|
||||
Some(path) => Some(resolve_token(path, resolver.as_ref()).await?),
|
||||
None => None,
|
||||
};
|
||||
let client = OciClientImpl::new(oci_config, auth_token)?;
|
||||
distribution_clients.push(Arc::new(client) as Arc<dyn DistributionClient>);
|
||||
info!("Registered OCI client: {}", oci_config.registry);
|
||||
}
|
||||
|
||||
// Ensure at least one backend is configured
|
||||
if source_clients.is_empty() && distribution_clients.is_empty() {
|
||||
return Err(RegistryError::Config(
|
||||
"No backends configured (gitea, forgejo, github, or oci required)".to_string(),
|
||||
@ -70,3 +89,37 @@ impl ClientFactory {
|
||||
Ok((source_clients, distribution_clients))
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a token path to its plaintext value.
|
||||
///
|
||||
/// Paths prefixed with `vault://` are resolved via VaultResolver (requires
|
||||
/// resolver to be Some). All other paths are read from the filesystem.
|
||||
async fn resolve_token(path: &str, resolver: Option<&VaultResolver>) -> Result<String> {
|
||||
if path.starts_with("vault://") {
|
||||
let resolver = resolver.ok_or_else(|| {
|
||||
RegistryError::Config(format!(
|
||||
"token_path '{}' is a vault:// reference but no vault_url was provided",
|
||||
path
|
||||
))
|
||||
})?;
|
||||
return resolver
|
||||
.try_resolve(path)
|
||||
.await
|
||||
.expect("try_resolve always returns Some for vault:// prefixed paths")
|
||||
.map_err(|e| {
|
||||
RegistryError::Config(format!("Vault resolution failed for '{}': {}", path, e))
|
||||
});
|
||||
}
|
||||
|
||||
std::fs::read_to_string(path)
|
||||
.map(|s| s.trim().to_string())
|
||||
.map_err(|e| RegistryError::Config(format!("Failed to read token from '{}': {}", path, e)))
|
||||
}
|
||||
|
||||
/// Resolve a token path, returning `None` on any failure.
|
||||
///
|
||||
/// Used for optional tokens (e.g., GitHub) where unauthenticated access is
|
||||
/// acceptable.
|
||||
async fn resolve_token_optional(path: &str, resolver: Option<&VaultResolver>) -> Option<String> {
|
||||
resolve_token(path, resolver).await.ok()
|
||||
}
|
||||
|
||||
@ -19,14 +19,17 @@ pub struct ForgejoClient {
|
||||
}
|
||||
|
||||
impl ForgejoClient {
|
||||
/// Create new Forgejo client from Gitea config
|
||||
pub fn new(config: &crate::config::GiteaConfig) -> Result<Self> {
|
||||
/// Create new Forgejo client with a pre-resolved token.
|
||||
///
|
||||
/// Token resolution (file read or vault:// fetch) is the caller's
|
||||
/// responsibility.
|
||||
pub fn new(config: &crate::config::GiteaConfig, token: String) -> Result<Self> {
|
||||
let backend_id = config
|
||||
.id
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("forgejo-{}", config.organization));
|
||||
|
||||
let inner = crate::gitea::GiteaClient::new(config)?;
|
||||
let inner = crate::gitea::GiteaClient::new(config, token)?;
|
||||
|
||||
Ok(Self { backend_id, inner })
|
||||
}
|
||||
|
||||
@ -1,501 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use reqwest::Client;
|
||||
use tracing::debug;
|
||||
use url::Url;
|
||||
|
||||
use super::traits::{BackendType, ExtensionClient, SourceClient};
|
||||
use crate::config::GiteaConfig;
|
||||
use crate::error::{RegistryError, Result};
|
||||
use crate::gitea::models::{GiteaRelease, GiteaRepository};
|
||||
use crate::models::{Extension, ExtensionSource, ExtensionType, ExtensionVersion};
|
||||
|
||||
/// Gitea API client
|
||||
pub struct GiteaClient {
|
||||
id: String,
|
||||
base_url: Url,
|
||||
organization: String,
|
||||
token: String,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl GiteaClient {
|
||||
/// Create new Gitea client
|
||||
pub fn new(config: &GiteaConfig) -> Result<Self> {
|
||||
let base_url = Url::parse(&config.url)
|
||||
.map_err(|e| RegistryError::Config(format!("Invalid Gitea URL: {}", e)))?;
|
||||
|
||||
let token = config.read_token()?;
|
||||
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(config.timeout_seconds))
|
||||
.danger_accept_invalid_certs(!config.verify_ssl)
|
||||
.build()
|
||||
.map_err(|e| RegistryError::Gitea(format!("Failed to create HTTP client: {}", e)))?;
|
||||
|
||||
let id = config
|
||||
.id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "gitea-default".to_string());
|
||||
|
||||
Ok(Self {
|
||||
id,
|
||||
base_url,
|
||||
organization: config.organization.clone(),
|
||||
token,
|
||||
client,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the backend ID
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
/// List all extensions from organization repositories
|
||||
pub async fn list_extensions_impl(
|
||||
&self,
|
||||
extension_type: Option<ExtensionType>,
|
||||
) -> Result<Vec<Extension>> {
|
||||
debug!(
|
||||
"Fetching repositories for organization: {}",
|
||||
self.organization
|
||||
);
|
||||
|
||||
let repos = self.list_repositories().await?;
|
||||
let mut extensions = Vec::new();
|
||||
|
||||
for repo in repos {
|
||||
// Skip archived repositories
|
||||
if repo.archived {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse extension type from repository name prefix
|
||||
let Some(ext_type) = self.parse_extension_type(&repo.name) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Filter by type if specified
|
||||
if let Some(filter_type) = extension_type {
|
||||
if ext_type != filter_type {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Get latest release for this repository
|
||||
let Ok(releases) = self.list_releases(&repo.name).await else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(latest_release) = releases.first() {
|
||||
if let Some(extension) = self.release_to_extension(&repo, latest_release, ext_type)
|
||||
{
|
||||
extensions.push(extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(extensions)
|
||||
}
|
||||
|
||||
/// Get specific extension metadata
|
||||
pub async fn get_extension_impl(
|
||||
&self,
|
||||
extension_type: ExtensionType,
|
||||
name: &str,
|
||||
) -> Result<Extension> {
|
||||
let repo_name = self.format_repo_name(extension_type, name);
|
||||
debug!("Fetching extension: {}", repo_name);
|
||||
|
||||
let repo = self.get_repository(&repo_name).await?;
|
||||
let releases = self.list_releases(&repo_name).await?;
|
||||
|
||||
let latest_release = releases.first().ok_or_else(|| {
|
||||
RegistryError::NotFound(format!("No releases found for {}", repo_name))
|
||||
})?;
|
||||
|
||||
self.release_to_extension(&repo, latest_release, extension_type)
|
||||
.ok_or_else(|| {
|
||||
RegistryError::NotFound(format!("Invalid extension metadata for {}", repo_name))
|
||||
})
|
||||
}
|
||||
|
||||
/// List all versions for an extension
|
||||
pub async fn list_versions_impl(
|
||||
&self,
|
||||
extension_type: ExtensionType,
|
||||
name: &str,
|
||||
) -> Result<Vec<ExtensionVersion>> {
|
||||
let repo_name = self.format_repo_name(extension_type, name);
|
||||
debug!("Fetching versions for: {}", repo_name);
|
||||
|
||||
let releases = self.list_releases(&repo_name).await?;
|
||||
|
||||
Ok(releases
|
||||
.iter()
|
||||
.map(|release| ExtensionVersion {
|
||||
version: release.tag_name.clone(),
|
||||
published_at: release.published_at.unwrap_or(release.created_at),
|
||||
download_url: release
|
||||
.assets
|
||||
.first()
|
||||
.map(|a| a.browser_download_url.clone()),
|
||||
checksum: None,
|
||||
size: release.assets.first().map(|a| a.size),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Download extension asset
|
||||
pub async fn download_extension_impl(
|
||||
&self,
|
||||
extension_type: ExtensionType,
|
||||
name: &str,
|
||||
version: &str,
|
||||
) -> Result<Bytes> {
|
||||
let repo_name = self.format_repo_name(extension_type, name);
|
||||
debug!("Downloading extension: {} version {}", repo_name, version);
|
||||
|
||||
let release = self.get_release(&repo_name, version).await?;
|
||||
|
||||
let asset = release.assets.first().ok_or_else(|| {
|
||||
RegistryError::NotFound(format!("No assets found for release {}", version))
|
||||
})?;
|
||||
|
||||
self.download_asset(&asset.browser_download_url).await
|
||||
}
|
||||
|
||||
/// List repositories in organization
|
||||
async fn list_repositories(&self) -> Result<Vec<GiteaRepository>> {
|
||||
let url = self
|
||||
.base_url
|
||||
.join(&format!("api/v1/orgs/{}/repos", self.organization))
|
||||
.map_err(|e| RegistryError::Gitea(format!("Invalid URL: {}", e)))?;
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(url)
|
||||
.header("Authorization", format!("token {}", self.token))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Gitea(format!("Request failed: {}", e)))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(RegistryError::Gitea(format!(
|
||||
"Failed to list repositories: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Gitea(format!("Failed to parse response: {}", e)))
|
||||
}
|
||||
|
||||
/// Get specific repository
|
||||
async fn get_repository(&self, repo_name: &str) -> Result<GiteaRepository> {
|
||||
let url = self
|
||||
.base_url
|
||||
.join(&format!("api/v1/repos/{}/{}", self.organization, repo_name))
|
||||
.map_err(|e| RegistryError::Gitea(format!("Invalid URL: {}", e)))?;
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(url)
|
||||
.header("Authorization", format!("token {}", self.token))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Gitea(format!("Request failed: {}", e)))?;
|
||||
|
||||
if response.status().as_u16() == 404 {
|
||||
return Err(RegistryError::NotFound(format!(
|
||||
"Repository not found: {}",
|
||||
repo_name
|
||||
)));
|
||||
}
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(RegistryError::Gitea(format!(
|
||||
"Failed to get repository: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Gitea(format!("Failed to parse response: {}", e)))
|
||||
}
|
||||
|
||||
/// List releases for repository
|
||||
async fn list_releases(&self, repo_name: &str) -> Result<Vec<GiteaRelease>> {
|
||||
let url = self
|
||||
.base_url
|
||||
.join(&format!(
|
||||
"api/v1/repos/{}/{}/releases",
|
||||
self.organization, repo_name
|
||||
))
|
||||
.map_err(|e| RegistryError::Gitea(format!("Invalid URL: {}", e)))?;
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(url)
|
||||
.header("Authorization", format!("token {}", self.token))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Gitea(format!("Request failed: {}", e)))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(RegistryError::Gitea(format!(
|
||||
"Failed to list releases: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut releases: Vec<GiteaRelease> = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Gitea(format!("Failed to parse response: {}", e)))?;
|
||||
|
||||
// Filter out drafts and prereleases, sort by date
|
||||
releases.retain(|r| !r.draft && !r.prerelease);
|
||||
releases.sort_by(|a, b| {
|
||||
let a_date = a.published_at.unwrap_or(a.created_at);
|
||||
let b_date = b.published_at.unwrap_or(b.created_at);
|
||||
b_date.cmp(&a_date)
|
||||
});
|
||||
|
||||
Ok(releases)
|
||||
}
|
||||
|
||||
/// Get specific release
|
||||
async fn get_release(&self, repo_name: &str, tag: &str) -> Result<GiteaRelease> {
|
||||
let url = self
|
||||
.base_url
|
||||
.join(&format!(
|
||||
"api/v1/repos/{}/{}/releases/tags/{}",
|
||||
self.organization, repo_name, tag
|
||||
))
|
||||
.map_err(|e| RegistryError::Gitea(format!("Invalid URL: {}", e)))?;
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(url)
|
||||
.header("Authorization", format!("token {}", self.token))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Gitea(format!("Request failed: {}", e)))?;
|
||||
|
||||
if response.status().as_u16() == 404 {
|
||||
return Err(RegistryError::NotFound(format!(
|
||||
"Release not found: {}",
|
||||
tag
|
||||
)));
|
||||
}
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(RegistryError::Gitea(format!(
|
||||
"Failed to get release: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Gitea(format!("Failed to parse response: {}", e)))
|
||||
}
|
||||
|
||||
/// Download asset from URL
|
||||
async fn download_asset(&self, url: &str) -> Result<Bytes> {
|
||||
let response = self
|
||||
.client
|
||||
.get(url)
|
||||
.header("Authorization", format!("token {}", self.token))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Gitea(format!("Download failed: {}", e)))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(RegistryError::Gitea(format!(
|
||||
"Download failed: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Gitea(format!("Failed to read response: {}", e)))
|
||||
}
|
||||
|
||||
/// Parse extension type from repository name
|
||||
fn parse_extension_type(&self, repo_name: &str) -> Option<ExtensionType> {
|
||||
if repo_name.ends_with("_prov") {
|
||||
Some(ExtensionType::Provider)
|
||||
} else if repo_name.ends_with("_taskserv") {
|
||||
Some(ExtensionType::Taskserv)
|
||||
} else if repo_name.ends_with("_cluster") {
|
||||
Some(ExtensionType::Cluster)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Format repository name from extension type and name
|
||||
fn format_repo_name(&self, extension_type: ExtensionType, name: &str) -> String {
|
||||
let suffix = match extension_type {
|
||||
ExtensionType::Provider => "_prov",
|
||||
ExtensionType::Taskserv => "_taskserv",
|
||||
ExtensionType::Cluster => "_cluster",
|
||||
};
|
||||
format!("{}{}", name, suffix)
|
||||
}
|
||||
|
||||
/// Convert Gitea release to Extension
|
||||
fn release_to_extension(
|
||||
&self,
|
||||
repo: &GiteaRepository,
|
||||
release: &GiteaRelease,
|
||||
extension_type: ExtensionType,
|
||||
) -> Option<Extension> {
|
||||
let name = self.extract_extension_name(&repo.name, extension_type)?;
|
||||
|
||||
Some(Extension {
|
||||
name,
|
||||
extension_type,
|
||||
version: release.tag_name.clone(),
|
||||
description: repo.description.clone().unwrap_or_default(),
|
||||
author: Some(repo.owner.login.clone()),
|
||||
repository: Some(repo.html_url.clone()),
|
||||
source: ExtensionSource::Gitea,
|
||||
published_at: release.published_at.unwrap_or(release.created_at),
|
||||
download_url: release
|
||||
.assets
|
||||
.first()
|
||||
.map(|a| a.browser_download_url.clone()),
|
||||
checksum: None,
|
||||
size: release.assets.first().map(|a| a.size),
|
||||
tags: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Extract extension name from repository name
|
||||
fn extract_extension_name(
|
||||
&self,
|
||||
repo_name: &str,
|
||||
extension_type: ExtensionType,
|
||||
) -> Option<String> {
|
||||
let suffix = match extension_type {
|
||||
ExtensionType::Provider => "_prov",
|
||||
ExtensionType::Taskserv => "_taskserv",
|
||||
ExtensionType::Cluster => "_cluster",
|
||||
};
|
||||
|
||||
repo_name.strip_suffix(suffix).map(|s| s.to_string())
|
||||
}
|
||||
|
||||
/// Check health of Gitea connection
|
||||
pub async fn health_check_impl(&self) -> Result<()> {
|
||||
let url = self
|
||||
.base_url
|
||||
.join("api/v1/version")
|
||||
.map_err(|e| RegistryError::Gitea(format!("Invalid URL: {}", e)))?;
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(url)
|
||||
.timeout(Duration::from_secs(5))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Gitea(format!("Health check failed: {}", e)))?;
|
||||
|
||||
if response.status().is_success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(RegistryError::Gitea(format!(
|
||||
"Health check returned: {}",
|
||||
response.status()
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExtensionClient for GiteaClient {
|
||||
async fn list_extensions(
|
||||
&self,
|
||||
extension_type: Option<ExtensionType>,
|
||||
) -> Result<Vec<Extension>> {
|
||||
self.list_extensions_impl(extension_type).await
|
||||
}
|
||||
|
||||
async fn get_extension(&self, extension_type: ExtensionType, name: &str) -> Result<Extension> {
|
||||
self.get_extension_impl(extension_type, name).await
|
||||
}
|
||||
|
||||
async fn list_versions(
|
||||
&self,
|
||||
extension_type: ExtensionType,
|
||||
name: &str,
|
||||
) -> Result<Vec<ExtensionVersion>> {
|
||||
self.list_versions_impl(extension_type, name).await
|
||||
}
|
||||
|
||||
async fn download_extension(
|
||||
&self,
|
||||
extension_type: ExtensionType,
|
||||
name: &str,
|
||||
version: &str,
|
||||
) -> Result<Bytes> {
|
||||
self.download_extension_impl(extension_type, name, version)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn health_check(&self) -> Result<()> {
|
||||
self.health_check_impl().await
|
||||
}
|
||||
|
||||
fn backend_id(&self) -> String {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn backend_type(&self) -> BackendType {
|
||||
BackendType::Gitea
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SourceClient for GiteaClient {
|
||||
async fn get_repository_url(
|
||||
&self,
|
||||
extension_type: ExtensionType,
|
||||
name: &str,
|
||||
) -> Result<String> {
|
||||
let repo_name = self.format_repo_name(extension_type, name);
|
||||
let repo = self.get_repository(&repo_name).await?;
|
||||
Ok(repo.html_url)
|
||||
}
|
||||
|
||||
async fn list_releases(&self, repo_name: &str) -> Result<Vec<String>> {
|
||||
let releases = self.list_releases(repo_name).await?;
|
||||
Ok(releases.iter().map(|r| r.tag_name.clone()).collect())
|
||||
}
|
||||
|
||||
async fn get_release_notes(
|
||||
&self,
|
||||
_extension_type: ExtensionType,
|
||||
_name: &str,
|
||||
version: &str,
|
||||
) -> Result<Option<String>> {
|
||||
// Gitea doesn't provide structured release notes via the API
|
||||
// We'd need to scrape the HTML or use a custom field
|
||||
// For now, just return None
|
||||
let _version = version;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@ -24,10 +24,12 @@ pub struct GitHubClient {
|
||||
}
|
||||
|
||||
impl GitHubClient {
|
||||
/// Create new GitHub client from Gitea config (reused for simplicity)
|
||||
pub fn new(config: &crate::config::GiteaConfig) -> Result<Self> {
|
||||
let token = config.read_token().ok();
|
||||
|
||||
/// Create new GitHub client with a pre-resolved token.
|
||||
///
|
||||
/// Token resolution (file read or vault:// fetch) is the caller's
|
||||
/// responsibility. Pass `None` for unauthenticated access (lower rate
|
||||
/// limits apply).
|
||||
pub fn new(config: &crate::config::GiteaConfig, token: Option<String>) -> Result<Self> {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(config.timeout_seconds))
|
||||
.danger_accept_invalid_certs(!config.verify_ssl)
|
||||
|
||||
@ -8,6 +8,7 @@ pub mod factory;
|
||||
pub mod forgejo;
|
||||
pub mod github;
|
||||
pub mod traits;
|
||||
pub mod vault_resolver;
|
||||
|
||||
pub use factory::ClientFactory;
|
||||
pub use forgejo::ForgejoClient;
|
||||
|
||||
@ -1,481 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use chrono::Utc;
|
||||
use reqwest::Client;
|
||||
use tracing::debug;
|
||||
|
||||
use super::traits::{BackendType, DistributionClient, ExtensionClient};
|
||||
use crate::config::OciConfig;
|
||||
use crate::error::{RegistryError, Result};
|
||||
use crate::models::{Extension, ExtensionSource, ExtensionType, ExtensionVersion};
|
||||
use crate::oci::models::{OciCatalog, OciManifest, OciTagsList};
|
||||
|
||||
/// OCI registry client
|
||||
pub struct OciClient {
|
||||
id: String,
|
||||
registry: String,
|
||||
namespace: String,
|
||||
auth_token: Option<String>,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl OciClient {
|
||||
/// Create new OCI client
|
||||
pub fn new(config: &OciConfig) -> Result<Self> {
|
||||
let auth_token = config.read_token()?;
|
||||
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(config.timeout_seconds))
|
||||
.danger_accept_invalid_certs(!config.verify_ssl)
|
||||
.build()
|
||||
.map_err(|e| RegistryError::Oci(format!("Failed to create HTTP client: {}", e)))?;
|
||||
|
||||
let id = config
|
||||
.id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "oci-default".to_string());
|
||||
|
||||
Ok(Self {
|
||||
id,
|
||||
registry: config.registry.clone(),
|
||||
namespace: config.namespace.clone(),
|
||||
auth_token,
|
||||
client,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the backend ID
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
/// List all extensions from OCI registry
|
||||
pub async fn list_extensions_impl(
|
||||
&self,
|
||||
extension_type: Option<ExtensionType>,
|
||||
) -> Result<Vec<Extension>> {
|
||||
debug!("Fetching artifacts from OCI registry: {}", self.registry);
|
||||
|
||||
let catalog = self.list_catalog().await?;
|
||||
let mut extensions = Vec::new();
|
||||
|
||||
for repo_name in catalog.repositories {
|
||||
// Skip if not in our namespace
|
||||
if !repo_name.starts_with(&format!("{}/", self.namespace)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse extension type and name from repository
|
||||
let Some((ext_type, _name)) = self.parse_repository_name(&repo_name) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Filter by type if specified
|
||||
if let Some(filter_type) = extension_type {
|
||||
if ext_type != filter_type {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Get tags for this repository
|
||||
let Ok(tags) = self.list_tags(&repo_name).await else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(latest_tag) = tags.tags.first() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Ok(manifest) = self.get_manifest(&repo_name, latest_tag).await {
|
||||
if let Some(extension) =
|
||||
self.manifest_to_extension(&repo_name, latest_tag, &manifest, ext_type)
|
||||
{
|
||||
extensions.push(extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(extensions)
|
||||
}
|
||||
|
||||
/// Get specific extension metadata
|
||||
pub async fn get_extension_impl(
|
||||
&self,
|
||||
extension_type: ExtensionType,
|
||||
name: &str,
|
||||
) -> Result<Extension> {
|
||||
let repo_name = self.format_repository_name(extension_type, name);
|
||||
debug!("Fetching extension: {}", repo_name);
|
||||
|
||||
let tags = self.list_tags(&repo_name).await?;
|
||||
let latest_tag = tags
|
||||
.tags
|
||||
.first()
|
||||
.ok_or_else(|| RegistryError::NotFound(format!("No tags found for {}", repo_name)))?;
|
||||
|
||||
let manifest = self.get_manifest(&repo_name, latest_tag).await?;
|
||||
|
||||
self.manifest_to_extension(&repo_name, latest_tag, &manifest, extension_type)
|
||||
.ok_or_else(|| {
|
||||
RegistryError::NotFound(format!("Invalid extension metadata for {}", repo_name))
|
||||
})
|
||||
}
|
||||
|
||||
/// List all versions for an extension
|
||||
pub async fn list_versions_impl(
|
||||
&self,
|
||||
extension_type: ExtensionType,
|
||||
name: &str,
|
||||
) -> Result<Vec<ExtensionVersion>> {
|
||||
let repo_name = self.format_repository_name(extension_type, name);
|
||||
debug!("Fetching versions for: {}", repo_name);
|
||||
|
||||
let tags = self.list_tags(&repo_name).await?;
|
||||
|
||||
let mut versions = Vec::new();
|
||||
for tag in tags.tags {
|
||||
if let Ok(manifest) = self.get_manifest(&repo_name, &tag).await {
|
||||
let size = manifest.layers.iter().map(|l| l.size).sum();
|
||||
|
||||
versions.push(ExtensionVersion {
|
||||
version: tag.clone(),
|
||||
published_at: Utc::now(),
|
||||
download_url: None,
|
||||
checksum: Some(manifest.config.digest.clone()),
|
||||
size: Some(size),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(versions)
|
||||
}
|
||||
|
||||
/// Pull extension artifact
|
||||
pub async fn download_extension_impl(
|
||||
&self,
|
||||
extension_type: ExtensionType,
|
||||
name: &str,
|
||||
tag: &str,
|
||||
) -> Result<Bytes> {
|
||||
let repo_name = self.format_repository_name(extension_type, name);
|
||||
debug!("Pulling extension: {} tag {}", repo_name, tag);
|
||||
|
||||
let manifest = self.get_manifest(&repo_name, tag).await?;
|
||||
|
||||
// Download first layer (assuming single-layer artifacts)
|
||||
let layer = manifest
|
||||
.layers
|
||||
.first()
|
||||
.ok_or_else(|| RegistryError::Oci("No layers found in manifest".to_string()))?;
|
||||
|
||||
self.download_blob(&repo_name, &layer.digest).await
|
||||
}
|
||||
|
||||
/// List catalog (all repositories)
|
||||
async fn list_catalog(&self) -> Result<OciCatalog> {
|
||||
let url = format!("https://{}/v2/_catalog", self.registry);
|
||||
|
||||
let mut request = self.client.get(&url);
|
||||
|
||||
if let Some(ref token) = self.auth_token {
|
||||
request = request.header("Authorization", format!("Bearer {}", token));
|
||||
}
|
||||
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Oci(format!("Request failed: {}", e)))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(RegistryError::Oci(format!(
|
||||
"Failed to list catalog: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Oci(format!("Failed to parse response: {}", e)))
|
||||
}
|
||||
|
||||
/// List tags for repository
|
||||
async fn list_tags(&self, repository: &str) -> Result<OciTagsList> {
|
||||
let url = format!("https://{}/v2/{}/tags/list", self.registry, repository);
|
||||
|
||||
let mut request = self.client.get(&url);
|
||||
|
||||
if let Some(ref token) = self.auth_token {
|
||||
request = request.header("Authorization", format!("Bearer {}", token));
|
||||
}
|
||||
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Oci(format!("Request failed: {}", e)))?;
|
||||
|
||||
if response.status().as_u16() == 404 {
|
||||
return Err(RegistryError::NotFound(format!(
|
||||
"Repository not found: {}",
|
||||
repository
|
||||
)));
|
||||
}
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(RegistryError::Oci(format!(
|
||||
"Failed to list tags: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Oci(format!("Failed to parse response: {}", e)))
|
||||
}
|
||||
|
||||
/// Get manifest for tag
|
||||
async fn get_manifest(&self, repository: &str, tag: &str) -> Result<OciManifest> {
|
||||
let url = format!(
|
||||
"https://{}/v2/{}/manifests/{}",
|
||||
self.registry, repository, tag
|
||||
);
|
||||
|
||||
let mut request = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("Accept", "application/vnd.oci.image.manifest.v1+json");
|
||||
|
||||
if let Some(ref token) = self.auth_token {
|
||||
request = request.header("Authorization", format!("Bearer {}", token));
|
||||
}
|
||||
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Oci(format!("Request failed: {}", e)))?;
|
||||
|
||||
if response.status().as_u16() == 404 {
|
||||
return Err(RegistryError::NotFound(format!(
|
||||
"Manifest not found: {}:{}",
|
||||
repository, tag
|
||||
)));
|
||||
}
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(RegistryError::Oci(format!(
|
||||
"Failed to get manifest: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Oci(format!("Failed to parse manifest: {}", e)))
|
||||
}
|
||||
|
||||
/// Download blob by digest
|
||||
async fn download_blob(&self, repository: &str, digest: &str) -> Result<Bytes> {
|
||||
let url = format!(
|
||||
"https://{}/v2/{}/blobs/{}",
|
||||
self.registry, repository, digest
|
||||
);
|
||||
|
||||
let mut request = self.client.get(&url);
|
||||
|
||||
if let Some(ref token) = self.auth_token {
|
||||
request = request.header("Authorization", format!("Bearer {}", token));
|
||||
}
|
||||
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Oci(format!("Download failed: {}", e)))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(RegistryError::Oci(format!(
|
||||
"Download failed: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Oci(format!("Failed to read response: {}", e)))
|
||||
}
|
||||
|
||||
/// Parse repository name into extension type and name
|
||||
fn parse_repository_name(&self, repo_name: &str) -> Option<(ExtensionType, String)> {
|
||||
let name = repo_name.strip_prefix(&format!("{}/", self.namespace))?;
|
||||
|
||||
name.strip_suffix("-provider")
|
||||
.map(|base_name| (ExtensionType::Provider, base_name.to_string()))
|
||||
.or_else(|| {
|
||||
name.strip_suffix("-taskserv")
|
||||
.map(|base_name| (ExtensionType::Taskserv, base_name.to_string()))
|
||||
})
|
||||
.or_else(|| {
|
||||
name.strip_suffix("-cluster")
|
||||
.map(|base_name| (ExtensionType::Cluster, base_name.to_string()))
|
||||
})
|
||||
}
|
||||
|
||||
/// Format repository name from extension type and name
|
||||
fn format_repository_name(&self, extension_type: ExtensionType, name: &str) -> String {
|
||||
let suffix = match extension_type {
|
||||
ExtensionType::Provider => "-provider",
|
||||
ExtensionType::Taskserv => "-taskserv",
|
||||
ExtensionType::Cluster => "-cluster",
|
||||
};
|
||||
format!("{}/{}{}", self.namespace, name, suffix)
|
||||
}
|
||||
|
||||
/// Convert OCI manifest to Extension
|
||||
fn manifest_to_extension(
|
||||
&self,
|
||||
repo_name: &str,
|
||||
tag: &str,
|
||||
manifest: &OciManifest,
|
||||
extension_type: ExtensionType,
|
||||
) -> Option<Extension> {
|
||||
let (_, name) = self.parse_repository_name(repo_name)?;
|
||||
|
||||
let description = manifest
|
||||
.annotations
|
||||
.as_ref()
|
||||
.and_then(|a| a.get("org.opencontainers.image.description"))
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let author = manifest
|
||||
.annotations
|
||||
.as_ref()
|
||||
.and_then(|a| a.get("org.opencontainers.image.authors"))
|
||||
.cloned();
|
||||
|
||||
let repository = manifest
|
||||
.annotations
|
||||
.as_ref()
|
||||
.and_then(|a| a.get("org.opencontainers.image.url"))
|
||||
.cloned();
|
||||
|
||||
let size = manifest.layers.iter().map(|l| l.size).sum();
|
||||
|
||||
Some(Extension {
|
||||
name,
|
||||
extension_type,
|
||||
version: tag.to_string(),
|
||||
description,
|
||||
author,
|
||||
repository,
|
||||
source: ExtensionSource::Oci,
|
||||
published_at: Utc::now(),
|
||||
download_url: None,
|
||||
checksum: Some(manifest.config.digest.clone()),
|
||||
size: Some(size),
|
||||
tags: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check health of OCI connection
|
||||
pub async fn health_check_impl(&self) -> Result<()> {
|
||||
let url = format!("https://{}/v2/", self.registry);
|
||||
|
||||
let mut request = self.client.get(&url).timeout(Duration::from_secs(5));
|
||||
|
||||
if let Some(ref token) = self.auth_token {
|
||||
request = request.header("Authorization", format!("Bearer {}", token));
|
||||
}
|
||||
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| RegistryError::Oci(format!("Health check failed: {}", e)))?;
|
||||
|
||||
if response.status().is_success() || response.status().as_u16() == 401 {
|
||||
// 401 means registry is up but auth is required
|
||||
Ok(())
|
||||
} else {
|
||||
Err(RegistryError::Oci(format!(
|
||||
"Health check returned: {}",
|
||||
response.status()
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExtensionClient for OciClient {
|
||||
async fn list_extensions(
|
||||
&self,
|
||||
extension_type: Option<ExtensionType>,
|
||||
) -> Result<Vec<Extension>> {
|
||||
self.list_extensions_impl(extension_type).await
|
||||
}
|
||||
|
||||
async fn get_extension(&self, extension_type: ExtensionType, name: &str) -> Result<Extension> {
|
||||
self.get_extension_impl(extension_type, name).await
|
||||
}
|
||||
|
||||
async fn list_versions(
|
||||
&self,
|
||||
extension_type: ExtensionType,
|
||||
name: &str,
|
||||
) -> Result<Vec<ExtensionVersion>> {
|
||||
self.list_versions_impl(extension_type, name).await
|
||||
}
|
||||
|
||||
async fn download_extension(
|
||||
&self,
|
||||
extension_type: ExtensionType,
|
||||
name: &str,
|
||||
version: &str,
|
||||
) -> Result<Bytes> {
|
||||
self.download_extension_impl(extension_type, name, version)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn health_check(&self) -> Result<()> {
|
||||
self.health_check_impl().await
|
||||
}
|
||||
|
||||
fn backend_id(&self) -> String {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn backend_type(&self) -> BackendType {
|
||||
BackendType::Oci
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DistributionClient for OciClient {
|
||||
async fn get_manifest(&self, repository: &str, tag: &str) -> Result<serde_json::Value> {
|
||||
let manifest = self.get_manifest(repository, tag).await?;
|
||||
Ok(serde_json::to_value(manifest)?)
|
||||
}
|
||||
|
||||
async fn list_catalog(&self) -> Result<Vec<String>> {
|
||||
let catalog = self.list_catalog().await?;
|
||||
Ok(catalog.repositories)
|
||||
}
|
||||
|
||||
async fn get_digest(&self, repository: &str, tag: &str) -> Result<String> {
|
||||
let manifest = self.get_manifest(repository, tag).await?;
|
||||
Ok(manifest.config.digest)
|
||||
}
|
||||
|
||||
async fn verify_artifact(
|
||||
&self,
|
||||
repository: &str,
|
||||
tag: &str,
|
||||
expected_digest: &str,
|
||||
) -> Result<bool> {
|
||||
let manifest = self.get_manifest(repository, tag).await?;
|
||||
Ok(manifest.config.digest == expected_digest)
|
||||
}
|
||||
}
|
||||
115
crates/extension-registry/src/client/vault_resolver.rs
Normal file
115
crates/extension-registry/src/client/vault_resolver.rs
Normal file
@ -0,0 +1,115 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::error::{RegistryError, Result};
|
||||
|
||||
const TOKEN_TTL: Duration = Duration::from_secs(300); // 5-minute in-memory cache
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SecretResponse {
|
||||
value: String,
|
||||
}
|
||||
|
||||
struct CachedToken {
|
||||
value: String,
|
||||
expires_at: Instant,
|
||||
}
|
||||
|
||||
/// Resolves `vault://path` references to their plaintext values
|
||||
/// via the vault-service HTTP API.
|
||||
///
|
||||
/// Tokens are cached in-memory for `TOKEN_TTL` to avoid repeated requests
|
||||
/// on every client creation.
|
||||
pub struct VaultResolver {
|
||||
vault_url: String,
|
||||
http: Client,
|
||||
cache: Arc<Mutex<HashMap<String, CachedToken>>>,
|
||||
}
|
||||
|
||||
impl VaultResolver {
|
||||
pub fn new(vault_url: String) -> Self {
|
||||
Self {
|
||||
vault_url,
|
||||
http: Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()
|
||||
.expect("reqwest client"),
|
||||
cache: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve `vault://path/to/secret` → plaintext value.
|
||||
///
|
||||
/// Returns `None` if the path is not a `vault://` reference.
|
||||
pub async fn try_resolve(&self, token_path: &str) -> Option<Result<String>> {
|
||||
let secret_path = token_path.strip_prefix("vault://")?;
|
||||
|
||||
// Check cache first
|
||||
{
|
||||
let cache = self.cache.lock();
|
||||
if let Some(cached) = cache.get(secret_path) {
|
||||
if cached.expires_at > Instant::now() {
|
||||
return Some(Ok(cached.value.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch from vault-service
|
||||
let url = format!("{}/v1/{}", self.vault_url, secret_path);
|
||||
let resp = match self.http.get(&url).send().await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Some(Err(RegistryError::Config(format!(
|
||||
"vault resolve HTTP error for '{}': {}",
|
||||
secret_path, e
|
||||
))))
|
||||
}
|
||||
};
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Some(Err(RegistryError::Config(format!(
|
||||
"vault resolve failed for '{}': HTTP {}",
|
||||
secret_path,
|
||||
resp.status()
|
||||
))));
|
||||
}
|
||||
|
||||
let body: SecretResponse = match resp.json().await {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
return Some(Err(RegistryError::Config(format!(
|
||||
"vault resolve parse error for '{}': {}",
|
||||
secret_path, e
|
||||
))))
|
||||
}
|
||||
};
|
||||
|
||||
info!(path = %secret_path, "Token resolved from vault");
|
||||
|
||||
// Cache the result
|
||||
{
|
||||
let mut cache = self.cache.lock();
|
||||
cache.insert(
|
||||
secret_path.to_string(),
|
||||
CachedToken {
|
||||
value: body.value.clone(),
|
||||
expires_at: Instant::now() + TOKEN_TTL,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Some(Ok(body.value))
|
||||
}
|
||||
|
||||
/// Expire all cached tokens, forcing re-resolution on next access.
|
||||
pub fn invalidate_cache(&self) {
|
||||
self.cache.lock().clear();
|
||||
warn!("VaultResolver: token cache invalidated");
|
||||
}
|
||||
}
|
||||
@ -27,6 +27,26 @@ pub struct Config {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load configuration from Nickel (extension-registry.ncl) with fallback to
|
||||
/// hierarchy
|
||||
pub fn load() -> Result<Self> {
|
||||
let mut config = Self::load_from_nickel()
|
||||
.or_else(|_| Self::load_from_hierarchy())
|
||||
.unwrap_or_default();
|
||||
|
||||
Self::migrate_to_multiinstance(&mut config);
|
||||
Self::apply_env_overrides_internal(&mut config);
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Load configuration from Nickel extension-registry.ncl
|
||||
fn load_from_nickel() -> Result<Self> {
|
||||
let config_json = platform_config::load_service_config_from_ncl("extension-registry")
|
||||
.map_err(|e| RegistryError::Config(format!("Failed to load from Nickel: {}", e)))?;
|
||||
|
||||
serde_json::from_value(config_json).map_err(RegistryError::Json)
|
||||
}
|
||||
|
||||
/// Load configuration from hierarchical sources with mode support
|
||||
///
|
||||
/// Priority order:
|
||||
@ -196,7 +216,10 @@ impl ConfigLoader for Config {
|
||||
let service = Self::service_name();
|
||||
|
||||
if let Some(path) = platform_config::resolve_config_path(service) {
|
||||
return Self::from_path(&path);
|
||||
return Self::from_path(&path).map_err(|e| {
|
||||
Box::new(std::io::Error::other(e.to_string()))
|
||||
as Box<dyn std::error::Error + Send + Sync>
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback to defaults
|
||||
@ -317,7 +340,9 @@ impl GiteaConfig {
|
||||
));
|
||||
}
|
||||
|
||||
if !Path::new(&self.token_path).exists() {
|
||||
// vault:// references are resolved at runtime; file check only for filesystem
|
||||
// paths
|
||||
if !self.token_path.starts_with("vault://") && !Path::new(&self.token_path).exists() {
|
||||
return Err(RegistryError::Config(format!(
|
||||
"Gitea token file not found: {}",
|
||||
self.token_path
|
||||
@ -327,8 +352,15 @@ impl GiteaConfig {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read token from file
|
||||
/// Read token from file. Only call for non-vault:// paths.
|
||||
pub fn read_token(&self) -> Result<String> {
|
||||
if self.token_path.starts_with("vault://") {
|
||||
return Err(RegistryError::Config(
|
||||
"token_path is a vault:// reference — use ClientFactory::create_from_config_async \
|
||||
to resolve"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
std::fs::read_to_string(&self.token_path)
|
||||
.map(|s| s.trim().to_string())
|
||||
.map_err(|e| RegistryError::Config(format!("Failed to read Gitea token: {}", e)))
|
||||
@ -364,7 +396,9 @@ impl OciConfig {
|
||||
}
|
||||
|
||||
if let Some(ref token_path) = self.auth_token_path {
|
||||
if !Path::new(token_path).exists() {
|
||||
// vault:// references are resolved at runtime; file check only for filesystem
|
||||
// paths
|
||||
if !token_path.starts_with("vault://") && !Path::new(token_path).exists() {
|
||||
return Err(RegistryError::Config(format!(
|
||||
"OCI token file not found: {}",
|
||||
token_path
|
||||
@ -375,9 +409,16 @@ impl OciConfig {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read auth token from file
|
||||
/// Read auth token from file. Only call for non-vault:// paths.
|
||||
pub fn read_token(&self) -> Result<Option<String>> {
|
||||
if let Some(ref path) = self.auth_token_path {
|
||||
if path.starts_with("vault://") {
|
||||
return Err(RegistryError::Config(
|
||||
"auth_token_path is a vault:// reference — use \
|
||||
ClientFactory::create_from_config_async to resolve"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
std::fs::read_to_string(path)
|
||||
.map(|s| Some(s.trim().to_string()))
|
||||
.map_err(|e| RegistryError::Config(format!("Failed to read OCI token: {}", e)))
|
||||
@ -443,8 +484,7 @@ fn default_ttl() -> u64 {
|
||||
|
||||
/// Load configuration from file
|
||||
pub fn load_config(path: &str) -> Result<Config> {
|
||||
let contents = std::fs::read_to_string(path)
|
||||
.map_err(|e| RegistryError::Config(format!("Failed to read config file: {}", e)))?;
|
||||
let contents = std::fs::read_to_string(path).map_err(RegistryError::Io)?;
|
||||
|
||||
let config: Config = toml::from_str(&contents)
|
||||
.map_err(|e| RegistryError::Config(format!("Failed to parse config: {}", e)))?;
|
||||
|
||||
105
crates/extension-registry/src/events.rs
Normal file
105
crates/extension-registry/src/events.rs
Normal file
@ -0,0 +1,105 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::StreamExt;
|
||||
use platform_nats::NatsBridge;
|
||||
use serde::Serialize;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::cache::ExtensionCache;
|
||||
use crate::models::ExtensionType;
|
||||
|
||||
/// Publishes NATS events for extension lifecycle operations.
|
||||
pub struct EventPublisher {
|
||||
bridge: Arc<NatsBridge>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ExtensionEvent<'a> {
|
||||
name: &'a str,
|
||||
version: &'a str,
|
||||
#[serde(rename = "type")]
|
||||
extension_type: ExtensionType,
|
||||
}
|
||||
|
||||
impl EventPublisher {
|
||||
pub fn new(bridge: Arc<NatsBridge>) -> Self {
|
||||
Self { bridge }
|
||||
}
|
||||
|
||||
/// Publish `provisioning.extensions.{type}.installed`.
|
||||
///
|
||||
/// Fire-and-forget: logs error on failure but never propagates to handler.
|
||||
pub async fn publish_installed(
|
||||
&self,
|
||||
extension_type: ExtensionType,
|
||||
name: &str,
|
||||
version: &str,
|
||||
) {
|
||||
let subject = format!("extensions.{}.installed", extension_type);
|
||||
let payload = ExtensionEvent {
|
||||
name,
|
||||
version,
|
||||
extension_type,
|
||||
};
|
||||
match self.bridge.publish_json(&subject, &payload).await {
|
||||
Ok(_) => {
|
||||
info!(
|
||||
subject = %subject,
|
||||
extension = %name,
|
||||
version = %version,
|
||||
"Extension installed event published"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
subject = %subject,
|
||||
extension = %name,
|
||||
"Failed to publish extension event: {}", e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn a background task that subscribes to workspace deploy-done events
|
||||
/// and invalidates the extension cache on each notification.
|
||||
///
|
||||
/// Subject: `provisioning.workspace.*.deploy.done` (filter on WORKSPACE stream)
|
||||
pub fn spawn_cache_invalidator(bridge: Arc<NatsBridge>, cache: Arc<ExtensionCache>) {
|
||||
tokio::spawn(async move {
|
||||
run_cache_invalidator(bridge, cache).await;
|
||||
});
|
||||
}
|
||||
|
||||
async fn run_cache_invalidator(bridge: Arc<NatsBridge>, cache: Arc<ExtensionCache>) {
|
||||
const STREAM: &str = "WORKSPACE";
|
||||
const CONSUMER: &str = "ext-registry-cache-invalidator";
|
||||
|
||||
let mut messages = match bridge.subscribe_pull(STREAM, CONSUMER).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
warn!("Extension registry cache invalidator: subscribe failed — {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!("Extension registry cache invalidator running on stream {STREAM}");
|
||||
|
||||
while let Some(msg_result) = messages.next().await {
|
||||
match msg_result {
|
||||
Ok(msg) => {
|
||||
// Subject pattern: provisioning.workspace.{ws_id}.deploy.done
|
||||
// Filter is applied at JetStream level; any message here triggers invalidation.
|
||||
let subject = msg.subject.as_str();
|
||||
info!(subject = %subject, "Workspace deploy detected — invalidating extension cache");
|
||||
cache.invalidate_all();
|
||||
if let Err(e) = msg.ack().await {
|
||||
warn!("Cache invalidator: ack failed — {e}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Cache invalidator: message error — {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -24,20 +24,21 @@ pub struct GiteaClient {
|
||||
}
|
||||
|
||||
impl GiteaClient {
|
||||
/// Create new Gitea client
|
||||
pub fn new(config: &GiteaConfig) -> Result<Self> {
|
||||
/// Create new Gitea client with a pre-resolved token.
|
||||
///
|
||||
/// Token resolution (file read or vault:// fetch) is the caller's
|
||||
/// responsibility. Use `ClientFactory::create_from_config_async` for
|
||||
/// automatic resolution.
|
||||
pub fn new(config: &GiteaConfig, token: String) -> Result<Self> {
|
||||
let base_url = Url::parse(&config.url)
|
||||
.map_err(|e| RegistryError::Config(format!("Invalid Gitea URL: {}", e)))?;
|
||||
|
||||
let token = config.read_token()?;
|
||||
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(config.timeout_seconds))
|
||||
.danger_accept_invalid_certs(!config.verify_ssl)
|
||||
.build()
|
||||
.map_err(|e| RegistryError::Gitea(format!("Failed to create HTTP client: {}", e)))?;
|
||||
|
||||
// Generate backend ID from config URL and organization, or use provided ID
|
||||
let backend_id = config
|
||||
.id
|
||||
.clone()
|
||||
|
||||
@ -10,6 +10,8 @@ pub mod cache;
|
||||
pub mod client;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
#[cfg(feature = "nats")]
|
||||
pub mod events;
|
||||
pub mod gitea;
|
||||
pub mod models;
|
||||
pub mod oci;
|
||||
|
||||
@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
|
||||
use axum::Router;
|
||||
use clap::Parser;
|
||||
use extension_registry::{ExtensionRegistry, API_VERSION, DEFAULT_PORT};
|
||||
use extension_registry::{config::Config, ExtensionRegistry, API_VERSION, DEFAULT_PORT};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
mod handlers;
|
||||
@ -36,19 +36,42 @@ struct Cli {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// Initialize tracing
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::from_default_env()
|
||||
.add_directive("info".parse().unwrap()),
|
||||
)
|
||||
.init();
|
||||
|
||||
// Parse CLI arguments
|
||||
// Parse CLI arguments FIRST (so --help works before any other processing)
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Initialize centralized observability (logging, metrics, health checks)
|
||||
let _guard = observability::init_from_env("extension-registry", env!("CARGO_PKG_VERSION"))?;
|
||||
|
||||
// Check if extension-registry is enabled in deployment-mode.ncl
|
||||
if let Ok(deployment) = platform_config::load_deployment_mode() {
|
||||
if let Ok(enabled) = deployment.is_service_enabled("extension_registry") {
|
||||
if !enabled {
|
||||
tracing::warn!("⚠ Extension Registry is DISABLED in deployment-mode.ncl");
|
||||
std::process::exit(1);
|
||||
}
|
||||
tracing::info!("✓ Extension Registry is ENABLED in deployment-mode.ncl");
|
||||
}
|
||||
}
|
||||
|
||||
// Load configuration from Nickel or hierarchy
|
||||
let mut config = Config::load()?;
|
||||
|
||||
// Apply CLI overrides if provided
|
||||
if !cli.host.is_empty() && cli.host != "0.0.0.0" {
|
||||
config.server.host = cli.host.clone();
|
||||
}
|
||||
if cli.port != 0 {
|
||||
config.server.port = cli.port;
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"🔧 Loaded extension-registry configuration (host: {}, port: {})",
|
||||
config.server.host,
|
||||
config.server.port
|
||||
);
|
||||
|
||||
// Create registry service
|
||||
let addr: SocketAddr = format!("{}:{}", cli.host, cli.port).parse()?;
|
||||
let addr: SocketAddr = format!("{}:{}", config.server.host, config.server.port).parse()?;
|
||||
let registry = Arc::new(ExtensionRegistry::new(addr));
|
||||
|
||||
// Create application state
|
||||
|
||||
@ -24,17 +24,17 @@ pub struct OciClient {
|
||||
}
|
||||
|
||||
impl OciClient {
|
||||
/// Create new OCI client
|
||||
pub fn new(config: &OciConfig) -> Result<Self> {
|
||||
let auth_token = config.read_token()?;
|
||||
|
||||
/// Create new OCI client with a pre-resolved auth token.
|
||||
///
|
||||
/// Token resolution (file read or vault:// fetch) is the caller's
|
||||
/// responsibility. Pass `None` for unauthenticated registry access.
|
||||
pub fn new(config: &OciConfig, auth_token: Option<String>) -> Result<Self> {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(config.timeout_seconds))
|
||||
.danger_accept_invalid_certs(!config.verify_ssl)
|
||||
.build()
|
||||
.map_err(|e| RegistryError::Oci(format!("Failed to create HTTP client: {}", e)))?;
|
||||
|
||||
// Generate backend ID from registry and namespace, or use provided ID
|
||||
let backend_id = config
|
||||
.id
|
||||
.clone()
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use axum::body::Body;
|
||||
use axum::http::{Request, StatusCode};
|
||||
use extension_registry::{build_routes, AppState, Config};
|
||||
use extension_registry::config::OciConfig;
|
||||
use extension_registry::{build_routes, AppState, Config};
|
||||
use http_body_util::BodyExt;
|
||||
use tower::ServiceExt;
|
||||
|
||||
@ -30,7 +30,9 @@ fn create_test_config() -> Config {
|
||||
#[ignore] // Requires OCI registry service to be running
|
||||
async fn test_health_check() {
|
||||
let config = create_test_config();
|
||||
let state = AppState::new(config).expect("Failed to create app state");
|
||||
let state = AppState::new(config, None)
|
||||
.await
|
||||
.expect("Failed to create app state");
|
||||
let app = build_routes(state);
|
||||
|
||||
let response = app
|
||||
@ -57,7 +59,9 @@ async fn test_health_check() {
|
||||
#[ignore] // Requires OCI registry or Gitea service to be running
|
||||
async fn test_list_extensions_empty() {
|
||||
let config = create_test_config();
|
||||
let state = AppState::new(config).expect("Failed to create app state");
|
||||
let state = AppState::new(config, None)
|
||||
.await
|
||||
.expect("Failed to create app state");
|
||||
let app = build_routes(state);
|
||||
|
||||
let response = app
|
||||
@ -84,7 +88,9 @@ async fn test_list_extensions_empty() {
|
||||
async fn test_get_nonexistent_extension() {
|
||||
let config = create_test_config();
|
||||
|
||||
let state = AppState::new(config).expect("Failed to create app state");
|
||||
let state = AppState::new(config, None)
|
||||
.await
|
||||
.expect("Failed to create app state");
|
||||
let app = build_routes(state);
|
||||
|
||||
let response = app
|
||||
@ -105,7 +111,9 @@ async fn test_get_nonexistent_extension() {
|
||||
async fn test_metrics_endpoint() {
|
||||
let config = create_test_config();
|
||||
|
||||
let state = AppState::new(config).expect("Failed to create app state");
|
||||
let state = AppState::new(config, None)
|
||||
.await
|
||||
.expect("Failed to create app state");
|
||||
let app = build_routes(state);
|
||||
|
||||
let response = app
|
||||
@ -131,7 +139,9 @@ async fn test_metrics_endpoint() {
|
||||
async fn test_cache_stats_endpoint() {
|
||||
let config = create_test_config();
|
||||
|
||||
let state = AppState::new(config).expect("Failed to create app state");
|
||||
let state = AppState::new(config, None)
|
||||
.await
|
||||
.expect("Failed to create app state");
|
||||
let app = build_routes(state);
|
||||
|
||||
let response = app
|
||||
@ -159,7 +169,9 @@ async fn test_cache_stats_endpoint() {
|
||||
async fn test_invalid_extension_type() {
|
||||
let config = create_test_config();
|
||||
|
||||
let state = AppState::new(config).expect("Failed to create app state");
|
||||
let state = AppState::new(config, None)
|
||||
.await
|
||||
.expect("Failed to create app state");
|
||||
let app = build_routes(state);
|
||||
|
||||
let response = app
|
||||
|
||||
@ -29,6 +29,9 @@ toml = { workspace = true }
|
||||
# Platform configuration
|
||||
platform-config = { workspace = true }
|
||||
|
||||
# Centralized observability (logging, metrics, health, tracing)
|
||||
observability = { workspace = true, features = ["logging", "metrics-prometheus", "health"] }
|
||||
|
||||
# Error handling
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
@ -1,106 +1,454 @@
|
||||
//! Configuration management for the Provisioning MCP Server
|
||||
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use platform_config::ConfigLoader;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
/// MCP Server configuration
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// Path to the provisioning system
|
||||
pub provisioning_path: PathBuf,
|
||||
|
||||
/// AI provider configuration
|
||||
pub ai: AIConfig,
|
||||
|
||||
/// Server configuration
|
||||
pub server: ServerConfig,
|
||||
|
||||
/// Debug mode
|
||||
pub debug: bool,
|
||||
pub mcp_server: MCPServerSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AIConfig {
|
||||
/// Enable AI capabilities
|
||||
pub enabled: bool,
|
||||
|
||||
/// AI provider (openai, claude, generic)
|
||||
pub provider: String,
|
||||
|
||||
/// API endpoint URL
|
||||
pub api_endpoint: Option<String>,
|
||||
|
||||
/// API key (loaded from environment)
|
||||
pub api_key: Option<String>,
|
||||
|
||||
/// Model name
|
||||
pub model: Option<String>,
|
||||
|
||||
/// Maximum tokens for responses
|
||||
pub max_tokens: u32,
|
||||
|
||||
/// Temperature for creativity (0.0-1.0)
|
||||
pub temperature: f32,
|
||||
|
||||
/// Request timeout in seconds
|
||||
pub timeout: u64,
|
||||
pub struct MCPServerSettings {
|
||||
pub workspace: WorkspaceConfig,
|
||||
pub server: ServerConfig,
|
||||
pub protocol: ProtocolConfig,
|
||||
#[serde(default)]
|
||||
pub tools: Option<ToolsConfig>,
|
||||
#[serde(default)]
|
||||
pub prompts: Option<PromptsConfig>,
|
||||
#[serde(default)]
|
||||
pub resources: Option<ResourcesConfig>,
|
||||
#[serde(default)]
|
||||
pub sampling: Option<SamplingConfig>,
|
||||
#[serde(default)]
|
||||
pub capabilities: Option<CapabilitiesConfig>,
|
||||
#[serde(default)]
|
||||
pub orchestrator_integration: Option<OrchestratorIntegrationConfig>,
|
||||
#[serde(default)]
|
||||
pub control_center_integration: Option<ControlCenterIntegrationConfig>,
|
||||
#[serde(default)]
|
||||
pub security: Option<SecurityConfig>,
|
||||
#[serde(default)]
|
||||
pub monitoring: Option<MonitoringConfig>,
|
||||
#[serde(default)]
|
||||
pub logging: Option<LoggingConfig>,
|
||||
#[serde(default)]
|
||||
pub performance: Option<PerformanceConfig>,
|
||||
#[serde(default)]
|
||||
pub build: Option<DockerBuildConfig>,
|
||||
}
|
||||
|
||||
/// Workspace configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorkspaceConfig {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
#[serde(default)]
|
||||
pub metadata: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Server configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServerConfig {
|
||||
/// Server name/identifier
|
||||
pub name: String,
|
||||
|
||||
/// Server version
|
||||
pub version: String,
|
||||
|
||||
/// Enable resource capabilities
|
||||
pub enable_resources: bool,
|
||||
|
||||
/// Enable tool change notifications
|
||||
pub enable_tool_notifications: bool,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provisioning_path: PathBuf::from("/usr/local/provisioning"),
|
||||
ai: AIConfig::default(),
|
||||
server: ServerConfig::default(),
|
||||
debug: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AIConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
provider: "openai".to_string(),
|
||||
api_endpoint: None,
|
||||
api_key: None,
|
||||
model: Some("gpt-4".to_string()),
|
||||
max_tokens: 2048,
|
||||
temperature: 0.3,
|
||||
timeout: 30,
|
||||
}
|
||||
}
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
#[serde(default)]
|
||||
pub workers: usize,
|
||||
}
|
||||
|
||||
impl Default for ServerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "provisioning-server-rust".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
enable_resources: true,
|
||||
enable_tool_notifications: true,
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 3000,
|
||||
workers: 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Protocol configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProtocolConfig {
|
||||
#[serde(default = "default_version")]
|
||||
pub version: String,
|
||||
#[serde(default)]
|
||||
pub transport: Option<TransportConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TransportConfig {
|
||||
pub endpoint: Option<String>,
|
||||
pub ws_path: Option<String>,
|
||||
pub timeout: Option<u64>,
|
||||
}
|
||||
|
||||
/// Tools configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolsConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
#[serde(default = "default_max_concurrent_tools")]
|
||||
pub max_concurrent: usize,
|
||||
#[serde(default = "default_tool_timeout")]
|
||||
pub timeout: u64,
|
||||
pub categories: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub validation: Option<ValidationConfig>,
|
||||
#[serde(default)]
|
||||
pub cache: Option<CacheConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ValidationConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub strict_mode: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CacheConfig {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
pub ttl: Option<u64>,
|
||||
}
|
||||
|
||||
/// Prompts configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PromptsConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
#[serde(default = "default_max_templates")]
|
||||
pub max_templates: usize,
|
||||
#[serde(default)]
|
||||
pub cache: Option<CacheConfig>,
|
||||
#[serde(default)]
|
||||
pub versioning: Option<VersioningConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VersioningConfig {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
pub max_versions: Option<usize>,
|
||||
}
|
||||
|
||||
/// Resources configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ResourcesConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
#[serde(default = "default_max_resource_size")]
|
||||
pub max_size: u64,
|
||||
pub types: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub cache: Option<ResourceCacheConfig>,
|
||||
#[serde(default)]
|
||||
pub validation: Option<ResourceValidationConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ResourceCacheConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
pub max_size_mb: Option<u64>,
|
||||
pub ttl: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ResourceValidationConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
pub max_depth: Option<usize>,
|
||||
}
|
||||
|
||||
/// Sampling configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SamplingConfig {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
pub max_tokens: Option<u64>,
|
||||
pub model: Option<String>,
|
||||
pub temperature: Option<f32>,
|
||||
#[serde(default)]
|
||||
pub cache: Option<CacheConfig>,
|
||||
}
|
||||
|
||||
/// Capabilities configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CapabilitiesConfig {
|
||||
#[serde(default)]
|
||||
pub tools: Option<CapabilityConfig>,
|
||||
#[serde(default)]
|
||||
pub prompts: Option<CapabilityConfig>,
|
||||
#[serde(default)]
|
||||
pub resources: Option<ResourcesCapabilityConfig>,
|
||||
#[serde(default)]
|
||||
pub sampling: Option<SamplingCapabilityConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CapabilityConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub list_changed_callback: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ResourcesCapabilityConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub list_changed_callback: bool,
|
||||
#[serde(default)]
|
||||
pub subscribe: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SamplingCapabilityConfig {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// Orchestrator integration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OrchestratorIntegrationConfig {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
pub endpoint: Option<String>,
|
||||
pub token: Option<String>,
|
||||
pub workspace: Option<String>,
|
||||
}
|
||||
|
||||
/// Control Center integration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ControlCenterIntegrationConfig {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
pub endpoint: Option<String>,
|
||||
#[serde(default = "default_true")]
|
||||
pub enforce_rbac: bool,
|
||||
}
|
||||
|
||||
/// Security configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SecurityConfig {
|
||||
#[serde(default)]
|
||||
pub tls: Option<TlsConfig>,
|
||||
#[serde(default)]
|
||||
pub rbac: Option<RbacConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TlsConfig {
|
||||
pub enabled: bool,
|
||||
pub cert_path: Option<String>,
|
||||
pub key_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RbacConfig {
|
||||
pub enabled: bool,
|
||||
pub policy_file: Option<String>,
|
||||
}
|
||||
|
||||
/// Monitoring configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MonitoringConfig {
|
||||
pub enabled: bool,
|
||||
pub metrics_interval_seconds: u64,
|
||||
pub health_check_interval_seconds: u64,
|
||||
pub min_memory_mb: u64,
|
||||
pub max_cpu_percent: f64,
|
||||
}
|
||||
|
||||
impl Default for MonitoringConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
metrics_interval_seconds: 60,
|
||||
health_check_interval_seconds: 30,
|
||||
min_memory_mb: 512,
|
||||
max_cpu_percent: 80.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Logging configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LoggingConfig {
|
||||
pub level: String,
|
||||
pub format: String,
|
||||
#[serde(default)]
|
||||
pub outputs: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub file: Option<FileLoggingConfig>,
|
||||
#[serde(default)]
|
||||
pub syslog: Option<SyslogConfig>,
|
||||
#[serde(default)]
|
||||
pub fields: Option<FieldsConfig>,
|
||||
#[serde(default)]
|
||||
pub sampling: Option<LogSamplingConfig>,
|
||||
#[serde(default)]
|
||||
pub modules: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub performance: Option<PerformanceLoggingConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FileLoggingConfig {
|
||||
pub path: Option<String>,
|
||||
pub max_size: Option<u64>,
|
||||
pub max_backups: Option<usize>,
|
||||
pub max_age: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub compress: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SyslogConfig {
|
||||
pub address: Option<String>,
|
||||
pub facility: Option<String>,
|
||||
pub protocol: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FieldsConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub service_name: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub hostname: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub pid: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub timestamp: bool,
|
||||
#[serde(default)]
|
||||
pub caller: bool,
|
||||
#[serde(default)]
|
||||
pub stack_trace: bool,
|
||||
pub custom: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LogSamplingConfig {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
pub initial: Option<u64>,
|
||||
pub thereafter: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PerformanceLoggingConfig {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
pub slow_threshold: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub memory_info: bool,
|
||||
}
|
||||
|
||||
impl Default for LoggingConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
level: "info".to_string(),
|
||||
format: "text".to_string(),
|
||||
outputs: None,
|
||||
file: None,
|
||||
syslog: None,
|
||||
fields: None,
|
||||
sampling: None,
|
||||
modules: None,
|
||||
performance: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Performance configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PerformanceConfig {
|
||||
pub pool_size: Option<usize>,
|
||||
pub buffer_size: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub compression: bool,
|
||||
pub compression_level: Option<String>,
|
||||
}
|
||||
|
||||
/// Docker build configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DockerBuildConfig {
|
||||
pub base_image: String,
|
||||
#[serde(default)]
|
||||
pub build_args: HashMap<String, String>,
|
||||
}
|
||||
|
||||
// Serde defaults
|
||||
fn default_version() -> String {
|
||||
"1.0".to_string()
|
||||
}
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
fn default_max_concurrent_tools() -> usize {
|
||||
5
|
||||
}
|
||||
fn default_tool_timeout() -> u64 {
|
||||
30000
|
||||
}
|
||||
fn default_max_templates() -> usize {
|
||||
100
|
||||
}
|
||||
fn default_max_resource_size() -> u64 {
|
||||
104857600
|
||||
}
|
||||
|
||||
impl Default for MCPServerSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
workspace: WorkspaceConfig {
|
||||
name: "mcp-server".to_string(),
|
||||
path: "/tmp/mcp-server".to_string(),
|
||||
metadata: HashMap::new(),
|
||||
},
|
||||
server: ServerConfig::default(),
|
||||
protocol: ProtocolConfig {
|
||||
version: "1.0".to_string(),
|
||||
transport: None,
|
||||
},
|
||||
tools: None,
|
||||
prompts: None,
|
||||
resources: None,
|
||||
sampling: None,
|
||||
capabilities: None,
|
||||
orchestrator_integration: None,
|
||||
control_center_integration: None,
|
||||
security: None,
|
||||
monitoring: Some(MonitoringConfig::default()),
|
||||
logging: Some(LoggingConfig::default()),
|
||||
performance: None,
|
||||
build: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load configuration from Nickel mcp-server.ncl
|
||||
pub fn load() -> Result<Self> {
|
||||
let config_json = platform_config::load_service_config_from_ncl("mcp-server")
|
||||
.context("Failed to load mcp-server configuration from Nickel")?;
|
||||
|
||||
let config: Config = serde_json::from_value(config_json)
|
||||
.context("Failed to deserialize mcp-server configuration")?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigLoader for Config {
|
||||
fn service_name() -> &'static str {
|
||||
"mcp-server"
|
||||
@ -108,25 +456,27 @@ impl ConfigLoader for Config {
|
||||
|
||||
fn load_from_hierarchy() -> std::result::Result<Self, Box<dyn std::error::Error + Send + Sync>>
|
||||
{
|
||||
// Use platform-config's hierarchy resolution
|
||||
let service = Self::service_name();
|
||||
|
||||
if let Some(path) = platform_config::resolve_config_path(service) {
|
||||
return Self::from_path(&path);
|
||||
}
|
||||
|
||||
// Fallback to defaults
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
fn apply_env_overrides(
|
||||
&mut self,
|
||||
) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
self.load_from_env().map_err(|e| {
|
||||
// Convert anyhow::Error to Box<dyn Error>
|
||||
let err_msg = format!("{}", e);
|
||||
Box::new(std::io::Error::other(err_msg)) as Box<dyn std::error::Error + Send + Sync>
|
||||
})
|
||||
if let Ok(host) = std::env::var("MCP_SERVER_HOST") {
|
||||
self.mcp_server.server.host = host;
|
||||
}
|
||||
if let Ok(port) = std::env::var("MCP_SERVER_PORT") {
|
||||
if let Ok(p) = port.parse() {
|
||||
self.mcp_server.server.port = p;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn from_path<P: AsRef<Path>>(
|
||||
@ -139,7 +489,10 @@ impl ConfigLoader for Config {
|
||||
})?;
|
||||
|
||||
serde_json::from_value(json_value).map_err(|e| {
|
||||
let err_msg = format!("Failed to deserialize config from {:?}: {}", path, e);
|
||||
let err_msg = format!(
|
||||
"Failed to deserialize mcp-server config from {:?}: {}",
|
||||
path, e
|
||||
);
|
||||
Box::new(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
err_msg,
|
||||
@ -148,142 +501,14 @@ impl ConfigLoader for Config {
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load configuration with hierarchical fallback logic:
|
||||
/// 1. Explicit config path (parameter or MCP_SERVER_CONFIG env var)
|
||||
/// 2. Mode-specific config:
|
||||
/// provisioning/platform/config/mcp-server.{mode}.ncl or .toml
|
||||
/// 3. Built-in defaults
|
||||
///
|
||||
/// Then environment variables override specific fields.
|
||||
pub fn load(
|
||||
config_path: Option<PathBuf>,
|
||||
provisioning_path: Option<PathBuf>,
|
||||
debug: bool,
|
||||
) -> Result<Self> {
|
||||
let mut config = if let Some(path) = config_path {
|
||||
Self::from_path(&path)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to load from path: {}", e))?
|
||||
} else {
|
||||
<Self as ConfigLoader>::load()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to load config: {}", e))?
|
||||
};
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Override with command line arguments
|
||||
if let Some(path) = provisioning_path {
|
||||
config.provisioning_path = path;
|
||||
}
|
||||
config.debug = debug;
|
||||
|
||||
// Validate configuration
|
||||
config.validate()?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Load configuration from file (legacy wrapper for compatibility)
|
||||
fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
Self::from_path(&path).map_err(|e| anyhow::anyhow!("Failed to load from file: {}", e))
|
||||
}
|
||||
|
||||
/// Load configuration from environment variables
|
||||
fn load_from_env(&mut self) -> Result<()> {
|
||||
// Provisioning path
|
||||
if let Ok(path) = env::var("PROVISIONING_PATH") {
|
||||
self.provisioning_path = PathBuf::from(path);
|
||||
}
|
||||
|
||||
// AI configuration
|
||||
if let Ok(enabled) = env::var("PROVISIONING_AI_ENABLED") {
|
||||
self.ai.enabled = enabled.parse().unwrap_or(true);
|
||||
}
|
||||
|
||||
if let Ok(provider) = env::var("PROVISIONING_AI_PROVIDER") {
|
||||
self.ai.provider = provider;
|
||||
}
|
||||
|
||||
if let Ok(endpoint) = env::var("PROVISIONING_AI_ENDPOINT") {
|
||||
self.ai.api_endpoint = Some(endpoint);
|
||||
}
|
||||
|
||||
// Load API keys from environment
|
||||
self.ai.api_key = match self.ai.provider.as_str() {
|
||||
"openai" => env::var("OPENAI_API_KEY").ok(),
|
||||
"claude" => env::var("ANTHROPIC_API_KEY").ok(),
|
||||
"generic" => env::var("LLM_API_KEY").ok(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Ok(model) = env::var("PROVISIONING_AI_MODEL") {
|
||||
self.ai.model = Some(model);
|
||||
}
|
||||
|
||||
if let Ok(max_tokens) = env::var("PROVISIONING_AI_MAX_TOKENS") {
|
||||
self.ai.max_tokens = max_tokens.parse().unwrap_or(2048);
|
||||
}
|
||||
|
||||
if let Ok(temperature) = env::var("PROVISIONING_AI_TEMPERATURE") {
|
||||
self.ai.temperature = temperature.parse().unwrap_or(0.3);
|
||||
}
|
||||
|
||||
if let Ok(timeout) = env::var("PROVISIONING_AI_TIMEOUT") {
|
||||
self.ai.timeout = timeout.parse().unwrap_or(30);
|
||||
}
|
||||
|
||||
// Debug mode
|
||||
if let Ok(debug) = env::var("PROVISIONING_DEBUG") {
|
||||
self.debug = debug.parse().unwrap_or(false);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate the configuration
|
||||
fn validate(&self) -> Result<()> {
|
||||
// Validate provisioning path exists
|
||||
if !self.provisioning_path.exists() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Provisioning path does not exist: {}",
|
||||
self.provisioning_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
// Check if the main provisioning script exists
|
||||
let provisioning_script = self.provisioning_path.join("core/nulib/provisioning");
|
||||
if !provisioning_script.exists() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Provisioning script not found: {}",
|
||||
provisioning_script.display()
|
||||
));
|
||||
}
|
||||
|
||||
// Validate AI configuration if enabled
|
||||
if self.ai.enabled {
|
||||
if self.ai.api_key.is_none() {
|
||||
tracing::warn!(
|
||||
"AI is enabled but no API key found for provider: {}",
|
||||
self.ai.provider
|
||||
);
|
||||
}
|
||||
|
||||
if self.ai.temperature < 0.0 || self.ai.temperature > 1.0 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"AI temperature must be between 0.0 and 1.0, got: {}",
|
||||
self.ai.temperature
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the provisioning command path
|
||||
pub fn provisioning_command(&self) -> PathBuf {
|
||||
self.provisioning_path.join("core/nulib/provisioning")
|
||||
}
|
||||
|
||||
/// Check if AI is available
|
||||
pub fn is_ai_available(&self) -> bool {
|
||||
self.ai.enabled && self.ai.api_key.is_some()
|
||||
#[test]
|
||||
fn test_default_config() {
|
||||
let config = Config::default();
|
||||
assert_eq!(config.mcp_server.server.port, 3000);
|
||||
assert_eq!(config.mcp_server.protocol.version, "1.0");
|
||||
}
|
||||
}
|
||||
|
||||
@ -233,14 +233,14 @@ impl ProvisioningEngine {
|
||||
fn execute_provisioning_command(&self, args: &[String]) -> Result<String> {
|
||||
debug!("Executing command: {:?}", args);
|
||||
|
||||
let cmd_path = self.config.provisioning_command();
|
||||
let cmd_path = "provisioning"; // Use default command name
|
||||
|
||||
let output = std::process::Command::new(&cmd_path)
|
||||
let output = std::process::Command::new(cmd_path)
|
||||
.args(args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.with_context(|| format!("Failed to execute command: {}", cmd_path.display()))?;
|
||||
.with_context(|| format!("Failed to execute command: {}", cmd_path))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -10,13 +10,27 @@ use serde_json::{json, Value};
|
||||
pub struct ProvisioningTools {
|
||||
client: Client,
|
||||
api_base_url: String,
|
||||
api_port: u16,
|
||||
dashboard_port: u16,
|
||||
}
|
||||
|
||||
impl ProvisioningTools {
|
||||
pub fn new(api_base_url: Option<String>) -> Self {
|
||||
Self::with_ports(api_base_url, None, None)
|
||||
}
|
||||
|
||||
pub fn with_ports(
|
||||
api_base_url: Option<String>,
|
||||
api_port: Option<u16>,
|
||||
dashboard_port: Option<u16>,
|
||||
) -> Self {
|
||||
let api_port = api_port.unwrap_or(3000);
|
||||
let dashboard_port = dashboard_port.unwrap_or(8080);
|
||||
Self {
|
||||
client: Client::new(),
|
||||
api_base_url: api_base_url.unwrap_or_else(|| "http://localhost:3000".to_string()),
|
||||
api_base_url: api_base_url.unwrap_or_else(|| format!("http://localhost:{}", api_port)),
|
||||
api_port,
|
||||
dashboard_port,
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,7 +177,7 @@ impl ProvisioningTools {
|
||||
|
||||
/// Start API server
|
||||
pub async fn start_api_server(&self, port: Option<u16>) -> Result<Value> {
|
||||
let port = port.unwrap_or(3000);
|
||||
let port = port.unwrap_or(self.api_port);
|
||||
let port_str = port.to_string();
|
||||
|
||||
self.execute_provisioning_command(&["api", "start", "--port", &port_str, "--background"])
|
||||
@ -210,7 +224,7 @@ impl ProvisioningTools {
|
||||
|
||||
/// Start existing dashboard
|
||||
pub async fn start_dashboard(&self, name: &str, port: Option<u16>) -> Result<Value> {
|
||||
let port = port.unwrap_or(8080);
|
||||
let port = port.unwrap_or(self.dashboard_port);
|
||||
let port_str = port.to_string();
|
||||
|
||||
self.execute_provisioning_command(&["dashboard", "start", name, &port_str])
|
||||
|
||||
@ -49,6 +49,11 @@ bollard = { workspace = true }
|
||||
|
||||
# HTTP client for DNS/OCI/services
|
||||
reqwest = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
git2 = { workspace = true, optional = true }
|
||||
parking_lot = { workspace = true }
|
||||
|
||||
# HTTP service clients (machines, init, AI) - enables remote service calls
|
||||
service-clients = { workspace = true }
|
||||
@ -56,6 +61,9 @@ service-clients = { workspace = true }
|
||||
# Platform configuration management
|
||||
platform-config = { workspace = true }
|
||||
|
||||
# Centralized observability (logging, metrics, health, tracing)
|
||||
observability = { workspace = true, features = ["logging", "metrics-prometheus", "health"] }
|
||||
|
||||
# LRU cache for OCI manifests
|
||||
lru = { workspace = true }
|
||||
|
||||
@ -94,6 +102,10 @@ shellexpand = { workspace = true }
|
||||
# SurrealDB storage backend (optional)
|
||||
surrealdb = { workspace = true, optional = true }
|
||||
|
||||
# Platform shared crates
|
||||
platform-nats = { workspace = true, optional = true }
|
||||
platform-db = { workspace = true, optional = true }
|
||||
|
||||
# ============================================================================
|
||||
# FEATURES - Module Organization for Coupling Reduction
|
||||
# ============================================================================
|
||||
@ -141,6 +153,12 @@ http-api = ["core"]
|
||||
# SurrealDB: Optional storage backend
|
||||
surrealdb = ["dep:surrealdb"]
|
||||
|
||||
# NATS event bus integration
|
||||
nats = ["dep:platform-nats", "dep:platform-db"]
|
||||
|
||||
# GitOps webhook handler (requires git2)
|
||||
gitops = ["dep:git2"]
|
||||
|
||||
# Default: All features enabled
|
||||
default = [
|
||||
"core",
|
||||
|
||||
@ -45,7 +45,7 @@ RUN cargo install cargo-chef --version 0.1.67
|
||||
COPY --from=planner /workspace/recipe.json recipe.json
|
||||
|
||||
# Build dependencies - This layer will be cached
|
||||
RUN cargo chef cook --release --recipe-path recipe.json
|
||||
RUN cargo chef cook --release --recipe-path recipe.json
|
||||
|
||||
# ============================================================================
|
||||
# Stage 3: BUILDER - Build source code
|
||||
@ -76,7 +76,7 @@ COPY stratumiops ./stratumiops
|
||||
|
||||
# Build release binary with parallelism
|
||||
ENV CARGO_BUILD_JOBS=4
|
||||
RUN cargo build --release --package provisioning-orchestrator
|
||||
RUN cargo build --release --package provisioning-orchestrator
|
||||
|
||||
# ============================================================================
|
||||
# Stage 4: RUNTIME - Minimal runtime image
|
||||
|
||||
@ -97,6 +97,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_default_builder_name() {
|
||||
let args = Args {
|
||||
config: None,
|
||||
config_dir: None,
|
||||
mode: None,
|
||||
port: 9090,
|
||||
data_dir: "./data".to_string(),
|
||||
storage_type: "filesystem".to_string(),
|
||||
|
||||
108
crates/orchestrator/src/audit/collector.rs
Normal file
108
crates/orchestrator/src/audit/collector.rs
Normal file
@ -0,0 +1,108 @@
|
||||
//! Global audit event collector.
|
||||
//!
|
||||
//! Subscribes to `provisioning.audit.>` via JetStream durable consumer,
|
||||
//! buffers events, and batch-inserts into SurrealDB `audit:events`.
|
||||
//! Flush triggers: 100 events accumulated OR 1-second timer, whichever first.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::StreamExt;
|
||||
use platform_db::SurrealPool;
|
||||
use platform_nats::NatsBridge;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::time;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
const BATCH_SIZE: usize = 100;
|
||||
const FLUSH_INTERVAL: Duration = Duration::from_secs(1);
|
||||
const STREAM_NAME: &str = "AUDIT";
|
||||
const CONSUMER_NAME: &str = "audit-collector";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AuditRecord {
|
||||
pub source_service: String,
|
||||
pub event_type: String,
|
||||
pub actor: Option<String>,
|
||||
pub target: Option<String>,
|
||||
pub session_id: Option<String>,
|
||||
pub workspace_id: Option<String>,
|
||||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||
pub payload: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Runs the audit collector loop until the process shuts down.
|
||||
///
|
||||
/// Intended to be spawned as a background `tokio::task`.
|
||||
pub async fn run_audit_collector(nats: Arc<NatsBridge>, db: Arc<SurrealPool>) {
|
||||
let mut ticker = time::interval(FLUSH_INTERVAL);
|
||||
let mut buffer: Vec<AuditRecord> = Vec::with_capacity(BATCH_SIZE);
|
||||
|
||||
let mut messages = match nats.subscribe_pull(STREAM_NAME, CONSUMER_NAME).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("audit collector: failed to subscribe — {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!("audit collector running (batch={BATCH_SIZE}, flush=1s)");
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = messages.next() => {
|
||||
match msg {
|
||||
Some(Ok(m)) => {
|
||||
match serde_json::from_slice::<AuditRecord>(&m.payload) {
|
||||
Ok(event) => {
|
||||
buffer.push(event);
|
||||
if buffer.len() >= BATCH_SIZE {
|
||||
let batch = std::mem::take(&mut buffer);
|
||||
flush_batch(batch, Arc::clone(&db)).await;
|
||||
}
|
||||
if let Err(e) = m.ack().await {
|
||||
warn!("audit collector: ack failed — {e}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("audit collector: deserialize failed — {e}");
|
||||
let _ = m.ack().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
error!("audit collector: message error — {e}");
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
_ = ticker.tick() => {
|
||||
if !buffer.is_empty() {
|
||||
let batch = std::mem::take(&mut buffer);
|
||||
flush_batch(batch, Arc::clone(&db)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn flush_batch(events: Vec<AuditRecord>, db: Arc<SurrealPool>) {
|
||||
let surreal = db.db();
|
||||
if let Err(e) = surreal.query("USE NS audit DB provisioning").await {
|
||||
error!("audit flush: USE NS failed — {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
let count = events.len();
|
||||
for event in events {
|
||||
if let Err(e) = surreal
|
||||
.create::<Option<AuditRecord>>("events")
|
||||
.content(event)
|
||||
.await
|
||||
{
|
||||
error!("audit flush: INSERT failed — {e}");
|
||||
}
|
||||
}
|
||||
|
||||
info!(count = count, "audit events flushed to SurrealDB");
|
||||
}
|
||||
@ -35,6 +35,8 @@
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
#[cfg(feature = "nats")]
|
||||
pub mod collector;
|
||||
pub mod logger;
|
||||
pub mod storage;
|
||||
pub mod types;
|
||||
|
||||
@ -279,6 +279,14 @@ pub enum ActionType {
|
||||
SystemBackup,
|
||||
SystemRestore,
|
||||
|
||||
// Workspace operations
|
||||
WorkspaceCreate,
|
||||
WorkspaceDelete,
|
||||
WorkspaceUpdate,
|
||||
WorkspaceSwitch,
|
||||
WorkspaceList,
|
||||
WorkspaceSync,
|
||||
|
||||
// Unknown/Custom
|
||||
Unknown,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -47,6 +47,12 @@ pub struct CreateServerWorkflow {
|
||||
pub servers: Vec<String>,
|
||||
pub check_mode: bool,
|
||||
pub wait: bool,
|
||||
// Rendered and compressed script prepared by CLI
|
||||
// If present, orchestrator executes this script directly without constructing commands
|
||||
#[serde(default)]
|
||||
pub script_compressed: Option<String>,
|
||||
#[serde(default)]
|
||||
pub script_encoding: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -85,20 +91,43 @@ pub fn validate_storage_type(s: &str) -> Result<String, String> {
|
||||
|
||||
// CLI arguments structure
|
||||
#[derive(clap::Parser, Clone)]
|
||||
#[command(author, version, about = "Multi-service task orchestration and batch workflow engine")]
|
||||
#[command(long_about = "Orchestrator - Manages distributed task execution, batch workflows, and cluster provisioning with state management and rollback recovery")]
|
||||
#[command(after_help = "CONFIGURATION HIERARCHY (highest to lowest priority):\n 1. CLI: -c/--config <path> (explicit file)\n 2. CLI: --config-dir <dir> --mode <mode> (directory + mode)\n 3. CLI: --config-dir <dir> (searches for orchestrator.ncl|toml|json)\n 4. CLI: --mode <mode> (searches in provisioning/platform/config/)\n 5. ENV: ORCHESTRATOR_CONFIG (explicit file)\n 6. ENV: PROVISIONING_CONFIG_DIR (searches for orchestrator.ncl|toml|json)\n 7. ENV: ORCHESTRATOR_MODE (mode-based in default path)\n 8. Built-in defaults\n\nEXAMPLES:\n # Explicit config file\n orchestrator -c ~/my-config.toml\n\n # Config directory with mode\n orchestrator --config-dir ~/configs --mode enterprise\n\n # Config directory (auto-discover file)\n orchestrator --config-dir ~/.config/provisioning\n\n # Via environment variables\n export ORCHESTRATOR_CONFIG=~/.config/orchestrator.toml\n orchestrator\n\n # Mode-based configuration\n orchestrator --mode solo")]
|
||||
#[command(
|
||||
author,
|
||||
version,
|
||||
about = "Multi-service task orchestration and batch workflow engine"
|
||||
)]
|
||||
#[command(
|
||||
long_about = "Orchestrator - Manages distributed task execution, batch workflows, and cluster \
|
||||
provisioning with state management and rollback recovery"
|
||||
)]
|
||||
#[command(
|
||||
after_help = "CONFIGURATION HIERARCHY (highest to lowest priority):\n 1. CLI: -c/--config \
|
||||
<path> (explicit file)\n 2. CLI: --config-dir <dir> --mode <mode> (directory + \
|
||||
mode)\n 3. CLI: --config-dir <dir> (searches for orchestrator.ncl|toml|json)\n \
|
||||
4. CLI: --mode <mode> (searches in provisioning/platform/config/)\n 5. ENV: \
|
||||
ORCHESTRATOR_CONFIG (explicit file)\n 6. ENV: PROVISIONING_CONFIG_DIR \
|
||||
(searches for orchestrator.ncl|toml|json)\n 7. ENV: ORCHESTRATOR_MODE \
|
||||
(mode-based in default path)\n 8. Built-in defaults\n\nEXAMPLES:\n # Explicit \
|
||||
config file\n orchestrator -c ~/my-config.toml\n\n # Config directory with \
|
||||
mode\n orchestrator --config-dir ~/configs --mode enterprise\n\n # Config \
|
||||
directory (auto-discover file)\n orchestrator --config-dir \
|
||||
~/.config/provisioning\n\n # Via environment variables\n export \
|
||||
ORCHESTRATOR_CONFIG=~/.config/orchestrator.toml\n orchestrator\n\n # \
|
||||
Mode-based configuration\n orchestrator --mode solo"
|
||||
)]
|
||||
pub struct Args {
|
||||
/// Configuration file path (highest priority)
|
||||
///
|
||||
/// Accepts absolute or relative path. Supports .ncl, .toml, and .json formats.
|
||||
/// Accepts absolute or relative path. Supports .ncl, .toml, and .json
|
||||
/// formats.
|
||||
#[arg(short = 'c', long, env = "ORCHESTRATOR_CONFIG")]
|
||||
pub config: Option<std::path::PathBuf>,
|
||||
|
||||
/// Configuration directory (searches for orchestrator.ncl|toml|json)
|
||||
///
|
||||
/// Searches for configuration files in order of preference: .ncl > .toml > .json
|
||||
/// Can also search for mode-specific files: orchestrator.{mode}.{ncl|toml|json}
|
||||
/// Searches for configuration files in order of preference: .ncl > .toml >
|
||||
/// .json Can also search for mode-specific files:
|
||||
/// orchestrator.{mode}.{ncl|toml|json}
|
||||
#[arg(long, env = "PROVISIONING_CONFIG_DIR")]
|
||||
pub config_dir: Option<std::path::PathBuf>,
|
||||
|
||||
@ -109,9 +138,9 @@ pub struct Args {
|
||||
#[arg(short = 'm', long, env = "ORCHESTRATOR_MODE")]
|
||||
pub mode: Option<String>,
|
||||
|
||||
/// Port to listen on
|
||||
#[arg(short = 'p', long, default_value = "9090")]
|
||||
pub port: u16,
|
||||
/// Port to listen on (overrides config if specified)
|
||||
#[arg(short = 'p', long)]
|
||||
pub port: Option<u16>,
|
||||
|
||||
/// Data directory for storage
|
||||
#[arg(short = 'd', long, default_value = "./data")]
|
||||
@ -203,6 +232,9 @@ pub mod break_glass;
|
||||
#[cfg(feature = "compliance")]
|
||||
pub mod compliance;
|
||||
|
||||
// GitOps: Webhook handler and git pull executor
|
||||
pub mod webhooks;
|
||||
|
||||
// Platform: Infrastructure integration
|
||||
#[cfg(feature = "platform")]
|
||||
pub mod dns;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
@ -9,11 +10,13 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use clap::Parser;
|
||||
use platform_config::{load_deployment_mode, PlatformStartup};
|
||||
// Use types from the library
|
||||
use provisioning_orchestrator::{
|
||||
audit::{AuditEvent, AuditFilter, AuditQuery, RetentionPolicy, SiemFormat},
|
||||
batch::{BatchOperationRequest, BatchOperationResult},
|
||||
compliance_routes,
|
||||
config::OrchestratorConfig,
|
||||
monitor::{MonitoringEvent, MonitoringEventType, SystemHealthStatus},
|
||||
rollback::{Checkpoint, RollbackResult, RollbackStatistics},
|
||||
state::{ProgressInfo, StateManagerStatistics, StateSnapshot, SystemMetrics},
|
||||
@ -21,6 +24,7 @@ use provisioning_orchestrator::{
|
||||
CreateTestEnvironmentRequest, RunTestRequest, TestEnvironment, TestEnvironmentResponse,
|
||||
TestResult,
|
||||
},
|
||||
webhooks::{handle_webhook, WebhookState, WorkspaceRegistry},
|
||||
workflow::WorkflowExecutionState,
|
||||
AppState, Args, ClusterWorkflow, CreateServerWorkflow, SharedState, TaskStatus,
|
||||
TaskservWorkflow, WorkflowTask,
|
||||
@ -63,45 +67,103 @@ async fn create_server_workflow(
|
||||
) -> Result<Json<ApiResponse<String>>, StatusCode> {
|
||||
let task_id = Uuid::new_v4().to_string();
|
||||
|
||||
let task = WorkflowTask {
|
||||
id: task_id.clone(),
|
||||
name: "create_servers".to_string(),
|
||||
command: format!("{} servers create", state.args.provisioning_path),
|
||||
args: vec![
|
||||
// PROPER ARCHITECTURE: CLI renders script, orchestrator executes it
|
||||
// If script_compressed is provided: execute it (that's ALL the orchestrator
|
||||
// does) If NOT provided: error (legacy mode should not happen)
|
||||
let task = if let Some(ref script_compressed) = workflow.script_compressed {
|
||||
// CLI has provided the COMPLETE SCRIPT ready to execute
|
||||
// No command construction, no decision logic
|
||||
// Just: decompress -> execute
|
||||
|
||||
// Store script in temp file for execution
|
||||
let script_file = format!("/tmp/orchestrator_script_{}.tar.gz.b64", task_id);
|
||||
std::fs::write(&script_file, script_compressed).ok();
|
||||
|
||||
WorkflowTask {
|
||||
id: task_id.clone(),
|
||||
name: if workflow.servers.is_empty() {
|
||||
"execute_servers_script_all".to_string()
|
||||
} else {
|
||||
format!("execute_servers_script_{}", workflow.servers.join("_"))
|
||||
},
|
||||
// Execute the decompressed script directly
|
||||
command: "bash".to_string(),
|
||||
args: vec![
|
||||
"-c".to_string(),
|
||||
// Decompress: base64 decode -> gunzip -> extract script.sh -> execute
|
||||
// CRITICAL: Use '+x' to DISABLE debug mode and prevent credential exposure
|
||||
// Even if script contains 'set -x', it won't execute with +x flag
|
||||
format!(
|
||||
"base64 -d < {} | gunzip | tar -xOf - script.sh | bash +x",
|
||||
script_file
|
||||
),
|
||||
],
|
||||
dependencies: vec![],
|
||||
status: TaskStatus::Pending,
|
||||
created_at: chrono::Utc::now(),
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
output: None,
|
||||
error: None,
|
||||
}
|
||||
} else {
|
||||
// LEGACY: Construct command from parameters (deprecated)
|
||||
let mut args = vec![
|
||||
"--infra".to_string(),
|
||||
workflow.infra.clone(),
|
||||
"--settings".to_string(),
|
||||
workflow.settings.clone(),
|
||||
if workflow.check_mode {
|
||||
"--check".to_string()
|
||||
];
|
||||
|
||||
for server in &workflow.servers {
|
||||
args.push(server.clone());
|
||||
}
|
||||
|
||||
if workflow.check_mode {
|
||||
args.push("--check".to_string());
|
||||
}
|
||||
if workflow.wait {
|
||||
args.push("--wait".to_string());
|
||||
}
|
||||
|
||||
WorkflowTask {
|
||||
id: task_id.clone(),
|
||||
name: if workflow.servers.is_empty() {
|
||||
"create_servers_all".to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
format!("create_servers_{}", workflow.servers.join("_"))
|
||||
},
|
||||
if workflow.wait {
|
||||
"--wait".to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect(),
|
||||
dependencies: vec![],
|
||||
status: TaskStatus::Pending,
|
||||
created_at: chrono::Utc::now(),
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
output: None,
|
||||
error: None,
|
||||
command: format!("{} servers create", state.args.provisioning_path),
|
||||
args,
|
||||
dependencies: vec![],
|
||||
status: TaskStatus::Pending,
|
||||
created_at: chrono::Utc::now(),
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
output: None,
|
||||
error: None,
|
||||
}
|
||||
};
|
||||
|
||||
let server_summary = if workflow.servers.is_empty() {
|
||||
"all servers".to_string()
|
||||
} else {
|
||||
format!("{} server(s)", workflow.servers.len())
|
||||
};
|
||||
|
||||
match state.task_storage.enqueue(task, 5).await {
|
||||
Ok(()) => {
|
||||
info!("Enqueued server creation workflow: {}", task_id);
|
||||
info!(
|
||||
"Enqueued server creation workflow ({}): {} | infra: {}",
|
||||
server_summary, task_id, workflow.infra
|
||||
);
|
||||
Ok(Json(ApiResponse::success(task_id)))
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to enqueue task: {}", e);
|
||||
error!(
|
||||
"Failed to enqueue server creation task ({}): {}",
|
||||
server_summary, e
|
||||
);
|
||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
@ -885,6 +947,11 @@ async fn process_tasks(state: SharedState) {
|
||||
continue;
|
||||
}
|
||||
|
||||
#[cfg(feature = "nats")]
|
||||
state
|
||||
.publish_task_status(&task.id, "running", Some(0), None)
|
||||
.await;
|
||||
|
||||
info!("Processing task: {} ({})", task.id, task.name);
|
||||
|
||||
let task_start = std::time::Instant::now();
|
||||
@ -901,6 +968,11 @@ async fn process_tasks(state: SharedState) {
|
||||
task.status = TaskStatus::Completed;
|
||||
task.completed_at = Some(chrono::Utc::now());
|
||||
|
||||
#[cfg(feature = "nats")]
|
||||
state
|
||||
.publish_task_status(&task.id, "completed", Some(100), None)
|
||||
.await;
|
||||
|
||||
// Record metrics
|
||||
metrics_collector.record_task_completion(task_duration.as_millis() as u64);
|
||||
|
||||
@ -939,6 +1011,11 @@ async fn process_tasks(state: SharedState) {
|
||||
task.status = TaskStatus::Failed;
|
||||
task.completed_at = Some(chrono::Utc::now());
|
||||
|
||||
#[cfg(feature = "nats")]
|
||||
state
|
||||
.publish_task_status(&task.id, "failed", None, Some(&e.to_string()))
|
||||
.await;
|
||||
|
||||
// Record metrics
|
||||
metrics_collector.record_task_failure();
|
||||
|
||||
@ -988,17 +1065,134 @@ async fn process_tasks(state: SharedState) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Solo mode helpers: spawn nats-server child process and wait for readiness
|
||||
#[cfg(feature = "nats")]
|
||||
mod solo_nats {
|
||||
use anyhow::{Context, Result};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::process::Command;
|
||||
use tokio::time::{timeout, Duration, Instant};
|
||||
use tracing::info;
|
||||
|
||||
/// Spawn `nats-server` as a child process with JetStream enabled.
|
||||
///
|
||||
/// The returned `Child` holds the process alive; drop it to kill the
|
||||
/// server. Uses `kill_on_drop(true)` so the process is cleaned up when
|
||||
/// `Child` is dropped.
|
||||
pub async fn spawn_nats_server(data_dir: &str) -> Result<tokio::process::Child> {
|
||||
let nats_store_dir = format!("{}/nats", data_dir);
|
||||
std::fs::create_dir_all(&nats_store_dir)
|
||||
.context("Failed to create NATS storage directory")?;
|
||||
|
||||
let child = Command::new("nats-server")
|
||||
.args(["-js", "-sd", &nats_store_dir, "-p", "4222"])
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
.context("Failed to spawn nats-server — ensure nats-server is in PATH")?;
|
||||
|
||||
wait_for_nats(4222).await?;
|
||||
info!("✓ NATS server (solo mode) ready on port 4222");
|
||||
Ok(child)
|
||||
}
|
||||
|
||||
/// Attempt TCP connect to 127.0.0.1:{port} in a loop until ready or
|
||||
/// timeout.
|
||||
async fn wait_for_nats(port: u16) -> Result<()> {
|
||||
let addr = format!("127.0.0.1:{}", port);
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
loop {
|
||||
if Instant::now() > deadline {
|
||||
return Err(anyhow::anyhow!(
|
||||
"NATS server did not become ready within 10 seconds on port {}",
|
||||
port
|
||||
));
|
||||
}
|
||||
if timeout(Duration::from_millis(200), TcpStream::connect(&addr))
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Parse CLI arguments FIRST (so --help works before any other processing)
|
||||
let args = Args::parse();
|
||||
let port = args.port;
|
||||
|
||||
info!("Starting provisioning orchestrator on port {}", port);
|
||||
// Initialize centralized observability (logging, metrics, health checks)
|
||||
let _guard = observability::init_from_env("orchestrator", env!("CARGO_PKG_VERSION"))
|
||||
.context("Failed to initialize observability")?;
|
||||
|
||||
// Initialize platform startup manager
|
||||
let deployment = load_deployment_mode().context("Failed to load deployment-mode.ncl")?;
|
||||
|
||||
// Check if orchestrator is enabled
|
||||
if !deployment.is_service_enabled("orchestrator")? {
|
||||
warn!("⚠ Orchestrator is DISABLED in deployment-mode.ncl");
|
||||
std::process::exit(1);
|
||||
}
|
||||
info!("✓ Orchestrator is ENABLED in deployment-mode.ncl");
|
||||
|
||||
// Validate dependencies
|
||||
let startup = PlatformStartup::new(&deployment.config)
|
||||
.context("Failed to initialize platform startup")?;
|
||||
startup
|
||||
.validate_dependencies("orchestrator")
|
||||
.context("Failed to validate orchestrator dependencies")?;
|
||||
|
||||
// Setup Git repositories
|
||||
let (_schemas_path, _configs_path) = startup
|
||||
.setup_git_repos()
|
||||
.context("Failed to setup Git repositories")?;
|
||||
|
||||
// Load orchestrator configuration from Nickel
|
||||
let config = OrchestratorConfig::load().context("Failed to load orchestrator configuration")?;
|
||||
|
||||
// Apply CLI overrides if provided
|
||||
let mut config = config;
|
||||
config.apply_cli_overrides(&args);
|
||||
|
||||
let port = config.orchestrator.server.port;
|
||||
|
||||
info!(
|
||||
"🔧 Loaded orchestrator configuration from NCL, binding to port {}",
|
||||
port
|
||||
);
|
||||
|
||||
// Solo mode: spawn embedded NATS server before connecting as client
|
||||
// The child is kept alive for the entire lifetime of main()
|
||||
#[cfg(feature = "nats")]
|
||||
let _solo_nats_child = if args.mode.as_deref() == Some("solo") {
|
||||
info!("Solo mode: starting embedded NATS server");
|
||||
Some(solo_nats::spawn_nats_server(&args.data_dir).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let state = Arc::new(AppState::new(args).await?);
|
||||
|
||||
// Build webhook state with empty registry (workspaces registered via API or
|
||||
// config)
|
||||
let webhook_state = Arc::new(WebhookState {
|
||||
registry: Arc::new(parking_lot::RwLock::new(WorkspaceRegistry::new())),
|
||||
});
|
||||
|
||||
// Start audit collector (NATS → SurrealDB)
|
||||
#[cfg(feature = "nats")]
|
||||
{
|
||||
use provisioning_orchestrator::audit::collector::run_audit_collector;
|
||||
let nats = Arc::clone(&state.nats);
|
||||
let db = Arc::clone(&state.db);
|
||||
tokio::spawn(async move {
|
||||
run_audit_collector(nats, db).await;
|
||||
});
|
||||
info!("✓ NATS audit collector started");
|
||||
}
|
||||
|
||||
// Start task processor
|
||||
let processor_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
@ -1071,6 +1265,11 @@ async fn main() -> Result<()> {
|
||||
)
|
||||
// Merge monitoring routes (includes /metrics, /ws, /events)
|
||||
.merge(state.monitoring_system.create_routes())
|
||||
// Webhook handler (separate state — workspace registry)
|
||||
.route(
|
||||
"/api/v1/webhooks/:workspace_id",
|
||||
post(handle_webhook).with_state(webhook_state),
|
||||
)
|
||||
.layer(CorsLayer::permissive())
|
||||
.with_state(state);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user