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 {
|
if participant.state() == ParticipantState::Clean {
|
||||||
return Ok(());
|
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
|
let root = configuration
|
||||||
.runtime_domain_root()
|
.runtime_domain_root()
|
||||||
.open_authority()
|
.open_authority()
|
||||||
.map_err(|error| error.to_string())?;
|
.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)?;
|
require_publication_labels(publication)?;
|
||||||
remove_exact_publication_resource(
|
remove_exact_publication_resource(
|
||||||
&root,
|
&root,
|
||||||
|
|
@ -815,6 +816,27 @@ fn recover_publication(
|
||||||
clean_role_after_absence(store, generation_id, ParticipantRole::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(
|
fn require_publication_labels(
|
||||||
ownership: &lazaret_core::records::PublicationOwnership,
|
ownership: &lazaret_core::records::PublicationOwnership,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
|
@ -1280,9 +1302,10 @@ fn hex_lower(bytes: &[u8]) -> String {
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fs::{self, File, OpenOptions};
|
use std::fs::{self, File, OpenOptions};
|
||||||
use std::os::unix::fs::MetadataExt;
|
|
||||||
use std::os::unix::fs::symlink;
|
use std::os::unix::fs::symlink;
|
||||||
|
use std::os::unix::fs::{FileTypeExt, MetadataExt};
|
||||||
use std::os::unix::net::UnixListener;
|
use std::os::unix::net::UnixListener;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use lazaret_core::configuration::{ConfigurationLoadRequest, load_trusted_configuration};
|
use lazaret_core::configuration::{ConfigurationLoadRequest, load_trusted_configuration};
|
||||||
use lazaret_core::identity::{
|
use lazaret_core::identity::{
|
||||||
|
|
@ -1517,6 +1540,61 @@ mod tests {
|
||||||
.unwrap();
|
.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]
|
#[test]
|
||||||
fn production_preflight_retries_every_recovery_boundary() {
|
fn production_preflight_retries_every_recovery_boundary() {
|
||||||
for failed in [
|
for failed in [
|
||||||
|
|
@ -1993,6 +2071,74 @@ mod tests {
|
||||||
prepared,
|
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 {
|
fn path_identity(root: &Path, path: &Path) -> MountIdentity {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue