1use 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 let mut camel_config: camel_config::config::CamelConfig =
56 camel_config::config::CamelConfig::from_file(&config_path).unwrap_or_else(|_| {
57 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 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 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 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 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 #[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") .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 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 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 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_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 register_bundle!(ctx, camel_config, camel_template::TemplateBundle);
317
318 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 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 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 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 #[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 for check in
484 camel_core::startup_validation::scan_route_definitions_for_sql_checks(&defs)
485 {
486 ctx.add_startup_check(check);
487 }
488 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 tracing::error!("Failed to add route '{}': {}", id, e);
496 }
497 }
498 }
499 Err(e) => {
500 tracing::error!("Failed to discover routes: {}", e);
502 std::process::exit(1);
503 }
504 }
505
506 if let Err(e) = ctx.start().await {
508 tracing::error!("Failed to start CamelContext: {}", e);
510 std::process::exit(1);
511 }
512
513 tracing::info!("camel-cli: context started");
514
515 let watch_enabled = cli_watch.unwrap_or(camel_config.watch);
518
519 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 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") .recv()
567 .await
568 }
569 #[cfg(not(unix))]
570 {
571 std::future::pending::<()>().await
572 }
573 } => tracing::info!("Received SIGTERM"),
574 }
575
576 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 jms_pool.begin_shutdown();
588 cxf_pool.begin_shutdown();
589
590 ctx.stop().await.unwrap_or_else(|e| {
592 tracing::error!("Error during shutdown: {}", e);
594 });
595
596 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 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 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#[cfg(test)]
628mod tests {
629 #[test]
631 fn startup_warning_emitted() {
632 let source = include_str!("run.rs");
633 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 #[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}