node: fetch trusted peers on clone

When a repository does not yet exist during a fetch, i.e. a clone,
then only delegates are being fetched.

Augment Namespaces::Many to hold the trusted and optional delegate peers
separately. Doing so allows the construction of the variant without
the repository existing, but tracking relationships existing.

This variant can then be used to build refspecs for both the trusted
peers and delegates when doing a cloning fetch.

Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
Fintan Halpenny 2023-03-28 17:39:38 +01:00
parent f78e010c55
commit adf6312a55
No known key found for this signature in database
GPG Key ID: 2552FB6F64066CB7
6 changed files with 46 additions and 87 deletions

View File

@ -1004,17 +1004,15 @@ where
tracking::Scope::Trusted => { tracking::Scope::Trusted => {
match self.tracking.namespaces_for(&self.storage, &message.rid) { match self.tracking.namespaces_for(&self.storage, &message.rid) {
Ok(Namespaces::All) => Ok(true), Ok(Namespaces::All) => Ok(true),
Ok(Namespaces::Many(nodes)) => { Ok(Namespaces::Many(mut trusted)) => {
// Get the set of trusted nodes except self. // Get the set of trusted nodes except self.
let my_id = self.node_id(); trusted.remove(&self.node_id());
let node_set: HashSet<_> =
nodes.iter().filter(|key| *key != &my_id).collect();
// Check if there is at least one trusted ref. // Check if there is at least one trusted ref.
Ok(message Ok(message
.refs .refs
.iter() .iter()
.any(|(pub_key, _refs)| node_set.contains(pub_key))) .any(|(pub_key, _refs)| trusted.contains(pub_key)))
} }
Ok(Namespaces::One(key)) => { Ok(Namespaces::One(key)) => {
Ok(message.refs.iter().any(|(pub_key, _refs)| pub_key == &key)) Ok(message.refs.iter().any(|(pub_key, _refs)| pub_key == &key))
@ -1279,8 +1277,8 @@ where
// SAFETY: `REF_REMOTE_LIMIT` is greater than 1, thus the bounded vec can hold at least // SAFETY: `REF_REMOTE_LIMIT` is greater than 1, thus the bounded vec can hold at least
// one remote. // one remote.
.unwrap(), .unwrap(),
Namespaces::Many(pks) => { Namespaces::Many(trusted) => {
for remote_id in pks.into_iter() { for remote_id in trusted.iter() {
if refs if refs
.push((*remote_id, repo.remote(remote_id)?.refs.unverified())) .push((*remote_id, repo.remote(remote_id)?.refs.unverified()))
.is_err() .is_err()

View File

@ -1,7 +1,7 @@
use std::collections::HashSet;
use std::ops; use std::ops;
use log::{error, warn}; use log::error;
use nonempty::NonEmpty;
use thiserror::Error; use thiserror::Error;
use radicle::crypto::PublicKey; use radicle::crypto::PublicKey;
@ -114,25 +114,25 @@ impl Config {
let nodes = self let nodes = self
.node_policies() .node_policies()
.map_err(|err| FailedNodes { rid: *rid, err })?; .map_err(|err| FailedNodes { rid: *rid, err })?;
let mut trusted: Vec<_> = nodes let mut trusted: HashSet<_> = nodes
.filter_map(|node| (node.policy == Policy::Track).then_some(node.id)) .filter_map(|node| (node.policy == Policy::Track).then_some(node.id))
.collect(); .collect();
let ns = if let Ok(repo) = storage.repository(*rid) { if let Ok(repo) = storage.repository(*rid) {
let delegates = repo let delegates = repo
.delegates() .delegates()
.map_err(|err| FailedDelegates { rid: *rid, err })? .map_err(|err| FailedDelegates { rid: *rid, err })?
.map(PublicKey::from); .map(PublicKey::from);
trusted.extend(delegates); trusted.extend(delegates);
NonEmpty::from_vec(trusted).map(Namespaces::Many)
} else {
Some(Namespaces::All)
}; };
if trusted.is_empty() {
ns.ok_or_else(|| { // Nb. returning All here because the
warn!(target: "service", "Attempted to fetch repo {rid} with no trusted peers"); // fetching logic will correctly determine
NoTrusted { rid: *rid } // trusted and delegate remotes.
}) Ok(Namespaces::All)
} else {
Ok(Namespaces::Many(trusted))
}
} }
}, },
} }

View File

@ -671,7 +671,7 @@ fn fetch<W: WriteRepository>(
let namespace = match namespaces.into() { let namespace = match namespaces.into() {
Namespaces::All => None, Namespaces::All => None,
Namespaces::One(ns) => Some(ns), Namespaces::One(ns) => Some(ns),
Namespaces::Many(ns) => Some(ns.head), Namespaces::Many(trusted) => trusted.into_iter().next(),
}; };
let mut updates = Vec::new(); let mut updates = Vec::new();
let mut callbacks = git::RemoteCallbacks::new(); let mut callbacks = git::RemoteCallbacks::new();

View File

@ -316,20 +316,6 @@ fn test_fetch_trusted_remotes() {
log::debug!(target: "test", "Fetch complete with {}", bob.id); log::debug!(target: "test", "Fetch complete with {}", bob.id);
let bob_repo = bob.storage.repository(acme).unwrap(); let bob_repo = bob.storage.repository(acme).unwrap();
let bob_remotes = bob_repo
.remote_ids()
.unwrap()
.collect::<Result<HashSet<_>, _>>()
.unwrap();
assert_eq!(bob_remotes, Some(alice.id).into_iter().collect());
// TODO(finto): we have to fetch again to get the other trusted remotes.
// At the moment, the existing Namespaces enum does not allow us
// to pass on what nodes are tracked, if there is no existing
// repository. Thus, the first fetch only attempts to clone the
// delegate.
bob.handle.fetch(acme, alice.id).unwrap();
assert!(result.is_success());
let bob_remotes = bob_repo let bob_remotes = bob_repo
.remote_ids() .remote_ids()
.unwrap() .unwrap()

View File

@ -3,11 +3,9 @@ pub use refspecs::{AsRefspecs, Refspec, SpecialRefs};
pub mod error; pub mod error;
use std::collections::BTreeMap; use std::collections::{BTreeMap, HashSet};
use std::ops::Deref; use std::ops::Deref;
use nonempty::NonEmpty;
use radicle::crypto::{PublicKey, Unverified, Verified}; use radicle::crypto::{PublicKey, Unverified, Verified};
use radicle::git::url; use radicle::git::url;
use radicle::prelude::{Doc, Id}; use radicle::prelude::{Doc, Id};
@ -69,9 +67,10 @@ pub struct StagingPhaseFinal<'a> {
pub(super) repo: StagedRepository, pub(super) repo: StagedRepository,
/// The original [`Storage`] we are finalising changes into. /// The original [`Storage`] we are finalising changes into.
production: &'a Storage, production: &'a Storage,
/// The remotes that the fetch is being performed for. These are /// The delegates and tracked remotes that the fetch is being
/// discovered after performing the fetch for [`StagingPhaseInitial`]. /// performed for. These are passed through from the
remotes: NonEmpty<RemoteId>, /// [`StagingPhaseInitial::namespaces`], if the variant is `Many`.
trusted: HashSet<RemoteId>,
_tmp: tempfile::TempDir, _tmp: tempfile::TempDir,
} }
@ -124,14 +123,24 @@ impl<'a> StagingPhaseInitial<'a> {
/// Convert the [`StagingPhaseInitial`] into [`StagingPhaseFinal`] to continue /// Convert the [`StagingPhaseInitial`] into [`StagingPhaseFinal`] to continue
/// the fetch process. /// the fetch process.
pub fn into_final(self) -> Result<StagingPhaseFinal<'a>, error::Transition> { pub fn into_final(self) -> Result<StagingPhaseFinal<'a>, error::Transition> {
let remotes = match &self.repo { let trusted = match &self.repo {
StagedRepository::Cloning(repo) => { StagedRepository::Cloning(repo) => {
log::debug!(target: "worker", "Loading remotes for clone"); log::debug!(target: "worker", "Loading remotes for clone");
let oid = ReadRepository::identity_head(repo)?; let oid = ReadRepository::identity_head(repo)?;
log::trace!(target: "worker", "Loading 'rad/id' @ {oid}"); log::trace!(target: "worker", "Loading 'rad/id' @ {oid}");
let (doc, _) = Doc::<Unverified>::load_at(oid, repo)?; let (doc, _) = Doc::<Unverified>::load_at(oid, repo)?;
let doc = doc.verified()?; let doc = doc.verified()?;
doc.delegates.map(PublicKey::from) let mut trusted = match self.namespaces.clone() {
Namespaces::All => HashSet::new(),
// TODO(finto): this is one of those cases where
// having the `One` variant doesn't make any
// sense.
Namespaces::One(pk) => [pk].into_iter().collect(),
Namespaces::Many(trusted) => trusted,
};
let delegates = doc.delegates.map(PublicKey::from);
trusted.extend(delegates);
trusted
} }
StagedRepository::Fetching(repo) => { StagedRepository::Fetching(repo) => {
log::debug!(target: "worker", "Loading remotes for fetching"); log::debug!(target: "worker", "Loading remotes for fetching");
@ -140,11 +149,12 @@ impl<'a> StagingPhaseInitial<'a> {
// namespaces_for so it's safe to just bundle this // namespaces_for so it's safe to just bundle this
// with Namespaces::All // with Namespaces::All
Namespaces::One(_) | Namespaces::All => { Namespaces::One(_) | Namespaces::All => {
let mut remotes = repo.delegates()?.map(PublicKey::from); let mut trusted = repo.remote_ids()?.collect::<Result<HashSet<_>, _>>()?;
remotes.extend(repo.remote_ids()?.collect::<Result<Vec<_>, _>>()?); trusted.extend(repo.delegates()?.map(PublicKey::from));
remotes trusted
} }
Namespaces::Many(remotes) => remotes,
Namespaces::Many(trusted) => trusted,
} }
} }
}; };
@ -152,7 +162,7 @@ impl<'a> StagingPhaseInitial<'a> {
Ok(StagingPhaseFinal { Ok(StagingPhaseFinal {
repo: self.repo, repo: self.repo,
production: self.production, production: self.production,
remotes, trusted,
_tmp: self._tmp, _tmp: self._tmp,
}) })
} }
@ -192,7 +202,7 @@ impl<'a> StagingPhaseFinal<'a> {
/// references. /// references.
pub fn refspecs(&self) -> Vec<Refspec<git::PatternString, git::PatternString>> { pub fn refspecs(&self) -> Vec<Refspec<git::PatternString, git::PatternString>> {
match self.repo { match self.repo {
StagedRepository::Cloning(_) => Namespaces::Many(self.remotes.clone()).as_refspecs(), StagedRepository::Cloning(_) => Namespaces::Many(self.trusted.clone()).as_refspecs(),
StagedRepository::Fetching(_) => { StagedRepository::Fetching(_) => {
self.remotes().fold(Vec::new(), |mut specs, remote| { self.remotes().fold(Vec::new(), |mut specs, remote| {
specs.extend(remote.as_refspecs()); specs.extend(remote.as_refspecs());
@ -266,7 +276,7 @@ impl<'a> StagingPhaseFinal<'a> {
} }
fn remotes(&self) -> impl Iterator<Item = Remote> + '_ { fn remotes(&self) -> impl Iterator<Item = Remote> + '_ {
self.remotes self.trusted
.iter() .iter()
.filter_map(|remote| match SignedRefs::load(remote, self.repo.deref()) { .filter_map(|remote| match SignedRefs::load(remote, self.repo.deref()) {
Ok(refs) => Some(Remote::new(*remote, refs)), Ok(refs) => Some(Remote::new(*remote, refs)),
@ -278,7 +288,7 @@ impl<'a> StagingPhaseFinal<'a> {
} }
fn verify(&self) -> BTreeMap<RemoteId, VerifiedRemote> { fn verify(&self) -> BTreeMap<RemoteId, VerifiedRemote> {
self.remotes self.trusted
.iter() .iter()
.map(|remote| { .map(|remote| {
let verification = match ( let verification = match (

View File

@ -1,7 +1,7 @@
pub mod git; pub mod git;
pub mod refs; pub mod refs;
use std::collections::hash_map; use std::collections::{hash_map, HashSet};
use std::ops::Deref; use std::ops::Deref;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::{fmt, io}; use std::{fmt, io};
@ -37,36 +37,7 @@ pub enum Namespaces {
/// A single namespace, by public key. /// A single namespace, by public key.
One(PublicKey), One(PublicKey),
/// Many namespaces, by public keys. /// Many namespaces, by public keys.
Many(NonEmpty<PublicKey>), Many(HashSet<PublicKey>),
}
impl Namespaces {
pub fn remotes<R>(repo: &R) -> Result<Option<Self>, refs::Error>
where
R: ReadRepository,
{
Ok(NonEmpty::collect(repo.remotes()?.keys().copied()).map(Self::Many))
}
pub fn delegates<R>(repo: &R) -> Result<Self, IdentityError>
where
R: ReadRepository,
{
Ok(Self::Many(repo.delegates()?.map(PublicKey::from)))
}
pub fn as_fetchspecs(&self) -> Vec<String> {
match self {
Self::All => vec![String::from("refs/namespaces/*:refs/namespaces/*")],
Self::One(pk) => vec![format!(
"refs/namespaces/{pk}/refs/*:refs/namespaces/{pk}/refs/*"
)],
Self::Many(pks) => pks
.iter()
.map(|pk| format!("refs/namespaces/{pk}/refs/*:refs/namespaces/{pk}/refs/*"))
.collect(),
}
}
} }
impl From<PublicKey> for Namespaces { impl From<PublicKey> for Namespaces {
@ -75,12 +46,6 @@ impl From<PublicKey> for Namespaces {
} }
} }
impl From<NonEmpty<PublicKey>> for Namespaces {
fn from(pks: NonEmpty<PublicKey>) -> Self {
Self::Many(pks)
}
}
/// Storage error. /// Storage error.
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {