Configurable API & Dashboard base path

Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
This commit is contained in:
Romain 2024-11-25 11:52:04 +01:00 committed by GitHub
parent 090db6d4b0
commit 0ec12c7aa7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 253 additions and 50 deletions

View file

@ -1,36 +1,88 @@
package dashboard
import (
"fmt"
"io/fs"
"net/http"
"strings"
"text/template"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/webui"
)
type indexTemplateData struct {
APIUrl string
}
// Handler expose dashboard routes.
type Handler struct {
BasePath string
assets fs.FS // optional assets, to override the webui.FS default
}
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
assets := h.assets
if assets == nil {
assets = webui.FS
}
// allow iframes from traefik domains only
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src
w.Header().Set("Content-Security-Policy", "frame-src 'self' https://traefik.io https://*.traefik.io;")
// The content type must be guessed by the file server.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
w.Header().Del("Content-Type")
if r.RequestURI == "/" {
indexTemplate, err := template.ParseFS(assets, "index.html")
if err != nil {
log.Error().Err(err).Msg("Unable to parse index template")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
apiPath := strings.TrimSuffix(h.BasePath, "/") + "/api/"
if err = indexTemplate.Execute(w, indexTemplateData{APIUrl: apiPath}); err != nil {
log.Error().Err(err).Msg("Unable to render index template")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
return
}
http.FileServerFS(assets).ServeHTTP(w, r)
}
// Append adds dashboard routes on the given router, optionally using the given
// assets (or webui.FS otherwise).
func Append(router *mux.Router, customAssets fs.FS) {
func Append(router *mux.Router, basePath string, customAssets fs.FS) error {
assets := customAssets
if assets == nil {
assets = webui.FS
}
indexTemplate, err := template.ParseFS(assets, "index.html")
if err != nil {
return fmt.Errorf("parsing index template: %w", err)
}
dashboardPath := strings.TrimSuffix(basePath, "/") + "/dashboard/"
// Expose dashboard
router.Methods(http.MethodGet).
Path("/").
Path(basePath).
HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
prefix := strings.TrimSuffix(req.Header.Get("X-Forwarded-Prefix"), "/")
http.Redirect(resp, req, prefix+"/dashboard/", http.StatusFound)
http.Redirect(resp, req, prefix+dashboardPath, http.StatusFound)
})
router.Methods(http.MethodGet).
PathPrefix("/dashboard/").
Path(dashboardPath).
HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// allow iframes from our domains only
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src
@ -40,22 +92,26 @@ func Append(router *mux.Router, customAssets fs.FS) {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
w.Header().Del("Content-Type")
http.StripPrefix("/dashboard/", http.FileServerFS(assets)).ServeHTTP(w, r)
apiPath := strings.TrimSuffix(basePath, "/") + "/api/"
if err = indexTemplate.Execute(w, indexTemplateData{APIUrl: apiPath}); err != nil {
log.Error().Err(err).Msg("Unable to render index template")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
})
}
func (g Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
assets := g.assets
if assets == nil {
assets = webui.FS
}
// allow iframes from our domains only
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src
w.Header().Set("Content-Security-Policy", "frame-src 'self' https://traefik.io https://*.traefik.io;")
// The content type must be guessed by the file server.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
w.Header().Del("Content-Type")
http.FileServerFS(assets).ServeHTTP(w, r)
router.Methods(http.MethodGet).
PathPrefix(dashboardPath).
HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// allow iframes from traefik domains only
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src
w.Header().Set("Content-Security-Policy", "frame-src 'self' https://traefik.io https://*.traefik.io;")
// The content type must be guessed by the file server.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
w.Header().Del("Content-Type")
http.StripPrefix(dashboardPath, http.FileServerFS(assets)).ServeHTTP(w, r)
})
return nil
}

View file

@ -78,38 +78,40 @@ func New(staticConfig static.Configuration, runtimeConfig *runtime.Configuration
func (h Handler) createRouter() *mux.Router {
router := mux.NewRouter().UseEncodedPath()
apiRouter := router.PathPrefix(h.staticConfig.API.BasePath).Subrouter().UseEncodedPath()
if h.staticConfig.API.Debug {
DebugHandler{}.Append(router)
DebugHandler{}.Append(apiRouter)
}
router.Methods(http.MethodGet).Path("/api/rawdata").HandlerFunc(h.getRuntimeConfiguration)
apiRouter.Methods(http.MethodGet).Path("/api/rawdata").HandlerFunc(h.getRuntimeConfiguration)
// Experimental endpoint
router.Methods(http.MethodGet).Path("/api/overview").HandlerFunc(h.getOverview)
apiRouter.Methods(http.MethodGet).Path("/api/overview").HandlerFunc(h.getOverview)
router.Methods(http.MethodGet).Path("/api/entrypoints").HandlerFunc(h.getEntryPoints)
router.Methods(http.MethodGet).Path("/api/entrypoints/{entryPointID}").HandlerFunc(h.getEntryPoint)
apiRouter.Methods(http.MethodGet).Path("/api/entrypoints").HandlerFunc(h.getEntryPoints)
apiRouter.Methods(http.MethodGet).Path("/api/entrypoints/{entryPointID}").HandlerFunc(h.getEntryPoint)
router.Methods(http.MethodGet).Path("/api/http/routers").HandlerFunc(h.getRouters)
router.Methods(http.MethodGet).Path("/api/http/routers/{routerID}").HandlerFunc(h.getRouter)
router.Methods(http.MethodGet).Path("/api/http/services").HandlerFunc(h.getServices)
router.Methods(http.MethodGet).Path("/api/http/services/{serviceID}").HandlerFunc(h.getService)
router.Methods(http.MethodGet).Path("/api/http/middlewares").HandlerFunc(h.getMiddlewares)
router.Methods(http.MethodGet).Path("/api/http/middlewares/{middlewareID}").HandlerFunc(h.getMiddleware)
apiRouter.Methods(http.MethodGet).Path("/api/http/routers").HandlerFunc(h.getRouters)
apiRouter.Methods(http.MethodGet).Path("/api/http/routers/{routerID}").HandlerFunc(h.getRouter)
apiRouter.Methods(http.MethodGet).Path("/api/http/services").HandlerFunc(h.getServices)
apiRouter.Methods(http.MethodGet).Path("/api/http/services/{serviceID}").HandlerFunc(h.getService)
apiRouter.Methods(http.MethodGet).Path("/api/http/middlewares").HandlerFunc(h.getMiddlewares)
apiRouter.Methods(http.MethodGet).Path("/api/http/middlewares/{middlewareID}").HandlerFunc(h.getMiddleware)
router.Methods(http.MethodGet).Path("/api/tcp/routers").HandlerFunc(h.getTCPRouters)
router.Methods(http.MethodGet).Path("/api/tcp/routers/{routerID}").HandlerFunc(h.getTCPRouter)
router.Methods(http.MethodGet).Path("/api/tcp/services").HandlerFunc(h.getTCPServices)
router.Methods(http.MethodGet).Path("/api/tcp/services/{serviceID}").HandlerFunc(h.getTCPService)
router.Methods(http.MethodGet).Path("/api/tcp/middlewares").HandlerFunc(h.getTCPMiddlewares)
router.Methods(http.MethodGet).Path("/api/tcp/middlewares/{middlewareID}").HandlerFunc(h.getTCPMiddleware)
apiRouter.Methods(http.MethodGet).Path("/api/tcp/routers").HandlerFunc(h.getTCPRouters)
apiRouter.Methods(http.MethodGet).Path("/api/tcp/routers/{routerID}").HandlerFunc(h.getTCPRouter)
apiRouter.Methods(http.MethodGet).Path("/api/tcp/services").HandlerFunc(h.getTCPServices)
apiRouter.Methods(http.MethodGet).Path("/api/tcp/services/{serviceID}").HandlerFunc(h.getTCPService)
apiRouter.Methods(http.MethodGet).Path("/api/tcp/middlewares").HandlerFunc(h.getTCPMiddlewares)
apiRouter.Methods(http.MethodGet).Path("/api/tcp/middlewares/{middlewareID}").HandlerFunc(h.getTCPMiddleware)
router.Methods(http.MethodGet).Path("/api/udp/routers").HandlerFunc(h.getUDPRouters)
router.Methods(http.MethodGet).Path("/api/udp/routers/{routerID}").HandlerFunc(h.getUDPRouter)
router.Methods(http.MethodGet).Path("/api/udp/services").HandlerFunc(h.getUDPServices)
router.Methods(http.MethodGet).Path("/api/udp/services/{serviceID}").HandlerFunc(h.getUDPService)
apiRouter.Methods(http.MethodGet).Path("/api/udp/routers").HandlerFunc(h.getUDPRouters)
apiRouter.Methods(http.MethodGet).Path("/api/udp/routers/{routerID}").HandlerFunc(h.getUDPRouter)
apiRouter.Methods(http.MethodGet).Path("/api/udp/services").HandlerFunc(h.getUDPServices)
apiRouter.Methods(http.MethodGet).Path("/api/udp/services/{serviceID}").HandlerFunc(h.getUDPService)
version.Handler{}.Append(router)
version.Handler{}.Append(apiRouter)
return router
}

View file

@ -3,6 +3,7 @@ package static
import (
"errors"
"fmt"
"path"
"strings"
"time"
@ -145,16 +146,18 @@ type TLSClientConfig struct {
// API holds the API configuration.
type API struct {
Insecure bool `description:"Activate API directly on the entryPoint named traefik." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
Dashboard bool `description:"Activate dashboard." json:"dashboard,omitempty" toml:"dashboard,omitempty" yaml:"dashboard,omitempty" export:"true"`
Debug bool `description:"Enable additional endpoints for debugging and profiling." json:"debug,omitempty" toml:"debug,omitempty" yaml:"debug,omitempty" export:"true"`
DisableDashboardAd bool `description:"Disable ad in the dashboard." json:"disableDashboardAd,omitempty" toml:"disableDashboardAd,omitempty" yaml:"disableDashboardAd,omitempty" export:"true"`
BasePath string `description:"Defines the base path where the API and Dashboard will be exposed." json:"basePath,omitempty" toml:"basePath,omitempty" yaml:"basePath,omitempty" export:"true"`
Insecure bool `description:"Activate API directly on the entryPoint named traefik." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
Dashboard bool `description:"Activate dashboard." json:"dashboard,omitempty" toml:"dashboard,omitempty" yaml:"dashboard,omitempty" export:"true"`
Debug bool `description:"Enable additional endpoints for debugging and profiling." json:"debug,omitempty" toml:"debug,omitempty" yaml:"debug,omitempty" export:"true"`
DisableDashboardAd bool `description:"Disable ad in the dashboard." json:"disableDashboardAd,omitempty" toml:"disableDashboardAd,omitempty" yaml:"disableDashboardAd,omitempty" export:"true"`
// TODO: Re-enable statistics
// Statistics *types.Statistics `description:"Enable more detailed statistics." json:"statistics,omitempty" toml:"statistics,omitempty" yaml:"statistics,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
}
// SetDefaults sets the default values.
func (a *API) SetDefaults() {
a.BasePath = "/"
a.Dashboard = true
}
@ -360,6 +363,10 @@ func (c *Configuration) ValidateConfiguration() error {
}
}
if c.API != nil && !path.IsAbs(c.API.BasePath) {
return errors.New("API basePath must be a valid absolute path")
}
return nil
}

View file

@ -4,6 +4,7 @@ import (
"net/http"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/api"
"github.com/traefik/traefik/v3/pkg/api/dashboard"
"github.com/traefik/traefik/v3/pkg/config/runtime"
@ -44,10 +45,13 @@ func NewManagerFactory(staticConfiguration static.Configuration, routinesPool *s
apiRouterBuilder := api.NewBuilder(staticConfiguration)
if staticConfiguration.API.Dashboard {
factory.dashboardHandler = dashboard.Handler{}
factory.dashboardHandler = dashboard.Handler{BasePath: staticConfiguration.API.BasePath}
factory.api = func(configuration *runtime.Configuration) http.Handler {
router := apiRouterBuilder(configuration).(*mux.Router)
dashboard.Append(router, nil)
if err := dashboard.Append(router, staticConfiguration.API.BasePath, nil); err != nil {
log.Error().Err(err).Msg("Error appending dashboard to API router")
}
return router
}
} else {