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
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
444
crates/lazaret-host-coordinator/src/git_routing.rs
Normal file
444
crates/lazaret-host-coordinator/src/git_routing.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue