1pub use error::*;
2use nom::bytes::complete::take_while1;
3use nom::character::complete::char;
4use nom::character::is_alphanumeric;
5use nom::combinator::opt;
6use nom::sequence::tuple;
7use nom::IResult;
8use package_manager::{Apt, Dnf, PackageManager, Pacman, Zypper};
9use std::io;
10use std::process::{Command, Output};
11use std::{
12 fs,
13 io::Write,
14 path::{Path, PathBuf},
15 str::FromStr,
16};
17use tera::{Context, Tera};
18pub mod error;
19pub mod package_manager;
20
21const DOCKERFILE: &str = "Dockerfile";
22const TEMPLATE_DIR: &str = "templates/*";
23
24#[derive(Debug, Clone)]
25pub enum Distro {
26 Ubuntu(Apt),
27 Debian(Apt),
28 Archlinux(Pacman),
29 OpenSuse(Zypper),
30 Fedora(Dnf),
31}
32
33impl Distro {
34 fn run_layer(self) -> String {
35 match self {
36 Self::Archlinux(pm) => run_layer(pm),
37 Self::Debian(pm) => run_layer(pm),
38 Self::Ubuntu(pm) => run_layer(pm),
39 Self::OpenSuse(pm) => run_layer(pm),
40 Self::Fedora(pm) => run_layer(pm),
41 }
42 }
43}
44
45impl FromStr for Distro {
46 type Err = DecodingError;
47
48 fn from_str(s: &str) -> Result<Self, Self::Err> {
49 let distro = match s.to_lowercase().as_str() {
50 "ubuntu" => Distro::Ubuntu(Apt::default()),
51 "debian" => Distro::Debian(Apt::default()),
52 "archlinux" => Distro::Archlinux(Pacman::default()),
53 "opensuse" => Distro::OpenSuse(Zypper::default()),
54 "fedora" => Distro::Fedora(Dnf::default()),
55 _ => unimplemented!("no support for {}", s),
56 };
57 Ok(distro)
58 }
59}
60
61pub trait ImageBuilder {
62 fn build_image(&self, image: &ImageMetadata<'_>) -> Result<Output, Error>;
63}
64
65pub struct Docker {
66 bin: String,
67}
68
69impl Docker {
70 fn new() -> Self {
71 Self {
72 bin: "docker".to_string(),
73 }
74 }
75}
76
77impl Default for Docker {
78 fn default() -> Self {
79 Self::new()
80 }
81}
82
83impl ImageBuilder for Docker {
84 fn build_image(&self, image: &ImageMetadata<'_>) -> Result<Output, Error> {
85 let s = image.dir().join(Path::new("Dockerfile"));
86 let child = Command::new(&self.bin)
87 .args(vec![
88 "build",
89 "-f",
90 s.to_str().unwrap(),
91 "-t",
92 &image.name(),
93 ".",
94 ])
95 .spawn()?;
96 let output = child.wait_with_output()?;
97 Ok(output)
98 }
99}
100
101pub struct Podman {
102 bin: String,
103}
104
105impl Podman {
106 fn new() -> Self {
107 Self {
108 bin: "podman".to_string(),
109 }
110 }
111}
112
113impl Default for Podman {
114 fn default() -> Self {
115 Self::new()
116 }
117}
118
119impl ImageBuilder for Podman {
120 fn build_image(&self, image: &ImageMetadata<'_>) -> Result<Output, Error> {
121 let output = Command::new(&self.bin)
122 .args(vec![
123 "build",
124 "-t",
125 &image.name(),
126 "-",
127 "<",
128 &image.dockerfile,
129 ])
130 .output()?;
131 Ok(output)
132 }
133}
134
135pub fn run_layer(package_manager: impl PackageManager) -> String {
136 let update = package_manager.update().join(" ");
137 let upgrade = package_manager.upgrade().join(" ");
138 let packages = vec!["sudo"];
139 let install = package_manager.install(packages.into_iter()).join(" ");
140 format!("{} && {} && {}", update, upgrade, install)
141}
142
143#[derive(Debug)]
144pub struct ImageMetadata<'a> {
145 image: &'a ImageName<'a>,
146 dockerfile: String,
147}
148
149impl<'a> ImageMetadata<'a> {
150 pub fn try_new(image: &'a ImageName<'a>) -> Result<Self, Error> {
151 let dist = Distro::from_str(image.name)?;
152 let tera = Tera::new(TEMPLATE_DIR)?;
153
154 let mut context = Context::new();
155 context.insert("name", image.name);
156 context.insert("version", image.tag);
157 context.insert("run_layer", &dist.run_layer());
158
159 let dockerfile = tera.render(DOCKERFILE, &context)?;
160 Ok(Self { image, dockerfile })
161 }
162
163 fn dir(&self) -> PathBuf {
164 PathBuf::from(format!("images/{}/{}", self.image.name, self.image.tag))
165 }
166
167 fn name(&self) -> String {
168 self.image.to_string()
169 }
170}
171
172pub fn write_dockerfile(image: &ImageMetadata<'_>) -> Result<(), Error> {
173 let path = image.dir();
174 fs::create_dir_all(&path)?;
175 let mut file = fs::File::create(path.join(Path::new("Dockerfile")))?;
176 file.write_all(image.dockerfile.as_bytes())?;
177 Ok(())
178}
179
180pub fn build_image(builder: impl ImageBuilder, image: &ImageMetadata<'_>) -> Result<(), Error> {
181 let output = builder.build_image(image)?;
182 io::stdout().write_all(&output.stdout)?;
183 io::stderr().write_all(&output.stderr)?;
184 Ok(())
185}
186
187#[derive(Debug)]
188pub struct ImageName<'a> {
189 name: &'a str,
190 tag: &'a str,
191}
192
193fn word(input: &str) -> IResult<&str, &str> {
194 take_while1(|c: char| is_alphanumeric(c as u8) || c == '-' || c == '_' || c == '.')(input)
195}
196
197impl<'a> ImageName<'a> {
198 pub fn parse(s: &'a str) -> Result<Self, Error> {
199 let (tag, name) = word(s).map_err(|e| Error::Nom(e.to_string()))?;
200
201 let (_, tag) = opt(tuple((char(':'), word)))(tag).map_err(|e| Error::Nom(e.to_string()))?;
202 Ok(Self {
203 name,
204 tag: tag.map(|(_, t)| t).unwrap_or("latest"),
205 })
206 }
207}
208
209impl<'a> ToString for ImageName<'a> {
210 fn to_string(&self) -> String {
211 format!("{}:{}", self.name, self.tag)
212 }
213}
214
215#[cfg(test)]
216mod test {
217 use std::vec;
218
219 use super::*;
220
221 #[test]
222 fn parse_image_name() {
223 struct Test<'a> {
224 args: &'a str,
225 expected: &'a str,
226 }
227
228 let images = vec![
229 Test {
230 args: "ubuntu:20.04",
231 expected: "ubuntu:20.04",
232 },
233 Test {
234 args: "archlinux",
235 expected: "archlinux:latest",
236 },
237 Test {
238 args: "fedora:37",
239 expected: "fedora:37",
240 },
241 Test {
242 args: "ubuntu:lunar-20230301",
243 expected: "ubuntu:lunar-20230301",
244 },
245 ];
246
247 for image in images {
248 let parsed = ImageName::parse(image.args).expect("A valid image name");
249 assert_eq!(image.expected, &parsed.to_string())
250 }
251 }
252}