Skip to content
Merged
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
44 changes: 34 additions & 10 deletions internal/dockerauth/dockerauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"net/http"
"os"
"os/exec"
Expand All @@ -32,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.
Expand Down Expand Up @@ -60,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,
}
}

Expand All @@ -85,15 +96,14 @@ 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 {
return nil, err
}
registryAuthPath, err := d.findAuthConfigPath()
if err != nil {
return nil, err
}
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)
Comment thread
Molter73 marked this conversation as resolved.
if err != nil {
return nil, err
}
}
Comment thread
iamkirkbater marked this conversation as resolved.

Expand All @@ -114,6 +124,20 @@ func (d *DockerAuth) GetAndVerifyCredentials(ctx context.Context, registry strin
}, nil
}

func (d *DockerAuth) findAuthConfigPath() (string, error) {
for _, path := range d.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) {
Expand Down
162 changes: 160 additions & 2 deletions internal/dockerauth/dockerauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"

Expand All @@ -24,7 +26,44 @@ func TestGetAndVerifyCredentialsFromEnv(t *testing.T) {
log := logger.New()
da := New(log)
da.skipCredVerification = true // Skip verification in tests
da.authFiles = []string{}

testGetAndVerifyCredentials(t, da)
}

func TestGetAndVerifyCredentialsFromAuthFile(t *testing.T) {
tests := []struct {
name string
authFile string
}{
{
name: "docker style auth path",
authFile: filepath.Join(t.TempDir(), ".docker", "config.json"),
}, {
name: "podman style auth path",
authFile: filepath.Join(t.TempDir(), ".config", "containers", "auth.json"),
}, {
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)
Comment thread
Molter73 marked this conversation as resolved.
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)
})
}
}

func testGetAndVerifyCredentials(t *testing.T, da *DockerAuth) {
creds, err := da.GetAndVerifyCredentials(t.Context(), constants.DefaultRegistry)
if err != nil {
t.Fatalf("GetAndVerifyCredentials failed: %v", err)
Expand Down Expand Up @@ -89,12 +128,13 @@ 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)
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")
Expand Down Expand Up @@ -173,6 +213,90 @@ func TestRepositoryRequiresAuth(t *testing.T) {
}
}

func TestFindAuthConfigPath(t *testing.T) {
tests := []struct {
name string
mockAuthPaths []string
expectAuthFileIndex uint32
expectErr bool
}{
{
"empty auth types",
[]string{},
0,
true,
},
{
"docker style auth path",
[]string{filepath.Join(t.TempDir(), ".docker", "config.json")},
0,
false,
},
{
"podman style auth path",
[]string{filepath.Join(t.TempDir(), ".config", "containers", "auth.json")},
0,
false,
},
{
"podman XDG style auth path",
[]string{filepath.Join(t.TempDir(), "containers", "auth.json")},
0,
false,
},
{
"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) {
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 {
assert.Error(t, err)
return
} else {
assert.NoError(t, err)
assert.Equal(t, authFile, tt.mockAuthPaths[tt.expectAuthFileIndex])
}
})
}
}

// 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()) {
Expand Down Expand Up @@ -204,3 +328,37 @@ 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())
}

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

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

return authFile
}
Loading