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(&registry.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  }