github.com/khulnasoft-lab/khulnasoft@v26.0.1-0.20240328202558-330a6f959fe0+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/internal/testutils"
    22  	"github.com/docker/docker/internal/testutils/specialimage"
    23  	"github.com/docker/docker/pkg/archive"
    24  	"github.com/docker/docker/testutil/fakecontext"
    25  	"github.com/opencontainers/go-digest"
    26  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    27  	"gotest.tools/v3/assert"
    28  	"gotest.tools/v3/assert/cmp"
    29  	is "gotest.tools/v3/assert/cmp"
    30  	"gotest.tools/v3/skip"
    31  )
    32  
    33  type imageSaveManifestEntry struct {
    34  	Config   string
    35  	RepoTags []string
    36  	Layers   []string
    37  }
    38  
    39  func tarIndexFS(t *testing.T, rdr io.Reader) fs.FS {
    40  	t.Helper()
    41  
    42  	dir := t.TempDir()
    43  
    44  	f, err := os.Create(filepath.Join(dir, "image.tar"))
    45  	assert.NilError(t, err)
    46  
    47  	// Do not close at the end of this function otherwise the indexer won't work
    48  	t.Cleanup(func() { f.Close() })
    49  
    50  	_, err = io.Copy(f, rdr)
    51  	assert.NilError(t, err)
    52  
    53  	return tar2go.NewIndex(f).FS()
    54  }
    55  
    56  func TestSaveCheckTimes(t *testing.T) {
    57  	ctx := setupTest(t)
    58  
    59  	t.Parallel()
    60  	client := testEnv.APIClient()
    61  
    62  	const repoName = "busybox:latest"
    63  	img, _, err := client.ImageInspectWithRaw(ctx, repoName)
    64  	assert.NilError(t, err)
    65  
    66  	rdr, err := client.ImageSave(ctx, []string{repoName})
    67  	assert.NilError(t, err)
    68  
    69  	tarfs := tarIndexFS(t, rdr)
    70  
    71  	dt, err := fs.ReadFile(tarfs, "manifest.json")
    72  	assert.NilError(t, err)
    73  
    74  	var ls []imageSaveManifestEntry
    75  	assert.NilError(t, json.Unmarshal(dt, &ls))
    76  	assert.Assert(t, cmp.Len(ls, 1))
    77  
    78  	info, err := fs.Stat(tarfs, ls[0].Config)
    79  	assert.NilError(t, err)
    80  
    81  	created, err := time.Parse(time.RFC3339, img.Created)
    82  	assert.NilError(t, err)
    83  
    84  	if testEnv.UsingSnapshotter() {
    85  		// containerd archive export sets the mod time to zero.
    86  		assert.Check(t, is.Equal(info.ModTime(), time.Unix(0, 0)))
    87  	} else {
    88  		assert.Check(t, is.Equal(info.ModTime().Format(time.RFC3339), created.Format(time.RFC3339)))
    89  	}
    90  }
    91  
    92  // Regression test for https://github.com/moby/moby/issues/47065
    93  func TestSaveOCI(t *testing.T) {
    94  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.44"), "OCI layout support was introduced in v25")
    95  
    96  	ctx := setupTest(t)
    97  	client := testEnv.APIClient()
    98  
    99  	const busybox = "busybox:latest"
   100  	inspectBusybox, _, err := client.ImageInspectWithRaw(ctx, busybox)
   101  	assert.NilError(t, err)
   102  
   103  	type testCase struct {
   104  		image                 string
   105  		expectedOCIRef        string
   106  		expectedContainerdRef string
   107  	}
   108  
   109  	testCases := []testCase{
   110  		// Busybox by tagged name
   111  		testCase{image: busybox, expectedContainerdRef: "docker.io/library/busybox:latest", expectedOCIRef: "latest"},
   112  
   113  		// Busybox by ID
   114  		testCase{image: inspectBusybox.ID},
   115  	}
   116  
   117  	if testEnv.DaemonInfo.OSType != "windows" {
   118  		multiLayerImage := specialimage.Load(ctx, t, client, specialimage.MultiLayer)
   119  		// Multi-layer image
   120  		testCases = append(testCases, testCase{image: multiLayerImage, expectedContainerdRef: "docker.io/library/multilayer:latest", expectedOCIRef: "latest"})
   121  
   122  	}
   123  
   124  	// Busybox frozen image will have empty RepoDigests when loaded into the
   125  	// graphdriver image store so we can't use it.
   126  	// This will work with the containerd image store though.
   127  	if len(inspectBusybox.RepoDigests) > 0 {
   128  		// Digested reference
   129  		testCases = append(testCases, testCase{
   130  			image: inspectBusybox.RepoDigests[0],
   131  		})
   132  	}
   133  
   134  	for _, tc := range testCases {
   135  		tc := tc
   136  		t.Run(tc.image, func(t *testing.T) {
   137  			// Get information about the original image.
   138  			inspect, _, err := client.ImageInspectWithRaw(ctx, tc.image)
   139  			assert.NilError(t, err)
   140  
   141  			rdr, err := client.ImageSave(ctx, []string{tc.image})
   142  			assert.NilError(t, err)
   143  			defer rdr.Close()
   144  
   145  			tarfs := tarIndexFS(t, rdr)
   146  
   147  			indexData, err := fs.ReadFile(tarfs, "index.json")
   148  			assert.NilError(t, err, "failed to read index.json")
   149  
   150  			var index ocispec.Index
   151  			assert.NilError(t, json.Unmarshal(indexData, &index), "failed to unmarshal index.json")
   152  
   153  			// All test images are single-platform, so they should have only one manifest.
   154  			assert.Assert(t, is.Len(index.Manifests, 1))
   155  
   156  			manifestData, err := fs.ReadFile(tarfs, "blobs/sha256/"+index.Manifests[0].Digest.Encoded())
   157  			assert.NilError(t, err)
   158  
   159  			var manifest ocispec.Manifest
   160  			assert.NilError(t, json.Unmarshal(manifestData, &manifest))
   161  
   162  			t.Run("Manifest", func(t *testing.T) {
   163  				assert.Check(t, is.Len(manifest.Layers, len(inspect.RootFS.Layers)))
   164  
   165  				var digests []string
   166  				// Check if layers referenced by the manifest exist in the archive
   167  				// and match the layers from the original image.
   168  				for _, l := range manifest.Layers {
   169  					layerPath := "blobs/sha256/" + l.Digest.Encoded()
   170  					stat, err := fs.Stat(tarfs, layerPath)
   171  					assert.NilError(t, err)
   172  
   173  					assert.Check(t, is.Equal(l.Size, stat.Size()))
   174  
   175  					f, err := tarfs.Open(layerPath)
   176  					assert.NilError(t, err)
   177  
   178  					layerDigest, err := testutils.UncompressedTarDigest(f)
   179  					f.Close()
   180  
   181  					assert.NilError(t, err)
   182  
   183  					digests = append(digests, layerDigest.String())
   184  				}
   185  
   186  				assert.Check(t, is.DeepEqual(digests, inspect.RootFS.Layers))
   187  			})
   188  
   189  			t.Run("Config", func(t *testing.T) {
   190  				configData, err := fs.ReadFile(tarfs, "blobs/sha256/"+manifest.Config.Digest.Encoded())
   191  				assert.NilError(t, err)
   192  
   193  				var config ocispec.Image
   194  				assert.NilError(t, json.Unmarshal(configData, &config))
   195  
   196  				var diffIDs []string
   197  				for _, l := range config.RootFS.DiffIDs {
   198  					diffIDs = append(diffIDs, l.String())
   199  				}
   200  
   201  				assert.Check(t, is.DeepEqual(diffIDs, inspect.RootFS.Layers))
   202  			})
   203  
   204  			t.Run("Containerd image name", func(t *testing.T) {
   205  				assert.Check(t, is.Equal(index.Manifests[0].Annotations["io.containerd.image.name"], tc.expectedContainerdRef))
   206  			})
   207  
   208  			t.Run("OCI reference tag", func(t *testing.T) {
   209  				assert.Check(t, is.Equal(index.Manifests[0].Annotations["org.opencontainers.image.ref.name"], tc.expectedOCIRef))
   210  			})
   211  
   212  		})
   213  	}
   214  }
   215  
   216  func TestSaveRepoWithMultipleImages(t *testing.T) {
   217  	ctx := setupTest(t)
   218  	client := testEnv.APIClient()
   219  
   220  	makeImage := func(from string, tag string) string {
   221  		id := container.Create(ctx, t, client, func(cfg *container.TestContainerConfig) {
   222  			cfg.Config.Image = from
   223  			cfg.Config.Cmd = []string{"true"}
   224  		})
   225  
   226  		res, err := client.ContainerCommit(ctx, id, containertypes.CommitOptions{Reference: tag})
   227  		assert.NilError(t, err)
   228  
   229  		err = client.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true})
   230  		assert.NilError(t, err)
   231  
   232  		return res.ID
   233  	}
   234  
   235  	busyboxImg, _, err := client.ImageInspectWithRaw(ctx, "busybox:latest")
   236  	assert.NilError(t, err)
   237  
   238  	const repoName = "foobar-save-multi-images-test"
   239  	const tagFoo = repoName + ":foo"
   240  	const tagBar = repoName + ":bar"
   241  
   242  	idFoo := makeImage("busybox:latest", tagFoo)
   243  	idBar := makeImage("busybox:latest", tagBar)
   244  	idBusybox := busyboxImg.ID
   245  
   246  	rdr, err := client.ImageSave(ctx, []string{repoName, "busybox:latest"})
   247  	assert.NilError(t, err)
   248  	defer rdr.Close()
   249  
   250  	tarfs := tarIndexFS(t, rdr)
   251  
   252  	dt, err := fs.ReadFile(tarfs, "manifest.json")
   253  	assert.NilError(t, err)
   254  
   255  	var mfstLs []imageSaveManifestEntry
   256  	assert.NilError(t, json.Unmarshal(dt, &mfstLs))
   257  
   258  	actual := make([]string, 0, len(mfstLs))
   259  	for _, m := range mfstLs {
   260  		actual = append(actual, strings.TrimPrefix(m.Config, "blobs/sha256/"))
   261  		// make sure the blob actually exists
   262  		_, err = fs.Stat(tarfs, m.Config)
   263  		assert.Check(t, err)
   264  	}
   265  
   266  	expected := []string{idBusybox, idFoo, idBar}
   267  	// prefixes are not in tar
   268  	for i := range expected {
   269  		expected[i] = digest.Digest(expected[i]).Encoded()
   270  	}
   271  
   272  	// With snapshotters, ID of the image is the ID of the manifest/index
   273  	// With graphdrivers, ID of the image is the ID of the image config
   274  	if testEnv.UsingSnapshotter() {
   275  		// ID of image won't match the Config ID from manifest.json
   276  		// Just check if manifests exist in blobs
   277  		for _, blob := range expected {
   278  			_, err = fs.Stat(tarfs, "blobs/sha256/"+blob)
   279  			assert.Check(t, err)
   280  		}
   281  	} else {
   282  		sort.Strings(actual)
   283  		sort.Strings(expected)
   284  		assert.Assert(t, cmp.DeepEqual(actual, expected), "archive does not contains the right layers: got %v, expected %v", actual, expected)
   285  	}
   286  }
   287  
   288  func TestSaveDirectoryPermissions(t *testing.T) {
   289  	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "Test is looking at linux specific details")
   290  
   291  	ctx := setupTest(t)
   292  	client := testEnv.APIClient()
   293  
   294  	layerEntries := []string{"opt/", "opt/a/", "opt/a/b/", "opt/a/b/c"}
   295  	layerEntriesAUFS := []string{"./", ".wh..wh.aufs", ".wh..wh.orph/", ".wh..wh.plnk/", "opt/", "opt/a/", "opt/a/b/", "opt/a/b/c"}
   296  
   297  	dockerfile := `FROM busybox
   298  RUN adduser -D user && mkdir -p /opt/a/b && chown -R user:user /opt/a
   299  RUN touch /opt/a/b/c && chown user:user /opt/a/b/c`
   300  
   301  	imgID := build.Do(ctx, t, client, fakecontext.New(t, t.TempDir(), fakecontext.WithDockerfile(dockerfile)))
   302  
   303  	rdr, err := client.ImageSave(ctx, []string{imgID})
   304  	assert.NilError(t, err)
   305  	defer rdr.Close()
   306  
   307  	tarfs := tarIndexFS(t, rdr)
   308  
   309  	dt, err := fs.ReadFile(tarfs, "manifest.json")
   310  	assert.NilError(t, err)
   311  
   312  	var mfstLs []imageSaveManifestEntry
   313  	assert.NilError(t, json.Unmarshal(dt, &mfstLs))
   314  
   315  	var found bool
   316  
   317  	for _, p := range mfstLs[0].Layers {
   318  		var entriesSansDev []string
   319  
   320  		f, err := tarfs.Open(p)
   321  		assert.NilError(t, err)
   322  
   323  		entries, err := listTar(f)
   324  		f.Close()
   325  		assert.NilError(t, err)
   326  
   327  		for _, e := range entries {
   328  			if !strings.Contains(e, "dev/") {
   329  				entriesSansDev = append(entriesSansDev, e)
   330  			}
   331  		}
   332  		assert.NilError(t, err, "encountered error while listing tar entries: %s", err)
   333  
   334  		if reflect.DeepEqual(entriesSansDev, layerEntries) || reflect.DeepEqual(entriesSansDev, layerEntriesAUFS) {
   335  			found = true
   336  			break
   337  		}
   338  	}
   339  
   340  	assert.Assert(t, found, "failed to find the layer with the right content listing")
   341  }
   342  
   343  func listTar(f io.Reader) ([]string, error) {
   344  	// If using the containerd snapshotter, the tar file may be compressed
   345  	dec, err := archive.DecompressStream(f)
   346  	if err != nil {
   347  		return nil, err
   348  	}
   349  	defer dec.Close()
   350  
   351  	tr := tar.NewReader(dec)
   352  	var entries []string
   353  
   354  	for {
   355  		th, err := tr.Next()
   356  		if err == io.EOF {
   357  			// end of tar archive
   358  			return entries, nil
   359  		}
   360  		if err != nil {
   361  			return entries, err
   362  		}
   363  		entries = append(entries, th.Name)
   364  	}
   365  }