Skip to content

Commit 1e55ace

Browse files
committed
Add oci index and layout files to exported tars
This makes the output of `docker save` fully OCI compliant. When using the containerd image store, this code is not used. That exporter will just use containerd's export method and should give us the output we want for multi-arch images. Signed-off-by: Brian Goff <[email protected]>
1 parent ddd67b2 commit 1e55ace

13 files changed

Lines changed: 720 additions & 160 deletions

File tree

image/image.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/docker/docker/dockerversion"
1515
"github.com/docker/docker/layer"
1616
"github.com/opencontainers/go-digest"
17+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
1718
)
1819

1920
// ID is the content-addressable ID of an image.
@@ -174,6 +175,17 @@ func (img *Image) OperatingSystem() string {
174175
return os
175176
}
176177

178+
// Platform generates an OCI platform from the image
179+
func (img *Image) Platform() ocispec.Platform {
180+
return ocispec.Platform{
181+
Architecture: img.Architecture,
182+
OS: img.OS,
183+
OSVersion: img.OSVersion,
184+
OSFeatures: img.OSFeatures,
185+
Variant: img.Variant,
186+
}
187+
}
188+
177189
// MarshalJSON serializes the image to JSON. It sorts the top-level keys so
178190
// that JSON that's been manipulated by a push/pull cycle with a legacy
179191
// registry won't end up with a different key order.

image/tarexport/save.go

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"path/filepath"
1010
"time"
1111

12+
"github.com/containerd/containerd/images"
1213
"github.com/docker/distribution"
1314
"github.com/docker/distribution/reference"
1415
"github.com/docker/docker/image"
@@ -18,6 +19,8 @@ import (
1819
"github.com/docker/docker/pkg/system"
1920
"github.com/moby/sys/sequential"
2021
"github.com/opencontainers/go-digest"
22+
"github.com/opencontainers/image-spec/specs-go"
23+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
2124
"github.com/pkg/errors"
2225
)
2326

@@ -190,14 +193,67 @@ func (s *saveSession) save(outStream io.Writer) error {
190193
var manifest []manifestItem
191194
var parentLinks []parentLink
192195

196+
var manifestDescriptors []ocispec.Descriptor
197+
193198
for id, imageDescr := range s.images {
194199
foreignSrcs, err := s.saveImage(id)
195200
if err != nil {
196201
return err
197202
}
198203

199-
var repoTags []string
200-
var layers []string
204+
var (
205+
repoTags []string
206+
layers []string
207+
foreign = make([]ocispec.Descriptor, 0, len(foreignSrcs))
208+
)
209+
210+
for _, desc := range foreignSrcs {
211+
foreign = append(foreign, ocispec.Descriptor{
212+
MediaType: desc.MediaType,
213+
Digest: desc.Digest,
214+
Size: desc.Size,
215+
URLs: desc.URLs,
216+
Annotations: desc.Annotations,
217+
Platform: desc.Platform,
218+
})
219+
}
220+
221+
imgPlat := imageDescr.image.Platform()
222+
223+
m := ocispec.Manifest{
224+
Versioned: specs.Versioned{
225+
SchemaVersion: 2,
226+
},
227+
MediaType: ocispec.MediaTypeImageManifest,
228+
Config: ocispec.Descriptor{
229+
MediaType: ocispec.MediaTypeImageConfig,
230+
Digest: digest.Digest(imageDescr.image.ID()),
231+
Size: int64(len(imageDescr.image.RawJSON())),
232+
Platform: &imgPlat,
233+
},
234+
Layers: foreign,
235+
}
236+
237+
data, err := json.Marshal(m)
238+
if err != nil {
239+
return errors.Wrap(err, "error marshaling manifest")
240+
}
241+
dgst := digest.FromBytes(data)
242+
243+
mFile := filepath.Join(s.outDir, "blobs", dgst.Algorithm().String(), dgst.Encoded())
244+
if err := os.MkdirAll(filepath.Dir(mFile), 0o755); err != nil {
245+
return errors.Wrap(err, "error creating blob directory")
246+
}
247+
if err := system.Chtimes(filepath.Dir(mFile), time.Unix(0, 0), time.Unix(0, 0)); err != nil {
248+
return errors.Wrap(err, "error setting blob directory timestamps")
249+
}
250+
if err := os.WriteFile(mFile, data, 0o644); err != nil {
251+
return errors.Wrap(err, "error writing oci manifest file")
252+
}
253+
if err := system.Chtimes(mFile, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
254+
return errors.Wrap(err, "error setting blob directory timestamps")
255+
}
256+
size := int64(len(data))
201257

202258
for _, ref := range imageDescr.refs {
203259
familiarName := reference.FamiliarName(ref)
@@ -206,6 +262,17 @@ func (s *saveSession) save(outStream io.Writer) error {
206262
}
207263
reposLegacy[familiarName][ref.Tag()] = imageDescr.layers[len(imageDescr.layers)-1].Encoded()
208264
repoTags = append(repoTags, reference.FamiliarString(ref))
265+
266+
manifestDescriptors = append(manifestDescriptors, ocispec.Descriptor{
267+
MediaType: ocispec.MediaTypeImageManifest,
268+
Digest: dgst,
269+
Size: size,
270+
Platform: m.Config.Platform,
271+
Annotations: map[string]string{
272+
images.AnnotationImageName: ref.String(),
273+
ocispec.AnnotationRefName: ref.Tag(),
274+
},
275+
})
209276
}
210277

211278
for _, l := range imageDescr.layers {
@@ -251,8 +318,8 @@ func (s *saveSession) save(outStream io.Writer) error {
251318
}
252319
}
253320

254-
manifestFileName := filepath.Join(tempDir, manifestFileName)
255-
f, err := os.OpenFile(manifestFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
321+
manifestPath := filepath.Join(tempDir, manifestFileName)
322+
f, err := os.OpenFile(manifestPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
256323
if err != nil {
257324
return err
258325
}
@@ -264,10 +331,34 @@ func (s *saveSession) save(outStream io.Writer) error {
264331

265332
f.Close()
266333

267-
if err := system.Chtimes(manifestFileName, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
334+
if err := system.Chtimes(manifestPath, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
268335
return err
269336
}
270337

338+
layoutPath := filepath.Join(tempDir, ociLayoutFilename)
339+
if err := os.WriteFile(layoutPath, []byte(ociLayoutContent), 0o644); err != nil {
340+
return errors.Wrap(err, "error writing oci layout file")
341+
}
342+
if err := system.Chtimes(layoutPath, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
343+
return errors.Wrap(err, "error setting oci layout file timestamps")
344+
}
345+
346+
data, err := json.Marshal(ocispec.Index{
347+
Versioned: specs.Versioned{
348+
SchemaVersion: 2,
349+
},
350+
MediaType: ocispec.MediaTypeImageIndex,
351+
Manifests: manifestDescriptors,
352+
})
353+
if err != nil {
354+
return errors.Wrap(err, "error marshaling oci index")
355+
}
356+
357+
idxFile := filepath.Join(s.outDir, ociIndexFileName)
358+
if err := os.WriteFile(idxFile, data, 0o644); err != nil {
359+
return errors.Wrap(err, "error writing oci index file")
360+
}
361+
271362
fs, err := archive.Tar(tempDir, archive.Uncompressed)
272363
if err != nil {
273364
return err
@@ -365,7 +456,7 @@ func (s *saveSession) saveLayer(id layer.ChainID, legacyImg image.V1Image, creat
365456
cfgDgst := digest.FromBytes(imageConfig)
366457
configPath := filepath.Join(outDir, cfgDgst.Algorithm().String(), cfgDgst.Encoded())
367458
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
368-
return distribution.Descriptor{}, fmt.Errorf("could not create layer dir parent: %w", err)
459+
return distribution.Descriptor{}, errors.Wrap(err, "could not create layer dir parent")
369460
}
370461

371462
if err := os.WriteFile(configPath, imageConfig, 0644); err != nil {
@@ -390,11 +481,11 @@ func (s *saveSession) saveLayer(id layer.ChainID, legacyImg image.V1Image, creat
390481
// We use sequential file access to avoid depleting the standby list on
391482
// Windows. On Linux, this equates to a regular os.Create.
392483
if err := os.MkdirAll(filepath.Dir(layerPath), 0755); err != nil {
393-
return distribution.Descriptor{}, fmt.Errorf("could not create layer dir parent: %w", err)
484+
return distribution.Descriptor{}, errors.Wrap(err, "could not create layer dir parent")
394485
}
395486
tarFile, err := sequential.Create(layerPath)
396487
if err != nil {
397-
return distribution.Descriptor{}, fmt.Errorf("error creating layer file: %w", err)
488+
return distribution.Descriptor{}, errors.Wrap(err, "error creating layer file")
398489
}
399490
defer tarFile.Close()
400491

@@ -411,7 +502,7 @@ func (s *saveSession) saveLayer(id layer.ChainID, legacyImg image.V1Image, creat
411502
for _, fname := range []string{outDir, configPath, layerPath} {
412503
// todo: maybe save layer created timestamp?
413504
if err := system.Chtimes(fname, createdTime, createdTime); err != nil {
414-
return distribution.Descriptor{}, err
505+
return distribution.Descriptor{}, errors.Wrap(err, "could not set layer timestamp")
415506
}
416507
}
417508

image/tarexport/tarexport.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ const (
1212
legacyLayerFileName = "layer.tar"
1313
legacyConfigFileName = "json"
1414
legacyRepositoriesFileName = "repositories"
15+
16+
ociIndexFileName = "index.json"
17+
ociLayoutFilename = "oci-layout"
18+
ociLayoutContent = `{"imageLayoutVersion": "1.0.0"}`
1519
)
1620

1721
type manifestItem struct {

0 commit comments

Comments
 (0)