Skip to main content

alien_bindings/providers/worker/
azure_container_app.rs

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