diff --git a/src/loader/mod.rs b/src/loader/mod.rs
index 5bb26dd7..b49bbbf2 100644
--- a/src/loader/mod.rs
+++ b/src/loader/mod.rs
@@ -249,7 +249,7 @@ async fn refresh(
unreachable!("source-specific loading succeeded and must have filled 'builder'")
});
- handle.finish_load(built);
+ handle.finish_load(built, soa.rdata.serial);
}
Err(err) => {
diff --git a/src/server/mod.rs b/src/server/mod.rs
index 0bc99fc6..71c14a8d 100644
--- a/src/server/mod.rs
+++ b/src/server/mod.rs
@@ -2,9 +2,9 @@
use std::{fmt, sync::Arc};
-use cascade_api::{ZoneReviewDecision, ZoneReviewResult};
use cascade_zonedata::{LoadedZoneReviewer, SignedZoneReviewer, ZoneViewer};
use domain::base::Serial;
+use tracing::{debug, error, info};
use crate::{
center::Center,
@@ -12,7 +12,7 @@ use crate::{
manager::Terminated,
units::zone_server::{Source, ZoneServer},
util::AbortOnDrop,
- zone::Zone,
+ zone::{UpcomingInstance, Zone, ZoneHandle, machine::ZoneStateMachine},
};
mod request;
@@ -62,15 +62,75 @@ impl LoadedReviewServer {
ZoneServer::new(Source::Unsigned).on_seek_approval_for_zone(center, zone, zone_serial)
}
- /// Process a review of a served instance.
+ /// Process a review of the upcoming loaded instance.
+ #[tracing::instrument(
+ level = "trace",
+ skip_all,
+ fields(zone = %zone.name, r#type = "loaded", zone_serial, ?decision)
+ )]
pub fn process_review(
center: &Arc
,
zone: &Arc,
zone_serial: Serial,
- decision: ZoneReviewDecision,
- ) -> ZoneReviewResult {
- // TODO: Inline.
- ZoneServer::new(Source::Unsigned).on_zone_review(center, zone, zone_serial, decision)
+ decision: cascade_api::ZoneReviewDecision,
+ ) -> cascade_api::ZoneReviewResult {
+ let mut state = zone.state.lock().unwrap();
+ let mut handle = ZoneHandle {
+ zone,
+ state: &mut state,
+ center,
+ };
+
+ // Ensure the zone is in loader review.
+ let ZoneStateMachine::LoadedReview(machine) = &mut handle.state.machine else {
+ debug!("The zone is not in loaded-review state");
+
+ return Err(cascade_api::ZoneReviewError::NotUnderReview);
+ };
+ if machine.decided {
+ debug!("The instance has already been reviewed");
+
+ return Err(cascade_api::ZoneReviewError::NotUnderReview);
+ }
+
+ // Look up the upcoming instance.
+ let Some(UpcomingInstance {
+ loaded: Some(loaded),
+ signed: None,
+ }) = &handle.state.instances.upcoming
+ else {
+ unreachable!("'UpcomingInstance' is inconsistent with 'LoadedReview'")
+ };
+
+ // Ensure the serial number is correct.
+ if Serial(loaded.serial.into()) != zone_serial {
+ debug!(
+ "The upcoming loaded instance has serial '{}', not '{zone_serial}'",
+ loaded.serial
+ );
+
+ return Err(cascade_api::ZoneReviewError::NotUnderReview);
+ }
+
+ // Remember that a review has been received.
+ machine.decided = true;
+
+ match decision {
+ cascade_api::ZoneReviewDecision::Approve => {
+ info!("Approving the upcoming loaded instance");
+
+ handle.approve_loaded();
+ }
+
+ cascade_api::ZoneReviewDecision::Reject => {
+ error!("Rejecting the upcoming loaded instance");
+
+ // TODO: Whether to soft or hard reject should be part of the policy
+ handle.hard_reject_loaded();
+ }
+ }
+
+ Ok(cascade_api::ZoneReviewOutput {})
}
/// Register a new zone.
@@ -151,15 +211,75 @@ impl SignedReviewServer {
ZoneServer::new(Source::Signed).on_seek_approval_for_zone(center, zone, zone_serial)
}
- /// Process a review of a served instance.
+ /// Process a review of the upcoming signed instance.
+ #[tracing::instrument(
+ level = "trace",
+ skip_all,
+ fields(zone = %zone.name, r#type = "signed", zone_serial, ?decision)
+ )]
pub fn process_review(
center: &Arc,
zone: &Arc,
zone_serial: Serial,
- decision: ZoneReviewDecision,
- ) -> ZoneReviewResult {
- // TODO: Inline.
- ZoneServer::new(Source::Signed).on_zone_review(center, zone, zone_serial, decision)
+ decision: cascade_api::ZoneReviewDecision,
+ ) -> cascade_api::ZoneReviewResult {
+ let mut state = zone.state.lock().unwrap();
+ let mut handle = ZoneHandle {
+ zone,
+ state: &mut state,
+ center,
+ };
+
+ // Ensure the zone is in signer review.
+ let ZoneStateMachine::SignedReview(machine) = &mut handle.state.machine else {
+ debug!("The zone is not in signed-review state");
+
+ return Err(cascade_api::ZoneReviewError::NotUnderReview);
+ };
+ if machine.decided {
+ debug!("The instance has already been reviewed");
+
+ return Err(cascade_api::ZoneReviewError::NotUnderReview);
+ }
+
+ // Look up the upcoming instance.
+ let Some(UpcomingInstance {
+ loaded: _,
+ signed: Some(signed),
+ }) = &handle.state.instances.upcoming
+ else {
+ unreachable!("'UpcomingInstance' is inconsistent with 'LoadedReview'")
+ };
+
+ // Ensure the serial number is correct.
+ if Serial(signed.serial.into()) != zone_serial {
+ debug!(
+ "The upcoming signed instance has serial '{}', not '{zone_serial}'",
+ signed.serial
+ );
+
+ return Err(cascade_api::ZoneReviewError::NotUnderReview);
+ }
+
+ // Remember that a review has been received.
+ machine.decided = true;
+
+ match decision {
+ cascade_api::ZoneReviewDecision::Approve => {
+ info!("Approving the upcoming signed instance");
+
+ handle.approve_signed();
+ }
+
+ cascade_api::ZoneReviewDecision::Reject => {
+ error!("Rejecting the upcoming signed instance");
+
+ // TODO: Whether to soft or hard reject should be part of the policy
+ handle.hard_reject_signed();
+ }
+ }
+
+ Ok(cascade_api::ZoneReviewOutput {})
}
/// Register a new zone.
diff --git a/src/signer/mod.rs b/src/signer/mod.rs
index 0ad89ca5..f76b0c24 100644
--- a/src/signer/mod.rs
+++ b/src/signer/mod.rs
@@ -23,7 +23,7 @@ use std::{
};
use cascade_zonedata::SignedZoneBuilder;
-use tracing::error;
+use tracing::{debug, error};
use crate::units::zone_signer::SignerError;
use crate::{
@@ -85,8 +85,16 @@ async fn sign(
match result {
Ok(()) => {
+ let soa = builder.next_signed().unwrap().soa().clone();
+
+ debug!(
+ zone = %zone.name,
+ serial = ?soa.rdata.serial,
+ "Generated a new signed instance of the zone"
+ );
+
let built = builder.finish().unwrap_or_else(|_| unreachable!());
- handle.finish_signing(built);
+ handle.finish_signing(built, soa.rdata.serial);
status.status.finish(true);
status.current_action = "Finished".to_string();
}
diff --git a/src/units/http_server.rs b/src/units/http_server.rs
index 7fbc9873..de9fc530 100644
--- a/src/units/http_server.rs
+++ b/src/units/http_server.rs
@@ -438,21 +438,18 @@ impl HttpServer {
}
});
- unsigned_serial = zone_state
- .storage
- .loaded_review_soa
- .as_ref()
- .map(|r| Serial::from(u32::from(r.rdata.serial)));
- signed_serial = zone_state
- .storage
- .signed_review_soa
- .as_ref()
- .map(|r| Serial::from(u32::from(r.rdata.serial)));
+ let upcoming = zone_state.instances.upcoming.as_ref();
+ unsigned_serial = upcoming
+ .and_then(|i| i.loaded.as_ref())
+ .map(|i| Serial(u32::from(i.serial)));
+ signed_serial = upcoming
+ .and_then(|i| i.signed.as_ref())
+ .map(|i| Serial(u32::from(i.serial)));
published_serial = zone_state
- .storage
- .published_soa
+ .instances
+ .current
.as_ref()
- .map(|r| Serial::from(u32::from(r.rdata.serial)));
+ .map(|i| Serial(u32::from(i.signed.serial)));
progress = match zone_state.machine {
ZoneStateMachine::Waiting(..) => Progress::Waiting,
@@ -467,11 +464,12 @@ impl HttpServer {
};
last_published = zone_state
- .last_published
+ .instances
+ .current
.as_ref()
- .map(|p| LastPublishedZone {
- loaded_serial: p.loaded_serial,
- signed_serial: p.signed_serial,
+ .map(|i| LastPublishedZone {
+ loaded_serial: Serial(i.loaded.serial.into()),
+ signed_serial: Serial(i.signed.serial.into()),
});
let mut found_error = None;
diff --git a/src/units/zone_server.rs b/src/units/zone_server.rs
index 2b105bad..0a41858f 100644
--- a/src/units/zone_server.rs
+++ b/src/units/zone_server.rs
@@ -26,9 +26,7 @@ use domain::tsig::{Algorithm, KeyStore};
use domain::zonetree::StoredName;
use tracing::{debug, error, info, trace, warn};
-use crate::api::{
- ZoneReviewDecision, ZoneReviewError, ZoneReviewOutput, ZoneReviewResult, ZoneReviewStatus,
-};
+use crate::api::{ZoneReviewDecision, ZoneReviewStatus};
use crate::center::Center;
use crate::config::SocketConfig;
use crate::daemon::SocketProvider;
@@ -37,10 +35,7 @@ use crate::manager::record_zone_event;
use crate::policy::NameserverCommsPolicy;
use crate::server::{LoadedReviewServer, PublicationServer, SignedReviewServer};
use crate::util::AbortOnDrop;
-use crate::zone::{
- HistoricalEvent, SignedZoneVersionState, UnsignedZoneVersionState, Zone, ZoneHandle,
- ZoneVersionReviewState,
-};
+use crate::zone::{HistoricalEvent, Zone, ZoneHandle};
/// The source of a zone server.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -315,37 +310,6 @@ impl ZoneServer {
"[{unit_name}]: Seeking approval for {zone_type} zone '{zone_name}' at serial {zone_serial}."
);
- // Mark this version of the zone as pending approval.
- //
- // TODO: These entries should have been created a long time ago, but
- // not all components use these fields yet. For now, they need to be
- // created over here -- hence 'or_insert_with()'.
- {
- let mut zone_state = zone.state.lock().unwrap();
- match self.source {
- Source::Unsigned => {
- zone_state
- .unsigned
- .entry(zone_serial)
- .or_insert_with(|| UnsignedZoneVersionState {
- review: Default::default(),
- })
- .review = ZoneVersionReviewState::Pending;
- }
- Source::Signed => {
- zone_state
- .signed
- .entry(zone_serial)
- .or_insert_with(|| SignedZoneVersionState {
- unsigned_serial: Serial::from(0), // TODO
- review: Default::default(),
- })
- .review = ZoneVersionReviewState::Pending;
- }
- Source::Published => unreachable!(),
- }
- }
-
record_zone_event(center, zone, pending_event, Some(zone_serial));
if review.cmd_hook.is_none() || review_server.is_none() {
@@ -451,31 +415,33 @@ impl ZoneServer {
"[{unit_name}]: Failed to execute hook '{hook}' for {zone_type} zone '{zone_name}' at serial {zone_serial}: {err}"
);
- {
- let mut zone_state = zone.state.lock().unwrap();
- match self.source {
- Source::Unsigned => {
- zone_state.record_event(
- HistoricalEvent::UnsignedHookFailed {
- err: err.to_string(),
- },
- Some(zone_serial),
- );
- zone_state.unsigned.get_mut(&zone_serial).unwrap().review =
- ZoneVersionReviewState::Rejected;
- }
- Source::Signed => {
- zone_state.record_event(
- HistoricalEvent::SignedHookFailed {
- err: err.to_string(),
- },
- Some(zone_serial),
- );
- zone_state.signed.get_mut(&zone_serial).unwrap().review =
- ZoneVersionReviewState::Rejected;
- }
- Source::Published => unreachable!(),
+ let mut state = zone.state.lock().unwrap();
+ let mut handle = ZoneHandle {
+ zone,
+ state: &mut state,
+ center,
+ };
+
+ match self.source {
+ Source::Unsigned => {
+ handle.state.record_event(
+ HistoricalEvent::UnsignedHookFailed {
+ err: err.to_string(),
+ },
+ Some(zone_serial),
+ );
+ handle.hard_reject_loaded();
+ }
+ Source::Signed => {
+ handle.state.record_event(
+ HistoricalEvent::SignedHookFailed {
+ err: err.to_string(),
+ },
+ Some(zone_serial),
+ );
+ handle.hard_reject_signed();
}
+ Source::Published => unreachable!(),
}
}
}
@@ -534,120 +500,6 @@ impl ZoneServer {
}
Ok(())
}
-
- pub fn on_zone_review(
- &self,
- center: &Arc,
- zone: &Arc,
- zone_serial: Serial,
- decision: ZoneReviewDecision,
- ) -> ZoneReviewResult {
- let unit_name = self.unit_name();
- let zone_name = &zone.name;
-
- // Look up the zone.
-
- let new_review_state = match decision {
- ZoneReviewDecision::Approve => ZoneVersionReviewState::Approved,
- ZoneReviewDecision::Reject => ZoneVersionReviewState::Rejected,
- };
-
- // Look up the version of the zone being reviewed.
- match self.source {
- Source::Unsigned => {
- {
- let mut zone_state = zone.state.lock().unwrap();
- let Some(version) = zone_state.unsigned.get_mut(&zone_serial) else {
- // 'on_seek_approval_for_zone_cmd()' should have created
- // this. Since it doesn't exist, the zone is not under
- // review.
-
- debug!(
- "[{unit_name}] Got a review for {zone_name}/{zone_serial}, but it was not pending review"
- );
- return Err(ZoneReviewError::NotUnderReview);
- };
-
- // Check that the zone was not already approved.
- if matches!(version.review, ZoneVersionReviewState::Approved) {
- // This version of the zone is no longer being reviewed.
- //
- // TODO: Differentiate this from 'NotUnderReview'?
-
- return Err(ZoneReviewError::NotUnderReview);
- }
-
- version.review = new_review_state;
- }
- if matches!(decision, ZoneReviewDecision::Approve) {
- info!(
- "Unsigned zone '{zone_name}' with serial {zone_serial} has been approved."
- );
- self.on_unsigned_zone_approved(center, zone, zone_serial);
- } else {
- error!(
- "Unsigned zone '{zone_name}' with serial {zone_serial} has been rejected."
- );
-
- // TODO: Whether to soft or hard reject should be part of the policy
- let mut state = zone.state.lock().unwrap();
- ZoneHandle {
- zone,
- state: &mut state,
- center,
- }
- .hard_reject_loaded();
- }
- }
-
- Source::Signed => {
- {
- let mut zone_state = zone.state.lock().unwrap();
- let Some(version) = zone_state.signed.get_mut(&zone_serial) else {
- // 'on_seek_approval_for_zone_cmd()' should have created
- // this. Since it doesn't exist, the zone is not under
- // review.
-
- debug!(
- "[{unit_name}] Got a review for {zone_name}/{zone_serial}, but it was not pending review"
- );
- return Err(ZoneReviewError::NotUnderReview);
- };
-
- // Check that the zone was not already approved.
- if matches!(version.review, ZoneVersionReviewState::Approved) {
- // This version of the zone is no longer being reviewed.
- //
- // TODO: Differentiate this from 'NotUnderReview'?
-
- return Err(ZoneReviewError::NotUnderReview);
- }
-
- version.review = new_review_state;
- }
- if matches!(decision, ZoneReviewDecision::Approve) {
- info!("Signed zone '{zone_name}' with serial {zone_serial} has been approved.");
- self.on_signed_zone_approved(center, zone, zone_serial);
- } else {
- error!(
- "Signed zone '{zone_name}' with serial {zone_serial} has been rejected."
- );
- // TODO: Whether to soft or hard reject should be part of the policy
- let mut state = zone.state.lock().unwrap();
- ZoneHandle {
- zone,
- state: &mut state,
- center,
- }
- .hard_reject_signed();
- }
- }
-
- Source::Published => unreachable!(),
- };
-
- Ok(ZoneReviewOutput {})
- }
}
impl std::fmt::Debug for ZoneServer {
diff --git a/src/zone/instance.rs b/src/zone/instance.rs
new file mode 100644
index 00000000..8ebda2f6
--- /dev/null
+++ b/src/zone/instance.rs
@@ -0,0 +1,340 @@
+//! Instances of zones.
+//!
+//! An _instance_ is a snapshot of the contents of a zone (the specific records
+//! within it). Cascade maintains information about various instances, e.g. how
+//! they were generated and information needed to re-sign them.
+//!
+//! Instances can be classified by their origin and purpose: _loaded_ instances
+//! are generated by Cascade's zone loader (by loading from a configured zone
+//! source) and _signed_ instances are generated by the zone signer (by signing
+//! a loaded instance or re-signing a prior signed instance). An "instance" in
+//! general refers to a pair of loaded and signed instances (where the signed
+//! instance was generated from the loaded instance).
+//!
+//! Instances (whether loaded or signed) can be in one of four states:
+//!
+//! - _Upcoming_ (i.e. _Next_): The instance is being generated and reviewed. If
+//! all goes well, it will become _Current_.
+//!
+//! - _Current_ (i.e. _Published_): The instance has been accepted and is now
+//! being published by Cascade as the latest authoritative version.
+//!
+//! - _Obsolete_: The instance was previously _Current_, but has since been
+//! replaced by a new _Current_ instance.
+//!
+//! - _Abandoned_: The instance failed to be accepted (e.g. due to an error in
+//! generation or being rejected at review).
+//!
+//! Instances are tracked by [`Instances`]. At any time, there are (at most)
+//! one loaded+signed pair of upcoming instances, and one loaded+signed pair
+//! of current instances. There may be any number of obsolete and abandoned
+//! instances.
+
+use domain::new::base::Serial;
+
+//----------- Instances --------------------------------------------------------
+
+/// State regarding instances of the zone.
+///
+/// See the module-level documentation for more information.
+#[derive(Debug, Default)]
+pub struct Instances {
+ /// The upcoming instance, if any.
+ ///
+ /// This is [`Some`] when a new instance is being built (a new instance of
+ /// the zone is being loaded, or the zone is being re-signed).
+ pub upcoming: Option,
+
+ /// The current/published instance.
+ ///
+ /// In general, this will be [`Some`]; most zones have a current instance.
+ /// It is [`None`] if an instance of the zone has never been accepted, or
+ /// the instance could not be restored from disk on startup.
+ pub current: Option,
+ //
+ // TODO:
+ // - The next usable loaded/signed instance IDs.
+ // These increase monotonically, even if instances are abandoned.
+ // - Obsolete instances.
+ // - Abandoned instances.
+}
+
+/// # Initiating operations
+impl Instances {
+ /// Initiate a new operation.
+ ///
+ /// An upcoming instance will be prepared.
+ ///
+ /// ## Panics
+ ///
+ /// Panics if an upcoming instance already exists.
+ pub fn start_load(&mut self) {
+ assert!(
+ self.upcoming.is_none(),
+ "Cannot start a load while an upcoming instance exists",
+ );
+
+ self.upcoming = Some(UpcomingInstance {
+ loaded: None,
+ signed: None,
+ });
+ }
+
+ /// Initiate a re-sign operation.
+ ///
+ /// ## Panics
+ ///
+ /// Panics if:
+ /// - An upcoming instance already exists.
+ /// - There is no current loaded instance of the zone.
+ pub fn start_resign(&mut self) {
+ assert!(
+ self.upcoming.is_none(),
+ "Cannot start a re-sign while an upcoming instance exists",
+ );
+
+ assert!(
+ self.current.is_some(),
+ "Cannot start a re-sign without a current loaded instance",
+ );
+
+ self.upcoming = Some(UpcomingInstance {
+ loaded: None,
+ signed: None,
+ });
+ }
+
+ /// Clear the current instance.
+ ///
+ /// This method should be called if the data for the zone is being cleared.
+ /// The current instance (if any) will be abandoned.
+ ///
+ /// ## Panics
+ ///
+ /// Panics if there is an upcoming instance of the zone. This method must be
+ /// called after the upcoming instance is resolved (applied or abandoned).
+ pub fn clear(&mut self) {
+ // TODO: Save the current instance to a list of obsolete ones.
+
+ assert!(
+ self.upcoming.is_none(),
+ "Cannot modify the current instance while an upcoming one exists"
+ );
+
+ self.current = None;
+ }
+}
+
+/// # Loading operations
+impl Instances {
+ /// Finish an ongoing load.
+ ///
+ /// ## Panics
+ ///
+ /// Panics if:
+ /// - There is no upcoming instance of the zone.
+ /// - The upcoming instance is not empty.
+ pub fn finish_load(&mut self, serial: Serial) {
+ let Some(upcoming) = &mut self.upcoming else {
+ panic!("There is no upcoming instance of the zone");
+ };
+
+ assert!(
+ upcoming.loaded.is_none() && upcoming.signed.is_none(),
+ "The upcoming instance is not empty"
+ );
+
+ upcoming.loaded = Some(LoadedInstance { serial });
+ }
+}
+
+/// # Signing operations
+impl Instances {
+ /// Finish an ongoing (re-)sign.
+ ///
+ /// ## Panics
+ ///
+ /// Panics if:
+ /// - There is no upcoming instance of the zone.
+ /// - An upcoming signed instance already exists.
+ pub fn finish_sign(&mut self, serial: Serial) {
+ let Some(upcoming) = &mut self.upcoming else {
+ panic!("There is no upcoming instance of the zone");
+ };
+
+ assert!(
+ upcoming.signed.is_none(),
+ "An upcoming signed instance already exists",
+ );
+
+ upcoming.signed = Some(SignedInstance { serial });
+ }
+}
+
+/// # Finalizing operations
+impl Instances {
+ /// Switch from the current instance to the upcoming one.
+ ///
+ /// The upcoming instance (which must exist) will replace the current one.
+ ///
+ /// ## Panics
+ ///
+ /// Panics if:
+ /// - There is no upcoming instance.
+ /// - The upcoming instance is malformed.
+ /// - A re-sign took place but there is no current loaded instance.
+ pub fn switch(&mut self) {
+ // TODO: Save the current instance to a list of obsolete ones.
+
+ let upcoming = self
+ .upcoming
+ .take()
+ .unwrap_or_else(|| panic!("There is no upcoming instance"));
+ let current = self.current.take();
+
+ match upcoming {
+ // A re-sign took place.
+ UpcomingInstance {
+ loaded: None,
+ signed: Some(signed),
+ } => {
+ let CurrentInstance { loaded, signed: _ } = current.unwrap_or_else(|| {
+ panic!("A re-sign took place but there is no current loaded instance")
+ });
+
+ self.current = Some(CurrentInstance { loaded, signed });
+ }
+
+ // A new loaded instance has appeared.
+ UpcomingInstance {
+ loaded: Some(loaded),
+ signed: Some(signed),
+ } => {
+ self.current = Some(CurrentInstance { loaded, signed });
+ }
+
+ // The upcoming instance is malformed.
+ _ => {
+ panic!("The upcoming instance is malformed")
+ }
+ }
+ }
+
+ /// Abandon the upcoming instance entirely.
+ ///
+ /// ## Panics
+ ///
+ /// Panics if an upcoming instance does not exist.
+ pub fn abandon(&mut self) {
+ // TODO: Add the upcoming instance to a list of abandoned ones.
+
+ assert!(self.upcoming.is_some(), "Nothing to abandon");
+
+ self.upcoming = None;
+ }
+}
+
+//----------- CurrentInstance --------------------------------------------------
+
+/// The current/published instance of a zone.
+///
+/// This type holds information about the current (loaded and signed) instance
+/// of a zone, which is published by Cascade.
+#[derive(Debug)]
+pub struct CurrentInstance {
+ /// The current loaded instance.
+ ///
+ /// This describes the instance loaded from the zone source.
+ pub loaded: LoadedInstance,
+
+ /// The current signed instance.
+ ///
+ /// This describes the signed instance generated by Cascade.
+ pub signed: SignedInstance,
+ //
+ // TODO:
+ // - When the instance was published.
+}
+
+//----------- UpcomingInstance -------------------------------------------------
+
+/// An upcoming instance of a zone.
+///
+/// This type holds information about an upcoming (loaded and signed) instance
+/// of a zone, which is being generated by Cascade to replace the current one.
+/// In case of a re-sign operation, only the signed instance is being replaced.
+///
+/// There are two reasons an upcoming instance may exist:
+///
+/// - A new instance of the zone is being loaded. The current instance can be
+/// in any state. An upcoming loaded instance will exist, and an upcoming
+/// signed instance might exist (if signing is enabled).
+///
+/// - The current instance of the zone is being re-signed. A current loaded
+/// instance exists, but a current signed instance might not (e.g. if signing
+/// was disabled and has now been enabled). An upcoming loaded instance will
+/// not exist, but an upcoming signed instance will exist.
+#[derive(Debug)]
+pub struct UpcomingInstance {
+ /// The upcoming loaded instance, if any.
+ ///
+ /// This describes the instance loaded from the zone source. It is [`Some`]
+ /// if/when loading is complete; if a re-sign operation is happening, it is
+ /// only [`None`].
+ pub loaded: Option,
+
+ /// The upcoming signed instance, if any.
+ ///
+ /// This describes the signed instance generated by Cascade. It is [`Some`]
+ /// if/when signing is complete.
+ pub signed: Option,
+ //
+ // TODO:
+ // - When the instance was initiated.
+ // - The cause of initiation (e.g. reception of DNS NOTIFY).
+ // - The policy used for generating the instance.
+ // (with such a field, policy changes during generation can be ignored.)
+}
+
+//----------- LoadedInstance ---------------------------------------------------
+
+/// A loaded instance of a zone.
+#[derive(Debug)]
+pub struct LoadedInstance {
+ /// The SOA serial of this instance.
+ pub serial: Serial,
+ //
+ // TODO:
+ // - Instance ID.
+ // - How the instance was loaded (time, source, duration, size).
+ // - Whether the zone contains DNSSEC records (for pass-through mode).
+ // - Basic details of the zone:
+ // - How many records it contains (especially NS, DS, glue records).
+ // - Total memory usage.
+}
+
+//----------- SignedInstance ---------------------------------------------------
+
+/// A signed instance of a zone.
+#[derive(Debug)]
+pub struct SignedInstance {
+ /// The SOA serial of this instance.
+ pub serial: Serial,
+ //
+ // TODO:
+ // - Instance ID.
+ // - How the instance was signed:
+ // - time, source, duration, size
+ // - Whether re-signing occurred, and what was re-signed.
+ // - Basic details of the signed zone:
+ // - How many records it contains (especially RRSIG, NSEC/NSEC3 records).
+ // - How many records from the loaded instance are included.
+ // - Total memory usage (excluding the loaded instance).
+ // - DNSSEC details:
+ // - NSEC or NSEC3 records (incl. rollovers).
+ // - ZONEMD (incl. algorithms and rollovers).
+ // - Zone signing keys (incl. rollovers).
+ // - The earliest signature expiration time.
+ // - Extensions/overrides used during signing
+ // - See 'apex_{remove,extra}'.
+}
diff --git a/src/zone/machine.rs b/src/zone/machine.rs
index 0d6860ae..6d57d9fb 100644
--- a/src/zone/machine.rs
+++ b/src/zone/machine.rs
@@ -1,5 +1,5 @@
use cascade_api::ZoneReviewStatus;
-use cascade_zonedata::{LoadedZoneBuilder, LoadedZoneBuilt, SignedZoneBuilder};
+use domain::new::base::Serial;
use tracing::{info, trace};
use crate::{
@@ -100,7 +100,7 @@ impl ZoneStateMachine {
/// # Initiating operations
impl<'a> ZoneHandle<'a> {
- pub(crate) fn try_start_load(&mut self) -> Option {
+ pub(crate) fn try_start_load(&mut self) -> Option {
// It's important that we first check the storage here instead of the
// zone state machine. The reason is that while the zone state machine
// is in the waiting state, the storage might still be persisting or
@@ -120,12 +120,14 @@ impl<'a> ZoneHandle<'a> {
transition.move_to(ZoneStateMachine::Loading(waiting.start_load()));
+ self.state.instances.start_load();
+
self.state.record_event(HistoricalEvent::StartedLoad, None);
Some(builder)
}
- pub(crate) fn try_start_resign(&mut self) -> Option {
+ pub(crate) fn try_start_resign(&mut self) -> Option {
// It's important that we first check the storage here instead of the
// zone state machine. The reason is that while the zone state machine
// is in the waiting state, the storage might still be persisting or
@@ -146,6 +148,8 @@ impl<'a> ZoneHandle<'a> {
transition.move_to(ZoneStateMachine::Signing(waiting.start_resign()));
+ self.state.instances.start_resign();
+
self.state.record_event(HistoricalEvent::StartedLoad, None);
Some(builder)
@@ -154,7 +158,7 @@ impl<'a> ZoneHandle<'a> {
/// # Loading operations
impl<'a> ZoneHandle<'a> {
- pub(crate) fn abandon_load(&mut self, builder: LoadedZoneBuilder) {
+ pub(crate) fn abandon_load(&mut self, builder: cascade_zonedata::LoadedZoneBuilder) {
let (transition, state) = self.state.machine.transition();
let ZoneStateMachine::Loading(loaded) = state else {
@@ -164,9 +168,12 @@ impl<'a> ZoneHandle<'a> {
transition.move_to(ZoneStateMachine::Waiting(loaded.abandon_load()));
self.storage().abandon_load(builder);
+
+ // Abandon the entire upcoming instance.
+ self.state.instances.abandon();
}
- pub(crate) fn finish_load(&mut self, built: LoadedZoneBuilt) {
+ pub(crate) fn finish_load(&mut self, built: cascade_zonedata::LoadedZoneBuilt, serial: Serial) {
let (transition, state) = self.state.machine.transition();
let ZoneStateMachine::Loading(loaded) = state else {
@@ -176,6 +183,8 @@ impl<'a> ZoneHandle<'a> {
transition.move_to(ZoneStateMachine::LoadedReview(loaded.finish_load()));
self.storage().finish_load(built);
+
+ self.state.instances.finish_load(serial);
}
}
@@ -215,6 +224,9 @@ impl<'a> ZoneHandle<'a> {
};
transition.move_to(ZoneStateMachine::Waiting(loaded.soft_reject()));
+
+ // Abandon the entire upcoming instance.
+ self.state.instances.abandon();
}
pub(crate) fn hard_reject_loaded(&mut self) {
@@ -275,14 +287,17 @@ impl<'a> ZoneHandle<'a> {
self.signer().enqueue_new_sign(builder);
}
- pub(crate) fn finish_signing(&mut self, built: cascade_zonedata::SignedZoneBuilt) {
+ pub(crate) fn finish_signing(
+ &mut self,
+ built: cascade_zonedata::SignedZoneBuilt,
+ serial: Serial,
+ ) {
self.state.record_event(
// TODO: Get the right trigger.
HistoricalEvent::SigningSucceeded {
trigger: SigningTrigger::Load.into(),
},
- // TODO: Get the serial in here.
- None,
+ Some(u32::from(serial).into()),
);
let (transition, state) = self.state.machine.transition();
@@ -294,10 +309,12 @@ impl<'a> ZoneHandle<'a> {
transition.move_to(ZoneStateMachine::SignedReview(signing.finish_signing()));
self.storage().finish_sign(built);
+
+ self.state.instances.finish_sign(serial);
}
- // Abandon the ongoing signing operation (but not due to failure).
- pub(crate) fn abandon_signing(&mut self, builder: SignedZoneBuilder) {
+ /// Abandon the ongoing signing operation (but not due to failure).
+ pub(crate) fn abandon_signing(&mut self, builder: cascade_zonedata::SignedZoneBuilder) {
let (transition, state) = self.state.machine.transition();
let ZoneStateMachine::Signing(signing) = state else {
@@ -309,9 +326,16 @@ impl<'a> ZoneHandle<'a> {
transition.move_to(ZoneStateMachine::Waiting(signing.abandon()));
self.storage().abandon_sign(builder);
+
+ // Abandon the entire upcoming instance.
+ self.state.instances.abandon();
}
- pub(crate) fn signing_failed(&mut self, builder: SignedZoneBuilder, err: SignerError) {
+ pub(crate) fn signing_failed(
+ &mut self,
+ builder: cascade_zonedata::SignedZoneBuilder,
+ err: SignerError,
+ ) {
let (transition, state) = self.state.machine.transition();
let ZoneStateMachine::Signing(signing) = state else {
@@ -321,6 +345,9 @@ impl<'a> ZoneHandle<'a> {
transition.move_to(ZoneStateMachine::SigningFailed(signing.signing_failed(err)));
self.storage().abandon_sign(builder);
+
+ // Abandon the entire upcoming instance.
+ self.state.instances.abandon();
}
}
@@ -359,6 +386,9 @@ impl<'a> ZoneHandle<'a> {
};
transition.move_to(ZoneStateMachine::Waiting(signed.soft_reject()));
+
+ // Abandon the entire upcoming instance.
+ self.state.instances.abandon();
}
pub(crate) fn hard_reject_signed(&mut self) {
@@ -376,6 +406,9 @@ impl<'a> ZoneHandle<'a> {
};
transition.move_to(ZoneStateMachine::HaltSigned(review.hard_reject()));
+
+ // Abandon the entire upcoming instance.
+ self.state.instances.abandon();
}
}
@@ -401,6 +434,8 @@ impl<'a> ZoneHandle<'a> {
};
transition.move_to(ZoneStateMachine::Waiting(signed.approve()));
+ self.state.instances.switch();
+
self.storage().start_cleanup(cleaner);
}
}
@@ -415,21 +450,24 @@ impl<'a> ZoneHandle<'a> {
let waiting = halt_loaded.reset();
transition.move_to(ZoneStateMachine::Waiting(waiting));
self.storage().abandon_loaded_review();
+ self.state.instances.abandon();
}
ZoneStateMachine::HaltSigned(halt_signed) => {
let waiting = halt_signed.reset();
transition.move_to(ZoneStateMachine::Waiting(waiting));
self.storage().abandon_signed_review();
+ self.state.instances.abandon();
}
ZoneStateMachine::SigningFailed(signing_failed) => {
let waiting = signing_failed.reset();
transition.move_to(ZoneStateMachine::Waiting(waiting));
+ self.state.instances.abandon();
}
_ => {
transition.move_to(state);
return Err(());
}
- };
+ }
Ok(())
}
@@ -553,7 +591,7 @@ pub struct Loading {}
impl Loading {
fn finish_load(self) -> LoadedReview {
- LoadedReview {}
+ LoadedReview { decided: false }
}
fn abandon_load(self) -> Waiting {
@@ -561,8 +599,15 @@ impl Loading {
}
}
+/// An upcoming loaded instance is being reviewed.
#[derive(Debug)]
-pub struct LoadedReview {}
+pub struct LoadedReview {
+ /// Whether a review decision has been received.
+ ///
+ /// If this is `true`, an approval/rejection for the zone has been received;
+ /// it is being processed.
+ pub decided: bool,
+}
impl LoadedReview {
fn approve(self) -> Signing {
@@ -596,7 +641,7 @@ pub struct Signing {}
impl Signing {
fn finish_signing(self) -> SignedReview {
- SignedReview {}
+ SignedReview { decided: false }
}
/// Abandon the signing operation (but not due to failure).
@@ -620,8 +665,15 @@ impl SigningFailed {
}
}
+/// An upcoming signed instance is being reviewed.
#[derive(Debug)]
-pub struct SignedReview {}
+pub struct SignedReview {
+ /// Whether a review decision has been received.
+ ///
+ /// If this is `true`, an approval/rejection for the zone has been received;
+ /// it is being processed.
+ pub decided: bool,
+}
impl SignedReview {
fn approve(self) -> Waiting {
diff --git a/src/zone/mod.rs b/src/zone/mod.rs
index 7a4d635d..cfb9d215 100644
--- a/src/zone/mod.rs
+++ b/src/zone/mod.rs
@@ -36,6 +36,9 @@ use crate::units::zone_signer::faketime_or_now;
mod storage;
pub use storage::{StorageState, StorageZoneHandle};
+mod instance;
+pub use instance::{CurrentInstance, Instances, LoadedInstance, SignedInstance, UpcomingInstance};
+
pub mod machine;
pub mod state;
@@ -181,9 +184,6 @@ pub struct ZoneState {
/// The policy (version) used by the zone.
pub policy: Option>,
- /// Metadata related to the last published zone version.
- pub last_published: Option,
-
/// An enqueued save of this state.
///
/// The enqueued save operation will persist the current state in a short
@@ -239,11 +239,8 @@ pub struct ZoneState {
/// serial for the Increment serial policy.
pub previous_serial: Option,
- /// Unsigned versions of the zone.
- pub unsigned: foldhash::HashMap,
-
- /// Signed versions of the zone.
- pub signed: foldhash::HashMap,
+ /// Instances of the zone.
+ pub instances: Instances,
/// History of interesting events that occurred for this zone.
pub history: Vec,
@@ -293,7 +290,6 @@ impl Default for ZoneState {
Self {
machine: Default::default(),
policy: Default::default(),
- last_published: Default::default(),
enqueued_save: Default::default(),
min_expiration: Default::default(),
next_min_expiration: Default::default(),
@@ -303,8 +299,7 @@ impl Default for ZoneState {
key_roll: Default::default(),
last_signature_refresh: faketime_or_now(),
previous_serial: Default::default(),
- unsigned: Default::default(),
- signed: Default::default(),
+ instances: Default::default(),
history: Default::default(),
loader: Default::default(),
signer: Default::default(),
@@ -314,55 +309,6 @@ impl Default for ZoneState {
}
}
-#[derive(Debug)]
-pub struct LastPublished {
- pub loaded_serial: Serial,
- pub signed_serial: Serial,
- // TODO:
- // - time of publish
- // - number of records
- // - size in bytes
-}
-
-/// The state of an unsigned version of a zone.
-#[derive(Clone, Debug)]
-pub struct UnsignedZoneVersionState {
- /// The review state of the zone version.
- pub review: ZoneVersionReviewState,
-}
-
-/// The state of a signed version of a zone.
-#[derive(Clone, Debug)]
-pub struct SignedZoneVersionState {
- /// The serial number of the corresponding unsigned version of the zone.
- pub unsigned_serial: Serial,
-
- /// The review state of the zone version.
- pub review: ZoneVersionReviewState,
-}
-
-/// The review state of a version of a zone.
-#[derive(Clone, Debug, Default)]
-pub enum ZoneVersionReviewState {
- /// The zone is pending review.
- ///
- /// If a review script has been configured, it is running now. Otherwise,
- /// the zone must be manually reviewed.
- #[default]
- Pending,
-
- /// The zone has been approved.
- ///
- /// This is a terminal state. The zone may have progressed further through
- /// the pipeline, so it is no longer possible to reject it.
- Approved,
-
- /// The zone has been rejected.
- ///
- /// The zone has not yet been approved; it can be approved at any time.
- Rejected,
-}
-
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HistoryItem {
pub when: SystemTime,
diff --git a/src/zone/storage.rs b/src/zone/storage.rs
index aa8013d5..06a55ab0 100644
--- a/src/zone/storage.rs
+++ b/src/zone/storage.rs
@@ -29,16 +29,15 @@ use cascade_zonedata::{
LoadedZoneBuilder, LoadedZoneBuilt, LoadedZonePersisted, LoadedZonePersister,
LoadedZoneRestored, LoadedZoneRestorer, LoadedZoneReviewer, SignedZoneBuilder, SignedZoneBuilt,
SignedZonePersisted, SignedZonePersister, SignedZoneRestored, SignedZoneRestorer,
- SignedZoneReviewer, SoaRecord, ZoneCleaner, ZoneDataStorage,
+ SignedZoneReviewer, ZoneCleaner, ZoneDataStorage,
};
-use domain::base::Serial;
use tracing::{info, trace, trace_span, warn};
use crate::{
center::Center,
server::{LoadedReviewServer, PublicationServer, SignedReviewServer},
util::BackgroundTasks,
- zone::{HistoricalEvent, LastPublished, Zone, ZoneHandle, ZoneState},
+ zone::{HistoricalEvent, Zone, ZoneHandle, ZoneState},
};
//----------- StorageZoneHandle ------------------------------------------------
@@ -185,8 +184,6 @@ impl StorageZoneHandle<'_> {
fields(zone = %self.zone.name),
)]
fn start_loaded_review(&mut self, loaded_reviewer: LoadedZoneReviewer) {
- self.state.storage.loaded_review_soa = loaded_reviewer.read().map(|r| r.soa().clone());
-
let zone = self.zone.clone();
let center = self.center.clone();
let span = trace_span!("start_loaded_review");
@@ -269,8 +266,6 @@ impl StorageZoneHandle<'_> {
info!("The loaded instance has been rejected; cleaning it up");
let (s, loaded_reviewer) = s.give_up();
- self.state.storage.loaded_review_soa =
- loaded_reviewer.read().map(|r| r.soa().clone());
transition.move_to(ZoneDataStorage::CleanLoadedPending(s));
loaded_reviewer
}
@@ -396,8 +391,6 @@ impl StorageZoneHandle<'_> {
trace!("Abandoning the ongoing sign operation");
let (s, loaded_reviewer) = s.give_up(builder);
- self.state.storage.loaded_review_soa =
- loaded_reviewer.read().map(|r| r.soa().clone());
transition.move_to(ZoneDataStorage::CleanLoadedPending(s));
loaded_reviewer
}
@@ -421,8 +414,6 @@ impl StorageZoneHandle<'_> {
fields(zone = %self.zone.name),
)]
fn start_signed_review(&mut self, signed_reviewer: SignedZoneReviewer) {
- self.state.storage.signed_review_soa = signed_reviewer.read().map(|r| r.soa().clone());
-
let zone = self.zone.clone();
let center = self.center.clone();
let span = trace_span!("start_signed_review");
@@ -504,10 +495,6 @@ impl StorageZoneHandle<'_> {
let new_s;
(new_s, loaded_reviewer, signed_reviewer) = s.give_up();
transition.move_to(ZoneDataStorage::CleanWholePending(new_s));
- self.state.storage.loaded_review_soa =
- loaded_reviewer.read().map(|r| r.soa().clone());
- self.state.storage.signed_review_soa =
- signed_reviewer.read().map(|r| r.soa().clone());
}
_ => panic!("The zone is not undergoing signer review"),
@@ -736,9 +723,6 @@ impl StorageZoneHandle<'_> {
),
};
- self.state.storage.published_soa = viewer.read().map(|r| r.soa().clone());
- self.state.storage.published_loaded_soa = viewer.read().map(|r| r.loaded().soa().clone());
-
// Spawn a background task to update the publication server.
let span = trace_span!("switch_publication_server");
let zone = self.zone.clone();
@@ -768,31 +752,6 @@ impl StorageZoneHandle<'_> {
_ => unreachable!("just transitioned to 'Switching'"),
};
- handle.state.last_published = Some(LastPublished {
- loaded_serial: Serial(
- handle
- .state
- .storage
- .published_loaded_soa
- .as_ref()
- .unwrap()
- .rdata
- .serial
- .into(),
- ),
- signed_serial: Serial(
- handle
- .state
- .storage
- .published_soa
- .as_ref()
- .unwrap()
- .rdata
- .serial
- .into(),
- ),
- });
-
handle.finish_switch(cleaner);
handle.state.storage.background_tasks.finish();
@@ -898,33 +857,6 @@ pub struct StorageState {
/// passed to [`StorageZoneHandle::abandon_loaded_restoration()`].
pub restorer: Option,
- /// The SOA record of the loaded instance of the zone being reviewed, if
- /// any.
- //
- // TODO: This should move into a component of 'ZoneState' tracking the
- // upcoming zone instance.
- pub loaded_review_soa: Option,
-
- /// The SOA record of the signed instance of the zone being reviewed, if
- /// any.
- //
- // TODO: This should move into a component of 'ZoneState' tracking the
- // upcoming zone instance.
- pub signed_review_soa: Option,
-
- /// The SOA record of the published instance of the zone, if any.
- //
- // TODO: This should move into a component of 'ZoneState' tracking the
- // current i.e. published zone instance.
- pub published_soa: Option,
-
- /// The SOA record of the loaded instance underlying the published instance
- /// of the zone, if any.
- //
- // TODO: This should move into a component of 'ZoneState' tracking the
- // current i.e. published zone instance.
- pub published_loaded_soa: Option,
-
/// Ongoing background tasks.
///
/// When the zone data needs to be cleaned or persisted, a background task
@@ -940,10 +872,6 @@ impl StorageState {
Self {
machine,
restorer: Some(restorer),
- loaded_review_soa: None,
- signed_review_soa: None,
- published_soa: None,
- published_loaded_soa: None,
background_tasks: Default::default(),
}
}