github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/docker/registry/internal/gcr_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 googleContainerRegistrySuite 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(&googleContainerRegistrySuite{}) 36 37 func (s *googleContainerRegistrySuite) getRegistry(c *gc.C) (registry.Registry, *gomock.Controller) { 38 ctrl := gomock.NewController(c) 39 40 s.imageRepoDetails = docker.ImageRepoDetails{ 41 Repository: "gcr.io/jujuqa", 42 } 43 s.authToken = base64.StdEncoding.EncodeToString([]byte("_json_key: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 if s.isPrivate { 52 gomock.InOrder( 53 // registry.Ping() 1st try failed - bearer token was missing. 54 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 55 func(req *http.Request) (*http.Response, error) { 56 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 57 c.Assert(req.Method, gc.Equals, `GET`) 58 c.Assert(req.URL.String(), gc.Equals, `https://gcr.io/v2/`) 59 return &http.Response{ 60 Request: req, 61 StatusCode: http.StatusUnauthorized, 62 Body: io.NopCloser(nil), 63 Header: http.Header{ 64 http.CanonicalHeaderKey("WWW-Authenticate"): []string{ 65 `Bearer realm="https://gcr.io/v2/token",service="gcr.io",scope="repository:jujuqa/jujud-operator:pull"`, 66 }, 67 }, 68 }, nil 69 }, 70 ), 71 // Refresh OAuth Token 72 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 73 func(req *http.Request) (*http.Response, error) { 74 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + s.authToken}}) 75 c.Assert(req.Method, gc.Equals, `GET`) 76 c.Assert(req.URL.String(), gc.Equals, `https://gcr.io/v2/token?scope=repository%3Ajujuqa%2Fjujud-operator%3Apull&service=gcr.io`) 77 return &http.Response{ 78 Request: req, 79 StatusCode: http.StatusOK, 80 Body: io.NopCloser(strings.NewReader(`{"token": "jwt-token", "access_token": "jwt-token","expires_in": 300}`)), 81 }, nil 82 }, 83 ), 84 // registry.Ping() 85 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 86 func(req *http.Request) (*http.Response, error) { 87 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer jwt-token"}}) 88 c.Assert(req.Method, gc.Equals, `GET`) 89 c.Assert(req.URL.String(), gc.Equals, `https://gcr.io/v2/`) 90 return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(nil)}, nil 91 }, 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.GoogleContainerRegistry) 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 *googleContainerRegistrySuite) TestInvalidUserName(c *gc.C) { 107 imageRepoDetails := docker.ImageRepoDetails{ 108 Repository: "gcr.io/jujuqa", 109 BasicAuthConfig: docker.BasicAuthConfig{ 110 Auth: docker.NewToken(base64.StdEncoding.EncodeToString([]byte("username:pwd"))), 111 }, 112 } 113 _, err := registry.New(imageRepoDetails) 114 c.Assert(err, gc.ErrorMatches, `validating the google container registry credential: google container registry username has to be "_json_key"`) 115 } 116 117 func (s *googleContainerRegistrySuite) TestPingPrivateRepository(c *gc.C) { 118 s.isPrivate = true 119 _, ctrl := s.getRegistry(c) 120 ctrl.Finish() 121 } 122 123 func (s *googleContainerRegistrySuite) TestTags(c *gc.C) { 124 // Use v2 for private repository. 125 s.isPrivate = true 126 reg, ctrl := s.getRegistry(c) 127 defer ctrl.Finish() 128 129 data := ` 130 {"name":"jujuqa/jujud-operator","tags":["2.9.10.1","2.9.10.2","2.9.10"]} 131 `[1:] 132 133 gomock.InOrder( 134 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 135 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 136 c.Assert(req.Method, gc.Equals, `GET`) 137 c.Assert(req.URL.String(), gc.Equals, `https://gcr.io/v2/jujuqa/jujud-operator/tags/list`) 138 return &http.Response{ 139 Request: req, 140 StatusCode: http.StatusUnauthorized, 141 Body: io.NopCloser(nil), 142 Header: http.Header{ 143 http.CanonicalHeaderKey("WWW-Authenticate"): []string{ 144 `Bearer realm="https://gcr.io/v2/token",service="gcr.io",scope="repository:jujuqa/jujud-operator:pull"`, 145 }, 146 }, 147 }, nil 148 }), 149 // Refresh OAuth Token 150 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 151 func(req *http.Request) (*http.Response, error) { 152 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + s.authToken}}) 153 c.Assert(req.Method, gc.Equals, `GET`) 154 c.Assert(req.URL.String(), gc.Equals, `https://gcr.io/v2/token?scope=repository%3Ajujuqa%2Fjujud-operator%3Apull&service=gcr.io`) 155 return &http.Response{ 156 Request: req, 157 StatusCode: http.StatusOK, 158 Body: io.NopCloser(strings.NewReader(`{"token": "jwt-token", "access_token": "jwt-token","expires_in": 300}`)), 159 }, nil 160 }, 161 ), 162 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 163 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer jwt-token"}}) 164 c.Assert(req.Method, gc.Equals, `GET`) 165 c.Assert(req.URL.String(), gc.Equals, `https://gcr.io/v2/jujuqa/jujud-operator/tags/list`) 166 resps := &http.Response{ 167 Request: req, 168 StatusCode: http.StatusOK, 169 Body: io.NopCloser(strings.NewReader(data)), 170 } 171 return resps, nil 172 }), 173 ) 174 vers, err := reg.Tags("jujud-operator") 175 c.Assert(err, jc.ErrorIsNil) 176 c.Assert(vers, jc.DeepEquals, tools.Versions{ 177 image.NewImageInfo(version.MustParse("2.9.10.1")), 178 image.NewImageInfo(version.MustParse("2.9.10.2")), 179 image.NewImageInfo(version.MustParse("2.9.10")), 180 }) 181 } 182 183 func (s *googleContainerRegistrySuite) TestTagsErrorResponse(c *gc.C) { 184 s.isPrivate = true 185 reg, ctrl := s.getRegistry(c) 186 defer ctrl.Finish() 187 188 data := ` 189 {"errors":[{"code":"UNAUTHORIZED","message":"authentication required"}]} 190 `[1:] 191 192 gomock.InOrder( 193 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 194 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 195 c.Assert(req.Method, gc.Equals, `GET`) 196 c.Assert(req.URL.String(), gc.Equals, `https://gcr.io/v2/jujuqa/jujud-operator/tags/list`) 197 return &http.Response{ 198 Request: req, 199 StatusCode: http.StatusUnauthorized, 200 Body: io.NopCloser(nil), 201 Header: http.Header{ 202 http.CanonicalHeaderKey("WWW-Authenticate"): []string{ 203 `Bearer realm="https://gcr.io/v2/token",service="gcr.io",scope="repository:jujuqa/jujud-operator:pull"`, 204 }, 205 }, 206 }, nil 207 }), 208 // Refresh OAuth Token 209 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 210 func(req *http.Request) (*http.Response, error) { 211 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + s.authToken}}) 212 c.Assert(req.Method, gc.Equals, `GET`) 213 c.Assert(req.URL.String(), gc.Equals, `https://gcr.io/v2/token?scope=repository%3Ajujuqa%2Fjujud-operator%3Apull&service=gcr.io`) 214 return &http.Response{ 215 Request: req, 216 StatusCode: http.StatusOK, 217 Body: io.NopCloser(strings.NewReader(`{"token": "jwt-token", "access_token": "jwt-token","expires_in": 300}`)), 218 }, nil 219 }, 220 ), 221 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 222 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer jwt-token"}}) 223 c.Assert(req.Method, gc.Equals, `GET`) 224 c.Assert(req.URL.String(), gc.Equals, `https://gcr.io/v2/jujuqa/jujud-operator/tags/list`) 225 resps := &http.Response{ 226 Request: req, 227 StatusCode: http.StatusForbidden, 228 Body: io.NopCloser(strings.NewReader(data)), 229 } 230 return resps, nil 231 }), 232 ) 233 _, err := reg.Tags("jujud-operator") 234 c.Assert(err, gc.ErrorMatches, `Get "https://gcr.io/v2/jujuqa/jujud-operator/tags/list": non-successful response status=403`) 235 }