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 {