Skip to main content

xianyun_infra/
traefik_tlsstore.rs

1//! Embeds the kustomization that wires Traefik's default certificate to
2//! caproni's dev CA, through a TLSStore naming the wildcard certificate
3//! Secret that the dev-CA kustomization's Certificate produces.
4//!
5//! Ported from `internal/infra/assets/traefik-tlsstore/assets.go` and
6//! `tlsstore_overlay_test.go`. See [`crate::dev_ca`]'s module doc for the
7//! `//go:embed` porting decision this module shares.
8//!
9//! It is a separate module from the dev-CA one because a kustomization is
10//! applied atomically: this resource needs the Traefik CRDs to be
11//! installed, while the dev-CA resources need cert-manager's and
12//! trust-manager's. Splitting them lets each be ordered after its own
13//! prerequisite, and lets a cluster running a non-Traefik ingress
14//! controller drop this one without losing the dev CA.
15
16use xianyun_backends::kustomize::FileSystem;
17
18/// Ported from the `//go:embed *.yaml` directive on `traefiktlsstore.FS`.
19pub const FILES: &[(&str, &str)] = &[
20    (
21        "kustomization.yaml",
22        include_str!("traefik_tlsstore_assets/kustomization.yaml"),
23    ),
24    (
25        "tlsstore.yaml",
26        include_str!("traefik_tlsstore_assets/tlsstore.yaml"),
27    ),
28];
29
30/// Ported from `assetstest.WriteFS`, specialized to [`FILES`]. See
31/// [`crate::dev_ca::write_files`] for the porting rationale.
32pub fn write_files(fsys: &dyn FileSystem, dir: &str) -> std::io::Result<()> {
33    for (name, content) in FILES {
34        fsys.write_file(&format!("{dir}/{name}"), content.as_bytes())?;
35    }
36
37    Ok(())
38}
39
40#[cfg(test)]
41mod tests {
42    use std::collections::HashMap;
43    use std::sync::Mutex;
44
45    use xianyun_backends::kustomize::{Backend, Client, FileSystem};
46    use xianyun_executor::exec::Client as ExecClient;
47
48    use super::*;
49
50    /// A minimal in-memory [`FileSystem`], standing in for
51    /// `filesys.MakeFsInMemory()`.
52    #[derive(Default)]
53    struct InMemoryFs {
54        files: Mutex<HashMap<String, Vec<u8>>>,
55    }
56
57    impl FileSystem for InMemoryFs {
58        fn write_file(&self, path: &str, content: &[u8]) -> std::io::Result<()> {
59            self.files
60                .lock()
61                .unwrap()
62                .insert(path.to_string(), content.to_vec());
63            Ok(())
64        }
65
66        fn read_file(&self, path: &str) -> std::io::Result<Vec<u8>> {
67            self.files
68                .lock()
69                .unwrap()
70                .get(path)
71                .cloned()
72                .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, path.to_string()))
73        }
74
75        fn walk(&self, dir: &str) -> std::io::Result<Vec<String>> {
76            let prefix = if dir.ends_with('/') {
77                dir.to_string()
78            } else {
79                format!("{dir}/")
80            };
81            Ok(self
82                .files
83                .lock()
84                .unwrap()
85                .keys()
86                .filter(|path| path.starts_with(&prefix) || *path == dir)
87                .cloned()
88                .collect())
89        }
90    }
91
92    /// Ported from `TestTLSStoreOverlay_Renders`. Renders the embedded
93    /// overlay with the real kustomize backend and asserts it emits
94    /// exactly the TLSStore, wiring Traefik's default certificate to the
95    /// Secret the dev-CA overlay's wildcard Certificate produces.
96    ///
97    /// Nothing is injected into this overlay, unlike the dev-CA one -- no
98    /// CA material and no hostname -- so a render with no preparation
99    /// beyond materializing the embedded files is the whole contract. The
100    /// build runs in process from an in-memory filesystem, so it never
101    /// touches disk or a CLI.
102    #[test]
103    fn tls_store_overlay_renders() {
104        let fsys = InMemoryFs::default();
105        write_files(&fsys, "/overlay").unwrap();
106
107        let backend = Client::with_fs(std::sync::Arc::new(fsys), std::sync::Arc::new(ExecClient));
108
109        let rendered = backend.build("/overlay").unwrap();
110
111        let doc: serde_yaml::Value =
112            serde_yaml::from_slice(&rendered).expect("the overlay must render a single resource");
113
114        assert_eq!(
115            doc["apiVersion"],
116            serde_yaml::Value::from("traefik.io/v1alpha1")
117        );
118        assert_eq!(doc["kind"], serde_yaml::Value::from("TLSStore"));
119
120        let meta = doc.get("metadata").expect("metadata must be present");
121        // Traefik only honors a default certificate from the TLSStore named
122        // "default", and reads it from its own namespace.
123        assert_eq!(meta["name"], serde_yaml::Value::from("default"));
124        assert_eq!(meta["namespace"], serde_yaml::Value::from("kube-system"));
125
126        let spec = doc.get("spec").expect("spec must be present");
127        let default_cert = spec
128            .get("defaultCertificate")
129            .expect("spec.defaultCertificate must be present");
130        assert_eq!(
131            default_cert["secretName"],
132            serde_yaml::Value::from("xianyun-wildcard-tls"),
133            "the default certificate must name the Secret the dev-CA wildcard Certificate issues"
134        );
135    }
136}