Skip to main content

alien_bindings/providers/function/
azure_container_app.rs

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