rpmoci/
lib.rs

1#![deny(missing_docs)]
2//! Create container images using DNF
3//!
4//! Copyright (C) Microsoft Corporation.
5//!
6//! This program is free software: you can redistribute it and/or modify
7//! it under the terms of the GNU General Public License as published by
8//! the Free Software Foundation, either version 3 of the License, or
9//! (at your option) any later version.
10//!
11//! This program is distributed in the hope that it will be useful,
12//! but WITHOUT ANY WARRANTY; without even the implied warranty of
13//! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14//! GNU General Public License for more details.
15//!
16//! You should have received a copy of the GNU General Public License
17//! along with this program.  If not, see <https://www.gnu.org/licenses/>.
18use std::{
19    fs,
20    path::{Path, PathBuf},
21    time::Instant,
22};
23
24use anyhow::{Context, bail};
25mod archive;
26pub mod cli;
27pub mod config;
28pub mod lockfile;
29pub mod write;
30use anyhow::Result;
31use cli::Command;
32use config::Config;
33use lockfile::Lockfile;
34
35pub(crate) const NAME: &str = "rpmoci";
36
37fn load_config_and_lock_file(
38    config_file: impl AsRef<Path>,
39) -> Result<(Config, PathBuf, Result<Option<Lockfile>>)> {
40    let config_file = config_file.as_ref();
41    let contents = std::fs::read_to_string(config_file)
42        .context(format!("Failed to read `{}`", config_file.display()))?;
43    let cfg: Config = toml::from_str(&contents)?;
44    let mut lockfile_path = PathBuf::from(config_file);
45    lockfile_path.set_extension("lock");
46    Ok((cfg, lockfile_path.clone(), read_lockfile(&lockfile_path)))
47}
48
49fn read_lockfile(lockfile: impl AsRef<Path>) -> Result<Option<Lockfile>> {
50    match std::fs::read_to_string(lockfile) {
51        Ok(d) => Ok(Some(toml::from_str(&d).context("Invalid lockfile")?)),
52        Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
53        Err(e) => Err(e.into()),
54    }
55}
56
57/// Run rpmoci
58pub fn main(command: Command) -> anyhow::Result<()> {
59    match command {
60        Command::Update {
61            manifest_path,
62            from_lockfile,
63        } => {
64            let (cfg, lockfile_path, existing_lockfile) = load_config_and_lock_file(manifest_path)?;
65
66            let lockfile = if let Ok(Some(lockfile)) = &existing_lockfile {
67                if lockfile.is_compatible_excluding_local_rpms(&cfg) && from_lockfile {
68                    lockfile.resolve_from_previous(&cfg)?
69                } else {
70                    if from_lockfile {
71                        bail!(
72                            "the lock file is not up-to-date. Use of --from-lockfile requires that the lock file is up-to-date"
73                        );
74                    }
75                    Lockfile::resolve_from_config(&cfg)?
76                }
77            } else {
78                if from_lockfile {
79                    bail!(
80                        "the lock file is not up-to-date. Use of --from-lockfile requires that the lock file is up-to-date"
81                    );
82                }
83                Lockfile::resolve_from_config(&cfg)?
84            };
85
86            lockfile.print_updates(existing_lockfile.unwrap_or_default().as_ref())?;
87            lockfile.write_to_file(lockfile_path)?;
88        }
89        Command::Build {
90            locked,
91            image,
92            tag,
93            vendor_dir,
94            manifest_path,
95            label,
96        } => {
97            let now = Instant::now();
98            let mut changed = false;
99            let (cfg, lockfile_path, existing_lockfile) = load_config_and_lock_file(manifest_path)?;
100            let lockfile = match (existing_lockfile, locked) {
101                (Ok(Some(lockfile)), true) => {
102                    // TODO: consider whether this can move to including local RPMs. (Subtlety here is that may
103                    // break scenarios where the user is using local RPMs that have a subset of the locked local RPM dependencies.)
104                    if !lockfile.is_compatible_excluding_local_rpms(&cfg) {
105                        bail!(format!(
106                            "the lock file {} needs to be updated but --locked was passed to prevent this",
107                            lockfile_path.display()
108                        ));
109                    }
110                    lockfile
111                }
112                (Ok(Some(lockfile)), false) => {
113                    if lockfile.is_compatible_including_local_rpms(&cfg)? {
114                        // Compatible lockfile, use it
115                        lockfile
116                    } else {
117                        // Incompatible lockfile, update it
118                        changed = true;
119                        write::ok(
120                            "Generating",
121                            format!(
122                                "new lock file. The existing lock file {} is not up-to-date.",
123                                lockfile_path.display()
124                            ),
125                        )?;
126                        Lockfile::resolve_from_config(&cfg)?
127                    }
128                }
129                (Err(err), false) => {
130                    write::error(
131                        "Warning",
132                        format!(
133                            "failed to parse existing lock file. Generating a new one. Error: {err}"
134                        ),
135                    )?;
136                    err.chain()
137                        .skip(1)
138                        .for_each(|cause| eprintln!("caused by: {cause}"));
139                    changed = true;
140                    Lockfile::resolve_from_config(&cfg)?
141                }
142                (Err(err), true) => {
143                    return Err(err.context(format!(
144                        "failed to parse existing lock file {}",
145                        lockfile_path.display()
146                    )));
147                }
148                (Ok(None), true) => {
149                    bail!(format!(
150                        "the lock file {} is missing and needs to be generated but --locked was passed to prevent this",
151                        lockfile_path.display()
152                    ))
153                }
154                (Ok(None), false) => {
155                    changed = true;
156                    Lockfile::resolve_from_config(&cfg)?
157                }
158            };
159
160            if changed {
161                lockfile.write_to_file(lockfile_path)?;
162            }
163
164            lockfile.build(
165                &cfg,
166                &image,
167                &tag,
168                vendor_dir.as_deref(),
169                label.into_iter().collect(),
170            )?;
171            let elapsed_time = now.elapsed();
172            write::ok(
173                "Success",
174                format!(
175                    "image '{}:{}' created in {:2}s",
176                    image,
177                    tag,
178                    elapsed_time.as_secs_f32()
179                ),
180            )?;
181        }
182        Command::Vendor {
183            out_dir,
184            manifest_path,
185        } => {
186            fs::create_dir_all(&out_dir).context("Failed to create vendor directory")?;
187            let (cfg, _lockfile_path, existing_lockfile) =
188                load_config_and_lock_file(manifest_path)?;
189
190            if let Ok(Some(lockfile)) = existing_lockfile {
191                if lockfile.is_compatible_excluding_local_rpms(&cfg) {
192                    lockfile.download_rpms(&cfg, &out_dir)?;
193                    lockfile.check_gpg_keys(&out_dir)?;
194                } else {
195                    bail!(
196                        "Lockfile out of date. `vendor` can only be run with a compatible lockfile"
197                    )
198                }
199            } else {
200                bail!(
201                    "No valid lockfile found. `vendor` can only be run with a compatible lockfile"
202                )
203            }
204        }
205    }
206    Ok(())
207}