Integrate routed Git workspaces with persistent state

This commit is contained in:
ms2k 2026-07-18 02:08:14 +09:00
commit 917409fcc6
8 changed files with 2363 additions and 119 deletions

View file

@ -23,6 +23,7 @@ harness = false
ed25519-dalek.workspace = true
getrandom.workspace = true
lazaret-core = { version = "=0.0.0", path = "../lazaret-core" }
lazaret-ignored-workspace = { version = "=0.0.0", path = "../lazaret-ignored-workspace" }
lazaret-linux = { version = "=0.0.0", path = "../lazaret-linux" }
lazaret-prepared-lower = { version = "=0.0.0", path = "../lazaret-prepared-lower" }
lazaret-runtime-supervisor = { version = "=0.0.0", path = "../lazaret-runtime-supervisor" }

View file

@ -4,7 +4,7 @@ use std::sync::Arc;
use std::sync::Mutex;
use lazaret_core::configuration::ResolvedDomainConfiguration;
use lazaret_core::identity::{BundleDigest, ContainerId, MountIdentity};
use lazaret_core::identity::{BundleDigest, ContainerId, HostPath, MountIdentity};
use lazaret_core::network::{CanonicalDerivedNetworkPolicy, ReachabilityExpectation};
use lazaret_core::records::Session;
use lazaret_core::records::{
@ -24,6 +24,7 @@ use crate::control::{
SessionRegistry, StagedControlFace, TrustAnchor,
};
use crate::domain_store::{DomainStoreMutation, HostCoordinatorDomainStore};
use crate::git_routing::GitRoutingCoordinator;
use crate::lifecycle::{
HostCoordinatorLifecycle, LifecycleAction, LifecycleAdapter, SessionAdmissionIdentity,
SessionLifecycleAdapter,
@ -77,6 +78,13 @@ pub struct ProductionBootstrapPlan<'a> {
pub interactive_executor: Arc<dyn InteractiveSessionExecutor>,
pub prompt_transport: Arc<SessionPromptTransport>,
pub(crate) runtime_staging: &'a crate::production::RuntimeStaging,
pub git_routing: Option<ProductionGitRouting>,
}
pub struct ProductionGitRouting {
pub mountpoint: HostPath,
pub host_root: HostPath,
pub isolated_root: HostPath,
}
pub struct ProductionLifecycleAdapter<'a> {
@ -88,6 +96,7 @@ pub struct ProductionLifecycleAdapter<'a> {
awaiting_runtime: Option<AwaitingAttachedRunsc>,
runtime: Option<CommittedSandboxRuntime>,
state_anchor_mount: Option<MountIdentity>,
git_routing_mount: Option<MountIdentity>,
state_anchor_release: Option<StateAnchorReleaseAuthority>,
state_anchor_absence: Option<StateAnchorAbsenceObservation>,
}
@ -184,6 +193,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
awaiting_runtime: None,
runtime: None,
state_anchor_mount: None,
git_routing_mount: None,
state_anchor_release: None,
state_anchor_absence: None,
},
@ -286,6 +296,24 @@ impl<'a> ProductionLifecycleAdapter<'a> {
)
.map_err(|error| failure(format!("mounting State Anchor: {error}")))?;
self.state_anchor_mount = Some(mount);
if let Some(routing) = &self.plan.git_routing {
let git_routing = GitRoutingCoordinator::new(&store);
git_routing
.begin_intent(
self.plan.generation_identity.generation_id,
routing.mountpoint.clone(),
routing.host_root.clone(),
routing.isolated_root.clone(),
)
.map_err(failure)?;
let git_mount = self
.awaiting_runtime
.as_mut()
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
.start_git_routing(&git_routing)
.map_err(|error| failure(format!("starting Git routing: {error}")))?;
self.git_routing_mount = Some(git_mount);
}
Ok(())
}
@ -465,6 +493,15 @@ impl<'a> ProductionLifecycleAdapter<'a> {
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
.stop_sandbox(&sandbox)
.map_err(failure)?;
if self.plan.git_routing.is_some() {
let git_routing = GitRoutingCoordinator::new(&store);
self.runtime
.as_mut()
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
.stop_git_routing(&git_routing)
.map_err(failure)?;
self.git_routing_mount = None;
}
self.plan.runtime_staging.cleanup_overlay().map_err(failure)
}

View file

@ -0,0 +1,444 @@
use std::fmt;
use lazaret_core::identity::{GenerationId, HostPath, MountIdentity};
use lazaret_core::records::{
GitRoutingOwnership, HostProcessOwnershipSlot, Participant, ParticipantOwnership,
ParticipantRole, ParticipantState, PersistedHostProcessOwnership, ProcessOwnershipCommitAck,
StagedProcessOwnership,
};
use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore};
#[derive(Debug)]
pub enum GitRoutingCoordinatorError {
DomainStore(DomainStoreError),
InvalidState(&'static str),
}
impl fmt::Display for GitRoutingCoordinatorError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DomainStore(error) => write!(formatter, "Domain Store failed: {error}"),
Self::InvalidState(detail) => write!(formatter, "Git routing refused: {detail}"),
}
}
}
impl std::error::Error for GitRoutingCoordinatorError {}
impl From<DomainStoreError> for GitRoutingCoordinatorError {
fn from(error: DomainStoreError) -> Self {
Self::DomainStore(error)
}
}
pub struct GitRoutingCoordinator<'a> {
store: &'a HostCoordinatorDomainStore,
}
impl<'a> GitRoutingCoordinator<'a> {
#[must_use]
pub const fn new(store: &'a HostCoordinatorDomainStore) -> Self {
Self { store }
}
/// # Errors
/// Refuses a different existing participant or a durable transition failure.
pub fn begin_intent(
&self,
generation_id: GenerationId,
mountpoint: HostPath,
host_root: HostPath,
isolated_root: HostPath,
) -> Result<(), GitRoutingCoordinatorError> {
let intent = participant(
generation_id,
ParticipantState::Intent,
GitRoutingOwnership {
mountpoint: Some(mountpoint),
host_root: Some(host_root),
isolated_root: Some(isolated_root),
daemon: PersistedHostProcessOwnership::unassigned(),
mount: None,
},
)?;
match self.participant(generation_id)? {
None => self.store.apply(&[
DomainStoreMutation::PutParticipant(Participant::bare_intent(
generation_id,
ParticipantRole::GitRouting,
)),
DomainStoreMutation::PutParticipant(intent),
])?,
Some(current) if current == intent => {}
Some(_) => {
return Err(GitRoutingCoordinatorError::InvalidState(
"Git-routing intent already differs",
));
}
}
Ok(())
}
/// # Errors
/// Refuses the wrong role tuple, changed ownership, or a non-Intent participant.
pub fn stage_daemon(
&self,
staged: StagedProcessOwnership,
) -> Result<(), GitRoutingCoordinatorError> {
self.require_staged(staged)?;
let current = self.participant(staged.generation_id())?.ok_or(
GitRoutingCoordinatorError::InvalidState("Git-routing participant is absent"),
)?;
let Some(ParticipantOwnership::GitRouting(mut ownership)) = current.ownership().cloned()
else {
return Err(GitRoutingCoordinatorError::InvalidState(
"Git-routing ownership is invalid",
));
};
let expected = PersistedHostProcessOwnership::staged(staged.identity());
if current.state() != ParticipantState::Intent || ownership.mount.is_some() {
return Err(GitRoutingCoordinatorError::InvalidState(
"Git-routing daemon cannot be staged in this phase",
));
}
if ownership.daemon == expected {
return Ok(());
}
if !ownership.daemon.is_unassigned() {
return Err(GitRoutingCoordinatorError::InvalidState(
"Git-routing daemon ownership changed",
));
}
ownership.daemon = expected;
self.put(staged.generation_id(), ParticipantState::Intent, ownership)
}
/// # Errors
/// Refuses an identity that is not the exact durable staged daemon.
pub fn acknowledge_daemon(
&self,
staged: StagedProcessOwnership,
) -> Result<ProcessOwnershipCommitAck, GitRoutingCoordinatorError> {
self.require_staged(staged)?;
let ownership = self.ownership(staged.generation_id())?;
if ownership.daemon != PersistedHostProcessOwnership::staged(staged.identity()) {
return Err(GitRoutingCoordinatorError::InvalidState(
"staged Git-routing daemon is not durable",
));
}
Ok(ProcessOwnershipCommitAck::v1(staged))
}
/// # Errors
/// Refuses changed staged ownership, missing paths, or repeated mount evidence.
pub fn commit_ready(
&self,
staged: StagedProcessOwnership,
mount: MountIdentity,
) -> Result<(), GitRoutingCoordinatorError> {
self.require_staged(staged)?;
let mut ownership = self.ownership(staged.generation_id())?;
if ownership.daemon != PersistedHostProcessOwnership::staged(staged.identity())
|| ownership.mount.is_some()
|| ownership.mountpoint.is_none()
|| ownership.host_root.is_none()
|| ownership.isolated_root.is_none()
{
return Err(GitRoutingCoordinatorError::InvalidState(
"Git-routing readiness evidence is incomplete",
));
}
ownership.daemon = PersistedHostProcessOwnership::committed(staged.identity());
ownership.mount = Some(mount);
self.put(
staged.generation_id(),
ParticipantState::Prepared,
ownership,
)
}
/// # Errors
/// Refuses a participant that is not Prepared.
pub fn begin_release(
&self,
generation_id: GenerationId,
) -> Result<MountIdentity, GitRoutingCoordinatorError> {
let ownership = self.ownership(generation_id)?;
let mount = ownership
.mount
.ok_or(GitRoutingCoordinatorError::InvalidState(
"Git-routing mount evidence is absent",
))?;
let current =
self.participant(generation_id)?
.ok_or(GitRoutingCoordinatorError::InvalidState(
"Git-routing participant is absent",
))?;
if current.state() != ParticipantState::Prepared {
return Err(GitRoutingCoordinatorError::InvalidState(
"Git routing is not Prepared",
));
}
self.put(generation_id, ParticipantState::ReleaseIntent, ownership)?;
Ok(mount)
}
/// # Errors
/// Refuses changed mount evidence or a non-release participant.
pub fn complete_clean(
&self,
generation_id: GenerationId,
stopped_mount: MountIdentity,
) -> Result<(), GitRoutingCoordinatorError> {
let current =
self.participant(generation_id)?
.ok_or(GitRoutingCoordinatorError::InvalidState(
"Git-routing participant is absent",
))?;
let Some(ParticipantOwnership::GitRouting(ownership)) = current.ownership() else {
return Err(GitRoutingCoordinatorError::InvalidState(
"Git-routing ownership is invalid",
));
};
if current.state() != ParticipantState::ReleaseIntent
|| ownership.mount != Some(stopped_mount)
{
return Err(GitRoutingCoordinatorError::InvalidState(
"Git-routing stop evidence does not match release intent",
));
}
let clean = Participant::snapshot(
generation_id,
ParticipantRole::GitRouting,
ParticipantState::Clean,
Some(ParticipantOwnership::GitRouting(GitRoutingOwnership {
mountpoint: None,
host_root: None,
isolated_root: None,
daemon: PersistedHostProcessOwnership::unassigned(),
mount: None,
})),
)
.map_err(|_| GitRoutingCoordinatorError::InvalidState("Clean snapshot is invalid"))?;
self.store
.apply(&[DomainStoreMutation::CleanParticipantAfterAbsence(clean)])?;
Ok(())
}
fn require_staged(
&self,
staged: StagedProcessOwnership,
) -> Result<(), GitRoutingCoordinatorError> {
if staged.domain_id() != self.store.snapshot()?.domain.domain_id
|| staged.slot() != HostProcessOwnershipSlot::GitRoutingDaemon
{
return Err(GitRoutingCoordinatorError::InvalidState(
"Git-routing staged identity has the wrong tuple",
));
}
Ok(())
}
fn ownership(
&self,
generation_id: GenerationId,
) -> Result<GitRoutingOwnership, GitRoutingCoordinatorError> {
self.participant(generation_id)?
.and_then(|participant| match participant.ownership().cloned() {
Some(ParticipantOwnership::GitRouting(ownership)) => Some(ownership),
_ => None,
})
.ok_or(GitRoutingCoordinatorError::InvalidState(
"Git-routing ownership is absent",
))
}
fn participant(
&self,
generation_id: GenerationId,
) -> Result<Option<Participant>, GitRoutingCoordinatorError> {
Ok(self
.store
.snapshot()?
.participants
.into_iter()
.find(|participant| {
participant.generation_id() == generation_id
&& participant.role() == ParticipantRole::GitRouting
}))
}
fn put(
&self,
generation_id: GenerationId,
state: ParticipantState,
ownership: GitRoutingOwnership,
) -> Result<(), GitRoutingCoordinatorError> {
self.store
.apply(&[DomainStoreMutation::PutParticipant(participant(
generation_id,
state,
ownership,
)?)])?;
Ok(())
}
}
fn participant(
generation_id: GenerationId,
state: ParticipantState,
ownership: GitRoutingOwnership,
) -> Result<Participant, GitRoutingCoordinatorError> {
Participant::snapshot(
generation_id,
ParticipantRole::GitRouting,
state,
Some(ParticipantOwnership::GitRouting(ownership)),
)
.map_err(|_| GitRoutingCoordinatorError::InvalidState("participant snapshot is invalid"))
}
#[cfg(test)]
mod tests {
use std::fs::OpenOptions;
use lazaret_core::identity::{
BootId, ConfigurationFingerprint, CoordinatorId, DeviceId, DomainId, HostPidLocator,
InodeId, MountId, NetworkSidecarId, PidfsDeviceId, PidfsInodeId, PidfsProcessAuthorityV1,
PidfsProcessIdentityV1, ProcessStartTimeTicks, SandboxId,
};
use lazaret_core::records::{Domain, DurableDecision, Generation, HostProcessOwnershipSlot};
use lazaret_linux::filesystem::try_exclusive_flock;
use super::*;
#[test]
fn exact_router_lifecycle_reaches_clean_only_after_matching_stop() {
let fixture = Fixture::new();
let coordinator = GitRoutingCoordinator::new(&fixture.store);
coordinator
.begin_intent(
GenerationId::new(4),
HostPath::new(b"/runtime/workspace".to_vec()),
HostPath::new(b"/host/workspace".to_vec()),
HostPath::new(b"/state/ignored".to_vec()),
)
.unwrap();
let staged = staged(HostProcessOwnershipSlot::GitRoutingDaemon);
coordinator.stage_daemon(staged).unwrap();
assert!(
coordinator
.acknowledge_daemon(staged)
.unwrap()
.acknowledges(&staged)
);
let expected_mount = mount(21);
coordinator.commit_ready(staged, expected_mount).unwrap();
assert_eq!(
coordinator.begin_release(GenerationId::new(4)).unwrap(),
expected_mount
);
assert!(
coordinator
.complete_clean(GenerationId::new(4), mount(22))
.is_err()
);
coordinator
.complete_clean(GenerationId::new(4), expected_mount)
.unwrap();
let participant = fixture
.store
.snapshot()
.unwrap()
.participants
.into_iter()
.find(|participant| participant.role() == ParticipantRole::GitRouting)
.unwrap();
assert_eq!(participant.state(), ParticipantState::Clean);
}
#[test]
fn wrong_process_role_cannot_enter_router_ownership() {
let fixture = Fixture::new();
let coordinator = GitRoutingCoordinator::new(&fixture.store);
coordinator
.begin_intent(
GenerationId::new(4),
HostPath::new(b"/runtime/workspace".to_vec()),
HostPath::new(b"/host/workspace".to_vec()),
HostPath::new(b"/state/ignored".to_vec()),
)
.unwrap();
assert!(
coordinator
.stage_daemon(staged(HostProcessOwnershipSlot::SandboxRuntimeSupervisor))
.is_err()
);
}
struct Fixture {
_tree: tempfile::TempDir,
store: HostCoordinatorDomainStore,
}
impl Fixture {
fn new() -> Self {
let tree = tempfile::tempdir().unwrap();
let lock = OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(tree.path().join("domain.lock"))
.unwrap();
let store = HostCoordinatorDomainStore::open(
try_exclusive_flock(lock).unwrap(),
&tree.path().join("domain.redb"),
&Domain {
domain_id: DomainId::new([1; 32]),
profile_name: "default".into(),
canonical_workspace: HostPath::new(b"/host/workspace".to_vec()),
},
)
.unwrap();
store
.apply(&[DomainStoreMutation::PutGeneration(Generation {
domain_id: DomainId::new([1; 32]),
generation_id: GenerationId::new(4),
configuration_fingerprint: ConfigurationFingerprint::new([9; 32]),
coordinator_id: CoordinatorId::new([2; 32]),
network_sidecar_id: NetworkSidecarId::new([10; 32]),
sandbox_id: SandboxId::new([11; 32]),
durable_decision: DurableDecision::None,
})])
.unwrap();
Self { _tree: tree, store }
}
}
fn staged(slot: HostProcessOwnershipSlot) -> StagedProcessOwnership {
StagedProcessOwnership::v1(
DomainId::new([1; 32]),
GenerationId::new(4),
CoordinatorId::new([2; 32]),
slot,
PidfsProcessIdentityV1::new(
PidfsProcessAuthorityV1 {
boot_id: BootId::new([3; 16]),
pidfs_st_dev: PidfsDeviceId::new(5),
pidfs_st_ino: PidfsInodeId::new(6),
},
HostPidLocator::new(7),
ProcessStartTimeTicks::new(8),
),
)
}
fn mount(value: u64) -> MountIdentity {
MountIdentity {
mount_id: MountId::new(value),
device: DeviceId::new(value + 1),
inode: InodeId::new(value + 2),
}
}
}

View file

@ -5,6 +5,7 @@ pub mod client_bootstrap;
pub mod control;
pub mod domain_store;
pub mod escalation;
pub mod git_routing;
pub mod lifecycle;
pub mod network;
pub mod production;

View file

@ -32,6 +32,13 @@ fn main() {
}
return;
}
if let Some(result) = lazaret_ignored_workspace::dispatch_private_router(&arguments) {
if let Err(error) = result {
eprintln!("ignored workspace router failed: {error}");
std::process::exit(1);
}
return;
}
if let Some(result) =
lazaret_runtime_supervisor::supervision::dispatch_private_runtime_role(&arguments)
{

View file

@ -2,7 +2,7 @@ use std::cell::RefCell;
use std::ffi::OsString;
use std::fmt;
use std::fs::File;
use std::io::Write;
use std::io::{Read, Write};
use std::os::fd::{AsFd, OwnedFd};
use std::os::unix::ffi::OsStringExt;
use std::os::unix::fs::{FileExt, PermissionsExt};
@ -10,18 +10,25 @@ use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use lazaret_core::configuration::{ConfinedPathPlan, ResolvedDomainConfiguration};
use lazaret_core::identity::{BundleDigest, ContainerId, MountIdentity};
use lazaret_core::identity::{BundleDigest, ContainerId, HostPath, MountIdentity};
use lazaret_core::network::{ReachabilityExpectation, derive_ipv4_policy};
use lazaret_core::records::{
Generation, GenerationIdentity, HostProcessOwnershipSlot, IpAddress, PreparedArtifact, Session,
};
use lazaret_ignored_workspace::git_common_directory;
use lazaret_linux::filesystem::{mount_identity, namespace_identity};
use lazaret_linux::process::{ProcessOwnershipContext, current_process_identity};
use lazaret_linux::process::{
ProcessOwnershipContext, TargetCommand, child_broker, current_process_identity,
};
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
use lazaret_runtime_supervisor::sandbox::{
RootProcess, SandboxBuildRequest, SandboxBundle, build_sandbox_bundle,
build_sandbox_bundle_with_workspace,
};
use lazaret_runtime_supervisor::supervision::{
RuntimeGitRoutingAuthority, RuntimeGitRoutingLaunch, RuntimeNetworkLaunch,
RuntimeSupervisorLaunch,
};
use lazaret_runtime_supervisor::supervision::{RuntimeNetworkLaunch, RuntimeSupervisorLaunch};
use lazaret_state_anchor::{StateAnchorLaunchAuthority, StateAnchorLayout};
use lazaret_storage_catalog::Catalog;
use rustix::event::{PollFd, PollFlags, Timespec, poll};
@ -30,6 +37,7 @@ use rustix::fs::{
unlinkat,
};
use rustix::io::{FdFlags, dup, fcntl_setfd};
use rustix::pipe::{PipeFlags, pipe_with};
use sha2::{Digest, Sha256};
use crate::bootstrap::{
@ -141,6 +149,7 @@ pub struct ProductionColdStart<'a> {
bundle_digest: BundleDigest,
invoking_uid: u32,
staging: RuntimeStaging,
git_routing: Option<GitRoutingLayout>,
}
impl<'a> ProductionColdStart<'a> {
@ -149,6 +158,7 @@ impl<'a> ProductionColdStart<'a> {
/// # Errors
/// Refuses mismatched generation/configuration/probe inputs, changed host authority, unsafe
/// staging paths, or component construction failure.
#[allow(clippy::too_many_lines)]
pub fn stage(inputs: ProductionColdStartInputs<'a>) -> Result<Self, ProductionColdStartError> {
require_inputs(&inputs)?;
let runtime_domain = host_path(
@ -168,16 +178,28 @@ impl<'a> ProductionColdStart<'a> {
inputs.invoking_uid,
)
.map_err(|error| failure(format!("runtime staging: {error}")))?;
let git_routing = GitRoutingLayout::create(
inputs.configuration,
inputs.container_id,
inputs.invoking_uid,
)
.map_err(|error| failure(format!("Git-routing staging: {error}")))?;
let expected_network_namespace = namespace_identity(Path::new(NETWORK_NAMESPACE_PATH))
.map_err(|error| failure(format!("network namespace: {error}")))?;
let sandbox_bundle = build_sandbox_bundle(SandboxBuildRequest {
let request = SandboxBuildRequest {
configuration: inputs.configuration,
container_id: inputs.container_id,
expected_network_namespace,
root_process: inputs.root_process,
terminal: None,
prepared_root: inputs.prepared_artifact_label.to_owned(),
})
};
let sandbox_bundle = match &git_routing {
Some(git_routing) => {
build_sandbox_bundle_with_workspace(request, git_routing.mountpoint_label.clone())
}
None => build_sandbox_bundle(request),
}
.map_err(|error| failure(format!("sandbox bundle: {error}")))?;
staging
.write_bundle(sandbox_bundle.specification_json())
@ -197,7 +219,7 @@ impl<'a> ProductionColdStart<'a> {
inputs.prepared_artifact_authority,
)
.map_err(|error| failure(format!("State Anchor authority: {error}")))?;
let runtime_launch = RuntimeSupervisorLaunch::from_networked_runsc_command(
let mut runtime_launch = RuntimeSupervisorLaunch::from_networked_runsc_command(
ownership,
sandbox_bundle.command(),
RuntimeNetworkLaunch {
@ -208,6 +230,13 @@ impl<'a> ProductionColdStart<'a> {
)
.map_err(|error| failure(format!("Runtime Supervisor launch: {error}")))?
.with_state_anchor(state_anchor);
if let Some(git_routing) = &git_routing {
runtime_launch = runtime_launch.with_git_routing(
git_routing
.launch_authority(inputs.configuration)
.map_err(|error| failure(format!("Git-routing authority: {error}")))?,
);
}
let bundle_digest =
BundleDigest::new(Sha256::digest(sandbox_bundle.specification_json()).into());
let generation_identity = GenerationIdentity {
@ -236,6 +265,7 @@ impl<'a> ProductionColdStart<'a> {
bundle_digest,
invoking_uid: inputs.invoking_uid,
staging,
git_routing,
})
}
@ -363,6 +393,13 @@ impl<'a> ProductionColdStart<'a> {
interactive_executor: Arc::new(interactive_executor),
prompt_transport,
runtime_staging: &self.staging,
git_routing: self.git_routing.as_ref().map(|routing| {
crate::bootstrap::ProductionGitRouting {
mountpoint: routing.mountpoint(),
host_root: routing.host_root(),
isolated_root: routing.isolated_root(),
}
}),
})
}
}
@ -383,6 +420,253 @@ fn require_inputs(inputs: &ProductionColdStartInputs<'_>) -> Result<(), Producti
Ok(())
}
struct GitRoutingLayout {
_runtime_directory: RuntimeGitRoutingDirectory,
workspace: File,
workspace_label: PathBuf,
isolated: File,
isolated_label: PathBuf,
mountpoint: File,
mountpoint_label: PathBuf,
git_common: File,
git_common_label: PathBuf,
transient_hooks: File,
transient_hooks_label: PathBuf,
transient_config: File,
transient_config_label: PathBuf,
global_excludes: Option<PathBuf>,
isolate_ignored: bool,
}
impl GitRoutingLayout {
fn create(
configuration: &ResolvedDomainConfiguration,
container_id: ContainerId,
invoking_uid: u32,
) -> Result<Option<Self>, ProductionColdStartError> {
let workspace_label = host_path(configuration.workspace().canonical_label().as_bytes());
let workspace = File::from(
configuration
.workspace()
.open_authority()
.map_err(failure)?,
);
match statat(&workspace, ".git", AtFlags::SYMLINK_NOFOLLOW) {
Ok(_) => {}
Err(rustix::io::Errno::NOENT) => return Ok(None),
Err(error) => return Err(failure(error)),
}
let persistent_root = configuration
.persistent_domain_root()
.open_authority()
.map_err(failure)?;
let isolated = open_or_create_private_directory(
&persistent_root,
"ignored-workspace",
invoking_uid,
false,
)?;
let runtime_root = configuration
.runtime_domain_root()
.open_authority()
.map_err(failure)?;
let runtime_parent =
open_or_create_private_directory(&runtime_root, "git-routing", invoking_uid, false)?;
let runtime_name = OsString::from(hex(container_id.as_bytes()));
let directory =
open_or_create_private_directory(&runtime_parent, &runtime_name, invoking_uid, true)?;
let identity = FileIdentity::capture(&directory)?;
let runtime_directory = RuntimeGitRoutingDirectory {
parent: runtime_parent,
directory,
name: runtime_name.clone(),
identity,
};
let mountpoint = open_or_create_private_directory(
&runtime_directory.directory,
"workspace",
invoking_uid,
true,
)?;
let transient_hooks = open_or_create_private_directory(
&runtime_directory.directory,
"hooks",
invoking_uid,
true,
)?;
let transient_config = openat2(
&runtime_directory.directory,
"config",
OFlags::RDWR | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::RUSR | Mode::WUSR,
confined_resolve_flags(),
)
.map_err(failure)?;
fchmod(&transient_config, Mode::RUSR | Mode::WUSR).map_err(failure)?;
require_private_file(&transient_config, invoking_uid, 0o600)?;
let isolated_label = host_path(
configuration
.persistent_domain_root()
.canonical_label()
.as_bytes(),
)
.join("ignored-workspace");
let runtime_label = host_path(
configuration
.runtime_domain_root()
.canonical_label()
.as_bytes(),
)
.join("git-routing")
.join(&runtime_name);
let mountpoint_label = runtime_label.join("workspace");
let transient_hooks_label = runtime_label.join("hooks");
let transient_config_label = runtime_label.join("config");
let git_common_path = git_common_directory(&workspace_label).map_err(failure)?;
let git_common = open_absolute_directory(&git_common_path)?;
let global_excludes = global_excludes_path(&workspace_label)?;
Ok(Some(Self {
_runtime_directory: runtime_directory,
workspace,
workspace_label,
isolated: File::from(isolated),
isolated_label,
mountpoint: File::from(mountpoint),
mountpoint_label,
git_common,
git_common_label: git_common_path,
transient_hooks: File::from(transient_hooks),
transient_hooks_label,
transient_config: File::from(transient_config),
transient_config_label,
global_excludes,
isolate_ignored: configuration.fingerprint_record().isolate_gitignored,
}))
}
fn launch_authority(
&self,
configuration: &ResolvedDomainConfiguration,
) -> Result<RuntimeGitRoutingLaunch, ProductionColdStartError> {
configuration
.workspace()
.open_authority()
.map_err(failure)?;
RuntimeGitRoutingLaunch::new(RuntimeGitRoutingAuthority {
workspace: self.workspace.try_clone().map_err(failure)?,
workspace_label: self.workspace_label.clone(),
isolated: self.isolated.try_clone().map_err(failure)?,
mountpoint: self.mountpoint.try_clone().map_err(failure)?,
mountpoint_label: self.mountpoint_label.clone(),
git_common: self.git_common.try_clone().map_err(failure)?,
git_common_label: self.git_common_label.clone(),
transient_hooks: self.transient_hooks.try_clone().map_err(failure)?,
transient_hooks_label: self.transient_hooks_label.clone(),
transient_config: self.transient_config.try_clone().map_err(failure)?,
transient_config_label: self.transient_config_label.clone(),
global_excludes: self.global_excludes.clone(),
isolate_ignored: self.isolate_ignored,
})
.map_err(failure)
}
fn mountpoint(&self) -> HostPath {
HostPath::new(
self.mountpoint_label
.as_os_str()
.as_encoded_bytes()
.to_vec(),
)
}
fn host_root(&self) -> HostPath {
HostPath::new(self.workspace_label.as_os_str().as_encoded_bytes().to_vec())
}
fn isolated_root(&self) -> HostPath {
HostPath::new(self.isolated_label.as_os_str().as_encoded_bytes().to_vec())
}
}
struct RuntimeGitRoutingDirectory {
parent: OwnedFd,
directory: OwnedFd,
name: OsString,
identity: FileIdentity,
}
impl Drop for RuntimeGitRoutingDirectory {
fn drop(&mut self) {
remove_staging_directory(&self.parent, &self.name, &self.directory, self.identity);
}
}
fn open_absolute_directory(path: &Path) -> Result<File, ProductionColdStartError> {
let descriptor = openat2(
rustix::fs::CWD,
path,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::empty(),
ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
)
.map_err(failure)?;
Ok(File::from(descriptor))
}
fn global_excludes_path(workspace: &Path) -> Result<Option<PathBuf>, ProductionColdStartError> {
let (reader, writer) = pipe_with(PipeFlags::CLOEXEC).map_err(failure)?;
let target = TargetCommand::new("git")
.argument("-C")
.argument(workspace)
.argument("config")
.argument("--path")
.argument("--get")
.argument("--default")
.argument("")
.argument("core.excludesFile");
child_broker()
.run_transient_with_private_stdout(&target, writer)
.map_err(|error| failure(format!("Git global-excludes discovery: {error}")))?;
let mut output = Vec::new();
File::from(reader)
.read_to_end(&mut output)
.map_err(failure)?;
parse_global_excludes_path(workspace, &output)
}
fn parse_global_excludes_path(
workspace: &Path,
output: &[u8],
) -> Result<Option<PathBuf>, ProductionColdStartError> {
let value = output.strip_suffix(b"\n").unwrap_or(output);
if value.contains(&0) {
return Err(ProductionColdStartError(
"Git global-excludes path is invalid".into(),
));
}
if value.is_empty() {
return Ok(default_global_excludes_path());
}
let path = PathBuf::from(OsString::from_vec(value.to_vec()));
Ok(Some(if path.is_absolute() {
path
} else {
workspace.join(path)
}))
}
fn default_global_excludes_path() -> Option<PathBuf> {
std::env::var_os("XDG_CONFIG_HOME")
.filter(|path| !path.is_empty())
.map(PathBuf::from)
.or_else(|| {
std::env::var_os("HOME")
.filter(|path| !path.is_empty())
.map(|home| PathBuf::from(home).join(".config"))
})
.map(|home| home.join("git/ignore"))
}
pub(crate) struct RuntimeStaging {
runsc_root: OwnedFd,
bundles: OwnedFd,
@ -778,4 +1062,16 @@ mod tests {
let descriptor: OwnedFd = File::open("/dev/null").unwrap().into();
assert!(BootstrapOwnerGuard::from_inherited(descriptor).is_err());
}
#[test]
fn repository_relative_global_excludes_are_workspace_relative() {
let root = tempfile::tempdir().unwrap();
let workspace = root.path().join("workspace");
assert_eq!(
super::parse_global_excludes_path(&workspace, b"policy/ignore\n").unwrap(),
Some(workspace.join("policy/ignore"))
);
assert!(super::parse_global_excludes_path(&workspace, b"bad\0path\n").is_err());
}
}

View file

@ -26,6 +26,7 @@ use rustix::pipe::{PipeFlags, pipe_with};
use rustix::process::Signal;
use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore};
use crate::git_routing::GitRoutingCoordinator;
use crate::network::{NetworkCoordinator, NetworkCoordinatorError};
const SUPERVISOR_EXIT_TIMEOUT: Duration = Duration::from_secs(5);
@ -377,6 +378,8 @@ pub struct AwaitingAttachedRunsc {
lifetime: Option<std::os::fd::OwnedFd>,
staged: Option<StagedProcessOwnership>,
helper_staged: Option<StagedProcessOwnership>,
git_routing_required: bool,
git_routing_mount: Option<MountIdentity>,
network_evidence: Option<NetworkReadinessEvidence>,
runsc_command: lazaret_runtime_supervisor::sandbox::RunscCommand,
}
@ -453,6 +456,67 @@ impl AwaitingAttachedRunsc {
Ok(mount)
}
/// Requests generation-local routing and commits exact staged and mount evidence.
///
/// # Errors
/// Refuses missing intent, event mismatch, changed ownership, or channel failure.
pub fn start_git_routing(
&mut self,
coordinator: &GitRoutingCoordinator<'_>,
) -> Result<MountIdentity, SandboxCoordinatorError> {
if !self.git_routing_required || self.git_routing_mount.is_some() {
return Err(SandboxCoordinatorError::InvalidState(
"Git routing is not awaiting start",
));
}
let control = self
.control
.as_ref()
.ok_or(SandboxCoordinatorError::InvalidState(
"Runtime Supervisor control channel is absent",
))?;
control.start_git_routing()?;
let staged = match control.receive()? {
SupervisorEvent::GitRoutingStaged(staged) => staged,
SupervisorEvent::Refused(detail) => {
return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
}
_ => {
return Err(SandboxCoordinatorError::InvalidState(
"Runtime Supervisor did not stage the Git router",
));
}
};
coordinator
.stage_daemon(staged)
.map_err(|error| SandboxCoordinatorError::Io(io::Error::other(error)))?;
let acknowledgment = coordinator
.acknowledge_daemon(staged)
.map_err(|error| SandboxCoordinatorError::Io(io::Error::other(error)))?;
control.acknowledge_git_routing(acknowledgment)?;
let (ready_staged, mount) = match control.receive()? {
SupervisorEvent::GitRoutingReady { staged, mount } => (staged, mount),
SupervisorEvent::Refused(detail) => {
return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
}
_ => {
return Err(SandboxCoordinatorError::InvalidState(
"Runtime Supervisor did not report Git-routing readiness",
));
}
};
if ready_staged != staged {
return Err(SandboxCoordinatorError::InvalidState(
"Git-routing readiness changed daemon ownership",
));
}
coordinator
.commit_ready(staged, mount)
.map_err(|error| SandboxCoordinatorError::Io(io::Error::other(error)))?;
self.git_routing_mount = Some(mount);
Ok(mount)
}
/// Releases the RS-owned sidecar install path after exact State Anchor readiness.
///
/// # Errors
@ -550,6 +614,8 @@ impl AwaitingAttachedRunsc {
lifetime: Some(coordinator_lifetime),
staged: None,
helper_staged: None,
git_routing_required: launch.has_git_routing(),
git_routing_mount: None,
network_evidence: None,
runsc_command: launch.runsc_command().clone(),
})
@ -693,6 +759,11 @@ impl AwaitingAttachedRunsc {
"attached runsc cannot start before network attestation",
));
}
if self.git_routing_required && self.git_routing_mount.is_none() {
return Err(SandboxCoordinatorError::InvalidState(
"attached runsc cannot start before Git-routing readiness",
));
}
let event = self
.control
.as_ref()
@ -944,6 +1015,31 @@ impl CommittedSandboxRuntime {
coordinator.complete_clean_after_absence(self.generation_id)
}
/// Stops exact generation-local Git routing after sandbox absence.
///
/// # Errors
/// Refuses missing release intent, changed mount evidence, or an unexpected RS response.
pub fn stop_git_routing(
&mut self,
coordinator: &GitRoutingCoordinator<'_>,
) -> Result<(), SandboxCoordinatorError> {
let expected = coordinator
.begin_release(self.generation_id)
.map_err(|error| SandboxCoordinatorError::Io(io::Error::other(error)))?;
self.control.stop_git_routing()?;
match self.control.receive()? {
SupervisorEvent::GitRoutingStopped { mount } if mount == expected => coordinator
.complete_clean(self.generation_id, mount)
.map_err(|error| SandboxCoordinatorError::Io(io::Error::other(error))),
SupervisorEvent::Refused(detail) => {
Err(SandboxCoordinatorError::Io(io::Error::other(detail)))
}
_ => Err(SandboxCoordinatorError::InvalidState(
"Runtime Supervisor did not stop exact Git routing",
)),
}
}
/// Stops only the RS-owned network resources after sandbox absence.
///
/// # Errors

View file

@ -41,6 +41,15 @@ fn validated_inputs_stage_one_complete_plan_and_drop_exact_runtime_artifacts() {
assert!(!control.exists());
}
#[test]
fn non_git_workspace_stages_without_a_routing_runtime() {
let fixture = ProductionFixture::new();
let staged = ProductionColdStart::stage(fixture.inputs(&fixture.escalation_client)).unwrap();
assert!(!fixture.runtime_domain.join("git-routing").exists());
drop(staged);
}
#[test]
fn cleanup_leaves_a_replacement_directory_untouched() {
let fixture = ProductionFixture::new();