github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/docker/registry/internal/dockerhub_test.go (about)

     1  // Copyright 2021 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package internal_test
     5  
     6  import (
     7  	"encoding/base64"
     8  	"io"
     9  	"net/http"
    10  	"strings"
    11  
    12  	"github.com/juju/testing"
    13  	jc "github.com/juju/testing/checkers"
    14  	"github.com/juju/version/v2"
    15  	"go.uber.org/mock/gomock"
    16  	gc "gopkg.in/check.v1"
    17  
    18  	"github.com/juju/juju/docker"
    19  	"github.com/juju/juju/docker/registry"
    20  	"github.com/juju/juju/docker/registry/image"
    21  	"github.com/juju/juju/docker/registry/internal"
    22  	"github.com/juju/juju/docker/registry/mocks"
    23  	"github.com/juju/juju/tools"
    24  )
    25  
    26  type dockerhubSuite struct {
    27  	testing.IsolationSuite
    28  
    29  	mockRoundTripper *mocks.MockRoundTripper
    30  	imageRepoDetails docker.ImageRepoDetails
    31  	isPrivate        bool
    32  	authToken        string
    33  }
    34  
    35  var _ = gc.Suite(&dockerhubSuite{})
    36  
    37  func (s *dockerhubSuite) getRegistry(c *gc.C) (registry.Registry, *gomock.Controller) {
    38  	ctrl := gomock.NewController(c)
    39  
    40  	s.imageRepoDetails = docker.ImageRepoDetails{
    41  		Repository: "docker.io/jujuqa",
    42  	}
    43  	s.authToken = base64.StdEncoding.EncodeToString([]byte("username:pwd"))
    44  	if s.isPrivate {
    45  		s.imageRepoDetails.BasicAuthConfig = docker.BasicAuthConfig{
    46  			Auth: docker.NewToken(s.authToken),
    47  		}
    48  	}
    49  
    50  	s.mockRoundTripper = mocks.NewMockRoundTripper(ctrl)
    51  	gomock.InOrder(
    52  		// registry.Ping() 1st try failed - bearer token was missing.
    53  		s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
    54  			func(req *http.Request) (*http.Response, error) {
    55  				c.Assert(req.Header, jc.DeepEquals, http.Header{})
    56  				c.Assert(req.Method, gc.Equals, `GET`)
    57  				c.Assert(req.URL.String(), gc.Equals, `https://index.docker.io/v2`)
    58  				return &http.Response{
    59  					Request:    req,
    60  					StatusCode: http.StatusUnauthorized,
    61  					Body:       io.NopCloser(nil),
    62  					Header: http.Header{
    63  						http.CanonicalHeaderKey("WWW-Authenticate"): []string{
    64  							`Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:jujuqa/jujud-operator:pull"`,
    65  						},
    66  					},
    67  				}, nil
    68  			},
    69  		),
    70  		// Refresh OAuth Token
    71  		s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
    72  			func(req *http.Request) (*http.Response, error) {
    73  				if s.isPrivate {
    74  					c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + s.authToken}})
    75  				}
    76  				c.Assert(req.Method, gc.Equals, `GET`)
    77  				c.Assert(req.URL.String(), gc.Equals, `https://auth.docker.io/token?scope=repository%3Ajujuqa%2Fjujud-operator%3Apull&service=registry.docker.io`)
    78  				return &http.Response{
    79  					Request:    req,
    80  					StatusCode: http.StatusOK,
    81  					Body:       io.NopCloser(strings.NewReader(`{"token": "jwt-token", "access_token": "jwt-token","expires_in": 300}`)),
    82  				}, nil
    83  			},
    84  		),
    85  		// registry.Ping()
    86  		s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
    87  			func(req *http.Request) (*http.Response, error) {
    88  				c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer " + `jwt-token`}})
    89  				c.Assert(req.Method, gc.Equals, `GET`)
    90  				c.Assert(req.URL.String(), gc.Equals, `https://index.docker.io/v2`)
    91  				return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(nil)}, nil
    92  			},
    93  		),
    94  	)
    95  	s.PatchValue(&registry.DefaultTransport, s.mockRoundTripper)
    96  
    97  	reg, err := registry.New(s.imageRepoDetails)
    98  	c.Assert(err, jc.ErrorIsNil)
    99  	_, ok := reg.(*internal.Dockerhub)
   100  	c.Assert(ok, jc.IsTrue)
   101  	err = reg.Ping()
   102  	c.Assert(err, jc.ErrorIsNil)
   103  	return reg, ctrl
   104  }
   105  
   106  func (s *dockerhubSuite) TestPingPublicRepository(c *gc.C) {
   107  	s.isPrivate = false
   108  	_, ctrl := s.getRegistry(c)
   109  	ctrl.Finish()
   110  }
   111  
   112  func (s *dockerhubSuite) TestPingPrivateRepository(c *gc.C) {
   113  	s.isPrivate = true
   114  	_, ctrl := s.getRegistry(c)
   115  	ctrl.Finish()
   116  }
   117  
   118  func (s *dockerhubSuite) TestTagsPublicRegistry(c *gc.C) {
   119  	// Use anonymous login for public repository.
   120  	s.isPrivate = false
   121  	reg, ctrl := s.getRegistry(c)
   122  	defer ctrl.Finish()
   123  
   124  	data := `
   125  {"name":"jujuqa/jujud-operator","tags":["2.9.10.1","2.9.10.2","2.9.10"]}
   126  `[1:]
   127  
   128  	gomock.InOrder(
   129  		s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
   130  			c.Assert(req.Header, jc.DeepEquals, http.Header{})
   131  			c.Assert(req.Method, gc.Equals, `GET`)
   132  			c.Assert(req.URL.String(), gc.Equals, `https://index.docker.io/v2/jujuqa/jujud-operator/tags/list`)
   133  			return &http.Response{
   134  				Request:    req,
   135  				StatusCode: http.StatusUnauthorized,
   136  				Body:       io.NopCloser(nil),
   137  				Header: http.Header{
   138  					http.CanonicalHeaderKey("WWW-Authenticate"): []string{
   139  						`Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:jujuqa/jujud-operator:pull"`,
   140  					},
   141  				},
   142  			}, nil
   143  		}),
   144  		// Refresh OAuth Token without credential.
   145  		s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
   146  			func(req *http.Request) (*http.Response, error) {
   147  				c.Assert(req.Header, jc.DeepEquals, http.Header{})
   148  				c.Assert(req.Method, gc.Equals, `GET`)
   149  				c.Assert(req.URL.String(), gc.Equals, `https://auth.docker.io/token?scope=repository%3Ajujuqa%2Fjujud-operator%3Apull&service=registry.docker.io`)
   150  				return &http.Response{
   151  					Request:    req,
   152  					StatusCode: http.StatusOK,
   153  					Body:       io.NopCloser(strings.NewReader(`{"token": "jwt-token", "access_token": "jwt-token","expires_in": 300}`)),
   154  				}, nil
   155  			},
   156  		),
   157  
   158  		s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
   159  			c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer jwt-token"}})
   160  			c.Assert(req.Method, gc.Equals, `GET`)
   161  			c.Assert(req.URL.String(), gc.Equals, `https://index.docker.io/v2/jujuqa/jujud-operator/tags/list`)
   162  			resps := &http.Response{
   163  				Request:    req,
   164  				StatusCode: http.StatusOK,
   165  				Body:       io.NopCloser(strings.NewReader(data)),
   166  			}
   167  			return resps, nil
   168  		}),
   169  	)
   170  	vers, err := reg.Tags("jujud-operator")
   171  	c.Assert(err, jc.ErrorIsNil)
   172  	c.Assert(vers, jc.DeepEquals, tools.Versions{
   173  		image.NewImageInfo(version.MustParse("2.9.10.1")),
   174  		image.NewImageInfo(version.MustParse("2.9.10.2")),
   175  		image.NewImageInfo(version.MustParse("2.9.10")),
   176  	})
   177  }
   178  
   179  func (s *dockerhubSuite) TestTagsPrivateRegistry(c *gc.C) {
   180  	// Use v2 for private repository.
   181  	s.isPrivate = true
   182  	reg, ctrl := s.getRegistry(c)
   183  	defer ctrl.Finish()
   184  
   185  	data := `
   186  {"name":"jujuqa/jujud-operator","tags":["2.9.10.1","2.9.10.2","2.9.10"]}
   187  `[1:]
   188  
   189  	gomock.InOrder(
   190  		s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
   191  			c.Assert(req.Header, jc.DeepEquals, http.Header{})
   192  			c.Assert(req.Method, gc.Equals, `GET`)
   193  			c.Assert(req.URL.String(), gc.Equals, `https://index.docker.io/v2/jujuqa/jujud-operator/tags/list`)
   194  			return &http.Response{
   195  				Request:    req,
   196  				StatusCode: http.StatusUnauthorized,
   197  				Body:       io.NopCloser(nil),
   198  				Header: http.Header{
   199  					http.CanonicalHeaderKey("WWW-Authenticate"): []string{
   200  						`Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:jujuqa/jujud-operator:pull"`,
   201  					},
   202  				},
   203  			}, nil
   204  		}),
   205  		// Refresh OAuth Token.
   206  		s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
   207  			func(req *http.Request) (*http.Response, error) {
   208  				c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + s.authToken}})
   209  				c.Assert(req.Method, gc.Equals, `GET`)
   210  				c.Assert(req.URL.String(), gc.Equals, `https://auth.docker.io/token?scope=repository%3Ajujuqa%2Fjujud-operator%3Apull&service=registry.docker.io`)
   211  				return &http.Response{
   212  					Request:    req,
   213  					StatusCode: http.StatusOK,
   214  					Body:       io.NopCloser(strings.NewReader(`{"token": "jwt-token", "access_token": "jwt-token","expires_in": 300}`)),
   215  				}, nil
   216  			},
   217  		),
   218  
   219  		s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
   220  			c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer jwt-token"}})
   221  			c.Assert(req.Method, gc.Equals, `GET`)
   222  			c.Assert(req.URL.String(), gc.Equals, `https://index.docker.io/v2/jujuqa/jujud-operator/tags/list`)
   223  			resps := &http.Response{
   224  				Request:    req,
   225  				StatusCode: http.StatusOK,
   226  				Body:       io.NopCloser(strings.NewReader(data)),
   227  			}
   228  			return resps, nil
   229  		}),
   230  	)
   231  	vers, err := reg.Tags("jujud-operator")
   232  	c.Assert(err, jc.ErrorIsNil)
   233  	c.Assert(vers, jc.DeepEquals, tools.Versions{
   234  		image.NewImageInfo(version.MustParse("2.9.10.1")),
   235  		image.NewImageInfo(version.MustParse("2.9.10.2")),
   236  		image.NewImageInfo(version.MustParse("2.9.10")),
   237  	})
   238  }
   239  
   240  func (s *dockerhubSuite) TestTagsErrorResponse(c *gc.C) {
   241  	s.isPrivate = true
   242  	reg, ctrl := s.getRegistry(c)
   243  	defer ctrl.Finish()
   244  
   245  	data := `
   246  {"errors":[{"code":"UNAUTHORIZED","message":"authentication required"}]}
   247  `[1:]
   248  
   249  	gomock.InOrder(
   250  		s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
   251  			c.Assert(req.Header, jc.DeepEquals, http.Header{})
   252  			c.Assert(req.Method, gc.Equals, `GET`)
   253  			c.Assert(req.URL.String(), gc.Equals, `https://index.docker.io/v2/jujuqa/jujud-operator/tags/list`)
   254  			return &http.Response{
   255  				Request:    req,
   256  				StatusCode: http.StatusUnauthorized,
   257  				Body:       io.NopCloser(nil),
   258  				Header: http.Header{
   259  					http.CanonicalHeaderKey("WWW-Authenticate"): []string{
   260  						`Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:jujuqa/jujud-operator:pull"`,
   261  					},
   262  				},
   263  			}, nil
   264  		}),
   265  		// Refresh OAuth Token
   266  		s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
   267  			func(req *http.Request) (*http.Response, error) {
   268  				c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + s.authToken}})
   269  				c.Assert(req.Method, gc.Equals, `GET`)
   270  				c.Assert(req.URL.String(), gc.Equals, `https://auth.docker.io/token?scope=repository%3Ajujuqa%2Fjujud-operator%3Apull&service=registry.docker.io`)
   271  				return &http.Response{
   272  					Request:    req,
   273  					StatusCode: http.StatusOK,
   274  					Body:       io.NopCloser(strings.NewReader(`{"token": "jwt-token", "access_token": "jwt-token","expires_in": 300}`)),
   275  				}, nil
   276  			},
   277  		),
   278  		s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
   279  			c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer jwt-token"}})
   280  			c.Assert(req.Method, gc.Equals, `GET`)
   281  			c.Assert(req.URL.String(), gc.Equals, `https://index.docker.io/v2/jujuqa/jujud-operator/tags/list`)
   282  			resps := &http.Response{
   283  				Request:    req,
   284  				StatusCode: http.StatusForbidden,
   285  				Body:       io.NopCloser(strings.NewReader(data)),
   286  			}
   287  			return resps, nil
   288  		}),
   289  	)
   290  	_, err := reg.Tags("jujud-operator")
   291  	c.Assert(err, gc.ErrorMatches, `Get "https://index.docker.io/v2/jujuqa/jujud-operator/tags/list": non-successful response status=403`)
   292  }