Merge pull request #38570 from thaJeztah/keep_your_head_up
Add HEAD support for /_ping endpoint
This commit is contained in:
commit
8a43b7bb99
9 changed files with 178 additions and 29 deletions
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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{
|
||||
|
|
Loading…
Add table
Reference in a new issue