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