symphonize/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::collections::HashMap;
4use std::fs::OpenOptions;
5
6use rc_conf::RcConf;
7use shvar::VariableProvider;
8use utf8path::Path;
9
10/////////////////////////////////////////////// Error //////////////////////////////////////////////
11
12#[derive(Debug)]
13pub enum Error {
14    Io(std::io::Error),
15    RcConf(rc_conf::Error),
16    K8sRc(k8src::Error),
17}
18
19impl From<std::io::Error> for Error {
20    fn from(err: std::io::Error) -> Self {
21        Self::Io(err)
22    }
23}
24
25impl From<rc_conf::Error> for Error {
26    fn from(err: rc_conf::Error) -> Self {
27        Self::RcConf(err)
28    }
29}
30
31impl From<k8src::Error> for Error {
32    fn from(err: k8src::Error) -> Self {
33        Self::K8sRc(err)
34    }
35}
36
37///////////////////////////////////////// SymphonizeOptions ////////////////////////////////////////
38
39/// SymphonizeOptions provides the options to symphonize.  Provide it with a working directory and
40/// release or debug mode.  Only debug mode is supported at the moment.
41#[derive(Clone, Debug, Default, Eq, PartialEq, arrrg_derive::CommandLine)]
42pub struct SymphonizeOptions {
43    #[arrrg(flag, "Do everything up to calling kubectl apply")]
44    dry_run: bool,
45}
46
47///////////////////////////////////// auto_infer_configuration /////////////////////////////////////
48
49pub fn paths_to_root(_options: &SymphonizeOptions) -> Result<Vec<Path<'static>>, std::io::Error> {
50    let mut cwd = Path::try_from(std::env::current_dir()?)
51        .map_err(|_| std::io::Error::other("current working directory not unicode"))?;
52    if !cwd.is_abs() && !cwd.has_root() {
53        return Err(std::io::Error::other("current working directory absolute"));
54    }
55    let mut candidates = vec![];
56    while cwd != Path::from("/") {
57        candidates.push(cwd.clone().into_owned());
58        if cwd.join(".git").exists() {
59            candidates.reverse();
60            return Ok(candidates);
61        }
62        cwd = cwd.dirname().into_owned();
63    }
64    Err(std::io::Error::other("no git directory found"))
65}
66
67////////////////////////////////////// autoinfer_configuration /////////////////////////////////////
68
69/// Automatically infer a Pid1Options from the SymphonizeOptions.
70pub fn autoinfer_configuration(
71    _options: &SymphonizeOptions,
72    paths_to_root: &[Path],
73) -> Result<String, Error> {
74    if paths_to_root.is_empty() {
75        return Err(std::io::Error::other("run symphonize within a git repository").into());
76    }
77    let repo = &paths_to_root[0];
78    let mut rc_conf_path = k8src::rc_conf_path(paths_to_root);
79    rc_conf_path += ":";
80    rc_conf_path += repo.join("rc.local").as_str();
81    Ok(rc_conf_path)
82}
83
84//////////////////////////////////////////// Symphonize ////////////////////////////////////////////
85
86pub struct Symphonize {
87    root: Path<'static>,
88    options: SymphonizeOptions,
89    rc_conf: RcConf,
90}
91
92impl Symphonize {
93    pub fn new(options: SymphonizeOptions, root: Path, rc_conf: RcConf) -> Self {
94        Self {
95            root: root.into_owned(),
96            options,
97            rc_conf,
98        }
99    }
100
101    pub fn apply(&mut self) -> Result<(), Error> {
102        self.build_images()?;
103        self.build_manifests()?;
104        if !self.options.dry_run {
105            self.apply_manifests()?;
106        }
107        Ok(())
108    }
109
110    pub fn build_images(&mut self) -> Result<(), Error> {
111        for containerfile in self.rc_conf.variables() {
112            let Some(prefix) = containerfile.strip_suffix("_CONTAINERFILE") else {
113                continue;
114            };
115            let Some(containerfile) = self.rc_conf.lookup(&containerfile) else {
116                continue;
117            };
118            let mut containerfile = Path::from(containerfile);
119            if containerfile.has_app_defined() {
120                containerfile = self.root.join(&containerfile.as_str()[2..]).into_owned();
121            }
122            let service = rc_conf::service_from_var_name(prefix);
123            let Some(image) = self.rc_conf.lookup_suffix(prefix, "IMAGE") else {
124                todo!();
125            };
126            let extra = HashMap::from_iter([
127                ("IMAGE", image.clone()),
128                ("CONTAINERFILE", containerfile.to_string()),
129                ("CONTAINERFILE_DIRNAME", containerfile.dirname().to_string()),
130            ]);
131            // Build the image.
132            let build = self.rc_conf.argv(&service, "IMAGE_BUILD", &extra)?;
133            if build.is_empty() {
134                todo!();
135            }
136            eprintln!("running {build:?}");
137            let child = std::process::Command::new(&build[0])
138                .args(&build[1..])
139                .spawn()?
140                .wait()?;
141            if !child.success() {
142                todo!();
143            }
144            // Push the image.
145            let push = self.rc_conf.argv(&service, "IMAGE_PUSH", &extra)?;
146            if push.is_empty() {
147                todo!();
148            }
149            eprintln!("running {push:?}");
150            let child = std::process::Command::new(&push[0])
151                .args(&push[1..])
152                .spawn()?
153                .wait()?;
154            if !child.success() {
155                todo!();
156            }
157        }
158        Ok(())
159    }
160
161    pub fn build_manifests(&mut self) -> Result<(), Error> {
162        let options = k8src::RegenerateOptions {
163            output: Some(self.target_dir().join("manifests").as_str().to_string()),
164            root: Some(self.root.as_str().to_string()),
165            overwrite: false,
166            verify: false,
167        };
168        drop(
169            OpenOptions::new()
170                .write(true)
171                .create(true)
172                .truncate(true)
173                .open(self.target_dir().join(".k8srcignore"))?,
174        );
175        if self.target_dir().join("manifests").exists() {
176            std::fs::remove_dir_all(self.target_dir().join("manifests"))?;
177        }
178        k8src::regenerate(options)?;
179        Ok(())
180    }
181
182    pub fn apply_manifests(&mut self) -> Result<(), Error> {
183        let cwd = Path::try_from(std::env::current_dir()?)
184            .map_err(|_| std::io::Error::other("current working directory not unicode"))?;
185        if !cwd.is_abs() && !cwd.has_root() {
186            return Err(std::io::Error::other("current working directory absolute").into());
187        }
188        let Some(relative) = cwd.as_str().strip_prefix(self.root.as_str()) else {
189            todo!();
190        };
191        let relative = relative.trim_start_matches('/');
192        let output = self.target_dir().join("manifests").join(relative);
193        let status = std::process::Command::new("kubectl")
194            .arg("apply")
195            .arg("-k")
196            .arg(output.as_str())
197            .spawn()?
198            .wait()?;
199        if status.success() {
200            Ok(())
201        } else {
202            todo!();
203        }
204    }
205
206    fn target_dir(&self) -> Path<'static> {
207        if let Some(target_dir) = self.rc_conf.lookup("SYMPHONIZE_TARGET_DIR") {
208            let target_dir = Path::from(target_dir);
209            if target_dir.has_app_defined() {
210                self.root.join(&target_dir.as_str()[2..]).into_owned()
211            } else {
212                target_dir
213            }
214        } else {
215            self.root.join("target/symphonize")
216        }
217    }
218}