From d0a7ae21b0e4d3174fd5ec41288204d4b0292019 Mon Sep 17 00:00:00 2001 From: Abhinav Dahiya Date: Fri, 4 Jan 2019 16:21:00 -0800 Subject: [PATCH] add healthz --- pkg/server/api.go | 47 ++++++-- pkg/server/api_test.go | 243 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 269 insertions(+), 21 deletions(-) diff --git a/pkg/server/api.go b/pkg/server/api.go index 542ffaa321..fcb3e39657 100644 --- a/pkg/server/api.go +++ b/pkg/server/api.go @@ -9,10 +9,6 @@ import ( "github.com/golang/glog" ) -const ( - apiPathConfig = "/config/" -) - type poolRequest struct { machinePool string } @@ -20,7 +16,7 @@ type poolRequest struct { // APIServer provides the HTTP(s) endpoint // for providing the machine configs. type APIServer struct { - handler *APIHandler + handler http.Handler port int insecure bool cert string @@ -31,8 +27,13 @@ type APIServer struct { // that runs the Machine Config Server as a // handler. func NewAPIServer(a *APIHandler, p int, is bool, c, k string) *APIServer { + mux := http.NewServeMux() + mux.Handle("/config/", a) + mux.Handle("/healthz", &healthHandler{}) + mux.Handle("/", &defaultHandler{}) + return &APIServer{ - handler: a, + handler: mux, port: p, insecure: is, cert: c, @@ -42,12 +43,9 @@ func NewAPIServer(a *APIHandler, p int, is bool, c, k string) *APIServer { // Serve launches the API Server. func (a *APIServer) Serve() { - mux := http.NewServeMux() - mux.Handle(apiPathConfig, a.handler) - mcs := &http.Server{ Addr: fmt.Sprintf(":%v", a.port), - Handler: mux, + Handler: a.handler, } glog.Info("launching server") @@ -129,3 +127,32 @@ func (sh *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { glog.Errorf("failed to write %v response: %v", cr, err) } } + +type healthHandler struct{} + +// ServeHTTP handles /healthz requests. +func (h *healthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Length", "0") + if r.Method == http.MethodGet || r.Method == http.MethodHead { + w.WriteHeader(http.StatusNoContent) + return + } + + w.WriteHeader(http.StatusMethodNotAllowed) + return +} + +// defaultHandler is the HTTP Handler for backstopping invalid requests. +type defaultHandler struct{} + +// ServeHTTP handles invalid requests. +func (h *defaultHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Length", "0") + if r.Method == http.MethodGet || r.Method == http.MethodHead { + w.WriteHeader(http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusMethodNotAllowed) + return +} diff --git a/pkg/server/api_test.go b/pkg/server/api_test.go index 8b632bf297..c4d179bdce 100644 --- a/pkg/server/api_test.go +++ b/pkg/server/api_test.go @@ -30,10 +30,144 @@ type scenario struct { func TestAPIHandler(t *testing.T) { scenarios := []scenario{ { - name: "get non-config path that does not exist", - request: httptest.NewRequest(http.MethodGet, "http://testrequest/does-not-exist", nil), + name: "get config path that does not exist", + request: httptest.NewRequest(http.MethodGet, "http://testrequest/config/does-not-exist", nil), + serverFunc: func(poolRequest) (*ignv2_2types.Config, error) { + return new(ignv2_2types.Config), fmt.Errorf("not acceptable") + }, + checkResponse: func(t *testing.T, response *http.Response) { + checkStatus(t, response, http.StatusInternalServerError) + checkContentLength(t, response, 0) + checkBodyLength(t, response, 0) + }, + }, + { + name: "get config path that exists", + request: httptest.NewRequest(http.MethodGet, "http://testrequest/config/master", nil), + serverFunc: func(poolRequest) (*ignv2_2types.Config, error) { + return new(ignv2_2types.Config), nil + }, + checkResponse: func(t *testing.T, response *http.Response) { + checkStatus(t, response, http.StatusOK) + checkContentType(t, response, "application/json") + checkContentLength(t, response, 114) + checkBodyLength(t, response, 114) + }, + }, + { + name: "head config path that exists", + request: httptest.NewRequest(http.MethodHead, "http://testrequest/config/master", nil), + serverFunc: func(poolRequest) (*ignv2_2types.Config, error) { + return new(ignv2_2types.Config), nil + }, + checkResponse: func(t *testing.T, response *http.Response) { + checkStatus(t, response, http.StatusOK) + checkContentType(t, response, "application/json") + checkContentLength(t, response, 114) + checkBodyLength(t, response, 0) + }, + }, + { + name: "post config path that exists", + request: httptest.NewRequest(http.MethodPost, "http://testrequest/config/master", nil), + serverFunc: func(poolRequest) (*ignv2_2types.Config, error) { + return new(ignv2_2types.Config), nil + }, + checkResponse: func(t *testing.T, response *http.Response) { + checkStatus(t, response, http.StatusMethodNotAllowed) + checkContentLength(t, response, 0) + checkBodyLength(t, response, 0) + }, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + w := httptest.NewRecorder() + ms := &mockServer{ + GetConfigFn: scenario.serverFunc, + } + handler := NewServerAPIHandler(ms) + handler.ServeHTTP(w, scenario.request) + + resp := w.Result() + defer resp.Body.Close() + scenario.checkResponse(t, resp) + }) + } +} + +func TestHealthzHandler(t *testing.T) { + scenarios := []scenario{ + { + name: "get healthz", + request: httptest.NewRequest(http.MethodGet, "http://testrequest/healthz", nil), + serverFunc: func(poolRequest) (*ignv2_2types.Config, error) { + return new(ignv2_2types.Config), nil + }, + checkResponse: func(t *testing.T, response *http.Response) { + checkStatus(t, response, http.StatusNoContent) + checkContentLength(t, response, 0) + checkBodyLength(t, response, 0) + }, + }, + { + name: "head healthz", + request: httptest.NewRequest(http.MethodHead, "http://testrequest/healthz", nil), + serverFunc: func(poolRequest) (*ignv2_2types.Config, error) { + return new(ignv2_2types.Config), nil + }, + checkResponse: func(t *testing.T, response *http.Response) { + checkStatus(t, response, http.StatusNoContent) + checkContentLength(t, response, 0) + checkBodyLength(t, response, 0) + }, + }, + { + name: "post healthz", + request: httptest.NewRequest(http.MethodPost, "http://testrequest/healthz", nil), + serverFunc: func(poolRequest) (*ignv2_2types.Config, error) { + return new(ignv2_2types.Config), nil + }, + checkResponse: func(t *testing.T, response *http.Response) { + checkStatus(t, response, http.StatusMethodNotAllowed) + checkContentLength(t, response, 0) + checkBodyLength(t, response, 0) + }, + }, + } + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + w := httptest.NewRecorder() + handler := &healthHandler{} + handler.ServeHTTP(w, scenario.request) + + resp := w.Result() + defer resp.Body.Close() + scenario.checkResponse(t, resp) + }) + } +} + +func TestDefaultHandler(t *testing.T) { + scenarios := []scenario{ + { + name: "get root", + request: httptest.NewRequest(http.MethodGet, "http://testrequest/", nil), + serverFunc: func(poolRequest) (*ignv2_2types.Config, error) { + return new(ignv2_2types.Config), nil + }, + checkResponse: func(t *testing.T, response *http.Response) { + checkStatus(t, response, http.StatusNotFound) + checkContentLength(t, response, 0) + checkBodyLength(t, response, 0) + }, + }, + { + name: "head root", + request: httptest.NewRequest(http.MethodHead, "http://testrequest/", nil), serverFunc: func(poolRequest) (*ignv2_2types.Config, error) { - return nil, nil + return new(ignv2_2types.Config), nil }, checkResponse: func(t *testing.T, response *http.Response) { checkStatus(t, response, http.StatusNotFound) @@ -41,6 +175,34 @@ func TestAPIHandler(t *testing.T) { checkBodyLength(t, response, 0) }, }, + { + name: "post root", + request: httptest.NewRequest(http.MethodPost, "http://testrequest/", nil), + serverFunc: func(poolRequest) (*ignv2_2types.Config, error) { + return new(ignv2_2types.Config), nil + }, + checkResponse: func(t *testing.T, response *http.Response) { + checkStatus(t, response, http.StatusMethodNotAllowed) + checkContentLength(t, response, 0) + checkBodyLength(t, response, 0) + }, + }, + } + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + w := httptest.NewRecorder() + handler := &defaultHandler{} + handler.ServeHTTP(w, scenario.request) + + resp := w.Result() + defer resp.Body.Close() + scenario.checkResponse(t, resp) + }) + } +} + +func TestAPIServer(t *testing.T) { + scenarios := []scenario{ { name: "get config path that does not exist", request: httptest.NewRequest(http.MethodGet, "http://testrequest/config/does-not-exist", nil), @@ -80,10 +242,10 @@ func TestAPIHandler(t *testing.T) { }, }, { - name: "post non-config path that does not exist", - request: httptest.NewRequest(http.MethodPost, "http://testrequest/post", nil), + name: "post config path that exists", + request: httptest.NewRequest(http.MethodPost, "http://testrequest/config/master", nil), serverFunc: func(poolRequest) (*ignv2_2types.Config, error) { - return nil, nil + return new(ignv2_2types.Config), nil }, checkResponse: func(t *testing.T, response *http.Response) { checkStatus(t, response, http.StatusMethodNotAllowed) @@ -92,8 +254,68 @@ func TestAPIHandler(t *testing.T) { }, }, { - name: "post config path that exists", - request: httptest.NewRequest(http.MethodPost, "http://testrequest/config/master", nil), + name: "get healthz", + request: httptest.NewRequest(http.MethodGet, "http://testrequest/healthz", nil), + serverFunc: func(poolRequest) (*ignv2_2types.Config, error) { + return new(ignv2_2types.Config), nil + }, + checkResponse: func(t *testing.T, response *http.Response) { + checkStatus(t, response, http.StatusNoContent) + checkContentLength(t, response, 0) + checkBodyLength(t, response, 0) + }, + }, + { + name: "head healthz", + request: httptest.NewRequest(http.MethodHead, "http://testrequest/healthz", nil), + serverFunc: func(poolRequest) (*ignv2_2types.Config, error) { + return new(ignv2_2types.Config), nil + }, + checkResponse: func(t *testing.T, response *http.Response) { + checkStatus(t, response, http.StatusNoContent) + checkContentLength(t, response, 0) + checkBodyLength(t, response, 0) + }, + }, + { + name: "post healthz", + request: httptest.NewRequest(http.MethodPost, "http://testrequest/healthz", nil), + serverFunc: func(poolRequest) (*ignv2_2types.Config, error) { + return new(ignv2_2types.Config), nil + }, + checkResponse: func(t *testing.T, response *http.Response) { + checkStatus(t, response, http.StatusMethodNotAllowed) + checkContentLength(t, response, 0) + checkBodyLength(t, response, 0) + }, + }, + { + name: "get root", + request: httptest.NewRequest(http.MethodGet, "http://testrequest/", nil), + serverFunc: func(poolRequest) (*ignv2_2types.Config, error) { + return new(ignv2_2types.Config), nil + }, + checkResponse: func(t *testing.T, response *http.Response) { + checkStatus(t, response, http.StatusNotFound) + checkContentLength(t, response, 0) + checkBodyLength(t, response, 0) + }, + }, + { + name: "head root", + request: httptest.NewRequest(http.MethodHead, "http://testrequest/", nil), + serverFunc: func(poolRequest) (*ignv2_2types.Config, error) { + return new(ignv2_2types.Config), nil + }, + checkResponse: func(t *testing.T, response *http.Response) { + checkStatus(t, response, http.StatusNotFound) + checkContentLength(t, response, 0) + checkBodyLength(t, response, 0) + }, + }, + { + name: "post root", + request: httptest.NewRequest(http.MethodPost, "http://testrequest/", nil), serverFunc: func(poolRequest) (*ignv2_2types.Config, error) { return new(ignv2_2types.Config), nil }, @@ -104,15 +326,14 @@ func TestAPIHandler(t *testing.T) { }, }, } - for _, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) { w := httptest.NewRecorder() ms := &mockServer{ GetConfigFn: scenario.serverFunc, } - handler := NewServerAPIHandler(ms) - handler.ServeHTTP(w, scenario.request) + server := NewAPIServer(NewServerAPIHandler(ms), 0, false, "", "") + server.handler.ServeHTTP(w, scenario.request) resp := w.Result() defer resp.Body.Close()