uiserver: upstream refactors done elsewhere (#8891)

This commit is contained in:
R.B. Boyer 2020-10-09 08:32:39 -05:00 committed by GitHub
parent 13cbdb5504
commit b4bf092db3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 149 additions and 102 deletions

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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,

View File

@ -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">