Skip to content

Commit

Permalink
Merge pull request #13400 from ycliuhw/enhance-ghcr
Browse files Browse the repository at this point in the history
#13400

This PR allows Github Container Registry using username and password to be authenticated for

- pulling `jujud-operator`, `juju-db` and `charm-base` images from k8s controllers, application operators, and sidecar pods;
- fetching available controller versions to upgrade for `upgrade-controller` command;


Tested registries are(the rest of the registries will be implemented and tested in the following PRs):

| Registry | Public | Private |
| --- | --- | --- |
| azurecr.io | ❌ | ✅ |
| docker.io | ✅ | ✅ |
| ecr | ❌ | ✅ |
| gcr.io | ✅ | ✅ |
| ghcr.io | ❌ | ✅ |
| registry.gitlab.com | ❌ | ✅ |
| quay.io | ✅ | ✅ |

## Checklist

 - [ ] ~Requires a [pylibjuju](https://github.com/juju/python-libjuju) change~
 - [ ] ~Added [integration tests](https://github.com/juju/juju/tree/develop/tests) for the PR~
 - [ ] ~Added or updated [doc.go](https://discourse.jujucharms.com/t/readme-in-packages/451) related to packages changed~
 - [x] Comments answer the question of why design decisions were made

## QA steps

```console
$ cat credential.json
{
 "auth": "xxx==",
 "repository": "ghcr.io/jujuqa"
}

# or 
$ cat credential.json
{
 "repository": "ghcr.io/jujuqa",
 "password": "ghp_xxxxxx"
}

$ juju bootstrap microk8s k1 --config caas-image-repo="'$(cat credential.json)'" 

```

## Documentation changes

https://discourse.charmhub.io/t/initial-private-registry-support/5079

## Bug reference


https://bugs.launchpad.net/juju/+bug/1935830
https://bugs.launchpad.net/juju/+bug/1940820
https://bugs.launchpad.net/juju/+bug/1935953
  • Loading branch information
jujubot committed Oct 7, 2021
2 parents f970413 + 12861ef commit 90e5368
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 35 deletions.
1 change: 1 addition & 0 deletions controller/config.go
Expand Up @@ -865,6 +865,7 @@ func validateCAASImageRepo(imageRepo string) (string, error) {

if err = r.Ping(); registryutils.IsPublicAPINotAvailableError(err) {
logger.Warningf("docker registry for %q requires authentication: %v", imageDetails.Repository, err)
logger.Warningf("upgrade controller will not work if no authentication provided!")
} else if err != nil {
return "", errors.Trace(err)
}
Expand Down
47 changes: 21 additions & 26 deletions docker/registry/internal/github.go
Expand Up @@ -27,37 +27,32 @@ func (c *githubContainerRegistry) 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 githubContainerRegistryTransport(transport http.RoundTripper, repoDetails *docker.ImageRepoDetails,
) (http.RoundTripper, error) {
if !repoDetails.BasicAuthConfig.Empty() {
bearerToken, err := getBearerTokenForGithub(repoDetails.Auth.Value)
if err != nil {
return nil, errors.Trace(err)
}
return newTokenTransport(
transport, "", "", "", bearerToken, true,
), nil
}
if !repoDetails.TokenAuthConfig.Empty() {
return nil, errors.NewNotValid(nil, "github only supports username and password or auth token")
}
return transport, nil
if repoDetails.BasicAuthConfig.Empty() {
// We allow github container registry config without auth details but we will raise PublicAPINotAvailableError.
// Because github allows image fetching without credential but their registry API always requires a credential.
return transport, nil
}
password := repoDetails.Password
if password == "" {
if repoDetails.Auth.Empty() {
return nil, errors.NewNotValid(nil, `github container registry requires {"username", "password"} or {"auth"} token`)
}
var err error
_, password, err = unpackAuthToken(repoDetails.Auth.Value)
if err != nil {
return nil, errors.Annotate(err, "getting password from the github container registry auth token")
}
if password == "" {
return nil, errors.NewNotValid(nil, "github container registry auth token contains empty password")
}
}
bearerToken := base64.StdEncoding.EncodeToString([]byte(password))
return newTokenTransport(transport, "", "", "", bearerToken, true), nil
}

func (c *githubContainerRegistry) WrapTransport(...TransportWrapper) (err error) {
Expand Down
73 changes: 64 additions & 9 deletions docker/registry/internal/github_test.go
Expand Up @@ -28,22 +28,29 @@ type githubSuite struct {
testing.IsolationSuite

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

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

func (s *githubSuite) TearDownTest(c *gc.C) {
s.imageRepoDetails = nil
s.IsolationSuite.TearDownTest(c)
}

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

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

Expand All @@ -64,7 +71,7 @@ func (s *githubSuite) getRegistry(c *gc.C) (registry.Registry, *gomock.Controlle
}
s.PatchValue(&registry.DefaultTransport, s.mockRoundTripper)

reg, err := registry.New(s.imageRepoDetails)
reg, err := registry.New(*s.imageRepoDetails)
c.Assert(err, jc.ErrorIsNil)
_, ok := reg.(*internal.GithubContainerRegistry)
c.Assert(ok, jc.IsTrue)
Expand All @@ -90,6 +97,54 @@ func (s *githubSuite) TestPingPrivateRepository(c *gc.C) {
ctrl.Finish()
}

func (s *githubSuite) TestPingPrivateRepositoryUserNamePassword(c *gc.C) {
s.imageRepoDetails = &docker.ImageRepoDetails{
Repository: "ghcr.io/jujuqa",
BasicAuthConfig: docker.BasicAuthConfig{
Username: "username",
Password: "pwd",
},
}
s.isPrivate = true
_, ctrl := s.getRegistry(c)
ctrl.Finish()
}

func (s *githubSuite) TestPingPrivateRepositoryNoCredential(c *gc.C) {
imageRepoDetails := docker.ImageRepoDetails{
Repository: "ghcr.io/jujuqa",
BasicAuthConfig: docker.BasicAuthConfig{
Username: "username",
},
}
_, err := registry.New(imageRepoDetails)
c.Assert(err, gc.ErrorMatches, `github container registry requires {"username", "password"} or {"auth"} token`)
}

func (s *githubSuite) TestPingPrivateRepositoryBadAuthTokenFormat(c *gc.C) {
authToken := base64.StdEncoding.EncodeToString([]byte("bad-auth"))
imageRepoDetails := docker.ImageRepoDetails{
Repository: "ghcr.io/jujuqa",
BasicAuthConfig: docker.BasicAuthConfig{
Auth: docker.NewToken(authToken),
},
}
_, err := registry.New(imageRepoDetails)
c.Assert(err, gc.ErrorMatches, `getting password from the github container registry auth token: registry auth token not valid`)
}

func (s *githubSuite) TestPingPrivateRepositoryBadAuthTokenNoPasswordIncluded(c *gc.C) {
authToken := base64.StdEncoding.EncodeToString([]byte("username:"))
imageRepoDetails := docker.ImageRepoDetails{
Repository: "ghcr.io/jujuqa",
BasicAuthConfig: docker.BasicAuthConfig{
Auth: docker.NewToken(authToken),
},
}
_, err := registry.New(imageRepoDetails)
c.Assert(err, gc.ErrorMatches, `github container registry auth token contains empty password`)
}

func (s *githubSuite) TestTagsPublic(c *gc.C) {
s.isPrivate = false
reg, ctrl := s.getRegistry(c)
Expand Down

0 comments on commit 90e5368

Please sign in to comment.