forked from ebhomengo/niki
1
0
Fork 0
niki/vendor/github.com/ory/dockertest/v3/docker/client.go

1098 lines
30 KiB
Go
Raw Normal View History

2024-08-29 03:26:59 +00:00
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0
// Copyright 2013 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package docker provides a client for the Docker remote API.
//
// See https://goo.gl/o2v3rk for more details on the remote API.
package docker
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/ory/dockertest/v3/docker/opts"
"github.com/ory/dockertest/v3/docker/pkg/homedir"
"github.com/ory/dockertest/v3/docker/pkg/jsonmessage"
"github.com/ory/dockertest/v3/docker/pkg/stdcopy"
)
const (
userAgent = "go-dockerclient"
unixProtocol = "unix"
namedPipeProtocol = "npipe"
)
var (
// ErrInvalidEndpoint is returned when the endpoint is not a valid HTTP URL.
ErrInvalidEndpoint = errors.New("invalid endpoint")
// ErrConnectionRefused is returned when the client cannot connect to the given endpoint.
ErrConnectionRefused = errors.New("cannot connect to Docker endpoint")
// ErrInactivityTimeout is returned when a streamable call has been inactive for some time.
ErrInactivityTimeout = errors.New("inactivity time exceeded timeout")
apiVersion112, _ = NewAPIVersion("1.12")
apiVersion119, _ = NewAPIVersion("1.19")
apiVersion124, _ = NewAPIVersion("1.24")
apiVersion125, _ = NewAPIVersion("1.25")
)
// APIVersion is an internal representation of a version of the Remote API.
type APIVersion []int
// NewAPIVersion returns an instance of APIVersion for the given string.
//
// The given string must be in the form <major>.<minor>.<patch>, where <major>,
// <minor> and <patch> are integer numbers.
func NewAPIVersion(input string) (APIVersion, error) {
if !strings.Contains(input, ".") {
return nil, fmt.Errorf("Unable to parse version %q", input)
}
raw := strings.Split(input, "-")
arr := strings.Split(raw[0], ".")
ret := make(APIVersion, len(arr))
var err error
for i, val := range arr {
ret[i], err = strconv.Atoi(val)
if err != nil {
return nil, fmt.Errorf("Unable to parse version %q: %q is not an integer", input, val)
}
}
return ret, nil
}
func (version APIVersion) String() string {
var str string
for i, val := range version {
str += strconv.Itoa(val)
if i < len(version)-1 {
str += "."
}
}
return str
}
// LessThan is a function for comparing APIVersion structs
func (version APIVersion) LessThan(other APIVersion) bool {
return version.compare(other) < 0
}
// LessThanOrEqualTo is a function for comparing APIVersion structs
func (version APIVersion) LessThanOrEqualTo(other APIVersion) bool {
return version.compare(other) <= 0
}
// GreaterThan is a function for comparing APIVersion structs
func (version APIVersion) GreaterThan(other APIVersion) bool {
return version.compare(other) > 0
}
// GreaterThanOrEqualTo is a function for comparing APIVersion structs
func (version APIVersion) GreaterThanOrEqualTo(other APIVersion) bool {
return version.compare(other) >= 0
}
func (version APIVersion) compare(other APIVersion) int {
for i, v := range version {
if i <= len(other)-1 {
otherVersion := other[i]
if v < otherVersion {
return -1
} else if v > otherVersion {
return 1
}
}
}
if len(version) > len(other) {
return 1
}
if len(version) < len(other) {
return -1
}
return 0
}
// Client is the basic type of this package. It provides methods for
// interaction with the API.
type Client struct {
SkipServerVersionCheck bool
HTTPClient *http.Client
TLSConfig *tls.Config
Dialer Dialer
endpoint string
endpointURL *url.URL
eventMonitor *eventMonitoringState
apiVersionMutex sync.RWMutex
requestedAPIVersion APIVersion
serverAPIVersion APIVersion
expectedAPIVersion APIVersion
}
// Dialer is an interface that allows network connections to be dialed
// (net.Dialer fulfills this interface) and named pipes (a shim using
// winio.DialPipe)
type Dialer interface {
Dial(network, address string) (net.Conn, error)
}
// NewClient returns a Client instance ready for communication with the given
// server endpoint. It will use the latest remote API version available in the
// server.
func NewClient(endpoint string) (*Client, error) {
client, err := NewVersionedClient(endpoint, "")
if err != nil {
return nil, err
}
client.SkipServerVersionCheck = true
return client, nil
}
// NewTLSClient returns a Client instance ready for TLS communications with the givens
// server endpoint, key and certificates . It will use the latest remote API version
// available in the server.
func NewTLSClient(endpoint string, cert, key, ca string) (*Client, error) {
client, err := NewVersionedTLSClient(endpoint, cert, key, ca, "")
if err != nil {
return nil, err
}
client.SkipServerVersionCheck = true
return client, nil
}
// NewTLSClientFromBytes returns a Client instance ready for TLS communications with the givens
// server endpoint, key and certificates (passed inline to the function as opposed to being
// read from a local file). It will use the latest remote API version available in the server.
func NewTLSClientFromBytes(endpoint string, certPEMBlock, keyPEMBlock, caPEMCert []byte) (*Client, error) {
client, err := NewVersionedTLSClientFromBytes(endpoint, certPEMBlock, keyPEMBlock, caPEMCert, "")
if err != nil {
return nil, err
}
client.SkipServerVersionCheck = true
return client, nil
}
// NewVersionedClient returns a Client instance ready for communication with
// the given server endpoint, using a specific remote API version.
func NewVersionedClient(endpoint string, apiVersionString string) (*Client, error) {
u, err := parseEndpoint(endpoint, false)
if err != nil {
return nil, err
}
var requestedAPIVersion APIVersion
if strings.Contains(apiVersionString, ".") {
requestedAPIVersion, err = NewAPIVersion(apiVersionString)
if err != nil {
return nil, err
}
}
c := &Client{
HTTPClient: defaultClient(),
Dialer: &net.Dialer{},
endpoint: endpoint,
endpointURL: u,
eventMonitor: new(eventMonitoringState),
requestedAPIVersion: requestedAPIVersion,
}
c.initializeNativeClient(defaultTransport)
return c, nil
}
// WithTransport replaces underlying HTTP client of Docker Client by accepting
// a function that returns pointer to a transport object.
func (c *Client) WithTransport(trFunc func() *http.Transport) {
c.initializeNativeClient(trFunc)
}
// NewVersionnedTLSClient is like NewVersionedClient, but with ann extra n.
//
// Deprecated: Use NewVersionedTLSClient instead.
func NewVersionnedTLSClient(endpoint string, cert, key, ca, apiVersionString string) (*Client, error) {
return NewVersionedTLSClient(endpoint, cert, key, ca, apiVersionString)
}
// NewVersionedTLSClient returns a Client instance ready for TLS communications with the givens
// server endpoint, key and certificates, using a specific remote API version.
func NewVersionedTLSClient(endpoint string, cert, key, ca, apiVersionString string) (*Client, error) {
var certPEMBlock []byte
var keyPEMBlock []byte
var caPEMCert []byte
if _, err := os.Stat(cert); !os.IsNotExist(err) {
certPEMBlock, err = os.ReadFile(cert)
if err != nil {
return nil, err
}
}
if _, err := os.Stat(key); !os.IsNotExist(err) {
keyPEMBlock, err = os.ReadFile(key)
if err != nil {
return nil, err
}
}
if _, err := os.Stat(ca); !os.IsNotExist(err) {
caPEMCert, err = os.ReadFile(ca)
if err != nil {
return nil, err
}
}
return NewVersionedTLSClientFromBytes(endpoint, certPEMBlock, keyPEMBlock, caPEMCert, apiVersionString)
}
// NewClientFromEnv returns a Client instance ready for communication created from
// Docker's default logic for the environment variables DOCKER_HOST, DOCKER_TLS_VERIFY, and DOCKER_CERT_PATH.
//
// See https://github.com/docker/docker/blob/1f963af697e8df3a78217f6fdbf67b8123a7db94/docker/docker.go#L68.
// See https://github.com/docker/compose/blob/81707ef1ad94403789166d2fe042c8a718a4c748/compose/cli/docker_client.py#L7.
func NewClientFromEnv() (*Client, error) {
client, err := NewVersionedClientFromEnv("")
if err != nil {
return nil, err
}
client.SkipServerVersionCheck = true
return client, nil
}
// NewVersionedClientFromEnv returns a Client instance ready for TLS communications created from
// Docker's default logic for the environment variables DOCKER_HOST, DOCKER_TLS_VERIFY, and DOCKER_CERT_PATH,
// and using a specific remote API version.
//
// See https://github.com/docker/docker/blob/1f963af697e8df3a78217f6fdbf67b8123a7db94/docker/docker.go#L68.
// See https://github.com/docker/compose/blob/81707ef1ad94403789166d2fe042c8a718a4c748/compose/cli/docker_client.py#L7.
func NewVersionedClientFromEnv(apiVersionString string) (*Client, error) {
dockerEnv, err := getDockerEnv()
if err != nil {
return nil, err
}
dockerHost := dockerEnv.dockerHost
if dockerEnv.dockerTLSVerify {
parts := strings.SplitN(dockerEnv.dockerHost, "://", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("could not split %s into two parts by ://", dockerHost)
}
cert := filepath.Join(dockerEnv.dockerCertPath, "cert.pem")
key := filepath.Join(dockerEnv.dockerCertPath, "key.pem")
ca := filepath.Join(dockerEnv.dockerCertPath, "ca.pem")
return NewVersionedTLSClient(dockerEnv.dockerHost, cert, key, ca, apiVersionString)
}
return NewVersionedClient(dockerEnv.dockerHost, apiVersionString)
}
// NewVersionedTLSClientFromBytes returns a Client instance ready for TLS communications with the givens
// server endpoint, key and certificates (passed inline to the function as opposed to being
// read from a local file), using a specific remote API version.
func NewVersionedTLSClientFromBytes(endpoint string, certPEMBlock, keyPEMBlock, caPEMCert []byte, apiVersionString string) (*Client, error) {
u, err := parseEndpoint(endpoint, true)
if err != nil {
return nil, err
}
var requestedAPIVersion APIVersion
if strings.Contains(apiVersionString, ".") {
requestedAPIVersion, err = NewAPIVersion(apiVersionString)
if err != nil {
return nil, err
}
}
tlsConfig := &tls.Config{}
if certPEMBlock != nil && keyPEMBlock != nil {
tlsCert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{tlsCert}
}
if caPEMCert == nil {
tlsConfig.InsecureSkipVerify = true
} else {
caPool := x509.NewCertPool()
if !caPool.AppendCertsFromPEM(caPEMCert) {
return nil, errors.New("Could not add RootCA pem")
}
tlsConfig.RootCAs = caPool
}
tr := defaultTransport()
tr.TLSClientConfig = tlsConfig
if err != nil {
return nil, err
}
c := &Client{
HTTPClient: &http.Client{Transport: tr},
TLSConfig: tlsConfig,
Dialer: &net.Dialer{},
endpoint: endpoint,
endpointURL: u,
eventMonitor: new(eventMonitoringState),
requestedAPIVersion: requestedAPIVersion,
}
c.initializeNativeClient(defaultTransport)
return c, nil
}
// SetTimeout takes a timeout and applies it to the HTTPClient. It should not
// be called concurrently with any other Client methods.
func (c *Client) SetTimeout(t time.Duration) {
if c.HTTPClient != nil {
c.HTTPClient.Timeout = t
}
}
func (c *Client) checkAPIVersion() error {
c.apiVersionMutex.Lock()
defer c.apiVersionMutex.Unlock()
if c.serverAPIVersion == nil {
serverAPIVersionString, err := c.getServerAPIVersionString()
if err != nil {
return err
}
c.serverAPIVersion, err = NewAPIVersion(serverAPIVersionString)
if err != nil {
return err
}
}
if c.requestedAPIVersion == nil {
c.expectedAPIVersion = c.serverAPIVersion
} else {
c.expectedAPIVersion = c.requestedAPIVersion
}
return nil
}
// Endpoint returns the current endpoint. It's useful for getting the endpoint
// when using functions that get this data from the environment (like
// NewClientFromEnv.
func (c *Client) Endpoint() string {
return c.endpoint
}
// Ping pings the docker server
//
// See https://goo.gl/wYfgY1 for more details.
func (c *Client) Ping() error {
return c.PingWithContext(context.Background())
}
// PingWithContext pings the docker server
// The context object can be used to cancel the ping request.
//
// See https://goo.gl/wYfgY1 for more details.
func (c *Client) PingWithContext(ctx context.Context) error {
path := "/_ping"
resp, err := c.do("GET", path, doOptions{context: ctx})
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return newError(resp)
}
resp.Body.Close()
return nil
}
func (c *Client) getServerAPIVersionString() (version string, err error) {
resp, err := c.do("GET", "/version", doOptions{})
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("Received unexpected status %d while trying to retrieve the server version", resp.StatusCode)
}
var versionResponse map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&versionResponse); err != nil {
return "", err
}
if version, ok := (versionResponse["ApiVersion"]).(string); ok {
return version, nil
}
return "", nil
}
type doOptions struct {
data interface{}
forceJSON bool
headers map[string]string
context context.Context
}
func (c *Client) do(method, path string, doOptions doOptions) (*http.Response, error) {
var params io.Reader
if doOptions.data != nil || doOptions.forceJSON {
buf, err := json.Marshal(doOptions.data)
if err != nil {
return nil, err
}
params = bytes.NewBuffer(buf)
}
if path != "/version" && !c.SkipServerVersionCheck && c.expectedAPIVersion == nil {
err := c.checkAPIVersion()
if err != nil {
return nil, err
}
}
protocol := c.endpointURL.Scheme
var u string
switch protocol {
case unixProtocol, namedPipeProtocol:
u = c.getFakeNativeURL(path)
default:
u = c.getURL(path)
}
req, err := http.NewRequest(method, u, params)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", userAgent)
if doOptions.data != nil {
req.Header.Set("Content-Type", "application/json")
} else if method == "POST" {
req.Header.Set("Content-Type", "plain/text")
}
for k, v := range doOptions.headers {
req.Header.Set(k, v)
}
ctx := doOptions.context
if ctx == nil {
ctx = context.Background()
}
resp, err := c.HTTPClient.Do(req.WithContext(ctx))
if err != nil {
if strings.Contains(err.Error(), "connection refused") {
return nil, ErrConnectionRefused
}
return nil, chooseError(ctx, err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
return nil, newError(resp)
}
return resp, nil
}
type streamOptions struct {
setRawTerminal bool
rawJSONStream bool
useJSONDecoder bool
headers map[string]string
in io.Reader
stdout io.Writer
stderr io.Writer
reqSent chan struct{}
// timeout is the initial connection timeout
timeout time.Duration
// Timeout with no data is received, it's reset every time new data
// arrives
inactivityTimeout time.Duration
context context.Context
}
// if error in context, return that instead of generic http error
func chooseError(ctx context.Context, err error) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
return err
}
}
func (c *Client) stream(method, path string, streamOptions streamOptions) error {
if (method == "POST" || method == "PUT") && streamOptions.in == nil {
streamOptions.in = bytes.NewReader(nil)
}
if path != "/version" && !c.SkipServerVersionCheck && c.expectedAPIVersion == nil {
err := c.checkAPIVersion()
if err != nil {
return err
}
}
req, err := http.NewRequest(method, c.getURL(path), streamOptions.in)
if err != nil {
return err
}
req.Header.Set("User-Agent", userAgent)
if method == "POST" {
req.Header.Set("Content-Type", "plain/text")
}
for key, val := range streamOptions.headers {
req.Header.Set(key, val)
}
var resp *http.Response
protocol := c.endpointURL.Scheme
address := c.endpointURL.Path
if streamOptions.stdout == nil {
streamOptions.stdout = io.Discard
}
if streamOptions.stderr == nil {
streamOptions.stderr = io.Discard
}
// make a sub-context so that our active cancellation does not affect parent
ctx := streamOptions.context
if ctx == nil {
ctx = context.Background()
}
subCtx, cancelRequest := context.WithCancel(ctx)
defer cancelRequest()
if protocol == unixProtocol || protocol == namedPipeProtocol {
var dial net.Conn
dial, err = c.Dialer.Dial(protocol, address)
if err != nil {
return err
}
go func() {
<-subCtx.Done()
dial.Close()
}()
breader := bufio.NewReader(dial)
err = req.Write(dial)
if err != nil {
return chooseError(subCtx, err)
}
// ReadResponse may hang if server does not replay
if streamOptions.timeout > 0 {
dial.SetDeadline(time.Now().Add(streamOptions.timeout))
}
if streamOptions.reqSent != nil {
close(streamOptions.reqSent)
}
if resp, err = http.ReadResponse(breader, req); err != nil {
// Cancel timeout for future I/O operations
if streamOptions.timeout > 0 {
dial.SetDeadline(time.Time{})
}
if strings.Contains(err.Error(), "connection refused") {
return ErrConnectionRefused
}
return chooseError(subCtx, err)
}
} else {
if resp, err = c.HTTPClient.Do(req.WithContext(subCtx)); err != nil {
if strings.Contains(err.Error(), "connection refused") {
return ErrConnectionRefused
}
return chooseError(subCtx, err)
}
if streamOptions.reqSent != nil {
close(streamOptions.reqSent)
}
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
return newError(resp)
}
var canceled uint32
if streamOptions.inactivityTimeout > 0 {
var ch chan<- struct{}
resp.Body, ch = handleInactivityTimeout(resp.Body, streamOptions.inactivityTimeout, cancelRequest, &canceled)
defer close(ch)
}
err = handleStreamResponse(resp, &streamOptions)
if err != nil {
if atomic.LoadUint32(&canceled) != 0 {
return ErrInactivityTimeout
}
return chooseError(subCtx, err)
}
return nil
}
func handleStreamResponse(resp *http.Response, streamOptions *streamOptions) error {
var err error
if !streamOptions.useJSONDecoder && resp.Header.Get("Content-Type") != "application/json" {
if streamOptions.setRawTerminal {
_, err = io.Copy(streamOptions.stdout, resp.Body)
} else {
_, err = stdcopy.StdCopy(streamOptions.stdout, streamOptions.stderr, resp.Body)
}
return err
}
// if we want to get raw json stream, just copy it back to output
// without decoding it
if streamOptions.rawJSONStream {
_, err = io.Copy(streamOptions.stdout, resp.Body)
return err
}
if st, ok := streamOptions.stdout.(interface {
io.Writer
FD() uintptr
IsTerminal() bool
}); ok {
err = jsonmessage.DisplayJSONMessagesToStream(resp.Body, st, nil)
} else {
err = jsonmessage.DisplayJSONMessagesStream(resp.Body, streamOptions.stdout, 0, false, nil)
}
return err
}
type proxyReader struct {
io.ReadCloser
calls uint64
}
func (p *proxyReader) callCount() uint64 {
return atomic.LoadUint64(&p.calls)
}
func (p *proxyReader) Read(data []byte) (int, error) {
atomic.AddUint64(&p.calls, 1)
return p.ReadCloser.Read(data)
}
func handleInactivityTimeout(reader io.ReadCloser, timeout time.Duration, cancelRequest func(), canceled *uint32) (io.ReadCloser, chan<- struct{}) {
done := make(chan struct{})
proxyReader := &proxyReader{ReadCloser: reader}
go func() {
var lastCallCount uint64
for {
select {
case <-time.After(timeout):
case <-done:
return
}
curCallCount := proxyReader.callCount()
if curCallCount == lastCallCount {
atomic.AddUint32(canceled, 1)
cancelRequest()
return
}
lastCallCount = curCallCount
}
}()
return proxyReader, done
}
type hijackOptions struct {
success chan struct{}
setRawTerminal bool
in io.Reader
stdout io.Writer
stderr io.Writer
data interface{}
}
// CloseWaiter is an interface with methods for closing the underlying resource
// and then waiting for it to finish processing.
type CloseWaiter interface {
io.Closer
Wait() error
}
type waiterFunc func() error
func (w waiterFunc) Wait() error { return w() }
type closerFunc func() error
func (c closerFunc) Close() error { return c() }
func (c *Client) hijack(method, path string, hijackOptions hijackOptions) (CloseWaiter, error) {
if path != "/version" && !c.SkipServerVersionCheck && c.expectedAPIVersion == nil {
err := c.checkAPIVersion()
if err != nil {
return nil, err
}
}
var params io.Reader
if hijackOptions.data != nil {
buf, err := json.Marshal(hijackOptions.data)
if err != nil {
return nil, err
}
params = bytes.NewBuffer(buf)
}
req, err := http.NewRequest(method, c.getURL(path), params)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "tcp")
protocol := c.endpointURL.Scheme
address := c.endpointURL.Path
if protocol != unixProtocol && protocol != namedPipeProtocol {
protocol = "tcp"
address = c.endpointURL.Host
}
var dial net.Conn
if c.TLSConfig != nil && protocol != unixProtocol && protocol != namedPipeProtocol {
netDialer, ok := c.Dialer.(*net.Dialer)
if !ok {
return nil, ErrTLSNotSupported
}
dial, err = tlsDialWithDialer(netDialer, protocol, address, c.TLSConfig)
if err != nil {
return nil, err
}
} else {
dial, err = c.Dialer.Dial(protocol, address)
if err != nil {
return nil, err
}
}
errs := make(chan error, 1)
quit := make(chan struct{})
go func() {
clientconn := httputil.NewClientConn(dial, nil)
defer clientconn.Close()
clientconn.Do(req)
if hijackOptions.success != nil {
hijackOptions.success <- struct{}{}
<-hijackOptions.success
}
rwc, br := clientconn.Hijack()
defer rwc.Close()
errChanOut := make(chan error, 1)
errChanIn := make(chan error, 2)
if hijackOptions.stdout == nil && hijackOptions.stderr == nil {
close(errChanOut)
} else {
// Only copy if hijackOptions.stdout and/or hijackOptions.stderr is actually set.
// Otherwise, if the only stream you care about is stdin, your attach session
// will "hang" until the container terminates, even though you're not reading
// stdout/stderr
if hijackOptions.stdout == nil {
hijackOptions.stdout = io.Discard
}
if hijackOptions.stderr == nil {
hijackOptions.stderr = io.Discard
}
go func() {
defer func() {
if hijackOptions.in != nil {
if closer, ok := hijackOptions.in.(io.Closer); ok {
closer.Close()
}
errChanIn <- nil
}
}()
var err error
if hijackOptions.setRawTerminal {
_, err = io.Copy(hijackOptions.stdout, br)
} else {
_, err = stdcopy.StdCopy(hijackOptions.stdout, hijackOptions.stderr, br)
}
errChanOut <- err
}()
}
go func() {
var err error
if hijackOptions.in != nil {
_, err = io.Copy(rwc, hijackOptions.in)
}
errChanIn <- err
rwc.(interface {
CloseWrite() error
}).CloseWrite()
}()
var errIn error
select {
case errIn = <-errChanIn:
case <-quit:
}
var errOut error
select {
case errOut = <-errChanOut:
case <-quit:
}
if errIn != nil {
errs <- errIn
} else {
errs <- errOut
}
}()
return struct {
closerFunc
waiterFunc
}{
closerFunc(func() error { close(quit); return nil }),
waiterFunc(func() error { return <-errs }),
}, nil
}
func (c *Client) getURL(path string) string {
urlStr := strings.TrimRight(c.endpointURL.String(), "/")
if c.endpointURL.Scheme == unixProtocol || c.endpointURL.Scheme == namedPipeProtocol {
urlStr = ""
}
if c.requestedAPIVersion != nil {
return fmt.Sprintf("%s/v%s%s", urlStr, c.requestedAPIVersion, path)
}
return fmt.Sprintf("%s%s", urlStr, path)
}
// getFakeNativeURL returns the URL needed to make an HTTP request over a UNIX
// domain socket to the given path.
func (c *Client) getFakeNativeURL(path string) string {
u := *c.endpointURL // Copy.
// Override URL so that net/http will not complain.
u.Scheme = "http"
u.Host = "unix.sock" // Doesn't matter what this is - it's not used.
u.Path = ""
urlStr := strings.TrimRight(u.String(), "/")
if c.requestedAPIVersion != nil {
return fmt.Sprintf("%s/v%s%s", urlStr, c.requestedAPIVersion, path)
}
return fmt.Sprintf("%s%s", urlStr, path)
}
func queryString(opts interface{}) string {
if opts == nil {
return ""
}
value := reflect.ValueOf(opts)
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
if value.Kind() != reflect.Struct {
return ""
}
items := url.Values(map[string][]string{})
for i := 0; i < value.NumField(); i++ {
field := value.Type().Field(i)
if field.PkgPath != "" {
continue
}
key := field.Tag.Get("qs")
if key == "" {
key = strings.ToLower(field.Name)
} else if key == "-" {
continue
}
addQueryStringValue(items, key, value.Field(i))
}
return items.Encode()
}
func addQueryStringValue(items url.Values, key string, v reflect.Value) {
switch v.Kind() {
case reflect.Bool:
if v.Bool() {
items.Add(key, "1")
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if v.Int() > 0 {
items.Add(key, strconv.FormatInt(v.Int(), 10))
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if v.Uint() > 0 {
items.Add(key, strconv.FormatUint(v.Uint(), 10))
}
case reflect.Float32, reflect.Float64:
if v.Float() > 0 {
items.Add(key, strconv.FormatFloat(v.Float(), 'f', -1, 64))
}
case reflect.String:
if v.String() != "" {
items.Add(key, v.String())
}
case reflect.Ptr:
if !v.IsNil() {
if b, err := json.Marshal(v.Interface()); err == nil {
items.Add(key, string(b))
}
}
case reflect.Map:
if len(v.MapKeys()) > 0 {
if b, err := json.Marshal(v.Interface()); err == nil {
items.Add(key, string(b))
}
}
case reflect.Array, reflect.Slice:
vLen := v.Len()
if vLen > 0 {
for i := 0; i < vLen; i++ {
addQueryStringValue(items, key, v.Index(i))
}
}
}
}
// Error represents failures in the API. It represents a failure from the API.
type Error struct {
Status int
Message string
}
func newError(resp *http.Response) *Error {
type ErrMsg struct {
Message string `json:"message"`
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return &Error{Status: resp.StatusCode, Message: fmt.Sprintf("cannot read body, err: %v", err)}
}
var emsg ErrMsg
err = json.Unmarshal(data, &emsg)
if err != nil {
return &Error{Status: resp.StatusCode, Message: string(data)}
}
return &Error{Status: resp.StatusCode, Message: emsg.Message}
}
func (e *Error) Error() string {
return fmt.Sprintf("API error (%d): %s", e.Status, e.Message)
}
func parseEndpoint(endpoint string, tls bool) (*url.URL, error) {
if endpoint != "" && !strings.Contains(endpoint, "://") {
endpoint = "tcp://" + endpoint
}
u, err := url.Parse(endpoint)
if err != nil {
return nil, ErrInvalidEndpoint
}
if tls && u.Scheme != "unix" {
u.Scheme = "https"
}
switch u.Scheme {
case unixProtocol, namedPipeProtocol:
return u, nil
case "http", "https", "tcp":
_, port, err := net.SplitHostPort(u.Host)
if err != nil {
if e, ok := err.(*net.AddrError); ok {
if e.Err == "missing port in address" {
return u, nil
}
}
return nil, ErrInvalidEndpoint
}
number, err := strconv.ParseInt(port, 10, 64)
if err == nil && number > 0 && number < 65536 {
if u.Scheme == "tcp" {
if tls {
u.Scheme = "https"
} else {
u.Scheme = "http"
}
}
return u, nil
}
return nil, ErrInvalidEndpoint
default:
return nil, ErrInvalidEndpoint
}
}
type dockerEnv struct {
dockerHost string
dockerTLSVerify bool
dockerCertPath string
}
func getDockerEnv() (*dockerEnv, error) {
dockerHost := os.Getenv("DOCKER_HOST")
var err error
if dockerHost == "" {
dockerHost = opts.DefaultHost
}
dockerTLSVerify := os.Getenv("DOCKER_TLS_VERIFY") != ""
var dockerCertPath string
if dockerTLSVerify {
dockerCertPath = os.Getenv("DOCKER_CERT_PATH")
if dockerCertPath == "" {
home := homedir.Get()
if home == "" {
return nil, errors.New("environment variable HOME must be set if DOCKER_CERT_PATH is not set")
}
dockerCertPath = filepath.Join(home, ".docker")
dockerCertPath, err = filepath.Abs(dockerCertPath)
if err != nil {
return nil, err
}
}
}
return &dockerEnv{
dockerHost: dockerHost,
dockerTLSVerify: dockerTLSVerify,
dockerCertPath: dockerCertPath,
}, nil
}
// defaultTransport returns a new http.Transport with similar default values to
// http.DefaultTransport, but with idle connections and keepalives disabled.
func defaultTransport() *http.Transport {
transport := defaultPooledTransport()
transport.DisableKeepAlives = true
transport.MaxIdleConnsPerHost = -1
return transport
}
// defaultPooledTransport returns a new http.Transport with similar default
// values to http.DefaultTransport. Do not use this for transient transports as
// it can leak file descriptors over time. Only use this for transports that
// will be re-used for the same host(s).
func defaultPooledTransport() *http.Transport {
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1,
}
return transport
}
// defaultClient returns a new http.Client with similar default values to
// http.Client, but with a non-shared Transport, idle connections disabled, and
// keepalives disabled.
func defaultClient() *http.Client {
return &http.Client{
Transport: defaultTransport(),
}
}