mirror of
https://github.com/stefanprodan/podinfo.git
synced 2026-08-23 22:26:29 +00:00
Add UI index handler
This commit is contained in:
@@ -12,28 +12,27 @@ Specifications:
|
||||
* Watches for secrets and configmaps changes and updates the in-memory cache
|
||||
* Prometheus instrumentation (RED metrics)
|
||||
* Dependency management with golang/dep
|
||||
* Structured logging with zerolog
|
||||
* Error handling with pkg/errors
|
||||
* Structured logging with zap
|
||||
* Tracing with Istio and Jaeger
|
||||
* Helm chart
|
||||
|
||||
Web API:
|
||||
|
||||
* `GET /` prints runtime information, environment variables, labels and annotations
|
||||
* `GET /` prints runtime information
|
||||
* `GET /version` prints podinfo version and git commit hash
|
||||
* `GET /metrics` http requests duration and Go runtime metrics
|
||||
* `GET /metrics` return HTTP requests duration and Go runtime metrics
|
||||
* `GET /healthz` used by Kubernetes liveness probe
|
||||
* `GET /readyz` used by Kubernetes readiness probe
|
||||
* `POST /readyz/enable` signals the Kubernetes LB that this instance is ready to receive traffic
|
||||
* `POST /readyz/disable` signals the Kubernetes LB to stop sending requests to this instance
|
||||
* `GET /error` returns code 500 and logs the error
|
||||
* `GET /status/{code}` returns the status code
|
||||
* `GET /panic` crashes the process with exit code 255
|
||||
* `POST /echo` echos the posted content, logs the SHA1 hash of the content
|
||||
* `GET /echoheaders` prints the request HTTP headers
|
||||
* `POST /job` long running job, json body: `{"wait":2}`
|
||||
* `GET /configs` prints the configmaps and/or secrets mounted in the `config` volume
|
||||
* `POST /echo` forwards the call to the backend service and echos the posted content
|
||||
* `GET /headers` returns a JSON with the request HTTP headers
|
||||
* `GET /delay/{seconds}` waits for the specified period
|
||||
* `GET /configs` returns a JSON with configmaps and/or secrets mounted in the `config` volume
|
||||
* `POST /write` writes the posted content to disk at /data/hash and returns the SHA1 hash of the content
|
||||
* `POST /read` receives a SHA1 hash and returns the content of the file /data/hash if exists
|
||||
* `POST /backend` forwards the call to the backend service on `http://backend-podinfo:9898/echo`
|
||||
* `GET /read/{hash}` returns the content of the file /data/hash if exists
|
||||
|
||||
### Guides
|
||||
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
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"
|
||||
)
|
||||
|
||||
func (s *Server) apiInfo(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/info" && r.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
host, _ := os.Hostname()
|
||||
color := os.Getenv("color")
|
||||
if len(color) < 1 {
|
||||
color = "blue"
|
||||
}
|
||||
|
||||
msg := os.Getenv("message")
|
||||
if len(msg) < 1 {
|
||||
msg = fmt.Sprintf("Greetings from podinfo v%v", version.VERSION)
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Message string `json:"message"`
|
||||
Version string `json:"version"`
|
||||
Revision string `json:"revision"`
|
||||
Hostname string `json:"hostname"`
|
||||
Color string `json:"color"`
|
||||
}{
|
||||
Message: msg,
|
||||
Version: version.VERSION,
|
||||
Revision: version.GITCOMMIT,
|
||||
Hostname: host,
|
||||
Color: color,
|
||||
}
|
||||
|
||||
d, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
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().Err(err).Msgf("backend call to %s failed", backendURL)
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,346 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/stefanprodan/k8s-podinfo/pkg/version"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func (s *Server) index(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Request %s received from %s on %s", r.Header.Get("x-request-id"), r.RemoteAddr, r.RequestURI)
|
||||
|
||||
if strings.Contains(r.UserAgent(), "Mozilla") {
|
||||
uiPath := os.Getenv("uiPath")
|
||||
if len(uiPath) < 1 {
|
||||
uiPath = "ui"
|
||||
}
|
||||
tmpl, err := template.New("vue.html").ParseFiles(path.Join(uiPath, "vue.html"))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(path.Join(uiPath, "vue.html") + err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
host, _ := os.Hostname()
|
||||
data := struct {
|
||||
Title string
|
||||
}{
|
||||
Title: host,
|
||||
}
|
||||
|
||||
if err := tmpl.Execute(w, data); err != nil {
|
||||
http.Error(w, path.Join(uiPath, "vue.html")+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
} else {
|
||||
resp, err := makeResponse()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
d, err := yaml.Marshal(resp)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(d)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *Server) echo(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "POST":
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Reading the request body failed: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
hash := hash(string(body))
|
||||
log.Debug().Msgf("Payload received from %s hash %s", r.RemoteAddr, hash)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
w.Write(body)
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotAcceptable)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) echoHeaders(w http.ResponseWriter, r *http.Request) {
|
||||
d, err := yaml.Marshal(r.Header)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(d)
|
||||
}
|
||||
|
||||
func (s *Server) backend(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "POST":
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Reading the request body failed: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
backendURL := os.Getenv("backendURL")
|
||||
if len(backendURL) > 0 {
|
||||
backendReq, err := http.NewRequest("POST", backendURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
log.Error().Msgf("Backend call failed: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// forward tracing headers
|
||||
copyTracingHeaders(r, backendReq)
|
||||
setVersionHeaders(backendReq)
|
||||
|
||||
resp, err := http.DefaultClient.Do(backendReq)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Backend call failed: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 500 {
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
return
|
||||
}
|
||||
rbody, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Reading the backend request body failed: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
log.Debug().Msgf("Payload received from backend: %s", string(rbody))
|
||||
|
||||
setResponseHeaders(w)
|
||||
w.Write(rbody)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("Backend not specified, set backendURL env var"))
|
||||
}
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotAcceptable)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) job(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "POST":
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Reading the request body failed: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
log.Debug().Msgf("Payload received from %s: %s", r.RemoteAddr, string(body))
|
||||
|
||||
job := struct {
|
||||
Wait int `json:"wait"`
|
||||
}{
|
||||
Wait: 0,
|
||||
}
|
||||
err = json.Unmarshal(body, &job)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Reading the request body failed: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
if job.Wait > 0 {
|
||||
time.Sleep(time.Duration(job.Wait) * time.Second)
|
||||
}
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
w.Write([]byte("Job done"))
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotAcceptable)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) write(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "POST":
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Reading the request body failed: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
hash := hash(string(body))
|
||||
err = ioutil.WriteFile(path.Join(dataPath, hash), body, 0644)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Writing file to /data failed: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Write command received from %s hash %s", r.RemoteAddr, hash)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
w.Write([]byte(hash))
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotAcceptable)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) read(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "POST":
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Reading the request body failed: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
hash := string(body)
|
||||
content, err := ioutil.ReadFile(path.Join(dataPath, hash))
|
||||
if err != nil {
|
||||
log.Error().Msgf("Reading file from /data/%s failed: %v", hash, err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Read command received from %s hash %s", r.RemoteAddr, hash)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
w.Write([]byte(content))
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotAcceptable)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) configs(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
files := make(map[string]string)
|
||||
if watcher != nil {
|
||||
watcher.Cache.Range(func(key interface{}, value interface{}) bool {
|
||||
files[key.(string)] = value.(string)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
d, err := yaml.Marshal(files)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(d)
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotAcceptable)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) version(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/version" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
resp := map[string]string{
|
||||
"version": version.VERSION,
|
||||
"commit": version.GITCOMMIT,
|
||||
}
|
||||
|
||||
d, err := yaml.Marshal(resp)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(d)
|
||||
}
|
||||
|
||||
func (s *Server) healthz(w http.ResponseWriter, r *http.Request) {
|
||||
if atomic.LoadInt32(&healthy) == 1 {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
}
|
||||
|
||||
func (s *Server) readyz(w http.ResponseWriter, r *http.Request) {
|
||||
if atomic.LoadInt32(&ready) == 1 {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
}
|
||||
|
||||
func (s *Server) enable(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.StoreInt32(&ready, 1)
|
||||
}
|
||||
|
||||
func (s *Server) disable(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.StoreInt32(&ready, 0)
|
||||
}
|
||||
|
||||
func (s *Server) error(w http.ResponseWriter, r *http.Request) {
|
||||
log.Error().Msg("Error triggered")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("Internal server error"))
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Server) panic(w http.ResponseWriter, r *http.Request) {
|
||||
log.Fatal().Msg("Kill switch triggered")
|
||||
}
|
||||
|
||||
func hash(input string) string {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(input))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestYamlResponse(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
srv := &Server{}
|
||||
handler := http.HandlerFunc(srv.index)
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Fatalf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusOK)
|
||||
}
|
||||
|
||||
expected := "external_ip"
|
||||
r := regexp.MustCompile(fmt.Sprintf("(?m:%s)", expected))
|
||||
if !r.MatchString(rr.Body.String()) {
|
||||
t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s",
|
||||
rr.Body.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthzNotReady(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/healthz", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
srv := &Server{}
|
||||
handler := http.HandlerFunc(srv.healthz)
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != http.StatusServiceUnavailable {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusServiceUnavailable)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadyzNotReady(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/readyz", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
srv := &Server{}
|
||||
handler := http.HandlerFunc(srv.readyz)
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != http.StatusServiceUnavailable {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusServiceUnavailable)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEchoResponse(t *testing.T) {
|
||||
expected := "test"
|
||||
req, err := http.NewRequest("POST", "/echo", strings.NewReader(expected))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
srv := &Server{}
|
||||
handler := http.HandlerFunc(srv.echo)
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != http.StatusAccepted {
|
||||
t.Fatalf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusAccepted)
|
||||
}
|
||||
|
||||
if rr.Body.String() != expected {
|
||||
t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s",
|
||||
rr.Body.String(), expected)
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Runtime map[string]string `json:"runtime" yaml:"runtime"`
|
||||
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
|
||||
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
|
||||
Environment map[string]string `json:"environment" yaml:"environment"`
|
||||
}
|
||||
|
||||
func makeResponse() (*Response, error) {
|
||||
labels, err := filesToMap("/etc/podinfod/metadata/labels")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
annotations, err := filesToMap("/etc/podinfod/metadata/annotations")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := &Response{
|
||||
Environment: envToMap(),
|
||||
Runtime: runtimeToMap(),
|
||||
Labels: labels,
|
||||
Annotations: annotations,
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func filesToMap(dir string) (map[string]string, error) {
|
||||
list := make(map[string]string, 0)
|
||||
if _, err := os.Stat(dir); err != nil {
|
||||
// path not found
|
||||
return list, nil
|
||||
}
|
||||
files := make([]string, 0)
|
||||
err := filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
|
||||
files = append(files, path)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Reading from %v failed", dir)
|
||||
}
|
||||
for _, path := range files {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s := bufio.NewScanner(file)
|
||||
for s.Scan() {
|
||||
kv := strings.Split(s.Text(), "=")
|
||||
if len(kv) > 1 {
|
||||
list[kv[0]] = strings.Replace(kv[1], "\"", "", -1)
|
||||
} else {
|
||||
list[kv[0]] = ""
|
||||
}
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func envToMap() map[string]string {
|
||||
list := make(map[string]string, 0)
|
||||
for _, env := range os.Environ() {
|
||||
kv := strings.Split(env, "=")
|
||||
if len(kv) > 1 {
|
||||
list[kv[0]] = strings.Replace(kv[1], "\"", "", -1)
|
||||
} else {
|
||||
list[kv[0]] = ""
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func runtimeToMap() map[string]string {
|
||||
info := map[string]string{
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"version": runtime.Version(),
|
||||
"max_procs": strconv.FormatInt(int64(runtime.GOMAXPROCS(0)), 10),
|
||||
"num_goroutine": strconv.FormatInt(int64(runtime.NumGoroutine()), 10),
|
||||
"num_cpu": strconv.FormatInt(int64(runtime.NumCPU()), 10),
|
||||
"external_ip": findIp("http://httpbin.org/ip"),
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func findIp(url string) string {
|
||||
ip := ""
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DisableKeepAlives: false,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
Timeout: time.Duration(1 * time.Second),
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Error().Err(errors.Wrapf(err, "cannot connect to %s", url)).Msg("timeout")
|
||||
return ip
|
||||
}
|
||||
|
||||
if res.Body != nil {
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode == http.StatusOK {
|
||||
contents, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return ip
|
||||
}
|
||||
jsonMap := make(map[string]string)
|
||||
err = json.Unmarshal([]byte(contents), &jsonMap)
|
||||
if err != nil {
|
||||
return ip
|
||||
}
|
||||
return jsonMap["origin"]
|
||||
}
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/stefanprodan/k8s-podinfo/pkg/fscache"
|
||||
)
|
||||
|
||||
var (
|
||||
healthy int32
|
||||
ready int32
|
||||
dataPath string
|
||||
watcher *fscache.Watcher
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
func NewServer(options ...func(*Server)) *Server {
|
||||
|
||||
s := &Server{mux: http.NewServeMux()}
|
||||
|
||||
for _, f := range options {
|
||||
f(s)
|
||||
}
|
||||
|
||||
s.mux.HandleFunc("/", s.index)
|
||||
s.mux.HandleFunc("/healthz", s.healthz)
|
||||
s.mux.HandleFunc("/readyz", s.readyz)
|
||||
s.mux.HandleFunc("/readyz/enable", s.enable)
|
||||
s.mux.HandleFunc("/readyz/disable", s.disable)
|
||||
s.mux.HandleFunc("/echo", s.echo)
|
||||
s.mux.HandleFunc("/echoheaders", s.echoHeaders)
|
||||
s.mux.HandleFunc("/backend", s.backend)
|
||||
s.mux.HandleFunc("/job", s.job)
|
||||
s.mux.HandleFunc("/read", s.read)
|
||||
s.mux.HandleFunc("/write", s.write)
|
||||
s.mux.HandleFunc("/error", s.error)
|
||||
s.mux.HandleFunc("/panic", s.panic)
|
||||
s.mux.HandleFunc("/configs", s.configs)
|
||||
s.mux.HandleFunc("/version", s.version)
|
||||
s.mux.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
// Register pprof handlers
|
||||
s.mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
s.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
s.mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
s.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
s.mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
|
||||
// API
|
||||
s.mux.HandleFunc("/api/info", s.apiInfo)
|
||||
s.mux.HandleFunc("/api/echo", s.apiEcho)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Server", runtime.Version())
|
||||
|
||||
s.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func ListenAndServe(port string, timeout time.Duration, stopCh <-chan struct{}) {
|
||||
inst := NewInstrument()
|
||||
srv := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: inst.Wrap(NewServer()),
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 1 * time.Minute,
|
||||
IdleTimeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
atomic.StoreInt32(&healthy, 1)
|
||||
atomic.StoreInt32(&ready, 1)
|
||||
|
||||
// local storage path
|
||||
dataPath = os.Getenv("data")
|
||||
if len(dataPath) < 1 {
|
||||
dataPath = "/data"
|
||||
}
|
||||
|
||||
// config path
|
||||
configPath := os.Getenv("configPath")
|
||||
if len(configPath) > 0 {
|
||||
var err error
|
||||
watcher, err = fscache.NewWatch(configPath)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("%s watch error", configPath)
|
||||
} else {
|
||||
watcher.Watch()
|
||||
}
|
||||
}
|
||||
|
||||
// run server in background
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
log.Fatal().Err(err).Msg("HTTP server crashed")
|
||||
}
|
||||
}()
|
||||
|
||||
// wait for SIGTERM or SIGINT
|
||||
<-stopCh
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
// all calls to /healthz and /readyz will fail from now on
|
||||
atomic.StoreInt32(&healthy, 0)
|
||||
atomic.StoreInt32(&ready, 0)
|
||||
|
||||
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 {
|
||||
log.Info().Msg("HTTP server stopped")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user