github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/docker/registry/internal/github_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 githubSuite struct {
    27  	testing.IsolationSuite
    28  
    29  	mockRoundTripper *mocks.MockRoundTripper
    30  	imageRepoDetails *docker.ImageRepoDetails
    31  	isPrivate        bool
    32  }
    33  
    34  var _ = gc.Suite(&githubSuite{})
    35  
    36  func (s *githubSuite) TearDownTest(c *gc.C) {
    37  	s.imageRepoDetails = nil
    38  	s.IsolationSuite.TearDownTest(c)
    39  }
    40  
    41  func (s *githubSuite) getRegistry(c *gc.C) (registry.Registry, *gomock.Controller) {
    42  	ctrl := gomock.NewController(c)
    43  
    44  	if s.imageRepoDetails == nil {
    45  		s.imageRepoDetails = &docker.ImageRepoDetails{
    46  			Repository: "ghcr.io/jujuqa",
    47  		}
    48  		if s.isPrivate {
    49  			authToken := base64.StdEncoding.EncodeToString([]byte("username:pwd"))
    50  			s.imageRepoDetails.BasicAuthConfig = docker.BasicAuthConfig{
    51  				Auth: docker.NewToken(authToken),
    52  			}
    53  		}
    54  	}
    55  
    56  	s.mockRoundTripper = mocks.NewMockRoundTripper(ctrl)
    57  	if s.isPrivate {
    58  		gomock.InOrder(
    59  			// registry.Ping()
    60  			s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
    61  				func(req *http.Request) (*http.Response, error) {
    62  					authToken := base64.StdEncoding.EncodeToString([]byte("pwd"))
    63  					c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer " + authToken}})
    64  					c.Assert(req.Method, gc.Equals, `GET`)
    65  					c.Assert(req.URL.String(), gc.Equals, `https://ghcr.io/v2/`)
    66  					return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(nil)}, nil
    67  				},
    68  			),
    69  		)
    70  	}
    71  	s.PatchValue(&registry.DefaultTransport, s.mockRoundTripper)
    72  
    73  	reg, err := registry.New(*s.imageRepoDetails)
    74  	c.Assert(err, jc.ErrorIsNil)
    75  	_, ok := reg.(*internal.GithubContainerRegistry)
    76  	c.Assert(ok, jc.IsTrue)
    77  	err = reg.Ping()
    78  	c.Assert(err, jc.ErrorIsNil)
    79  	return reg, ctrl
    80  }
    81  
    82  func (s *githubSuite) TestPingPublicRepository(c *gc.C) {
    83  	s.isPrivate = false
    84  	_, ctrl := s.getRegistry(c)
    85  	ctrl.Finish()
    86  }
    87  
    88  func (s *githubSuite) TestPingPrivateRepository(c *gc.C) {
    89  	s.isPrivate = true
    90  	_, ctrl := s.getRegistry(c)
    91  	ctrl.Finish()
    92  }
    93  
    94  func (s *githubSuite) TestPingPrivateRepositoryUserNamePassword(c *gc.C) {
    95  	s.imageRepoDetails = &docker.ImageRepoDetails{
    96  		Repository: "ghcr.io/jujuqa",
    97  		BasicAuthConfig: docker.BasicAuthConfig{
    98  			Username: "username",
    99  			Password: "pwd",
   100  		},
   101  	}
   102  	s.isPrivate = true
   103  	_, ctrl := s.getRegistry(c)
   104  	ctrl.Finish()
   105  }
   106  
   107  func (s *githubSuite) TestPingPrivateRepositoryNoCredential(c *gc.C) {
   108  	imageRepoDetails := docker.ImageRepoDetails{
   109  		Repository: "ghcr.io/jujuqa",
   110  		BasicAuthConfig: docker.BasicAuthConfig{
   111  			Username: "username",
   112  		},
   113  	}
   114  	_, err := registry.New(imageRepoDetails)
   115  	c.Assert(err, gc.ErrorMatches, `github container registry requires {"username", "password"} or {"auth"} token`)
   116  }
   117  
   118  func (s *githubSuite) TestPingPrivateRepositoryBadAuthTokenFormat(c *gc.C) {
   119  	authToken := base64.StdEncoding.EncodeToString([]byte("bad-auth"))
   120  	imageRepoDetails := docker.ImageRepoDetails{
   121  		Repository: "ghcr.io/jujuqa",
   122  		BasicAuthConfig: docker.BasicAuthConfig{
   123  			Auth: docker.NewToken(authToken),
   124  		},
   125  	}
   126  	_, err := registry.New(imageRepoDetails)
   127  	c.Assert(err, gc.ErrorMatches, `getting password from the github container registry auth token: registry auth token not valid`)
   128  }
   129  
   130  func (s *githubSuite) TestPingPrivateRepositoryBadAuthTokenNoPasswordIncluded(c *gc.C) {
   131  	authToken := base64.StdEncoding.EncodeToString([]byte("username:"))
   132  	imageRepoDetails := docker.ImageRepoDetails{
   133  		Repository: "ghcr.io/jujuqa",
   134  		BasicAuthConfig: docker.BasicAuthConfig{
   135  			Auth: docker.NewToken(authToken),
   136  		},
   137  	}
   138  	_, err := registry.New(imageRepoDetails)
   139  	c.Assert(err, gc.ErrorMatches, `github container registry auth token contains empty password`)
   140  }
   141  
   142  func (s *githubSuite) TestTagsPublicRegistry(c *gc.C) {
   143  	// Use anonymous login for public repository.
   144  	s.isPrivate = false
   145  	reg, ctrl := s.getRegistry(c)
   146  	defer ctrl.Finish()
   147  
   148  	data := `
   149  {"name":"jujuqa/jujud-operator","tags":["2.9.10.1","2.9.10.2","2.9.10"]}
   150  `[1:]
   151  
   152  	gomock.InOrder(
   153  		s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
   154  			c.Assert(req.Header, jc.DeepEquals, http.Header{})
   155  			c.Assert(req.Method, gc.Equals, `GET`)
   156  			c.Assert(req.URL.String(), gc.Equals, `https://ghcr.io/v2/jujuqa/jujud-operator/tags/list`)
   157  			return &http.Response{
   158  				Request:    req,
   159  				StatusCode: http.StatusUnauthorized,
   160  				Body:       io.NopCloser(nil),
   161  				Header: http.Header{
   162  					http.CanonicalHeaderKey("WWW-Authenticate"): []string{
   163  						`Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:jujuqa/jujud-operator:pull"`,
   164  					},
   165  				},
   166  			}, nil
   167  		}),
   168  		// Refresh OAuth Token without credential.
   169  		s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
   170  			func(req *http.Request) (*http.Response, error) {
   171  				c.Assert(req.Header, jc.DeepEquals, http.Header{})
   172  				c.Assert(req.Method, gc.Equals, `GET`)
   173  				c.Assert(req.URL.String(), gc.Equals, `https://ghcr.io/token?scope=repository%3Ajujuqa%2Fjujud-operator%3Apull&service=ghcr.io`)
   174  				return &http.Response{
   175  					Request:    req,
   176  					StatusCode: http.StatusOK,
   177  					Body:       io.NopCloser(strings.NewReader(`{"token": "jwt-token", "access_token": "jwt-token","expires_in": 300}`)),
   178  				}, nil
   179  			},
   180  		),
   181  		s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
   182  			c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer jwt-token"}})
   183  			c.Assert(req.Method, gc.Equals, `GET`)
   184  			c.Assert(req.URL.String(), gc.Equals, `https://ghcr.io/v2/jujuqa/jujud-operator/tags/list`)
   185  			resps := &http.Response{
   186  				Request:    req,
   187  				StatusCode: http.StatusOK,
   188  				Body:       io.NopCloser(strings.NewReader(data)),
   189  			}
   190  			return resps, nil
   191  		}),
   192  	)
   193  	vers, err := reg.Tags("jujud-operator")
   194  	c.Assert(err, jc.ErrorIsNil)
   195  	c.Assert(vers, jc.DeepEquals, tools.Versions{
   196  		image.NewImageInfo(version.MustParse("2.9.10.1")),
   197  		image.NewImageInfo(version.MustParse("2.9.10.2")),
   198  		image.NewImageInfo(version.MustParse("2.9.10")),
   199  	})
   200  }
   201  
   202  func (s *githubSuite) TestTagsPrivateRegistry(c *gc.C) {
   203  	// Use v2 for private repository.
   204  	s.isPrivate = true
   205  	reg, ctrl := s.getRegistry(c)
   206  	defer ctrl.Finish()
   207  
   208  	data := `
   209  {"name":"jujuqa/jujud-operator","tags":["2.9.10.1","2.9.10.2","2.9.10"]}
   210  `[1:]
   211  
   212  	gomock.InOrder(
   213  		s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
   214  			authToken := base64.StdEncoding.EncodeToString([]byte("pwd"))
   215  			c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer " + authToken}})
   216  			c.Assert(req.Method, gc.Equals, `GET`)
   217  			c.Assert(req.URL.String(), gc.Equals, `https://ghcr.io/v2/jujuqa/jujud-operator/tags/list`)
   218  			resps := &http.Response{
   219  				Request:    req,
   220  				StatusCode: http.StatusOK,
   221  				Body:       io.NopCloser(strings.NewReader(data)),
   222  			}
   223  			return resps, nil
   224  		}),
   225  	)
   226  	vers, err := reg.Tags("jujud-operator")
   227  	c.Assert(err, jc.ErrorIsNil)
   228  	c.Assert(vers, jc.DeepEquals, tools.Versions{
   229  		image.NewImageInfo(version.MustParse("2.9.10.1")),
   230  		image.NewImageInfo(version.MustParse("2.9.10.2")),
   231  		image.NewImageInfo(version.MustParse("2.9.10")),
   232  	})
   233  }
   234  
   235  func (s *githubSuite) TestTagsErrorResponse(c *gc.C) {
   236  	s.isPrivate = true
   237  	reg, ctrl := s.getRegistry(c)
   238  	defer ctrl.Finish()
   239  
   240  	data := `
   241  {"errors":[{"code":"UNAUTHORIZED","message":"authentication required"}]}
   242  `[1:]
   243  
   244  	gomock.InOrder(
   245  		s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
   246  			authToken := base64.StdEncoding.EncodeToString([]byte("pwd"))
   247  			c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer " + authToken}})
   248  			c.Assert(req.Method, gc.Equals, `GET`)
   249  			c.Assert(req.URL.String(), gc.Equals, `https://ghcr.io/v2/jujuqa/jujud-operator/tags/list`)
   250  			resps := &http.Response{
   251  				Request:    req,
   252  				StatusCode: http.StatusForbidden,
   253  				Body:       io.NopCloser(strings.NewReader(data)),
   254  			}
   255  			return resps, nil
   256  		}),
   257  	)
   258  	_, err := reg.Tags("jujud-operator")
   259  	c.Assert(err, gc.ErrorMatches, `Get "https://ghcr.io/v2/jujuqa/jujud-operator/tags/list": non-successful response status=403`)
   260  }