Configuration options for experimental CLI features were deprecated in docker 19.03 (3172219932), and enabled by default since docker 20.10 (977d3ae046). This field was deprecated inc8f9187157, which is part of the 28.x release, and is unused. This patch removes the field. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
442 lines
14 KiB
Go
442 lines
14 KiB
Go
package configfile
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/docker/cli/cli/config/credentials"
|
|
"github.com/docker/cli/cli/config/memorystore"
|
|
"github.com/docker/cli/cli/config/types"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// ConfigFile ~/.docker/config.json file info
|
|
type ConfigFile struct {
|
|
AuthConfigs map[string]types.AuthConfig `json:"auths"`
|
|
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
|
|
PsFormat string `json:"psFormat,omitempty"`
|
|
ImagesFormat string `json:"imagesFormat,omitempty"`
|
|
NetworksFormat string `json:"networksFormat,omitempty"`
|
|
PluginsFormat string `json:"pluginsFormat,omitempty"`
|
|
VolumesFormat string `json:"volumesFormat,omitempty"`
|
|
StatsFormat string `json:"statsFormat,omitempty"`
|
|
DetachKeys string `json:"detachKeys,omitempty"`
|
|
CredentialsStore string `json:"credsStore,omitempty"`
|
|
CredentialHelpers map[string]string `json:"credHelpers,omitempty"`
|
|
Filename string `json:"-"` // Note: for internal use only
|
|
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"`
|
|
ServicesFormat string `json:"servicesFormat,omitempty"`
|
|
TasksFormat string `json:"tasksFormat,omitempty"`
|
|
SecretFormat string `json:"secretFormat,omitempty"`
|
|
ConfigFormat string `json:"configFormat,omitempty"`
|
|
NodesFormat string `json:"nodesFormat,omitempty"`
|
|
PruneFilters []string `json:"pruneFilters,omitempty"`
|
|
Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
|
|
CurrentContext string `json:"currentContext,omitempty"`
|
|
CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"`
|
|
Plugins map[string]map[string]string `json:"plugins,omitempty"`
|
|
Aliases map[string]string `json:"aliases,omitempty"`
|
|
Features map[string]string `json:"features,omitempty"`
|
|
}
|
|
|
|
type configEnvAuth struct {
|
|
Auth string `json:"auth"`
|
|
}
|
|
|
|
type configEnv struct {
|
|
AuthConfigs map[string]configEnvAuth `json:"auths"`
|
|
}
|
|
|
|
// DockerEnvConfigKey is an environment variable that contains a JSON encoded
|
|
// credential config. It only supports storing the credentials as a base64
|
|
// encoded string in the format base64("username:pat").
|
|
//
|
|
// Adding additional fields will produce a parsing error.
|
|
//
|
|
// Example:
|
|
//
|
|
// {
|
|
// "auths": {
|
|
// "example.test": {
|
|
// "auth": base64-encoded-username-pat
|
|
// }
|
|
// }
|
|
// }
|
|
const DockerEnvConfigKey = "DOCKER_AUTH_CONFIG"
|
|
|
|
// ProxyConfig contains proxy configuration settings
|
|
type ProxyConfig struct {
|
|
HTTPProxy string `json:"httpProxy,omitempty"`
|
|
HTTPSProxy string `json:"httpsProxy,omitempty"`
|
|
NoProxy string `json:"noProxy,omitempty"`
|
|
FTPProxy string `json:"ftpProxy,omitempty"`
|
|
AllProxy string `json:"allProxy,omitempty"`
|
|
}
|
|
|
|
// New initializes an empty configuration file for the given filename 'fn'
|
|
func New(fn string) *ConfigFile {
|
|
return &ConfigFile{
|
|
AuthConfigs: make(map[string]types.AuthConfig),
|
|
HTTPHeaders: make(map[string]string),
|
|
Filename: fn,
|
|
Plugins: make(map[string]map[string]string),
|
|
Aliases: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
// LoadFromReader reads the configuration data given and sets up the auth config
|
|
// information with given directory and populates the receiver object
|
|
func (configFile *ConfigFile) LoadFromReader(configData io.Reader) error {
|
|
if err := json.NewDecoder(configData).Decode(configFile); err != nil && !errors.Is(err, io.EOF) {
|
|
return err
|
|
}
|
|
var err error
|
|
for addr, ac := range configFile.AuthConfigs {
|
|
if ac.Auth != "" {
|
|
ac.Username, ac.Password, err = decodeAuth(ac.Auth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
ac.Auth = ""
|
|
ac.ServerAddress = addr
|
|
configFile.AuthConfigs[addr] = ac
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ContainsAuth returns whether there is authentication configured
|
|
// in this file or not.
|
|
func (configFile *ConfigFile) ContainsAuth() bool {
|
|
return configFile.CredentialsStore != "" ||
|
|
len(configFile.CredentialHelpers) > 0 ||
|
|
len(configFile.AuthConfigs) > 0
|
|
}
|
|
|
|
// GetAuthConfigs returns the mapping of repo to auth configuration
|
|
func (configFile *ConfigFile) GetAuthConfigs() map[string]types.AuthConfig {
|
|
if configFile.AuthConfigs == nil {
|
|
configFile.AuthConfigs = make(map[string]types.AuthConfig)
|
|
}
|
|
return configFile.AuthConfigs
|
|
}
|
|
|
|
// SaveToWriter encodes and writes out all the authorization information to
|
|
// the given writer
|
|
func (configFile *ConfigFile) SaveToWriter(writer io.Writer) error {
|
|
// Encode sensitive data into a new/temp struct
|
|
tmpAuthConfigs := make(map[string]types.AuthConfig, len(configFile.AuthConfigs))
|
|
for k, authConfig := range configFile.AuthConfigs {
|
|
authCopy := authConfig
|
|
// encode and save the authstring, while blanking out the original fields
|
|
authCopy.Auth = encodeAuth(&authCopy)
|
|
authCopy.Username = ""
|
|
authCopy.Password = ""
|
|
authCopy.ServerAddress = ""
|
|
tmpAuthConfigs[k] = authCopy
|
|
}
|
|
|
|
saveAuthConfigs := configFile.AuthConfigs
|
|
configFile.AuthConfigs = tmpAuthConfigs
|
|
defer func() { configFile.AuthConfigs = saveAuthConfigs }()
|
|
|
|
// User-Agent header is automatically set, and should not be stored in the configuration
|
|
for v := range configFile.HTTPHeaders {
|
|
if strings.EqualFold(v, "User-Agent") {
|
|
delete(configFile.HTTPHeaders, v)
|
|
}
|
|
}
|
|
|
|
data, err := json.MarshalIndent(configFile, "", "\t")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = writer.Write(data)
|
|
return err
|
|
}
|
|
|
|
// Save encodes and writes out all the authorization information
|
|
func (configFile *ConfigFile) Save() (retErr error) {
|
|
if configFile.Filename == "" {
|
|
return errors.New("can't save config with empty filename")
|
|
}
|
|
|
|
dir := filepath.Dir(configFile.Filename)
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
return err
|
|
}
|
|
temp, err := os.CreateTemp(dir, filepath.Base(configFile.Filename))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
// ignore error as the file may already be closed when we reach this.
|
|
_ = temp.Close()
|
|
if retErr != nil {
|
|
if err := os.Remove(temp.Name()); err != nil {
|
|
logrus.WithError(err).WithField("file", temp.Name()).Debug("Error cleaning up temp file")
|
|
}
|
|
}
|
|
}()
|
|
|
|
err = configFile.SaveToWriter(temp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := temp.Close(); err != nil {
|
|
return fmt.Errorf("error closing temp file: %w", err)
|
|
}
|
|
|
|
// Handle situation where the configfile is a symlink, and allow for dangling symlinks
|
|
cfgFile := configFile.Filename
|
|
if f, err := filepath.EvalSymlinks(cfgFile); err == nil {
|
|
cfgFile = f
|
|
} else if os.IsNotExist(err) {
|
|
// extract the path from the error if the configfile does not exist or is a dangling symlink
|
|
var pathError *os.PathError
|
|
if errors.As(err, &pathError) {
|
|
cfgFile = pathError.Path
|
|
}
|
|
}
|
|
|
|
// Try copying the current config file (if any) ownership and permissions
|
|
copyFilePermissions(cfgFile, temp.Name())
|
|
return os.Rename(temp.Name(), cfgFile)
|
|
}
|
|
|
|
// ParseProxyConfig computes proxy configuration by retrieving the config for the provided host and
|
|
// then checking this against any environment variables provided to the container
|
|
func (configFile *ConfigFile) ParseProxyConfig(host string, runOpts map[string]*string) map[string]*string {
|
|
var cfgKey string
|
|
|
|
if _, ok := configFile.Proxies[host]; !ok {
|
|
cfgKey = "default"
|
|
} else {
|
|
cfgKey = host
|
|
}
|
|
|
|
config := configFile.Proxies[cfgKey]
|
|
permitted := map[string]*string{
|
|
"HTTP_PROXY": &config.HTTPProxy,
|
|
"HTTPS_PROXY": &config.HTTPSProxy,
|
|
"NO_PROXY": &config.NoProxy,
|
|
"FTP_PROXY": &config.FTPProxy,
|
|
"ALL_PROXY": &config.AllProxy,
|
|
}
|
|
m := runOpts
|
|
if m == nil {
|
|
m = make(map[string]*string)
|
|
}
|
|
for k := range permitted {
|
|
if *permitted[k] == "" {
|
|
continue
|
|
}
|
|
if _, ok := m[k]; !ok {
|
|
m[k] = permitted[k]
|
|
}
|
|
if _, ok := m[strings.ToLower(k)]; !ok {
|
|
m[strings.ToLower(k)] = permitted[k]
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
// encodeAuth creates a base64 encoded string to containing authorization information
|
|
func encodeAuth(authConfig *types.AuthConfig) string {
|
|
if authConfig.Username == "" && authConfig.Password == "" {
|
|
return ""
|
|
}
|
|
|
|
authStr := authConfig.Username + ":" + authConfig.Password
|
|
msg := []byte(authStr)
|
|
encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg)))
|
|
base64.StdEncoding.Encode(encoded, msg)
|
|
return string(encoded)
|
|
}
|
|
|
|
// decodeAuth decodes a base64 encoded string and returns username and password
|
|
func decodeAuth(authStr string) (string, string, error) {
|
|
if authStr == "" {
|
|
return "", "", nil
|
|
}
|
|
|
|
decLen := base64.StdEncoding.DecodedLen(len(authStr))
|
|
decoded := make([]byte, decLen)
|
|
authByte := []byte(authStr)
|
|
n, err := base64.StdEncoding.Decode(decoded, authByte)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if n > decLen {
|
|
return "", "", errors.New("something went wrong decoding auth config")
|
|
}
|
|
userName, password, ok := strings.Cut(string(decoded), ":")
|
|
if !ok || userName == "" {
|
|
return "", "", errors.New("invalid auth configuration file")
|
|
}
|
|
return userName, strings.Trim(password, "\x00"), nil
|
|
}
|
|
|
|
// GetCredentialsStore returns a new credentials store from the settings in the
|
|
// configuration file
|
|
func (configFile *ConfigFile) GetCredentialsStore(registryHostname string) credentials.Store {
|
|
store := credentials.NewFileStore(configFile)
|
|
|
|
if helper := getConfiguredCredentialStore(configFile, registryHostname); helper != "" {
|
|
store = newNativeStore(configFile, helper)
|
|
}
|
|
|
|
envConfig := os.Getenv(DockerEnvConfigKey)
|
|
if envConfig == "" {
|
|
return store
|
|
}
|
|
|
|
authConfig, err := parseEnvConfig(envConfig)
|
|
if err != nil {
|
|
_, _ = fmt.Fprintln(os.Stderr, "Failed to create credential store from DOCKER_AUTH_CONFIG: ", err)
|
|
return store
|
|
}
|
|
|
|
// use DOCKER_AUTH_CONFIG if set
|
|
// it uses the native or file store as a fallback to fetch and store credentials
|
|
envStore, err := memorystore.New(
|
|
memorystore.WithAuthConfig(authConfig),
|
|
memorystore.WithFallbackStore(store),
|
|
)
|
|
if err != nil {
|
|
_, _ = fmt.Fprintln(os.Stderr, "Failed to create credential store from DOCKER_AUTH_CONFIG: ", err)
|
|
return store
|
|
}
|
|
|
|
return envStore
|
|
}
|
|
|
|
func parseEnvConfig(v string) (map[string]types.AuthConfig, error) {
|
|
envConfig := &configEnv{}
|
|
decoder := json.NewDecoder(strings.NewReader(v))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(envConfig); err != nil && !errors.Is(err, io.EOF) {
|
|
return nil, err
|
|
}
|
|
if decoder.More() {
|
|
return nil, errors.New("DOCKER_AUTH_CONFIG does not support more than one JSON object")
|
|
}
|
|
|
|
authConfigs := make(map[string]types.AuthConfig)
|
|
for addr, envAuth := range envConfig.AuthConfigs {
|
|
if envAuth.Auth == "" {
|
|
return nil, fmt.Errorf("DOCKER_AUTH_CONFIG environment variable is missing key `auth` for %s", addr)
|
|
}
|
|
username, password, err := decodeAuth(envAuth.Auth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
authConfigs[addr] = types.AuthConfig{
|
|
Username: username,
|
|
Password: password,
|
|
ServerAddress: addr,
|
|
}
|
|
}
|
|
return authConfigs, nil
|
|
}
|
|
|
|
// var for unit testing.
|
|
var newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store {
|
|
return credentials.NewNativeStore(configFile, helperSuffix)
|
|
}
|
|
|
|
// GetAuthConfig for a repository from the credential store
|
|
func (configFile *ConfigFile) GetAuthConfig(registryHostname string) (types.AuthConfig, error) {
|
|
return configFile.GetCredentialsStore(registryHostname).Get(registryHostname)
|
|
}
|
|
|
|
// getConfiguredCredentialStore returns the credential helper configured for the
|
|
// given registry, the default credsStore, or the empty string if neither are
|
|
// configured.
|
|
func getConfiguredCredentialStore(c *ConfigFile, registryHostname string) string {
|
|
if c.CredentialHelpers != nil && registryHostname != "" {
|
|
if helper, exists := c.CredentialHelpers[registryHostname]; exists {
|
|
return helper
|
|
}
|
|
}
|
|
return c.CredentialsStore
|
|
}
|
|
|
|
// GetAllCredentials returns all of the credentials stored in all of the
|
|
// configured credential stores.
|
|
func (configFile *ConfigFile) GetAllCredentials() (map[string]types.AuthConfig, error) {
|
|
auths := make(map[string]types.AuthConfig)
|
|
addAll := func(from map[string]types.AuthConfig) {
|
|
for reg, ac := range from {
|
|
auths[reg] = ac
|
|
}
|
|
}
|
|
|
|
defaultStore := configFile.GetCredentialsStore("")
|
|
newAuths, err := defaultStore.GetAll()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
addAll(newAuths)
|
|
|
|
// Auth configs from a registry-specific helper should override those from the default store.
|
|
for registryHostname := range configFile.CredentialHelpers {
|
|
newAuth, err := configFile.GetAuthConfig(registryHostname)
|
|
if err != nil {
|
|
// TODO(thaJeztah): use context-logger, so that this output can be suppressed (in tests).
|
|
logrus.WithError(err).Warnf("Failed to get credentials for registry: %s", registryHostname)
|
|
continue
|
|
}
|
|
auths[registryHostname] = newAuth
|
|
}
|
|
return auths, nil
|
|
}
|
|
|
|
// GetFilename returns the file name that this config file is based on.
|
|
func (configFile *ConfigFile) GetFilename() string {
|
|
return configFile.Filename
|
|
}
|
|
|
|
// PluginConfig retrieves the requested option for the given plugin.
|
|
func (configFile *ConfigFile) PluginConfig(pluginname, option string) (string, bool) {
|
|
if configFile.Plugins == nil {
|
|
return "", false
|
|
}
|
|
pluginConfig, ok := configFile.Plugins[pluginname]
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
value, ok := pluginConfig[option]
|
|
return value, ok
|
|
}
|
|
|
|
// SetPluginConfig sets the option to the given value for the given
|
|
// plugin. Passing a value of "" will remove the option. If removing
|
|
// the final config item for a given plugin then also cleans up the
|
|
// overall plugin entry.
|
|
func (configFile *ConfigFile) SetPluginConfig(pluginname, option, value string) {
|
|
if configFile.Plugins == nil {
|
|
configFile.Plugins = make(map[string]map[string]string)
|
|
}
|
|
pluginConfig, ok := configFile.Plugins[pluginname]
|
|
if !ok {
|
|
pluginConfig = make(map[string]string)
|
|
configFile.Plugins[pluginname] = pluginConfig
|
|
}
|
|
if value != "" {
|
|
pluginConfig[option] = value
|
|
} else {
|
|
delete(pluginConfig, option)
|
|
}
|
|
if len(pluginConfig) == 0 {
|
|
delete(configFile.Plugins, pluginname)
|
|
}
|
|
}
|