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(®istry.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 }