alien_bindings/providers/worker/
azure_container_app.rs1use 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#[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 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 pub async fn get_worker_url(&self) -> Result<Option<String>> {
49 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 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 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 Ok(ingress
101 .fqdn
102 .clone()
103 .map(|fqdn| format!("https://{}", fqdn)));
104 }
105 }
106 }
107 Ok(None)
108 }
109 Err(_) => Ok(None), }
111 }
112
113 async fn resolve_target_url(&self, target_worker: &str) -> Result<String> {
115 if !target_worker.is_empty() {
116 if target_worker.starts_with("http://") || target_worker.starts_with("https://") {
118 Ok(target_worker.to_string())
120 } else {
121 self.get_private_url()
123 }
124 } else {
125 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 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 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 for (key, value) in &request.headers {
167 req_builder = req_builder.header(key, value);
168 }
169
170 if !request.body.is_empty() {
172 req_builder = req_builder.body(request.body.clone());
173 }
174
175 if let Some(timeout) = request.timeout {
177 req_builder = req_builder.timeout(timeout);
178 }
179
180 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 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 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 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 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), }
274 }
275
276 fn as_any(&self) -> &dyn std::any::Any {
277 self
278 }
279}