summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/peers.rs12
-rw-r--r--src/config/mod.rs14
-rw-r--r--src/force.rs72
-rw-r--r--src/main.rs30
-rw-r--r--src/remake/api.rs47
5 files changed, 121 insertions, 54 deletions
diff --git a/src/api/peers.rs b/src/api/peers.rs
index 919f187..dc4bd3b 100644
--- a/src/api/peers.rs
+++ b/src/api/peers.rs
@@ -100,8 +100,8 @@ pub(crate) struct PeerListResponse {
#[derive(Debug, Deserialize)]
pub(crate) struct DeletePeerQuery {
- #[serde(default)]
- force: bool,
+ /// The checks to override (`force`): `alive`.
+ force: Option<String>,
}
#[utoipa::path(
@@ -197,12 +197,12 @@ pub(crate) async fn list_peers(
tag = "Peers",
params(
("peer" = String, Path, description = "Peer public key"),
- ("force" = Option<bool>, Query, description = "Also forget a peer that is currently alive; it returns on its next message")
+ ("force" = Option<String>, Query, description = "`true` or `alive`: also forget a peer that is currently alive; it returns on its next message")
),
responses(
(status = 204, description = "Swarm-wide forget recorded"),
(status = 400, description = "Not a public key, or the local node's own key"),
- (status = 409, description = "Peer is alive; pass force=true"),
+ (status = 409, description = "Peer is alive; pass force=alive"),
(status = 503, description = "Node is not ready")
)
)]
@@ -217,6 +217,8 @@ pub(crate) async fn delete_peer(
if peer.is_empty() || peer == state.local_id() {
return Err(StatusCode::BAD_REQUEST);
}
+ let force = crate::force::Force::parse(query.force.as_deref(), &["alive"])
+ .map_err(|_| StatusCode::BAD_REQUEST)?;
// Unknown locally is fine: other nodes may still know the peer.
if let Some(peer_state_mutex) = state.get_peer_state(&peer).await {
@@ -224,7 +226,7 @@ pub(crate) async fn delete_peer(
.lock()
.await
.is_alive(&state.timing, Instant::now())
- && !query.force
+ && !force.allows("alive")
{
return Err(StatusCode::CONFLICT);
}
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 0771e7e..c83ca70 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -65,6 +65,9 @@ const fn option(
}
}
+/// What `--force` may override at start (`force`).
+pub(crate) const FORCE: &[&str] = &["swarmId"];
+
/// Every option. Names are the long option names and the configuration file keys.
pub(crate) const OPTIONS: &[OptionSpec] = &[
option(
@@ -110,6 +113,13 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[
"Start a new swarm, as its genesis node, instead of joining one through seeds. Every start with it starts another new swarm: drop it once the swarm exists, and give the node seeds instead. Can't be combined with seeds. With a --state-file that remembers a swarm, it starts that swarm again instead (empty; nodes of it still running merge with this one).",
),
option(
+ "force",
+ Kind::List,
+ "NAME,[NAME...]",
+ None,
+ "Override checks that otherwise refuse to start: true for all, or names: swarmId (a backup of another swarm than the state file remembers).",
+ ),
+ option(
"recover-from",
Kind::Text,
"PATH",
@@ -238,6 +248,8 @@ pub(crate) struct Settings {
pub(crate) genesis: bool,
pub(crate) state_file: Option<PathBuf>,
pub(crate) recover_from: Option<PathBuf>,
+ #[serde(default)]
+ pub(crate) force: Vec<String>,
pub(crate) swarm_key_file: Option<PathBuf>,
pub(crate) alias: Option<String>,
#[serde(default = "default_keepalive_secs")]
@@ -311,6 +323,8 @@ impl Settings {
);
}
}
+ crate::force::Force::parse(self.force.iter().map(String::as_str), FORCE)
+ .map_err(|reason| anyhow!("force: {reason}"))?;
// Contradictions, not overrides: an error wherever the options come from.
if self.genesis && !self.seeds.is_empty() {
bail!(
diff --git a/src/force.rs b/src/force.rs
new file mode 100644
index 0000000..40f0ffc
--- /dev/null
+++ b/src/force.rs
@@ -0,0 +1,72 @@
+//! `force`: overriding checks that otherwise refuse. Conflicts are errors by default;
+//! each check that can be overridden has a name, and `force` lists the names to
+//! override, or is `true` for all of them (`false`, or nothing, for none).
+//!
+//! The same format everywhere: the `?force=` query parameter of the API and the
+//! `--force` option. Names are camelCase, like the API's fields (`switchDelaySecs`),
+//! on the command line too. An unknown name is an error, so a typo can't silently
+//! override nothing.
+
+/// The checks a `force` value overrides.
+#[derive(Debug, Clone, Default, PartialEq, Eq)]
+pub(crate) struct Force {
+ all: bool,
+ names: Vec<&'static str>,
+}
+
+impl Force {
+ /// Parses `values` (each `true`, `false`, or a comma-separated list of names),
+ /// allowing only the names in `known`.
+ pub(crate) fn parse<'a>(
+ values: impl IntoIterator<Item = &'a str>,
+ known: &[&'static str],
+ ) -> Result<Self, String> {
+ let mut force = Force::default();
+ for value in values {
+ for name in value.split(',').map(str::trim) {
+ match name {
+ "" | "false" => {}
+ "true" => force.all = true,
+ name => match known.iter().find(|known| **known == name) {
+ Some(known) => force.names.push(known),
+ None => {
+ return Err(format!(
+ "unknown force {name:?}: use true, or a list of {}",
+ known.join(", ")
+ ));
+ }
+ },
+ }
+ }
+ }
+ Ok(force)
+ }
+
+ /// Whether the check `name` is overridden.
+ pub(crate) fn allows(&self, name: &str) -> bool {
+ self.all || self.names.contains(&name)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ const KNOWN: &[&str] = &["switchDelaySecs", "supersede"];
+
+ #[test]
+ fn all_some_or_none() {
+ let all = Force::parse(["true"], KNOWN).unwrap();
+ assert!(all.allows("supersede") && all.allows("switchDelaySecs"));
+
+ let some = Force::parse(["supersede"], KNOWN).unwrap();
+ assert!(some.allows("supersede") && !some.allows("switchDelaySecs"));
+ let list = Force::parse(["switchDelaySecs, supersede"], KNOWN).unwrap();
+ assert!(list.allows("supersede") && list.allows("switchDelaySecs"));
+
+ for none in [vec![], vec!["false"], vec![""]] {
+ assert!(!Force::parse(none, KNOWN).unwrap().allows("supersede"));
+ }
+ assert!(Force::parse(["swarm-id"], KNOWN).is_err());
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index 76e5ff0..88cbc6f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,7 +9,7 @@ use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::time::Duration;
-use anyhow::{Context, Result};
+use anyhow::{Context, Result, bail};
use automerge::AutoCommit;
use tokio::sync::{RwLock, broadcast, mpsc, watch};
use tracing::{debug, error, info, trace, warn};
@@ -19,6 +19,7 @@ mod backoff;
mod config;
mod document;
mod files;
+mod force;
mod logging;
mod remake;
mod state;
@@ -87,6 +88,8 @@ async fn main() -> Result<()> {
"Using Mycelium REST API"
);
+ let force = force::Force::parse(args.force.iter().map(String::as_str), config::FORCE)
+ .map_err(anyhow::Error::msg)?;
let files = Arc::new(files::Files::default());
let mut seeds: Vec<String> = args.seeds.clone();
let (state_file, remembered) = match &args.state_file {
@@ -123,6 +126,22 @@ async fn main() -> Result<()> {
Some(path) => Some(bootstrap::load_backup(path, &files)?),
None => None,
};
+ if let (Some(backup), Some(remembered)) = (&recovered, &remembered) {
+ if backup.swarm_id != remembered.swarm_id {
+ if !force.allows("swarmId") {
+ bail!(
+ "The backup is of swarm {}, but the state file remembers swarm {}; add --force swarmId to recover the backup's swarm anyway",
+ backup.swarm_id,
+ remembered.swarm_id
+ );
+ }
+ warn!(
+ backup = %backup.swarm_id,
+ remembered = %remembered.swarm_id,
+ "The backup is of another swarm than the remembered one; recovering the backup's (--force swarmId)"
+ );
+ }
+ }
let (initial_doc, recovered_epoch, recovered_swarm) = match recovered {
Some(backup) => (backup.doc, Some(backup.epoch), Some(backup.swarm_id)),
None => (AutoCommit::new(), None, None),
@@ -193,16 +212,9 @@ async fn main() -> Result<()> {
trace!("Application state initialized");
// Join the remembered swarm and no other; HELLOs claim it. A backup to recover from
- // names its swarm itself, which wins.
+ // names its swarm itself (checked against the remembered one above).
let start = match recovered_swarm {
Some(swarm_id) => {
- if let Some(remembered) = remembered.filter(|r| r.swarm_id != swarm_id) {
- warn!(
- backup = %swarm_id,
- remembered = %remembered.swarm_id,
- "The backup is of another swarm than the remembered one; recovering the backup's"
- );
- }
state.set_expected_swarm_id(swarm_id).await?;
bootstrap::Start::Recovered
}
diff --git a/src/remake/api.rs b/src/remake/api.rs
index 3530423..6b1a30c 100644
--- a/src/remake/api.rs
+++ b/src/remake/api.rs
@@ -73,6 +73,7 @@ use super::records::{
};
use super::{Host, kind, vote};
use crate::document::store::StoreError;
+use crate::force::Force;
#[derive(Debug, Default, Deserialize)]
pub(crate) struct WaitQuery {
@@ -85,7 +86,7 @@ pub(crate) struct WaitQuery {
pub(crate) struct StartQuery {
#[serde(default)]
wait: bool,
- /// The checks to override (`Force`).
+ /// The checks to override (`force`).
force: Option<String>,
// Only for an import uploaded as `application/octet-stream`, which has no JSON
// body to carry them:
@@ -160,42 +161,8 @@ struct StartRequest {
switch_delay_secs: Option<u64>,
}
-/// The checks `?force=` overrides: `true` all of them, or a comma-separated list.
-#[derive(Debug, Default, Clone, Copy)]
-struct Force {
- switch_delay: bool,
- supersede: bool,
-}
-
-impl Force {
- fn parse(value: Option<&str>) -> Result<Self, String> {
- let mut force = Force::default();
- match value.map(str::trim) {
- None | Some("" | "false") => {}
- Some("true") => {
- force = Force {
- switch_delay: true,
- supersede: true,
- };
- }
- Some(list) => {
- for name in list.split(',').map(str::trim) {
- match name {
- "switchDelaySecs" => force.switch_delay = true,
- "supersede" => force.supersede = true,
- other => {
- return Err(format!(
- "unknown force {other:?}: use true, or a list of \
- switchDelaySecs and supersede"
- ));
- }
- }
- }
- }
- }
- Ok(force)
- }
-}
+/// What `?force=` may override on a proposal (`force`).
+const FORCE: &[&str] = &["switchDelaySecs", "supersede"];
/// An expired remake of the epoch a proposal would supersede.
#[derive(Debug)]
@@ -333,7 +300,7 @@ async fn start_kind<H: Host>(
if !host.is_node_ready().await {
return error_response(StatusCode::SERVICE_UNAVAILABLE, "node is not ready", None);
}
- let force = match Force::parse(query.force.as_deref()) {
+ let force = match Force::parse(query.force.as_deref(), FORCE) {
Ok(force) => force,
Err(reason) => return error_response(StatusCode::BAD_REQUEST, reason, None),
};
@@ -436,7 +403,7 @@ async fn start_kind<H: Host>(
let keepalive = dormant_after / 4;
let switch_delay = request.switch_delay_secs.map(Duration::from_secs);
match switch_delay {
- Some(delay) if delay < keepalive && force.switch_delay => {
+ Some(delay) if delay < keepalive && force.allows("switchDelaySecs") => {
warn!(
switch_delay_secs = delay.as_secs(),
keepalive_secs = keepalive.as_secs(),
@@ -502,7 +469,7 @@ async fn start_kind<H: Host>(
id.clone(),
proposal,
base,
- force.supersede,
+ force.allows("supersede"),
now,
grace,
));