From adf4bf772d6316d6e166542b44b52536472745b5 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 23 Feb 2021 18:21:37 +0100 Subject: [PATCH] API: add "Swarm" header to _ping endpoint This adds an additional "Swarm" header to the _ping endpoint response, which allows a client to detect if Swarm is enabled on the daemon, without having to call additional endpoints. This change is not versioned in the API, and will be returned irregardless of the API version that is used. Clients should fall back to using other endpoints to get this information if the header is not present. Signed-off-by: Sebastiaan van Stijn --- api/server/router/system/backend.go | 5 +++ api/server/router/system/system_routes.go | 13 +++++++ api/swagger.yaml | 14 ++++++++ daemon/cluster/swarm.go | 17 +++++++++ docs/api/version-history.md | 15 ++++++++ integration/system/ping_test.go | 43 +++++++++++++++++++++++ 6 files changed, 107 insertions(+) diff --git a/api/server/router/system/backend.go b/api/server/router/system/backend.go index 1d704348c4..940bd4050e 100644 --- a/api/server/router/system/backend.go +++ b/api/server/router/system/backend.go @@ -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 +} diff --git a/api/server/router/system/system_routes.go b/api/server/router/system/system_routes.go index 44f80dba5f..d95b5fc13f 100644 --- a/api/server/router/system/system_routes.go +++ b/api/server/router/system/system_routes.go @@ -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() diff --git a/api/swagger.yaml b/api/swagger.yaml index bf93564f15..f0e463bd16 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -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" diff --git a/daemon/cluster/swarm.go b/daemon/cluster/swarm.go index e7bb7cc21d..b5b2ffd765 100644 --- a/daemon/cluster/swarm.go +++ b/daemon/cluster/swarm.go @@ -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) diff --git a/docs/api/version-history.md b/docs/api/version-history.md index 9035659ad1..9440dabd1f 100644 --- a/docs/api/version-history.md +++ b/docs/api/version-history.md @@ -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 diff --git a/integration/system/ping_test.go b/integration/system/ping_test.go index cd7a1c8eff..788d2df59e 100644 --- a/integration/system/ping_test.go +++ b/integration/system/ping_test.go @@ -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 {