github.com/moby/docker@v26.1.3+incompatible/testutil/request/request.go (about)

     1  package request // import "github.com/docker/docker/testutil/request"
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"fmt"
     7  	"io"
     8  	"net"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"path/filepath"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/docker/docker/client"
    17  	"github.com/docker/docker/pkg/ioutils"
    18  	"github.com/docker/docker/testutil/environment"
    19  	"github.com/docker/go-connections/sockets"
    20  	"github.com/docker/go-connections/tlsconfig"
    21  	"github.com/pkg/errors"
    22  	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    23  	"gotest.tools/v3/assert"
    24  )
    25  
    26  // NewAPIClient returns a docker API client configured from environment variables
    27  func NewAPIClient(t testing.TB, ops ...client.Opt) client.APIClient {
    28  	t.Helper()
    29  	ops = append([]client.Opt{client.FromEnv}, ops...)
    30  	clt, err := client.NewClientWithOpts(ops...)
    31  	assert.NilError(t, err)
    32  	return clt
    33  }
    34  
    35  // DaemonTime provides the current time on the daemon host
    36  func DaemonTime(ctx context.Context, t testing.TB, client client.APIClient, testEnv *environment.Execution) time.Time {
    37  	t.Helper()
    38  	if testEnv.IsLocalDaemon() {
    39  		return time.Now()
    40  	}
    41  
    42  	info, err := client.Info(ctx)
    43  	assert.NilError(t, err)
    44  
    45  	dt, err := time.Parse(time.RFC3339Nano, info.SystemTime)
    46  	assert.NilError(t, err, "invalid time format in GET /info response")
    47  	return dt
    48  }
    49  
    50  // DaemonUnixTime returns the current time on the daemon host with nanoseconds precision.
    51  // It return the time formatted how the client sends timestamps to the server.
    52  func DaemonUnixTime(ctx context.Context, t testing.TB, client client.APIClient, testEnv *environment.Execution) string {
    53  	t.Helper()
    54  	dt := DaemonTime(ctx, t, client, testEnv)
    55  	return fmt.Sprintf("%d.%09d", dt.Unix(), int64(dt.Nanosecond()))
    56  }
    57  
    58  // Post creates and execute a POST request on the specified host and endpoint, with the specified request modifiers
    59  func Post(ctx context.Context, endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
    60  	return Do(ctx, endpoint, append(modifiers, Method(http.MethodPost))...)
    61  }
    62  
    63  // Delete creates and execute a DELETE request on the specified host and endpoint, with the specified request modifiers
    64  func Delete(ctx context.Context, endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
    65  	return Do(ctx, endpoint, append(modifiers, Method(http.MethodDelete))...)
    66  }
    67  
    68  // Get creates and execute a GET request on the specified host and endpoint, with the specified request modifiers
    69  func Get(ctx context.Context, endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
    70  	return Do(ctx, endpoint, modifiers...)
    71  }
    72  
    73  // Head creates and execute a HEAD request on the specified host and endpoint, with the specified request modifiers
    74  func Head(ctx context.Context, endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
    75  	return Do(ctx, endpoint, append(modifiers, Method(http.MethodHead))...)
    76  }
    77  
    78  // Do creates and execute a request on the specified endpoint, with the specified request modifiers
    79  func Do(ctx context.Context, endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
    80  	opts := &Options{
    81  		host: DaemonHost(),
    82  	}
    83  	for _, mod := range modifiers {
    84  		mod(opts)
    85  	}
    86  	req, err := newRequest(endpoint, opts)
    87  	if err != nil {
    88  		return nil, nil, err
    89  	}
    90  	req = req.WithContext(ctx)
    91  
    92  	httpClient, err := newHTTPClient(opts.host)
    93  	if err != nil {
    94  		return nil, nil, err
    95  	}
    96  
    97  	resp, err := httpClient.Do(req)
    98  	var body io.ReadCloser
    99  	if resp != nil {
   100  		body = ioutils.NewReadCloserWrapper(resp.Body, func() error {
   101  			defer resp.Body.Close()
   102  			return nil
   103  		})
   104  	}
   105  	return resp, body, err
   106  }
   107  
   108  // ReadBody read the specified ReadCloser content and returns it
   109  func ReadBody(b io.ReadCloser) ([]byte, error) {
   110  	defer b.Close()
   111  	return io.ReadAll(b)
   112  }
   113  
   114  // newRequest creates a new http Request to the specified host and endpoint, with the specified request modifiers
   115  func newRequest(endpoint string, opts *Options) (*http.Request, error) {
   116  	hostURL, err := client.ParseHostURL(opts.host)
   117  	if err != nil {
   118  		return nil, errors.Wrapf(err, "failed parsing url %q", opts.host)
   119  	}
   120  	req, err := http.NewRequest(http.MethodGet, endpoint, nil)
   121  	if err != nil {
   122  		return nil, errors.Wrap(err, "failed to create request")
   123  	}
   124  
   125  	if os.Getenv("DOCKER_TLS_VERIFY") != "" {
   126  		req.URL.Scheme = "https"
   127  	} else {
   128  		req.URL.Scheme = "http"
   129  	}
   130  	req.URL.Host = hostURL.Host
   131  
   132  	if hostURL.Scheme == "unix" || hostURL.Scheme == "npipe" {
   133  		// Override host header for non-tcp connections.
   134  		req.Host = client.DummyHost
   135  	}
   136  
   137  	for _, config := range opts.requestModifiers {
   138  		if err := config(req); err != nil {
   139  			return nil, err
   140  		}
   141  	}
   142  
   143  	return req, nil
   144  }
   145  
   146  // newHTTPClient creates an http client for the specific host
   147  // TODO: Share more code with client.defaultHTTPClient
   148  func newHTTPClient(host string) (*http.Client, error) {
   149  	// FIXME(vdemeester) 10*time.Second timeout of SockRequest… ?
   150  	hostURL, err := client.ParseHostURL(host)
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  	transport := new(http.Transport)
   155  	if hostURL.Scheme == "tcp" && os.Getenv("DOCKER_TLS_VERIFY") != "" {
   156  		// Setup the socket TLS configuration.
   157  		tlsConfig, err := getTLSConfig()
   158  		if err != nil {
   159  			return nil, err
   160  		}
   161  		transport = &http.Transport{TLSClientConfig: tlsConfig}
   162  	}
   163  	transport.DisableKeepAlives = true
   164  	err = sockets.ConfigureTransport(transport, hostURL.Scheme, hostURL.Host)
   165  	return &http.Client{Transport: otelhttp.NewTransport(transport)}, err
   166  }
   167  
   168  func getTLSConfig() (*tls.Config, error) {
   169  	dockerCertPath := os.Getenv("DOCKER_CERT_PATH")
   170  
   171  	if dockerCertPath == "" {
   172  		return nil, errors.New("DOCKER_TLS_VERIFY specified, but no DOCKER_CERT_PATH environment variable")
   173  	}
   174  
   175  	option := &tlsconfig.Options{
   176  		CAFile:   filepath.Join(dockerCertPath, "ca.pem"),
   177  		CertFile: filepath.Join(dockerCertPath, "cert.pem"),
   178  		KeyFile:  filepath.Join(dockerCertPath, "key.pem"),
   179  	}
   180  	tlsConfig, err := tlsconfig.Client(*option)
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  
   185  	return tlsConfig, nil
   186  }
   187  
   188  // DaemonHost return the daemon host string for this test execution
   189  func DaemonHost() string {
   190  	daemonURLStr := client.DefaultDockerHost
   191  	if daemonHostVar := os.Getenv("DOCKER_HOST"); daemonHostVar != "" {
   192  		daemonURLStr = daemonHostVar
   193  	}
   194  	return daemonURLStr
   195  }
   196  
   197  // SockConn opens a connection on the specified socket
   198  func SockConn(timeout time.Duration, daemon string) (net.Conn, error) {
   199  	daemonURL, err := url.Parse(daemon)
   200  	if err != nil {
   201  		return nil, errors.Wrapf(err, "could not parse url %q", daemon)
   202  	}
   203  
   204  	var c net.Conn
   205  	switch daemonURL.Scheme {
   206  	case "npipe":
   207  		return npipeDial(daemonURL.Path, timeout)
   208  	case "unix":
   209  		return net.DialTimeout(daemonURL.Scheme, daemonURL.Path, timeout)
   210  	case "tcp":
   211  		if os.Getenv("DOCKER_TLS_VERIFY") != "" {
   212  			// Setup the socket TLS configuration.
   213  			tlsConfig, err := getTLSConfig()
   214  			if err != nil {
   215  				return nil, err
   216  			}
   217  			dialer := &net.Dialer{Timeout: timeout}
   218  			return tls.DialWithDialer(dialer, daemonURL.Scheme, daemonURL.Host, tlsConfig)
   219  		}
   220  		return net.DialTimeout(daemonURL.Scheme, daemonURL.Host, timeout)
   221  	default:
   222  		return c, errors.Errorf("unknown scheme %v (%s)", daemonURL.Scheme, daemon)
   223  	}
   224  }