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#[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 sha256: 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 {
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 #[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 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}