dotcloud/docker

View on GitHub
testutil/request/request.go

Summary

Maintainability
A
0 mins
Test Coverage
package request // import "github.com/docker/docker/testutil/request"

import (
    "context"
    "crypto/tls"
    "fmt"
    "io"
    "net"
    "net/http"
    "net/url"
    "os"
    "path/filepath"
    "testing"
    "time"

    "github.com/docker/docker/client"
    "github.com/docker/docker/pkg/ioutils"
    "github.com/docker/docker/testutil/environment"
    "github.com/docker/go-connections/sockets"
    "github.com/docker/go-connections/tlsconfig"
    "github.com/pkg/errors"
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    "gotest.tools/v3/assert"
)

// NewAPIClient returns a docker API client configured from environment variables
func NewAPIClient(t testing.TB, ops ...client.Opt) client.APIClient {
    t.Helper()
    ops = append([]client.Opt{client.FromEnv}, ops...)
    clt, err := client.NewClientWithOpts(ops...)
    assert.NilError(t, err)
    return clt
}

// DaemonTime provides the current time on the daemon host
func DaemonTime(ctx context.Context, t testing.TB, client client.APIClient, testEnv *environment.Execution) time.Time {
    t.Helper()
    if testEnv.IsLocalDaemon() {
        return time.Now()
    }

    info, err := client.Info(ctx)
    assert.NilError(t, err)

    dt, err := time.Parse(time.RFC3339Nano, info.SystemTime)
    assert.NilError(t, err, "invalid time format in GET /info response")
    return dt
}

// DaemonUnixTime returns the current time on the daemon host with nanoseconds precision.
// It return the time formatted how the client sends timestamps to the server.
func DaemonUnixTime(ctx context.Context, t testing.TB, client client.APIClient, testEnv *environment.Execution) string {
    t.Helper()
    dt := DaemonTime(ctx, t, client, testEnv)
    return fmt.Sprintf("%d.%09d", dt.Unix(), int64(dt.Nanosecond()))
}

// Post creates and execute a POST request on the specified host and endpoint, with the specified request modifiers
func Post(ctx context.Context, endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
    return Do(ctx, endpoint, append(modifiers, Method(http.MethodPost))...)
}

// Delete creates and execute a DELETE request on the specified host and endpoint, with the specified request modifiers
func Delete(ctx context.Context, endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
    return Do(ctx, endpoint, append(modifiers, Method(http.MethodDelete))...)
}

// Get creates and execute a GET request on the specified host and endpoint, with the specified request modifiers
func Get(ctx context.Context, endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
    return Do(ctx, endpoint, modifiers...)
}

// Head creates and execute a HEAD request on the specified host and endpoint, with the specified request modifiers
func Head(ctx context.Context, endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
    return Do(ctx, endpoint, append(modifiers, Method(http.MethodHead))...)
}

// Do creates and execute a request on the specified endpoint, with the specified request modifiers
func Do(ctx context.Context, endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
    opts := &Options{
        host: DaemonHost(),
    }
    for _, mod := range modifiers {
        mod(opts)
    }
    req, err := newRequest(endpoint, opts)
    if err != nil {
        return nil, nil, err
    }
    req = req.WithContext(ctx)

    httpClient, err := newHTTPClient(opts.host)
    if err != nil {
        return nil, nil, err
    }

    resp, err := httpClient.Do(req)
    var body io.ReadCloser
    if resp != nil {
        body = ioutils.NewReadCloserWrapper(resp.Body, func() error {
            defer resp.Body.Close()
            return nil
        })
    }
    return resp, body, err
}

// ReadBody read the specified ReadCloser content and returns it
func ReadBody(b io.ReadCloser) ([]byte, error) {
    defer b.Close()
    return io.ReadAll(b)
}

// newRequest creates a new http Request to the specified host and endpoint, with the specified request modifiers
func newRequest(endpoint string, opts *Options) (*http.Request, error) {
    hostURL, err := client.ParseHostURL(opts.host)
    if err != nil {
        return nil, errors.Wrapf(err, "failed parsing url %q", opts.host)
    }
    req, err := http.NewRequest(http.MethodGet, endpoint, nil)
    if err != nil {
        return nil, errors.Wrap(err, "failed to create request")
    }

    if os.Getenv("DOCKER_TLS_VERIFY") != "" {
        req.URL.Scheme = "https"
    } else {
        req.URL.Scheme = "http"
    }
    req.URL.Host = hostURL.Host

    if hostURL.Scheme == "unix" || hostURL.Scheme == "npipe" {
        // Override host header for non-tcp connections.
        req.Host = client.DummyHost
    }

    for _, config := range opts.requestModifiers {
        if err := config(req); err != nil {
            return nil, err
        }
    }

    return req, nil
}

// newHTTPClient creates an http client for the specific host
// TODO: Share more code with client.defaultHTTPClient
func newHTTPClient(host string) (*http.Client, error) {
    // FIXME(vdemeester) 10*time.Second timeout of SockRequest… ?
    hostURL, err := client.ParseHostURL(host)
    if err != nil {
        return nil, err
    }
    transport := new(http.Transport)
    if hostURL.Scheme == "tcp" && os.Getenv("DOCKER_TLS_VERIFY") != "" {
        // Setup the socket TLS configuration.
        tlsConfig, err := getTLSConfig()
        if err != nil {
            return nil, err
        }
        transport = &http.Transport{TLSClientConfig: tlsConfig}
    }
    transport.DisableKeepAlives = true
    err = sockets.ConfigureTransport(transport, hostURL.Scheme, hostURL.Host)
    return &http.Client{Transport: otelhttp.NewTransport(transport)}, err
}

func getTLSConfig() (*tls.Config, error) {
    dockerCertPath := os.Getenv("DOCKER_CERT_PATH")

    if dockerCertPath == "" {
        return nil, errors.New("DOCKER_TLS_VERIFY specified, but no DOCKER_CERT_PATH environment variable")
    }

    option := &tlsconfig.Options{
        CAFile:   filepath.Join(dockerCertPath, "ca.pem"),
        CertFile: filepath.Join(dockerCertPath, "cert.pem"),
        KeyFile:  filepath.Join(dockerCertPath, "key.pem"),
    }
    tlsConfig, err := tlsconfig.Client(*option)
    if err != nil {
        return nil, err
    }

    return tlsConfig, nil
}

// DaemonHost return the daemon host string for this test execution
func DaemonHost() string {
    daemonURLStr := client.DefaultDockerHost
    if daemonHostVar := os.Getenv("DOCKER_HOST"); daemonHostVar != "" {
        daemonURLStr = daemonHostVar
    }
    return daemonURLStr
}

// SockConn opens a connection on the specified socket
func SockConn(timeout time.Duration, daemon string) (net.Conn, error) {
    daemonURL, err := url.Parse(daemon)
    if err != nil {
        return nil, errors.Wrapf(err, "could not parse url %q", daemon)
    }

    var c net.Conn
    switch daemonURL.Scheme {
    case "npipe":
        return npipeDial(daemonURL.Path, timeout)
    case "unix":
        return net.DialTimeout(daemonURL.Scheme, daemonURL.Path, timeout)
    case "tcp":
        if os.Getenv("DOCKER_TLS_VERIFY") != "" {
            // Setup the socket TLS configuration.
            tlsConfig, err := getTLSConfig()
            if err != nil {
                return nil, err
            }
            dialer := &net.Dialer{Timeout: timeout}
            return tls.DialWithDialer(dialer, daemonURL.Scheme, daemonURL.Host, tlsConfig)
        }
        return net.DialTimeout(daemonURL.Scheme, daemonURL.Host, timeout)
    default:
        return c, errors.Errorf("unknown scheme %v (%s)", daemonURL.Scheme, daemon)
    }
}