New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
pkg/docker: lock auth.json before reading/writing #1506
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,7 @@ import ( | |
"github.com/containers/image/v5/pkg/sysregistriesv2" | ||
"github.com/containers/image/v5/types" | ||
"github.com/containers/storage/pkg/homedir" | ||
"github.com/containers/storage/pkg/lockfile" | ||
helperclient "github.com/docker/docker-credential-helpers/client" | ||
"github.com/docker/docker-credential-helpers/credentials" | ||
"github.com/hashicorp/go-multierror" | ||
|
@@ -541,6 +542,29 @@ func getPathToAuthWithOS(sys *types.SystemContext, goOS string) (string, bool, e | |
// 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 _, err := os.Stat(path); err != nil { | ||
if os.IsNotExist(err) { | ||
var auths dockerConfigFile | ||
auths.AuthConfigs = map[string]dockerAuthConfig{} | ||
return auths, nil | ||
} | ||
return dockerConfigFile{}, err | ||
} | ||
|
||
// Make sure to read-lock the file before reading it. | ||
lock, err := lockfile.GetLockfile(path) | ||
if err != nil { | ||
return dockerConfigFile{}, fmt.Errorf("creating lock: %w", err) | ||
} | ||
lock.RLock() | ||
defer lock.Unlock() | ||
|
||
return readJSONFileLocked(path, legacyFormat) | ||
} | ||
|
||
// Implements readJSONFile but without locking the specified path. | ||
// The caller is responsible for synchronizing access to the path. | ||
func readJSONFileLocked(path string, legacyFormat bool) (dockerConfigFile, error) { | ||
var auths dockerConfigFile | ||
|
||
raw, err := ioutil.ReadFile(path) | ||
|
@@ -552,6 +576,14 @@ func readJSONFile(path string, legacyFormat bool) (dockerConfigFile, error) { | |
return dockerConfigFile{}, err | ||
} | ||
|
||
// When creating a file for the first time, it may be empty at this | ||
// point as it was created by the write lock. Hence, return early here | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That’s surprising to me (and incompatible with older versions of consumers, though that’s a small concern — they could easily race with See elsewhere about sharing the lock with JSON. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's a valid concern but I do not have a good/definitive answer/idea. We could use a central one (similar to the short-names lock) but old client would still compete with newer ones. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking about using a separate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The underlying If so, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I was considering that but didn't like the user experience. I would find it surprising/annoying if I do a
Holy frog, that is true and buried in the man pages. That indeed is a K.O. for this approach - we could do ref-counting on the path but that seems a bit too hacky for my taste. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to be explicit, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. … the point about flock(2) might be an option, but it is distinctly less portable (and the Linux man pages suggest that some implementations implement it on top of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A possible partial alternative: use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like we're between a rock and hard place. I will close this PR as I ran out of time and really do not have a good idea yet. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let’s at least do the |
||
// to prevent an unmarshal error; an empty file is not valid JSON. | ||
if len(raw) == 0 { | ||
auths.AuthConfigs = map[string]dockerAuthConfig{} | ||
return auths, nil | ||
} | ||
|
||
if legacyFormat { | ||
if err = json.Unmarshal(raw, &auths.AuthConfigs); err != nil { | ||
return dockerConfigFile{}, errors.Wrapf(err, "unmarshaling JSON at %q", path) | ||
|
@@ -590,7 +622,15 @@ func modifyJSON(sys *types.SystemContext, editor func(auths *dockerConfigFile) ( | |
return "", err | ||
} | ||
|
||
auths, err := readJSONFile(path, false) | ||
// Make sure to write-lock the file before writing to it. | ||
lock, err := lockfile.GetLockfile(path) | ||
if err != nil { | ||
return "", fmt.Errorf("creating lock: %w", err) | ||
} | ||
lock.Lock() | ||
defer lock.Unlock() | ||
|
||
auths, err := readJSONFileLocked(path, false) | ||
if err != nil { | ||
return "", errors.Wrapf(err, "reading JSON file %q", path) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn’t the path parameter a path of the lock? Which contains a random value to support
.Modified
? How canauth.json
be a JSON and a random string at the same time?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Touch()
andModified()
don't work in this scenario but I think that's OK in this scenario since we're controlling it here.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I have not been reading that carefully enough; the contents are not actually used if we call neither
Touch()
norModified()
.Still, that feels like an implementation detail that could change any time. I suppose our tests would catch it…