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

Add loading credentials from docker cli config #833

Merged
merged 2 commits into from
May 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,45 @@ INFO[0002] Created image with ID "cae0ac317cca74ba" and name "weaveworks/ignite-

Now the `weaveworks/ignite-ubuntu` image is imported and ready for VM use.

### Configuring image registries

Ignite's runtime configuration for image registry uses the docker client
configuration. To add a new registry to docker client configuration, run
`docker login <registry-address>`. This will create `$HOME/.docker/config.json`
in the user's home directory. When ignite runs, it'll check the user's home
directory for docker client configuration file, load the registry configuration
if found and use it.

An example of a docker client configuration file:

```json

{
"auths": {
"https://index.docker.io/v1/": {
"auth": "<token>"
},
"gcr.io": {
"auth": "<token>"
}
}
}
```

The value of token is based on the registry provider. For `index.docker.io`,
the token is a base64 encoded value of `<username>:<auth-token>`. For `gcr.io`,
it's a [json key][json-key] file. Using docker
[credential helpers][credential-helpers] also works but please ensure that the
required credential helper program is installed to handle the credentials. If
the docker client configuration contains `"credHelpers"` block, but the
associated helper program isn't installed or not configured properly, ignite
image pull will fail with errors related to the specific credential helper. In
presence of both auth tokens and credential helpers in a configuration file,
credential helper takes precedence.

[json-key]: https://cloud.google.com/container-registry/docs/advanced-authentication#json-key
[credential-helpers]: https://docs.docker.com/engine/reference/commandline/login/#credential-helpers

## Creating a new VM based on the imported image

The `images` are read-only references of what every VM based on them should contain.
Expand Down
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: This 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