Skip to content

Commit 1cb7e13

Browse files
authored
authn: Add NewConfigKeychain to load a config from explicit path (google#1603)
* authn: Add NewConfigKeychain to load a config from explicit path * more better godocs * use sync.Once instead of mutex
1 parent 348cd86 commit 1cb7e13

File tree

2 files changed

+119
-31
lines changed

2 files changed

+119
-31
lines changed

pkg/authn/keychain.go

+82-27
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,27 @@ type Keychain interface {
4848
// defaultKeychain implements Keychain with the semantics of the standard Docker
4949
// credential keychain.
5050
type defaultKeychain struct {
51-
mu sync.Mutex
51+
once sync.Once
52+
cfg types.AuthConfig
53+
54+
configFilePath string
5255
}
5356

5457
var (
55-
// DefaultKeychain implements Keychain by interpreting the docker config file.
58+
// DefaultKeychain implements Keychain by interpreting the Docker config file.
59+
// This matches the behavior of tools like `docker` and `podman`.
60+
//
61+
// This keychain looks for credentials configured in a few places, in order:
62+
//
63+
// 1. $HOME/.docker/config.json
64+
// 2. $DOCKER_CONFIG/config.json
65+
// 3. $XDG_RUNTIME_DIR/containers/auth.json (for compatibility with Podman)
66+
//
67+
// If a config file is found and can be parsed, Resolve will return credentials
68+
// configured by the file for the given registry.
69+
//
70+
// If no config file is found, Resolve returns Anonymous.
71+
// If a config file is found but can't be parsed, Resolve returns an error.
5672
DefaultKeychain = RefreshingKeychain(&defaultKeychain{}, 5*time.Minute)
5773
)
5874

@@ -62,11 +78,16 @@ const (
6278
DefaultAuthKey = "https://" + name.DefaultRegistry + "/v1/"
6379
)
6480

65-
// Resolve implements Keychain.
66-
func (dk *defaultKeychain) Resolve(target Resource) (Authenticator, error) {
67-
dk.mu.Lock()
68-
defer dk.mu.Unlock()
81+
// NewConfigKeychain implements Keychain by interpreting the Docker config file
82+
// at the specified file path.
83+
//
84+
// It acts like DefaultKeychain except that the exact path of the file can be specified,
85+
// instead of being dependent on environment variables and conventional file names.
86+
func NewConfigKeychain(filename string) Keychain {
87+
return &defaultKeychain{configFilePath: filename}
88+
}
6989

90+
func getDefaultConfigFile() (*configfile.ConfigFile, error) {
7091
// Podman users may have their container registry auth configured in a
7192
// different location, that Docker packages aren't aware of.
7293
// If the Docker config file isn't found, we'll fallback to look where
@@ -99,39 +120,73 @@ func (dk *defaultKeychain) Resolve(target Resource) (Authenticator, error) {
99120
} else {
100121
f, err := os.Open(filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), "containers/auth.json"))
101122
if err != nil {
102-
return Anonymous, nil
123+
return nil, nil
103124
}
104125
defer f.Close()
105126
cf, err = config.LoadFromReader(f)
106127
if err != nil {
107128
return nil, err
108129
}
109130
}
131+
return cf, nil
132+
}
110133

111-
// See:
112-
// https://github.com/google/ko/issues/90
113-
// https://github.com/moby/moby/blob/fc01c2b481097a6057bec3cd1ab2d7b4488c50c4/registry/config.go#L397-L404
114-
var cfg, empty types.AuthConfig
115-
for _, key := range []string{
116-
target.String(),
117-
target.RegistryStr(),
118-
} {
119-
if key == name.DefaultRegistry {
120-
key = DefaultAuthKey
134+
func (dk *defaultKeychain) Resolve(target Resource) (Authenticator, error) {
135+
var err error
136+
var empty types.AuthConfig
137+
dk.once.Do(func() {
138+
var cf *configfile.ConfigFile
139+
if dk.configFilePath == "" {
140+
cf, err = getDefaultConfigFile()
141+
if err != nil {
142+
return
143+
}
144+
if cf == nil {
145+
dk.cfg = empty
146+
return
147+
}
148+
} else {
149+
var f *os.File
150+
f, err = os.Open(dk.configFilePath)
151+
if err != nil {
152+
return
153+
}
154+
defer f.Close()
155+
cf, err = config.LoadFromReader(f)
156+
if err != nil {
157+
return
158+
}
121159
}
122160

123-
cfg, err = cf.GetAuthConfig(key)
124-
if err != nil {
125-
return nil, err
126-
}
127-
// cf.GetAuthConfig automatically sets the ServerAddress attribute. Since
128-
// we don't make use of it, clear the value for a proper "is-empty" test.
129-
// See: https://github.com/google/go-containerregistry/issues/1510
130-
cfg.ServerAddress = ""
131-
if cfg != empty {
132-
break
161+
// See:
162+
// https://github.com/google/ko/issues/90
163+
// https://github.com/moby/moby/blob/fc01c2b481097a6057bec3cd1ab2d7b4488c50c4/registry/config.go#L397-L404
164+
for _, key := range []string{
165+
target.String(),
166+
target.RegistryStr(),
167+
} {
168+
if key == name.DefaultRegistry {
169+
key = DefaultAuthKey
170+
}
171+
172+
dk.cfg, err = cf.GetAuthConfig(key)
173+
if err != nil {
174+
return
175+
}
176+
// cf.GetAuthConfig automatically sets the ServerAddress attribute. Since
177+
// we don't make use of it, clear the value for a proper "is-empty" test.
178+
// See: https://github.com/google/go-containerregistry/issues/1510
179+
dk.cfg.ServerAddress = ""
180+
if dk.cfg != empty {
181+
break
182+
}
133183
}
184+
})
185+
if err != nil {
186+
return nil, err
134187
}
188+
189+
cfg := dk.cfg
135190
if cfg == empty {
136191
return Anonymous, nil
137192
}

pkg/authn/keychain_test.go

+37-4
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func TestPodmanConfig(t *testing.T) {
113113
// At first, $DOCKER_CONFIG is unset and $HOME/.docker/config.json isn't
114114
// found, but Podman auth is configured. This should return Podman's
115115
// auth.
116-
auth, err := DefaultKeychain.Resolve(testRegistry)
116+
auth, err := NewConfigKeychain("").Resolve(testRegistry)
117117
if err != nil {
118118
t.Fatalf("Resolve() = %v", err)
119119
}
@@ -140,7 +140,7 @@ func TestPodmanConfig(t *testing.T) {
140140
t.Fatalf("write %q: %v", cfg, err)
141141
}
142142
defer func() { os.Remove(cfg) }()
143-
auth, err = DefaultKeychain.Resolve(testRegistry)
143+
auth, err = NewConfigKeychain("").Resolve(testRegistry)
144144
if err != nil {
145145
t.Fatalf("Resolve() = %v", err)
146146
}
@@ -164,7 +164,7 @@ func TestPodmanConfig(t *testing.T) {
164164
cd := setupConfigFile(t, content)
165165
defer os.RemoveAll(filepath.Dir(cd))
166166

167-
auth, err = DefaultKeychain.Resolve(testRegistry)
167+
auth, err = NewConfigKeychain("").Resolve(testRegistry)
168168
if err != nil {
169169
t.Fatalf("Resolve() = %v", err)
170170
}
@@ -181,6 +181,39 @@ func TestPodmanConfig(t *testing.T) {
181181
}
182182
}
183183

184+
func TestAuthConfigPath(t *testing.T) {
185+
tmpdir := os.Getenv("TEST_TMPDIR")
186+
if tmpdir == "" {
187+
tmpdir = t.TempDir()
188+
}
189+
fresh++
190+
p := filepath.Join(tmpdir, fmt.Sprintf("%d", fresh))
191+
if err := os.MkdirAll(filepath.Join(p, "custom"), 0777); err != nil {
192+
t.Fatalf("mkdir %s/custom: %v", p, err)
193+
}
194+
cfg := filepath.Join(p, "cfg.xml")
195+
content := fmt.Sprintf(`{"auths": {"test.io": {"auth": %q}}}`, encode("foo", "bar"))
196+
if err := os.WriteFile(cfg, []byte(content), 0600); err != nil {
197+
t.Fatalf("write %q: %v", cfg, err)
198+
}
199+
200+
auth, err := NewConfigKeychain(cfg).Resolve(testRegistry)
201+
if err != nil {
202+
t.Fatalf("Resolve() = %v", err)
203+
}
204+
got, err := auth.Authorization()
205+
if err != nil {
206+
t.Fatal(err)
207+
}
208+
want := &AuthConfig{
209+
Username: "foo",
210+
Password: "bar",
211+
}
212+
if !reflect.DeepEqual(got, want) {
213+
t.Errorf("got %+v, want %+v", got, want)
214+
}
215+
}
216+
184217
func encode(user, pass string) string {
185218
delimited := fmt.Sprintf("%s:%s", user, pass)
186219
return base64.StdEncoding.EncodeToString([]byte(delimited))
@@ -277,7 +310,7 @@ func TestVariousPaths(t *testing.T) {
277310
// For some reason, these tempdirs don't get cleaned up.
278311
defer os.RemoveAll(filepath.Dir(cd))
279312

280-
auth, err := DefaultKeychain.Resolve(test.target)
313+
auth, err := NewConfigKeychain("").Resolve(test.target)
281314
if test.wantErr {
282315
if err == nil {
283316
t.Fatal("wanted err, got nil")

0 commit comments

Comments
 (0)