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, AzureTokenCache};
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 =
26 AzureContainerAppsClient::new(client.clone(), AzureTokenCache::new(config));
27 Self {
28 client,
29 container_apps_client,
30 binding,
31 }
32 }
33
34 fn get_private_url(&self) -> Result<String> {
36 self.binding
37 .private_url
38 .clone()
39 .into_value("function", "private_url")
40 .context(ErrorData::BindingConfigInvalid {
41 binding_name: "function".to_string(),
42 reason: "Failed to resolve private_url from binding".to_string(),
43 })
44 }
45
46 pub async fn get_function_url(&self) -> Result<Option<String>> {
48 if let Some(url_binding) = &self.binding.public_url {
50 let url = url_binding
51 .clone()
52 .into_value("function", "public_url")
53 .context(ErrorData::BindingConfigInvalid {
54 binding_name: "function".to_string(),
55 reason: "Failed to resolve public_url from binding".to_string(),
56 })?;
57 return Ok(Some(url));
58 }
59
60 let resource_group_name = self
62 .binding
63 .resource_group_name
64 .clone()
65 .into_value("function", "resource_group_name")
66 .context(ErrorData::BindingConfigInvalid {
67 binding_name: "function".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("function", "container_app_name")
76 .context(ErrorData::BindingConfigInvalid {
77 binding_name: "function".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 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 Ok(ingress
97 .fqdn
98 .clone()
99 .map(|fqdn| format!("https://{}", fqdn)));
100 }
101 }
102 }
103 Ok(None)
104 }
105 Err(_) => Ok(None), }
107 }
108
109 async fn resolve_target_url(&self, target_function: &str) -> Result<String> {
111 if !target_function.is_empty() {
112 if target_function.starts_with("http://") || target_function.starts_with("https://") {
114 Ok(target_function.to_string())
116 } else {
117 self.get_private_url()
119 }
120 } else {
121 self.get_private_url()
123 }
124 }
125}
126
127impl Binding for ContainerAppFunction {}
128
129#[async_trait]
130impl Function for ContainerAppFunction {
131 async fn invoke(&self, request: FunctionInvokeRequest) -> Result<FunctionInvokeResponse> {
132 let target_url = self.resolve_target_url(&request.target_function).await?;
133
134 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 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: "Function 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 for (key, value) in &request.headers {
163 req_builder = req_builder.header(key, value);
164 }
165
166 if !request.body.is_empty() {
168 req_builder = req_builder.body(request.body.clone());
169 }
170
171 if let Some(timeout) = request.timeout {
173 req_builder = req_builder.timeout(timeout);
174 }
175
176 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 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(FunctionInvokeResponse {
207 status,
208 headers,
209 body,
210 })
211 }
212
213 async fn get_function_url(&self) -> Result<Option<String>> {
214 if let Some(url_binding) = &self.binding.public_url {
216 let url = url_binding
217 .clone()
218 .into_value("function", "public_url")
219 .context(ErrorData::BindingConfigInvalid {
220 binding_name: "function".to_string(),
221 reason: "Failed to resolve public_url from binding".to_string(),
222 })?;
223 return Ok(Some(url));
224 }
225
226 let resource_group_name = self
228 .binding
229 .resource_group_name
230 .clone()
231 .into_value("function", "resource_group_name")
232 .context(ErrorData::BindingConfigInvalid {
233 binding_name: "function".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("function", "container_app_name")
242 .context(ErrorData::BindingConfigInvalid {
243 binding_name: "function".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 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), }
267 }
268
269 fn as_any(&self) -> &dyn std::any::Any {
270 self
271 }
272}