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