Skip to content
Open
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
9 changes: 9 additions & 0 deletions config/auth.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"errors"
"fmt"

"github.com/moby/moby/api/pkg/authconfig"
Expand All @@ -17,6 +18,10 @@ const tokenUsername = "<token>"
// The returned map is keyed by the registry registry hostname for each image.
func AuthConfigs(images ...string) (map[string]registry.AuthConfig, error) {
cfg, err := Load()
if errors.Is(err, ErrConfigFileNotFound) {
cfg = Config{}
err = nil
}
if err != nil {
return nil, fmt.Errorf("load config: %w", err)
}
Expand All @@ -30,6 +35,10 @@ func AuthConfigs(images ...string) (map[string]registry.AuthConfig, error) {
// If the config doesn't exist, it will attempt to load registry credentials using the default credential helper for the platform.
func AuthConfigForHostname(hostname string) (registry.AuthConfig, error) {
cfg, err := Load()
if errors.Is(err, ErrConfigFileNotFound) {
cfg = Config{}
err = nil
}
if err != nil {
return registry.AuthConfig{}, fmt.Errorf("load config: %w", err)
}
Expand Down
79 changes: 79 additions & 0 deletions config/auth_missing_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package config

import (
"errors"
"os/exec"
"testing"

"github.com/stretchr/testify/require"
)

// setupConfigDirWithoutFile creates a temporary directory and sets DOCKER_CONFIG
// to point to it, but does NOT create a config.json file. This exercises the
// ErrConfigFileNotFound code path in Load/Filepath.
func setupConfigDirWithoutFile(t *testing.T) {
t.Helper()
t.Setenv(EnvOverrideDir, t.TempDir())
}

func TestAuthConfigs_ConfigNotFound(t *testing.T) {
setupConfigDirWithoutFile(t)
mockExecCommand(t)

authConfigs, err := AuthConfigs("some.io/repo/image:tag")
require.NoError(t, err)
require.Contains(t, authConfigs, "some.io")
require.Empty(t, authConfigs["some.io"].Username)
require.Empty(t, authConfigs["some.io"].Password)
}

func TestAuthConfigs_ConfigNotFound_FallsBackToCredentialHelper(t *testing.T) {
setupConfigDirWithoutFile(t)

execLookPath = func(string) (string, error) {
return "", errors.New("helper unreachable")
}
t.Cleanup(func() { execLookPath = exec.LookPath })

_, err := AuthConfigs("some.io/repo/image:tag")
require.Error(t, err)
require.ErrorContains(t, err, "helper unreachable")
}

func TestAuthConfigForHostname_ConfigNotFound(t *testing.T) {
setupConfigDirWithoutFile(t)
mockExecCommand(t)

creds, err := AuthConfigForHostname("some.io")
require.NoError(t, err)
require.Empty(t, creds.Username)
require.Empty(t, creds.Password)
}

func TestAuthConfigForHostname_ConfigNotFound_FallsBackToCredentialHelper(t *testing.T) {
setupConfigDirWithoutFile(t)

execLookPath = func(string) (string, error) {
return "", errors.New("helper unreachable")
}
t.Cleanup(func() { execLookPath = exec.LookPath })

_, err := AuthConfigForHostname("some.io")
require.Error(t, err)
require.ErrorContains(t, err, "helper unreachable")
}

func TestLoad_ConfigNotFound_ReturnsSentinel(t *testing.T) {
setupConfigDirWithoutFile(t)

_, err := Load()
require.ErrorIs(t, err, ErrConfigFileNotFound)
}

func TestFilepath_ConfigNotFound_ReturnsSentinel(t *testing.T) {
setupConfigDirWithoutFile(t)

_, err := Filepath()
require.ErrorIs(t, err, ErrConfigFileNotFound)
require.Contains(t, err.Error(), "config file does not exist")
}
7 changes: 7 additions & 0 deletions config/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package config

import "errors"

// ErrConfigFileNotFound is used as a sentinel error to distinguish when a docker config file is not present,
// so that cases without a config file work, but cases with an invalid config file still fail
var ErrConfigFileNotFound = errors.New("config file not found")
5 changes: 4 additions & 1 deletion config/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func Filepath() (string, error) {

configFilePath := filepath.Join(dir, FileName)
if !fileExists(configFilePath) {
return "", fmt.Errorf("config file does not exist (%s)", configFilePath)
return "", errors.Join(fmt.Errorf("config file does not exist (%s)", configFilePath), ErrConfigFileNotFound)
}
Comment thread
mdelapenya marked this conversation as resolved.
Comment on lines 84 to 87

return configFilePath, nil
Expand All @@ -109,6 +109,9 @@ func Load() (Config, error) {
}

cfg, err = loadFromFilepath(p)
if errors.Is(err, os.ErrNotExist) {
return cfg, nil
}
Comment on lines 111 to +114
if err != nil {
return cfg, fmt.Errorf("load config: %w", err)
}
Expand Down
3 changes: 3 additions & 0 deletions context/context.add.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ func New(name string, opts ...CreateContextOption) (*Context, error) {
// set the context as the current context if the option is set
if defaultOptions.current {
cfg, err := config.Load()
if errors.Is(err, config.ErrConfigFileNotFound) {
return ctx, nil
}
if err != nil {
return nil, fmt.Errorf("load config: %w", err)
Comment thread
mdelapenya marked this conversation as resolved.
}
Expand Down
4 changes: 4 additions & 0 deletions context/context.delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ func (ctx *Context) Delete() error {
if ctx.isCurrent {
// reset the current context to the default context
cfg, err := config.Load()
if errors.Is(err, config.ErrConfigFileNotFound) {
return nil
}

if err != nil {
return fmt.Errorf("load config: %w", err)
}
Expand Down
4 changes: 4 additions & 0 deletions context/current.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package context

import (
"errors"
"fmt"
"net/url"
"os"
Expand All @@ -25,6 +26,9 @@ func Current() (string, error) {
if os.IsNotExist(err) {
return DefaultContextName, nil
}
if errors.Is(err, config.ErrConfigFileNotFound) {
return DefaultContextName, nil
}
return "", fmt.Errorf("load docker config: %w", err)
}

Expand Down
106 changes: 106 additions & 0 deletions context/missing_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package context

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"

"github.com/docker/go-sdk/config"
)

// setupDockerDirWithoutConfigFile creates a ~/.docker directory in a temp home
// so that config.Dir() succeeds, but does NOT create config.json, so that
// config.Load() returns ErrConfigFileNotFound.
func setupDockerDirWithoutConfigFile(tb testing.TB) {
tb.Helper()
tmpDir := tb.TempDir()
tb.Setenv("HOME", tmpDir)
tb.Setenv("USERPROFILE", tmpDir) // Windows support
require.NoError(tb, os.MkdirAll(filepath.Join(tmpDir, ".docker"), 0o755))
}

// removeConfigFile deletes the config.json from the current DOCKER_CONFIG dir.
func removeConfigFile(tb testing.TB) {
tb.Helper()
dir, err := config.Dir()
require.NoError(tb, err)
require.NoError(tb, os.Remove(filepath.Join(dir, config.FileName)))
}

func TestCurrent_ConfigNotFound(t *testing.T) {
setupDockerDirWithoutConfigFile(t)

current, err := Current()
require.NoError(t, err)
require.Equal(t, DefaultContextName, current)
}

func TestInspect_ConfigNotFound(t *testing.T) {
SetupTestDockerContexts(t, 1, 3) // creates config.json with currentContext=context1
removeConfigFile(t) // simulate a fresh install without config.json

ctx, err := Inspect("context1")
require.NoError(t, err)
require.Equal(t, "context1", ctx.Name)
require.Equal(t, "tcp://127.0.0.1:1", ctx.Endpoints["docker"].Host)

require.NotEmpty(t, ctx.encodedName, "encodedName should be set even when config is missing")
require.False(t, ctx.isCurrent, "isCurrent should be false when config file is missing")
}

func TestStore_Inspect_ConfigNotFound(t *testing.T) {
SetupTestDockerContexts(t, 1, 3)
removeConfigFile(t)

metaDir, err := metaRoot()
require.NoError(t, err)
s := &store{root: metaDir}

ctx, err := s.inspect("context1")
require.NoError(t, err)
require.Equal(t, "context1", ctx.Name)
require.NotEmpty(t, ctx.encodedName)
require.False(t, ctx.isCurrent)
}

func TestNew_AsCurrent_ConfigNotFound(t *testing.T) {
setupDockerDirWithoutConfigFile(t)

ctx, err := New("newctx",
WithHost("tcp://127.0.0.1:9999"),
AsCurrent(),
)
require.NoError(t, err)
defer func() { require.NoError(t, ctx.Delete()) }()

require.Equal(t, "newctx", ctx.Name)
require.False(t, ctx.isCurrent, "isCurrent should be false when config file is missing")

list, err := List()
require.NoError(t, err)
require.Contains(t, list, "newctx")

current, err := Current()
require.NoError(t, err)
require.NotEqual(t, "newctx", current, "current should not be the new context without a config file")
}

func TestDelete_CurrentContext_ConfigNotFound(t *testing.T) {
SetupTestDockerContexts(t, 1, 3) // creates config.json + contexts

ctx, err := New("deleteme",
WithHost("tcp://127.0.0.1:9999"),
AsCurrent(),
)
require.NoError(t, err)
require.True(t, ctx.isCurrent, "new context should be current")

removeConfigFile(t)

require.NoError(t, ctx.Delete(), "delete should not fail when config file is missing")

_, err = Inspect("deleteme")
require.ErrorIs(t, err, ErrDockerContextNotFound)
}
7 changes: 5 additions & 2 deletions context/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,14 +139,17 @@ func (s *store) inspect(ctxName string) (Context, error) {
return Context{}, ErrDockerHostNotSet
}

ctx.encodedName = digest.FromString(ctx.Name).Encoded()

cfg, err := config.Load()
if errors.Is(err, config.ErrConfigFileNotFound) {
return *ctx, nil
}
if err != nil {
Comment thread
mdelapenya marked this conversation as resolved.
return Context{}, fmt.Errorf("load config: %w", err)
}
ctx.isCurrent = cfg.CurrentContext == ctx.Name

ctx.encodedName = digest.FromString(ctx.Name).Encoded()

return *ctx, nil
}
}
Expand Down