Skip to content

Commit

Permalink
Well known config (#291)
Browse files Browse the repository at this point in the history
- Added Warg client support for `.well-known` path, which required the
`warg_client::FileSystemClient::new_with_config()` methods to be
`async`;
- Support both `warg login <registry-url>` and `warg login --registry
<registry-url>` (also for `warg logout`);
- Sets the default registry, without configuration, to be
`bytecodealliance.org`; [See
discussions](bytecodealliance/wasm-pkg-tools#3 (comment))

Follow on work to respect HTTP cache headers.
  • Loading branch information
calvinrp authored May 16, 2024
1 parent 14b98ac commit ac3342b
Show file tree
Hide file tree
Showing 23 changed files with 222 additions and 95 deletions.
16 changes: 16 additions & 0 deletions crates/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ pub mod v1;

use serde::{de::Unexpected, Deserialize, Serialize};

/// Relative URL path for the `WellKnownConfig`.
pub const WELL_KNOWN_PATH: &str = ".well-known/wasm-pkg/registry.json";

/// This config allows a domain to point to another URL where the registry
/// API is hosted.
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WellKnownConfig {
/// For OCI registries, the domain name where the registry is hosted.
pub oci_registry: Option<String>,
/// For OCI registries, a name prefix to use before the namespace.
pub oci_namespace_prefix: Option<String>,
/// For Warg registries, the URL where the registry is hosted.
pub warg_url: Option<String>,
}

/// A utility type for serializing and deserializing constant status codes.
struct Status<const CODE: u16>;

Expand Down
64 changes: 51 additions & 13 deletions crates/client/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,24 @@ use secrecy::{ExposeSecret, Secret};
use serde::de::DeserializeOwned;
use std::borrow::Cow;
use thiserror::Error;
use warg_api::v1::{
content::{ContentError, ContentSourcesResponse},
fetch::{
FetchError, FetchLogsRequest, FetchLogsResponse, FetchPackageNamesRequest,
FetchPackageNamesResponse,
use warg_api::{
v1::{
content::{ContentError, ContentSourcesResponse},
fetch::{
FetchError, FetchLogsRequest, FetchLogsResponse, FetchPackageNamesRequest,
FetchPackageNamesResponse,
},
ledger::{LedgerError, LedgerSourcesResponse},
monitor::{CheckpointVerificationResponse, MonitorError},
package::{ContentSource, PackageError, PackageRecord, PublishRecordRequest},
paths,
proof::{
ConsistencyRequest, ConsistencyResponse, InclusionRequest, InclusionResponse,
ProofError,
},
REGISTRY_HEADER_NAME, REGISTRY_HINT_HEADER_NAME,
},
ledger::{LedgerError, LedgerSourcesResponse},
monitor::{CheckpointVerificationResponse, MonitorError},
package::{ContentSource, PackageError, PackageRecord, PublishRecordRequest},
paths,
proof::{
ConsistencyRequest, ConsistencyResponse, InclusionRequest, InclusionResponse, ProofError,
},
REGISTRY_HEADER_NAME, REGISTRY_HINT_HEADER_NAME,
WellKnownConfig, WELL_KNOWN_PATH,
};
use warg_crypto::hash::{AnyHash, HashError, Sha256};
use warg_protocol::{
Expand Down Expand Up @@ -107,6 +111,9 @@ pub enum ClientError {
/// The provided log was not found with hint header.
#[error("log `{0}` was not found in this registry, but the registry provided the hint header: `{1:?}`")]
LogNotFoundWithHint(LogId, HeaderValue),
/// Invalid well-known config.
#[error("registry `{0}` returned an invalid well-known config")]
InvalidWellKnownConfig(String),
/// An other error occurred during the requested operation.
#[error(transparent)]
Other(#[from] anyhow::Error),
Expand Down Expand Up @@ -216,6 +223,37 @@ impl Client {
pub fn url(&self) -> &RegistryUrl {
&self.url
}
/// Gets the `.well-known` configuration registry URL.
pub async fn well_known_config(&self) -> Result<Option<RegistryUrl>, ClientError> {
let url = self.url.join(WELL_KNOWN_PATH);
tracing::debug!(url, "getting `.well-known` config",);

let res = self.client.get(url).send().await?;

if !res.status().is_success() {
tracing::debug!(
"the `.well-known` config request returned HTTP status `{status}`",
status = res.status()
);
return Ok(None);
}

if let Some(warg_url) = res
.json::<WellKnownConfig>()
.await
.map_err(|e| {
tracing::debug!("parsing `.well-known` config failed: {e}");
ClientError::InvalidWellKnownConfig(self.url.registry_domain().to_string())
})?
.warg_url
{
Ok(Some(RegistryUrl::new(warg_url)?))
} else {
tracing::debug!("the `.well-known` config did not have a `wargUrl` set");
Ok(None)
}
}

/// Gets the latest checkpoint from the registry.
pub async fn latest_checkpoint(
&self,
Expand Down
7 changes: 1 addition & 6 deletions crates/client/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,13 +300,8 @@ impl Config {

pub(crate) fn storage_paths_for_url(
&self,
url: Option<&str>,
registry_url: RegistryUrl,
) -> Result<StoragePaths, ClientError> {
let registry_url = RegistryUrl::new(
url.or(self.home_url.as_deref())
.ok_or(ClientError::NoHomeRegistryUrl)?,
)?;

let label = registry_url.safe_label();
let registries_dir = self.registries_dir()?.join(label);
let content_dir = self.content_dir()?;
Expand Down
95 changes: 66 additions & 29 deletions crates/client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ pub use self::registry_url::RegistryUrl;

const DEFAULT_WAIT_INTERVAL: Duration = Duration::from_secs(1);

/// For Bytecode Alliance projects, the default registry is set to `bytecodealliance.org`.
/// The `.well-known` config path may resolve to another domain where the registry is hosted.
pub const DEFAULT_REGISTRY: &str = "bytecodealliance.org";

/// A client for a Warg registry.
pub struct Client<R, C, N>
where
Expand Down Expand Up @@ -1358,6 +1362,39 @@ pub enum StorageLockResult<T> {
}

impl FileSystemClient {
async fn storage_paths(
url: Option<&str>,
config: &Config,
disable_interactive: bool,
) -> Result<StoragePaths, ClientError> {
let checking_url_for_well_known = RegistryUrl::new(
url.or(config.home_url.as_deref())
.unwrap_or(DEFAULT_REGISTRY),
)?;

let url = if let Some(warg_url) =
api::Client::new(checking_url_for_well_known.to_string(), None)?
.well_known_config()
.await?
{
if !disable_interactive && warg_url != checking_url_for_well_known {
println!(
"Resolved `{well_known}` to registry hosted on `{registry}`",
well_known = checking_url_for_well_known.registry_domain(),
registry = warg_url.registry_domain(),
);
}
warg_url
} else {
RegistryUrl::new(
url.or(config.home_url.as_deref())
.ok_or(ClientError::NoHomeRegistryUrl)?,
)?
};

config.storage_paths_for_url(url)
}

/// Attempts to create a client for the given registry URL.
///
/// If the URL is `None`, the home registry URL is used; if there is no home registry
Expand All @@ -1366,30 +1403,20 @@ impl FileSystemClient {
/// If a lock cannot be acquired for a storage directory, then
/// `NewClientResult::Blocked` is returned with the path to the
/// directory that could not be locked.
pub fn try_new_with_config(
url: Option<&str>,
pub async fn try_new_with_config(
registry: Option<&str>,
config: &Config,
mut auth_token: Option<Secret<String>>,
) -> Result<StorageLockResult<Self>, ClientError> {
let disable_interactive =
cfg!(not(feature = "cli-interactive")) || config.disable_interactive;

let StoragePaths {
registry_url: url,
registries_dir,
content_dir,
namespace_map_path,
} = config.storage_paths_for_url(url)?;

let (packages, content, namespace_map) = match (
FileSystemRegistryStorage::try_lock(registries_dir.clone())?,
FileSystemContentStorage::try_lock(content_dir.clone())?,
FileSystemNamespaceMapStorage::new(namespace_map_path.clone()),
) {
(Some(packages), Some(content), namespace_map) => (packages, content, namespace_map),
(None, _, _) => return Ok(StorageLockResult::NotAcquired(registries_dir)),
(_, None, _) => return Ok(StorageLockResult::NotAcquired(content_dir)),
};

let disable_interactive =
cfg!(not(feature = "cli-interactive")) || config.disable_interactive;
} = Self::storage_paths(registry, config, disable_interactive).await?;

let (keyring_backend, keys) = if cfg!(feature = "keyring") {
(config.keyring_backend.clone(), config.keys.clone())
Expand All @@ -1402,6 +1429,16 @@ impl FileSystemClient {
auth_token = crate::keyring::Keyring::from_config(config)?.get_auth_token(&url)?
}

let (packages, content, namespace_map) = match (
FileSystemRegistryStorage::try_lock(registries_dir.clone())?,
FileSystemContentStorage::try_lock(content_dir.clone())?,
FileSystemNamespaceMapStorage::new(namespace_map_path.clone()),
) {
(Some(packages), Some(content), namespace_map) => (packages, content, namespace_map),
(None, _, _) => return Ok(StorageLockResult::NotAcquired(registries_dir)),
(_, None, _) => return Ok(StorageLockResult::NotAcquired(content_dir)),
};

Ok(StorageLockResult::Acquired(Self::new(
url.into_url(),
packages,
Expand All @@ -1427,10 +1464,11 @@ impl FileSystemClient {
///
/// Same as calling `try_new_with_config` with
/// `Config::from_default_file()?.unwrap_or_default()`.
pub fn try_new_with_default_config(
pub async fn try_new_with_default_config(
url: Option<&str>,
) -> Result<StorageLockResult<Self>, ClientError> {
Self::try_new_with_config(url, &Config::from_default_file()?.unwrap_or_default(), None)
.await
}

/// Creates a client for the given registry URL.
Expand All @@ -1439,20 +1477,20 @@ impl FileSystemClient {
/// URL, an error is returned.
///
/// This method blocks if storage locks cannot be acquired.
pub fn new_with_config(
url: Option<&str>,
pub async fn new_with_config(
registry: Option<&str>,
config: &Config,
mut auth_token: Option<Secret<String>>,
) -> Result<Self, ClientError> {
let disable_interactive =
cfg!(not(feature = "cli-interactive")) || config.disable_interactive;

let StoragePaths {
registry_url,
registry_url: url,
registries_dir,
content_dir,
namespace_map_path,
} = config.storage_paths_for_url(url)?;

let disable_interactive =
cfg!(not(feature = "cli-interactive")) || config.disable_interactive;
} = Self::storage_paths(registry, config, disable_interactive).await?;

let (keyring_backend, keys) = if cfg!(feature = "keyring") {
(config.keyring_backend.clone(), config.keys.clone())
Expand All @@ -1462,12 +1500,11 @@ impl FileSystemClient {

#[cfg(feature = "keyring")]
if auth_token.is_none() && config.keyring_auth {
auth_token =
crate::keyring::Keyring::from_config(config)?.get_auth_token(&registry_url)?
auth_token = crate::keyring::Keyring::from_config(config)?.get_auth_token(&url)?
}

Self::new(
registry_url.into_url(),
url.into_url(),
FileSystemRegistryStorage::lock(registries_dir)?,
FileSystemContentStorage::lock(content_dir)?,
FileSystemNamespaceMapStorage::new(namespace_map_path),
Expand All @@ -1489,8 +1526,8 @@ impl FileSystemClient {
///
/// Same as calling `new_with_config` with
/// `Config::from_default_file()?.unwrap_or_default()`.
pub fn new_with_default_config(url: Option<&str>) -> Result<Self, ClientError> {
Self::new_with_config(url, &Config::from_default_file()?.unwrap_or_default(), None)
pub async fn new_with_default_config(url: Option<&str>) -> Result<Self, ClientError> {
Self::new_with_config(url, &Config::from_default_file()?.unwrap_or_default(), None).await
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/client/src/registry_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use url::{Host, Url};

/// The base URL of a registry server.
// Note: The inner Url always has a scheme and host.
#[derive(Clone)]
#[derive(Clone, Eq, PartialEq)]
pub struct RegistryUrl(Url);

impl RegistryUrl {
Expand Down
8 changes: 5 additions & 3 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,19 @@ impl CommonOptions {
}

/// Creates the warg client to use.
pub fn create_client(&self, config: &Config) -> Result<FileSystemClient, ClientError> {
pub async fn create_client(&self, config: &Config) -> Result<FileSystemClient, ClientError> {
let client =
match FileSystemClient::try_new_with_config(self.registry.as_deref(), config, None)? {
match FileSystemClient::try_new_with_config(self.registry.as_deref(), config, None)
.await?
{
StorageLockResult::Acquired(client) => Ok(client),
StorageLockResult::NotAcquired(path) => {
println!(
"blocking on lock for directory `{path}`...",
path = path.display()
);

FileSystemClient::new_with_config(self.registry.as_deref(), config, None)
FileSystemClient::new_with_config(self.registry.as_deref(), config, None).await
}
}?;
Ok(client)
Expand Down
2 changes: 1 addition & 1 deletion src/commands/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ impl BundleCommand {
/// Executes the command.
pub async fn exec(self) -> Result<()> {
let config = self.common.read_config()?;
let client = self.common.create_client(&config)?;
let client = self.common.create_client(&config).await?;

let info = client.package(&self.package).await?;
client.bundle_component(&info).await?;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/clear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ impl ClearCommand {
/// Executes the command.
pub async fn exec(self) -> Result<()> {
let config = self.common.read_config()?;
let client = self.common.create_client(&config)?;
let client = self.common.create_client(&config).await?;

println!("clearing local content cache...");
client.clear_content_cache().await?;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ impl ConfigCommand {

// reset when changing home registry
if changing_home_registry {
let client = self.common.create_client(&config)?;
let client = self.common.create_client(&config).await?;
client.reset_namespaces().await?;
client.reset_registry().await?;
}
Expand Down
2 changes: 1 addition & 1 deletion src/commands/dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ impl DependenciesCommand {
/// Executes the command.
pub async fn exec(self) -> Result<()> {
let config = self.common.read_config()?;
let client = self.common.create_client(&config)?;
let client = self.common.create_client(&config).await?;

let info = client.package(&self.package).await?;
Self::print_package_info(&client, &info).await?;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ impl DownloadCommand {
/// Executes the command.
pub async fn exec(self) -> Result<()> {
let config = self.common.read_config()?;
let client = self.common.create_client(&config)?;
let client = self.common.create_client(&config).await?;

println!("Downloading `{name}`...", name = self.name);

Expand Down
4 changes: 2 additions & 2 deletions src/commands/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ impl InfoCommand {
/// Executes the command.
pub async fn exec(self) -> Result<()> {
let config = self.common.read_config()?;
let client = self.common.create_client(&config)?;
let client = self.common.create_client(&config).await?;

println!("\nRegistry: {url}", url = client.url());
print!("\nRegistry: {url} ", url = client.url().registry_domain());
if config.keyring_auth
&& Keyring::from_config(&config)?
.get_auth_token(client.url())?
Expand Down
2 changes: 1 addition & 1 deletion src/commands/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ impl LockCommand {
/// Executes the command.
pub async fn exec(self) -> Result<()> {
let config = self.common.read_config()?;
let client = self.common.create_client(&config)?;
let client = self.common.create_client(&config).await?;

let info = client.package(&self.package).await?;
client.lock_component(&info).await?;
Expand Down
Loading

0 comments on commit ac3342b

Please sign in to comment.