Skip to content

Commit 4f532bc

Browse files
committed
test
1 parent fda8665 commit 4f532bc

18 files changed

+224
-127
lines changed

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Ensure consistent line endings for files used in diff tests
2+
*.yaml text eol=lf

.github/github.bazelrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@ common --keep_going
1818

1919
# Github MacOS runners do not support docker
2020
test:macos --test_tag_filters=-"requires-docker"
21+
22+
# Linux runners uses legacy image storage with docker
23+
test:linux --@rules_docker_compose//docker_compose/settings:toolchain_default_digest_mode=docker-legacy

docker_compose/private/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ bzl_library(
44
name = "bzl_lib",
55
srcs = glob(["*.bzl"]),
66
visibility = ["//docker_compose:__pkg__"],
7+
deps = [
8+
"@bazel_skylib//rules:common_settings",
9+
],
710
)

docker_compose/private/digest/digest.go

Lines changed: 82 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,26 @@
1-
// Package digest provides utilities for computing container image digests.
1+
// Package digest provides utilities for reading container image digests.
22
//
3-
// This package handles the conversion between OCI (Open Container Initiative) image
4-
// manifests and Docker V2 Schema 2 manifests, and computes the appropriate digests
5-
// for each format.
3+
// This package handles reading digests from OCI (Open Container Initiative) image
4+
// layouts and manifest files, supporting both OCI manifest digests and Docker
5+
// image IDs (manifest digests after OCI-to-Docker conversion).
66
//
77
// # OCI Image Manifest Specification
88
//
99
// The OCI Image Manifest specification defines the format for OCI container images:
1010
// https://github.com/opencontainers/image-spec/blob/main/manifest.md
1111
//
12-
// # Docker Registry HTTP API V2 - Manifest Schema 2
12+
// # OCI Image Layout
1313
//
14-
// The Docker V2 Schema 2 manifest format is documented here:
15-
// https://docs.docker.com/registry/spec/manifest-v2-2/
14+
// The OCI Image Layout specifies how OCI image content is stored on disk:
15+
// https://github.com/opencontainers/image-spec/blob/main/image-layout.md
1616
//
17-
// # Media Type Conversion
17+
// # Docker Image ID
1818
//
19-
// When converting from OCI to Docker format, the following media types are mapped:
19+
// When Docker loads an OCI image with `docker load`, it converts the OCI manifest
20+
// to Docker V2 Schema 2 format and uses the SHA256 digest of the converted manifest
21+
// as the image ID. This is visible in `docker images` output and `docker inspect`.
2022
//
21-
// OCI Media Type -> Docker Media Type
22-
// application/vnd.oci.image.manifest.v1+json -> application/vnd.docker.distribution.manifest.v2+json
23-
// application/vnd.oci.image.config.v1+json -> application/vnd.docker.container.image.v1+json
24-
// application/vnd.oci.image.layer.v1.tar+gzip -> application/vnd.docker.image.rootfs.diff.tar.gzip
25-
// application/vnd.oci.image.layer.v1.tar -> application/vnd.docker.image.rootfs.diff.tar
26-
// application/vnd.oci.image.layer.v1.tar+zstd -> application/vnd.docker.image.rootfs.diff.tar+zstd
27-
//
28-
// The digest computed from the Docker V2 manifest is what Docker uses as the image ID
29-
// after loading an OCI image with `docker load`.
23+
// See: https://docs.docker.com/registry/spec/manifest-v2-2/
3024
package digest
3125

3226
import (
@@ -49,30 +43,26 @@ var ociToDockerMediaTypes = map[string]string{
4943
"application/vnd.oci.image.layer.v1.tar+zstd": "application/vnd.docker.image.rootfs.diff.tar+zstd",
5044
}
5145

52-
// ConvertOCIToDockerMediaType converts an OCI media type to its Docker V2 equivalent.
53-
// If the media type is not recognized as an OCI type, it is returned unchanged.
54-
//
55-
// See OCI media types: https://github.com/opencontainers/image-spec/blob/main/media-types.md
56-
// See Docker media types: https://docs.docker.com/registry/spec/manifest-v2-2/#media-types
57-
func ConvertOCIToDockerMediaType(ociMediaType string) string {
46+
// convertOCIToDockerMediaType converts an OCI media type to its Docker V2 equivalent.
47+
func convertOCIToDockerMediaType(ociMediaType string) string {
5848
if dockerType, ok := ociToDockerMediaTypes[ociMediaType]; ok {
5949
return dockerType
6050
}
6151
return ociMediaType
6252
}
6353

64-
// DockerManifest represents a Docker V2 Schema 2 manifest.
54+
// dockerManifest represents a Docker V2 Schema 2 manifest.
55+
// Field order matches Docker's serialization for digest computation.
6556
// See: https://docs.docker.com/registry/spec/manifest-v2-2/#image-manifest-field-descriptions
66-
type DockerManifest struct {
57+
type dockerManifest struct {
6758
SchemaVersion int `json:"schemaVersion"`
6859
MediaType string `json:"mediaType"`
69-
Config DockerManifestDescriptor `json:"config"`
70-
Layers []DockerManifestDescriptor `json:"layers"`
60+
Config dockerManifestDescriptor `json:"config"`
61+
Layers []dockerManifestDescriptor `json:"layers"`
7162
}
7263

73-
// DockerManifestDescriptor represents a content descriptor in a Docker V2 manifest.
74-
// See: https://docs.docker.com/registry/spec/manifest-v2-2/#image-manifest-field-descriptions
75-
type DockerManifestDescriptor struct {
64+
// dockerManifestDescriptor represents a content descriptor in a Docker V2 manifest.
65+
type dockerManifestDescriptor struct {
7666
MediaType string `json:"mediaType"`
7767
Digest string `json:"digest"`
7868
Size int64 `json:"size"`
@@ -104,47 +94,6 @@ type OCIIndex struct {
10494
} `json:"manifests"`
10595
}
10696

107-
// ConvertOCIManifestToDocker converts an OCI manifest to a Docker V2 Schema 2 manifest.
108-
// The conversion involves changing the media types from OCI format to Docker format.
109-
//
110-
// OCI Manifest spec: https://github.com/opencontainers/image-spec/blob/main/manifest.md
111-
// Docker V2 Schema 2 spec: https://docs.docker.com/registry/spec/manifest-v2-2/
112-
func ConvertOCIManifestToDocker(ociManifest *OCIManifest) *DockerManifest {
113-
dockerManifest := &DockerManifest{
114-
SchemaVersion: 2,
115-
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
116-
Config: DockerManifestDescriptor{
117-
MediaType: ConvertOCIToDockerMediaType(ociManifest.Config.MediaType),
118-
Digest: ociManifest.Config.Digest,
119-
Size: ociManifest.Config.Size,
120-
},
121-
Layers: make([]DockerManifestDescriptor, len(ociManifest.Layers)),
122-
}
123-
124-
for i, layer := range ociManifest.Layers {
125-
dockerManifest.Layers[i] = DockerManifestDescriptor{
126-
MediaType: ConvertOCIToDockerMediaType(layer.MediaType),
127-
Digest: layer.Digest,
128-
Size: layer.Size,
129-
}
130-
}
131-
132-
return dockerManifest
133-
}
134-
135-
// ComputeDockerManifestDigest computes the SHA256 digest of a Docker V2 manifest.
136-
// This is the digest that Docker uses as the image ID.
137-
func ComputeDockerManifestDigest(manifest *DockerManifest) (string, error) {
138-
// Serialize to JSON without indentation to match Docker's format
139-
manifestJSON, err := json.Marshal(manifest)
140-
if err != nil {
141-
return "", fmt.Errorf("failed to marshal Docker manifest: %v", err)
142-
}
143-
144-
hash := sha256.Sum256(manifestJSON)
145-
return fmt.Sprintf("sha256:%x", hash), nil
146-
}
147-
14897
// ReadOCIManifestDigest reads the manifest digest from an OCI layout directory.
14998
// This reads the first manifest digest from index.json.
15099
//
@@ -173,19 +122,50 @@ func ReadOCIManifestDigest(ociLayoutDir string) (string, error) {
173122
return digest, nil
174123
}
175124

176-
// ReadDockerManifestDigestFromOCILayout reads an OCI layout, converts the manifest
125+
// ReadConfigDigestFromOCILayout reads the config digest from an OCI layout.
126+
// This is the digest that Docker Engine (without containerd) uses as the image ID.
127+
//
128+
// OCI Image Layout spec: https://github.com/opencontainers/image-spec/blob/main/image-layout.md
129+
func ReadConfigDigestFromOCILayout(ociLayoutDir string) (string, error) {
130+
// Read the OCI index to get the manifest digest
131+
ociManifestDigest, err := ReadOCIManifestDigest(ociLayoutDir)
132+
if err != nil {
133+
return "", err
134+
}
135+
136+
// Read the OCI manifest blob
137+
manifestPath := filepath.Join(ociLayoutDir, "blobs", "sha256", ociManifestDigest[7:])
138+
manifestData, err := os.ReadFile(manifestPath)
139+
if err != nil {
140+
return "", fmt.Errorf("failed to read manifest blob %s: %v", manifestPath, err)
141+
}
142+
143+
// Parse OCI manifest to get the config digest
144+
var ociManifest OCIManifest
145+
if err := json.Unmarshal(manifestData, &ociManifest); err != nil {
146+
return "", fmt.Errorf("failed to parse OCI manifest: %v", err)
147+
}
148+
149+
if ociManifest.Config.Digest == "" {
150+
return "", fmt.Errorf("no config digest found in OCI manifest")
151+
}
152+
153+
return ociManifest.Config.Digest, nil
154+
}
155+
156+
// ReadDockerContainerdImageIDFromOCILayout reads an OCI layout, converts the manifest
177157
// to Docker V2 format, and returns the Docker manifest digest.
178158
//
179-
// This is the digest that Docker uses as the image ID after loading an OCI image
180-
// with `docker load`. The conversion involves:
159+
// This is the digest that Docker (with containerd storage) uses as the image ID
160+
// after `docker load`. The conversion involves:
181161
// 1. Reading the OCI index.json to find the manifest digest
182162
// 2. Reading the OCI manifest blob
183163
// 3. Converting media types from OCI to Docker format
184164
// 4. Computing the SHA256 digest of the resulting Docker manifest
185165
//
186166
// OCI Image Layout spec: https://github.com/opencontainers/image-spec/blob/main/image-layout.md
187167
// Docker V2 Schema 2 spec: https://docs.docker.com/registry/spec/manifest-v2-2/
188-
func ReadDockerManifestDigestFromOCILayout(ociLayoutDir string) (string, error) {
168+
func ReadDockerContainerdImageIDFromOCILayout(ociLayoutDir string) (string, error) {
189169
// Read the OCI index to get the manifest digest
190170
ociManifestDigest, err := ReadOCIManifestDigest(ociLayoutDir)
191171
if err != nil {
@@ -206,10 +186,34 @@ func ReadDockerManifestDigestFromOCILayout(ociLayoutDir string) (string, error)
206186
}
207187

208188
// Convert to Docker V2 manifest
209-
dockerManifest := ConvertOCIManifestToDocker(&ociManifest)
189+
dockerMfst := dockerManifest{
190+
SchemaVersion: 2,
191+
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
192+
Config: dockerManifestDescriptor{
193+
MediaType: convertOCIToDockerMediaType(ociManifest.Config.MediaType),
194+
Digest: ociManifest.Config.Digest,
195+
Size: ociManifest.Config.Size,
196+
},
197+
Layers: make([]dockerManifestDescriptor, len(ociManifest.Layers)),
198+
}
199+
200+
for i, layer := range ociManifest.Layers {
201+
dockerMfst.Layers[i] = dockerManifestDescriptor{
202+
MediaType: convertOCIToDockerMediaType(layer.MediaType),
203+
Digest: layer.Digest,
204+
Size: layer.Size,
205+
}
206+
}
207+
208+
// Serialize to compact JSON (no whitespace) to match Docker's format
209+
manifestJSON, err := json.Marshal(dockerMfst)
210+
if err != nil {
211+
return "", fmt.Errorf("failed to marshal Docker manifest: %v", err)
212+
}
210213

211214
// Compute and return the Docker manifest digest
212-
return ComputeDockerManifestDigest(dockerManifest)
215+
hash := sha256.Sum256(manifestJSON)
216+
return fmt.Sprintf("sha256:%x", hash), nil
213217
}
214218

215219
// ReadConfigDigestFromManifestFile reads the config digest from a manifest JSON file.

docker_compose/private/merger/merger.go

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ func debugLog(format string, args ...interface{}) {
2020
}
2121
}
2222

23+
// normalizeLineEndings converts CRLF to LF for consistent output across platforms
24+
func normalizeLineEndings(data []byte) []byte {
25+
return []byte(strings.ReplaceAll(string(data), "\r\n", "\n"))
26+
}
27+
2328
type Args struct {
2429
DockerCompose string
2530
Output string
@@ -73,8 +78,8 @@ func parseArgs() (*Args, error) {
7378
if len(files) == 0 {
7479
return nil, fmt.Errorf("at least one -file flag is required")
7580
}
76-
if args.DigestMode != "oci" && args.DigestMode != "docker" {
77-
return nil, fmt.Errorf("invalid -digest-mode: %s (must be 'oci' or 'docker')", args.DigestMode)
81+
if args.DigestMode != "oci" && args.DigestMode != "docker-legacy" && args.DigestMode != "docker-containerd" {
82+
return nil, fmt.Errorf("invalid -digest-mode: %s (must be 'oci', 'docker-legacy', or 'docker-containerd')", args.DigestMode)
7883
}
7984

8085
args.Files = files
@@ -199,7 +204,10 @@ func readDigestFromManifestFile(manifestFile string) (string, error) {
199204
}
200205

201206
// loadImageManifestsWithDigests loads image manifests and extracts digests
202-
// digestMode can be "oci" (uses manifest digest) or "docker" (uses Docker V2 manifest digest for docker load compatibility)
207+
// digestMode can be:
208+
// - "oci": uses OCI manifest digest (for OCI-compatible tools)
209+
// - "docker-legacy": uses config digest (for Docker without containerd storage)
210+
// - "docker-containerd": uses Docker V2 manifest digest (for Docker with containerd storage)
203211
func loadImageManifestsWithDigests(manifestPaths []string, digestMode string) (map[string]string, error) {
204212
// Map of repository to digest
205213
imageDigests := make(map[string]string)
@@ -237,22 +245,27 @@ func loadImageManifestsWithDigests(manifestPaths []string, digestMode string) (m
237245

238246
var dgst string
239247
if manifest.OCILayoutDir != "" {
240-
if digestMode == "docker" {
241-
// Compute Docker V2 manifest digest for docker load compatibility
242-
dgst, err = digest.ReadDockerManifestDigestFromOCILayout(manifest.OCILayoutDir)
243-
} else {
244-
// Use OCI manifest digest for OCI mode (default)
248+
switch digestMode {
249+
case "docker-legacy":
250+
// Config digest for Docker without containerd storage
251+
dgst, err = digest.ReadConfigDigestFromOCILayout(manifest.OCILayoutDir)
252+
case "docker-containerd":
253+
// Docker V2 manifest digest for Docker with containerd storage
254+
dgst, err = digest.ReadDockerContainerdImageIDFromOCILayout(manifest.OCILayoutDir)
255+
default: // "oci" or unspecified
256+
// OCI manifest digest
245257
dgst, err = digest.ReadOCIManifestDigest(manifest.OCILayoutDir)
246258
}
247259
if err != nil {
248260
return nil, err
249261
}
250262
} else if manifest.ManifestFile != "" {
251-
if digestMode == "docker" {
263+
switch digestMode {
264+
case "docker-legacy", "docker-containerd":
252265
// Use config digest for docker load compatibility (rules_img)
253266
dgst, err = digest.ReadConfigDigestFromManifestFile(manifest.ManifestFile)
254-
} else {
255-
// Use manifest digest for OCI mode (default)
267+
default: // "oci" or unspecified
268+
// Use manifest digest for OCI mode
256269
dgst, err = readDigestFromManifestFile(manifest.ManifestFile)
257270
}
258271
if err != nil {
@@ -273,7 +286,7 @@ func loadImageManifestsWithDigests(manifestPaths []string, digestMode string) (m
273286
}
274287

275288
// generateLockFileFromManifests generates a lock file by mapping images from docker-compose config to digests from image manifests
276-
// digestMode can be "oci" (uses manifest digest) or "docker" (uses Docker V2 manifest digest for docker load compatibility)
289+
// digestMode can be "oci", "docker", or "docker-desktop"
277290
func generateLockFileFromManifests(dockerCompose string, cmdArgs []string, manifestPaths []string, lockPath string, digestMode string) error {
278291
// Get list of images from docker-compose config --images
279292
imagesArgs := append(cmdArgs, "config", "--images")
@@ -332,8 +345,17 @@ func generateLockFileFromManifests(dockerCompose string, cmdArgs []string, manif
332345
}
333346
}
334347

348+
// Create lock file structure with mode and digests
349+
lockFile := struct {
350+
Mode string `json:"mode"`
351+
Digests map[string]string `json:"digests"`
352+
}{
353+
Mode: digestMode,
354+
Digests: lockData,
355+
}
356+
335357
// Write lock file as JSON
336-
lockJSON, err := json.MarshalIndent(lockData, "", " ")
358+
lockJSON, err := json.MarshalIndent(lockFile, "", " ")
337359
if err != nil {
338360
return fmt.Errorf("failed to marshal lock data: %v", err)
339361
}
@@ -401,9 +423,16 @@ func main() {
401423
// Generate lock file if requested
402424
if args.OutputLock != "" {
403425
if len(args.ImageManifests) == 0 {
404-
// No images - create an empty lock file
426+
// No images - create an empty lock file with mode
405427
debugLog("No image manifests provided, creating empty lock file")
406-
emptyLock := []byte("{}\n")
428+
emptyLockData := struct {
429+
Mode string `json:"mode"`
430+
Digests map[string]string `json:"digests"`
431+
}{
432+
Mode: args.DigestMode,
433+
Digests: map[string]string{},
434+
}
435+
emptyLock, _ := json.MarshalIndent(emptyLockData, "", " ")
407436
if err := os.WriteFile(args.OutputLock, emptyLock, 0644); err != nil {
408437
fmt.Fprintf(os.Stderr, "Error writing empty lock file: %v\n", err)
409438
os.Exit(1)
@@ -482,7 +511,8 @@ func main() {
482511
}
483512
}
484513

485-
finalOutput := output
514+
// Normalize line endings for consistent output across platforms (CRLF -> LF)
515+
finalOutput := normalizeLineEndings(output)
486516
debugLog("Final YAML size: %d bytes", len(finalOutput))
487517

488518
// Ensure output directory exists

0 commit comments

Comments
 (0)