Add support for serving additional metrics provider JS in the UI (#8743)

This commit is contained in:
Paul Banks 2020-10-08 18:03:13 +01:00 committed by GitHub
parent d7c476f812
commit aa3f9e9b4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 215 additions and 53 deletions

File diff suppressed because one or more lines are too long

5
agent/uiserver/testdata/bar.js vendored Normal file
View File

@ -0,0 +1,5 @@
var Bar = {};
Bar.hello = function(){
return "I am a bar";
}

View File

@ -0,0 +1,16 @@
// foo.js
var Foo = {};
Foo.hello = function(){
return "I am a foo";
}
// bar.js
var Bar = {};
Bar.hello = function(){
return "I am a bar";
}

5
agent/uiserver/testdata/foo.js vendored Normal file
View File

@ -0,0 +1,5 @@
var Foo = {};
Foo.hello = function(){
return "I am a foo";
}

View File

@ -51,5 +51,13 @@ func uiTemplateDataFromConfig(cfg *config.RuntimeConfig) (map[string]interface{}
// 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 {
d["ExtraScripts"] = []string{
cfg.UIConfig.ContentPath + compiledProviderJSPath,
}
}
return d, err
}

View File

@ -6,6 +6,7 @@ import (
"io/ioutil"
"net/http"
"os"
"path"
"regexp"
"strings"
"sync/atomic"
@ -16,6 +17,10 @@ import (
"github.com/hashicorp/go-hclog"
)
const (
compiledProviderJSPath = "assets/compiled-metrics-providers.js"
)
// 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
@ -57,7 +62,16 @@ func NewHandler(agentCfg *config.RuntimeConfig, logger hclog.Logger) *Handler {
// 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
// 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
// current config. Everything else works fine that way so we should to.
pathTrimmed := strings.TrimLeft(r.URL.Path, "/")
if pathTrimmed == compiledProviderJSPath {
h.serveUIMetricsProviders(w, r)
return
}
s := h.getState()
if s == nil {
panic("nil state")
@ -133,6 +147,59 @@ func (h *Handler) getState() *reloadableState {
return nil
}
func (h *Handler) serveUIMetricsProviders(resp http.ResponseWriter, req *http.Request) {
// Reload config in case it's changed
state := h.getState()
if len(state.cfg.MetricsProviderFiles) < 1 {
http.Error(resp, "No provider JS files configured", http.StatusNotFound)
return
}
var buf bytes.Buffer
// Open each one and concatenate them
for _, file := range state.cfg.MetricsProviderFiles {
if err := concatFile(&buf, file); err != nil {
http.Error(resp, "Internal Server Error", http.StatusInternalServerError)
h.logger.Error("failed serving metrics provider js file", "file", file, "error", err)
return
}
}
// Done!
resp.Header()["Content-Type"] = []string{"application/javascript"}
_, err := buf.WriteTo(resp)
if err != nil {
http.Error(resp, "Internal Server Error", http.StatusInternalServerError)
h.logger.Error("failed writing ui metrics provider files: %s", err)
return
}
}
func concatFile(buf *bytes.Buffer, file string) error {
base := path.Base(file)
_, err := buf.WriteString("// " + base + "\n\n")
if err != nil {
return fmt.Errorf("failed writing provider JS files: %w", err)
}
// Attempt to open the file
f, err := os.Open(file)
if err != nil {
return fmt.Errorf("failed opening ui metrics provider JS file: %w", err)
}
defer f.Close()
_, err = buf.ReadFrom(f)
if err != nil {
return fmt.Errorf("failed reading ui metrics provider JS file: %w", err)
}
_, err = buf.WriteString("\n\n")
if err != nil {
return fmt.Errorf("failed writing provider JS files: %w", err)
}
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")

View File

@ -16,7 +16,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestUIServer(t *testing.T) {
func TestUIServerIndex(t *testing.T) {
cases := []struct {
name string
cfg *config.RuntimeConfig
@ -80,6 +80,19 @@ func TestUIServer(t *testing.T) {
"CONSUL_ACLS_ENABLED": true,
},
},
{
name: "serving metrics provider js",
cfg: basicUIEnabledConfig(
withMetricsProvider("foo"),
withMetricsProviderFiles("testdata/foo.js", "testdata/bar.js"),
),
path: "/",
wantStatus: http.StatusOK,
wantContains: []string{
"<!-- CONSUL_VERSION:",
`<script src="/ui/assets/compiled-metrics-providers.js">`,
},
},
}
for _, tc := range cases {
@ -152,7 +165,8 @@ type cfgFunc func(cfg *config.RuntimeConfig)
func basicUIEnabledConfig(opts ...cfgFunc) *config.RuntimeConfig {
cfg := &config.RuntimeConfig{
UIConfig: config.UIConfig{
Enabled: true,
Enabled: true,
ContentPath: "/ui/",
},
}
for _, f := range opts {
@ -175,6 +189,12 @@ func withMetricsProvider(name string) cfgFunc {
}
}
func withMetricsProviderFiles(names ...string) cfgFunc {
return func(cfg *config.RuntimeConfig) {
cfg.UIConfig.MetricsProviderFiles = names
}
}
func withMetricsProviderOptions(jsonStr string) cfgFunc {
return func(cfg *config.RuntimeConfig) {
cfg.UIConfig.MetricsProviderOptionsJSON = jsonStr
@ -251,3 +271,43 @@ func TestCustomDir(t *testing.T) {
require.Equal(t, http.StatusOK, rec.Code)
require.Contains(t, rec.Body.String(), "test")
}
func TestCompiledJS(t *testing.T) {
cfg := basicUIEnabledConfig(
withMetricsProvider("foo"),
withMetricsProviderFiles("testdata/foo.js", "testdata/bar.js"),
)
h := NewHandler(cfg, testutil.Logger(t))
paths := []string{
"/" + compiledProviderJSPath,
// We need to work even without the initial slash because the agent uses
// http.StripPrefix with the entire ContentPath which includes a trailing
// slash. This apparently works fine for the assetFS etc. so we need to
// also tolerate it when the URL doesn't have a slash at the start of the
// path.
compiledProviderJSPath,
}
for _, path := range paths {
t.Run(path, func(t *testing.T) {
// NewRequest doesn't like paths with no leading slash but we need to test
// a request with a URL that has that so just create with root path and
// then manually modify the URL path so it emulates one that has been
// doctored by http.StripPath.
req := httptest.NewRequest("GET", "/", nil)
req.URL.Path = path
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
require.Equal(t, rec.Result().Header["Content-Type"][0], "application/javascript")
wantCompiled, err := ioutil.ReadFile("testdata/compiled-metrics-providers-golden.js")
require.NoError(t, err)
require.Equal(t, rec.Body.String(), string(wantCompiled))
})
}
}

View File

@ -41,5 +41,6 @@ module.exports = ({ appName, environment, rootURL, config }) => `
}
};
</script>
${environment === 'production' ? `{{ range .ExtraScripts }} <script src="{{.}}"></script> {{ end }}` : ``}
${environment === 'test' ? `<script src="${rootURL}assets/tests.js"></script>` : ``}
`;