podman
1155 строк · 31.8 Кб
1// Copyright 2013 go-dockerclient authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package docker provides a client for the Docker remote API.
6//
7// See https://goo.gl/o2v3rk for more details on the remote API.
8package docker
9
10import (
11"bufio"
12"bytes"
13"context"
14"crypto/tls"
15"crypto/x509"
16"encoding/json"
17"errors"
18"fmt"
19"io"
20"net"
21"net/http"
22"net/http/httputil"
23"net/url"
24"os"
25"path/filepath"
26"reflect"
27"runtime"
28"strconv"
29"strings"
30"sync/atomic"
31"time"
32
33"github.com/docker/docker/pkg/homedir"
34"github.com/docker/docker/pkg/jsonmessage"
35"github.com/docker/docker/pkg/stdcopy"
36)
37
38const (
39userAgent = "go-dockerclient"
40
41unixProtocol = "unix"
42namedPipeProtocol = "npipe"
43)
44
45var (
46// ErrInvalidEndpoint is returned when the endpoint is not a valid HTTP URL.
47ErrInvalidEndpoint = errors.New("invalid endpoint")
48
49// ErrConnectionRefused is returned when the client cannot connect to the given endpoint.
50ErrConnectionRefused = errors.New("cannot connect to Docker endpoint")
51
52// ErrInactivityTimeout is returned when a streamable call has been inactive for some time.
53ErrInactivityTimeout = errors.New("inactivity time exceeded timeout")
54
55apiVersion112, _ = NewAPIVersion("1.12")
56apiVersion118, _ = NewAPIVersion("1.18")
57apiVersion119, _ = NewAPIVersion("1.19")
58apiVersion121, _ = NewAPIVersion("1.21")
59apiVersion124, _ = NewAPIVersion("1.24")
60apiVersion125, _ = NewAPIVersion("1.25")
61apiVersion135, _ = NewAPIVersion("1.35")
62)
63
64// APIVersion is an internal representation of a version of the Remote API.
65type APIVersion []int
66
67// NewAPIVersion returns an instance of APIVersion for the given string.
68//
69// The given string must be in the form <major>.<minor>.<patch>, where <major>,
70// <minor> and <patch> are integer numbers.
71func NewAPIVersion(input string) (APIVersion, error) {
72if !strings.Contains(input, ".") {
73return nil, fmt.Errorf("unable to parse version %q", input)
74}
75raw := strings.Split(input, "-")
76arr := strings.Split(raw[0], ".")
77ret := make(APIVersion, len(arr))
78var err error
79for i, val := range arr {
80ret[i], err = strconv.Atoi(val)
81if err != nil {
82return nil, fmt.Errorf("unable to parse version %q: %q is not an integer", input, val)
83}
84}
85return ret, nil
86}
87
88func (version APIVersion) String() string {
89parts := make([]string, len(version))
90for i, val := range version {
91parts[i] = strconv.Itoa(val)
92}
93return strings.Join(parts, ".")
94}
95
96// LessThan is a function for comparing APIVersion structs.
97func (version APIVersion) LessThan(other APIVersion) bool {
98return version.compare(other) < 0
99}
100
101// LessThanOrEqualTo is a function for comparing APIVersion structs.
102func (version APIVersion) LessThanOrEqualTo(other APIVersion) bool {
103return version.compare(other) <= 0
104}
105
106// GreaterThan is a function for comparing APIVersion structs.
107func (version APIVersion) GreaterThan(other APIVersion) bool {
108return version.compare(other) > 0
109}
110
111// GreaterThanOrEqualTo is a function for comparing APIVersion structs.
112func (version APIVersion) GreaterThanOrEqualTo(other APIVersion) bool {
113return version.compare(other) >= 0
114}
115
116func (version APIVersion) compare(other APIVersion) int {
117for i, v := range version {
118if i <= len(other)-1 {
119otherVersion := other[i]
120
121if v < otherVersion {
122return -1
123} else if v > otherVersion {
124return 1
125}
126}
127}
128if len(version) > len(other) {
129return 1
130}
131if len(version) < len(other) {
132return -1
133}
134return 0
135}
136
137// Client is the basic type of this package. It provides methods for
138// interaction with the API.
139type Client struct {
140SkipServerVersionCheck bool
141HTTPClient *http.Client
142TLSConfig *tls.Config
143Dialer Dialer
144
145endpoint string
146endpointURL *url.URL
147eventMonitor *eventMonitoringState
148requestedAPIVersion APIVersion
149serverAPIVersion APIVersion
150expectedAPIVersion APIVersion
151}
152
153// Dialer is an interface that allows network connections to be dialed
154// (net.Dialer fulfills this interface) and named pipes (a shim using
155// winio.DialPipe)
156type Dialer interface {
157Dial(network, address string) (net.Conn, error)
158}
159
160// NewClient returns a Client instance ready for communication with the given
161// server endpoint. It will use the latest remote API version available in the
162// server.
163func NewClient(endpoint string) (*Client, error) {
164client, err := NewVersionedClient(endpoint, "")
165if err != nil {
166return nil, err
167}
168client.SkipServerVersionCheck = true
169return client, nil
170}
171
172// NewTLSClient returns a Client instance ready for TLS communications with the givens
173// server endpoint, key and certificates . It will use the latest remote API version
174// available in the server.
175func NewTLSClient(endpoint string, cert, key, ca string) (*Client, error) {
176client, err := NewVersionedTLSClient(endpoint, cert, key, ca, "")
177if err != nil {
178return nil, err
179}
180client.SkipServerVersionCheck = true
181return client, nil
182}
183
184// NewTLSClientFromBytes returns a Client instance ready for TLS communications with the givens
185// server endpoint, key and certificates (passed inline to the function as opposed to being
186// read from a local file). It will use the latest remote API version available in the server.
187func NewTLSClientFromBytes(endpoint string, certPEMBlock, keyPEMBlock, caPEMCert []byte) (*Client, error) {
188client, err := NewVersionedTLSClientFromBytes(endpoint, certPEMBlock, keyPEMBlock, caPEMCert, "")
189if err != nil {
190return nil, err
191}
192client.SkipServerVersionCheck = true
193return client, nil
194}
195
196// NewVersionedClient returns a Client instance ready for communication with
197// the given server endpoint, using a specific remote API version.
198func NewVersionedClient(endpoint string, apiVersionString string) (*Client, error) {
199u, err := parseEndpoint(endpoint, false)
200if err != nil {
201return nil, err
202}
203var requestedAPIVersion APIVersion
204if strings.Contains(apiVersionString, ".") {
205requestedAPIVersion, err = NewAPIVersion(apiVersionString)
206if err != nil {
207return nil, err
208}
209}
210c := &Client{
211HTTPClient: defaultClient(),
212Dialer: &net.Dialer{},
213endpoint: endpoint,
214endpointURL: u,
215eventMonitor: new(eventMonitoringState),
216requestedAPIVersion: requestedAPIVersion,
217}
218c.initializeNativeClient(defaultTransport)
219return c, nil
220}
221
222// WithTransport replaces underlying HTTP client of Docker Client by accepting
223// a function that returns pointer to a transport object.
224func (c *Client) WithTransport(trFunc func() *http.Transport) {
225c.initializeNativeClient(trFunc)
226}
227
228// NewVersionnedTLSClient is like NewVersionedClient, but with ann extra n.
229//
230// Deprecated: Use NewVersionedTLSClient instead.
231func NewVersionnedTLSClient(endpoint string, cert, key, ca, apiVersionString string) (*Client, error) {
232return NewVersionedTLSClient(endpoint, cert, key, ca, apiVersionString)
233}
234
235// NewVersionedTLSClient returns a Client instance ready for TLS communications with the givens
236// server endpoint, key and certificates, using a specific remote API version.
237func NewVersionedTLSClient(endpoint string, cert, key, ca, apiVersionString string) (*Client, error) {
238var certPEMBlock []byte
239var keyPEMBlock []byte
240var caPEMCert []byte
241if _, err := os.Stat(cert); !os.IsNotExist(err) {
242certPEMBlock, err = os.ReadFile(cert)
243if err != nil {
244return nil, err
245}
246}
247if _, err := os.Stat(key); !os.IsNotExist(err) {
248keyPEMBlock, err = os.ReadFile(key)
249if err != nil {
250return nil, err
251}
252}
253if _, err := os.Stat(ca); !os.IsNotExist(err) {
254caPEMCert, err = os.ReadFile(ca)
255if err != nil {
256return nil, err
257}
258}
259return NewVersionedTLSClientFromBytes(endpoint, certPEMBlock, keyPEMBlock, caPEMCert, apiVersionString)
260}
261
262// NewClientFromEnv returns a Client instance ready for communication created from
263// Docker's default logic for the environment variables DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH,
264// and DOCKER_API_VERSION.
265//
266// See https://github.com/docker/docker/blob/1f963af697e8df3a78217f6fdbf67b8123a7db94/docker/docker.go#L68.
267// See https://github.com/docker/compose/blob/81707ef1ad94403789166d2fe042c8a718a4c748/compose/cli/docker_client.py#L7.
268// See https://github.com/moby/moby/blob/28d7dba41d0c0d9c7f0dafcc79d3c59f2b3f5dc3/client/options.go#L51
269func NewClientFromEnv() (*Client, error) {
270apiVersionString := os.Getenv("DOCKER_API_VERSION")
271client, err := NewVersionedClientFromEnv(apiVersionString)
272if err != nil {
273return nil, err
274}
275client.SkipServerVersionCheck = apiVersionString == ""
276return client, nil
277}
278
279// NewVersionedClientFromEnv returns a Client instance ready for TLS communications created from
280// Docker's default logic for the environment variables DOCKER_HOST, DOCKER_TLS_VERIFY, and DOCKER_CERT_PATH,
281// and using a specific remote API version.
282//
283// See https://github.com/docker/docker/blob/1f963af697e8df3a78217f6fdbf67b8123a7db94/docker/docker.go#L68.
284// See https://github.com/docker/compose/blob/81707ef1ad94403789166d2fe042c8a718a4c748/compose/cli/docker_client.py#L7.
285func NewVersionedClientFromEnv(apiVersionString string) (*Client, error) {
286dockerEnv, err := getDockerEnv()
287if err != nil {
288return nil, err
289}
290dockerHost := dockerEnv.dockerHost
291if dockerEnv.dockerTLSVerify {
292parts := strings.SplitN(dockerEnv.dockerHost, "://", 2)
293if len(parts) != 2 {
294return nil, fmt.Errorf("could not split %s into two parts by ://", dockerHost)
295}
296cert := filepath.Join(dockerEnv.dockerCertPath, "cert.pem")
297key := filepath.Join(dockerEnv.dockerCertPath, "key.pem")
298ca := filepath.Join(dockerEnv.dockerCertPath, "ca.pem")
299return NewVersionedTLSClient(dockerEnv.dockerHost, cert, key, ca, apiVersionString)
300}
301return NewVersionedClient(dockerEnv.dockerHost, apiVersionString)
302}
303
304// NewVersionedTLSClientFromBytes returns a Client instance ready for TLS communications with the givens
305// server endpoint, key and certificates (passed inline to the function as opposed to being
306// read from a local file), using a specific remote API version.
307func NewVersionedTLSClientFromBytes(endpoint string, certPEMBlock, keyPEMBlock, caPEMCert []byte, apiVersionString string) (*Client, error) {
308u, err := parseEndpoint(endpoint, true)
309if err != nil {
310return nil, err
311}
312var requestedAPIVersion APIVersion
313if strings.Contains(apiVersionString, ".") {
314requestedAPIVersion, err = NewAPIVersion(apiVersionString)
315if err != nil {
316return nil, err
317}
318}
319tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12}
320if certPEMBlock != nil && keyPEMBlock != nil {
321tlsCert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
322if err != nil {
323return nil, err
324}
325tlsConfig.Certificates = []tls.Certificate{tlsCert}
326}
327if caPEMCert == nil {
328tlsConfig.InsecureSkipVerify = true
329} else {
330caPool := x509.NewCertPool()
331if !caPool.AppendCertsFromPEM(caPEMCert) {
332return nil, errors.New("could not add RootCA pem")
333}
334tlsConfig.RootCAs = caPool
335}
336tr := defaultTransport()
337tr.TLSClientConfig = tlsConfig
338if err != nil {
339return nil, err
340}
341c := &Client{
342HTTPClient: &http.Client{Transport: tr},
343TLSConfig: tlsConfig,
344Dialer: &net.Dialer{},
345endpoint: endpoint,
346endpointURL: u,
347eventMonitor: new(eventMonitoringState),
348requestedAPIVersion: requestedAPIVersion,
349}
350c.initializeNativeClient(defaultTransport)
351return c, nil
352}
353
354// SetTimeout takes a timeout and applies it to the HTTPClient. It should not
355// be called concurrently with any other Client methods.
356func (c *Client) SetTimeout(t time.Duration) {
357if c.HTTPClient != nil {
358c.HTTPClient.Timeout = t
359}
360}
361
362func (c *Client) checkAPIVersion() error {
363serverAPIVersionString, err := c.getServerAPIVersionString()
364if err != nil {
365return err
366}
367c.serverAPIVersion, err = NewAPIVersion(serverAPIVersionString)
368if err != nil {
369return err
370}
371if c.requestedAPIVersion == nil {
372c.expectedAPIVersion = c.serverAPIVersion
373} else {
374c.expectedAPIVersion = c.requestedAPIVersion
375}
376return nil
377}
378
379// Endpoint returns the current endpoint. It's useful for getting the endpoint
380// when using functions that get this data from the environment (like
381// NewClientFromEnv.
382func (c *Client) Endpoint() string {
383return c.endpoint
384}
385
386// Ping pings the docker server
387//
388// See https://goo.gl/wYfgY1 for more details.
389func (c *Client) Ping() error {
390return c.PingWithContext(context.TODO())
391}
392
393// PingWithContext pings the docker server
394// The context object can be used to cancel the ping request.
395//
396// See https://goo.gl/wYfgY1 for more details.
397func (c *Client) PingWithContext(ctx context.Context) error {
398path := "/_ping"
399resp, err := c.do(http.MethodGet, path, doOptions{context: ctx})
400if err != nil {
401return err
402}
403if resp.StatusCode != http.StatusOK {
404return newError(resp)
405}
406resp.Body.Close()
407return nil
408}
409
410func (c *Client) getServerAPIVersionString() (version string, err error) {
411resp, err := c.do(http.MethodGet, "/version", doOptions{})
412if err != nil {
413return "", err
414}
415defer resp.Body.Close()
416if resp.StatusCode != http.StatusOK {
417return "", fmt.Errorf("received unexpected status %d while trying to retrieve the server version", resp.StatusCode)
418}
419var versionResponse map[string]any
420if err := json.NewDecoder(resp.Body).Decode(&versionResponse); err != nil {
421return "", err
422}
423if version, ok := (versionResponse["ApiVersion"]).(string); ok {
424return version, nil
425}
426return "", nil
427}
428
429type doOptions struct {
430data any
431forceJSON bool
432headers map[string]string
433context context.Context
434}
435
436func (c *Client) do(method, path string, doOptions doOptions) (*http.Response, error) {
437var params io.Reader
438if doOptions.data != nil || doOptions.forceJSON {
439buf, err := json.Marshal(doOptions.data)
440if err != nil {
441return nil, err
442}
443params = bytes.NewBuffer(buf)
444}
445if path != "/version" && !c.SkipServerVersionCheck && c.expectedAPIVersion == nil {
446err := c.checkAPIVersion()
447if err != nil {
448return nil, err
449}
450}
451protocol := c.endpointURL.Scheme
452var u string
453switch protocol {
454case unixProtocol, namedPipeProtocol:
455u = c.getFakeNativeURL(path)
456default:
457u = c.getURL(path)
458}
459
460req, err := http.NewRequest(method, u, params)
461if err != nil {
462return nil, err
463}
464req.Header.Set("User-Agent", userAgent)
465if doOptions.data != nil {
466req.Header.Set("Content-Type", "application/json")
467} else if method == http.MethodPost {
468req.Header.Set("Content-Type", "plain/text")
469}
470
471for k, v := range doOptions.headers {
472req.Header.Set(k, v)
473}
474
475ctx := doOptions.context
476if ctx == nil {
477ctx = context.Background()
478}
479
480resp, err := c.HTTPClient.Do(req.WithContext(ctx))
481if err != nil {
482if strings.Contains(err.Error(), "connection refused") {
483return nil, ErrConnectionRefused
484}
485
486return nil, chooseError(ctx, err)
487}
488if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest {
489return nil, newError(resp)
490}
491return resp, nil
492}
493
494type streamOptions struct {
495setRawTerminal bool
496rawJSONStream bool
497useJSONDecoder bool
498headers map[string]string
499in io.Reader
500stdout io.Writer
501stderr io.Writer
502reqSent chan struct{}
503// timeout is the initial connection timeout
504timeout time.Duration
505// Timeout with no data is received, it's reset every time new data
506// arrives
507inactivityTimeout time.Duration
508context context.Context
509}
510
511func chooseError(ctx context.Context, err error) error {
512select {
513case <-ctx.Done():
514return context.Cause(ctx)
515default:
516return err
517}
518}
519
520func (c *Client) stream(method, path string, streamOptions streamOptions) error {
521if (method == http.MethodPost || method == http.MethodPut) && streamOptions.in == nil {
522streamOptions.in = bytes.NewReader(nil)
523}
524if path != "/version" && !c.SkipServerVersionCheck && c.expectedAPIVersion == nil {
525err := c.checkAPIVersion()
526if err != nil {
527return err
528}
529}
530return c.streamURL(method, c.getURL(path), streamOptions)
531}
532
533func (c *Client) streamURL(method, url string, streamOptions streamOptions) error {
534if (method == http.MethodPost || method == http.MethodPut) && streamOptions.in == nil {
535streamOptions.in = bytes.NewReader(nil)
536}
537if !c.SkipServerVersionCheck && c.expectedAPIVersion == nil {
538err := c.checkAPIVersion()
539if err != nil {
540return err
541}
542}
543
544// make a sub-context so that our active cancellation does not affect parent
545ctx := streamOptions.context
546if ctx == nil {
547ctx = context.Background()
548}
549subCtx, cancelRequest := context.WithCancel(ctx)
550defer cancelRequest()
551
552req, err := http.NewRequestWithContext(ctx, method, url, streamOptions.in)
553if err != nil {
554return err
555}
556req.Header.Set("User-Agent", userAgent)
557if method == http.MethodPost {
558req.Header.Set("Content-Type", "plain/text")
559}
560for key, val := range streamOptions.headers {
561req.Header.Set(key, val)
562}
563var resp *http.Response
564protocol := c.endpointURL.Scheme
565address := c.endpointURL.Path
566if streamOptions.stdout == nil {
567streamOptions.stdout = io.Discard
568}
569if streamOptions.stderr == nil {
570streamOptions.stderr = io.Discard
571}
572
573if protocol == unixProtocol || protocol == namedPipeProtocol {
574var dial net.Conn
575dial, err = c.Dialer.Dial(protocol, address)
576if err != nil {
577return err
578}
579go func() {
580<-subCtx.Done()
581dial.Close()
582}()
583breader := bufio.NewReader(dial)
584err = req.Write(dial)
585if err != nil {
586return chooseError(subCtx, err)
587}
588
589// ReadResponse may hang if server does not replay
590if streamOptions.timeout > 0 {
591dial.SetDeadline(time.Now().Add(streamOptions.timeout))
592}
593
594if streamOptions.reqSent != nil {
595close(streamOptions.reqSent)
596}
597if resp, err = http.ReadResponse(breader, req); err != nil {
598// Cancel timeout for future I/O operations
599if streamOptions.timeout > 0 {
600dial.SetDeadline(time.Time{})
601}
602if strings.Contains(err.Error(), "connection refused") {
603return ErrConnectionRefused
604}
605
606return chooseError(subCtx, err)
607}
608defer resp.Body.Close()
609} else {
610if resp, err = c.HTTPClient.Do(req.WithContext(subCtx)); err != nil {
611if strings.Contains(err.Error(), "connection refused") {
612return ErrConnectionRefused
613}
614return chooseError(subCtx, err)
615}
616defer resp.Body.Close()
617if streamOptions.reqSent != nil {
618close(streamOptions.reqSent)
619}
620}
621if resp.StatusCode < 200 || resp.StatusCode >= 400 {
622return newError(resp)
623}
624var canceled uint32
625if streamOptions.inactivityTimeout > 0 {
626var ch chan<- struct{}
627resp.Body, ch = handleInactivityTimeout(resp.Body, streamOptions.inactivityTimeout, cancelRequest, &canceled)
628defer close(ch)
629}
630err = handleStreamResponse(resp, &streamOptions)
631if err != nil {
632if atomic.LoadUint32(&canceled) != 0 {
633return ErrInactivityTimeout
634}
635return chooseError(subCtx, err)
636}
637return nil
638}
639
640func handleStreamResponse(resp *http.Response, streamOptions *streamOptions) error {
641var err error
642if !streamOptions.useJSONDecoder && resp.Header.Get("Content-Type") != "application/json" {
643if streamOptions.setRawTerminal {
644_, err = io.Copy(streamOptions.stdout, resp.Body)
645} else {
646_, err = stdcopy.StdCopy(streamOptions.stdout, streamOptions.stderr, resp.Body)
647}
648return err
649}
650// if we want to get raw json stream, just copy it back to output
651// without decoding it
652if streamOptions.rawJSONStream {
653_, err = io.Copy(streamOptions.stdout, resp.Body)
654return err
655}
656if st, ok := streamOptions.stdout.(stream); ok {
657err = jsonmessage.DisplayJSONMessagesToStream(resp.Body, st, nil)
658} else {
659err = jsonmessage.DisplayJSONMessagesStream(resp.Body, streamOptions.stdout, 0, false, nil)
660}
661return err
662}
663
664type stream interface {
665io.Writer
666FD() uintptr
667IsTerminal() bool
668}
669
670type proxyReader struct {
671io.ReadCloser
672calls uint64
673}
674
675func (p *proxyReader) callCount() uint64 {
676return atomic.LoadUint64(&p.calls)
677}
678
679func (p *proxyReader) Read(data []byte) (int, error) {
680atomic.AddUint64(&p.calls, 1)
681return p.ReadCloser.Read(data)
682}
683
684func handleInactivityTimeout(reader io.ReadCloser, timeout time.Duration, cancelRequest func(), canceled *uint32) (io.ReadCloser, chan<- struct{}) {
685done := make(chan struct{})
686proxyReader := &proxyReader{ReadCloser: reader}
687go func() {
688var lastCallCount uint64
689for {
690select {
691case <-time.After(timeout):
692case <-done:
693return
694}
695curCallCount := proxyReader.callCount()
696if curCallCount == lastCallCount {
697atomic.AddUint32(canceled, 1)
698cancelRequest()
699return
700}
701lastCallCount = curCallCount
702}
703}()
704return proxyReader, done
705}
706
707type hijackOptions struct {
708success chan struct{}
709setRawTerminal bool
710in io.Reader
711stdout io.Writer
712stderr io.Writer
713data any
714}
715
716// CloseWaiter is an interface with methods for closing the underlying resource
717// and then waiting for it to finish processing.
718type CloseWaiter interface {
719io.Closer
720Wait() error
721}
722
723type waiterFunc func() error
724
725func (w waiterFunc) Wait() error { return w() }
726
727type closerFunc func() error
728
729func (c closerFunc) Close() error { return c() }
730
731func (c *Client) hijack(method, path string, hijackOptions hijackOptions) (CloseWaiter, error) {
732if path != "/version" && !c.SkipServerVersionCheck && c.expectedAPIVersion == nil {
733err := c.checkAPIVersion()
734if err != nil {
735return nil, err
736}
737}
738var params io.Reader
739if hijackOptions.data != nil {
740buf, err := json.Marshal(hijackOptions.data)
741if err != nil {
742return nil, err
743}
744params = bytes.NewBuffer(buf)
745}
746req, err := http.NewRequest(method, c.getURL(path), params)
747if err != nil {
748return nil, err
749}
750req.Header.Set("Content-Type", "application/json")
751req.Header.Set("Connection", "Upgrade")
752req.Header.Set("Upgrade", "tcp")
753protocol := c.endpointURL.Scheme
754address := c.endpointURL.Path
755if protocol != unixProtocol && protocol != namedPipeProtocol {
756protocol = "tcp"
757address = c.endpointURL.Host
758}
759var dial net.Conn
760if c.TLSConfig != nil && protocol != unixProtocol && protocol != namedPipeProtocol {
761netDialer, ok := c.Dialer.(*net.Dialer)
762if !ok {
763return nil, ErrTLSNotSupported
764}
765dial, err = tlsDialWithDialer(netDialer, protocol, address, c.TLSConfig)
766if err != nil {
767return nil, err
768}
769} else {
770dial, err = c.Dialer.Dial(protocol, address)
771if err != nil {
772return nil, err
773}
774}
775
776errs := make(chan error, 1)
777quit := make(chan struct{})
778go func() {
779//lint:ignore SA1019 the alternative doesn't quite work, so keep using the deprecated thing.
780clientconn := httputil.NewClientConn(dial, nil)
781defer clientconn.Close()
782clientconn.Do(req)
783if hijackOptions.success != nil {
784hijackOptions.success <- struct{}{}
785<-hijackOptions.success
786}
787rwc, br := clientconn.Hijack()
788defer rwc.Close()
789
790errChanOut := make(chan error, 1)
791errChanIn := make(chan error, 2)
792if hijackOptions.stdout == nil && hijackOptions.stderr == nil {
793close(errChanOut)
794} else {
795// Only copy if hijackOptions.stdout and/or hijackOptions.stderr is actually set.
796// Otherwise, if the only stream you care about is stdin, your attach session
797// will "hang" until the container terminates, even though you're not reading
798// stdout/stderr
799if hijackOptions.stdout == nil {
800hijackOptions.stdout = io.Discard
801}
802if hijackOptions.stderr == nil {
803hijackOptions.stderr = io.Discard
804}
805
806go func() {
807defer func() {
808if hijackOptions.in != nil {
809if closer, ok := hijackOptions.in.(io.Closer); ok {
810closer.Close()
811}
812errChanIn <- nil
813}
814}()
815
816var err error
817if hijackOptions.setRawTerminal {
818_, err = io.Copy(hijackOptions.stdout, br)
819} else {
820_, err = stdcopy.StdCopy(hijackOptions.stdout, hijackOptions.stderr, br)
821}
822errChanOut <- err
823}()
824}
825
826go func() {
827var err error
828if hijackOptions.in != nil {
829_, err = io.Copy(rwc, hijackOptions.in)
830}
831errChanIn <- err
832rwc.(interface {
833CloseWrite() error
834}).CloseWrite()
835}()
836
837var errIn error
838select {
839case errIn = <-errChanIn:
840case <-quit:
841}
842
843var errOut error
844select {
845case errOut = <-errChanOut:
846case <-quit:
847}
848
849if errIn != nil {
850errs <- errIn
851} else {
852errs <- errOut
853}
854}()
855
856return struct {
857closerFunc
858waiterFunc
859}{
860closerFunc(func() error { close(quit); return nil }),
861waiterFunc(func() error { return <-errs }),
862}, nil
863}
864
865func (c *Client) getURL(path string) string {
866urlStr := strings.TrimRight(c.endpointURL.String(), "/")
867if c.endpointURL.Scheme == unixProtocol || c.endpointURL.Scheme == namedPipeProtocol {
868urlStr = ""
869}
870if c.requestedAPIVersion != nil {
871return fmt.Sprintf("%s/v%s%s", urlStr, c.requestedAPIVersion, path)
872}
873return fmt.Sprintf("%s%s", urlStr, path)
874}
875
876func (c *Client) getPath(basepath string, opts any) (string, error) {
877queryStr, requiredAPIVersion := queryStringVersion(opts)
878return c.pathVersionCheck(basepath, queryStr, requiredAPIVersion)
879}
880
881func (c *Client) pathVersionCheck(basepath, queryStr string, requiredAPIVersion APIVersion) (string, error) {
882urlStr := strings.TrimRight(c.endpointURL.String(), "/")
883if c.endpointURL.Scheme == unixProtocol || c.endpointURL.Scheme == namedPipeProtocol {
884urlStr = ""
885}
886if c.requestedAPIVersion != nil {
887if c.requestedAPIVersion.GreaterThanOrEqualTo(requiredAPIVersion) {
888return fmt.Sprintf("%s/v%s%s?%s", urlStr, c.requestedAPIVersion, basepath, queryStr), nil
889}
890return "", fmt.Errorf("API %s requires version %s, requested version %s is insufficient",
891basepath, requiredAPIVersion, c.requestedAPIVersion)
892}
893if requiredAPIVersion != nil {
894return fmt.Sprintf("%s/v%s%s?%s", urlStr, requiredAPIVersion, basepath, queryStr), nil
895}
896return fmt.Sprintf("%s%s?%s", urlStr, basepath, queryStr), nil
897}
898
899// getFakeNativeURL returns the URL needed to make an HTTP request over a UNIX
900// domain socket to the given path.
901func (c *Client) getFakeNativeURL(path string) string {
902u := *c.endpointURL // Copy.
903
904// Override URL so that net/http will not complain.
905u.Scheme = "http"
906u.Host = "unix.sock" // Doesn't matter what this is - it's not used.
907u.Path = ""
908urlStr := strings.TrimRight(u.String(), "/")
909if c.requestedAPIVersion != nil {
910return fmt.Sprintf("%s/v%s%s", urlStr, c.requestedAPIVersion, path)
911}
912return fmt.Sprintf("%s%s", urlStr, path)
913}
914
915func queryStringVersion(opts any) (string, APIVersion) {
916if opts == nil {
917return "", nil
918}
919value := reflect.ValueOf(opts)
920if value.Kind() == reflect.Ptr {
921value = value.Elem()
922}
923if value.Kind() != reflect.Struct {
924return "", nil
925}
926var apiVersion APIVersion
927items := url.Values(map[string][]string{})
928for i := 0; i < value.NumField(); i++ {
929field := value.Type().Field(i)
930if field.PkgPath != "" {
931continue
932}
933key := field.Tag.Get("qs")
934if key == "" {
935key = strings.ToLower(field.Name)
936} else if key == "-" {
937continue
938}
939if addQueryStringValue(items, key, value.Field(i)) {
940verstr := field.Tag.Get("ver")
941if verstr != "" {
942ver, _ := NewAPIVersion(verstr)
943if apiVersion == nil {
944apiVersion = ver
945} else if ver.GreaterThan(apiVersion) {
946apiVersion = ver
947}
948}
949}
950}
951return items.Encode(), apiVersion
952}
953
954func queryString(opts any) string {
955s, _ := queryStringVersion(opts)
956return s
957}
958
959func addQueryStringValue(items url.Values, key string, v reflect.Value) bool {
960switch v.Kind() {
961case reflect.Bool:
962if v.Bool() {
963items.Add(key, "1")
964return true
965}
966case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
967if v.Int() > 0 {
968items.Add(key, strconv.FormatInt(v.Int(), 10))
969return true
970}
971case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
972if v.Uint() > 0 {
973items.Add(key, strconv.FormatUint(v.Uint(), 10))
974return true
975}
976case reflect.Float32, reflect.Float64:
977if v.Float() > 0 {
978items.Add(key, strconv.FormatFloat(v.Float(), 'f', -1, 64))
979return true
980}
981case reflect.String:
982if v.String() != "" {
983items.Add(key, v.String())
984return true
985}
986case reflect.Ptr:
987if !v.IsNil() {
988if b, err := json.Marshal(v.Interface()); err == nil {
989items.Add(key, string(b))
990return true
991}
992}
993case reflect.Map:
994if len(v.MapKeys()) > 0 {
995if b, err := json.Marshal(v.Interface()); err == nil {
996items.Add(key, string(b))
997return true
998}
999}
1000case reflect.Array, reflect.Slice:
1001vLen := v.Len()
1002var valuesAdded int
1003if vLen > 0 {
1004for i := 0; i < vLen; i++ {
1005if addQueryStringValue(items, key, v.Index(i)) {
1006valuesAdded++
1007}
1008}
1009}
1010return valuesAdded > 0
1011}
1012return false
1013}
1014
1015// Error represents failures in the API. It represents a failure from the API.
1016type Error struct {
1017Status int
1018Message string
1019}
1020
1021func newError(resp *http.Response) *Error {
1022type ErrMsg struct {
1023Message string `json:"message"`
1024}
1025defer resp.Body.Close()
1026data, err := io.ReadAll(resp.Body)
1027if err != nil {
1028return &Error{Status: resp.StatusCode, Message: fmt.Sprintf("cannot read body, err: %v", err)}
1029}
1030var emsg ErrMsg
1031err = json.Unmarshal(data, &emsg)
1032if err != nil {
1033return &Error{Status: resp.StatusCode, Message: string(data)}
1034}
1035return &Error{Status: resp.StatusCode, Message: emsg.Message}
1036}
1037
1038func (e *Error) Error() string {
1039return fmt.Sprintf("API error (%d): %s", e.Status, e.Message)
1040}
1041
1042func parseEndpoint(endpoint string, tls bool) (*url.URL, error) {
1043if endpoint != "" && !strings.Contains(endpoint, "://") {
1044endpoint = "tcp://" + endpoint
1045}
1046u, err := url.Parse(endpoint)
1047if err != nil {
1048return nil, ErrInvalidEndpoint
1049}
1050if tls && u.Scheme != "unix" {
1051u.Scheme = "https"
1052}
1053switch u.Scheme {
1054case unixProtocol, namedPipeProtocol:
1055return u, nil
1056case "http", "https", "tcp":
1057_, port, err := net.SplitHostPort(u.Host)
1058if err != nil {
1059var e *net.AddrError
1060if errors.As(err, &e) {
1061if e.Err == "missing port in address" {
1062return u, nil
1063}
1064}
1065return nil, ErrInvalidEndpoint
1066}
1067number, err := strconv.ParseInt(port, 10, 64)
1068if err == nil && number > 0 && number < 65536 {
1069if u.Scheme == "tcp" {
1070if tls {
1071u.Scheme = "https"
1072} else {
1073u.Scheme = "http"
1074}
1075}
1076return u, nil
1077}
1078return nil, ErrInvalidEndpoint
1079default:
1080return nil, ErrInvalidEndpoint
1081}
1082}
1083
1084type dockerEnv struct {
1085dockerHost string
1086dockerTLSVerify bool
1087dockerCertPath string
1088}
1089
1090func getDockerEnv() (*dockerEnv, error) {
1091dockerHost := os.Getenv("DOCKER_HOST")
1092var err error
1093if dockerHost == "" {
1094dockerHost = defaultHost
1095}
1096dockerTLSVerify := os.Getenv("DOCKER_TLS_VERIFY") != ""
1097var dockerCertPath string
1098if dockerTLSVerify {
1099dockerCertPath = os.Getenv("DOCKER_CERT_PATH")
1100if dockerCertPath == "" {
1101home := homedir.Get()
1102if home == "" {
1103return nil, errors.New("environment variable HOME must be set if DOCKER_CERT_PATH is not set")
1104}
1105dockerCertPath = filepath.Join(home, ".docker")
1106dockerCertPath, err = filepath.Abs(dockerCertPath)
1107if err != nil {
1108return nil, err
1109}
1110}
1111}
1112return &dockerEnv{
1113dockerHost: dockerHost,
1114dockerTLSVerify: dockerTLSVerify,
1115dockerCertPath: dockerCertPath,
1116}, nil
1117}
1118
1119// defaultTransport returns a new http.Transport with similar default values to
1120// http.DefaultTransport, but with idle connections and keepalives disabled.
1121func defaultTransport() *http.Transport {
1122transport := defaultPooledTransport()
1123transport.DisableKeepAlives = true
1124transport.MaxIdleConnsPerHost = -1
1125return transport
1126}
1127
1128// defaultPooledTransport returns a new http.Transport with similar default
1129// values to http.DefaultTransport. Do not use this for transient transports as
1130// it can leak file descriptors over time. Only use this for transports that
1131// will be re-used for the same host(s).
1132func defaultPooledTransport() *http.Transport {
1133transport := &http.Transport{
1134Proxy: http.ProxyFromEnvironment,
1135DialContext: (&net.Dialer{
1136Timeout: 30 * time.Second,
1137KeepAlive: 30 * time.Second,
1138}).DialContext,
1139MaxIdleConns: 100,
1140IdleConnTimeout: 90 * time.Second,
1141TLSHandshakeTimeout: 10 * time.Second,
1142ExpectContinueTimeout: 1 * time.Second,
1143MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1,
1144}
1145return transport
1146}
1147
1148// defaultClient returns a new http.Client with similar default values to
1149// http.Client, but with a non-shared Transport, idle connections disabled, and
1150// keepalives disabled.
1151func defaultClient() *http.Client {
1152return &http.Client{
1153Transport: defaultTransport(),
1154}
1155}
1156