22 KiB
daemon-cli Architecture
daemon-cli is the orchestration hub of the prov-ecosystem, providing unified daemon and CLI interfaces for all ecosystem services.
Table of Contents
- Architecture Overview
- Stable API Contracts
- Module Organization
- Core Concepts
- Extension Points
- Best Practices
- Integration Examples
Architecture Overview
Design Pattern: Plugin + Hub Architecture
┌─────────────────────────────────────────────────────────┐
│ daemon-cli │
│ (Orchestration Hub) │
├─────────────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────────┐ │
│ │ Core Infrastructure │ │
│ │ ├─ DaemonConfig (Configuration) │ │
│ │ ├─ EventBus (Pub-Sub) │ │
│ │ └─ OperationRegistry (Plugin Discovery) │ │
│ └──────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────────┐ │
│ │ Execution Layers │ │
│ │ ├─ HTTP API (provd daemon) │ │
│ │ ├─ CLI Client (provctl) │ │
│ │ └─ Dual-mode Execution (daemon or direct) │ │
│ └──────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────────┐ │
│ │ Observability & Integration │ │
│ │ ├─ Health Probes (Kubernetes-ready) │ │
│ │ ├─ Webhooks (Event Ingestion) │ │
│ │ ├─ Hierarchical Caching (3-layer LRU) │ │
│ │ ├─ Internationalization (i18n) │ │
│ │ └─ Config Rendering (KCL/Nickel/Tera) │ │
│ └──────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────────┐ │
│ │ Ecosystem Operations (Plugins) │ │
│ │ ├─ valida (config validation) │ │
│ │ ├─ encrypt (encryption) │ │
│ │ ├─ runtime (container runtime detection) │ │
│ │ ├─ init-servs (service management) │ │
│ │ ├─ observability (monitoring) │ │
│ │ └─ audit (security scanning) │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Execution Flow
Client Request
↓
┌─ provctl (CLI) or HTTP API endpoint
├─ Parse input and create Operation request
├─ EventBus publishes "operation_requested" event
├─ OperationRegistry selects matching EcosystemOperation
├─ Check HierarchicalCache for cached result
├─ If miss: Execute operation (e.g., valida::ValidaOperation::execute)
├─ Cache result with TTL
├─ EventBus publishes "operation_completed" event
└─ Return result to client
Stable API Contracts
Why Contracts Matter
The daemon-cli module is heavily depended on by other crates (52+ dependents). To maintain compatibility:
- Stable APIs are guaranteed to remain compatible across minor version updates
- Internal modules may change between versions without notice
- Contracts module explicitly defines stability boundaries
Accessing Stable APIs
Always prefer importing from the contracts module:
// ✅ GOOD - Stable contract
use daemon_cli::contracts::stable::{EcosystemOperation, OperationRegistry};
use daemon_cli::contracts::observability::HealthProbe;
// ⚠️ OK but less clear - Direct import (same types, but unstable guarantee)
use daemon_cli::{EcosystemOperation, HealthProbe};
// ❌ AVOID - Internal implementation details
use daemon_cli::cache::HierarchicalCache; // Not in contracts
use daemon_cli::rendering::TemplateEngine; // Not in contracts
Stable API Categories
1. Core/Stable - Orchestration Contract
pub mod contracts::stable {
pub use crate::core::{DaemonConfig, DaemonError, Result};
pub use crate::orchestration::{EcosystemOperation, OperationRegistry};
pub use crate::api::AppState;
pub use crate::events::{Event, EventBus, EventType, EventPayload};
}
Use When:
- Implementing new operations (extend
EcosystemOperation) - Registering with
OperationRegistry - Publishing events to
EventBus - Accessing shared
AppState
2. Execution/Stable - CLI and Output
pub mod contracts::execution {
pub use crate::cli::{DaemonClient, OfflineMode, OutputFormat};
pub use crate::config_renderer::{ConfigRenderer, RenderRequest};
}
Use When:
- Running in CLI mode (use
DaemonClientorOfflineMode) - Rendering configuration (KCL, Nickel, Tera templates)
- Formatting output (JSON, YAML, Table, etc.)
3. Observability/Stable - Monitoring
pub mod contracts::observability {
pub use crate::health::{HealthMetrics, HealthProbe, ProbeStatus};
}
Use When:
- Implementing health check endpoints
- Collecting metrics
- Exposing readiness/liveness probes for Kubernetes
4. I18n/Stable - Internationalization
pub mod contracts::i18n {
pub use crate::i18n::{
I18nManager, TranslationRequest, TranslationResponse,
TranslationContext,
};
}
Use When:
- Supporting multiple languages in messages
- Translating output
- Managing locale-aware formatting
5. Integration/Stable - External Systems
pub mod contracts::integration {
pub use crate::webhooks::{WebHookEvent, WebHookHandler, WebHookStore};
}
Use When:
- Ingesting webhook events
- Storing webhook configurations
- Integrating with external CI/CD systems
Module Organization
Tier 1: Core Infrastructure (Stable)
| Module | Purpose | Stability |
|---|---|---|
core |
Configuration, errors, types | STABLE |
orchestration |
Operation trait, registry | STABLE |
events |
Event bus, pub-sub | STABLE |
What to know:
DaemonConfigloads from YAML/TOML/environmentEcosystemOperationis the extension point for ecosystem cratesEventBususes async-broadcast for real-time events
Tier 2: Execution Layers (Stable via Contracts)
| Module | Purpose | Stability |
|---|---|---|
api |
Axum HTTP server | STABLE (via contracts) |
cli |
DaemonClient + OfflineMode | STABLE (via contracts) |
config_renderer |
Multi-format rendering | STABLE (via contracts) |
What to know:
DaemonClientconnects to HTTP daemon over networkOfflineModeexecutes operations directly (no daemon)- Choose mode based on deployment: cloud (daemon) vs embedded (offline)
Tier 3: Observability & Integration (Stable via Contracts)
| Module | Purpose | Stability |
|---|---|---|
health |
K8s probes, metrics | STABLE (via contracts) |
webhooks |
Event ingestion | STABLE (via contracts) |
i18n |
Internationalization | STABLE (via contracts) |
What to know:
HealthProbeexposes readiness/liveness for KubernetesWebHookStorepersists webhook configurationsI18nManagerhandles message translation
Tier 4: Internal Implementation (Implementation Details - May Change)
| Module | Purpose | Note |
|---|---|---|
cache |
3-layer LRU caching | Internal, use contracts instead |
rendering |
Template engine | Internal, use config_renderer |
nushell |
Shell integration | Feature-gated internal |
Important: These modules are NOT in contracts because they are implementation details and subject to change.
Core Concepts
1. EcosystemOperation - The Extension Point
The EcosystemOperation trait is how ecosystem crates (valida, encrypt, runtime, etc.) integrate:
#[async_trait]
pub trait EcosystemOperation: Send + Sync {
fn name(&self) -> &'static str;
async fn execute(&self, params: serde_json::Value)
-> Result<OperationResult, OperationError>;
fn cache_key(&self, params: &serde_json::Value) -> String;
}
Pattern: Self-Registration
// In valida crate:
pub struct ValidaOperation;
#[async_trait]
impl EcosystemOperation for ValidaOperation {
fn name(&self) -> &'static str { "valida" }
async fn execute(&self, params: serde_json::Value)
-> Result<OperationResult, OperationError> {
let config_path = params["config_path"].as_str()?;
let rules = load_rules(config_path)?;
validate_config(&rules)?;
Ok(OperationResult { status: "success", data: json!({}) })
}
fn cache_key(&self, params: &serde_json::Value) -> String {
format!("valida:{}", params["config_path"])
}
}
// Registration happens via OperationRegistry::register()
registry.register(Arc::new(ValidaOperation));
2. Hierarchical Caching - Three Levels
Not exposed in stable API (implementation detail), but important to understand:
Level 1: Command Cache (1 hour TTL)
└─ Full operation results cached by cache_key
└─ E.g., "valida:path/to/config.yaml" → validation result
Level 2: Config Cache (5 minutes TTL)
└─ Parsed configuration objects
└─ Shared across operations
Level 3: Module Cache (Permanent)
└─ Loaded modules, plugins, binaries
└─ Only evicted on explicit clear()
Performance Impact: Operations execute in <100ms when cached.
3. Event Bus - Pub-Sub Architecture
Stable API:
pub use daemon_cli::contracts::stable::EventBus;
// Publishing events
bus.publish(Event {
event_type: EventType::OperationStarted,
payload: EventPayload::Operation { name: "valida".into(), params: json!({}) },
});
// Subscribing to events
let mut subscriber = bus.subscribe();
while let Some(event) = subscriber.recv().await {
println!("Event: {:?}", event);
}
Use Cases:
- Audit logging: publish every operation for compliance
- Real-time UI updates: push results to web dashboards
- Workflow coordination: trigger dependent operations
4. Dual-Mode Execution
HTTP Daemon Mode (Default):
provctl --daemon
↓
HTTP request to local daemon (provd)
↓
Daemon executes via OperationRegistry
↓
Response returned to client
Offline Mode (Embedded):
provctl --offline
↓
Create OfflineMode executor directly
↓
Execute operations in-process
↓
Return results immediately
Extension Points
Adding a New Operation
Step 1: Implement EcosystemOperation
// In your crate's main lib.rs
use daemon_cli::contracts::stable::EcosystemOperation;
use async_trait::async_trait;
pub struct MyCustomOperation;
#[async_trait]
impl EcosystemOperation for MyCustomOperation {
fn name(&self) -> &'static str { "my-custom" }
async fn execute(&self, params: serde_json::Value)
-> daemon_cli::Result<OperationResult> {
// Your implementation
Ok(OperationResult {
status: "success".into(),
data: json!({ "result": "data" }),
})
}
fn cache_key(&self, params: &serde_json::Value) -> String {
format!("my-custom:{}", params["input"])
}
}
Step 2: Register with Registry
// In daemon-cli's setup code
use daemon_cli::contracts::stable::OperationRegistry;
let registry = OperationRegistry::new();
registry.register(Arc::new(MyCustomOperation));
Step 3: Access via API or CLI
# Via REST API
curl -X POST http://localhost:9999/execute \
-H "Content-Type: application/json" \
-d '{"operation": "my-custom", "params": {"input": "value"}}'
# Via CLI
provctl execute my-custom --input value
Adding Health Checks
Implement custom health probe:
use daemon_cli::contracts::observability::{HealthProbe, ProbeStatus};
struct MyServiceProbe;
#[async_trait]
impl HealthProbe for MyServiceProbe {
fn name(&self) -> &str { "my-service" }
async fn check(&self) -> ProbeStatus {
if my_service_is_healthy().await {
ProbeStatus::Healthy
} else {
ProbeStatus::Unhealthy("Service unavailable".into())
}
}
}
// Register probe
daemon.register_probe(Box::new(MyServiceProbe));
Kubernetes Usage:
livenessProbe:
httpGet:
path: /health/live
port: 9999
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/ready
port: 9999
initialDelaySeconds: 5
periodSeconds: 5
Adding Webhook Handlers
Implement webhook processor:
use daemon_cli::contracts::integration::WebHookEvent;
async fn handle_github_webhook(event: WebHookEvent) {
match event.event_type.as_str() {
"push" => {
let branch = event.payload["ref"].as_str().unwrap_or("unknown");
println!("Push to {}", branch);
// Trigger deployment, run tests, etc.
}
"pull_request" => {
println!("PR event: {:?}", event.payload["action"]);
}
_ => {}
}
}
// Register webhook
daemon.register_webhook_handler("github", Box::new(handle_github_webhook)).await?;
Best Practices
1. Use Contracts for Imports
// ✅ Good: Explicit stability contract
use daemon_cli::contracts::stable::EcosystemOperation;
use daemon_cli::contracts::observability::HealthProbe;
// ⚠️ Acceptable: Direct import (less clear about stability)
use daemon_cli::EcosystemOperation;
// ❌ Avoid: Implementation details
use daemon_cli::cache::CacheEntry;
2. Handle Errors Properly
use daemon_cli::contracts::stable::Result;
#[async_trait]
impl EcosystemOperation for MyOp {
async fn execute(&self, params: serde_json::Value) -> Result<OperationResult> {
// Use ? operator for propagation
let value = params["required_field"]
.as_str()
.ok_or(DaemonError::validation("required_field is required"))?;
Ok(OperationResult { status: "success".into(), data: json!({}) })
}
}
3. Implement Caching for Performance
fn cache_key(&self, params: &serde_json::Value) -> String {
// Include all inputs that affect output
format!(
"my-op:{}:{}",
params["input_a"],
params["input_b"]
)
}
4. Publish Events for Observability
#[async_trait]
impl EcosystemOperation for MyOp {
async fn execute(&self, params: serde_json::Value) -> Result<OperationResult> {
// Publish start event
bus.publish(Event {
event_type: EventType::OperationStarted,
payload: EventPayload::Operation { name: self.name().into(), params },
});
let result = self.do_work().await?;
// Publish completion event
bus.publish(Event {
event_type: EventType::OperationCompleted,
payload: EventPayload::OperationResult { name: self.name().into(), result: json!(result) },
});
Ok(result)
}
}
5. Support Configuration Rendering
use daemon_cli::contracts::execution::{ConfigRenderer, RenderRequest};
async fn render_config(&self, params: serde_json::Value) -> Result<String> {
let renderer = ConfigRenderer::new();
let request = RenderRequest {
language: ConfigLanguage::Tera,
template: params["template"].as_str().unwrap().into(),
context: params["context"].clone(),
};
let response = renderer.render(&request).await?;
Ok(response.rendered_output)
}
Integration Examples
Example 1: Implementing a Custom Validator
Goal: Add a new validation operation for Helm charts.
// my_validator crate
use daemon_cli::contracts::stable::EcosystemOperation;
use async_trait::async_trait;
pub struct HelmValidatorOp;
#[async_trait]
impl EcosystemOperation for HelmValidatorOp {
fn name(&self) -> &'static str { "helm-validator" }
async fn execute(&self, params: serde_json::Value) -> Result<OperationResult> {
let chart_path = params["chart_path"].as_str()?;
// Validate Helm chart
let is_valid = validate_helm_chart(chart_path).await?;
Ok(OperationResult {
status: if is_valid { "success" } else { "failed" }.into(),
data: json!({
"valid": is_valid,
"chart": chart_path,
}),
})
}
fn cache_key(&self, params: &serde_json::Value) -> String {
format!("helm-validator:{}", params["chart_path"])
}
}
Register in daemon-cli:
registry.register(Arc::new(HelmValidatorOp));
Use via CLI:
provctl execute helm-validator --chart-path ./my-chart/
Example 2: Publishing Events to External System
Goal: Send operation results to a monitoring system.
use daemon_cli::contracts::stable::EventBus;
pub struct MonitoringBridge {
bus: Arc<EventBus>,
remote_url: String,
}
impl MonitoringBridge {
pub async fn start(self) {
let mut subscriber = self.bus.subscribe();
while let Some(event) = subscriber.recv().await {
// Forward to remote monitoring system
self.send_to_monitoring(&event).await.ok();
}
}
async fn send_to_monitoring(&self, event: &Event) -> Result<()> {
let client = reqwest::Client::new();
client
.post(&format!("{}/events", self.remote_url))
.json(event)
.send()
.await?;
Ok(())
}
}
Example 3: Health Check for Custom Service
use daemon_cli::contracts::observability::{HealthProbe, ProbeStatus};
pub struct DatabaseProbe {
connection_string: String,
}
#[async_trait]
impl HealthProbe for DatabaseProbe {
fn name(&self) -> &str { "database" }
async fn check(&self) -> ProbeStatus {
match connect_to_db(&self.connection_string).await {
Ok(_) => ProbeStatus::Healthy,
Err(e) => ProbeStatus::Unhealthy(format!("DB connection failed: {}", e)),
}
}
}
Versioning & Compatibility
Semantic Versioning
- Major (0.x.y → 1.0.0): Breaking changes to stable API
- Minor (1.x.y → 1.1.0): New features (backwards compatible)
- Patch (1.0.x → 1.0.1): Bug fixes
Stable API Guarantees
Within a major version:
- ✅ New fields may be added to structs (with defaults)
- ✅ New methods may be added to traits
- ❌ Existing fields/methods will not be removed or changed
- ❌ Stable types will not move to different modules
Internal Module Changes
Between any versions:
- Internal modules (not in
contracts) may:- Change function signatures
- Reorganize code
- Be replaced with different implementations
Troubleshooting
Operation Not Found
Problem: OperationError::OperationNotFound("my-op")
Solution: Ensure operation is registered before use.
registry.register(Arc::new(MyOp)); // Must happen before daemon starts
Cache Stale Data
Problem: Operation returns old results.
Solution: Clear cache or use different cache key.
fn cache_key(&self, params: &serde_json::Value) -> String {
// Include version or timestamp to invalidate when needed
format!("my-op:{}:v{}", params["input"], params["version"])
}
Webhook Events Not Received
Problem: WebHook handler never called.
Solution: Verify webhook is registered and endpoint is reachable.
daemon.register_webhook("github", handler).await?;
// Then configure webhook in GitHub to point to: http://daemon-host:9999/webhooks/github
Health Check Always Fails
Problem: Readiness probe always returning Unhealthy.
Solution: Verify probe logic and dependencies are available.
async fn check(&self) -> ProbeStatus {
// Must complete within timeout
match timeout(Duration::from_secs(5), self.check_impl()).await {
Ok(result) => result,
Err(_) => ProbeStatus::Unhealthy("Check timed out".into()),
}
}
References
- EcosystemOperation: Extension point for operations
- EventBus: Real-time event distribution
- OperationRegistry: Operation discovery and execution
- AppState: Shared state across daemon
- contracts module: Stable API definitions