mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-12 22:00:31 +00:00
fix: Docker dashboard for namespaced images, library/ auto-prepend for Hub official images (v0.2.32)
Docker dashboard: - build_docker_index now finds manifests segment by position, not fixed index - Correctly indexes library/alpine, grafana/grafana, and other namespaced images Docker proxy: - Auto-prepend library/ for single-segment names when upstream returns 404 - Applies to both manifests and blobs - nginx, alpine, node now work without explicit library/ prefix - Cached under original name for future local hits
This commit is contained in:
@@ -214,6 +214,38 @@ async fn download_blob(
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-prepend library/ for single-segment names (Docker Hub official images)
|
||||
if !name.contains('/') {
|
||||
let library_name = format!("library/{}", name);
|
||||
for upstream in &state.config.docker.upstreams {
|
||||
if let Ok(data) = fetch_blob_from_upstream(
|
||||
&state.http_client,
|
||||
&upstream.url,
|
||||
&library_name,
|
||||
&digest,
|
||||
&state.docker_auth,
|
||||
state.config.docker.proxy_timeout,
|
||||
upstream.auth.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
let storage = state.storage.clone();
|
||||
let key_clone = key.clone();
|
||||
let data_clone = data.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = storage.put(&key_clone, &data_clone).await;
|
||||
});
|
||||
|
||||
return (
|
||||
StatusCode::OK,
|
||||
[(header::CONTENT_TYPE, "application/octet-stream")],
|
||||
Bytes::from(data),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StatusCode::NOT_FOUND.into_response()
|
||||
}
|
||||
|
||||
@@ -453,6 +485,57 @@ async fn get_manifest(
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-prepend library/ for single-segment names (Docker Hub official images)
|
||||
// e.g., "nginx" -> "library/nginx", "alpine" -> "library/alpine"
|
||||
if !name.contains('/') {
|
||||
let library_name = format!("library/{}", name);
|
||||
for upstream in &state.config.docker.upstreams {
|
||||
if let Ok((data, content_type)) = fetch_manifest_from_upstream(
|
||||
&state.http_client,
|
||||
&upstream.url,
|
||||
&library_name,
|
||||
&reference,
|
||||
&state.docker_auth,
|
||||
state.config.docker.proxy_timeout,
|
||||
upstream.auth.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
state.metrics.record_download("docker");
|
||||
state.metrics.record_cache_miss();
|
||||
state.activity.push(ActivityEntry::new(
|
||||
ActionType::ProxyFetch,
|
||||
format!("{}:{}", name, reference),
|
||||
"docker",
|
||||
"PROXY",
|
||||
));
|
||||
|
||||
use sha2::Digest;
|
||||
let digest = format!("sha256:{:x}", sha2::Sha256::digest(&data));
|
||||
|
||||
// Cache under original name for future local hits
|
||||
let storage = state.storage.clone();
|
||||
let key_clone = key.clone();
|
||||
let data_clone = data.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = storage.put(&key_clone, &data_clone).await;
|
||||
});
|
||||
|
||||
state.repo_index.invalidate("docker");
|
||||
|
||||
return (
|
||||
StatusCode::OK,
|
||||
[
|
||||
(header::CONTENT_TYPE, content_type),
|
||||
(HeaderName::from_static("docker-content-digest"), digest),
|
||||
],
|
||||
Bytes::from(data),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StatusCode::NOT_FOUND.into_response()
|
||||
}
|
||||
|
||||
|
||||
@@ -173,35 +173,41 @@ async fn build_docker_index(storage: &Storage) -> Vec<RepoInfo> {
|
||||
}
|
||||
|
||||
if let Some(rest) = key.strip_prefix("docker/") {
|
||||
// Support both single-segment and namespaced images:
|
||||
// docker/alpine/manifests/latest.json → name="alpine"
|
||||
// docker/library/alpine/manifests/latest.json → name="library/alpine"
|
||||
let parts: Vec<_> = rest.split('/').collect();
|
||||
if parts.len() >= 3 && parts[1] == "manifests" && key.ends_with(".json") {
|
||||
let name = parts[0].to_string();
|
||||
let entry = repos.entry(name).or_insert((0, 0, 0));
|
||||
entry.0 += 1;
|
||||
let manifest_pos = parts.iter().position(|&p| p == "manifests");
|
||||
if let Some(pos) = manifest_pos {
|
||||
if pos >= 1 && key.ends_with(".json") {
|
||||
let name = parts[..pos].join("/");
|
||||
let entry = repos.entry(name).or_insert((0, 0, 0));
|
||||
entry.0 += 1;
|
||||
|
||||
if let Ok(data) = storage.get(key).await {
|
||||
if let Ok(m) = serde_json::from_slice::<serde_json::Value>(&data) {
|
||||
let cfg = m
|
||||
.get("config")
|
||||
.and_then(|c| c.get("size"))
|
||||
.and_then(|s| s.as_u64())
|
||||
.unwrap_or(0);
|
||||
let layers: u64 = m
|
||||
.get("layers")
|
||||
.and_then(|l| l.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|l| l.get("size").and_then(|s| s.as_u64()))
|
||||
.sum()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
entry.1 += cfg + layers;
|
||||
if let Ok(data) = storage.get(key).await {
|
||||
if let Ok(m) = serde_json::from_slice::<serde_json::Value>(&data) {
|
||||
let cfg = m
|
||||
.get("config")
|
||||
.and_then(|c| c.get("size"))
|
||||
.and_then(|s| s.as_u64())
|
||||
.unwrap_or(0);
|
||||
let layers: u64 = m
|
||||
.get("layers")
|
||||
.and_then(|l| l.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|l| l.get("size").and_then(|s| s.as_u64()))
|
||||
.sum()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
entry.1 += cfg + layers;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(meta) = storage.stat(key).await {
|
||||
if meta.modified > entry.2 {
|
||||
entry.2 = meta.modified;
|
||||
if let Some(meta) = storage.stat(key).await {
|
||||
if meta.modified > entry.2 {
|
||||
entry.2 = meta.modified;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user