Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Warn user about implicit seeding policy
Adrian Duke committed 2 months ago
commit d13556d142ff062bd7e302a43dc4946402cdaae7
parent 714f8b5899ac28620ddadc4499a6f2608c4e38b5
9 files changed +226 -20
added crates/radicle-cli/examples/rad-seed-policy-allow-no-scope.md
@@ -0,0 +1,7 @@
+
We want to ensure that a warning is printed when the `scope` field is missing in the `seedingPolicy`.
+

+
``` alice
+
$ rad node status
+
! Warning: Configuration option 'node.seedingPolicy.scope' is not set, and thus takes the value 'all' by default. The default value will change to 'followed' in a future release. Please edit your configuration file, and set it to one of ['all', 'followed'] explicitly.
+
[..]
+
```
modified crates/radicle-cli/examples/rad-warn-old-nodes.md
@@ -38,8 +38,8 @@ $ rad debug
    "RAD_RNG_SEED": "0"
  },
  "warnings": [
-
    "Value of configuration option `node.connect` at index 0 mentions node with address 'ash.radicle.garden:8776', which has been renamed to 'rosa.radicle.xyz:8776'. Please update your configuration.",
-
    "Value of configuration option `preferredSeeds` at index 0 mentions node with address 'seed.radicle.garden:8776', which has been renamed to 'iris.radicle.xyz:8776'. Please update your configuration."
+
    "Value of configuration option `node.connect` at index 0 mentions node with address 'ash.radicle.garden:8776', which has been renamed to 'rosa.radicle.xyz:8776'. Please edit your configuration file to use the new address.",
+
    "Value of configuration option `preferredSeeds` at index 0 mentions node with address 'seed.radicle.garden:8776', which has been renamed to 'iris.radicle.xyz:8776'. Please edit your configuration file to use the new address."
  ]
}
```
@@ -48,8 +48,8 @@ Also, `rad node status` will warn us:

```
$ rad node status
-
! Warning: Value of configuration option `node.connect` at index 0 mentions node with address 'ash.radicle.garden:8776', which has been renamed to 'rosa.radicle.xyz:8776'. Please update your configuration.
-
! Warning: Value of configuration option `preferredSeeds` at index 0 mentions node with address 'seed.radicle.garden:8776', which has been renamed to 'iris.radicle.xyz:8776'. Please update your configuration.
+
! Warning: Value of configuration option `node.connect` at index 0 mentions node with address 'ash.radicle.garden:8776', which has been renamed to 'rosa.radicle.xyz:8776'. Please edit your configuration file to use the new address.
+
! Warning: Value of configuration option `preferredSeeds` at index 0 mentions node with address 'seed.radicle.garden:8776', which has been renamed to 'iris.radicle.xyz:8776'. Please edit your configuration file to use the new address.
Node is stopped.
To start it, run `rad node start`.
```
modified crates/radicle-cli/src/commands/debug.rs
@@ -131,7 +131,7 @@ fn stderr_of(bin: &str, args: &[&str]) -> anyhow::Result<String> {

fn collect_warnings(profile: Option<&Profile>) -> Vec<String> {
    match profile {
-
        Some(profile) => crate::warning::nodes_renamed(&profile.config),
+
        Some(profile) => crate::warning::config_warnings(&profile.config),
        None => vec!["No Radicle profile found.".to_string()],
    }
}
modified crates/radicle-cli/src/commands/node/control.rs
@@ -257,7 +257,7 @@ pub fn connect_many(
}

pub fn status(node: &Node, profile: &Profile) -> anyhow::Result<()> {
-
    for warning in crate::warning::nodes_renamed(&profile.config) {
+
    for warning in crate::warning::config_warnings(&profile.config) {
        term::warning(warning);
    }

modified crates/radicle-cli/src/warning.rs
@@ -22,26 +22,51 @@ fn nodes_renamed_for_option(
    option: &'static str,
    iter: impl IntoIterator<Item = ConnectAddress>,
) -> Vec<String> {
-
    let mut warnings: Vec<String> = vec![];
-

-
    for (i, value) in iter.into_iter().enumerate() {
+
    iter.into_iter().enumerate().fold(Vec::new(), |mut warnings, (i, value)| {
        let old: Address = value.into();
        if let Some(new) = NODES_RENAMED.get(&old) {
            warnings.push(format!(
-
                "Value of configuration option `{option}` at index {i} mentions node with address '{old}', which has been renamed to '{new}'. Please update your configuration."
+
                "Value of configuration option `{option}` at index {i} mentions node with address '{old}', which has been renamed to '{new}'. Please edit your configuration file to use the new address."
            ));
        }
-
    }
-

-
    warnings
+
        warnings
+
    })
}

-
pub(crate) fn nodes_renamed(config: &Config) -> Vec<String> {
+
fn nodes_renamed(config: &Config) -> Vec<String> {
    let mut warnings = nodes_renamed_for_option("node.connect", config.node.connect.clone());
    warnings.extend(nodes_renamed_for_option(
        "preferredSeeds",
        config.preferred_seeds.clone(),
    ));
+

+
    warnings
+
}
+

+
fn implicit_seeding_policy_allow_scope(config: &Config) -> Vec<String> {
+
    use radicle::node::config::DefaultSeedingPolicy;
+
    use radicle::node::policy;
+

+
    let DefaultSeedingPolicy::Allow { scope } = config.node.seeding_policy else {
+
        return vec![];
+
    };
+
    if scope.is_implicit() {
+
        let scope = scope.into_inner();
+
        vec![format!(
+
                "Configuration option 'node.seedingPolicy.scope' is not set, and thus takes the value '{scope}' by default. The default value will change to '{}' in a future release. Please edit your configuration file, and set it to one of ['{}', '{}'] explicitly.",
+
                policy::Scope::Followed,
+
                policy::Scope::All,
+
                policy::Scope::Followed,
+
            )]
+
    } else {
+
        vec![]
+
    }
+
}
+

+
pub(crate) fn config_warnings(config: &Config) -> Vec<String> {
+
    let mut warnings = nodes_renamed(config);
+
    warnings.extend(implicit_seeding_policy_allow_scope(config));
+

    warnings
}

modified crates/radicle-cli/tests/commands.rs
@@ -2871,3 +2871,24 @@ fn rad_workflow() {
    )
    .unwrap();
}
+

+
#[test]
+
fn rad_seed_policy_allow_no_scope() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node_with(Config {
+
        seeding_policy: DefaultSeedingPolicy::Allow {
+
            scope: node::config::Scope::implicit(),
+
        },
+
        ..Config::test(Alias::new("alice"))
+
    });
+

+
    let alice = alice.spawn();
+

+
    test(
+
        "examples/rad-seed-policy-allow-no-scope.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+
}
modified crates/radicle/Cargo.toml
@@ -72,6 +72,7 @@ pretty_assertions = { workspace = true }
qcheck = { workspace = true }
qcheck-macros = { workspace = true }
radicle-cob = { workspace = true, features = ["stable-commit-ids", "test"] }
+
radicle-core = {workspace = true, features = ["qcheck"]}
radicle-crypto = { workspace = true, features = ["test"] }
radicle-git-metadata = { workspace = true }
tempfile = { workspace = true }
modified crates/radicle/src/node/config.rs
@@ -9,9 +9,11 @@ use serde::{Deserialize, Serialize};
use serde_json as json;

use crate::node;
-
use crate::node::policy::{Scope, SeedingPolicy};
+
use crate::node::policy::SeedingPolicy;
use crate::node::{Address, Alias, NodeId};

+
use super::policy;
+

/// Peer-to-peer protocol version.
pub type ProtocolVersion = u8;

@@ -364,13 +366,13 @@ pub enum AddressConfig {

/// Default seeding policy. Applies when no repository policies for the given repo are found.
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
-
#[serde(rename_all = "camelCase", tag = "default")]
+
#[serde(tag = "default", rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum DefaultSeedingPolicy {
    /// Allow seeding.
    Allow {
        /// Seeding scope.
-
        #[serde(default)]
+
        #[serde(skip_serializing_if = "Scope::is_implicit")]
        scope: Scope,
    },
    /// Block seeding.
@@ -378,6 +380,58 @@ pub enum DefaultSeedingPolicy {
    Block,
}

+
/// [`Scope`] provides a schema for [`policy::Scope`], where the inner scope is optional.
+
///
+
/// It is used in [`DefaultSeedingPolicy`] to allow for optionally setting the
+
/// scope in the [`DefaultSeedingPolicy::Allow`] variant.
+
/// This materializes as the `seedingPolicy.scope` configuration value, when
+
/// `"default": "allow"` is set.
+
///
+
/// [`Scope`] is introduced to allow migrating to a required value in a future
+
/// version (post v1.6.x).
+
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+
#[serde(transparent)]
+
pub struct Scope(Option<policy::Scope>);
+

+
impl Scope {
+
    /// Construct the implicit scope, where the default value,
+
    /// [`policy::Scope::All`], is chosen for the final scope value.
+
    pub fn implicit() -> Self {
+
        Self(None)
+
    }
+

+
    /// Construct the explicit scope, where the given [`policy::Scope`] is used.
+
    pub fn explicit(scope: policy::Scope) -> Self {
+
        Self(Some(scope))
+
    }
+

+
    /// Resolve this [`Scope`] to its [`policy::Scope`] value.
+
    ///
+
    /// If the scope is implicit, then [`policy::Scope::All`] is returned.
+
    pub fn into_inner(self) -> policy::Scope {
+
        self.0.unwrap_or(policy::Scope::All)
+
    }
+

+
    /// Returns `true` when the scope is implicit, i.e. no [`policy::Scope`] was
+
    /// given.
+
    pub fn is_implicit(&self) -> bool {
+
        self.0.is_none()
+
    }
+

+
    /// Construct the explicit [`Scope`] where the inner scope is
+
    /// [`policy::Scope::All`].
+
    fn all() -> Self {
+
        Self::explicit(policy::Scope::All)
+
    }
+

+
    /// Construct the explicit [`Scope`] where the inner scope is
+
    /// [`policy::Scope::Followed`].
+
    fn followed() -> Self {
+
        Self::explicit(policy::Scope::Followed)
+
    }
+
}
+

impl DefaultSeedingPolicy {
    /// Is this an "allow" policy.
    pub fn is_allow(&self) -> bool {
@@ -386,7 +440,16 @@ impl DefaultSeedingPolicy {

    /// Seed everything from anyone.
    pub fn permissive() -> Self {
-
        Self::Allow { scope: Scope::All }
+
        Self::Allow {
+
            scope: Scope::all(),
+
        }
+
    }
+

+
    /// Seed only delegate changes.
+
    pub fn followed() -> Self {
+
        Self::Allow {
+
            scope: Scope::followed(),
+
        }
    }
}

@@ -394,7 +457,9 @@ impl From<DefaultSeedingPolicy> for SeedingPolicy {
    fn from(policy: DefaultSeedingPolicy) -> Self {
        match policy {
            DefaultSeedingPolicy::Block => Self::Block,
-
            DefaultSeedingPolicy::Allow { scope } => Self::Allow { scope },
+
            DefaultSeedingPolicy::Allow { scope } => SeedingPolicy::Allow {
+
                scope: scope.into_inner(),
+
            },
        }
    }
}
@@ -646,6 +711,10 @@ wrapper!(
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
+
    use super::{DefaultSeedingPolicy, Scope};
+
    use crate::node::policy;
+
    use serde_json::json;
+

    #[test]
    fn partial() {
        use super::Config;
@@ -683,4 +752,85 @@ mod test {
        );
        assert_eq!(config.limits.connection.outbound.0, 1337);
    }
+

+
    #[test]
+
    fn deserialize_migrating_scope() {
+
        let seeding_policy: DefaultSeedingPolicy = serde_json::from_value(json!({
+
            "default": "allow"
+
        }))
+
        .unwrap();
+

+
        assert_eq!(
+
            seeding_policy,
+
            DefaultSeedingPolicy::Allow { scope: Scope(None) }
+
        );
+

+
        let seeding_policy: DefaultSeedingPolicy = serde_json::from_value(json!({
+
            "default": "allow",
+
            "scope": null
+
        }))
+
        .unwrap();
+

+
        assert_eq!(
+
            seeding_policy,
+
            DefaultSeedingPolicy::Allow { scope: Scope(None) }
+
        );
+

+
        let seeding_policy: DefaultSeedingPolicy = serde_json::from_value(json!({
+
            "default": "allow",
+
            "scope": "all"
+
        }))
+
        .unwrap();
+

+
        assert_eq!(
+
            seeding_policy,
+
            DefaultSeedingPolicy::Allow {
+
                scope: Scope(Some(policy::Scope::All))
+
            }
+
        );
+

+
        let seeding_policy: DefaultSeedingPolicy = serde_json::from_value(json!({
+
            "default": "allow",
+
            "scope": "followed"
+
        }))
+
        .unwrap();
+

+
        assert_eq!(
+
            seeding_policy,
+
            DefaultSeedingPolicy::Allow {
+
                scope: Scope(Some(policy::Scope::Followed))
+
            }
+
        )
+
    }
+

+
    #[test]
+
    fn serialize_migrating_scope() {
+
        assert_eq!(
+
            json!({
+
                "default": "allow"
+
            }),
+
            serde_json::to_value(DefaultSeedingPolicy::Allow { scope: Scope(None) }).unwrap()
+
        );
+

+
        assert_eq!(
+
            json!({
+
                "default": "allow",
+
                "scope": "all"
+
            }),
+
            serde_json::to_value(DefaultSeedingPolicy::Allow {
+
                scope: Scope(Some(policy::Scope::All))
+
            })
+
            .unwrap()
+
        );
+
        assert_eq!(
+
            json!({
+
                "default": "allow",
+
                "scope": "followed"
+
            }),
+
            serde_json::to_value(DefaultSeedingPolicy::Allow {
+
                scope: Scope(Some(policy::Scope::Followed))
+
            })
+
            .unwrap()
+
        );
+
    }
}
modified crates/radicle/src/profile/config.rs
@@ -192,7 +192,9 @@ impl Config {
            ) {
                log::warn!(target: "radicle", "Overwriting `seedingPolicy` configuration");
                cfg.node.seeding_policy = match policy {
-
                    Policy::Allow => DefaultSeedingPolicy::Allow { scope },
+
                    Policy::Allow => DefaultSeedingPolicy::Allow {
+
                        scope: node::config::Scope::explicit(scope),
+
                    },
                    Policy::Block => DefaultSeedingPolicy::Block,
                }
            }