cmd/relui,internal/relui: serve from sub-paths
This change allows relui to correctly serve from a path, like
build.golang.org/releases. It adds a base-url flag which is used to
prefix all paths referenced in the application.
For golang/go#47401
Change-Id: Ib8f6fe429591ceabfaf0f419e5258a677b375ff8
Reviewed-on: https://go-review.googlesource.com/c/build/+/363975
Trust: Alexander Rakoczy <alex@golang.org>
Run-TryBot: Alexander Rakoczy <alex@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/internal/relui/templates/home.html b/internal/relui/templates/home.html
index cd8f73c..ebd946d 100644
--- a/internal/relui/templates/home.html
+++ b/internal/relui/templates/home.html
@@ -7,7 +7,7 @@
<section class="Workflows">
<div class="Workflows-header">
<h2>Workflows</h2>
- <a href="/workflows/new" class="Button">New</a>
+ <a href="{{baseLink "/workflows/new"}}" class="Button">New</a>
</div>
<ul class="WorkflowList">
{{range $workflow := .Workflows}}
diff --git a/internal/relui/templates/layout.html b/internal/relui/templates/layout.html
index bf0a4aa..69f6ec1 100644
--- a/internal/relui/templates/layout.html
+++ b/internal/relui/templates/layout.html
@@ -7,7 +7,7 @@
<html lang="en">
<title>Go Releases</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
- <link rel="stylesheet" href="/static/styles.css" />
+ <link rel="stylesheet" href="{{baseLink "/static/styles.css"}}" />
<body class="Site">
<header class="Site-header">
<div class="Header">
diff --git a/internal/relui/templates/new_workflow.html b/internal/relui/templates/new_workflow.html
index bdcf547..756cd5f 100644
--- a/internal/relui/templates/new_workflow.html
+++ b/internal/relui/templates/new_workflow.html
@@ -6,7 +6,7 @@
{{define "content"}}
<section class="NewWorkflow">
<h2>New Go Release</h2>
- <form action="/workflows/new" method="get">
+ <form action="{{baseLink "/workflows/new"}}" method="get">
<label for="workflow.name">Workflow:</label>
<select id="workflow.name" name="workflow.name" onchange="this.form.submit()">
<option value="">Select Workflow</option>
diff --git a/internal/relui/web.go b/internal/relui/web.go
index 0536cd1..0565d89 100644
--- a/internal/relui/web.go
+++ b/internal/relui/web.go
@@ -15,6 +15,7 @@
"log"
"mime"
"net/http"
+ "net/url"
"path"
"github.com/google/uuid"
@@ -41,26 +42,31 @@
})
}
-var (
- homeTmpl = template.Must(template.Must(layoutTmpl.Clone()).ParseFS(templates, "templates/home.html"))
- layoutTmpl = template.Must(template.ParseFS(templates, "templates/layout.html"))
- newWorkflowTmpl = template.Must(template.Must(layoutTmpl.Clone()).ParseFS(templates, "templates/new_workflow.html"))
-)
-
// Server implements the http handlers for relui.
type Server struct {
- db *pgxpool.Pool
- m *http.ServeMux
- w *Worker
+ db *pgxpool.Pool
+ m *http.ServeMux
+ w *Worker
+ baseURL *url.URL
+
+ homeTmpl *template.Template
+ newWorkflowTmpl *template.Template
}
// NewServer initializes a server with the provided connection pool.
-func NewServer(p *pgxpool.Pool, w *Worker) *Server {
+func NewServer(p *pgxpool.Pool, w *Worker, baseURL *url.URL) *Server {
s := &Server{
- db: p,
- m: new(http.ServeMux),
- w: w,
+ db: p,
+ m: new(http.ServeMux),
+ w: w,
+ baseURL: baseURL,
}
+ helpers := map[string]interface{}{
+ "baseLink": s.BaseLink,
+ }
+ layout := template.Must(template.New("layout.html").Funcs(helpers).ParseFS(templates, "templates/layout.html"))
+ s.homeTmpl = template.Must(template.Must(layout.Clone()).Funcs(helpers).ParseFS(templates, "templates/home.html"))
+ s.newWorkflowTmpl = template.Must(template.Must(layout.Clone()).Funcs(helpers).ParseFS(templates, "templates/new_workflow.html"))
s.m.Handle("/workflows/create", http.HandlerFunc(s.createWorkflowHandler))
s.m.Handle("/workflows/new", http.HandlerFunc(s.newWorkflowHandler))
s.m.Handle("/", fileServerHandler(static, http.HandlerFunc(s.homeHandler)))
@@ -68,13 +74,35 @@
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- s.m.ServeHTTP(w, r)
+ if s.baseURL == nil || s.baseURL.Path == "/" {
+ s.m.ServeHTTP(w, r)
+ return
+ }
+ http.StripPrefix(s.baseURL.Path, s.m)
}
func (s *Server) Serve(port string) error {
return http.ListenAndServe(":"+port, s.m)
}
+func (s *Server) BaseLink(target string) string {
+ if s.baseURL == nil {
+ return target
+ }
+ u, err := url.Parse(target)
+ if err != nil {
+ log.Printf("BaseLink: url.Parse(%q) = %v, %v", target, u, err)
+ return target
+ }
+ if u.IsAbs() {
+ return u.String()
+ }
+ u.Scheme = s.baseURL.Scheme
+ u.Host = s.baseURL.Host
+ u.Path = path.Join(s.baseURL.Path, u.Path)
+ return u.String()
+}
+
type homeResponse struct {
Workflows []db.Workflow
WorkflowTasks map[uuid.UUID][]db.Task
@@ -104,7 +132,7 @@
return
}
out := bytes.Buffer{}
- if err := homeTmpl.Execute(&out, resp); err != nil {
+ if err := s.homeTmpl.Execute(&out, resp); err != nil {
log.Printf("homeHandler: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
@@ -156,7 +184,7 @@
Definitions: Definitions(),
Name: r.FormValue("workflow.name"),
}
- if err := newWorkflowTmpl.Execute(&out, resp); err != nil {
+ if err := s.newWorkflowTmpl.Execute(&out, resp); err != nil {
log.Printf("newWorkflowHandler: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
diff --git a/internal/relui/web_test.go b/internal/relui/web_test.go
index a968410..52dfe92 100644
--- a/internal/relui/web_test.go
+++ b/internal/relui/web_test.go
@@ -116,7 +116,7 @@
req := httptest.NewRequest(http.MethodGet, "/", nil)
w := httptest.NewRecorder()
- s := NewServer(p, NewWorker(p, &PGListener{p}))
+ s := NewServer(p, NewWorker(p, &PGListener{p}), nil)
s.homeHandler(w, req)
resp := w.Result()
@@ -152,7 +152,7 @@
req := httptest.NewRequest(http.MethodGet, u.String(), nil)
w := httptest.NewRecorder()
- s := &Server{}
+ s := NewServer(nil, nil, nil)
s.newWorkflowHandler(w, req)
resp := w.Result()
@@ -219,7 +219,7 @@
rec := httptest.NewRecorder()
q := db.New(p)
- s := NewServer(p, NewWorker(p, &PGListener{p}))
+ s := NewServer(p, NewWorker(p, &PGListener{p}), nil)
s.createWorkflowHandler(rec, req)
resp := rec.Result()
@@ -368,3 +368,55 @@
func nullString(val string) sql.NullString {
return sql.NullString{String: val, Valid: true}
}
+
+func TestServerBaseLink(t *testing.T) {
+ cases := []struct {
+ desc string
+ baseURL string
+ target string
+ want string
+ }{
+ {
+ desc: "no baseURL, relative",
+ target: "/workflows",
+ want: "/workflows",
+ },
+ {
+ desc: "no baseURL, absolute",
+ target: "https://example.test/something",
+ want: "https://example.test/something",
+ },
+ {
+ desc: "absolute baseURL, relative",
+ baseURL: "https://example.test/releases",
+ target: "/workflows",
+ want: "https://example.test/releases/workflows",
+ },
+ {
+ desc: "relative baseURL, relative",
+ baseURL: "/releases",
+ target: "/workflows",
+ want: "/releases/workflows",
+ },
+ {
+ desc: "absolute baseURL, absolute",
+ baseURL: "https://example.test/releases",
+ target: "https://example.test/something",
+ want: "https://example.test/something",
+ },
+ }
+ for _, c := range cases {
+ t.Run(c.desc, func(t *testing.T) {
+ base, err := url.Parse(c.baseURL)
+ if err != nil {
+ t.Fatalf("url.Parse(%q) = %v, %v, wanted no error", c.baseURL, base, err)
+ }
+ s := NewServer(nil, nil, base)
+
+ got := s.BaseLink(c.target)
+ if got != c.want {
+ t.Errorf("s.BaseLink(%q) = %q, wanted %q", c.target, got, c.want)
+ }
+ })
+ }
+}