1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
|
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)
}
}
|