зеркало из https://github.com/golang/build.git
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:
Родитель
0a596508df
Коммит
9076251d22
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче