1
0
mirror of https://github.com/containous/traefik.git synced 2024-12-21 09:34:05 +03:00

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

@ -87,8 +87,44 @@ rule = "Host(`traefik.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashb
??? example "Dashboard Dynamic Configuration Examples"
--8<-- "content/operations/include-dashboard-examples.md"
### Custom API Base Path
As shown above, by default Traefik exposes its API and Dashboard under the `/` base path,
which means that respectively the API is served under the `/api` path,
and the dashboard under the `/dashboard` path.
However, it is possible to configure this base path:
```yaml tab="File (YAML)"
api:
# Customizes the base path:
# - Serving API under `/traefik/api`
# - Serving Dashboard under `/traefik/dashboard`
basePath: /traefik
```
```toml tab="File (TOML)"
[api]
# Customizes the base path:
# - Serving API under `/traefik/api`
# - Serving Dashboard under `/traefik/dashboard`
basePath = "/traefik"
```
```bash tab="CLI"
# Customizes the base path:
# - Serving API under `/traefik/api`
# - Serving Dashboard under `/traefik/dashboard`
--api.basePath=/traefik
```
??? example "Dashboard Under Custom Path Dynamic Configuration Examples"
--8<-- "content/operations/include-dashboard-custom-path-examples.md"
## Insecure Mode
!!! warning "Please note that this mode is incompatible with the [custom API base path option](#custom-api-base-path)."
When _insecure_ mode is enabled, one can access the dashboard on the `traefik` port (default: `8080`) of the Traefik instance,
at the following URL: `http://<Traefik IP>:8080/dashboard/` (trailing slash is mandatory).

View File

@ -0,0 +1,83 @@
```yaml tab="Docker & Swarm"
# Dynamic Configuration
labels:
- "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`) && PathPrefix(`/traefik`)"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=test:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/,test2:$$apr1$$d9hr9HBB$$4HxwgUir3HP4EsggP/QNo0"
```
```yaml tab="Docker (Swarm)"
# Dynamic Configuration
deploy:
labels:
- "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`) && PathPrefix(`/traefik`)"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=test:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/,test2:$$apr1$$d9hr9HBB$$4HxwgUir3HP4EsggP/QNo0"
# Dummy service for Swarm port detection. The port can be any valid integer value.
- "traefik.http.services.dummy-svc.loadbalancer.server.port=9999"
```
```yaml tab="Kubernetes CRD"
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: traefik-dashboard
spec:
routes:
- match: Host(`traefik.example.com`) && PathPrefix(`/traefik`)
kind: Rule
services:
- name: api@internal
kind: TraefikService
middlewares:
- name: auth
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: auth
spec:
basicAuth:
secret: secretName # Kubernetes secret named "secretName"
```
```yaml tab="Consul Catalog"
# Dynamic Configuration
- "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`) && PathPrefix(`/traefik`)"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=test:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/,test2:$$apr1$$d9hr9HBB$$4HxwgUir3HP4EsggP/QNo0"
```
```yaml tab="File (YAML)"
# Dynamic Configuration
http:
routers:
dashboard:
rule: Host(`traefik.example.com`) && PathPrefix(`/traefik`)
service: api@internal
middlewares:
- auth
middlewares:
auth:
basicAuth:
users:
- "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"
- "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"
```
```toml tab="File (TOML)"
# Dynamic Configuration
[http.routers.my-api]
rule = "Host(`traefik.example.com`) && PathPrefix(`/traefik`)"
service = "api@internal"
middlewares = ["auth"]
[http.middlewares.auth.basicAuth]
users = [
"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
]
```

View File

@ -42,6 +42,9 @@ Access log format: json | common (Default: ```common```)
`--api`:
Enable api/dashboard. (Default: ```false```)
`--api.basepath`:
Defines the base path where the API and Dashboard will be exposed. (Default: ```/```)
`--api.dashboard`:
Activate dashboard. (Default: ```true```)

View File

@ -42,6 +42,9 @@ Access log format: json | common (Default: ```common```)
`TRAEFIK_API`:
Enable api/dashboard. (Default: ```false```)
`TRAEFIK_API_BASEPATH`:
Defines the base path where the API and Dashboard will be exposed. (Default: ```/```)
`TRAEFIK_API_DASHBOARD`:
Activate dashboard. (Default: ```true```)

View File

@ -294,6 +294,7 @@
name1 = "foobar"
[api]
basePath = "foobar"
insecure = true
dashboard = true
debug = true

View File

@ -330,6 +330,7 @@ providers:
name0: foobar
name1: foobar
api:
basePath: foobar
insecure: true
dashboard: true
debug: true

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 {

View File

@ -1,6 +1,13 @@
<!DOCTYPE html>
<html>
<head>
{{if .APIUrl}}
<script>
window.APIURL = "{{.APIUrl}}"
</script>
{{end}}
<title><%= productName %></title>
<meta charset="utf-8">

View File

@ -4,7 +4,7 @@ import { APP } from '../_helpers/APP'
// Set config defaults when creating the instance
const api = axios.create({
baseURL: APP.config.apiUrl
baseURL: window.APIURL || APP.config.apiUrl
})
export default boot(({ app }) => {