Skip to content

Commit ee5d36f

Browse files
Cargo.toml: switch to a workspace
Split into a few separate crates: - libraries: - composefs - composefs-oci - composefs-boot - binaries: - cfsctl - composefs-setup-root - erofs-debug Move our lint config (which only forbids missing debug impls) to the workspace level and have all crates inherit from that. Add a new workflow for testing that we can `cargo package` everything. We need a nightly cargo in order to do this with workspaces containing inter-dependent crates: rust-lang/cargo#13947 Make 'oci' an optional feature of cfsctl, but enable it by default. Adjust our rawhide bls example (which included --no-default-features) to *not* disable that. This is not a huge improvement in terms of compile speed, and it has some drawbacks (like 'cargo run' no longer defaulting to cfsctl) but it seems like the right step at this point. I want to start to add some more experimental code without making it part of the main crate. Signed-off-by: Allison Karlitskaya <[email protected]>
0 parents  commit ee5d36f

File tree

2 files changed

+380
-0
lines changed

2 files changed

+380
-0
lines changed

Cargo.toml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[package]
2+
name = "cfsctl"
3+
description = "Command-line utility for composefs"
4+
default-run = "cfsctl"
5+
6+
edition.workspace = true
7+
license.workspace = true
8+
readme.workspace = true
9+
repository.workspace = true
10+
rust-version.workspace = true
11+
version.workspace = true
12+
13+
[features]
14+
default = ['pre-6.15', 'oci']
15+
oci = ['composefs-oci']
16+
rhel9 = ['composefs/rhel9']
17+
'pre-6.15' = ['composefs/pre-6.15']
18+
19+
[dependencies]
20+
anyhow = { version = "1.0.87", default-features = false }
21+
clap = { version = "4.0.1", default-features = false, features = ["std", "help", "usage", "derive"] }
22+
composefs = { workspace = true }
23+
composefs-boot = { workspace = true }
24+
composefs-oci = { workspace = true, optional = true }
25+
env_logger = { version = "0.11.0", default-features = false }
26+
hex = { version = "0.4.0", default-features = false }
27+
rustix = { version = "1.0.0", default-features = false, features = ["fs", "process"] }
28+
tokio = { version = "1.24.2", default-features = false }
29+
30+
[lints]
31+
workspace = true

src/main.rs

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

0 commit comments

Comments
 (0)