libtigen/
lib.rs

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}