From 256e7abbef86df8bc8657e64767da289cd500be0 Mon Sep 17 00:00:00 2001 From: David Trudgian Date: Wed, 18 Oct 2023 13:42:49 +0100 Subject: [PATCH] feat: default AUFS -> OverlayFS whiteout conversion for squashfs By default, replace AUFS whiteout markers with OverlayFS whiteout markers when converting a layer from TAR to SquashFS. It is expected that SquashFS layers will be used via direct mount, rather than extraction to disk. Therefore, the whiteout markers must be in a format that supports assembling multiple mounted layers into a rootfs, using OverlayFS. A `.wh.file` marker indicating a whiteout of `file` is replaced by `file` as a 0:0 character device. A `dir/.wh..wh..opq` marker indicating an opaque directory is replaced with the `trusted.overlay.opaque="y"` xattr on `dir`. A two-pass approach is required. AUFS directory opaque markers are not required to be adjacent to the directory to which they apply, in the tar stream. As `tar2sqfs` and `sqfstar` do not support tar files with two instances of the same dir/file, we cannot append an additional tar entry if we come across an opaque marker distant from the target directory entry. A new option `OptNoConvertWhiteout` will disable whiteout conversion. This can be employed by callers to avoid unneccessary processing where the layer is part of a squashed / single-layer image that will not have any whiteout markers. Fixes #25 --- pkg/mutate/squashfs.go | 71 +++++++++- pkg/mutate/squashfs_test.go | 102 ++++++++++++++- .../AUFSBlob_sqfstar.golden | Bin 0 -> 4096 bytes .../AUFSBlob_sqfstar_noConvertWhiteout.golden | Bin 0 -> 4096 bytes .../AUFSBlob_tar2sqfs.golden | Bin 0 -> 4096 bytes ...AUFSBlob_tar2sqfs_noConvertWhiteout.golden | Bin 0 -> 4096 bytes .../HelloWorldBlob_sqfstar.golden | Bin 4096 -> 4096 bytes ...WorldBlob_sqfstar_noConvertWhiteout.golden | Bin 0 -> 4096 bytes ...orldBlob_tar2sqfs_noConvertWhiteout.golden | Bin 0 -> 4096 bytes pkg/mutate/whiteout.go | 122 ++++++++++++++++++ pkg/mutate/whiteout_test.go | 69 ++++++++++ ...16f7e8649e94fb4fc21fe77e8310c060f61caaff8a | 1 + ...b52c051cccf33d85f60827bd269c978fd95f05c3c9 | 16 +++ ...c3832cee4a2f725b15aeb258791640185c3126b2bf | Bin 0 -> 252 bytes .../images/aufs-docker-v2-manifest/index.json | 1 + .../images/aufs-docker-v2-manifest/oci-layout | 3 + 16 files changed, 374 insertions(+), 11 deletions(-) create mode 100644 pkg/mutate/testdata/Test_SquashfsLayer/AUFSBlob_sqfstar.golden create mode 100644 pkg/mutate/testdata/Test_SquashfsLayer/AUFSBlob_sqfstar_noConvertWhiteout.golden create mode 100644 pkg/mutate/testdata/Test_SquashfsLayer/AUFSBlob_tar2sqfs.golden create mode 100644 pkg/mutate/testdata/Test_SquashfsLayer/AUFSBlob_tar2sqfs_noConvertWhiteout.golden create mode 100644 pkg/mutate/testdata/Test_SquashfsLayer/HelloWorldBlob_sqfstar_noConvertWhiteout.golden create mode 100644 pkg/mutate/testdata/Test_SquashfsLayer/HelloWorldBlob_tar2sqfs_noConvertWhiteout.golden create mode 100644 pkg/mutate/whiteout.go create mode 100644 pkg/mutate/whiteout_test.go create mode 100644 test/images/aufs-docker-v2-manifest/blobs/sha256/44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a create mode 100644 test/images/aufs-docker-v2-manifest/blobs/sha256/6c9c1b8d1adba535a40046b52c051cccf33d85f60827bd269c978fd95f05c3c9 create mode 100644 test/images/aufs-docker-v2-manifest/blobs/sha256/da55812559dec81445c289c3832cee4a2f725b15aeb258791640185c3126b2bf create mode 100644 test/images/aufs-docker-v2-manifest/index.json create mode 100644 test/images/aufs-docker-v2-manifest/oci-layout diff --git a/pkg/mutate/squashfs.go b/pkg/mutate/squashfs.go index 8ce9c79..90ee505 100644 --- a/pkg/mutate/squashfs.go +++ b/pkg/mutate/squashfs.go @@ -20,9 +20,10 @@ import ( const layerMediaType types.MediaType = "application/vnd.sylabs.image.layer.v1.squashfs" type squashfsConverter struct { - converter string // Path to converter program. - args []string // Arguments required for converter program. - dir string // Working directory. + converter string // Path to converter program. + args []string // Arguments required for converter program. + dir string // Working directory. + noConvertWhiteout bool // Skip default conversion of whiteout markers from AUFS -> OverlayFS } // SquashfsConverterOpt are used to specify squashfs converter options. @@ -45,6 +46,15 @@ func OptSquashfsLayerConverter(converter string) SquashfsConverterOpt { var errSquashfsConverterNotSupported = errors.New("squashfs converter not supported") +// OptNoConvertWhiteout is set to skip the default conversion of whiteout / +// opaque markers from AUFS to OverlayFS format. +func OptNoConvertWhiteout(b bool) SquashfsConverterOpt { + return func(c *squashfsConverter) error { + c.noConvertWhiteout = b + return nil + } +} + // SquashfsLayer converts the base layer into a layer using the squashfs format. A dir must be // specified, which is used as a working directory during conversion. The caller is responsible for // cleaning up dir. @@ -124,6 +134,53 @@ func (c *squashfsConverter) makeSquashfs(r io.Reader) (string, error) { return path, nil } +// convertWhiteout accepts a Layer l and returns: +// +// - A ReadCloser providing the layer tar, passed through a filter that converts +// whiteout markers from AUFS -> OverlayFS, if c.noConvertWhiteout is false. +// In this case, any error from the filter will propagate via the returned channel. +// - A ReadCloser providing the layer tar directly, if c.noConvertWhiteout is true. +// +// Note that when conversion is performed, the layer is read twice. +func (c *squashfsConverter) convertWhiteout(l v1.Layer) (io.ReadCloser, chan error, error) { + rc, err := l.Uncompressed() + if err != nil { + return nil, nil, err + } + + // No conversion - direct tar stream from the layer. + if c.noConvertWhiteout { + return rc, nil, nil + } + + // Conversion - first, scan for opaque directories and presence of file + // whiteout markers. + opaquePaths, fileWhiteout, err := scanAUFSWhiteouts(rc) + if err != nil { + return nil, nil, err + } + rc.Close() + + rc, err = l.Uncompressed() + if err != nil { + return nil, nil, err + } + + // Nothing found to filter + if len(opaquePaths) == 0 && !fileWhiteout { + return rc, nil, nil + } + + filterErr := make(chan error, 1) + pr, pw := io.Pipe() + go func() { + defer rc.Close() + filterErr <- whiteoutFilter(rc, pw, opaquePaths) + close(filterErr) + }() + return pr, filterErr, nil +} + type squashfsLayer struct { base v1.Layer converter *squashfsConverter @@ -170,7 +227,7 @@ func (l *squashfsLayer) populate() error { return nil } - rc, err := l.base.Uncompressed() + rc, filterErr, err := l.converter.convertWhiteout(l.base) if err != nil { return err } @@ -181,6 +238,12 @@ func (l *squashfsLayer) populate() error { return err } + if filterErr != nil { + if err = <-filterErr; err != nil { + return err + } + } + f, err := os.Open(path) if err != nil { return err diff --git a/pkg/mutate/squashfs_test.go b/pkg/mutate/squashfs_test.go index 71b385d..dd5f648 100644 --- a/pkg/mutate/squashfs_test.go +++ b/pkg/mutate/squashfs_test.go @@ -49,18 +49,21 @@ func diffSquashFS(tb testing.TB, pathA, pathB string, diffArgs ...string) { func Test_SquashfsLayer(t *testing.T) { tests := []struct { - name string - layer v1.Layer - converter string - diffArgs []string + name string + layer v1.Layer + converter string + noConvertWhiteout bool + diffArgs []string }{ + // HelloWorld layer contains no whiteouts - conversion should have no effect on output. { name: "HelloWorldBlob_sqfstar", layer: testLayer(t, "hello-world-docker-v2-manifest", v1.Hash{ Algorithm: "sha256", Hex: "7050e35b49f5e348c4809f5eff915842962cb813f32062d3bbdd35c750dd7d01", }), - converter: "sqfstar", + converter: "sqfstar", + noConvertWhiteout: false, // Some versions of squashfs-tools do not implement '-root-uid'/'-root-gid', so ignore // differences in ownership. diffArgs: []string{"--no-owner"}, @@ -71,7 +74,89 @@ func Test_SquashfsLayer(t *testing.T) { Algorithm: "sha256", Hex: "7050e35b49f5e348c4809f5eff915842962cb813f32062d3bbdd35c750dd7d01", }), - converter: "tar2sqfs", + converter: "tar2sqfs", + noConvertWhiteout: false, + }, + { + name: "HelloWorldBlob_sqfstar_noConvertWhiteout", + layer: testLayer(t, "hello-world-docker-v2-manifest", v1.Hash{ + Algorithm: "sha256", + Hex: "7050e35b49f5e348c4809f5eff915842962cb813f32062d3bbdd35c750dd7d01", + }), + converter: "sqfstar", + noConvertWhiteout: true, + // Some versions of squashfs-tools do not implement '-root-uid'/'-root-gid', so ignore + // differences in ownership. + diffArgs: []string{"--no-owner"}, + }, + { + name: "HelloWorldBlob_tar2sqfs_noConvertWhiteout", + layer: testLayer(t, "hello-world-docker-v2-manifest", v1.Hash{ + Algorithm: "sha256", + Hex: "7050e35b49f5e348c4809f5eff915842962cb813f32062d3bbdd35c750dd7d01", + }), + converter: "tar2sqfs", + noConvertWhiteout: true, + }, + // AUFS layer contains whiteouts. Should be converted to overlayfs form when noConvertWhiteout = false. + // + // Original (AUFS) + // All regular files. + // + // [drwxr-xr-x] . + // ├── [drwxr-xr-x] dir + // │   └── [-rw-r--r--] .wh..wh..opq + // └── [-rw-r--r--] .wh.file + // + // Converted (OverlayFS) + // .wh.file becomes file as a char 0:0 device + // dir/.wh..wh..opq becomes trusted.overlay.opaque="y" xattr on dir + // + // [drwxr-xr-x] . + // ├── [drwxr-xr-x] dir + // └── [crw-r--r--] file + // + { + name: "AUFSBlob_sqfstar", + layer: testLayer(t, "aufs-docker-v2-manifest", v1.Hash{ + Algorithm: "sha256", + Hex: "da55812559dec81445c289c3832cee4a2f725b15aeb258791640185c3126b2bf", + }), + converter: "sqfstar", + noConvertWhiteout: false, + // Some versions of squashfs-tools do not implement '-root-uid'/'-root-gid', so ignore + // differences in ownership. + diffArgs: []string{"--no-owner"}, + }, + { + name: "AUFSBlob_tar2sqfs", + layer: testLayer(t, "aufs-docker-v2-manifest", v1.Hash{ + Algorithm: "sha256", + Hex: "da55812559dec81445c289c3832cee4a2f725b15aeb258791640185c3126b2bf", + }), + converter: "tar2sqfs", + noConvertWhiteout: false, + }, + { + name: "AUFSBlob_sqfstar_noConvertWhiteout", + layer: testLayer(t, "aufs-docker-v2-manifest", v1.Hash{ + Algorithm: "sha256", + Hex: "da55812559dec81445c289c3832cee4a2f725b15aeb258791640185c3126b2bf", + }), + converter: "sqfstar", + noConvertWhiteout: true, + // Some versions of squashfs-tools do not implement '-root-uid'/'-root-gid', so ignore + // differences in ownership. + diffArgs: []string{"--no-owner"}, + }, + { + name: "AUFSBlob_tar2sqfs_noConvertWhiteout", + layer: testLayer(t, "aufs-docker-v2-manifest", v1.Hash{ + Algorithm: "sha256", + Hex: "da55812559dec81445c289c3832cee4a2f725b15aeb258791640185c3126b2bf", + }), + converter: "tar2sqfs", + noConvertWhiteout: true, }, } for _, tt := range tests { @@ -82,7 +167,10 @@ func Test_SquashfsLayer(t *testing.T) { t.Skip(err) } - l, err := SquashfsLayer(tt.layer, t.TempDir(), OptSquashfsLayerConverter(tt.converter)) + l, err := SquashfsLayer(tt.layer, t.TempDir(), + OptSquashfsLayerConverter(tt.converter), + OptNoConvertWhiteout(tt.noConvertWhiteout), + ) if err != nil { t.Fatal(err) } diff --git a/pkg/mutate/testdata/Test_SquashfsLayer/AUFSBlob_sqfstar.golden b/pkg/mutate/testdata/Test_SquashfsLayer/AUFSBlob_sqfstar.golden new file mode 100644 index 0000000000000000000000000000000000000000..109d5c62346b687338db253926b53dd909ef8fa7 GIT binary patch literal 4096 zcmc~OE-YqdfB_~jgONdyA%TIBfd$BOfJ*T&LKp|3{4Y>C0ZPw?(z~Jbe<-kIsJQhw zq2lxj1{P!Um&=qc4VPG_nVu_HP)^PPJ)78c+22MvM7yc^Md%0Zcbue?1m;xKa zu1*dCIiS|$lm-Sji?c^2Op08>n6kmhz<5dOvn5On5_-&HEDbM#E{9nt-oVJf$B~-h literal 0 HcmV?d00001 diff --git a/pkg/mutate/testdata/Test_SquashfsLayer/AUFSBlob_sqfstar_noConvertWhiteout.golden b/pkg/mutate/testdata/Test_SquashfsLayer/AUFSBlob_sqfstar_noConvertWhiteout.golden new file mode 100644 index 0000000000000000000000000000000000000000..8d9f3f9605a113596ad9f6f2aaf5b07de3199e74 GIT binary patch literal 4096 zcmc~OE-YqYfB_~jgONdyA%TIB0VJLPm3jfCA3*8

=xSFNM;#(Bz#NDsCkwMI1W8 zz_NGtm*9gu3P1Q+0@Rp=L+aE5=1F$jC?AgAVIv&F&)jSzuvy{c>zGUasg0+a&sdbHEfq zjY*RtSs31@arCh?ykurzfVl_itWjz-1V%$(Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ON LU^E0qWC#EN0P#lZ literal 0 HcmV?d00001 diff --git a/pkg/mutate/testdata/Test_SquashfsLayer/AUFSBlob_tar2sqfs.golden b/pkg/mutate/testdata/Test_SquashfsLayer/AUFSBlob_tar2sqfs.golden new file mode 100644 index 0000000000000000000000000000000000000000..f7679aa389837f56c949114b8904c889807e1681 GIT binary patch literal 4096 zcmc~OE-YqdfB_~jgONdyA%KCAfd$BOfJ$*QLKp|2{LfH20ZPw;(*Mzb1w+NH#|agu zPcX0;o4;J%B;n(5<_rU)ffP%GvKp z$pLjHr!+9QS)4sGVN&E0#*__42F6QLpDkfxkkDfmV`+E^boef)z2Xgw415gvWvN9u ziIsZ!1&M{FsURm;3N%1f06oOOAP&SQpaM`IF+7AR7^O!;U^E0qLtr!nMnhmU1V%$( WGz3ONU^E0qLtr!nMnhn@h5!I9D?{u6 literal 0 HcmV?d00001 diff --git a/pkg/mutate/testdata/Test_SquashfsLayer/AUFSBlob_tar2sqfs_noConvertWhiteout.golden b/pkg/mutate/testdata/Test_SquashfsLayer/AUFSBlob_tar2sqfs_noConvertWhiteout.golden new file mode 100644 index 0000000000000000000000000000000000000000..2b424cb9c91552ee76fa704a0656553c942e2ea3 GIT binary patch literal 4096 zcmc~OE-YqYfB_~jgONdyA%KaI0VJLPm3j!JZ$jz+Pymx(fF=%;w_~Wdm7ElD=mZ0c zvH8p8O_B~DV_cw2@MQx8fT9jIJ+Qx-a__cKUrHj3fb6L4bJjS^FMvYvy|ag5=+;T6fs7I*X$g< zEDbN285k}@9W+XfhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb3O>0L5xd AVgLXD literal 0 HcmV?d00001 diff --git a/pkg/mutate/testdata/Test_SquashfsLayer/HelloWorldBlob_sqfstar.golden b/pkg/mutate/testdata/Test_SquashfsLayer/HelloWorldBlob_sqfstar.golden index 64b06091fcf673146b6e0c8cd0e4803d4f9d5449..e74b9981932976c9fcbc8fd25df04526c264cfa5 100644 GIT binary patch delta 38 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 F{0pmWL<;}_ literal 0 HcmV?d00001 diff --git a/pkg/mutate/testdata/Test_SquashfsLayer/HelloWorldBlob_tar2sqfs_noConvertWhiteout.golden b/pkg/mutate/testdata/Test_SquashfsLayer/HelloWorldBlob_tar2sqfs_noConvertWhiteout.golden new file mode 100644 index 0000000000000000000000000000000000000000..ec6f5e4b5a6273c07478fc2f76c2ae8f86159c64 GIT binary patch literal 4096 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 literal 0 HcmV?d00001 diff --git a/pkg/mutate/whiteout.go b/pkg/mutate/whiteout.go new file mode 100644 index 0000000..2f56d3f --- /dev/null +++ b/pkg/mutate/whiteout.go @@ -0,0 +1,122 @@ +// Copyright 2023 Sylabs Inc. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +package mutate + +import ( + "archive/tar" + "errors" + "fmt" + "io" + "path/filepath" + "strings" +) + +const ( + aufsWhiteoutPrefix = ".wh." + aufsOpaqueMarker = ".wh..wh..opq" +) + +var errUnexpectedOpaque = errors.New("unexpected opaque marker") + +// scanAUFSWhiteouts reads a TAR stream, returning a map of :true for +// directories in the tar that contain an AUFS .wh..wh..opq opaque directory +// marker file, and a boolean indicating the presence of any .wh. markers. +// Note that paths returned are clean, per filepath.Clean. +func scanAUFSWhiteouts(in io.Reader) (map[string]bool, bool, error) { + opaquePaths := map[string]bool{} + fileWhiteout := false + + tr := tar.NewReader(in) + for { + header, err := tr.Next() + + if err == io.EOF { + return opaquePaths, fileWhiteout, nil + } + if err != nil { + return nil, false, err + } + + base := filepath.Base(header.Name) + + if base == aufsOpaqueMarker { + parent := filepath.Dir(header.Name) + opaquePaths[parent] = true + } + + if !fileWhiteout && strings.HasPrefix(base, aufsWhiteoutPrefix) { + fileWhiteout = true + } + } +} + +// whiteOutFilter streams a tar file from in to out, replacing AUFS whiteout +// markers with OverlayFS whiteout markers. Due to unrestricted ordering of +// markers vs their target, the list of opaquePaths must be obtained prior to +// filtering and provided to this filter. +func whiteoutFilter(in io.ReadCloser, out io.WriteCloser, opaquePaths map[string]bool) error { + tr := tar.NewReader(in) + tw := tar.NewWriter(out) + defer out.Close() + defer tw.Close() + + for { + header, err := tr.Next() + + if err == io.EOF { + return nil + } + if err != nil { + return err + } + + // Must force to PAX format, to accommodate xattrs + header.Format = tar.FormatPAX + + clean := filepath.Clean(header.Name) + base := filepath.Base(header.Name) + parent := filepath.Dir(header.Name) + + // Don't include .wh..wh..opq opaque directory markers in output. + if base == aufsOpaqueMarker { + // If we don't know the target should be opaque, then provided opaquePaths is incorrect. + if !opaquePaths[parent] { + return fmt.Errorf("%q: %w", parent, errUnexpectedOpaque) + } + continue + } + // Set overlayfs xattr on a dir that was previously found to contain a .wh..wh..opq marker. + if opq := opaquePaths[clean]; opq { + if header.PAXRecords == nil { + header.PAXRecords = map[string]string{} + } + header.PAXRecords["SCHILY.xattr."+"trusted.overlay.opaque"] = "y" + } + // Replace a `.wh.` marker with a char dev 0 at + if strings.HasPrefix(base, aufsWhiteoutPrefix) { + target := filepath.Join(parent, strings.TrimPrefix(base, aufsWhiteoutPrefix)) + header.Name = target + header.Typeflag = tar.TypeChar + header.Devmajor = 0 + header.Devminor = 0 + if err := tw.WriteHeader(header); err != nil { + return err + } + continue + } + + if err := tw.WriteHeader(header); err != nil { + return err + } + + // Disable gosec G110: Potential DoS vulnerability via decompression bomb. + // We are just filtering a flow directly from tar reader to tar writer - we aren't reading + // into memory beyond the stdlib buffering. + //nolint:gosec + if _, err := io.Copy(tw, tr); err != nil { + return err + } + } +} diff --git a/pkg/mutate/whiteout_test.go b/pkg/mutate/whiteout_test.go new file mode 100644 index 0000000..a9fbee6 --- /dev/null +++ b/pkg/mutate/whiteout_test.go @@ -0,0 +1,69 @@ +// Copyright 2023 Sylabs Inc. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +package mutate + +import ( + "reflect" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +func Test_scanAUFSOpaque(t *testing.T) { + tests := []struct { + name string + layer v1.Layer + expectOpaque map[string]bool + expectFileWhiteout bool + }{ + // HelloWorld layer contains no opaque markers + { + name: "HelloWorldTar", + layer: testLayer(t, "hello-world-docker-v2-manifest", v1.Hash{ + Algorithm: "sha256", + Hex: "7050e35b49f5e348c4809f5eff915842962cb813f32062d3bbdd35c750dd7d01", + }), + expectOpaque: map[string]bool{}, + expectFileWhiteout: false, + }, + // AUFS layer contains a single opaque marker on dir + // [drwxr-xr-x] . + // ├── [drwxr-xr-x] dir + // │   └── [-rw-r--r--] .wh..wh..opq + // └── [-rw-r--r--] .wh.file + { + name: "AUFSTar", + layer: testLayer(t, "aufs-docker-v2-manifest", v1.Hash{ + Algorithm: "sha256", + Hex: "da55812559dec81445c289c3832cee4a2f725b15aeb258791640185c3126b2bf", + }), + expectOpaque: map[string]bool{ + "dir": true, + }, + expectFileWhiteout: true, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + rc, err := tt.layer.Uncompressed() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { rc.Close() }) + + opaque, fileWhiteout, err := scanAUFSWhiteouts(rc) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !reflect.DeepEqual(tt.expectOpaque, opaque) { + t.Errorf("opaque directories - expected: %v, got: %v", tt.expectOpaque, opaque) + } + if fileWhiteout != tt.expectFileWhiteout { + t.Errorf("file whiteout(s) - expected: %v, got: %v", tt.expectFileWhiteout, fileWhiteout) + } + }) + } +} diff --git a/test/images/aufs-docker-v2-manifest/blobs/sha256/44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a b/test/images/aufs-docker-v2-manifest/blobs/sha256/44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a new file mode 100644 index 0000000..902a739 --- /dev/null +++ b/test/images/aufs-docker-v2-manifest/blobs/sha256/44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a @@ -0,0 +1 @@ +{"architecture":"arm64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/hello"],"Image":"sha256:cc0fff24c4ece63ade5d9f549e42c926cf569112c4f5c439a4a57f3f33f5588b","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"b2af51419cbf516f3c99b877a64906b21afedc175bd3cd082eb5798e2f277bb4","container_config":{"Hostname":"b2af51419cbf","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"/hello\"]"],"Image":"sha256:cc0fff24c4ece63ade5d9f549e42c926cf569112c4f5c439a4a57f3f33f5588b","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"created":"2022-03-19T16:12:58.923371954Z","docker_version":"20.10.12","history":[{"created":"2022-03-19T16:12:58.834095198Z","created_by":"/bin/sh -c #(nop) COPY file:a79dd5bda1e77203401956a93401d3aef45221fc750295a4291896f3386f4f54 in / "},{"created":"2022-03-19T16:12:58.923371954Z","created_by":"/bin/sh -c #(nop) CMD [\"/hello\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:efb53921da3394806160641b72a2cbd34ca1a9a8345ac670a85a04ad3d0e3507"]},"variant":"v8"} \ No newline at end of file diff --git a/test/images/aufs-docker-v2-manifest/blobs/sha256/6c9c1b8d1adba535a40046b52c051cccf33d85f60827bd269c978fd95f05c3c9 b/test/images/aufs-docker-v2-manifest/blobs/sha256/6c9c1b8d1adba535a40046b52c051cccf33d85f60827bd269c978fd95f05c3c9 new file mode 100644 index 0000000..aa59ff8 --- /dev/null +++ b/test/images/aufs-docker-v2-manifest/blobs/sha256/6c9c1b8d1adba535a40046b52c051cccf33d85f60827bd269c978fd95f05c3c9 @@ -0,0 +1,16 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 2, + "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 252, + "digest": "sha256:da55812559dec81445c289c3832cee4a2f725b15aeb258791640185c3126b2bf" + } + ] + } \ No newline at end of file diff --git a/test/images/aufs-docker-v2-manifest/blobs/sha256/da55812559dec81445c289c3832cee4a2f725b15aeb258791640185c3126b2bf b/test/images/aufs-docker-v2-manifest/blobs/sha256/da55812559dec81445c289c3832cee4a2f725b15aeb258791640185c3126b2bf new file mode 100644 index 0000000000000000000000000000000000000000..ccadfc71033942813b3dac846de8ff38b8622581 GIT binary patch literal 252 zcmV2-_HexbjW&rKeiwc4;6oBD9%3dJ#xZCp-b`gZi z3(?!>iXbQ`!3EL(fefaw1OLo&m0U3j&c#)NlgIT=iFIOvkz|!=+4b2Uj`N=p1AA*H z^!HMT(#8CvJp23pvHPZO*6Gfe)L&fu|9!CZ2LJ#70000000000_{0Srg##o2C;$N6 CVR8ll literal 0 HcmV?d00001 diff --git a/test/images/aufs-docker-v2-manifest/index.json b/test/images/aufs-docker-v2-manifest/index.json new file mode 100644 index 0000000..fe1cd39 --- /dev/null +++ b/test/images/aufs-docker-v2-manifest/index.json @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","size":525,"digest":"sha256:6c9c1b8d1adba535a40046b52c051cccf33d85f60827bd269c978fd95f05c3c9","platform":{"architecture":"arm64","os":"linux","variant":"v8"}}]} \ No newline at end of file diff --git a/test/images/aufs-docker-v2-manifest/oci-layout b/test/images/aufs-docker-v2-manifest/oci-layout new file mode 100644 index 0000000..224a869 --- /dev/null +++ b/test/images/aufs-docker-v2-manifest/oci-layout @@ -0,0 +1,3 @@ +{ + "imageLayoutVersion": "1.0.0" +} \ No newline at end of file