Skip to content

Commit

Permalink
Add tests for the registry package;
Browse files Browse the repository at this point in the history
  • Loading branch information
ycliuhw committed Sep 3, 2021
1 parent 06a5476 commit 81f9c61
Show file tree
Hide file tree
Showing 11 changed files with 932 additions and 146 deletions.
57 changes: 38 additions & 19 deletions docker/registry/acr.go
Expand Up @@ -11,8 +11,10 @@ import (
"github.com/juju/errors"

"github.com/juju/juju/docker"
"github.com/juju/juju/tools"
)

// TODO(ycliuhw): test and verify ACR integration further.
type acr struct {
*baseClient
}
Expand All @@ -39,29 +41,46 @@ func getUserNameFromAuthForACR(auth string) (string, error) {
}

func (c *acr) WrapTransport() error {
if !c.repoDetails.IsPrivate() {
return nil
}
transport := c.client.Transport
if !c.repoDetails.TokenAuthConfig.Empty() {
username := c.repoDetails.Username
if username == "" {
var err error
username, err = getUserNameFromAuthForACR(c.repoDetails.Auth)
if err != nil {
return errors.Trace(err)
if c.repoDetails.IsPrivate() {
if !c.repoDetails.TokenAuthConfig.Empty() {
username := c.repoDetails.Username
if username == "" {
var err error
username, err = getUserNameFromAuthForACR(c.repoDetails.Auth)
if err != nil {
return errors.Trace(err)
}
}
password := c.repoDetails.Password
if password == "" {
password = c.repoDetails.IdentityToken
}
transport = newTokenTransport(
transport,
username, password,
"", "",
)
}
password := c.repoDetails.Password
if password == "" {
password = c.repoDetails.IdentityToken
}
transport = newTokenTransport(
transport,
username, password,
"", "",
)
}
c.client.Transport = errorTransport{transport}
return nil
}

// Tags fetches tags for an OCI image.
func (c acr) Tags(imageName string) (versions tools.Versions, err error) {
apiVersion := c.repoDetails.APIVersion()

if apiVersion == docker.APIVersionV1 {
url := c.url("/repositories/%s/tags", imageName)
var response tagsResponseV1
return c.fetchTags(url, &response)
}
if apiVersion == docker.APIVersionV2 {
url := c.url("/%s/tags/list", imageName)
var response tagsResponseV2
return c.fetchTags(url, &response)
}
// This should never happen.
return nil, nil
}
16 changes: 9 additions & 7 deletions docker/registry/dockerhub.go
Expand Up @@ -31,14 +31,16 @@ func (c *dockerhub) Match() bool {
}

func (c *dockerhub) WrapTransport() error {
if !c.repoDetails.IsPrivate() {
return nil
}
transport := c.client.Transport
if !c.repoDetails.BasicAuthConfig.Empty() {
transport = newTokenTransport(
transport, c.repoDetails.Username, c.repoDetails.Password, c.repoDetails.Auth, "",
)
if c.repoDetails.IsPrivate() {
if !c.repoDetails.BasicAuthConfig.Empty() {
transport = newTokenTransport(
transport, c.repoDetails.Username, c.repoDetails.Password, c.repoDetails.Auth, "",
)
}
if !c.repoDetails.TokenAuthConfig.Empty() {
return errors.New("dockerhub only supports username and password or auth token")
}
}
c.client.Transport = errorTransport{transport}
return nil
Expand Down
252 changes: 252 additions & 0 deletions docker/registry/dockerhub_test.go
@@ -0,0 +1,252 @@
// Copyright 2021 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package registry_test

import (
"encoding/base64"
"io/ioutil"
"net/http"
"strings"

"github.com/golang/mock/gomock"
"github.com/juju/testing"
jc "github.com/juju/testing/checkers"
"github.com/juju/version/v2"
gc "gopkg.in/check.v1"

"github.com/juju/juju/docker"
"github.com/juju/juju/docker/registry"
"github.com/juju/juju/docker/registry/mocks"
"github.com/juju/juju/feature"
coretesting "github.com/juju/juju/testing"
"github.com/juju/juju/tools"
)

type dockerhubSuite struct {
testing.IsolationSuite
coretesting.JujuOSEnvSuite

mockRoundTripper *mocks.MockRoundTripper
imageRepoDetails docker.ImageRepoDetails
isPrivate bool
}

var _ = gc.Suite(&dockerhubSuite{})

func (s *dockerhubSuite) SetUpTest(c *gc.C) {
s.IsolationSuite.SetUpTest(c)
s.JujuOSEnvSuite.SetUpTest(c)
s.SetFeatureFlags(feature.PrivateRegistry)
}

func (s *dockerhubSuite) TearDownTest(c *gc.C) {
s.IsolationSuite.TearDownTest(c)
s.JujuOSEnvSuite.TearDownTest(c)

s.mockRoundTripper = nil
}

func (s *dockerhubSuite) getRegistry(c *gc.C) (registry.Registry, *gomock.Controller) {
ctrl := gomock.NewController(c)

s.imageRepoDetails = docker.ImageRepoDetails{
Repository: "jujuqa",
}
authToken := base64.StdEncoding.EncodeToString([]byte("username:pwd"))
if s.isPrivate {
s.imageRepoDetails.BasicAuthConfig = docker.BasicAuthConfig{
Auth: authToken,
}
}

s.mockRoundTripper = mocks.NewMockRoundTripper(ctrl)
if s.isPrivate {
gomock.InOrder(
// registry.Ping() 1st try failed - bearer token was missing.
s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
func(req *http.Request) (*http.Response, error) {
c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer "}})
c.Assert(req.Method, gc.Equals, `GET`)
c.Assert(req.URL.String(), gc.Equals, `https://index.docker.io/v2`)
return &http.Response{
Request: req,
StatusCode: http.StatusUnauthorized,
Body: ioutil.NopCloser(nil),
Header: http.Header{
http.CanonicalHeaderKey("WWW-Authenticate"): []string{
`Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:jujuqa/jujud-operator:pull"`,
},
},
}, nil
},
),
// Refresh OAuth Token
s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
func(req *http.Request) (*http.Response, error) {
c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + authToken}})
c.Assert(req.Method, gc.Equals, `GET`)
c.Assert(req.URL.String(), gc.Equals, `https://auth.docker.io/token?scope=repository%3Ajujuqa%2Fjujud-operator%3Apull&service=registry.docker.io`)
return &http.Response{
Request: req,
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(strings.NewReader(`{"token": "jwt-token", "access_token": "jwt-token","expires_in": 300}`)),
}, nil
},
),
// registry.Ping()
s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
func(req *http.Request) (*http.Response, error) {
c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer " + `jwt-token`}})
c.Assert(req.Method, gc.Equals, `GET`)
c.Assert(req.URL.String(), gc.Equals, `https://index.docker.io/v2`)
return &http.Response{StatusCode: http.StatusOK, Body: ioutil.NopCloser(nil)}, nil
},
),
)
} else {
gomock.InOrder(
// registry.Ping()
s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
func(req *http.Request) (*http.Response, error) {
c.Assert(req.Method, gc.Equals, `GET`)
c.Assert(req.URL.String(), gc.Equals, `https://index.docker.io/v1`)
return &http.Response{Request: req, StatusCode: http.StatusOK, Body: ioutil.NopCloser(nil)}, nil
},
),
)
}
s.PatchValue(&registry.DefaultTransport, s.mockRoundTripper)

reg, err := registry.New(s.imageRepoDetails)
c.Assert(err, jc.ErrorIsNil)
return reg, ctrl
}

func (s *dockerhubSuite) TestPingPublicRepository(c *gc.C) {
s.isPrivate = false
_, ctrl := s.getRegistry(c)
ctrl.Finish()
}

func (s *dockerhubSuite) TestPingPrivateRepository(c *gc.C) {
s.isPrivate = true
_, ctrl := s.getRegistry(c)
ctrl.Finish()
}

func (s *dockerhubSuite) TestTagsV1(c *gc.C) {
// Use v1 for public repository.
s.isPrivate = false
reg, ctrl := s.getRegistry(c)
defer ctrl.Finish()

data := `
[{"name": "2.9.10.1"},{"name": "2.9.10.2"},{"name": "2.9.10"}]
`[1:]

gomock.InOrder(
s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
c.Assert(req.Header, jc.DeepEquals, http.Header{})
c.Assert(req.Method, gc.Equals, `GET`)
c.Assert(req.URL.String(), gc.Equals, `https://index.docker.io/v1/repositories/jujuqa/jujud-operator/tags`)
resps := &http.Response{
Request: req,
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(strings.NewReader(data)),
}
return resps, nil
}),
)
vers, err := reg.Tags("jujud-operator")
c.Assert(err, jc.ErrorIsNil)
c.Assert(vers, jc.DeepEquals, tools.Versions{
registry.NewImageInfo(version.MustParse("2.9.10.1")),
registry.NewImageInfo(version.MustParse("2.9.10.2")),
registry.NewImageInfo(version.MustParse("2.9.10")),
})
}

func (s *dockerhubSuite) TestTagsV2(c *gc.C) {
// Use v2 for private repository.
s.isPrivate = true
reg, ctrl := s.getRegistry(c)
defer ctrl.Finish()

data := `
{"name":"jujuqa/jujud-operator","tags":["2.9.10.1","2.9.10.2","2.9.10"]}
`[1:]

gomock.InOrder(
s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer jwt-token"}})
c.Assert(req.Method, gc.Equals, `GET`)
c.Assert(req.URL.String(), gc.Equals, `https://index.docker.io/v2/jujuqa/jujud-operator/tags/list`)
resps := &http.Response{
Request: req,
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(strings.NewReader(data)),
}
return resps, nil
}),
)
vers, err := reg.Tags("jujud-operator")
c.Assert(err, jc.ErrorIsNil)
c.Assert(vers, jc.DeepEquals, tools.Versions{
registry.NewImageInfo(version.MustParse("2.9.10.1")),
registry.NewImageInfo(version.MustParse("2.9.10.2")),
registry.NewImageInfo(version.MustParse("2.9.10")),
})
}

func (s *dockerhubSuite) TestTagsErrorResponseV1(c *gc.C) {
s.isPrivate = false
reg, ctrl := s.getRegistry(c)
defer ctrl.Finish()

data := `
{"errors":[{"code":"UNAUTHORIZED","message":"authentication required"}]}
`[1:]

gomock.InOrder(
s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
c.Assert(req.Header, jc.DeepEquals, http.Header{})
c.Assert(req.Method, gc.Equals, `GET`)
c.Assert(req.URL.String(), gc.Equals, `https://index.docker.io/v1/repositories/jujuqa/jujud-operator/tags`)
resps := &http.Response{
Request: req,
StatusCode: http.StatusForbidden,
Body: ioutil.NopCloser(strings.NewReader(data)),
}
return resps, nil
}),
)
_, err := reg.Tags("jujud-operator")
c.Assert(err, gc.ErrorMatches, `Get "https://index.docker.io/v1/repositories/jujuqa/jujud-operator/tags": non-successful response \(status=403 body=.*\)`)
}

func (s *dockerhubSuite) TestTagsErrorResponseV2(c *gc.C) {
s.isPrivate = true
reg, ctrl := s.getRegistry(c)
defer ctrl.Finish()

data := `
{"errors":[{"code":"UNAUTHORIZED","message":"authentication required"}]}
`[1:]

gomock.InOrder(
s.mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer jwt-token"}})
c.Assert(req.Method, gc.Equals, `GET`)
c.Assert(req.URL.String(), gc.Equals, `https://index.docker.io/v2/jujuqa/jujud-operator/tags/list`)
resps := &http.Response{
Request: req,
StatusCode: http.StatusForbidden,
Body: ioutil.NopCloser(strings.NewReader(data)),
}
return resps, nil
}),
)
_, err := reg.Tags("jujud-operator")
c.Assert(err, gc.ErrorMatches, `Get "https://index.docker.io/v2/jujuqa/jujud-operator/tags/list": non-successful response \(status=403 body=.*\)`)
}
20 changes: 10 additions & 10 deletions docker/registry/github.go
Expand Up @@ -43,19 +43,19 @@ func getBearerTokenForGithub(auth string) (string, error) {
}

func (c *github) WrapTransport() error {
if !c.repoDetails.IsPrivate() {
return nil
}
transport := c.client.Transport
if !c.repoDetails.BasicAuthConfig.Empty() {
bearerToken, err := getBearerTokenForGithub(c.repoDetails.Auth)
if err != nil {
return errors.Trace(err)
if c.repoDetails.IsPrivate() {
if !c.repoDetails.BasicAuthConfig.Empty() {
bearerToken, err := getBearerTokenForGithub(c.repoDetails.Auth)
if err != nil {
return errors.Trace(err)
}
transport = newTokenTransport(
transport, "", "", "", bearerToken,
)
}
transport = newTokenTransport(
transport, "", "", "", bearerToken,
)
}
// TODO(ycliuhw): support github public registry.
c.client.Transport = errorTransport{transport}
return nil
}
Expand Down

0 comments on commit 81f9c61

Please sign in to comment.