177 lines
5.1 KiB
Go
177 lines
5.1 KiB
Go
|
package uiserver
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"io/ioutil"
|
||
|
"net/http"
|
||
|
"os"
|
||
|
"regexp"
|
||
|
"strings"
|
||
|
"sync/atomic"
|
||
|
"text/template"
|
||
|
|
||
|
"github.com/hashicorp/consul/agent/config"
|
||
|
"github.com/hashicorp/consul/logging"
|
||
|
"github.com/hashicorp/go-hclog"
|
||
|
)
|
||
|
|
||
|
// Handler is the http.Handler that serves the Consul UI. It may serve from the
|
||
|
// compiled-in AssetFS or from and external dir. It provides a few important
|
||
|
// transformations on the index.html file and includes a proxy for metrics
|
||
|
// backends.
|
||
|
type Handler struct {
|
||
|
// state is a reloadableState struct accessed through an atomic value to make
|
||
|
// it safe to reload at run time. Each call to ServeHTTP will see the latest
|
||
|
// version of the state without internal locking needed.
|
||
|
state atomic.Value
|
||
|
logger hclog.Logger
|
||
|
}
|
||
|
|
||
|
// reloadableState encapsulates all the state that might be modified during
|
||
|
// ReloadConfig.
|
||
|
type reloadableState struct {
|
||
|
cfg *config.UIConfig
|
||
|
srv http.Handler
|
||
|
err error
|
||
|
}
|
||
|
|
||
|
// NewHandler returns a Handler that can be used to serve UI http requests. It
|
||
|
// accepts a full agent config since properties like ACLs being enabled affect
|
||
|
// the UI so we need more than just UIConfig parts.
|
||
|
func NewHandler(agentCfg *config.RuntimeConfig, logger hclog.Logger) *Handler {
|
||
|
h := &Handler{
|
||
|
logger: logger.Named(logging.UIServer),
|
||
|
}
|
||
|
// Don't return the error since this is likely the result of a
|
||
|
// misconfiguration and reloading config could fix it. Instead we'll capture
|
||
|
// it and return an error for all calls to ServeHTTP so the misconfiguration
|
||
|
// is visible. Sadly we can't log effectively
|
||
|
if err := h.ReloadConfig(agentCfg); err != nil {
|
||
|
h.state.Store(reloadableState{
|
||
|
err: err,
|
||
|
})
|
||
|
}
|
||
|
return h
|
||
|
}
|
||
|
|
||
|
// ServeHTTP implements http.Handler and serves UI HTTP requests
|
||
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||
|
// TODO: special case for compiled metrics assets in later PR
|
||
|
s := h.getState()
|
||
|
if s == nil {
|
||
|
panic("nil state")
|
||
|
}
|
||
|
if s.err != nil {
|
||
|
http.Error(w, "UI server is misconfigured.", http.StatusInternalServerError)
|
||
|
h.logger.Error("Failed to configure UI server: %s", s.err)
|
||
|
return
|
||
|
}
|
||
|
s.srv.ServeHTTP(w, r)
|
||
|
}
|
||
|
|
||
|
// ReloadConfig is called by the agent when the configuration is reloaded and
|
||
|
// updates the UIConfig values the handler uses to serve requests.
|
||
|
func (h *Handler) ReloadConfig(newCfg *config.RuntimeConfig) error {
|
||
|
newState := reloadableState{
|
||
|
cfg: &newCfg.UIConfig,
|
||
|
}
|
||
|
|
||
|
var fs http.FileSystem
|
||
|
|
||
|
if newCfg.UIConfig.Dir == "" {
|
||
|
// Serve from assetFS
|
||
|
fs = assetFS()
|
||
|
} else {
|
||
|
fs = http.Dir(newCfg.UIConfig.Dir)
|
||
|
}
|
||
|
|
||
|
// Render a new index.html with the new config values ready to serve.
|
||
|
buf, info, err := renderIndex(newCfg, fs)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Create a new fs that serves the rendered index file or falls back to the
|
||
|
// underlying FS.
|
||
|
fs = &bufIndexFS{
|
||
|
fs: fs,
|
||
|
indexRendered: buf,
|
||
|
indexInfo: info,
|
||
|
}
|
||
|
|
||
|
// Wrap the buffering FS our redirect FS. This needs to happen later so that
|
||
|
// redirected requests for /index.html get served the rendered version not the
|
||
|
// original.
|
||
|
fs = &redirectFS{fs: fs}
|
||
|
newState.srv = http.FileServer(fs)
|
||
|
|
||
|
// Store the new state
|
||
|
h.state.Store(newState)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// getState is a helper to access the atomic internal state
|
||
|
func (h *Handler) getState() *reloadableState {
|
||
|
if cfg, ok := h.state.Load().(reloadableState); ok {
|
||
|
return &cfg
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func renderIndex(cfg *config.RuntimeConfig, fs http.FileSystem) ([]byte, os.FileInfo, error) {
|
||
|
// Open the original index.html
|
||
|
f, err := fs.Open("/index.html")
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
defer f.Close()
|
||
|
|
||
|
content, err := ioutil.ReadAll(f)
|
||
|
if err != nil {
|
||
|
return nil, nil, fmt.Errorf("failed reading index.html: %s", err)
|
||
|
}
|
||
|
info, err := f.Stat()
|
||
|
if err != nil {
|
||
|
return nil, nil, fmt.Errorf("failed reading metadata for index.html: %s", err)
|
||
|
}
|
||
|
|
||
|
// Create template data from the current config.
|
||
|
tplData, err := uiTemplateDataFromConfig(cfg)
|
||
|
if err != nil {
|
||
|
return nil, nil, fmt.Errorf("failed loading UI config for template: %s", err)
|
||
|
}
|
||
|
|
||
|
// Sadly we can't perform all the replacements we need with Go template
|
||
|
// because some of them end up being rendered into an escaped json encoded
|
||
|
// meta tag by Ember build which messes up the Go template tags. After a few
|
||
|
// iterations of grossness, this seemed like the least bad for now. note we
|
||
|
// have to match the encoded double quotes around the JSON string value that
|
||
|
// is there as a placeholder so the end result is an actual JSON bool not a
|
||
|
// string containing "false" etc.
|
||
|
re := regexp.MustCompile(`%22__RUNTIME_BOOL_[A-Za-z0-9-_]+__%22`)
|
||
|
|
||
|
content = []byte(re.ReplaceAllStringFunc(string(content), func(str string) string {
|
||
|
// Trim the prefix and __ suffix
|
||
|
varName := strings.TrimSuffix(strings.TrimPrefix(str, "%22__RUNTIME_BOOL_"), "__%22")
|
||
|
if v, ok := tplData[varName].(bool); ok && v {
|
||
|
return "true"
|
||
|
}
|
||
|
return "false"
|
||
|
}))
|
||
|
|
||
|
tpl, err := template.New("index").Parse(string(content))
|
||
|
if err != nil {
|
||
|
return nil, nil, fmt.Errorf("failed parsing index.html template: %s", err)
|
||
|
}
|
||
|
|
||
|
var buf bytes.Buffer
|
||
|
|
||
|
err = tpl.Execute(&buf, tplData)
|
||
|
if err != nil {
|
||
|
return nil, nil, fmt.Errorf("failed to render index.html: %s", err)
|
||
|
}
|
||
|
|
||
|
return buf.Bytes(), info, nil
|
||
|
}
|