Skip to content

Commit

Permalink
Merge pull request #1765 from cgwalters/add-system-path-prep
Browse files Browse the repository at this point in the history
config: use `authPath` struct consistently
  • Loading branch information
rhatdan authored Dec 20, 2022
2 parents d31f2c5 + 7920ffb commit debf9d8
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 56 deletions.
108 changes: 57 additions & 51 deletions pkg/docker/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ type dockerConfigFile struct {
CredHelpers map[string]string `json:"credHelpers,omitempty"`
}

type authPath struct {
path string
legacyFormat bool
}

var (
defaultPerUIDPathFormat = filepath.FromSlash("/run/containers/%d/auth.json")
xdgConfigHomePath = filepath.FromSlash("containers/auth.json")
Expand All @@ -52,6 +47,19 @@ var (
ErrNotSupported = errors.New("not supported")
)

// authPath combines a path to a file with container registry access keys,
// along with expected properties of that path (currently just whether it's)
// legacy format or not.
type authPath struct {
path string
legacyFormat bool
}

// newAuthPathDefault constructs an authPath in non-legacy format.
func newAuthPathDefault(path string) authPath {
return authPath{path: path, legacyFormat: false}
}

// SetCredentials stores the username and password in a location
// appropriate for sys and the users’ configuration.
// A valid key is a repository, a namespace within a registry, or a registry hostname;
Expand Down Expand Up @@ -149,8 +157,8 @@ func GetAllCredentials(sys *types.SystemContext) (map[string]types.DockerAuthCon
// Special-case the built-in helper for auth files.
case sysregistriesv2.AuthenticationFileHelper:
for _, path := range getAuthFilePaths(sys, homedir.Get()) {
// readJSONFile returns an empty map in case the path doesn't exist.
auths, err := readJSONFile(path.path, path.legacyFormat)
// parse returns an empty map in case the path doesn't exist.
auths, err := path.parse()
if err != nil {
return nil, fmt.Errorf("reading JSON file %q: %w", path.path, err)
}
Expand Down Expand Up @@ -208,9 +216,9 @@ func GetAllCredentials(sys *types.SystemContext) (map[string]types.DockerAuthCon
// by tests.
func getAuthFilePaths(sys *types.SystemContext, homeDir string) []authPath {
paths := []authPath{}
pathToAuth, lf, err := getPathToAuth(sys)
pathToAuth, err := getPathToAuth(sys)
if err == nil {
paths = append(paths, authPath{path: pathToAuth, legacyFormat: lf})
paths = append(paths, pathToAuth)
} else {
// Error means that the path set for XDG_RUNTIME_DIR does not exist
// but we don't want to completely fail in the case that the user is pulling a public image
Expand All @@ -222,14 +230,12 @@ func getAuthFilePaths(sys *types.SystemContext, homeDir string) []authPath {
if xdgCfgHome == "" {
xdgCfgHome = filepath.Join(homeDir, ".config")
}
paths = append(paths, authPath{path: filepath.Join(xdgCfgHome, xdgConfigHomePath), legacyFormat: false})
paths = append(paths, newAuthPathDefault(filepath.Join(xdgCfgHome, xdgConfigHomePath)))
if dockerConfig := os.Getenv("DOCKER_CONFIG"); dockerConfig != "" {
paths = append(paths,
authPath{path: filepath.Join(dockerConfig, "config.json"), legacyFormat: false},
)
paths = append(paths, newAuthPathDefault(filepath.Join(dockerConfig, "config.json")))
} else {
paths = append(paths,
authPath{path: filepath.Join(homeDir, dockerHomePath), legacyFormat: false},
newAuthPathDefault(filepath.Join(homeDir, dockerHomePath)),
)
}
paths = append(paths,
Expand Down Expand Up @@ -281,7 +287,7 @@ func getCredentialsWithHomeDir(sys *types.SystemContext, key, homeDir string) (t
// Anonymous function to query credentials from auth files.
getCredentialsFromAuthFiles := func() (types.DockerAuthConfig, string, error) {
for _, path := range getAuthFilePaths(sys, homeDir) {
authConfig, err := findCredentialsInFile(key, registry, path.path, path.legacyFormat)
authConfig, err := findCredentialsInFile(key, registry, path)
if err != nil {
return types.DockerAuthConfig{}, "", err
}
Expand Down Expand Up @@ -502,26 +508,26 @@ func listAuthsFromCredHelper(credHelper string) (map[string]string, error) {

// getPathToAuth gets the path of the auth.json file used for reading and writing credentials
// returns the path, and a bool specifies whether the file is in legacy format
func getPathToAuth(sys *types.SystemContext) (string, bool, error) {
func getPathToAuth(sys *types.SystemContext) (authPath, error) {
return getPathToAuthWithOS(sys, runtime.GOOS)
}

// getPathToAuthWithOS is an internal implementation detail of getPathToAuth,
// it exists only to allow testing it with an artificial runtime.GOOS.
func getPathToAuthWithOS(sys *types.SystemContext, goOS string) (string, bool, error) {
func getPathToAuthWithOS(sys *types.SystemContext, goOS string) (authPath, error) {
if sys != nil {
if sys.AuthFilePath != "" {
return sys.AuthFilePath, false, nil
return newAuthPathDefault(sys.AuthFilePath), nil
}
if sys.LegacyFormatAuthFilePath != "" {
return sys.LegacyFormatAuthFilePath, true, nil
return authPath{path: sys.LegacyFormatAuthFilePath, legacyFormat: true}, nil
}
if sys.RootForImplicitAbsolutePaths != "" {
return filepath.Join(sys.RootForImplicitAbsolutePaths, fmt.Sprintf(defaultPerUIDPathFormat, os.Getuid())), false, nil
return newAuthPathDefault(filepath.Join(sys.RootForImplicitAbsolutePaths, fmt.Sprintf(defaultPerUIDPathFormat, os.Getuid()))), nil
}
}
if goOS == "windows" || goOS == "darwin" {
return filepath.Join(homedir.Get(), nonLinuxAuthFilePath), false, nil
return newAuthPathDefault(filepath.Join(homedir.Get(), nonLinuxAuthFilePath)), nil
}

runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
Expand All @@ -533,20 +539,20 @@ func getPathToAuthWithOS(sys *types.SystemContext, goOS string) (string, bool, e
// This means the user set the XDG_RUNTIME_DIR variable and either forgot to create the directory
// or made a typo while setting the environment variable,
// so return an error referring to $XDG_RUNTIME_DIR instead of xdgRuntimeDirPath inside.
return "", false, fmt.Errorf("%q directory set by $XDG_RUNTIME_DIR does not exist. Either create the directory or unset $XDG_RUNTIME_DIR.: %w", runtimeDir, err)
return authPath{}, fmt.Errorf("%q directory set by $XDG_RUNTIME_DIR does not exist. Either create the directory or unset $XDG_RUNTIME_DIR.: %w", runtimeDir, err)
} // else ignore err and let the caller fail accessing xdgRuntimeDirPath.
return filepath.Join(runtimeDir, xdgRuntimeDirPath), false, nil
return newAuthPathDefault(filepath.Join(runtimeDir, xdgRuntimeDirPath)), nil
}
return fmt.Sprintf(defaultPerUIDPathFormat, os.Getuid()), false, nil
return newAuthPathDefault(fmt.Sprintf(defaultPerUIDPathFormat, os.Getuid())), nil
}

// readJSONFile unmarshals the authentications stored in the auth.json file and returns it
// parse unmarshals the authentications stored in the auth.json file and returns it
// or returns an empty dockerConfigFile data structure if auth.json does not exist
// if the file exists and is empty, readJSONFile returns an error
func readJSONFile(path string, legacyFormat bool) (dockerConfigFile, error) {
// if the file exists and is empty, this function returns an error.
func (path authPath) parse() (dockerConfigFile, error) {
var auths dockerConfigFile

raw, err := os.ReadFile(path)
raw, err := os.ReadFile(path.path)
if err != nil {
if os.IsNotExist(err) {
auths.AuthConfigs = map[string]dockerAuthConfig{}
Expand All @@ -555,15 +561,15 @@ func readJSONFile(path string, legacyFormat bool) (dockerConfigFile, error) {
return dockerConfigFile{}, err
}

if legacyFormat {
if path.legacyFormat {
if err = json.Unmarshal(raw, &auths.AuthConfigs); err != nil {
return dockerConfigFile{}, fmt.Errorf("unmarshaling JSON at %q: %w", path, err)
return dockerConfigFile{}, fmt.Errorf("unmarshaling JSON at %q: %w", path.path, err)
}
return auths, nil
}

if err = json.Unmarshal(raw, &auths); err != nil {
return dockerConfigFile{}, fmt.Errorf("unmarshaling JSON at %q: %w", path, err)
return dockerConfigFile{}, fmt.Errorf("unmarshaling JSON at %q: %w", path.path, err)
}

if auths.AuthConfigs == nil {
Expand All @@ -583,41 +589,41 @@ func readJSONFile(path string, legacyFormat bool) (dockerConfigFile, error) {
// The editor may also return a human-readable description of the updated location; if it is "",
// the file itself is used.
func modifyJSON(sys *types.SystemContext, editor func(auths *dockerConfigFile) (bool, string, error)) (string, error) {
path, legacyFormat, err := getPathToAuth(sys)
path, err := getPathToAuth(sys)
if err != nil {
return "", err
}
if legacyFormat {
return "", fmt.Errorf("writes to %s using legacy format are not supported", path)
if path.legacyFormat {
return "", fmt.Errorf("writes to %s using legacy format are not supported", path.path)
}

dir := filepath.Dir(path)
dir := filepath.Dir(path.path)
if err = os.MkdirAll(dir, 0700); err != nil {
return "", err
}

auths, err := readJSONFile(path, false)
auths, err := path.parse()
if err != nil {
return "", fmt.Errorf("reading JSON file %q: %w", path, err)
return "", fmt.Errorf("reading JSON file %q: %w", path.path, err)
}

updated, description, err := editor(&auths)
if err != nil {
return "", fmt.Errorf("updating %q: %w", path, err)
return "", fmt.Errorf("updating %q: %w", path.path, err)
}
if updated {
newData, err := json.MarshalIndent(auths, "", "\t")
if err != nil {
return "", fmt.Errorf("marshaling JSON %q: %w", path, err)
return "", fmt.Errorf("marshaling JSON %q: %w", path.path, err)
}

if err = ioutils.AtomicWriteFile(path, newData, 0600); err != nil {
return "", fmt.Errorf("writing to file %q: %w", path, err)
if err = ioutils.AtomicWriteFile(path.path, newData, 0600); err != nil {
return "", fmt.Errorf("writing to file %q: %w", path.path, err)
}
}

if description == "" {
description = path
description = path.path
}
return description, nil
}
Expand Down Expand Up @@ -671,25 +677,25 @@ func deleteAuthFromCredHelper(credHelper, registry string) error {

// findCredentialsInFile looks for credentials matching "key"
// (which is "registry" or a namespace in "registry") in "path".
func findCredentialsInFile(key, registry, path string, legacyFormat bool) (types.DockerAuthConfig, error) {
auths, err := readJSONFile(path, legacyFormat)
func findCredentialsInFile(key, registry string, path authPath) (types.DockerAuthConfig, error) {
auths, err := path.parse()
if err != nil {
return types.DockerAuthConfig{}, fmt.Errorf("reading JSON file %q: %w", path, err)
return types.DockerAuthConfig{}, fmt.Errorf("reading JSON file %q: %w", path.path, err)
}

// First try cred helpers. They should always be normalized.
// This intentionally uses "registry", not "key"; we don't support namespaced
// credentials in helpers.
if ch, exists := auths.CredHelpers[registry]; exists {
logrus.Debugf("Looking up in credential helper %s based on credHelpers entry in %s", ch, path)
logrus.Debugf("Looking up in credential helper %s based on credHelpers entry in %s", ch, path.path)
return getAuthFromCredHelper(ch, registry)
}

// Support sub-registry namespaces in auth.
// (This is not a feature of ~/.docker/config.json; we support it even for
// those files as an extension.)
var keys []string
if !legacyFormat {
if !path.legacyFormat {
keys = authKeysForKey(key)
} else {
keys = []string{registry}
Expand All @@ -699,7 +705,7 @@ func findCredentialsInFile(key, registry, path string, legacyFormat bool) (types
// keys we prefer exact matches as well.
for _, key := range keys {
if val, exists := auths.AuthConfigs[key]; exists {
return decodeDockerAuth(path, key, val)
return decodeDockerAuth(path.path, key, val)
}
}

Expand All @@ -713,14 +719,14 @@ func findCredentialsInFile(key, registry, path string, legacyFormat bool) (types
// so account for that as well.
registry = normalizeRegistry(registry)
for k, v := range auths.AuthConfigs {
if normalizeAuthFileKey(k, legacyFormat) == registry {
return decodeDockerAuth(path, k, v)
if normalizeAuthFileKey(k, path.legacyFormat) == registry {
return decodeDockerAuth(path.path, k, v)
}
}

// Only log this if we found nothing; getCredentialsWithHomeDir logs the
// source of found data.
logrus.Debugf("No credentials matching %s found in %s", key, path)
logrus.Debugf("No credentials matching %s found in %s", key, path.path)
return types.DockerAuthConfig{}, nil
}

Expand Down
9 changes: 4 additions & 5 deletions pkg/docker/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,12 @@ func TestGetPathToAuth(t *testing.T) {
if c.xrd == "" {
os.Unsetenv("XDG_RUNTIME_DIR")
}
res, lf, err := getPathToAuthWithOS(c.sys, c.os)
res, err := getPathToAuthWithOS(c.sys, c.os)
if c.expected == "" {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, c.expected, res)
assert.Equal(t, c.legacyFormat, lf)
assert.Equal(t, authPath{path: c.expected, legacyFormat: c.legacyFormat}, res)
}
})
}
Expand Down Expand Up @@ -652,7 +651,7 @@ func TestSetCredentials(t *testing.T) {
}

// Read the resulting file and verify it contains the expected keys
auth, err := readJSONFile(tmpFile.Name(), false)
auth, err := newAuthPathDefault(tmpFile.Name()).parse()
require.NoError(t, err)
assert.Len(t, auth.AuthConfigs, len(writtenCredentials))
// auth.AuthConfigs and writtenCredentials are both maps, i.e. their keys are unique;
Expand Down Expand Up @@ -772,7 +771,7 @@ func TestRemoveAuthentication(t *testing.T) {
}
}

auth, err := readJSONFile(tmpFile.Name(), false)
auth, err := newAuthPathDefault(tmpFile.Name()).parse()
require.NoError(t, err)

tc.assert(auth)
Expand Down

0 comments on commit debf9d8

Please sign in to comment.