From 0f5e629c7b75d076586df8f8f6e3114408a59d9f Mon Sep 17 00:00:00 2001 From: Mauro Ezequiel Moltrasio Date: Wed, 30 Sep 2026 17:26:48 +0200 Subject: [PATCH 1/4] Fallback to podman auth.json when .docker/config.json is not found On machines that run podman, `.docker/config.json` may not exist. The [podman login manpage](https://docs.podman.io/en/latest/markdown/podman-login.1.html) explicitly states it uses either `XDG_RUNTIME_DIR/containers/auth.json` or `~/.config/containers/auth.json`, so this change makes it so we try to use these files when the docker specific one is not found. --- internal/dockerauth/dockerauth.go | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/internal/dockerauth/dockerauth.go b/internal/dockerauth/dockerauth.go index 244fc30..180f982 100644 --- a/internal/dockerauth/dockerauth.go +++ b/internal/dockerauth/dockerauth.go @@ -87,14 +87,25 @@ func (d *DockerAuth) GetAndVerifyCredentials(ctx context.Context, registry strin if username == "" { // Try to get from Docker config file. dockerConfigPath := filepath.Join(os.Getenv("HOME"), ".docker", "config.json") - d.logger.Dimf("REGISTRY_USERNAME/REGISTRY_PASSWORD unset. Trying to obtain Docker credentials from config file: %s", dockerConfigPath) - if _, err := os.Stat(dockerConfigPath); err == nil { - var err error - username, password, err = d.getCredentialsFromDockerConfig(dockerConfigPath, host) - if err != nil { + _, err := os.Stat(dockerConfigPath) + if err != nil { + // No .docker/config.json file found, check podman auth.json files + xdgRuntimePath := os.Getenv("XDG_RUNTIME_DIR") + if xdgRuntimePath != "" { + dockerConfigPath = filepath.Join(xdgRuntimePath, "containers", "auth.json") + } else { + dockerConfigPath = filepath.Join(os.Getenv("HOME"), ".config", "containers", "auth.json") + } + + if _, err = os.Stat(dockerConfigPath); err != nil { return nil, err } } + d.logger.Dimf("REGISTRY_USERNAME/REGISTRY_PASSWORD unset. Trying to obtain Docker credentials from config file: %s", dockerConfigPath) + username, password, err = d.getCredentialsFromDockerConfig(dockerConfigPath, host) + if err != nil { + return nil, err + } } if username == "" || password == "" { From 7b885210f5a48a4a259712b5f279482aaaeca015 Mon Sep 17 00:00:00 2001 From: Mauro Ezequiel Moltrasio Date: Thu, 1 Oct 2026 11:57:43 +0200 Subject: [PATCH 2/4] Add tests for podman auth cases --- internal/dockerauth/dockerauth.go | 40 +++--- internal/dockerauth/dockerauth_test.go | 164 ++++++++++++++++++++++++- 2 files changed, 186 insertions(+), 18 deletions(-) diff --git a/internal/dockerauth/dockerauth.go b/internal/dockerauth/dockerauth.go index 180f982..41569ea 100644 --- a/internal/dockerauth/dockerauth.go +++ b/internal/dockerauth/dockerauth.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "fmt" + "io/fs" "net/http" "os" "os/exec" @@ -85,24 +86,12 @@ func (d *DockerAuth) GetAndVerifyCredentials(ctx context.Context, registry strin } if username == "" { - // Try to get from Docker config file. - dockerConfigPath := filepath.Join(os.Getenv("HOME"), ".docker", "config.json") - _, err := os.Stat(dockerConfigPath) + registryAuthPath, err := d.findAuthConfigPath() if err != nil { - // No .docker/config.json file found, check podman auth.json files - xdgRuntimePath := os.Getenv("XDG_RUNTIME_DIR") - if xdgRuntimePath != "" { - dockerConfigPath = filepath.Join(xdgRuntimePath, "containers", "auth.json") - } else { - dockerConfigPath = filepath.Join(os.Getenv("HOME"), ".config", "containers", "auth.json") - } - - if _, err = os.Stat(dockerConfigPath); err != nil { - return nil, err - } + return nil, err } - d.logger.Dimf("REGISTRY_USERNAME/REGISTRY_PASSWORD unset. Trying to obtain Docker credentials from config file: %s", dockerConfigPath) - username, password, err = d.getCredentialsFromDockerConfig(dockerConfigPath, host) + d.logger.Dimf("REGISTRY_USERNAME/REGISTRY_PASSWORD unset. Trying to obtain registry credentials from config file: %s", registryAuthPath) + username, password, err = d.getCredentialsFromDockerConfig(registryAuthPath, host) if err != nil { return nil, err } @@ -125,6 +114,25 @@ func (d *DockerAuth) GetAndVerifyCredentials(ctx context.Context, registry strin }, nil } +func (d *DockerAuth) findAuthConfigPath() (string, error) { + authFiles := []string{ + filepath.Join(os.Getenv("HOME"), ".docker", "config.json"), + filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), "containers", "auth.json"), + filepath.Join(os.Getenv("HOME"), ".config", "containers", "auth.json"), + } + for _, path := range authFiles { + _, err := os.Stat(path) + if errors.Is(err, fs.ErrNotExist) { + d.logger.Dimf("%q not found", path) + } else if err != nil { + return "", err + } else { + return path, nil + } + } + return "", errors.New("no registry authentication file found") +} + // getCredentialsFromDockerConfig extracts credentials from existing Docker config // for the given registry host. func (d *DockerAuth) getCredentialsFromDockerConfig(configPath, host string) (string, string, error) { diff --git a/internal/dockerauth/dockerauth_test.go b/internal/dockerauth/dockerauth_test.go index ec623de..70b57bc 100644 --- a/internal/dockerauth/dockerauth_test.go +++ b/internal/dockerauth/dockerauth_test.go @@ -7,6 +7,8 @@ import ( "fmt" "net/http" "net/http/httptest" + "os" + "path/filepath" "strings" "testing" @@ -25,6 +27,41 @@ func TestGetAndVerifyCredentialsFromEnv(t *testing.T) { da := New(log) da.skipCredVerification = true // Skip verification in tests + testGetAndVerifyCredentials(t, da) +} + +func TestGetAndVerifyCredentialsFromAuthFile(t *testing.T) { + tests := []struct { + name string + mockType uint32 + }{ + { + name: "docker auth", + mockType: DOCKER_MOCK_AUTH, + }, { + name: "podman auth", + mockType: PODMAN_MOCK_AUTH, + }, { + name: "podman XDG auth", + mockType: PODMAN_XDG_MOCK_AUTH, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setupMockAuthEnvironment(t) + createMockAuthFile(t, tt.mockType) + + log := logger.New() + da := New(log) + da.skipCredVerification = true // Skip verification in tests + + testGetAndVerifyCredentials(t, da) + }) + } +} + +func testGetAndVerifyCredentials(t *testing.T, da *DockerAuth) { creds, err := da.GetAndVerifyCredentials(t.Context(), constants.DefaultRegistry) if err != nil { t.Fatalf("GetAndVerifyCredentials failed: %v", err) @@ -89,8 +126,8 @@ func TestGetAndVerifyCredentialsNoCredentials(t *testing.T) { t.Setenv("REGISTRY_USERNAME", "") t.Setenv("REGISTRY_PASSWORD", "") - // Use a temporary home directory to simulate missing credentials. - t.Setenv("HOME", t.TempDir()) + // Use temporary directories to simulate missing credentials. + setupMockAuthEnvironment(t) log := logger.New() da := New(log) @@ -173,6 +210,76 @@ func TestRepositoryRequiresAuth(t *testing.T) { } } +func TestFindAuthConfigPath(t *testing.T) { + tests := []struct { + name string + mockAuthTypes []uint32 + expectType uint32 + expectErr bool + }{ + { + "empty auth types", + []uint32{}, + 0, + true, + }, + { + "docker auth", + []uint32{DOCKER_MOCK_AUTH}, + DOCKER_MOCK_AUTH, + false, + }, + { + "podman auth", + []uint32{PODMAN_MOCK_AUTH}, + PODMAN_MOCK_AUTH, + false, + }, + { + "podman XDG auth", + []uint32{PODMAN_XDG_MOCK_AUTH}, + PODMAN_XDG_MOCK_AUTH, + false, + }, + { + "docker auth takes precedence", + []uint32{DOCKER_MOCK_AUTH, PODMAN_MOCK_AUTH, PODMAN_XDG_MOCK_AUTH}, + DOCKER_MOCK_AUTH, + false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setupMockAuthEnvironment(t) + + for _, mockType := range tt.mockAuthTypes { + createMockAuthFile(t, mockType) + } + log := logger.New() + da := New(log) + + authFile, err := da.findAuthConfigPath() + if tt.expectErr { + assert.Error(t, err) + return + } else { + assert.NoError(t, err) + var expectedPath string + switch tt.expectType { + case DOCKER_MOCK_AUTH: + expectedPath = filepath.Join(os.Getenv("HOME"), ".docker", "config.json") + case PODMAN_MOCK_AUTH: + expectedPath = filepath.Join(os.Getenv("HOME"), ".config", "containers", "auth.json") + case PODMAN_XDG_MOCK_AUTH: + expectedPath = filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), "containers", "auth.json") + } + assert.Equal(t, authFile, expectedPath) + } + }) + } +} + // newFakeRegistry starts an httptest server that simulates an OCI registry's // authentication and tags-list endpoints. func newFakeRegistry(t *testing.T, challengeAuth bool, tokenStatus, tagsListStatus int) (string, func()) { @@ -204,3 +311,56 @@ func newFakeRegistry(t *testing.T, challengeAuth bool, tokenStatus, tagsListStat registryAddr = strings.TrimPrefix(server.URL, "http://") return registryAddr, server.Close } + +func setupMockAuthEnvironment(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + t.Setenv("XDG_RUNTIME_DIR", t.TempDir()) +} + +const ( + _ uint32 = iota + DOCKER_MOCK_AUTH + PODMAN_MOCK_AUTH + PODMAN_XDG_MOCK_AUTH +) + +func createMockAuthFile(t *testing.T, mockType uint32) { + var authDir string + var authFile string + switch mockType { + case DOCKER_MOCK_AUTH: + authDir = filepath.Join(os.Getenv("HOME"), ".docker") + authFile = filepath.Join(authDir, "config.json") + case PODMAN_MOCK_AUTH: + authDir = filepath.Join(os.Getenv("HOME"), ".config", "containers") + authFile = filepath.Join(authDir, "auth.json") + case PODMAN_XDG_MOCK_AUTH: + authDir = filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), "containers") + authFile = filepath.Join(authDir, "auth.json") + } + + err := os.MkdirAll(authDir, 0755) + if err != nil { + t.Fatalf("Auth directory creation failed: %s", err) + } + + f, err := os.OpenFile(authFile, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + t.Fatalf("Auth file creation failed: %s", err) + } + defer f.Close() + + mockCredentials := base64.StdEncoding.EncodeToString([]byte("user:pass")) + mockAuth := fmt.Sprintf(`{ + "auths": { + "quay.io": { + "auth": %q + } + } + }`, mockCredentials) + + _, err = f.WriteString(mockAuth) + if err != nil { + t.Fatalf("Writing credentials failed: %s", err) + } +} From 1a1a9442212e61a5d96bf598960a918415b73179 Mon Sep 17 00:00:00 2001 From: Mauro Ezequiel Moltrasio Date: Thu, 1 Oct 2026 12:52:17 +0200 Subject: [PATCH 3/4] Skip XDG path when var is unset --- internal/dockerauth/dockerauth.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/dockerauth/dockerauth.go b/internal/dockerauth/dockerauth.go index 41569ea..a994d8e 100644 --- a/internal/dockerauth/dockerauth.go +++ b/internal/dockerauth/dockerauth.go @@ -115,11 +115,13 @@ func (d *DockerAuth) GetAndVerifyCredentials(ctx context.Context, registry strin } func (d *DockerAuth) findAuthConfigPath() (string, error) { - authFiles := []string{ - filepath.Join(os.Getenv("HOME"), ".docker", "config.json"), - filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), "containers", "auth.json"), - filepath.Join(os.Getenv("HOME"), ".config", "containers", "auth.json"), + authFiles := []string{filepath.Join(os.Getenv("HOME"), ".docker", "config.json")} + xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR") + if xdgRuntimeDir != "" { + authFiles = append(authFiles, filepath.Join(xdgRuntimeDir, "containers", "auth.json")) } + authFiles = append(authFiles, filepath.Join(os.Getenv("HOME"), ".config", "containers", "auth.json")) + for _, path := range authFiles { _, err := os.Stat(path) if errors.Is(err, fs.ErrNotExist) { From 7a0e997f862abd294f42a8729b61f2c47b15572d Mon Sep 17 00:00:00 2001 From: Mauro Ezequiel Moltrasio Date: Thu, 1 Oct 2026 17:05:45 +0200 Subject: [PATCH 4/4] Make authFiles injectable --- internal/dockerauth/dockerauth.go | 21 +++-- internal/dockerauth/dockerauth_test.go | 122 ++++++++++++------------- 2 files changed, 72 insertions(+), 71 deletions(-) diff --git a/internal/dockerauth/dockerauth.go b/internal/dockerauth/dockerauth.go index a994d8e..3968f5f 100644 --- a/internal/dockerauth/dockerauth.go +++ b/internal/dockerauth/dockerauth.go @@ -33,6 +33,8 @@ func splitRegistryHost(registry string) (host, path string) { type DockerAuth struct { logger *logger.Logger skipCredVerification bool + + authFiles []string } // DockerConfig represents Docker configuration structure. @@ -61,8 +63,16 @@ type Credentials struct { // New creates a new DockerAuth instance. func New(log *logger.Logger) *DockerAuth { + authFiles := []string{filepath.Join(os.Getenv("HOME"), ".docker", "config.json")} + xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR") + if xdgRuntimeDir != "" { + authFiles = append(authFiles, filepath.Join(xdgRuntimeDir, "containers", "auth.json")) + } + authFiles = append(authFiles, filepath.Join(os.Getenv("HOME"), ".config", "containers", "auth.json")) + return &DockerAuth{ - logger: log, + logger: log, + authFiles: authFiles, } } @@ -115,14 +125,7 @@ func (d *DockerAuth) GetAndVerifyCredentials(ctx context.Context, registry strin } func (d *DockerAuth) findAuthConfigPath() (string, error) { - authFiles := []string{filepath.Join(os.Getenv("HOME"), ".docker", "config.json")} - xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR") - if xdgRuntimeDir != "" { - authFiles = append(authFiles, filepath.Join(xdgRuntimeDir, "containers", "auth.json")) - } - authFiles = append(authFiles, filepath.Join(os.Getenv("HOME"), ".config", "containers", "auth.json")) - - for _, path := range authFiles { + for _, path := range d.authFiles { _, err := os.Stat(path) if errors.Is(err, fs.ErrNotExist) { d.logger.Dimf("%q not found", path) diff --git a/internal/dockerauth/dockerauth_test.go b/internal/dockerauth/dockerauth_test.go index 70b57bc..37d3672 100644 --- a/internal/dockerauth/dockerauth_test.go +++ b/internal/dockerauth/dockerauth_test.go @@ -26,6 +26,7 @@ func TestGetAndVerifyCredentialsFromEnv(t *testing.T) { log := logger.New() da := New(log) da.skipCredVerification = true // Skip verification in tests + da.authFiles = []string{} testGetAndVerifyCredentials(t, da) } @@ -33,28 +34,29 @@ func TestGetAndVerifyCredentialsFromEnv(t *testing.T) { func TestGetAndVerifyCredentialsFromAuthFile(t *testing.T) { tests := []struct { name string - mockType uint32 + authFile string }{ { - name: "docker auth", - mockType: DOCKER_MOCK_AUTH, + name: "docker style auth path", + authFile: filepath.Join(t.TempDir(), ".docker", "config.json"), }, { - name: "podman auth", - mockType: PODMAN_MOCK_AUTH, + name: "podman style auth path", + authFile: filepath.Join(t.TempDir(), ".config", "containers", "auth.json"), }, { - name: "podman XDG auth", - mockType: PODMAN_XDG_MOCK_AUTH, + name: "podman XDG style auth path", + authFile: filepath.Join(t.TempDir(), "containers", "auth.json"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { setupMockAuthEnvironment(t) - createMockAuthFile(t, tt.mockType) + authFile := createMockAuthFile(t, tt.authFile) log := logger.New() da := New(log) da.skipCredVerification = true // Skip verification in tests + da.authFiles = []string{authFile} testGetAndVerifyCredentials(t, da) }) @@ -132,6 +134,7 @@ func TestGetAndVerifyCredentialsNoCredentials(t *testing.T) { log := logger.New() da := New(log) da.skipCredVerification = true // Skip verification in tests + da.authFiles = []string{} _, err := da.GetAndVerifyCredentials(t.Context(), constants.DefaultRegistry) assert.Errorf(t, err, "Expected error when no credentials are available") @@ -212,52 +215,75 @@ func TestRepositoryRequiresAuth(t *testing.T) { func TestFindAuthConfigPath(t *testing.T) { tests := []struct { - name string - mockAuthTypes []uint32 - expectType uint32 - expectErr bool + name string + mockAuthPaths []string + expectAuthFileIndex uint32 + expectErr bool }{ { "empty auth types", - []uint32{}, + []string{}, 0, true, }, { - "docker auth", - []uint32{DOCKER_MOCK_AUTH}, - DOCKER_MOCK_AUTH, + "docker style auth path", + []string{filepath.Join(t.TempDir(), ".docker", "config.json")}, + 0, false, }, { - "podman auth", - []uint32{PODMAN_MOCK_AUTH}, - PODMAN_MOCK_AUTH, + "podman style auth path", + []string{filepath.Join(t.TempDir(), ".config", "containers", "auth.json")}, + 0, false, }, { - "podman XDG auth", - []uint32{PODMAN_XDG_MOCK_AUTH}, - PODMAN_XDG_MOCK_AUTH, + "podman XDG style auth path", + []string{filepath.Join(t.TempDir(), "containers", "auth.json")}, + 0, false, }, { - "docker auth takes precedence", - []uint32{DOCKER_MOCK_AUTH, PODMAN_MOCK_AUTH, PODMAN_XDG_MOCK_AUTH}, - DOCKER_MOCK_AUTH, + "Use first path", + []string{ + filepath.Join(t.TempDir(), ".docker", "config.json"), + filepath.Join(t.TempDir(), ".config", "containers", "auth.json"), + filepath.Join(t.TempDir(), "containers", "auth.json"), + }, + 0, + false, + }, + { + "Use middle path", + []string{ + filepath.Join(t.TempDir(), ".docker", "config.json"), + filepath.Join(t.TempDir(), ".config", "containers", "auth.json"), + filepath.Join(t.TempDir(), "containers", "auth.json"), + }, + 1, + false, + }, { + "Use last path", + []string{ + filepath.Join(t.TempDir(), ".docker", "config.json"), + filepath.Join(t.TempDir(), ".config", "containers", "auth.json"), + filepath.Join(t.TempDir(), "containers", "auth.json"), + }, + 2, false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - setupMockAuthEnvironment(t) - - for _, mockType := range tt.mockAuthTypes { - createMockAuthFile(t, mockType) + if !tt.expectErr { + createMockAuthFile(t, tt.mockAuthPaths[tt.expectAuthFileIndex]) } + log := logger.New() da := New(log) + da.authFiles = tt.mockAuthPaths authFile, err := da.findAuthConfigPath() if tt.expectErr { @@ -265,16 +291,7 @@ func TestFindAuthConfigPath(t *testing.T) { return } else { assert.NoError(t, err) - var expectedPath string - switch tt.expectType { - case DOCKER_MOCK_AUTH: - expectedPath = filepath.Join(os.Getenv("HOME"), ".docker", "config.json") - case PODMAN_MOCK_AUTH: - expectedPath = filepath.Join(os.Getenv("HOME"), ".config", "containers", "auth.json") - case PODMAN_XDG_MOCK_AUTH: - expectedPath = filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), "containers", "auth.json") - } - assert.Equal(t, authFile, expectedPath) + assert.Equal(t, authFile, tt.mockAuthPaths[tt.expectAuthFileIndex]) } }) } @@ -317,29 +334,8 @@ func setupMockAuthEnvironment(t *testing.T) { t.Setenv("XDG_RUNTIME_DIR", t.TempDir()) } -const ( - _ uint32 = iota - DOCKER_MOCK_AUTH - PODMAN_MOCK_AUTH - PODMAN_XDG_MOCK_AUTH -) - -func createMockAuthFile(t *testing.T, mockType uint32) { - var authDir string - var authFile string - switch mockType { - case DOCKER_MOCK_AUTH: - authDir = filepath.Join(os.Getenv("HOME"), ".docker") - authFile = filepath.Join(authDir, "config.json") - case PODMAN_MOCK_AUTH: - authDir = filepath.Join(os.Getenv("HOME"), ".config", "containers") - authFile = filepath.Join(authDir, "auth.json") - case PODMAN_XDG_MOCK_AUTH: - authDir = filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), "containers") - authFile = filepath.Join(authDir, "auth.json") - } - - err := os.MkdirAll(authDir, 0755) +func createMockAuthFile(t *testing.T, authFile string) string { + err := os.MkdirAll(filepath.Dir(authFile), 0755) if err != nil { t.Fatalf("Auth directory creation failed: %s", err) } @@ -363,4 +359,6 @@ func createMockAuthFile(t *testing.T, mockType uint32) { if err != nil { t.Fatalf("Writing credentials failed: %s", err) } + + return authFile }