Skip to main content

xianyun_deploy/
mirrord.rs

1//! Generates mirrord configuration files from a reloader config.
2//!
3//! Ported from `internal/mirrord/config.go`.
4
5use std::io::Write;
6use std::os::unix::fs::OpenOptionsExt;
7
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11use xianyun_core::config::latest::ReloaderConfig;
12
13/// Errors from generating a mirrord configuration.
14#[derive(Debug, Error)]
15pub enum MirrordError {
16    /// The reloader type is not `"mirror"`.
17    #[error("reloader type is not 'mirror'")]
18    NotMirrorReloader,
19    /// The mirror configuration is missing.
20    #[error("mirror configuration is missing")]
21    MissingMirrorConfig,
22    /// The target configuration is missing.
23    #[error("target configuration is missing")]
24    MissingTargetConfig,
25    /// Failed opening the mirrord config file.
26    #[error("opening mirrord config file {0}: {1}")]
27    OpenFile(String, std::io::Error),
28    /// Failed marshalling the mirrord config to JSON.
29    #[error("marshaling mirrord config: {0}")]
30    Marshal(#[from] serde_json::Error),
31    /// Failed writing the mirrord config file.
32    #[error("writing mirrord config file {0}: {1}")]
33    WriteFile(String, std::io::Error),
34}
35
36/// The complete mirrord configuration structure.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct Config {
39    pub target: TargetConfig,
40    pub feature: FeatureConfig,
41    pub agent: AgentConfig,
42}
43
44/// The target Kubernetes resource for mirrord.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct TargetConfig {
47    pub path: String,
48    pub namespace: String,
49}
50
51/// The features to enable in mirrord.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct FeatureConfig {
54    pub network: NetworkConfig,
55    pub fs: FsConfig,
56    pub env: EnvConfig,
57}
58
59/// Outgoing network interception configuration.
60#[derive(Debug, Clone, Default, Serialize, Deserialize)]
61pub struct OutgoingConfig {
62    #[serde(skip_serializing_if = "std::ops::Not::not", default)]
63    pub ignore_localhost: bool,
64}
65
66/// How mirrord intercepts incoming traffic.
67#[derive(Debug, Clone, Default, Serialize, Deserialize)]
68pub struct IncomingConfig {
69    #[serde(skip_serializing_if = "String::is_empty", default)]
70    pub mode: String,
71}
72
73/// Network interception configuration.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct NetworkConfig {
76    pub incoming: IncomingConfig,
77    pub outgoing: OutgoingConfig,
78}
79
80/// Filesystem mount configuration.
81#[derive(Debug, Clone, Default, Serialize, Deserialize)]
82pub struct FsConfig {
83    pub mode: String,
84    #[serde(skip_serializing_if = "Vec::is_empty", default)]
85    pub read_write: Vec<String>,
86    #[serde(skip_serializing_if = "Vec::is_empty", default)]
87    pub read_only: Vec<String>,
88    #[serde(skip_serializing_if = "Option::is_none", default)]
89    pub mapping: Option<std::collections::HashMap<String, String>>,
90}
91
92/// Excludes environment variables from being inherited from the remote
93/// pod. See <https://metalbear.com/mirrord/docs/config/options#feature-env-exclude>.
94#[derive(Debug, Clone, Default, Serialize, Deserialize)]
95pub struct EnvConfig {
96    #[serde(skip_serializing_if = "Vec::is_empty", default)]
97    pub exclude: Vec<String>,
98}
99
100/// The mirrord agent configuration.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct AgentConfig {
103    pub namespace: String,
104}
105
106/// Creates a mirrord configuration from a reloader config and writes it
107/// to the specified file. The caller is responsible for cleaning up the
108/// generated file.
109pub fn generate_config(
110    reloader_config: &ReloaderConfig,
111    filename: &str,
112) -> Result<(), MirrordError> {
113    if reloader_config.reloader_type != "mirror" {
114        return Err(MirrordError::NotMirrorReloader);
115    }
116
117    let mirror = reloader_config
118        .mirror
119        .as_ref()
120        .ok_or(MirrordError::MissingMirrorConfig)?;
121
122    let target = mirror
123        .target
124        .as_ref()
125        .ok_or(MirrordError::MissingTargetConfig)?;
126
127    // Build target path: {type}/{name} or {type}/{name}/container/{container}
128    let mut target_path = format!("{}/{}", target.kubernetes_type, target.name);
129    if !target.container.is_empty() {
130        target_path = format!("{target_path}/container/{}", target.container);
131    }
132
133    // Build outgoing network configuration.
134    let mut outgoing = OutgoingConfig::default();
135    let mut incoming_mode = "steal".to_string();
136
137    if let Some(network) = &mirror.network {
138        if let Some(out) = &network.outgoing {
139            outgoing.ignore_localhost = out.ignore_localhost;
140        }
141
142        if let Some(inc) = &network.incoming
143            && !inc.mode.is_empty()
144        {
145            incoming_mode = inc.mode.clone();
146        }
147    }
148
149    // Build filesystem configuration.
150    let mut fs_mode = "write".to_string();
151    if let Some(filesystem) = &mirror.filesystem
152        && !filesystem.mode.is_empty()
153    {
154        fs_mode = filesystem.mode.clone();
155    }
156
157    let mut cfg = Config {
158        target: TargetConfig {
159            path: target_path,
160            namespace: target.namespace.clone(),
161        },
162        feature: FeatureConfig {
163            network: NetworkConfig {
164                incoming: IncomingConfig {
165                    mode: incoming_mode,
166                },
167                outgoing,
168            },
169            fs: FsConfig {
170                mode: fs_mode,
171                ..Default::default()
172            },
173            env: EnvConfig {
174                // GODEBUG is not guaranteed to be back- or
175                // forwards-compatible.
176                exclude: vec!["GODEBUG".to_string()],
177            },
178        },
179        agent: AgentConfig {
180            namespace: target.namespace.clone(),
181        },
182    };
183
184    if let Some(filesystem) = &mirror.filesystem {
185        cfg.feature.fs.read_write = filesystem.read_write.clone();
186        cfg.feature.fs.read_only = filesystem.read_only.clone();
187        cfg.feature.fs.mapping = filesystem
188            .mapping
189            .as_ref()
190            .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
191    }
192
193    write_json_config(&cfg, filename)
194}
195
196/// Writes `cfg` to `filename` as indented JSON. Unlike Go's
197/// `encoding/json`, `serde_json` never HTML-escapes `<`, `>`, or `&`, so
198/// regex special characters in mapping patterns are always written
199/// literally without any extra configuration.
200fn write_json_config(cfg: &Config, filename: &str) -> Result<(), MirrordError> {
201    let mut file = std::fs::OpenOptions::new()
202        .write(true)
203        .create(true)
204        .truncate(true)
205        .mode(0o600)
206        .open(filename)
207        .map_err(|e| MirrordError::OpenFile(filename.to_string(), e))?;
208
209    let body = serde_json::to_vec_pretty(cfg)?;
210
211    file.write_all(&body)
212        .and_then(|()| file.write_all(b"\n"))
213        .map_err(|e| MirrordError::WriteFile(filename.to_string(), e))
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use xianyun_core::config::latest::{
220        FilesystemConfig, MirrorConfig, MirrorIncomingConfig, MirrorNetworkConfig,
221        MirrorOutgoingConfig, MirrorTargetConfig,
222    };
223    use xianyun_core::orderedmap::OrderedMap;
224
225    fn read_config(path: &std::path::Path) -> serde_json::Value {
226        let data = std::fs::read_to_string(path).unwrap();
227        serde_json::from_str(&data).unwrap()
228    }
229
230    // Ported from internal/mirrord/config_test.go:16 (TestGenerateConfig_Success)
231    #[test]
232    fn test_generate_config_success() {
233        let tmp_dir = tempfile::tempdir().unwrap();
234        let config_file = tmp_dir.path().join("mirrord-config.json");
235
236        let reloader_config = ReloaderConfig {
237            reloader_type: "mirror".to_string(),
238            mirror: Some(MirrorConfig {
239                target: Some(MirrorTargetConfig {
240                    namespace: "gitlab".to_string(),
241                    kubernetes_type: "pod".to_string(),
242                    name: "gitlab-gitaly-0".to_string(),
243                    ..Default::default()
244                }),
245                filesystem: Some(FilesystemConfig {
246                    read_write: vec!["^/home/git/repositories".to_string()],
247                    read_only: vec![
248                        "^/etc/gitaly".to_string(),
249                        "^/srv/gitlab-shell".to_string(),
250                        "^/etc/gitlab-secrets".to_string(),
251                    ],
252                    ..Default::default()
253                }),
254                ..Default::default()
255            }),
256            ..Default::default()
257        };
258
259        generate_config(&reloader_config, config_file.to_str().unwrap()).unwrap();
260        assert!(config_file.exists());
261
262        let cfg = read_config(&config_file);
263        assert_eq!(cfg["target"]["path"], "pod/gitlab-gitaly-0");
264        assert_eq!(cfg["target"]["namespace"], "gitlab");
265        assert_eq!(cfg["feature"]["network"]["incoming"]["mode"], "steal");
266        assert_eq!(
267            cfg["feature"]["network"]["outgoing"]["ignore_localhost"],
268            serde_json::Value::Null
269        );
270        assert_eq!(cfg["feature"]["fs"]["mode"], "write");
271        assert_eq!(
272            cfg["feature"]["fs"]["read_write"],
273            serde_json::json!(["^/home/git/repositories"])
274        );
275        assert_eq!(
276            cfg["feature"]["fs"]["read_only"],
277            serde_json::json!(["^/etc/gitaly", "^/srv/gitlab-shell", "^/etc/gitlab-secrets"])
278        );
279        assert_eq!(cfg["agent"]["namespace"], "gitlab");
280    }
281
282    // Ported from internal/mirrord/config_test.go:75 (TestGenerateConfig_WithoutFilesystem)
283    #[test]
284    fn test_generate_config_without_filesystem() {
285        let tmp_dir = tempfile::tempdir().unwrap();
286        let config_file = tmp_dir.path().join("mirrord-config.json");
287
288        let reloader_config = ReloaderConfig {
289            reloader_type: "mirror".to_string(),
290            mirror: Some(MirrorConfig {
291                target: Some(MirrorTargetConfig {
292                    namespace: "gitlab".to_string(),
293                    kubernetes_type: "deployment".to_string(),
294                    name: "gitlab-webservice-default-0".to_string(),
295                    ..Default::default()
296                }),
297                ..Default::default()
298            }),
299            ..Default::default()
300        };
301
302        generate_config(&reloader_config, config_file.to_str().unwrap()).unwrap();
303
304        let cfg = read_config(&config_file);
305        assert_eq!(
306            cfg["target"]["path"],
307            "deployment/gitlab-webservice-default-0"
308        );
309        assert_eq!(cfg["target"]["namespace"], "gitlab");
310        assert_eq!(cfg["feature"]["fs"]["mode"], "write");
311        assert!(cfg["feature"]["fs"].get("read_write").is_none());
312        assert!(cfg["feature"]["fs"].get("read_only").is_none());
313    }
314
315    // Ported from internal/mirrord/config_test.go:114 (TestGenerateConfig_DifferentResourceTypes)
316    #[test]
317    fn test_generate_config_different_resource_types() {
318        let cases = [
319            ("pod", "gitlab-gitaly-0", "pod/gitlab-gitaly-0"),
320            (
321                "deployment",
322                "gitlab-webservice",
323                "deployment/gitlab-webservice",
324            ),
325            ("statefulset", "gitlab-gitaly", "statefulset/gitlab-gitaly"),
326            ("daemonset", "gitlab-runner", "daemonset/gitlab-runner"),
327        ];
328
329        for (kubernetes_type, resource_name, expected_path) in cases {
330            let tmp_dir = tempfile::tempdir().unwrap();
331            let config_file = tmp_dir.path().join("mirrord-config.json");
332
333            let reloader_config = ReloaderConfig {
334                reloader_type: "mirror".to_string(),
335                mirror: Some(MirrorConfig {
336                    target: Some(MirrorTargetConfig {
337                        namespace: "gitlab".to_string(),
338                        kubernetes_type: kubernetes_type.to_string(),
339                        name: resource_name.to_string(),
340                        ..Default::default()
341                    }),
342                    ..Default::default()
343                }),
344                ..Default::default()
345            };
346
347            generate_config(&reloader_config, config_file.to_str().unwrap()).unwrap();
348
349            let cfg = read_config(&config_file);
350            assert_eq!(
351                cfg["target"]["path"], expected_path,
352                "type={kubernetes_type}"
353            );
354        }
355    }
356
357    // Ported from internal/mirrord/config_test.go:163 (TestGenerateConfig_TargetContainer)
358    #[test]
359    fn test_generate_config_target_container() {
360        let cases = [
361            (
362                "webservice",
363                "deployment/gitlab-webservice/container/webservice",
364            ),
365            ("", "deployment/gitlab-webservice"),
366        ];
367
368        for (container, expected_path) in cases {
369            let tmp_dir = tempfile::tempdir().unwrap();
370            let config_file = tmp_dir.path().join("mirrord-config.json");
371
372            let reloader_config = ReloaderConfig {
373                reloader_type: "mirror".to_string(),
374                mirror: Some(MirrorConfig {
375                    target: Some(MirrorTargetConfig {
376                        namespace: "gitlab".to_string(),
377                        kubernetes_type: "deployment".to_string(),
378                        name: "gitlab-webservice".to_string(),
379                        container: container.to_string(),
380                    }),
381                    ..Default::default()
382                }),
383                ..Default::default()
384            };
385
386            generate_config(&reloader_config, config_file.to_str().unwrap()).unwrap();
387
388            let cfg = read_config(&config_file);
389            assert_eq!(
390                cfg["target"]["path"], expected_path,
391                "container={container}"
392            );
393        }
394    }
395
396    // Ported from internal/mirrord/config_test.go:210 (TestGenerateConfig_IncomingMode)
397    #[test]
398    fn test_generate_config_incoming_mode() {
399        let cases = [
400            (None, "steal"),
401            (Some("steal"), "steal"),
402            (Some("mirror"), "mirror"),
403            (Some("off"), "off"),
404        ];
405
406        for (incoming, expected_mode) in cases {
407            let tmp_dir = tempfile::tempdir().unwrap();
408            let config_file = tmp_dir.path().join("mirrord-config.json");
409
410            let reloader_config = ReloaderConfig {
411                reloader_type: "mirror".to_string(),
412                mirror: Some(MirrorConfig {
413                    target: Some(MirrorTargetConfig {
414                        namespace: "default".to_string(),
415                        kubernetes_type: "deployment".to_string(),
416                        name: "my-app".to_string(),
417                        ..Default::default()
418                    }),
419                    network: Some(MirrorNetworkConfig {
420                        incoming: incoming.map(|mode| MirrorIncomingConfig {
421                            mode: mode.to_string(),
422                        }),
423                        ..Default::default()
424                    }),
425                    ..Default::default()
426                }),
427                ..Default::default()
428            };
429
430            generate_config(&reloader_config, config_file.to_str().unwrap()).unwrap();
431
432            let cfg = read_config(&config_file);
433            assert_eq!(cfg["feature"]["network"]["incoming"]["mode"], expected_mode);
434        }
435    }
436
437    // Ported from internal/mirrord/config_test.go:264 (TestGenerateConfig_OmitsKubeconfig)
438    #[test]
439    fn test_generate_config_omits_kubeconfig() {
440        let tmp_dir = tempfile::tempdir().unwrap();
441        let config_file = tmp_dir.path().join("mirrord-config.json");
442
443        let reloader_config = ReloaderConfig {
444            reloader_type: "mirror".to_string(),
445            mirror: Some(MirrorConfig {
446                target: Some(MirrorTargetConfig {
447                    namespace: "gitlab".to_string(),
448                    kubernetes_type: "pod".to_string(),
449                    name: "gitlab-gitaly-0".to_string(),
450                    ..Default::default()
451                }),
452                ..Default::default()
453            }),
454            ..Default::default()
455        };
456
457        generate_config(&reloader_config, config_file.to_str().unwrap()).unwrap();
458
459        let data = std::fs::read_to_string(&config_file).unwrap();
460        assert!(!data.contains("kubeconfig"));
461    }
462
463    // Ported from internal/mirrord/config_test.go:294 (TestGenerateConfig_NotMirrorRepository)
464    #[test]
465    fn test_generate_config_not_mirror_repository() {
466        let tmp_dir = tempfile::tempdir().unwrap();
467        let config_file = tmp_dir.path().join("mirrord-config.json");
468
469        let reloader_config = ReloaderConfig {
470            reloader_type: "helm".to_string(),
471            ..Default::default()
472        };
473
474        let err = generate_config(&reloader_config, config_file.to_str().unwrap()).unwrap_err();
475        assert!(matches!(err, MirrordError::NotMirrorReloader));
476        assert!(!config_file.exists());
477    }
478
479    // Ported from internal/mirrord/config_test.go:310 (TestGenerateConfig_MissingMirrorConfig)
480    #[test]
481    fn test_generate_config_missing_mirror_config() {
482        let tmp_dir = tempfile::tempdir().unwrap();
483        let config_file = tmp_dir.path().join("mirrord-config.json");
484
485        let reloader_config = ReloaderConfig {
486            reloader_type: "mirror".to_string(),
487            mirror: None,
488            ..Default::default()
489        };
490
491        let err = generate_config(&reloader_config, config_file.to_str().unwrap()).unwrap_err();
492        assert!(matches!(err, MirrordError::MissingMirrorConfig));
493        assert!(!config_file.exists());
494    }
495
496    // Ported from internal/mirrord/config_test.go:327 (TestGenerateConfig_MissingTargetConfig)
497    #[test]
498    fn test_generate_config_missing_target_config() {
499        let tmp_dir = tempfile::tempdir().unwrap();
500        let config_file = tmp_dir.path().join("mirrord-config.json");
501
502        let reloader_config = ReloaderConfig {
503            reloader_type: "mirror".to_string(),
504            mirror: Some(MirrorConfig {
505                target: None,
506                ..Default::default()
507            }),
508            ..Default::default()
509        };
510
511        let err = generate_config(&reloader_config, config_file.to_str().unwrap()).unwrap_err();
512        assert!(matches!(err, MirrordError::MissingTargetConfig));
513        assert!(!config_file.exists());
514    }
515
516    // Ported from internal/mirrord/config_test.go:344 (TestGenerateConfig_InvalidPath)
517    #[test]
518    fn test_generate_config_invalid_path() {
519        let config_file = "/nonexistent/directory/mirrord-config.json";
520
521        let reloader_config = ReloaderConfig {
522            reloader_type: "mirror".to_string(),
523            mirror: Some(MirrorConfig {
524                target: Some(MirrorTargetConfig {
525                    namespace: "gitlab".to_string(),
526                    kubernetes_type: "pod".to_string(),
527                    name: "gitlab-gitaly-0".to_string(),
528                    ..Default::default()
529                }),
530                ..Default::default()
531            }),
532            ..Default::default()
533        };
534
535        let err = generate_config(&reloader_config, config_file).unwrap_err();
536        assert!(err.to_string().contains("opening mirrord config file"));
537    }
538
539    // Ported from internal/mirrord/config_test.go:369 (TestGenerateConfig_JSONFormat)
540    #[test]
541    fn test_generate_config_json_format() {
542        let tmp_dir = tempfile::tempdir().unwrap();
543        let config_file = tmp_dir.path().join("mirrord-config.json");
544
545        let reloader_config = ReloaderConfig {
546            reloader_type: "mirror".to_string(),
547            mirror: Some(MirrorConfig {
548                target: Some(MirrorTargetConfig {
549                    namespace: "gitlab".to_string(),
550                    kubernetes_type: "pod".to_string(),
551                    name: "gitlab-gitaly-0".to_string(),
552                    ..Default::default()
553                }),
554                filesystem: Some(FilesystemConfig {
555                    read_write: vec!["^/home/git/repositories".to_string()],
556                    read_only: vec!["^/etc/gitaly".to_string()],
557                    ..Default::default()
558                }),
559                ..Default::default()
560            }),
561            ..Default::default()
562        };
563
564        generate_config(&reloader_config, config_file.to_str().unwrap()).unwrap();
565
566        let data = std::fs::read_to_string(&config_file).unwrap();
567        let _: serde_json::Value = serde_json::from_str(&data).unwrap();
568
569        assert!(data.contains('\n'));
570        assert!(data.contains("  "));
571    }
572
573    // Ported from internal/mirrord/config_test.go:403 (TestGenerateConfig_FilesystemMode)
574    #[test]
575    fn test_generate_config_filesystem_mode() {
576        let cases = [
577            (None, "write"),
578            (Some("write"), "write"),
579            (Some("local"), "local"),
580            (Some("localwithoverrides"), "localwithoverrides"),
581            (Some("read"), "read"),
582        ];
583
584        for (mode, expected_mode) in cases {
585            let tmp_dir = tempfile::tempdir().unwrap();
586            let config_file = tmp_dir.path().join("mirrord-config.json");
587
588            let filesystem = mode.map(|m| FilesystemConfig {
589                mode: m.to_string(),
590                ..Default::default()
591            });
592
593            let reloader_config = ReloaderConfig {
594                reloader_type: "mirror".to_string(),
595                mirror: Some(MirrorConfig {
596                    target: Some(MirrorTargetConfig {
597                        namespace: "gitlab".to_string(),
598                        kubernetes_type: "pod".to_string(),
599                        name: "gitlab-webservice-0".to_string(),
600                        ..Default::default()
601                    }),
602                    filesystem,
603                    ..Default::default()
604                }),
605                ..Default::default()
606            };
607
608            generate_config(&reloader_config, config_file.to_str().unwrap()).unwrap();
609
610            let cfg = read_config(&config_file);
611            assert_eq!(cfg["feature"]["fs"]["mode"], expected_mode);
612        }
613    }
614
615    // Ported from internal/mirrord/config_test.go:450 (TestGenerateConfig_IgnoreLocalhostOutgoing)
616    #[test]
617    fn test_generate_config_ignore_localhost_outgoing() {
618        let tmp_dir = tempfile::tempdir().unwrap();
619        let config_file = tmp_dir.path().join("mirrord-config.json");
620
621        let reloader_config = ReloaderConfig {
622            reloader_type: "mirror".to_string(),
623            mirror: Some(MirrorConfig {
624                target: Some(MirrorTargetConfig {
625                    namespace: "gitlab".to_string(),
626                    kubernetes_type: "pod".to_string(),
627                    name: "gitlab-webservice-0".to_string(),
628                    ..Default::default()
629                }),
630                network: Some(MirrorNetworkConfig {
631                    outgoing: Some(MirrorOutgoingConfig {
632                        ignore_localhost: true,
633                    }),
634                    ..Default::default()
635                }),
636                ..Default::default()
637            }),
638            ..Default::default()
639        };
640
641        generate_config(&reloader_config, config_file.to_str().unwrap()).unwrap();
642
643        let cfg = read_config(&config_file);
644        assert_eq!(
645            cfg["feature"]["network"]["outgoing"]["ignore_localhost"],
646            true
647        );
648    }
649
650    // Ported from internal/mirrord/config_test.go:476 (TestGenerateConfig_WithoutNetwork)
651    #[test]
652    fn test_generate_config_without_network() {
653        let tmp_dir = tempfile::tempdir().unwrap();
654        let config_file = tmp_dir.path().join("mirrord-config.json");
655
656        let reloader_config = ReloaderConfig {
657            reloader_type: "mirror".to_string(),
658            mirror: Some(MirrorConfig {
659                target: Some(MirrorTargetConfig {
660                    namespace: "gitlab".to_string(),
661                    kubernetes_type: "pod".to_string(),
662                    name: "gitlab-webservice-0".to_string(),
663                    ..Default::default()
664                }),
665                ..Default::default()
666            }),
667            ..Default::default()
668        };
669
670        generate_config(&reloader_config, config_file.to_str().unwrap()).unwrap();
671
672        let cfg = read_config(&config_file);
673        assert_eq!(
674            cfg["feature"]["network"]["outgoing"]["ignore_localhost"],
675            serde_json::Value::Null
676        );
677
678        let data = std::fs::read_to_string(&config_file).unwrap();
679        assert!(!data.contains("ignore_localhost"));
680    }
681
682    // Ported from internal/mirrord/config_test.go:503 (TestGenerateConfig_WithFSMapping)
683    #[test]
684    fn test_generate_config_with_fs_mapping() {
685        let tmp_dir = tempfile::tempdir().unwrap();
686        let config_file = tmp_dir.path().join("mirrord-config.json");
687
688        let mut mapping: OrderedMap<String, String> = OrderedMap::new();
689        mapping.set(
690            r"^/home/(?<user>\S+)/dev/tomcat".to_string(),
691            "/etc/tomcat".to_string(),
692        );
693        mapping.set(
694            r"^/home/(?<user>\S+)/dev/config".to_string(),
695            "/mnt/configs".to_string(),
696        );
697
698        let reloader_config = ReloaderConfig {
699            reloader_type: "mirror".to_string(),
700            mirror: Some(MirrorConfig {
701                target: Some(MirrorTargetConfig {
702                    namespace: "gitlab".to_string(),
703                    kubernetes_type: "pod".to_string(),
704                    name: "gitlab-gitaly-0".to_string(),
705                    ..Default::default()
706                }),
707                filesystem: Some(FilesystemConfig {
708                    mapping: Some(mapping),
709                    ..Default::default()
710                }),
711                ..Default::default()
712            }),
713            ..Default::default()
714        };
715
716        generate_config(&reloader_config, config_file.to_str().unwrap()).unwrap();
717
718        let data = std::fs::read_to_string(&config_file).unwrap();
719        // Named capture groups use < and > which must not be HTML-escaped.
720        assert!(!data.contains(r"\u003c"));
721        assert!(!data.contains(r"\u003e"));
722        assert!(data.contains("(?<user>"));
723
724        let cfg = read_config(&config_file);
725        let got_mapping = &cfg["feature"]["fs"]["mapping"];
726        assert_eq!(got_mapping.as_object().unwrap().len(), 2);
727        assert_eq!(
728            got_mapping[r"^/home/(?<user>\S+)/dev/tomcat"],
729            "/etc/tomcat"
730        );
731        assert_eq!(
732            got_mapping[r"^/home/(?<user>\S+)/dev/config"],
733            "/mnt/configs"
734        );
735    }
736
737    // Ported from internal/mirrord/config_test.go:539 (TestGenerateConfig_WithoutFSMapping)
738    #[test]
739    fn test_generate_config_without_fs_mapping() {
740        let tmp_dir = tempfile::tempdir().unwrap();
741        let config_file = tmp_dir.path().join("mirrord-config.json");
742
743        let reloader_config = ReloaderConfig {
744            reloader_type: "mirror".to_string(),
745            mirror: Some(MirrorConfig {
746                target: Some(MirrorTargetConfig {
747                    namespace: "gitlab".to_string(),
748                    kubernetes_type: "pod".to_string(),
749                    name: "gitlab-gitaly-0".to_string(),
750                    ..Default::default()
751                }),
752                filesystem: Some(FilesystemConfig {
753                    read_write: vec!["^/home/git/repositories".to_string()],
754                    ..Default::default()
755                }),
756                ..Default::default()
757            }),
758            ..Default::default()
759        };
760
761        generate_config(&reloader_config, config_file.to_str().unwrap()).unwrap();
762
763        let data = std::fs::read_to_string(&config_file).unwrap();
764        assert!(!data.contains("\"mapping\""));
765    }
766
767    // Ported from internal/mirrord/config_test.go:564 (TestGenerateConfig_FilePermissions)
768    #[test]
769    fn test_generate_config_file_permissions() {
770        use std::os::unix::fs::PermissionsExt;
771
772        let tmp_dir = tempfile::tempdir().unwrap();
773        let config_file = tmp_dir.path().join("mirrord-config.json");
774
775        let reloader_config = ReloaderConfig {
776            reloader_type: "mirror".to_string(),
777            mirror: Some(MirrorConfig {
778                target: Some(MirrorTargetConfig {
779                    namespace: "gitlab".to_string(),
780                    kubernetes_type: "pod".to_string(),
781                    name: "gitlab-gitaly-0".to_string(),
782                    ..Default::default()
783                }),
784                ..Default::default()
785            }),
786            ..Default::default()
787        };
788
789        generate_config(&reloader_config, config_file.to_str().unwrap()).unwrap();
790
791        let meta = std::fs::metadata(&config_file).unwrap();
792        assert_eq!(meta.permissions().mode() & 0o777, 0o600);
793    }
794
795    // Ported from internal/mirrord/example_test.go:12 (ExampleGenerateConfig)
796    #[test]
797    fn example_generate_config() {
798        let tmp_dir = tempfile::tempdir().unwrap();
799        let config_file = tmp_dir.path().join("example-mirrord-config.json");
800
801        let reloader_config = ReloaderConfig {
802            reloader_type: "mirror".to_string(),
803            mirror: Some(MirrorConfig {
804                target: Some(MirrorTargetConfig {
805                    namespace: "gitlab".to_string(),
806                    kubernetes_type: "pod".to_string(),
807                    name: "gitlab-gitaly-0".to_string(),
808                    ..Default::default()
809                }),
810                filesystem: Some(FilesystemConfig {
811                    read_write: vec!["^/home/git/repositories".to_string()],
812                    read_only: vec![
813                        "^/etc/gitaly".to_string(),
814                        "^/srv/gitlab-shell".to_string(),
815                        "^/etc/gitlab-secrets".to_string(),
816                    ],
817                    ..Default::default()
818                }),
819                ..Default::default()
820            }),
821            ..Default::default()
822        };
823
824        generate_config(&reloader_config, config_file.to_str().unwrap()).unwrap();
825
826        let data = std::fs::read_to_string(&config_file).unwrap();
827
828        let expected = r#"{
829  "target": {
830    "path": "pod/gitlab-gitaly-0",
831    "namespace": "gitlab"
832  },
833  "feature": {
834    "network": {
835      "incoming": {
836        "mode": "steal"
837      },
838      "outgoing": {}
839    },
840    "fs": {
841      "mode": "write",
842      "read_write": [
843        "^/home/git/repositories"
844      ],
845      "read_only": [
846        "^/etc/gitaly",
847        "^/srv/gitlab-shell",
848        "^/etc/gitlab-secrets"
849      ]
850    },
851    "env": {
852      "exclude": [
853        "GODEBUG"
854      ]
855    }
856  },
857  "agent": {
858    "namespace": "gitlab"
859  }
860}
861"#;
862
863        assert_eq!(data, expected);
864    }
865}