1use regex::Regex;
2use std::fmt;
3use std::fmt::{Display, Formatter};
4use thiserror::Error as ThisError;
5
6pub const PATTERN_RFC_1123_DNS_LABEL: &str = r"^[a-z0-9]+(-[a-z0-9]+)*$";
8
9#[derive(Debug, Clone, PartialEq, Eq, ThisError)]
10pub enum KubernetesSecretNameParseError {
11 #[error("cannot be empty")]
12 Empty,
13 #[error("length ({name_len}) exceeds 63 characters")]
14 TooLong { name_len: usize },
15 #[error("must only contain lowercase alphanumeric characters or hyphens (-), and start and end with a lowercase alphanumeric character")]
16 InvalidFormat,
17}
18
19pub fn validate_kubernetes_secret_name(name: &str) -> Result<(), KubernetesSecretNameParseError> {
21 if name.is_empty() {
22 Err(KubernetesSecretNameParseError::Empty)
23 } else if name.len() > 63 {
24 Err(KubernetesSecretNameParseError::TooLong {
25 name_len: name.len(),
26 })
27 } else {
28 let re = Regex::new(PATTERN_RFC_1123_DNS_LABEL).expect("valid regular expression");
29 if re.is_match(name) {
30 Ok(())
31 } else {
32 Err(KubernetesSecretNameParseError::InvalidFormat)
33 }
34 }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, ThisError)]
38pub enum KubernetesSecretDataKeyParseError {
39 #[error("cannot be empty")]
40 Empty,
41 #[error("length ({data_key_len}) exceeds 255 characters")]
42 TooLong { data_key_len: usize },
43 #[error("must only contain lowercase alphanumeric characters and hyphens (-), and start and end with a lowercase alphanumeric character")]
44 InvalidFormat,
45}
46
47pub fn validate_kubernetes_secret_data_key(
60 data_key: &str,
61) -> Result<(), KubernetesSecretDataKeyParseError> {
62 if data_key.is_empty() {
63 Err(KubernetesSecretDataKeyParseError::Empty)
64 } else if data_key.len() > 255 {
65 Err(KubernetesSecretDataKeyParseError::TooLong {
66 data_key_len: data_key.len(),
67 })
68 } else {
69 let re = Regex::new(PATTERN_RFC_1123_DNS_LABEL).expect("valid regular expression");
70 if re.is_match(data_key) {
71 Ok(())
72 } else {
73 Err(KubernetesSecretDataKeyParseError::InvalidFormat)
74 }
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
81pub enum SecretRef {
82 Kubernetes {
84 name: String,
86 data_key: String,
88 },
89}
90
91impl Display for SecretRef {
92 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
93 match self {
94 SecretRef::Kubernetes { name, data_key } => {
95 write!(f, "${{secret:kubernetes:{name}/{data_key}}}")
96 }
97 }
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub enum MaybeSecretRef {
103 String(String),
104 SecretRef(SecretRef),
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, ThisError)]
108pub enum MaybeSecretRefParseError {
109 #[error("secret reference '{secret_ref_str}' does not specify a valid provider (for example: 'kubernetes:')")]
110 InvalidProvider { secret_ref_str: String },
111 #[error("Kubernetes secret reference '{secret_ref_str}' is not valid: does not follow format `<name>/<data key>`")]
112 InvalidKubernetesSecretFormat { secret_ref_str: String },
113 #[error(
114 "Kubernetes secret reference '{secret_ref_str}' has name '{name}' which is not valid: {e}"
115 )]
116 InvalidKubernetesSecretName {
117 secret_ref_str: String,
118 name: String,
119 e: KubernetesSecretNameParseError,
120 },
121 #[error(
122 "Kubernetes secret reference '{secret_ref_str}' has data key '{data_key}' which is not valid: {e}"
123 )]
124 InvalidKubernetesSecretDataKey {
125 secret_ref_str: String,
126 data_key: String,
127 e: KubernetesSecretDataKeyParseError,
128 },
129}
130
131impl MaybeSecretRef {
132 pub fn new(value: String) -> Result<MaybeSecretRef, MaybeSecretRefParseError> {
150 if value.starts_with("${secret:") && value.ends_with('}') {
151 let from_idx_incl = 9;
154 let till_idx_excl = value.len() - 1;
155 let content = value[from_idx_incl..till_idx_excl].to_string();
156 if let Some(kubernetes_content) = content.strip_prefix("kubernetes:") {
157 if let Some((name, data_key)) = kubernetes_content.split_once("/") {
158 if let Err(e) = validate_kubernetes_secret_name(name) {
159 Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
160 secret_ref_str: value,
161 name: name.to_string(),
162 e,
163 })
164 } else if let Err(e) = validate_kubernetes_secret_data_key(data_key) {
165 Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
166 secret_ref_str: value,
167 data_key: data_key.to_string(),
168 e,
169 })
170 } else {
171 Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
172 name: name.to_string(),
173 data_key: data_key.to_string(),
174 }))
175 }
176 } else {
177 Err(MaybeSecretRefParseError::InvalidKubernetesSecretFormat {
178 secret_ref_str: value,
179 })
180 }
181 } else {
182 Err(MaybeSecretRefParseError::InvalidProvider {
183 secret_ref_str: value,
184 })
185 }
186 } else {
187 Ok(MaybeSecretRef::String(value))
188 }
189 }
190}
191
192impl Display for MaybeSecretRef {
193 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
194 match self {
195 MaybeSecretRef::String(plain_str) => {
196 write!(f, "{plain_str}")
197 }
198 MaybeSecretRef::SecretRef(secret_ref) => {
199 write!(f, "{secret_ref}")
200 }
201 }
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::{
208 validate_kubernetes_secret_data_key, validate_kubernetes_secret_name,
209 KubernetesSecretDataKeyParseError, KubernetesSecretNameParseError, MaybeSecretRef,
210 };
211 use super::{MaybeSecretRefParseError, SecretRef};
212
213 #[test]
214 #[rustfmt::skip] fn secret_ref_format() {
216 assert_eq!(
217 format!("{}", SecretRef::Kubernetes {
218 name: "example".to_string(),
219 data_key: "value".to_string(),
220 }),
221 "${secret:kubernetes:example/value}"
222 );
223 }
224
225 #[test]
226 #[rustfmt::skip] fn maybe_secret_ref_format() {
228 assert_eq!(
229 format!("{}", MaybeSecretRef::String("example".to_string())),
230 "example"
231 );
232 assert_eq!(
233 format!("{}", MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
234 name: "example".to_string(),
235 data_key: "value".to_string(),
236 })),
237 "${secret:kubernetes:example/value}"
238 );
239 }
240
241 fn test_values_and_expectations(
244 values_and_expectations: Vec<(&str, Result<MaybeSecretRef, MaybeSecretRefParseError>)>,
245 ) {
246 for (value, expectation) in values_and_expectations {
247 let outcome = MaybeSecretRef::new(value.to_string());
248 assert_eq!(outcome, expectation);
249 if let Ok(maybe_secret_ref) = outcome {
250 assert_eq!(maybe_secret_ref.to_string(), value);
251 }
252 }
253 }
254
255 #[test]
256 #[rustfmt::skip] fn maybe_secret_ref_parse_string() {
258 let values_and_expectations = vec![
259 ("", Ok(MaybeSecretRef::String("".to_string()))),
260 ("a", Ok(MaybeSecretRef::String("a".to_string()))),
261 ("1", Ok(MaybeSecretRef::String("1".to_string()))),
262 ("example", Ok(MaybeSecretRef::String("example".to_string()))),
263 ("EXAMPLE", Ok(MaybeSecretRef::String("EXAMPLE".to_string()))),
264 ("123", Ok(MaybeSecretRef::String("123".to_string()))),
265 ("/path/to/file.txt", Ok(MaybeSecretRef::String("/path/to/file.txt".to_string()))),
266 ("$abc", Ok(MaybeSecretRef::String("$abc".to_string()))),
267 ("${secret", Ok(MaybeSecretRef::String("${secret".to_string()))),
268 ("}", Ok(MaybeSecretRef::String("}".to_string()))),
269 ("${secre:}", Ok(MaybeSecretRef::String("${secre:}".to_string()))),
270 ("\u{1F642}", Ok(MaybeSecretRef::String("\u{1F642}".to_string()))),
272 ];
273 test_values_and_expectations(values_and_expectations);
274 }
275
276 #[test]
277 #[rustfmt::skip] fn maybe_secret_ref_parse_secret_ref_kubernetes() {
279 let secret_ref_name_len_63 = format!("${{secret:kubernetes:{}/b}}", "a".repeat(63));
280 let secret_ref_name_len_64 = format!("${{secret:kubernetes:{}/b}}", "a".repeat(64));
281 let secret_ref_data_key_len_255 = format!("${{secret:kubernetes:a/{}}}", "b".repeat(255));
282 let secret_ref_data_key_len_256 = format!("${{secret:kubernetes:a/{}}}", "b".repeat(256));
283 let values_and_expectations = vec![
284 ("${secret:kubernetes:}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretFormat {
285 secret_ref_str: "${secret:kubernetes:}".to_string()
286 })),
287 ("${secret:kubernetes:ab}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretFormat {
288 secret_ref_str: "${secret:kubernetes:ab}".to_string()
289 })),
290 ("${secret:kubernetes:/}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
291 secret_ref_str: "${secret:kubernetes:/}".to_string(),
292 name: "".to_string(),
293 e: KubernetesSecretNameParseError::Empty
294 })),
295 ("${secret:kubernetes:/b}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
296 secret_ref_str: "${secret:kubernetes:/b}".to_string(),
297 name: "".to_string(),
298 e: KubernetesSecretNameParseError::Empty
299 })),
300 ("${secret:kubernetes:a/}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
301 secret_ref_str: "${secret:kubernetes:a/}".to_string(),
302 data_key: "".to_string(),
303 e: KubernetesSecretDataKeyParseError::Empty
304 })),
305 ("${secret:kubernetes:A/b}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
307 secret_ref_str: "${secret:kubernetes:A/b}".to_string(),
308 name: "A".to_string(),
309 e: KubernetesSecretNameParseError::InvalidFormat
310 })),
311 ("${secret:kubernetes:-a/b}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
313 secret_ref_str: "${secret:kubernetes:-a/b}".to_string(),
314 name: "-a".to_string(),
315 e: KubernetesSecretNameParseError::InvalidFormat
316 })),
317 ("${secret:kubernetes:a-/b}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
319 secret_ref_str: "${secret:kubernetes:a-/b}".to_string(),
320 name: "a-".to_string(),
321 e: KubernetesSecretNameParseError::InvalidFormat
322 })),
323 ("${secret:kubernetes:a/B}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
325 secret_ref_str: "${secret:kubernetes:a/B}".to_string(),
326 data_key: "B".to_string(),
327 e: KubernetesSecretDataKeyParseError::InvalidFormat
328 })),
329 ("${secret:kubernetes:a/-b}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
331 secret_ref_str: "${secret:kubernetes:a/-b}".to_string(),
332 data_key: "-b".to_string(),
333 e: KubernetesSecretDataKeyParseError::InvalidFormat
334 })),
335 ("${secret:kubernetes:a/b-}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
337 secret_ref_str: "${secret:kubernetes:a/b-}".to_string(),
338 data_key: "b-".to_string(),
339 e: KubernetesSecretDataKeyParseError::InvalidFormat
340 })),
341 (&secret_ref_name_len_64, Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
343 secret_ref_str: secret_ref_name_len_64.to_string(),
344 name: "a".repeat(64).to_string(),
345 e: KubernetesSecretNameParseError::TooLong {
346 name_len: 64
347 }
348 })),
349 (&secret_ref_data_key_len_256, Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
351 secret_ref_str: secret_ref_data_key_len_256.to_string(),
352 data_key: "b".repeat(256).to_string(),
353 e: KubernetesSecretDataKeyParseError::TooLong {
354 data_key_len: 256
355 }
356 })),
357 ("${secret:kubernetes:\u{1F642}/b}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
360 secret_ref_str: "${secret:kubernetes:\u{1F642}/b}".to_string(),
361 name: "\u{1F642}".to_string(),
362 e: KubernetesSecretNameParseError::InvalidFormat
363 })),
364 ("${secret:kubernetes:a/\u{1F642}}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
367 secret_ref_str: "${secret:kubernetes:a/\u{1F642}}".to_string(),
368 data_key: "\u{1F642}".to_string(),
369 e: KubernetesSecretDataKeyParseError::InvalidFormat
370 })),
371 ("${secret:kubernetes:a/b}", Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
372 name: "a".to_string(),
373 data_key: "b".to_string()
374 }))),
375 ("${secret:kubernetes:0/1}", Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
376 name: "0".to_string(),
377 data_key: "1".to_string()
378 }))),
379 ("${secret:kubernetes:a0/b1}", Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
380 name: "a0".to_string(),
381 data_key: "b1".to_string()
382 }))),
383 ("${secret:kubernetes:0a/1b}", Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
384 name: "0a".to_string(),
385 data_key: "1b".to_string()
386 }))),
387 ("${secret:kubernetes:a-b/c-d}", Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
388 name: "a-b".to_string(),
389 data_key: "c-d".to_string()
390 }))),
391 (&secret_ref_name_len_63, Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
392 name: "a".repeat(63),
393 data_key: "b".to_string()
394 }))),
395 (&secret_ref_data_key_len_255, Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
396 name: "a".to_string(),
397 data_key: "b".repeat(255),
398 }))),
399 ];
400 test_values_and_expectations(values_and_expectations);
401 }
402
403 #[test]
404 #[rustfmt::skip] fn kubernetes_secret_name_validation() {
406 let name_len_63 = "a".repeat(63);
407 let name_len_64 = "a".repeat(64);
408 for (value, expectation) in vec![
409 ("a", Ok(())),
410 ("0", Ok(())),
411 ("a0", Ok(())),
412 ("0a", Ok(())),
413 ("a-0", Ok(())),
414 (&name_len_63, Ok(())),
415 ("", Err(KubernetesSecretNameParseError::Empty)),
416 ("a-", Err(KubernetesSecretNameParseError::InvalidFormat)),
417 ("-a", Err(KubernetesSecretNameParseError::InvalidFormat)),
418 (&name_len_64, Err(KubernetesSecretNameParseError::TooLong {
419 name_len: 64
420 })),
421 ] {
422 assert_eq!(validate_kubernetes_secret_name(value), expectation);
423 }
424 }
425
426 #[test]
427 #[rustfmt::skip] fn kubernetes_secret_data_key_validation() {
429 let data_key_len_255 = "a".repeat(255);
430 let data_key_len_256 = "a".repeat(256);
431 for (value, expectation) in vec![
432 ("a", Ok(())),
433 ("0", Ok(())),
434 ("a0", Ok(())),
435 ("0a", Ok(())),
436 ("a-0", Ok(())),
437 (&data_key_len_255, Ok(())),
438 ("", Err(KubernetesSecretDataKeyParseError::Empty)),
439 ("a-", Err(KubernetesSecretDataKeyParseError::InvalidFormat)),
440 ("-a", Err(KubernetesSecretDataKeyParseError::InvalidFormat)),
441 (&data_key_len_256, Err(KubernetesSecretDataKeyParseError::TooLong {
442 data_key_len: 256
443 })),
444 ] {
445 assert_eq!(validate_kubernetes_secret_data_key(value), expectation);
446 }
447 }
448}