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 }