mirror of https://github.com/docker/cli.git
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))
|
|
}
|