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
9pub const PATTERN_ENV_VAR_NAME: &str = r"^[a-zA-Z_][a-zA-Z0-9_]*$";
11
12#[derive(Debug, Clone, PartialEq, Eq, ThisError)]
13pub enum EnvVarNameParseError {
14 #[error("cannot be empty")]
15 Empty,
16 #[error(
17 "must only contain alphanumeric characters and underscores (_), and start with a letter or underscore"
18 )]
19 InvalidFormat,
20}
21
22pub fn validate_env_var_name(name: &str) -> Result<(), EnvVarNameParseError> {
24 if name.is_empty() {
25 Err(EnvVarNameParseError::Empty)
26 } else {
27 let re = Regex::new(PATTERN_ENV_VAR_NAME).expect("valid regular expression");
28 if re.is_match(name) {
29 Ok(())
30 } else {
31 Err(EnvVarNameParseError::InvalidFormat)
32 }
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, ThisError)]
37pub enum KubernetesSecretNameParseError {
38 #[error("cannot be empty")]
39 Empty,
40 #[error("length ({name_len}) exceeds 63 characters")]
41 TooLong { name_len: usize },
42 #[error(
43 "must only contain lowercase alphanumeric characters or hyphens (-), and start and end with a lowercase alphanumeric character"
44 )]
45 InvalidFormat,
46}
47
48pub fn validate_kubernetes_secret_name(name: &str) -> Result<(), KubernetesSecretNameParseError> {
50 if name.is_empty() {
51 Err(KubernetesSecretNameParseError::Empty)
52 } else if name.len() > 63 {
53 Err(KubernetesSecretNameParseError::TooLong {
54 name_len: name.len(),
55 })
56 } else {
57 let re = Regex::new(PATTERN_RFC_1123_DNS_LABEL).expect("valid regular expression");
58 if re.is_match(name) {
59 Ok(())
60 } else {
61 Err(KubernetesSecretNameParseError::InvalidFormat)
62 }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, ThisError)]
67pub enum KubernetesSecretDataKeyParseError {
68 #[error("cannot be empty")]
69 Empty,
70 #[error("length ({data_key_len}) exceeds 255 characters")]
71 TooLong { data_key_len: usize },
72 #[error(
73 "must only contain lowercase alphanumeric characters and hyphens (-), and start and end with a lowercase alphanumeric character"
74 )]
75 InvalidFormat,
76}
77
78pub fn validate_kubernetes_secret_data_key(
91 data_key: &str,
92) -> Result<(), KubernetesSecretDataKeyParseError> {
93 if data_key.is_empty() {
94 Err(KubernetesSecretDataKeyParseError::Empty)
95 } else if data_key.len() > 255 {
96 Err(KubernetesSecretDataKeyParseError::TooLong {
97 data_key_len: data_key.len(),
98 })
99 } else {
100 let re = Regex::new(PATTERN_RFC_1123_DNS_LABEL).expect("valid regular expression");
101 if re.is_match(data_key) {
102 Ok(())
103 } else {
104 Err(KubernetesSecretDataKeyParseError::InvalidFormat)
105 }
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
112pub enum SecretRef {
113 Kubernetes {
115 name: String,
117 data_key: String,
119 },
120 EnvVar {
122 name: String,
124 },
125}
126
127impl Display for SecretRef {
128 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
129 match self {
130 SecretRef::Kubernetes { name, data_key } => {
131 write!(f, "${{secret:kubernetes:{name}/{data_key}}}")
132 }
133 SecretRef::EnvVar { name } => {
134 write!(f, "${{env:{name}}}")
135 }
136 }
137 }
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub enum MaybeSecretRef {
142 String(String),
143 SecretRef(SecretRef),
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, ThisError)]
147pub enum MaybeSecretRefParseError {
148 #[error(
149 "secret reference '{secret_ref_str}' does not specify a valid provider (for example: 'kubernetes:')"
150 )]
151 InvalidProvider { secret_ref_str: String },
152 #[error(
153 "Kubernetes secret reference '{secret_ref_str}' is not valid: does not follow format `<name>/<data key>`"
154 )]
155 InvalidKubernetesSecretFormat { secret_ref_str: String },
156 #[error(
157 "Kubernetes secret reference '{secret_ref_str}' has name '{name}' which is not valid: {e}"
158 )]
159 InvalidKubernetesSecretName {
160 secret_ref_str: String,
161 name: String,
162 e: KubernetesSecretNameParseError,
163 },
164 #[error(
165 "Kubernetes secret reference '{secret_ref_str}' has data key '{data_key}' which is not valid: {e}"
166 )]
167 InvalidKubernetesSecretDataKey {
168 secret_ref_str: String,
169 data_key: String,
170 e: KubernetesSecretDataKeyParseError,
171 },
172 #[error(
173 "environment variable reference '{env_ref_str}' has name '{name}' which is not valid: {e}"
174 )]
175 InvalidEnvVarName {
176 env_ref_str: String,
177 name: String,
178 e: EnvVarNameParseError,
179 },
180 #[error("environment variable reference '{env_ref_str}' is not valid: name cannot be empty")]
181 EmptyEnvVarName { env_ref_str: String },
182}
183
184impl MaybeSecretRef {
185 pub fn new(value: String) -> Result<MaybeSecretRef, MaybeSecretRefParseError> {
210 let env_prefix = "${env:";
211 if value.starts_with("${secret:") && value.ends_with('}') {
212 let from_idx_incl = 9;
215 let till_idx_excl = value.len() - 1;
216 let content = value[from_idx_incl..till_idx_excl].to_string();
217 if let Some(kubernetes_content) = content.strip_prefix("kubernetes:") {
218 if let Some((name, data_key)) = kubernetes_content.split_once("/") {
219 if let Err(e) = validate_kubernetes_secret_name(name) {
220 Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
221 secret_ref_str: value,
222 name: name.to_string(),
223 e,
224 })
225 } else if let Err(e) = validate_kubernetes_secret_data_key(data_key) {
226 Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
227 secret_ref_str: value,
228 data_key: data_key.to_string(),
229 e,
230 })
231 } else {
232 Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
233 name: name.to_string(),
234 data_key: data_key.to_string(),
235 }))
236 }
237 } else {
238 Err(MaybeSecretRefParseError::InvalidKubernetesSecretFormat {
239 secret_ref_str: value,
240 })
241 }
242 } else {
243 Err(MaybeSecretRefParseError::InvalidProvider {
244 secret_ref_str: value,
245 })
246 }
247 } else if value.starts_with(env_prefix) && value.ends_with('}') {
248 let name = value
251 .trim_start_matches(env_prefix)
252 .trim_end_matches("}")
253 .to_string();
254 if name.is_empty() {
255 Err(MaybeSecretRefParseError::EmptyEnvVarName { env_ref_str: value })
256 } else if let Err(e) = validate_env_var_name(&name) {
257 Err(MaybeSecretRefParseError::InvalidEnvVarName {
258 env_ref_str: value,
259 name,
260 e,
261 })
262 } else {
263 Ok(MaybeSecretRef::SecretRef(SecretRef::EnvVar { name }))
264 }
265 } else {
266 Ok(MaybeSecretRef::String(value))
267 }
268 }
269}
270
271impl Display for MaybeSecretRef {
272 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
273 match self {
274 MaybeSecretRef::String(plain_str) => {
275 write!(f, "{plain_str}")
276 }
277 MaybeSecretRef::SecretRef(secret_ref) => {
278 write!(f, "{secret_ref}")
279 }
280 }
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::{
287 EnvVarNameParseError, KubernetesSecretDataKeyParseError, KubernetesSecretNameParseError,
288 MaybeSecretRef, validate_env_var_name, validate_kubernetes_secret_data_key,
289 validate_kubernetes_secret_name,
290 };
291 use super::{MaybeSecretRefParseError, SecretRef};
292
293 #[test]
294 #[rustfmt::skip] fn secret_ref_format() {
296 assert_eq!(
297 format!("{}", SecretRef::Kubernetes {
298 name: "example".to_string(),
299 data_key: "value".to_string(),
300 }),
301 "${secret:kubernetes:example/value}"
302 );
303 assert_eq!(
304 format!("{}", SecretRef::EnvVar {
305 name: "MY_VAR".to_string(),
306 }),
307 "${env:MY_VAR}"
308 );
309 }
310
311 #[test]
312 #[rustfmt::skip] fn maybe_secret_ref_format() {
314 assert_eq!(
315 format!("{}", MaybeSecretRef::String("example".to_string())),
316 "example"
317 );
318 assert_eq!(
319 format!("{}", MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
320 name: "example".to_string(),
321 data_key: "value".to_string(),
322 })),
323 "${secret:kubernetes:example/value}"
324 );
325 }
326
327 fn test_values_and_expectations(
330 values_and_expectations: Vec<(&str, Result<MaybeSecretRef, MaybeSecretRefParseError>)>,
331 ) {
332 for (value, expectation) in values_and_expectations {
333 let outcome = MaybeSecretRef::new(value.to_string());
334 assert_eq!(outcome, expectation);
335 if let Ok(maybe_secret_ref) = outcome {
336 assert_eq!(maybe_secret_ref.to_string(), value);
337 }
338 }
339 }
340
341 #[test]
342 #[rustfmt::skip] fn maybe_secret_ref_parse_string() {
344 let values_and_expectations = vec![
345 ("", Ok(MaybeSecretRef::String("".to_string()))),
346 ("a", Ok(MaybeSecretRef::String("a".to_string()))),
347 ("1", Ok(MaybeSecretRef::String("1".to_string()))),
348 ("example", Ok(MaybeSecretRef::String("example".to_string()))),
349 ("EXAMPLE", Ok(MaybeSecretRef::String("EXAMPLE".to_string()))),
350 ("123", Ok(MaybeSecretRef::String("123".to_string()))),
351 ("/path/to/file.txt", Ok(MaybeSecretRef::String("/path/to/file.txt".to_string()))),
352 ("$abc", Ok(MaybeSecretRef::String("$abc".to_string()))),
353 ("${secret", Ok(MaybeSecretRef::String("${secret".to_string()))),
354 ("}", Ok(MaybeSecretRef::String("}".to_string()))),
355 ("${secre:}", Ok(MaybeSecretRef::String("${secre:}".to_string()))),
356 ("\u{1F642}", Ok(MaybeSecretRef::String("\u{1F642}".to_string()))),
358 ];
359 test_values_and_expectations(values_and_expectations);
360 }
361
362 #[test]
363 #[rustfmt::skip] fn maybe_secret_ref_parse_secret_ref_kubernetes() {
365 let secret_ref_name_len_63 = format!("${{secret:kubernetes:{}/b}}", "a".repeat(63));
366 let secret_ref_name_len_64 = format!("${{secret:kubernetes:{}/b}}", "a".repeat(64));
367 let secret_ref_data_key_len_255 = format!("${{secret:kubernetes:a/{}}}", "b".repeat(255));
368 let secret_ref_data_key_len_256 = format!("${{secret:kubernetes:a/{}}}", "b".repeat(256));
369 let values_and_expectations = vec![
370 ("${secret:kubernetes:}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretFormat {
371 secret_ref_str: "${secret:kubernetes:}".to_string()
372 })),
373 ("${secret:kubernetes:ab}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretFormat {
374 secret_ref_str: "${secret:kubernetes:ab}".to_string()
375 })),
376 ("${secret:kubernetes:/}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
377 secret_ref_str: "${secret:kubernetes:/}".to_string(),
378 name: "".to_string(),
379 e: KubernetesSecretNameParseError::Empty
380 })),
381 ("${secret:kubernetes:/b}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
382 secret_ref_str: "${secret:kubernetes:/b}".to_string(),
383 name: "".to_string(),
384 e: KubernetesSecretNameParseError::Empty
385 })),
386 ("${secret:kubernetes:a/}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
387 secret_ref_str: "${secret:kubernetes:a/}".to_string(),
388 data_key: "".to_string(),
389 e: KubernetesSecretDataKeyParseError::Empty
390 })),
391 ("${secret:kubernetes:A/b}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
393 secret_ref_str: "${secret:kubernetes:A/b}".to_string(),
394 name: "A".to_string(),
395 e: KubernetesSecretNameParseError::InvalidFormat
396 })),
397 ("${secret:kubernetes:-a/b}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
399 secret_ref_str: "${secret:kubernetes:-a/b}".to_string(),
400 name: "-a".to_string(),
401 e: KubernetesSecretNameParseError::InvalidFormat
402 })),
403 ("${secret:kubernetes:a-/b}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
405 secret_ref_str: "${secret:kubernetes:a-/b}".to_string(),
406 name: "a-".to_string(),
407 e: KubernetesSecretNameParseError::InvalidFormat
408 })),
409 ("${secret:kubernetes:a/B}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
411 secret_ref_str: "${secret:kubernetes:a/B}".to_string(),
412 data_key: "B".to_string(),
413 e: KubernetesSecretDataKeyParseError::InvalidFormat
414 })),
415 ("${secret:kubernetes:a/-b}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
417 secret_ref_str: "${secret:kubernetes:a/-b}".to_string(),
418 data_key: "-b".to_string(),
419 e: KubernetesSecretDataKeyParseError::InvalidFormat
420 })),
421 ("${secret:kubernetes:a/b-}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
423 secret_ref_str: "${secret:kubernetes:a/b-}".to_string(),
424 data_key: "b-".to_string(),
425 e: KubernetesSecretDataKeyParseError::InvalidFormat
426 })),
427 (&secret_ref_name_len_64, Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
429 secret_ref_str: secret_ref_name_len_64.to_string(),
430 name: "a".repeat(64).to_string(),
431 e: KubernetesSecretNameParseError::TooLong {
432 name_len: 64
433 }
434 })),
435 (&secret_ref_data_key_len_256, Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
437 secret_ref_str: secret_ref_data_key_len_256.to_string(),
438 data_key: "b".repeat(256).to_string(),
439 e: KubernetesSecretDataKeyParseError::TooLong {
440 data_key_len: 256
441 }
442 })),
443 ("${secret:kubernetes:\u{1F642}/b}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
446 secret_ref_str: "${secret:kubernetes:\u{1F642}/b}".to_string(),
447 name: "\u{1F642}".to_string(),
448 e: KubernetesSecretNameParseError::InvalidFormat
449 })),
450 ("${secret:kubernetes:a/\u{1F642}}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
453 secret_ref_str: "${secret:kubernetes:a/\u{1F642}}".to_string(),
454 data_key: "\u{1F642}".to_string(),
455 e: KubernetesSecretDataKeyParseError::InvalidFormat
456 })),
457 ("${secret:kubernetes:a/b}", Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
458 name: "a".to_string(),
459 data_key: "b".to_string()
460 }))),
461 ("${secret:kubernetes:0/1}", Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
462 name: "0".to_string(),
463 data_key: "1".to_string()
464 }))),
465 ("${secret:kubernetes:a0/b1}", Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
466 name: "a0".to_string(),
467 data_key: "b1".to_string()
468 }))),
469 ("${secret:kubernetes:0a/1b}", Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
470 name: "0a".to_string(),
471 data_key: "1b".to_string()
472 }))),
473 ("${secret:kubernetes:a-b/c-d}", Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
474 name: "a-b".to_string(),
475 data_key: "c-d".to_string()
476 }))),
477 (&secret_ref_name_len_63, Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
478 name: "a".repeat(63),
479 data_key: "b".to_string()
480 }))),
481 (&secret_ref_data_key_len_255, Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
482 name: "a".to_string(),
483 data_key: "b".repeat(255),
484 }))),
485 ];
486 test_values_and_expectations(values_and_expectations);
487 }
488
489 #[test]
490 #[rustfmt::skip] fn kubernetes_secret_name_validation() {
492 let name_len_63 = "a".repeat(63);
493 let name_len_64 = "a".repeat(64);
494 for (value, expectation) in vec![
495 ("a", Ok(())),
496 ("0", Ok(())),
497 ("a0", Ok(())),
498 ("0a", Ok(())),
499 ("a-0", Ok(())),
500 (&name_len_63, Ok(())),
501 ("", Err(KubernetesSecretNameParseError::Empty)),
502 ("a-", Err(KubernetesSecretNameParseError::InvalidFormat)),
503 ("-a", Err(KubernetesSecretNameParseError::InvalidFormat)),
504 (&name_len_64, Err(KubernetesSecretNameParseError::TooLong {
505 name_len: 64
506 })),
507 ] {
508 assert_eq!(validate_kubernetes_secret_name(value), expectation);
509 }
510 }
511
512 #[test]
513 #[rustfmt::skip] fn kubernetes_secret_data_key_validation() {
515 let data_key_len_255 = "a".repeat(255);
516 let data_key_len_256 = "a".repeat(256);
517 for (value, expectation) in vec![
518 ("a", Ok(())),
519 ("0", Ok(())),
520 ("a0", Ok(())),
521 ("0a", Ok(())),
522 ("a-0", Ok(())),
523 (&data_key_len_255, Ok(())),
524 ("", Err(KubernetesSecretDataKeyParseError::Empty)),
525 ("a-", Err(KubernetesSecretDataKeyParseError::InvalidFormat)),
526 ("-a", Err(KubernetesSecretDataKeyParseError::InvalidFormat)),
527 (&data_key_len_256, Err(KubernetesSecretDataKeyParseError::TooLong {
528 data_key_len: 256
529 })),
530 ] {
531 assert_eq!(validate_kubernetes_secret_data_key(value), expectation);
532 }
533 }
534
535 #[test]
536 #[rustfmt::skip] fn env_var_name_validation() {
538 for (value, expectation) in vec![
539 ("A", Ok(())),
540 ("a", Ok(())),
541 ("_", Ok(())),
542 ("A1", Ok(())),
543 ("MY_VAR", Ok(())),
544 ("_MY_VAR", Ok(())),
545 ("MY_VAR_123", Ok(())),
546 ("", Err(EnvVarNameParseError::Empty)),
547 ("1A", Err(EnvVarNameParseError::InvalidFormat)),
548 ("MY-VAR", Err(EnvVarNameParseError::InvalidFormat)),
549 ("MY VAR", Err(EnvVarNameParseError::InvalidFormat)),
550 ("MY.VAR", Err(EnvVarNameParseError::InvalidFormat)),
551 ] {
552 assert_eq!(validate_env_var_name(value), expectation);
553 }
554 }
555
556 #[test]
557 #[rustfmt::skip] fn maybe_secret_ref_parse_env_var() {
559 let values_and_expectations = vec![
560 ("${env:A}", Ok(MaybeSecretRef::SecretRef(SecretRef::EnvVar { name: "A".to_string() }))),
562 ("${env:MY_VAR}", Ok(MaybeSecretRef::SecretRef(SecretRef::EnvVar { name: "MY_VAR".to_string() }))),
563 ("${env:_MY_VAR}", Ok(MaybeSecretRef::SecretRef(SecretRef::EnvVar { name: "_MY_VAR".to_string() }))),
564 ("${env:MY_VAR_123}", Ok(MaybeSecretRef::SecretRef(SecretRef::EnvVar { name: "MY_VAR_123".to_string() }))),
565 ("${env:}", Err(MaybeSecretRefParseError::EmptyEnvVarName {
567 env_ref_str: "${env:}".to_string()
568 })),
569 ("${env:1VAR}", Err(MaybeSecretRefParseError::InvalidEnvVarName {
571 env_ref_str: "${env:1VAR}".to_string(),
572 name: "1VAR".to_string(),
573 e: EnvVarNameParseError::InvalidFormat
574 })),
575 ("${env:MY-VAR}", Err(MaybeSecretRefParseError::InvalidEnvVarName {
577 env_ref_str: "${env:MY-VAR}".to_string(),
578 name: "MY-VAR".to_string(),
579 e: EnvVarNameParseError::InvalidFormat
580 })),
581 ("${env:", Ok(MaybeSecretRef::String("${env:".to_string()))),
583 ("$env:MY_VAR}", Ok(MaybeSecretRef::String("$env:MY_VAR}".to_string()))),
585 ];
586 test_values_and_expectations(values_and_expectations);
587 }
588}