Integrate routed Git workspaces with persistent state
This commit is contained in:
parent
943c56eea8
commit
917409fcc6
6 changed files with 2363 additions and 119 deletions
|
|
@ -13,15 +13,16 @@ fn main() {
|
|||
|
||||
fn run() -> io::Result<()> {
|
||||
let arguments = env::args_os().skip(1).collect::<Vec<_>>();
|
||||
if arguments.len() != 3 {
|
||||
if arguments.len() != 4 {
|
||||
return Err(io::Error::other(
|
||||
"usage: lazaret-git-sentry MOUNTED_WORKSPACE HOST_GIT_COMMON_DIRECTORY TRANSIENT_HOOKS",
|
||||
"usage: lazaret-git-sentry MOUNTED_WORKSPACE HOST_GIT_COMMON_DIRECTORY TRANSIENT_HOOKS TRANSIENT_CONFIG",
|
||||
));
|
||||
}
|
||||
let sentry = GitSentry::install(&GitSentryConfiguration {
|
||||
mounted_workspace: PathBuf::from(&arguments[0]),
|
||||
host_git_common_directory: PathBuf::from(&arguments[1]),
|
||||
transient_hooks: PathBuf::from(&arguments[2]),
|
||||
transient_config: PathBuf::from(&arguments[3]),
|
||||
})?;
|
||||
println!("ready");
|
||||
io::stdout().flush()?;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,16 @@ pub struct GitClassifier {
|
|||
current: Mutex<Arc<SnapshotInner>>,
|
||||
}
|
||||
|
||||
/// Resolves the root repository's effective common Git directory.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses a non-repository workspace or malformed linked-worktree metadata.
|
||||
pub fn git_common_directory(workspace: &Path) -> io::Result<PathBuf> {
|
||||
let workspace = workspace.canonicalize()?;
|
||||
let root = BackingRoot::open(&workspace)?;
|
||||
Ok(repository_metadata(&workspace, &root, Path::new(""))?.common_dir)
|
||||
}
|
||||
|
||||
impl GitClassifier {
|
||||
/// # Errors
|
||||
/// Refuses a non-repository workspace or unreadable Git policy input.
|
||||
|
|
@ -386,6 +396,7 @@ struct RepositoryMetadata {
|
|||
index: PathBuf,
|
||||
config: PathBuf,
|
||||
info_exclude: PathBuf,
|
||||
common_dir: PathBuf,
|
||||
}
|
||||
|
||||
fn repository_metadata(
|
||||
|
|
@ -432,6 +443,7 @@ fn repository_metadata(
|
|||
index: git_dir.join("index"),
|
||||
config: common_dir.join("config"),
|
||||
info_exclude: common_dir.join("info/exclude"),
|
||||
common_dir,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
189
crates/lazaret-ignored-workspace/src/daemon.rs
Normal file
189
crates/lazaret-ignored-workspace/src/daemon.rs
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
use std::ffi::{OsStr, OsString};
|
||||
use std::fs::File;
|
||||
use std::io::{self, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rustix::fs::{FileType, Mode, OFlags, ResolveFlags, fstat, openat2};
|
||||
|
||||
use crate::IgnoredWorkspace;
|
||||
use crate::filesystem::RouterConfiguration;
|
||||
|
||||
pub const PRIVATE_ROUTER_ROLE: &str = "--lazaret-private-ignored-workspace-router-v1";
|
||||
|
||||
#[must_use]
|
||||
pub fn dispatch_private_router(arguments: &[OsString]) -> Option<io::Result<()>> {
|
||||
if arguments
|
||||
.first()
|
||||
.is_none_or(|argument| argument != PRIVATE_ROUTER_ROLE)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(parse(arguments).and_then(run))
|
||||
}
|
||||
|
||||
struct Arguments {
|
||||
workspace_fd: i32,
|
||||
workspace_label: PathBuf,
|
||||
isolated_fd: i32,
|
||||
mountpoint: PathBuf,
|
||||
global_excludes: Option<PathBuf>,
|
||||
isolate_ignored: bool,
|
||||
threads: usize,
|
||||
}
|
||||
|
||||
fn parse(arguments: &[OsString]) -> io::Result<Arguments> {
|
||||
let mut arguments = arguments.iter().skip(1);
|
||||
let workspace_fd = number(named(&mut arguments, "--workspace-fd")?)?;
|
||||
let workspace_label = PathBuf::from(named(&mut arguments, "--workspace-label")?);
|
||||
let isolated_fd = number(named(&mut arguments, "--isolated-fd")?)?;
|
||||
let mountpoint = PathBuf::from(named(&mut arguments, "--mountpoint")?);
|
||||
let global_excludes = match named(&mut arguments, "--global-excludes")? {
|
||||
value if value.is_empty() => None,
|
||||
value => Some(PathBuf::from(value)),
|
||||
};
|
||||
let isolate_ignored = match named(&mut arguments, "--isolate-ignored")?.to_str() {
|
||||
Some("true") => true,
|
||||
Some("false") => false,
|
||||
_ => return Err(io::Error::other("router isolation mode is invalid")),
|
||||
};
|
||||
let threads = number(named(&mut arguments, "--threads")?)?;
|
||||
if arguments.next().is_some() || threads == 0 {
|
||||
return Err(io::Error::other("router arguments are invalid"));
|
||||
}
|
||||
Ok(Arguments {
|
||||
workspace_fd,
|
||||
workspace_label,
|
||||
isolated_fd,
|
||||
mountpoint,
|
||||
global_excludes,
|
||||
isolate_ignored,
|
||||
threads,
|
||||
})
|
||||
}
|
||||
|
||||
fn run(arguments: Arguments) -> io::Result<()> {
|
||||
let workspace = inherited_exact_directory(arguments.workspace_fd, &arguments.workspace_label)?;
|
||||
let isolated = inherited(arguments.isolated_fd)?;
|
||||
IgnoredWorkspace::serve_from_authorities(
|
||||
RouterConfiguration {
|
||||
workspace_label: arguments.workspace_label,
|
||||
workspace,
|
||||
isolated_state: isolated,
|
||||
mountpoint: arguments.mountpoint,
|
||||
global_excludes: arguments.global_excludes,
|
||||
isolate_ignored: arguments.isolate_ignored,
|
||||
threads: arguments.threads,
|
||||
},
|
||||
|| {
|
||||
let mut output = io::stdout().lock();
|
||||
output.write_all(b"ready\n")?;
|
||||
output.flush()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn inherited_exact_directory(descriptor: i32, label: &std::path::Path) -> io::Result<File> {
|
||||
let inherited = inherited(descriptor)?;
|
||||
let expected = fstat(&inherited)?;
|
||||
let reopened = File::from(openat2(
|
||||
rustix::fs::CWD,
|
||||
label,
|
||||
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::empty(),
|
||||
ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
|
||||
)?);
|
||||
let observed = fstat(&reopened)?;
|
||||
if expected.st_dev != observed.st_dev
|
||||
|| expected.st_ino != observed.st_ino
|
||||
|| !FileType::from_raw_mode(observed.st_mode).is_dir()
|
||||
{
|
||||
return Err(io::Error::other(
|
||||
"namespace-local workspace differs from inherited authority",
|
||||
));
|
||||
}
|
||||
Ok(reopened)
|
||||
}
|
||||
|
||||
fn inherited(descriptor: i32) -> io::Result<File> {
|
||||
if descriptor < 0 {
|
||||
return Err(io::Error::other("router descriptor is invalid"));
|
||||
}
|
||||
File::open(format!("/proc/self/fd/{descriptor}"))
|
||||
}
|
||||
|
||||
fn named<'a>(
|
||||
arguments: &mut impl Iterator<Item = &'a OsString>,
|
||||
name: &str,
|
||||
) -> io::Result<&'a OsString> {
|
||||
if arguments.next().is_none_or(|argument| argument != name) {
|
||||
return Err(io::Error::other(format!(
|
||||
"router argument {name} is missing"
|
||||
)));
|
||||
}
|
||||
arguments
|
||||
.next()
|
||||
.ok_or_else(|| io::Error::other(format!("router argument {name} has no value")))
|
||||
}
|
||||
|
||||
fn number<T>(value: &OsStr) -> io::Result<T>
|
||||
where
|
||||
T: std::str::FromStr,
|
||||
{
|
||||
value
|
||||
.to_str()
|
||||
.and_then(|value| value.parse().ok())
|
||||
.ok_or_else(|| io::Error::other("router numeric argument is invalid"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn exact_private_arguments_parse() {
|
||||
let arguments = valid_arguments();
|
||||
let parsed = parse(&arguments).unwrap();
|
||||
assert_eq!(parsed.workspace_fd, 3);
|
||||
assert_eq!(parsed.isolated_fd, 4);
|
||||
assert_eq!(parsed.threads, 2);
|
||||
assert!(parsed.isolate_ignored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_private_arguments_are_refused() {
|
||||
let mut extra = valid_arguments();
|
||||
extra.push("unexpected".into());
|
||||
assert!(parse(&extra).is_err());
|
||||
|
||||
let mut zero_threads = valid_arguments();
|
||||
*zero_threads.last_mut().unwrap() = "0".into();
|
||||
assert!(parse(&zero_threads).is_err());
|
||||
|
||||
let mut missing = valid_arguments();
|
||||
missing.remove(2);
|
||||
assert!(parse(&missing).is_err());
|
||||
}
|
||||
|
||||
fn valid_arguments() -> Vec<OsString> {
|
||||
[
|
||||
PRIVATE_ROUTER_ROLE,
|
||||
"--workspace-fd",
|
||||
"3",
|
||||
"--workspace-label",
|
||||
"/workspace",
|
||||
"--isolated-fd",
|
||||
"4",
|
||||
"--mountpoint",
|
||||
"/runtime/workspace",
|
||||
"--global-excludes",
|
||||
"",
|
||||
"--isolate-ignored",
|
||||
"true",
|
||||
"--threads",
|
||||
"2",
|
||||
]
|
||||
.into_iter()
|
||||
.map(OsString::from)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
|
@ -44,10 +44,21 @@ pub struct MountConfiguration {
|
|||
pub threads: usize,
|
||||
}
|
||||
|
||||
pub(crate) struct RouterConfiguration {
|
||||
pub workspace_label: PathBuf,
|
||||
pub workspace: File,
|
||||
pub isolated_state: File,
|
||||
pub mountpoint: PathBuf,
|
||||
pub global_excludes: Option<PathBuf>,
|
||||
pub isolate_ignored: bool,
|
||||
pub threads: usize,
|
||||
}
|
||||
|
||||
pub struct IgnoredWorkspace {
|
||||
host: BackingRoot,
|
||||
isolated: BackingRoot,
|
||||
classifier: GitClassifier,
|
||||
isolate_ignored: bool,
|
||||
nodes: Mutex<NodeTable>,
|
||||
files: Mutex<HashMap<u64, Arc<OpenFile>>>,
|
||||
directories: Mutex<HashMap<u64, DirectoryStream>>,
|
||||
|
|
@ -55,6 +66,21 @@ pub struct IgnoredWorkspace {
|
|||
next_handle: AtomicU64,
|
||||
}
|
||||
|
||||
fn mount_config(threads: usize) -> Config {
|
||||
let mut config = Config::default();
|
||||
config.mount_options.extend([
|
||||
MountOption::FSName("lazaret-ignored-workspace".to_owned()),
|
||||
MountOption::RW,
|
||||
MountOption::Exec,
|
||||
MountOption::NoSuid,
|
||||
MountOption::NoDev,
|
||||
MountOption::DefaultPermissions,
|
||||
]);
|
||||
config.n_threads = Some(threads.max(1));
|
||||
config.clone_fd = true;
|
||||
config
|
||||
}
|
||||
|
||||
impl IgnoredWorkspace {
|
||||
/// # Errors
|
||||
/// Refuses roots that are not distinct, descriptor-openable directories in a Git workspace.
|
||||
|
|
@ -86,11 +112,41 @@ impl IgnoredWorkspace {
|
|||
Self::from_roots(workspace_label, host, isolated, global_excludes)
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
/// Refuses aliased or non-directory authorities and unreadable Git policy inputs.
|
||||
pub fn from_authorities_with_policy(
|
||||
workspace_label: PathBuf,
|
||||
workspace: File,
|
||||
isolated_state: File,
|
||||
global_excludes: Option<PathBuf>,
|
||||
isolate_ignored: bool,
|
||||
) -> io::Result<Self> {
|
||||
let host = BackingRoot::from_file(workspace)?;
|
||||
let isolated = BackingRoot::from_file(isolated_state)?;
|
||||
Self::from_roots_with_policy(
|
||||
workspace_label,
|
||||
host,
|
||||
isolated,
|
||||
global_excludes,
|
||||
isolate_ignored,
|
||||
)
|
||||
}
|
||||
|
||||
fn from_roots(
|
||||
workspace_label: PathBuf,
|
||||
host: BackingRoot,
|
||||
isolated: BackingRoot,
|
||||
global_excludes: Option<PathBuf>,
|
||||
) -> io::Result<Self> {
|
||||
Self::from_roots_with_policy(workspace_label, host, isolated, global_excludes, true)
|
||||
}
|
||||
|
||||
fn from_roots_with_policy(
|
||||
workspace_label: PathBuf,
|
||||
host: BackingRoot,
|
||||
isolated: BackingRoot,
|
||||
global_excludes: Option<PathBuf>,
|
||||
isolate_ignored: bool,
|
||||
) -> io::Result<Self> {
|
||||
if host.identity()? == isolated.identity()? {
|
||||
return Err(io::Error::other("workspace and isolated authorities alias"));
|
||||
|
|
@ -102,6 +158,7 @@ impl IgnoredWorkspace {
|
|||
host,
|
||||
isolated,
|
||||
classifier,
|
||||
isolate_ignored,
|
||||
nodes: Mutex::new(NodeTable::new()),
|
||||
files: Mutex::new(HashMap::new()),
|
||||
directories: Mutex::new(HashMap::new()),
|
||||
|
|
@ -118,20 +175,36 @@ impl IgnoredWorkspace {
|
|||
&configuration.isolated_state,
|
||||
configuration.global_excludes,
|
||||
)?;
|
||||
let mut config = Config::default();
|
||||
config.mount_options.extend([
|
||||
MountOption::FSName("lazaret-ignored-workspace".to_owned()),
|
||||
MountOption::RW,
|
||||
MountOption::Exec,
|
||||
MountOption::NoSuid,
|
||||
MountOption::NoDev,
|
||||
MountOption::DefaultPermissions,
|
||||
]);
|
||||
config.n_threads = Some(configuration.threads.max(1));
|
||||
config.clone_fd = true;
|
||||
let config = mount_config(configuration.threads);
|
||||
fuser::mount2(filesystem, configuration.mountpoint, &config)
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
/// Returns mount, readiness, control-channel, unmount, or daemon I/O failures.
|
||||
pub(crate) fn serve_from_authorities(
|
||||
configuration: RouterConfiguration,
|
||||
ready: impl FnOnce() -> io::Result<()>,
|
||||
) -> io::Result<()> {
|
||||
let filesystem = Self::from_authorities_with_policy(
|
||||
configuration.workspace_label,
|
||||
configuration.workspace,
|
||||
configuration.isolated_state,
|
||||
configuration.global_excludes,
|
||||
configuration.isolate_ignored,
|
||||
)?;
|
||||
let session = fuser::spawn_mount2(
|
||||
filesystem,
|
||||
configuration.mountpoint,
|
||||
&mount_config(configuration.threads),
|
||||
)?;
|
||||
if let Err(error) = ready() {
|
||||
let _ = session.umount_and_join();
|
||||
return Err(error);
|
||||
}
|
||||
std::io::copy(&mut std::io::stdin().lock(), &mut std::io::sink())?;
|
||||
session.umount_and_join()
|
||||
}
|
||||
|
||||
fn root(&self, backing: Classification) -> &BackingRoot {
|
||||
match backing {
|
||||
Classification::Host => &self.host,
|
||||
|
|
@ -171,7 +244,7 @@ impl IgnoredWorkspace {
|
|||
.as_ref()
|
||||
.or(host.as_ref())
|
||||
.is_some_and(|stat| stat.st_mode & libc::S_IFMT == libc::S_IFDIR);
|
||||
let backing = snapshot.classify(path, directory);
|
||||
let backing = self.classify(snapshot, path, directory);
|
||||
if snapshot.is_tracked(path) {
|
||||
if let (Some(host), Some(isolated)) = (&host, &isolated)
|
||||
&& host.st_mode & libc::S_IFMT != isolated.st_mode & libc::S_IFMT
|
||||
|
|
@ -200,13 +273,26 @@ impl IgnoredWorkspace {
|
|||
path: &Path,
|
||||
directory: bool,
|
||||
) -> io::Result<Classification> {
|
||||
let backing = snapshot.classify(path, directory);
|
||||
let backing = self.classify(snapshot, path, directory);
|
||||
if backing == Classification::Isolated {
|
||||
self.isolated.ensure_parent_directories(path)?;
|
||||
}
|
||||
Ok(backing)
|
||||
}
|
||||
|
||||
fn classify(
|
||||
&self,
|
||||
snapshot: &ClassificationSnapshot,
|
||||
path: &Path,
|
||||
directory: bool,
|
||||
) -> Classification {
|
||||
if self.isolate_ignored {
|
||||
snapshot.classify(path, directory)
|
||||
} else {
|
||||
Classification::Host
|
||||
}
|
||||
}
|
||||
|
||||
fn open_file(&self, inode: INodeNo, flags: i32) -> io::Result<u64> {
|
||||
let path = self.path_for(inode)?;
|
||||
let snapshot = self.classifier.snapshot_for(&path);
|
||||
|
|
@ -262,7 +348,7 @@ impl IgnoredWorkspace {
|
|||
.resolve(&snapshot, path)
|
||||
.and_then(|entry| self.attributes(&entry))
|
||||
{
|
||||
Ok(attributes) => reply.entry(&TTL, &attributes, Generation(snapshot.generation())),
|
||||
Ok(attributes) => reply.entry(&TTL, &attributes, Generation(0)),
|
||||
Err(error) => reply.error(errno(&error)),
|
||||
}
|
||||
}
|
||||
|
|
@ -643,7 +729,8 @@ impl Filesystem for IgnoredWorkspace {
|
|||
let destination = self.child_path(new_parent, new_name)?;
|
||||
let snapshot = self.classifier.snapshot_for_paths(&[&source, &destination]);
|
||||
let source_entry = self.resolve(&snapshot, &source)?;
|
||||
let destination_backing = snapshot.classify(
|
||||
let destination_backing = self.classify(
|
||||
&snapshot,
|
||||
&destination,
|
||||
source_entry.stat.st_mode & libc::S_IFMT == libc::S_IFDIR,
|
||||
);
|
||||
|
|
@ -673,7 +760,7 @@ impl Filesystem for IgnoredWorkspace {
|
|||
let destination = self.child_path(new_parent, new_name)?;
|
||||
let snapshot = self.classifier.snapshot_for_paths(&[&source, &destination]);
|
||||
let source_entry = self.resolve(&snapshot, &source)?;
|
||||
let destination_backing = snapshot.classify(&destination, false);
|
||||
let destination_backing = self.classify(&snapshot, &destination, false);
|
||||
if source_entry.backing != destination_backing {
|
||||
return Err(io::Error::from_raw_os_error(libc::EXDEV));
|
||||
}
|
||||
|
|
@ -1105,13 +1192,13 @@ impl Filesystem for IgnoredWorkspace {
|
|||
access_mode: flags & libc::O_ACCMODE,
|
||||
}),
|
||||
);
|
||||
Ok((attributes, handle, snapshot.generation()))
|
||||
Ok((attributes, handle))
|
||||
})();
|
||||
match result {
|
||||
Ok((attributes, handle, generation)) => reply.created(
|
||||
Ok((attributes, handle)) => reply.created(
|
||||
&TTL,
|
||||
&attributes,
|
||||
Generation(generation),
|
||||
Generation(0),
|
||||
FileHandle(handle),
|
||||
FopenFlags::empty(),
|
||||
),
|
||||
|
|
@ -1552,6 +1639,27 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_isolation_routes_ignored_paths_to_the_host_backing() {
|
||||
let fixture = Fixture::new();
|
||||
repository(&fixture.workspace);
|
||||
fs::write(fixture.workspace.join(".gitignore"), b"ignored/\n").unwrap();
|
||||
let filesystem = IgnoredWorkspace::from_authorities_with_policy(
|
||||
fixture.workspace.clone(),
|
||||
fs::File::open(&fixture.workspace).unwrap(),
|
||||
fs::File::open(&fixture.isolated).unwrap(),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let path = std::path::Path::new("ignored/item");
|
||||
let snapshot = filesystem.classifier.snapshot_for(path);
|
||||
assert_eq!(
|
||||
filesystem.classify(&snapshot, path, false),
|
||||
Classification::Host
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a usable host FUSE device"]
|
||||
fn physical_router_preserves_lock_domains_across_transition() {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
mod backing;
|
||||
mod classifier;
|
||||
mod daemon;
|
||||
mod filesystem;
|
||||
mod sentry;
|
||||
|
||||
pub use classifier::{Classification, ClassificationSnapshot, GitClassifier};
|
||||
pub use classifier::{Classification, ClassificationSnapshot, GitClassifier, git_common_directory};
|
||||
pub use daemon::{PRIVATE_ROUTER_ROLE, dispatch_private_router};
|
||||
pub use filesystem::{IgnoredWorkspace, MountConfiguration};
|
||||
pub use sentry::{GitSentry, GitSentryConfiguration};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::os::fd::{AsRawFd, RawFd};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use rustix::fs::{Mode, OFlags};
|
||||
use rustix::mount::{MountFlags, UnmountFlags, mount_bind, mount_remount, unmount};
|
||||
use rustix::fs::{FileType, Mode, OFlags, fstat};
|
||||
use rustix::mount::{
|
||||
MountFlags, MoveMountFlags, OpenTreeFlags, UnmountFlags, mount_remount, move_mount, open_tree,
|
||||
unmount,
|
||||
};
|
||||
|
||||
use crate::backing::BackingRoot;
|
||||
|
||||
|
|
@ -13,87 +15,254 @@ pub struct GitSentryConfiguration {
|
|||
pub mounted_workspace: PathBuf,
|
||||
pub host_git_common_directory: PathBuf,
|
||||
pub transient_hooks: PathBuf,
|
||||
pub transient_config: PathBuf,
|
||||
}
|
||||
|
||||
pub struct GitSentry {
|
||||
config_target: File,
|
||||
hook_target: File,
|
||||
config: InstalledMount,
|
||||
hooks: InstalledMount,
|
||||
}
|
||||
|
||||
struct InstalledMount {
|
||||
mounted_target: File,
|
||||
cleanup_target: File,
|
||||
cleanup_label: PathBuf,
|
||||
}
|
||||
|
||||
impl InstalledMount {
|
||||
fn unmount(self) -> io::Result<()> {
|
||||
descriptor_move(&self.mounted_target, &self.cleanup_target)?;
|
||||
unmount(self.cleanup_label, UnmountFlags::DETACH).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl GitSentry {
|
||||
/// # Errors
|
||||
/// Refuses linked or missing sources and targets and incomplete protective mounts.
|
||||
pub fn install(configuration: &GitSentryConfiguration) -> io::Result<Self> {
|
||||
let _workspace = BackingRoot::open(&configuration.mounted_workspace)?;
|
||||
let workspace = BackingRoot::open(&configuration.mounted_workspace)?;
|
||||
let common = BackingRoot::open(&configuration.host_git_common_directory)?;
|
||||
let transient = BackingRoot::open(&configuration.transient_hooks)?;
|
||||
let transient_hooks = BackingRoot::open(&configuration.transient_hooks)?;
|
||||
let transient_config = File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&configuration.transient_config)?;
|
||||
Self::install_from_roots(
|
||||
&workspace,
|
||||
&common,
|
||||
&transient_hooks,
|
||||
configuration.transient_hooks.clone(),
|
||||
transient_config,
|
||||
configuration.transient_config.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
let hook_source = transient.duplicate()?;
|
||||
let hook_target = common.open_entry(
|
||||
Path::new("hooks"),
|
||||
OFlags::RDONLY | OFlags::DIRECTORY,
|
||||
Mode::empty(),
|
||||
)?;
|
||||
descriptor_bind(&hook_source, &hook_target)?;
|
||||
let hook_target = common.open_entry(
|
||||
Path::new("hooks"),
|
||||
OFlags::RDONLY | OFlags::DIRECTORY,
|
||||
Mode::empty(),
|
||||
)?;
|
||||
/// # Errors
|
||||
/// Refuses aliased, linked, or incomplete authorities and incomplete protective mounts.
|
||||
pub fn install_from_authorities(
|
||||
mounted_workspace: File,
|
||||
host_git_common_directory: File,
|
||||
transient_hooks: File,
|
||||
transient_hooks_label: PathBuf,
|
||||
transient_config: File,
|
||||
transient_config_label: PathBuf,
|
||||
) -> io::Result<Self> {
|
||||
let workspace = BackingRoot::from_file(mounted_workspace)?;
|
||||
let common = BackingRoot::from_file(host_git_common_directory)?;
|
||||
let transient_hooks = BackingRoot::from_file(transient_hooks)?;
|
||||
Self::install_from_roots(
|
||||
&workspace,
|
||||
&common,
|
||||
&transient_hooks,
|
||||
transient_hooks_label,
|
||||
transient_config,
|
||||
transient_config_label,
|
||||
)
|
||||
}
|
||||
|
||||
let config_source =
|
||||
common.open_entry(Path::new("config"), OFlags::RDONLY, Mode::empty())?;
|
||||
let config_target =
|
||||
common.open_entry(Path::new("config"), OFlags::RDONLY, Mode::empty())?;
|
||||
descriptor_bind(&config_source, &config_target)?;
|
||||
let config_target =
|
||||
common.open_entry(Path::new("config"), OFlags::RDONLY, Mode::empty())?;
|
||||
if let Err(error) = descriptor_remount_read_only(&config_target) {
|
||||
let _ = descriptor_unmount(&hook_target);
|
||||
return Err(error);
|
||||
fn install_from_roots(
|
||||
_workspace: &BackingRoot,
|
||||
common: &BackingRoot,
|
||||
transient_hooks: &BackingRoot,
|
||||
transient_hooks_label: PathBuf,
|
||||
transient_config: File,
|
||||
transient_config_label: PathBuf,
|
||||
) -> io::Result<Self> {
|
||||
require_empty_regular_file(&transient_config)
|
||||
.map_err(|error| sentry_error("validating transient config", &error))?;
|
||||
let config_source = common
|
||||
.open_entry(Path::new("config"), OFlags::RDONLY, Mode::empty())
|
||||
.map_err(|error| sentry_error("opening host config", &error))?;
|
||||
require_regular_file(&config_source)
|
||||
.map_err(|error| sentry_error("validating host config", &error))?;
|
||||
let staged_config = File::from(
|
||||
descriptor_bind(&config_source, &transient_config)
|
||||
.map_err(|error| sentry_error("staging config mount", &error))?,
|
||||
);
|
||||
if let Err(error) = descriptor_remount_read_only(&transient_config_label)
|
||||
.map_err(|error| sentry_error("making config mount read-only", &error))
|
||||
{
|
||||
return Err(cleanup_staged(error, &transient_config_label));
|
||||
}
|
||||
let config_target =
|
||||
match common.open_entry(Path::new("config"), OFlags::RDONLY, Mode::empty()) {
|
||||
Ok(config_target) => config_target,
|
||||
Err(error) => {
|
||||
return Err(cleanup_staged(
|
||||
sentry_error("opening config mount target", &error),
|
||||
&transient_config_label,
|
||||
));
|
||||
}
|
||||
};
|
||||
if let Err(error) = descriptor_move(&staged_config, &config_target)
|
||||
.map_err(|error| sentry_error("installing config mount", &error))
|
||||
{
|
||||
return Err(cleanup_staged(error, &transient_config_label));
|
||||
}
|
||||
let config = InstalledMount {
|
||||
mounted_target: staged_config,
|
||||
cleanup_target: transient_config,
|
||||
cleanup_label: transient_config_label,
|
||||
};
|
||||
|
||||
let hook_source = match transient_hooks.duplicate() {
|
||||
Ok(hook_source) => hook_source,
|
||||
Err(error) => {
|
||||
return Err(cleanup_installed(
|
||||
sentry_error("retaining transient hooks", &error),
|
||||
config,
|
||||
));
|
||||
}
|
||||
};
|
||||
let hook_target = match common.open_entry(
|
||||
Path::new("hooks"),
|
||||
OFlags::RDONLY | OFlags::DIRECTORY,
|
||||
Mode::empty(),
|
||||
) {
|
||||
Ok(hook_target) => hook_target,
|
||||
Err(error) => {
|
||||
return Err(cleanup_installed(
|
||||
sentry_error("opening hooks mount target", &error),
|
||||
config,
|
||||
));
|
||||
}
|
||||
};
|
||||
let mounted_hooks = match descriptor_bind(&hook_source, &hook_target)
|
||||
.map_err(|error| sentry_error("installing hooks mount", &error))
|
||||
{
|
||||
Ok(mounted_hooks) => File::from(mounted_hooks),
|
||||
Err(error) => {
|
||||
return Err(cleanup_installed(error, config));
|
||||
}
|
||||
};
|
||||
Ok(Self {
|
||||
config_target,
|
||||
hook_target,
|
||||
config,
|
||||
hooks: InstalledMount {
|
||||
mounted_target: mounted_hooks,
|
||||
cleanup_target: hook_source,
|
||||
cleanup_label: transient_hooks_label,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
/// Returns an error unless both exact protective mountpoints are detached.
|
||||
pub fn unmount(self) -> io::Result<()> {
|
||||
let config = descriptor_unmount(&self.config_target);
|
||||
let hooks = descriptor_unmount(&self.hook_target);
|
||||
config.and(hooks)
|
||||
let config = self.config.unmount();
|
||||
let hooks = self.hooks.unmount();
|
||||
match (config, hooks) {
|
||||
(Ok(()), Ok(())) => Ok(()),
|
||||
(config, hooks) => Err(io::Error::other(format!(
|
||||
"config={config:?}, hooks={hooks:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn descriptor_bind(source: &File, target: &File) -> io::Result<()> {
|
||||
Ok(mount_bind(
|
||||
proc_fd(source.as_raw_fd()),
|
||||
proc_fd(target.as_raw_fd()),
|
||||
)?)
|
||||
fn sentry_error(action: &str, error: &io::Error) -> io::Error {
|
||||
io::Error::new(error.kind(), format!("{action}: {error}"))
|
||||
}
|
||||
|
||||
fn descriptor_remount_read_only(target: &File) -> io::Result<()> {
|
||||
let result = mount_remount(
|
||||
proc_fd(target.as_raw_fd()),
|
||||
fn cleanup_staged(error: io::Error, label: &Path) -> io::Error {
|
||||
combine_cleanup(
|
||||
error,
|
||||
unmount(label, UnmountFlags::DETACH).map_err(Into::into),
|
||||
)
|
||||
}
|
||||
|
||||
fn cleanup_installed(error: io::Error, mount: InstalledMount) -> io::Error {
|
||||
combine_cleanup(error, mount.unmount())
|
||||
}
|
||||
|
||||
fn combine_cleanup(error: io::Error, cleanup: io::Result<()>) -> io::Error {
|
||||
match cleanup {
|
||||
Ok(()) => error,
|
||||
Err(cleanup) => io::Error::new(error.kind(), format!("{error}; cleanup failed: {cleanup}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn descriptor_bind(source: &File, target: &File) -> io::Result<std::os::fd::OwnedFd> {
|
||||
let mount = open_tree(
|
||||
source,
|
||||
"",
|
||||
OpenTreeFlags::OPEN_TREE_CLONE
|
||||
| OpenTreeFlags::OPEN_TREE_CLOEXEC
|
||||
| OpenTreeFlags::AT_EMPTY_PATH,
|
||||
)?;
|
||||
move_mount(
|
||||
&mount,
|
||||
"",
|
||||
target,
|
||||
"",
|
||||
MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH | MoveMountFlags::MOVE_MOUNT_T_EMPTY_PATH,
|
||||
)
|
||||
.map_err(io::Error::from)?;
|
||||
Ok(mount)
|
||||
}
|
||||
|
||||
fn descriptor_move(source: &File, target: &File) -> io::Result<()> {
|
||||
move_mount(
|
||||
source,
|
||||
"",
|
||||
target,
|
||||
"",
|
||||
MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH | MoveMountFlags::MOVE_MOUNT_T_EMPTY_PATH,
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn descriptor_remount_read_only(target: &Path) -> io::Result<()> {
|
||||
mount_remount(
|
||||
target,
|
||||
MountFlags::BIND
|
||||
| MountFlags::RDONLY
|
||||
| MountFlags::NOSUID
|
||||
| MountFlags::NODEV
|
||||
| MountFlags::NOEXEC,
|
||||
"",
|
||||
);
|
||||
if result.is_err() {
|
||||
let _ = unmount(proc_fd(target.as_raw_fd()), UnmountFlags::DETACH);
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn require_empty_regular_file(file: &File) -> io::Result<()> {
|
||||
let metadata = fstat(file)?;
|
||||
if !FileType::from_raw_mode(metadata.st_mode).is_file() {
|
||||
return Err(io::Error::other(
|
||||
"transient Git config authority is not a regular file",
|
||||
));
|
||||
}
|
||||
Ok(result?)
|
||||
if metadata.st_size != 0 {
|
||||
return Err(io::Error::other(
|
||||
"transient Git config authority is not an empty regular file",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn descriptor_unmount(target: &File) -> io::Result<()> {
|
||||
Ok(unmount(proc_fd(target.as_raw_fd()), UnmountFlags::DETACH)?)
|
||||
}
|
||||
|
||||
fn proc_fd(fd: RawFd) -> PathBuf {
|
||||
PathBuf::from(format!("/proc/self/fd/{fd}"))
|
||||
fn require_regular_file(file: &File) -> io::Result<()> {
|
||||
if FileType::from_raw_mode(fstat(file)?.st_mode).is_file() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::other("host Git config is not a regular file"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue