Skip to content

Commit 2a8007f

Browse files
cfsctl: turn it into a library
bootc is embedding this. Let's make it a library. Let's also make sure it works with SHA-512, which is what bootc is using. Signed-off-by: Allison Karlitskaya <[email protected]>
1 parent 462c747 commit 2a8007f

File tree

2 files changed

+382
-363
lines changed

2 files changed

+382
-363
lines changed

crates/cfsctl/src/lib.rs

Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
//! Command-line control utility for composefs repositories and images.
2+
//!
3+
//! `cfsctl` provides a comprehensive interface for managing composefs repositories,
4+
//! creating and mounting filesystem images, handling OCI containers, and performing
5+
//! repository maintenance operations like garbage collection.
6+
7+
use std::{
8+
fs::create_dir_all,
9+
path::{Path, PathBuf},
10+
sync::Arc,
11+
};
12+
13+
use anyhow::Result;
14+
use clap::{Parser, Subcommand};
15+
16+
use rustix::fs::CWD;
17+
18+
use composefs_boot::{write_boot, BootOps};
19+
20+
use composefs::{fsverity::FsVerityHashValue, repository::Repository};
21+
22+
/// cfsctl
23+
#[derive(Debug, Parser)]
24+
#[clap(name = "cfsctl", version)]
25+
pub struct App {
26+
#[clap(long, group = "repopath")]
27+
repo: Option<PathBuf>,
28+
#[clap(long, group = "repopath")]
29+
user: bool,
30+
#[clap(long, group = "repopath")]
31+
system: bool,
32+
33+
/// Sets the repository to insecure before running any operation and
34+
/// prepend '?' to the composefs kernel command line when writing
35+
/// boot entry.
36+
#[clap(long)]
37+
insecure: bool,
38+
39+
#[clap(subcommand)]
40+
cmd: Command,
41+
}
42+
43+
#[cfg(feature = "oci")]
44+
#[derive(Debug, Subcommand)]
45+
enum 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)]
99+
enum 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+
#[cfg(feature = "oci")]
116+
Oci {
117+
#[clap(subcommand)]
118+
cmd: OciCommand,
119+
},
120+
/// Mounts a composefs, possibly enforcing fsverity of the image
121+
Mount {
122+
/// the name of the image to mount, either an fs-verity hash or prefixed with 'ref/'
123+
name: String,
124+
/// the mountpoint
125+
mountpoint: String,
126+
},
127+
CreateImage {
128+
path: PathBuf,
129+
#[clap(long)]
130+
bootable: bool,
131+
#[clap(long)]
132+
stat_root: bool,
133+
image_name: Option<String>,
134+
},
135+
ComputeId {
136+
path: PathBuf,
137+
#[clap(long)]
138+
bootable: bool,
139+
#[clap(long)]
140+
stat_root: bool,
141+
},
142+
CreateDumpfile {
143+
path: PathBuf,
144+
#[clap(long)]
145+
bootable: bool,
146+
#[clap(long)]
147+
stat_root: bool,
148+
},
149+
ImageObjects {
150+
name: String,
151+
},
152+
#[cfg(feature = "http")]
153+
Fetch {
154+
url: String,
155+
name: String,
156+
},
157+
}
158+
159+
fn verity_opt<ObjectId: FsVerityHashValue>(opt: &Option<String>) -> Result<Option<ObjectId>> {
160+
Ok(match opt {
161+
Some(value) => Some(FsVerityHashValue::from_hex(value)?),
162+
None => None,
163+
})
164+
}
165+
166+
/// Execute the main function of cfsctl with the parsed arguments.
167+
///
168+
/// You should do something like this:
169+
///
170+
/// ```
171+
/// #[tokio::main]
172+
/// async fn main() -> Result<()> {
173+
/// env_logger::init()?;
174+
/// cfsctl::main(cfsctl::App::parse()).await
175+
/// }
176+
/// ```
177+
pub async fn main<ObjectId: FsVerityHashValue>(args: App) -> Result<()> {
178+
let mut repo: Repository<ObjectId> = (if let Some(path) = &args.repo {
179+
Repository::open_path(CWD, path)
180+
} else if args.system {
181+
Repository::open_system()
182+
} else if args.user {
183+
Repository::open_user()
184+
} else if rustix::process::getuid().is_root() {
185+
Repository::open_system()
186+
} else {
187+
Repository::open_user()
188+
})?;
189+
190+
repo.set_insecure(args.insecure);
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+
#[cfg(feature = "oci")]
207+
Command::Oci { cmd: oci_cmd } => match oci_cmd {
208+
OciCommand::ImportLayer { name, digest } => {
209+
let object_id = composefs_oci::import_layer(
210+
&Arc::new(repo),
211+
&digest,
212+
name.as_deref(),
213+
&mut std::io::stdin(),
214+
)?;
215+
println!("{}", object_id.to_id());
216+
}
217+
OciCommand::LsLayer { name } => {
218+
composefs_oci::ls_layer(&repo, &name)?;
219+
}
220+
OciCommand::Dump {
221+
ref config_name,
222+
ref config_verity,
223+
} => {
224+
let verity = verity_opt(config_verity)?;
225+
let mut fs =
226+
composefs_oci::image::create_filesystem(&repo, config_name, verity.as_ref())?;
227+
fs.print_dumpfile()?;
228+
}
229+
OciCommand::ComputeId {
230+
ref config_name,
231+
ref config_verity,
232+
bootable,
233+
} => {
234+
let verity = verity_opt(config_verity)?;
235+
let mut fs =
236+
composefs_oci::image::create_filesystem(&repo, config_name, verity.as_ref())?;
237+
if bootable {
238+
fs.transform_for_boot(&repo)?;
239+
}
240+
let id = fs.compute_image_id();
241+
println!("{}", id.to_hex());
242+
}
243+
OciCommand::CreateImage {
244+
ref config_name,
245+
ref config_verity,
246+
bootable,
247+
ref image_name,
248+
} => {
249+
let verity = verity_opt(config_verity)?;
250+
let mut fs =
251+
composefs_oci::image::create_filesystem(&repo, config_name, verity.as_ref())?;
252+
if bootable {
253+
fs.transform_for_boot(&repo)?;
254+
}
255+
let image_id = fs.commit_image(&repo, image_name.as_deref())?;
256+
println!("{}", image_id.to_id());
257+
}
258+
OciCommand::Pull { ref image, name } => {
259+
let (digest, verity) =
260+
composefs_oci::pull(&Arc::new(repo), image, name.as_deref(), None).await?;
261+
262+
println!("config {digest}");
263+
println!("verity {}", verity.to_hex());
264+
}
265+
OciCommand::Seal {
266+
ref config_name,
267+
ref config_verity,
268+
} => {
269+
let verity = verity_opt(config_verity)?;
270+
let (digest, verity) =
271+
composefs_oci::seal(&Arc::new(repo), config_name, verity.as_ref())?;
272+
println!("config {digest}");
273+
println!("verity {}", verity.to_id());
274+
}
275+
OciCommand::Mount {
276+
ref name,
277+
ref mountpoint,
278+
} => {
279+
composefs_oci::mount(&repo, name, mountpoint, None)?;
280+
}
281+
OciCommand::PrepareBoot {
282+
ref config_name,
283+
ref config_verity,
284+
ref bootdir,
285+
ref entry_id,
286+
ref cmdline,
287+
} => {
288+
let verity = verity_opt(config_verity)?;
289+
let mut fs =
290+
composefs_oci::image::create_filesystem(&repo, config_name, verity.as_ref())?;
291+
let entries = fs.transform_for_boot(&repo)?;
292+
let id = fs.commit_image(&repo, None)?;
293+
294+
let Some(entry) = entries.into_iter().next() else {
295+
anyhow::bail!("No boot entries!");
296+
};
297+
298+
let cmdline_refs: Vec<&str> = cmdline.iter().map(String::as_str).collect();
299+
write_boot::write_boot_simple(
300+
&repo,
301+
entry,
302+
&id,
303+
args.insecure,
304+
bootdir,
305+
None,
306+
entry_id.as_deref(),
307+
&cmdline_refs,
308+
)?;
309+
310+
let state = args
311+
.repo
312+
.as_ref()
313+
.map(|p: &PathBuf| p.parent().unwrap())
314+
.unwrap_or(Path::new("/sysroot"))
315+
.join("state/deploy")
316+
.join(id.to_hex());
317+
318+
create_dir_all(state.join("var"))?;
319+
create_dir_all(state.join("etc/upper"))?;
320+
create_dir_all(state.join("etc/work"))?;
321+
}
322+
},
323+
Command::ComputeId {
324+
ref path,
325+
bootable,
326+
stat_root,
327+
} => {
328+
let mut fs = composefs::fs::read_filesystem(CWD, path, Some(&repo), stat_root)?;
329+
if bootable {
330+
fs.transform_for_boot(&repo)?;
331+
}
332+
let id = fs.compute_image_id();
333+
println!("{}", id.to_hex());
334+
}
335+
Command::CreateImage {
336+
ref path,
337+
bootable,
338+
stat_root,
339+
ref image_name,
340+
} => {
341+
let mut fs = composefs::fs::read_filesystem(CWD, path, Some(&repo), stat_root)?;
342+
if bootable {
343+
fs.transform_for_boot(&repo)?;
344+
}
345+
let id = fs.commit_image(&repo, image_name.as_deref())?;
346+
println!("{}", id.to_id());
347+
}
348+
Command::CreateDumpfile {
349+
ref path,
350+
bootable,
351+
stat_root,
352+
} => {
353+
let mut fs = composefs::fs::read_filesystem(CWD, path, Some(&repo), stat_root)?;
354+
if bootable {
355+
fs.transform_for_boot(&repo)?;
356+
}
357+
fs.print_dumpfile()?;
358+
}
359+
Command::Mount { name, mountpoint } => {
360+
repo.mount_at(&name, &mountpoint)?;
361+
}
362+
Command::ImageObjects { name } => {
363+
let objects = repo.objects_for_image(&name)?;
364+
for object in objects {
365+
println!("{}", object.to_id());
366+
}
367+
}
368+
Command::GC => {
369+
repo.gc()?;
370+
}
371+
#[cfg(feature = "http")]
372+
Command::Fetch { url, name } => {
373+
let (digest, verity) = composefs_http::download(&url, &name, Arc::new(repo)).await?;
374+
println!("content {digest}");
375+
println!("verity {}", verity.to_hex());
376+
}
377+
}
378+
Ok(())
379+
}

0 commit comments

Comments
 (0)