When attempting to load a config-file that exists, but is not accessible for the current user, we should not discard the error. This patch makes sure that the error is returned by Load(), but does not yet change LoadDefaultConfigFile, as this requires a change in signature. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
449 lines
12 KiB
Go
449 lines
12 KiB
Go
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/docker/cli/cli/config/configfile"
|
|
"github.com/docker/cli/cli/config/credentials"
|
|
"gotest.tools/v3/assert"
|
|
is "gotest.tools/v3/assert/cmp"
|
|
"gotest.tools/v3/skip"
|
|
)
|
|
|
|
func setupConfigDir(t *testing.T) string {
|
|
t.Helper()
|
|
tmpdir := t.TempDir()
|
|
oldDir := Dir()
|
|
SetDir(tmpdir)
|
|
t.Cleanup(func() {
|
|
SetDir(oldDir)
|
|
})
|
|
return tmpdir
|
|
}
|
|
|
|
func TestEmptyConfigDir(t *testing.T) {
|
|
tmpHome := setupConfigDir(t)
|
|
|
|
config, err := Load("")
|
|
assert.NilError(t, err)
|
|
|
|
expectedConfigFilename := filepath.Join(tmpHome, ConfigFileName)
|
|
assert.Check(t, is.Equal(expectedConfigFilename, config.Filename))
|
|
|
|
// Now save it and make sure it shows up in new form
|
|
saveConfigAndValidateNewFormat(t, config, tmpHome)
|
|
}
|
|
|
|
func TestMissingFile(t *testing.T) {
|
|
tmpHome := t.TempDir()
|
|
|
|
config, err := Load(tmpHome)
|
|
assert.NilError(t, err)
|
|
|
|
// Now save it and make sure it shows up in new form
|
|
saveConfigAndValidateNewFormat(t, config, tmpHome)
|
|
}
|
|
|
|
// TestLoadDanglingSymlink verifies that we gracefully handle dangling symlinks.
|
|
//
|
|
// TODO(thaJeztah): consider whether we want dangling symlinks to be an error condition instead.
|
|
func TestLoadDanglingSymlink(t *testing.T) {
|
|
cfgDir := t.TempDir()
|
|
cfgFile := filepath.Join(cfgDir, ConfigFileName)
|
|
err := os.Symlink(filepath.Join(cfgDir, "no-such-file"), cfgFile)
|
|
assert.NilError(t, err)
|
|
|
|
config, err := Load(cfgDir)
|
|
assert.NilError(t, err)
|
|
|
|
// Now save it and make sure it shows up in new form
|
|
saveConfigAndValidateNewFormat(t, config, cfgDir)
|
|
|
|
// Make sure we kept the symlink.
|
|
fi, err := os.Lstat(cfgFile)
|
|
assert.NilError(t, err)
|
|
assert.Equal(t, fi.Mode()&os.ModeSymlink, os.ModeSymlink, "expected %v to be a symlink", cfgFile)
|
|
}
|
|
|
|
func TestLoadNoPermissions(t *testing.T) {
|
|
if runtime.GOOS != "windows" {
|
|
skip.If(t, os.Getuid() == 0, "cannot test permission denied when running as root")
|
|
}
|
|
cfgDir := t.TempDir()
|
|
cfgFile := filepath.Join(cfgDir, ConfigFileName)
|
|
err := os.WriteFile(cfgFile, []byte(`{}`), os.FileMode(0o000))
|
|
assert.NilError(t, err)
|
|
|
|
_, err = Load(cfgDir)
|
|
assert.ErrorIs(t, err, os.ErrPermission)
|
|
}
|
|
|
|
func TestSaveFileToDirs(t *testing.T) {
|
|
tmpHome := filepath.Join(t.TempDir(), ".docker")
|
|
config, err := Load(tmpHome)
|
|
assert.NilError(t, err)
|
|
|
|
// Now save it and make sure it shows up in new form
|
|
saveConfigAndValidateNewFormat(t, config, tmpHome)
|
|
}
|
|
|
|
func TestEmptyFile(t *testing.T) {
|
|
tmpHome := t.TempDir()
|
|
|
|
fn := filepath.Join(tmpHome, ConfigFileName)
|
|
err := os.WriteFile(fn, []byte(""), 0o600)
|
|
assert.NilError(t, err)
|
|
|
|
_, err = Load(tmpHome)
|
|
assert.NilError(t, err)
|
|
}
|
|
|
|
func TestEmptyJSON(t *testing.T) {
|
|
tmpHome := t.TempDir()
|
|
|
|
fn := filepath.Join(tmpHome, ConfigFileName)
|
|
err := os.WriteFile(fn, []byte("{}"), 0o600)
|
|
assert.NilError(t, err)
|
|
|
|
config, err := Load(tmpHome)
|
|
assert.NilError(t, err)
|
|
|
|
// Now save it and make sure it shows up in new form
|
|
saveConfigAndValidateNewFormat(t, config, tmpHome)
|
|
}
|
|
|
|
func TestNewJSON(t *testing.T) {
|
|
tmpHome := t.TempDir()
|
|
|
|
fn := filepath.Join(tmpHome, ConfigFileName)
|
|
js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv" } } }`
|
|
if err := os.WriteFile(fn, []byte(js), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config, err := Load(tmpHome)
|
|
assert.NilError(t, err)
|
|
|
|
ac := config.AuthConfigs["https://index.docker.io/v1/"]
|
|
assert.Equal(t, ac.Username, "joejoe")
|
|
assert.Equal(t, ac.Password, "hello")
|
|
|
|
// Now save it and make sure it shows up in new form
|
|
configStr := saveConfigAndValidateNewFormat(t, config, tmpHome)
|
|
|
|
expConfStr := `{
|
|
"auths": {
|
|
"https://index.docker.io/v1/": {
|
|
"auth": "am9lam9lOmhlbGxv"
|
|
}
|
|
}
|
|
}`
|
|
|
|
if configStr != expConfStr {
|
|
t.Fatalf("Should have save in new form: \n%s\n not \n%s", configStr, expConfStr)
|
|
}
|
|
}
|
|
|
|
func TestNewJSONNoEmail(t *testing.T) {
|
|
tmpHome := t.TempDir()
|
|
|
|
fn := filepath.Join(tmpHome, ConfigFileName)
|
|
js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv" } } }`
|
|
if err := os.WriteFile(fn, []byte(js), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config, err := Load(tmpHome)
|
|
assert.NilError(t, err)
|
|
|
|
ac := config.AuthConfigs["https://index.docker.io/v1/"]
|
|
assert.Equal(t, ac.Username, "joejoe")
|
|
assert.Equal(t, ac.Password, "hello")
|
|
|
|
// Now save it and make sure it shows up in new form
|
|
configStr := saveConfigAndValidateNewFormat(t, config, tmpHome)
|
|
|
|
expConfStr := `{
|
|
"auths": {
|
|
"https://index.docker.io/v1/": {
|
|
"auth": "am9lam9lOmhlbGxv"
|
|
}
|
|
}
|
|
}`
|
|
|
|
if configStr != expConfStr {
|
|
t.Fatalf("Should have save in new form: \n%s\n not \n%s", configStr, expConfStr)
|
|
}
|
|
}
|
|
|
|
func TestJSONWithPsFormat(t *testing.T) {
|
|
tmpHome := t.TempDir()
|
|
|
|
fn := filepath.Join(tmpHome, ConfigFileName)
|
|
js := `{
|
|
"auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } },
|
|
"psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}"
|
|
}`
|
|
if err := os.WriteFile(fn, []byte(js), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config, err := Load(tmpHome)
|
|
assert.NilError(t, err)
|
|
|
|
if config.PsFormat != `table {{.ID}}\t{{.Label "com.docker.label.cpu"}}` {
|
|
t.Fatalf("Unknown ps format: %s\n", config.PsFormat)
|
|
}
|
|
|
|
// Now save it and make sure it shows up in new form
|
|
configStr := saveConfigAndValidateNewFormat(t, config, tmpHome)
|
|
if !strings.Contains(configStr, `"psFormat":`) ||
|
|
!strings.Contains(configStr, "{{.ID}}") {
|
|
t.Fatalf("Should have save in new form: %s", configStr)
|
|
}
|
|
}
|
|
|
|
func TestJSONWithCredentialStore(t *testing.T) {
|
|
tmpHome := t.TempDir()
|
|
|
|
fn := filepath.Join(tmpHome, ConfigFileName)
|
|
js := `{
|
|
"auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } },
|
|
"credsStore": "crazy-secure-storage"
|
|
}`
|
|
if err := os.WriteFile(fn, []byte(js), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config, err := Load(tmpHome)
|
|
assert.NilError(t, err)
|
|
|
|
if config.CredentialsStore != "crazy-secure-storage" {
|
|
t.Fatalf("Unknown credential store: %s\n", config.CredentialsStore)
|
|
}
|
|
|
|
// Now save it and make sure it shows up in new form
|
|
configStr := saveConfigAndValidateNewFormat(t, config, tmpHome)
|
|
if !strings.Contains(configStr, `"credsStore":`) ||
|
|
!strings.Contains(configStr, "crazy-secure-storage") {
|
|
t.Fatalf("Should have save in new form: %s", configStr)
|
|
}
|
|
}
|
|
|
|
func TestJSONWithCredentialHelpers(t *testing.T) {
|
|
tmpHome := t.TempDir()
|
|
|
|
fn := filepath.Join(tmpHome, ConfigFileName)
|
|
js := `{
|
|
"auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } },
|
|
"credHelpers": { "images.io": "images-io", "containers.com": "crazy-secure-storage" }
|
|
}`
|
|
if err := os.WriteFile(fn, []byte(js), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config, err := Load(tmpHome)
|
|
assert.NilError(t, err)
|
|
|
|
if config.CredentialHelpers == nil {
|
|
t.Fatal("config.CredentialHelpers was nil")
|
|
} else if config.CredentialHelpers["images.io"] != "images-io" ||
|
|
config.CredentialHelpers["containers.com"] != "crazy-secure-storage" {
|
|
t.Fatalf("Credential helpers not deserialized properly: %v\n", config.CredentialHelpers)
|
|
}
|
|
|
|
// Now save it and make sure it shows up in new form
|
|
configStr := saveConfigAndValidateNewFormat(t, config, tmpHome)
|
|
if !strings.Contains(configStr, `"credHelpers":`) ||
|
|
!strings.Contains(configStr, "images.io") ||
|
|
!strings.Contains(configStr, "images-io") ||
|
|
!strings.Contains(configStr, "containers.com") ||
|
|
!strings.Contains(configStr, "crazy-secure-storage") {
|
|
t.Fatalf("Should have save in new form: %s", configStr)
|
|
}
|
|
}
|
|
|
|
// Save it and make sure it shows up in new form
|
|
func saveConfigAndValidateNewFormat(t *testing.T, config *configfile.ConfigFile, configDir string) string {
|
|
t.Helper()
|
|
assert.NilError(t, config.Save())
|
|
|
|
buf, err := os.ReadFile(filepath.Join(configDir, ConfigFileName))
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.Contains(string(buf), `"auths":`))
|
|
return string(buf)
|
|
}
|
|
|
|
func TestConfigDir(t *testing.T) {
|
|
tmpHome := t.TempDir()
|
|
|
|
if Dir() == tmpHome {
|
|
t.Fatalf("Expected ConfigDir to be different than %s by default, but was the same", tmpHome)
|
|
}
|
|
|
|
// Update configDir
|
|
SetDir(tmpHome)
|
|
|
|
if Dir() != tmpHome {
|
|
t.Fatalf("Expected ConfigDir to %s, but was %s", tmpHome, Dir())
|
|
}
|
|
}
|
|
|
|
func TestJSONReaderNoFile(t *testing.T) {
|
|
js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } } }`
|
|
|
|
config, err := LoadFromReader(strings.NewReader(js))
|
|
assert.NilError(t, err)
|
|
|
|
ac := config.AuthConfigs["https://index.docker.io/v1/"]
|
|
assert.Equal(t, ac.Username, "joejoe")
|
|
assert.Equal(t, ac.Password, "hello")
|
|
}
|
|
|
|
func TestJSONWithPsFormatNoFile(t *testing.T) {
|
|
js := `{
|
|
"auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } },
|
|
"psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}"
|
|
}`
|
|
config, err := LoadFromReader(strings.NewReader(js))
|
|
assert.NilError(t, err)
|
|
|
|
if config.PsFormat != `table {{.ID}}\t{{.Label "com.docker.label.cpu"}}` {
|
|
t.Fatalf("Unknown ps format: %s\n", config.PsFormat)
|
|
}
|
|
}
|
|
|
|
func TestJSONSaveWithNoFile(t *testing.T) {
|
|
js := `{
|
|
"auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv" } },
|
|
"psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}"
|
|
}`
|
|
config, err := LoadFromReader(strings.NewReader(js))
|
|
assert.NilError(t, err)
|
|
err = config.Save()
|
|
assert.ErrorContains(t, err, "with empty filename")
|
|
|
|
tmpHome := t.TempDir()
|
|
|
|
fn := filepath.Join(tmpHome, ConfigFileName)
|
|
f, _ := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
|
defer f.Close()
|
|
|
|
assert.NilError(t, config.SaveToWriter(f))
|
|
buf, err := os.ReadFile(filepath.Join(tmpHome, ConfigFileName))
|
|
assert.NilError(t, err)
|
|
expConfStr := `{
|
|
"auths": {
|
|
"https://index.docker.io/v1/": {
|
|
"auth": "am9lam9lOmhlbGxv"
|
|
}
|
|
},
|
|
"psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}"
|
|
}`
|
|
if string(buf) != expConfStr {
|
|
t.Fatalf("Should have save in new form: \n%s\nnot \n%s", string(buf), expConfStr)
|
|
}
|
|
}
|
|
|
|
func TestLoadDefaultConfigFile(t *testing.T) {
|
|
dir := setupConfigDir(t)
|
|
buffer := new(bytes.Buffer)
|
|
|
|
filename := filepath.Join(dir, ConfigFileName)
|
|
content := []byte(`{"PsFormat": "format"}`)
|
|
err := os.WriteFile(filename, content, 0o644)
|
|
assert.NilError(t, err)
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
configFile := LoadDefaultConfigFile(buffer)
|
|
credStore := credentials.DetectDefaultStore("")
|
|
expected := configfile.New(filename)
|
|
expected.CredentialsStore = credStore
|
|
expected.PsFormat = "format"
|
|
|
|
assert.Check(t, is.DeepEqual(expected, configFile))
|
|
assert.Check(t, is.Equal(buffer.String(), ""))
|
|
})
|
|
|
|
t.Run("permission error", func(t *testing.T) {
|
|
if runtime.GOOS != "windows" {
|
|
skip.If(t, os.Getuid() == 0, "cannot test permission denied when running as root")
|
|
}
|
|
err = os.Chmod(filename, 0o000)
|
|
assert.NilError(t, err)
|
|
|
|
buffer.Reset()
|
|
_ = LoadDefaultConfigFile(buffer)
|
|
warnings := buffer.String()
|
|
|
|
assert.Check(t, is.Contains(warnings, "WARNING:"))
|
|
assert.Check(t, is.Contains(warnings, os.ErrPermission.Error()))
|
|
})
|
|
}
|
|
|
|
func TestConfigPath(t *testing.T) {
|
|
oldDir := Dir()
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
dir string
|
|
path []string
|
|
expected string
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "valid_path",
|
|
dir: "dummy",
|
|
path: []string{"a", "b"},
|
|
expected: filepath.Join("dummy", "a", "b"),
|
|
},
|
|
{
|
|
name: "valid_path_absolute_dir",
|
|
dir: "/dummy",
|
|
path: []string{"a", "b"},
|
|
expected: filepath.Join("/dummy", "a", "b"),
|
|
},
|
|
{
|
|
name: "invalid_relative_path",
|
|
dir: "dummy",
|
|
path: []string{"e", "..", "..", "f"},
|
|
expectedErr: fmt.Sprintf("is outside of root config directory %q", "dummy"),
|
|
},
|
|
{
|
|
name: "invalid_absolute_path",
|
|
dir: "dummy",
|
|
path: []string{"/a", "..", ".."},
|
|
expectedErr: fmt.Sprintf("is outside of root config directory %q", "dummy"),
|
|
},
|
|
} {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
SetDir(tc.dir)
|
|
f, err := Path(tc.path...)
|
|
assert.Equal(t, f, tc.expected)
|
|
if tc.expectedErr == "" {
|
|
assert.NilError(t, err)
|
|
} else {
|
|
assert.ErrorContains(t, err, tc.expectedErr)
|
|
}
|
|
})
|
|
}
|
|
|
|
SetDir(oldDir)
|
|
}
|
|
|
|
// TestSetDir verifies that Dir() does not overwrite the value set through
|
|
// SetDir() if it has not been run before.
|
|
func TestSetDir(t *testing.T) {
|
|
const expected = "my_config_dir"
|
|
resetConfigDir()
|
|
SetDir(expected)
|
|
assert.Check(t, is.Equal(Dir(), expected))
|
|
}
|