mod config;
mod label;
mod local;
mod meta;
mod oci;
mod package;
mod release;
mod toml;
use std::collections::HashMap;
use async_trait::async_trait;
use bytes::Bytes;
use futures_util::stream::BoxStream;
use local::LocalSource;
use oci::{OciConfig, OciSource};
use oci_distribution::errors::OciDistributionError;
pub use semver::Version;
pub use oci_distribution::client as oci_client;
pub use crate::{
config::ClientConfig,
package::PackageRef,
release::{ContentDigest, Release},
};
use crate::{
label::{InvalidLabel, Label},
meta::RegistryMeta,
};
pub struct Client {
config: ClientConfig,
sources: HashMap<String, Box<dyn PackageSource>>,
}
#[async_trait]
trait PackageSource {
async fn list_all_versions(&mut self, package: &PackageRef) -> Result<Vec<Version>, Error>;
async fn get_release(
&mut self,
package: &PackageRef,
version: &Version,
) -> Result<Release, Error>;
async fn stream_content(
&mut self,
package: &PackageRef,
release: &Release,
) -> Result<BoxStream<Result<Bytes, Error>>, Error>;
}
impl Client {
pub fn new(config: ClientConfig) -> Self {
Self {
config,
sources: Default::default(),
}
}
pub fn from_default_config_file() -> Result<Option<Self>, Error> {
Ok(ClientConfig::from_default_file()?.map(Self::new))
}
pub async fn list_all_versions(&mut self, package: &PackageRef) -> Result<Vec<Version>, Error> {
let source = self.resolve_source(package).await?;
source.list_all_versions(package).await
}
pub async fn get_release(
&mut self,
package: &PackageRef,
version: &Version,
) -> Result<Release, Error> {
let source = self.resolve_source(package).await?;
source.get_release(package, version).await
}
pub async fn stream_content(
&mut self,
package: &PackageRef,
release: &Release,
) -> Result<BoxStream<Result<Bytes, Error>>, Error> {
let source = self.resolve_source(package).await?;
source.stream_content(package, release).await
}
async fn resolve_source(
&mut self,
package: &PackageRef,
) -> Result<&mut dyn PackageSource, Error> {
let registry = self.config.resolve_package_registry(package)?.to_owned();
if !self.sources.contains_key(®istry) {
let registry_config = self
.config
.registry_configs
.get(®istry)
.cloned()
.unwrap_or_default();
tracing::debug!("Resolved registry config: {registry_config:?}");
let source: Box<dyn PackageSource> = match registry_config {
config::RegistryConfig::Local(config) => Box::new(LocalSource::new(config)),
config::RegistryConfig::Oci(config) => {
Box::new(self.build_oci_client(®istry, config).await?)
}
};
self.sources.insert(registry.clone(), source);
}
Ok(self.sources.get_mut(®istry).unwrap().as_mut())
}
async fn build_oci_client(
&mut self,
registry: &str,
config: OciConfig,
) -> Result<OciSource, Error> {
tracing::debug!("Building new OCI client for {registry:?}");
let registry_meta = match RegistryMeta::fetch(registry).await {
Ok(Some(meta)) => {
tracing::debug!("Got registry metadata {meta:?}");
meta
}
Ok(None) => {
tracing::debug!("Metadata not found");
Default::default()
}
Err(err) => {
tracing::warn!("Error fetching registry metadata: {err}");
Default::default()
}
};
OciSource::new(registry.to_string(), config, registry_meta)
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("failed to get registry credentials: {0:#}")]
CredentialError(anyhow::Error),
#[error("invalid config: {0:#}")]
InvalidConfig(anyhow::Error),
#[error("invalid content digest: {0}")]
InvalidContentDigest(String),
#[error("invalid label: {0}")]
InvalidLabel(#[from] InvalidLabel),
#[error("invalid package ref: {0}")]
InvalidPackageRef(String),
#[error("invalid package manifest: {0}")]
InvalidPackageManifest(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("OCI error: {0}")]
OciError(#[from] OciDistributionError),
#[error("no registry configured for namespace {0:?}")]
NoRegistryForNamespace(Label),
#[error("registry metadata error: {0:#}")]
RegistryMeta(#[source] anyhow::Error),
#[error("invalid version: {0}")]
VersionError(#[from] semver::Error),
}