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