c8d/save-load: Reimplement non-c8d idiosyncrasies
- Fixes: https://github.com/moby/moby/issues/45461

Changes:
1. Implement a behavior from the graphdriver's export where `docker save something` (untagged reference) would export all images matching the specified repository.

2. Don't save image name/reference if it was selected by a digest. This is a bit weird behavior, and we probably should have a flag to allow to opt-out/opt-in, but it seems to be an explicit behavior and there's a test that checks for it (`TestSaveLoadNoTag`).

3. Fix `docker load` message not outputting image tag.



**- How to verify it**
```bash
$ make TEST_FILTER='(TestSave|TestLoad)' \
             TEST_IGNORE_CGROUP_CHECK=1 \
             DOCKER_GRAPHDRIVER=overlayfs \
             TEST_INTEGRATION_USE_SNAPSHOTTER=1 \
             test-integration
```

<details>
<summary>Diff</summary>

```diff
-=== Failed
-=== FAIL: arm64.integration.image TestSaveRepoWithMultipleImages (1.69s)
-    save_test.go:126: assertion failed: error is not nil: Error response from daemon: No such image: foobar-save-multi-images-test:latest
-
=== FAIL: arm64.integration.image TestSaveCheckTimes (0.23s)
    save_test.go:81: assertion failed: 2020-06-02T21:39:45Z (string) != 1970-01-01T00:00:00Z (string): expected: 2020-06-02 21:39:45.374289041 +0000 UTC, actual: 1970-01-01 00:00:00 +0000 UTC
There was a fire near the place I live and I feel as though I have been smoking smuggled russian ciggarettes every day for 50 years.
-DONE 3 tests, 2 failures in 4.721s
+DONE 3 tests, 1 failure in 4.723s
...
-=== Skipped
-=== SKIP: arm64.integration-cli TestDockerSchema1RegistrySuite (0.01s)
-    check_test.go:368: testEnv.UsingSnapshotter()
-
-=== Failed
-=== FAIL: arm64.integration-cli TestDockerCLISaveLoadSuite/TestLoadZeroSizeLayer (0.19s)
-    docker_cli_save_load_test.go:199: assertion failed:
-        Command:  /usr/local/cli-integration/docker load -i testdata/emptyLayer.tar
-        ExitCode: 1
-        Error:    exit status 1
-        Stdout:
-        Stderr:   unrecognized image format
-
-
-        Failures:
-        ExitCode was 1 expected 0
-        Expected no error
-    --- FAIL: TestDockerCLISaveLoadSuite/TestLoadZeroSizeLayer (0.19s)
-
-=== FAIL: arm64.integration-cli TestDockerCLISaveLoadSuite/TestSaveAndLoadRepoFlags (0.84s)
-    docker_cli_save_load_test.go:160: assertion failed:
-        --- before
-        +++ after
-        @@ -71,5 +71,5 @@
-                 },
-                 "Metadata": {
-        -            "LastTagTime": "2023-09-22T11:20:58.556805794Z"
-        +            "LastTagTime": "2023-09-22T11:20:58.64523996Z"
-                 }
-             }
-        : inspect is not the same after a save / load
-    --- FAIL: TestDockerCLISaveLoadSuite/TestSaveAndLoadRepoFlags (0.84s)
-
-=== FAIL: arm64.integration-cli TestDockerCLISaveLoadSuite/TestSaveAndLoadWithProgressBar (4.66s)
-    docker_cli_save_load_unix_test.go:87: assertion failed: expression is false: strings.Contains(out, expected)
-    --- FAIL: TestDockerCLISaveLoadSuite/TestSaveAndLoadWithProgressBar (4.66s)
-
-=== FAIL: arm64.integration-cli TestDockerCLISaveLoadSuite/TestSaveLoadNoTag (0.78s)
-    docker_cli_save_load_test.go:254: assertion failed: strings.Contains(out, "Loaded image: ") is true
-    --- FAIL: TestDockerCLISaveLoadSuite/TestSaveLoadNoTag (0.78s)
-
-=== FAIL: arm64.integration-cli TestDockerCLISaveLoadSuite/TestSaveLoadParents (1.68s)
-    docker_cli_save_load_test.go:224: tmpdir /tmp/save-load-parents1920803891
-    docker_cli_save_load_test.go:233: assertion failed:  (inspectOut string) != sha256:895384f1ece8c022db097c18c965b586b69a52cec7ea3d8ea5d4befe37c1c4a1 (idFoo string)
-    --- FAIL: TestDockerCLISaveLoadSuite/TestSaveLoadParents (1.68s)
-
-=== FAIL: arm64.integration-cli TestDockerCLISaveLoadSuite/TestSaveMultipleNames (0.20s)
-    docker_cli_save_load_test.go:188: assertion failed: error is not nil: exit status 1: failed to save multiple repos: , exit status 1
-    --- FAIL: TestDockerCLISaveLoadSuite/TestSaveMultipleNames (0.20s)
-
-=== FAIL: arm64.integration-cli TestDockerCLISaveLoadSuite/TestSaveSingleTag (0.22s)
-    docker_cli_save_load_test.go:100: assertion failed: error is not nil: exit status 1: failed to save repo with image ID and 'repositories' file: , exit status 1
-    --- FAIL: TestDockerCLISaveLoadSuite/TestSaveSingleTag (0.22s)
-
-=== FAIL: arm64.integration-cli TestDockerCLISaveLoadSuite (11.84s)
-
-DONE 62 tests, 1 skipped, 8 failures in 12.239s
+PASS
+
+=== Skipped
+=== SKIP: arm64.integration-cli TestDockerCLISaveLoadSuite/TestLoadZeroSizeLayer (0.11s)
+    docker_cli_save_load_test.go:230: testEnv.UsingSnapshotter(): input archive is not OCI compatible
+    --- SKIP: TestDockerCLISaveLoadSuite/TestLoadZeroSizeLayer (0.11s)
+
+=== SKIP: arm64.integration-cli TestDockerCLISaveLoadSuite/TestSaveAndLoadWithProgressBar (0.09s)
+    docker_cli_save_load_unix_test.go:76: testEnv.UsingSnapshotter(): TODO: Not implemented yet
+    --- SKIP: TestDockerCLISaveLoadSuite/TestSaveAndLoadWithProgressBar (0.09s)
+
+=== SKIP: arm64.integration-cli TestDockerCLISaveLoadSuite/TestSaveLoadParents (0.10s)
+    docker_cli_save_load_test.go:241: testEnv.UsingSnapshotter(): Parent image property is not supported with containerd
+    --- SKIP: TestDockerCLISaveLoadSuite/TestSaveLoadParents (0.10s)
+
+=== SKIP: arm64.integration-cli TestDockerSchema1RegistrySuite (0.01s)
+    check_test.go:368: testEnv.UsingSnapshotter()
+
+DONE 62 tests, 4 skipped in 10.034s
```

</details>

**- Description for the changelog**
```release-note
- containerd-integration: `docker save` is now able to export images from all tags of the repository
```

**- A picture of a cute animal (not mandatory but encouraged)**


integration-cli/docker_cli_save_load_unix_test.go
//go:build !windows

package main

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"strings"
	"testing"
	"time"

	"github.com/creack/pty"
	"github.com/docker/docker/integration-cli/cli/build"
	"github.com/docker/docker/testutil"
	"gotest.tools/v3/assert"
	"gotest.tools/v3/icmd"
)

// save a repo and try to load it using stdout
func (s *DockerCLISaveLoadSuite) TestSaveAndLoadRepoStdout(c *testing.T) {
	name := "test-save-and-load-repo-stdout"
	dockerCmd(c, "run", "--name", name, "busybox", "true")

	repoName := "foobar-save-load-test"
	before, _ := dockerCmd(c, "commit", name, repoName)
	before = strings.TrimRight(before, "\n")

	tmpFile, err := os.CreateTemp("", "foobar-save-load-test.tar")
	assert.NilError(c, err)
	defer os.Remove(tmpFile.Name())

	icmd.RunCmd(icmd.Cmd{
		Command: []string{dockerBinary, "save", repoName},
		Stdout:  tmpFile,
	}).Assert(c, icmd.Success)

	tmpFile, err = os.Open(tmpFile.Name())
	assert.NilError(c, err)
	defer tmpFile.Close()

	deleteImages(repoName)

	icmd.RunCmd(icmd.Cmd{
		Command: []string{dockerBinary, "load"},
		Stdin:   tmpFile,
	}).Assert(c, icmd.Success)

	after := inspectField(c, repoName, "Id")
	after = strings.TrimRight(after, "\n")

	assert.Equal(c, after, before, "inspect is not the same after a save / load")

	deleteImages(repoName)

	pty, tty, err := pty.Open()
	assert.NilError(c, err)
	cmd := exec.Command(dockerBinary, "save", repoName)
	cmd.Stdin = tty
	cmd.Stdout = tty
	cmd.Stderr = tty
	assert.NilError(c, cmd.Start())
	assert.ErrorContains(c, cmd.Wait(), "", "did not break writing to a TTY")

	buf := make([]byte, 1024)

	n, err := pty.Read(buf)
	assert.NilError(c, err, "could not read tty output")
	assert.Assert(c, strings.Contains(string(buf[:n]), "cowardly refusing"), "help output is not being yielded")
}

func (s *DockerCLISaveLoadSuite) TestSaveAndLoadWithProgressBar(c *testing.T) {
	name := "test-load"
	buildImageSuccessfully(c, name, build.WithDockerfile(`FROM busybox
	RUN touch aa
	`))

	tmptar := name + ".tar"
	dockerCmd(c, "save", "-o", tmptar, name)
	defer os.Remove(tmptar)

	dockerCmd(c, "rmi", name)
	dockerCmd(c, "tag", "busybox", name)
	out, _ := dockerCmd(c, "load", "-i", tmptar)
	expected := fmt.Sprintf("The image %s:latest already exists, renaming the old one with ID", name)
	assert.Assert(c, strings.Contains(out, expected))
}

// fail because load didn't receive data from stdin
func (s *DockerCLISaveLoadSuite) TestLoadNoStdinFail(c *testing.T) {
	pty, tty, err := pty.Open()
	assert.NilError(c, err)
	ctx, cancel := context.WithTimeout(testutil.GetContext(c), 5*time.Second)
	defer cancel()
	cmd := exec.CommandContext(ctx, dockerBinary, "load")
	cmd.Stdin = tty
	cmd.Stdout = tty
	cmd.Stderr = tty
	assert.ErrorContains(c, cmd.Run(), "", "docker-load should fail")

	buf := make([]byte, 1024)

	n, err := pty.Read(buf)
	assert.NilError(c, err) // could not read tty output
	assert.Assert(c, strings.Contains(string(buf[:n]), "requested load from stdin, but stdin is empty"))
}

//go:build !windows

package main

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"strings"
	"testing"
	"time"

	"github.com/creack/pty"
	"github.com/docker/docker/integration-cli/cli/build"
	"github.com/docker/docker/testutil"
	"gotest.tools/v3/assert"
	"gotest.tools/v3/icmd"
	"gotest.tools/v3/skip"
)

// save a repo and try to load it using stdout
func (s *DockerCLISaveLoadSuite) TestSaveAndLoadRepoStdout(c *testing.T) {
	name := "test-save-and-load-repo-stdout"
	dockerCmd(c, "run", "--name", name, "busybox", "true")

	repoName := "foobar-save-load-test"
	before, _ := dockerCmd(c, "commit", name, repoName)
	before = strings.TrimRight(before, "\n")

	tmpFile, err := os.CreateTemp("", "foobar-save-load-test.tar")
	assert.NilError(c, err)
	defer os.Remove(tmpFile.Name())

	icmd.RunCmd(icmd.Cmd{
		Command: []string{dockerBinary, "save", repoName},
		Stdout:  tmpFile,
	}).Assert(c, icmd.Success)

	tmpFile, err = os.Open(tmpFile.Name())
	assert.NilError(c, err)
	defer tmpFile.Close()

	deleteImages(repoName)

	icmd.RunCmd(icmd.Cmd{
		Command: []string{dockerBinary, "load"},
		Stdin:   tmpFile,
	}).Assert(c, icmd.Success)

	after := inspectField(c, repoName, "Id")
	after = strings.TrimRight(after, "\n")

	assert.Equal(c, after, before, "inspect is not the same after a save / load")

	deleteImages(repoName)

	pty, tty, err := pty.Open()
	assert.NilError(c, err)
	cmd := exec.Command(dockerBinary, "save", repoName)
	cmd.Stdin = tty
	cmd.Stdout = tty
	cmd.Stderr = tty
	assert.NilError(c, cmd.Start())
	assert.ErrorContains(c, cmd.Wait(), "", "did not break writing to a TTY")

	buf := make([]byte, 1024)

	n, err := pty.Read(buf)
	assert.NilError(c, err, "could not read tty output")
	assert.Assert(c, strings.Contains(string(buf[:n]), "cowardly refusing"), "help output is not being yielded")
}

func (s *DockerCLISaveLoadSuite) TestSaveAndLoadWithProgressBar(c *testing.T) {
	// TODO(vvoland): https://github.com/moby/moby/issues/43910
	skip.If(c, testEnv.UsingSnapshotter(), "TODO: Not implemented yet")

	name := "test-load"
	buildImageSuccessfully(c, name, build.WithDockerfile(`FROM busybox
	RUN touch aa
	`))

	tmptar := name + ".tar"
	dockerCmd(c, "save", "-o", tmptar, name)
	defer os.Remove(tmptar)

	dockerCmd(c, "rmi", name)
	dockerCmd(c, "tag", "busybox", name)
	out, _ := dockerCmd(c, "load", "-i", tmptar)
	expected := fmt.Sprintf("The image %s:latest already exists, renaming the old one with ID", name)
	assert.Assert(c, strings.Contains(out, expected))
}

// fail because load didn't receive data from stdin
func (s *DockerCLISaveLoadSuite) TestLoadNoStdinFail(c *testing.T) {
	pty, tty, err := pty.Open()
	assert.NilError(c, err)
	ctx, cancel := context.WithTimeout(testutil.GetContext(c), 5*time.Second)
	defer cancel()
	cmd := exec.CommandContext(ctx, dockerBinary, "load")
	cmd.Stdin = tty
	cmd.Stdout = tty
	cmd.Stderr = tty
	assert.ErrorContains(c, cmd.Run(), "", "docker-load should fail")

	buf := make([]byte, 1024)

	n, err := pty.Read(buf)
	assert.NilError(c, err) // could not read tty output
	assert.Assert(c, strings.Contains(string(buf[:n]), "requested load from stdin, but stdin is empty"))
}

Added a skip, because it's just a visual nuisance which doesn't affect the save/load itself and we have a tracking issue for  it.

Let me know if you think we should leave this one failing until we really fix it.