diff --git a/crates/spfs/src/config.rs b/crates/spfs/src/config.rs index 4c9f3854e5..551c1c582e 100644 --- a/crates/spfs/src/config.rs +++ b/crates/spfs/src/config.rs @@ -8,11 +8,12 @@ use std::sync::{Arc, RwLock}; use derive_builder::Builder; use once_cell::sync::OnceCell; +use relative_path::RelativePath; use serde::{Deserialize, Serialize}; use storage::{FromConfig, FromUrl}; use tokio_stream::StreamExt; -use crate::storage::TagNamespaceBuf; +use crate::storage::{TagNamespaceBuf, TagStorageMut}; use crate::{runtime, storage, tracking, Error, Result}; #[cfg(test)] @@ -152,20 +153,35 @@ pub struct RemoteConfig { #[builder(setter(strip_option), default)] #[serde(default, skip_serializing_if = "Option::is_none")] pub when: Option, + #[builder(setter(strip_option), default)] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tag_namespace: Option, #[serde(flatten)] pub inner: RepositoryConfig, } impl ToAddress for RemoteConfig { fn to_address(&self) -> Result { - let mut inner = self.inner.to_address()?; - if let Some(when) = &self.when { + let Self { + when, + tag_namespace, + inner, + } = self; + let mut inner = inner.to_address()?; + if let Some(when) = when { let query = format!("when={when}"); match inner.query() { None | Some("") => inner.set_query(Some(&query)), Some(q) => inner.set_query(Some(&format!("{q}&{query}"))), } } + if let Some(tag_namespace) = tag_namespace { + let query = format!("tag_namespace={}", tag_namespace.as_rel_path()); + match inner.query() { + None | Some("") => inner.set_query(Some(&query)), + Some(q) => inner.set_query(Some(&format!("{q}&{query}"))), + } + } Ok(inner) } } @@ -195,8 +211,14 @@ impl RemoteConfig { pub async fn from_address(url: url::Url) -> Result { let mut builder = RemoteConfigBuilder::default(); for (k, v) in url.query_pairs() { - if let "when" = k.as_ref() { - builder.when(tracking::TimeSpec::parse(v)?); + match k.as_ref() { + "when" => { + builder.when(tracking::TimeSpec::parse(v)?); + } + "tag_namespace" => { + builder.tag_namespace(TagNamespaceBuf::new(RelativePath::new(&v))); + } + _ => (), } } let result = match url.scheme() { @@ -233,7 +255,12 @@ impl RemoteConfig { /// Open a handle to a repository using this configuration pub async fn open(&self) -> storage::OpenRepositoryResult { - let handle = match self.inner.clone() { + let Self { + when, + tag_namespace, + inner, + } = self; + let mut handle: storage::RepositoryHandle = match inner.clone() { RepositoryConfig::Fs(config) => { storage::fs::FsRepository::from_config(config).await?.into() } @@ -247,10 +274,27 @@ impl RemoteConfig { .await? .into(), }; - match &self.when { - None => Ok(handle), - Some(ts) => Ok(handle.into_pinned(ts.to_datetime_from_now())), - } + // Set tag namespace first before pinning, because it is not possible + // to set the tag namespace on a pinned handle. + let handle = match tag_namespace { + None => handle, + Some(tag_namespace) => { + handle + .try_set_tag_namespace(Some(tag_namespace.clone())) + .map_err( + |err| storage::OpenRepositoryError::FailedToSetTagNamespace { + tag_namespace: tag_namespace.clone(), + source: Box::new(err), + }, + )?; + handle + } + }; + let handle = match when { + None => handle, + Some(ts) => handle.into_pinned(ts.to_datetime_from_now()), + }; + Ok(handle) } } diff --git a/crates/spfs/src/config_test.rs b/crates/spfs/src/config_test.rs index 567673b820..5600306583 100644 --- a/crates/spfs/src/config_test.rs +++ b/crates/spfs/src/config_test.rs @@ -104,6 +104,25 @@ async fn test_remote_config_pinned_from_address() { ) } +#[rstest] +#[tokio::test] +async fn test_remote_config_with_tag_namespace_from_address() { + let address = + url::Url::parse("http2://test.local?lazy=true&tag_namespace=ns").expect("a valid url"); + let config = RemoteConfig::from_address(address) + .await + .expect("can parse address with 'tag_namespace' query"); + let repo = config + .open() + .await + .expect("should open repo address with tag namespace"); + assert_eq!( + repo.get_tag_namespace().unwrap().as_rel_path(), + "ns", + "using a tag_namespace query should create a repo with a tag namespace" + ) +} + static ENV_MUTEX: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| std::sync::Mutex::new(())); diff --git a/crates/spfs/src/storage/error.rs b/crates/spfs/src/storage/error.rs index 42088340ae..b424bd0491 100644 --- a/crates/spfs/src/storage/error.rs +++ b/crates/spfs/src/storage/error.rs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 // https://github.com/imageworks/spk +use super::TagNamespaceBuf; + #[derive(Debug, miette::Diagnostic, thiserror::Error)] #[diagnostic()] pub enum OpenRepositoryError { @@ -90,6 +92,12 @@ pub enum OpenRepositoryError { }, #[error("Pinned repository is read only")] RepositoryIsPinned, + + #[error("Failed to set tag namespace '{tag_namespace}'")] + FailedToSetTagNamespace { + tag_namespace: TagNamespaceBuf, + source: Box, + }, } impl OpenRepositoryError { diff --git a/crates/spfs/src/storage/tag_namespace.rs b/crates/spfs/src/storage/tag_namespace.rs index 4c1a476e9f..68104bf69a 100644 --- a/crates/spfs/src/storage/tag_namespace.rs +++ b/crates/spfs/src/storage/tag_namespace.rs @@ -12,6 +12,7 @@ pub const TAG_NAMESPACE_MARKER: &str = "#ns"; /// A borrowed tag namespace name #[repr(transparent)] +#[derive(Debug)] pub struct TagNamespace(RelativePath); impl TagNamespace { @@ -57,6 +58,12 @@ impl std::borrow::Borrow for TagNamespaceBuf { } } +impl std::fmt::Display for TagNamespaceBuf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + impl std::ops::Deref for TagNamespaceBuf { type Target = TagNamespace;