uiserver: upstream refactors done elsewhere (#8891)
This commit is contained in:
parent
13cbdb5504
commit
b4bf092db3
|
@ -276,7 +276,11 @@ func (s *HTTPHandlers) handler(enableDebug bool) http.Handler {
|
|||
if s.IsUIEnabled() {
|
||||
// Note that we _don't_ support reloading ui_config.{enabled, content_dir,
|
||||
// content_path} since this only runs at initial startup.
|
||||
uiHandler := uiserver.NewHandler(s.agent.config, s.agent.logger.Named(logging.HTTP))
|
||||
uiHandler := uiserver.NewHandler(
|
||||
s.agent.config,
|
||||
s.agent.logger.Named(logging.HTTP),
|
||||
s.uiTemplateDataTransform(),
|
||||
)
|
||||
s.configReloaders = append(s.configReloaders, uiHandler.ReloadConfig)
|
||||
|
||||
// Wrap it to add the headers specified by the http_config.response_headers
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/agent/uiserver"
|
||||
)
|
||||
|
||||
func (s *HTTPHandlers) parseEntMeta(req *http.Request, entMeta *structs.EnterpriseMeta) error {
|
||||
|
@ -67,3 +68,9 @@ func parseACLAuthMethodEnterpriseMeta(req *http.Request, _ *structs.ACLAuthMetho
|
|||
func (s *HTTPHandlers) enterpriseHandler(next http.Handler) http.Handler {
|
||||
return next
|
||||
}
|
||||
|
||||
// uiTemplateDataTransform returns an optional uiserver.UIDataTransform to allow
|
||||
// altering UI data in enterprise.
|
||||
func (s *HTTPHandlers) uiTemplateDataTransform() uiserver.UIDataTransform {
|
||||
return nil
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -2,14 +2,12 @@ package uiserver
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/hashicorp/consul/agent/config"
|
||||
)
|
||||
|
||||
// uiTemplateDataFromConfig returns the set of variables that should be injected
|
||||
// into the UI's Env based on the given runtime UI config.
|
||||
// uiTemplateDataFromConfig returns the base set of variables that should be
|
||||
// injected into the UI's Env based on the given runtime UI config.
|
||||
func uiTemplateDataFromConfig(cfg *config.RuntimeConfig) (map[string]interface{}, error) {
|
||||
|
||||
uiCfg := map[string]interface{}{
|
||||
|
@ -32,25 +30,9 @@ func uiTemplateDataFromConfig(cfg *config.RuntimeConfig) (map[string]interface{}
|
|||
d := map[string]interface{}{
|
||||
"ContentPath": cfg.UIConfig.ContentPath,
|
||||
"ACLsEnabled": cfg.ACLsEnabled,
|
||||
"UIConfig": uiCfg,
|
||||
}
|
||||
|
||||
err := uiTemplateDataFromConfigEnterprise(cfg, d, uiCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Render uiCfg down to JSON ready to inject into the template
|
||||
bs, err := json.Marshal(uiCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed marshalling UI Env JSON: %s", err)
|
||||
}
|
||||
// Need to also URLEncode it as it is passed through a META tag value. Path
|
||||
// variant is correct to avoid converting spaces to "+". Note we don't just
|
||||
// use html/template because it strips comments and uses a different encoding
|
||||
// for this param than Ember which is OK but just one more weird thing to
|
||||
// account for in the source...
|
||||
d["UIConfigJSON"] = url.PathEscape(string(bs))
|
||||
|
||||
// Also inject additional provider scripts if needed, otherwise strip the
|
||||
// comment.
|
||||
if len(cfg.UIConfig.MetricsProviderFiles) > 0 {
|
||||
|
@ -59,5 +41,5 @@ func uiTemplateDataFromConfig(cfg *config.RuntimeConfig) (map[string]interface{}
|
|||
}
|
||||
}
|
||||
|
||||
return d, err
|
||||
return d, nil
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
// +build !consulent
|
||||
|
||||
package uiserver
|
||||
|
||||
import "github.com/hashicorp/consul/agent/config"
|
||||
|
||||
func uiTemplateDataFromConfigEnterprise(_ *config.RuntimeConfig, _ map[string]interface{}, _ map[string]interface{}) error {
|
||||
return nil
|
||||
}
|
|
@ -2,9 +2,11 @@ package uiserver
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
|
@ -31,6 +33,7 @@ type Handler struct {
|
|||
// version of the state without internal locking needed.
|
||||
state atomic.Value
|
||||
logger hclog.Logger
|
||||
transform UIDataTransform
|
||||
}
|
||||
|
||||
// reloadableState encapsulates all the state that might be modified during
|
||||
|
@ -41,12 +44,23 @@ type reloadableState struct {
|
|||
err error
|
||||
}
|
||||
|
||||
// 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
|
||||
// inject enterprise-only feature flags into the template without making this
|
||||
// package inherently dependent on Enterprise-only code.
|
||||
//
|
||||
// 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
|
||||
// directly to inject additional context.
|
||||
type UIDataTransform func(cfg *config.RuntimeConfig, data map[string]interface{}) 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 {
|
||||
func NewHandler(agentCfg *config.RuntimeConfig, logger hclog.Logger, transform UIDataTransform) *Handler {
|
||||
h := &Handler{
|
||||
logger: logger.Named(logging.UIServer),
|
||||
transform: transform,
|
||||
}
|
||||
// Don't return the error since this is likely the result of a
|
||||
// misconfiguration and reloading config could fix it. Instead we'll capture
|
||||
|
@ -101,7 +115,7 @@ func (h *Handler) ReloadConfig(newCfg *config.RuntimeConfig) error {
|
|||
}
|
||||
|
||||
// Render a new index.html with the new config values ready to serve.
|
||||
buf, info, err := renderIndex(newCfg, fs)
|
||||
buf, info, err := h.renderIndex(newCfg, fs)
|
||||
if _, ok := err.(*os.PathError); ok && newCfg.UIConfig.Dir != "" {
|
||||
// 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
|
||||
|
@ -200,7 +214,7 @@ func concatFile(buf *bytes.Buffer, file string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func renderIndex(cfg *config.RuntimeConfig, fs http.FileSystem) ([]byte, os.FileInfo, error) {
|
||||
func (h *Handler) 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 {
|
||||
|
@ -210,17 +224,24 @@ func renderIndex(cfg *config.RuntimeConfig, fs http.FileSystem) ([]byte, os.File
|
|||
|
||||
content, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed reading index.html: %s", err)
|
||||
return nil, nil, fmt.Errorf("failed reading index.html: %w", err)
|
||||
}
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed reading metadata for index.html: %s", err)
|
||||
return nil, nil, fmt.Errorf("failed reading metadata for index.html: %w", 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)
|
||||
return nil, nil, fmt.Errorf("failed loading UI config for template: %w", err)
|
||||
}
|
||||
|
||||
// Allow caller to apply additional data transformations if needed.
|
||||
if h.transform != nil {
|
||||
if err := h.transform(cfg, tplData); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed running transform: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Sadly we can't perform all the replacements we need with Go template
|
||||
|
@ -241,16 +262,24 @@ func renderIndex(cfg *config.RuntimeConfig, fs http.FileSystem) ([]byte, os.File
|
|||
return "false"
|
||||
}))
|
||||
|
||||
tpl, err := template.New("index").Parse(string(content))
|
||||
tpl, err := template.New("index").Funcs(template.FuncMap{
|
||||
"jsonEncodeAndEscape": func(data map[string]interface{}) (string, error) {
|
||||
bs, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed parsing index.html template: %s", err)
|
||||
return "", fmt.Errorf("failed jsonEncodeAndEscape: %w", err)
|
||||
}
|
||||
return url.PathEscape(string(bs)), nil
|
||||
},
|
||||
}).Parse(string(content))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed parsing index.html template: %w", 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 nil, nil, fmt.Errorf("failed to render index.html: %w", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), info, nil
|
||||
|
|
|
@ -21,6 +21,7 @@ func TestUIServerIndex(t *testing.T) {
|
|||
name string
|
||||
cfg *config.RuntimeConfig
|
||||
path string
|
||||
tx UIDataTransform
|
||||
wantStatus int
|
||||
wantContains []string
|
||||
wantNotContains []string
|
||||
|
@ -51,20 +52,28 @@ func TestUIServerIndex(t *testing.T) {
|
|||
name: "injecting metrics vars",
|
||||
cfg: basicUIEnabledConfig(
|
||||
withMetricsProvider("foo"),
|
||||
withMetricsProviderOptions(`{"bar":1}`),
|
||||
withMetricsProviderOptions(`{"a-very-unlikely-string":1}`),
|
||||
),
|
||||
path: "/",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContains: []string{
|
||||
"<!-- CONSUL_VERSION:",
|
||||
},
|
||||
wantNotContains: []string{
|
||||
// This is a quick check to be sure that we actually URL encoded the
|
||||
// JSON ui settings too. The assertions below could pass just fine even
|
||||
// if we got that wrong because the decode would be a no-op if it wasn't
|
||||
// URL encoded. But this just ensures that we don't see the raw values
|
||||
// in the output because the quotes should be encoded.
|
||||
`"a-very-unlikely-string"`,
|
||||
},
|
||||
wantEnv: map[string]interface{}{
|
||||
"CONSUL_ACLS_ENABLED": false,
|
||||
},
|
||||
wantUICfgJSON: `{
|
||||
"metrics_provider": "foo",
|
||||
"metrics_provider_options": {
|
||||
"bar":1
|
||||
"a-very-unlikely-string":1
|
||||
},
|
||||
"metrics_proxy_enabled": false,
|
||||
"dashboard_url_templates": null
|
||||
|
@ -80,6 +89,31 @@ func TestUIServerIndex(t *testing.T) {
|
|||
"CONSUL_ACLS_ENABLED": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "external transformation",
|
||||
cfg: basicUIEnabledConfig(
|
||||
withMetricsProvider("foo"),
|
||||
),
|
||||
path: "/",
|
||||
tx: func(cfg *config.RuntimeConfig, data map[string]interface{}) error {
|
||||
data["SSOEnabled"] = true
|
||||
o := data["UIConfig"].(map[string]interface{})
|
||||
o["metrics_provider"] = "bar"
|
||||
return nil
|
||||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContains: []string{
|
||||
"<!-- CONSUL_VERSION:",
|
||||
},
|
||||
wantEnv: map[string]interface{}{
|
||||
"CONSUL_SSO_ENABLED": true,
|
||||
},
|
||||
wantUICfgJSON: `{
|
||||
"metrics_provider": "bar",
|
||||
"metrics_proxy_enabled": false,
|
||||
"dashboard_url_templates": null
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "serving metrics provider js",
|
||||
cfg: basicUIEnabledConfig(
|
||||
|
@ -98,7 +132,7 @@ func TestUIServerIndex(t *testing.T) {
|
|||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
h := NewHandler(tc.cfg, testutil.Logger(t))
|
||||
h := NewHandler(tc.cfg, testutil.Logger(t), tc.tx)
|
||||
|
||||
req := httptest.NewRequest("GET", tc.path, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
@ -205,7 +239,7 @@ func withMetricsProviderOptions(jsonStr string) cfgFunc {
|
|||
// beyond the first request. The initial implementation did not as it shared an
|
||||
// bytes.Reader between callers.
|
||||
func TestMultipleIndexRequests(t *testing.T) {
|
||||
h := NewHandler(basicUIEnabledConfig(), testutil.Logger(t))
|
||||
h := NewHandler(basicUIEnabledConfig(), testutil.Logger(t), nil)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
|
@ -220,7 +254,7 @@ func TestMultipleIndexRequests(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestReload(t *testing.T) {
|
||||
h := NewHandler(basicUIEnabledConfig(), testutil.Logger(t))
|
||||
h := NewHandler(basicUIEnabledConfig(), testutil.Logger(t), nil)
|
||||
|
||||
{
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
|
@ -261,7 +295,7 @@ func TestCustomDir(t *testing.T) {
|
|||
|
||||
cfg := basicUIEnabledConfig()
|
||||
cfg.UIConfig.Dir = uiDir
|
||||
h := NewHandler(cfg, testutil.Logger(t))
|
||||
h := NewHandler(cfg, testutil.Logger(t), nil)
|
||||
|
||||
req := httptest.NewRequest("GET", "/test-file", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
@ -277,7 +311,7 @@ func TestCompiledJS(t *testing.T) {
|
|||
withMetricsProvider("foo"),
|
||||
withMetricsProviderFiles("testdata/foo.js", "testdata/bar.js"),
|
||||
)
|
||||
h := NewHandler(cfg, testutil.Logger(t))
|
||||
h := NewHandler(cfg, testutil.Logger(t), nil)
|
||||
|
||||
paths := []string{
|
||||
"/" + compiledProviderJSPath,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
module.exports = ({ appName, environment, rootURL, config }) => `
|
||||
<!-- CONSUL_VERSION: ${config.CONSUL_VERSION} -->
|
||||
<meta name="consul-ui/ui_config" content="{{ .UIConfigJSON }}" />
|
||||
<meta name="consul-ui/ui_config" content="{{ jsonEncodeAndEscape .UIConfig }}" />
|
||||
|
||||
<link rel="icon" type="image/png" href="${rootURL}assets/favicon-32x32.png" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="${rootURL}assets/favicon-16x16.png" sizes="16x16">
|
||||
|
|
Loading…
Reference in New Issue