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 }