Files
docker-cli/cli/config/config_test.go
Sebastiaan van Stijn 0dabdd1a0d cli/command: move TestExperimentalCLI to cli/config
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>
2025-04-01 17:06:39 +02:00

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))
}