123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- package image
- import (
- "archive/tar"
- "encoding/json"
- "io"
- "io/fs"
- "os"
- "path/filepath"
- "reflect"
- "sort"
- "strings"
- "testing"
- "time"
- "github.com/cpuguy83/tar2go"
- containertypes "github.com/docker/docker/api/types/container"
- "github.com/docker/docker/api/types/versions"
- "github.com/docker/docker/integration/internal/build"
- "github.com/docker/docker/integration/internal/container"
- "github.com/docker/docker/internal/testutils"
- "github.com/docker/docker/internal/testutils/specialimage"
- "github.com/docker/docker/pkg/archive"
- "github.com/docker/docker/testutil/fakecontext"
- "github.com/opencontainers/go-digest"
- ocispec "github.com/opencontainers/image-spec/specs-go/v1"
- "gotest.tools/v3/assert"
- "gotest.tools/v3/assert/cmp"
- is "gotest.tools/v3/assert/cmp"
- "gotest.tools/v3/skip"
- )
- type imageSaveManifestEntry struct {
- Config string
- RepoTags []string
- Layers []string
- }
- func tarIndexFS(t *testing.T, rdr io.Reader) fs.FS {
- t.Helper()
- dir := t.TempDir()
- f, err := os.Create(filepath.Join(dir, "image.tar"))
- assert.NilError(t, err)
- // Do not close at the end of this function otherwise the indexer won't work
- t.Cleanup(func() { f.Close() })
- _, err = io.Copy(f, rdr)
- assert.NilError(t, err)
- return tar2go.NewIndex(f).FS()
- }
- func TestSaveCheckTimes(t *testing.T) {
- ctx := setupTest(t)
- t.Parallel()
- client := testEnv.APIClient()
- const repoName = "busybox:latest"
- img, _, err := client.ImageInspectWithRaw(ctx, repoName)
- assert.NilError(t, err)
- rdr, err := client.ImageSave(ctx, []string{repoName})
- assert.NilError(t, err)
- tarfs := tarIndexFS(t, rdr)
- dt, err := fs.ReadFile(tarfs, "manifest.json")
- assert.NilError(t, err)
- var ls []imageSaveManifestEntry
- assert.NilError(t, json.Unmarshal(dt, &ls))
- assert.Assert(t, cmp.Len(ls, 1))
- info, err := fs.Stat(tarfs, ls[0].Config)
- assert.NilError(t, err)
- created, err := time.Parse(time.RFC3339, img.Created)
- assert.NilError(t, err)
- if testEnv.UsingSnapshotter() {
- // containerd archive export sets the mod time to zero.
- assert.Check(t, is.Equal(info.ModTime(), time.Unix(0, 0)))
- } else {
- assert.Check(t, is.Equal(info.ModTime().Format(time.RFC3339), created.Format(time.RFC3339)))
- }
- }
- // Regression test for https://github.com/moby/moby/issues/47065
- func TestSaveOCI(t *testing.T) {
- skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.44"), "OCI layout support was introduced in v25")
- ctx := setupTest(t)
- client := testEnv.APIClient()
- const busybox = "busybox:latest"
- inspectBusybox, _, err := client.ImageInspectWithRaw(ctx, busybox)
- assert.NilError(t, err)
- type testCase struct {
- image string
- expectedOCIRef string
- expectedContainerdRef string
- }
- testCases := []testCase{
- // Busybox by tagged name
- testCase{image: busybox, expectedContainerdRef: "docker.io/library/busybox:latest", expectedOCIRef: "latest"},
- // Busybox by ID
- testCase{image: inspectBusybox.ID},
- }
- if testEnv.DaemonInfo.OSType != "windows" {
- multiLayerImage := specialimage.Load(ctx, t, client, specialimage.MultiLayer)
- // Multi-layer image
- testCases = append(testCases, testCase{image: multiLayerImage, expectedContainerdRef: "docker.io/library/multilayer:latest", expectedOCIRef: "latest"})
- }
- // Busybox frozen image will have empty RepoDigests when loaded into the
- // graphdriver image store so we can't use it.
- // This will work with the containerd image store though.
- if len(inspectBusybox.RepoDigests) > 0 {
- // Digested reference
- testCases = append(testCases, testCase{
- image: inspectBusybox.RepoDigests[0],
- })
- }
- for _, tc := range testCases {
- tc := tc
- t.Run(tc.image, func(t *testing.T) {
- // Get information about the original image.
- inspect, _, err := client.ImageInspectWithRaw(ctx, tc.image)
- assert.NilError(t, err)
- rdr, err := client.ImageSave(ctx, []string{tc.image})
- assert.NilError(t, err)
- defer rdr.Close()
- tarfs := tarIndexFS(t, rdr)
- indexData, err := fs.ReadFile(tarfs, "index.json")
- assert.NilError(t, err, "failed to read index.json")
- var index ocispec.Index
- assert.NilError(t, json.Unmarshal(indexData, &index), "failed to unmarshal index.json")
- // All test images are single-platform, so they should have only one manifest.
- assert.Assert(t, is.Len(index.Manifests, 1))
- manifestData, err := fs.ReadFile(tarfs, "blobs/sha256/"+index.Manifests[0].Digest.Encoded())
- assert.NilError(t, err)
- var manifest ocispec.Manifest
- assert.NilError(t, json.Unmarshal(manifestData, &manifest))
- t.Run("Manifest", func(t *testing.T) {
- assert.Check(t, is.Len(manifest.Layers, len(inspect.RootFS.Layers)))
- var digests []string
- // Check if layers referenced by the manifest exist in the archive
- // and match the layers from the original image.
- for _, l := range manifest.Layers {
- layerPath := "blobs/sha256/" + l.Digest.Encoded()
- stat, err := fs.Stat(tarfs, layerPath)
- assert.NilError(t, err)
- assert.Check(t, is.Equal(l.Size, stat.Size()))
- f, err := tarfs.Open(layerPath)
- assert.NilError(t, err)
- layerDigest, err := testutils.UncompressedTarDigest(f)
- f.Close()
- assert.NilError(t, err)
- digests = append(digests, layerDigest.String())
- }
- assert.Check(t, is.DeepEqual(digests, inspect.RootFS.Layers))
- })
- t.Run("Config", func(t *testing.T) {
- configData, err := fs.ReadFile(tarfs, "blobs/sha256/"+manifest.Config.Digest.Encoded())
- assert.NilError(t, err)
- var config ocispec.Image
- assert.NilError(t, json.Unmarshal(configData, &config))
- var diffIDs []string
- for _, l := range config.RootFS.DiffIDs {
- diffIDs = append(diffIDs, l.String())
- }
- assert.Check(t, is.DeepEqual(diffIDs, inspect.RootFS.Layers))
- })
- t.Run("Containerd image name", func(t *testing.T) {
- assert.Check(t, is.Equal(index.Manifests[0].Annotations["io.containerd.image.name"], tc.expectedContainerdRef))
- })
- t.Run("OCI reference tag", func(t *testing.T) {
- assert.Check(t, is.Equal(index.Manifests[0].Annotations["org.opencontainers.image.ref.name"], tc.expectedOCIRef))
- })
- })
- }
- }
- func TestSaveRepoWithMultipleImages(t *testing.T) {
- ctx := setupTest(t)
- client := testEnv.APIClient()
- makeImage := func(from string, tag string) string {
- id := container.Create(ctx, t, client, func(cfg *container.TestContainerConfig) {
- cfg.Config.Image = from
- cfg.Config.Cmd = []string{"true"}
- })
- res, err := client.ContainerCommit(ctx, id, containertypes.CommitOptions{Reference: tag})
- assert.NilError(t, err)
- err = client.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true})
- assert.NilError(t, err)
- return res.ID
- }
- busyboxImg, _, err := client.ImageInspectWithRaw(ctx, "busybox:latest")
- assert.NilError(t, err)
- const repoName = "foobar-save-multi-images-test"
- const tagFoo = repoName + ":foo"
- const tagBar = repoName + ":bar"
- idFoo := makeImage("busybox:latest", tagFoo)
- idBar := makeImage("busybox:latest", tagBar)
- idBusybox := busyboxImg.ID
- rdr, err := client.ImageSave(ctx, []string{repoName, "busybox:latest"})
- assert.NilError(t, err)
- defer rdr.Close()
- tarfs := tarIndexFS(t, rdr)
- dt, err := fs.ReadFile(tarfs, "manifest.json")
- assert.NilError(t, err)
- var mfstLs []imageSaveManifestEntry
- assert.NilError(t, json.Unmarshal(dt, &mfstLs))
- actual := make([]string, 0, len(mfstLs))
- for _, m := range mfstLs {
- actual = append(actual, strings.TrimPrefix(m.Config, "blobs/sha256/"))
- // make sure the blob actually exists
- _, err = fs.Stat(tarfs, m.Config)
- assert.Check(t, err)
- }
- expected := []string{idBusybox, idFoo, idBar}
- // prefixes are not in tar
- for i := range expected {
- expected[i] = digest.Digest(expected[i]).Encoded()
- }
- // With snapshotters, ID of the image is the ID of the manifest/index
- // With graphdrivers, ID of the image is the ID of the image config
- if testEnv.UsingSnapshotter() {
- // ID of image won't match the Config ID from manifest.json
- // Just check if manifests exist in blobs
- for _, blob := range expected {
- _, err = fs.Stat(tarfs, "blobs/sha256/"+blob)
- assert.Check(t, err)
- }
- } else {
- sort.Strings(actual)
- sort.Strings(expected)
- assert.Assert(t, cmp.DeepEqual(actual, expected), "archive does not contains the right layers: got %v, expected %v", actual, expected)
- }
- }
- func TestSaveDirectoryPermissions(t *testing.T) {
- skip.If(t, testEnv.DaemonInfo.OSType == "windows", "Test is looking at linux specific details")
- ctx := setupTest(t)
- client := testEnv.APIClient()
- layerEntries := []string{"opt/", "opt/a/", "opt/a/b/", "opt/a/b/c"}
- layerEntriesAUFS := []string{"./", ".wh..wh.aufs", ".wh..wh.orph/", ".wh..wh.plnk/", "opt/", "opt/a/", "opt/a/b/", "opt/a/b/c"}
- dockerfile := `FROM busybox
- RUN adduser -D user && mkdir -p /opt/a/b && chown -R user:user /opt/a
- RUN touch /opt/a/b/c && chown user:user /opt/a/b/c`
- imgID := build.Do(ctx, t, client, fakecontext.New(t, t.TempDir(), fakecontext.WithDockerfile(dockerfile)))
- rdr, err := client.ImageSave(ctx, []string{imgID})
- assert.NilError(t, err)
- defer rdr.Close()
- tarfs := tarIndexFS(t, rdr)
- dt, err := fs.ReadFile(tarfs, "manifest.json")
- assert.NilError(t, err)
- var mfstLs []imageSaveManifestEntry
- assert.NilError(t, json.Unmarshal(dt, &mfstLs))
- var found bool
- for _, p := range mfstLs[0].Layers {
- var entriesSansDev []string
- f, err := tarfs.Open(p)
- assert.NilError(t, err)
- entries, err := listTar(f)
- f.Close()
- assert.NilError(t, err)
- for _, e := range entries {
- if !strings.Contains(e, "dev/") {
- entriesSansDev = append(entriesSansDev, e)
- }
- }
- assert.NilError(t, err, "encountered error while listing tar entries: %s", err)
- if reflect.DeepEqual(entriesSansDev, layerEntries) || reflect.DeepEqual(entriesSansDev, layerEntriesAUFS) {
- found = true
- break
- }
- }
- assert.Assert(t, found, "failed to find the layer with the right content listing")
- }
- func listTar(f io.Reader) ([]string, error) {
- // If using the containerd snapshotter, the tar file may be compressed
- dec, err := archive.DecompressStream(f)
- if err != nil {
- return nil, err
- }
- defer dec.Close()
- tr := tar.NewReader(dec)
- var entries []string
- for {
- th, err := tr.Next()
- if err == io.EOF {
- // end of tar archive
- return entries, nil
- }
- if err != nil {
- return entries, err
- }
- entries = append(entries, th.Name)
- }
- }
|