github.com/khulnasoft-lab/khulnasoft@v26.0.1-0.20240328202558-330a6f959fe0+incompatible/integration-cli/docker_cli_push_test.go (about)

     1  package main
     2  
     3  import (
     4  	"archive/tar"
     5  	"context"
     6  	"fmt"
     7  	"net/http"
     8  	"net/http/httptest"
     9  	"os"
    10  	"strings"
    11  	"sync"
    12  	"testing"
    13  
    14  	"github.com/distribution/reference"
    15  	"github.com/docker/docker/api/types/versions"
    16  	"github.com/docker/docker/integration-cli/cli"
    17  	"github.com/docker/docker/integration-cli/cli/build"
    18  	"gotest.tools/v3/assert"
    19  	is "gotest.tools/v3/assert/cmp"
    20  	"gotest.tools/v3/icmd"
    21  )
    22  
    23  type DockerCLIPushSuite struct {
    24  	ds *DockerSuite
    25  }
    26  
    27  func (s *DockerCLIPushSuite) TearDownTest(ctx context.Context, c *testing.T) {
    28  	s.ds.TearDownTest(ctx, c)
    29  }
    30  
    31  func (s *DockerCLIPushSuite) OnTimeout(c *testing.T) {
    32  	s.ds.OnTimeout(c)
    33  }
    34  
    35  func (s *DockerRegistrySuite) TestPushBusyboxImage(c *testing.T) {
    36  	const imgRepo = privateRegistryURL + "/dockercli/busybox"
    37  	// tag the image to upload it to the private registry
    38  	cli.DockerCmd(c, "tag", "busybox", imgRepo)
    39  	// push the image to the registry
    40  	cli.DockerCmd(c, "push", imgRepo)
    41  }
    42  
    43  // pushing an image without a prefix should throw an error
    44  func (s *DockerCLIPushSuite) TestPushUnprefixedRepo(c *testing.T) {
    45  	out, _, err := dockerCmdWithError("push", "busybox")
    46  	assert.ErrorContains(c, err, "", "pushing an unprefixed repo didn't result in a non-zero exit status: %s", out)
    47  }
    48  
    49  func (s *DockerRegistrySuite) TestPushUntagged(c *testing.T) {
    50  	const imgRepo = privateRegistryURL + "/dockercli/busybox"
    51  
    52  	out, _, err := dockerCmdWithError("push", imgRepo)
    53  	assert.ErrorContains(c, err, "", "pushing the image to the private registry should have failed: output %q", out)
    54  	const expected = "An image does not exist locally with the tag"
    55  	assert.Assert(c, strings.Contains(out, expected), "pushing the image failed")
    56  }
    57  
    58  func (s *DockerRegistrySuite) TestPushBadTag(c *testing.T) {
    59  	const imgRepo = privateRegistryURL + "/dockercli/busybox:latest"
    60  
    61  	out, _, err := dockerCmdWithError("push", imgRepo)
    62  	assert.ErrorContains(c, err, "", "pushing the image to the private registry should have failed: output %q", out)
    63  	const expected = "does not exist"
    64  	assert.Assert(c, strings.Contains(out, expected), "pushing the image failed")
    65  }
    66  
    67  func (s *DockerRegistrySuite) TestPushMultipleTags(c *testing.T) {
    68  	const imgRepo = privateRegistryURL + "/dockercli/busybox"
    69  	const repoTag1 = imgRepo + ":t1"
    70  	const repoTag2 = imgRepo + ":t2"
    71  	// tag the image and upload it to the private registry
    72  	cli.DockerCmd(c, "tag", "busybox", repoTag1)
    73  	cli.DockerCmd(c, "tag", "busybox", repoTag2)
    74  
    75  	args := []string{"push"}
    76  	if versions.GreaterThanOrEqualTo(DockerCLIVersion(c), "20.10.0") {
    77  		// 20.10 CLI removed implicit push all tags and requires the "--all" flag
    78  		args = append(args, "--all-tags")
    79  	}
    80  	args = append(args, imgRepo)
    81  
    82  	cli.DockerCmd(c, args...)
    83  
    84  	imageAlreadyExists := ": Image already exists"
    85  
    86  	// Ensure layer list is equivalent for repoTag1 and repoTag2
    87  	out1 := cli.DockerCmd(c, "push", repoTag1).Combined()
    88  	var out1Lines []string
    89  	for _, outputLine := range strings.Split(out1, "\n") {
    90  		if strings.Contains(outputLine, imageAlreadyExists) {
    91  			out1Lines = append(out1Lines, outputLine)
    92  		}
    93  	}
    94  
    95  	out2 := cli.DockerCmd(c, "push", repoTag2).Combined()
    96  	var out2Lines []string
    97  	for _, outputLine := range strings.Split(out2, "\n") {
    98  		if strings.Contains(outputLine, imageAlreadyExists) {
    99  			out2Lines = append(out2Lines, outputLine)
   100  		}
   101  	}
   102  	assert.DeepEqual(c, out1Lines, out2Lines)
   103  }
   104  
   105  func (s *DockerRegistrySuite) TestPushEmptyLayer(c *testing.T) {
   106  	const imgRepo = privateRegistryURL + "/dockercli/emptylayer"
   107  
   108  	emptyTarball, err := os.CreateTemp("", "empty_tarball")
   109  	assert.NilError(c, err, "Unable to create test file")
   110  
   111  	tw := tar.NewWriter(emptyTarball)
   112  	err = tw.Close()
   113  	assert.NilError(c, err, "Error creating empty tarball")
   114  
   115  	freader, err := os.Open(emptyTarball.Name())
   116  	assert.NilError(c, err, "Could not open test tarball")
   117  	defer freader.Close()
   118  
   119  	icmd.RunCmd(icmd.Cmd{
   120  		Command: []string{dockerBinary, "import", "-", imgRepo},
   121  		Stdin:   freader,
   122  	}).Assert(c, icmd.Success)
   123  
   124  	// Now verify we can push it
   125  	out, _, err := dockerCmdWithError("push", imgRepo)
   126  	assert.NilError(c, err, "pushing the image to the private registry has failed: %s", out)
   127  }
   128  
   129  // TestConcurrentPush pushes multiple tags to the same repo
   130  // concurrently.
   131  func (s *DockerRegistrySuite) TestConcurrentPush(c *testing.T) {
   132  	const imgRepo = privateRegistryURL + "/dockercli/busybox"
   133  
   134  	var repos []string
   135  	for _, tag := range []string{"push1", "push2", "push3"} {
   136  		repo := fmt.Sprintf("%v:%v", imgRepo, tag)
   137  		buildImageSuccessfully(c, repo, build.WithDockerfile(fmt.Sprintf(`
   138  	FROM busybox
   139  	ENTRYPOINT ["/bin/echo"]
   140  	ENV FOO foo
   141  	ENV BAR bar
   142  	CMD echo %s
   143  `, repo)))
   144  		repos = append(repos, repo)
   145  	}
   146  
   147  	// Push tags, in parallel
   148  	results := make(chan error, len(repos))
   149  
   150  	for _, repo := range repos {
   151  		go func(repo string) {
   152  			result := icmd.RunCommand(dockerBinary, "push", repo)
   153  			results <- result.Error
   154  		}(repo)
   155  	}
   156  
   157  	for range repos {
   158  		err := <-results
   159  		assert.NilError(c, err, "concurrent push failed with error: %v", err)
   160  	}
   161  
   162  	// Clear local images store.
   163  	args := append([]string{"rmi"}, repos...)
   164  	cli.DockerCmd(c, args...)
   165  
   166  	// Re-pull and run individual tags, to make sure pushes succeeded
   167  	for _, repo := range repos {
   168  		cli.DockerCmd(c, "pull", repo)
   169  		cli.DockerCmd(c, "inspect", repo)
   170  		out := cli.DockerCmd(c, "run", "--rm", repo).Combined()
   171  		assert.Equal(c, strings.TrimSpace(out), "/bin/sh -c echo "+repo)
   172  	}
   173  }
   174  
   175  func (s *DockerRegistrySuite) TestCrossRepositoryLayerPush(c *testing.T) {
   176  	const sourceRepoName = privateRegistryURL + "/crossrepopush/busybox"
   177  
   178  	// tag the image to upload it to the private registry
   179  	cli.DockerCmd(c, "tag", "busybox", sourceRepoName)
   180  	// push the image to the registry
   181  	out1, _, err := dockerCmdWithError("push", sourceRepoName)
   182  	assert.NilError(c, err, "pushing the image to the private registry has failed: %s", out1)
   183  	// ensure that none of the layers were mounted from another repository during push
   184  	assert.Assert(c, !strings.Contains(out1, "Mounted from"))
   185  
   186  	digest1 := reference.DigestRegexp.FindString(out1)
   187  	assert.Assert(c, len(digest1) > 0, "no digest found for pushed manifest")
   188  
   189  	const destRepoName = privateRegistryURL + "/crossrepopush/img"
   190  
   191  	// retag the image to upload the same layers to another repo in the same registry
   192  	cli.DockerCmd(c, "tag", "busybox", destRepoName)
   193  	// push the image to the registry
   194  	out2, _, err := dockerCmdWithError("push", destRepoName)
   195  	assert.NilError(c, err, "pushing the image to the private registry has failed: %s", out2)
   196  
   197  	// ensure that layers were mounted from the first repo during push
   198  	assert.Assert(c, strings.Contains(out2, "Mounted from crossrepopush/busybox"))
   199  
   200  	digest2 := reference.DigestRegexp.FindString(out2)
   201  	assert.Assert(c, len(digest2) > 0, "no digest found for pushed manifest")
   202  	assert.Equal(c, digest1, digest2)
   203  
   204  	// ensure that pushing again produces the same digest
   205  	out3, _, err := dockerCmdWithError("push", destRepoName)
   206  	assert.NilError(c, err, "pushing the image to the private registry has failed: %s", out3)
   207  
   208  	digest3 := reference.DigestRegexp.FindString(out3)
   209  	assert.Assert(c, len(digest3) > 0, "no digest found for pushed manifest")
   210  	assert.Equal(c, digest3, digest2)
   211  
   212  	// ensure that we can pull and run the cross-repo-pushed repository
   213  	cli.DockerCmd(c, "rmi", destRepoName)
   214  	cli.DockerCmd(c, "pull", destRepoName)
   215  	out4 := cli.DockerCmd(c, "run", destRepoName, "echo", "-n", "hello world").Combined()
   216  	assert.Equal(c, out4, "hello world")
   217  }
   218  
   219  func (s *DockerRegistryAuthHtpasswdSuite) TestPushNoCredentialsNoRetry(c *testing.T) {
   220  	const imgRepo = privateRegistryURL + "/busybox"
   221  	cli.DockerCmd(c, "tag", "busybox", imgRepo)
   222  	out, _, err := dockerCmdWithError("push", imgRepo)
   223  	assert.ErrorContains(c, err, "", out)
   224  	assert.Assert(c, !strings.Contains(out, "Retrying"))
   225  	assert.Assert(c, strings.Contains(out, "no basic auth credentials"))
   226  }
   227  
   228  // This may be flaky but it's needed not to regress on unauthorized push, see #21054
   229  func (s *DockerCLIPushSuite) TestPushToCentralRegistryUnauthorized(c *testing.T) {
   230  	testRequires(c, Network)
   231  
   232  	const imgRepo = "test/busybox"
   233  	cli.DockerCmd(c, "tag", "busybox", imgRepo)
   234  	out, _, err := dockerCmdWithError("push", imgRepo)
   235  	assert.ErrorContains(c, err, "", out)
   236  	assert.Assert(c, !strings.Contains(out, "Retrying"))
   237  }
   238  
   239  func getTestTokenService(status int, body string, retries int) *httptest.Server {
   240  	var mu sync.Mutex
   241  	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   242  		mu.Lock()
   243  		if retries > 0 {
   244  			w.Header().Set("Content-Type", "application/json")
   245  			w.WriteHeader(http.StatusServiceUnavailable)
   246  			w.Write([]byte(`{"errors":[{"code":"UNAVAILABLE","message":"cannot create token at this time"}]}`))
   247  			retries--
   248  		} else {
   249  			w.Header().Set("Content-Type", "application/json")
   250  			w.WriteHeader(status)
   251  			w.Write([]byte(body))
   252  		}
   253  		mu.Unlock()
   254  	}))
   255  }
   256  
   257  func (s *DockerRegistryAuthTokenSuite) TestPushTokenServiceUnauthResponse(c *testing.T) {
   258  	ts := getTestTokenService(http.StatusUnauthorized, `{"errors": [{"Code":"UNAUTHORIZED", "message": "a message", "detail": null}]}`, 0)
   259  	defer ts.Close()
   260  	s.setupRegistryWithTokenService(c, ts.URL)
   261  
   262  	const imgRepo = privateRegistryURL + "/busybox"
   263  	cli.DockerCmd(c, "tag", "busybox", imgRepo)
   264  	out, _, err := dockerCmdWithError("push", imgRepo)
   265  	assert.ErrorContains(c, err, "", out)
   266  
   267  	assert.Check(c, !strings.Contains(out, "Retrying"))
   268  
   269  	// Auth service errors are not part of the spec and containerd doesn't parse them.
   270  	if testEnv.UsingSnapshotter() {
   271  		assert.Check(c, is.Contains(out, "failed to authorize: failed to fetch anonymous token"))
   272  		assert.Check(c, is.Contains(out, "401 Unauthorized"))
   273  	} else {
   274  		assert.Check(c, is.Contains(out, "unauthorized: a message"))
   275  	}
   276  }
   277  
   278  func (s *DockerRegistryAuthTokenSuite) TestPushMisconfiguredTokenServiceResponseUnauthorized(c *testing.T) {
   279  	ts := getTestTokenService(http.StatusUnauthorized, `{"error": "unauthorized"}`, 0)
   280  	defer ts.Close()
   281  	s.setupRegistryWithTokenService(c, ts.URL)
   282  
   283  	const imgRepo = privateRegistryURL + "/busybox"
   284  	cli.DockerCmd(c, "tag", "busybox", imgRepo)
   285  	out, _, err := dockerCmdWithError("push", imgRepo)
   286  	assert.ErrorContains(c, err, "", out)
   287  	assert.Assert(c, !strings.Contains(out, "Retrying"))
   288  
   289  	// Auth service errors are not part of the spec and containerd doesn't parse them.
   290  	if testEnv.UsingSnapshotter() {
   291  		assert.Check(c, is.Contains(out, "failed to authorize: failed to fetch anonymous token"))
   292  		assert.Check(c, is.Contains(out, "401 Unauthorized"))
   293  	} else {
   294  		split := strings.Split(out, "\n")
   295  		assert.Check(c, is.Contains(split[len(split)-2], "unauthorized: authentication required"))
   296  	}
   297  }
   298  
   299  func (s *DockerRegistryAuthTokenSuite) TestPushMisconfiguredTokenServiceResponseError(c *testing.T) {
   300  	ts := getTestTokenService(http.StatusTooManyRequests, `{"errors": [{"code":"TOOMANYREQUESTS","message":"out of tokens"}]}`, 3)
   301  	defer ts.Close()
   302  	s.setupRegistryWithTokenService(c, ts.URL)
   303  
   304  	const imgRepo = privateRegistryURL + "/busybox"
   305  	cli.DockerCmd(c, "tag", "busybox", imgRepo)
   306  	out, _, err := dockerCmdWithError("push", imgRepo)
   307  	assert.ErrorContains(c, err, "", out)
   308  	// TODO: isolate test so that it can be guaranteed that the 503 will trigger xfer retries
   309  	// assert.Assert(c, strings.Contains(out, "Retrying"))
   310  	// assert.Assert(c, !strings.Contains(out, "Retrying in 15"))
   311  
   312  	// Auth service errors are not part of the spec and containerd doesn't parse them.
   313  	if testEnv.UsingSnapshotter() {
   314  		assert.Check(c, is.Contains(out, "failed to authorize: failed to fetch anonymous token"))
   315  		assert.Check(c, is.Contains(out, "503 Service Unavailable"))
   316  	} else {
   317  		split := strings.Split(out, "\n")
   318  		assert.Check(c, is.Equal(split[len(split)-2], "toomanyrequests: out of tokens"))
   319  	}
   320  }
   321  
   322  func (s *DockerRegistryAuthTokenSuite) TestPushMisconfiguredTokenServiceResponseUnparsable(c *testing.T) {
   323  	ts := getTestTokenService(http.StatusForbidden, `no way`, 0)
   324  	defer ts.Close()
   325  	s.setupRegistryWithTokenService(c, ts.URL)
   326  
   327  	const imgRepo = privateRegistryURL + "/busybox"
   328  	cli.DockerCmd(c, "tag", "busybox", imgRepo)
   329  	out, _, err := dockerCmdWithError("push", imgRepo)
   330  	assert.ErrorContains(c, err, "", out)
   331  	assert.Check(c, !strings.Contains(out, "Retrying"))
   332  
   333  	// Auth service errors are not part of the spec and containerd doesn't parse them.
   334  	if testEnv.UsingSnapshotter() {
   335  		assert.Check(c, is.Contains(out, "failed to authorize: failed to fetch anonymous token"))
   336  		assert.Check(c, is.Contains(out, "403 Forbidden"))
   337  	} else {
   338  		split := strings.Split(out, "\n")
   339  		assert.Check(c, is.Contains(split[len(split)-2], "error parsing HTTP 403 response body: "))
   340  	}
   341  }
   342  
   343  func (s *DockerRegistryAuthTokenSuite) TestPushMisconfiguredTokenServiceResponseNoToken(c *testing.T) {
   344  	ts := getTestTokenService(http.StatusOK, `{"something": "wrong"}`, 0)
   345  	defer ts.Close()
   346  	s.setupRegistryWithTokenService(c, ts.URL)
   347  
   348  	const imgRepo = privateRegistryURL + "/busybox"
   349  	cli.DockerCmd(c, "tag", "busybox", imgRepo)
   350  	out, _, err := dockerCmdWithError("push", imgRepo)
   351  	assert.ErrorContains(c, err, "", out)
   352  	assert.Assert(c, !strings.Contains(out, "Retrying"))
   353  	split := strings.Split(out, "\n")
   354  	assert.Check(c, is.Contains(split[len(split)-2], "authorization server did not include a token in the response"))
   355  }