From 4a5b96626577f5337635b551e17eb00d08fb9d53 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 17 Aug 2026 10:46:28 +0200 Subject: [PATCH 1/6] [doc/man/cascade-zone] Add `zone maintenance` subcmd docs --- doc/manual/source/man/cascade-zone.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/manual/source/man/cascade-zone.rst b/doc/manual/source/man/cascade-zone.rst index 78adcc01f..bee9d3d09 100644 --- a/doc/manual/source/man/cascade-zone.rst +++ b/doc/manual/source/man/cascade-zone.rst @@ -88,6 +88,19 @@ Commands Get the history of a single zone. +.. subcmd:: maintenance + + Enable or disable maintenance mode for the zone. + + In maintenance mode, Cascade will not act on the zone autonomously (e.g. to + load new zone data or refresh signatures). This quiet state is helpful for + debugging and changing zone configuration. + + .. note:: If maintenance mode is enabled while a new instance of the zone + is being built (i.e. loading, signing, review, etc. is ongoing), + it will not be canceled; maintenance mode will go into effect once + the operation completes. + Options for :subcmd:`zone add` ------------------------------ From f4c34fc8556c085485bf979041a1ba93072e60aa Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 17 Aug 2026 10:46:28 +0200 Subject: [PATCH 2/6] Only allow zone removal for zones in maintenance mode --- crates/api/src/lib.rs | 4 ++-- crates/cli/src/commands/zone.rs | 2 ++ src/center.rs | 15 +++++++++------ src/zone/machine.rs | 4 ++++ 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index fd96e69ad..36b587655 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -353,14 +353,14 @@ pub struct ZoneRemoveResult { #[derive(Deserialize, Serialize, Debug, Clone)] pub enum ZoneRemoveError { NotFound, - MidRestoration, + NotInMaintenanceMode, } impl fmt::Display for ZoneRemoveError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::NotFound => "no such zone was found", - Self::MidRestoration => "the zone is being restored from disk", + Self::NotInMaintenanceMode => "the zone is not in maintenance mode", }) } } diff --git a/crates/cli/src/commands/zone.rs b/crates/cli/src/commands/zone.rs index 42f136460..1f181e5b4 100644 --- a/crates/cli/src/commands/zone.rs +++ b/crates/cli/src/commands/zone.rs @@ -57,6 +57,8 @@ pub enum ZoneCommand { }, /// Remove a zone + /// + /// The zone must be in maintenance mode. #[command(name = "remove")] Remove { name: ZoneName }, diff --git a/src/center.rs b/src/center.rs index 7909445f6..2090ef712 100644 --- a/src/center.rs +++ b/src/center.rs @@ -265,8 +265,11 @@ pub fn remove_zone(center: &Arc
, name: Name) -> Result<(), ZoneRe let ZoneByName(zone) = state.zones.get(&name).ok_or(ZoneRemoveError::NotFound)?; // TODO(#871): support removing a zone during restoration. - if zone.read().storage.is_restoring() { - return Err(ZoneRemoveError::MidRestoration); + // The zone must be halted or in maintenance mode. + if let zone = zone.read() + && !(zone.maintenance_mode && (zone.machine.is_waiting() || zone.machine.is_halted())) + { + return Err(ZoneRemoveError::NotInMaintenanceMode); } let ZoneByName(zone) = state @@ -518,8 +521,8 @@ pub enum ZoneRemoveError { /// No such name could be found. NotFound, - /// The zone is being restored from disk. - MidRestoration, + /// The zone is not in maintenance mode. + NotInMaintenanceMode, } impl std::error::Error for ZoneRemoveError {} @@ -528,7 +531,7 @@ impl fmt::Display for ZoneRemoveError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::NotFound => "no such zone was found", - Self::MidRestoration => "the zone is being restored from disk", + Self::NotInMaintenanceMode => "the zone is not in maintenance mode", }) } } @@ -537,7 +540,7 @@ impl From for api::ZoneRemoveError { fn from(value: ZoneRemoveError) -> Self { match value { ZoneRemoveError::NotFound => Self::NotFound, - ZoneRemoveError::MidRestoration => Self::MidRestoration, + ZoneRemoveError::NotInMaintenanceMode => Self::NotInMaintenanceMode, } } } diff --git a/src/zone/machine.rs b/src/zone/machine.rs index d4e1f8698..ea1cb5d16 100644 --- a/src/zone/machine.rs +++ b/src/zone/machine.rs @@ -86,6 +86,10 @@ pub enum ZoneStateMachine { } impl ZoneStateMachine { + pub fn is_waiting(&self) -> bool { + matches!(self, Self::Waiting(_)) + } + pub fn is_halted(&self) -> bool { matches!( self, From 8a0ca6d38bfd7df26b9c05b904d5f34a4cef2591 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 17 Aug 2026 10:46:28 +0200 Subject: [PATCH 3/6] [doc/man/cascade-zone] Mention conditions for zone removal --- doc/manual/source/man/cascade-zone.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/manual/source/man/cascade-zone.rst b/doc/manual/source/man/cascade-zone.rst index bee9d3d09..e85629a1c 100644 --- a/doc/manual/source/man/cascade-zone.rst +++ b/doc/manual/source/man/cascade-zone.rst @@ -50,6 +50,9 @@ Commands Remove a zone. + Maintenance mode must be enabled (see :subcmd:`zone maintenance`). The + zone must be passive (with no ongoing operations) or in a hard-halt state. + .. note:: Once removed, downstream servers will no longer be able to fetch the zone! From cc8a8211aa73b27d8e6aa741d8d32bf8cb8272ab Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 17 Aug 2026 10:59:15 +0200 Subject: [PATCH 4/6] [integration-tests] Enable maintenance mode before removing zones --- .../scripts/tests-change-denial.sh | 1 + .../incremental-signing/scripts/tests.sh | 1 + .../review-unsigned-zone2/scripts/tests.sh | 17 +++++++++-------- integration-tests/tests/persist-zone/action.yml | 1 + integration-tests/tests/remove-zone/action.yml | 3 +++ .../tests/upstream-tsig/action.yml | 2 ++ 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/integration-tests/incremental-signing/scripts/tests-change-denial.sh b/integration-tests/incremental-signing/scripts/tests-change-denial.sh index e77e54b42..bcd43c41b 100755 --- a/integration-tests/incremental-signing/scripts/tests-change-denial.sh +++ b/integration-tests/incremental-signing/scripts/tests-change-denial.sh @@ -67,6 +67,7 @@ do $CASCADE policy reload $CASCADE zone reload example + $CASCADE zone maintanence enable example for i in 1 2 3 4 5 6 7 8 9 10 do dig @127.0.0.1 -p 8053 example soa | diff --git a/integration-tests/incremental-signing/scripts/tests.sh b/integration-tests/incremental-signing/scripts/tests.sh index 0c73a8892..cca90ab16 100755 --- a/integration-tests/incremental-signing/scripts/tests.sh +++ b/integration-tests/incremental-signing/scripts/tests.sh @@ -39,6 +39,7 @@ do } cp zones/incremental-signing-test${test}-input2.zone example.in $CASCADE zone reload example + $CASCADE zone maintenance enable example for i in 1 2 3 4 5 6 7 8 9 10 do dig @127.0.0.1 -p 8053 example soa | diff --git a/integration-tests/review-unsigned-zone2/scripts/tests.sh b/integration-tests/review-unsigned-zone2/scripts/tests.sh index 58726f4ac..4d9d2c8c7 100755 --- a/integration-tests/review-unsigned-zone2/scripts/tests.sh +++ b/integration-tests/review-unsigned-zone2/scripts/tests.sh @@ -9,21 +9,22 @@ do cp zones/test${test}.zone example.in $CASCADE zone add --source $PWD/example.in --policy review-test example --import-csk-file $KEY + $CASCADE zone maintenance enable example + serial="$test$test$test$test$test" # Wait for the zone to be signed. for i in 1 2 3 4 5 6 7 8 9 10 do - dig @127.0.0.1 -p 8053 example soa | - grep $serial && break - echo zone is not signed yet, sleeping - sleep 1 + dig @127.0.0.1 -p 8053 example soa | + grep $serial && break + echo zone is not signed yet, sleeping + sleep 1 done dig @127.0.0.1 -p 8053 example soa | - grep $serial || - { - echo zone is not signed yet, giving up - exit 1 + grep $serial || { + echo zone is not signed yet, giving up + exit 1 } $CASCADE zone remove example done diff --git a/integration-tests/tests/persist-zone/action.yml b/integration-tests/tests/persist-zone/action.yml index 18e63344f..9b2fd1aa2 100644 --- a/integration-tests/tests/persist-zone/action.yml +++ b/integration-tests/tests/persist-zone/action.yml @@ -101,6 +101,7 @@ runs: - name: Delete the zone run: | + cascade zone maintenance enable i.dont.exist cascade zone remove i.dont.exist # ------------------------------------------------------------------------ diff --git a/integration-tests/tests/remove-zone/action.yml b/integration-tests/tests/remove-zone/action.yml index 734f9a462..4b6069686 100644 --- a/integration-tests/tests/remove-zone/action.yml +++ b/integration-tests/tests/remove-zone/action.yml @@ -45,6 +45,9 @@ runs: sleep 1 done + - name: Enable maintenance mode + run: cascade zone maintenance enable example.test + - name: Check zone status run: | timeout=10 # seconds diff --git a/integration-tests/tests/upstream-tsig/action.yml b/integration-tests/tests/upstream-tsig/action.yml index 2e16a74a5..96d1ca92d 100644 --- a/integration-tests/tests/upstream-tsig/action.yml +++ b/integration-tests/tests/upstream-tsig/action.yml @@ -37,6 +37,7 @@ runs: - name: Add zone without the required TSIG key. run: | cascade zone add --policy default --source "127.0.0.1:1055" example-tsig.test + cascade zone maintenance enable example-tsig.test - name: Check zone status run: | @@ -71,6 +72,7 @@ runs: esac cascade zone add --policy default --source "127.0.0.1:1055^tsig-key" example-tsig.test + cascade zone maintenance enable example-tsig.test - name: Check zone status run: | From a69e75365e33d0c09be7f74d4b91666c307f164e Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 17 Aug 2026 11:13:35 +0200 Subject: [PATCH 5/6] Rebuild documentation --- doc/manual/build/man/cascade-zone.1 | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/doc/manual/build/man/cascade-zone.1 b/doc/manual/build/man/cascade-zone.1 index ae9e644f0..430ea3e39 100644 --- a/doc/manual/build/man/cascade-zone.1 +++ b/doc/manual/build/man/cascade-zone.1 @@ -54,7 +54,7 @@ cascade-zone \- Manage zones .sp \fBcascade\fP \fB[GLOBAL OPTIONS]\fP zone \fI\%history\fP \fB\fP .sp -\fBcascade\fP \fB[GLOBAL OPTIONS]\fP zone \fBmaintenance\fP \fB\fP \fB\fP +\fBcascade\fP \fB[GLOBAL OPTIONS]\fP zone \fI\%maintenance\fP \fB\fP \fB\fP .SH DESCRIPTION .sp Manage Cascade\(aqs zones. @@ -73,6 +73,9 @@ Add a new zone. .B remove Remove a zone. .sp +Maintenance mode must be enabled (see \fBzone maintenance\fP). The +zone must be passive (with no ongoing operations) or in a hard\-halt state. +.sp \fBNOTE:\fP .INDENT 7.0 .INDENT 3.5 @@ -124,6 +127,25 @@ Reset the pipeline for a zone to get it out of a halted state. .B history Get the history of a single zone. .UNINDENT +.INDENT 0.0 +.TP +.B maintenance +Enable or disable maintenance mode for the zone. +.sp +In maintenance mode, Cascade will not act on the zone autonomously (e.g. to +load new zone data or refresh signatures). This quiet state is helpful for +debugging and changing zone configuration. +.sp +\fBNOTE:\fP +.INDENT 7.0 +.INDENT 3.5 +If maintenance mode is enabled while a new instance of the zone +is being built (i.e. loading, signing, review, etc. is ongoing), +it will not be canceled; maintenance mode will go into effect once +the operation completes. +.UNINDENT +.UNINDENT +.UNINDENT .SH OPTIONS FOR ZONE ADD .INDENT 0.0 .TP From fa6035284255d2a6178f996043e92188150cb421 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 17 Aug 2026 11:36:51 +0200 Subject: [PATCH 6/6] Satisfy Clippy --- src/center.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/center.rs b/src/center.rs index 2090ef712..cae2dd5df 100644 --- a/src/center.rs +++ b/src/center.rs @@ -264,12 +264,13 @@ pub fn remove_zone(center: &Arc
, name: Name) -> Result<(), ZoneRe let ZoneByName(zone) = state.zones.get(&name).ok_or(ZoneRemoveError::NotFound)?; - // TODO(#871): support removing a zone during restoration. - // The zone must be halted or in maintenance mode. - if let zone = zone.read() - && !(zone.maintenance_mode && (zone.machine.is_waiting() || zone.machine.is_halted())) { - return Err(ZoneRemoveError::NotInMaintenanceMode); + let zone = zone.read(); + // The zone must be in maintenance mode, and passive/halted. + // TODO(#871): support removing a zone during restoration. + if !zone.maintenance_mode || !zone.machine.is_waiting() && !zone.machine.is_halted() { + return Err(ZoneRemoveError::NotInMaintenanceMode); + } } let ZoneByName(zone) = state