Skip to main content

alien_bindings/providers/worker/
azure_container_app.rs

1use crate::error::{ErrorData, Result};
2use crate::traits::{Binding, Worker, WorkerInvokeRequest, WorkerInvokeResponse};
3use alien_azure_clients::container_apps::{AzureContainerAppsClient, ContainerAppsApi};
4use alien_azure_clients::{AzureClientConfig, AzureTokenCache};
5use alien_core::bindings::ContainerAppWorkerBinding;
6use alien_error::{AlienError, Context, IntoAlienError};
7use async_trait::async_trait;
8use reqwest::Client;
9use std::collections::BTreeMap;
10
11/// Azure Container Apps worker binding implementation
12#[derive(Debug)]
13pub struct ContainerAppWorker {
14    client: Client,
15    container_apps_client: AzureContainerAppsClient,
16    binding: ContainerAppWorkerBinding,
17}
18
19impl ContainerAppWorker {
20    pub fn new(
21        client: Client,
22        config: AzureClientConfig,
23        binding: ContainerAppWorkerBinding,
24    ) -> Self {
25        let container_apps_client =
26            AzureContainerAppsClient::new(client.clone(), AzureTokenCache::new(config));
27        Self {
28            client,
29            container_apps_client,
30            binding,
31        }
32    }
33
34    /// Get the private URL from the binding, resolving template expressions if needed
35    fn get_private_url(&self) -> Result<String> {
36        self.binding
37            .private_url
38            .clone()
39            .into_value("worker", "private_url")
40            .context(ErrorData::BindingConfigInvalid {
41                binding_name: "worker".to_string(),
42                reason: "Failed to resolve private_url from binding".to_string(),
43            })
44    }
45
46    /// Get the public URL from the binding if available
47    pub async fn get_worker_url(&self) -> Result<Option<String>> {
48        // First check if we have it in the binding
49        if let Some(url_binding) = &self.binding.public_url {
50            let url = url_binding
51                .clone()
52                .into_value("worker", "public_url")
53                .context(ErrorData::BindingConfigInvalid {
54                    binding_name: "worker".to_string(),
55                    reason: "Failed to resolve public_url from binding".to_string(),
56                })?;
57            return Ok(Some(url));
58        }
59
60        // If not in binding, try to fetch it from Azure
61        let resource_group_name = self
62            .binding
63            .resource_group_name
64            .clone()
65            .into_value("worker", "resource_group_name")
66            .context(ErrorData::BindingConfigInvalid {
67                binding_name: "worker".to_string(),
68                reason: "Failed to resolve resource_group_name from binding".to_string(),
69            })?;
70
71        let container_app_name = self
72            .binding
73            .container_app_name
74            .clone()
75            .into_value("worker", "container_app_name")
76            .context(ErrorData::BindingConfigInvalid {
77                binding_name: "worker".to_string(),
78                reason: "Failed to resolve container_app_name from binding".to_string(),
79            })?;
80
81        match self
82            .container_apps_client
83            .get_container_app(&resource_group_name, &container_app_name)
84            .await
85        {
86            Ok(container_app) => {
87                // Check if there's a public ingress configuration
88                if let Some(configuration) = &container_app.properties {
89                    if let Some(ingress) = &configuration
90                        .configuration
91                        .as_ref()
92                        .and_then(|c| c.ingress.as_ref())
93                    {
94                        if ingress.external {
95                            // Return the FQDN if available
96                            return Ok(ingress
97                                .fqdn
98                                .clone()
99                                .map(|fqdn| format!("https://{}", fqdn)));
100                        }
101                    }
102                }
103                Ok(None)
104            }
105            Err(_) => Ok(None), // Container App doesn't exist or no public URL
106        }
107    }
108
109    /// Resolve the target URL for invocation
110    async fn resolve_target_url(&self, target_worker: &str) -> Result<String> {
111        if !target_worker.is_empty() {
112            // Check if target_worker looks like a URL (starts with http)
113            if target_worker.starts_with("http://") || target_worker.starts_with("https://") {
114                // Use the provided target worker as URL
115                Ok(target_worker.to_string())
116            } else {
117                // target_worker is likely a path/identifier, use binding URL
118                self.get_private_url()
119            }
120        } else {
121            // Use the private URL from binding
122            self.get_private_url()
123        }
124    }
125}
126
127impl Binding for ContainerAppWorker {}
128
129#[async_trait]
130impl Worker for ContainerAppWorker {
131    async fn invoke(&self, request: WorkerInvokeRequest) -> Result<WorkerInvokeResponse> {
132        let target_url = self.resolve_target_url(&request.target_worker).await?;
133
134        // Construct the full URL with path
135        let url = if request.path.starts_with('/') {
136            format!("{}{}", target_url.trim_end_matches('/'), request.path)
137        } else {
138            format!("{}/{}", target_url.trim_end_matches('/'), request.path)
139        };
140
141        // Build the HTTP request
142        let method = match request.method.to_uppercase().as_str() {
143            "GET" => reqwest::Method::GET,
144            "POST" => reqwest::Method::POST,
145            "PUT" => reqwest::Method::PUT,
146            "DELETE" => reqwest::Method::DELETE,
147            "PATCH" => reqwest::Method::PATCH,
148            "HEAD" => reqwest::Method::HEAD,
149            "OPTIONS" => reqwest::Method::OPTIONS,
150            _ => {
151                return Err(AlienError::new(ErrorData::InvalidInput {
152                    operation_context: "Worker invocation".to_string(),
153                    details: format!("Unsupported HTTP method: {}", request.method),
154                    field_name: Some("method".to_string()),
155                }));
156            }
157        };
158
159        let mut req_builder = self.client.request(method, &url);
160
161        // Add headers
162        for (key, value) in &request.headers {
163            req_builder = req_builder.header(key, value);
164        }
165
166        // Add body if present
167        if !request.body.is_empty() {
168            req_builder = req_builder.body(request.body.clone());
169        }
170
171        // Set timeout if specified
172        if let Some(timeout) = request.timeout {
173            req_builder = req_builder.timeout(timeout);
174        }
175
176        // Send the request
177        let response =
178            req_builder
179                .send()
180                .await
181                .into_alien_error()
182                .context(ErrorData::HttpRequestFailed {
183                    url: url.clone(),
184                    method: request.method.clone(),
185                })?;
186
187        // Extract response components
188        let status = response.status().as_u16();
189
190        let headers = response
191            .headers()
192            .iter()
193            .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
194            .collect::<BTreeMap<String, String>>();
195
196        let body = response
197            .bytes()
198            .await
199            .into_alien_error()
200            .context(ErrorData::HttpRequestFailed {
201                url: url.clone(),
202                method: "READ_BODY".to_string(),
203            })?
204            .to_vec();
205
206        Ok(WorkerInvokeResponse {
207            status,
208            headers,
209            body,
210        })
211    }
212
213    async fn get_worker_url(&self) -> Result<Option<String>> {
214        // First check if we have it in the binding
215        if let Some(url_binding) = &self.binding.public_url {
216            let url = url_binding
217                .clone()
218                .into_value("worker", "public_url")
219                .context(ErrorData::BindingConfigInvalid {
220                    binding_name: "worker".to_string(),
221                    reason: "Failed to resolve public_url from binding".to_string(),
222                })?;
223            return Ok(Some(url));
224        }
225
226        // If not in binding, try to fetch it from Azure
227        let resource_group_name = self
228            .binding
229            .resource_group_name
230            .clone()
231            .into_value("worker", "resource_group_name")
232            .context(ErrorData::BindingConfigInvalid {
233                binding_name: "worker".to_string(),
234                reason: "Failed to resolve resource_group_name from binding".to_string(),
235            })?;
236
237        let container_app_name = self
238            .binding
239            .container_app_name
240            .clone()
241            .into_value("worker", "container_app_name")
242            .context(ErrorData::BindingConfigInvalid {
243                binding_name: "worker".to_string(),
244                reason: "Failed to resolve container_app_name from binding".to_string(),
245            })?;
246
247        match self
248            .container_apps_client
249            .get_container_app(&resource_group_name, &container_app_name)
250            .await
251        {
252            Ok(container_app) => {
253                // Extract the URL from the container app configuration
254                if let Some(properties) = &container_app.properties {
255                    if let Some(configuration) = &properties.configuration {
256                        if let Some(ingress) = &configuration.ingress {
257                            if let Some(fqdn) = &ingress.fqdn {
258                                return Ok(Some(format!("https://{}", fqdn)));
259                            }
260                        }
261                    }
262                }
263                Ok(None)
264            }
265            Err(_) => Ok(None), // Container app doesn't exist or no public URL
266        }
267    }
268
269    fn as_any(&self) -> &dyn std::any::Any {
270        self
271    }
272}