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>
This commit is contained in:
Alexander Rakoczy 2021-11-15 11:41:14 -05:00
Родитель 0a596508df
Коммит 9076251d22
7 изменённых файлов: 115 добавлений и 25 удалений

Просмотреть файл

@ -24,6 +24,7 @@ spec:
- "--"
- "./relui"
- "--listen-https-selfsigned=:444"
- "--base-url=https://build.golang.org/releases"
ports:
- containerPort: 444
env:

Просмотреть файл

@ -9,6 +9,7 @@ import (
"context"
"flag"
"log"
"net/url"
"github.com/jackc/pgx/v4/pgxpool"
"golang.org/x/build/internal/https"
@ -16,9 +17,10 @@ import (
)
var (
pgConnect = flag.String("pg-connect", "", "Postgres connection string or URI. If empty, libpq connection defaults are used.")
migrateOnly = flag.Bool("migrate-only", false, "Exit after running migrations. Migrations are run by default.")
baseURL = flag.String("base-url", "", "Prefix URL for routing and links.")
downUp = flag.Bool("migrate-down-up", false, "Run all Up migration steps, then the last down migration step, followed by the final up migration. Exits after completion.")
migrateOnly = flag.Bool("migrate-only", false, "Exit after running migrations. Migrations are run by default.")
pgConnect = flag.String("pg-connect", "", "Postgres connection string or URI. If empty, libpq connection defaults are used.")
)
func main() {
@ -47,7 +49,14 @@ func main() {
if err := w.ResumeAll(ctx); err != nil {
log.Printf("w.ResumeAll() = %v", err)
}
s := relui.NewServer(db, w)
var base *url.URL
if *baseURL != "" {
base, err = url.Parse(*baseURL)
if err != nil {
log.Fatalf("url.Parse(%q) = %v, %v", *baseURL, base, err)
}
}
s := relui.NewServer(db, w, base)
if err != nil {
log.Fatalf("relui.NewServer() = %v", err)
}

Просмотреть файл

@ -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}}

Просмотреть файл

@ -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">

Просмотреть файл

@ -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>

Просмотреть файл

@ -15,6 +15,7 @@ import (
"log"
"mime"
"net/http"
"net/url"
"path"
"github.com/google/uuid"
@ -41,26 +42,31 @@ func fileServerHandler(fs fs.FS, next http.Handler) http.Handler {
})
}
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 NewServer(p *pgxpool.Pool, w *Worker) *Server {
}
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 @@ func (s *Server) homeHandler(w http.ResponseWriter, r *http.Request) {
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 @@ func (s *Server) newWorkflowHandler(w http.ResponseWriter, r *http.Request) {
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

Просмотреть файл

@ -116,7 +116,7 @@ func TestServerHomeHandler(t *testing.T) {
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 @@ func TestServerNewWorkflowHandler(t *testing.T) {
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 @@ func TestServerCreateWorkflowHandler(t *testing.T) {
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 TestSameUUIDVariant(t *testing.T) {
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)
}
})
}
}