Skip to main content

camel_cli/commands/
run.rs

1//! `camel run` subcommand body.
2//!
3//! Owns the 7 ADR-0012 `error!` sites migrated in Phase C (see ADR-0012 +
4//! Phase C plan). Each site has a `// log-policy: …` annotation that
5//! classifies it per the ADR taxonomy.
6//!
7//! All sites in this file are category (c) or (d) — system-broken /
8//! bootstrap. The annotations are added in the Infra cluster task (Task 9),
9//! NOT in this split commit.
10
11use camel_api::datasource::DatasourceCatalog;
12use camel_bean::BeanProcessor;
13use camel_core::datasource::RuntimeDatasourceCatalog;
14use std::sync::Arc;
15use std::time::Duration;
16use tokio_util::sync::CancellationToken;
17
18struct BridgeCleanup {
19    xslt: Arc<camel_xslt::XsltBridgeRuntime>,
20    xj: Arc<camel_xj::XjBridgeRuntime>,
21    validator: Option<Arc<camel_component_validator::xsd_bridge::XsdBridgeBackend>>,
22}
23
24#[async_trait::async_trait]
25impl camel_api::lifecycle::Lifecycle for BridgeCleanup {
26    fn name(&self) -> &str {
27        "bridge-cleanup"
28    }
29
30    async fn start(&mut self) -> Result<(), camel_api::CamelError> {
31        Ok(())
32    }
33
34    async fn stop(&mut self) -> Result<(), camel_api::CamelError> {
35        self.xslt.shutdown().await;
36        self.xj.shutdown().await;
37        if let Some(validator) = &self.validator {
38            validator.shutdown().await;
39        }
40        Ok(())
41    }
42}
43
44pub async fn run(
45    routes_override: Option<String>,
46    config_path: String,
47    cli_watch: Option<bool>,
48    otel: bool,
49    otel_endpoint: Option<String>,
50    service_name: Option<String>,
51    health_port: Option<u16>,
52) -> Result<(), camel_api::CamelError> {
53    // 1. Load config (fall back to empty config with serde defaults if Camel.toml not found)
54    let mut camel_config: camel_config::config::CamelConfig =
55        camel_config::config::CamelConfig::from_file(&config_path).unwrap_or_else(|_| {
56            // Build an empty config so serde defaults apply
57            config::Config::builder()
58                .build()
59                .and_then(|c| c.try_deserialize())
60                .unwrap_or_else(|e| {
61                    eprintln!("Failed to build default config: {e}");
62                    std::process::exit(1);
63                })
64        });
65
66    // 1b. Apply OTel CLI overrides (--otel-endpoint and --service-name imply --otel)
67    let otel_enabled = otel || otel_endpoint.is_some() || service_name.is_some();
68    if otel_enabled {
69        let otel_cfg =
70            camel_config
71                .observability
72                .otel
73                .get_or_insert(camel_config::OtelCamelConfig {
74                    enabled: true,
75                    endpoint: "http://localhost:4317".to_string(),
76                    service_name: "rust-camel".to_string(),
77                    ..Default::default()
78                });
79        otel_cfg.enabled = true;
80        if let Some(ep) = otel_endpoint {
81            otel_cfg.endpoint = ep;
82        }
83        if let Some(name) = service_name {
84            otel_cfg.service_name = name;
85        }
86    }
87
88    if let Some(port) = health_port {
89        let health_cfg = camel_config
90            .observability
91            .health
92            .get_or_insert(camel_config::config::HealthCamelConfig::default());
93        health_cfg.enabled = true;
94        health_cfg.port = port;
95    }
96
97    // 2. Build context with beans registry (also initialises tracing subscriber)
98    let beans_registry = {
99        let bean_reg = std::sync::Arc::new(std::sync::Mutex::new(camel_bean::BeanRegistry::new()));
100        if camel_config.beans.is_empty() {
101            None
102        } else {
103            Some(bean_reg)
104        }
105    };
106
107    let mut ctx = camel_config::config::CamelConfig::configure_context_with_beans(
108        &camel_config,
109        beans_registry.clone(),
110    )
111    .await
112    .unwrap_or_else(|e| {
113        eprintln!("Failed to configure CamelContext: {e}");
114        std::process::exit(1);
115    });
116
117    match camel_function::FunctionRuntimeService::with_default_container_provider(
118        camel_function::FunctionConfig::default(),
119    ) {
120        Ok(svc) => ctx = ctx.with_lifecycle(svc),
121        Err(e) => tracing::warn!("Function runtime disabled: {e}"),
122    }
123
124    // 3a. Create datasource catalog from configured datasources, wiring health registry
125    let datasource_catalog: Arc<dyn DatasourceCatalog> = {
126        let catalog = RuntimeDatasourceCatalog::new(camel_config.datasources.clone())
127            .with_health_registry(ctx.health_registry());
128        Arc::new(catalog)
129    };
130
131    // Load WASM beans after context is created (needs component registry)
132    #[cfg(feature = "wasm")]
133    if let Some(ref bean_reg) = beans_registry {
134        let component_registry = ctx.registry_arc();
135        let plugins_dir_raw = camel_config
136            .components
137            .raw
138            .get("wasm")
139            .and_then(|v| v.get("plugins_dir"))
140            .and_then(|v| v.as_str())
141            .unwrap_or("plugins");
142        let config_dir = std::path::Path::new(&config_path)
143            .parent()
144            .map(|p| {
145                if p.as_os_str().is_empty() {
146                    std::path::Path::new(".")
147                } else {
148                    p
149                }
150            })
151            .unwrap_or(std::path::Path::new("."));
152        let camel_root = config_dir.canonicalize().unwrap_or_else(|e| {
153            eprintln!("Error: cannot resolve project root: {e}");
154            std::process::exit(1);
155        });
156        crate::commands::plugin::validate_plugins_dir(&camel_root, plugins_dir_raw).unwrap_or_else(
157            |e| {
158                eprintln!("Error: invalid plugins_dir: {e}");
159                std::process::exit(1);
160            },
161        );
162        let plugins_dir = camel_root.join(plugins_dir_raw);
163        for (bean_name, bean_cfg) in &camel_config.beans {
164            tracing::info!(bean = %bean_name, plugin = %bean_cfg.plugin, "registering WASM bean");
165
166            if !bean_cfg
167                .plugin
168                .chars()
169                .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
170            {
171                eprintln!(
172                    "Invalid bean plugin name '{}': must be alphanumeric with - or _",
173                    bean_cfg.plugin
174                );
175                std::process::exit(1);
176            }
177
178            let wasm_path = plugins_dir.join(format!("{}.wasm", bean_cfg.plugin));
179            let canonical_plugins = plugins_dir.canonicalize().unwrap_or_else(|_| {
180                eprintln!("Plugins directory not found: {}", plugins_dir.display());
181                std::process::exit(1);
182            });
183            let canonical_path = wasm_path.canonicalize().unwrap_or_else(|_| {
184                eprintln!("WASM bean plugin not found: {}", wasm_path.display());
185                std::process::exit(1);
186            });
187            if !canonical_path.starts_with(&canonical_plugins) {
188                eprintln!(
189                    "Bean plugin path escapes plugins directory: {}",
190                    bean_cfg.plugin
191                );
192                std::process::exit(1);
193            }
194            let wasm_config =
195                camel_component_wasm::config::WasmConfig::from_limits(&bean_cfg.limits);
196            let wasm_bean = camel_component_wasm::bean::WasmBean::new(
197                &wasm_path,
198                wasm_config,
199                component_registry.clone(),
200                bean_cfg.config.clone(),
201            )
202            .await
203            .unwrap_or_else(|e| {
204                eprintln!("Failed to load WASM bean '{}': {}", bean_name, e);
205                std::process::exit(1);
206            });
207            tracing::info!(
208                bean = %bean_name,
209                plugin = %bean_cfg.plugin,
210                methods = ?wasm_bean.methods(),
211                "WASM bean loaded"
212            );
213            bean_reg
214                .lock()
215                .expect("beans registry lock") // allow-unwrap
216                .register(bean_name, wasm_bean)
217                .unwrap_or_else(|e| {
218                    eprintln!("Bean registration failed for '{}': {}", bean_name, e);
219                    std::process::exit(1);
220                });
221        }
222    }
223
224    // 3. Determine route patterns
225    let patterns: Vec<String> = if let Some(p) = routes_override {
226        vec![p]
227    } else if !camel_config.routes.is_empty() {
228        camel_config.routes.clone()
229    } else {
230        vec!["routes/*.yaml".to_string()]
231    };
232
233    tracing::info!("camel-cli: loading routes from patterns: {:?}", patterns);
234
235    let security_compile_context =
236        crate::build_security_compile_context_from_config(&camel_config, ctx.registry_arc())
237            .await?;
238
239    // Define register_bundle! macro — looks up config key in ComponentsConfig::raw,
240    // falling back to an empty table so bundles always register with their serde defaults.
241    // Uses UFCS to invoke ComponentBundle methods without requiring trait in scope
242    macro_rules! register_bundle {
243        ($ctx:expr, $cfg:expr, $Bundle:ty) => {
244            let raw = $cfg
245                .components
246                .raw
247                .get(<$Bundle as camel_component_api::ComponentBundle>::config_key())
248                .cloned()
249                .unwrap_or_else(|| toml::Value::Table(toml::map::Map::new()));
250            match <$Bundle as camel_component_api::ComponentBundle>::from_toml(raw) {
251                Ok(bundle) => <$Bundle as camel_component_api::ComponentBundle>::register_all(
252                    bundle, &mut $ctx,
253                ),
254                Err(e) => {
255                    return Err(camel_api::CamelError::Config(format!(
256                        "Failed to load {} config: {}",
257                        <$Bundle as camel_component_api::ComponentBundle>::config_key(),
258                        e
259                    )));
260                }
261            }
262        };
263    }
264
265    // Register built-in components (no config needed)
266    ctx.register_component(camel_component_timer::TimerComponent::new());
267    ctx.register_component(camel_component_log::LogComponent::new());
268    ctx.register_component(camel_component_direct::DirectComponent::new());
269    ctx.register_component(camel_component_seda::SedaComponent::new());
270    ctx.register_component(camel_component_mock::MockComponent::new());
271    ctx.register_component(camel_component_controlbus::ControlBusComponent::new());
272    let validator_component = camel_component_validator::ValidatorComponent::new();
273    let validator_backend = validator_component.xsd_bridge_backend();
274    ctx.register_component(validator_component);
275
276    let xslt_component = camel_xslt::XsltComponent::default();
277    let xslt_runtime = xslt_component.bridge_runtime();
278    ctx.register_component(xslt_component);
279
280    let xj_component = camel_xj::XjComponent::default();
281    let xj_runtime = xj_component.bridge_runtime();
282    ctx.register_component(xj_component);
283
284    ctx = ctx.with_lifecycle(BridgeCleanup {
285        xslt: xslt_runtime,
286        xj: xj_runtime,
287        validator: validator_backend,
288    });
289
290    // Register HTTP, WS, File, Container (always-on in camel-cli, no feature flag)
291    register_bundle!(ctx, camel_config, camel_component_http::HttpBundle);
292    #[cfg(feature = "http-static")]
293    register_bundle!(ctx, camel_config, camel_component_http::HttpStaticBundle);
294    register_bundle!(ctx, camel_config, camel_component_ws::WsBundle);
295    register_bundle!(ctx, camel_config, camel_component_file::FileBundle);
296    register_bundle!(
297        ctx,
298        camel_config,
299        camel_component_container::ContainerBundle
300    );
301
302    // Register optional/feature-gated bundles
303    let jms_pool = {
304        let raw = camel_config
305            .components
306            .raw
307            .get("jms")
308            .cloned()
309            .unwrap_or_else(|| toml::Value::Table(toml::map::Map::new()));
310        match <camel_component_jms::JmsBundle as camel_component_api::ComponentBundle>::from_toml(
311            raw,
312        ) {
313            Ok(bundle) => {
314                let pool = bundle.pool();
315                <camel_component_jms::JmsBundle as camel_component_api::ComponentBundle>::register_all(bundle, &mut ctx);
316                pool
317            }
318            Err(e) => {
319                return Err(camel_api::CamelError::Config(format!(
320                    "Failed to load jms config: {e}"
321                )));
322            }
323        }
324    };
325
326    let cxf_pool = {
327        let raw = camel_config
328            .components
329            .raw
330            .get("cxf")
331            .cloned()
332            .unwrap_or_else(|| toml::Value::Table(toml::map::Map::new()));
333        match <camel_component_cxf::CxfBundle as camel_component_api::ComponentBundle>::from_toml(
334            raw,
335        ) {
336            Ok(bundle) => {
337                let pool = bundle.pool();
338                <camel_component_cxf::CxfBundle as camel_component_api::ComponentBundle>::register_all(bundle, &mut ctx);
339                pool
340            }
341            Err(e) => {
342                return Err(camel_api::CamelError::Config(format!(
343                    "Failed to load cxf config: {e}"
344                )));
345            }
346        }
347    };
348
349    #[cfg(feature = "kafka")]
350    register_bundle!(ctx, camel_config, camel_component_kafka::KafkaBundle);
351    #[cfg(feature = "mqtt")]
352    register_bundle!(ctx, camel_config, camel_component_mqtt::MqttBundle);
353    register_bundle!(ctx, camel_config, camel_master::MasterBundle);
354    register_bundle!(
355        ctx,
356        camel_config,
357        camel_component_opensearch::OpenSearchBundle
358    );
359    register_bundle!(ctx, camel_config, camel_component_redis::RedisBundle);
360    {
361        let sql_raw = camel_config
362            .components
363            .raw
364            .get(<camel_component_sql::SqlBundle as camel_component_api::ComponentBundle>::config_key())
365            .cloned()
366            .unwrap_or_else(|| toml::Value::Table(toml::map::Map::new()));
367        match <camel_component_sql::SqlBundle as camel_component_api::ComponentBundle>::from_toml(
368            sql_raw,
369        ) {
370            Ok(bundle) => {
371                let bundle = bundle.with_catalog(Arc::clone(&datasource_catalog));
372                <camel_component_sql::SqlBundle as camel_component_api::ComponentBundle>::register_all(bundle, &mut ctx);
373            }
374            Err(e) => {
375                // log-policy: system-broken
376                tracing::error!("failed to initialize SQL bundle: {}", e);
377            }
378        }
379    }
380    #[cfg(feature = "surrealdb")]
381    {
382        let surrealdb_raw = camel_config
383            .components
384            .raw
385            .get(<camel_component_surrealdb::SurrealDbBundle as camel_component_api::ComponentBundle>::config_key())
386            .cloned()
387            .unwrap_or_else(|| toml::Value::Table(toml::map::Map::new()));
388        match <camel_component_surrealdb::SurrealDbBundle as camel_component_api::ComponentBundle>::from_toml(
389            surrealdb_raw,
390        ) {
391            Ok(bundle) => {
392                let bundle = bundle.with_catalog(Arc::clone(&datasource_catalog));
393                <camel_component_surrealdb::SurrealDbBundle as camel_component_api::ComponentBundle>::register_all(
394                    bundle, &mut ctx,
395                );
396            }
397            Err(e) => {
398                // log-policy: system-broken
399                tracing::error!("failed to initialize SurrealDB bundle: {}", e);
400            }
401        }
402    }
403    #[cfg(feature = "grpc")]
404    register_bundle!(ctx, camel_config, camel_component_grpc::GrpcBundle);
405
406    #[cfg(feature = "llm")]
407    register_bundle!(ctx, camel_config, camel_component_llm::LlmBundle);
408
409    #[cfg(feature = "wasm")]
410    {
411        let base_dir = std::path::Path::new(&config_path)
412            .parent()
413            .unwrap_or(std::path::Path::new("."))
414            .to_path_buf();
415        let wasm_bundle = camel_component_wasm::WasmBundle::new(ctx.registry_arc(), base_dir);
416        <camel_component_wasm::WasmBundle as camel_component_api::ComponentBundle>::register_all(
417            wasm_bundle,
418            &mut ctx,
419        );
420    }
421
422    // Languages are registered in `configure_context_with_beans` (camel-config)
423    // via `camel_core::languages_from_config(&config.languages)`, which applies
424    // Camel.toml [languages.*.limits] and registers js/javascript/rhai/
425    // jsonpath/xpath under feature gates. A direct `CamelContext::builder().build()`
426    // caller gets `LanguagesConfig::default()` (rust-camel runtime defaults).
427
428    // 5. Discover and load initial routes
429    match camel_dsl::discover_routes_with_threshold_and_security(
430        &patterns,
431        camel_config.stream_caching.threshold,
432        security_compile_context.clone(),
433    ) {
434        Ok(defs) => {
435            for def in defs {
436                let id = def.route_id().to_string();
437                if let Err(e) = ctx.add_route_definition(def).await {
438                    // log-policy: system-broken
439                    tracing::error!("Failed to add route '{}': {}", id, e);
440                }
441            }
442        }
443        Err(e) => {
444            // log-policy: system-broken
445            tracing::error!("Failed to discover routes: {}", e);
446            std::process::exit(1);
447        }
448    }
449
450    // 6. Start context
451    if let Err(e) = ctx.start().await {
452        // log-policy: system-broken
453        tracing::error!("Failed to start CamelContext: {}", e);
454        std::process::exit(1);
455    }
456
457    tracing::info!("camel-cli: context started");
458
459    // 7. Resolve whether to enable the file watcher:
460    //    CLI flag takes precedence; falls back to Camel.toml `watch` field (default: false).
461    let watch_enabled = cli_watch.unwrap_or(camel_config.watch);
462
463    // 8. Optionally start file watcher in background
464    let watcher_shutdown = CancellationToken::new();
465    if watch_enabled {
466        let ctrl = ctx.runtime_execution_handle();
467        let watch_patterns = patterns.clone();
468        let watch_security_compile_context = security_compile_context.clone();
469        let drain_timeout = std::time::Duration::from_millis(camel_config.drain_timeout_ms);
470        let debounce = std::time::Duration::from_millis(camel_config.watch_debounce_ms);
471        let watcher_token = watcher_shutdown.clone();
472        tokio::spawn(async move {
473            let watch_dirs = camel_core::reload_watcher::resolve_watch_dirs(&watch_patterns);
474            let result = camel_core::reload_watcher::watch_and_reload(
475                watch_dirs,
476                ctrl,
477                move || {
478                    camel_dsl::discover_routes_with_threshold_and_security(
479                        &watch_patterns,
480                        camel_config.stream_caching.threshold,
481                        watch_security_compile_context.clone(),
482                    )
483                    .map_err(|e| camel_api::CamelError::RouteError(e.to_string()))
484                },
485                Some(watcher_token),
486                drain_timeout,
487                debounce,
488            )
489            .await;
490            if let Err(e) = result {
491                // log-policy: system-broken
492                tracing::error!("File watcher failed: {}", e);
493            }
494        });
495        tracing::info!(
496            "camel-cli: hot-reload watching {:?}. Press Ctrl+C to stop.",
497            patterns
498        );
499    } else {
500        tracing::info!("camel-cli: running (hot-reload disabled). Press Ctrl+C to stop.");
501    }
502
503    tokio::select! {
504        _ = tokio::signal::ctrl_c() => tracing::info!("Received Ctrl+C"),
505        _ = async {
506            #[cfg(unix)]
507            {
508                tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
509                    .expect("Failed to install SIGTERM handler") // allow-unwrap
510                    .recv()
511                    .await
512            }
513            #[cfg(not(unix))]
514            {
515                std::future::pending::<()>().await
516            }
517        } => tracing::info!("Received SIGTERM"),
518    }
519
520    // Second Ctrl+C = force exit
521    let force_exit = tokio::spawn(async {
522        tokio::signal::ctrl_c().await.ok();
523        tracing::warn!("Second Ctrl+C — forcing exit");
524        std::process::exit(1);
525    });
526
527    tracing::info!("camel-cli: shutting down...");
528    watcher_shutdown.cancel();
529
530    // Signal pools to stop restarting BEFORE context shutdown
531    jms_pool.begin_shutdown();
532    cxf_pool.begin_shutdown();
533
534    // Stop context (routes + lifecycle services)
535    ctx.stop().await.unwrap_or_else(|e| {
536        // log-policy: system-broken
537        tracing::error!("Error during shutdown: {}", e);
538    });
539
540    // Tear down bridge pools with timeouts
541    const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(30);
542
543    match tokio::time::timeout(SHUTDOWN_TIMEOUT, jms_pool.shutdown()).await {
544        Ok(Ok(())) => {}
545        Ok(Err(e)) => {
546            // log-policy: system-broken
547            tracing::error!("JMS pool shutdown failed: {}", e);
548        }
549        Err(_) => tracing::warn!("JMS pool shutdown timed out after 30s"),
550    }
551
552    match tokio::time::timeout(SHUTDOWN_TIMEOUT, cxf_pool.shutdown()).await {
553        Ok(Ok(())) => {}
554        Ok(Err(e)) => {
555            // log-policy: system-broken
556            tracing::error!("CXF pool shutdown failed: {}", e);
557        }
558        Err(_) => tracing::warn!("CXF pool shutdown timed out after 30s"),
559    }
560
561    force_exit.abort();
562
563    tracing::info!("camel-cli: stopped");
564    Ok(())
565}