diff --git a/pkg/server/api.go b/pkg/server/api.go index 60412d1..782170a 100644 --- a/pkg/server/api.go +++ b/pkg/server/api.go @@ -1,11 +1,16 @@ package server import ( + "bytes" + "context" "encoding/json" "fmt" + "io/ioutil" "net/http" "os" + "time" + "github.com/rs/zerolog/log" "github.com/stefanprodan/k8s-podinfo/pkg/version" ) @@ -52,3 +57,122 @@ func (s *Server) apiInfo(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(d) } + +func (s *Server) apiEcho(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/echo" && r.Method != http.MethodPost { + w.WriteHeader(http.StatusNotFound) + return + } + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Error().Msgf("Reading the request body failed: %v", err) + jsonError(w, "invalid request body", http.StatusBadRequest) + return + } + + backendURL := os.Getenv("backendURL") + if len(backendURL) > 0 { + backendReq, err := http.NewRequest("POST", backendURL, bytes.NewReader(body)) + if err != nil { + log.Error().Err(err).Msgf("%v backend call failed", r.URL.Path) + jsonError(w, "backend call failed", http.StatusInternalServerError) + return + } + + // forward headers + copyTracingHeaders(r, backendReq) + setVersionHeaders(backendReq) + + // TODO: make the timeout configurable + ctx, cancel := context.WithTimeout(backendReq.Context(), 2*time.Minute) + defer cancel() + + // call backend + resp, err := http.DefaultClient.Do(backendReq.WithContext(ctx)) + if err != nil { + log.Error().Msgf("%v backend call failed", r.URL.Path) + jsonError(w, "backend call failed", http.StatusInternalServerError) + return + } + + defer resp.Body.Close() + + // copy error status from backend and exit + if resp.StatusCode >= 400 { + w.WriteHeader(resp.StatusCode) + return + } + + // forward the received body + rbody, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Error().Err(err).Msgf("%v reading the backend request body failed", r.URL.Path) + jsonError(w, "backend call failed", http.StatusInternalServerError) + return + } + + // set logLevel=info when load testing + log.Debug().Msgf("Payload received %v from backend: %s", r.URL.Path, string(rbody)) + + setResponseHeaders(w) + w.Write(rbody) + } else { + setResponseHeaders(w) + w.Write(body) + } +} + +func copyTracingHeaders(from *http.Request, to *http.Request) { + headers := []string{ + "x-request-id", + "x-b3-traceid", + "x-b3-spanid", + "x-b3-parentspanid", + "x-b3-sampled", + "x-b3-flags", + "x-ot-span-context", + } + + for i := range headers { + headerValue := from.Header.Get(headers[i]) + if len(headerValue) > 0 { + to.Header.Set(headers[i], headerValue) + } + } +} + +func setVersionHeaders(r *http.Request) { + r.Header.Set("X-API-Version", version.VERSION) + r.Header.Set("X-API-Revision", version.GITCOMMIT) +} + +func setResponseHeaders(w http.ResponseWriter) { + color := os.Getenv("color") + if len(color) < 1 { + color = "blue" + } + w.Header().Set("X-Color", color) + w.WriteHeader(http.StatusAccepted) +} + +func jsonError(w http.ResponseWriter, error string, code int) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(code) + + data := struct { + Code int `json:"code"` + Message string `json:"message"` + }{ + Code: code, + Message: error, + } + + body, err := json.Marshal(data) + if err != nil { + log.Debug().Err(err).Msg("jsonError marshal failed") + } else { + w.Write(body) + } +} diff --git a/pkg/server/handlers.go b/pkg/server/handlers.go index 250b27d..68f30e2 100644 --- a/pkg/server/handlers.go +++ b/pkg/server/handlers.go @@ -104,27 +104,6 @@ func (s *Server) echoHeaders(w http.ResponseWriter, r *http.Request) { w.Write(d) } -func copyTracingHeaders(from *http.Request, to *http.Request) { - headers := []string{ - "x-request-id", - "x-b3-traceid", - "x-b3-spanid", - "x-b3-parentspanid", - "x-b3-sampled", - "x-b3-flags", - "x-ot-span-context", - } - - for i := range headers { - headerValue := from.Header.Get(headers[i]) - if len(headerValue) > 0 { - to.Header.Set(headers[i], headerValue) - } - } - - to.Header.Set("X-API-Version", version.VERSION) -} - func (s *Server) backend(w http.ResponseWriter, r *http.Request) { switch r.Method { case "POST": @@ -136,7 +115,7 @@ func (s *Server) backend(w http.ResponseWriter, r *http.Request) { return } - backendURL := os.Getenv("backend_url") + backendURL := os.Getenv("backendURL") if len(backendURL) > 0 { backendReq, err := http.NewRequest("POST", backendURL, bytes.NewReader(body)) if err != nil { @@ -148,6 +127,7 @@ func (s *Server) backend(w http.ResponseWriter, r *http.Request) { // forward tracing headers copyTracingHeaders(r, backendReq) + setVersionHeaders(backendReq) resp, err := http.DefaultClient.Do(backendReq) if err != nil { @@ -170,17 +150,11 @@ func (s *Server) backend(w http.ResponseWriter, r *http.Request) { } log.Debug().Msgf("Payload received from backend: %s", string(rbody)) - color := os.Getenv("color") - if len(color) < 1 { - color = "blue" - } - w.Header().Set("X-Color", color) - - w.WriteHeader(http.StatusAccepted) + setResponseHeaders(w) w.Write(rbody) } else { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("Backend not specified, set backend_url env var")) + w.Write([]byte("Backend not specified, set backendURL env var")) } default: w.WriteHeader(http.StatusNotAcceptable) diff --git a/pkg/server/server.go b/pkg/server/server.go index a8230ba..aa3651e 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -59,6 +59,7 @@ func NewServer(options ...func(*Server)) *Server { // API s.mux.HandleFunc("/api/info", s.apiInfo) + s.mux.HandleFunc("/api/echo", s.apiEcho) return s } @@ -118,6 +119,12 @@ func ListenAndServe(port string, timeout time.Duration, stopCh <-chan struct{}) log.Info().Msgf("Shutting down HTTP server with timeout: %v", timeout) + // wait for Kubernetes readiness probe + // to remove this instance from the load balancer + // the readiness check interval must lower than the timeout + time.Sleep(timeout) + + // attempt graceful shutdown if err := srv.Shutdown(ctx); err != nil { log.Error().Err(err).Msg("HTTP server graceful shutdown failed") } else { diff --git a/ui/vue.html b/ui/vue.html index 64faa6d..4e235ac 100644 --- a/ui/vue.html +++ b/ui/vue.html @@ -166,7 +166,7 @@ }, postBackend: function() { var self = this - fetch("/backend", { + fetch("/api/echo", { method: 'post', headers: { "Content-type": "application/json; charset=UTF-8",