Support path prefix (#1714)

closes #1636 
closes #1429
supersedes #1586

Uses a different approach: just take the index.html compiled by vite and
replace the paths to js and other files using regex. This is not
compatible with the dev proxy which is also the reason why we can't use
go templates for this.
This commit is contained in:
qwerty287 2023-04-29 17:51:50 +02:00 committed by GitHub
parent 4384344c22
commit b90e7904a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 46 additions and 11 deletions

View file

@ -45,6 +45,11 @@ var flags = []cli.Flag{
Name: "server-host", Name: "server-host",
Usage: "server fully qualified url (<scheme>://<host>)", Usage: "server fully qualified url (<scheme>://<host>)",
}, },
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_ROOT_URL"},
Name: "root-url",
Usage: "server url root (used for statics loading when having a url path prefix)",
},
&cli.StringFlag{ &cli.StringFlag{
EnvVars: []string{"WOODPECKER_SERVER_ADDR"}, EnvVars: []string{"WOODPECKER_SERVER_ADDR"},
Name: "server-addr", Name: "server-addr",

View file

@ -354,6 +354,7 @@ func setupEvilGlobals(c *cli.Context, v store.Store, f forge.Forge) {
server.Config.Server.StatusContext = c.String("status-context") server.Config.Server.StatusContext = c.String("status-context")
server.Config.Server.StatusContextFormat = c.String("status-context-format") server.Config.Server.StatusContextFormat = c.String("status-context-format")
server.Config.Server.SessionExpires = c.Duration("session-expires") server.Config.Server.SessionExpires = c.Duration("session-expires")
server.Config.Server.RootURL = strings.TrimSuffix(c.String("root-url"), "/")
server.Config.Pipeline.Networks = c.StringSlice("network") server.Config.Pipeline.Networks = c.StringSlice("network")
server.Config.Pipeline.Volumes = c.StringSlice("volume") server.Config.Pipeline.Volumes = c.StringSlice("volume")
server.Config.Pipeline.Privileged = c.StringSlice("escalate") server.Config.Pipeline.Privileged = c.StringSlice("escalate")

View file

@ -174,3 +174,5 @@ A [Prometheus endpoint](./90-prometheus.md) is exposed.
## Behind a proxy ## Behind a proxy
See the [proxy guide](./70-proxy.md) if you want to see a setup behind Apache, Nginx, Caddy or ngrok. See the [proxy guide](./70-proxy.md) if you want to see a setup behind Apache, Nginx, Caddy or ngrok.
In the case you need to use Woodpecker with a URL path prefix (like: https://example.org/woodpecker/), you can use the option [`WOODPECKER_ROOT_URL`](./10-server-config.md#woodpecker_root_url).

View file

@ -404,6 +404,12 @@ Specify a configuration service endpoint, see [Configuration Extension](./100-ex
Specify how many seconds before timeout when fetching the Woodpecker configuration from a Forge Specify how many seconds before timeout when fetching the Woodpecker configuration from a Forge
### `WOODPECKER_ROOT_URL`
> Default: ``
Server URL path prefix (used for statics loading when having a url path prefix), should start with `/`
Example: `WOODPECKER_ROOT_URL=/woodpecker`
--- ---

View file

@ -66,6 +66,7 @@ var Config = struct {
StatusContext string StatusContext string
StatusContextFormat string StatusContextFormat string
SessionExpires time.Duration SessionExpires time.Duration
RootURL string
// Open bool // Open bool
// Orgs map[string]struct{} // Orgs map[string]struct{}
// Admins map[string]struct{} // Admins map[string]struct{}

View file

@ -45,6 +45,7 @@ func Config(c *gin.Context) {
"docs": server.Config.Server.Docs, "docs": server.Config.Server.Docs,
"version": version.String(), "version": version.String(),
"forge": server.Config.Services.Forge.Name(), "forge": server.Config.Services.Forge.Name(),
"root_url": server.Config.Server.RootURL,
} }
// default func map with json parser. // default func map with json parser.
@ -61,7 +62,10 @@ func Config(c *gin.Context) {
if err := tmpl.Execute(c.Writer, configData); err != nil { if err := tmpl.Execute(c.Writer, configData); err != nil {
log.Error().Err(err).Msgf("could not execute template") log.Error().Err(err).Msgf("could not execute template")
c.AbortWithStatus(http.StatusInternalServerError) c.AbortWithStatus(http.StatusInternalServerError)
return
} }
c.Status(http.StatusOK)
} }
const configTemplate = ` const configTemplate = `
@ -70,4 +74,5 @@ window.WOODPECKER_CSRF = "{{ .csrf }}";
window.WOODPECKER_VERSION = "{{ .version }}"; window.WOODPECKER_VERSION = "{{ .version }}";
window.WOODPECKER_DOCS = "{{ .docs }}"; window.WOODPECKER_DOCS = "{{ .docs }}";
window.WOODPECKER_FORGE = "{{ .forge }}"; window.WOODPECKER_FORGE = "{{ .forge }}";
window.WOODPECKER_ROOT_URL = "{{ .root_url }}";
` `

View file

@ -18,21 +18,27 @@ import (
"crypto/md5" "crypto/md5"
"fmt" "fmt"
"net/http" "net/http"
"regexp"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/woodpecker-ci/woodpecker/server"
"github.com/woodpecker-ci/woodpecker/web" "github.com/woodpecker-ci/woodpecker/web"
) )
// etag is an identifier for a resource version // etag is an identifier for a resource version
// it lets caches determine if resource is still the same and not send it again // it lets caches determine if resource is still the same and not send it again
var etag = fmt.Sprintf("%x", md5.Sum([]byte(time.Now().String()))) var (
etag = fmt.Sprintf("%x", md5.Sum([]byte(time.Now().String())))
indexHTML []byte
)
// New returns a gin engine to serve the web frontend. // New returns a gin engine to serve the web frontend.
func New() (*gin.Engine, error) { func New() (*gin.Engine, error) {
e := gin.New() e := gin.New()
indexHTML = parseIndex()
e.Use(setupCache) e.Use(setupCache)
@ -64,15 +70,22 @@ func redirect(location string, status ...int) func(ctx *gin.Context) {
func handleIndex(c *gin.Context) { func handleIndex(c *gin.Context) {
rw := c.Writer rw := c.Writer
rw.Header().Set("Content-Type", "text/html; charset=UTF-8")
rw.WriteHeader(http.StatusOK)
if _, err := rw.Write(indexHTML); err != nil {
log.Error().Err(err).Msg("can not write index.html")
}
}
func parseIndex() []byte {
data, err := web.Lookup("index.html") data, err := web.Lookup("index.html")
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("can not find index.html") log.Fatal().Err(err).Msg("can not find index.html")
} }
rw.Header().Set("Content-Type", "text/html; charset=UTF-8") if server.Config.Server.RootURL == "" {
rw.WriteHeader(200) return data
if _, err := rw.Write(data); err != nil {
log.Error().Err(err).Msg("can not write index.html")
} }
return regexp.MustCompile(`/\S+\.(js|css|png|svg)`).ReplaceAll(data, []byte(server.Config.Server.RootURL+"$0"))
} }
func setupCache(c *gin.Context) { func setupCache(c *gin.Context) {

View file

@ -7,7 +7,7 @@ let apiClient: WoodpeckerClient | undefined;
export default (): WoodpeckerClient => { export default (): WoodpeckerClient => {
if (!apiClient) { if (!apiClient) {
const config = useConfig(); const config = useConfig();
const server = ''; const server = config.rootURL ?? '';
const token = null; const token = null;
const csrf = config.csrf || null; const csrf = config.csrf || null;

View file

@ -7,6 +7,7 @@ declare global {
WOODPECKER_VERSION: string | undefined; WOODPECKER_VERSION: string | undefined;
WOODPECKER_CSRF: string | undefined; WOODPECKER_CSRF: string | undefined;
WOODPECKER_FORGE: string | undefined; WOODPECKER_FORGE: string | undefined;
WOODPECKER_ROOT_URL: string | undefined;
} }
} }
@ -16,4 +17,5 @@ export default () => ({
version: window.WOODPECKER_VERSION, version: window.WOODPECKER_VERSION,
csrf: window.WOODPECKER_CSRF || null, csrf: window.WOODPECKER_CSRF || null,
forge: window.WOODPECKER_FORGE || null, forge: window.WOODPECKER_FORGE || null,
rootURL: window.WOODPECKER_ROOT_URL || null,
}); });