mirror of
https://github.com/containers/podman.git
synced 2025-06-04 13:08:55 +08:00

Add a new "image" mount type to `--mount`. The source of the mount is the name or ID of an image. The destination is the path inside the container. Image mounts further support an optional `rw,readwrite` parameter which if set to "true" will yield the mount writable inside the container. Note that no changes are propagated to the image mount on the host (which in any case is read only). Mounts are overlay mounts. To support read-only overlay mounts, vendor a non-release version of Buildah. Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
1158 lines
32 KiB
Go
1158 lines
32 KiB
Go
// 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"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/docker/docker/pkg/homedir"
|
|
"github.com/docker/docker/pkg/jsonmessage"
|
|
"github.com/docker/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")
|
|
apiVersion118, _ = NewAPIVersion("1.18")
|
|
apiVersion119, _ = NewAPIVersion("1.19")
|
|
apiVersion121, _ = NewAPIVersion("1.21")
|
|
apiVersion124, _ = NewAPIVersion("1.24")
|
|
apiVersion125, _ = NewAPIVersion("1.25")
|
|
apiVersion135, _ = NewAPIVersion("1.35")
|
|
)
|
|
|
|
// 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 {
|
|
parts := make([]string, len(version))
|
|
for i, val := range version {
|
|
parts[i] = strconv.Itoa(val)
|
|
}
|
|
return strings.Join(parts, ".")
|
|
}
|
|
|
|
// 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
|
|
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 = ioutil.ReadFile(cert)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if _, err := os.Stat(key); !os.IsNotExist(err) {
|
|
keyPEMBlock, err = ioutil.ReadFile(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if _, err := os.Stat(ca); !os.IsNotExist(err) {
|
|
caPEMCert, err = ioutil.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, DOCKER_CERT_PATH,
|
|
// and DOCKER_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.
|
|
// See https://github.com/moby/moby/blob/28d7dba41d0c0d9c7f0dafcc79d3c59f2b3f5dc3/client/options.go#L51
|
|
func NewClientFromEnv() (*Client, error) {
|
|
apiVersionString := os.Getenv("DOCKER_API_VERSION")
|
|
client, err := NewVersionedClientFromEnv(apiVersionString)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
client.SkipServerVersionCheck = apiVersionString == ""
|
|
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{MinVersion: tls.VersionTLS12}
|
|
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 {
|
|
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.TODO())
|
|
}
|
|
|
|
// 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(http.MethodGet, 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(http.MethodGet, "/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 == http.MethodPost {
|
|
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
|
|
}
|
|
|
|
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 == http.MethodPost || method == http.MethodPut) && streamOptions.in == nil {
|
|
streamOptions.in = bytes.NewReader(nil)
|
|
}
|
|
if path != "/version" && !c.SkipServerVersionCheck && c.expectedAPIVersion == nil {
|
|
err := c.checkAPIVersion()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return c.streamURL(method, c.getURL(path), streamOptions)
|
|
}
|
|
|
|
func (c *Client) streamURL(method, url string, streamOptions streamOptions) error {
|
|
if (method == http.MethodPost || method == http.MethodPut) && streamOptions.in == nil {
|
|
streamOptions.in = bytes.NewReader(nil)
|
|
}
|
|
if !c.SkipServerVersionCheck && c.expectedAPIVersion == nil {
|
|
err := c.checkAPIVersion()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, url, streamOptions.in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("User-Agent", userAgent)
|
|
if method == http.MethodPost {
|
|
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 = ioutil.Discard
|
|
}
|
|
if streamOptions.stderr == nil {
|
|
streamOptions.stderr = ioutil.Discard
|
|
}
|
|
|
|
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)
|
|
}
|
|
defer resp.Body.Close()
|
|
} 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)
|
|
}
|
|
defer resp.Body.Close()
|
|
if streamOptions.reqSent != nil {
|
|
close(streamOptions.reqSent)
|
|
}
|
|
}
|
|
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.(stream); ok {
|
|
err = jsonmessage.DisplayJSONMessagesToStream(resp.Body, st, nil)
|
|
} else {
|
|
err = jsonmessage.DisplayJSONMessagesStream(resp.Body, streamOptions.stdout, 0, false, nil)
|
|
}
|
|
return err
|
|
}
|
|
|
|
type stream interface {
|
|
io.Writer
|
|
FD() uintptr
|
|
IsTerminal() bool
|
|
}
|
|
|
|
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() {
|
|
//nolint:staticcheck
|
|
clientconn := httputil.NewClientConn(dial, nil)
|
|
defer clientconn.Close()
|
|
//nolint:bodyclose
|
|
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 = ioutil.Discard
|
|
}
|
|
if hijackOptions.stderr == nil {
|
|
hijackOptions.stderr = ioutil.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)
|
|
}
|
|
|
|
func (c *Client) getPath(basepath string, opts interface{}) (string, error) {
|
|
queryStr, requiredAPIVersion := queryStringVersion(opts)
|
|
return c.pathVersionCheck(basepath, queryStr, requiredAPIVersion)
|
|
}
|
|
|
|
func (c *Client) pathVersionCheck(basepath, queryStr string, requiredAPIVersion APIVersion) (string, error) {
|
|
urlStr := strings.TrimRight(c.endpointURL.String(), "/")
|
|
if c.endpointURL.Scheme == unixProtocol || c.endpointURL.Scheme == namedPipeProtocol {
|
|
urlStr = ""
|
|
}
|
|
if c.requestedAPIVersion != nil {
|
|
if c.requestedAPIVersion.GreaterThanOrEqualTo(requiredAPIVersion) {
|
|
return fmt.Sprintf("%s/v%s%s?%s", urlStr, c.requestedAPIVersion, basepath, queryStr), nil
|
|
}
|
|
return "", fmt.Errorf("API %s requires version %s, requested version %s is insufficient",
|
|
basepath, requiredAPIVersion, c.requestedAPIVersion)
|
|
}
|
|
if requiredAPIVersion != nil {
|
|
return fmt.Sprintf("%s/v%s%s?%s", urlStr, requiredAPIVersion, basepath, queryStr), nil
|
|
}
|
|
return fmt.Sprintf("%s%s?%s", urlStr, basepath, queryStr), nil
|
|
}
|
|
|
|
// 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 queryStringVersion(opts interface{}) (string, APIVersion) {
|
|
if opts == nil {
|
|
return "", nil
|
|
}
|
|
value := reflect.ValueOf(opts)
|
|
if value.Kind() == reflect.Ptr {
|
|
value = value.Elem()
|
|
}
|
|
if value.Kind() != reflect.Struct {
|
|
return "", nil
|
|
}
|
|
var apiVersion APIVersion
|
|
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
|
|
}
|
|
if addQueryStringValue(items, key, value.Field(i)) {
|
|
verstr := field.Tag.Get("ver")
|
|
if verstr != "" {
|
|
ver, _ := NewAPIVersion(verstr)
|
|
if apiVersion == nil {
|
|
apiVersion = ver
|
|
} else if ver.GreaterThan(apiVersion) {
|
|
apiVersion = ver
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return items.Encode(), apiVersion
|
|
}
|
|
|
|
func queryString(opts interface{}) string {
|
|
s, _ := queryStringVersion(opts)
|
|
return s
|
|
}
|
|
|
|
func addQueryStringValue(items url.Values, key string, v reflect.Value) bool {
|
|
//nolint:exhaustive
|
|
switch v.Kind() {
|
|
case reflect.Bool:
|
|
if v.Bool() {
|
|
items.Add(key, "1")
|
|
return true
|
|
}
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
if v.Int() > 0 {
|
|
items.Add(key, strconv.FormatInt(v.Int(), 10))
|
|
return true
|
|
}
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
if v.Uint() > 0 {
|
|
items.Add(key, strconv.FormatUint(v.Uint(), 10))
|
|
return true
|
|
}
|
|
case reflect.Float32, reflect.Float64:
|
|
if v.Float() > 0 {
|
|
items.Add(key, strconv.FormatFloat(v.Float(), 'f', -1, 64))
|
|
return true
|
|
}
|
|
case reflect.String:
|
|
if v.String() != "" {
|
|
items.Add(key, v.String())
|
|
return true
|
|
}
|
|
case reflect.Ptr:
|
|
if !v.IsNil() {
|
|
if b, err := json.Marshal(v.Interface()); err == nil {
|
|
items.Add(key, string(b))
|
|
return true
|
|
}
|
|
}
|
|
case reflect.Map:
|
|
if len(v.MapKeys()) > 0 {
|
|
if b, err := json.Marshal(v.Interface()); err == nil {
|
|
items.Add(key, string(b))
|
|
return true
|
|
}
|
|
}
|
|
case reflect.Array, reflect.Slice:
|
|
vLen := v.Len()
|
|
var valuesAdded int
|
|
if vLen > 0 {
|
|
for i := 0; i < vLen; i++ {
|
|
if addQueryStringValue(items, key, v.Index(i)) {
|
|
valuesAdded++
|
|
}
|
|
}
|
|
}
|
|
return valuesAdded > 0
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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 := ioutil.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 = 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(),
|
|
}
|
|
}
|