Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to use JS file for UI configuration (#123 from jaeger-ui) #2707

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions cmd/query/app/fixture/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
<meta charset="UTF-8">
<base href="/" data-inject-target="BASE_URL"/>
<title>Test Page</title>
<!-- JAEGER_CONFIG=DEFAULT_CONFIG; -->
<!-- JAEGER_VERSION=DEFAULT_VERSION; -->
<!--
// JAEGER_CONFIG_JS
// the line above may be replaced by user-provided JS file that should define a UIConfig function.
JAEGER_CONFIG=DEFAULT_CONFIG;
JAEGER_VERSION=DEFAULT_VERSION;
-->
</html>
10 changes: 10 additions & 0 deletions cmd/query/app/fixture/ui-config-malformed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
() => {
return {
menu: [
{
label: "GitHub",
url: "https://github.com/jaegertracing/jaeger"
}
]
}
}
10 changes: 10 additions & 0 deletions cmd/query/app/fixture/ui-config-menu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
function UIConfig(){
return {
menu: [
{
label: "GitHub",
url: "https://github.com/jaegertracing/jaeger"
}
]
}
}
5 changes: 5 additions & 0 deletions cmd/query/app/fixture/ui-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function UIConfig(){
return {
x: "y"
}
}
61 changes: 38 additions & 23 deletions cmd/query/app/static_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package app

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
Expand All @@ -39,6 +40,7 @@ var (

// The following patterns are searched and replaced in the index.html as a way of customizing the UI.
configPattern = regexp.MustCompile("JAEGER_CONFIG *= *DEFAULT_CONFIG;")
configJsPattern = regexp.MustCompile(`(?im)^\s*\/\/\s*JAEGER_CONFIG_JS.*\n.*`)
versionPattern = regexp.MustCompile("JAEGER_VERSION *= *DEFAULT_VERSION;")
basePathPattern = regexp.MustCompile(`<base href="/"`) // Note: tag is not closed
)
Expand Down Expand Up @@ -72,6 +74,11 @@ type StaticAssetsHandlerOptions struct {
Logger *zap.Logger
}

type loadedConfig struct {
regexp *regexp.Regexp
config []byte
}

// NewStaticAssetsHandler returns a StaticAssetsHandler
func NewStaticAssetsHandler(staticAssetsRoot string, options StaticAssetsHandlerOptions) (*StaticAssetsHandler, error) {
assetsFS := ui.StaticFiles
Expand Down Expand Up @@ -105,17 +112,11 @@ func loadAndEnrichIndexHTML(open func(string) (http.File, error), options Static
return nil, fmt.Errorf("cannot load index.html: %w", err)
}
// replace UI config
configString := "JAEGER_CONFIG = DEFAULT_CONFIG"
if config, err := loadUIConfig(options.UIConfigPath); err != nil {
if configObject, err := loadUIConfig(options.UIConfigPath); err != nil {
return nil, err
} else if config != nil {
// TODO if we want to support other config formats like YAML, we need to normalize `config` to be
// suitable for json.Marshal(). For example, YAML parser may return a map that has keys of type
// interface{}, and json.Marshal() is unable to serialize it.
bytes, _ := json.Marshal(config)
configString = fmt.Sprintf("JAEGER_CONFIG = %v", string(bytes))
}
indexBytes = configPattern.ReplaceAll(indexBytes, []byte(configString+";"))
} else if configObject != nil {
indexBytes = configObject.regexp.ReplaceAll(indexBytes, configObject.config)
}
// replace Jaeger version
versionJSON, _ := json.Marshal(version.Get())
versionString := fmt.Sprintf("JAEGER_VERSION = %s;", string(versionJSON))
Expand Down Expand Up @@ -210,30 +211,44 @@ func loadIndexHTML(open func(string) (http.File, error)) ([]byte, error) {
return indexBytes, nil
}

func loadUIConfig(uiConfig string) (map[string]interface{}, error) {
func loadUIConfig(uiConfig string) (*loadedConfig, error) {
if uiConfig == "" {
return nil, nil
}
ext := filepath.Ext(uiConfig)
bytes, err := ioutil.ReadFile(filepath.Clean(uiConfig))
bytesConfig, err := ioutil.ReadFile(filepath.Clean(uiConfig))
if err != nil {
return nil, fmt.Errorf("cannot read UI config file %v: %w", uiConfig, err)
}
var r []byte

var c map[string]interface{}
var unmarshal func([]byte, interface{}) error

ext := filepath.Ext(uiConfig)
switch strings.ToLower(ext) {
case ".json":
unmarshal = json.Unmarshal
default:
return nil, fmt.Errorf("unrecognized UI config file format %v", uiConfig)
}
var c map[string]interface{}

if err := json.Unmarshal(bytesConfig, &c); err != nil {
return nil, fmt.Errorf("cannot parse UI config file %v: %w", uiConfig, err)
}
r, _ = json.Marshal(c)

return &loadedConfig{
regexp: configPattern,
config: append([]byte("JAEGER_CONFIG = "), append(r, byte(';'))...),
}, nil
case ".js":
r = bytes.TrimSpace(bytesConfig)
re := regexp.MustCompile(`function\s+UIConfig(\s)?\(\s?\)(\s)?{`)
if !re.Match(r) {
return nil, fmt.Errorf("UI config file must define function UIConfig(): %v", uiConfig)
}

if err := unmarshal(bytes, &c); err != nil {
return nil, fmt.Errorf("cannot parse UI config file %v: %w", uiConfig, err)
return &loadedConfig{
regexp: configJsPattern,
config: r,
}, nil
default:
return nil, fmt.Errorf("unrecognized UI config file format, expecting .js or .json file: %v", uiConfig)
}
return c, nil
}

// RegisterRoutes registers routes for this handler on the given router
Expand Down
90 changes: 74 additions & 16 deletions cmd/query/app/static_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package app

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
Expand Down Expand Up @@ -54,10 +55,31 @@ func TestRegisterStaticHandler(t *testing.T) {
subroute bool // should we create a subroute?
baseURL string // expected URL prefix
expectedBaseHTML string // substring to match in the home page
UIConfigPath string // path to UI config
expectedUIConfig string // expected UI config
}{
{basePath: "", baseURL: "/", expectedBaseHTML: `<base href="/"`},
{basePath: "/", baseURL: "/", expectedBaseHTML: `<base href="/"`},
{basePath: "/jaeger", baseURL: "/jaeger/", expectedBaseHTML: `<base href="/jaeger/"`, subroute: true},
{
basePath: "",
baseURL: "/",
expectedBaseHTML: `<base href="/"`,
UIConfigPath: "",
expectedUIConfig: "JAEGER_CONFIG=DEFAULT_CONFIG;",
},
{
basePath: "/",
baseURL: "/",
expectedBaseHTML: `<base href="/"`,
UIConfigPath: "fixture/ui-config.json",
expectedUIConfig: `JAEGER_CONFIG = {"x":"y"};`,
},
{
basePath: "/jaeger",
baseURL: "/jaeger/",
expectedBaseHTML: `<base href="/jaeger/"`,
subroute: true,
UIConfigPath: "fixture/ui-config.js",
expectedUIConfig: "function UIConfig(){",
},
}
httpClient = &http.Client{
Timeout: 2 * time.Second,
Expand All @@ -72,7 +94,7 @@ func TestRegisterStaticHandler(t *testing.T) {
RegisterStaticHandler(r, logger, &QueryOptions{
StaticAssets: "fixture",
BasePath: testCase.basePath,
UIConfig: "fixture/ui-config.json",
UIConfig: testCase.UIConfigPath,
})

server := httptest.NewServer(r)
Expand All @@ -94,7 +116,7 @@ func TestRegisterStaticHandler(t *testing.T) {
assert.Contains(t, respString, "Test Favicon") // this text is present in fixtures/favicon.ico

html := httpGet("") // get home page
assert.Contains(t, html, `JAEGER_CONFIG = {"x":"y"};`, "actual: %v", html)
assert.Contains(t, html, testCase.expectedUIConfig, "actual: %v", html)
assert.Contains(t, html, `JAEGER_VERSION = {"gitCommit":"","gitVersion":"","buildDate":""};`, "actual: %v", html)
assert.Contains(t, html, testCase.expectedBaseHTML, "actual: %v", html)

Expand Down Expand Up @@ -164,7 +186,7 @@ func TestHotReloadUIConfigTempFile(t *testing.T) {
func TestLoadUIConfig(t *testing.T) {
type testCase struct {
configFile string
expected map[string]interface{}
expected *loadedConfig
expectedError string
}

Expand All @@ -181,33 +203,69 @@ func TestLoadUIConfig(t *testing.T) {
}

run("no config", testCase{})
run("invalid config", testCase{
run("invalid json config", testCase{
configFile: "invalid",
expectedError: "cannot read UI config file invalid: open invalid: no such file or directory",
})
run("unsupported type", testCase{
configFile: "fixture/ui-config.toml",
expectedError: "unrecognized UI config file format fixture/ui-config.toml",
expectedError: "unrecognized UI config file format, expecting .js or .json file: fixture/ui-config.toml",
})
run("malformed", testCase{
configFile: "fixture/ui-config-malformed.json",
expectedError: "cannot parse UI config file fixture/ui-config-malformed.json: invalid character '=' after object key",
})
run("json", testCase{
configFile: "fixture/ui-config.json",
expected: map[string]interface{}{"x": "y"},
expected: &loadedConfig{
config: []byte(`JAEGER_CONFIG = {"x":"y"};`),
regexp: configPattern,
},
})
c, _ := json.Marshal(map[string]interface{}{
"menu": []interface{}{
map[string]interface{}{
"label": "GitHub",
"url": "https://github.com/jaegertracing/jaeger",
},
},
})
run("json-menu", testCase{
configFile: "fixture/ui-config-menu.json",
expected: map[string]interface{}{
"menu": []interface{}{
map[string]interface{}{
"label": "GitHub",
"url": "https://github.com/jaegertracing/jaeger",
},
},
expected: &loadedConfig{
config: append([]byte("JAEGER_CONFIG = "), append(c, byte(';'))...),
regexp: configPattern,
},
})
run("malformed js config", testCase{
configFile: "fixture/ui-config-malformed.js",
expectedError: "UI config file must define function UIConfig(): fixture/ui-config-malformed.js",
})
run("js", testCase{
configFile: "fixture/ui-config.js",
expected: &loadedConfig{
regexp: configJsPattern,
config: []byte(`function UIConfig(){
return {
x: "y"
}
}`)},
})
run("js-menu", testCase{
configFile: "fixture/ui-config-menu.js",
expected: &loadedConfig{
regexp: configJsPattern,
config: []byte(`function UIConfig(){
return {
menu: [
{
label: "GitHub",
url: "https://github.com/jaegertracing/jaeger"
}
]
}
}`)},
})
}

type fakeFile struct {
Expand Down