This test was only testing whether we could load a legacy config-file that
contained the "experimental" (experimental CLI) option. Experimental cli
options are disabled since 977d3ae046 (20.10),
and now enabled by default, but we should not fail to start the cli if the
config-file contains the option.
Move the test to the config package, as it doesn't need the cli for this.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
485 lines
13 KiB
Go
485 lines
13 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 TestMalformedJSON(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.Check(t, is.ErrorContains(err, fmt.Sprintf(`parsing config file (%s):`, fn)))
|
|
}
|
|
|
|
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()))
|
|
})
|
|
}
|
|
|
|
// The CLI no longer disables/hides experimental CLI features, however, we need
|
|
// to verify that existing configuration files do not break
|
|
func TestLoadLegacyExperimental(t *testing.T) {
|
|
tests := []struct {
|
|
doc string
|
|
configfile string
|
|
}{
|
|
{
|
|
doc: "default",
|
|
configfile: `{}`,
|
|
},
|
|
{
|
|
doc: "experimental",
|
|
configfile: `{
|
|
"experimental": "enabled"
|
|
}`,
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.doc, func(t *testing.T) {
|
|
_, err := LoadFromReader(strings.NewReader(tc.configfile))
|
|
assert.NilError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
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"),
|
|
},
|
|
} {
|
|
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))
|
|
}
|