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",
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{
EnvVars: []string{"WOODPECKER_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.StatusContextFormat = c.String("status-context-format")
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.Volumes = c.StringSlice("volume")
server.Config.Pipeline.Privileged = c.StringSlice("escalate")

View file

@ -174,3 +174,5 @@ A [Prometheus endpoint](./90-prometheus.md) is exposed.
## Behind a proxy
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
### `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
StatusContextFormat string
SessionExpires time.Duration
RootURL string
// Open bool
// Orgs map[string]struct{}
// Admins map[string]struct{}

View file

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

View file

@ -18,21 +18,27 @@ import (
"crypto/md5"
"fmt"
"net/http"
"regexp"
"time"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/woodpecker-ci/woodpecker/server"
"github.com/woodpecker-ci/woodpecker/web"
)
// etag is an identifier for a resource version
// 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.
func New() (*gin.Engine, error) {
e := gin.New()
indexHTML = parseIndex()
e.Use(setupCache)
@ -64,15 +70,22 @@ func redirect(location string, status ...int) func(ctx *gin.Context) {
func handleIndex(c *gin.Context) {
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")
if err != nil {
log.Fatal().Err(err).Msg("can not find index.html")
}
rw.Header().Set("Content-Type", "text/html; charset=UTF-8")
rw.WriteHeader(200)
if _, err := rw.Write(data); err != nil {
log.Error().Err(err).Msg("can not write index.html")
if server.Config.Server.RootURL == "" {
return data
}
return regexp.MustCompile(`/\S+\.(js|css|png|svg)`).ReplaceAll(data, []byte(server.Config.Server.RootURL+"$0"))
}
func setupCache(c *gin.Context) {

View file

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

View file

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