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
69 changes: 69 additions & 0 deletions pkg/oci/credentials_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package oci

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

func TestPodmanAuthPaths(t *testing.T) {
t.Run("returns XDG_RUNTIME_DIR path first on Linux-like env", func(t *testing.T) {
t.Setenv("XDG_RUNTIME_DIR", "/run/user/1000")
t.Setenv("XDG_CONFIG_HOME", "")

paths := podmanAuthPaths()

if len(paths) < 1 {
t.Fatal("expected at least 1 path")
}
want := filepath.Join("/run/user/1000", "containers", "auth.json")
if paths[0] != want {
t.Errorf("paths[0] = %q, want %q", paths[0], want)
}
})

t.Run("includes XDG_CONFIG_HOME path as fallback", func(t *testing.T) {
t.Setenv("XDG_RUNTIME_DIR", "/run/user/1000")
t.Setenv("XDG_CONFIG_HOME", "/home/testuser/.config")

paths := podmanAuthPaths()

if len(paths) < 2 {
t.Fatalf("expected at least 2 paths, got %d", len(paths))
}
want := filepath.Join("/home/testuser/.config", "containers", "auth.json")
if paths[1] != want {
t.Errorf("paths[1] = %q, want %q", paths[1], want)
}
})

t.Run("deduplicates when XDG_RUNTIME_DIR and XDG_CONFIG_HOME match", func(t *testing.T) {
t.Setenv("XDG_RUNTIME_DIR", "/tmp/shared-config")
t.Setenv("XDG_CONFIG_HOME", "/tmp/shared-config")

paths := podmanAuthPaths()

if len(paths) != 1 {
t.Fatalf("expected 1 path, got %d: %v", len(paths), paths)
}
})

t.Run("falls back to home config when no XDG vars set", func(t *testing.T) {
t.Setenv("XDG_RUNTIME_DIR", "")
t.Setenv("XDG_CONFIG_HOME", "")

paths := podmanAuthPaths()

if len(paths) == 0 {
t.Fatal("expected at least 1 path")
}
home, err := os.UserHomeDir()
if err != nil {
t.Skip("cannot determine home dir")
}
want := filepath.Join(home, ".config", "containers", "auth.json")
if paths[0] != want {
t.Errorf("paths[0] = %q, want %q", paths[0], want)
}
})
}
57 changes: 38 additions & 19 deletions pkg/oci/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"crypto/tls"
"fmt"
"log/slog"
"net/http"
"os"
"path/filepath"
Expand Down Expand Up @@ -81,40 +82,58 @@ func newAuthClient(store credentials.Store, skipTLSVerify bool) *auth.Client {
}

// credentialStore returns a credential store that checks Docker config first,
// then falls back to Podman's auth.json.
// then falls back to Podman auth files found via podmanAuthPaths.
func credentialStore() (credentials.Store, error) {
dockerStore, err := credentials.NewStoreFromDocker(credentials.StoreOptions{})
if err != nil {
return nil, err
}

podmanPath := podmanAuthPath()
if podmanPath == "" {
return dockerStore, nil
}

if _, err := os.Stat(podmanPath); err != nil {
return dockerStore, nil
var fallbacks []credentials.Store
for _, p := range podmanAuthPaths() {
if _, err := os.Stat(p); err != nil {
slog.Debug("podman auth file not found", "path", p)
continue
}
store, err := credentials.NewStore(p, credentials.StoreOptions{})
if err != nil {
slog.Debug("skipping unreadable podman auth file", "path", p, "error", err)
continue
}
fallbacks = append(fallbacks, store)
}

podmanStore, err := credentials.NewStore(podmanPath, credentials.StoreOptions{})
if err != nil {
// Podman config unreadable but Docker config works — fall back gracefully.
if len(fallbacks) == 0 {
return dockerStore, nil
}

return credentials.NewStoreWithFallbacks(dockerStore, podmanStore), nil
return credentials.NewStoreWithFallbacks(dockerStore, fallbacks...), nil
}

func podmanAuthPath() string {
// podmanAuthPaths returns Podman/containers auth file locations in search
// order per the containers-auth.json(5) spec:
// 1. $XDG_RUNTIME_DIR/containers/auth.json (Linux primary)
// 2. $XDG_CONFIG_HOME/containers/auth.json (fallback; defaults to ~/.config)
func podmanAuthPaths() []string {
var paths []string

if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" {
return filepath.Join(xdg, "containers", "auth.json")
paths = append(paths, filepath.Join(xdg, "containers", "auth.json"))
}
home, err := os.UserHomeDir()
if err != nil {
return ""

configHome := os.Getenv("XDG_CONFIG_HOME")
if configHome == "" {
if home, err := os.UserHomeDir(); err == nil {
configHome = filepath.Join(home, ".config")
}
}
return filepath.Join(home, ".config", "containers", "auth.json")
if configHome != "" {
p := filepath.Join(configHome, "containers", "auth.json")
if len(paths) == 0 || paths[0] != p {
paths = append(paths, p)
}
}

return paths
}

// splitRefTag splits an OCI reference into repository and tag/digest.
Expand Down
Loading