use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DockerRegistry {
DockerHub,
Ghcr,
Quay,
Gcr,
}
impl DockerRegistry {
pub fn name(&self) -> &'static str {
match self {
Self::DockerHub => "docker-hub",
Self::Ghcr => "ghcr",
Self::Quay => "quay",
Self::Gcr => "gcr",
}
}
pub fn prefix(&self) -> &'static str {
match self {
Self::DockerHub => "",
Self::Ghcr => "ghcr.io/",
Self::Quay => "quay.io/",
Self::Gcr => "gcr.io/",
}
}
pub fn all() -> &'static [DockerRegistry] {
&[Self::DockerHub, Self::Ghcr, Self::Quay, Self::Gcr]
}
pub fn docker_hub() -> &'static [DockerRegistry] {
&[Self::DockerHub]
}
pub fn ghcr() -> &'static [DockerRegistry] {
&[Self::Ghcr]
}
pub fn cloud() -> &'static [DockerRegistry] {
&[Self::Quay, Self::Gcr]
}
}
struct DetectedRegistry {
registry: DockerRegistry,
clean_name: String,
}
struct FetchedPackage {
package: PackageMeta,
#[allow(dead_code)]
registry: DockerRegistry,
}
pub struct Docker {
registries: Vec<DockerRegistry>,
}
impl Docker {
pub fn all() -> Self {
Self {
registries: DockerRegistry::all().to_vec(),
}
}
pub fn hub() -> Self {
Self {
registries: DockerRegistry::docker_hub().to_vec(),
}
}
pub fn ghcr() -> Self {
Self {
registries: DockerRegistry::ghcr().to_vec(),
}
}
pub fn cloud() -> Self {
Self {
registries: DockerRegistry::cloud().to_vec(),
}
}
pub fn with_registries(registries: &[DockerRegistry]) -> Self {
Self {
registries: registries.to_vec(),
}
}
fn detect_registry(name: &str) -> DetectedRegistry {
if name.starts_with("ghcr.io/") {
DetectedRegistry {
registry: DockerRegistry::Ghcr,
clean_name: name.trim_start_matches("ghcr.io/").to_string(),
}
} else if name.starts_with("quay.io/") {
DetectedRegistry {
registry: DockerRegistry::Quay,
clean_name: name.trim_start_matches("quay.io/").to_string(),
}
} else if name.starts_with("gcr.io/") {
DetectedRegistry {
registry: DockerRegistry::Gcr,
clean_name: name.trim_start_matches("gcr.io/").to_string(),
}
} else {
DetectedRegistry {
registry: DockerRegistry::DockerHub,
clean_name: name.to_string(),
}
}
}
fn fetch_from_dockerhub(name: &str) -> Result<FetchedPackage, IndexError> {
let (namespace, repo) = if name.contains('/') {
let parts: Vec<&str> = name.splitn(2, '/').collect();
(parts[0], parts[1])
} else {
("library", name)
};
let url = format!(
"https://hub.docker.com/v2/repositories/{}/{}/",
namespace, repo
);
let response: serde_json::Value = ureq::get(&url)
.call()
.map_err(|_| IndexError::NotFound(name.to_string()))?
.into_json()?;
let tags_url = format!(
"https://hub.docker.com/v2/repositories/{}/{}/tags?page_size=1&ordering=-last_updated",
namespace, repo
);
let tags: serde_json::Value = ureq::get(&tags_url)
.call()
.map_err(|_| IndexError::NotFound(name.to_string()))?
.into_json()?;
let latest_tag = tags["results"]
.as_array()
.and_then(|arr| arr.first())
.and_then(|t| t["name"].as_str())
.unwrap_or("latest");
let keywords: Vec<String> = response["categories"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|c| c["slug"].as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let mut extra = HashMap::new();
extra.insert(
"source_repo".to_string(),
serde_json::Value::String("docker-hub".to_string()),
);
Ok(FetchedPackage {
package: PackageMeta {
name: format!(
"{}/{}",
namespace,
response["name"].as_str().unwrap_or(repo)
),
version: latest_tag.to_string(),
description: response["description"].as_str().map(String::from),
homepage: None,
repository: None,
license: None,
binaries: Vec::new(),
keywords,
maintainers: vec![
response["namespace"]
.as_str()
.unwrap_or(namespace)
.to_string(),
],
published: response["last_updated"].as_str().map(String::from),
downloads: response["pull_count"].as_u64(),
archive_url: None,
checksum: None,
extra,
},
registry: DockerRegistry::DockerHub,
})
}
fn fetch_versions_dockerhub(name: &str) -> Result<Vec<VersionMeta>, IndexError> {
let (namespace, repo) = if name.contains('/') {
let parts: Vec<&str> = name.splitn(2, '/').collect();
(parts[0], parts[1])
} else {
("library", name)
};
let url = format!(
"https://hub.docker.com/v2/repositories/{}/{}/tags?page_size=50&ordering=-last_updated",
namespace, repo
);
let response: serde_json::Value = ureq::get(&url)
.call()
.map_err(|_| IndexError::NotFound(name.to_string()))?
.into_json()?;
let tags = response["results"]
.as_array()
.ok_or_else(|| IndexError::NotFound(name.to_string()))?;
Ok(tags
.iter()
.filter_map(|t| {
Some(VersionMeta {
version: format!("{} (docker-hub)", t["name"].as_str()?),
released: t["last_updated"].as_str().map(String::from),
yanked: false,
})
})
.collect())
}
fn fetch_all_versions_dockerhub(name: &str) -> Result<Vec<PackageMeta>, IndexError> {
let (namespace, repo) = if name.contains('/') {
let parts: Vec<&str> = name.splitn(2, '/').collect();
(parts[0], parts[1])
} else {
("library", name)
};
let repo_url = format!(
"https://hub.docker.com/v2/repositories/{}/{}/",
namespace, repo
);
let repo_info: serde_json::Value = ureq::get(&repo_url)
.call()
.map_err(|_| IndexError::NotFound(name.to_string()))?
.into_json()?;
let description = repo_info["description"].as_str().map(String::from);
let pull_count = repo_info["pull_count"].as_u64();
let tags_url = format!(
"https://hub.docker.com/v2/repositories/{}/{}/tags?page_size=100&ordering=-last_updated",
namespace, repo
);
let response: serde_json::Value = ureq::get(&tags_url)
.call()
.map_err(|_| IndexError::NotFound(name.to_string()))?
.into_json()?;
let tags = response["results"]
.as_array()
.ok_or_else(|| IndexError::NotFound(name.to_string()))?;
let full_name = format!("{}/{}", namespace, repo);
Ok(tags
.iter()
.filter_map(|t| {
let tag_name = t["name"].as_str()?;
let mut extra = HashMap::new();
extra.insert(
"source_repo".to_string(),
serde_json::Value::String("docker-hub".to_string()),
);
if let Some(digest) = t["digest"].as_str() {
extra.insert(
"digest".to_string(),
serde_json::Value::String(digest.to_string()),
);
}
if let Some(size) = t["full_size"].as_u64() {
extra.insert("size".to_string(), serde_json::Value::Number(size.into()));
}
if let Some(images) = t["images"].as_array() {
let archs: Vec<serde_json::Value> = images
.iter()
.filter_map(|img| {
img["architecture"]
.as_str()
.map(|a| serde_json::Value::String(a.to_string()))
})
.collect();
if !archs.is_empty() {
extra.insert("architectures".to_string(), serde_json::Value::Array(archs));
}
let os_list: Vec<serde_json::Value> = images
.iter()
.filter_map(|img| {
img["os"]
.as_str()
.map(|o| serde_json::Value::String(o.to_string()))
})
.collect();
if !os_list.is_empty() {
let unique: std::collections::HashSet<_> =
os_list.iter().filter_map(|v| v.as_str()).collect();
let unique_vec: Vec<serde_json::Value> = unique
.into_iter()
.map(|s| serde_json::Value::String(s.to_string()))
.collect();
extra.insert("os".to_string(), serde_json::Value::Array(unique_vec));
}
}
Some(PackageMeta {
name: full_name.clone(),
version: tag_name.to_string(),
description: description.clone(),
homepage: None,
repository: None,
license: None,
binaries: Vec::new(),
keywords: Vec::new(),
maintainers: vec![namespace.to_string()],
published: t["last_updated"].as_str().map(String::from),
downloads: pull_count,
archive_url: None,
checksum: t["digest"].as_str().map(String::from),
extra,
})
})
.collect())
}
fn fetch_from_quay(name: &str) -> Result<FetchedPackage, IndexError> {
let (namespace, repo) = if name.contains('/') {
let parts: Vec<&str> = name.splitn(2, '/').collect();
(parts[0], parts[1])
} else {
return Err(IndexError::Parse(
"Quay.io requires namespace/repo format".into(),
));
};
let url = format!("https://quay.io/api/v1/repository/{}/{}", namespace, repo);
let response: serde_json::Value = ureq::get(&url)
.call()
.map_err(|_| IndexError::NotFound(name.to_string()))?
.into_json()?;
let latest_tag = response["tags"]
.as_object()
.and_then(|tags| tags.keys().next())
.map(|s| s.as_str())
.unwrap_or("latest");
let mut extra = HashMap::new();
extra.insert(
"source_repo".to_string(),
serde_json::Value::String("quay".to_string()),
);
Ok(FetchedPackage {
package: PackageMeta {
name: format!("quay.io/{}/{}", namespace, repo),
version: latest_tag.to_string(),
description: response["description"].as_str().map(String::from),
homepage: None,
repository: None,
license: None,
binaries: Vec::new(),
keywords: Vec::new(),
maintainers: vec![namespace.to_string()],
published: None,
downloads: None,
archive_url: None,
checksum: None,
extra,
},
registry: DockerRegistry::Quay,
})
}
fn fetch_versions_quay(name: &str) -> Result<Vec<VersionMeta>, IndexError> {
let (namespace, repo) = if name.contains('/') {
let parts: Vec<&str> = name.splitn(2, '/').collect();
(parts[0], parts[1])
} else {
return Err(IndexError::Parse(
"Quay.io requires namespace/repo format".into(),
));
};
let url = format!(
"https://quay.io/api/v1/repository/{}/{}/tag/",
namespace, repo
);
let response: serde_json::Value = ureq::get(&url)
.call()
.map_err(|_| IndexError::NotFound(name.to_string()))?
.into_json()?;
let tags = response["tags"]
.as_array()
.ok_or_else(|| IndexError::NotFound(name.to_string()))?;
Ok(tags
.iter()
.filter_map(|t| {
Some(VersionMeta {
version: format!("{} (quay)", t["name"].as_str()?),
released: t["last_modified"].as_str().map(String::from),
yanked: false,
})
})
.collect())
}
fn search_dockerhub(query: &str) -> Result<Vec<PackageMeta>, IndexError> {
let url = format!(
"https://hub.docker.com/v2/search/repositories?query={}&page_size=25",
query
);
let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
let results = response["results"]
.as_array()
.ok_or_else(|| IndexError::Parse("Invalid search response".into()))?;
let mut extra = HashMap::new();
extra.insert(
"source_repo".to_string(),
serde_json::Value::String("docker-hub".to_string()),
);
Ok(results
.iter()
.filter_map(|img| {
let name = if img["is_official"].as_bool().unwrap_or(false) {
format!("library/{}", img["repo_name"].as_str()?)
} else {
img["repo_name"].as_str()?.to_string()
};
Some(PackageMeta {
name,
version: "latest".to_string(),
description: img["short_description"].as_str().map(String::from),
homepage: None,
repository: None,
license: None,
binaries: Vec::new(),
keywords: Vec::new(),
maintainers: Vec::new(),
published: None,
downloads: img["pull_count"].as_u64(),
archive_url: None,
checksum: None,
extra: extra.clone(),
})
})
.collect())
}
fn search_quay(query: &str) -> Result<Vec<PackageMeta>, IndexError> {
let url = format!("https://quay.io/api/v1/find/repositories?query={}", query);
let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
let results = response["results"]
.as_array()
.ok_or_else(|| IndexError::Parse("Invalid search response".into()))?;
let mut extra = HashMap::new();
extra.insert(
"source_repo".to_string(),
serde_json::Value::String("quay".to_string()),
);
Ok(results
.iter()
.filter_map(|repo| {
let namespace = repo["namespace"]["name"].as_str()?;
let name = repo["name"].as_str()?;
Some(PackageMeta {
name: format!("quay.io/{}/{}", namespace, name),
version: "latest".to_string(),
description: repo["description"].as_str().map(String::from),
homepage: None,
repository: None,
license: None,
binaries: Vec::new(),
keywords: Vec::new(),
maintainers: vec![namespace.to_string()],
published: None,
downloads: None,
archive_url: None,
checksum: None,
extra: extra.clone(),
})
})
.collect())
}
}
impl PackageIndex for Docker {
fn ecosystem(&self) -> &'static str {
"docker"
}
fn display_name(&self) -> &'static str {
"Container Registries (Docker)"
}
fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
let detected = Self::detect_registry(name);
if self.registries.contains(&detected.registry) {
return match detected.registry {
DockerRegistry::DockerHub => {
Self::fetch_from_dockerhub(&detected.clean_name).map(|f| f.package)
}
DockerRegistry::Quay => {
Self::fetch_from_quay(&detected.clean_name).map(|f| f.package)
}
DockerRegistry::Ghcr | DockerRegistry::Gcr => {
let mut extra = HashMap::new();
extra.insert(
"source_repo".to_string(),
serde_json::Value::String(detected.registry.name().to_string()),
);
Ok(PackageMeta {
name: format!("{}{}", detected.registry.prefix(), detected.clean_name),
version: "latest".to_string(),
description: None,
homepage: None,
repository: None,
license: None,
binaries: Vec::new(),
keywords: Vec::new(),
maintainers: Vec::new(),
published: None,
downloads: None,
archive_url: None,
checksum: None,
extra,
})
}
};
}
for ®istry in &self.registries {
let result = match registry {
DockerRegistry::DockerHub => Self::fetch_from_dockerhub(name),
DockerRegistry::Quay => Self::fetch_from_quay(name),
DockerRegistry::Ghcr | DockerRegistry::Gcr => continue, };
if let Ok(fetched) = result {
return Ok(fetched.package);
}
}
Err(IndexError::NotFound(name.to_string()))
}
fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
let detected = Self::detect_registry(name);
let mut all_versions = Vec::new();
if self.registries.contains(&detected.registry) {
let versions = match detected.registry {
DockerRegistry::DockerHub => Self::fetch_versions_dockerhub(&detected.clean_name),
DockerRegistry::Quay => Self::fetch_versions_quay(&detected.clean_name),
DockerRegistry::Ghcr | DockerRegistry::Gcr => {
Err(IndexError::Parse("Registry requires authentication".into()))
}
};
if let Ok(v) = versions {
return Ok(v);
}
}
for ®istry in &self.registries {
let result = match registry {
DockerRegistry::DockerHub => Self::fetch_versions_dockerhub(name),
DockerRegistry::Quay => Self::fetch_versions_quay(name),
DockerRegistry::Ghcr | DockerRegistry::Gcr => continue,
};
if let Ok(versions) = result {
all_versions.extend(versions);
}
}
if all_versions.is_empty() {
return Err(IndexError::NotFound(name.to_string()));
}
Ok(all_versions)
}
fn fetch_all_versions(&self, name: &str) -> Result<Vec<PackageMeta>, IndexError> {
let detected = Self::detect_registry(name);
if self.registries.contains(&detected.registry) {
return match detected.registry {
DockerRegistry::DockerHub => {
Self::fetch_all_versions_dockerhub(&detected.clean_name)
}
DockerRegistry::Quay | DockerRegistry::Ghcr | DockerRegistry::Gcr => {
let versions = self.fetch_versions(name)?;
Ok(versions
.into_iter()
.map(|v| PackageMeta {
name: name.to_string(),
version: v.version,
published: v.released,
..Default::default()
})
.collect())
}
};
}
if self.registries.contains(&DockerRegistry::DockerHub)
&& let Ok(versions) = Self::fetch_all_versions_dockerhub(name)
{
return Ok(versions);
}
Err(IndexError::NotFound(name.to_string()))
}
fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
let mut results = Vec::new();
if self.registries.contains(&DockerRegistry::DockerHub)
&& let Ok(packages) = Self::search_dockerhub(query)
{
results.extend(packages);
}
if self.registries.contains(&DockerRegistry::Quay)
&& let Ok(packages) = Self::search_quay(query)
{
results.extend(packages);
}
Ok(results)
}
}