bootc_lib/
cfsctl.rs

1use std::{
2    ffi::OsString,
3    fs::{create_dir_all, File},
4    io::BufWriter,
5    path::{Path, PathBuf},
6    sync::Arc,
7};
8
9use anyhow::{Context, Result};
10use camino::Utf8PathBuf;
11use clap::{Parser, Subcommand};
12
13use rustix::fs::CWD;
14
15use composefs_boot::{write_boot, BootOps};
16
17use composefs::{
18    dumpfile,
19    fsverity::{FsVerityHashValue, Sha512HashValue},
20    repository::Repository,
21};
22
23/// cfsctl
24#[derive(Debug, Parser)]
25#[clap(name = "cfsctl", version)]
26pub struct App {
27    #[clap(long, group = "repopath")]
28    repo: Option<PathBuf>,
29    #[clap(long, group = "repopath")]
30    user: bool,
31    #[clap(long, group = "repopath")]
32    system: bool,
33
34    /// Sets the repository to insecure before running any operation and
35    /// prepend '?' to the composefs kernel command line when writing
36    /// boot entry.
37    #[clap(long)]
38    insecure: bool,
39
40    #[clap(subcommand)]
41    cmd: Command,
42}
43
44#[derive(Debug, Subcommand)]
45enum OciCommand {
46    /// Stores a tar file as a splitstream in the repository.
47    ImportLayer {
48        sha256: String,
49        name: Option<String>,
50    },
51    /// Lists the contents of a tar stream
52    LsLayer {
53        /// the name of the stream
54        name: String,
55    },
56    Dump {
57        config_name: String,
58        config_verity: Option<String>,
59    },
60    Pull {
61        image: String,
62        name: Option<String>,
63    },
64    ComputeId {
65        config_name: String,
66        config_verity: Option<String>,
67        #[clap(long)]
68        bootable: bool,
69    },
70    CreateImage {
71        config_name: String,
72        config_verity: Option<String>,
73        #[clap(long)]
74        bootable: bool,
75        #[clap(long)]
76        image_name: Option<String>,
77    },
78    Seal {
79        config_name: String,
80        config_verity: Option<String>,
81    },
82    Mount {
83        name: String,
84        mountpoint: String,
85    },
86    PrepareBoot {
87        config_name: String,
88        config_verity: Option<String>,
89        #[clap(long, default_value = "/boot")]
90        bootdir: PathBuf,
91        #[clap(long)]
92        entry_id: Option<String>,
93        #[clap(long)]
94        cmdline: Vec<String>,
95    },
96}
97
98#[derive(Debug, Subcommand)]
99enum Command {
100    /// Take a transaction lock on the repository.
101    /// This prevents garbage collection from occurring.
102    Transaction,
103    /// Reconstitutes a split stream and writes it to stdout
104    Cat {
105        /// the name of the stream to cat, either a sha256 digest or prefixed with 'ref/'
106        name: String,
107    },
108    /// Perform garbage collection
109    GC,
110    /// Imports a composefs image (unsafe!)
111    ImportImage {
112        reference: String,
113    },
114    /// Commands for dealing with OCI layers
115    Oci {
116        #[clap(subcommand)]
117        cmd: OciCommand,
118    },
119    /// Mounts a composefs, possibly enforcing fsverity of the image
120    Mount {
121        /// the name of the image to mount, either a sha256 digest or prefixed with 'ref/'
122        name: String,
123        /// the mountpoint
124        mountpoint: String,
125    },
126    CreateImage {
127        path: PathBuf,
128        #[clap(long)]
129        bootable: bool,
130        #[clap(long)]
131        stat_root: bool,
132        image_name: Option<String>,
133    },
134    ComputeId {
135        path: PathBuf,
136        /// Write the dumpfile to the provided target
137        #[clap(long)]
138        write_dumpfile_to: Option<Utf8PathBuf>,
139        #[clap(long)]
140        bootable: bool,
141        #[clap(long)]
142        stat_root: bool,
143    },
144    CreateDumpfile {
145        path: PathBuf,
146        #[clap(long)]
147        bootable: bool,
148        #[clap(long)]
149        stat_root: bool,
150    },
151    ImageObjects {
152        name: String,
153    },
154}
155
156fn verity_opt(opt: &Option<String>) -> Result<Option<Sha512HashValue>> {
157    Ok(opt.as_ref().map(FsVerityHashValue::from_hex).transpose()?)
158}
159
160pub(crate) async fn run_from_iter<I>(args: I) -> Result<()>
161where
162    I: IntoIterator,
163    I::Item: Into<OsString> + Clone,
164{
165    let args = App::parse_from(
166        std::iter::once(OsString::from("cfs")).chain(args.into_iter().map(Into::into)),
167    );
168
169    let repo = if let Some(path) = &args.repo {
170        let mut r = Repository::open_path(CWD, path)?;
171        r.set_insecure(args.insecure);
172        Arc::new(r)
173    } else if args.user {
174        let mut r = Repository::open_user()?;
175        r.set_insecure(args.insecure);
176        Arc::new(r)
177    } else {
178        if args.insecure {
179            anyhow::bail!("Cannot override insecure state for system repo");
180        }
181        let system_store = crate::cli::get_storage().await?;
182        system_store.get_ensure_composefs()?
183    };
184    let repo = &repo;
185
186    match args.cmd {
187        Command::Transaction => {
188            // just wait for ^C
189            loop {
190                std::thread::park();
191            }
192        }
193        Command::Cat { name } => {
194            repo.merge_splitstream(&name, None, &mut std::io::stdout())?;
195        }
196        Command::ImportImage { reference } => {
197            let image_id = repo.import_image(&reference, &mut std::io::stdin())?;
198            println!("{}", image_id.to_id());
199        }
200        Command::Oci { cmd: oci_cmd } => match oci_cmd {
201            OciCommand::ImportLayer { name, sha256 } => {
202                let object_id = composefs_oci::import_layer(
203                    &repo,
204                    &composefs::util::parse_sha256(sha256)?,
205                    name.as_deref(),
206                    &mut std::io::stdin(),
207                )?;
208                println!("{}", object_id.to_id());
209            }
210            OciCommand::LsLayer { name } => {
211                composefs_oci::ls_layer(&repo, &name)?;
212            }
213            OciCommand::Dump {
214                ref config_name,
215                ref config_verity,
216            } => {
217                let verity = verity_opt(config_verity)?;
218                let mut fs =
219                    composefs_oci::image::create_filesystem(&repo, config_name, verity.as_ref())?;
220                fs.print_dumpfile()?;
221            }
222            OciCommand::ComputeId {
223                ref config_name,
224                ref config_verity,
225                bootable,
226            } => {
227                let verity = verity_opt(config_verity)?;
228                let mut fs =
229                    composefs_oci::image::create_filesystem(&repo, config_name, verity.as_ref())?;
230                if bootable {
231                    fs.transform_for_boot(&repo)?;
232                }
233                let id = fs.compute_image_id();
234                println!("{}", id.to_hex());
235            }
236            OciCommand::CreateImage {
237                ref config_name,
238                ref config_verity,
239                bootable,
240                ref image_name,
241            } => {
242                let verity = verity_opt(config_verity)?;
243                let mut fs =
244                    composefs_oci::image::create_filesystem(&repo, config_name, verity.as_ref())?;
245                if bootable {
246                    fs.transform_for_boot(&repo)?;
247                }
248                let image_id = fs.commit_image(&repo, image_name.as_deref())?;
249                println!("{}", image_id.to_id());
250            }
251            OciCommand::Pull { ref image, name } => {
252                let (sha256, verity) =
253                    composefs_oci::pull(&repo, image, name.as_deref(), None).await?;
254
255                println!("sha256 {}", hex::encode(sha256));
256                println!("verity {}", verity.to_hex());
257            }
258            OciCommand::Seal {
259                ref config_name,
260                ref config_verity,
261            } => {
262                let verity = verity_opt(config_verity)?;
263                let (sha256, verity) = composefs_oci::seal(&repo, config_name, verity.as_ref())?;
264                println!("sha256 {}", hex::encode(sha256));
265                println!("verity {}", verity.to_id());
266            }
267            OciCommand::Mount {
268                ref name,
269                ref mountpoint,
270            } => {
271                composefs_oci::mount(&repo, name, mountpoint, None)?;
272            }
273            OciCommand::PrepareBoot {
274                ref config_name,
275                ref config_verity,
276                ref bootdir,
277                ref entry_id,
278                ref cmdline,
279            } => {
280                let verity = verity_opt(config_verity)?;
281                let mut fs =
282                    composefs_oci::image::create_filesystem(&repo, config_name, verity.as_ref())?;
283                let entries = fs.transform_for_boot(&repo)?;
284                let id = fs.commit_image(&repo, None)?;
285
286                let Some(entry) = entries.into_iter().next() else {
287                    anyhow::bail!("No boot entries!");
288                };
289
290                let cmdline_refs: Vec<&str> = cmdline.iter().map(String::as_str).collect();
291                write_boot::write_boot_simple(
292                    &repo,
293                    entry,
294                    &id,
295                    args.insecure,
296                    bootdir,
297                    None,
298                    entry_id.as_deref(),
299                    &cmdline_refs,
300                )?;
301
302                let state = args
303                    .repo
304                    .as_ref()
305                    .map(|p: &PathBuf| p.parent().unwrap_or(p))
306                    .unwrap_or(Path::new("/sysroot"))
307                    .join("state/deploy")
308                    .join(id.to_hex());
309
310                create_dir_all(state.join("var"))?;
311                create_dir_all(state.join("etc/upper"))?;
312                create_dir_all(state.join("etc/work"))?;
313            }
314        },
315        Command::ComputeId {
316            ref path,
317            write_dumpfile_to,
318            bootable,
319            stat_root,
320        } => {
321            let mut fs = composefs::fs::read_filesystem(CWD, path, Some(&repo), stat_root)?;
322            if bootable {
323                fs.transform_for_boot(&repo)?;
324            }
325            let id = fs.compute_image_id();
326            println!("{}", id.to_hex());
327            if let Some(path) = write_dumpfile_to.as_deref() {
328                let mut w = File::create(path)
329                    .with_context(|| format!("Opening {path}"))
330                    .map(BufWriter::new)?;
331                dumpfile::write_dumpfile(&mut w, &fs).context("Writing dumpfile")?;
332            }
333        }
334        Command::CreateImage {
335            ref path,
336            bootable,
337            stat_root,
338            ref image_name,
339        } => {
340            let mut fs = composefs::fs::read_filesystem(CWD, path, Some(&repo), stat_root)?;
341            if bootable {
342                fs.transform_for_boot(&repo)?;
343            }
344            let id = fs.commit_image(&repo, image_name.as_deref())?;
345            println!("{}", id.to_id());
346        }
347        Command::CreateDumpfile {
348            ref path,
349            bootable,
350            stat_root,
351        } => {
352            let mut fs = composefs::fs::read_filesystem(CWD, path, Some(&repo), stat_root)?;
353            if bootable {
354                fs.transform_for_boot(&repo)?;
355            }
356            fs.print_dumpfile()?;
357        }
358        Command::Mount { name, mountpoint } => {
359            repo.mount_at(&name, &mountpoint)?;
360        }
361        Command::ImageObjects { name } => {
362            let objects = repo.objects_for_image(&name)?;
363            for object in objects {
364                println!("{}", object.to_id());
365            }
366        }
367        Command::GC => {
368            repo.gc()?;
369        }
370    }
371    Ok(())
372}