github.com/AliyunContainerService/cli@v0.0.0-20181009023821-814ced4b30d0/cli/command/cli.go (about) 1 package command 2 3 import ( 4 "context" 5 "io" 6 "net" 7 "net/http" 8 "os" 9 "path/filepath" 10 "runtime" 11 "time" 12 13 "github.com/docker/cli/cli" 14 "github.com/docker/cli/cli/config" 15 cliconfig "github.com/docker/cli/cli/config" 16 "github.com/docker/cli/cli/config/configfile" 17 "github.com/docker/cli/cli/connhelper" 18 cliflags "github.com/docker/cli/cli/flags" 19 manifeststore "github.com/docker/cli/cli/manifest/store" 20 registryclient "github.com/docker/cli/cli/registry/client" 21 "github.com/docker/cli/cli/trust" 22 dopts "github.com/docker/cli/opts" 23 clitypes "github.com/docker/cli/types" 24 "github.com/docker/docker/api" 25 "github.com/docker/docker/api/types" 26 registrytypes "github.com/docker/docker/api/types/registry" 27 "github.com/docker/docker/client" 28 "github.com/docker/go-connections/tlsconfig" 29 "github.com/pkg/errors" 30 "github.com/spf13/cobra" 31 "github.com/theupdateframework/notary" 32 notaryclient "github.com/theupdateframework/notary/client" 33 "github.com/theupdateframework/notary/passphrase" 34 ) 35 36 // Streams is an interface which exposes the standard input and output streams 37 type Streams interface { 38 In() *InStream 39 Out() *OutStream 40 Err() io.Writer 41 } 42 43 // Cli represents the docker command line client. 44 type Cli interface { 45 Client() client.APIClient 46 Out() *OutStream 47 Err() io.Writer 48 In() *InStream 49 SetIn(in *InStream) 50 ConfigFile() *configfile.ConfigFile 51 ServerInfo() ServerInfo 52 ClientInfo() ClientInfo 53 NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) 54 DefaultVersion() string 55 ManifestStore() manifeststore.Store 56 RegistryClient(bool) registryclient.RegistryClient 57 ContentTrustEnabled() bool 58 NewContainerizedEngineClient(sockPath string) (clitypes.ContainerizedClient, error) 59 } 60 61 // DockerCli is an instance the docker command line client. 62 // Instances of the client can be returned from NewDockerCli. 63 type DockerCli struct { 64 configFile *configfile.ConfigFile 65 in *InStream 66 out *OutStream 67 err io.Writer 68 client client.APIClient 69 serverInfo ServerInfo 70 clientInfo ClientInfo 71 contentTrust bool 72 newContainerizeClient func(string) (clitypes.ContainerizedClient, error) 73 } 74 75 // DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified. 76 func (cli *DockerCli) DefaultVersion() string { 77 return cli.clientInfo.DefaultVersion 78 } 79 80 // Client returns the APIClient 81 func (cli *DockerCli) Client() client.APIClient { 82 return cli.client 83 } 84 85 // Out returns the writer used for stdout 86 func (cli *DockerCli) Out() *OutStream { 87 return cli.out 88 } 89 90 // Err returns the writer used for stderr 91 func (cli *DockerCli) Err() io.Writer { 92 return cli.err 93 } 94 95 // SetIn sets the reader used for stdin 96 func (cli *DockerCli) SetIn(in *InStream) { 97 cli.in = in 98 } 99 100 // In returns the reader used for stdin 101 func (cli *DockerCli) In() *InStream { 102 return cli.in 103 } 104 105 // ShowHelp shows the command help. 106 func ShowHelp(err io.Writer) func(*cobra.Command, []string) error { 107 return func(cmd *cobra.Command, args []string) error { 108 cmd.SetOutput(err) 109 cmd.HelpFunc()(cmd, args) 110 return nil 111 } 112 } 113 114 // ConfigFile returns the ConfigFile 115 func (cli *DockerCli) ConfigFile() *configfile.ConfigFile { 116 return cli.configFile 117 } 118 119 // ServerInfo returns the server version details for the host this client is 120 // connected to 121 func (cli *DockerCli) ServerInfo() ServerInfo { 122 return cli.serverInfo 123 } 124 125 // ClientInfo returns the client details for the cli 126 func (cli *DockerCli) ClientInfo() ClientInfo { 127 return cli.clientInfo 128 } 129 130 // ContentTrustEnabled returns whether content trust has been enabled by an 131 // environment variable. 132 func (cli *DockerCli) ContentTrustEnabled() bool { 133 return cli.contentTrust 134 } 135 136 // ManifestStore returns a store for local manifests 137 func (cli *DockerCli) ManifestStore() manifeststore.Store { 138 // TODO: support override default location from config file 139 return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests")) 140 } 141 142 // RegistryClient returns a client for communicating with a Docker distribution 143 // registry 144 func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient { 145 resolver := func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig { 146 return ResolveAuthConfig(ctx, cli, index) 147 } 148 return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure) 149 } 150 151 // Initialize the dockerCli runs initialization that must happen after command 152 // line flags are parsed. 153 func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { 154 cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err) 155 156 var err error 157 cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile) 158 if tlsconfig.IsErrEncryptedKey(err) { 159 passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil) 160 newClient := func(password string) (client.APIClient, error) { 161 opts.Common.TLSOptions.Passphrase = password 162 return NewAPIClientFromFlags(opts.Common, cli.configFile) 163 } 164 cli.client, err = getClientWithPassword(passRetriever, newClient) 165 } 166 if err != nil { 167 return err 168 } 169 var experimentalValue string 170 // Environment variable always overrides configuration 171 if experimentalValue = os.Getenv("DOCKER_CLI_EXPERIMENTAL"); experimentalValue == "" { 172 experimentalValue = cli.configFile.Experimental 173 } 174 hasExperimental, err := isEnabled(experimentalValue) 175 if err != nil { 176 return errors.Wrap(err, "Experimental field") 177 } 178 cli.clientInfo = ClientInfo{ 179 DefaultVersion: cli.client.ClientVersion(), 180 HasExperimental: hasExperimental, 181 } 182 cli.initializeFromClient() 183 return nil 184 } 185 186 func isEnabled(value string) (bool, error) { 187 switch value { 188 case "enabled": 189 return true, nil 190 case "", "disabled": 191 return false, nil 192 default: 193 return false, errors.Errorf("%q is not valid, should be either enabled or disabled", value) 194 } 195 } 196 197 func (cli *DockerCli) initializeFromClient() { 198 ping, err := cli.client.Ping(context.Background()) 199 if err != nil { 200 // Default to true if we fail to connect to daemon 201 cli.serverInfo = ServerInfo{HasExperimental: true} 202 203 if ping.APIVersion != "" { 204 cli.client.NegotiateAPIVersionPing(ping) 205 } 206 return 207 } 208 209 cli.serverInfo = ServerInfo{ 210 HasExperimental: ping.Experimental, 211 OSType: ping.OSType, 212 BuildkitVersion: ping.BuilderVersion, 213 } 214 cli.client.NegotiateAPIVersionPing(ping) 215 } 216 217 func getClientWithPassword(passRetriever notary.PassRetriever, newClient func(password string) (client.APIClient, error)) (client.APIClient, error) { 218 for attempts := 0; ; attempts++ { 219 passwd, giveup, err := passRetriever("private", "encrypted TLS private", false, attempts) 220 if giveup || err != nil { 221 return nil, errors.Wrap(err, "private key is encrypted, but could not get passphrase") 222 } 223 224 apiclient, err := newClient(passwd) 225 if !tlsconfig.IsErrEncryptedKey(err) { 226 return apiclient, err 227 } 228 } 229 } 230 231 // NotaryClient provides a Notary Repository to interact with signed metadata for an image 232 func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) { 233 return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...) 234 } 235 236 // NewContainerizedEngineClient returns a containerized engine client 237 func (cli *DockerCli) NewContainerizedEngineClient(sockPath string) (clitypes.ContainerizedClient, error) { 238 return cli.newContainerizeClient(sockPath) 239 } 240 241 // ServerInfo stores details about the supported features and platform of the 242 // server 243 type ServerInfo struct { 244 HasExperimental bool 245 OSType string 246 BuildkitVersion types.BuilderVersion 247 } 248 249 // ClientInfo stores details about the supported features of the client 250 type ClientInfo struct { 251 HasExperimental bool 252 DefaultVersion string 253 } 254 255 // NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err. 256 func NewDockerCli(in io.ReadCloser, out, err io.Writer, isTrusted bool, containerizedFn func(string) (clitypes.ContainerizedClient, error)) *DockerCli { 257 return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err, contentTrust: isTrusted, newContainerizeClient: containerizedFn} 258 } 259 260 // NewAPIClientFromFlags creates a new APIClient from command line flags 261 func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) { 262 unparsedHost, err := getUnparsedServerHost(opts.Hosts) 263 if err != nil { 264 return &client.Client{}, err 265 } 266 var clientOpts []func(*client.Client) error 267 helper, err := connhelper.GetConnectionHelper(unparsedHost) 268 if err != nil { 269 return &client.Client{}, err 270 } 271 if helper == nil { 272 clientOpts = append(clientOpts, withHTTPClient(opts.TLSOptions)) 273 host, err := dopts.ParseHost(opts.TLSOptions != nil, unparsedHost) 274 if err != nil { 275 return &client.Client{}, err 276 } 277 clientOpts = append(clientOpts, client.WithHost(host)) 278 } else { 279 clientOpts = append(clientOpts, func(c *client.Client) error { 280 httpClient := &http.Client{ 281 // No tls 282 // No proxy 283 Transport: &http.Transport{ 284 DialContext: helper.Dialer, 285 }, 286 } 287 return client.WithHTTPClient(httpClient)(c) 288 }) 289 clientOpts = append(clientOpts, client.WithHost(helper.Host)) 290 clientOpts = append(clientOpts, client.WithDialContext(helper.Dialer)) 291 } 292 293 customHeaders := configFile.HTTPHeaders 294 if customHeaders == nil { 295 customHeaders = map[string]string{} 296 } 297 customHeaders["User-Agent"] = UserAgent() 298 clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders)) 299 300 verStr := api.DefaultVersion 301 if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" { 302 verStr = tmpStr 303 } 304 clientOpts = append(clientOpts, client.WithVersion(verStr)) 305 306 return client.NewClientWithOpts(clientOpts...) 307 } 308 309 func getUnparsedServerHost(hosts []string) (string, error) { 310 var host string 311 switch len(hosts) { 312 case 0: 313 host = os.Getenv("DOCKER_HOST") 314 case 1: 315 host = hosts[0] 316 default: 317 return "", errors.New("Please specify only one -H") 318 } 319 return host, nil 320 } 321 322 func withHTTPClient(tlsOpts *tlsconfig.Options) func(*client.Client) error { 323 return func(c *client.Client) error { 324 if tlsOpts == nil { 325 // Use the default HTTPClient 326 return nil 327 } 328 329 opts := *tlsOpts 330 opts.ExclusiveRootPools = true 331 tlsConfig, err := tlsconfig.Client(opts) 332 if err != nil { 333 return err 334 } 335 336 httpClient := &http.Client{ 337 Transport: &http.Transport{ 338 TLSClientConfig: tlsConfig, 339 DialContext: (&net.Dialer{ 340 KeepAlive: 30 * time.Second, 341 Timeout: 30 * time.Second, 342 }).DialContext, 343 }, 344 CheckRedirect: client.CheckRedirect, 345 } 346 return client.WithHTTPClient(httpClient)(c) 347 } 348 } 349 350 // UserAgent returns the user agent string used for making API requests 351 func UserAgent() string { 352 return "Docker-Client/" + cli.Version + " (" + runtime.GOOS + ")" 353 }