docker/cli

saving configuration file is not forward-compatible

thaJeztah opened this issue · 0 comments

Description

The config/configfile package always uses the current schema of the config-file to save the file to disk. This can be problematic, as there's many code-bases that use this code to authenticate (and store credentials), and those code-bases may be using an older version of the code. For example, go-containerregistry up until recently (previous patch release) used v24.0 as dependency;
https://github.com/google/go-containerregistry/blob/v0.20.1/go.mod#L7

Those libraries will continue to work, because they're only using the "auths" field from the configuration file, but if someone would log in using those tools they may be discarding configuration options from the config-file. As an example, c5016c6 (#4376) introduced a new "features" field in the CLI configuration file. That change is part of Docker v26.1 and up, but older versions are not aware of it.

Reproducer

I'm running this inside a container, which doesn't have a credentials-helper configured. The container has an older version of the Docker CLI (v25.0);

$ docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock docker:25.0-cli sh

$ docker --version
Docker version 25.0.5, build 5dc9bcc

Inside the container, I create a config-file in a temporary directory that contains experimental and features fields;

$ mkdir temp-config
$ echo '{"experimental":"enabled", "features":{"hooks": "true", "something-else": "false"}}' > temp-config/config.json

When running docker login or docker logout, the config is overwritten with only fields known by that version of the code;

$ cat ./temp-config/config.json
{"experimental":"enabled", "features":{"hooks": "true", "something-else": "false"}}

$ docker --config=./temp-config/ logout
Removing login credentials for https://index.docker.io/v1/

$ cat temp-config/config.json
{
	"auths": {},
	"experimental": "enabled"
}

Some other actions, such as switching the context will result in the same;

# reset the config-file
$ echo '{"experimental":"enabled", "features":{"hooks": "true", "something-else": "false"}}' > temp-config/config.json

$ docker --config=./temp-config/ context create somecontext
somecontext
Successfully created context "somecontext"

$ cat ./temp-config/config.json
{"experimental":"enabled", "features":{"hooks": "true", "something-else": "false"}}

$ docker --config=./temp-config/ context use somecontext
somecontext
Current context is now "somecontext"

$ cat ./temp-config/config.json
{
	"auths": {},
	"experimental": "enabled",
	"currentContext": "somecontext"
}

Solutions

The overall design of configuration file(s) as used by the CLI and plugins needs some re-thinking. This problem is partially because we have a poor separation of "config" and "state"; storing authentication in the config-file itself was never a great choice (and amplifies this issue), and ideally, the CLI would not mutate settings created by the user (or at least limited to actual config changes, not "state").

For actions mentioned (login, logout, switching default context), we only modify a single field in the config-file any other changes present in the config file don't have to be "understood" by the code, and we could change the code to "patch" the config-file.

i.e., when updating the config-file;

  1. read the config into a generic map[string]any
  2. patch the field to update (auths or currentContext)
  3. marshal the map[string]any to JSON and write it to the file.