github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/docker/registry/internal/github_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 githubSuite struct { 27 testing.IsolationSuite 28 29 mockRoundTripper *mocks.MockRoundTripper 30 imageRepoDetails *docker.ImageRepoDetails 31 isPrivate bool 32 } 33 34 var _ = gc.Suite(&githubSuite{}) 35 36 func (s *githubSuite) TearDownTest(c *gc.C) { 37 s.imageRepoDetails = nil 38 s.IsolationSuite.TearDownTest(c) 39 } 40 41 func (s *githubSuite) getRegistry(c *gc.C) (registry.Registry, *gomock.Controller) { 42 ctrl := gomock.NewController(c) 43 44 if s.imageRepoDetails == nil { 45 s.imageRepoDetails = &docker.ImageRepoDetails{ 46 Repository: "ghcr.io/jujuqa", 47 } 48 if s.isPrivate { 49 authToken := base64.StdEncoding.EncodeToString([]byte("username:pwd")) 50 s.imageRepoDetails.BasicAuthConfig = docker.BasicAuthConfig{ 51 Auth: docker.NewToken(authToken), 52 } 53 } 54 } 55 56 s.mockRoundTripper = mocks.NewMockRoundTripper(ctrl) 57 if s.isPrivate { 58 gomock.InOrder( 59 // registry.Ping() 60 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 61 func(req *http.Request) (*http.Response, error) { 62 authToken := base64.StdEncoding.EncodeToString([]byte("pwd")) 63 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer " + authToken}}) 64 c.Assert(req.Method, gc.Equals, `GET`) 65 c.Assert(req.URL.String(), gc.Equals, `https://ghcr.io/v2/`) 66 return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(nil)}, nil 67 }, 68 ), 69 ) 70 } 71 s.PatchValue(®istry.DefaultTransport, s.mockRoundTripper) 72 73 reg, err := registry.New(*s.imageRepoDetails) 74 c.Assert(err, jc.ErrorIsNil) 75 _, ok := reg.(*internal.GithubContainerRegistry) 76 c.Assert(ok, jc.IsTrue) 77 err = reg.Ping() 78 c.Assert(err, jc.ErrorIsNil) 79 return reg, ctrl 80 } 81 82 func (s *githubSuite) TestPingPublicRepository(c *gc.C) { 83 s.isPrivate = false 84 _, ctrl := s.getRegistry(c) 85 ctrl.Finish() 86 } 87 88 func (s *githubSuite) TestPingPrivateRepository(c *gc.C) { 89 s.isPrivate = true 90 _, ctrl := s.getRegistry(c) 91 ctrl.Finish() 92 } 93 94 func (s *githubSuite) TestPingPrivateRepositoryUserNamePassword(c *gc.C) { 95 s.imageRepoDetails = &docker.ImageRepoDetails{ 96 Repository: "ghcr.io/jujuqa", 97 BasicAuthConfig: docker.BasicAuthConfig{ 98 Username: "username", 99 Password: "pwd", 100 }, 101 } 102 s.isPrivate = true 103 _, ctrl := s.getRegistry(c) 104 ctrl.Finish() 105 } 106 107 func (s *githubSuite) TestPingPrivateRepositoryNoCredential(c *gc.C) { 108 imageRepoDetails := docker.ImageRepoDetails{ 109 Repository: "ghcr.io/jujuqa", 110 BasicAuthConfig: docker.BasicAuthConfig{ 111 Username: "username", 112 }, 113 } 114 _, err := registry.New(imageRepoDetails) 115 c.Assert(err, gc.ErrorMatches, `github container registry requires {"username", "password"} or {"auth"} token`) 116 } 117 118 func (s *githubSuite) TestPingPrivateRepositoryBadAuthTokenFormat(c *gc.C) { 119 authToken := base64.StdEncoding.EncodeToString([]byte("bad-auth")) 120 imageRepoDetails := docker.ImageRepoDetails{ 121 Repository: "ghcr.io/jujuqa", 122 BasicAuthConfig: docker.BasicAuthConfig{ 123 Auth: docker.NewToken(authToken), 124 }, 125 } 126 _, err := registry.New(imageRepoDetails) 127 c.Assert(err, gc.ErrorMatches, `getting password from the github container registry auth token: registry auth token not valid`) 128 } 129 130 func (s *githubSuite) TestPingPrivateRepositoryBadAuthTokenNoPasswordIncluded(c *gc.C) { 131 authToken := base64.StdEncoding.EncodeToString([]byte("username:")) 132 imageRepoDetails := docker.ImageRepoDetails{ 133 Repository: "ghcr.io/jujuqa", 134 BasicAuthConfig: docker.BasicAuthConfig{ 135 Auth: docker.NewToken(authToken), 136 }, 137 } 138 _, err := registry.New(imageRepoDetails) 139 c.Assert(err, gc.ErrorMatches, `github container registry auth token contains empty password`) 140 } 141 142 func (s *githubSuite) TestTagsPublicRegistry(c *gc.C) { 143 // Use anonymous login for public repository. 144 s.isPrivate = false 145 reg, ctrl := s.getRegistry(c) 146 defer ctrl.Finish() 147 148 data := ` 149 {"name":"jujuqa/jujud-operator","tags":["2.9.10.1","2.9.10.2","2.9.10"]} 150 `[1:] 151 152 gomock.InOrder( 153 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 154 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 155 c.Assert(req.Method, gc.Equals, `GET`) 156 c.Assert(req.URL.String(), gc.Equals, `https://ghcr.io/v2/jujuqa/jujud-operator/tags/list`) 157 return &http.Response{ 158 Request: req, 159 StatusCode: http.StatusUnauthorized, 160 Body: io.NopCloser(nil), 161 Header: http.Header{ 162 http.CanonicalHeaderKey("WWW-Authenticate"): []string{ 163 `Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:jujuqa/jujud-operator:pull"`, 164 }, 165 }, 166 }, nil 167 }), 168 // Refresh OAuth Token without credential. 169 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 170 func(req *http.Request) (*http.Response, error) { 171 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 172 c.Assert(req.Method, gc.Equals, `GET`) 173 c.Assert(req.URL.String(), gc.Equals, `https://ghcr.io/token?scope=repository%3Ajujuqa%2Fjujud-operator%3Apull&service=ghcr.io`) 174 return &http.Response{ 175 Request: req, 176 StatusCode: http.StatusOK, 177 Body: io.NopCloser(strings.NewReader(`{"token": "jwt-token", "access_token": "jwt-token","expires_in": 300}`)), 178 }, nil 179 }, 180 ), 181 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 182 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer jwt-token"}}) 183 c.Assert(req.Method, gc.Equals, `GET`) 184 c.Assert(req.URL.String(), gc.Equals, `https://ghcr.io/v2/jujuqa/jujud-operator/tags/list`) 185 resps := &http.Response{ 186 Request: req, 187 StatusCode: http.StatusOK, 188 Body: io.NopCloser(strings.NewReader(data)), 189 } 190 return resps, nil 191 }), 192 ) 193 vers, err := reg.Tags("jujud-operator") 194 c.Assert(err, jc.ErrorIsNil) 195 c.Assert(vers, jc.DeepEquals, tools.Versions{ 196 image.NewImageInfo(version.MustParse("2.9.10.1")), 197 image.NewImageInfo(version.MustParse("2.9.10.2")), 198 image.NewImageInfo(version.MustParse("2.9.10")), 199 }) 200 } 201 202 func (s *githubSuite) TestTagsPrivateRegistry(c *gc.C) { 203 // Use v2 for private repository. 204 s.isPrivate = true 205 reg, ctrl := s.getRegistry(c) 206 defer ctrl.Finish() 207 208 data := ` 209 {"name":"jujuqa/jujud-operator","tags":["2.9.10.1","2.9.10.2","2.9.10"]} 210 `[1:] 211 212 gomock.InOrder( 213 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 214 authToken := base64.StdEncoding.EncodeToString([]byte("pwd")) 215 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer " + authToken}}) 216 c.Assert(req.Method, gc.Equals, `GET`) 217 c.Assert(req.URL.String(), gc.Equals, `https://ghcr.io/v2/jujuqa/jujud-operator/tags/list`) 218 resps := &http.Response{ 219 Request: req, 220 StatusCode: http.StatusOK, 221 Body: io.NopCloser(strings.NewReader(data)), 222 } 223 return resps, nil 224 }), 225 ) 226 vers, err := reg.Tags("jujud-operator") 227 c.Assert(err, jc.ErrorIsNil) 228 c.Assert(vers, jc.DeepEquals, tools.Versions{ 229 image.NewImageInfo(version.MustParse("2.9.10.1")), 230 image.NewImageInfo(version.MustParse("2.9.10.2")), 231 image.NewImageInfo(version.MustParse("2.9.10")), 232 }) 233 } 234 235 func (s *githubSuite) TestTagsErrorResponse(c *gc.C) { 236 s.isPrivate = true 237 reg, ctrl := s.getRegistry(c) 238 defer ctrl.Finish() 239 240 data := ` 241 {"errors":[{"code":"UNAUTHORIZED","message":"authentication required"}]} 242 `[1:] 243 244 gomock.InOrder( 245 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 246 authToken := base64.StdEncoding.EncodeToString([]byte("pwd")) 247 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer " + authToken}}) 248 c.Assert(req.Method, gc.Equals, `GET`) 249 c.Assert(req.URL.String(), gc.Equals, `https://ghcr.io/v2/jujuqa/jujud-operator/tags/list`) 250 resps := &http.Response{ 251 Request: req, 252 StatusCode: http.StatusForbidden, 253 Body: io.NopCloser(strings.NewReader(data)), 254 } 255 return resps, nil 256 }), 257 ) 258 _, err := reg.Tags("jujud-operator") 259 c.Assert(err, gc.ErrorMatches, `Get "https://ghcr.io/v2/jujuqa/jujud-operator/tags/list": non-successful response status=403`) 260 }