github.com/rish1988/moby@v25.0.2+incompatible/integration/image/save_test.go (about)

     1  package image
     2  
     3  import (
     4  	"archive/tar"
     5  	"encoding/json"
     6  	"io"
     7  	"io/fs"
     8  	"os"
     9  	"path/filepath"
    10  	"reflect"
    11  	"sort"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/cpuguy83/tar2go"
    17  	containertypes "github.com/docker/docker/api/types/container"
    18  	"github.com/docker/docker/api/types/versions"
    19  	"github.com/docker/docker/integration/internal/build"
    20  	"github.com/docker/docker/integration/internal/container"
    21  	"github.com/docker/docker/pkg/archive"
    22  	"github.com/docker/docker/testutil/fakecontext"
    23  	"github.com/opencontainers/go-digest"
    24  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    25  	"gotest.tools/v3/assert"
    26  	"gotest.tools/v3/assert/cmp"
    27  	is "gotest.tools/v3/assert/cmp"
    28  	"gotest.tools/v3/skip"
    29  )
    30  
    31  type imageSaveManifestEntry struct {
    32  	Config   string
    33  	RepoTags []string
    34  	Layers   []string
    35  }
    36  
    37  func tarIndexFS(t *testing.T, rdr io.Reader) fs.FS {
    38  	t.Helper()
    39  
    40  	dir := t.TempDir()
    41  
    42  	f, err := os.Create(filepath.Join(dir, "image.tar"))
    43  	assert.NilError(t, err)
    44  
    45  	// Do not close at the end of this function otherwise the indexer won't work
    46  	t.Cleanup(func() { f.Close() })
    47  
    48  	_, err = io.Copy(f, rdr)
    49  	assert.NilError(t, err)
    50  
    51  	return tar2go.NewIndex(f).FS()
    52  }
    53  
    54  func TestSaveCheckTimes(t *testing.T) {
    55  	ctx := setupTest(t)
    56  
    57  	t.Parallel()
    58  	client := testEnv.APIClient()
    59  
    60  	const repoName = "busybox:latest"
    61  	img, _, err := client.ImageInspectWithRaw(ctx, repoName)
    62  	assert.NilError(t, err)
    63  
    64  	rdr, err := client.ImageSave(ctx, []string{repoName})
    65  	assert.NilError(t, err)
    66  
    67  	tarfs := tarIndexFS(t, rdr)
    68  
    69  	dt, err := fs.ReadFile(tarfs, "manifest.json")
    70  	assert.NilError(t, err)
    71  
    72  	var ls []imageSaveManifestEntry
    73  	assert.NilError(t, json.Unmarshal(dt, &ls))
    74  	assert.Assert(t, cmp.Len(ls, 1))
    75  
    76  	info, err := fs.Stat(tarfs, ls[0].Config)
    77  	assert.NilError(t, err)
    78  
    79  	created, err := time.Parse(time.RFC3339, img.Created)
    80  	assert.NilError(t, err)
    81  
    82  	if testEnv.UsingSnapshotter() {
    83  		// containerd archive export sets the mod time to zero.
    84  		assert.Check(t, is.Equal(info.ModTime(), time.Unix(0, 0)))
    85  	} else {
    86  		assert.Check(t, is.Equal(info.ModTime().Format(time.RFC3339), created.Format(time.RFC3339)))
    87  	}
    88  }
    89  
    90  // Regression test for https://github.com/moby/moby/issues/47065
    91  func TestSaveCheckManifestLayers(t *testing.T) {
    92  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.44"), "OCI layout support was introduced in v25")
    93  
    94  	ctx := setupTest(t)
    95  	client := testEnv.APIClient()
    96  
    97  	t.Parallel()
    98  
    99  	const repoName = "busybox:latest"
   100  	img, _, err := client.ImageInspectWithRaw(ctx, repoName)
   101  	assert.NilError(t, err)
   102  
   103  	rdr, err := client.ImageSave(ctx, []string{repoName})
   104  	assert.NilError(t, err)
   105  
   106  	tarfs := tarIndexFS(t, rdr)
   107  
   108  	indexData, err := fs.ReadFile(tarfs, "index.json")
   109  	assert.NilError(t, err)
   110  
   111  	var index ocispec.Index
   112  	assert.NilError(t, json.Unmarshal(indexData, &index))
   113  
   114  	assert.Assert(t, is.Len(index.Manifests, 1))
   115  
   116  	manifestData, err := fs.ReadFile(tarfs, "blobs/sha256/"+index.Manifests[0].Digest.Encoded())
   117  	assert.NilError(t, err)
   118  
   119  	var manifest ocispec.Manifest
   120  	assert.NilError(t, json.Unmarshal(manifestData, &manifest))
   121  
   122  	assert.Check(t, is.Len(manifest.Layers, len(img.RootFS.Layers)))
   123  	for _, l := range manifest.Layers {
   124  		stat, err := fs.Stat(tarfs, "blobs/sha256/"+l.Digest.Encoded())
   125  		if !assert.Check(t, err) {
   126  			continue
   127  		}
   128  
   129  		assert.Check(t, is.Equal(l.Size, stat.Size()))
   130  	}
   131  }
   132  
   133  func TestSaveRepoWithMultipleImages(t *testing.T) {
   134  	ctx := setupTest(t)
   135  	client := testEnv.APIClient()
   136  
   137  	makeImage := func(from string, tag string) string {
   138  		id := container.Create(ctx, t, client, func(cfg *container.TestContainerConfig) {
   139  			cfg.Config.Image = from
   140  			cfg.Config.Cmd = []string{"true"}
   141  		})
   142  
   143  		res, err := client.ContainerCommit(ctx, id, containertypes.CommitOptions{Reference: tag})
   144  		assert.NilError(t, err)
   145  
   146  		err = client.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true})
   147  		assert.NilError(t, err)
   148  
   149  		return res.ID
   150  	}
   151  
   152  	busyboxImg, _, err := client.ImageInspectWithRaw(ctx, "busybox:latest")
   153  	assert.NilError(t, err)
   154  
   155  	const repoName = "foobar-save-multi-images-test"
   156  	const tagFoo = repoName + ":foo"
   157  	const tagBar = repoName + ":bar"
   158  
   159  	idFoo := makeImage("busybox:latest", tagFoo)
   160  	idBar := makeImage("busybox:latest", tagBar)
   161  	idBusybox := busyboxImg.ID
   162  
   163  	rdr, err := client.ImageSave(ctx, []string{repoName, "busybox:latest"})
   164  	assert.NilError(t, err)
   165  	defer rdr.Close()
   166  
   167  	tarfs := tarIndexFS(t, rdr)
   168  
   169  	dt, err := fs.ReadFile(tarfs, "manifest.json")
   170  	assert.NilError(t, err)
   171  
   172  	var mfstLs []imageSaveManifestEntry
   173  	assert.NilError(t, json.Unmarshal(dt, &mfstLs))
   174  
   175  	actual := make([]string, 0, len(mfstLs))
   176  	for _, m := range mfstLs {
   177  		actual = append(actual, strings.TrimPrefix(m.Config, "blobs/sha256/"))
   178  		// make sure the blob actually exists
   179  		_, err = fs.Stat(tarfs, m.Config)
   180  		assert.Check(t, err)
   181  	}
   182  
   183  	expected := []string{idBusybox, idFoo, idBar}
   184  	// prefixes are not in tar
   185  	for i := range expected {
   186  		expected[i] = digest.Digest(expected[i]).Encoded()
   187  	}
   188  
   189  	// With snapshotters, ID of the image is the ID of the manifest/index
   190  	// With graphdrivers, ID of the image is the ID of the image config
   191  	if testEnv.UsingSnapshotter() {
   192  		// ID of image won't match the Config ID from manifest.json
   193  		// Just check if manifests exist in blobs
   194  		for _, blob := range expected {
   195  			_, err = fs.Stat(tarfs, "blobs/sha256/"+blob)
   196  			assert.Check(t, err)
   197  		}
   198  	} else {
   199  		sort.Strings(actual)
   200  		sort.Strings(expected)
   201  		assert.Assert(t, cmp.DeepEqual(actual, expected), "archive does not contains the right layers: got %v, expected %v", actual, expected)
   202  	}
   203  }
   204  
   205  func TestSaveDirectoryPermissions(t *testing.T) {
   206  	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "Test is looking at linux specific details")
   207  
   208  	ctx := setupTest(t)
   209  	client := testEnv.APIClient()
   210  
   211  	layerEntries := []string{"opt/", "opt/a/", "opt/a/b/", "opt/a/b/c"}
   212  	layerEntriesAUFS := []string{"./", ".wh..wh.aufs", ".wh..wh.orph/", ".wh..wh.plnk/", "opt/", "opt/a/", "opt/a/b/", "opt/a/b/c"}
   213  
   214  	dockerfile := `FROM busybox
   215  RUN adduser -D user && mkdir -p /opt/a/b && chown -R user:user /opt/a
   216  RUN touch /opt/a/b/c && chown user:user /opt/a/b/c`
   217  
   218  	imgID := build.Do(ctx, t, client, fakecontext.New(t, t.TempDir(), fakecontext.WithDockerfile(dockerfile)))
   219  
   220  	rdr, err := client.ImageSave(ctx, []string{imgID})
   221  	assert.NilError(t, err)
   222  	defer rdr.Close()
   223  
   224  	tarfs := tarIndexFS(t, rdr)
   225  
   226  	dt, err := fs.ReadFile(tarfs, "manifest.json")
   227  	assert.NilError(t, err)
   228  
   229  	var mfstLs []imageSaveManifestEntry
   230  	assert.NilError(t, json.Unmarshal(dt, &mfstLs))
   231  
   232  	var found bool
   233  
   234  	for _, p := range mfstLs[0].Layers {
   235  		var entriesSansDev []string
   236  
   237  		f, err := tarfs.Open(p)
   238  		assert.NilError(t, err)
   239  
   240  		entries, err := listTar(f)
   241  		f.Close()
   242  		assert.NilError(t, err)
   243  
   244  		for _, e := range entries {
   245  			if !strings.Contains(e, "dev/") {
   246  				entriesSansDev = append(entriesSansDev, e)
   247  			}
   248  		}
   249  		assert.NilError(t, err, "encountered error while listing tar entries: %s", err)
   250  
   251  		if reflect.DeepEqual(entriesSansDev, layerEntries) || reflect.DeepEqual(entriesSansDev, layerEntriesAUFS) {
   252  			found = true
   253  			break
   254  		}
   255  	}
   256  
   257  	assert.Assert(t, found, "failed to find the layer with the right content listing")
   258  }
   259  
   260  func listTar(f io.Reader) ([]string, error) {
   261  	// If using the containerd snapshotter, the tar file may be compressed
   262  	dec, err := archive.DecompressStream(f)
   263  	if err != nil {
   264  		return nil, err
   265  	}
   266  	defer dec.Close()
   267  
   268  	tr := tar.NewReader(dec)
   269  	var entries []string
   270  
   271  	for {
   272  		th, err := tr.Next()
   273  		if err == io.EOF {
   274  			// end of tar archive
   275  			return entries, nil
   276  		}
   277  		if err != nil {
   278  			return entries, err
   279  		}
   280  		entries = append(entries, th.Name)
   281  	}
   282  }