mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-01-02 21:58:43 +00:00
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:
parent
4384344c22
commit
b90e7904a5
9 changed files with 46 additions and 11 deletions
|
@ -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",
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -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{}
|
||||||
|
|
|
@ -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 }}";
|
||||||
`
|
`
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue