github.com/xeptore/docker-cli@v20.10.14+incompatible/cli/config/configfile/file.go (about)

     1  package configfile
     2  
     3  import (
     4  	"encoding/base64"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  
    13  	"github.com/docker/cli/cli/config/credentials"
    14  	"github.com/docker/cli/cli/config/types"
    15  	"github.com/pkg/errors"
    16  	"github.com/sirupsen/logrus"
    17  )
    18  
    19  const (
    20  	// This constant is only used for really old config files when the
    21  	// URL wasn't saved as part of the config file and it was just
    22  	// assumed to be this value.
    23  	defaultIndexServer = "https://index.docker.io/v1/"
    24  )
    25  
    26  // ConfigFile ~/.docker/config.json file info
    27  type ConfigFile struct {
    28  	AuthConfigs          map[string]types.AuthConfig  `json:"auths"`
    29  	HTTPHeaders          map[string]string            `json:"HttpHeaders,omitempty"`
    30  	PsFormat             string                       `json:"psFormat,omitempty"`
    31  	ImagesFormat         string                       `json:"imagesFormat,omitempty"`
    32  	NetworksFormat       string                       `json:"networksFormat,omitempty"`
    33  	PluginsFormat        string                       `json:"pluginsFormat,omitempty"`
    34  	VolumesFormat        string                       `json:"volumesFormat,omitempty"`
    35  	StatsFormat          string                       `json:"statsFormat,omitempty"`
    36  	DetachKeys           string                       `json:"detachKeys,omitempty"`
    37  	CredentialsStore     string                       `json:"credsStore,omitempty"`
    38  	CredentialHelpers    map[string]string            `json:"credHelpers,omitempty"`
    39  	Filename             string                       `json:"-"` // Note: for internal use only
    40  	ServiceInspectFormat string                       `json:"serviceInspectFormat,omitempty"`
    41  	ServicesFormat       string                       `json:"servicesFormat,omitempty"`
    42  	TasksFormat          string                       `json:"tasksFormat,omitempty"`
    43  	SecretFormat         string                       `json:"secretFormat,omitempty"`
    44  	ConfigFormat         string                       `json:"configFormat,omitempty"`
    45  	NodesFormat          string                       `json:"nodesFormat,omitempty"`
    46  	PruneFilters         []string                     `json:"pruneFilters,omitempty"`
    47  	Proxies              map[string]ProxyConfig       `json:"proxies,omitempty"`
    48  	Experimental         string                       `json:"experimental,omitempty"`
    49  	StackOrchestrator    string                       `json:"stackOrchestrator,omitempty"`
    50  	Kubernetes           *KubernetesConfig            `json:"kubernetes,omitempty"`
    51  	CurrentContext       string                       `json:"currentContext,omitempty"`
    52  	CLIPluginsExtraDirs  []string                     `json:"cliPluginsExtraDirs,omitempty"`
    53  	Plugins              map[string]map[string]string `json:"plugins,omitempty"`
    54  	Aliases              map[string]string            `json:"aliases,omitempty"`
    55  }
    56  
    57  // ProxyConfig contains proxy configuration settings
    58  type ProxyConfig struct {
    59  	HTTPProxy  string `json:"httpProxy,omitempty"`
    60  	HTTPSProxy string `json:"httpsProxy,omitempty"`
    61  	NoProxy    string `json:"noProxy,omitempty"`
    62  	FTPProxy   string `json:"ftpProxy,omitempty"`
    63  }
    64  
    65  // KubernetesConfig contains Kubernetes orchestrator settings
    66  type KubernetesConfig struct {
    67  	AllNamespaces string `json:"allNamespaces,omitempty"`
    68  }
    69  
    70  // New initializes an empty configuration file for the given filename 'fn'
    71  func New(fn string) *ConfigFile {
    72  	return &ConfigFile{
    73  		AuthConfigs: make(map[string]types.AuthConfig),
    74  		HTTPHeaders: make(map[string]string),
    75  		Filename:    fn,
    76  		Plugins:     make(map[string]map[string]string),
    77  		Aliases:     make(map[string]string),
    78  	}
    79  }
    80  
    81  // LegacyLoadFromReader reads the non-nested configuration data given and sets up the
    82  // auth config information with given directory and populates the receiver object
    83  func (configFile *ConfigFile) LegacyLoadFromReader(configData io.Reader) error {
    84  	b, err := ioutil.ReadAll(configData)
    85  	if err != nil {
    86  		return err
    87  	}
    88  
    89  	if err := json.Unmarshal(b, &configFile.AuthConfigs); err != nil {
    90  		arr := strings.Split(string(b), "\n")
    91  		if len(arr) < 2 {
    92  			return errors.Errorf("The Auth config file is empty")
    93  		}
    94  		authConfig := types.AuthConfig{}
    95  		origAuth := strings.Split(arr[0], " = ")
    96  		if len(origAuth) != 2 {
    97  			return errors.Errorf("Invalid Auth config file")
    98  		}
    99  		authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1])
   100  		if err != nil {
   101  			return err
   102  		}
   103  		authConfig.ServerAddress = defaultIndexServer
   104  		configFile.AuthConfigs[defaultIndexServer] = authConfig
   105  	} else {
   106  		for k, authConfig := range configFile.AuthConfigs {
   107  			authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth)
   108  			if err != nil {
   109  				return err
   110  			}
   111  			authConfig.Auth = ""
   112  			authConfig.ServerAddress = k
   113  			configFile.AuthConfigs[k] = authConfig
   114  		}
   115  	}
   116  	return nil
   117  }
   118  
   119  // LoadFromReader reads the configuration data given and sets up the auth config
   120  // information with given directory and populates the receiver object
   121  func (configFile *ConfigFile) LoadFromReader(configData io.Reader) error {
   122  	if err := json.NewDecoder(configData).Decode(&configFile); err != nil && !errors.Is(err, io.EOF) {
   123  		return err
   124  	}
   125  	var err error
   126  	for addr, ac := range configFile.AuthConfigs {
   127  		if ac.Auth != "" {
   128  			ac.Username, ac.Password, err = decodeAuth(ac.Auth)
   129  			if err != nil {
   130  				return err
   131  			}
   132  		}
   133  		ac.Auth = ""
   134  		ac.ServerAddress = addr
   135  		configFile.AuthConfigs[addr] = ac
   136  	}
   137  	return checkKubernetesConfiguration(configFile.Kubernetes)
   138  }
   139  
   140  // ContainsAuth returns whether there is authentication configured
   141  // in this file or not.
   142  func (configFile *ConfigFile) ContainsAuth() bool {
   143  	return configFile.CredentialsStore != "" ||
   144  		len(configFile.CredentialHelpers) > 0 ||
   145  		len(configFile.AuthConfigs) > 0
   146  }
   147  
   148  // GetAuthConfigs returns the mapping of repo to auth configuration
   149  func (configFile *ConfigFile) GetAuthConfigs() map[string]types.AuthConfig {
   150  	return configFile.AuthConfigs
   151  }
   152  
   153  // SaveToWriter encodes and writes out all the authorization information to
   154  // the given writer
   155  func (configFile *ConfigFile) SaveToWriter(writer io.Writer) error {
   156  	// Encode sensitive data into a new/temp struct
   157  	tmpAuthConfigs := make(map[string]types.AuthConfig, len(configFile.AuthConfigs))
   158  	for k, authConfig := range configFile.AuthConfigs {
   159  		authCopy := authConfig
   160  		// encode and save the authstring, while blanking out the original fields
   161  		authCopy.Auth = encodeAuth(&authCopy)
   162  		authCopy.Username = ""
   163  		authCopy.Password = ""
   164  		authCopy.ServerAddress = ""
   165  		tmpAuthConfigs[k] = authCopy
   166  	}
   167  
   168  	saveAuthConfigs := configFile.AuthConfigs
   169  	configFile.AuthConfigs = tmpAuthConfigs
   170  	defer func() { configFile.AuthConfigs = saveAuthConfigs }()
   171  
   172  	// User-Agent header is automatically set, and should not be stored in the configuration
   173  	for v := range configFile.HTTPHeaders {
   174  		if strings.EqualFold(v, "User-Agent") {
   175  			delete(configFile.HTTPHeaders, v)
   176  		}
   177  	}
   178  
   179  	data, err := json.MarshalIndent(configFile, "", "\t")
   180  	if err != nil {
   181  		return err
   182  	}
   183  	_, err = writer.Write(data)
   184  	return err
   185  }
   186  
   187  // Save encodes and writes out all the authorization information
   188  func (configFile *ConfigFile) Save() (retErr error) {
   189  	if configFile.Filename == "" {
   190  		return errors.Errorf("Can't save config with empty filename")
   191  	}
   192  
   193  	dir := filepath.Dir(configFile.Filename)
   194  	if err := os.MkdirAll(dir, 0700); err != nil {
   195  		return err
   196  	}
   197  	temp, err := ioutil.TempFile(dir, filepath.Base(configFile.Filename))
   198  	if err != nil {
   199  		return err
   200  	}
   201  	defer func() {
   202  		temp.Close()
   203  		if retErr != nil {
   204  			if err := os.Remove(temp.Name()); err != nil {
   205  				logrus.WithError(err).WithField("file", temp.Name()).Debug("Error cleaning up temp file")
   206  			}
   207  		}
   208  	}()
   209  
   210  	err = configFile.SaveToWriter(temp)
   211  	if err != nil {
   212  		return err
   213  	}
   214  
   215  	if err := temp.Close(); err != nil {
   216  		return errors.Wrap(err, "error closing temp file")
   217  	}
   218  
   219  	// Handle situation where the configfile is a symlink
   220  	cfgFile := configFile.Filename
   221  	if f, err := os.Readlink(cfgFile); err == nil {
   222  		cfgFile = f
   223  	}
   224  
   225  	// Try copying the current config file (if any) ownership and permissions
   226  	copyFilePermissions(cfgFile, temp.Name())
   227  	return os.Rename(temp.Name(), cfgFile)
   228  }
   229  
   230  // ParseProxyConfig computes proxy configuration by retrieving the config for the provided host and
   231  // then checking this against any environment variables provided to the container
   232  func (configFile *ConfigFile) ParseProxyConfig(host string, runOpts map[string]*string) map[string]*string {
   233  	var cfgKey string
   234  
   235  	if _, ok := configFile.Proxies[host]; !ok {
   236  		cfgKey = "default"
   237  	} else {
   238  		cfgKey = host
   239  	}
   240  
   241  	config := configFile.Proxies[cfgKey]
   242  	permitted := map[string]*string{
   243  		"HTTP_PROXY":  &config.HTTPProxy,
   244  		"HTTPS_PROXY": &config.HTTPSProxy,
   245  		"NO_PROXY":    &config.NoProxy,
   246  		"FTP_PROXY":   &config.FTPProxy,
   247  	}
   248  	m := runOpts
   249  	if m == nil {
   250  		m = make(map[string]*string)
   251  	}
   252  	for k := range permitted {
   253  		if *permitted[k] == "" {
   254  			continue
   255  		}
   256  		if _, ok := m[k]; !ok {
   257  			m[k] = permitted[k]
   258  		}
   259  		if _, ok := m[strings.ToLower(k)]; !ok {
   260  			m[strings.ToLower(k)] = permitted[k]
   261  		}
   262  	}
   263  	return m
   264  }
   265  
   266  // encodeAuth creates a base64 encoded string to containing authorization information
   267  func encodeAuth(authConfig *types.AuthConfig) string {
   268  	if authConfig.Username == "" && authConfig.Password == "" {
   269  		return ""
   270  	}
   271  
   272  	authStr := authConfig.Username + ":" + authConfig.Password
   273  	msg := []byte(authStr)
   274  	encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg)))
   275  	base64.StdEncoding.Encode(encoded, msg)
   276  	return string(encoded)
   277  }
   278  
   279  // decodeAuth decodes a base64 encoded string and returns username and password
   280  func decodeAuth(authStr string) (string, string, error) {
   281  	if authStr == "" {
   282  		return "", "", nil
   283  	}
   284  
   285  	decLen := base64.StdEncoding.DecodedLen(len(authStr))
   286  	decoded := make([]byte, decLen)
   287  	authByte := []byte(authStr)
   288  	n, err := base64.StdEncoding.Decode(decoded, authByte)
   289  	if err != nil {
   290  		return "", "", err
   291  	}
   292  	if n > decLen {
   293  		return "", "", errors.Errorf("Something went wrong decoding auth config")
   294  	}
   295  	arr := strings.SplitN(string(decoded), ":", 2)
   296  	if len(arr) != 2 {
   297  		return "", "", errors.Errorf("Invalid auth configuration file")
   298  	}
   299  	password := strings.Trim(arr[1], "\x00")
   300  	return arr[0], password, nil
   301  }
   302  
   303  // GetCredentialsStore returns a new credentials store from the settings in the
   304  // configuration file
   305  func (configFile *ConfigFile) GetCredentialsStore(registryHostname string) credentials.Store {
   306  	if helper := getConfiguredCredentialStore(configFile, registryHostname); helper != "" {
   307  		return newNativeStore(configFile, helper)
   308  	}
   309  	return credentials.NewFileStore(configFile)
   310  }
   311  
   312  // var for unit testing.
   313  var newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store {
   314  	return credentials.NewNativeStore(configFile, helperSuffix)
   315  }
   316  
   317  // GetAuthConfig for a repository from the credential store
   318  func (configFile *ConfigFile) GetAuthConfig(registryHostname string) (types.AuthConfig, error) {
   319  	return configFile.GetCredentialsStore(registryHostname).Get(registryHostname)
   320  }
   321  
   322  // getConfiguredCredentialStore returns the credential helper configured for the
   323  // given registry, the default credsStore, or the empty string if neither are
   324  // configured.
   325  func getConfiguredCredentialStore(c *ConfigFile, registryHostname string) string {
   326  	if c.CredentialHelpers != nil && registryHostname != "" {
   327  		if helper, exists := c.CredentialHelpers[registryHostname]; exists {
   328  			return helper
   329  		}
   330  	}
   331  	return c.CredentialsStore
   332  }
   333  
   334  // GetAllCredentials returns all of the credentials stored in all of the
   335  // configured credential stores.
   336  func (configFile *ConfigFile) GetAllCredentials() (map[string]types.AuthConfig, error) {
   337  	auths := make(map[string]types.AuthConfig)
   338  	addAll := func(from map[string]types.AuthConfig) {
   339  		for reg, ac := range from {
   340  			auths[reg] = ac
   341  		}
   342  	}
   343  
   344  	defaultStore := configFile.GetCredentialsStore("")
   345  	newAuths, err := defaultStore.GetAll()
   346  	if err != nil {
   347  		return nil, err
   348  	}
   349  	addAll(newAuths)
   350  
   351  	// Auth configs from a registry-specific helper should override those from the default store.
   352  	for registryHostname := range configFile.CredentialHelpers {
   353  		newAuth, err := configFile.GetAuthConfig(registryHostname)
   354  		if err != nil {
   355  			return nil, err
   356  		}
   357  		auths[registryHostname] = newAuth
   358  	}
   359  	return auths, nil
   360  }
   361  
   362  // GetFilename returns the file name that this config file is based on.
   363  func (configFile *ConfigFile) GetFilename() string {
   364  	return configFile.Filename
   365  }
   366  
   367  // PluginConfig retrieves the requested option for the given plugin.
   368  func (configFile *ConfigFile) PluginConfig(pluginname, option string) (string, bool) {
   369  	if configFile.Plugins == nil {
   370  		return "", false
   371  	}
   372  	pluginConfig, ok := configFile.Plugins[pluginname]
   373  	if !ok {
   374  		return "", false
   375  	}
   376  	value, ok := pluginConfig[option]
   377  	return value, ok
   378  }
   379  
   380  // SetPluginConfig sets the option to the given value for the given
   381  // plugin. Passing a value of "" will remove the option. If removing
   382  // the final config item for a given plugin then also cleans up the
   383  // overall plugin entry.
   384  func (configFile *ConfigFile) SetPluginConfig(pluginname, option, value string) {
   385  	if configFile.Plugins == nil {
   386  		configFile.Plugins = make(map[string]map[string]string)
   387  	}
   388  	pluginConfig, ok := configFile.Plugins[pluginname]
   389  	if !ok {
   390  		pluginConfig = make(map[string]string)
   391  		configFile.Plugins[pluginname] = pluginConfig
   392  	}
   393  	if value != "" {
   394  		pluginConfig[option] = value
   395  	} else {
   396  		delete(pluginConfig, option)
   397  	}
   398  	if len(pluginConfig) == 0 {
   399  		delete(configFile.Plugins, pluginname)
   400  	}
   401  }
   402  
   403  func checkKubernetesConfiguration(kubeConfig *KubernetesConfig) error {
   404  	if kubeConfig == nil {
   405  		return nil
   406  	}
   407  	switch kubeConfig.AllNamespaces {
   408  	case "":
   409  	case "enabled":
   410  	case "disabled":
   411  	default:
   412  		return fmt.Errorf("invalid 'kubernetes.allNamespaces' value, should be 'enabled' or 'disabled': %s", kubeConfig.AllNamespaces)
   413  	}
   414  	return nil
   415  }