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

Attempt 2: Check for Podman's auth in DefaultKeychain #1185

Merged
merged 4 commits into from
Nov 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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/google/go-cmp v0.5.6
github.com/gorilla/mux v1.8.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2-0.20210730191737-8e42a01fb1b7
github.com/spf13/cobra v1.2.1
Expand Down
1 change: 1 addition & 0 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pkg/authn/k8schain/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 49 additions & 3 deletions pkg/authn/keychain.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ package authn

import (
"os"
"path/filepath"
"sync"

"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/types"
"github.com/google/go-containerregistry/pkg/name"
"github.com/mitchellh/go-homedir"
)

// Resource represents a registry or repository that can be authenticated against.
Expand Down Expand Up @@ -62,9 +65,52 @@ const (
func (dk *defaultKeychain) Resolve(target Resource) (Authenticator, error) {
dk.mu.Lock()
defer dk.mu.Unlock()
cf, err := config.Load(os.Getenv("DOCKER_CONFIG"))
if err != nil {
return nil, err

// Podman users may have their container registry auth configured in a
// different location, that Docker packages aren't aware of.
// If the Docker config file isn't found, we'll fallback to look where
// Podman configures it, and parse that as a Docker auth config instead.

// First, check $HOME/.docker/config.json
foundDockerConfig := false
home, err := homedir.Dir()
if err == nil {
if _, err := os.Stat(filepath.Join(home, ".docker/config.json")); err == nil {
foundDockerConfig = true
}
}
// If $HOME/.docker/config.json isn't found, check $DOCKER_CONFIG (if set)
if !foundDockerConfig && os.Getenv("DOCKER_CONFIG") != "" {
if _, err := os.Stat(filepath.Join(os.Getenv("DOCKER_CONFIG"), "config.json")); err == nil {
foundDockerConfig = true
}
}
// If either of those locations are found, load it using Docker's
// config.Load, which may fail if the config can't be parsed.
//
// If neither was found, look for Podman's auth at
// $XDG_RUNTIME_DIR/containers/auth.json and attempt to load it as a
// Docker config.
//
// If neither are found, fallback to Anonymous.
var cf *configfile.ConfigFile
if foundDockerConfig {
cf, err = config.Load(os.Getenv("DOCKER_CONFIG"))
if err != nil {
return nil, err
}
} else {
f, err := os.Open(filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), "containers/auth.json"))
if os.IsNotExist(err) {
return Anonymous, nil
} else if err != nil {
return nil, err
}
defer f.Close()
cf, err = config.LoadFromReader(f)
if err != nil {
return nil, err
}
}

// See:
Expand Down
163 changes: 139 additions & 24 deletions pkg/authn/keychain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"encoding/base64"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"reflect"
Expand All @@ -32,6 +33,20 @@ var (
defaultRegistry, _ = name.NewRegistry(name.DefaultRegistry, name.WeakValidation)
)

func TestMain(m *testing.M) {
// Set $HOME to a temp empty dir, to ensure $HOME/.docker/config.json
// isn't unexpectedly found.
tmp, err := ioutil.TempDir("", "keychain_test_home")
if err != nil {
log.Fatal(err)
}
os.Setenv("HOME", tmp)
os.Exit(func() int {
defer os.RemoveAll(tmp)
return m.Run()
}())
}

// setupConfigDir sets up an isolated configDir() for this test.
func setupConfigDir(t *testing.T) string {
tmpdir := os.Getenv("TEST_TMPDIR")
Expand All @@ -43,8 +58,9 @@ func setupConfigDir(t *testing.T) string {
}
}

fresh = fresh + 1
p := fmt.Sprintf("%s/%d", tmpdir, fresh)
fresh++
p := filepath.Join(tmpdir, fmt.Sprintf("%d", fresh))
t.Logf("DOCKER_CONFIG=%s", p)
os.Setenv("DOCKER_CONFIG", p)
if err := os.Mkdir(p, 0777); err != nil {
t.Fatalf("mkdir %q: %v", p, err)
Expand Down Expand Up @@ -77,33 +93,130 @@ func TestNoConfig(t *testing.T) {
}
}

func TestPodmanConfig(t *testing.T) {
tmpdir := os.Getenv("TEST_TMPDIR")
if tmpdir == "" {
var err error
tmpdir, err = ioutil.TempDir("", "keychain_test")
if err != nil {
t.Fatalf("creating temp dir: %v", err)
}
}
fresh++
p := filepath.Join(tmpdir, fmt.Sprintf("%d", fresh))
os.Setenv("XDG_RUNTIME_DIR", p)
os.Unsetenv("DOCKER_CONFIG")
if err := os.MkdirAll(filepath.Join(p, "containers"), 0777); err != nil {
t.Fatalf("mkdir %s/containers: %v", p, err)
}
cfg := filepath.Join(p, "containers/auth.json")
content := fmt.Sprintf(`{"auths": {"test.io": {"auth": %q}}}`, encode("foo", "bar"))
if err := ioutil.WriteFile(cfg, []byte(content), 0600); err != nil {
t.Fatalf("write %q: %v", cfg, err)
}

// At first, $DOCKER_CONFIG is unset and $HOME/.docker/config.json isn't
// found, but Podman auth is configured. This should return Podman's
// auth.
auth, err := DefaultKeychain.Resolve(testRegistry)
if err != nil {
t.Fatalf("Resolve() = %v", err)
}
got, err := auth.Authorization()
if err != nil {
t.Fatal(err)
}
want := &AuthConfig{
Username: "foo",
Password: "bar",
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %+v, want %+v", got, want)
}

// Now, configure $HOME/.docker/config.json, which should override
// Podman auth and be used.
if err := os.MkdirAll(filepath.Join(os.Getenv("HOME"), ".docker"), 0777); err != nil {
t.Fatalf("mkdir $HOME/.docker: %v", err)
}
cfg = filepath.Join(os.Getenv("HOME"), ".docker/config.json")
content = fmt.Sprintf(`{"auths": {"test.io": {"auth": %q}}}`, encode("home-foo", "home-bar"))
if err := ioutil.WriteFile(cfg, []byte(content), 0600); err != nil {
t.Fatalf("write %q: %v", cfg, err)
}
auth, err = DefaultKeychain.Resolve(testRegistry)
if err != nil {
t.Fatalf("Resolve() = %v", err)
}
got, err = auth.Authorization()
if err != nil {
t.Fatal(err)
}
want = &AuthConfig{
Username: "home-foo",
Password: "home-bar",
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %+v, want %+v", got, want)
}

// Then, configure DOCKER_CONFIG with a valid config file with different
// auth configured.
// This demonstrates that DOCKER_CONFIG is preferred over Podman auth
// and $HOME/.docker/config.json.
content = fmt.Sprintf(`{"auths": {"test.io": {"auth": %q}}}`, encode("another-foo", "another-bar"))
cd := setupConfigFile(t, content)
defer os.RemoveAll(filepath.Dir(cd))

auth, err = DefaultKeychain.Resolve(testRegistry)
if err != nil {
t.Fatalf("Resolve() = %v", err)
}
got, err = auth.Authorization()
if err != nil {
t.Fatal(err)
}
want = &AuthConfig{
Username: "another-foo",
Password: "another-bar",
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %+v, want %+v", got, want)
}
}

func encode(user, pass string) string {
delimited := fmt.Sprintf("%s:%s", user, pass)
return base64.StdEncoding.EncodeToString([]byte(delimited))
}

func TestVariousPaths(t *testing.T) {
tests := []struct {
desc string
content string
wantErr bool
target name.Registry
cfg *AuthConfig
}{{
desc: "invalid config file",
target: testRegistry,
content: `}{`,
wantErr: true,
}, {
desc: "creds store does not exist",
target: testRegistry,
content: `{"credsStore":"#definitely-does-not-exist"}`,
wantErr: true,
}, {
desc: "valid config file",
target: testRegistry,
content: fmt.Sprintf(`{"auths": {"test.io": {"auth": %q}}}`, encode("foo", "bar")),
cfg: &AuthConfig{
Username: "foo",
Password: "bar",
},
}, {
desc: "valid config file; default registry",
target: defaultRegistry,
content: fmt.Sprintf(`{"auths": {"%s": {"auth": %q}}}`, DefaultAuthKey, encode("foo", "bar")),
cfg: &AuthConfig{
Expand All @@ -113,29 +226,31 @@ func TestVariousPaths(t *testing.T) {
}}

for _, test := range tests {
cd := setupConfigFile(t, test.content)
// For some reason, these tempdirs don't get cleaned up.
defer os.RemoveAll(filepath.Dir(cd))

auth, err := DefaultKeychain.Resolve(test.target)
if test.wantErr {
if err == nil {
t.Fatal("wanted err, got nil")
} else if err != nil {
// success
continue
t.Run(test.desc, func(t *testing.T) {
cd := setupConfigFile(t, test.content)
// For some reason, these tempdirs don't get cleaned up.
defer os.RemoveAll(filepath.Dir(cd))

auth, err := DefaultKeychain.Resolve(test.target)
if test.wantErr {
if err == nil {
t.Fatal("wanted err, got nil")
} else if err != nil {
// success
return
}
}
if err != nil {
t.Fatalf("wanted nil, got err: %v", err)
}
cfg, err := auth.Authorization()
if err != nil {
t.Fatal(err)
}
}
if err != nil {
t.Fatalf("wanted nil, got err: %v", err)
}
cfg, err := auth.Authorization()
if err != nil {
t.Fatal(err)
}

if !reflect.DeepEqual(cfg, test.cfg) {
t.Errorf("got %+v, want %+v", cfg, test.cfg)
}
if !reflect.DeepEqual(cfg, test.cfg) {
t.Errorf("got %+v, want %+v", cfg, test.cfg)
}
})
}
}
21 changes: 21 additions & 0 deletions vendor/github.com/mitchellh/go-homedir/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions vendor/github.com/mitchellh/go-homedir/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions vendor/github.com/mitchellh/go-homedir/go.mod

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading