tags: container,源码分析

docker cli ConfigFile()源码分析

本文编写时,最新release为v20.10.7, 因此代码均为v20.10.7分支的代码


在执行很多命令时,都需要读取docker cli的配置,即调用ConfigFile(),理解这个过程是有必要的。

https://github.com/docker/cli/blob/v20.10.7/cli/command/cli.go#L127

func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
    if cli.configFile == nil {
        cli.loadConfigFile()
    }
    return cli.configFile
}

func (cli *DockerCli) loadConfigFile() {
    cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
}

LoadDefaultConfigFile实现如下

https://github.com/docker/cli/blob/v20.10.7/cli/config/config.go#L151

func LoadDefaultConfigFile(stderr io.Writer) *configfile.ConfigFile {
    configFile, err := Load(Dir())
    if err != nil {
        fmt.Fprintf(stderr, "WARNING: Error loading config file: %v\n", err)
    }
    if printLegacyFileWarning {
        _, _ = fmt.Fprintln(stderr, "WARNING: Support for the legacy ~/.dockercfg configuration file and file-format is deprecated and will be removed in an upcoming release")
    }
    if !configFile.ContainsAuth() {
        configFile.CredentialsStore = credentials.DetectDefaultStore(configFile.CredentialsStore)
    }
    return configFile
}

Config目录优先从环境变量DOCKER_CONFIG获取,如果未设置环境变量,则默认为$HOME/.docker

https://github.com/docker/cli/blob/v20.10.7/cli/config/config.go#L63

const (
    ...
    configFileDir  = ".docker"
    ...
)

func resetConfigDir() {
    configDir = ""
    initConfigDir = new(sync.Once)
}

func setConfigDir() {
    if configDir != "" {
        return
    }
    configDir = os.Getenv("DOCKER_CONFIG")
    if configDir == "" {
        configDir = filepath.Join(getHomeDir(), configFileDir)
    }
}

// Dir returns the directory the configuration file is stored in
func Dir() string {
    initConfigDir.Do(setConfigDir)
    return configDir
}

优先从config.json文件读取配置,如果不存在,则从.dockercfg文件读取

https://github.com/docker/cli/blob/v20.10.7/cli/config/config.go#L113

const (
    ConfigFileName = "config.json"
    ...
    oldConfigfile  = ".dockercfg"
)

func Load(configDir string) (*configfile.ConfigFile, error) {
    printLegacyFileWarning = false

    if configDir == "" {
        configDir = Dir()
    }

    filename := filepath.Join(configDir, ConfigFileName)
    configFile := configfile.New(filename)

    // Try happy path first - latest config file
    if file, err := os.Open(filename); err == nil {
        defer file.Close()
        err = configFile.LoadFromReader(file)
        if err != nil {
            err = errors.Wrap(err, filename)
        }
        return configFile, err
    } else if !os.IsNotExist(err) {
        // if file is there but we can't stat it for any reason other
        // than it doesn't exist then stop
        return configFile, errors.Wrap(err, filename)
    }

    // Can't find latest config file so check for the old one
    filename = filepath.Join(getHomeDir(), oldConfigfile)
    if file, err := os.Open(filename); err == nil {
        printLegacyFileWarning = true
        defer file.Close()
        if err := configFile.LegacyLoadFromReader(file); err != nil {
            return configFile, errors.Wrap(err, filename)
        }
    }
    return configFile, nil
}

config.json文件解析过程如下,先使用json反序列化到ConfigFile结构体,如果有Auth字段,则使用base64解码,解析得到username, password

https://github.com/docker/cli/blob/v20.10.7/cli/config/configfile/file.go#L121

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 checkKubernetesConfiguration(configFile.Kubernetes)
}


// 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.Errorf("Something went wrong decoding auth config")
    }
    arr := strings.SplitN(string(decoded), ":", 2)
    if len(arr) != 2 {
        return "", "", errors.Errorf("Invalid auth configuration file")
    }
    password := strings.Trim(arr[1], "\x00")
    return arr[0], password, nil
}

.dockercfg文件的解析过程类似, 略

如果不存在认证相关的配置,则检测默认的CredentialsStore

func LoadDefaultConfigFile(stderr io.Writer) *configfile.ConfigFile {
    ...
    if !configFile.ContainsAuth() {
        configFile.CredentialsStore = credentials.DetectDefaultStore(configFile.CredentialsStore)
    }
    ...