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#[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#[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
47pub 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
67pub 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
84pub 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 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 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}