use crate::error::{ErrorData, Result};
use crate::traits::{Binding, Function, FunctionInvokeRequest, FunctionInvokeResponse};
use alien_azure_clients::container_apps::{AzureContainerAppsClient, ContainerAppsApi};
use alien_azure_clients::{AzureClientConfig, AzureTokenCache};
use alien_core::bindings::ContainerAppFunctionBinding;
use alien_error::{AlienError, Context, IntoAlienError};
use async_trait::async_trait;
use reqwest::Client;
use std::collections::BTreeMap;
#[derive(Debug)]
pub struct ContainerAppFunction {
client: Client,
container_apps_client: AzureContainerAppsClient,
binding: ContainerAppFunctionBinding,
}
impl ContainerAppFunction {
pub fn new(
client: Client,
config: AzureClientConfig,
binding: ContainerAppFunctionBinding,
) -> Self {
let container_apps_client =
AzureContainerAppsClient::new(client.clone(), AzureTokenCache::new(config));
Self {
client,
container_apps_client,
binding,
}
}
fn get_private_url(&self) -> Result<String> {
self.binding
.private_url
.clone()
.into_value("function", "private_url")
.context(ErrorData::BindingConfigInvalid {
binding_name: "function".to_string(),
reason: "Failed to resolve private_url from binding".to_string(),
})
}
pub async fn get_function_url(&self) -> Result<Option<String>> {
if let Some(url_binding) = &self.binding.public_url {
let url = url_binding
.clone()
.into_value("function", "public_url")
.context(ErrorData::BindingConfigInvalid {
binding_name: "function".to_string(),
reason: "Failed to resolve public_url from binding".to_string(),
})?;
return Ok(Some(url));
}
let resource_group_name = self
.binding
.resource_group_name
.clone()
.into_value("function", "resource_group_name")
.context(ErrorData::BindingConfigInvalid {
binding_name: "function".to_string(),
reason: "Failed to resolve resource_group_name from binding".to_string(),
})?;
let container_app_name = self
.binding
.container_app_name
.clone()
.into_value("function", "container_app_name")
.context(ErrorData::BindingConfigInvalid {
binding_name: "function".to_string(),
reason: "Failed to resolve container_app_name from binding".to_string(),
})?;
match self
.container_apps_client
.get_container_app(&resource_group_name, &container_app_name)
.await
{
Ok(container_app) => {
if let Some(configuration) = &container_app.properties {
if let Some(ingress) = &configuration
.configuration
.as_ref()
.and_then(|c| c.ingress.as_ref())
{
if ingress.external {
return Ok(ingress
.fqdn
.clone()
.map(|fqdn| format!("https://{}", fqdn)));
}
}
}
Ok(None)
}
Err(_) => Ok(None), }
}
async fn resolve_target_url(&self, target_function: &str) -> Result<String> {
if !target_function.is_empty() {
if target_function.starts_with("http://") || target_function.starts_with("https://") {
Ok(target_function.to_string())
} else {
self.get_private_url()
}
} else {
self.get_private_url()
}
}
}
impl Binding for ContainerAppFunction {}
#[async_trait]
impl Function for ContainerAppFunction {
async fn invoke(&self, request: FunctionInvokeRequest) -> Result<FunctionInvokeResponse> {
let target_url = self.resolve_target_url(&request.target_function).await?;
let url = if request.path.starts_with('/') {
format!("{}{}", target_url.trim_end_matches('/'), request.path)
} else {
format!("{}/{}", target_url.trim_end_matches('/'), request.path)
};
let method = match request.method.to_uppercase().as_str() {
"GET" => reqwest::Method::GET,
"POST" => reqwest::Method::POST,
"PUT" => reqwest::Method::PUT,
"DELETE" => reqwest::Method::DELETE,
"PATCH" => reqwest::Method::PATCH,
"HEAD" => reqwest::Method::HEAD,
"OPTIONS" => reqwest::Method::OPTIONS,
_ => {
return Err(AlienError::new(ErrorData::InvalidInput {
operation_context: "Function invocation".to_string(),
details: format!("Unsupported HTTP method: {}", request.method),
field_name: Some("method".to_string()),
}));
}
};
let mut req_builder = self.client.request(method, &url);
for (key, value) in &request.headers {
req_builder = req_builder.header(key, value);
}
if !request.body.is_empty() {
req_builder = req_builder.body(request.body.clone());
}
if let Some(timeout) = request.timeout {
req_builder = req_builder.timeout(timeout);
}
let response =
req_builder
.send()
.await
.into_alien_error()
.context(ErrorData::HttpRequestFailed {
url: url.clone(),
method: request.method.clone(),
})?;
let status = response.status().as_u16();
let headers = response
.headers()
.iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
.collect::<BTreeMap<String, String>>();
let body = response
.bytes()
.await
.into_alien_error()
.context(ErrorData::HttpRequestFailed {
url: url.clone(),
method: "READ_BODY".to_string(),
})?
.to_vec();
Ok(FunctionInvokeResponse {
status,
headers,
body,
})
}
async fn get_function_url(&self) -> Result<Option<String>> {
if let Some(url_binding) = &self.binding.public_url {
let url = url_binding
.clone()
.into_value("function", "public_url")
.context(ErrorData::BindingConfigInvalid {
binding_name: "function".to_string(),
reason: "Failed to resolve public_url from binding".to_string(),
})?;
return Ok(Some(url));
}
let resource_group_name = self
.binding
.resource_group_name
.clone()
.into_value("function", "resource_group_name")
.context(ErrorData::BindingConfigInvalid {
binding_name: "function".to_string(),
reason: "Failed to resolve resource_group_name from binding".to_string(),
})?;
let container_app_name = self
.binding
.container_app_name
.clone()
.into_value("function", "container_app_name")
.context(ErrorData::BindingConfigInvalid {
binding_name: "function".to_string(),
reason: "Failed to resolve container_app_name from binding".to_string(),
})?;
match self
.container_apps_client
.get_container_app(&resource_group_name, &container_app_name)
.await
{
Ok(container_app) => {
if let Some(properties) = &container_app.properties {
if let Some(configuration) = &properties.configuration {
if let Some(ingress) = &configuration.ingress {
if let Some(fqdn) = &ingress.fqdn {
return Ok(Some(format!("https://{}", fqdn)));
}
}
}
}
Ok(None)
}
Err(_) => Ok(None), }
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}