alien_bindings/providers/vault/
kubernetes_secret.rs1use crate::error::{ErrorData, Result};
2use alien_error::{Context, ContextError, IntoAlienError};
3use alien_k8s_clients::secrets::SecretsApi;
4use async_trait::async_trait;
5use k8s_openapi::api::core::v1::Secret;
6use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
7use std::collections::BTreeMap;
8use std::sync::Arc;
9
10#[derive(Debug)]
20pub struct KubernetesSecretVault {
21 client: Arc<dyn SecretsApi>,
22 namespace: String,
23 vault_prefix: String,
24}
25
26impl KubernetesSecretVault {
27 pub fn new(client: Arc<dyn SecretsApi>, namespace: String, vault_prefix: String) -> Self {
34 Self {
35 client,
36 namespace,
37 vault_prefix,
38 }
39 }
40
41 fn secret_resource_name(&self, secret_name: &str) -> String {
45 let combined = format!("{}-{}", self.vault_prefix, secret_name);
46
47 let clean = combined
49 .chars()
50 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
51 .collect::<String>()
52 .to_lowercase()
53 .replace('_', "-");
54
55 if clean.len() > 253 {
57 clean[..253].to_string()
58 } else {
59 clean
60 }
61 }
62}
63
64#[async_trait]
65impl crate::traits::Binding for KubernetesSecretVault {}
66
67#[async_trait]
68impl crate::traits::Vault for KubernetesSecretVault {
69 async fn get_secret(&self, secret_name: &str) -> Result<String> {
71 let secret_resource_name = self.secret_resource_name(secret_name);
72
73 let secret = self
75 .client
76 .get_secret(&self.namespace, &secret_resource_name)
77 .await
78 .context(ErrorData::CloudPlatformError {
79 message: format!("Failed to get secret '{}'", secret_name),
80 resource_id: None,
81 })?;
82
83 let value = secret
85 .data
86 .as_ref()
87 .and_then(|data| data.get("value"))
88 .ok_or_else(|| {
89 alien_error::AlienError::new(ErrorData::CloudPlatformError {
90 message: format!("Secret '{}' has no 'value' field", secret_name),
91 resource_id: None,
92 })
93 })?;
94
95 let decoded = String::from_utf8(value.0.clone())
97 .into_alien_error()
98 .context(ErrorData::CloudPlatformError {
99 message: format!("Failed to decode secret '{}' value", secret_name),
100 resource_id: None,
101 })?;
102
103 Ok(decoded)
104 }
105
106 async fn set_secret(&self, secret_name: &str, value: &str) -> Result<()> {
108 let secret_resource_name = self.secret_resource_name(secret_name);
109
110 let mut data = BTreeMap::new();
112 data.insert(
113 "value".to_string(),
114 k8s_openapi::ByteString(value.as_bytes().to_vec()),
115 );
116
117 let secret = Secret {
118 metadata: ObjectMeta {
119 name: Some(secret_resource_name.clone()),
120 namespace: Some(self.namespace.clone()),
121 labels: Some({
122 let mut labels = BTreeMap::new();
123 labels.insert("managed-by".to_string(), "operator".to_string());
124 labels.insert("vault-prefix".to_string(), self.vault_prefix.clone());
125 labels
126 }),
127 ..Default::default()
128 },
129 data: Some(data),
130 ..Default::default()
131 };
132
133 match self.client.create_secret(&self.namespace, &secret).await {
135 Ok(_) => Ok(()),
136 Err(e) => {
137 if matches!(
139 e.error,
140 Some(alien_client_core::ErrorData::RemoteResourceConflict { .. })
141 ) {
142 self.client
143 .update_secret(&self.namespace, &secret_resource_name, &secret)
144 .await
145 .context(ErrorData::CloudPlatformError {
146 message: format!("Failed to update secret '{}'", secret_name),
147 resource_id: None,
148 })?;
149 Ok(())
150 } else {
151 Err(e.context(ErrorData::CloudPlatformError {
152 message: format!("Failed to create secret '{}'", secret_name),
153 resource_id: None,
154 }))
155 }
156 }
157 }
158 }
159
160 async fn delete_secret(&self, secret_name: &str) -> Result<()> {
162 let secret_resource_name = self.secret_resource_name(secret_name);
163
164 match self
165 .client
166 .delete_secret(&self.namespace, &secret_resource_name)
167 .await
168 {
169 Ok(()) => Ok(()),
170 Err(error)
171 if matches!(
172 error.error,
173 Some(alien_client_core::ErrorData::RemoteResourceNotFound { .. })
174 ) =>
175 {
176 Ok(())
177 }
178 Err(error) => Err(error.context(ErrorData::CloudPlatformError {
179 message: format!("Failed to delete secret '{secret_name}'"),
180 resource_id: None,
181 })),
182 }
183 }
184
185 async fn list_secrets(&self) -> Result<Vec<String>> {
189 let label_selector = format!("managed-by=operator,vault-prefix={}", self.vault_prefix);
192
193 let list = self
194 .client
195 .list_secrets(&self.namespace, Some(label_selector), None)
196 .await
197 .context(ErrorData::CloudPlatformError {
198 message: format!(
199 "Failed to list secrets for vault prefix '{}'",
200 self.vault_prefix
201 ),
202 resource_id: None,
203 })?;
204
205 let name_prefix = self.secret_resource_name("");
208
209 let names = list
210 .items
211 .into_iter()
212 .filter_map(|secret| secret.metadata.name)
213 .filter_map(|name| name.strip_prefix(&name_prefix).map(str::to_string))
214 .collect();
215
216 Ok(names)
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_secret_resource_name_generation() {
226 let vault_prefix = "acme-monitoring-secrets";
228 let secret_name = "API_KEY";
229 let combined = format!("{}-{}", vault_prefix, secret_name);
230 let result = combined
231 .chars()
232 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
233 .collect::<String>()
234 .to_lowercase()
235 .replace('_', "-");
236
237 assert_eq!(result, "acme-monitoring-secrets-api-key");
238
239 let secret_name2 = "MY_SECRET_KEY";
241 let combined2 = format!("{}-{}", vault_prefix, secret_name2);
242 let result2 = combined2
243 .chars()
244 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
245 .collect::<String>()
246 .to_lowercase()
247 .replace('_', "-");
248
249 assert_eq!(result2, "acme-monitoring-secrets-my-secret-key");
250
251 let long_name = "A".repeat(300);
253 let combined3 = format!("{}-{}", vault_prefix, long_name);
254 let result3 = combined3
255 .chars()
256 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
257 .collect::<String>()
258 .to_lowercase();
259 let truncated = if result3.len() > 253 {
260 result3[..253].to_string()
261 } else {
262 result3
263 };
264 assert!(truncated.len() <= 253);
265 }
266}