2018-04-17 04:22:04 -04:00
|
|
|
package request // import "github.com/docker/docker/internal/test/request"
|
2016-12-30 04:49:36 -05:00
|
|
|
|
|
|
|
import (
|
2018-04-17 04:22:04 -04:00
|
|
|
"context"
|
2016-12-30 04:49:36 -05:00
|
|
|
"crypto/tls"
|
2018-04-17 04:22:04 -04:00
|
|
|
"fmt"
|
2016-12-30 04:49:36 -05:00
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"time"
|
|
|
|
|
2018-04-17 04:22:04 -04:00
|
|
|
"github.com/docker/docker/client"
|
2018-04-18 11:16:55 -04:00
|
|
|
"github.com/docker/docker/internal/test"
|
2018-04-17 04:22:04 -04:00
|
|
|
"github.com/docker/docker/internal/test/environment"
|
2017-03-06 10:35:27 -05:00
|
|
|
"github.com/docker/docker/opts"
|
2016-12-30 04:49:36 -05:00
|
|
|
"github.com/docker/docker/pkg/ioutils"
|
|
|
|
"github.com/docker/go-connections/sockets"
|
|
|
|
"github.com/docker/go-connections/tlsconfig"
|
|
|
|
"github.com/pkg/errors"
|
2018-06-11 09:32:11 -04:00
|
|
|
"gotest.tools/assert"
|
2016-12-30 04:49:36 -05:00
|
|
|
)
|
|
|
|
|
2018-04-17 04:22:04 -04:00
|
|
|
// NewAPIClient returns a docker API client configured from environment variables
|
2019-04-09 19:11:53 -04:00
|
|
|
func NewAPIClient(t assert.TestingT, ops ...client.Opt) client.APIClient {
|
2018-04-18 11:16:55 -04:00
|
|
|
if ht, ok := t.(test.HelperT); ok {
|
|
|
|
ht.Helper()
|
|
|
|
}
|
2019-04-09 19:11:53 -04:00
|
|
|
ops = append([]client.Opt{client.FromEnv}, ops...)
|
2018-04-17 04:22:04 -04:00
|
|
|
clt, err := client.NewClientWithOpts(ops...)
|
|
|
|
assert.NilError(t, err)
|
|
|
|
return clt
|
2017-02-28 11:12:30 -05:00
|
|
|
}
|
|
|
|
|
2018-04-17 04:22:04 -04:00
|
|
|
// DaemonTime provides the current time on the daemon host
|
|
|
|
func DaemonTime(ctx context.Context, t assert.TestingT, client client.APIClient, testEnv *environment.Execution) time.Time {
|
2018-04-18 11:16:55 -04:00
|
|
|
if ht, ok := t.(test.HelperT); ok {
|
|
|
|
ht.Helper()
|
|
|
|
}
|
2018-04-17 04:22:04 -04:00
|
|
|
if testEnv.IsLocalDaemon() {
|
|
|
|
return time.Now()
|
2017-02-28 11:12:30 -05:00
|
|
|
}
|
|
|
|
|
2018-04-17 04:22:04 -04:00
|
|
|
info, err := client.Info(ctx)
|
|
|
|
assert.NilError(t, err)
|
2017-02-28 11:12:30 -05:00
|
|
|
|
2018-04-17 04:22:04 -04:00
|
|
|
dt, err := time.Parse(time.RFC3339Nano, info.SystemTime)
|
|
|
|
assert.NilError(t, err, "invalid time format in GET /info response")
|
|
|
|
return dt
|
2016-12-30 04:49:36 -05:00
|
|
|
}
|
|
|
|
|
2018-04-17 04:22:04 -04:00
|
|
|
// 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 assert.TestingT, client client.APIClient, testEnv *environment.Execution) string {
|
2018-04-18 11:16:55 -04:00
|
|
|
if ht, ok := t.(test.HelperT); ok {
|
|
|
|
ht.Helper()
|
|
|
|
}
|
2018-04-17 04:22:04 -04:00
|
|
|
dt := DaemonTime(ctx, t, client, testEnv)
|
|
|
|
return fmt.Sprintf("%d.%09d", dt.Unix(), int64(dt.Nanosecond()))
|
2016-12-30 04:49:36 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Post creates and execute a POST request on the specified host and endpoint, with the specified request modifiers
|
2018-04-17 04:22:04 -04:00
|
|
|
func Post(endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
|
2017-03-06 10:35:27 -05:00
|
|
|
return Do(endpoint, append(modifiers, Method(http.MethodPost))...)
|
2016-12-30 04:49:36 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Delete creates and execute a DELETE request on the specified host and endpoint, with the specified request modifiers
|
2018-04-17 04:22:04 -04:00
|
|
|
func Delete(endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
|
2017-03-06 10:35:27 -05:00
|
|
|
return Do(endpoint, append(modifiers, Method(http.MethodDelete))...)
|
2016-12-30 04:49:36 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get creates and execute a GET request on the specified host and endpoint, with the specified request modifiers
|
2018-04-17 04:22:04 -04:00
|
|
|
func Get(endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
|
2017-03-06 10:35:27 -05:00
|
|
|
return Do(endpoint, modifiers...)
|
2016-12-30 04:49:36 -05:00
|
|
|
}
|
|
|
|
|
Add HEAD support for /_ping endpoint
Monitoring systems and load balancers are usually configured to use HEAD
requests for health monitoring. The /_ping endpoint currently does not
support this type of request, which means that those systems have fallback
to GET requests.
This patch adds support for HEAD requests on the /_ping endpoint.
Although optional, this patch also returns `Content-Type` and `Content-Length`
headers in case of a HEAD request; Refering to RFC 7231, section 4.3.2:
The HEAD method is identical to GET except that the server MUST NOT
send a message body in the response (i.e., the response terminates at
the end of the header section). The server SHOULD send the same
header fields in response to a HEAD request as it would have sent if
the request had been a GET, except that the payload header fields
(Section 3.3) MAY be omitted. This method can be used for obtaining
metadata about the selected representation without transferring the
representation data and is often used for testing hypertext links for
validity, accessibility, and recent modification.
A payload within a HEAD request message has no defined semantics;
sending a payload body on a HEAD request might cause some existing
implementations to reject the request.
The response to a HEAD request is cacheable; a cache MAY use it to
satisfy subsequent HEAD requests unless otherwise indicated by the
Cache-Control header field (Section 5.2 of [RFC7234]). A HEAD
response might also have an effect on previously cached responses to
GET; see Section 4.3.5 of [RFC7234].
With this patch applied, either `GET` or `HEAD` requests work; the only
difference is that the body is empty in case of a `HEAD` request;
curl -i --unix-socket /var/run/docker.sock http://localhost/_ping
HTTP/1.1 200 OK
Api-Version: 1.40
Cache-Control: no-cache, no-store, must-revalidate
Docker-Experimental: false
Ostype: linux
Pragma: no-cache
Server: Docker/dev (linux)
Date: Mon, 14 Jan 2019 12:35:16 GMT
Content-Length: 2
Content-Type: text/plain; charset=utf-8
OK
curl --head -i --unix-socket /var/run/docker.sock http://localhost/_ping
HTTP/1.1 200 OK
Api-Version: 1.40
Cache-Control: no-cache, no-store, must-revalidate
Content-Length: 0
Content-Type: text/plain; charset=utf-8
Docker-Experimental: false
Ostype: linux
Pragma: no-cache
Server: Docker/dev (linux)
Date: Mon, 14 Jan 2019 12:34:15 GMT
The client is also updated to use `HEAD` by default, but fallback to `GET`
if the daemon does not support this method.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-01-14 12:08:49 -05:00
|
|
|
// Head creates and execute a HEAD request on the specified host and endpoint, with the specified request modifiers
|
|
|
|
func Head(endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
|
|
|
|
return Do(endpoint, append(modifiers, Method(http.MethodHead))...)
|
|
|
|
}
|
|
|
|
|
2017-03-06 10:35:27 -05:00
|
|
|
// Do creates and execute a request on the specified endpoint, with the specified request modifiers
|
2018-04-17 04:22:04 -04:00
|
|
|
func Do(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)
|
2016-12-30 04:49:36 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
2018-04-17 04:22:04 -04:00
|
|
|
client, err := newHTTPClient(opts.host)
|
2016-12-30 04:49:36 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
resp, err := client.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
|
|
|
|
}
|
|
|
|
|
2018-04-17 04:22:04 -04:00
|
|
|
// ReadBody read the specified ReadCloser content and returns it
|
|
|
|
func ReadBody(b io.ReadCloser) ([]byte, error) {
|
|
|
|
defer b.Close()
|
|
|
|
return ioutil.ReadAll(b)
|
|
|
|
}
|
|
|
|
|
2017-09-19 16:12:29 -04:00
|
|
|
// newRequest creates a new http Request to the specified host and endpoint, with the specified request modifiers
|
2018-04-17 04:22:04 -04:00
|
|
|
func newRequest(endpoint string, opts *Options) (*http.Request, error) {
|
|
|
|
hostURL, err := client.ParseHostURL(opts.host)
|
2016-12-30 04:49:36 -05:00
|
|
|
if err != nil {
|
2018-04-17 04:22:04 -04:00
|
|
|
return nil, errors.Wrapf(err, "failed parsing url %q", opts.host)
|
2016-12-30 04:49:36 -05:00
|
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", endpoint, nil)
|
|
|
|
if err != nil {
|
2017-09-19 16:12:29 -04:00
|
|
|
return nil, errors.Wrap(err, "failed to create request")
|
2016-12-30 04:49:36 -05:00
|
|
|
}
|
|
|
|
|
2017-09-08 11:17:28 -04:00
|
|
|
if os.Getenv("DOCKER_TLS_VERIFY") != "" {
|
|
|
|
req.URL.Scheme = "https"
|
|
|
|
} else {
|
|
|
|
req.URL.Scheme = "http"
|
|
|
|
}
|
2018-04-17 04:22:04 -04:00
|
|
|
req.URL.Host = hostURL.Host
|
2016-12-30 04:49:36 -05:00
|
|
|
|
2018-04-17 04:22:04 -04:00
|
|
|
for _, config := range opts.requestModifiers {
|
2016-12-30 04:49:36 -05:00
|
|
|
if err := config(req); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
2018-04-17 04:22:04 -04:00
|
|
|
|
2016-12-30 04:49:36 -05:00
|
|
|
return req, nil
|
|
|
|
}
|
|
|
|
|
2017-09-19 16:12:29 -04:00
|
|
|
// newHTTPClient creates an http client for the specific host
|
|
|
|
// TODO: Share more code with client.defaultHTTPClient
|
|
|
|
func newHTTPClient(host string) (*http.Client, error) {
|
2016-12-30 04:49:36 -05:00
|
|
|
// FIXME(vdemeester) 10*time.Second timeout of SockRequest… ?
|
2018-04-17 04:22:04 -04:00
|
|
|
hostURL, err := client.ParseHostURL(host)
|
2016-12-30 04:49:36 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
transport := new(http.Transport)
|
2018-04-17 04:22:04 -04:00
|
|
|
if hostURL.Scheme == "tcp" && os.Getenv("DOCKER_TLS_VERIFY") != "" {
|
2016-12-30 04:49:36 -05:00
|
|
|
// Setup the socket TLS configuration.
|
|
|
|
tlsConfig, err := getTLSConfig()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
transport = &http.Transport{TLSClientConfig: tlsConfig}
|
|
|
|
}
|
2017-01-11 15:38:52 -05:00
|
|
|
transport.DisableKeepAlives = true
|
2018-04-17 04:22:04 -04:00
|
|
|
err = sockets.ConfigureTransport(transport, hostURL.Scheme, hostURL.Host)
|
2017-09-19 16:12:29 -04:00
|
|
|
return &http.Client{Transport: transport}, err
|
2016-12-30 04:49:36 -05:00
|
|
|
}
|
|
|
|
|
2018-04-17 04:22:04 -04:00
|
|
|
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
|
2017-06-05 11:12:26 -04:00
|
|
|
}
|
|
|
|
|
2018-04-17 04:22:04 -04:00
|
|
|
// DaemonHost return the daemon host string for this test execution
|
|
|
|
func DaemonHost() string {
|
|
|
|
daemonURLStr := "unix://" + opts.DefaultUnixSocket
|
|
|
|
if daemonHostVar := os.Getenv("DOCKER_HOST"); daemonHostVar != "" {
|
|
|
|
daemonURLStr = daemonHostVar
|
|
|
|
}
|
|
|
|
return daemonURLStr
|
2017-08-21 18:50:40 -04:00
|
|
|
}
|
|
|
|
|
2016-12-30 04:49:36 -05:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
}
|