Merge pull request #38570 from thaJeztah/keep_your_head_up

Add HEAD support for /_ping endpoint
This commit is contained in:
Sebastiaan van Stijn 2019-01-31 21:54:58 +01:00 committed by GitHub
commit 8a43b7bb99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 178 additions and 29 deletions

View file

@ -30,6 +30,7 @@ func NewRouter(b Backend, c ClusterBackend, fscache *fscache.FSCache, builder *b
r.routes = []router.Route{
router.NewOptionsRoute("/{anyroute:.*}", optionsHandler),
router.NewGetRoute("/_ping", r.pingHandler),
router.NewHeadRoute("/_ping", r.pingHandler),
router.NewGetRoute("/events", r.getEvents),
router.NewGetRoute("/info", r.getInfo),
router.NewGetRoute("/version", r.getVersion),

View file

@ -34,6 +34,11 @@ func (s *systemRouter) pingHandler(ctx context.Context, w http.ResponseWriter, r
if bv := builderVersion; bv != "" {
w.Header().Set("Builder-Version", string(bv))
}
if r.Method == http.MethodHead {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Length", "0")
return nil
}
_, err := w.Write([]byte{'O', 'K'})
return err
}

View file

@ -7133,6 +7133,38 @@ paths:
type: "string"
default: "no-cache"
tags: ["System"]
head:
summary: "Ping"
description: "This is a dummy endpoint you can use to test if the server is accessible."
operationId: "SystemPingHead"
produces: ["text/plain"]
responses:
200:
description: "no error"
schema:
type: "string"
example: "(empty)"
headers:
API-Version:
type: "string"
description: "Max API Version the server supports"
BuildKit-Version:
type: "string"
description: "Default version of docker image builder"
Docker-Experimental:
type: "boolean"
description: "If the server is running with experimental mode enabled"
Cache-Control:
type: "string"
default: "no-cache, no-store, must-revalidate"
Pragma:
type: "string"
default: "no-cache"
500:
description: "server error"
schema:
$ref: "#/definitions/ErrorResponse"
tags: ["System"]
/commit:
post:
summary: "Create a new image from a container"

View file

@ -2,34 +2,56 @@ package client // import "github.com/docker/docker/client"
import (
"context"
"net/http"
"path"
"github.com/docker/docker/api/types"
)
// Ping pings the server and returns the value of the "Docker-Experimental", "Builder-Version", "OS-Type" & "API-Version" headers
// Ping pings the server and returns the value of the "Docker-Experimental",
// "Builder-Version", "OS-Type" & "API-Version" headers. It attempts to use
// a HEAD request on the endpoint, but falls back to GET if HEAD is not supported
// by the daemon.
func (cli *Client) Ping(ctx context.Context) (types.Ping, error) {
var ping types.Ping
req, err := cli.buildRequest("GET", path.Join(cli.basePath, "/_ping"), nil, nil)
req, err := cli.buildRequest("HEAD", path.Join(cli.basePath, "/_ping"), nil, nil)
if err != nil {
return ping, err
}
serverResp, err := cli.doRequest(ctx, req)
if err == nil {
defer ensureReaderClosed(serverResp)
switch serverResp.statusCode {
case http.StatusOK, http.StatusInternalServerError:
// Server handled the request, so parse the response
return parsePingResponse(cli, serverResp)
}
}
req, err = cli.buildRequest("GET", path.Join(cli.basePath, "/_ping"), nil, nil)
if err != nil {
return ping, err
}
serverResp, err = cli.doRequest(ctx, req)
if err != nil {
return ping, err
}
defer ensureReaderClosed(serverResp)
if serverResp.header != nil {
ping.APIVersion = serverResp.header.Get("API-Version")
if serverResp.header.Get("Docker-Experimental") == "true" {
ping.Experimental = true
}
ping.OSType = serverResp.header.Get("OSType")
if bv := serverResp.header.Get("Builder-Version"); bv != "" {
ping.BuilderVersion = types.BuilderVersion(bv)
}
}
return ping, cli.checkResponseErr(serverResp)
return parsePingResponse(cli, serverResp)
}
func parsePingResponse(cli *Client, resp serverResponse) (types.Ping, error) {
var ping types.Ping
if resp.header == nil {
return ping, cli.checkResponseErr(resp)
}
ping.APIVersion = resp.header.Get("API-Version")
ping.OSType = resp.header.Get("OSType")
if resp.header.Get("Docker-Experimental") == "true" {
ping.Experimental = true
}
if bv := resp.header.Get("Builder-Version"); bv != "" {
ping.BuilderVersion = types.BuilderVersion(bv)
}
return ping, cli.checkResponseErr(resp)
}

View file

@ -81,3 +81,49 @@ func TestPingSuccess(t *testing.T) {
assert.Check(t, is.Equal(true, ping.Experimental))
assert.Check(t, is.Equal("awesome", ping.APIVersion))
}
// TestPingHeadFallback tests that the client falls back to GET if HEAD fails.
func TestPingHeadFallback(t *testing.T) {
tests := []struct {
status int
expected string
}{
{
status: http.StatusOK,
expected: "HEAD",
},
{
status: http.StatusInternalServerError,
expected: "HEAD",
},
{
status: http.StatusNotFound,
expected: "HEAD, GET",
},
{
status: http.StatusMethodNotAllowed,
expected: "HEAD, GET",
},
}
for _, tc := range tests {
tc := tc
t.Run(http.StatusText(tc.status), func(t *testing.T) {
var reqs []string
client := &Client{
client: newMockClient(func(req *http.Request) (*http.Response, error) {
reqs = append(reqs, req.Method)
resp := &http.Response{StatusCode: http.StatusOK}
if req.Method == http.MethodHead {
resp.StatusCode = tc.status
}
resp.Header = http.Header{}
resp.Header.Add("API-Version", strings.Join(reqs, ", "))
return resp, nil
}),
}
ping, _ := client.Ping(context.Background())
assert.Check(t, is.Equal(ping.APIVersion, tc.expected))
})
}
}

View file

@ -195,17 +195,21 @@ func (cli *Client) checkResponseErr(serverResp serverResponse) error {
return nil
}
bodyMax := 1 * 1024 * 1024 // 1 MiB
bodyR := &io.LimitedReader{
R: serverResp.body,
N: int64(bodyMax),
}
body, err := ioutil.ReadAll(bodyR)
if err != nil {
return err
}
if bodyR.N == 0 {
return fmt.Errorf("request returned %s with a message (> %d bytes) for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), bodyMax, serverResp.reqURL)
var body []byte
var err error
if serverResp.body != nil {
bodyMax := 1 * 1024 * 1024 // 1 MiB
bodyR := &io.LimitedReader{
R: serverResp.body,
N: int64(bodyMax),
}
body, err = ioutil.ReadAll(bodyR)
if err != nil {
return err
}
if bodyR.N == 0 {
return fmt.Errorf("request returned %s with a message (> %d bytes) for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), bodyMax, serverResp.reqURL)
}
}
if len(body) == 0 {
return fmt.Errorf("request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), serverResp.reqURL)

View file

@ -17,9 +17,14 @@ keywords: "API, Docker, rcli, REST, documentation"
[Docker Engine API v1.40](https://docs.docker.com/engine/api/v1.40/) documentation
* `GET /_ping` now sets `Cache-Control` and `Pragma` headers to prevent the result
from being cached. This change is not versioned, and affects all API versions
if the daemon has this patch.
* The `/_ping` endpoint can now be accessed both using `GET` or `HEAD` requests.
when accessed using a `HEAD` request, all headers are returned, but the body
is empty (`Content-Length: 0`). This change is not versioned, and affects all
API versions if the daemon has this patch. Clients are recommended to try
using `HEAD`, but fallback to `GET` if the `HEAD` requests fails.
* `GET /_ping` and `HEAD /_ping` now set `Cache-Control` and `Pragma` headers to
prevent the result from being cached. This change is not versioned, and affects
all API versions if the daemon has this patch.
* `GET /services` now returns `Sysctls` as part of the `ContainerSpec`.
* `GET /services/{id}` now returns `Sysctls` as part of the `ContainerSpec`.
* `POST /services/create` now accepts `Sysctls` as part of the `ContainerSpec`.

View file

@ -5,8 +5,10 @@ import (
"strings"
"testing"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/internal/test/request"
"gotest.tools/assert"
"gotest.tools/skip"
)
func TestPingCacheHeaders(t *testing.T) {
@ -20,6 +22,33 @@ func TestPingCacheHeaders(t *testing.T) {
assert.Equal(t, hdr(res, "Pragma"), "no-cache")
}
func TestPingGet(t *testing.T) {
defer setupTest(t)()
res, body, err := request.Get("/_ping")
assert.NilError(t, err)
b, err := request.ReadBody(body)
assert.NilError(t, err)
assert.Equal(t, string(b), "OK")
assert.Equal(t, res.StatusCode, http.StatusOK)
assert.Check(t, hdr(res, "API-Version") != "")
}
func TestPingHead(t *testing.T) {
skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "skip test from new feature")
defer setupTest(t)()
res, body, err := request.Head("/_ping")
assert.NilError(t, err)
b, err := request.ReadBody(body)
assert.NilError(t, err)
assert.Equal(t, 0, len(b))
assert.Equal(t, res.StatusCode, http.StatusOK)
assert.Check(t, hdr(res, "API-Version") != "")
}
func hdr(res *http.Response, name string) string {
val, ok := res.Header[http.CanonicalHeaderKey(name)]
if !ok || len(val) == 0 {

View file

@ -77,6 +77,11 @@ func Get(endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadC
return Do(endpoint, modifiers...)
}
// 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))...)
}
// Do creates and execute a request on the specified endpoint, with the specified request modifiers
func Do(endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
opts := &Options{