github.com/moby/docker@v26.1.3+incompatible/integration-cli/docker_cli_save_load_test.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/docker/docker/api/types"
    14  	"github.com/docker/docker/integration-cli/cli"
    15  	"github.com/docker/docker/integration-cli/cli/build"
    16  	"github.com/docker/docker/internal/testutils/specialimage"
    17  	"gotest.tools/v3/assert"
    18  	is "gotest.tools/v3/assert/cmp"
    19  	"gotest.tools/v3/icmd"
    20  	"gotest.tools/v3/skip"
    21  )
    22  
    23  type DockerCLISaveLoadSuite struct {
    24  	ds *DockerSuite
    25  }
    26  
    27  func (s *DockerCLISaveLoadSuite) TearDownTest(ctx context.Context, c *testing.T) {
    28  	s.ds.TearDownTest(ctx, c)
    29  }
    30  
    31  func (s *DockerCLISaveLoadSuite) OnTimeout(c *testing.T) {
    32  	s.ds.OnTimeout(c)
    33  }
    34  
    35  // save a repo using gz compression and try to load it using stdout
    36  func (s *DockerCLISaveLoadSuite) TestSaveXzAndLoadRepoStdout(c *testing.T) {
    37  	testRequires(c, DaemonIsLinux)
    38  	name := "test-save-xz-and-load-repo-stdout"
    39  	cli.DockerCmd(c, "run", "--name", name, "busybox", "true")
    40  
    41  	imgRepoName := "foobar-save-load-test-xz-gz"
    42  	out := cli.DockerCmd(c, "commit", name, imgRepoName).Combined()
    43  
    44  	cli.DockerCmd(c, "inspect", imgRepoName)
    45  
    46  	repoTarball, err := RunCommandPipelineWithOutput(
    47  		exec.Command(dockerBinary, "save", imgRepoName),
    48  		exec.Command("xz", "-c"),
    49  		exec.Command("gzip", "-c"))
    50  	assert.NilError(c, err, "failed to save repo: %v %v", out, err)
    51  	deleteImages(imgRepoName)
    52  
    53  	icmd.RunCmd(icmd.Cmd{
    54  		Command: []string{dockerBinary, "load"},
    55  		Stdin:   strings.NewReader(repoTarball),
    56  	}).Assert(c, icmd.Expected{
    57  		ExitCode: 1,
    58  	})
    59  
    60  	after, _, err := dockerCmdWithError("inspect", imgRepoName)
    61  	assert.ErrorContains(c, err, "", "the repo should not exist: %v", after)
    62  }
    63  
    64  // save a repo using xz+gz compression and try to load it using stdout
    65  func (s *DockerCLISaveLoadSuite) TestSaveXzGzAndLoadRepoStdout(c *testing.T) {
    66  	testRequires(c, DaemonIsLinux)
    67  	name := "test-save-xz-gz-and-load-repo-stdout"
    68  	cli.DockerCmd(c, "run", "--name", name, "busybox", "true")
    69  
    70  	repoName := "foobar-save-load-test-xz-gz"
    71  	cli.DockerCmd(c, "commit", name, repoName)
    72  
    73  	cli.DockerCmd(c, "inspect", repoName)
    74  
    75  	out, err := RunCommandPipelineWithOutput(
    76  		exec.Command(dockerBinary, "save", repoName),
    77  		exec.Command("xz", "-c"),
    78  		exec.Command("gzip", "-c"))
    79  	assert.NilError(c, err, "failed to save repo: %v %v", out, err)
    80  
    81  	deleteImages(repoName)
    82  
    83  	icmd.RunCmd(icmd.Cmd{
    84  		Command: []string{dockerBinary, "load"},
    85  		Stdin:   strings.NewReader(out),
    86  	}).Assert(c, icmd.Expected{
    87  		ExitCode: 1,
    88  	})
    89  
    90  	after, _, err := dockerCmdWithError("inspect", repoName)
    91  	assert.ErrorContains(c, err, "", "the repo should not exist: %v", after)
    92  }
    93  
    94  func (s *DockerCLISaveLoadSuite) TestSaveSingleTag(c *testing.T) {
    95  	testRequires(c, DaemonIsLinux)
    96  	imgRepoName := "foobar-save-single-tag-test"
    97  	cli.DockerCmd(c, "tag", "busybox:latest", fmt.Sprintf("%v:latest", imgRepoName))
    98  
    99  	out := cli.DockerCmd(c, "images", "-q", "--no-trunc", imgRepoName).Stdout()
   100  	cleanedImageID := strings.TrimSpace(out)
   101  
   102  	filesFilter := fmt.Sprintf("(^manifest.json$|%v)", cleanedImageID)
   103  	if testEnv.UsingSnapshotter() {
   104  		filesFilter = fmt.Sprintf("(^index.json$|^manifest.json$|%v)", cleanedImageID)
   105  	}
   106  	out, err := RunCommandPipelineWithOutput(
   107  		exec.Command(dockerBinary, "save", fmt.Sprintf("%v:latest", imgRepoName)),
   108  		exec.Command("tar", "t"),
   109  		exec.Command("grep", "-E", filesFilter))
   110  	assert.NilError(c, err, "failed to save repo with image ID and index files: %s, %v", out, err)
   111  }
   112  
   113  func (s *DockerCLISaveLoadSuite) TestSaveImageId(c *testing.T) {
   114  	testRequires(c, DaemonIsLinux)
   115  
   116  	emptyFSImage := loadSpecialImage(c, specialimage.EmptyFS)
   117  
   118  	imgRepoName := "foobar-save-image-id-test"
   119  	cli.DockerCmd(c, "tag", emptyFSImage, fmt.Sprintf("%v:latest", imgRepoName))
   120  
   121  	out := cli.DockerCmd(c, "images", "-q", "--no-trunc", imgRepoName).Stdout()
   122  	cleanedLongImageID := strings.TrimPrefix(strings.TrimSpace(out), "sha256:")
   123  
   124  	out = cli.DockerCmd(c, "images", "-q", imgRepoName).Stdout()
   125  	cleanedShortImageID := strings.TrimSpace(out)
   126  
   127  	// Make sure IDs are not empty
   128  	assert.Assert(c, cleanedLongImageID != "", "Id should not be empty.")
   129  	assert.Assert(c, cleanedShortImageID != "", "Id should not be empty.")
   130  
   131  	saveCmd := exec.Command(dockerBinary, "save", cleanedShortImageID)
   132  	tarCmd := exec.Command("tar", "t")
   133  
   134  	var err error
   135  	tarCmd.Stdin, err = saveCmd.StdoutPipe()
   136  	assert.Assert(c, err == nil, "cannot set stdout pipe for tar: %v", err)
   137  	grepCmd := exec.Command("grep", cleanedLongImageID)
   138  	grepCmd.Stdin, err = tarCmd.StdoutPipe()
   139  	assert.Assert(c, err == nil, "cannot set stdout pipe for grep: %v", err)
   140  
   141  	assert.Assert(c, tarCmd.Start() == nil, "tar failed with error: %v", err)
   142  	assert.Assert(c, saveCmd.Start() == nil, "docker save failed with error: %v", err)
   143  	defer func() {
   144  		saveCmd.Wait()
   145  		tarCmd.Wait()
   146  		cli.DockerCmd(c, "rmi", imgRepoName)
   147  	}()
   148  
   149  	out, _, err = runCommandWithOutput(grepCmd)
   150  
   151  	assert.Assert(c, err == nil, "failed to save repo with image ID: %s, %v", out, err)
   152  }
   153  
   154  // save a repo and try to load it using flags
   155  func (s *DockerCLISaveLoadSuite) TestSaveAndLoadRepoFlags(c *testing.T) {
   156  	testRequires(c, DaemonIsLinux)
   157  	const name = "test-save-and-load-repo-flags"
   158  	cli.DockerCmd(c, "run", "--name", name, "busybox", "true")
   159  
   160  	const imgRepoName = "foobar-save-load-test"
   161  
   162  	deleteImages(imgRepoName)
   163  	cli.DockerCmd(c, "commit", name, imgRepoName)
   164  
   165  	beforeStr := cli.DockerCmd(c, "inspect", imgRepoName).Stdout()
   166  
   167  	out, err := RunCommandPipelineWithOutput(
   168  		exec.Command(dockerBinary, "save", imgRepoName),
   169  		exec.Command(dockerBinary, "load"))
   170  	assert.NilError(c, err, "failed to save and load repo: %s, %v", out, err)
   171  
   172  	afterStr := cli.DockerCmd(c, "inspect", imgRepoName).Stdout()
   173  
   174  	var before, after []types.ImageInspect
   175  	err = json.Unmarshal([]byte(beforeStr), &before)
   176  	assert.NilError(c, err, "failed to parse inspect 'before' output")
   177  	err = json.Unmarshal([]byte(afterStr), &after)
   178  	assert.NilError(c, err, "failed to parse inspect 'after' output")
   179  
   180  	assert.Assert(c, is.Len(before, 1))
   181  	assert.Assert(c, is.Len(after, 1))
   182  
   183  	if testEnv.UsingSnapshotter() {
   184  		// Ignore LastTagTime difference with c8d.
   185  		// It is not stored in the image archive, but in the imageStore
   186  		// which is a graphdrivers implementation detail.
   187  		//
   188  		// It works because we load the image into the same daemon which saved
   189  		// the image. It would still fail with the graphdrivers if the image
   190  		// was loaded into a different daemon (which should be the case in a
   191  		// real-world scenario).
   192  		before[0].Metadata.LastTagTime = after[0].Metadata.LastTagTime
   193  	}
   194  
   195  	assert.Check(c, is.DeepEqual(before, after), "inspect is not the same after a save / load")
   196  }
   197  
   198  func (s *DockerCLISaveLoadSuite) TestSaveWithNoExistImage(c *testing.T) {
   199  	testRequires(c, DaemonIsLinux)
   200  
   201  	imgName := "foobar-non-existing-image"
   202  
   203  	out, _, err := dockerCmdWithError("save", "-o", "test-img.tar", imgName)
   204  	assert.ErrorContains(c, err, "", "save image should fail for non-existing image")
   205  	assert.Assert(c, strings.Contains(out, fmt.Sprintf("No such image: %s", imgName)))
   206  }
   207  
   208  func (s *DockerCLISaveLoadSuite) TestSaveMultipleNames(c *testing.T) {
   209  	testRequires(c, DaemonIsLinux)
   210  
   211  	emptyFSImage := loadSpecialImage(c, specialimage.EmptyFS)
   212  
   213  	const imgRepoName = "foobar-save-multi-name-test"
   214  
   215  	oneTag := fmt.Sprintf("%v-one:latest", imgRepoName)
   216  	twoTag := fmt.Sprintf("%v-two:latest", imgRepoName)
   217  
   218  	cli.DockerCmd(c, "tag", emptyFSImage, oneTag)
   219  	cli.DockerCmd(c, "tag", emptyFSImage, twoTag)
   220  
   221  	out, err := RunCommandPipelineWithOutput(
   222  		exec.Command(dockerBinary, "save", strings.TrimSuffix(oneTag, ":latest"), twoTag),
   223  		exec.Command("tar", "xO", "index.json"),
   224  	)
   225  	assert.NilError(c, err, "failed to save multiple repos: %s, %v", out, err)
   226  
   227  	assert.Check(c, is.Contains(out, oneTag))
   228  	assert.Check(c, is.Contains(out, twoTag))
   229  }
   230  
   231  // Test loading a weird image where one of the layers is of zero size.
   232  // The layer.tar file is actually zero bytes, no padding or anything else.
   233  // See issue: 18170
   234  func (s *DockerCLISaveLoadSuite) TestLoadZeroSizeLayer(c *testing.T) {
   235  	// TODO(vvoland): Create an OCI image with 0 bytes layer.
   236  	skip.If(c, testEnv.UsingSnapshotter(), "input archive is not OCI compatible")
   237  
   238  	// this will definitely not work if using remote daemon
   239  	// very weird test
   240  	testRequires(c, DaemonIsLinux, testEnv.IsLocalDaemon)
   241  
   242  	cli.DockerCmd(c, "load", "-i", "testdata/emptyLayer.tar")
   243  }
   244  
   245  func (s *DockerCLISaveLoadSuite) TestSaveLoadParents(c *testing.T) {
   246  	testRequires(c, DaemonIsLinux)
   247  	skip.If(c, testEnv.UsingSnapshotter(), "Parent image property is not supported with containerd")
   248  
   249  	makeImage := func(from string, addfile string) string {
   250  		id := cli.DockerCmd(c, "run", "-d", from, "touch", addfile).Stdout()
   251  		id = strings.TrimSpace(id)
   252  
   253  		imageID := cli.DockerCmd(c, "commit", id).Stdout()
   254  		imageID = strings.TrimSpace(imageID)
   255  
   256  		cli.DockerCmd(c, "rm", "-f", id)
   257  		return imageID
   258  	}
   259  
   260  	idFoo := makeImage("busybox", "foo")
   261  	idBar := makeImage(idFoo, "bar")
   262  
   263  	tmpDir, err := os.MkdirTemp("", "save-load-parents")
   264  	assert.NilError(c, err)
   265  	defer os.RemoveAll(tmpDir)
   266  
   267  	c.Log("tmpdir", tmpDir)
   268  
   269  	outfile := filepath.Join(tmpDir, "out.tar")
   270  
   271  	cli.DockerCmd(c, "save", "-o", outfile, idBar, idFoo)
   272  	cli.DockerCmd(c, "rmi", idBar)
   273  	cli.DockerCmd(c, "load", "-i", outfile)
   274  
   275  	inspectOut := inspectField(c, idBar, "Parent")
   276  	assert.Equal(c, inspectOut, idFoo)
   277  
   278  	inspectOut = inspectField(c, idFoo, "Parent")
   279  	assert.Equal(c, inspectOut, "")
   280  }
   281  
   282  func (s *DockerCLISaveLoadSuite) TestSaveLoadNoTag(c *testing.T) {
   283  	testRequires(c, DaemonIsLinux)
   284  
   285  	name := "saveloadnotag"
   286  
   287  	buildImageSuccessfully(c, name, build.WithDockerfile("FROM busybox\nENV foo=bar"))
   288  	id := inspectField(c, name, "Id")
   289  
   290  	// Test to make sure that save w/o name just shows imageID during load
   291  	out, err := RunCommandPipelineWithOutput(
   292  		exec.Command(dockerBinary, "save", id),
   293  		exec.Command(dockerBinary, "load"))
   294  	assert.NilError(c, err, "failed to save and load repo: %s, %v", out, err)
   295  
   296  	// Should not show 'name' but should show the image ID during the load
   297  	assert.Assert(c, !strings.Contains(out, "Loaded image: "))
   298  	assert.Assert(c, strings.Contains(out, "Loaded image ID:"))
   299  	assert.Assert(c, strings.Contains(out, id))
   300  	// Test to make sure that save by name shows that name during load
   301  	out, err = RunCommandPipelineWithOutput(
   302  		exec.Command(dockerBinary, "save", name),
   303  		exec.Command(dockerBinary, "load"))
   304  	assert.NilError(c, err, "failed to save and load repo: %s, %v", out, err)
   305  
   306  	assert.Assert(c, strings.Contains(out, "Loaded image: "+name+":latest"))
   307  	assert.Assert(c, !strings.Contains(out, "Loaded image ID:"))
   308  }