From ca54790e9a1f67dccd69e45efc05b85ffaefff6e Mon Sep 17 00:00:00 2001 From: Drew Hammond <2405390+drewhammond@users.noreply.github.com> Date: Thu, 2 Nov 2023 21:02:28 -0400 Subject: [PATCH] support configurable base_path (#250) * support configurable base_path * temporarily enable docker builds for this branch * rewrite assets path in prod too * handle no trailing slash * strip trailing slash (#246) * document usage * redirect user if they somehow bypass the subpath * redirect user when trailing slash provided (#246) * remove temporary build test --- .github/workflows/build-images.yml | 1 + config/config.go | 2 ++ defaults.ini | 4 +++ internal/app/api/api.go | 9 +++++- internal/app/app.go | 17 +++++++++++ internal/app/ui/manifest.go | 4 +-- internal/app/ui/routes.go | 28 ++++++++++++++----- ui/templates/cookbook_recipes.html | 2 +- ui/templates/cookbooks.html | 2 +- ui/templates/databag_items.html | 2 +- ui/templates/databags.html | 2 +- ui/templates/environment.html | 2 +- ui/templates/environments.html | 2 +- ui/templates/layouts/master.html | 4 +-- ui/templates/layouts/nav.html | 14 +++++----- ui/templates/nodes.html | 2 +- ui/templates/partials/cookbook/header.html | 11 ++++---- ui/templates/partials/cookbook_file_list.html | 14 +++++----- ui/templates/partials/node/header.html | 8 +++--- ui/templates/policy-group.html | 2 +- ui/templates/policy-revision.html | 2 +- ui/templates/policy.html | 2 +- ui/templates/role.html | 4 +-- ui/templates/roles.html | 2 +- ui/vite.config.js | 2 +- 25 files changed, 95 insertions(+), 49 deletions(-) diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml index de63afe..479ecca 100644 --- a/.github/workflows/build-images.yml +++ b/.github/workflows/build-images.yml @@ -40,6 +40,7 @@ jobs: ${{ env.GHCR_REPO }} tags: | type=semver,pattern={{version}} + type=ref,event=branch - name: Log in to Container Registry uses: docker/login-action@v3 with: diff --git a/config/config.go b/config/config.go index 64dd926..dd009a3 100644 --- a/config/config.go +++ b/config/config.go @@ -19,6 +19,7 @@ format = json request_logging = true [server] +base_path = / trusted_proxies = `) @@ -42,6 +43,7 @@ type loggingConfig struct { } type serverConfig struct { + BasePath string `mapstructure:"base_path"` EnableGzip bool `mapstructure:"enable_gzip"` TrustedProxies string `mapstructure:"trusted_proxies"` } diff --git a/defaults.ini b/defaults.ini index 9ef92b6..94424cd 100644 --- a/defaults.ini +++ b/defaults.ini @@ -21,6 +21,10 @@ destination = stdout request_logging = true [server] +# To serve from a sub path (e.g. in a reverse proxy configuration), specify the sub path here. +# If accessed without the sub path, chefbrowser will redirect the user to the sub path. +base_path = / + # Comma-separated list of proxy servers or networks (in CIDR format) from which to trust request headers # containing alternate client IP addresses (e.g. X-Forwarded-For or X-Real-IP). Leave empty to ignore these headers trusted_proxies = diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 3f454b5..4f225e6 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -10,6 +10,8 @@ import ( "github.com/labstack/echo/v4/middleware" ) +var basePath = "" + type Service struct { log *logging.Logger config *config.Config @@ -24,13 +26,14 @@ func New(config *config.Config, engine *echo.Echo, chef *chef.Service, logger *l log: logger, engine: engine, } + basePath = config.Server.BasePath return &s } func (s *Service) RegisterRoutes() { s.log.Info("registering API routes") - router := s.engine.Group("/api") + router := s.engine.Group(urlWithBasePath("/api")) { router.Use(middleware.CORS()) // nodes @@ -72,6 +75,10 @@ func (s *Service) RegisterRoutes() { } } +func urlWithBasePath(path string) string { + return basePath + path +} + type HealthResponse struct { Success bool `json:"success"` Message string `json:"message"` diff --git a/internal/app/app.go b/internal/app/app.go index 65a569c..838abba 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -3,6 +3,8 @@ package app import ( "fmt" "net" + "net/http" + "path" "strings" "github.com/drewhammond/chefbrowser/config" @@ -39,6 +41,10 @@ func New(cfg *config.Config) { engine.Debug = true } + engine.Pre(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{ + RedirectCode: http.StatusMovedPermanently, + })) + engine.Use(middleware.Recover()) if cfg.Logging.RequestLogging { @@ -68,6 +74,8 @@ func New(cfg *config.Config) { chefService := chef.New(cfg, logger) + cfg.Server.BasePath = normalizeBasePath(cfg.Server.BasePath) + app := AppService{ Log: logger, Chef: chefService, @@ -83,3 +91,12 @@ func New(cfg *config.Config) { app.Log.Fatal("failed to start web server", zap.Error(err)) } } + +// normalizeBasePath cleans and strips trailing slashes from the configured base_path +func normalizeBasePath(p string) string { + p = path.Clean(p) + if p == "." || p == "/" { + return "" + } + return p +} diff --git a/internal/app/ui/manifest.go b/internal/app/ui/manifest.go index 5182c88..02ae9be 100644 --- a/internal/app/ui/manifest.go +++ b/internal/app/ui/manifest.go @@ -48,8 +48,8 @@ func (v *Vite) generateTags() error { return err } } else { - v.HTMLTags = ` -` + v.HTMLTags = ` +` } return nil diff --git a/internal/app/ui/routes.go b/internal/app/ui/routes.go index c7cde66..884ff69 100644 --- a/internal/app/ui/routes.go +++ b/internal/app/ui/routes.go @@ -20,7 +20,10 @@ import ( "go.uber.org/zap" ) -var viteFS = echo.MustSubFS(ui.Embedded, "dist") +var ( + viteFS = echo.MustSubFS(ui.Embedded, "dist") + basePath = "" +) func embeddedFH(config goview.Config, tmpl string) (string, error) { path := filepath.Join(config.Root, tmpl) @@ -42,6 +45,7 @@ func New(config *config.Config, engine *echo.Echo, chef *chef.Service, logger *l log: logger, engine: engine, } + basePath = config.Server.BasePath return &s } @@ -52,7 +56,7 @@ func (s *Service) RegisterRoutes() { disableCache := false if s.config.App.AppMode == "development" { s.log.Warn("development mode enabled! view cache is disabled and templates are not loaded from embed.FS") - templateRoot = "internal/app/ui/templates" + templateRoot = "ui/templates" disableCache = true } @@ -68,7 +72,7 @@ func (s *Service) RegisterRoutes() { vCfg := ViteConfig{ Environment: s.config.App.AppMode, - Base: "/ui", + Base: urlWithBasePath("/ui"), } if s.config.App.AppMode == "production" { @@ -83,6 +87,7 @@ func (s *Service) RegisterRoutes() { viteTags := vite.HTMLTags cfg.Funcs["makeRunListURL"] = s.makeRunListURL + cfg.Funcs["base_path"] = func() string { return basePath } cfg.Funcs["app_version"] = func() string { return version.Get().Version } cfg.Funcs["vite_assets"] = func() template.HTML { return template.HTML(viteTags) @@ -95,8 +100,13 @@ func (s *Service) RegisterRoutes() { s.engine.Renderer = ev + s.engine.GET(urlWithBasePath(""), func(c echo.Context) error { + return c.Redirect(http.StatusFound, urlWithBasePath("/ui/nodes")) + }) + + // Always redirect to base path if somehow bypassed s.engine.GET("/", func(c echo.Context) error { - return c.Redirect(http.StatusFound, "/ui/nodes") + return c.Redirect(http.StatusFound, urlWithBasePath("/ui/nodes")) }) s.engine.RouteNotFound("/*", func(c echo.Context) error { @@ -105,10 +115,10 @@ func (s *Service) RegisterRoutes() { }) }) - router := s.engine.Group("/ui") + router := s.engine.Group(urlWithBasePath("/ui")) { router.GET("/", func(c echo.Context) error { - return c.Redirect(http.StatusFound, "/ui/nodes") + return c.Redirect(http.StatusFound, urlWithBasePath("/ui/nodes")) }) router.GET("/nodes", s.getNodes) router.GET("/nodes/:name", s.getNode) @@ -156,7 +166,7 @@ func CacheControlMiddleware(next echo.HandlerFunc) echo.HandlerFunc { func ViteHandler() echo.HandlerFunc { fs := http.FS(viteFS) - h := http.StripPrefix("/ui", http.FileServer(fs)) + h := http.StripPrefix(urlWithBasePath("/ui"), http.FileServer(fs)) return echo.WrapHandler(h) } @@ -615,3 +625,7 @@ func (s *Service) getPolicyGroup(c echo.Context) error { "title": fmt.Sprintf("Policy groups > %s", name), }) } + +func urlWithBasePath(path string) string { + return basePath + path +} diff --git a/ui/templates/cookbook_recipes.html b/ui/templates/cookbook_recipes.html index 1bc1992..94e6e39 100644 --- a/ui/templates/cookbook_recipes.html +++ b/ui/templates/cookbook_recipes.html @@ -9,7 +9,7 @@

Recipes

diff --git a/ui/templates/cookbooks.html b/ui/templates/cookbooks.html index 7654a67..aeb885b 100644 --- a/ui/templates/cookbooks.html +++ b/ui/templates/cookbooks.html @@ -8,7 +8,7 @@

Cookbooks ({{ len .cookbooks }})

  • {{$versions.Name}} {{range .Versions}} - {{.}} + {{.}} {{end}}
  • {{end}} diff --git a/ui/templates/databag_items.html b/ui/templates/databag_items.html index 939385b..f437da1 100644 --- a/ui/templates/databag_items.html +++ b/ui/templates/databag_items.html @@ -2,7 +2,7 @@

    {{.databag}}

    {{end}} diff --git a/ui/templates/databags.html b/ui/templates/databags.html index 81be576..0223964 100644 --- a/ui/templates/databags.html +++ b/ui/templates/databags.html @@ -2,7 +2,7 @@

    Data Bags ({{ len .databags }})

    {{end}} diff --git a/ui/templates/environment.html b/ui/templates/environment.html index 85068d0..417c026 100644 --- a/ui/templates/environment.html +++ b/ui/templates/environment.html @@ -6,7 +6,7 @@

    {{.environment.Name}}

    {{.environment.Description}}

    - View + View Nodes
    diff --git a/ui/templates/environments.html b/ui/templates/environments.html index 1479f5b..a79df19 100644 --- a/ui/templates/environments.html +++ b/ui/templates/environments.html @@ -2,7 +2,7 @@

    Environments ({{ len .environments }})

    {{end}} diff --git a/ui/templates/layouts/master.html b/ui/templates/layouts/master.html index 06002c1..e2c13ea 100644 --- a/ui/templates/layouts/master.html +++ b/ui/templates/layouts/master.html @@ -51,8 +51,8 @@ {{include "layouts/head"}} - - + + {{include "layouts/nav"}} diff --git a/ui/templates/layouts/nav.html b/ui/templates/layouts/nav.html index 12701e6..88fd480 100644 --- a/ui/templates/layouts/nav.html +++ b/ui/templates/layouts/nav.html @@ -1,7 +1,7 @@