use std::fmt;
use futures::prelude::*;
use log::trace;
use reqwest::{Method, Response, StatusCode, Url};
use serde::{Deserialize, Serialize};
use crate::{
errors::{self, *},
mediatypes::MediaTypes,
};
mod config;
pub use self::config::Config;
mod catalog;
mod auth;
pub use auth::WwwHeaderParseError;
pub mod manifest;
mod tags;
mod blobs;
mod referrers;
mod content_digest;
pub(crate) use self::content_digest::ContentDigest;
pub use self::content_digest::ContentDigestError;
#[derive(Clone, Debug)]
pub struct Client {
base_url: String,
credentials: Option<(String, String)>,
user_agent: Option<String>,
auth: Option<auth::Auth>,
client: reqwest::Client,
accepted_types: Vec<(MediaTypes, Option<f64>)>,
}
impl Client {
pub fn configure() -> Config {
Config::default()
}
pub async fn ensure_v2_registry(self) -> Result<Self> {
if !self.is_v2_supported().await? {
Err(Error::V2NotSupported)
} else {
Ok(self)
}
}
pub async fn is_v2_supported(&self) -> Result<bool> {
match self.is_v2_supported_and_authorized().await {
Ok((v2_supported, _)) => Ok(v2_supported),
Err(crate::Error::UnexpectedHttpStatus(_)) => Ok(false),
Err(e) => Err(e),
}
}
pub async fn is_v2_supported_and_authorized(&self) -> Result<(bool, bool)> {
let api_header = "Docker-Distribution-API-Version";
let api_version = "registry/2.0";
let v2_endpoint = format!("{}/v2/", self.base_url);
let request = reqwest::Url::parse(&v2_endpoint).map(|url| {
trace!("GET {url:?}");
self.build_reqwest(Method::GET, url)
})?;
let response = request.send().await?;
match (response.status(), response.headers().get(api_header)) {
(StatusCode::OK, Some(x)) => Ok((x == api_version, true)),
(StatusCode::UNAUTHORIZED, Some(x)) => Ok((x == api_version, false)),
(s, v) => {
trace!("Got unexpected status {s}, header version {v:?}");
Err(crate::Error::UnexpectedHttpStatus(s))
}
}
}
fn build_reqwest(&self, method: Method, url: Url) -> reqwest::RequestBuilder {
let mut builder = self.client.request(method, url);
if let Some(auth) = &self.auth {
builder = auth.add_auth_headers(builder);
};
if let Some(ua) = &self.user_agent {
builder = builder.header(reqwest::header::USER_AGENT, ua.as_str());
};
builder
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct ApiError {
code: String,
message: Option<String>,
detail: Option<Box<serde_json::value::RawValue>>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, thiserror::Error)]
pub struct ApiErrors {
errors: Option<Vec<ApiError>>,
}
impl ApiError {
pub fn code(&self) -> &str {
&self.code
}
pub fn message(&self) -> Option<&str> {
self.message.as_deref()
}
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({})", self.code)?;
if let Some(message) = &self.message {
write!(f, ", message: {message}")?;
}
if let Some(detail) = &self.detail {
write!(f, ", detail: {detail}")?;
}
Ok(())
}
}
impl ApiErrors {
pub async fn from(r: Response) -> errors::Error {
match r.json::<ApiErrors>().await {
Ok(e) => errors::Error::Api(e),
Err(e) => errors::Error::Reqwest(e),
}
}
pub fn errors(&self) -> &Option<Vec<ApiError>> {
&self.errors
}
}
impl fmt::Display for ApiErrors {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.errors.is_none() {
return Ok(());
}
for error in self.errors.as_ref().unwrap().iter() {
write!(f, "({error})")?
}
Ok(())
}
}