Merge pull request #42064 from thaJeztah/swarm_ping

API: add "Swarm" header to _ping endpoint
This commit is contained in:
Sebastiaan van Stijn 2022-03-26 14:39:50 +01:00 committed by GitHub
commit aaf70b5c6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 145 additions and 0 deletions

View file

@ -38,3 +38,8 @@ type Backend interface {
type ClusterBackend interface {
Info() swarm.Info
}
// StatusProvider provides methods to get the swarm status of the current node.
type StatusProvider interface {
Status() string
}

View file

@ -13,6 +13,7 @@ import (
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/swarm"
timetypes "github.com/docker/docker/api/types/time"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/pkg/ioutils"
@ -34,6 +35,9 @@ func (s *systemRouter) pingHandler(ctx context.Context, w http.ResponseWriter, r
if bv := builderVersion; bv != "" {
w.Header().Set("Builder-Version", string(bv))
}
w.Header().Set("Swarm", s.swarmStatus())
if r.Method == http.MethodHead {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Length", "0")
@ -43,6 +47,15 @@ func (s *systemRouter) pingHandler(ctx context.Context, w http.ResponseWriter, r
return err
}
func (s *systemRouter) swarmStatus() string {
if s.cluster != nil {
if p, ok := s.cluster.(StatusProvider); ok {
return p.Status()
}
}
return string(swarm.LocalNodeStateInactive)
}
func (s *systemRouter) getInfo(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
info := s.backend.SystemInfo()

View file

@ -8377,6 +8377,13 @@ paths:
Docker-Experimental:
type: "boolean"
description: "If the server is running with experimental mode enabled"
Swarm:
type: "string"
enum: ["inactive", "pending", "error", "locked", "active/worker", "active/manager"]
description: |
Contains information about Swarm status of the daemon,
and if the daemon is acting as a manager or worker node.
default: "inactive"
Cache-Control:
type: "string"
default: "no-cache, no-store, must-revalidate"
@ -8416,6 +8423,13 @@ paths:
Docker-Experimental:
type: "boolean"
description: "If the server is running with experimental mode enabled"
Swarm:
type: "string"
enum: ["inactive", "pending", "error", "locked", "active/worker", "active/manager"]
description: |
Contains information about Swarm status of the daemon,
and if the daemon is acting as a manager or worker node.
default: "inactive"
Cache-Control:
type: "string"
default: "no-cache, no-store, must-revalidate"

View file

@ -213,6 +213,16 @@ type Info struct {
Warnings []string `json:",omitempty"`
}
// Status provides information about the current swarm status and role,
// obtained from the "Swarm" header in the API response.
type Status struct {
// NodeState represents the state of the node.
NodeState LocalNodeState
// ControlAvailable indicates if the node is a swarm manager.
ControlAvailable bool
}
// Peer represents a peer.
type Peer struct {
NodeID string

View file

@ -188,6 +188,15 @@ type Ping struct {
OSType string
Experimental bool
BuilderVersion BuilderVersion
// SwarmStatus provides information about the current swarm status of the
// engine, obtained from the "Swarm" header in the API response.
//
// It can be a nil struct if the API version does not provide this header
// in the ping response, or if an error occurred, in which case the client
// should use other ways to get the current swarm status, such as the /swarm
// endpoint.
SwarmStatus *swarm.Status
}
// ComponentVersion describes the version information for a specific component.

View file

@ -4,8 +4,10 @@ import (
"context"
"net/http"
"path"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/errdefs"
)
@ -61,6 +63,13 @@ func parsePingResponse(cli *Client, resp serverResponse) (types.Ping, error) {
if bv := resp.header.Get("Builder-Version"); bv != "" {
ping.BuilderVersion = types.BuilderVersion(bv)
}
if si := resp.header.Get("Swarm"); si != "" {
parts := strings.SplitN(si, "/", 2)
ping.SwarmStatus = &swarm.Status{
NodeState: swarm.LocalNodeState(parts[0]),
ControlAvailable: len(parts) == 2 && parts[1] == "manager",
}
}
err := cli.checkResponseErr(resp)
return ping, errdefs.FromStatusCode(err, resp.statusCode)
}

View file

@ -8,6 +8,7 @@ import (
"strings"
"testing"
"github.com/docker/docker/api/types/swarm"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
@ -25,6 +26,7 @@ func TestPingFail(t *testing.T) {
resp.Header = http.Header{}
resp.Header.Set("API-Version", "awesome")
resp.Header.Set("Docker-Experimental", "true")
resp.Header.Set("Swarm", "inactive")
}
resp.Body = io.NopCloser(strings.NewReader("some error with the server"))
return resp, nil
@ -35,12 +37,15 @@ func TestPingFail(t *testing.T) {
assert.ErrorContains(t, err, "some error with the server")
assert.Check(t, is.Equal(false, ping.Experimental))
assert.Check(t, is.Equal("", ping.APIVersion))
var si *swarm.Status
assert.Check(t, is.Equal(si, ping.SwarmStatus))
withHeader = true
ping2, err := client.Ping(context.Background())
assert.ErrorContains(t, err, "some error with the server")
assert.Check(t, is.Equal(true, ping2.Experimental))
assert.Check(t, is.Equal("awesome", ping2.APIVersion))
assert.Check(t, is.Equal(swarm.Status{NodeState: "inactive"}, *ping2.SwarmStatus))
}
// TestPingWithError tests the case where there is a protocol error in the ping.
@ -52,6 +57,7 @@ func TestPingWithError(t *testing.T) {
resp.Header = http.Header{}
resp.Header.Set("API-Version", "awesome")
resp.Header.Set("Docker-Experimental", "true")
resp.Header.Set("Swarm", "active/manager")
resp.Body = io.NopCloser(strings.NewReader("some error with the server"))
return resp, errors.New("some error")
}),
@ -61,6 +67,8 @@ func TestPingWithError(t *testing.T) {
assert.ErrorContains(t, err, "some error")
assert.Check(t, is.Equal(false, ping.Experimental))
assert.Check(t, is.Equal("", ping.APIVersion))
var si *swarm.Status
assert.Check(t, is.Equal(si, ping.SwarmStatus))
}
// TestPingSuccess tests that we are able to get the expected API headers/ping
@ -72,6 +80,7 @@ func TestPingSuccess(t *testing.T) {
resp.Header = http.Header{}
resp.Header.Set("API-Version", "awesome")
resp.Header.Set("Docker-Experimental", "true")
resp.Header.Set("Swarm", "active/manager")
resp.Body = io.NopCloser(strings.NewReader("OK"))
return resp, nil
}),
@ -80,6 +89,7 @@ func TestPingSuccess(t *testing.T) {
assert.NilError(t, err)
assert.Check(t, is.Equal(true, ping.Experimental))
assert.Check(t, is.Equal("awesome", ping.APIVersion))
assert.Check(t, is.Equal(swarm.Status{NodeState: "active", ControlAvailable: true}, *ping.SwarmStatus))
}
// TestPingHeadFallback tests that the client falls back to GET if HEAD fails.

View file

@ -492,6 +492,23 @@ func (c *Cluster) Info() types.Info {
return info
}
// Status returns a textual representation of the node's swarm status and role (manager/worker)
func (c *Cluster) Status() string {
c.mu.RLock()
s := c.currentNodeState()
c.mu.RUnlock()
state := string(s.status)
if s.status == types.LocalNodeStateActive {
if s.IsActiveManager() || s.IsManager() {
state += "/manager"
} else {
state += "/worker"
}
}
return state
}
func validateAndSanitizeInitRequest(req *types.InitRequest) error {
var err error
req.ListenAddr, err = validateAddr(req.ListenAddr)

View file

@ -50,6 +50,21 @@ keywords: "API, Docker, rcli, REST, documentation"
if they are not set.
* `GET /info` now omits the `KernelMemory` and `KernelMemoryTCP` if they are not
supported by the host or host's configuration (if cgroups v2 are in use).
* `GET /_ping` and `HEAD /_ping` now return a `Swarm` header, which allows a
client to detect if Swarm is enabled on the daemon, without having to call
additional endpoints.
This change is not versioned, and affects all API versions if the daemon has
this patch. Clients must consider this header "optional", and fall back to
using other endpoints to get this information if the header is not present.
The `Swarm` header can contain one of the following values:
- "inactive"
- "pending"
- "error"
- "locked"
- "active/worker"
- "active/manager"
## v1.41 API changes

View file

@ -1,11 +1,14 @@
package system // import "github.com/docker/docker/integration/system"
import (
"context"
"net/http"
"strings"
"testing"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/testutil/daemon"
"github.com/docker/docker/testutil/request"
"gotest.tools/v3/assert"
"gotest.tools/v3/skip"
@ -50,6 +53,46 @@ func TestPingHead(t *testing.T) {
assert.Check(t, hdr(res, "API-Version") != "")
}
func TestPingSwarmHeader(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon)
skip.If(t, testEnv.DaemonInfo.OSType == "windows")
defer setupTest(t)()
d := daemon.New(t)
d.Start(t)
defer d.Stop(t)
client := d.NewClientT(t)
defer client.Close()
ctx := context.TODO()
t.Run("before swarm init", func(t *testing.T) {
res, _, err := request.Get("/_ping")
assert.NilError(t, err)
assert.Equal(t, res.StatusCode, http.StatusOK)
assert.Equal(t, hdr(res, "Swarm"), "inactive")
})
_, err := client.SwarmInit(ctx, swarm.InitRequest{ListenAddr: "127.0.0.1", AdvertiseAddr: "127.0.0.1:2377"})
assert.NilError(t, err)
t.Run("after swarm init", func(t *testing.T) {
res, _, err := request.Get("/_ping", request.Host(d.Sock()))
assert.NilError(t, err)
assert.Equal(t, res.StatusCode, http.StatusOK)
assert.Equal(t, hdr(res, "Swarm"), "active/manager")
})
err = client.SwarmLeave(ctx, true)
assert.NilError(t, err)
t.Run("after swarm leave", func(t *testing.T) {
res, _, err := request.Get("/_ping", request.Host(d.Sock()))
assert.NilError(t, err)
assert.Equal(t, res.StatusCode, http.StatusOK)
assert.Equal(t, hdr(res, "Swarm"), "inactive")
})
}
func hdr(res *http.Response, name string) string {
val, ok := res.Header[http.CanonicalHeaderKey(name)]
if !ok || len(val) == 0 {