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