mirror of https://github.com/containers/image.git
Add docker/config package to containers/image/pkg
This package is used in authenticating a user for kpod login and can be used for authentication in kpod push, pull etc. Signed-off-by: umohnani8 <umohnani@redhat.com>
This commit is contained in:
parent
063852766c
commit
f28367e1ae
|
|
@ -3,7 +3,6 @@ package docker
|
|||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -15,11 +14,10 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/containers/image/docker/reference"
|
||||
"github.com/containers/image/pkg/docker/config"
|
||||
"github.com/containers/image/pkg/tlsclientconfig"
|
||||
"github.com/containers/image/types"
|
||||
"github.com/containers/storage/pkg/homedir"
|
||||
"github.com/docker/distribution/registry/client"
|
||||
helperclient "github.com/docker/docker-credential-helpers/client"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
|
|
@ -27,13 +25,8 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
dockerHostname = "docker.io"
|
||||
dockerRegistry = "registry-1.docker.io"
|
||||
dockerAuthRegistry = "https://index.docker.io/v1/"
|
||||
|
||||
dockerCfg = ".docker"
|
||||
dockerCfgFileName = "config.json"
|
||||
dockerCfgObsolete = ".dockercfg"
|
||||
dockerHostname = "docker.io"
|
||||
dockerRegistry = "registry-1.docker.io"
|
||||
|
||||
systemPerHostCertDirPath = "/etc/docker/certs.d"
|
||||
|
||||
|
|
@ -51,9 +44,13 @@ const (
|
|||
extensionSignatureTypeAtomic = "atomic" // extensionSignature.Type
|
||||
)
|
||||
|
||||
// ErrV1NotSupported is returned when we're trying to talk to a
|
||||
// docker V1 registry.
|
||||
var ErrV1NotSupported = errors.New("can't talk to a V1 docker registry")
|
||||
var (
|
||||
// ErrV1NotSupported is returned when we're trying to talk to a
|
||||
// docker V1 registry.
|
||||
ErrV1NotSupported = errors.New("can't talk to a V1 docker registry")
|
||||
// ErrUnauthorizedForCredentials is returned when the status code returned is 401
|
||||
ErrUnauthorizedForCredentials = errors.New("unable to retrieve auth token: invalid username/password")
|
||||
)
|
||||
|
||||
// extensionSignature and extensionSignatureList come from github.com/openshift/origin/pkg/dockerregistry/server/signaturedispatcher.go:
|
||||
// signature represents a Docker image signature.
|
||||
|
|
@ -128,52 +125,147 @@ func dockerCertDir(ctx *types.SystemContext, hostPort string) string {
|
|||
return filepath.Join(hostCertDir, hostPort)
|
||||
}
|
||||
|
||||
// newDockerClient returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry)
|
||||
// “write” specifies whether the client will be used for "write" access (in particular passed to lookaside.go:toplevelFromSection)
|
||||
func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool, actions string) (*dockerClient, error) {
|
||||
registry := reference.Domain(ref.ref)
|
||||
if registry == dockerHostname {
|
||||
registry = dockerRegistry
|
||||
func setupCertificates(dir string, tlsc *tls.Config) error {
|
||||
logrus.Debugf("Looking for TLS certificates and private keys in %s", dir)
|
||||
fs, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
if os.IsPermission(err) {
|
||||
logrus.Debugf("Skipping scan of %s due to permission error: %v", dir, err)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
username, password, err := getAuth(ctx, reference.Domain(ref.ref))
|
||||
|
||||
for _, f := range fs {
|
||||
fullPath := filepath.Join(dir, f.Name())
|
||||
if strings.HasSuffix(f.Name(), ".crt") {
|
||||
systemPool, err := tlsconfig.SystemCertPool()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to get system cert pool")
|
||||
}
|
||||
tlsc.RootCAs = systemPool
|
||||
logrus.Debugf(" crt: %s", fullPath)
|
||||
data, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tlsc.RootCAs.AppendCertsFromPEM(data)
|
||||
}
|
||||
if strings.HasSuffix(f.Name(), ".cert") {
|
||||
certName := f.Name()
|
||||
keyName := certName[:len(certName)-5] + ".key"
|
||||
logrus.Debugf(" cert: %s", fullPath)
|
||||
if !hasFile(fs, keyName) {
|
||||
return errors.Errorf("missing key %s for client certificate %s. Note that CA certificates should use the extension .crt", keyName, certName)
|
||||
}
|
||||
cert, err := tls.LoadX509KeyPair(filepath.Join(dir, certName), filepath.Join(dir, keyName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tlsc.Certificates = append(tlsc.Certificates, cert)
|
||||
}
|
||||
if strings.HasSuffix(f.Name(), ".key") {
|
||||
keyName := f.Name()
|
||||
certName := keyName[:len(keyName)-4] + ".cert"
|
||||
logrus.Debugf(" key: %s", fullPath)
|
||||
if !hasFile(fs, certName) {
|
||||
return errors.Errorf("missing client certificate %s for key %s", certName, keyName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasFile(files []os.FileInfo, name string) bool {
|
||||
for _, f := range files {
|
||||
if f.Name() == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// newDockerClientFromRef returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry)
|
||||
// “write” specifies whether the client will be used for "write" access (in particular passed to lookaside.go:toplevelFromSection)
|
||||
func newDockerClientFromRef(ctx *types.SystemContext, ref dockerReference, write bool, actions string) (*dockerClient, error) {
|
||||
registry := reference.Domain(ref.ref)
|
||||
username, password, err := config.GetAuthentication(ctx, reference.Domain(ref.ref))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error getting username and password")
|
||||
}
|
||||
sigBase, err := configuredSignatureStorageBase(ctx, ref, write)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remoteName := reference.Path(ref.ref)
|
||||
|
||||
return newDockerClientWithDetails(ctx, registry, username, password, actions, sigBase, remoteName)
|
||||
}
|
||||
|
||||
// newDockerClientWithDetails returns a new dockerClient instance for the given parameters
|
||||
func newDockerClientWithDetails(ctx *types.SystemContext, registry, username, password, actions string, sigBase signatureStorageBase, remoteName string) (*dockerClient, error) {
|
||||
hostName := registry
|
||||
if registry == dockerHostname {
|
||||
registry = dockerRegistry
|
||||
}
|
||||
tr := tlsclientconfig.NewTransport()
|
||||
tr.TLSClientConfig = serverDefault()
|
||||
|
||||
// It is undefined whether the host[:port] string for dockerHostname should be dockerHostname or dockerRegistry,
|
||||
// because docker/docker does not read the certs.d subdirectory at all in that case. We use the user-visible
|
||||
// dockerHostname here, because it is more symmetrical to read the configuration in that case as well, and because
|
||||
// generally the UI hides the existence of the different dockerRegistry. But note that this behavior is
|
||||
// undocumented and may change if docker/docker changes.
|
||||
certDir := dockerCertDir(ctx, reference.Domain(ref.ref))
|
||||
certDir := dockerCertDir(ctx, hostName)
|
||||
if err := tlsclientconfig.SetupCertificates(certDir, tr.TLSClientConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ctx != nil && ctx.DockerInsecureSkipTLSVerify {
|
||||
tr.TLSClientConfig.InsecureSkipVerify = true
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
sigBase, err := configuredSignatureStorageBase(ctx, ref, write)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dockerClient{
|
||||
ctx: ctx,
|
||||
registry: registry,
|
||||
username: username,
|
||||
password: password,
|
||||
client: client,
|
||||
client: &http.Client{Transport: tr},
|
||||
signatureBase: sigBase,
|
||||
scope: authScope{
|
||||
actions: actions,
|
||||
remoteName: reference.Path(ref.ref),
|
||||
remoteName: remoteName,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CheckAuth validates the credentials by attempting to log into the registry
|
||||
// returns an error if an error occcured while making the http request or the status code received was 401
|
||||
func CheckAuth(ctx context.Context, sCtx *types.SystemContext, username, password, registry string) error {
|
||||
newLoginClient, err := newDockerClientWithDetails(sCtx, registry, username, password, "", nil, "")
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error creating new docker client")
|
||||
}
|
||||
|
||||
resp, err := newLoginClient.makeRequest(ctx, "GET", "/v2/", nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return nil
|
||||
case http.StatusUnauthorized:
|
||||
return ErrUnauthorizedForCredentials
|
||||
default:
|
||||
return errors.Errorf("error occured with status code %q", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// makeRequest creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
|
||||
// The host name and schema is taken from the client or autodetected, and the path is relative to it, i.e. the path usually starts with /v2/.
|
||||
func (c *dockerClient) makeRequest(ctx context.Context, method, path string, headers map[string][]string, stream io.Reader) (*http.Response, error) {
|
||||
|
|
@ -245,7 +337,10 @@ func (c *dockerClient) setupRequestAuth(req *http.Request) error {
|
|||
return errors.Errorf("missing realm in bearer auth challenge")
|
||||
}
|
||||
service, _ := challenge.Parameters["service"] // Will be "" if not present
|
||||
scope := fmt.Sprintf("repository:%s:%s", c.scope.remoteName, c.scope.actions)
|
||||
var scope string
|
||||
if c.scope.remoteName != "" && c.scope.actions != "" {
|
||||
scope = fmt.Sprintf("repository:%s:%s", c.scope.remoteName, c.scope.actions)
|
||||
}
|
||||
token, err := c.getBearerToken(req.Context(), realm, service, scope)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -291,7 +386,7 @@ func (c *dockerClient) getBearerToken(ctx context.Context, realm, service, scope
|
|||
defer res.Body.Close()
|
||||
switch res.StatusCode {
|
||||
case http.StatusUnauthorized:
|
||||
return nil, errors.Errorf("unable to retrieve auth token: 401 unauthorized")
|
||||
return nil, ErrUnauthorizedForCredentials
|
||||
case http.StatusOK:
|
||||
break
|
||||
default:
|
||||
|
|
@ -315,65 +410,6 @@ func (c *dockerClient) getBearerToken(ctx context.Context, realm, service, scope
|
|||
return &token, nil
|
||||
}
|
||||
|
||||
func getAuth(ctx *types.SystemContext, registry string) (string, string, error) {
|
||||
if ctx != nil && ctx.DockerAuthConfig != nil {
|
||||
return ctx.DockerAuthConfig.Username, ctx.DockerAuthConfig.Password, nil
|
||||
}
|
||||
var dockerAuth dockerConfigFile
|
||||
dockerCfgPath := filepath.Join(getDefaultConfigDir(".docker"), dockerCfgFileName)
|
||||
if _, err := os.Stat(dockerCfgPath); err == nil {
|
||||
j, err := ioutil.ReadFile(dockerCfgPath)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if err := json.Unmarshal(j, &dockerAuth); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
} else if os.IsNotExist(err) {
|
||||
// try old config path
|
||||
oldDockerCfgPath := filepath.Join(getDefaultConfigDir(dockerCfgObsolete))
|
||||
if _, err := os.Stat(oldDockerCfgPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", "", nil
|
||||
}
|
||||
return "", "", errors.Wrap(err, oldDockerCfgPath)
|
||||
}
|
||||
|
||||
j, err := ioutil.ReadFile(oldDockerCfgPath)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if err := json.Unmarshal(j, &dockerAuth.AuthConfigs); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
} else if err != nil {
|
||||
return "", "", errors.Wrap(err, dockerCfgPath)
|
||||
}
|
||||
|
||||
// First try cred helpers. They should always be normalized.
|
||||
if ch, exists := dockerAuth.CredHelpers[registry]; exists {
|
||||
return getAuthFromCredHelper(ch, registry)
|
||||
}
|
||||
|
||||
// I'm feeling lucky.
|
||||
if c, exists := dockerAuth.AuthConfigs[registry]; exists {
|
||||
return decodeDockerAuth(c.Auth)
|
||||
}
|
||||
|
||||
// bad luck; let's normalize the entries first
|
||||
registry = normalizeRegistry(registry)
|
||||
normalizedAuths := map[string]dockerAuthConfig{}
|
||||
for k, v := range dockerAuth.AuthConfigs {
|
||||
normalizedAuths[normalizeRegistry(k)] = v
|
||||
}
|
||||
if c, exists := normalizedAuths[registry]; exists {
|
||||
return decodeDockerAuth(c.Auth)
|
||||
}
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
// detectProperties detects various properties of the registry.
|
||||
// See the dockerClient documentation for members which are affected by this.
|
||||
func (c *dockerClient) detectProperties(ctx context.Context) error {
|
||||
|
|
@ -456,67 +492,3 @@ func (c *dockerClient) getExtensionsSignatures(ctx context.Context, ref dockerRe
|
|||
}
|
||||
return &parsedBody, nil
|
||||
}
|
||||
|
||||
func getDefaultConfigDir(confPath string) string {
|
||||
return filepath.Join(homedir.Get(), confPath)
|
||||
}
|
||||
|
||||
type dockerAuthConfig struct {
|
||||
Auth string `json:"auth,omitempty"`
|
||||
}
|
||||
|
||||
type dockerConfigFile struct {
|
||||
AuthConfigs map[string]dockerAuthConfig `json:"auths"`
|
||||
CredHelpers map[string]string `json:"credHelpers,omitempty"`
|
||||
}
|
||||
|
||||
func getAuthFromCredHelper(credHelper, registry string) (string, string, error) {
|
||||
helperName := fmt.Sprintf("docker-credential-%s", credHelper)
|
||||
p := helperclient.NewShellProgramFunc(helperName)
|
||||
creds, err := helperclient.Get(p, registry)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return creds.Username, creds.Secret, nil
|
||||
}
|
||||
|
||||
func decodeDockerAuth(s string) (string, string, error) {
|
||||
decoded, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
parts := strings.SplitN(string(decoded), ":", 2)
|
||||
if len(parts) != 2 {
|
||||
// if it's invalid just skip, as docker does
|
||||
return "", "", nil
|
||||
}
|
||||
user := parts[0]
|
||||
password := strings.Trim(parts[1], "\x00")
|
||||
return user, password, nil
|
||||
}
|
||||
|
||||
// convertToHostname converts a registry url which has http|https prepended
|
||||
// to just an hostname.
|
||||
// Copied from github.com/docker/docker/registry/auth.go
|
||||
func convertToHostname(url string) string {
|
||||
stripped := url
|
||||
if strings.HasPrefix(url, "http://") {
|
||||
stripped = strings.TrimPrefix(url, "http://")
|
||||
} else if strings.HasPrefix(url, "https://") {
|
||||
stripped = strings.TrimPrefix(url, "https://")
|
||||
}
|
||||
|
||||
nameParts := strings.SplitN(stripped, "/", 2)
|
||||
|
||||
return nameParts[0]
|
||||
}
|
||||
|
||||
func normalizeRegistry(registry string) string {
|
||||
normalized := convertToHostname(registry)
|
||||
switch normalized {
|
||||
case "registry-1.docker.io", "docker.io":
|
||||
return "index.docker.io"
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,16 @@ package docker
|
|||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
//"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/containers/image/pkg/docker/config"
|
||||
"github.com/containers/image/types"
|
||||
"github.com/containers/storage/pkg/homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
@ -88,173 +89,196 @@ func TestDockerCertDir(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetAuth(t *testing.T) {
|
||||
origHomeDir := homedir.Get()
|
||||
tmpDir, err := ioutil.TempDir("", "test_docker_client_get_auth")
|
||||
origXDG := os.Getenv("XDG_RUNTIME_DIR")
|
||||
tmpDir1, err := ioutil.TempDir("", "test_docker_client_get_auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("using temporary home directory: %q", tmpDir)
|
||||
// override homedir
|
||||
os.Setenv(homedir.Key(), tmpDir)
|
||||
t.Logf("using temporary XDG_RUNTIME_DIR directory: %q", tmpDir1)
|
||||
// override XDG_RUNTIME_DIR
|
||||
os.Setenv("XDG_RUNTIME_DIR", tmpDir1)
|
||||
defer func() {
|
||||
err := os.RemoveAll(tmpDir)
|
||||
err := os.RemoveAll(tmpDir1)
|
||||
if err != nil {
|
||||
t.Logf("failed to cleanup temporary home directory %q: %v", tmpDir, err)
|
||||
t.Logf("failed to cleanup temporary home directory %q: %v", tmpDir1, err)
|
||||
}
|
||||
os.Setenv("XDG_RUNTIME_DIR", origXDG)
|
||||
}()
|
||||
|
||||
origHomeDir := homedir.Get()
|
||||
tmpDir2, err := ioutil.TempDir("", "test_docker_client_get_auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("using temporary home directory: %q", tmpDir2)
|
||||
//override homedir
|
||||
os.Setenv(homedir.Key(), tmpDir2)
|
||||
defer func() {
|
||||
err := os.RemoveAll(tmpDir2)
|
||||
if err != nil {
|
||||
t.Logf("failed to cleanup temporary home directory %q: %v", tmpDir2, err)
|
||||
}
|
||||
os.Setenv(homedir.Key(), origHomeDir)
|
||||
}()
|
||||
|
||||
configDir := filepath.Join(tmpDir, ".docker")
|
||||
if err := os.Mkdir(configDir, 0750); err != nil {
|
||||
configDir1 := filepath.Join(tmpDir1, "containers")
|
||||
if err := os.MkdirAll(configDir1, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
configPath := filepath.Join(configDir, "config.json")
|
||||
configDir2 := filepath.Join(tmpDir2, ".docker")
|
||||
if err := os.MkdirAll(configDir2, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
configPaths := [2]string{filepath.Join(configDir1, "auth.json"), filepath.Join(configDir2, "config.json")}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
hostname string
|
||||
authConfig testAuthConfig
|
||||
expectedUsername string
|
||||
expectedPassword string
|
||||
expectedError error
|
||||
ctx *types.SystemContext
|
||||
}{
|
||||
{
|
||||
name: "empty hostname",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{"localhost:5000": testAuthConfigData{"bob", "password"}}),
|
||||
},
|
||||
{
|
||||
name: "no auth config",
|
||||
hostname: "index.docker.io",
|
||||
},
|
||||
{
|
||||
name: "match one",
|
||||
hostname: "example.org",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{"example.org": testAuthConfigData{"joe", "mypass"}}),
|
||||
expectedUsername: "joe",
|
||||
expectedPassword: "mypass",
|
||||
},
|
||||
{
|
||||
name: "match none",
|
||||
hostname: "registry.example.org",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{"example.org": testAuthConfigData{"joe", "mypass"}}),
|
||||
},
|
||||
{
|
||||
name: "match docker.io",
|
||||
hostname: "docker.io",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
|
||||
"example.org": testAuthConfigData{"example", "org"},
|
||||
"index.docker.io": testAuthConfigData{"index", "docker.io"},
|
||||
"docker.io": testAuthConfigData{"docker", "io"},
|
||||
}),
|
||||
expectedUsername: "docker",
|
||||
expectedPassword: "io",
|
||||
},
|
||||
{
|
||||
name: "match docker.io normalized",
|
||||
hostname: "docker.io",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
|
||||
"example.org": testAuthConfigData{"bob", "pw"},
|
||||
"https://index.docker.io/v1": testAuthConfigData{"alice", "wp"},
|
||||
}),
|
||||
expectedUsername: "alice",
|
||||
expectedPassword: "wp",
|
||||
},
|
||||
{
|
||||
name: "normalize registry",
|
||||
hostname: "https://docker.io/v1",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
|
||||
"docker.io": testAuthConfigData{"user", "pw"},
|
||||
"localhost:5000": testAuthConfigData{"joe", "pass"},
|
||||
}),
|
||||
expectedUsername: "user",
|
||||
expectedPassword: "pw",
|
||||
},
|
||||
{
|
||||
name: "match localhost",
|
||||
hostname: "http://localhost",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
|
||||
"docker.io": testAuthConfigData{"user", "pw"},
|
||||
"localhost": testAuthConfigData{"joe", "pass"},
|
||||
"example.com": testAuthConfigData{"alice", "pwd"},
|
||||
}),
|
||||
expectedUsername: "joe",
|
||||
expectedPassword: "pass",
|
||||
},
|
||||
{
|
||||
name: "match ip",
|
||||
hostname: "10.10.3.56:5000",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
|
||||
"10.10.30.45": testAuthConfigData{"user", "pw"},
|
||||
"localhost": testAuthConfigData{"joe", "pass"},
|
||||
"10.10.3.56": testAuthConfigData{"alice", "pwd"},
|
||||
"10.10.3.56:5000": testAuthConfigData{"me", "mine"},
|
||||
}),
|
||||
expectedUsername: "me",
|
||||
expectedPassword: "mine",
|
||||
},
|
||||
{
|
||||
name: "match port",
|
||||
hostname: "https://localhost:5000",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
|
||||
"https://127.0.0.1:5000": testAuthConfigData{"user", "pw"},
|
||||
"http://localhost": testAuthConfigData{"joe", "pass"},
|
||||
"https://localhost:5001": testAuthConfigData{"alice", "pwd"},
|
||||
"localhost:5000": testAuthConfigData{"me", "mine"},
|
||||
}),
|
||||
expectedUsername: "me",
|
||||
expectedPassword: "mine",
|
||||
},
|
||||
{
|
||||
name: "use system context",
|
||||
hostname: "example.org",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
|
||||
"example.org": testAuthConfigData{"user", "pw"},
|
||||
}),
|
||||
expectedUsername: "foo",
|
||||
expectedPassword: "bar",
|
||||
ctx: &types.SystemContext{
|
||||
DockerAuthConfig: &types.DockerAuthConfig{
|
||||
Username: "foo",
|
||||
Password: "bar",
|
||||
for _, configPath := range configPaths {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
hostname string
|
||||
authConfig testAuthConfig
|
||||
expectedUsername string
|
||||
expectedPassword string
|
||||
expectedError error
|
||||
ctx *types.SystemContext
|
||||
}{
|
||||
{
|
||||
name: "empty hostname",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{"localhost:5000": testAuthConfigData{"bob", "password"}}),
|
||||
},
|
||||
{
|
||||
name: "no auth config",
|
||||
hostname: "index.docker.io",
|
||||
},
|
||||
{
|
||||
name: "match one",
|
||||
hostname: "example.org",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{"example.org": testAuthConfigData{"joe", "mypass"}}),
|
||||
expectedUsername: "joe",
|
||||
expectedPassword: "mypass",
|
||||
},
|
||||
{
|
||||
name: "match none",
|
||||
hostname: "registry.example.org",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{"example.org": testAuthConfigData{"joe", "mypass"}}),
|
||||
},
|
||||
{
|
||||
name: "match docker.io",
|
||||
hostname: "docker.io",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
|
||||
"example.org": testAuthConfigData{"example", "org"},
|
||||
"index.docker.io": testAuthConfigData{"index", "docker.io"},
|
||||
"docker.io": testAuthConfigData{"docker", "io"},
|
||||
}),
|
||||
expectedUsername: "docker",
|
||||
expectedPassword: "io",
|
||||
},
|
||||
{
|
||||
name: "match docker.io normalized",
|
||||
hostname: "docker.io",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
|
||||
"example.org": testAuthConfigData{"bob", "pw"},
|
||||
"https://index.docker.io/v1": testAuthConfigData{"alice", "wp"},
|
||||
}),
|
||||
expectedUsername: "alice",
|
||||
expectedPassword: "wp",
|
||||
},
|
||||
{
|
||||
name: "normalize registry",
|
||||
hostname: "https://docker.io/v1",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
|
||||
"docker.io": testAuthConfigData{"user", "pw"},
|
||||
"localhost:5000": testAuthConfigData{"joe", "pass"},
|
||||
}),
|
||||
expectedUsername: "user",
|
||||
expectedPassword: "pw",
|
||||
},
|
||||
{
|
||||
name: "match localhost",
|
||||
hostname: "http://localhost",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
|
||||
"docker.io": testAuthConfigData{"user", "pw"},
|
||||
"localhost": testAuthConfigData{"joe", "pass"},
|
||||
"example.com": testAuthConfigData{"alice", "pwd"},
|
||||
}),
|
||||
expectedUsername: "joe",
|
||||
expectedPassword: "pass",
|
||||
},
|
||||
{
|
||||
name: "match ip",
|
||||
hostname: "10.10.3.56:5000",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
|
||||
"10.10.30.45": testAuthConfigData{"user", "pw"},
|
||||
"localhost": testAuthConfigData{"joe", "pass"},
|
||||
"10.10.3.56": testAuthConfigData{"alice", "pwd"},
|
||||
"10.10.3.56:5000": testAuthConfigData{"me", "mine"},
|
||||
}),
|
||||
expectedUsername: "me",
|
||||
expectedPassword: "mine",
|
||||
},
|
||||
{
|
||||
name: "match port",
|
||||
hostname: "https://localhost:5000",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
|
||||
"https://127.0.0.1:5000": testAuthConfigData{"user", "pw"},
|
||||
"http://localhost": testAuthConfigData{"joe", "pass"},
|
||||
"https://localhost:5001": testAuthConfigData{"alice", "pwd"},
|
||||
"localhost:5000": testAuthConfigData{"me", "mine"},
|
||||
}),
|
||||
expectedUsername: "me",
|
||||
expectedPassword: "mine",
|
||||
},
|
||||
{
|
||||
name: "use system context",
|
||||
hostname: "example.org",
|
||||
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
|
||||
"example.org": testAuthConfigData{"user", "pw"},
|
||||
}),
|
||||
expectedUsername: "foo",
|
||||
expectedPassword: "bar",
|
||||
ctx: &types.SystemContext{
|
||||
DockerAuthConfig: &types.DockerAuthConfig{
|
||||
Username: "foo",
|
||||
Password: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
contents, err := json.MarshalIndent(&tc.authConfig, "", " ")
|
||||
if err != nil {
|
||||
t.Errorf("[%s] failed to marshal authConfig: %v", tc.name, err)
|
||||
continue
|
||||
}
|
||||
if err := ioutil.WriteFile(configPath, contents, 0640); err != nil {
|
||||
t.Errorf("[%s] failed to write file %q: %v", tc.name, configPath, err)
|
||||
continue
|
||||
}
|
||||
} {
|
||||
contents, err := json.MarshalIndent(&tc.authConfig, "", " ")
|
||||
if err != nil {
|
||||
t.Errorf("[%s] failed to marshal authConfig: %v", tc.name, err)
|
||||
continue
|
||||
}
|
||||
if err := ioutil.WriteFile(configPath, contents, 0640); err != nil {
|
||||
t.Errorf("[%s] failed to write file %q: %v", tc.name, configPath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
var ctx *types.SystemContext
|
||||
if tc.ctx != nil {
|
||||
ctx = tc.ctx
|
||||
}
|
||||
username, password, err := getAuth(ctx, tc.hostname)
|
||||
if err == nil && tc.expectedError != nil {
|
||||
t.Errorf("[%s] got unexpected non error and username=%q, password=%q", tc.name, username, password)
|
||||
continue
|
||||
}
|
||||
if err != nil && tc.expectedError == nil {
|
||||
t.Errorf("[%s] got unexpected error: %#+v", tc.name, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(err, tc.expectedError) {
|
||||
t.Errorf("[%s] got unexpected error: %#+v != %#+v", tc.name, err, tc.expectedError)
|
||||
continue
|
||||
}
|
||||
var ctx *types.SystemContext
|
||||
if tc.ctx != nil {
|
||||
ctx = tc.ctx
|
||||
}
|
||||
username, password, err := config.GetAuthentication(ctx, tc.hostname)
|
||||
if err == nil && tc.expectedError != nil {
|
||||
t.Errorf("[%s] got unexpected non error and username=%q, password=%q", tc.name, username, password)
|
||||
continue
|
||||
}
|
||||
if err != nil && tc.expectedError == nil {
|
||||
t.Errorf("[%s] got unexpected error: %#+v", tc.name, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(err, tc.expectedError) {
|
||||
t.Errorf("[%s] got unexpected error: %#+v != %#+v", tc.name, err, tc.expectedError)
|
||||
continue
|
||||
}
|
||||
|
||||
if username != tc.expectedUsername {
|
||||
t.Errorf("[%s] got unexpected user name: %q != %q", tc.name, username, tc.expectedUsername)
|
||||
}
|
||||
if password != tc.expectedPassword {
|
||||
t.Errorf("[%s] got unexpected user name: %q != %q", tc.name, password, tc.expectedPassword)
|
||||
if username != tc.expectedUsername {
|
||||
t.Errorf("[%s] got unexpected user name: %q != %q", tc.name, username, tc.expectedUsername)
|
||||
}
|
||||
if password != tc.expectedPassword {
|
||||
t.Errorf("[%s] got unexpected user name: %q != %q", tc.name, password, tc.expectedPassword)
|
||||
}
|
||||
}
|
||||
os.RemoveAll(configPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -316,7 +340,7 @@ func TestGetAuthFromLegacyFile(t *testing.T) {
|
|||
continue
|
||||
}
|
||||
|
||||
username, password, err := getAuth(nil, tc.hostname)
|
||||
username, password, err := config.GetAuthentication(nil, tc.hostname)
|
||||
if err == nil && tc.expectedError != nil {
|
||||
t.Errorf("[%s] got unexpected non error and username=%q, password=%q", tc.name, username, password)
|
||||
continue
|
||||
|
|
@ -387,7 +411,7 @@ func TestGetAuthPreferNewConfig(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
username, password, err := getAuth(nil, "index.docker.io")
|
||||
username, password, err := config.GetAuthentication(nil, "index.docker.io")
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %#+v", err)
|
||||
}
|
||||
|
|
@ -401,30 +425,46 @@ func TestGetAuthPreferNewConfig(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetAuthFailsOnBadInput(t *testing.T) {
|
||||
origHomeDir := homedir.Get()
|
||||
tmpDir, err := ioutil.TempDir("", "test_docker_client_get_auth")
|
||||
origXDG := os.Getenv("XDG_RUNTIME_DIR")
|
||||
tmpDir1, err := ioutil.TempDir("", "test_docker_client_get_auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("using temporary home directory: %q", tmpDir)
|
||||
t.Logf("using temporary XDG_RUNTIME_DIR directory: %q", tmpDir1)
|
||||
// override homedir
|
||||
os.Setenv(homedir.Key(), tmpDir)
|
||||
os.Setenv("XDG_RUNTIME_DIR", tmpDir1)
|
||||
defer func() {
|
||||
err := os.RemoveAll(tmpDir)
|
||||
err := os.RemoveAll(tmpDir1)
|
||||
if err != nil {
|
||||
t.Logf("failed to cleanup temporary home directory %q: %v", tmpDir, err)
|
||||
t.Logf("failed to cleanup temporary home directory %q: %v", tmpDir1, err)
|
||||
}
|
||||
os.Setenv("XDG_RUNTIME_DIR", origXDG)
|
||||
}()
|
||||
|
||||
origHomeDir := homedir.Get()
|
||||
tmpDir2, err := ioutil.TempDir("", "test_docker_client_get_auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("using temporary home directory: %q", tmpDir2)
|
||||
// override homedir
|
||||
os.Setenv(homedir.Key(), tmpDir2)
|
||||
defer func() {
|
||||
err := os.RemoveAll(tmpDir2)
|
||||
if err != nil {
|
||||
t.Logf("failed to cleanup temporary home directory %q: %v", tmpDir2, err)
|
||||
}
|
||||
os.Setenv(homedir.Key(), origHomeDir)
|
||||
}()
|
||||
|
||||
configDir := filepath.Join(tmpDir, ".docker")
|
||||
configDir := filepath.Join(tmpDir1, "containers")
|
||||
if err := os.Mkdir(configDir, 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
configPath := filepath.Join(configDir, "config.json")
|
||||
configPath := filepath.Join(configDir, "auth.json")
|
||||
|
||||
// no config file present
|
||||
username, password, err := getAuth(nil, "index.docker.io")
|
||||
username, password, err := config.GetAuthentication(nil, "index.docker.io")
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %#+v", err)
|
||||
}
|
||||
|
|
@ -435,18 +475,18 @@ func TestGetAuthFailsOnBadInput(t *testing.T) {
|
|||
if err := ioutil.WriteFile(configPath, []byte("Json rocks! Unless it doesn't."), 0640); err != nil {
|
||||
t.Fatalf("failed to write file %q: %v", configPath, err)
|
||||
}
|
||||
username, password, err = getAuth(nil, "index.docker.io")
|
||||
username, password, err = config.GetAuthentication(nil, "index.docker.io")
|
||||
if err == nil {
|
||||
t.Fatalf("got unexpected non-error: username=%q, password=%q", username, password)
|
||||
}
|
||||
if _, ok := err.(*json.SyntaxError); !ok {
|
||||
t.Fatalf("expected os.PathError, not: %#+v", err)
|
||||
if _, ok := errors.Cause(err).(*json.SyntaxError); !ok {
|
||||
t.Fatalf("expected JSON syntax error, not: %#+v", err)
|
||||
}
|
||||
|
||||
// remove the invalid config file
|
||||
os.RemoveAll(configPath)
|
||||
// no config file present
|
||||
username, password, err = getAuth(nil, "index.docker.io")
|
||||
username, password, err = config.GetAuthentication(nil, "index.docker.io")
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %#+v", err)
|
||||
}
|
||||
|
|
@ -454,16 +494,16 @@ func TestGetAuthFailsOnBadInput(t *testing.T) {
|
|||
t.Fatalf("got unexpected not empty username/password: %q/%q", username, password)
|
||||
}
|
||||
|
||||
configPath = filepath.Join(tmpDir, ".dockercfg")
|
||||
configPath = filepath.Join(tmpDir2, ".dockercfg")
|
||||
if err := ioutil.WriteFile(configPath, []byte("I'm certainly not a json string."), 0640); err != nil {
|
||||
t.Fatalf("failed to write file %q: %v", configPath, err)
|
||||
}
|
||||
username, password, err = getAuth(nil, "index.docker.io")
|
||||
username, password, err = config.GetAuthentication(nil, "index.docker.io")
|
||||
if err == nil {
|
||||
t.Fatalf("got unexpected non-error: username=%q, password=%q", username, password)
|
||||
}
|
||||
if _, ok := err.(*json.SyntaxError); !ok {
|
||||
t.Fatalf("expected os.PathError, not: %#+v", err)
|
||||
if _, ok := errors.Cause(err).(*json.SyntaxError); !ok {
|
||||
t.Fatalf("expected JSON syntax error, not: %#+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ type dockerImageDestination struct {
|
|||
|
||||
// newImageDestination creates a new ImageDestination for the specified image reference.
|
||||
func newImageDestination(ctx *types.SystemContext, ref dockerReference) (types.ImageDestination, error) {
|
||||
c, err := newDockerClient(ctx, ref, true, "pull,push")
|
||||
c, err := newDockerClientFromRef(ctx, ref, true, "pull,push")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ type dockerImageSource struct {
|
|||
// newImageSource creates a new ImageSource for the specified image reference.
|
||||
// The caller must call .Close() on the returned ImageSource.
|
||||
func newImageSource(ctx *types.SystemContext, ref dockerReference) (*dockerImageSource, error) {
|
||||
c, err := newDockerClient(ctx, ref, false, "pull")
|
||||
c, err := newDockerClientFromRef(ctx, ref, false, "pull")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -298,7 +298,7 @@ func (s *dockerImageSource) getSignaturesFromAPIExtension(ctx context.Context) (
|
|||
|
||||
// deleteImage deletes the named image from the registry, if supported.
|
||||
func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
|
||||
c, err := newDockerClient(ctx, ref, true, "push")
|
||||
c, err := newDockerClientFromRef(ctx, ref, true, "push")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,6 @@ func (d *ociArchiveImageDestination) Commit() error {
|
|||
src := d.tempDirRef.tempDirectory
|
||||
// path to save tarred up file
|
||||
dst := d.ref.resolvedFile
|
||||
|
||||
return tarDirectory(src, dst)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,295 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/image/types"
|
||||
helperclient "github.com/docker/docker-credential-helpers/client"
|
||||
"github.com/docker/docker-credential-helpers/credentials"
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type dockerAuthConfig struct {
|
||||
Auth string `json:"auth,omitempty"`
|
||||
}
|
||||
|
||||
type dockerConfigFile struct {
|
||||
AuthConfigs map[string]dockerAuthConfig `json:"auths"`
|
||||
CredHelpers map[string]string `json:"credHelpers,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
defaultPath = "/run/user"
|
||||
authCfg = "containers"
|
||||
authCfgFileName = "auth.json"
|
||||
dockerCfg = ".docker"
|
||||
dockerCfgFileName = "config.json"
|
||||
dockerLegacyCfg = ".dockercfg"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotLoggedIn is returned for users not logged into a registry
|
||||
// that they are trying to logout of
|
||||
ErrNotLoggedIn = errors.New("not logged in")
|
||||
)
|
||||
|
||||
// SetAuthentication stores the username and password in the auth.json file
|
||||
func SetAuthentication(ctx *types.SystemContext, registry, username, password string) error {
|
||||
return modifyJSON(ctx, func(auths *dockerConfigFile) (bool, error) {
|
||||
if ch, exists := auths.CredHelpers[registry]; exists {
|
||||
return false, setAuthToCredHelper(ch, registry, username, password)
|
||||
}
|
||||
|
||||
creds := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
||||
newCreds := dockerAuthConfig{Auth: creds}
|
||||
auths.AuthConfigs[registry] = newCreds
|
||||
return true, nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetAuthentication returns the registry credentials stored in
|
||||
// either auth.json file or .docker/config.json
|
||||
// If an entry is not found empty strings are returned for the username and password
|
||||
func GetAuthentication(ctx *types.SystemContext, registry string) (string, string, error) {
|
||||
if ctx != nil && ctx.DockerAuthConfig != nil {
|
||||
return ctx.DockerAuthConfig.Username, ctx.DockerAuthConfig.Password, nil
|
||||
}
|
||||
|
||||
dockerLegacyPath := filepath.Join(homedir.Get(), dockerLegacyCfg)
|
||||
paths := [3]string{getPathToAuth(ctx), filepath.Join(homedir.Get(), dockerCfg, dockerCfgFileName), dockerLegacyPath}
|
||||
|
||||
for _, path := range paths {
|
||||
legacyFormat := path == dockerLegacyPath
|
||||
username, password, err := findAuthentication(registry, path, legacyFormat)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if username != "" && password != "" {
|
||||
return username, password, nil
|
||||
}
|
||||
}
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
// GetUserLoggedIn returns the username logged in to registry from either
|
||||
// auth.json or XDG_RUNTIME_DIR
|
||||
// Used to tell the user if someone is logged in to the registry when logging in
|
||||
func GetUserLoggedIn(ctx *types.SystemContext, registry string) string {
|
||||
path := getPathToAuth(ctx)
|
||||
username, _, _ := findAuthentication(registry, path, false)
|
||||
if username != "" {
|
||||
return username
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// RemoveAuthentication deletes the credentials stored in auth.json
|
||||
func RemoveAuthentication(ctx *types.SystemContext, registry string) error {
|
||||
return modifyJSON(ctx, func(auths *dockerConfigFile) (bool, error) {
|
||||
// First try cred helpers.
|
||||
if ch, exists := auths.CredHelpers[registry]; exists {
|
||||
return false, deleteAuthFromCredHelper(ch, registry)
|
||||
}
|
||||
|
||||
if _, ok := auths.AuthConfigs[registry]; ok {
|
||||
delete(auths.AuthConfigs, registry)
|
||||
} else if _, ok := auths.AuthConfigs[normalizeRegistry(registry)]; ok {
|
||||
delete(auths.AuthConfigs, normalizeRegistry(registry))
|
||||
} else {
|
||||
return false, ErrNotLoggedIn
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveAllAuthentication deletes all the credentials stored in auth.json
|
||||
func RemoveAllAuthentication(ctx *types.SystemContext) error {
|
||||
return modifyJSON(ctx, func(auths *dockerConfigFile) (bool, error) {
|
||||
auths.CredHelpers = make(map[string]string)
|
||||
auths.AuthConfigs = make(map[string]dockerAuthConfig)
|
||||
return true, nil
|
||||
})
|
||||
}
|
||||
|
||||
// getPath gets the path of the auth.json file
|
||||
// The path can be overriden by the user if the overwrite-path flag is set
|
||||
// If the flag is not set and XDG_RUNTIME_DIR is ser, the auth.json file is saved in XDG_RUNTIME_DIR/containers
|
||||
// Otherwise, the auth.json file is stored in /run/user/UID/containers
|
||||
func getPathToAuth(ctx *types.SystemContext) string {
|
||||
if ctx != nil {
|
||||
if ctx.AuthFilePath != "" {
|
||||
return ctx.AuthFilePath
|
||||
}
|
||||
if ctx.RootForImplicitAbsolutePaths != "" {
|
||||
return filepath.Join(ctx.RootForImplicitAbsolutePaths, defaultPath, strconv.Itoa(os.Getuid()), authCfg, authCfgFileName)
|
||||
}
|
||||
}
|
||||
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
|
||||
if runtimeDir == "" {
|
||||
runtimeDir = filepath.Join(defaultPath, strconv.Itoa(os.Getuid()))
|
||||
}
|
||||
return filepath.Join(runtimeDir, authCfg, authCfgFileName)
|
||||
}
|
||||
|
||||
// readJSONFile unmarshals the authentications stored in the auth.json file and returns it
|
||||
// or returns an empty dockerConfigFile data structure if auth.json does not exist
|
||||
// if the file exists and is empty, readJSONFile returns an error
|
||||
func readJSONFile(path string, legacyFormat bool) (dockerConfigFile, error) {
|
||||
var auths dockerConfigFile
|
||||
|
||||
raw, err := ioutil.ReadFile(path)
|
||||
if os.IsNotExist(err) {
|
||||
auths.AuthConfigs = map[string]dockerAuthConfig{}
|
||||
return auths, nil
|
||||
}
|
||||
|
||||
if legacyFormat {
|
||||
if err = json.Unmarshal(raw, &auths.AuthConfigs); err != nil {
|
||||
return dockerConfigFile{}, errors.Wrapf(err, "error unmarshaling JSON at %q", path)
|
||||
}
|
||||
return auths, nil
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(raw, &auths); err != nil {
|
||||
return dockerConfigFile{}, errors.Wrapf(err, "error unmarshaling JSON at %q", path)
|
||||
}
|
||||
|
||||
return auths, nil
|
||||
}
|
||||
|
||||
// modifyJSON writes to auth.json if the dockerConfigFile has been updated
|
||||
func modifyJSON(ctx *types.SystemContext, editor func(auths *dockerConfigFile) (bool, error)) error {
|
||||
path := getPathToAuth(ctx)
|
||||
dir := filepath.Dir(path)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err = os.Mkdir(dir, 0700); err != nil {
|
||||
return errors.Wrapf(err, "error creating directory %q", dir)
|
||||
}
|
||||
}
|
||||
|
||||
auths, err := readJSONFile(path, false)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading JSON file %q", path)
|
||||
}
|
||||
|
||||
updated, err := editor(&auths)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error updating %q", path)
|
||||
}
|
||||
if updated {
|
||||
newData, err := json.MarshalIndent(auths, "", "\t")
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error marshaling JSON %q", path)
|
||||
}
|
||||
|
||||
if err = ioutil.WriteFile(path, newData, 0755); err != nil {
|
||||
return errors.Wrapf(err, "error writing to file %q", path)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAuthFromCredHelper(credHelper, registry string) (string, string, error) {
|
||||
helperName := fmt.Sprintf("docker-credential-%s", credHelper)
|
||||
p := helperclient.NewShellProgramFunc(helperName)
|
||||
creds, err := helperclient.Get(p, registry)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return creds.Username, creds.Secret, nil
|
||||
}
|
||||
|
||||
func setAuthToCredHelper(credHelper, registry, username, password string) error {
|
||||
helperName := fmt.Sprintf("docker-credential-%s", credHelper)
|
||||
p := helperclient.NewShellProgramFunc(helperName)
|
||||
creds := &credentials.Credentials{
|
||||
ServerURL: registry,
|
||||
Username: username,
|
||||
Secret: password,
|
||||
}
|
||||
return helperclient.Store(p, creds)
|
||||
}
|
||||
|
||||
func deleteAuthFromCredHelper(credHelper, registry string) error {
|
||||
helperName := fmt.Sprintf("docker-credential-%s", credHelper)
|
||||
p := helperclient.NewShellProgramFunc(helperName)
|
||||
return helperclient.Erase(p, registry)
|
||||
}
|
||||
|
||||
// findAuthentication looks for auth of registry in path
|
||||
func findAuthentication(registry, path string, legacyFormat bool) (string, string, error) {
|
||||
auths, err := readJSONFile(path, legacyFormat)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrapf(err, "error reading JSON file %q", path)
|
||||
}
|
||||
|
||||
// First try cred helpers. They should always be normalized.
|
||||
if ch, exists := auths.CredHelpers[registry]; exists {
|
||||
return getAuthFromCredHelper(ch, registry)
|
||||
}
|
||||
|
||||
// I'm feeling lucky
|
||||
if val, exists := auths.AuthConfigs[registry]; exists {
|
||||
return decodeDockerAuth(val.Auth)
|
||||
}
|
||||
|
||||
// bad luck; let's normalize the entries first
|
||||
registry = normalizeRegistry(registry)
|
||||
normalizedAuths := map[string]dockerAuthConfig{}
|
||||
for k, v := range auths.AuthConfigs {
|
||||
normalizedAuths[normalizeRegistry(k)] = v
|
||||
}
|
||||
if val, exists := normalizedAuths[registry]; exists {
|
||||
return decodeDockerAuth(val.Auth)
|
||||
}
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
func decodeDockerAuth(s string) (string, string, error) {
|
||||
decoded, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
parts := strings.SplitN(string(decoded), ":", 2)
|
||||
if len(parts) != 2 {
|
||||
// if it's invalid just skip, as docker does
|
||||
return "", "", nil
|
||||
}
|
||||
user := parts[0]
|
||||
password := strings.Trim(parts[1], "\x00")
|
||||
return user, password, nil
|
||||
}
|
||||
|
||||
// convertToHostname converts a registry url which has http|https prepended
|
||||
// to just an hostname.
|
||||
// Copied from github.com/docker/docker/registry/auth.go
|
||||
func convertToHostname(url string) string {
|
||||
stripped := url
|
||||
if strings.HasPrefix(url, "http://") {
|
||||
stripped = strings.TrimPrefix(url, "http://")
|
||||
} else if strings.HasPrefix(url, "https://") {
|
||||
stripped = strings.TrimPrefix(url, "https://")
|
||||
}
|
||||
|
||||
nameParts := strings.SplitN(stripped, "/", 2)
|
||||
|
||||
return nameParts[0]
|
||||
}
|
||||
|
||||
func normalizeRegistry(registry string) string {
|
||||
normalized := convertToHostname(registry)
|
||||
switch normalized {
|
||||
case "registry-1.docker.io", "docker.io":
|
||||
return "index.docker.io"
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
|
@ -304,6 +304,8 @@ type SystemContext struct {
|
|||
RegistriesDirPath string
|
||||
// Path to the system-wide registries configuration file
|
||||
SystemRegistriesConfPath string
|
||||
// If not "", overrides the default path for the authentication file
|
||||
AuthFilePath string
|
||||
|
||||
// === OCI.Transport overrides ===
|
||||
// If not "", a directory containing a CA certificate (ending with ".crt"),
|
||||
|
|
|
|||
Loading…
Reference in New Issue