0. 为什么要写docker源码的分析
docker的概念大家都懂,但是只知道docker怎么用似乎并没有卵用,毕竟现在对容器技术炒得火热,但是漫山遍野都是怎么用docker,少有对docker的细节的表述。最完整的是《Docker源码分析》一书,但是时间久远,随着docker版本的迅速迭代,很多地方已经对不上了。
正好在准备秋招,于是趁着自己有找工作的动力和压力,把源码撸一遍,讲的不清楚,也不细,好歹也算对得起自己,要死也要站着死。
docker daemon的初始化
docker首先会进行daemon的初始化,入口在cmd/dockerd/docker.go的main()
函数,主要完成了标准输出流/错误流的设置,通过newDaemonCommand()
接口生成一个新的命令并执行。
函数的最开始有reexec.Init()
判断语句,参考stackoverflow上的提问,大概意思是这个初始化程序只对daemon有效,有了这个就不用docker -d
了。
func main() { if reexec.Init() { return } // Set terminal emulation based on platform as required. _, stdout, stderr := term.StdStreams() // @jhowardmsft - maybe there is a historic reason why on non-Windows, stderr is used // here. However, on Windows it makes no sense and there is no need. if runtime.GOOS == "windows" { logrus.SetOutput(stdout) } else { logrus.SetOutput(stderr) } cmd := newDaemonCommand() cmd.SetOutput(stdout) if err := cmd.Execute(); err != nil { fmt.Fprintf(stderr, "%s\n", err) os.Exit(1) } }
再到newDaemonCommand()
里看,可以看到docker采用了cobra这样一个API管理框架,并且定义了runDaemon
这个函数来运行docker daemon。那么定义的RunE
是在哪儿执行的呢?实际上在将cmd返回之后会进行Execute()
的操作,其中就执行了一系列的PreRunE
, RunE
, PostRunE
等等一系列操作,而这些操作都是在cobra的设置中注册的。
func newDaemonCommand() *cobra.Command { opts := newDaemonOptions(config.New()) cmd := &cobra.Command{ Use: "dockerd [OPTIONS]", Short: "A self-sufficient runtime for containers.", SilenceUsage: true, SilenceErrors: true, Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { opts.flags = cmd.Flags() return runDaemon(opts) }, DisableFlagsInUseLine: true, Version: fmt.Sprintf("%s, build %s", dockerversion.Version, dockerversion.GitCommit), } ... return cmd }
runDaemon
的内容非常简单,对于UNIX系统而言,只需要新建daemonCli,然后启动它。对于windows会复杂一些,暂时不介绍windows的方式。NewDaemonCli()
返回了一个DaemonCli的结构体,里面有配置信息、flag、API server、daemon本身和鉴权有关的一些内容。真正的建立一个daemon的过程集中在start()
中,其中初始化了daemon的一些组件,如API server, router, registry, pluginStore等,并通过NewDaemon()
初始化了daemon,至此DaemonCli的内容已经初始化完毕。
func runDaemon(opts *daemonOptions) error { daemonCli := NewDaemonCli() return daemonCli.start(opts) }// DaemonCli represents the daemon CLI.type DaemonCli struct { *config.Config configFile *string flags *pflag.FlagSet api *apiserver.Server d *daemon.Daemon authzMiddleware *authorization.Middleware // authzMiddleware enables to dynamically reload the authorization plugins}func (cli *DaemonCli) start(opts *daemonOptions) (err error) { // 只贴出关键的代码 opts.SetDefaultOptions(opts.flags) loadDaemonCliConfig(opts) // 配置信息 setDefaultUmask() // Create the daemon root before we create ANY other files (PID, or migrate keys) daemon.CreateDaemonRoot(cli.Config) pidfile.New(cli.Pidfile) // process ID newAPIServerConfig(cli) // 新建API server registry.NewService(cli.Config.ServiceOptions) // 新建registry libcontainerd.New // 新建libcontainerd对象 // Notify that the API is active, but before daemon is set up. preNotifySystem() plugin.NewStore() // plugin // 重点!!!通过config, registry, containerd, pluginStore来真正创建了daemon daemon.NewDaemon(cli.Config, registryService, containerdRemote, pluginStore) validateAuthzPlugins() // 鉴权相关 startMetricsServer() createAndStartCluster() RestartSwarmContainers() newRouterOptions(cli.Config, d) // 配置router go cli.api.Wait(serveAPIWait) // APIServer开始监听、提供服务 // after the daemon is done setting up we can notify systemd api notifySystem() }
需要额外关注一下NewDaemon()
的流程,这是一个守护进程的真正创建者。这段代码非常的长,不过注释挺多的,还比较容易看得出它大概在干啥。整体流程:
设置MTU
检查RootKeyLimit(和能启动的容器数量有关)
检查daemon配置
检查网络环境(linux bridge)
检查platform是否支持(比如darwin, linux, windows等等)
检查当前系统是否满足要求
setupRemappedRoot(User namespaces的隔离,将容器内的用户映射为宿主机上的普通用户)
设置路径名的环境变量
设置异常处理
一些底层的和堆栈有关的设置
设置Seccomp机制(安全机制)
设置AppArmor(同样是安全机制,所以docker就是简单粗暴的把安全机制都堆在一起用?)
初始化与镜像存储相关的目录及Store,默认在/var/lib/docker/containers
设置graphDriver,参考文档
设置registry service, layer store, image store, volumn service,对应镜像、卷
日志的设置
调用
NewClient()
生成一个libcontainerd实例和daemon通信
而NewClient()
被定义在libcontainerd模块中,从代码里能够看到采用的通信方式是GRPC,一旦client建立,就会并发的处理event stream。
func NewDaemon(config *config.Config, registryService registry.Service, containerdRemote libcontainerd.Remote, pluginStore *plugin.Store) (daemon *Daemon, err error) { setDefaultMtu(config) // Ensure that we have a correct root key limit for launching containers. if err := ModifyRootKeyLimit(); err != nil { logrus.Warnf("unable to modify root key limit, number of containers could be limited by this quota: %v", err) } // Ensure we have compatible and valid configuration options if err := verifyDaemonSettings(config); err != nil { return nil, err } // Do we have a disabled network? config.DisableBridge = isBridgeNetworkDisabled(config) // Verify the platform is supported as a daemon if !platformSupported { return nil, errSystemNotSupported } // Validate platform-specific requirements if err := checkSystem(); err != nil { return nil, err } idMappings, err := setupRemappedRoot(config) if err != nil { return nil, err } rootIDs := idMappings.RootPair() if err := setupDaemonProcess(config); err != nil { return nil, err } // set up the tmpDir to use a canonical path tmp, err := prepareTempDir(config.Root, rootIDs) if err != nil { return nil, fmt.Errorf("Unable to get the TempDir under %s: %s", config.Root, err) } realTmp, err := getRealPath(tmp) if err != nil { return nil, fmt.Errorf("Unable to get the full path to the TempDir (%s): %s", tmp, err) } if runtime.GOOS == "windows" { if _, err := os.Stat(realTmp); err != nil && os.IsNotExist(err) { if err := system.MkdirAll(realTmp, 0700, ""); err != nil { return nil, fmt.Errorf("Unable to create the TempDir (%s): %s", realTmp, err) } } os.Setenv("TEMP", realTmp) os.Setenv("TMP", realTmp) } else { os.Setenv("TMPDIR", realTmp) } d := &Daemon{ configStore: config, PluginStore: pluginStore, startupDone: make(chan struct{}), } // Ensure the daemon is properly shutdown if there is a failure during // initialization defer func() { if err != nil { if err := d.Shutdown(); err != nil { logrus.Error(err) } } }() if err := d.setGenericResources(config); err != nil { return nil, err } // set up SIGUSR1 handler on Unix-like systems, or a Win32 global event // on Windows to dump Go routine stacks stackDumpDir := config.Root if execRoot := config.GetExecRoot(); execRoot != "" { stackDumpDir = execRoot } d.setupDumpStackTrap(stackDumpDir) if err := d.setupSeccompProfile(); err != nil { return nil, err } // Set the default isolation mode (only applicable on Windows) if err := d.setDefaultIsolation(); err != nil { return nil, fmt.Errorf("error setting default isolation mode: %v", err) } if err := configureMaxThreads(config); err != nil { logrus.Warnf("Failed to configure golang's threads limit: %v", err) } if err := ensureDefaultAppArmorProfile(); err != nil { logrus.Errorf(err.Error()) } daemonRepo := filepath.Join(config.Root, "containers") if err := idtools.MkdirAllAndChown(daemonRepo, 0700, rootIDs); err != nil { return nil, err } // Create the directory where we'll store the runtime scripts (i.e. in // order to support runtimeArgs) daemonRuntimes := filepath.Join(config.Root, "runtimes") if err := system.MkdirAll(daemonRuntimes, 0700, ""); err != nil { return nil, err } if err := d.loadRuntimes(); err != nil { return nil, err } if runtime.GOOS == "windows" { if err := system.MkdirAll(filepath.Join(config.Root, "credentialspecs"), 0, ""); err != nil { return nil, err } } // On Windows we don't support the environment variable, or a user supplied graphdriver // as Windows has no choice in terms of which graphdrivers to use. It's a case of // running Windows containers on Windows - windowsfilter, running Linux containers on Windows, // lcow. Unix platforms however run a single graphdriver for all containers, and it can // be set through an environment variable, a daemon start parameter, or chosen through // initialization of the layerstore through driver priority order for example. d.graphDrivers = make(map[string]string) layerStores := make(map[string]layer.Store) if runtime.GOOS == "windows" { d.graphDrivers[runtime.GOOS] = "windowsfilter" if system.LCOWSupported() { d.graphDrivers["linux"] = "lcow" } } else { driverName := os.Getenv("DOCKER_DRIVER") if driverName == "" { driverName = config.GraphDriver } else { logrus.Infof("Setting the storage driver from the $DOCKER_DRIVER environment variable (%s)", driverName) } d.graphDrivers[runtime.GOOS] = driverName // May still be empty. Layerstore init determines instead. } d.RegistryService = registryService logger.RegisterPluginGetter(d.PluginStore) metricsSockPath, err := d.listenMetricsSock() if err != nil { return nil, err } registerMetricsPluginCallback(d.PluginStore, metricsSockPath) createPluginExec := func(m *plugin.Manager) (plugin.Executor, error) { return pluginexec.New(getPluginExecRoot(config.Root), containerdRemote, m) } // Plugin system initialization should happen before restore. Do not change order. d.pluginManager, err = plugin.NewManager(plugin.ManagerConfig{ Root: filepath.Join(config.Root, "plugins"), ExecRoot: getPluginExecRoot(config.Root), Store: d.PluginStore, CreateExecutor: createPluginExec, RegistryService: registryService, LiveRestoreEnabled: config.LiveRestoreEnabled, LogPluginEvent: d.LogPluginEvent, // todo: make private AuthzMiddleware: config.AuthzMiddleware, }) if err != nil { return nil, errors.Wrap(err, "couldn't create plugin manager") } if err := d.setupDefaultLogConfig(); err != nil { return nil, err } for operatingSystem, gd := range d.graphDrivers { layerStores[operatingSystem], err = layer.NewStoreFromOptions(layer.StoreOptions{ Root: config.Root, MetadataStorePathTemplate: filepath.Join(config.Root, "image", "%s", "layerdb"), GraphDriver: gd, GraphDriverOptions: config.GraphOptions, IDMappings: idMappings, PluginGetter: d.PluginStore, ExperimentalEnabled: config.Experimental, OS: operatingSystem, }) if err != nil { return nil, err } } // As layerstore initialization may set the driver for os := range d.graphDrivers { d.graphDrivers[os] = layerStores[os].DriverName() } // Configure and validate the kernels security support. Note this is a Linux/FreeBSD // operation only, so it is safe to pass *just* the runtime OS graphdriver. if err := configureKernelSecuritySupport(config, d.graphDrivers[runtime.GOOS]); err != nil { return nil, err } imageRoot := filepath.Join(config.Root, "image", d.graphDrivers[runtime.GOOS]) ifs, err := image.NewFSStoreBackend(filepath.Join(imageRoot, "imagedb")) if err != nil { return nil, err } lgrMap := make(map[string]image.LayerGetReleaser) for os, ls := range layerStores { lgrMap[os] = ls } imageStore, err := image.NewImageStore(ifs, lgrMap) if err != nil { return nil, err } d.volumes, err = volumesservice.NewVolumeService(config.Root, d.PluginStore, rootIDs, d) if err != nil { return nil, err } trustKey, err := loadOrCreateTrustKey(config.TrustKeyPath) if err != nil { return nil, err } trustDir := filepath.Join(config.Root, "trust") if err := system.MkdirAll(trustDir, 0700, ""); err != nil { return nil, err } // We have a single tag/reference store for the daemon globally. However, it's // stored under the graphdriver. On host platforms which only support a single // container OS, but multiple selectable graphdrivers, this means depending on which // graphdriver is chosen, the global reference store is under there. For // platforms which support multiple container operating systems, this is slightly // more problematic as where does the global ref store get located? Fortunately, // for Windows, which is currently the only daemon supporting multiple container // operating systems, the list of graphdrivers available isn't user configurable. // For backwards compatibility, we just put it under the windowsfilter // directory regardless. refStoreLocation := filepath.Join(imageRoot, `repositories.json`) rs, err := refstore.NewReferenceStore(refStoreLocation) if err != nil { return nil, fmt.Errorf("Couldn't create reference store repository: %s", err) } distributionMetadataStore, err := dmetadata.NewFSMetadataStore(filepath.Join(imageRoot, "distribution")) if err != nil { return nil, err } // No content-addressability migration on Windows as it never supported pre-CA if runtime.GOOS != "windows" { migrationStart := time.Now() if err := v1.Migrate(config.Root, d.graphDrivers[runtime.GOOS], layerStores[runtime.GOOS], imageStore, rs, distributionMetadataStore); err != nil { logrus.Errorf("Graph migration failed: %q. Your old graph data was found to be too inconsistent for upgrading to content-addressable storage. Some of the old data was probably not upgraded. We recommend starting over with a clean storage directory if possible.", err) } logrus.Infof("Graph migration to content-addressability took %.2f seconds", time.Since(migrationStart).Seconds()) } // Discovery is only enabled when the daemon is launched with an address to advertise. When // initialized, the daemon is registered and we can store the discovery backend as it's read-only if err := d.initDiscovery(config); err != nil { return nil, err } sysInfo := sysinfo.New(false) // Check if Devices cgroup is mounted, it is hard requirement for container security, // on Linux. if runtime.GOOS == "linux" && !sysInfo.CgroupDevicesEnabled { return nil, errors.New("Devices cgroup isn't mounted") } d.ID = trustKey.PublicKey().KeyID() d.repository = daemonRepo d.containers = container.NewMemoryStore() if d.containersReplica, err = container.NewViewDB(); err != nil { return nil, err } d.execCommands = exec.NewStore() d.idIndex = truncindex.NewTruncIndex([]string{}) d.statsCollector = d.newStatsCollector(1 * time.Second) d.EventsService = events.New() d.root = config.Root d.idMappings = idMappings d.seccompEnabled = sysInfo.Seccomp d.apparmorEnabled = sysInfo.AppArmor d.linkIndex = newLinkIndex() // TODO: imageStore, distributionMetadataStore, and ReferenceStore are only // used above to run migration. They could be initialized in ImageService // if migration is called from daemon/images. layerStore might move as well. d.imageService = images.NewImageService(images.ImageServiceConfig{ ContainerStore: d.containers, DistributionMetadataStore: distributionMetadataStore, EventsService: d.EventsService, ImageStore: imageStore, LayerStores: layerStores, MaxConcurrentDownloads: *config.MaxConcurrentDownloads, MaxConcurrentUploads: *config.MaxConcurrentUploads, ReferenceStore: rs, RegistryService: registryService, TrustKey: trustKey, }) go d.execCommandGC() d.containerd, err = containerdRemote.NewClient(ContainersNamespace, d) if err != nil { return nil, err } if err := d.restore(); err != nil { return nil, err } close(d.startupDone) // FIXME: this method never returns an error info, _ := d.SystemInfo() engineInfo.WithValues( dockerversion.Version, dockerversion.GitCommit, info.Architecture, info.Driver, info.KernelVersion, info.OperatingSystem, info.OSType, info.ID, ).Set(1) engineCpus.Set(float64(info.NCPU)) engineMemory.Set(float64(info.MemTotal)) gd := "" for os, driver := range d.graphDrivers { if len(gd) > 0 { gd += ", " } gd += driver if len(d.graphDrivers) > 1 { gd = fmt.Sprintf("%s (%s)", gd, os) } } logrus.WithFields(logrus.Fields{ "version": dockerversion.Version, "commit": dockerversion.GitCommit, "graphdriver(s)": gd, }).Info("Docker daemon") return d, nil}func (r *remote) NewClient(ns string, b Backend) (Client, error) { c := &client{ stateDir: r.stateDir, logger: r.logger.WithField("namespace", ns), namespace: ns, backend: b, containers: make(map[string]*container), } rclient, err := containerd.New(r.GRPC.Address, containerd.WithDefaultNamespace(ns)) if err != nil { return nil, err } c.remote = rclient go c.processEventStream(r.shutdownContext) r.Lock() r.clients = append(r.clients, c) r.Unlock() return c, nil}
至此,docker daemon初始化完毕。
docker client的初始化
发现一个坑,moby中的代码并不全,在目录cmd/下只有dockerd文件夹,而没有docker文件夹。对比docker/docker-ce可以发现,后者在components目录下包含了cli和engine两部分,engine对应的是moby项目,cli对应的是docker-ce和docker-ee采用的客户端的代码。docker将moby从docker的整体中分离出去,docker-ce作为独立的产品存在。而moby甚至没有包括docker client的代码。
client的初始化相对简单,在cli/cmd/docker/docker.go里面。许多版本之前daemon和client是共用一个可执行文件的,现在已经分开了,但是基本流程还是比较类似。上文提到过reexec只在daemon初始化才执行,在client的初始化中,首先进行标准输入输出流的配置,之后通过NewDockerCli
新建client,再解析出命令并Execute()
,完成一个request的生命周期。
func main() { // Set terminal emulation based on platform as required. stdin, stdout, stderr := term.StdStreams() logrus.SetOutput(stderr) dockerCli := command.NewDockerCli(stdin, stdout, stderr, contentTrustEnabled()) 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) } }
NewDockerCli
的作用是返回一个DockerCli的实例,并设置好它的IO
// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
DockerCli实例包含的内容不多,除了配置文件外,有I/O和标准错误输出,还包含了server和client的info以及一个client instance。配置文件在~/.docker/config.json中;APIClient包含了common和experimental两类,也符合我们对docker功能的认知。
type APIClient interface { CommonAPIClient apiClientExperimental }type DockerCli struct { configFile *configfile.ConfigFile in *InStream out *OutStream err io.Writer client client.APIClient serverInfo ServerInfo clientInfo ClientInfo contentTrust bool}
NewDockerCommand
同时生成一个可供server执行的docker命令。和daemon的分析类似,这里定义的PersistentPreRunE
是之后在执行时调用的handler,它根据flags进行参数的设置,之后的dockerPreRun
也只是参数设置的环节。真正将client初始化是在Initialize
里,它通过NewAPIClientFromFlags
中的NewClientWithOpts
真正新建了client,并判断是否需要启用experimental的功能,最后调用initializeFromClient()
用ping的方式判断和daemon的连接是否建立。之后return的是一个bool类型,判断command是否被client支持。简而言之,这个函数做了docker命令被送往server执行前的准备和检查工作。
另一方面,cmd的生成在之后进行,通过flags来进行cmd的设置,将cmd的输出绑定到dockerCli的输出等等,最后返回了cmd本身。
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, 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) if err := dockerCli.Initialize(opts); err != nil { return err } return isSupported(cmd, dockerCli) }, Version: fmt.Sprintf("%s, build %s", cli.Version, cli.GitCommit), DisableFlagsInUseLine: true, } cli.SetupRootCommand(cmd) flags = cmd.Flags() flags.BoolP("version", "v", false, "Print version information and quit") flags.StringVar(&opts.ConfigDir, "config", cliconfig.Dir(), "Location of client config files") opts.Common.InstallFlags(flags) setFlagErrorFunc(dockerCli, cmd, flags, opts) setHelpFunc(dockerCli, cmd, flags, opts) cmd.SetOutput(dockerCli.Out()) commands.AddCommands(cmd, dockerCli) disableFlagsInUseLine(cmd) setValidateArgs(dockerCli, cmd, flags, opts) return cmd }
还有一个要注意的地方,docker如何知道用户的什么命令代表什么操作呢,这些处理函数的注册在SetupRootCommand()
里,会把interface.go里定义的操作注册到cmd里,这样server就知道什么cmd对应着什么样的处理函数。而command最终被发送到server的逻辑也都包装在interface的函数中,比如ContainerCreate
中有一句serverResp, err := cli.post(ctx, "/containers/create", query, body, nil)
。简而言之,docker client在初始化的过程中为cmd注册了不同的handler,根据用户输入的flags生成正确的cmd,并查找到应该执行哪个handler,在handler中负责将cmd发送到server,而server收到后就由daemon进行处理(具体步骤以后分析)。
func SetupRootCommand(rootCmd *cobra.Command) { cobra.AddTemplateFunc("hasSubCommands", hasSubCommands) cobra.AddTemplateFunc("hasManagementSubCommands", hasManagementSubCommands) cobra.AddTemplateFunc("operationSubCommands", operationSubCommands) cobra.AddTemplateFunc("managementSubCommands", managementSubCommands) cobra.AddTemplateFunc("wrappedFlagUsages", wrappedFlagUsages) rootCmd.SetUsageTemplate(usageTemplate) rootCmd.SetHelpTemplate(helpTemplate) rootCmd.SetFlagErrorFunc(FlagErrorFunc) rootCmd.SetHelpCommand(helpCommand) rootCmd.SetVersionTemplate("Docker version {{.Version}}\n") rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage") rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help") rootCmd.PersistentFlags().Lookup("help").Hidden = true}
而moby中的client这个文件夹下定义了所有docker client的API操作,常用的docker都可以找到具体实现。按照代码的说明:
You use the library by creating a client object and calling methods on it. The
client can be created either from environment variables with NewEnvClient, or
configured manually with NewClient.
docker为我们提供了两种初始化client的方法,一种是通过环境变量,一种是通过手动配置。首先看一下client结构体的定义。client包含了下列成员:
scheme: HTTP or HTTPS
host: server的地址
proto: client与server的协议,如unix socket等
addr: client的地址
basePath
client: 真正收发消息的客户端
version: server的版本号,兼容性?
customHTTPHeaders: 用户配置的HTTP头
manualOverride: 用户自己设置version时设为true
新建client的API有两种,分别是NewEnvClient()
和NewClient()
,但是可以看到第一种已经作废,这两种高层的API都将调用NewClientWithOpts()
,从而将新建client的API统一。具体为用FromEnv
获取环境变量配置client。支持的环境变量包括四种,分别是:
Supported environment variables:
DOCKER_HOST to set the url to the docker server.
DOCKER_API_VERSION to set the version of the API to reach, leave empty for latest.
DOCKER_CERT_PATH to load the TLS certificates from.
DOCKER_TLS_VERIFY to enable or disable TLS verification, off by default.
在NewClientWithOpts
中,调用了defaultHTTPClient
来初始化client,再根据输入的变长参数将配置覆盖。
// Deprecated: use NewClientWithOpts(FromEnv)func NewEnvClient() (*Client, error) { return NewClientWithOpts(FromEnv) }func NewClient(host string, version string, client *http.Client, httpHeaders map[string]string) (*Client, error) { return NewClientWithOpts(WithHost(host), WithVersion(version), WithHTTPClient(client), WithHTTPHeaders(httpHeaders)) }func NewClientWithOpts(ops ...func(*Client) error) (*Client, error) { client, err := defaultHTTPClient(DefaultDockerHost) if err != nil { return nil, err } c := &Client{ host: DefaultDockerHost, version: api.DefaultVersion, scheme: "http", client: client, proto: defaultProto, addr: defaultAddr, } for _, op := range ops { if err := op(c); err != nil { return nil, err } } if _, ok := c.client.Transport.(http.RoundTripper); !ok { return nil, fmt.Errorf("unable to verify TLS configuration, invalid transport %v", c.client.Transport) } tlsConfig := resolveTLSConfig(c.client.Transport) if tlsConfig != nil { // TODO(stevvooe): This isn't really the right way to write clients in Go. // `NewClient` should probably only take an `*http.Client` and work from there. // Unfortunately, the model of having a host-ish/url-thingy as the connection // string has us confusing protocol and transport layers. We continue doing // this to avoid breaking existing clients but this should be addressed. c.scheme = "https" } return c, nil}
作者:lovenashbest
链接:https://www.jianshu.com/p/2519ac06aa1d
热门评论
你是真的牛哔,别人看原理,你撸代码