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/
3024package digest
3125
3226import (
@@ -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.
0 commit comments