Skip to content

Commit ac3342b

Browse files
authored
Well known config (#291)
- 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.
1 parent 14b98ac commit ac3342b

23 files changed

+222
-95
lines changed

crates/api/src/lib.rs

+16
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ pub mod v1;
55

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

8+
/// Relative URL path for the `WellKnownConfig`.
9+
pub const WELL_KNOWN_PATH: &str = ".well-known/wasm-pkg/registry.json";
10+
11+
/// This config allows a domain to point to another URL where the registry
12+
/// API is hosted.
13+
#[derive(Debug, Default, Deserialize, Serialize)]
14+
#[serde(rename_all = "camelCase")]
15+
pub struct WellKnownConfig {
16+
/// For OCI registries, the domain name where the registry is hosted.
17+
pub oci_registry: Option<String>,
18+
/// For OCI registries, a name prefix to use before the namespace.
19+
pub oci_namespace_prefix: Option<String>,
20+
/// For Warg registries, the URL where the registry is hosted.
21+
pub warg_url: Option<String>,
22+
}
23+
824
/// A utility type for serializing and deserializing constant status codes.
925
struct Status<const CODE: u16>;
1026

crates/client/src/api.rs

+51-13
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,24 @@ use secrecy::{ExposeSecret, Secret};
1212
use serde::de::DeserializeOwned;
1313
use std::borrow::Cow;
1414
use thiserror::Error;
15-
use warg_api::v1::{
16-
content::{ContentError, ContentSourcesResponse},
17-
fetch::{
18-
FetchError, FetchLogsRequest, FetchLogsResponse, FetchPackageNamesRequest,
19-
FetchPackageNamesResponse,
15+
use warg_api::{
16+
v1::{
17+
content::{ContentError, ContentSourcesResponse},
18+
fetch::{
19+
FetchError, FetchLogsRequest, FetchLogsResponse, FetchPackageNamesRequest,
20+
FetchPackageNamesResponse,
21+
},
22+
ledger::{LedgerError, LedgerSourcesResponse},
23+
monitor::{CheckpointVerificationResponse, MonitorError},
24+
package::{ContentSource, PackageError, PackageRecord, PublishRecordRequest},
25+
paths,
26+
proof::{
27+
ConsistencyRequest, ConsistencyResponse, InclusionRequest, InclusionResponse,
28+
ProofError,
29+
},
30+
REGISTRY_HEADER_NAME, REGISTRY_HINT_HEADER_NAME,
2031
},
21-
ledger::{LedgerError, LedgerSourcesResponse},
22-
monitor::{CheckpointVerificationResponse, MonitorError},
23-
package::{ContentSource, PackageError, PackageRecord, PublishRecordRequest},
24-
paths,
25-
proof::{
26-
ConsistencyRequest, ConsistencyResponse, InclusionRequest, InclusionResponse, ProofError,
27-
},
28-
REGISTRY_HEADER_NAME, REGISTRY_HINT_HEADER_NAME,
32+
WellKnownConfig, WELL_KNOWN_PATH,
2933
};
3034
use warg_crypto::hash::{AnyHash, HashError, Sha256};
3135
use warg_protocol::{
@@ -107,6 +111,9 @@ pub enum ClientError {
107111
/// The provided log was not found with hint header.
108112
#[error("log `{0}` was not found in this registry, but the registry provided the hint header: `{1:?}`")]
109113
LogNotFoundWithHint(LogId, HeaderValue),
114+
/// Invalid well-known config.
115+
#[error("registry `{0}` returned an invalid well-known config")]
116+
InvalidWellKnownConfig(String),
110117
/// An other error occurred during the requested operation.
111118
#[error(transparent)]
112119
Other(#[from] anyhow::Error),
@@ -216,6 +223,37 @@ impl Client {
216223
pub fn url(&self) -> &RegistryUrl {
217224
&self.url
218225
}
226+
/// Gets the `.well-known` configuration registry URL.
227+
pub async fn well_known_config(&self) -> Result<Option<RegistryUrl>, ClientError> {
228+
let url = self.url.join(WELL_KNOWN_PATH);
229+
tracing::debug!(url, "getting `.well-known` config",);
230+
231+
let res = self.client.get(url).send().await?;
232+
233+
if !res.status().is_success() {
234+
tracing::debug!(
235+
"the `.well-known` config request returned HTTP status `{status}`",
236+
status = res.status()
237+
);
238+
return Ok(None);
239+
}
240+
241+
if let Some(warg_url) = res
242+
.json::<WellKnownConfig>()
243+
.await
244+
.map_err(|e| {
245+
tracing::debug!("parsing `.well-known` config failed: {e}");
246+
ClientError::InvalidWellKnownConfig(self.url.registry_domain().to_string())
247+
})?
248+
.warg_url
249+
{
250+
Ok(Some(RegistryUrl::new(warg_url)?))
251+
} else {
252+
tracing::debug!("the `.well-known` config did not have a `wargUrl` set");
253+
Ok(None)
254+
}
255+
}
256+
219257
/// Gets the latest checkpoint from the registry.
220258
pub async fn latest_checkpoint(
221259
&self,

crates/client/src/config.rs

+1-6
Original file line numberDiff line numberDiff line change
@@ -300,13 +300,8 @@ impl Config {
300300

301301
pub(crate) fn storage_paths_for_url(
302302
&self,
303-
url: Option<&str>,
303+
registry_url: RegistryUrl,
304304
) -> Result<StoragePaths, ClientError> {
305-
let registry_url = RegistryUrl::new(
306-
url.or(self.home_url.as_deref())
307-
.ok_or(ClientError::NoHomeRegistryUrl)?,
308-
)?;
309-
310305
let label = registry_url.safe_label();
311306
let registries_dir = self.registries_dir()?.join(label);
312307
let content_dir = self.content_dir()?;

crates/client/src/lib.rs

+66-29
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ pub use self::registry_url::RegistryUrl;
5757

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

60+
/// For Bytecode Alliance projects, the default registry is set to `bytecodealliance.org`.
61+
/// The `.well-known` config path may resolve to another domain where the registry is hosted.
62+
pub const DEFAULT_REGISTRY: &str = "bytecodealliance.org";
63+
6064
/// A client for a Warg registry.
6165
pub struct Client<R, C, N>
6266
where
@@ -1358,6 +1362,39 @@ pub enum StorageLockResult<T> {
13581362
}
13591363

13601364
impl FileSystemClient {
1365+
async fn storage_paths(
1366+
url: Option<&str>,
1367+
config: &Config,
1368+
disable_interactive: bool,
1369+
) -> Result<StoragePaths, ClientError> {
1370+
let checking_url_for_well_known = RegistryUrl::new(
1371+
url.or(config.home_url.as_deref())
1372+
.unwrap_or(DEFAULT_REGISTRY),
1373+
)?;
1374+
1375+
let url = if let Some(warg_url) =
1376+
api::Client::new(checking_url_for_well_known.to_string(), None)?
1377+
.well_known_config()
1378+
.await?
1379+
{
1380+
if !disable_interactive && warg_url != checking_url_for_well_known {
1381+
println!(
1382+
"Resolved `{well_known}` to registry hosted on `{registry}`",
1383+
well_known = checking_url_for_well_known.registry_domain(),
1384+
registry = warg_url.registry_domain(),
1385+
);
1386+
}
1387+
warg_url
1388+
} else {
1389+
RegistryUrl::new(
1390+
url.or(config.home_url.as_deref())
1391+
.ok_or(ClientError::NoHomeRegistryUrl)?,
1392+
)?
1393+
};
1394+
1395+
config.storage_paths_for_url(url)
1396+
}
1397+
13611398
/// Attempts to create a client for the given registry URL.
13621399
///
13631400
/// If the URL is `None`, the home registry URL is used; if there is no home registry
@@ -1366,30 +1403,20 @@ impl FileSystemClient {
13661403
/// If a lock cannot be acquired for a storage directory, then
13671404
/// `NewClientResult::Blocked` is returned with the path to the
13681405
/// directory that could not be locked.
1369-
pub fn try_new_with_config(
1370-
url: Option<&str>,
1406+
pub async fn try_new_with_config(
1407+
registry: Option<&str>,
13711408
config: &Config,
13721409
mut auth_token: Option<Secret<String>>,
13731410
) -> Result<StorageLockResult<Self>, ClientError> {
1411+
let disable_interactive =
1412+
cfg!(not(feature = "cli-interactive")) || config.disable_interactive;
1413+
13741414
let StoragePaths {
13751415
registry_url: url,
13761416
registries_dir,
13771417
content_dir,
13781418
namespace_map_path,
1379-
} = config.storage_paths_for_url(url)?;
1380-
1381-
let (packages, content, namespace_map) = match (
1382-
FileSystemRegistryStorage::try_lock(registries_dir.clone())?,
1383-
FileSystemContentStorage::try_lock(content_dir.clone())?,
1384-
FileSystemNamespaceMapStorage::new(namespace_map_path.clone()),
1385-
) {
1386-
(Some(packages), Some(content), namespace_map) => (packages, content, namespace_map),
1387-
(None, _, _) => return Ok(StorageLockResult::NotAcquired(registries_dir)),
1388-
(_, None, _) => return Ok(StorageLockResult::NotAcquired(content_dir)),
1389-
};
1390-
1391-
let disable_interactive =
1392-
cfg!(not(feature = "cli-interactive")) || config.disable_interactive;
1419+
} = Self::storage_paths(registry, config, disable_interactive).await?;
13931420

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

1432+
let (packages, content, namespace_map) = match (
1433+
FileSystemRegistryStorage::try_lock(registries_dir.clone())?,
1434+
FileSystemContentStorage::try_lock(content_dir.clone())?,
1435+
FileSystemNamespaceMapStorage::new(namespace_map_path.clone()),
1436+
) {
1437+
(Some(packages), Some(content), namespace_map) => (packages, content, namespace_map),
1438+
(None, _, _) => return Ok(StorageLockResult::NotAcquired(registries_dir)),
1439+
(_, None, _) => return Ok(StorageLockResult::NotAcquired(content_dir)),
1440+
};
1441+
14051442
Ok(StorageLockResult::Acquired(Self::new(
14061443
url.into_url(),
14071444
packages,
@@ -1427,10 +1464,11 @@ impl FileSystemClient {
14271464
///
14281465
/// Same as calling `try_new_with_config` with
14291466
/// `Config::from_default_file()?.unwrap_or_default()`.
1430-
pub fn try_new_with_default_config(
1467+
pub async fn try_new_with_default_config(
14311468
url: Option<&str>,
14321469
) -> Result<StorageLockResult<Self>, ClientError> {
14331470
Self::try_new_with_config(url, &Config::from_default_file()?.unwrap_or_default(), None)
1471+
.await
14341472
}
14351473

14361474
/// Creates a client for the given registry URL.
@@ -1439,20 +1477,20 @@ impl FileSystemClient {
14391477
/// URL, an error is returned.
14401478
///
14411479
/// This method blocks if storage locks cannot be acquired.
1442-
pub fn new_with_config(
1443-
url: Option<&str>,
1480+
pub async fn new_with_config(
1481+
registry: Option<&str>,
14441482
config: &Config,
14451483
mut auth_token: Option<Secret<String>>,
14461484
) -> Result<Self, ClientError> {
1485+
let disable_interactive =
1486+
cfg!(not(feature = "cli-interactive")) || config.disable_interactive;
1487+
14471488
let StoragePaths {
1448-
registry_url,
1489+
registry_url: url,
14491490
registries_dir,
14501491
content_dir,
14511492
namespace_map_path,
1452-
} = config.storage_paths_for_url(url)?;
1453-
1454-
let disable_interactive =
1455-
cfg!(not(feature = "cli-interactive")) || config.disable_interactive;
1493+
} = Self::storage_paths(registry, config, disable_interactive).await?;
14561494

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

14631501
#[cfg(feature = "keyring")]
14641502
if auth_token.is_none() && config.keyring_auth {
1465-
auth_token =
1466-
crate::keyring::Keyring::from_config(config)?.get_auth_token(&registry_url)?
1503+
auth_token = crate::keyring::Keyring::from_config(config)?.get_auth_token(&url)?
14671504
}
14681505

14691506
Self::new(
1470-
registry_url.into_url(),
1507+
url.into_url(),
14711508
FileSystemRegistryStorage::lock(registries_dir)?,
14721509
FileSystemContentStorage::lock(content_dir)?,
14731510
FileSystemNamespaceMapStorage::new(namespace_map_path),
@@ -1489,8 +1526,8 @@ impl FileSystemClient {
14891526
///
14901527
/// Same as calling `new_with_config` with
14911528
/// `Config::from_default_file()?.unwrap_or_default()`.
1492-
pub fn new_with_default_config(url: Option<&str>) -> Result<Self, ClientError> {
1493-
Self::new_with_config(url, &Config::from_default_file()?.unwrap_or_default(), None)
1529+
pub async fn new_with_default_config(url: Option<&str>) -> Result<Self, ClientError> {
1530+
Self::new_with_config(url, &Config::from_default_file()?.unwrap_or_default(), None).await
14941531
}
14951532
}
14961533

crates/client/src/registry_url.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use url::{Host, Url};
55

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

1111
impl RegistryUrl {

src/commands.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,19 @@ impl CommonOptions {
6666
}
6767

6868
/// Creates the warg client to use.
69-
pub fn create_client(&self, config: &Config) -> Result<FileSystemClient, ClientError> {
69+
pub async fn create_client(&self, config: &Config) -> Result<FileSystemClient, ClientError> {
7070
let client =
71-
match FileSystemClient::try_new_with_config(self.registry.as_deref(), config, None)? {
71+
match FileSystemClient::try_new_with_config(self.registry.as_deref(), config, None)
72+
.await?
73+
{
7274
StorageLockResult::Acquired(client) => Ok(client),
7375
StorageLockResult::NotAcquired(path) => {
7476
println!(
7577
"blocking on lock for directory `{path}`...",
7678
path = path.display()
7779
);
7880

79-
FileSystemClient::new_with_config(self.registry.as_deref(), config, None)
81+
FileSystemClient::new_with_config(self.registry.as_deref(), config, None).await
8082
}
8183
}?;
8284
Ok(client)

src/commands/bundle.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ impl BundleCommand {
1919
/// Executes the command.
2020
pub async fn exec(self) -> Result<()> {
2121
let config = self.common.read_config()?;
22-
let client = self.common.create_client(&config)?;
22+
let client = self.common.create_client(&config).await?;
2323

2424
let info = client.package(&self.package).await?;
2525
client.bundle_component(&info).await?;

src/commands/clear.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ impl ClearCommand {
1414
/// Executes the command.
1515
pub async fn exec(self) -> Result<()> {
1616
let config = self.common.read_config()?;
17-
let client = self.common.create_client(&config)?;
17+
let client = self.common.create_client(&config).await?;
1818

1919
println!("clearing local content cache...");
2020
client.clear_content_cache().await?;

src/commands/config.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ impl ConfigCommand {
139139

140140
// reset when changing home registry
141141
if changing_home_registry {
142-
let client = self.common.create_client(&config)?;
142+
let client = self.common.create_client(&config).await?;
143143
client.reset_namespaces().await?;
144144
client.reset_registry().await?;
145145
}

src/commands/dependencies.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ impl DependenciesCommand {
3131
/// Executes the command.
3232
pub async fn exec(self) -> Result<()> {
3333
let config = self.common.read_config()?;
34-
let client = self.common.create_client(&config)?;
34+
let client = self.common.create_client(&config).await?;
3535

3636
let info = client.package(&self.package).await?;
3737
Self::print_package_info(&client, &info).await?;

src/commands/download.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ impl DownloadCommand {
2828
/// Executes the command.
2929
pub async fn exec(self) -> Result<()> {
3030
let config = self.common.read_config()?;
31-
let client = self.common.create_client(&config)?;
31+
let client = self.common.create_client(&config).await?;
3232

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

src/commands/info.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ impl InfoCommand {
2929
/// Executes the command.
3030
pub async fn exec(self) -> Result<()> {
3131
let config = self.common.read_config()?;
32-
let client = self.common.create_client(&config)?;
32+
let client = self.common.create_client(&config).await?;
3333

34-
println!("\nRegistry: {url}", url = client.url());
34+
print!("\nRegistry: {url} ", url = client.url().registry_domain());
3535
if config.keyring_auth
3636
&& Keyring::from_config(&config)?
3737
.get_auth_token(client.url())?

src/commands/lock.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ impl LockCommand {
1919
/// Executes the command.
2020
pub async fn exec(self) -> Result<()> {
2121
let config = self.common.read_config()?;
22-
let client = self.common.create_client(&config)?;
22+
let client = self.common.create_client(&config).await?;
2323

2424
let info = client.package(&self.package).await?;
2525
client.lock_component(&info).await?;

0 commit comments

Comments
 (0)