mirror of https://github.com/hashicorp/consul
uiserver: upstream refactors done elsewhere (#8891)
parent
47d7df5c91
commit
b6f686fecb
|
@ -276,7 +276,11 @@ func (s *HTTPHandlers) handler(enableDebug bool) http.Handler {
|
||||||
if s.IsUIEnabled() {
|
if s.IsUIEnabled() {
|
||||||
// Note that we _don't_ support reloading ui_config.{enabled, content_dir,
|
// Note that we _don't_ support reloading ui_config.{enabled, content_dir,
|
||||||
// content_path} since this only runs at initial startup.
|
// 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)
|
s.configReloaders = append(s.configReloaders, uiHandler.ReloadConfig)
|
||||||
|
|
||||||
// Wrap it to add the headers specified by the http_config.response_headers
|
// Wrap it to add the headers specified by the http_config.response_headers
|
||||||
|
|
|
@ -8,6 +8,7 @@ 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 {
|
||||||
|
@ -67,3 +68,9 @@ func parseACLAuthMethodEnterpriseMeta(req *http.Request, _ *structs.ACLAuthMetho
|
||||||
func (s *HTTPHandlers) enterpriseHandler(next http.Handler) http.Handler {
|
func (s *HTTPHandlers) enterpriseHandler(next http.Handler) http.Handler {
|
||||||
return next
|
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 (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/config"
|
"github.com/hashicorp/consul/agent/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// uiTemplateDataFromConfig returns the set of variables that should be injected
|
// uiTemplateDataFromConfig returns the base set of variables that should be
|
||||||
// into the UI's Env based on the given runtime UI config.
|
// injected into the UI's Env based on the given runtime UI config.
|
||||||
func uiTemplateDataFromConfig(cfg *config.RuntimeConfig) (map[string]interface{}, error) {
|
func uiTemplateDataFromConfig(cfg *config.RuntimeConfig) (map[string]interface{}, error) {
|
||||||
|
|
||||||
uiCfg := map[string]interface{}{
|
uiCfg := map[string]interface{}{
|
||||||
|
@ -32,25 +30,9 @@ func uiTemplateDataFromConfig(cfg *config.RuntimeConfig) (map[string]interface{}
|
||||||
d := map[string]interface{}{
|
d := map[string]interface{}{
|
||||||
"ContentPath": cfg.UIConfig.ContentPath,
|
"ContentPath": cfg.UIConfig.ContentPath,
|
||||||
"ACLsEnabled": cfg.ACLsEnabled,
|
"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
|
// Also inject additional provider scripts if needed, otherwise strip the
|
||||||
// comment.
|
// comment.
|
||||||
if len(cfg.UIConfig.MetricsProviderFiles) > 0 {
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
@ -31,6 +33,7 @@ type Handler struct {
|
||||||
// version of the state without internal locking needed.
|
// version of the state without internal locking needed.
|
||||||
state atomic.Value
|
state atomic.Value
|
||||||
logger hclog.Logger
|
logger hclog.Logger
|
||||||
|
transform UIDataTransform
|
||||||
}
|
}
|
||||||
|
|
||||||
// reloadableState encapsulates all the state that might be modified during
|
// reloadableState encapsulates all the state that might be modified during
|
||||||
|
@ -41,12 +44,23 @@ type reloadableState struct {
|
||||||
err error
|
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
|
// 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) *Handler {
|
func NewHandler(agentCfg *config.RuntimeConfig, logger hclog.Logger, transform UIDataTransform) *Handler {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
logger: logger.Named(logging.UIServer),
|
logger: logger.Named(logging.UIServer),
|
||||||
|
transform: transform,
|
||||||
}
|
}
|
||||||
// Don't return the error since this is likely the result of a
|
// Don't return the error since this is likely the result of a
|
||||||
// misconfiguration and reloading config could fix it. Instead we'll capture
|
// 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.
|
// 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 != "" {
|
if _, ok := err.(*os.PathError); ok && newCfg.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
|
||||||
|
@ -200,7 +214,7 @@ func concatFile(buf *bytes.Buffer, file string) error {
|
||||||
return nil
|
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
|
// Open the original index.html
|
||||||
f, err := fs.Open("/index.html")
|
f, err := fs.Open("/index.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -210,17 +224,24 @@ func renderIndex(cfg *config.RuntimeConfig, fs http.FileSystem) ([]byte, os.File
|
||||||
|
|
||||||
content, err := ioutil.ReadAll(f)
|
content, err := ioutil.ReadAll(f)
|
||||||
if err != nil {
|
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()
|
info, err := f.Stat()
|
||||||
if err != nil {
|
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.
|
// Create template data from the current config.
|
||||||
tplData, err := uiTemplateDataFromConfig(cfg)
|
tplData, err := uiTemplateDataFromConfig(cfg)
|
||||||
if err != nil {
|
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
|
// 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"
|
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 {
|
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
|
var buf bytes.Buffer
|
||||||
|
|
||||||
err = tpl.Execute(&buf, tplData)
|
err = tpl.Execute(&buf, tplData)
|
||||||
if err != nil {
|
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
|
return buf.Bytes(), info, nil
|
||||||
|
|
|
@ -21,6 +21,7 @@ func TestUIServerIndex(t *testing.T) {
|
||||||
name string
|
name string
|
||||||
cfg *config.RuntimeConfig
|
cfg *config.RuntimeConfig
|
||||||
path string
|
path string
|
||||||
|
tx UIDataTransform
|
||||||
wantStatus int
|
wantStatus int
|
||||||
wantContains []string
|
wantContains []string
|
||||||
wantNotContains []string
|
wantNotContains []string
|
||||||
|
@ -51,20 +52,28 @@ func TestUIServerIndex(t *testing.T) {
|
||||||
name: "injecting metrics vars",
|
name: "injecting metrics vars",
|
||||||
cfg: basicUIEnabledConfig(
|
cfg: basicUIEnabledConfig(
|
||||||
withMetricsProvider("foo"),
|
withMetricsProvider("foo"),
|
||||||
withMetricsProviderOptions(`{"bar":1}`),
|
withMetricsProviderOptions(`{"a-very-unlikely-string":1}`),
|
||||||
),
|
),
|
||||||
path: "/",
|
path: "/",
|
||||||
wantStatus: http.StatusOK,
|
wantStatus: http.StatusOK,
|
||||||
wantContains: []string{
|
wantContains: []string{
|
||||||
"<!-- CONSUL_VERSION:",
|
"<!-- 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{}{
|
wantEnv: map[string]interface{}{
|
||||||
"CONSUL_ACLS_ENABLED": false,
|
"CONSUL_ACLS_ENABLED": false,
|
||||||
},
|
},
|
||||||
wantUICfgJSON: `{
|
wantUICfgJSON: `{
|
||||||
"metrics_provider": "foo",
|
"metrics_provider": "foo",
|
||||||
"metrics_provider_options": {
|
"metrics_provider_options": {
|
||||||
"bar":1
|
"a-very-unlikely-string":1
|
||||||
},
|
},
|
||||||
"metrics_proxy_enabled": false,
|
"metrics_proxy_enabled": false,
|
||||||
"dashboard_url_templates": null
|
"dashboard_url_templates": null
|
||||||
|
@ -80,6 +89,31 @@ func TestUIServerIndex(t *testing.T) {
|
||||||
"CONSUL_ACLS_ENABLED": true,
|
"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",
|
name: "serving metrics provider js",
|
||||||
cfg: basicUIEnabledConfig(
|
cfg: basicUIEnabledConfig(
|
||||||
|
@ -98,7 +132,7 @@ func TestUIServerIndex(t *testing.T) {
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
tc := tc
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
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)
|
req := httptest.NewRequest("GET", tc.path, nil)
|
||||||
rec := httptest.NewRecorder()
|
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
|
// beyond the first request. The initial implementation did not as it shared an
|
||||||
// bytes.Reader between callers.
|
// bytes.Reader between callers.
|
||||||
func TestMultipleIndexRequests(t *testing.T) {
|
func TestMultipleIndexRequests(t *testing.T) {
|
||||||
h := NewHandler(basicUIEnabledConfig(), testutil.Logger(t))
|
h := NewHandler(basicUIEnabledConfig(), testutil.Logger(t), nil)
|
||||||
|
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
@ -220,7 +254,7 @@ func TestMultipleIndexRequests(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReload(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)
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
@ -261,7 +295,7 @@ func TestCustomDir(t *testing.T) {
|
||||||
|
|
||||||
cfg := basicUIEnabledConfig()
|
cfg := basicUIEnabledConfig()
|
||||||
cfg.UIConfig.Dir = uiDir
|
cfg.UIConfig.Dir = uiDir
|
||||||
h := NewHandler(cfg, testutil.Logger(t))
|
h := NewHandler(cfg, testutil.Logger(t), nil)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/test-file", nil)
|
req := httptest.NewRequest("GET", "/test-file", nil)
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
|
@ -277,7 +311,7 @@ func TestCompiledJS(t *testing.T) {
|
||||||
withMetricsProvider("foo"),
|
withMetricsProvider("foo"),
|
||||||
withMetricsProviderFiles("testdata/foo.js", "testdata/bar.js"),
|
withMetricsProviderFiles("testdata/foo.js", "testdata/bar.js"),
|
||||||
)
|
)
|
||||||
h := NewHandler(cfg, testutil.Logger(t))
|
h := NewHandler(cfg, testutil.Logger(t), nil)
|
||||||
|
|
||||||
paths := []string{
|
paths := []string{
|
||||||
"/" + compiledProviderJSPath,
|
"/" + compiledProviderJSPath,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
module.exports = ({ appName, environment, rootURL, config }) => `
|
module.exports = ({ appName, environment, rootURL, config }) => `
|
||||||
<!-- CONSUL_VERSION: ${config.CONSUL_VERSION} -->
|
<!-- 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-32x32.png" sizes="32x32">
|
||||||
<link rel="icon" type="image/png" href="${rootURL}assets/favicon-16x16.png" sizes="16x16">
|
<link rel="icon" type="image/png" href="${rootURL}assets/favicon-16x16.png" sizes="16x16">
|
||||||
|
|
Loading…
Reference in New Issue