Integrate routed Git workspaces with persistent state
This commit is contained in:
parent
943c56eea8
commit
917409fcc6
1 changed files with 2363 additions and 119 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue