github.com/ali-iotechsys/cli@v20.10.0+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 data, err := json.MarshalIndent(configFile, "", "\t") 173 if err != nil { 174 return err 175 } 176 _, err = writer.Write(data) 177 return err 178 } 179 180 // Save encodes and writes out all the authorization information 181 func (configFile *ConfigFile) Save() (retErr error) { 182 if configFile.Filename == "" { 183 return errors.Errorf("Can't save config with empty filename") 184 } 185 186 dir := filepath.Dir(configFile.Filename) 187 if err := os.MkdirAll(dir, 0700); err != nil { 188 return err 189 } 190 temp, err := ioutil.TempFile(dir, filepath.Base(configFile.Filename)) 191 if err != nil { 192 return err 193 } 194 defer func() { 195 temp.Close() 196 if retErr != nil { 197 if err := os.Remove(temp.Name()); err != nil { 198 logrus.WithError(err).WithField("file", temp.Name()).Debug("Error cleaning up temp file") 199 } 200 } 201 }() 202 203 err = configFile.SaveToWriter(temp) 204 if err != nil { 205 return err 206 } 207 208 if err := temp.Close(); err != nil { 209 return errors.Wrap(err, "error closing temp file") 210 } 211 212 // Handle situation where the configfile is a symlink 213 cfgFile := configFile.Filename 214 if f, err := os.Readlink(cfgFile); err == nil { 215 cfgFile = f 216 } 217 218 // Try copying the current config file (if any) ownership and permissions 219 copyFilePermissions(cfgFile, temp.Name()) 220 return os.Rename(temp.Name(), cfgFile) 221 } 222 223 // ParseProxyConfig computes proxy configuration by retrieving the config for the provided host and 224 // then checking this against any environment variables provided to the container 225 func (configFile *ConfigFile) ParseProxyConfig(host string, runOpts map[string]*string) map[string]*string { 226 var cfgKey string 227 228 if _, ok := configFile.Proxies[host]; !ok { 229 cfgKey = "default" 230 } else { 231 cfgKey = host 232 } 233 234 config := configFile.Proxies[cfgKey] 235 permitted := map[string]*string{ 236 "HTTP_PROXY": &config.HTTPProxy, 237 "HTTPS_PROXY": &config.HTTPSProxy, 238 "NO_PROXY": &config.NoProxy, 239 "FTP_PROXY": &config.FTPProxy, 240 } 241 m := runOpts 242 if m == nil { 243 m = make(map[string]*string) 244 } 245 for k := range permitted { 246 if *permitted[k] == "" { 247 continue 248 } 249 if _, ok := m[k]; !ok { 250 m[k] = permitted[k] 251 } 252 if _, ok := m[strings.ToLower(k)]; !ok { 253 m[strings.ToLower(k)] = permitted[k] 254 } 255 } 256 return m 257 } 258 259 // encodeAuth creates a base64 encoded string to containing authorization information 260 func encodeAuth(authConfig *types.AuthConfig) string { 261 if authConfig.Username == "" && authConfig.Password == "" { 262 return "" 263 } 264 265 authStr := authConfig.Username + ":" + authConfig.Password 266 msg := []byte(authStr) 267 encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg))) 268 base64.StdEncoding.Encode(encoded, msg) 269 return string(encoded) 270 } 271 272 // decodeAuth decodes a base64 encoded string and returns username and password 273 func decodeAuth(authStr string) (string, string, error) { 274 if authStr == "" { 275 return "", "", nil 276 } 277 278 decLen := base64.StdEncoding.DecodedLen(len(authStr)) 279 decoded := make([]byte, decLen) 280 authByte := []byte(authStr) 281 n, err := base64.StdEncoding.Decode(decoded, authByte) 282 if err != nil { 283 return "", "", err 284 } 285 if n > decLen { 286 return "", "", errors.Errorf("Something went wrong decoding auth config") 287 } 288 arr := strings.SplitN(string(decoded), ":", 2) 289 if len(arr) != 2 { 290 return "", "", errors.Errorf("Invalid auth configuration file") 291 } 292 password := strings.Trim(arr[1], "\x00") 293 return arr[0], password, nil 294 } 295 296 // GetCredentialsStore returns a new credentials store from the settings in the 297 // configuration file 298 func (configFile *ConfigFile) GetCredentialsStore(registryHostname string) credentials.Store { 299 if helper := getConfiguredCredentialStore(configFile, registryHostname); helper != "" { 300 return newNativeStore(configFile, helper) 301 } 302 return credentials.NewFileStore(configFile) 303 } 304 305 // var for unit testing. 306 var newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store { 307 return credentials.NewNativeStore(configFile, helperSuffix) 308 } 309 310 // GetAuthConfig for a repository from the credential store 311 func (configFile *ConfigFile) GetAuthConfig(registryHostname string) (types.AuthConfig, error) { 312 return configFile.GetCredentialsStore(registryHostname).Get(registryHostname) 313 } 314 315 // getConfiguredCredentialStore returns the credential helper configured for the 316 // given registry, the default credsStore, or the empty string if neither are 317 // configured. 318 func getConfiguredCredentialStore(c *ConfigFile, registryHostname string) string { 319 if c.CredentialHelpers != nil && registryHostname != "" { 320 if helper, exists := c.CredentialHelpers[registryHostname]; exists { 321 return helper 322 } 323 } 324 return c.CredentialsStore 325 } 326 327 // GetAllCredentials returns all of the credentials stored in all of the 328 // configured credential stores. 329 func (configFile *ConfigFile) GetAllCredentials() (map[string]types.AuthConfig, error) { 330 auths := make(map[string]types.AuthConfig) 331 addAll := func(from map[string]types.AuthConfig) { 332 for reg, ac := range from { 333 auths[reg] = ac 334 } 335 } 336 337 defaultStore := configFile.GetCredentialsStore("") 338 newAuths, err := defaultStore.GetAll() 339 if err != nil { 340 return nil, err 341 } 342 addAll(newAuths) 343 344 // Auth configs from a registry-specific helper should override those from the default store. 345 for registryHostname := range configFile.CredentialHelpers { 346 newAuth, err := configFile.GetAuthConfig(registryHostname) 347 if err != nil { 348 return nil, err 349 } 350 auths[registryHostname] = newAuth 351 } 352 return auths, nil 353 } 354 355 // GetFilename returns the file name that this config file is based on. 356 func (configFile *ConfigFile) GetFilename() string { 357 return configFile.Filename 358 } 359 360 // PluginConfig retrieves the requested option for the given plugin. 361 func (configFile *ConfigFile) PluginConfig(pluginname, option string) (string, bool) { 362 if configFile.Plugins == nil { 363 return "", false 364 } 365 pluginConfig, ok := configFile.Plugins[pluginname] 366 if !ok { 367 return "", false 368 } 369 value, ok := pluginConfig[option] 370 return value, ok 371 } 372 373 // SetPluginConfig sets the option to the given value for the given 374 // plugin. Passing a value of "" will remove the option. If removing 375 // the final config item for a given plugin then also cleans up the 376 // overall plugin entry. 377 func (configFile *ConfigFile) SetPluginConfig(pluginname, option, value string) { 378 if configFile.Plugins == nil { 379 configFile.Plugins = make(map[string]map[string]string) 380 } 381 pluginConfig, ok := configFile.Plugins[pluginname] 382 if !ok { 383 pluginConfig = make(map[string]string) 384 configFile.Plugins[pluginname] = pluginConfig 385 } 386 if value != "" { 387 pluginConfig[option] = value 388 } else { 389 delete(pluginConfig, option) 390 } 391 if len(pluginConfig) == 0 { 392 delete(configFile.Plugins, pluginname) 393 } 394 } 395 396 func checkKubernetesConfiguration(kubeConfig *KubernetesConfig) error { 397 if kubeConfig == nil { 398 return nil 399 } 400 switch kubeConfig.AllNamespaces { 401 case "": 402 case "enabled": 403 case "disabled": 404 default: 405 return fmt.Errorf("invalid 'kubernetes.allNamespaces' value, should be 'enabled' or 'disabled': %s", kubeConfig.AllNamespaces) 406 } 407 return nil 408 }