mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-12 12:40: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:
@@ -8,7 +8,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.31"
|
version = "0.2.32"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["DevITWay <devitway@gmail.com>"]
|
authors = ["DevITWay <devitway@gmail.com>"]
|
||||||
|
|||||||
@@ -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()
|
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()
|
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/") {
|
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();
|
let parts: Vec<_> = rest.split('/').collect();
|
||||||
if parts.len() >= 3 && parts[1] == "manifests" && key.ends_with(".json") {
|
let manifest_pos = parts.iter().position(|&p| p == "manifests");
|
||||||
let name = parts[0].to_string();
|
if let Some(pos) = manifest_pos {
|
||||||
let entry = repos.entry(name).or_insert((0, 0, 0));
|
if pos >= 1 && key.ends_with(".json") {
|
||||||
entry.0 += 1;
|
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(data) = storage.get(key).await {
|
||||||
if let Ok(m) = serde_json::from_slice::<serde_json::Value>(&data) {
|
if let Ok(m) = serde_json::from_slice::<serde_json::Value>(&data) {
|
||||||
let cfg = m
|
let cfg = m
|
||||||
.get("config")
|
.get("config")
|
||||||
.and_then(|c| c.get("size"))
|
.and_then(|c| c.get("size"))
|
||||||
.and_then(|s| s.as_u64())
|
.and_then(|s| s.as_u64())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let layers: u64 = m
|
let layers: u64 = m
|
||||||
.get("layers")
|
.get("layers")
|
||||||
.and_then(|l| l.as_array())
|
.and_then(|l| l.as_array())
|
||||||
.map(|arr| {
|
.map(|arr| {
|
||||||
arr.iter()
|
arr.iter()
|
||||||
.filter_map(|l| l.get("size").and_then(|s| s.as_u64()))
|
.filter_map(|l| l.get("size").and_then(|s| s.as_u64()))
|
||||||
.sum()
|
.sum()
|
||||||
})
|
})
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
entry.1 += cfg + layers;
|
entry.1 += cfg + layers;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(meta) = storage.stat(key).await {
|
if let Some(meta) = storage.stat(key).await {
|
||||||
if meta.modified > entry.2 {
|
if meta.modified > entry.2 {
|
||||||
entry.2 = meta.modified;
|
entry.2 = meta.modified;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user