web: behavior: use go:embed, re-parse index.html
structural changes: * switch from packr to go:embed! * move many-tiny-handler-packages to single web package behavior change: * add --web-public-dir flag for specifying a live directory to serve instead of the embedded FS. this is set by the dev Dockerfile. * we now parse the index.html template on every request, but there shouldn't be many requests in the first place since the web UI is a single-page app Signed-off-by: Alex Suraci <suraci.alex@gmail.com>
This commit is contained in:
parent
8c05348336
commit
f715c438ee
11
Dockerfile
11
Dockerfile
|
@ -14,13 +14,15 @@ RUN grep '^replace' go.mod || go mod download
|
|||
COPY ./cmd/init/init.c /tmp/init.c
|
||||
RUN gcc -O2 -static -o /usr/local/concourse/bin/init /tmp/init.c && rm /tmp/init.c
|
||||
|
||||
# build Concourse without using 'packr' so that the volume in the next stage
|
||||
# can live-update
|
||||
# copy the rest separately so we don't constantly rebuild init
|
||||
COPY . .
|
||||
|
||||
# build 'concourse' binary
|
||||
RUN go build -gcflags=all="-N -l" -o /usr/local/concourse/bin/concourse \
|
||||
./cmd/concourse
|
||||
RUN set -x && \
|
||||
go build -ldflags '-extldflags "-static"' -o /tmp/fly ./fly && \
|
||||
|
||||
# build 'fly' binary and update web CLI asset
|
||||
RUN go build -ldflags '-extldflags "-static"' -o /tmp/fly ./fly && \
|
||||
tar -C /tmp -czf /usr/local/concourse/fly-assets/fly-$(go env GOOS)-$(go env GOARCH).tgz fly && \
|
||||
rm /tmp/fly
|
||||
|
||||
|
@ -29,3 +31,4 @@ FROM base
|
|||
|
||||
# set up a volume so locally built web UI changes auto-propagate
|
||||
VOLUME /src
|
||||
ENV CONCOURSE_WEB_PUBLIC_DIR=/src/web/public
|
||||
|
|
|
@ -160,6 +160,7 @@ type RunCommand struct {
|
|||
GardenRequestTimeout time.Duration `long:"garden-request-timeout" default:"5m" description:"How long to wait for requests to Garden to complete. 0 means no timeout."`
|
||||
|
||||
CLIArtifactsDir flag.Dir `long:"cli-artifacts-dir" description:"Directory containing downloadable CLI binaries."`
|
||||
WebPublicDir flag.Dir `long:"web-public-dir" description:"Web public/ directory to serve live for local development."`
|
||||
|
||||
Metrics struct {
|
||||
HostName string `long:"metrics-host-name" description:"Host string to attach to emitted metrics."`
|
||||
|
@ -1356,7 +1357,7 @@ func (cmd *RunCommand) oldKey() *encryption.Key {
|
|||
}
|
||||
|
||||
func (cmd *RunCommand) constructWebHandler(logger lager.Logger) (http.Handler, error) {
|
||||
webHandler, err := web.NewHandler(logger)
|
||||
webHandler, err := web.NewHandler(logger, cmd.WebPublicDir.Path())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package publichandler
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
|
@ -1,4 +1,4 @@
|
|||
package publichandler_test
|
||||
package web_test
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
|
@ -11,7 +11,7 @@ import (
|
|||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/concourse/concourse/web/publichandler"
|
||||
"github.com/concourse/concourse/web"
|
||||
)
|
||||
|
||||
var _ = Describe("CacheNearlyForever", func() {
|
||||
|
@ -20,7 +20,7 @@ var _ = Describe("CacheNearlyForever", func() {
|
|||
fmt.Fprint(w, "The wrapped handler was called!")
|
||||
})
|
||||
|
||||
wrappedHandler := publichandler.CacheNearlyForever(insideHandler)
|
||||
wrappedHandler := web.CacheNearlyForever(insideHandler)
|
||||
recorder := httptest.NewRecorder()
|
||||
request, err := http.NewRequest("GET", "/", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
@ -39,7 +39,7 @@ var _ = Describe("CacheNearlyForever", func() {
|
|||
fmt.Fprint(w, strings.Repeat("abc123", 1000))
|
||||
})
|
||||
|
||||
wrappedHandler := publichandler.CacheNearlyForever(insideHandler)
|
||||
wrappedHandler := web.CacheNearlyForever(insideHandler)
|
||||
recorder := httptest.NewRecorder()
|
||||
request, err := http.NewRequest("GET", "/", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
|
@ -1,40 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"code.cloudfoundry.org/lager"
|
||||
"github.com/concourse/concourse/web"
|
||||
"github.com/concourse/concourse/web/proxyhandler"
|
||||
)
|
||||
|
||||
func NewLogger() lager.Logger {
|
||||
logger := lager.NewLogger("web")
|
||||
logger.RegisterSink(lager.NewReconfigurableSink(lager.NewPrettySink(os.Stdout, lager.DEBUG), lager.DEBUG))
|
||||
return logger
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
logger := NewLogger()
|
||||
|
||||
proxyHandler, err := proxyhandler.NewHandler(logger, "http://localhost:8080")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
webHandler, err := web.NewHandler(logger)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
http.Handle("/api/", proxyHandler)
|
||||
http.Handle("/auth/", proxyHandler)
|
||||
http.Handle("/oauth/", proxyHandler)
|
||||
http.Handle("/", webHandler)
|
||||
|
||||
if err = http.ListenAndServe(":8081", nil); err != nil {
|
||||
logger.Error("server-error", err)
|
||||
}
|
||||
}
|
|
@ -1,30 +1,35 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"code.cloudfoundry.org/lager"
|
||||
"github.com/concourse/concourse/web/indexhandler"
|
||||
"github.com/concourse/concourse/web/publichandler"
|
||||
"github.com/concourse/concourse/web/robotshandler"
|
||||
)
|
||||
|
||||
func NewHandler(logger lager.Logger) (http.Handler, error) {
|
||||
indexHandler, err := indexhandler.NewHandler(logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//go:embed public
|
||||
var publicEmbed embed.FS
|
||||
|
||||
publicHandler, err := publichandler.NewHandler()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func NewHandler(logger lager.Logger, livePublicDir string) (http.Handler, error) {
|
||||
var publicFS fs.FS
|
||||
if livePublicDir != "" {
|
||||
publicFS = os.DirFS(livePublicDir)
|
||||
} else {
|
||||
var err error
|
||||
publicFS, err = fs.Sub(publicEmbed, "public")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("public fs sub: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
robotsHandler := robotshandler.NewHandler()
|
||||
|
||||
webMux := http.NewServeMux()
|
||||
webMux.Handle("/public/", publicHandler)
|
||||
webMux.Handle("/robots.txt", robotsHandler)
|
||||
webMux.Handle("/", indexHandler)
|
||||
|
||||
webMux.Handle("/public/", PublicHandler(publicFS))
|
||||
webMux.Handle("/robots.txt", RobotsHandler)
|
||||
webMux.Handle("/", IndexHandler(logger, publicFS))
|
||||
|
||||
return webMux, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"code.cloudfoundry.org/lager"
|
||||
)
|
||||
|
||||
func IndexHandler(logger lager.Logger, publicFS fs.FS) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log := logger.Session("index")
|
||||
|
||||
tfuncs := &indexTemplateFuncs{
|
||||
publicFS: publicFS,
|
||||
assetIDs: map[string]string{},
|
||||
}
|
||||
|
||||
funcs := template.FuncMap{
|
||||
"asset": tfuncs.asset,
|
||||
}
|
||||
|
||||
t, err := template.New("web").Funcs(funcs).ParseFS(publicFS, "index.html")
|
||||
if err != nil {
|
||||
log.Error("failed-to-parse-templates", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = t.ExecuteTemplate(w, "index.html", indexTemplateData{
|
||||
CSRFToken: r.FormValue("csrf_token"),
|
||||
AuthToken: r.Header.Get("Authorization"),
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("failed-to-build-template", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type indexTemplateData struct {
|
||||
CSRFToken string
|
||||
AuthToken string
|
||||
}
|
||||
|
||||
type indexTemplateFuncs struct {
|
||||
publicFS fs.FS
|
||||
|
||||
assetIDs map[string]string
|
||||
assetsL sync.Mutex
|
||||
}
|
||||
|
||||
func (funcs *indexTemplateFuncs) asset(asset string) (string, error) {
|
||||
funcs.assetsL.Lock()
|
||||
defer funcs.assetsL.Unlock()
|
||||
|
||||
id, found := funcs.assetIDs[asset]
|
||||
if !found {
|
||||
hash := md5.New()
|
||||
|
||||
contents, err := fs.ReadFile(funcs.publicFS, asset)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = hash.Write(contents)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
id = fmt.Sprintf("%x", hash.Sum(nil))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("/public/%s?id=%s", asset, id), nil
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
package indexhandler
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"code.cloudfoundry.org/lager"
|
||||
"github.com/gobuffalo/packr"
|
||||
)
|
||||
|
||||
type templateData struct {
|
||||
CSRFToken string
|
||||
AuthToken string
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
logger lager.Logger
|
||||
template *template.Template
|
||||
}
|
||||
|
||||
func NewHandler(logger lager.Logger) (http.Handler, error) {
|
||||
tfuncs := &templateFuncs{
|
||||
assetIDs: map[string]string{},
|
||||
}
|
||||
|
||||
funcs := template.FuncMap{
|
||||
"asset": tfuncs.asset,
|
||||
}
|
||||
|
||||
box := packr.NewBox("../public")
|
||||
|
||||
src, err := box.MustBytes("index.html")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t, err := template.New("index").Funcs(funcs).Parse(string(src))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &handler{
|
||||
logger: logger,
|
||||
template: t,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
log := h.logger.Session("index")
|
||||
|
||||
err := h.template.Execute(w, templateData{
|
||||
CSRFToken: r.FormValue("csrf_token"),
|
||||
AuthToken: r.Header.Get("Authorization"),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("failed-to-build-template", err, lager.Data{})
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
package indexhandler
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/gobuffalo/packr"
|
||||
)
|
||||
|
||||
type templateFuncs struct {
|
||||
assetIDs map[string]string
|
||||
assetsL sync.Mutex
|
||||
}
|
||||
|
||||
func (funcs *templateFuncs) asset(asset string) (string, error) {
|
||||
funcs.assetsL.Lock()
|
||||
defer funcs.assetsL.Unlock()
|
||||
|
||||
box := packr.NewBox("../public")
|
||||
|
||||
id, found := funcs.assetIDs[asset]
|
||||
if !found {
|
||||
hash := md5.New()
|
||||
|
||||
contents, err := box.MustBytes(asset)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = hash.Write(contents)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
id = fmt.Sprintf("%x", hash.Sum(nil))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("/public/%s?id=%s", asset, id), nil
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package proxyhandler
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"code.cloudfoundry.org/lager"
|
||||
)
|
||||
|
||||
func NewHandler(logger lager.Logger, host string) (http.Handler, error) {
|
||||
|
||||
targetUrl, err := url.Parse(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: 24 * time.Hour,
|
||||
KeepAlive: 24 * time.Hour,
|
||||
}
|
||||
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: dialer.Dial,
|
||||
TLSHandshakeTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
handler := httputil.NewSingleHostReverseProxy(targetUrl)
|
||||
handler.FlushInterval = 100 * time.Millisecond
|
||||
handler.Transport = transport
|
||||
|
||||
return handler, nil
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func PublicHandler(publicFS fs.FS) http.Handler {
|
||||
return CacheNearlyForever(
|
||||
http.StripPrefix(
|
||||
"/public/",
|
||||
http.FileServer(http.FS(publicFS)),
|
||||
),
|
||||
)
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package publichandler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gobuffalo/packr"
|
||||
)
|
||||
|
||||
func NewHandler() (http.Handler, error) {
|
||||
return CacheNearlyForever(http.StripPrefix("/public/", http.FileServer(packr.NewBox("../public")))), nil
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var RobotsHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "User-agent: *")
|
||||
fmt.Fprintln(w, "Disallow: /")
|
||||
})
|
|
@ -1,17 +0,0 @@
|
|||
package robotshandler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func NewHandler() http.Handler {
|
||||
return &handler{}
|
||||
}
|
||||
|
||||
type handler struct{}
|
||||
|
||||
func (self *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "User-agent: *")
|
||||
fmt.Fprintln(w, "Disallow: /")
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
package publichandler_test
|
||||
package web_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPublichandler(t *testing.T) {
|
||||
func TestWebHandler(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Public Handler Suite")
|
||||
RunSpecs(t, "Web Handler Suite")
|
||||
}
|
Loading…
Reference in New Issue