mirror of
https://github.com/stefanprodan/podinfo.git
synced 2026-08-19 04:06:27 +00:00
Add support for configurable URL path prefix in podinfo application
This commit is contained in:
@@ -66,6 +66,7 @@ The following tables lists the configurable parameters of the podinfo chart and
|
||||
| `ui.color` | `#34577c` | UI color |
|
||||
| `ui.message` | `None` | UI greetings message |
|
||||
| `ui.logo` | `None` | UI logo |
|
||||
| `prefix` | `/` | URL path prefix for HTTP routes (e.g. `/foo` maps `/api/info` to `/foo/api/info`) |
|
||||
| `faults.delay` | `false` | Random HTTP response delays between 0 and 5 seconds |
|
||||
| `faults.error` | `false` | 1/3 chances of a random HTTP response error |
|
||||
| `faults.unhealthy` | `false` | When set, the healthy state is never reached |
|
||||
|
||||
@@ -53,6 +53,7 @@ spec:
|
||||
command:
|
||||
- ./podinfo
|
||||
- --port={{ .Values.service.httpPort | default 9898 }}
|
||||
- --prefix={{ .Values.prefix | default "/" }}
|
||||
{{- if .Values.host }}
|
||||
- --host={{ .Values.host }}
|
||||
{{- end }}
|
||||
@@ -152,7 +153,7 @@ spec:
|
||||
- podcli
|
||||
- check
|
||||
- http
|
||||
- localhost:{{ .Values.service.httpPort | default 9898 }}/healthz
|
||||
- localhost:{{ .Values.service.httpPort | default 9898 }}{{ trimSuffix "/" (.Values.prefix | default "/") }}/healthz
|
||||
{{- with .Values.probes.startup }}
|
||||
initialDelaySeconds: {{ .initialDelaySeconds | default 1 }}
|
||||
timeoutSeconds: {{ .timeoutSeconds | default 5 }}
|
||||
@@ -167,7 +168,7 @@ spec:
|
||||
- podcli
|
||||
- check
|
||||
- http
|
||||
- localhost:{{ .Values.service.httpPort | default 9898 }}/healthz
|
||||
- localhost:{{ .Values.service.httpPort | default 9898 }}{{ trimSuffix "/" (.Values.prefix | default "/") }}/healthz
|
||||
{{- with .Values.probes.liveness }}
|
||||
initialDelaySeconds: {{ .initialDelaySeconds | default 1 }}
|
||||
timeoutSeconds: {{ .timeoutSeconds | default 5 }}
|
||||
@@ -181,7 +182,7 @@ spec:
|
||||
- podcli
|
||||
- check
|
||||
- http
|
||||
- localhost:{{ .Values.service.httpPort | default 9898 }}/readyz
|
||||
- localhost:{{ .Values.service.httpPort | default 9898 }}{{ trimSuffix "/" (.Values.prefix | default "/") }}/readyz
|
||||
{{- with .Values.probes.readiness }}
|
||||
initialDelaySeconds: {{ .initialDelaySeconds | default 1 }}
|
||||
timeoutSeconds: {{ .timeoutSeconds | default 5 }}
|
||||
|
||||
@@ -11,7 +11,7 @@ metadata:
|
||||
{{- end }}
|
||||
spec:
|
||||
endpoints:
|
||||
- path: /metrics
|
||||
- path: {{ trimSuffix "/" (.Values.prefix | default "/") }}/metrics
|
||||
port: http
|
||||
interval: {{ .Values.serviceMonitor.interval }}
|
||||
namespaceSelector:
|
||||
|
||||
@@ -19,7 +19,7 @@ spec:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
curl -s ${PODINFO_SVC}/api/info | grep version
|
||||
curl -s ${PODINFO_SVC}{{ trimSuffix "/" (.Values.prefix | default "/") }}/api/info | grep version
|
||||
env:
|
||||
- name: PODINFO_SVC
|
||||
value: "{{ template "podinfo.fullname" . }}.{{ include "podinfo.namespace" . }}:{{ .Values.service.externalPort }}"
|
||||
|
||||
@@ -20,9 +20,9 @@ spec:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
curl -sk ${PODINFO_SVC}/api/info | grep version
|
||||
curl -sk ${PODINFO_SVC}{{ trimSuffix "/" (.Values.prefix | default "/") }}/api/info | grep version
|
||||
env:
|
||||
- name: PODINFO_SVC
|
||||
value: "https://{{ template "podinfo.fullname" . }}.{{ include "podinfo.namespace" . }}:{{ .Values.tls.port }}"
|
||||
restartPolicy: Never
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -12,6 +12,8 @@ image:
|
||||
pullPolicy: IfNotPresent
|
||||
pullSecrets: []
|
||||
|
||||
prefix: /
|
||||
|
||||
ui:
|
||||
color: "#34577c"
|
||||
message: ""
|
||||
|
||||
@@ -30,6 +30,7 @@ func main() {
|
||||
// flags definition
|
||||
fs := pflag.NewFlagSet("default", pflag.ContinueOnError)
|
||||
fs.String("host", "", "Host to bind service to")
|
||||
fs.String("prefix", "/", "URL path prefix for HTTP routes")
|
||||
fs.Int("port", 9898, "HTTP port to bind service to")
|
||||
fs.Int("secure-port", 0, "HTTPS port")
|
||||
fs.Int("port-metrics", 0, "metrics port")
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Index godoc
|
||||
@@ -25,11 +26,13 @@ func (s *Server) indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Title string
|
||||
Logo string
|
||||
Title string
|
||||
Logo string
|
||||
Prefix string
|
||||
}{
|
||||
Title: s.config.Hostname,
|
||||
Logo: s.config.UILogo,
|
||||
Title: s.config.Hostname,
|
||||
Logo: s.config.UILogo,
|
||||
Prefix: strings.TrimSuffix(s.config.Prefix, "/"),
|
||||
}
|
||||
|
||||
if err := tmpl.Execute(w, data); err != nil {
|
||||
|
||||
@@ -22,6 +22,7 @@ func NewMockServer() *Server {
|
||||
UIPath: ".ui",
|
||||
UIMessage: "Greetings",
|
||||
Hostname: "localhost",
|
||||
Prefix: "/",
|
||||
}
|
||||
|
||||
logger, _ := zap.NewDevelopment()
|
||||
|
||||
+80
-35
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/gomodule/redigo/redis"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
_ "github.com/stefanprodan/podinfo/pkg/api/http/docs"
|
||||
"github.com/stefanprodan/podinfo/pkg/api/http/docs"
|
||||
"github.com/stefanprodan/podinfo/pkg/fscache"
|
||||
httpSwagger "github.com/swaggo/http-swagger"
|
||||
"github.com/swaggo/swag"
|
||||
@@ -57,6 +57,7 @@ type Config struct {
|
||||
ConfigPath string `mapstructure:"config-path"`
|
||||
CertPath string `mapstructure:"cert-path"`
|
||||
Host string `mapstructure:"host"`
|
||||
Prefix string `mapstructure:"prefix"`
|
||||
Port string `mapstructure:"port"`
|
||||
SecurePort string `mapstructure:"secure-port"`
|
||||
PortMetrics int `mapstructure:"port-metrics"`
|
||||
@@ -84,6 +85,9 @@ type Server struct {
|
||||
}
|
||||
|
||||
func NewServer(config *Config, logger *zap.Logger) (*Server, error) {
|
||||
config.Prefix = normalizePrefix(config.Prefix)
|
||||
docs.SwaggerInfo.BasePath = config.Prefix
|
||||
|
||||
srv := &Server{
|
||||
router: mux.NewRouter(),
|
||||
logger: logger,
|
||||
@@ -93,44 +97,85 @@ func NewServer(config *Config, logger *zap.Logger) (*Server, error) {
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
func normalizePrefix(prefix string) string {
|
||||
prefix = strings.TrimSpace(prefix)
|
||||
if prefix == "" || prefix == "/" {
|
||||
return "/"
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(prefix, "/") {
|
||||
prefix = "/" + prefix
|
||||
}
|
||||
|
||||
prefix = path.Clean(prefix)
|
||||
if prefix == "." || prefix == "" {
|
||||
return "/"
|
||||
}
|
||||
|
||||
return prefix
|
||||
}
|
||||
|
||||
func (s *Server) prefixedPath(route string) string {
|
||||
prefix := s.config.Prefix
|
||||
if prefix == "" || prefix == "/" {
|
||||
return route
|
||||
}
|
||||
|
||||
if route == "/" {
|
||||
return prefix
|
||||
}
|
||||
|
||||
if strings.HasPrefix(route, "/") {
|
||||
return prefix + route
|
||||
}
|
||||
|
||||
return prefix + "/" + route
|
||||
}
|
||||
|
||||
func (s *Server) registerHandlers() {
|
||||
s.router.Handle("/metrics", promhttp.Handler())
|
||||
s.router.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux)
|
||||
s.router.HandleFunc("/", s.indexHandler).HeadersRegexp("User-Agent", "^Mozilla.*").Methods("GET")
|
||||
s.router.HandleFunc("/", s.infoHandler).Methods("GET")
|
||||
s.router.HandleFunc("/version", s.versionHandler).Methods("GET")
|
||||
s.router.HandleFunc("/echo", s.echoHandler)
|
||||
s.router.PathPrefix("/echo/").HandlerFunc(s.echoHandler)
|
||||
s.router.HandleFunc("/env", s.envHandler).Methods("GET", "POST")
|
||||
s.router.HandleFunc("/headers", s.echoHeadersHandler).Methods("GET", "POST")
|
||||
s.router.HandleFunc("/delay/{wait:[0-9]+}", s.delayHandler).Methods("GET").Name("delay")
|
||||
s.router.HandleFunc("/healthz", s.healthzHandler).Methods("GET")
|
||||
s.router.HandleFunc("/readyz", s.readyzHandler).Methods("GET")
|
||||
s.router.HandleFunc("/readyz/enable", s.enableReadyHandler).Methods("POST")
|
||||
s.router.HandleFunc("/readyz/disable", s.disableReadyHandler).Methods("POST")
|
||||
s.router.HandleFunc("/panic", s.panicHandler).Methods("GET")
|
||||
s.router.HandleFunc("/status/{code:[0-9]+}", s.statusHandler).Methods("GET", "POST", "PUT").Name("status")
|
||||
s.router.HandleFunc("/store", s.storeWriteHandler).Methods("POST", "PUT")
|
||||
s.router.HandleFunc("/store/{hash}", s.storeReadHandler).Methods("GET").Name("store")
|
||||
s.router.HandleFunc("/cache/{key}", s.cacheWriteHandler).Methods("POST", "PUT")
|
||||
s.router.HandleFunc("/cache/{key}", s.cacheDeleteHandler).Methods("DELETE")
|
||||
s.router.HandleFunc("/cache/{key}", s.cacheReadHandler).Methods("GET").Name("cache")
|
||||
s.router.HandleFunc("/configs", s.configReadHandler).Methods("GET")
|
||||
s.router.HandleFunc("/token", s.tokenGenerateHandler).Methods("POST")
|
||||
s.router.HandleFunc("/token/validate", s.tokenValidateHandler).Methods("GET")
|
||||
s.router.HandleFunc("/api/info", s.infoHandler).Methods("GET")
|
||||
s.router.HandleFunc("/api/echo", s.echoHandler)
|
||||
s.router.PathPrefix("/api/echo/").HandlerFunc(s.echoHandler)
|
||||
s.router.HandleFunc("/ws/echo", s.echoWsHandler)
|
||||
s.router.HandleFunc("/chunked", s.chunkedHandler)
|
||||
s.router.HandleFunc("/chunked/{wait:[0-9]+}", s.chunkedHandler)
|
||||
s.router.PathPrefix("/swagger/").Handler(httpSwagger.Handler(
|
||||
httpSwagger.URL("/swagger/doc.json"),
|
||||
rootPath := s.prefixedPath("/")
|
||||
|
||||
s.router.Handle(s.prefixedPath("/metrics"), promhttp.Handler())
|
||||
s.router.PathPrefix(s.prefixedPath("/debug/pprof/")).Handler(http.DefaultServeMux)
|
||||
s.router.HandleFunc(rootPath, s.indexHandler).HeadersRegexp("User-Agent", "^Mozilla.*").Methods("GET")
|
||||
s.router.HandleFunc(rootPath, s.infoHandler).Methods("GET")
|
||||
if rootPath != "/" {
|
||||
s.router.HandleFunc(rootPath+"/", s.indexHandler).HeadersRegexp("User-Agent", "^Mozilla.*").Methods("GET")
|
||||
s.router.HandleFunc(rootPath+"/", s.infoHandler).Methods("GET")
|
||||
}
|
||||
s.router.HandleFunc(s.prefixedPath("/version"), s.versionHandler).Methods("GET")
|
||||
s.router.HandleFunc(s.prefixedPath("/echo"), s.echoHandler)
|
||||
s.router.PathPrefix(s.prefixedPath("/echo/")).HandlerFunc(s.echoHandler)
|
||||
s.router.HandleFunc(s.prefixedPath("/env"), s.envHandler).Methods("GET", "POST")
|
||||
s.router.HandleFunc(s.prefixedPath("/headers"), s.echoHeadersHandler).Methods("GET", "POST")
|
||||
s.router.HandleFunc(s.prefixedPath("/delay/{wait:[0-9]+}"), s.delayHandler).Methods("GET").Name("delay")
|
||||
s.router.HandleFunc(s.prefixedPath("/healthz"), s.healthzHandler).Methods("GET")
|
||||
s.router.HandleFunc(s.prefixedPath("/readyz"), s.readyzHandler).Methods("GET")
|
||||
s.router.HandleFunc(s.prefixedPath("/readyz/enable"), s.enableReadyHandler).Methods("POST")
|
||||
s.router.HandleFunc(s.prefixedPath("/readyz/disable"), s.disableReadyHandler).Methods("POST")
|
||||
s.router.HandleFunc(s.prefixedPath("/panic"), s.panicHandler).Methods("GET")
|
||||
s.router.HandleFunc(s.prefixedPath("/status/{code:[0-9]+}"), s.statusHandler).Methods("GET", "POST", "PUT").Name("status")
|
||||
s.router.HandleFunc(s.prefixedPath("/store"), s.storeWriteHandler).Methods("POST", "PUT")
|
||||
s.router.HandleFunc(s.prefixedPath("/store/{hash}"), s.storeReadHandler).Methods("GET").Name("store")
|
||||
s.router.HandleFunc(s.prefixedPath("/cache/{key}"), s.cacheWriteHandler).Methods("POST", "PUT")
|
||||
s.router.HandleFunc(s.prefixedPath("/cache/{key}"), s.cacheDeleteHandler).Methods("DELETE")
|
||||
s.router.HandleFunc(s.prefixedPath("/cache/{key}"), s.cacheReadHandler).Methods("GET").Name("cache")
|
||||
s.router.HandleFunc(s.prefixedPath("/configs"), s.configReadHandler).Methods("GET")
|
||||
s.router.HandleFunc(s.prefixedPath("/token"), s.tokenGenerateHandler).Methods("POST")
|
||||
s.router.HandleFunc(s.prefixedPath("/token/validate"), s.tokenValidateHandler).Methods("GET")
|
||||
s.router.HandleFunc(s.prefixedPath("/api/info"), s.infoHandler).Methods("GET")
|
||||
s.router.HandleFunc(s.prefixedPath("/api/echo"), s.echoHandler)
|
||||
s.router.PathPrefix(s.prefixedPath("/api/echo/")).HandlerFunc(s.echoHandler)
|
||||
s.router.HandleFunc(s.prefixedPath("/ws/echo"), s.echoWsHandler)
|
||||
s.router.HandleFunc(s.prefixedPath("/chunked"), s.chunkedHandler)
|
||||
s.router.HandleFunc(s.prefixedPath("/chunked/{wait:[0-9]+}"), s.chunkedHandler)
|
||||
s.router.PathPrefix(s.prefixedPath("/swagger/")).Handler(httpSwagger.Handler(
|
||||
httpSwagger.URL(s.prefixedPath("/swagger/doc.json")),
|
||||
))
|
||||
s.router.HandleFunc("/swagger.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
s.router.HandleFunc(s.prefixedPath("/swagger.json"), func(w http.ResponseWriter, r *http.Request) {
|
||||
doc, err := swag.ReadDoc()
|
||||
if err != nil {
|
||||
s.logger.Error("swagger error", zap.Error(err), zap.String("path", "/swagger.json"))
|
||||
s.logger.Error("swagger error", zap.Error(err), zap.String("path", s.prefixedPath("/swagger.json")))
|
||||
}
|
||||
w.Write([]byte(doc))
|
||||
})
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizePrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
output string
|
||||
}{
|
||||
{name: "default root", input: "/", output: "/"},
|
||||
{name: "empty", input: "", output: "/"},
|
||||
{name: "no leading slash", input: "foo", output: "/foo"},
|
||||
{name: "trailing slash", input: "/foo/", output: "/foo"},
|
||||
{name: "double slash", input: "//foo//bar//", output: "/foo/bar"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := normalizePrefix(tt.input); got != tt.output {
|
||||
t.Fatalf("normalizePrefix(%q) = %q, want %q", tt.input, got, tt.output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterHandlersWithPrefix(t *testing.T) {
|
||||
srv := NewMockServer()
|
||||
srv.config.Prefix = normalizePrefix("/foo")
|
||||
srv.registerHandlers()
|
||||
|
||||
reqRootSlash, err := http.NewRequest("GET", "/foo/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rrRootSlash := httptest.NewRecorder()
|
||||
srv.router.ServeHTTP(rrRootSlash, reqRootSlash)
|
||||
|
||||
if rrRootSlash.Code != http.StatusOK {
|
||||
t.Fatalf("GET /foo/ returned %d, want %d", rrRootSlash.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "/foo/api/info", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
srv.router.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("GET /foo/api/info returned %d, want %d", rr.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
reqNoPrefix, err := http.NewRequest("GET", "/api/info", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rrNoPrefix := httptest.NewRecorder()
|
||||
srv.router.ServeHTTP(rrNoPrefix, reqNoPrefix)
|
||||
|
||||
if rrNoPrefix.Code != http.StatusNotFound {
|
||||
t.Fatalf("GET /api/info returned %d, want %d", rrNoPrefix.Code, http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -62,7 +62,7 @@
|
||||
Powered
|
||||
by <a class="white--text" href="https://github.com/stefanprodan/podinfo" target="_blank">podinfo</a>
|
||||
version ${ info.version } revision ${ info.revision }
|
||||
Swagger <a class="white--text" href="swagger/">docs</a>
|
||||
Swagger <a class="white--text" :href="prefix + '/swagger/'">docs</a>
|
||||
</div>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
@@ -73,6 +73,9 @@
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
|
||||
<script>
|
||||
window.podinfoPrefix = {{ .Prefix }};
|
||||
</script>
|
||||
<script>
|
||||
new Vue({
|
||||
delimiters: ['${', '}'],
|
||||
@@ -85,6 +88,7 @@
|
||||
color: '',
|
||||
pings: 0,
|
||||
calls: 0,
|
||||
prefix: window.podinfoPrefix === '/' ? '' : window.podinfoPrefix,
|
||||
tlName1: '',
|
||||
tlColor1: 'grey',
|
||||
tlName2: '',
|
||||
@@ -102,7 +106,7 @@
|
||||
getInfo: function() {
|
||||
const xhr = new XMLHttpRequest();
|
||||
let self = this;
|
||||
xhr.open('GET', "api/info")
|
||||
xhr.open('GET', self.prefix + "/api/info")
|
||||
xhr.onload = function () {
|
||||
data = JSON.parse(xhr.responseText)
|
||||
if (self.info.version) {
|
||||
@@ -138,7 +142,7 @@
|
||||
},
|
||||
postBackend: function() {
|
||||
var self = this
|
||||
fetch("api/echo", {
|
||||
fetch(self.prefix + "/api/echo", {
|
||||
method: 'post',
|
||||
headers: {
|
||||
"Content-type": "application/json; charset=UTF-8",
|
||||
|
||||
Reference in New Issue
Block a user