Refuse unrecorded publication artifacts during recovery
This commit is contained in:
parent
2bc5982a62
commit
d597b05484
1 changed files with 151 additions and 5 deletions
|
|
@ -785,16 +785,17 @@ fn recover_publication(
|
|||
if participant.state() == ParticipantState::Clean {
|
||||
return Ok(());
|
||||
}
|
||||
let Some(ParticipantOwnership::Publication(publication)) = participant.ownership() else {
|
||||
if participant.ownership().is_none() {
|
||||
return clean_role_after_absence(store, generation_id, ParticipantRole::Publication);
|
||||
}
|
||||
return Err("publication ownership shape is invalid".into());
|
||||
};
|
||||
let root = configuration
|
||||
.runtime_domain_root()
|
||||
.open_authority()
|
||||
.map_err(|error| error.to_string())?;
|
||||
let Some(ParticipantOwnership::Publication(publication)) = participant.ownership() else {
|
||||
if participant.ownership().is_none() {
|
||||
require_bare_publication_absent(&root)?;
|
||||
return clean_role_after_absence(store, generation_id, ParticipantRole::Publication);
|
||||
}
|
||||
return Err("publication ownership shape is invalid".into());
|
||||
};
|
||||
require_publication_labels(publication)?;
|
||||
remove_exact_publication_resource(
|
||||
&root,
|
||||
|
|
@ -815,6 +816,27 @@ fn recover_publication(
|
|||
clean_role_after_absence(store, generation_id, ParticipantRole::Publication)
|
||||
}
|
||||
|
||||
fn require_bare_publication_absent(root: &std::os::fd::OwnedFd) -> Result<(), String> {
|
||||
for label in [
|
||||
STAGED_CONTROL_SOCKET_NAME,
|
||||
STAGED_PUBLICATION_IDENTITY_NAME,
|
||||
CONTROL_SOCKET_NAME,
|
||||
PUBLICATION_IDENTITY_NAME,
|
||||
] {
|
||||
match openat(
|
||||
root,
|
||||
label,
|
||||
OFlags::PATH | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::empty(),
|
||||
) {
|
||||
Err(rustix::io::Errno::NOENT) => {}
|
||||
Err(error) => return Err(error.to_string()),
|
||||
Ok(_) => return Err("bare publication intent has a fixed-label artifact".into()),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn require_publication_labels(
|
||||
ownership: &lazaret_core::records::PublicationOwnership,
|
||||
) -> Result<(), String> {
|
||||
|
|
@ -1280,9 +1302,10 @@ fn hex_lower(bytes: &[u8]) -> String {
|
|||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::os::unix::fs::symlink;
|
||||
use std::os::unix::fs::{FileTypeExt, MetadataExt};
|
||||
use std::os::unix::net::UnixListener;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use lazaret_core::configuration::{ConfigurationLoadRequest, load_trusted_configuration};
|
||||
use lazaret_core::identity::{
|
||||
|
|
@ -1517,6 +1540,61 @@ mod tests {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum PublicationFixtureBoundary {
|
||||
TrulyAbsent,
|
||||
AfterStagedIdentity,
|
||||
AfterStagedSocket,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bare_publication_intent_requires_every_fixed_label_to_be_absent() {
|
||||
let absent = ProductionRecoveryFixture::new();
|
||||
absent.prepare_bare_publication(PublicationFixtureBoundary::TrulyAbsent);
|
||||
recover_publication(&absent.configuration, &absent.store, GenerationId::new(2)).unwrap();
|
||||
assert_eq!(absent.publication(2).state(), ParticipantState::Clean);
|
||||
|
||||
let identity = ProductionRecoveryFixture::new();
|
||||
identity.prepare_bare_publication(PublicationFixtureBoundary::AfterStagedIdentity);
|
||||
assert!(
|
||||
recover_publication(
|
||||
&identity.configuration,
|
||||
&identity.store,
|
||||
GenerationId::new(2)
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
identity
|
||||
.runtime_domain()
|
||||
.join(STAGED_PUBLICATION_IDENTITY_NAME)
|
||||
.is_file()
|
||||
);
|
||||
assert_eq!(identity.publication(2).state(), ParticipantState::Intent);
|
||||
assert!(identity.publication(2).ownership().is_none());
|
||||
|
||||
let socket = ProductionRecoveryFixture::new();
|
||||
socket.prepare_bare_publication(PublicationFixtureBoundary::AfterStagedSocket);
|
||||
assert!(
|
||||
recover_publication(&socket.configuration, &socket.store, GenerationId::new(2))
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
socket
|
||||
.runtime_domain()
|
||||
.join(STAGED_PUBLICATION_IDENTITY_NAME)
|
||||
.is_file()
|
||||
);
|
||||
assert!(
|
||||
fs::symlink_metadata(socket.runtime_domain().join(STAGED_CONTROL_SOCKET_NAME))
|
||||
.unwrap()
|
||||
.file_type()
|
||||
.is_socket()
|
||||
);
|
||||
assert_eq!(socket.publication(2).state(), ParticipantState::Intent);
|
||||
assert!(socket.publication(2).ownership().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn production_preflight_retries_every_recovery_boundary() {
|
||||
for failed in [
|
||||
|
|
@ -1993,6 +2071,74 @@ mod tests {
|
|||
prepared,
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_domain(&self) -> PathBuf {
|
||||
Path::new(std::ffi::OsStr::from_bytes(
|
||||
self.configuration
|
||||
.runtime_domain_root()
|
||||
.canonical_label()
|
||||
.as_bytes(),
|
||||
))
|
||||
.to_path_buf()
|
||||
}
|
||||
|
||||
fn prepare_bare_publication(&self, boundary: PublicationFixtureBoundary) {
|
||||
let runtime_domain = self.runtime_domain();
|
||||
fs::remove_file(runtime_domain.join(CONTROL_SOCKET_NAME)).unwrap();
|
||||
fs::remove_file(runtime_domain.join(PUBLICATION_IDENTITY_NAME)).unwrap();
|
||||
let generation = Generation {
|
||||
domain_id: self.configuration.domain_id(),
|
||||
generation_id: GenerationId::new(2),
|
||||
configuration_fingerprint: self.configuration.configuration_fingerprint(),
|
||||
coordinator_id: CoordinatorId::new([13; 32]),
|
||||
network_sidecar_id: NetworkSidecarId::new([14; 32]),
|
||||
sandbox_id: SandboxId::new([15; 32]),
|
||||
durable_decision: DurableDecision::None,
|
||||
};
|
||||
self.store
|
||||
.apply(&[DomainStoreMutation::PutGeneration(generation.clone())])
|
||||
.unwrap();
|
||||
self.store
|
||||
.apply(&[
|
||||
DomainStoreMutation::PutGeneration(Generation {
|
||||
durable_decision: DurableDecision::Preparing,
|
||||
..generation
|
||||
}),
|
||||
DomainStoreMutation::PutParticipant(Participant::bare_intent(
|
||||
GenerationId::new(2),
|
||||
ParticipantRole::Publication,
|
||||
)),
|
||||
])
|
||||
.unwrap();
|
||||
if !matches!(boundary, PublicationFixtureBoundary::TrulyAbsent) {
|
||||
fs::write(
|
||||
runtime_domain.join(STAGED_PUBLICATION_IDENTITY_NAME),
|
||||
b"identity",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
if matches!(boundary, PublicationFixtureBoundary::AfterStagedSocket) {
|
||||
let root = File::open(runtime_domain).unwrap();
|
||||
drop(
|
||||
UnixListener::bind(format!(
|
||||
"/proc/self/fd/{}/{}",
|
||||
root.as_raw_fd(),
|
||||
STAGED_CONTROL_SOCKET_NAME
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn publication(&self, generation: u64) -> Participant {
|
||||
role(
|
||||
&self.store,
|
||||
GenerationId::new(generation),
|
||||
ParticipantRole::Publication,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn path_identity(root: &Path, path: &Path) -> MountIdentity {
|
||||
|
|
|
|||
Loading…
Reference in a new issue