mirror of https://github.com/status-im/consul.git
http: fix a bug that would cause runtimeConfig to be cached
This bug would result in the UI not having the correct settings in Consul enterprise, which could produce many warnings in the logs. This bug occured because the index page, which includes a map of configuration was rendered when the HTTPHandler is first created. This PR changes the UIServer to instead render the index page when the page is requested. The rendering does not appear to be all that expensive, so rendering it when requested should not cause much extra latency.
This commit is contained in:
parent
eba632da13
commit
c98805f505
|
@ -288,7 +288,7 @@ func (s *HTTPHandlers) handler(enableDebug bool) http.Handler {
|
||||||
uiHandler := uiserver.NewHandler(
|
uiHandler := uiserver.NewHandler(
|
||||||
s.agent.config,
|
s.agent.config,
|
||||||
s.agent.logger.Named(logging.HTTP),
|
s.agent.logger.Named(logging.HTTP),
|
||||||
s.uiTemplateDataTransform(),
|
s.uiTemplateDataTransform,
|
||||||
)
|
)
|
||||||
s.configReloaders = append(s.configReloaders, uiHandler.ReloadConfig)
|
s.configReloaders = append(s.configReloaders, uiHandler.ReloadConfig)
|
||||||
|
|
||||||
|
@ -298,10 +298,7 @@ func (s *HTTPHandlers) handler(enableDebug bool) http.Handler {
|
||||||
uiHandler,
|
uiHandler,
|
||||||
s.agent.config.HTTPResponseHeaders,
|
s.agent.config.HTTPResponseHeaders,
|
||||||
)
|
)
|
||||||
mux.Handle(
|
mux.Handle("/robots.txt", uiHandlerWithHeaders)
|
||||||
"/robots.txt",
|
|
||||||
uiHandlerWithHeaders,
|
|
||||||
)
|
|
||||||
mux.Handle(
|
mux.Handle(
|
||||||
s.agent.config.UIConfig.ContentPath,
|
s.agent.config.UIConfig.ContentPath,
|
||||||
http.StripPrefix(
|
http.StripPrefix(
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/consul/agent/uiserver"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *HTTPHandlers) parseEntMeta(req *http.Request, entMeta *structs.EnterpriseMeta) error {
|
func (s *HTTPHandlers) parseEntMeta(req *http.Request, entMeta *structs.EnterpriseMeta) error {
|
||||||
|
@ -71,6 +70,6 @@ func (s *HTTPHandlers) enterpriseHandler(next http.Handler) http.Handler {
|
||||||
|
|
||||||
// uiTemplateDataTransform returns an optional uiserver.UIDataTransform to allow
|
// uiTemplateDataTransform returns an optional uiserver.UIDataTransform to allow
|
||||||
// altering UI data in enterprise.
|
// altering UI data in enterprise.
|
||||||
func (s *HTTPHandlers) uiTemplateDataTransform() uiserver.UIDataTransform {
|
func (s *HTTPHandlers) uiTemplateDataTransform(data map[string]interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,9 +12,10 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-hclog"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/config"
|
"github.com/hashicorp/consul/agent/config"
|
||||||
"github.com/hashicorp/consul/logging"
|
"github.com/hashicorp/consul/logging"
|
||||||
"github.com/hashicorp/go-hclog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -26,22 +27,14 @@ const (
|
||||||
// transformations on the index.html file and includes a proxy for metrics
|
// transformations on the index.html file and includes a proxy for metrics
|
||||||
// backends.
|
// backends.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
// state is a reloadableState struct accessed through an atomic value to make
|
// runtimeConfig is a struct accessed through an atomic value to make
|
||||||
// it safe to reload at run time. Each call to ServeHTTP will see the latest
|
// it safe to reload at run time. Each call to ServeHTTP will see the latest
|
||||||
// version of the state without internal locking needed.
|
// version of the state without internal locking needed.
|
||||||
state atomic.Value
|
runtimeConfig atomic.Value
|
||||||
logger hclog.Logger
|
logger hclog.Logger
|
||||||
transform UIDataTransform
|
transform UIDataTransform
|
||||||
}
|
}
|
||||||
|
|
||||||
// reloadableState encapsulates all the state that might be modified during
|
|
||||||
// ReloadConfig.
|
|
||||||
type reloadableState struct {
|
|
||||||
cfg *config.UIConfig
|
|
||||||
srv http.Handler
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// UIDataTransform is an optional dependency that allows the agent to add
|
// UIDataTransform is an optional dependency that allows the agent to add
|
||||||
// additional data into the UI index as needed. For example we use this to
|
// additional data into the UI index as needed. For example we use this to
|
||||||
// inject enterprise-only feature flags into the template without making this
|
// inject enterprise-only feature flags into the template without making this
|
||||||
|
@ -50,71 +43,60 @@ type reloadableState struct {
|
||||||
// It is passed the current RuntimeConfig being applied and a map containing the
|
// It is passed the current RuntimeConfig being applied and a map containing the
|
||||||
// current data that will be passed to the template. It should be modified
|
// current data that will be passed to the template. It should be modified
|
||||||
// directly to inject additional context.
|
// directly to inject additional context.
|
||||||
type UIDataTransform func(cfg *config.RuntimeConfig, data map[string]interface{}) error
|
type UIDataTransform func(data map[string]interface{}) error
|
||||||
|
|
||||||
// NewHandler returns a Handler that can be used to serve UI http requests. It
|
// 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
|
// accepts a full agent config since properties like ACLs being enabled affect
|
||||||
// the UI so we need more than just UIConfig parts.
|
// the UI so we need more than just UIConfig parts.
|
||||||
func NewHandler(agentCfg *config.RuntimeConfig, logger hclog.Logger, transform UIDataTransform) *Handler {
|
func NewHandler(runtimeCfg *config.RuntimeConfig, logger hclog.Logger, transform UIDataTransform) *Handler {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
logger: logger.Named(logging.UIServer),
|
logger: logger.Named(logging.UIServer),
|
||||||
transform: transform,
|
transform: transform,
|
||||||
}
|
}
|
||||||
// Don't return the error since this is likely the result of a
|
h.runtimeConfig.Store(runtimeCfg)
|
||||||
// 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
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP implements http.Handler and serves UI HTTP requests
|
// ServeHTTP implements http.Handler and serves UI HTTP requests
|
||||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// We need to support the path being trimmed by http.StripTags just like the
|
// We need to support the path being trimmed by http.StripTags just like the
|
||||||
// file servers do since http.StripPrefix will remove the leading slash in our
|
// file servers do since http.StripPrefix will remove the leading slash in our
|
||||||
// current config. Everything else works fine that way so we should to.
|
// current config. Everything else works fine that way so we should to.
|
||||||
pathTrimmed := strings.TrimLeft(r.URL.Path, "/")
|
pathTrimmed := strings.TrimLeft(r.URL.Path, "/")
|
||||||
if pathTrimmed == compiledProviderJSPath {
|
if pathTrimmed == compiledProviderJSPath {
|
||||||
h.serveUIMetricsProviders(w, r)
|
h.serveUIMetricsProviders(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s := h.getState()
|
srv, err := h.handleIndex()
|
||||||
if s == nil {
|
if err != nil {
|
||||||
panic("nil state")
|
|
||||||
}
|
|
||||||
if s.err != nil {
|
|
||||||
http.Error(w, "UI server is misconfigured.", http.StatusInternalServerError)
|
http.Error(w, "UI server is misconfigured.", http.StatusInternalServerError)
|
||||||
h.logger.Error("Failed to configure UI server: %s", s.err)
|
h.logger.Error("Failed to configure UI server: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.srv.ServeHTTP(w, r)
|
srv.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReloadConfig is called by the agent when the configuration is reloaded and
|
// ReloadConfig is called by the agent when the configuration is reloaded and
|
||||||
// updates the UIConfig values the handler uses to serve requests.
|
// updates the UIConfig values the handler uses to serve requests.
|
||||||
func (h *Handler) ReloadConfig(newCfg *config.RuntimeConfig) error {
|
func (h *Handler) ReloadConfig(newCfg *config.RuntimeConfig) error {
|
||||||
newState := reloadableState{
|
h.runtimeConfig.Store(newCfg)
|
||||||
cfg: &newCfg.UIConfig,
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var fs http.FileSystem
|
func (h *Handler) handleIndex() (http.Handler, error) {
|
||||||
|
cfg := h.getRuntimeConfig()
|
||||||
|
|
||||||
if newCfg.UIConfig.Dir == "" {
|
var fs http.FileSystem
|
||||||
// Serve from assetFS
|
if cfg.UIConfig.Dir == "" {
|
||||||
fs = assetFS()
|
fs = assetFS()
|
||||||
} else {
|
} else {
|
||||||
fs = http.Dir(newCfg.UIConfig.Dir)
|
fs = http.Dir(cfg.UIConfig.Dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render a new index.html with the new config values ready to serve.
|
// Render a new index.html with the new config values ready to serve.
|
||||||
buf, info, err := h.renderIndex(newCfg, fs)
|
buf, info, err := h.renderIndex(cfg, fs)
|
||||||
if _, ok := err.(*os.PathError); ok && newCfg.UIConfig.Dir != "" {
|
if _, ok := err.(*os.PathError); ok && cfg.UIConfig.Dir != "" {
|
||||||
// A Path error indicates that there is no index.html. This could happen if
|
// A Path error indicates that there is no index.html. This could happen if
|
||||||
// the user configured their own UI dir and is serving something that is not
|
// the user configured their own UI dir and is serving something that is not
|
||||||
// our usual UI. This won't work perfectly because our uiserver will still
|
// our usual UI. This won't work perfectly because our uiserver will still
|
||||||
|
@ -123,13 +105,12 @@ func (h *Handler) ReloadConfig(newCfg *config.RuntimeConfig) error {
|
||||||
// breaking change although quite an edge case. Instead, continue but just
|
// breaking change although quite an edge case. Instead, continue but just
|
||||||
// return a 404 response for the index.html and log a warning.
|
// return a 404 response for the index.html and log a warning.
|
||||||
h.logger.Warn("ui_config.dir does not contain an index.html. Index templating and redirects to index.html are disabled.")
|
h.logger.Warn("ui_config.dir does not contain an index.html. Index templating and redirects to index.html are disabled.")
|
||||||
} else if err != nil {
|
return http.FileServer(fs), nil
|
||||||
return err
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// buf can be nil in the PathError case above. We should skip this part but
|
|
||||||
// still serve the rest of the files in that case.
|
|
||||||
if buf != nil {
|
|
||||||
// Create a new fs that serves the rendered index file or falls back to the
|
// Create a new fs that serves the rendered index file or falls back to the
|
||||||
// underlying FS.
|
// underlying FS.
|
||||||
fs = &bufIndexFS{
|
fs = &bufIndexFS{
|
||||||
|
@ -141,29 +122,22 @@ func (h *Handler) ReloadConfig(newCfg *config.RuntimeConfig) error {
|
||||||
// Wrap the buffering FS our redirect FS. This needs to happen later so that
|
// 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
|
// redirected requests for /index.html get served the rendered version not the
|
||||||
// original.
|
// original.
|
||||||
fs = &redirectFS{fs: fs}
|
return http.FileServer(&redirectFS{fs: fs}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
newState.srv = http.FileServer(fs)
|
// getRuntimeConfig is a helper to atomically access the runtime config.
|
||||||
|
func (h *Handler) getRuntimeConfig() *config.RuntimeConfig {
|
||||||
// Store the new state
|
if cfg, ok := h.runtimeConfig.Load().(*config.RuntimeConfig); ok {
|
||||||
h.state.Store(newState)
|
return cfg
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) serveUIMetricsProviders(resp http.ResponseWriter, req *http.Request) {
|
func (h *Handler) serveUIMetricsProviders(resp http.ResponseWriter) {
|
||||||
// Reload config in case it's changed
|
// Reload config in case it's changed
|
||||||
state := h.getState()
|
cfg := h.getRuntimeConfig()
|
||||||
|
|
||||||
if len(state.cfg.MetricsProviderFiles) < 1 {
|
if len(cfg.UIConfig.MetricsProviderFiles) < 1 {
|
||||||
http.Error(resp, "No provider JS files configured", http.StatusNotFound)
|
http.Error(resp, "No provider JS files configured", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -171,7 +145,7 @@ func (h *Handler) serveUIMetricsProviders(resp http.ResponseWriter, req *http.Re
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
// Open each one and concatenate them
|
// Open each one and concatenate them
|
||||||
for _, file := range state.cfg.MetricsProviderFiles {
|
for _, file := range cfg.UIConfig.MetricsProviderFiles {
|
||||||
if err := concatFile(&buf, file); err != nil {
|
if err := concatFile(&buf, file); err != nil {
|
||||||
http.Error(resp, "Internal Server Error", http.StatusInternalServerError)
|
http.Error(resp, "Internal Server Error", http.StatusInternalServerError)
|
||||||
h.logger.Error("failed serving metrics provider js file", "file", file, "error", err)
|
h.logger.Error("failed serving metrics provider js file", "file", file, "error", err)
|
||||||
|
@ -237,7 +211,7 @@ func (h *Handler) renderIndex(cfg *config.RuntimeConfig, fs http.FileSystem) ([]
|
||||||
|
|
||||||
// Allow caller to apply additional data transformations if needed.
|
// Allow caller to apply additional data transformations if needed.
|
||||||
if h.transform != nil {
|
if h.transform != nil {
|
||||||
if err := h.transform(cfg, tplData); err != nil {
|
if err := h.transform(tplData); err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed running transform: %w", err)
|
return nil, nil, fmt.Errorf("failed running transform: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,10 @@ import (
|
||||||
|
|
||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/config"
|
"github.com/hashicorp/consul/agent/config"
|
||||||
"github.com/hashicorp/consul/sdk/testutil"
|
"github.com/hashicorp/consul/sdk/testutil"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUIServerIndex(t *testing.T) {
|
func TestUIServerIndex(t *testing.T) {
|
||||||
|
@ -105,7 +106,7 @@ func TestUIServerIndex(t *testing.T) {
|
||||||
withMetricsProvider("foo"),
|
withMetricsProvider("foo"),
|
||||||
),
|
),
|
||||||
path: "/",
|
path: "/",
|
||||||
tx: func(cfg *config.RuntimeConfig, data map[string]interface{}) error {
|
tx: func(data map[string]interface{}) error {
|
||||||
data["SSOEnabled"] = true
|
data["SSOEnabled"] = true
|
||||||
o := data["UIConfig"].(map[string]interface{})
|
o := data["UIConfig"].(map[string]interface{})
|
||||||
o["metrics_provider"] = "bar"
|
o["metrics_provider"] = "bar"
|
||||||
|
|
Loading…
Reference in New Issue