e8dc902781
Integration tests will now configure clients to propagate traces as well as create spans for all tests. Some extra changes were needed (or desired for trace propagation) in the test helpers to pass through tracing spans via context. Signed-off-by: Brian Goff <cpuguy83@gmail.com>
224 lines
7 KiB
Go
224 lines
7 KiB
Go
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)
|
|
}
|
|
}
|