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  }