在《pull命令实现与镜像存储(1)》我们了解到拉取镜像时,runPull函数通过reference.ParseNamed(opts.remote)得到一个描述镜像信息的Named对象。以命令docker pull ubuntu:latest为例,将得到一个NamedTagged对象。我们再看下reference.ParseNamed(image):
// ParseNamed parses s and returns a syntactically valid reference implementing
// the Named interface. The reference must have a name, otherwise an error is
// returned.
// If an error was encountered it is returned, along with a nil Reference.
func ParseNamed(s string) (Named, error) {
named, err := distreference.ParseNamed(s)
if err != nil {
return nil, fmt.Errorf("Error parsing reference: %q is not a valid repository/tag: %s", s, err)
}
// If no valid hostname is found, the default hostname is used./如果没有有效的主机名,则使用默认的主机名docker.io
//将distreference.Namded转化为reference.Named
r, err := WithName(named.Name())
if err != nil {
return nil, err
}
if canonical, isCanonical := named.(distreference.Canonical); isCanonical {
//将distreference.Canonical转化为reference.Canonical
return WithDigest(r, canonical.Digest())
}
if tagged, isTagged := named.(distreference.NamedTagged); isTagged {
//将distreference.NamedTagged转化为reference.NamedTagged
return WithTag(r, tagged.Tag())
}
return r, nil
}
可以看到,如果我们传入的参数是tag形式,那么将执行distreference.NamedTagged转化为reference.NamedTagged。我们知道reference.NamedTagged只是Named的子接口,是不能直接使用的,一定要有一个实现类。我们进WithTag函数看下,看下reference.NamedTagged的实现形式:
// WithTag combines the name from "name" and the tag from "tag" to form a
// reference incorporating both the name and the tag.
func WithTag(name Named, tag string) (NamedTagged, error) {
r, err := distreference.WithTag(name, tag)
if err != nil {
return nil, err
}
//reference.NamedTagged接口的实现类
return &taggedRef{namedRef{r}}, nil
}
可以看到返回的是taggedRef对象,即是NamedTagged的实现类,我们看下该类:
type namedRef struct {
distreference.Named
}
type taggedRef struct {
namedRef
}
可以看到taggedRef 实际上包含了一个distreference.Named,有go语言的特性知,taggedRef 将继承distreference.Named的所有方法,即是taggedRef 拥有distreference.taggedReference的所有方法(distreference.NamedTagged是distreference.Named的子接口,distreference.taggedReference是distreference.NamedTagged的实现类)。
好了,现在我们得到一个taggedRef镜像的描述对象,我们回到runPull函数:
func runPull(dockerCli *command.DockerCli, opts pullOptions) error {
//从参数中解析出带镜像仓库地址等信息的镜像引用,如果参数中没有仓库地址信息,则使用默认的docker.io
distributionRef, err := reference.ParseNamed(opts.remote)
if err != nil {
return err
}
......
//加了禁止镜像校验disable-content-trust,而且不带数字签名的tag
if command.IsTrusted() && !registryRef.HasDigest() {
// Check if tag is digest
err = trustedPull(ctx, dockerCli, repoInfo, registryRef, authConfig, requestPrivilege)
} else {
//向dockerd发送拉取镜像请求
err = imagePullPrivileged(ctx, dockerCli, authConfig, distributionRef.String(), requestPrivilege, opts.all)
}
if err != nil {
if strings.Contains(err.Error(), "target is a plugin") {
return errors.New(err.Error() + " - Use `docker plugin install`")
}
return err
}
return nil
}
runPull对该对象进行一些检查,我们不关心;如果加了disable-content-trust选项,还会做其他一些事情才继续往下,我们先不管。我们直接看到:
err = imagePullPrivileged(ctx, dockerCli, authConfig, distributionRef.String(), requestPrivilege, opts.all)
可以看到我们得到taggedRef对象distributionRef,字符串化之后,作为imagePullPrivileged函数的参数,distributionRef的String函数来自于distreference.taggedReference:
func (t taggedReference) String() string {
return t.name + ":" + t.tag
}
可以发现,都已了一大圈,还是我们传入时的形式,即如例:ubunu:latest。不知基于何考虑,我们先不去深究,我们继续分析函imagePullPrivileged:
// imagePullPrivileged pulls the image and displays it to the output
func imagePullPrivileged(ctx context.Context, cli *command.DockerCli, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc, all bool) error {
//在docker官网注册的账户信息
encodedAuth, err := command.EncodeAuthToBase64(authConfig)
if err != nil {
return err
}
options := types.ImagePullOptions{
RegistryAuth: encodedAuth,
PrivilegeFunc: requestPrivilege,
All: all,
}
//实现在docker\client\image_pull.go
responseBody, err := cli.Client().ImagePull(ctx, ref, options)
if err != nil {
return err
}
defer responseBody.Close()
return jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil)
}
我们之前了解到docker实现为一个C/S架构,docker作为客户端接受用户命令,解析完之后,将向dockerd发送请求,然后由dockerd完成用户的请求,下面的语句就是docker向dockerd发送请求:
responseBody, err := cli.Client().ImagePull(ctx, ref, options)
我们看下cli.Client():
// Client returns the APIClient
func (cli *DockerCli) Client() client.APIClient {
return cli.client
}
那这个cli.client是什么呢?为了连贯性,我们先不分析这个client怎么来的,我们直接给出client的image_pull
实现代码在文件docker\client\image_pull.go,我们看下:
func (cli *Client) ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) {
//这里又解析了一遍,搞不懂为何这么绕来绕去,
repository, tag, err := reference.Parse(ref)
if err != nil {
return nil, err
}
query := url.Values{}
query.Set("fromImage", repository)
if tag != "" && !options.All {
query.Set("tag", tag)
}
//发送http请求到dockerd
resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth)
if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
newAuthHeader, privilegeErr := options.PrivilegeFunc()
if privilegeErr != nil {
return nil, privilegeErr
}
resp, err = cli.tryImageCreate(ctx, query, newAuthHeader)
}
if err != nil {
return nil, err
}
return resp.body, nil
}
可以看到,ImagePull函数又把我们传入的参数解析了一遍,而且使用同样的方法,解析出镜像名和tag,作为http的header,发送到dockerd(真实无语了),这样在dockerd将接收到镜像名和tag等结构化参数。我们看下这些参数将发送到那里:
func (cli *Client) tryImageCreate(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) {
headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
return cli.post(ctx, "/images/create", query, nil, headers)
}
可以看到将发送到服务端的这个URL:/images/create。至此客户端这边的工作也先告一段落。后面我们将分析dockerd关于拉取镜像的代码。
最后,我们讲下client是怎么来的。docker首先执行main函数:
func main() {
// Set terminal emulation based on platform as required.
stdin, stdout, stderr := term.StdStreams()
logrus.SetOutput(stderr)
dockerCli := command.NewDockerCli(stdin, stdout, stderr)
//主命令
cmd := newDockerCommand(dockerCli)
//执行命令
if err := cmd.Execute(); err != nil {
if sterr, ok := err.(cli.StatusError); ok {
if sterr.Status != "" {
fmt.Fprintln(stderr, sterr.Status)
}
// StatusError should only be used for errors, and all errors should
// have a non-zero exit status, so never exit with 0
if sterr.StatusCode == 0 {
os.Exit(1)
}
os.Exit(sterr.StatusCode)
}
fmt.Fprintln(stderr, err)
os.Exit(1)
}
}
在main函数,初始化一个DockerCli对象。client是作为cli的成员出现的,所以我们留意下cli是怎么初始化的。先看NewDockerCli:
// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
func NewDockerCli(in io.ReadCloser, out, err io.Writer) *DockerCli {
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err}
}
很简单,传入输入输出对象,初始化一个cli
接着 cmd := newDockerCommand(dockerCli):
func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
//选项结构对象,之后通过传入的选项选项参数进行填充
opts := cliflags.NewClientOptions()
//选项集合
var flags *pflag.FlagSet
//定义主命令
cmd := &cobra.Command{
Use: "docker [OPTIONS] COMMAND [arg...]",
Short: "A self-sufficient runtime for containers.",
SilenceUsage: true,
SilenceErrors: true,
TraverseChildren: true,
Args: noArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if opts.Version {
showVersion()
return nil
}
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
return nil
},
//在run之前执行
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// flags must be the top-level command flags, not cmd.Flags()
opts.Common.SetDefaultOptions(flags)
dockerPreRun(opts)
//初始化一个client
return dockerCli.Initialize(opts)
},
}
//设置默认的处理方式
cli.SetupRootCommand(cmd)
//为主命令添加选项
flags = cmd.Flags()
flags.BoolVarP(&opts.Version, "version", "v", false, "Print version information and quit")
flags.StringVar(&opts.ConfigDir, "config", cliconfig.ConfigDir(), "Location of client config files")
//为主命令添加公共选项
opts.Common.InstallFlags(flags)
//设置命令的输入
cmd.SetOutput(dockerCli.Out())
//添加daemon选项,其实已经作废了,应为新代码已经将客户端和服务端守护进程命令分开了,不需要这个选项区分启动客户端还是服务端
cmd.AddCommand(newDaemonCommand())
// AddCommands adds all the commands from cli/command to the root command
//添加子命令及子命令的选项
commands.AddCommands(cmd, dockerCli)
return cmd
}
看到:
//在run之前执行
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// flags must be the top-level command flags, not cmd.Flags()
opts.Common.SetDefaultOptions(flags)
dockerPreRun(opts)
//初始化一个client
return dockerCli.Initialize(opts)
},
这部分代码将在命令执行之前被执行,我们看到有dockerCli.Initialize(opts):
// Initialize the dockerCli runs initialization that must happen after command
// line flags are parsed.
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
cli.configFile = LoadDefaultConfigFile(cli.err)
var err error
//生成client对象
cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
if err != nil {
return err
}
if opts.Common.TrustKey == "" {
cli.keyFile = filepath.Join(cliconfig.ConfigDir(), cliflags.DefaultTrustKeyFile)
} else {
cli.keyFile = opts.Common.TrustKey
}
return nil
}
可以看到cli的client就是在这里被赋值的,进入client就可以找到client中实现了我们所有的命令执行函数。