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#[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 #[clap(long)]
38 insecure: bool,
39
40 #[clap(subcommand)]
41 cmd: Command,
42}
43
44#[derive(Debug, Subcommand)]
45enum OciCommand {
46 ImportLayer {
48 digest: String,
49 name: Option<String>,
50 },
51 LsLayer {
53 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 Transaction,
103 Cat {
105 name: String,
107 },
108 GC,
110 ImportImage {
112 reference: String,
113 },
114 Oci {
116 #[clap(subcommand)]
117 cmd: OciCommand,
118 },
119 Mount {
121 name: String,
123 mountpoint: String,
125 },
126 CreateImage {
128 path: PathBuf,
129 #[clap(long)]
130 bootable: bool,
131 #[clap(long)]
133 no_propagate_usr_to_root: bool,
134 image_name: Option<String>,
135 },
136 ComputeId {
138 path: PathBuf,
139 #[clap(long)]
141 write_dumpfile_to: Option<Utf8PathBuf>,
142 #[clap(long)]
143 bootable: bool,
144 #[clap(long)]
146 no_propagate_usr_to_root: bool,
147 },
148 CreateDumpfile {
150 path: PathBuf,
151 #[clap(long)]
152 bootable: bool,
153 #[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 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}