Skip to content

Commit f2a8bb6

Browse files
authored
fix: search all Podman auth file locations per containers-auth.json(5) spec (#46)
* fix: search all Podman auth file locations per containers-auth.json(5) spec On Linux, Podman writes credentials to $XDG_RUNTIME_DIR/containers/auth.json, but skillctl only checked one path. Now podmanAuthPaths() returns all candidate locations in spec order and credentialStore() chains them as fallbacks. Fixes #45 Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Pavel Anni <panni@redhat.com> * fix: strengthen dedupe test and add debug logging for auth paths Replace tautological dedupe test with one that exercises the actual dedup branch (both XDG vars pointing to same directory). Add slog.Debug for skipped auth files to help users troubleshoot credential issues. Addresses review feedback from PR #46. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Pavel Anni <panni@redhat.com> --------- Signed-off-by: Pavel Anni <panni@redhat.com>
1 parent 92dde21 commit f2a8bb6

2 files changed

Lines changed: 107 additions & 19 deletions

File tree

pkg/oci/credentials_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package oci
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
func TestPodmanAuthPaths(t *testing.T) {
10+
t.Run("returns XDG_RUNTIME_DIR path first on Linux-like env", func(t *testing.T) {
11+
t.Setenv("XDG_RUNTIME_DIR", "/run/user/1000")
12+
t.Setenv("XDG_CONFIG_HOME", "")
13+
14+
paths := podmanAuthPaths()
15+
16+
if len(paths) < 1 {
17+
t.Fatal("expected at least 1 path")
18+
}
19+
want := filepath.Join("/run/user/1000", "containers", "auth.json")
20+
if paths[0] != want {
21+
t.Errorf("paths[0] = %q, want %q", paths[0], want)
22+
}
23+
})
24+
25+
t.Run("includes XDG_CONFIG_HOME path as fallback", func(t *testing.T) {
26+
t.Setenv("XDG_RUNTIME_DIR", "/run/user/1000")
27+
t.Setenv("XDG_CONFIG_HOME", "/home/testuser/.config")
28+
29+
paths := podmanAuthPaths()
30+
31+
if len(paths) < 2 {
32+
t.Fatalf("expected at least 2 paths, got %d", len(paths))
33+
}
34+
want := filepath.Join("/home/testuser/.config", "containers", "auth.json")
35+
if paths[1] != want {
36+
t.Errorf("paths[1] = %q, want %q", paths[1], want)
37+
}
38+
})
39+
40+
t.Run("deduplicates when XDG_RUNTIME_DIR and XDG_CONFIG_HOME match", func(t *testing.T) {
41+
t.Setenv("XDG_RUNTIME_DIR", "/tmp/shared-config")
42+
t.Setenv("XDG_CONFIG_HOME", "/tmp/shared-config")
43+
44+
paths := podmanAuthPaths()
45+
46+
if len(paths) != 1 {
47+
t.Fatalf("expected 1 path, got %d: %v", len(paths), paths)
48+
}
49+
})
50+
51+
t.Run("falls back to home config when no XDG vars set", func(t *testing.T) {
52+
t.Setenv("XDG_RUNTIME_DIR", "")
53+
t.Setenv("XDG_CONFIG_HOME", "")
54+
55+
paths := podmanAuthPaths()
56+
57+
if len(paths) == 0 {
58+
t.Fatal("expected at least 1 path")
59+
}
60+
home, err := os.UserHomeDir()
61+
if err != nil {
62+
t.Skip("cannot determine home dir")
63+
}
64+
want := filepath.Join(home, ".config", "containers", "auth.json")
65+
if paths[0] != want {
66+
t.Errorf("paths[0] = %q, want %q", paths[0], want)
67+
}
68+
})
69+
}

pkg/oci/push.go

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"crypto/tls"
66
"fmt"
7+
"log/slog"
78
"net/http"
89
"os"
910
"path/filepath"
@@ -81,40 +82,58 @@ func newAuthClient(store credentials.Store, skipTLSVerify bool) *auth.Client {
8182
}
8283

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

91-
podmanPath := podmanAuthPath()
92-
if podmanPath == "" {
93-
return dockerStore, nil
94-
}
95-
96-
if _, err := os.Stat(podmanPath); err != nil {
97-
return dockerStore, nil
92+
var fallbacks []credentials.Store
93+
for _, p := range podmanAuthPaths() {
94+
if _, err := os.Stat(p); err != nil {
95+
slog.Debug("podman auth file not found", "path", p)
96+
continue
97+
}
98+
store, err := credentials.NewStore(p, credentials.StoreOptions{})
99+
if err != nil {
100+
slog.Debug("skipping unreadable podman auth file", "path", p, "error", err)
101+
continue
102+
}
103+
fallbacks = append(fallbacks, store)
98104
}
99105

100-
podmanStore, err := credentials.NewStore(podmanPath, credentials.StoreOptions{})
101-
if err != nil {
102-
// Podman config unreadable but Docker config works — fall back gracefully.
106+
if len(fallbacks) == 0 {
103107
return dockerStore, nil
104108
}
105-
106-
return credentials.NewStoreWithFallbacks(dockerStore, podmanStore), nil
109+
return credentials.NewStoreWithFallbacks(dockerStore, fallbacks...), nil
107110
}
108111

109-
func podmanAuthPath() string {
112+
// podmanAuthPaths returns Podman/containers auth file locations in search
113+
// order per the containers-auth.json(5) spec:
114+
// 1. $XDG_RUNTIME_DIR/containers/auth.json (Linux primary)
115+
// 2. $XDG_CONFIG_HOME/containers/auth.json (fallback; defaults to ~/.config)
116+
func podmanAuthPaths() []string {
117+
var paths []string
118+
110119
if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" {
111-
return filepath.Join(xdg, "containers", "auth.json")
120+
paths = append(paths, filepath.Join(xdg, "containers", "auth.json"))
112121
}
113-
home, err := os.UserHomeDir()
114-
if err != nil {
115-
return ""
122+
123+
configHome := os.Getenv("XDG_CONFIG_HOME")
124+
if configHome == "" {
125+
if home, err := os.UserHomeDir(); err == nil {
126+
configHome = filepath.Join(home, ".config")
127+
}
116128
}
117-
return filepath.Join(home, ".config", "containers", "auth.json")
129+
if configHome != "" {
130+
p := filepath.Join(configHome, "containers", "auth.json")
131+
if len(paths) == 0 || paths[0] != p {
132+
paths = append(paths, p)
133+
}
134+
}
135+
136+
return paths
118137
}
119138

120139
// splitRefTag splits an OCI reference into repository and tag/digest.

0 commit comments

Comments
 (0)