Skip to main content

alien_bindings/providers/vault/
gcp_secret_manager.rs

1use crate::error::{ErrorData, Result};
2use alien_error::{Context, ContextError};
3use alien_gcp_clients::secret_manager::{
4    AddSecretVersionRequest, AutomaticReplication, Replication, ReplicationPolicy, Secret,
5    SecretManagerApi, SecretManagerClient, SecretPayload,
6};
7use async_trait::async_trait;
8use base64::{engine::general_purpose::STANDARD as base64_standard, Engine as _};
9use std::sync::Arc;
10use tracing::{debug, warn};
11
12/// GCP Secret Manager vault binding implementation
13#[derive(Debug)]
14pub struct GcpSecretManagerVault {
15    client: Arc<SecretManagerClient>,
16    vault_prefix: String,
17    project_id: String,
18}
19
20impl GcpSecretManagerVault {
21    /// Create a new GCP Secret Manager vault binding
22    pub fn new(client: Arc<SecretManagerClient>, vault_prefix: String, project_id: String) -> Self {
23        Self {
24            client,
25            vault_prefix,
26            project_id,
27        }
28    }
29
30    /// Generate the full secret name with vault prefix
31    fn full_secret_name(&self, secret_name: &str) -> String {
32        format!("{}-{}", self.vault_prefix, secret_name)
33    }
34
35    /// Generate the secret resource name for GCP API
36    fn secret_resource_name(&self, secret_name: &str) -> String {
37        format!(
38            "projects/{}/secrets/{}",
39            self.project_id,
40            self.full_secret_name(secret_name)
41        )
42    }
43}
44
45#[async_trait]
46impl crate::traits::Binding for GcpSecretManagerVault {}
47
48#[async_trait]
49impl crate::traits::Vault for GcpSecretManagerVault {
50    /// Get a secret value by name
51    async fn get_secret(&self, secret_name: &str) -> Result<String> {
52        let secret_resource = self.secret_resource_name(secret_name);
53
54        // Get the latest version of the secret
55        let response = self
56            .client
57            .access_secret_version(format!(
58                "{}/versions/latest",
59                self.full_secret_name(secret_name)
60            ))
61            .await
62            .context(ErrorData::CloudPlatformError {
63                message: format!("Failed to access secret version '{}'", secret_resource),
64                resource_id: None,
65            })?;
66
67        // Extract the payload from the response
68        let payload = response.payload.ok_or_else(|| {
69            alien_error::AlienError::new(ErrorData::CloudPlatformError {
70                message: format!("Secret '{}' has no payload", secret_resource),
71                resource_id: None,
72            })
73        })?;
74
75        // Decode the base64-encoded payload
76        let base64_data = payload.data.ok_or_else(|| {
77            alien_error::AlienError::new(ErrorData::CloudPlatformError {
78                message: format!("Secret '{}' has no data", secret_resource),
79                resource_id: None,
80            })
81        })?;
82
83        // Decode from base64 to bytes, then to UTF-8 string
84        let data = base64_standard.decode(base64_data).map_err(|e| {
85            alien_error::AlienError::new(ErrorData::CloudPlatformError {
86                message: format!(
87                    "Failed to decode base64 data for secret '{}': {}",
88                    secret_resource, e
89                ),
90                resource_id: None,
91            })
92        })?;
93
94        String::from_utf8(data).map_err(|e| {
95            alien_error::AlienError::new(ErrorData::CloudPlatformError {
96                message: format!(
97                    "Secret '{}' contains invalid UTF-8 data: {}",
98                    secret_resource, e
99                ),
100                resource_id: None,
101            })
102        })
103    }
104
105    /// Set a secret value
106    async fn set_secret(&self, secret_name: &str, value: &str) -> Result<()> {
107        let secret_resource = self.secret_resource_name(secret_name);
108        let full_secret_name = self.full_secret_name(secret_name);
109
110        // Prepare the payload once
111        let payload = SecretPayload::builder()
112            .data(base64_standard.encode(value))
113            .build();
114
115        let request = AddSecretVersionRequest::builder()
116            .payload(payload.clone())
117            .build();
118
119        // Try to add a new version to the secret
120        match self
121            .client
122            .add_secret_version(full_secret_name.clone(), request)
123            .await
124        {
125            Ok(_) => Ok(()),
126            Err(e)
127                if e.error
128                    .as_ref()
129                    .map(|err| {
130                        matches!(
131                            err,
132                            alien_client_core::ErrorData::RemoteResourceNotFound { .. }
133                        )
134                    })
135                    .unwrap_or(false) =>
136            {
137                // Secret doesn't exist, create it first with automatic replication
138                let replication = Replication::builder()
139                    .replication_policy(ReplicationPolicy::Automatic(
140                        AutomaticReplication::builder().build(),
141                    ))
142                    .build();
143
144                let secret = Secret::builder().replication(replication).build();
145
146                // Create the secret
147                self.client
148                    .create_secret(full_secret_name.clone(), secret)
149                    .await
150                    .context(ErrorData::CloudPlatformError {
151                        message: format!("Failed to create secret '{}'", secret_resource),
152                        resource_id: None,
153                    })?;
154
155                // Now add the version with retry logic for potential race conditions
156                let add_request = AddSecretVersionRequest::builder().payload(payload).build();
157
158                // Retry adding the version with a small delay to handle race conditions
159                let mut last_error = None;
160                for attempt in 0..3 {
161                    if attempt > 0 {
162                        // Small delay between retries to allow GCP to propagate the secret creation
163                        tokio::time::sleep(std::time::Duration::from_millis(
164                            100 * (attempt as u64),
165                        ))
166                        .await;
167                    }
168
169                    debug!(
170                        "Attempting to add secret version (attempt {}/3) for secret: {}",
171                        attempt + 1,
172                        full_secret_name
173                    );
174                    match self
175                        .client
176                        .add_secret_version(full_secret_name.clone(), add_request.clone())
177                        .await
178                    {
179                        Ok(_) => {
180                            debug!(
181                                "Successfully added secret version for: {}",
182                                full_secret_name
183                            );
184                            return Ok(());
185                        }
186                        Err(e) => {
187                            warn!(
188                                "Failed to add secret version (attempt {}/3) for {}: {:?}",
189                                attempt + 1,
190                                full_secret_name,
191                                e
192                            );
193                            last_error = Some(e);
194                            // Continue to retry unless it's the last attempt
195                        }
196                    }
197                }
198
199                // If we got here, all retries failed
200                if let Some(e) = last_error {
201                    return Err(e.context(ErrorData::CloudPlatformError {
202                        message: format!(
203                            "Failed to add version to secret '{}' after {} attempts",
204                            secret_resource, 3
205                        ),
206                        resource_id: None,
207                    }));
208                }
209
210                Ok(())
211            }
212            Err(e) => Err(e.context(ErrorData::CloudPlatformError {
213                message: format!("Failed to set secret '{}'", secret_resource),
214                resource_id: None,
215            })),
216        }
217    }
218
219    /// Delete a secret
220    async fn delete_secret(&self, secret_name: &str) -> Result<()> {
221        let secret_resource = self.secret_resource_name(secret_name);
222
223        match self
224            .client
225            .delete_secret(self.full_secret_name(secret_name))
226            .await
227        {
228            Ok(_) => Ok(()),
229            Err(e)
230                if e.error
231                    .as_ref()
232                    .map(|err| {
233                        matches!(
234                            err,
235                            alien_client_core::ErrorData::RemoteResourceNotFound { .. }
236                        )
237                    })
238                    .unwrap_or(false) =>
239            {
240                // Secret doesn't exist - this is fine for delete operations (idempotent)
241                debug!(
242                    "Secret '{}' was not found during deletion - treating as success",
243                    secret_resource
244                );
245                Ok(())
246            }
247            Err(e) => Err(e.context(ErrorData::CloudPlatformError {
248                message: format!("Failed to delete secret '{}'", secret_resource),
249                resource_id: None,
250            })),
251        }
252    }
253}