github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/docker/registry/internal/acr_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/image" 20 "github.com/juju/juju/docker/registry/internal" 21 "github.com/juju/juju/docker/registry/mocks" 22 "github.com/juju/juju/tools" 23 ) 24 25 type azureContainerRegistrySuite struct { 26 testing.IsolationSuite 27 28 mockRoundTripper *mocks.MockRoundTripper 29 imageRepoDetails docker.ImageRepoDetails 30 isPrivate bool 31 authToken string 32 } 33 34 var _ = gc.Suite(&azureContainerRegistrySuite{}) 35 36 func (s *azureContainerRegistrySuite) getRegistry(c *gc.C) (*internal.AzureContainerRegistry, *gomock.Controller) { 37 ctrl := gomock.NewController(c) 38 39 s.imageRepoDetails = docker.ImageRepoDetails{ 40 Repository: "jujuqa.azurecr.io", 41 } 42 s.authToken = base64.StdEncoding.EncodeToString([]byte("service-principal-id:service-principal-password")) 43 if s.isPrivate { 44 s.imageRepoDetails.BasicAuthConfig = docker.BasicAuthConfig{ 45 Auth: docker.NewToken(s.authToken), 46 Username: "service-principal-id", 47 Password: "service-principal-password", 48 } 49 } 50 51 s.mockRoundTripper = mocks.NewMockRoundTripper(ctrl) 52 if s.isPrivate { 53 gomock.InOrder( 54 // registry.Ping() 1st try failed - bearer token was missing. 55 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 56 func(req *http.Request) (*http.Response, error) { 57 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 58 c.Assert(req.Method, gc.Equals, `GET`) 59 c.Assert(req.URL.String(), gc.Equals, `https://jujuqa.azurecr.io/v2`) 60 return &http.Response{ 61 Request: req, 62 StatusCode: http.StatusUnauthorized, 63 Body: io.NopCloser(nil), 64 Header: http.Header{ 65 http.CanonicalHeaderKey("WWW-Authenticate"): []string{ 66 `Bearer realm="https://jujuqa.azurecr.io/oauth2/token",service="jujuqa.azurecr.io",scope="repository:jujud-operator:metadata_read"`, 67 }, 68 }, 69 }, nil 70 }, 71 ), 72 // Refresh OAuth Token 73 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 74 func(req *http.Request) (*http.Response, error) { 75 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + s.authToken}}) 76 c.Assert(req.Method, gc.Equals, `GET`) 77 c.Assert(req.URL.String(), gc.Equals, `https://jujuqa.azurecr.io/oauth2/token?scope=repository%3Ajujud-operator%3Ametadata_read&service=jujuqa.azurecr.io`) 78 return &http.Response{ 79 Request: req, 80 StatusCode: http.StatusOK, 81 Body: io.NopCloser(strings.NewReader(`{"access_token": "jwt-token"}`)), 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://jujuqa.azurecr.io/v2`) 91 return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(nil)}, nil 92 }, 93 ), 94 ) 95 } 96 97 reg, err := internal.NewAzureContainerRegistry(s.imageRepoDetails, s.mockRoundTripper) 98 c.Assert(err, jc.ErrorIsNil) 99 err = internal.InitProvider(reg) 100 if !s.imageRepoDetails.IsPrivate() { 101 c.Assert(err, gc.ErrorMatches, `username and password are required for registry "jujuqa.azurecr.io"`) 102 return nil, ctrl 103 } 104 c.Assert(err, jc.ErrorIsNil) 105 client, ok := reg.(*internal.AzureContainerRegistry) 106 c.Assert(ok, jc.IsTrue) 107 err = reg.Ping() 108 c.Assert(err, jc.ErrorIsNil) 109 return client, ctrl 110 } 111 112 func (s *azureContainerRegistrySuite) TestPingPublicRepository(c *gc.C) { 113 s.isPrivate = false 114 _, ctrl := s.getRegistry(c) 115 ctrl.Finish() 116 } 117 118 func (s *azureContainerRegistrySuite) TestPingPrivateRepository(c *gc.C) { 119 s.isPrivate = true 120 _, ctrl := s.getRegistry(c) 121 ctrl.Finish() 122 } 123 124 func (s *azureContainerRegistrySuite) TestTagsV2(c *gc.C) { 125 // Use v2 for private repository. 126 s.isPrivate = true 127 reg, ctrl := s.getRegistry(c) 128 defer ctrl.Finish() 129 130 data := ` 131 {"name":"jujud-operator","tags":["2.9.10.1","2.9.10.2","2.9.10"]} 132 `[1:] 133 134 gomock.InOrder( 135 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 136 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 137 c.Assert(req.Method, gc.Equals, `GET`) 138 c.Assert(req.URL.String(), gc.Equals, `https://jujuqa.azurecr.io/v2/jujud-operator/tags/list`) 139 return &http.Response{ 140 Request: req, 141 StatusCode: http.StatusUnauthorized, 142 Body: io.NopCloser(nil), 143 Header: http.Header{ 144 http.CanonicalHeaderKey("WWW-Authenticate"): []string{ 145 `Bearer realm="https://jujuqa.azurecr.io/oauth2/token",service="jujuqa.azurecr.io",scope="repository:jujud-operator:metadata_read"`, 146 }, 147 }, 148 }, nil 149 }), 150 // Refresh OAuth Token 151 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 152 func(req *http.Request) (*http.Response, error) { 153 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + s.authToken}}) 154 c.Assert(req.Method, gc.Equals, `GET`) 155 c.Assert(req.URL.String(), gc.Equals, `https://jujuqa.azurecr.io/oauth2/token?scope=repository%3Ajujud-operator%3Ametadata_read&service=jujuqa.azurecr.io`) 156 return &http.Response{ 157 Request: req, 158 StatusCode: http.StatusOK, 159 Body: io.NopCloser(strings.NewReader(`{"access_token": "jwt-token"}`)), 160 }, nil 161 }, 162 ), 163 164 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 165 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer jwt-token"}}) 166 c.Assert(req.Method, gc.Equals, `GET`) 167 c.Assert(req.URL.String(), gc.Equals, `https://jujuqa.azurecr.io/v2/jujud-operator/tags/list`) 168 resps := &http.Response{ 169 Request: req, 170 StatusCode: http.StatusOK, 171 Body: io.NopCloser(strings.NewReader(data)), 172 } 173 return resps, nil 174 }), 175 ) 176 vers, err := reg.Tags("jujud-operator") 177 c.Assert(err, jc.ErrorIsNil) 178 c.Assert(vers, jc.DeepEquals, tools.Versions{ 179 image.NewImageInfo(version.MustParse("2.9.10.1")), 180 image.NewImageInfo(version.MustParse("2.9.10.2")), 181 image.NewImageInfo(version.MustParse("2.9.10")), 182 }) 183 } 184 185 func (s *azureContainerRegistrySuite) TestTagsErrorResponseV2(c *gc.C) { 186 s.isPrivate = true 187 reg, ctrl := s.getRegistry(c) 188 defer ctrl.Finish() 189 190 data := ` 191 {"errors":[{"code":"UNAUTHORIZED","message":"authentication required"}]} 192 `[1:] 193 194 gomock.InOrder( 195 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 196 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 197 c.Assert(req.Method, gc.Equals, `GET`) 198 c.Assert(req.URL.String(), gc.Equals, `https://jujuqa.azurecr.io/v2/jujud-operator/tags/list`) 199 return &http.Response{ 200 Request: req, 201 StatusCode: http.StatusUnauthorized, 202 Body: io.NopCloser(nil), 203 Header: http.Header{ 204 http.CanonicalHeaderKey("WWW-Authenticate"): []string{ 205 `Bearer realm="https://jujuqa.azurecr.io/oauth2/token",service="jujuqa.azurecr.io",scope="repository:jujud-operator:metadata_read"`, 206 }, 207 }, 208 }, nil 209 }), 210 // Refresh OAuth Token 211 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 212 func(req *http.Request) (*http.Response, error) { 213 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + s.authToken}}) 214 c.Assert(req.Method, gc.Equals, `GET`) 215 c.Assert(req.URL.String(), gc.Equals, `https://jujuqa.azurecr.io/oauth2/token?scope=repository%3Ajujud-operator%3Ametadata_read&service=jujuqa.azurecr.io`) 216 return &http.Response{ 217 Request: req, 218 StatusCode: http.StatusOK, 219 Body: io.NopCloser(strings.NewReader(`{"access_token": "jwt-token"}`)), 220 }, nil 221 }, 222 ), 223 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 224 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer jwt-token"}}) 225 c.Assert(req.Method, gc.Equals, `GET`) 226 c.Assert(req.URL.String(), gc.Equals, `https://jujuqa.azurecr.io/v2/jujud-operator/tags/list`) 227 resps := &http.Response{ 228 Request: req, 229 StatusCode: http.StatusForbidden, 230 Body: io.NopCloser(strings.NewReader(data)), 231 } 232 return resps, nil 233 }), 234 ) 235 _, err := reg.Tags("jujud-operator") 236 c.Assert(err, gc.ErrorMatches, `Get "https://jujuqa.azurecr.io/v2/jujud-operator/tags/list": non-successful response status=403`) 237 } 238 239 func (s *azureContainerRegistrySuite) assertGetManifestsSchemaVersion1(c *gc.C, responseData, contentType string, result *internal.ManifestsResult) { 240 // Use v2 for private repository. 241 s.isPrivate = true 242 reg, ctrl := s.getRegistry(c) 243 defer ctrl.Finish() 244 245 gomock.InOrder( 246 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 247 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 248 c.Assert(req.Method, gc.Equals, `GET`) 249 c.Assert(req.URL.String(), gc.Equals, `https://jujuqa.azurecr.io/v2/jujud-operator/manifests/2.9.10`) 250 return &http.Response{ 251 Request: req, 252 StatusCode: http.StatusUnauthorized, 253 Body: io.NopCloser(nil), 254 Header: http.Header{ 255 http.CanonicalHeaderKey("WWW-Authenticate"): []string{ 256 `Bearer realm="https://jujuqa.azurecr.io/oauth2/token",service="jujuqa.azurecr.io",scope="repository:jujud-operator:metadata_read"`, 257 }, 258 }, 259 }, nil 260 }), 261 // Refresh OAuth Token 262 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 263 func(req *http.Request) (*http.Response, error) { 264 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + s.authToken}}) 265 c.Assert(req.Method, gc.Equals, `GET`) 266 c.Assert(req.URL.String(), gc.Equals, `https://jujuqa.azurecr.io/oauth2/token?scope=repository%3Ajujud-operator%3Ametadata_read&service=jujuqa.azurecr.io`) 267 return &http.Response{ 268 Request: req, 269 StatusCode: http.StatusOK, 270 Body: io.NopCloser(strings.NewReader(`{"access_token": "jwt-token"}`)), 271 }, nil 272 }, 273 ), 274 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 275 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer jwt-token"}}) 276 c.Assert(req.Method, gc.Equals, `GET`) 277 c.Assert(req.URL.String(), gc.Equals, `https://jujuqa.azurecr.io/v2/jujud-operator/manifests/2.9.10`) 278 resps := &http.Response{ 279 Header: http.Header{ 280 http.CanonicalHeaderKey("Content-Type"): []string{contentType}, 281 }, 282 Request: req, 283 StatusCode: http.StatusOK, 284 Body: io.NopCloser(strings.NewReader(responseData)), 285 } 286 return resps, nil 287 }), 288 ) 289 manifests, err := reg.GetManifests("jujud-operator", "2.9.10") 290 c.Assert(err, jc.ErrorIsNil) 291 c.Assert(manifests, jc.DeepEquals, result) 292 } 293 294 func (s *azureContainerRegistrySuite) TestGetManifestsSchemaVersion1(c *gc.C) { 295 s.assertGetManifestsSchemaVersion1(c, 296 ` 297 { "schemaVersion": 1, "name": "jujuqa/jujud-operator", "tag": "2.9.13", "architecture": "amd64"} 298 `[1:], 299 `application/vnd.docker.distribution.manifest.v1+prettyjws`, 300 &internal.ManifestsResult{Architecture: "amd64"}, 301 ) 302 } 303 304 func (s *azureContainerRegistrySuite) TestGetManifestsSchemaVersion2(c *gc.C) { 305 s.assertGetManifestsSchemaVersion1(c, 306 ` 307 { 308 "schemaVersion": 2, 309 "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 310 "config": { 311 "mediaType": "application/vnd.docker.container.image.v1+json", 312 "size": 4596, 313 "digest": "sha256:f0609d8a844f7271411c1a9c5d7a898fd9f9c5a4844e3bc7db6d725b54671ac1" 314 } 315 } 316 `[1:], 317 `application/vnd.docker.distribution.manifest.v2+prettyjws`, 318 &internal.ManifestsResult{Digest: "sha256:f0609d8a844f7271411c1a9c5d7a898fd9f9c5a4844e3bc7db6d725b54671ac1"}, 319 ) 320 } 321 322 func (s *azureContainerRegistrySuite) TestGetBlobs(c *gc.C) { 323 // Use v2 for private repository. 324 s.isPrivate = true 325 reg, ctrl := s.getRegistry(c) 326 defer ctrl.Finish() 327 328 gomock.InOrder( 329 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 330 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 331 c.Assert(req.Method, gc.Equals, `GET`) 332 c.Assert(req.URL.String(), gc.Equals, 333 `https://jujuqa.azurecr.io/v2/jujud-operator/blobs/sha256:f0609d8a844f7271411c1a9c5d7a898fd9f9c5a4844e3bc7db6d725b54671ac1`, 334 ) 335 return &http.Response{ 336 Request: req, 337 StatusCode: http.StatusUnauthorized, 338 Body: io.NopCloser(nil), 339 Header: http.Header{ 340 http.CanonicalHeaderKey("WWW-Authenticate"): []string{ 341 `Bearer realm="https://jujuqa.azurecr.io/oauth2/token",service="jujuqa.azurecr.io",scope="repository:jujud-operator:metadata_read"`, 342 }, 343 }, 344 }, nil 345 }), 346 // Refresh OAuth Token 347 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 348 func(req *http.Request) (*http.Response, error) { 349 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + s.authToken}}) 350 c.Assert(req.Method, gc.Equals, `GET`) 351 c.Assert(req.URL.String(), gc.Equals, `https://jujuqa.azurecr.io/oauth2/token?scope=repository%3Ajujud-operator%3Ametadata_read&service=jujuqa.azurecr.io`) 352 return &http.Response{ 353 Request: req, 354 StatusCode: http.StatusOK, 355 Body: io.NopCloser(strings.NewReader(`{"access_token": "jwt-token"}`)), 356 }, nil 357 }, 358 ), 359 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 360 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer jwt-token"}}) 361 c.Assert(req.Method, gc.Equals, `GET`) 362 c.Assert(req.URL.String(), gc.Equals, 363 `https://jujuqa.azurecr.io/v2/jujud-operator/blobs/sha256:f0609d8a844f7271411c1a9c5d7a898fd9f9c5a4844e3bc7db6d725b54671ac1`, 364 ) 365 resps := &http.Response{ 366 Request: req, 367 StatusCode: http.StatusOK, 368 Body: io.NopCloser(strings.NewReader(` 369 {"architecture":"amd64"} 370 `[1:])), 371 } 372 return resps, nil 373 }), 374 ) 375 manifests, err := reg.GetBlobs("jujud-operator", "sha256:f0609d8a844f7271411c1a9c5d7a898fd9f9c5a4844e3bc7db6d725b54671ac1") 376 c.Assert(err, jc.ErrorIsNil) 377 c.Assert(manifests, jc.DeepEquals, &internal.BlobsResponse{Architecture: "amd64"}) 378 }