github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/docker/registry/internal/ecr_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 "context" 8 "io" 9 "net/http" 10 "strings" 11 "time" 12 13 "github.com/aws/aws-sdk-go-v2/aws" 14 "github.com/aws/aws-sdk-go-v2/service/ecr" 15 "github.com/aws/aws-sdk-go-v2/service/ecr/types" 16 "github.com/juju/testing" 17 jc "github.com/juju/testing/checkers" 18 "github.com/juju/version/v2" 19 "go.uber.org/mock/gomock" 20 gc "gopkg.in/check.v1" 21 22 "github.com/juju/juju/docker" 23 "github.com/juju/juju/docker/registry" 24 "github.com/juju/juju/docker/registry/image" 25 "github.com/juju/juju/docker/registry/internal" 26 internalmocks "github.com/juju/juju/docker/registry/internal/mocks" 27 "github.com/juju/juju/docker/registry/mocks" 28 "github.com/juju/juju/tools" 29 ) 30 31 type elasticContainerRegistrySuite struct { 32 testing.IsolationSuite 33 34 mockRoundTripper *mocks.MockRoundTripper 35 mockECRAPI *internalmocks.MockECRInterface 36 imageRepoDetails docker.ImageRepoDetails 37 isPrivate bool 38 } 39 40 var _ = gc.Suite(&elasticContainerRegistrySuite{}) 41 42 func (s *elasticContainerRegistrySuite) getRegistry(c *gc.C, ensureAsserts func()) (*internal.ElasticContainerRegistry, *gomock.Controller) { 43 ctrl := gomock.NewController(c) 44 45 s.mockRoundTripper = mocks.NewMockRoundTripper(ctrl) 46 s.mockECRAPI = internalmocks.NewMockECRInterface(ctrl) 47 48 if s.imageRepoDetails.Empty() { 49 s.imageRepoDetails = docker.ImageRepoDetails{ 50 Repository: "66668888.dkr.ecr.eu-west-1.amazonaws.com", 51 Region: "ap-southeast-2", 52 } 53 if s.isPrivate { 54 s.imageRepoDetails.BasicAuthConfig = docker.BasicAuthConfig{ 55 Username: "aws_access_key_id", 56 Password: "aws_secret_access_key", 57 } 58 } 59 } 60 if ensureAsserts != nil { 61 ensureAsserts() 62 } else { 63 if s.imageRepoDetails.IsPrivate() { 64 s.mockECRAPI.EXPECT().GetAuthorizationToken(gomock.Any(), &ecr.GetAuthorizationTokenInput{}).Return( 65 &ecr.GetAuthorizationTokenOutput{ 66 AuthorizationData: []types.AuthorizationData{ 67 {AuthorizationToken: aws.String(`xxxx===`)}, 68 }, 69 }, nil, 70 ).AnyTimes() 71 } 72 } 73 74 reg, err := internal.NewElasticContainerRegistryForTest( 75 s.imageRepoDetails, s.mockRoundTripper, 76 func(context.Context, string, string, string) (internal.ECRInterface, error) { 77 return s.mockECRAPI, nil 78 }, 79 ) 80 c.Assert(err, jc.ErrorIsNil) 81 err = internal.InitProvider(reg) 82 if !s.imageRepoDetails.IsPrivate() { 83 c.Assert(err, gc.ErrorMatches, `empty credential for elastic container registry`) 84 return nil, ctrl 85 } 86 c.Assert(err, jc.ErrorIsNil) 87 client, ok := reg.(*internal.ElasticContainerRegistry) 88 c.Assert(ok, jc.IsTrue) 89 err = reg.Ping() 90 c.Assert(err, jc.ErrorIsNil) 91 return client, ctrl 92 } 93 94 func (s *elasticContainerRegistrySuite) TestInvalidImageRepoDetails(c *gc.C) { 95 imageRepoDetails := docker.ImageRepoDetails{ 96 Repository: "66668888.dkr.ecr.eu-west-1.amazonaws.com", 97 ServerAddress: "66668888.dkr.ecr.eu-west-1.amazonaws.com", 98 BasicAuthConfig: docker.BasicAuthConfig{}, 99 } 100 _, err := registry.New(imageRepoDetails) 101 c.Check(err, gc.ErrorMatches, `empty credential for elastic container registry`) 102 103 imageRepoDetails = docker.ImageRepoDetails{ 104 Repository: "66668888.dkr.ecr.eu-west-1.amazonaws.com", 105 ServerAddress: "66668888.dkr.ecr.eu-west-1.amazonaws.com", 106 Region: "ap-southeast-2", 107 BasicAuthConfig: docker.BasicAuthConfig{ 108 Username: "aws_access_key_id", 109 }, 110 } 111 _, err = registry.New(imageRepoDetails) 112 c.Check(err, gc.ErrorMatches, `username and password are required for registry "66668888.dkr.ecr.eu-west-1.amazonaws.com"`) 113 114 imageRepoDetails = docker.ImageRepoDetails{ 115 Repository: "66668888.dkr.ecr.eu-west-1.amazonaws.com", 116 ServerAddress: "66668888.dkr.ecr.eu-west-1.amazonaws.com", 117 Region: "ap-southeast-2", 118 BasicAuthConfig: docker.BasicAuthConfig{ 119 Password: "aws_secret_access_key", 120 }, 121 } 122 _, err = registry.New(imageRepoDetails) 123 c.Check(err, gc.ErrorMatches, `username and password are required for registry "66668888.dkr.ecr.eu-west-1.amazonaws.com"`) 124 125 imageRepoDetails = docker.ImageRepoDetails{ 126 Repository: "66668888.dkr.ecr.eu-west-1.amazonaws.com", 127 ServerAddress: "66668888.dkr.ecr.eu-west-1.amazonaws.com", 128 BasicAuthConfig: docker.BasicAuthConfig{ 129 Username: "aws_access_key_id", 130 Password: "aws_secret_access_key", 131 }, 132 } 133 _, err = registry.New(imageRepoDetails) 134 c.Check(err, gc.ErrorMatches, `region is required`) 135 } 136 137 func setImageRepoDetails(c *gc.C, reg registry.Registry, i docker.ImageRepoDetails) { 138 registry, ok := reg.(*internal.ElasticContainerRegistry) 139 c.Assert(ok, jc.IsTrue) 140 registry.SetImageRepoDetails(i) 141 } 142 143 func (s *elasticContainerRegistrySuite) TestShouldRefreshAuthAuthTokenMissing(c *gc.C) { 144 reg, ctrl := s.getRegistry(c, nil) 145 defer ctrl.Finish() 146 repoDetails := docker.ImageRepoDetails{ 147 Repository: "66668888.dkr.ecr.eu-west-1.amazonaws.com", 148 ServerAddress: "66668888.dkr.ecr.eu-west-1.amazonaws.com", 149 Region: "ap-southeast-2", 150 BasicAuthConfig: docker.BasicAuthConfig{ 151 Username: "aws_access_key_id", 152 Password: "aws_secret_access_key", 153 }, 154 } 155 setImageRepoDetails(c, reg, repoDetails) 156 shouldRefreshAuth, tick := reg.ShouldRefreshAuth() 157 c.Assert(tick, gc.Equals, time.Duration(0)) 158 c.Assert(shouldRefreshAuth, jc.IsTrue) 159 } 160 161 func (s *elasticContainerRegistrySuite) TestShouldRefreshNoExpireTime(c *gc.C) { 162 reg, ctrl := s.getRegistry(c, nil) 163 defer ctrl.Finish() 164 repoDetails := docker.ImageRepoDetails{ 165 Repository: "66668888.dkr.ecr.eu-west-1.amazonaws.com", 166 ServerAddress: "66668888.dkr.ecr.eu-west-1.amazonaws.com", 167 Region: "ap-southeast-2", 168 BasicAuthConfig: docker.BasicAuthConfig{ 169 Username: "aws_access_key_id", 170 Password: "aws_secret_access_key", 171 }, 172 } 173 repoDetails.Auth = docker.NewToken(`xxx===`) 174 setImageRepoDetails(c, reg, repoDetails) 175 shouldRefreshAuth, tick := reg.ShouldRefreshAuth() 176 c.Assert(tick, gc.Equals, time.Duration(0)) 177 c.Assert(shouldRefreshAuth, jc.IsTrue) 178 } 179 180 func (s *elasticContainerRegistrySuite) TestShouldRefreshTokenExpired(c *gc.C) { 181 reg, ctrl := s.getRegistry(c, nil) 182 defer ctrl.Finish() 183 repoDetails := docker.ImageRepoDetails{ 184 Repository: "66668888.dkr.ecr.eu-west-1.amazonaws.com", 185 ServerAddress: "66668888.dkr.ecr.eu-west-1.amazonaws.com", 186 Region: "ap-southeast-2", 187 BasicAuthConfig: docker.BasicAuthConfig{ 188 Username: "aws_access_key_id", 189 Password: "aws_secret_access_key", 190 }, 191 } 192 // expires in 5 mins. 193 expiredTime := time.Now().Add(-1 * time.Second).Add(5 * time.Minute) 194 repoDetails.Auth = &docker.Token{ 195 Value: `xxx===`, 196 ExpiresAt: &expiredTime, 197 } 198 setImageRepoDetails(c, reg, repoDetails) 199 shouldRefreshAuth, tick := reg.ShouldRefreshAuth() 200 c.Assert(tick, gc.Equals, time.Duration(0)) 201 c.Assert(shouldRefreshAuth, jc.IsTrue) 202 203 // // already expired. 204 expiredTime = time.Now().Add(-1 * time.Second) 205 repoDetails.Auth = &docker.Token{ 206 Value: `xxx===`, 207 ExpiresAt: &expiredTime, 208 } 209 setImageRepoDetails(c, reg, repoDetails) 210 shouldRefreshAuth, tick = reg.ShouldRefreshAuth() 211 c.Assert(tick, gc.Equals, time.Duration(0)) 212 c.Assert(shouldRefreshAuth, jc.IsTrue) 213 } 214 215 func (s *elasticContainerRegistrySuite) TestShouldRefreshTokenNoNeedRefresh(c *gc.C) { 216 expiredTime := time.Now().Add(3 * time.Minute).Add(5 * time.Minute) 217 reg, ctrl := s.getRegistry(c, nil) 218 defer ctrl.Finish() 219 repoDetails := docker.ImageRepoDetails{ 220 Repository: "66668888.dkr.ecr.eu-west-1.amazonaws.com", 221 ServerAddress: "66668888.dkr.ecr.eu-west-1.amazonaws.com", 222 Region: "ap-southeast-2", 223 BasicAuthConfig: docker.BasicAuthConfig{ 224 Username: "aws_access_key_id", 225 Password: "aws_secret_access_key", 226 }, 227 } 228 repoDetails.Auth = &docker.Token{ 229 Value: `xxx===`, 230 ExpiresAt: &expiredTime, 231 } 232 setImageRepoDetails(c, reg, repoDetails) 233 shouldRefreshAuth, tick := reg.ShouldRefreshAuth() 234 c.Assert(shouldRefreshAuth, jc.IsFalse) 235 c.Assert(tick, gc.NotNil) 236 c.Assert(tick.Round(time.Minute), gc.DeepEquals, 3*time.Minute) 237 } 238 239 func (s *elasticContainerRegistrySuite) TestPingPublicRepository(c *gc.C) { 240 s.isPrivate = false 241 _, ctrl := s.getRegistry(c, nil) 242 ctrl.Finish() 243 } 244 245 func (s *elasticContainerRegistrySuite) TestPingPrivateRepository(c *gc.C) { 246 s.isPrivate = true 247 _, ctrl := s.getRegistry(c, nil) 248 ctrl.Finish() 249 } 250 251 func (s *elasticContainerRegistrySuite) TestTags(c *gc.C) { 252 // Use v2 for private repository. 253 s.isPrivate = true 254 reg, ctrl := s.getRegistry(c, nil) 255 defer ctrl.Finish() 256 257 data := ` 258 {"name":"jujuqa/jujud-operator","tags":["2.9.10.1","2.9.10.2","2.9.10"]} 259 `[1:] 260 261 gomock.InOrder( 262 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 263 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic xxxx==="}}) 264 c.Assert(req.Method, gc.Equals, `GET`) 265 c.Assert(req.URL.String(), gc.Equals, `https://66668888.dkr.ecr.eu-west-1.amazonaws.com/v2/jujud-operator/tags/list`) 266 resps := &http.Response{ 267 Request: req, 268 StatusCode: http.StatusOK, 269 Body: io.NopCloser(strings.NewReader(data)), 270 } 271 return resps, nil 272 }), 273 ) 274 vers, err := reg.Tags("jujud-operator") 275 c.Assert(err, jc.ErrorIsNil) 276 c.Assert(vers, jc.DeepEquals, tools.Versions{ 277 image.NewImageInfo(version.MustParse("2.9.10.1")), 278 image.NewImageInfo(version.MustParse("2.9.10.2")), 279 image.NewImageInfo(version.MustParse("2.9.10")), 280 }) 281 } 282 283 func (s *elasticContainerRegistrySuite) TestTagsErrorResponse(c *gc.C) { 284 s.isPrivate = true 285 reg, ctrl := s.getRegistry(c, nil) 286 defer ctrl.Finish() 287 288 data := ` 289 {"errors":[{"code":"UNAUTHORIZED","message":"authentication required"}]} 290 `[1:] 291 292 gomock.InOrder( 293 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 294 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic xxxx==="}}) 295 c.Assert(req.Method, gc.Equals, `GET`) 296 c.Assert(req.URL.String(), gc.Equals, `https://66668888.dkr.ecr.eu-west-1.amazonaws.com/v2/jujud-operator/tags/list`) 297 resps := &http.Response{ 298 Request: req, 299 StatusCode: http.StatusForbidden, 300 Body: io.NopCloser(strings.NewReader(data)), 301 } 302 return resps, nil 303 }), 304 ) 305 _, err := reg.Tags("jujud-operator") 306 c.Assert(err, gc.ErrorMatches, `Get "https://66668888.dkr.ecr.eu-west-1.amazonaws.com/v2/jujud-operator/tags/list": non-successful response status=403`) 307 } 308 309 func (s *elasticContainerRegistrySuite) assertGetManifestsSchemaVersion1(c *gc.C, responseData, contentType string, result *internal.ManifestsResult) { 310 // Use v2 for private repository. 311 s.isPrivate = true 312 reg, ctrl := s.getRegistry(c, nil) 313 defer ctrl.Finish() 314 315 gomock.InOrder( 316 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 317 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic xxxx==="}}) 318 c.Assert(req.Method, gc.Equals, `GET`) 319 c.Assert(req.URL.String(), gc.Equals, `https://66668888.dkr.ecr.eu-west-1.amazonaws.com/v2/jujud-operator/manifests/2.9.10`) 320 resps := &http.Response{ 321 Header: http.Header{ 322 http.CanonicalHeaderKey("Content-Type"): []string{contentType}, 323 }, 324 Request: req, 325 StatusCode: http.StatusOK, 326 Body: io.NopCloser(strings.NewReader(responseData)), 327 } 328 return resps, nil 329 }), 330 ) 331 manifests, err := reg.GetManifests("jujud-operator", "2.9.10") 332 c.Assert(err, jc.ErrorIsNil) 333 c.Assert(manifests, jc.DeepEquals, result) 334 } 335 336 func (s *elasticContainerRegistrySuite) TestGetManifestsSchemaVersion1(c *gc.C) { 337 s.assertGetManifestsSchemaVersion1(c, 338 ` 339 { "schemaVersion": 1, "name": "jujuqa/jujud-operator", "tag": "2.9.13", "architecture": "amd64"} 340 `[1:], 341 `application/vnd.docker.distribution.manifest.v1+prettyjws`, 342 &internal.ManifestsResult{Architecture: "amd64"}, 343 ) 344 } 345 346 func (s *elasticContainerRegistrySuite) TestGetManifestsSchemaVersion2(c *gc.C) { 347 s.assertGetManifestsSchemaVersion1(c, 348 ` 349 { 350 "schemaVersion": 2, 351 "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 352 "config": { 353 "mediaType": "application/vnd.docker.container.image.v1+json", 354 "size": 4596, 355 "digest": "sha256:f0609d8a844f7271411c1a9c5d7a898fd9f9c5a4844e3bc7db6d725b54671ac1" 356 } 357 } 358 `[1:], 359 `application/vnd.docker.distribution.manifest.v2+prettyjws`, 360 &internal.ManifestsResult{Digest: "sha256:f0609d8a844f7271411c1a9c5d7a898fd9f9c5a4844e3bc7db6d725b54671ac1"}, 361 ) 362 } 363 364 func (s *elasticContainerRegistrySuite) TestGetBlobs(c *gc.C) { 365 // Use v2 for private repository. 366 s.isPrivate = true 367 reg, ctrl := s.getRegistry(c, nil) 368 defer ctrl.Finish() 369 370 gomock.InOrder( 371 s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 372 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic xxxx==="}}) 373 c.Assert(req.Method, gc.Equals, `GET`) 374 c.Assert(req.URL.String(), gc.Equals, 375 `https://66668888.dkr.ecr.eu-west-1.amazonaws.com/v2/jujud-operator/blobs/sha256:f0609d8a844f7271411c1a9c5d7a898fd9f9c5a4844e3bc7db6d725b54671ac1`, 376 ) 377 resps := &http.Response{ 378 Request: req, 379 StatusCode: http.StatusOK, 380 Body: io.NopCloser(strings.NewReader(` 381 {"architecture":"amd64"} 382 `[1:])), 383 } 384 return resps, nil 385 }), 386 ) 387 manifests, err := reg.GetBlobs("jujud-operator", "sha256:f0609d8a844f7271411c1a9c5d7a898fd9f9c5a4844e3bc7db6d725b54671ac1") 388 c.Assert(err, jc.ErrorIsNil) 389 c.Assert(manifests, jc.DeepEquals, &internal.BlobsResponse{Architecture: "amd64"}) 390 }