Skip to content

Commit ccd9ef6

Browse files
authored
Use default keychain to resolve OCI credentials (#8274)
The ECR credential helper stopped working for one of our on-prem users after they switched from podman to OCI. This is because `oci.Resolve()` doesn't respect credential helpers. This PR enables the default keychain in `oci.Resolve()` (behind a flag) so that credential helpers will work. The default keychain respects commonly used container auth config files, including `.docker/config.json` and podman's `$XDG_RUNTIME_DIR/containers/auth.json`. The customer was specifically trying to use `~/.config/containers/auth.json` which doesn't quite work after this PR, but will work if we either patch google/go-containerregistry#2052 or if the customer switches to `~/.docker/config.json` instead (that file is generally the least common denominator and is supported by docker, podman, and now oci after this PR). The plan is to either enable this new flag by default (but make sure it's disabled for our cloud executors), or just enable it in the helm charts.
1 parent 85a7d04 commit ccd9ef6

File tree

3 files changed

+101
-7
lines changed

3 files changed

+101
-7
lines changed

enterprise/server/util/oci/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ go_test(
3232
":oci",
3333
"//enterprise/server/remote_execution/platform",
3434
"//proto:registry_go_proto",
35+
"//server/testutil/testenviron",
36+
"//server/testutil/testfs",
3537
"//server/testutil/testregistry",
3638
"//server/util/proto",
3739
"//server/util/status",

enterprise/server/util/oci/oci.go

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ import (
2424
)
2525

2626
var (
27-
registries = flag.Slice("executor.container_registries", []Registry{}, "")
28-
mirrors = flag.Slice("executor.container_registry_mirrors", []MirrorConfig{}, "")
27+
registries = flag.Slice("executor.container_registries", []Registry{}, "")
28+
mirrors = flag.Slice("executor.container_registry_mirrors", []MirrorConfig{}, "")
29+
defaultKeychainEnabled = flag.Bool("executor.container_registry_default_keychain_enabled", false, "Enable the default container registry keychain, respecting both docker configs and podman configs.")
2930
)
3031

3132
type MirrorConfig struct {
@@ -76,7 +77,7 @@ func CredentialsFromProto(creds *rgpb.Credentials) (Credentials, error) {
7677
// Extracts the container registry Credentials from the provided platform
7778
// properties, falling back to credentials specified in
7879
// --executor.container_registries if the platform properties credentials are
79-
// absent.
80+
// absent, then falling back to the default keychain (docker/podman config JSON)
8081
func CredentialsFromProperties(props *platform.Properties) (Credentials, error) {
8182
imageRef := props.ContainerImage
8283
if imageRef == "" {
@@ -92,10 +93,6 @@ func CredentialsFromProperties(props *platform.Properties) (Credentials, error)
9293

9394
// If no credentials were provided, fallback to any specified by
9495
// --executor.container_registries.
95-
if len(*registries) == 0 {
96-
return Credentials{}, nil
97-
}
98-
9996
ref, err := reference.ParseNormalizedNamed(imageRef)
10097
if err != nil {
10198
log.Debugf("Failed to parse image ref %q: %s", imageRef, err)
@@ -113,9 +110,44 @@ func CredentialsFromProperties(props *platform.Properties) (Credentials, error)
113110
}
114111
}
115112

113+
// No matching registries were found in the executor config. Fall back to
114+
// the default keychain.
115+
if *defaultKeychainEnabled {
116+
return resolveWithDefaultKeychain(ref)
117+
}
118+
116119
return Credentials{}, nil
117120
}
118121

122+
// Reads the auth configuration from a set of commonly supported config file
123+
// locations such as ~/.docker/config.json or
124+
// $XDG_RUNTIME_DIR/containers/auth.json, and returns any configured
125+
// credentials, possibly by invoking a credential helper if applicable.
126+
func resolveWithDefaultKeychain(ref reference.Named) (Credentials, error) {
127+
// TODO: parse the errors below and if they're 403/401 errors then return
128+
// Unauthenticated/PermissionDenied
129+
ctrRef, err := ctrname.ParseReference(ref.String())
130+
if err != nil {
131+
log.Debugf("Failed to parse image ref %q: %s", ref.String(), err)
132+
return Credentials{}, nil
133+
}
134+
authenticator, err := authn.DefaultKeychain.Resolve(ctrRef.Context())
135+
if err != nil {
136+
return Credentials{}, status.UnavailableErrorf("resolve default keychain: %s", err)
137+
}
138+
authConfig, err := authenticator.Authorization()
139+
if err != nil {
140+
return Credentials{}, status.UnavailableErrorf("authorize via default keychain: %s", err)
141+
}
142+
if authConfig == nil {
143+
return Credentials{}, nil
144+
}
145+
return Credentials{
146+
Username: authConfig.Username,
147+
Password: authConfig.Password,
148+
}, nil
149+
}
150+
119151
func credentials(username, password string) (Credentials, error) {
120152
if username == "" && password != "" {
121153
return Credentials{}, status.InvalidArgumentError(

enterprise/server/util/oci/oci_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ import (
66
"context"
77
"io"
88
"net/http"
9+
"os"
10+
"path/filepath"
911
"regexp"
1012
"runtime"
1113
"testing"
1214

1315
"github.com/buildbuddy-io/buildbuddy/enterprise/server/remote_execution/platform"
1416
"github.com/buildbuddy-io/buildbuddy/enterprise/server/util/oci"
17+
"github.com/buildbuddy-io/buildbuddy/server/testutil/testenviron"
18+
"github.com/buildbuddy-io/buildbuddy/server/testutil/testfs"
1519
"github.com/buildbuddy-io/buildbuddy/server/testutil/testregistry"
1620
"github.com/buildbuddy-io/buildbuddy/server/util/proto"
1721
"github.com/buildbuddy-io/buildbuddy/server/util/status"
@@ -135,6 +139,62 @@ func TestCredentialsFromProperties(t *testing.T) {
135139
}
136140
}
137141

142+
func TestCredentialsFromProperties_DefaultKeychain(t *testing.T) {
143+
tmp := testfs.MakeTempDir(t)
144+
testfs.WriteAllFileContents(t, tmp, map[string]string{
145+
// Write a credential helper that always returns the creds
146+
// test-user:test-pass
147+
"bin/docker-credential-test": `#!/usr/bin/env sh
148+
if [ "$1" = "get" ]; then
149+
echo '{"ServerURL":"","Username":"test-user","Secret":"test-pass"}'
150+
exit 0
151+
else
152+
echo "Not implemented"
153+
exit 1
154+
fi
155+
`,
156+
// Set up docker-credential-test as the credential helper for
157+
// "testregistry.io". Note the "docker-credential-" prefix is omitted.
158+
".docker/config.json": `{"credHelpers": {"testregistry.io": "test"}}`,
159+
})
160+
testfs.MakeExecutable(t, tmp, "bin/docker-credential-test")
161+
// The default keychain reads config.json from the path specified in the
162+
// DOCKER_CONFIG environment variable, if set. Point that to our directory.
163+
testenviron.Set(t, "DOCKER_CONFIG", filepath.Join(tmp, ".docker"))
164+
// Add our directory to PATH so the default keychain can find our credential
165+
// helper binary.
166+
testenviron.Set(t, "PATH", filepath.Join(tmp, "bin")+":"+os.Getenv("PATH"))
167+
168+
for _, test := range []struct {
169+
Name string
170+
DefaultKeychainEnabled bool
171+
ExpectedCredentials oci.Credentials
172+
}{
173+
{
174+
Name: "invokes credential helper if default keychain enabled",
175+
DefaultKeychainEnabled: true,
176+
ExpectedCredentials: oci.Credentials{Username: "test-user", Password: "test-pass"},
177+
},
178+
{
179+
Name: "returns empty credentials if default keychain disabled",
180+
DefaultKeychainEnabled: false,
181+
ExpectedCredentials: oci.Credentials{},
182+
},
183+
} {
184+
t.Run(test.Name, func(t *testing.T) {
185+
flags.Set(t, "executor.container_registry_default_keychain_enabled", test.DefaultKeychainEnabled)
186+
187+
creds, err := oci.CredentialsFromProperties(&platform.Properties{
188+
ContainerImage: "testregistry.io/test-repo/test-img",
189+
})
190+
require.NoError(t, err)
191+
192+
require.Equal(t, test.ExpectedCredentials, creds)
193+
})
194+
}
195+
196+
}
197+
138198
func creds(username, password string) oci.Credentials {
139199
return oci.Credentials{Username: username, Password: password}
140200
}

0 commit comments

Comments
 (0)