Skip to main content

alien_bindings/providers/function/
gcp_cloudrun.rs

1use crate::error::{ErrorData, Result};
2use crate::traits::{Binding, Function, FunctionInvokeRequest, FunctionInvokeResponse};
3use alien_core::bindings::CloudRunFunctionBinding;
4use alien_error::{AlienError, Context, IntoAlienError};
5use alien_gcp_clients::cloudrun::{CloudRunApi, CloudRunClient};
6use alien_gcp_clients::GcpClientConfig;
7use async_trait::async_trait;
8use reqwest::Client;
9use std::collections::BTreeMap;
10
11/// GCP Cloud Run function binding implementation
12#[derive(Debug)]
13pub struct CloudRunFunction {
14    client: Client,
15    cloudrun_client: CloudRunClient,
16    binding: CloudRunFunctionBinding,
17}
18
19impl CloudRunFunction {
20    pub fn new(client: Client, config: GcpClientConfig, binding: CloudRunFunctionBinding) -> Self {
21        let cloudrun_client = CloudRunClient::new(client.clone(), config);
22        Self {
23            client,
24            cloudrun_client,
25            binding,
26        }
27    }
28
29    /// Get the private URL from the binding, resolving template expressions if needed
30    fn get_private_url(&self) -> Result<String> {
31        self.binding
32            .private_url
33            .clone()
34            .into_value("function", "private_url")
35            .context(ErrorData::BindingConfigInvalid {
36                binding_name: "function".to_string(),
37                reason: "Failed to resolve private_url from binding".to_string(),
38            })
39    }
40
41    /// Resolve the target URL for invocation
42    async fn resolve_target_url(&self, target_function: &str) -> Result<String> {
43        if !target_function.is_empty() {
44            // Check if target_function looks like a URL (starts with http)
45            if target_function.starts_with("http://") || target_function.starts_with("https://") {
46                // Use the provided target function as URL
47                Ok(target_function.to_string())
48            } else {
49                // target_function is likely a path/identifier, use binding URL
50                self.get_private_url()
51            }
52        } else {
53            // Use the private URL from binding
54            self.get_private_url()
55        }
56    }
57}
58
59impl Binding for CloudRunFunction {}
60
61#[async_trait]
62impl Function for CloudRunFunction {
63    async fn invoke(&self, request: FunctionInvokeRequest) -> Result<FunctionInvokeResponse> {
64        let target_url = self.resolve_target_url(&request.target_function).await?;
65
66        // Construct the full URL with path
67        let url = if request.path.starts_with('/') {
68            format!("{}{}", target_url.trim_end_matches('/'), request.path)
69        } else {
70            format!("{}/{}", target_url.trim_end_matches('/'), request.path)
71        };
72
73        // Build the HTTP request
74        let method = match request.method.to_uppercase().as_str() {
75            "GET" => reqwest::Method::GET,
76            "POST" => reqwest::Method::POST,
77            "PUT" => reqwest::Method::PUT,
78            "DELETE" => reqwest::Method::DELETE,
79            "PATCH" => reqwest::Method::PATCH,
80            "HEAD" => reqwest::Method::HEAD,
81            "OPTIONS" => reqwest::Method::OPTIONS,
82            _ => {
83                return Err(AlienError::new(ErrorData::InvalidInput {
84                    operation_context: "Function invocation".to_string(),
85                    details: format!("Unsupported HTTP method: {}", request.method),
86                    field_name: Some("method".to_string()),
87                }));
88            }
89        };
90
91        let mut req_builder = self.client.request(method, &url);
92
93        // Add headers
94        for (key, value) in &request.headers {
95            req_builder = req_builder.header(key, value);
96        }
97
98        // Add body if present
99        if !request.body.is_empty() {
100            req_builder = req_builder.body(request.body.clone());
101        }
102
103        // Set timeout if specified
104        if let Some(timeout) = request.timeout {
105            req_builder = req_builder.timeout(timeout);
106        }
107
108        // Send the request
109        let response =
110            req_builder
111                .send()
112                .await
113                .into_alien_error()
114                .context(ErrorData::HttpRequestFailed {
115                    url: url.clone(),
116                    method: request.method.clone(),
117                })?;
118
119        // Extract response components
120        let status = response.status().as_u16();
121
122        let headers = response
123            .headers()
124            .iter()
125            .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
126            .collect::<BTreeMap<String, String>>();
127
128        let body = response
129            .bytes()
130            .await
131            .into_alien_error()
132            .context(ErrorData::HttpRequestFailed {
133                url: url.clone(),
134                method: "READ_BODY".to_string(),
135            })?
136            .to_vec();
137
138        Ok(FunctionInvokeResponse {
139            status,
140            headers,
141            body,
142        })
143    }
144
145    async fn get_function_url(&self) -> Result<Option<String>> {
146        // First check if we have it in the binding
147        if let Some(url_binding) = &self.binding.public_url {
148            let url = url_binding
149                .clone()
150                .into_value("function", "public_url")
151                .context(ErrorData::BindingConfigInvalid {
152                    binding_name: "function".to_string(),
153                    reason: "Failed to resolve public_url from binding".to_string(),
154                })?;
155            return Ok(Some(url));
156        }
157
158        // If not in binding, try to fetch it from GCP
159        let service_name = self
160            .binding
161            .service_name
162            .clone()
163            .into_value("function", "service_name")
164            .context(ErrorData::BindingConfigInvalid {
165                binding_name: "function".to_string(),
166                reason: "Failed to resolve service_name from binding".to_string(),
167            })?;
168
169        let location = self
170            .binding
171            .location
172            .clone()
173            .into_value("function", "location")
174            .context(ErrorData::BindingConfigInvalid {
175                binding_name: "function".to_string(),
176                reason: "Failed to resolve location from binding".to_string(),
177            })?;
178
179        match self
180            .cloudrun_client
181            .get_service(location, service_name)
182            .await
183        {
184            Ok(service) => {
185                // Return the first URL if available
186                Ok(service.urls.first().cloned())
187            }
188            Err(_) => Ok(None), // Service doesn't exist or no public URL
189        }
190    }
191
192    fn as_any(&self) -> &dyn std::any::Any {
193        self
194    }
195}