github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/engine/client/client.go (about) 1 /* 2 Package client is a Go client for the Docker Engine API. 3 4 For more information about the Engine API, see the documentation: 5 https://docs.docker.com/engine/api/ 6 7 # Usage 8 9 You use the library by creating a client object and calling methods on it. The 10 client can be created either from environment variables with NewClientWithOpts(client.FromEnv), 11 or configured manually with NewClient(). 12 13 For example, to list running containers (the equivalent of "docker ps"): 14 15 package main 16 17 import ( 18 "context" 19 "fmt" 20 21 "github.com/docker/docker/api/types" 22 "github.com/docker/docker/client" 23 ) 24 25 func main() { 26 cli, err := client.NewClientWithOpts(client.FromEnv) 27 if err != nil { 28 panic(err) 29 } 30 31 containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{}) 32 if err != nil { 33 panic(err) 34 } 35 36 for _, container := range containers { 37 fmt.Printf("%s %s\n", container.ID[:10], container.Image) 38 } 39 } 40 */ 41 package client // import "github.com/docker/docker/client" 42 43 import ( 44 "context" 45 "fmt" 46 "net" 47 "net/http" 48 "net/url" 49 "path" 50 "strings" 51 52 "github.com/docker/docker/api" 53 "github.com/docker/docker/api/types" 54 "github.com/docker/docker/api/types/versions" 55 "github.com/docker/go-connections/sockets" 56 "github.com/pkg/errors" 57 ) 58 59 // ErrRedirect is the error returned by checkRedirect when the request is non-GET. 60 var ErrRedirect = errors.New("unexpected redirect in response") 61 62 // Client is the API client that performs all operations 63 // against a docker server. 64 type Client struct { 65 // scheme sets the scheme for the client 66 scheme string 67 // host holds the server address to connect to 68 host string 69 // proto holds the client protocol i.e. unix. 70 proto string 71 // addr holds the client address. 72 addr string 73 // basePath holds the path to prepend to the requests. 74 basePath string 75 // client used to send and receive http requests. 76 client *http.Client 77 // version of the server to talk to. 78 version string 79 // custom http headers configured by users. 80 customHTTPHeaders map[string]string 81 // manualOverride is set to true when the version was set by users. 82 manualOverride bool 83 84 // negotiateVersion indicates if the client should automatically negotiate 85 // the API version to use when making requests. API version negotiation is 86 // performed on the first request, after which negotiated is set to "true" 87 // so that subsequent requests do not re-negotiate. 88 negotiateVersion bool 89 90 // negotiated indicates that API version negotiation took place 91 negotiated bool 92 } 93 94 // CheckRedirect specifies the policy for dealing with redirect responses: 95 // If the request is non-GET return `ErrRedirect`. Otherwise use the last response. 96 // 97 // Go 1.8 changes behavior for HTTP redirects (specifically 301, 307, and 308) in the client . 98 // The Docker client (and by extension docker API client) can be made to send a request 99 // like POST /containers//start where what would normally be in the name section of the URL is empty. 100 // This triggers an HTTP 301 from the daemon. 101 // In go 1.8 this 301 will be converted to a GET request, and ends up getting a 404 from the daemon. 102 // This behavior change manifests in the client in that before the 301 was not followed and 103 // the client did not generate an error, but now results in a message like Error response from daemon: page not found. 104 func CheckRedirect(req *http.Request, via []*http.Request) error { 105 if via[0].Method == http.MethodGet { 106 return http.ErrUseLastResponse 107 } 108 return ErrRedirect 109 } 110 111 // NewClientWithOpts initializes a new API client with default values. It takes functors 112 // to modify values when creating it, like `NewClientWithOpts(WithVersion(…))` 113 // It also initializes the custom http headers to add to each request. 114 // 115 // It won't send any version information if the version number is empty. It is 116 // highly recommended that you set a version or your client may break if the 117 // server is upgraded. 118 func NewClientWithOpts(ops ...Opt) (*Client, error) { 119 client, err := defaultHTTPClient(DefaultDockerHost) 120 if err != nil { 121 return nil, err 122 } 123 c := &Client{ 124 host: DefaultDockerHost, 125 version: api.DefaultVersion, 126 client: client, 127 proto: defaultProto, 128 addr: defaultAddr, 129 } 130 131 for _, op := range ops { 132 if err := op(c); err != nil { 133 return nil, err 134 } 135 } 136 137 if c.scheme == "" { 138 c.scheme = "http" 139 140 tlsConfig := resolveTLSConfig(c.client.Transport) 141 if tlsConfig != nil { 142 // TODO(stevvooe): This isn't really the right way to write clients in Go. 143 // `NewClient` should probably only take an `*http.Client` and work from there. 144 // Unfortunately, the model of having a host-ish/url-thingy as the connection 145 // string has us confusing protocol and transport layers. We continue doing 146 // this to avoid breaking existing clients but this should be addressed. 147 c.scheme = "https" 148 } 149 } 150 151 return c, nil 152 } 153 154 func defaultHTTPClient(host string) (*http.Client, error) { 155 url, err := ParseHostURL(host) 156 if err != nil { 157 return nil, err 158 } 159 transport := new(http.Transport) 160 sockets.ConfigureTransport(transport, url.Scheme, url.Host) 161 return &http.Client{ 162 Transport: transport, 163 CheckRedirect: CheckRedirect, 164 }, nil 165 } 166 167 // Close the transport used by the client 168 func (cli *Client) Close() error { 169 if t, ok := cli.client.Transport.(*http.Transport); ok { 170 t.CloseIdleConnections() 171 } 172 return nil 173 } 174 175 // getAPIPath returns the versioned request path to call the api. 176 // It appends the query parameters to the path if they are not empty. 177 func (cli *Client) getAPIPath(ctx context.Context, p string, query url.Values) string { 178 var apiPath string 179 if cli.negotiateVersion && !cli.negotiated { 180 cli.NegotiateAPIVersion(ctx) 181 } 182 if cli.version != "" { 183 v := strings.TrimPrefix(cli.version, "v") 184 apiPath = path.Join(cli.basePath, "/v"+v, p) 185 } else { 186 apiPath = path.Join(cli.basePath, p) 187 } 188 return (&url.URL{Path: apiPath, RawQuery: query.Encode()}).String() 189 } 190 191 // ClientVersion returns the API version used by this client. 192 func (cli *Client) ClientVersion() string { 193 return cli.version 194 } 195 196 // NegotiateAPIVersion queries the API and updates the version to match the 197 // API version. Any errors are silently ignored. If a manual override is in place, 198 // either through the `DOCKER_API_VERSION` environment variable, or if the client 199 // was initialized with a fixed version (`opts.WithVersion(xx)`), no negotiation 200 // will be performed. 201 func (cli *Client) NegotiateAPIVersion(ctx context.Context) { 202 if !cli.manualOverride { 203 ping, _ := cli.Ping(ctx) 204 cli.negotiateAPIVersionPing(ping) 205 } 206 } 207 208 // NegotiateAPIVersionPing updates the client version to match the Ping.APIVersion 209 // if the ping version is less than the default version. If a manual override is 210 // in place, either through the `DOCKER_API_VERSION` environment variable, or if 211 // the client was initialized with a fixed version (`opts.WithVersion(xx)`), no 212 // negotiation is performed. 213 func (cli *Client) NegotiateAPIVersionPing(p types.Ping) { 214 if !cli.manualOverride { 215 cli.negotiateAPIVersionPing(p) 216 } 217 } 218 219 // negotiateAPIVersionPing queries the API and updates the version to match the 220 // API version. Any errors are silently ignored. 221 func (cli *Client) negotiateAPIVersionPing(p types.Ping) { 222 // try the latest version before versioning headers existed 223 if p.APIVersion == "" { 224 p.APIVersion = "1.24" 225 } 226 227 // if the client is not initialized with a version, start with the latest supported version 228 if cli.version == "" { 229 cli.version = api.DefaultVersion 230 } 231 232 // if server version is lower than the client version, downgrade 233 if versions.LessThan(p.APIVersion, cli.version) { 234 cli.version = p.APIVersion 235 } 236 237 // Store the results, so that automatic API version negotiation (if enabled) 238 // won't be performed on the next request. 239 if cli.negotiateVersion { 240 cli.negotiated = true 241 } 242 } 243 244 // DaemonHost returns the host address used by the client 245 func (cli *Client) DaemonHost() string { 246 return cli.host 247 } 248 249 // HTTPClient returns a copy of the HTTP client bound to the server 250 func (cli *Client) HTTPClient() *http.Client { 251 c := *cli.client 252 return &c 253 } 254 255 // ParseHostURL parses a url string, validates the string is a host url, and 256 // returns the parsed URL 257 func ParseHostURL(host string) (*url.URL, error) { 258 protoAddrParts := strings.SplitN(host, "://", 2) 259 if len(protoAddrParts) == 1 { 260 return nil, fmt.Errorf("unable to parse docker host `%s`", host) 261 } 262 263 var basePath string 264 proto, addr := protoAddrParts[0], protoAddrParts[1] 265 if proto == "tcp" { 266 parsed, err := url.Parse("tcp://" + addr) 267 if err != nil { 268 return nil, err 269 } 270 addr = parsed.Host 271 basePath = parsed.Path 272 } 273 return &url.URL{ 274 Scheme: proto, 275 Host: addr, 276 Path: basePath, 277 }, nil 278 } 279 280 // CustomHTTPHeaders returns the custom http headers stored by the client. 281 func (cli *Client) CustomHTTPHeaders() map[string]string { 282 m := make(map[string]string) 283 for k, v := range cli.customHTTPHeaders { 284 m[k] = v 285 } 286 return m 287 } 288 289 // SetCustomHTTPHeaders that will be set on every HTTP request made by the client. 290 // Deprecated: use WithHTTPHeaders when creating the client. 291 func (cli *Client) SetCustomHTTPHeaders(headers map[string]string) { 292 cli.customHTTPHeaders = headers 293 } 294 295 // Dialer returns a dialer for a raw stream connection, with HTTP/1.1 header, that can be used for proxying the daemon connection. 296 // Used by `docker dial-stdio` (docker/cli#889). 297 func (cli *Client) Dialer() func(context.Context) (net.Conn, error) { 298 return func(ctx context.Context) (net.Conn, error) { 299 if transport, ok := cli.client.Transport.(*http.Transport); ok { 300 if transport.DialContext != nil && transport.TLSClientConfig == nil { 301 return transport.DialContext(ctx, cli.proto, cli.addr) 302 } 303 } 304 return fallbackDial(cli.proto, cli.addr, resolveTLSConfig(cli.client.Transport)) 305 } 306 }