Skip to content
This repository has been archived by the owner on Dec 7, 2023. It is now read-only.

Commit

Permalink
Add loading credentials from docker cli config
Browse files Browse the repository at this point in the history
This adds a package pkg/runtime/auth with helpers to read the docker cli
config and load the credentials for the host name of a given image. This
is based on nerdctl's dockerconfigresolver.

When using containerd as the runtime, the credentials from docker cli
config is loaded into a containerd remote resolver and passed to the
containerd remote option used for pulling.

When using docker as the runtime, the credentials from docker cli config
is loaded into the docker image pull options in the required format.
  • Loading branch information
darkowlzz committed May 9, 2021
1 parent 591e8e2 commit e7ec78d
Show file tree
Hide file tree
Showing 33 changed files with 2,702 additions and 2 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
github.com/containernetworking/plugins v0.8.7
github.com/containers/image v3.0.2+incompatible
github.com/coreos/go-iptables v0.4.5
github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492
github.com/docker/docker v20.10.6+incompatible
github.com/docker/go-connections v0.4.0
github.com/firecracker-microvm/firecracker-go-sdk v0.22.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -263,13 +263,15 @@ github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492 h1:FwssHbCDJD025h+BchanCwE1Q8fyMgqDr2mOQAWOLGw=
github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v0.0.0-20190711223531-1fb7fffdb266 h1:6BCth6L0iZKTU3F0OxqlkECwdmUDLbHdD9qz6HXlpb4=
github.com/docker/distribution v0.0.0-20190711223531-1fb7fffdb266/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.6+incompatible h1:oXI3Vas8TI8Eu/EjH4srKHJBVqraSzJybhxY7Om9faQ=
github.com/docker/docker v20.10.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ=
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
Expand Down
97 changes: 97 additions & 0 deletions pkg/runtime/auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package auth

import (
"fmt"

"github.com/containerd/containerd/remotes/docker"
dockercliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/credentials"
dockercliconfigtypes "github.com/docker/cli/cli/config/types"
log "github.com/sirupsen/logrus"
)

// NOTE: The file is based on nerdctl's dockerconfigresolver.
// Refer: https://github.com/containerd/nerdctl/blob/v0.8.1/pkg/imgutil/dockerconfigresolver/dockerconfigresolver.go

// AuthCreds is for docker.WithAuthCreds used in containerd remote resolver.
type AuthCreds func(string) (string, string, error)

// NewAuthCreds returns an AuthCreds which loads the credentials from the
// docker client config.
func NewAuthCreds(refHostname string) (AuthCreds, error) {
// Load does not raise an error on ENOENT
dockerConfigFile, err := dockercliconfig.Load("")
if err != nil {
return nil, err
}

// DefaultHost converts "docker.io" to "registry-1.docker.io",
// which is wanted by credFunc .
credFuncExpectedHostname, err := docker.DefaultHost(refHostname)
if err != nil {
return nil, err
}

var credFunc AuthCreds

authConfigHostnames := []string{refHostname}
if refHostname == "docker.io" || refHostname == "registry-1.docker.io" {
// "docker.io" appears as ""https://index.docker.io/v1/" in ~/.docker/config.json .
// GetAuthConfig takes the hostname part as the argument: "index.docker.io"
authConfigHostnames = append([]string{"index.docker.io"}, refHostname)
}

for _, authConfigHostname := range authConfigHostnames {
// GetAuthConfig does not raise an error on ENOENT
ac, err := dockerConfigFile.GetAuthConfig(authConfigHostname)
if err != nil {
log.Errorf("cannot get auth config for authConfigHostname=%q (refHostname=%q): %v",
authConfigHostname, refHostname, err)
} else {
// When refHostname is "docker.io":
// - credFuncExpectedHostname: "registry-1.docker.io"
// - credFuncArg: "registry-1.docker.io"
// - authConfigHostname: "index.docker.io"
// - ac.ServerAddress: "https://index.docker.io/v1/".
if !isAuthConfigEmpty(ac) {
if ac.ServerAddress == "" {
log.Warnf("failed to get ac.ServerAddress for authConfigHostname=%q (refHostname=%q)",
authConfigHostname, refHostname)
} else {
acsaHostname := credentials.ConvertToHostname(ac.ServerAddress)
if acsaHostname != authConfigHostname {
return nil, fmt.Errorf("expected the hostname part of ac.ServerAddress (%q) to be authConfigHostname=%q, got %q",
ac.ServerAddress, authConfigHostname, acsaHostname)
}
}

// if ac.RegistryToken != "" {
// // Even containerd/CRI does not support RegistryToken as of v1.4.3,
// // so, nobody is actually using RegistryToken?
// log.Warnf("ac.RegistryToken (for %q) is not supported yet (FIXME)", authConfigHostname)
// }

credFunc = func(credFuncArg string) (string, string, error) {
if credFuncArg != credFuncExpectedHostname {
return "", "", fmt.Errorf("expected credFuncExpectedHostname=%q (refHostname=%q), got credFuncArg=%q",
credFuncExpectedHostname, refHostname, credFuncArg)
}
if ac.IdentityToken != "" {
return "", ac.IdentityToken, nil
}
return ac.Username, ac.Password, nil
}
break
}
}
}
// credFunc can be nil here.
return credFunc, nil
}

func isAuthConfigEmpty(ac dockercliconfigtypes.AuthConfig) bool {
if ac.IdentityToken != "" || ac.Username != "" || ac.Password != "" || ac.RegistryToken != "" {
return false
}
return true
}
50 changes: 49 additions & 1 deletion pkg/runtime/containerd/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/weaveworks/ignite/pkg/preflight"
"github.com/weaveworks/ignite/pkg/resolvconf"
"github.com/weaveworks/ignite/pkg/runtime"
"github.com/weaveworks/ignite/pkg/runtime/auth"
"github.com/weaveworks/ignite/pkg/util"

"github.com/containerd/console"
Expand All @@ -29,6 +30,9 @@ import (
"github.com/containerd/containerd/namespaces"
"github.com/containerd/containerd/oci"
"github.com/containerd/containerd/plugin"
refdocker "github.com/containerd/containerd/reference/docker"
"github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker"
v2shim "github.com/containerd/containerd/runtime/v2/shim"
"github.com/containerd/containerd/snapshots"
"github.com/opencontainers/go-digest"
Expand Down Expand Up @@ -138,9 +142,53 @@ func GetContainerdClient() (*ctdClient, error) {
}, nil
}

// newRemoteResolver returns a remote resolver with auth info for a given
// host name.
func newRemoteResolver(refHostname string) (remotes.Resolver, error) {
var authzOpts []docker.AuthorizerOpt
if authCreds, err := auth.NewAuthCreds(refHostname); err != nil {
return nil, err
} else {
authzOpts = append(authzOpts, docker.WithAuthCreds(authCreds))
}
authz := docker.NewDockerAuthorizer(authzOpts...)

// TODO: Add plain http option.
regOpts := []docker.RegistryOpt{
docker.WithAuthorizer(authz),
}

// TODO: Add option to skip verifying HTTPS cert.
resolverOpts := docker.ResolverOptions{
Hosts: docker.ConfigureDefaultRegistries(regOpts...),
}

resolver := docker.NewResolver(resolverOpts)
return resolver, nil
}

func (cc *ctdClient) PullImage(image meta.OCIImageRef) error {
log.Debugf("containerd: Pulling image %q", image)
_, err := cc.client.Pull(cc.ctx, image.Normalized(), containerd.WithPullUnpack)

// Get the domain name from the image.
named, err := refdocker.ParseDockerRef(image.String())
if err != nil {
return err
}
refDomain := refdocker.Domain(named)

// Create a remote resolver for the domain.
resolver, err := newRemoteResolver(refDomain)
if err != nil {
return err
}

opts := []containerd.RemoteOpt{
containerd.WithResolver(resolver),
containerd.WithPullUnpack,
}

_, err = cc.client.Pull(cc.ctx, image.Normalized(), opts...)
return err
}

Expand Down
42 changes: 41 additions & 1 deletion pkg/runtime/docker/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ package docker

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"time"

refdocker "github.com/containerd/containerd/reference/docker"
"github.com/containerd/containerd/remotes/docker"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
cont "github.com/docker/docker/api/types/container"
Expand All @@ -18,6 +22,7 @@ import (
"github.com/weaveworks/ignite/pkg/preflight"
"github.com/weaveworks/ignite/pkg/preflight/checkers"
"github.com/weaveworks/ignite/pkg/runtime"
"github.com/weaveworks/ignite/pkg/runtime/auth"
"github.com/weaveworks/ignite/pkg/util"
)

Expand Down Expand Up @@ -47,7 +52,42 @@ func GetDockerClient() (*dockerClient, error) {

func (dc *dockerClient) PullImage(image meta.OCIImageRef) (err error) {
var rc io.ReadCloser
if rc, err = dc.client.ImagePull(context.Background(), image.Normalized(), types.ImagePullOptions{}); err == nil {

opts := types.ImagePullOptions{}

// Get the domain name from the image.
named, err := refdocker.ParseDockerRef(image.String())
if err != nil {
return err
}
refDomain := refdocker.Domain(named)
// Default the host for docker.io.
refDomain, err = docker.DefaultHost(refDomain)
if err != nil {
return err
}

// Get available credentials from docker cli config.
authCreds, err := auth.NewAuthCreds(refDomain)
if err != nil {
return err
}
if authCreds != nil {
// Encode the credentials and set it in the pull options.
authConfig := types.AuthConfig{}
authConfig.Username, authConfig.Password, err = authCreds(refDomain)
if err != nil {
return err
}
encodedJSON, err := json.Marshal(authConfig)
if err != nil {
return err
}
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
opts.RegistryAuth = authStr
}

if rc, err = dc.client.ImagePull(context.Background(), image.Normalized(), opts); err == nil {
// Don't output the pull command
defer util.DeferErr(&err, rc.Close)
_, err = io.Copy(ioutil.Discard, rc)
Expand Down
Loading

0 comments on commit e7ec78d

Please sign in to comment.