alien_bindings/providers/function/
azure_container_app.rs1use 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#[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 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 pub async fn get_function_url(&self) -> Result<Option<String>> {
47 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 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 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 Ok(ingress
96 .fqdn
97 .clone()
98 .map(|fqdn| format!("https://{}", fqdn)));
99 }
100 }
101 }
102 Ok(None)
103 }
104 Err(_) => Ok(None), }
106 }
107
108 async fn resolve_target_url(&self, target_function: &str) -> Result<String> {
110 if !target_function.is_empty() {
111 if target_function.starts_with("http://") || target_function.starts_with("https://") {
113 Ok(target_function.to_string())
115 } else {
116 self.get_private_url()
118 }
119 } else {
120 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 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 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 for (key, value) in &request.headers {
162 req_builder = req_builder.header(key, value);
163 }
164
165 if !request.body.is_empty() {
167 req_builder = req_builder.body(request.body.clone());
168 }
169
170 if let Some(timeout) = request.timeout {
172 req_builder = req_builder.timeout(timeout);
173 }
174
175 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 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 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 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 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), }
266 }
267
268 fn as_any(&self) -> &dyn std::any::Any {
269 self
270 }
271}