bootc_lib/
cfsctl.rs

1use std::{
2    ffi::OsString,
3    fs::{File, create_dir_all},
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::{BootOps, write_boot};
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        digest: 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 content identifier 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 an fs-verity hash or prefixed with 'ref/'
122        name: String,
123        /// the mountpoint
124        mountpoint: String,
125    },
126    /// Creates a composefs image from a filesystem
127    CreateImage {
128        path: PathBuf,
129        #[clap(long)]
130        bootable: bool,
131        /// Don't copy /usr metadata to root directory (use if root already has well-defined metadata)
132        #[clap(long)]
133        no_propagate_usr_to_root: bool,
134        image_name: Option<String>,
135    },
136    /// Computes the composefs image ID for a filesystem
137    ComputeId {
138        path: PathBuf,
139        /// Write the dumpfile to the provided target
140        #[clap(long)]
141        write_dumpfile_to: Option<Utf8PathBuf>,
142        #[clap(long)]
143        bootable: bool,
144        /// Don't copy /usr metadata to root directory (use if root already has well-defined metadata)
145        #[clap(long)]
146        no_propagate_usr_to_root: bool,
147    },
148    /// Outputs the composefs dumpfile format for a filesystem
149    CreateDumpfile {
150        path: PathBuf,
151        #[clap(long)]
152        bootable: bool,
153        /// Don't copy /usr metadata to root directory (use if root already has well-defined metadata)
154        #[clap(long)]
155        no_propagate_usr_to_root: bool,
156    },
157    ImageObjects {
158        name: String,
159    },
160}
161
162fn verity_opt(opt: &Option<String>) -> Result<Option<Sha512HashValue>> {
163    Ok(opt.as_ref().map(FsVerityHashValue::from_hex).transpose()?)
164}
165
166pub(crate) async fn run_from_iter<I>(args: I) -> Result<()>
167where
168    I: IntoIterator,
169    I::Item: Into<OsString> + Clone,
170{
171    let args = App::parse_from(
172        std::iter::once(OsString::from("cfs")).chain(args.into_iter().map(Into::into)),
173    );
174
175    let repo = if let Some(path) = &args.repo {
176        let mut r = Repository::open_path(CWD, path)?;
177        r.set_insecure(args.insecure);
178        Arc::new(r)
179    } else if args.user {
180        let mut r = Repository::open_user()?;
181        r.set_insecure(args.insecure);
182        Arc::new(r)
183    } else {
184        if args.insecure {
185            anyhow::bail!("Cannot override insecure state for system repo");
186        }
187        let system_store = crate::cli::get_storage().await?;
188        system_store.get_ensure_composefs()?
189    };
190    let repo = &repo;
191
192    match args.cmd {
193        Command::Transaction => {
194            // just wait for ^C
195            loop {
196                std::thread::park();
197            }
198        }
199        Command::Cat { name } => {
200            repo.merge_splitstream(&name, None, None, &mut std::io::stdout())?;
201        }
202        Command::ImportImage { reference } => {
203            let image_id = repo.import_image(&reference, &mut std::io::stdin())?;
204            println!("{}", image_id.to_id());
205        }
206        Command::Oci { cmd: oci_cmd } => match oci_cmd {
207            OciCommand::ImportLayer { name, digest } => {
208                let object_id = composefs_oci::import_layer(
209                    repo,
210                    &digest,
211                    name.as_deref(),
212                    &mut std::io::stdin(),
213                )?;
214                println!("{}", object_id.to_id());
215            }
216            OciCommand::LsLayer { name } => {
217                composefs_oci::ls_layer(repo, &name)?;
218            }
219            OciCommand::Dump {
220                ref config_name,
221                ref config_verity,
222            } => {
223                let verity = verity_opt(config_verity)?;
224                let fs =
225                    composefs_oci::image::create_filesystem(repo, config_name, verity.as_ref())?;
226                fs.print_dumpfile()?;
227            }
228            OciCommand::ComputeId {
229                ref config_name,
230                ref config_verity,
231                bootable,
232            } => {
233                let verity = verity_opt(config_verity)?;
234                let mut fs =
235                    composefs_oci::image::create_filesystem(repo, config_name, verity.as_ref())?;
236                if bootable {
237                    fs.transform_for_boot(repo)?;
238                }
239                let id = fs.compute_image_id();
240                println!("{}", id.to_hex());
241            }
242            OciCommand::CreateImage {
243                ref config_name,
244                ref config_verity,
245                bootable,
246                ref image_name,
247            } => {
248                let verity = verity_opt(config_verity)?;
249                let mut fs =
250                    composefs_oci::image::create_filesystem(repo, config_name, verity.as_ref())?;
251                if bootable {
252                    fs.transform_for_boot(repo)?;
253                }
254                let image_id = fs.commit_image(repo, image_name.as_deref())?;
255                println!("{}", image_id.to_id());
256            }
257            OciCommand::Pull { ref image, name } => {
258                let (digest, verity) =
259                    composefs_oci::pull(repo, image, name.as_deref(), None).await?;
260
261                println!("config {digest}");
262                println!("verity {}", verity.to_hex());
263            }
264            OciCommand::Seal {
265                ref config_name,
266                ref config_verity,
267            } => {
268                let verity = verity_opt(config_verity)?;
269                let (digest, verity) = composefs_oci::seal(repo, config_name, verity.as_ref())?;
270                println!("config {digest}");
271                println!("verity {}", verity.to_id());
272            }
273            OciCommand::Mount {
274                ref name,
275                ref mountpoint,
276            } => {
277                composefs_oci::mount(repo, name, mountpoint, None)?;
278            }
279            OciCommand::PrepareBoot {
280                ref config_name,
281                ref config_verity,
282                ref bootdir,
283                ref entry_id,
284                ref cmdline,
285            } => {
286                let verity = verity_opt(config_verity)?;
287                let mut fs =
288                    composefs_oci::image::create_filesystem(repo, config_name, verity.as_ref())?;
289                let entries = fs.transform_for_boot(repo)?;
290                let id = fs.commit_image(repo, None)?;
291
292                let Some(entry) = entries.into_iter().next() else {
293                    anyhow::bail!("No boot entries!");
294                };
295
296                let cmdline_refs: Vec<&str> = cmdline.iter().map(String::as_str).collect();
297                write_boot::write_boot_simple(
298                    repo,
299                    entry,
300                    &id,
301                    args.insecure,
302                    bootdir,
303                    None,
304                    entry_id.as_deref(),
305                    &cmdline_refs,
306                )?;
307
308                let state = args
309                    .repo
310                    .as_ref()
311                    .map(|p: &PathBuf| p.parent().unwrap())
312                    .unwrap_or(Path::new("/sysroot"))
313                    .join("state/deploy")
314                    .join(id.to_hex());
315
316                create_dir_all(state.join("var"))?;
317                create_dir_all(state.join("etc/upper"))?;
318                create_dir_all(state.join("etc/work"))?;
319            }
320        },
321        Command::ComputeId {
322            ref path,
323            write_dumpfile_to,
324            bootable,
325            no_propagate_usr_to_root,
326        } => {
327            let mut fs = if no_propagate_usr_to_root {
328                composefs::fs::read_filesystem(CWD, path, Some(repo.as_ref()))?
329            } else {
330                composefs::fs::read_container_root(CWD, path, Some(repo.as_ref()))?
331            };
332            if bootable {
333                fs.transform_for_boot(repo)?;
334            }
335            let id = fs.compute_image_id();
336            println!("{}", id.to_hex());
337            if let Some(path) = write_dumpfile_to.as_deref() {
338                let mut w = File::create(path)
339                    .with_context(|| format!("Opening {path}"))
340                    .map(BufWriter::new)?;
341                dumpfile::write_dumpfile(&mut w, &fs).context("Writing dumpfile")?;
342            }
343        }
344        Command::CreateImage {
345            ref path,
346            bootable,
347            no_propagate_usr_to_root,
348            ref image_name,
349        } => {
350            let mut fs = if no_propagate_usr_to_root {
351                composefs::fs::read_filesystem(CWD, path, Some(repo.as_ref()))?
352            } else {
353                composefs::fs::read_container_root(CWD, path, Some(repo.as_ref()))?
354            };
355            if bootable {
356                fs.transform_for_boot(repo)?;
357            }
358            let id = fs.commit_image(repo, image_name.as_deref())?;
359            println!("{}", id.to_id());
360        }
361        Command::CreateDumpfile {
362            ref path,
363            bootable,
364            no_propagate_usr_to_root,
365        } => {
366            let mut fs = if no_propagate_usr_to_root {
367                composefs::fs::read_filesystem(CWD, path, Some(repo.as_ref()))?
368            } else {
369                composefs::fs::read_container_root(CWD, path, Some(repo.as_ref()))?
370            };
371            if bootable {
372                fs.transform_for_boot(repo)?;
373            }
374            fs.print_dumpfile()?;
375        }
376        Command::Mount { name, mountpoint } => {
377            repo.mount_at(&name, &mountpoint)?;
378        }
379        Command::ImageObjects { name } => {
380            let objects = repo.objects_for_image(&name)?;
381            for object in objects {
382                println!("{}", object.to_id());
383            }
384        }
385        Command::GC => {
386            repo.gc()?;
387        }
388    }
389    Ok(())
390}