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  }