Add support for serving additional metrics provider JS in the UI (#8743)
This commit is contained in:
parent
d7c476f812
commit
aa3f9e9b4f
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,5 @@
|
|||
var Bar = {};
|
||||
|
||||
Bar.hello = function(){
|
||||
return "I am a bar";
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
var Foo = {};
|
||||
|
||||
Foo.hello = function(){
|
||||
return "I am a foo";
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>` : ``}
|
||||
`;
|
||||
|
|
Loading…
Reference in New Issue