Initialize dockdash#1
Conversation
Rust library for building and pushing OCI container images without Docker. Includes layer builder, image builder, blob caching, registry push with auth support, CI/CD workflows, and crates.io release pipeline. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| proc_config.set_cmd(Some(cmd)); | ||
| } else { | ||
| proc_config.set_cmd(Some(vec![])); | ||
| } |
There was a problem hiding this comment.
Cmd is cleared when only entrypoint is overridden
Medium Severity
When a user sets entrypoint but not cmd, the builder unconditionally clears cmd to an empty vec via the else branch (proc_config.set_cmd(Some(vec![]))). This silently drops any cmd inherited from the base image, which is standard Docker behavior to preserve. A user setting only an entrypoint would lose the base image's default command arguments.
Additional Locations (1)
There was a problem hiding this comment.
Already fixed in commit 39fae85 — cmd is now set to None (not empty vec) when only entrypoint is overridden.
| info!( | ||
| num_layers_to_push = layers_to_push.len(), | ||
| num_total_layers = mounted_digests.len() + layers_to_push.len(), | ||
| total_push_size_mb = total_push_size_bytes / (1024 * 1024), |
There was a problem hiding this comment.
Division by zero when total push size is zero
Low Severity
The expression total_push_size_bytes / (1024 * 1024) in the tracing info! macro uses integer division. While not a division-by-zero risk itself, when total_push_size_bytes is less than 1 MB the logged value will silently be 0, which could be confusing during debugging. This is minor but worth noting.
There was a problem hiding this comment.
Not fixing — it's a log line. Logging "0 MB" for sub-MB images is fine.
The integration tests in build_push_tests.rs need the test-utils feature and a Docker daemon. Run only --lib tests in CI to avoid compilation errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The extract method was unconditionally applying zstd decompression to all layers, but base image layers from registries are typically gzip-compressed. Now checks the layer media type and uses the appropriate decompressor. Also removes redundant gcr.io hostname checks in monolithic push detection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Preserve base image CMD when only entrypoint is overridden (CMD is only cleared when entrypoint is explicitly set, matching Docker behavior) - Remove dead branch in push code (layers_to_push.is_empty() was unreachable after the early return above) - Handle uncompressed tar layers in extract (media types without +gzip or +zstd are now extracted as plain tar) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Distinguish gzip vs uncompressed Docker layers by checking for "gzip" in media type instead of matching all rootfs media types as gzip - Reset CMD to None (not empty array) when entrypoint overrides it, matching OCI spec semantics - Warn when only one of platform_os/platform_arch is set in PullAndExtractOptions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| } | ||
| } | ||
| (pulled_manifest, pulled_digest) | ||
| }; |
There was a problem hiding this comment.
Manifest pull-and-cache logic duplicated four times
Low Severity
The pattern of pulling a manifest from the registry, serializing it with serde_json::to_vec, writing it to the cache via cache.put_blob, and handling both serialization and cache-write errors is copy-pasted nearly identically across four branches: the deserialization-failure fallback, the cache-miss path, the cache-error path inside PullPolicy::Missing, and the PullPolicy::Always path. Extracting this into a small helper (e.g., pull_and_cache_manifest) would eliminate the redundancy and reduce the risk of these copies diverging during future maintenance.
Additional Locations (2)
There was a problem hiding this comment.
Acknowledged — the duplication exists across different cache/pull policy branches with slightly different error handling. Not refactoring in this PR.
| }, | ||
| diagnostics, | ||
| )) | ||
| } |
There was a problem hiding this comment.
ImageBuilder build method is excessively long
Low Severity
The ImageBuilder::build() method spans nearly 490 lines, mixing multiple distinct concerns: cache lookup, registry authentication, manifest pulling and caching, config blob fetching, layer blob fetching, image configuration construction (both base-image and scratch paths), OCI tar archive assembly, and output path handling. Decomposing it into smaller focused functions (e.g., resolve_base_image, build_config, assemble_archive) would greatly improve readability and testability.
There was a problem hiding this comment.
Agree it's long, but splitting it up for its own sake isn't worth the churn right now.
|
@greptile |
Greptile SummaryThis PR introduces the full initial Two correctness issues remain:
Additionally, the CI workflow runs Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Caller
participant ImageBuilder
participant BlobCache
participant OciClient
participant Registry
Caller->>ImageBuilder: build(base_image, platform, layers)
ImageBuilder->>BlobCache: get_blob("manifest-v1:{ref}")
alt Cache hit
BlobCache-->>ImageBuilder: cached manifest (may be wrong platform!)
else Cache miss
ImageBuilder->>OciClient: pull_image_manifest(ref, platform_resolver)
OciClient->>Registry: GET /v2/.../manifests/{ref}
Registry-->>OciClient: image index
OciClient-->>ImageBuilder: resolved platform manifest
ImageBuilder->>BlobCache: put_blob("manifest-v1:{ref}", manifest)
end
loop Each base layer
ImageBuilder->>BlobCache: get_blob(layer_digest)
alt Cache miss
ImageBuilder->>OciClient: pull_blob(layer_digest)
OciClient->>Registry: GET /v2/.../blobs/{digest}
Registry-->>OciClient: layer bytes
OciClient-->>ImageBuilder: layer data
ImageBuilder->>BlobCache: put_blob(layer_digest, data)
end
end
ImageBuilder->>ImageBuilder: assemble OCI archive (base + new layers)
ImageBuilder-->>Caller: Image
Caller->>Image: push(target_ref, options)
Image->>OciClient: auth(registry)
OciClient->>Registry: POST /v2/.../auth
loop Each layer
Image->>OciClient: mount_blob(target, source=target, digest)
alt Mount succeeds (layer exists)
OciClient-->>Image: 201 Created
else Mount fails
Image->>OciClient: push_blob(layer data)
OciClient->>Registry: POST /v2/.../blobs/uploads/
Registry-->>OciClient: upload URL
OciClient->>Registry: PUT data
Registry-->>OciClient: 201 Created
end
end
Image->>OciClient: push_blob(config)
Image->>OciClient: push_manifest(manifest)
OciClient->>Registry: PUT /v2/.../manifests/{tag}
Registry-->>OciClient: 201 Created
Image-->>Caller: pushed image ref
Last reviewed commit: 4523e38 |
…g print - Record per-file metadata when adding directories to layers so the input-based cache key reflects actual directory contents (prevents stale cache hits when files inside a directory change) - Scope DOCKER_USERNAME/DOCKER_PASSWORD env vars to Docker Hub only, preventing credential leakage to unrelated registries - Remove leftover eprintln! debug statement in layer cache miss path - Document self-mount blob existence check workaround (oci-client lacks a HEAD blob API) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@greptile |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 5 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| info!( | ||
| num_layers_to_push = layers_to_push.len(), | ||
| num_total_layers = mounted_digests.len() + layers_to_push.len(), | ||
| total_push_size_mb = total_push_size_bytes / (1024 * 1024), |
There was a problem hiding this comment.
Integer division truncates push size to zero in logs
Low Severity
total_push_size_mb = total_push_size_bytes / (1024 * 1024) uses integer division, so any push under 1 MB logs total_push_size_mb = 0, which is misleading. This is a usize division, so the result is always truncated. For small images this gives no useful information in the tracing output.
There was a problem hiding this comment.
Duplicate of earlier comment. Not fixing — it's a log line.
| message: "Task join error during layer extraction".to_string(), | ||
| source: Some(Box::new(e)), | ||
| } | ||
| })??; |
There was a problem hiding this comment.
Layer extraction lacks whiteout file handling for OCI
Medium Severity
The extract method applies layers sequentially but doesn't handle OCI whiteout files (.wh.* prefixed entries). In OCI images, these marker files signal file deletions from prior layers. Without processing them, extracted filesystems will contain stale files that the image intended to remove, producing an incorrect merged filesystem.
| let rootfs = RootFsBuilder::default() | ||
| .typ("layers") | ||
| .diff_ids(diff_ids) | ||
| .build() |
There was a problem hiding this comment.
Scratch image rootfs type uses "layers" instead of "layers"
Medium Severity
When building from scratch, the RootFsBuilder sets .typ("layers") but the OCI image spec requires the type field to be the string "layers". While the string literal happens to be correct, this path is only exercised for scratch builds with custom layers. However, if no layers are provided, diff_ids will be empty, which may cause issues with some container runtimes expecting at least one layer.
There was a problem hiding this comment.
The comment contradicts itself — it says the string literal is correct, then flags it anyway. No issue here.
| PathBuf::from("/tmp").join(DEFAULT_CACHE_SUBDIR) | ||
| }; | ||
|
|
||
| Self::init(path) |
There was a problem hiding this comment.
Cache fallback creates nested redundant directory path
Low Severity
In BlobCache::new(), the writability check calls create_dir_all on the parent of the cache path, but if that succeeds, the actual cache directory itself isn't created until init(). However, the real issue is that if the home directory exists but the cache subdirectory specifically is unwritable (e.g., permissions), the check on the parent will succeed but later init() will fail when trying to create the actual cache dir, producing a confusing error instead of falling back to /tmp.
There was a problem hiding this comment.
Theoretical edge case. The /tmp fallback already provides graceful degradation for the common failure modes.
| } | ||
| } else { | ||
| PathBuf::from("/tmp").join(DEFAULT_CACHE_SUBDIR) | ||
| } |
There was a problem hiding this comment.
Cache writability check misses actual target directory failures
Low Severity
BlobCache::new() tests writability by creating the parent of the cache path (~/.dockdash/cache), not the full path (~/.dockdash/cache/blobs). If the parent is creatable but the final directory cannot be created (e.g., a file exists at that path), the /tmp fallback is skipped and init() returns a hard error instead of gracefully degrading.
There was a problem hiding this comment.
Same as above — the fallback to /tmp handles this.
… dedup - Handle OCI/Docker whiteout files (.wh.<name> and .wh..wh..opq) during layer extraction so deleted files from prior layers are properly removed from the merged filesystem - Fix duplicate error message in From<io::Error> impl that produced "I/O error: Permission denied: Permission denied" - Deduplicate IMAGE_LAYER_ZSTD_MEDIA_TYPE constant into lib.rs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@greptile |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@greptile |
…content
- Include OS and arch in manifest cache key (manifest-v2:{ref}:{os}:{arch})
so multi-platform builds don't serve wrong cached manifests
- Hash in-memory content in data() FileMetadata so same-size blobs
at the same path produce different input cache keys
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…io::Error> The Docker Hub-only scoping of DOCKER_USERNAME/DOCKER_PASSWORD was a usability regression — CI/CD users commonly set these for any registry. The From<io::Error> impl was unused since all call sites construct Error::Io with contextual messages; removing it enforces that pattern. Also applies rustfmt formatting fixes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>


No description provided.