Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/registry oauth2 #13296

Merged
merged 13 commits into from Sep 8, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/juju/commands/upgradecontroller.go
Expand Up @@ -64,7 +64,7 @@ See also:

func newUpgradeControllerCommand(options ...modelcmd.WrapControllerOption) cmd.Command {
command := &upgradeControllerCommand{}
command.registryAPIFunc = registry.NewRegistry
command.registryAPIFunc = registry.New
return modelcmd.WrapController(command, options...)
}

Expand Down Expand Up @@ -305,7 +305,7 @@ func (c *baseUpgradeCommand) initCAASVersions(
for _, a := range streamsAgents {
streamsVersions.Add(a.Version.Number.String())
}
logger.Debugf("found available tags: %v", availableTags)
logger.Debugf("found available tags: %q", availableTags)
var matchingTags tools.Versions
for _, t := range availableTags {
vers := t.AgentVersion()
Expand Down
2 changes: 1 addition & 1 deletion cmd/juju/commands/upgrademodel.go
Expand Up @@ -82,7 +82,7 @@ See also:

func newUpgradeJujuCommand() cmd.Command {
command := &upgradeJujuCommand{}
command.registryAPIFunc = registry.NewRegistry
command.registryAPIFunc = registry.New
return modelcmd.Wrap(command)
}

Expand Down
3 changes: 2 additions & 1 deletion controller/config.go
Expand Up @@ -857,11 +857,12 @@ func validateCAASImageRepo(imageRepo string) (string, error) {
return "", errors.Trace(err)
}
if imageDetails.IsPrivate() {
r, err := registry.NewRegistry(*imageDetails)
r, err := registry.New(*imageDetails)
if err != nil {
return "", errors.Trace(err)
}
*imageDetails = r.ImageRepoDetails()
func() { _ = r.Close() }()
ycliuhw marked this conversation as resolved.
Show resolved Hide resolved
}
return imageDetails.String(), nil
}
Expand Down
7 changes: 3 additions & 4 deletions docker/auth.go
Expand Up @@ -168,11 +168,10 @@ func (rid *ImageRepoDetails) init() error {
}

func (rid ImageRepoDetails) APIVersion() APIVersion {
v := APIVersionV1
if !rid.TokenAuthConfig.Empty() {
v = APIVersionV2
if rid.IsPrivate() {
return APIVersionV2
}
return v
return APIVersionV1
}

func fileExists(p string) (bool, error) {
Expand Down
4 changes: 4 additions & 0 deletions docker/docker.go
Expand Up @@ -32,6 +32,10 @@ func (info imageInfo) AgentVersion() version.Number {
return info.version
}

func (info imageInfo) String() string {
return info.version.String()
}

// NewImageInfo creates an imageInfo.
func NewImageInfo(ver version.Number) tools.HasVersion {
return &imageInfo{version: ver}
Expand Down
67 changes: 67 additions & 0 deletions docker/registry/acr.go
@@ -0,0 +1,67 @@
// Copyright 2021 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package registry

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

"github.com/juju/errors"

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

type acr struct {
*baseClient
}

func newACR(repoDetails docker.ImageRepoDetails, transport http.RoundTripper) RegistryInternal {
ycliuhw marked this conversation as resolved.
Show resolved Hide resolved
c := newBase(repoDetails, DefaultTransport)
return &acr{c}
}

func (c *acr) Match() bool {
return strings.Contains(c.repoDetails.ServerAddress, "azurecr.io")
}

func getUserNameFromAuthForACR(auth string) (string, error) {
content, err := base64.StdEncoding.DecodeString(auth)
if err != nil {
return "", errors.Trace(err)
}
parts := strings.Split(string(content), ":")
if len(parts) < 1 {
return "", errors.NotValidf("auth %q", auth)
}
return parts[0], nil
}

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)
}
}
password := c.repoDetails.Password
if password == "" {
password = c.repoDetails.IdentityToken
}
transport = newTokenTransport(
transport,
username, password,
"", "",
)
}
c.client.Transport = errorTransport{transport}
return nil
}
69 changes: 69 additions & 0 deletions docker/registry/dockerhub.go
@@ -0,0 +1,69 @@
// Copyright 2021 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package registry

import (
"net/http"
"net/url"
"strings"

"github.com/juju/errors"

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

const (
dockerServerAddress = "index.docker.io"
)

type dockerhub struct {
*baseClient
}

func newDockerhub(repoDetails docker.ImageRepoDetails, transport http.RoundTripper) RegistryInternal {
c := newBase(repoDetails, DefaultTransport)
return &dockerhub{c}
}

func (c *dockerhub) Match() bool {
return c.repoDetails.ServerAddress == "" || strings.Contains(c.repoDetails.ServerAddress, "docker")
}

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, "",
)
}
c.client.Transport = errorTransport{transport}
return nil
}

func (c *dockerhub) DecideBaseURL() error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DecideBaseURL seems like it should be called something like Initialise or something.
Actually, the functionality here seems to pertain to the repoDetails value - should this be an internal method used to set up things whenever the repoDetails is set, ie should this be called from newDockerHub and not exposed as a separate method?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DecideBaseURL is exported for generating mocks and it's one of the methods called in initClient.
So DecideBaseURL is just one of the steps of Initialise.
I will move the provider integration code to internal sub-package in the following PR.

if c.repoDetails.ServerAddress == "" {
c.repoDetails.ServerAddress = dockerServerAddress
}
if err := c.baseClient.DecideBaseURL(); err != nil {
return errors.Trace(err)
}
url, err := url.Parse(c.repoDetails.ServerAddress)
if err != nil {
return errors.Trace(err)
}
logger.Criticalf("dockerhub.DecideBaseURL url.String(1) => %q", url.String())
url.Scheme = "https"
logger.Criticalf("dockerhub.DecideBaseURL url.String(2) => %q", url.String())
addr := url.String()
if !strings.HasSuffix(addr, "/") {
// This "/" matters because docker uses url string for the credential key and expects the trailing slash.
addr += "/"
}
c.repoDetails.ServerAddress = addr
logger.Criticalf("dockerhub.DecideBaseURL c.repoDetails.ServerAddress => %q", c.repoDetails.ServerAddress)
return nil
}
30 changes: 30 additions & 0 deletions docker/registry/gcr.go
@@ -0,0 +1,30 @@
// Copyright 2021 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package registry

import (
"net/http"
"strings"

"github.com/juju/errors"

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

type gcr struct {
*baseClient
}

func newGCR(repoDetails docker.ImageRepoDetails, transport http.RoundTripper) RegistryInternal {
ycliuhw marked this conversation as resolved.
Show resolved Hide resolved
c := newBase(repoDetails, DefaultTransport)
return &gcr{c}
}

func (c *gcr) Match() bool {
return strings.Contains(c.repoDetails.ServerAddress, "gcr.io")
}

func (c *gcr) WrapTransport() error {
return errors.NotSupportedf("GCR")
}
76 changes: 76 additions & 0 deletions docker/registry/github.go
@@ -0,0 +1,76 @@
// Copyright 2021 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package registry

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

"github.com/juju/errors"

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

type github struct {
*baseClient
}

func newGithub(repoDetails docker.ImageRepoDetails, transport http.RoundTripper) RegistryInternal {
ycliuhw marked this conversation as resolved.
Show resolved Hide resolved
c := newBase(repoDetails, DefaultTransport)
return &github{c}
}

func (c *github) Match() bool {
return strings.Contains(c.repoDetails.ServerAddress, "ghcr.io")
}

func getBearerTokenForGithub(auth string) (string, error) {
if auth == "" {
return "", errors.NotValidf("empty github container registry auth token")
}
content, err := base64.StdEncoding.DecodeString(auth)
if err != nil {
return "", errors.Trace(err)
}
parts := strings.Split(string(content), ":")
if len(parts) < 2 {
return "", errors.NotValidf("github container registry auth token %q", auth)
}
token := parts[1]
return base64.StdEncoding.EncodeToString([]byte(token)), nil
}

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)
}
transport = newTokenTransport(
transport, "", "", "", bearerToken,
)
}
c.client.Transport = errorTransport{transport}
return nil
}

// Ping pings the github endpoint.
func (c github) Ping() error {
url := c.url("/")
if !strings.HasSuffix(url, "/") {
// github v2 root endpoint requires the trailing slash(otherwise 404 returns).
url += "/"
}
logger.Debugf("github ping %q", url)
resp, err := c.client.Get(url)
if resp != nil {
defer resp.Body.Close()
}
return errors.Trace(err)
}
38 changes: 38 additions & 0 deletions docker/registry/gitlab.go
@@ -0,0 +1,38 @@
// Copyright 2021 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package registry

import (
"net/http"
"strings"

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

type gitlab struct {
*baseClient
}

func newGitlab(repoDetails docker.ImageRepoDetails, transport http.RoundTripper) RegistryInternal {
ycliuhw marked this conversation as resolved.
Show resolved Hide resolved
c := newBase(repoDetails, DefaultTransport)
return &gitlab{c}
}

func (c *gitlab) Match() bool {
return strings.Contains(c.repoDetails.ServerAddress, "registry.gitlab.com")
}

func (c *gitlab) 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, "",
)
}
c.client.Transport = errorTransport{transport}
return nil
}
40 changes: 40 additions & 0 deletions docker/registry/interface.go
@@ -0,0 +1,40 @@
// Copyright 2021 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package registry

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

//go:generate go run github.com/golang/mock/mockgen -package mocks -destination mocks/http_mock.go net/http RoundTripper
//go:generate go run github.com/golang/mock/mockgen -package mocks -destination mocks/registry_mock.go github.com/juju/juju/docker/registry Registry,RegistryInternal,Matcher,Initializer

// Registry provides APIs to interact with the OCI provider client.
type Registry interface {
Tags(string) (tools.Versions, error)
Close() error
Ping() error
ImageRepoDetails() docker.ImageRepoDetails
}

// RegistryInternal provides methods of registry clients for internal operations.
// It's exported for generating mocks for tests.
type RegistryInternal interface {
Matcher
Registry
Initializer
}

// Matcher provides a method for selecting which registry client to use.
type Matcher interface {
Match() bool
}

// Initializer provides methods for initializing the registry client.
type Initializer interface {
WrapTransport() error
DecideBaseURL() error
Ping() error
}