libnetwork/diagnostic: clean up Server type
<!--
Please make sure you've read and understood our contributing guidelines;
https://github.com/moby/moby/blob/master/CONTRIBUTING.md

** Make sure all your commits include a signature generated with `git commit -s` **

For additional information on our contributing process, read our contributing
guide https://docs.docker.com/opensource/code/

If this is a bug fix, make sure your description includes "fixes #xxxx", or
"closes #xxxx"

Please provide the following information:
-->

**- What I did**

Refactored `diagnostic.Server` to be more idiomatic.
- Un-embedded the mutex field to stop it from implementing `sync.Locker`
- Folded the `Init` method into the `diagnostic.New()` constructor because constructors are supposed to return constructed objects
- Refactored the diagnostic request handlers into standard `http.Handler` functions as there was no reason (aside from ignorance of the Go language) for the handlers to be implemented the way they were

**- How I did it**
By knowing what a closure is.

**- How to verify it**
Add a `network-diagnostic-port` entry to daemon.json, start the daemon and SIGHUP it to trigger a config reload. (The diagnostic server is not started until the config is reloaded. Smells like a bug...to address separately.) cURL the different endpoints to make sure they still work and don't crash the daemon. Initialize a swarm and check that the networkdb diagnostic endpoints are registered, work, and don't crash the daemon.

**- Description for the changelog**
<!--
Write a short (one line) summary that describes the changes in this
pull request for inclusion in the changelog:
-->


**- A picture of a cute animal (not mandatory but encouraged)**


libnetwork/diagnostic/server.go
package diagnostic

import (
	"context"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"strconv"
	"sync"
	"sync/atomic"
	"time"

	"github.com/containerd/log"
	"github.com/docker/docker/libnetwork/internal/caller"
	"github.com/docker/docker/pkg/stack"
)

// HTTPHandlerFunc TODO
type HTTPHandlerFunc func(interface{}, http.ResponseWriter, *http.Request)

type httpHandlerCustom struct {
	ctx interface{}
	F   func(interface{}, http.ResponseWriter, *http.Request)
}

// ServeHTTP TODO
func (h httpHandlerCustom) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	h.F(h.ctx, w, r)
}

var diagPaths2Func = map[string]HTTPHandlerFunc{
	"/":          notImplemented,
	"/help":      help,
	"/ready":     ready,
	"/stackdump": stackTrace,
}

// Server when the debug is enabled exposes a
// This data structure is protected by the Agent mutex so does not require and additional mutex here
type Server struct {
	enable            int32
	srv               *http.Server
	port              int
	mux               *http.ServeMux
	registeredHanders map[string]bool
	sync.Mutex
}

// New creates a new diagnostic server
func New() *Server {
	return &Server{
		registeredHanders: make(map[string]bool),
	}
}

// Init initialize the mux for the http handling and register the base hooks
func (s *Server) Init() {
	s.mux = http.NewServeMux()

	// Register local handlers
	s.RegisterHandler(s, diagPaths2Func)
}

// RegisterHandler allows to register new handlers to the mux and to a specific path
func (s *Server) RegisterHandler(ctx interface{}, hdlrs map[string]HTTPHandlerFunc) {
	s.Lock()
	defer s.Unlock()
	for path, fun := range hdlrs {
		if _, ok := s.registeredHanders[path]; ok {
			continue
		}
		s.mux.Handle(path, httpHandlerCustom{ctx, fun})
		s.registeredHanders[path] = true
	}
}

// ServeHTTP this is the method called bu the ListenAndServe, and is needed to allow us to
// use our custom mux
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	s.mux.ServeHTTP(w, r)
}

// EnableDiagnostic opens a TCP socket to debug the passed network DB
func (s *Server) EnableDiagnostic(ip string, port int) {
	s.Lock()
	defer s.Unlock()

	s.port = port

	if s.enable == 1 {
		log.G(context.TODO()).Info("The server is already up and running")
		return
	}

	log.G(context.TODO()).Infof("Starting the diagnostic server listening on %d for commands", port)
	srv := &http.Server{
		Addr:              net.JoinHostPort(ip, strconv.Itoa(port)),
		Handler:           s,
		ReadHeaderTimeout: 5 * time.Minute, // "G112: Potential Slowloris Attack (gosec)"; not a real concern for our use, so setting a long timeout.
	}
	s.srv = srv
	s.enable = 1
	go func(n *Server) {
		// Ignore ErrServerClosed that is returned on the Shutdown call
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.G(context.TODO()).Errorf("ListenAndServe error: %s", err)
			atomic.SwapInt32(&n.enable, 0)
		}
	}(s)
}

// DisableDiagnostic stop the dubug and closes the tcp socket
func (s *Server) DisableDiagnostic() {
	s.Lock()
	defer s.Unlock()

	s.srv.Shutdown(context.Background()) //nolint:errcheck
	s.srv = nil
	s.enable = 0
	log.G(context.TODO()).Info("Disabling the diagnostic server")
}

// IsDiagnosticEnabled returns true when the debug is enabled
func (s *Server) IsDiagnosticEnabled() bool {
	s.Lock()
	defer s.Unlock()
	return s.enable == 1
}

func notImplemented(ctx interface{}, w http.ResponseWriter, r *http.Request) {
	_ = r.ParseForm()
	_, jsonOutput := ParseHTTPFormOptions(r)
	rsp := WrongCommand("not implemented", fmt.Sprintf("URL path: %s no method implemented check /help\n", r.URL.Path))

	// audit logs
	log.G(context.TODO()).WithFields(log.Fields{
		"component": "diagnostic",
		"remoteIP":  r.RemoteAddr,
		"method":    caller.Name(0),
		"url":       r.URL.String(),
	}).Info("command not implemented done")

	_, _ = HTTPReply(w, rsp, jsonOutput)
}

func help(ctx interface{}, w http.ResponseWriter, r *http.Request) {
	_ = r.ParseForm()
	_, jsonOutput := ParseHTTPFormOptions(r)

	// audit logs
	log.G(context.TODO()).WithFields(log.Fields{
		"component": "diagnostic",
		"remoteIP":  r.RemoteAddr,
		"method":    caller.Name(0),
		"url":       r.URL.String(),
	}).Info("help done")

	n, ok := ctx.(*Server)
	var result string
	if ok {
		for path := range n.registeredHanders {
			result += fmt.Sprintf("%s\n", path)
		}
		_, _ = HTTPReply(w, CommandSucceed(&StringCmd{Info: result}), jsonOutput)
	}
}

func ready(ctx interface{}, w http.ResponseWriter, r *http.Request) {
	_ = r.ParseForm()
	_, jsonOutput := ParseHTTPFormOptions(r)

	// audit logs
	log.G(context.TODO()).WithFields(log.Fields{
		"component": "diagnostic",
		"remoteIP":  r.RemoteAddr,
		"method":    caller.Name(0),
		"url":       r.URL.String(),
	}).Info("ready done")
	_, _ = HTTPReply(w, CommandSucceed(&StringCmd{Info: "OK"}), jsonOutput)
}

func stackTrace(ctx interface{}, w http.ResponseWriter, r *http.Request) {
	_ = r.ParseForm()
	_, jsonOutput := ParseHTTPFormOptions(r)

	// audit logs
	logger := log.G(context.TODO()).WithFields(log.Fields{"component": "diagnostic", "remoteIP": r.RemoteAddr, "method": caller.Name(0), "url": r.URL.String()})
	logger.Info("stack trace")

	path, err := stack.DumpToFile("/tmp/")
	if err != nil {
		logger.WithError(err).Error("failed to write goroutines dump")
		_, _ = HTTPReply(w, FailCommand(err), jsonOutput)
	} else {
		logger.Info("stack trace done")
		_, _ = HTTPReply(w, CommandSucceed(&StringCmd{Info: "goroutine stacks written to " + path}), jsonOutput)
	}
}

// DebugHTTPForm helper to print the form url parameters
func DebugHTTPForm(r *http.Request) {
	for k, v := range r.Form {
		log.G(context.TODO()).Debugf("Form[%q] = %q\n", k, v)
	}
}

// JSONOutput contains details on JSON output printing
type JSONOutput struct {
	enable      bool
	prettyPrint bool
}

// ParseHTTPFormOptions easily parse the JSON printing options
func ParseHTTPFormOptions(r *http.Request) (bool, *JSONOutput) {
	_, unsafe := r.Form["unsafe"]
	v, enableJSON := r.Form["json"]
	var pretty bool
	if len(v) > 0 {
		pretty = v[0] == "pretty"
	}
	return unsafe, &JSONOutput{enable: enableJSON, prettyPrint: pretty}
}

// HTTPReply helper function that takes care of sending the message out
func HTTPReply(w http.ResponseWriter, r *HTTPResult, j *JSONOutput) (int, error) {
	var response []byte
	if j.enable {
		w.Header().Set("Content-Type", "application/json")
		var err error
		if j.prettyPrint {
			response, err = json.MarshalIndent(r, "", "  ")
			if err != nil {
				response, _ = json.MarshalIndent(FailCommand(err), "", "  ")
			}
		} else {
			response, err = json.Marshal(r)
			if err != nil {
				response, _ = json.Marshal(FailCommand(err))
			}
		}
	} else {
		response = []byte(r.String())
	}
	return fmt.Fprint(w, string(response))
}

package diagnostic

import (
	"context"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"strconv"
	"sync"
	"sync/atomic"
	"time"

	"github.com/containerd/log"
	"github.com/docker/docker/libnetwork/internal/caller"
	"github.com/docker/docker/pkg/stack"
)

// Server when the debug is enabled exposes a
// This data structure is protected by the Agent mutex so does not require and additional mutex here
type Server struct {
	mu       sync.Mutex
	enable   int32
	srv      *http.Server
	port     int
	mux      *http.ServeMux
	handlers map[string]http.Handler
}

// New creates a new diagnostic server
func New() *Server {
	s := &Server{
		mux:      http.NewServeMux(),
		handlers: make(map[string]http.Handler),
	}
	s.HandleFunc("/", notImplemented)
	s.HandleFunc("/help", s.help)
	s.HandleFunc("/ready", ready)
	s.HandleFunc("/stackdump", stackTrace)
	return s
}

// Handle registers the handler for the given pattern,
// replacing any existing handler.
func (s *Server) Handle(pattern string, handler http.Handler) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if _, ok := s.handlers[pattern]; !ok {
		// Register a handler on the mux which allows the underlying handler to
		// be dynamically switched out. The http.ServeMux will panic if one
		// attempts to register a handler for the same pattern twice.
		s.mux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
			s.mu.Lock()
			h := s.handlers[pattern]
			s.mu.Unlock()
			h.ServeHTTP(w, r)
		})
	}
	s.handlers[pattern] = handler
}

// Handle registers the handler function for the given pattern,
// replacing any existing handler.
func (s *Server) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) {
	s.Handle(pattern, http.HandlerFunc(handler))
}

// ServeHTTP this is the method called bu the ListenAndServe, and is needed to allow us to
// use our custom mux
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	s.mux.ServeHTTP(w, r)
}

// EnableDiagnostic opens a TCP socket to debug the passed network DB
func (s *Server) EnableDiagnostic(ip string, port int) {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.port = port

	if s.enable == 1 {
		log.G(context.TODO()).Info("The server is already up and running")
		return
	}

	log.G(context.TODO()).Infof("Starting the diagnostic server listening on %d for commands", port)
	srv := &http.Server{
		Addr:              net.JoinHostPort(ip, strconv.Itoa(port)),
		Handler:           s,
		ReadHeaderTimeout: 5 * time.Minute, // "G112: Potential Slowloris Attack (gosec)"; not a real concern for our use, so setting a long timeout.
	}
	s.srv = srv
	s.enable = 1
	go func(n *Server) {
		// Ignore ErrServerClosed that is returned on the Shutdown call
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.G(context.TODO()).Errorf("ListenAndServe error: %s", err)
			atomic.SwapInt32(&n.enable, 0)
		}
	}(s)
}

// DisableDiagnostic stop the dubug and closes the tcp socket
func (s *Server) DisableDiagnostic() {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.srv.Shutdown(context.Background()) //nolint:errcheck
	s.srv = nil
	s.enable = 0
	log.G(context.TODO()).Info("Disabling the diagnostic server")
}

// IsDiagnosticEnabled returns true when the debug is enabled
func (s *Server) IsDiagnosticEnabled() bool {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.enable == 1
}

func notImplemented(w http.ResponseWriter, r *http.Request) {
	_ = r.ParseForm()
	_, jsonOutput := ParseHTTPFormOptions(r)
	rsp := WrongCommand("not implemented", fmt.Sprintf("URL path: %s no method implemented check /help\n", r.URL.Path))

	// audit logs
	log.G(context.TODO()).WithFields(log.Fields{
		"component": "diagnostic",
		"remoteIP":  r.RemoteAddr,
		"method":    caller.Name(0),
		"url":       r.URL.String(),
	}).Info("command not implemented done")

	_, _ = HTTPReply(w, rsp, jsonOutput)
}

func (s *Server) help(w http.ResponseWriter, r *http.Request) {
	_ = r.ParseForm()
	_, jsonOutput := ParseHTTPFormOptions(r)

	// audit logs
	log.G(context.TODO()).WithFields(log.Fields{
		"component": "diagnostic",
		"remoteIP":  r.RemoteAddr,
		"method":    caller.Name(0),
		"url":       r.URL.String(),
	}).Info("help done")

	var result string
	s.mu.Lock()
	for path := range s.handlers {
		result += fmt.Sprintf("%s\n", path)
	}
	s.mu.Unlock()
	_, _ = HTTPReply(w, CommandSucceed(&StringCmd{Info: result}), jsonOutput)
}

func ready(w http.ResponseWriter, r *http.Request) {
	_ = r.ParseForm()
	_, jsonOutput := ParseHTTPFormOptions(r)

	// audit logs
	log.G(context.TODO()).WithFields(log.Fields{
		"component": "diagnostic",
		"remoteIP":  r.RemoteAddr,
		"method":    caller.Name(0),
		"url":       r.URL.String(),
	}).Info("ready done")
	_, _ = HTTPReply(w, CommandSucceed(&StringCmd{Info: "OK"}), jsonOutput)
}

func stackTrace(w http.ResponseWriter, r *http.Request) {
	_ = r.ParseForm()
	_, jsonOutput := ParseHTTPFormOptions(r)

	// audit logs
	logger := log.G(context.TODO()).WithFields(log.Fields{"component": "diagnostic", "remoteIP": r.RemoteAddr, "method": caller.Name(0), "url": r.URL.String()})
	logger.Info("stack trace")

	path, err := stack.DumpToFile("/tmp/")
	if err != nil {
		logger.WithError(err).Error("failed to write goroutines dump")
		_, _ = HTTPReply(w, FailCommand(err), jsonOutput)
	} else {
		logger.Info("stack trace done")
		_, _ = HTTPReply(w, CommandSucceed(&StringCmd{Info: "goroutine stacks written to " + path}), jsonOutput)
	}
}

// DebugHTTPForm helper to print the form url parameters
func DebugHTTPForm(r *http.Request) {
	for k, v := range r.Form {
		log.G(context.TODO()).Debugf("Form[%q] = %q\n", k, v)
	}
}

// JSONOutput contains details on JSON output printing
type JSONOutput struct {
	enable      bool
	prettyPrint bool
}

// ParseHTTPFormOptions easily parse the JSON printing options
func ParseHTTPFormOptions(r *http.Request) (bool, *JSONOutput) {
	_, unsafe := r.Form["unsafe"]
	v, enableJSON := r.Form["json"]
	var pretty bool
	if len(v) > 0 {
		pretty = v[0] == "pretty"
	}
	return unsafe, &JSONOutput{enable: enableJSON, prettyPrint: pretty}
}

// HTTPReply helper function that takes care of sending the message out
func HTTPReply(w http.ResponseWriter, r *HTTPResult, j *JSONOutput) (int, error) {
	var response []byte
	if j.enable {
		w.Header().Set("Content-Type", "application/json")
		var err error
		if j.prettyPrint {
			response, err = json.MarshalIndent(r, "", "  ")
			if err != nil {
				response, _ = json.MarshalIndent(FailCommand(err), "", "  ")
			}
		} else {
			response, err = json.Marshal(r)
			if err != nil {
				response, _ = json.Marshal(FailCommand(err))
			}
		}
	} else {
		response = []byte(r.String())
	}
	return fmt.Fprint(w, string(response))
}

Yes! Good catch!