Skip to main content

camel_component_exec/
endpoint.rs

1//! ExecEndpoint — producer-only endpoint for system command execution.
2//!
3//! URI form: `exec:{profile-name}`. Consumers return an error — exec is
4//! producer-only. The producer runs the enforcement flow defined by the
5//! resolved exec profile.
6
7use std::sync::Arc;
8
9use camel_api::{BoxProcessor, CamelError};
10use camel_component_api::{Consumer, Endpoint, ProducerContext, RuntimeObservability};
11use tokio::sync::Semaphore;
12
13use crate::config::ExecGlobalConfig;
14use crate::producer::ExecProducer;
15
16/// An endpoint backed by a named exec profile.
17///
18/// Each `exec:{name}` URI resolves to one profile. The endpoint caches the
19/// global config Arc so every producer creation shares the same validated
20/// config state (including the startup-pinned canonical workspace root).
21pub struct ExecEndpoint {
22    uri: String,
23    profile_name: String,
24    global: Arc<ExecGlobalConfig>,
25}
26
27impl ExecEndpoint {
28    pub fn new(uri: String, profile_name: String, global: Arc<ExecGlobalConfig>) -> Self {
29        Self {
30            uri,
31            profile_name,
32            global,
33        }
34    }
35}
36
37impl Endpoint for ExecEndpoint {
38    fn uri(&self) -> &str {
39        &self.uri
40    }
41
42    fn create_consumer(
43        &self,
44        _rt: Arc<dyn RuntimeObservability>,
45    ) -> Result<Box<dyn Consumer>, CamelError> {
46        // CamelError has no UnsupportedOperation variant; InvalidUri is the
47        // closest classification for a producer-only component. The message
48        // "producer-only" is asserted by endpoint_is_producer_only test.
49        Err(CamelError::InvalidUri(
50            "exec component is producer-only".into(),
51        ))
52    }
53
54    fn create_producer(
55        &self,
56        rt: Arc<dyn RuntimeObservability>,
57        ctx: &ProducerContext,
58    ) -> Result<BoxProcessor, CamelError> {
59        let profile = self
60            .global
61            .profile(&self.profile_name)
62            .ok_or_else(|| {
63                CamelError::EndpointCreationFailed(format!(
64                    "exec profile {:?} not found",
65                    self.profile_name
66                ))
67            })?
68            .clone();
69
70        let route_id = ctx.route_id().unwrap_or("unknown").to_string();
71        let semaphore = Arc::new(Semaphore::new(profile.concurrency));
72        let host_env =
73            Arc::new(std::env::vars().collect::<std::collections::HashMap<String, String>>());
74
75        let producer = ExecProducer {
76            profile: Arc::new(profile),
77            global: Arc::clone(&self.global),
78            route_id,
79            host_env,
80            semaphore,
81            rt: Some(rt),
82        };
83
84        Ok(BoxProcessor::new(producer))
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use std::sync::Arc;
91
92    use camel_api::CamelError;
93    use camel_component_api::{Component, NoOpComponentContext, RuntimeObservability};
94
95    use crate::ExecComponent;
96    use crate::config::ExecGlobalConfig;
97
98    fn component_with_echo() -> ExecComponent {
99        let cfg: ExecGlobalConfig = toml::from_str(
100            r#"
101[[profiles]]
102name = "echo"
103executable = "echo"
104"#,
105        )
106        .unwrap();
107        ExecComponent::new(cfg)
108    }
109
110    fn extract_err<T>(result: Result<T, CamelError>) -> CamelError {
111        match result {
112            Err(e) => e,
113            Ok(_) => panic!("expected Err, got Ok"),
114        }
115    }
116
117    #[test]
118    fn create_endpoint_rejects_missing_prefix() {
119        let c = component_with_echo();
120        let ctx = NoOpComponentContext;
121        let err = extract_err(c.create_endpoint("notecho:echo", &ctx));
122        assert!(
123            matches!(&err, CamelError::InvalidUri(_)),
124            "expected InvalidUri, got {err:?}"
125        );
126        assert!(
127            err.to_string().contains("exec:"),
128            "should mention 'exec:': {err}"
129        );
130    }
131
132    #[test]
133    fn create_endpoint_rejects_empty_profile() {
134        let c = component_with_echo();
135        let ctx = NoOpComponentContext;
136        let err = extract_err(c.create_endpoint("exec:", &ctx));
137        assert!(
138            matches!(&err, CamelError::InvalidUri(_)),
139            "expected InvalidUri, got {err:?}"
140        );
141        assert!(
142            err.to_string().contains("profile name"),
143            "should mention 'profile name': {err}"
144        );
145    }
146
147    #[test]
148    fn create_endpoint_strips_query_string() {
149        let c = component_with_echo();
150        let ctx = NoOpComponentContext;
151        let ep = c
152            .create_endpoint("exec:echo?foo=bar", &ctx)
153            .expect("should succeed with query string");
154        assert_eq!(ep.uri(), "exec:echo?foo=bar");
155    }
156
157    #[test]
158    fn create_endpoint_rejects_unknown_profile() {
159        let c = component_with_echo();
160        let ctx = NoOpComponentContext;
161        let err = extract_err(c.create_endpoint("exec:nope", &ctx));
162        assert!(
163            matches!(&err, CamelError::EndpointCreationFailed(_)),
164            "expected EndpointCreationFailed, got {err:?}"
165        );
166        assert!(
167            err.to_string().contains("not configured"),
168            "should mention 'not configured': {err}"
169        );
170    }
171
172    #[test]
173    fn create_endpoint_valid_profile_ok() {
174        let c = component_with_echo();
175        let ctx = NoOpComponentContext;
176        let ep = c
177            .create_endpoint("exec:echo", &ctx)
178            .expect("should succeed");
179        assert_eq!(ep.uri(), "exec:echo");
180    }
181
182    #[test]
183    fn endpoint_is_producer_only() {
184        let c = component_with_echo();
185        let ctx = NoOpComponentContext;
186        let ep = c
187            .create_endpoint("exec:echo", &ctx)
188            .expect("should succeed");
189        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoOpComponentContext);
190        let err = extract_err(ep.create_consumer(rt));
191        assert!(
192            matches!(&err, CamelError::InvalidUri(_)),
193            "expected InvalidUri, got {err:?}"
194        );
195        assert!(
196            err.to_string().contains("producer-only"),
197            "should mention 'producer-only': {err}"
198        );
199    }
200}