alien_bindings/providers/vault/
gcp_secret_manager.rs1use 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#[derive(Debug)]
14pub struct GcpSecretManagerVault {
15 client: Arc<SecretManagerClient>,
16 vault_prefix: String,
17 project_id: String,
18}
19
20impl GcpSecretManagerVault {
21 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 fn full_secret_name(&self, secret_name: &str) -> String {
32 format!("{}-{}", self.vault_prefix, secret_name)
33 }
34
35 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 async fn get_secret(&self, secret_name: &str) -> Result<String> {
52 let secret_resource = self.secret_resource_name(secret_name);
53
54 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 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 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 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 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 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 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 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 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 let add_request = AddSecretVersionRequest::builder().payload(payload).build();
157
158 let mut last_error = None;
160 for attempt in 0..3 {
161 if attempt > 0 {
162 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 }
196 }
197 }
198
199 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 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 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}