Skip to content

Commit

Permalink
server: push serveUIAssets into package ui
Browse files Browse the repository at this point in the history
Better separate concerns by making package ui responsible for
providing an HTTP handler that can serve its assets. As a side effect of
the refactor, fix rendering of the "no UI installed" page in short
binaries, which was broken in cockroachdb#25195.

This is not pure code movement, so ui.Handler should be reviewed as if
it were new code.

Release note: None
  • Loading branch information
benesch committed Sep 13, 2018
1 parent d1d666c commit 9333e4f
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 97 deletions.
9 changes: 9 additions & 0 deletions pkg/build/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ func (b Info) Short() string {
b.Distribution, b.Tag, plat, b.Time, b.GoVersion)
}

// GoTime parses the utcTime string and returns a time.Time.
func (b Info) GoTime() time.Time {
val, err := time.Parse(TimeFormat, b.Time)
if err != nil {
return time.Time{}
}
return val
}

// Timestamp parses the utcTime string and returns the number of seconds since epoch.
func (b Info) Timestamp() (int64, error) {
val, err := time.Parse(TimeFormat, b.Time)
Expand Down
59 changes: 10 additions & 49 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ import (
"compress/gzip"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"html/template"
"io"
"io/ioutil"
"math"
Expand All @@ -33,7 +31,6 @@ import (
"sync/atomic"
"time"

assetfs "github.com/elazarl/go-bindata-assetfs"
raven "github.com/getsentry/raven-go"
gwruntime "github.com/grpc-ecosystem/grpc-gateway/runtime"
opentracing "github.com/opentracing/opentracing-go"
Expand All @@ -42,7 +39,6 @@ import (

"github.com/cockroachdb/cmux"
"github.com/cockroachdb/cockroach/pkg/base"
"github.com/cockroachdb/cockroach/pkg/build"
"github.com/cockroachdb/cockroach/pkg/gossip"
"github.com/cockroachdb/cockroach/pkg/internal/client"
"github.com/cockroachdb/cockroach/pkg/jobs"
Expand Down Expand Up @@ -1236,17 +1232,19 @@ func (s *Server) Start(ctx context.Context) error {
// endpoints.
s.mux.Handle(debug.Endpoint, debug.NewServer(s.st))

fileServer := http.FileServer(&assetfs.AssetFS{
Asset: ui.Asset,
AssetDir: ui.AssetDir,
AssetInfo: ui.AssetInfo,
})

// Serve UI assets. This needs to be before the gRPC handlers are registered, otherwise
// the `s.mux.Handle("/", ...)` would cover all URLs, allowing anonymous access.
maybeAuthMux := newAuthenticationMuxAllowAnonymous(
s.authentication, serveUIAssets(fileServer, s.cfg),
)
s.authentication, ui.Handler(ui.Config{
ExperimentalUseLogin: s.cfg.EnableWebSessionAuthentication,
LoginEnabled: s.cfg.RequireWebSession(),
GetUser: func(ctx context.Context) *string {
if u, ok := ctx.Value(webSessionUserKey{}).(string); ok {
return &u
}
return nil
},
}))
s.mux.Handle("/", maybeAuthMux)

// Initialize grpc-gateway mux and context in order to get the /health
Expand Down Expand Up @@ -1951,43 +1949,6 @@ func (w *gzipResponseWriter) Close() error {
return err
}

func serveUIAssets(fileServer http.Handler, cfg Config) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.URL.Path != "/" {
fileServer.ServeHTTP(writer, request)
return
}

// Construct arguments for template.
tmplArgs := ui.IndexHTMLArgs{
ExperimentalUseLogin: cfg.EnableWebSessionAuthentication,
LoginEnabled: cfg.RequireWebSession(),
Tag: build.GetInfo().Tag,
Version: build.VersionPrefix(),
}
loggedInUser, ok := request.Context().Value(webSessionUserKey{}).(string)
if ok && loggedInUser != "" {
tmplArgs.LoggedInUser = &loggedInUser
}

argsJSON, err := json.Marshal(tmplArgs)
if err != nil {
http.Error(writer, err.Error(), 500)
return
}

// Execute the template.
writer.Header().Add("Content-Type", "text/html")
if err := ui.IndexHTMLTemplate.Execute(writer, map[string]template.JS{
"DataFromServer": template.JS(string(argsJSON)),
}); err != nil {
wrappedErr := errors.Wrap(err, "templating index.html")
http.Error(writer, wrappedErr.Error(), 500)
log.Error(request.Context(), wrappedErr)
}
})
}

func init() {
tracing.RegisterTagRemapping("n", "node")
}
89 changes: 66 additions & 23 deletions pkg/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"testing"
Expand All @@ -43,6 +44,7 @@ import (
"github.com/cockroachdb/cockroach/pkg/storage/engine"
"github.com/cockroachdb/cockroach/pkg/testutils"
"github.com/cockroachdb/cockroach/pkg/testutils/serverutils"
"github.com/cockroachdb/cockroach/pkg/ui"
"github.com/cockroachdb/cockroach/pkg/util/hlc"
"github.com/cockroachdb/cockroach/pkg/util/httputil"
"github.com/cockroachdb/cockroach/pkg/util/leaktest"
Expand Down Expand Up @@ -858,6 +860,17 @@ func TestServeIndexHTML(t *testing.T) {
</html>
`

linkInFakeUI := func() {
ui.Asset = func(string) (_ []byte, _ error) { return }
ui.AssetDir = func(name string) (_ []string, _ error) { return }
ui.AssetInfo = func(name string) (_ os.FileInfo, _ error) { return }
}
unlinkFakeUI := func() {
ui.Asset = nil
ui.AssetDir = nil
ui.AssetInfo = nil
}

t.Run("Insecure mode", func(t *testing.T) {
s, _, _ := serverutils.StartServer(t, base.TestServerArgs{
Insecure: true,
Expand All @@ -874,32 +887,62 @@ func TestServeIndexHTML(t *testing.T) {
t.Fatal(err)
}

resp, err := client.Get(s.AdminURL())
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 200 {
t.Fatalf("expected status code 200; got %d", resp.StatusCode)
}
respBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
respString := string(respBytes)
expected := fmt.Sprintf(
htmlTemplate,
fmt.Sprintf(
`{"ExperimentalUseLogin":false,"LoginEnabled":false,"LoggedInUser":null,"Tag":"%s","Version":"%s"}`,
build.GetInfo().Tag,
build.VersionPrefix(),
),
)
if respString != expected {
t.Fatalf("expected %s; got %s", expected, respString)
}
t.Run("short build", func(t *testing.T) {
resp, err := client.Get(s.AdminURL())
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 200 {
t.Fatalf("expected status code 200; got %d", resp.StatusCode)
}
respBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
respString := string(respBytes)
expected := fmt.Sprintf(`<!DOCTYPE html>
<title>CockroachDB</title>
Binary built without web UI.
<hr>
<em>%s</em>`,
build.GetInfo().Short())
if respString != expected {
t.Fatalf("expected %s; got %s", expected, respString)
}
})

t.Run("non-short build", func(t *testing.T) {
linkInFakeUI()
defer unlinkFakeUI()
resp, err := client.Get(s.AdminURL())
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 200 {
t.Fatalf("expected status code 200; got %d", resp.StatusCode)
}
respBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
respString := string(respBytes)
expected := fmt.Sprintf(
htmlTemplate,
fmt.Sprintf(
`{"ExperimentalUseLogin":false,"LoginEnabled":false,"LoggedInUser":null,"Tag":"%s","Version":"%s"}`,
build.GetInfo().Tag,
build.VersionPrefix(),
),
)
if respString != expected {
t.Fatalf("expected %s; got %s", expected, respString)
}
})
})

t.Run("Secure mode", func(t *testing.T) {
linkInFakeUI()
defer unlinkFakeUI()
s, _, _ := serverutils.StartServer(t, base.TestServerArgs{})
defer s.Stopper().Stop(context.TODO())
tsrv := s.(*TestServer)
Expand Down
94 changes: 69 additions & 25 deletions pkg/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,22 @@
package ui

import (
"bytes"
"context"
"fmt"
"html/template"
"net/http"
"os"

"github.com/cockroachdb/cockroach/pkg/build"
"github.com/cockroachdb/cockroach/pkg/util/log"
assetfs "github.com/elazarl/go-bindata-assetfs"
"github.com/pkg/errors"
)

var indexHTML = []byte(fmt.Sprintf(`<!DOCTYPE html>
<title>CockroachDB</title>
Binary built without web UI.
<hr>
<em>%s</em>`, build.GetInfo().Short()))

// Asset loads and returns the asset for the given name. It returns an error if
// the asset could not be found or could not be loaded.
var Asset = func(name string) ([]byte, error) {
if name == "index.html" {
return indexHTML, nil
}
return nil, os.ErrNotExist
}
var Asset func(name string) ([]byte, error)

// AssetDir returns the file names below a certain directory in the embedded
// filesystem.
Expand All @@ -58,21 +53,22 @@ var Asset = func(name string) ([]byte, error) {
// AssetDir("data") returns []string{"foo.txt", "img"}
// AssetDir("data/img") returns []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") return errors
var AssetDir = func(name string) ([]string, error) {
if name == "" {
return []string{"index.html"}, nil
}
return nil, os.ErrNotExist
}
var AssetDir func(name string) ([]string, error)

// AssetInfo loads and returns metadata for the asset with the given name. It
// returns an error if the asset could not be found or could not be loaded.
var AssetInfo func(name string) (os.FileInfo, error)

// IndexHTMLTemplate takes arguments about the current session and returns HTML which
// includes the UI JavaScript bundles, plus a script tag which sets the currently logged in user
// so that the UI JavaScript can decide whether to show a login page.
var IndexHTMLTemplate = template.Must(template.New("index").Parse(`<!DOCTYPE html>
// haveUI returns whether the admin UI has been linked into the binary.
func haveUI() bool {
return Asset != nil && AssetDir != nil && AssetInfo != nil
}

// indexTemplate takes arguments about the current session and returns HTML
// which includes the UI JavaScript bundles, plus a script tag which sets the
// currently logged in user so that the UI JavaScript can decide whether to show
// a login page.
var indexHTMLTemplate = template.Must(template.New("index").Parse(`<!DOCTYPE html>
<html>
<head>
<title>Cockroach Console</title>
Expand All @@ -83,7 +79,7 @@ var IndexHTMLTemplate = template.Must(template.New("index").Parse(`<!DOCTYPE htm
<div id="react-layout"></div>
<script>
window.dataFromServer = {{ .DataFromServer }};
window.dataFromServer = {{.}};
</script>
<script src="protos.dll.js" type="text/javascript"></script>
Expand All @@ -93,11 +89,59 @@ var IndexHTMLTemplate = template.Must(template.New("index").Parse(`<!DOCTYPE htm
</html>
`))

// IndexHTMLArgs are the arguments to IndexHTMLTemplate.
type IndexHTMLArgs struct {
type indexHTMLArgs struct {
ExperimentalUseLogin bool
LoginEnabled bool
LoggedInUser *string
Tag string
Version string
}

// bareIndexHTML is used in place of indexHTMLTemplate when the binary is built
// without the web UI.
var bareIndexHTML = []byte(fmt.Sprintf(`<!DOCTYPE html>
<title>CockroachDB</title>
Binary built without web UI.
<hr>
<em>%s</em>`, build.GetInfo().Short()))

// Config contains the configuration parameters for Handler.
type Config struct {
ExperimentalUseLogin bool
LoginEnabled bool
GetUser func(ctx context.Context) *string
}

// Handler returns an http.Handler that serves the UI.
func Handler(cfg Config) http.Handler {
fileServer := http.FileServer(&assetfs.AssetFS{
Asset: Asset,
AssetDir: AssetDir,
AssetInfo: AssetInfo,
})
buildInfo := build.GetInfo()

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !haveUI() {
http.ServeContent(w, r, "index.html", buildInfo.GoTime(), bytes.NewReader(bareIndexHTML))
return
}

if r.URL.Path != "/" {
fileServer.ServeHTTP(w, r)
return
}

if err := indexHTMLTemplate.Execute(w, indexHTMLArgs{
ExperimentalUseLogin: cfg.ExperimentalUseLogin,
LoginEnabled: cfg.LoginEnabled,
LoggedInUser: cfg.GetUser(r.Context()),
Tag: buildInfo.Tag,
Version: build.VersionPrefix(),
}); err != nil {
err = errors.Wrap(err, "templating index.html")
http.Error(w, err.Error(), 500)
log.Error(r.Context(), err)
}
})
}

0 comments on commit 9333e4f

Please sign in to comment.