From 15e442e7367a845986a936f7539d79facc4afc8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20SZKIBA?= Date: Mon, 1 Jul 2024 18:34:23 +0200 Subject: [PATCH 1/3] refactor: removed JavaScript configuration customization feature --- .dashboard.js | 7 - .golangci.yml | 5 +- .../assets/packages/report/.testcontext.js | 3 +- dashboard/customize.go | 243 +----------------- dashboard/customize_test.go | 147 ----------- 5 files changed, 4 insertions(+), 401 deletions(-) delete mode 100644 .dashboard.js diff --git a/.dashboard.js b/.dashboard.js deleted file mode 100644 index 6a2c273..0000000 --- a/.dashboard.js +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs -// -// SPDX-License-Identifier: AGPL-3.0-only - -export default function (config) { - return config; -} diff --git a/.golangci.yml b/.golangci.yml index b8b20f5..7690d16 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -32,12 +32,9 @@ issues: - path: js\/modules\/k6\/http\/.*_test\.go linters: # k6/http module's tests are quite complex because they often have several nested levels. - # The module is in maintainance mode, so we don't intend to port the tests to a parallel version. + # The module is in maintenance mode, so we don't intend to port the tests to a parallel version. - paralleltest - tparallel - - linters: - - staticcheck # Tracked in https://github.com/grafana/xk6-grpc/issues/14 - text: "The entire proto file grpc/reflection/v1alpha/reflection.proto is marked as deprecated." - linters: - forbidigo text: 'use of `os\.(SyscallError|Signal|Interrupt)` forbidden' diff --git a/dashboard/assets/packages/report/.testcontext.js b/dashboard/assets/packages/report/.testcontext.js index 9bea59a..3f61832 100644 --- a/dashboard/assets/packages/report/.testcontext.js +++ b/dashboard/assets/packages/report/.testcontext.js @@ -8,7 +8,6 @@ import { readFileSync } from "fs" import { gunzipSync, gzipSync } from "zlib" import config from "../config/dist/config.json" -import custom from "../../../../.dashboard.js" let testdata = "" @@ -16,7 +15,7 @@ if (process.env.NODE_ENV != "production") { let data = readFileSync(".testdata.ndjson.gz") let text = gunzipSync(Buffer.from(data, "base64")).toString("utf8") - let conf = { event: "config", data: custom(config) } + let conf = { event: "config", data: config } testdata = gzipSync(JSON.stringify(conf) + "\n" + text).toString("base64") } diff --git a/dashboard/customize.go b/dashboard/customize.go index 78cd4e9..e900cf6 100644 --- a/dashboard/customize.go +++ b/dashboard/customize.go @@ -6,30 +6,14 @@ package dashboard import ( "encoding/json" - "errors" - "fmt" "io" - "path/filepath" - "reflect" - "strings" - "github.com/grafana/sobek" - "github.com/sirupsen/logrus" - "go.k6.io/k6/js/compiler" - "go.k6.io/k6/lib" "go.k6.io/k6/lib/fsext" ) -const ( - defaultConfig = ".dashboard.js" - defaultAltConfig = ".dashboard.json" -) +const defaultAltConfig = ".dashboard.json" func findDefaultConfig(fs fsext.Fs) string { - if exists(fs, defaultConfig) { - return defaultConfig - } - if exists(fs, defaultAltConfig) { return defaultAltConfig } @@ -46,11 +30,7 @@ func customize(uiConfig json.RawMessage, proc *process) (json.RawMessage, error) } } - if filepath.Ext(filename) == ".json" { - return loadConfigJSON(filename, proc) - } - - return loadConfigJS(filename, uiConfig, proc) + return loadConfigJSON(filename, proc) } func loadConfigJSON(filename string, proc *process) (json.RawMessage, error) { @@ -80,222 +60,3 @@ func exists(fs fsext.Fs, filename string) bool { return true } - -type configLoader struct { - runtime *sobek.Runtime - compiler *compiler.Compiler - defaultConfig *sobek.Object - proc *process -} - -func newConfigLoader(defaultConfig json.RawMessage, proc *process) (*configLoader, error) { - comp := compiler.New(proc.logger) - - comp.Options.CompatibilityMode = lib.CompatibilityModeExtended - comp.Options.Strict = true - - con := newConfigConsole(proc.logger) - - runtime := sobek.New() - - runtime.SetFieldNameMapper(sobek.UncapFieldNameMapper()) - - if err := runtime.Set("console", con); err != nil { - return nil, err - } - - def, err := toObject(runtime, defaultConfig) - if err != nil { - return nil, err - } - - loader := &configLoader{ - runtime: runtime, - compiler: comp, - defaultConfig: def, - proc: proc, - } - - return loader, nil -} - -func (loader *configLoader) load(filename string) (json.RawMessage, error) { - file, err := loader.proc.fs.Open(filename) - if err != nil { - return nil, err - } - - src, err := io.ReadAll(file) - if err != nil { - return nil, err - } - - val, err := loader.eval(src, filename) - if err != nil { - return nil, err - } - - obj := val.ToObject(loader.runtime) - - return obj.MarshalJSON() -} - -func isObject(val sobek.Value) bool { - return val != nil && val.ExportType() != nil && val.ExportType().Kind() == reflect.Map -} - -func (loader *configLoader) eval(src []byte, filename string) (*sobek.Object, error) { - prog, _, err := loader.compiler.Compile(string(src), filename, false) - if err != nil { - return nil, err - } - - exports := loader.runtime.NewObject() - module := loader.runtime.NewObject() - - if err = module.Set("exports", exports); err != nil { - return nil, err - } - - val, err := loader.runtime.RunProgram(prog) - if err != nil { - return nil, err - } - - call, isCallable := sobek.AssertFunction(val) - if !isCallable { - return nil, fmt.Errorf("%w, file: %s", errNotFunction, filename) - } - - _, err = call(exports, module, exports) - if err != nil { - return nil, err - } - - def := exports.Get("default") - if def == nil { - return nil, fmt.Errorf("%w, file: %s", errNoExport, filename) - } - - if call, isCallable = sobek.AssertFunction(def); isCallable { - def, err = call(exports, loader.defaultConfig) - if err != nil { - return nil, err - } - - if !isObject(def) { - return nil, errConfigNotObject - } - } - - return def.ToObject(loader.runtime), nil -} - -// toObject use JavaScript JSON.parse to create native goja object -// there could be a better solution.... (but Object.UnmarshallJSON is missing). -func toObject(runtime *sobek.Runtime, bin json.RawMessage) (*sobek.Object, error) { - val := runtime.Get("JSON").ToObject(runtime).Get("parse") - - call, _ := sobek.AssertFunction(val) - - val, err := call(runtime.GlobalObject(), runtime.ToValue(string(bin))) - if err != nil { - return nil, err - } - - return val.ToObject(runtime), nil -} - -func loadConfigJS( - filename string, - config json.RawMessage, - proc *process, -) (json.RawMessage, error) { - loader, err := newConfigLoader(config, proc) - if err != nil { - return nil, err - } - - return loader.load(filename) -} - -// configConsole represents a JS configConsole implemented as a logrus.Logger. -type configConsole struct { - logger logrus.FieldLogger -} - -// Creates a console with the standard logrus logger. -func newConfigConsole(logger logrus.FieldLogger) *configConsole { - return &configConsole{logger.WithField("source", "console").WithField("extension", "dashboard")} -} - -func (c configConsole) log(level logrus.Level, args ...sobek.Value) { - var strs strings.Builder - - for i := 0; i < len(args); i++ { - if i > 0 { - strs.WriteString(" ") - } - - strs.WriteString(c.valueString(args[i])) - } - - msg := strs.String() - - switch level { - case logrus.DebugLevel: - c.logger.Debug(msg) - - case logrus.InfoLevel: - c.logger.Info(msg) - - case logrus.WarnLevel: - c.logger.Warn(msg) - - case logrus.ErrorLevel: - c.logger.Error(msg) - - default: - c.logger.Info(msg) - } -} - -func (c configConsole) Log(args ...sobek.Value) { - c.Info(args...) -} - -func (c configConsole) Debug(args ...sobek.Value) { - c.log(logrus.DebugLevel, args...) -} - -func (c configConsole) Info(args ...sobek.Value) { - c.log(logrus.InfoLevel, args...) -} - -func (c configConsole) Warn(args ...sobek.Value) { - c.log(logrus.WarnLevel, args...) -} - -func (c configConsole) Error(args ...sobek.Value) { - c.log(logrus.ErrorLevel, args...) -} - -func (c configConsole) valueString(value sobek.Value) string { - mv, ok := value.(json.Marshaler) - if !ok { - return value.String() - } - - bin, err := json.Marshal(mv) - if err != nil { - return value.String() - } - - return string(bin) -} - -var ( - errNotFunction = errors.New("not a function") - errNoExport = errors.New("missing default export") - errConfigNotObject = errors.New("returned configuration is not an object") -) diff --git a/dashboard/customize_test.go b/dashboard/customize_test.go index d210a62..6a1f02c 100644 --- a/dashboard/customize_test.go +++ b/dashboard/customize_test.go @@ -8,9 +8,6 @@ import ( _ "embed" "testing" - "github.com/grafana/sobek" - "github.com/sirupsen/logrus" - logtest "github.com/sirupsen/logrus/hooks/test" "github.com/stretchr/testify/assert" "github.com/tidwall/gjson" ) @@ -47,149 +44,5 @@ func Test_customize(t *testing.T) { assert.False(t, gjson.GetBytes(conf, `tabs.#(id="custom")`).Exists()) } -func Test_customize_env_found(t *testing.T) { - t.Setenv("XK6_DASHBOARD_CONFIG", "testdata/customize/config-custom.js") - - th := helper(t).osFs() - - conf, err := customize(testconfig, th.proc) - - assert.NoError(t, err) - - assert.True(t, gjson.GetBytes(conf, `tabs.#(id="custom")`).Exists()) - - t.Setenv("XK6_DASHBOARD_CONFIG", "testdata/customize/config.json") - - assert.NoError(t, err) -} - //go:embed testdata/customize/config/config.json var testconfig []byte - -func TestConfigInReadme(t *testing.T) { - t.Parallel() - - th := helper(t).osFs() - - conf, err := loadConfigJS("../.dashboard.js", testconfig, th.proc) - - assert.NoError(t, err) - - assert.NotNil(t, gjson.GetBytes(conf, "tabs.custom")) - - loader, err := newConfigLoader(testconfig, th.proc) - - assert.NoError(t, err) - - _, err = loader.load("testdata/customize/config-custom.js") - - assert.NoError(t, err) -} - -func assertMessageAndLevel(t *testing.T, expr string, message string, level logrus.Level) { - t.Helper() - - runtime := sobek.New() - - runtime.SetFieldNameMapper(sobek.UncapFieldNameMapper()) - - logger, hook := logtest.NewNullLogger() - _ = runtime.Set("console", newConfigConsole(logger)) - - logger.SetLevel(level) - - _, err := runtime.RunString(expr) - - assert.NoError(t, err) - - entry := hook.LastEntry() - assert.NotNil(t, entry) - - assert.Equal(t, message, entry.Message) - assert.Equal(t, level, entry.Level) -} - -func TestConfigConsoleLevels(t *testing.T) { - t.Parallel() - - assertMessageAndLevel(t, `console.log("a")`, "a", logrus.InfoLevel) - assertMessageAndLevel(t, `console.debug("a")`, "a", logrus.DebugLevel) - assertMessageAndLevel(t, `console.info("a")`, "a", logrus.InfoLevel) - assertMessageAndLevel(t, `console.warn("a")`, "a", logrus.WarnLevel) - assertMessageAndLevel(t, `console.error("a")`, "a", logrus.ErrorLevel) - - assertMessageAndLevel(t, `console.log("a", "b")`, "a b", logrus.InfoLevel) -} - -func TestConfigConsoleJSON(t *testing.T) { - t.Parallel() - - assertMessageAndLevel( - t, - `let obj = {foo:"bar"}; console.log(obj)`, - `{"foo":"bar"}`, - logrus.InfoLevel, - ) -} - -func Test_loadConfigJS_error(t *testing.T) { - t.Parallel() - - th := helper(t).osFs() - - conf, err := loadConfigJSON("testdata/customize/config.json", th.proc) - - assert.NoError(t, err) - - assert.NotNil(t, gjson.GetBytes(conf, "tabs.custom")) - - _, err = loadConfigJS("testdata/customize/config-bad.json", testconfig, th.proc) - - assert.Error(t, err) - - _, err = loadConfigJS("testdata/customize/config-not-exists.json", testconfig, th.proc) - - assert.Error(t, err) - - conf, err = loadConfigJS( - "testdata/customize/config-custom.js", - []byte("42='foo'"), - th.proc, - ) - - assert.Nil(t, conf) - assert.Error(t, err) -} - -func Test_configLoader_eval_error(t *testing.T) { - t.Parallel() - - th := helper(t).osFs() - - loader, err := newConfigLoader(testconfig, th.proc) - - assert.NoError(t, err) - - obj, err := loader.eval([]byte("invalid script"), "") - - assert.Error(t, err) - assert.Nil(t, obj) - - // no default export - obj, err = loader.eval([]byte("let answer = 42"), "") - - assert.Error(t, err) - assert.Nil(t, obj) - - // no return value from export function - obj, err = loader.eval([]byte("export default function() {}"), "") - - assert.Error(t, err) - assert.Nil(t, obj) - - // error in default export function - obj, err = loader.eval([]byte("export default function() {throw Error()}"), "") - - assert.Error(t, err) - assert.Nil(t, obj) -} From edae976140206da0d0209a7f3ff0146e7b54433e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20SZKIBA?= Date: Mon, 1 Jul 2024 18:36:37 +0200 Subject: [PATCH 2/3] tidy --- go.mod | 6 ------ go.sum | 15 --------------- 2 files changed, 21 deletions(-) diff --git a/go.mod b/go.mod index 3fb5ee8..c033452 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/grafana/xk6-dashboard go 1.20 require ( - github.com/grafana/sobek v0.0.0-20240607083612-4f0cd64f4e78 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/r3labs/sse/v2 v2.10.0 github.com/sirupsen/logrus v1.9.3 @@ -17,14 +16,9 @@ require ( require ( github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dlclark/regexp2 v1.9.0 // indirect - github.com/dop251/goja v0.0.0-20240516125602-ccbae20bcec2 // indirect - github.com/evanw/esbuild v0.21.2 // indirect github.com/fatih/color v1.16.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect - github.com/google/pprof v0.0.0-20230728192033-2ba5b33183c6 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/go.sum b/go.sum index fee1ee4..87dd03b 100644 --- a/go.sum +++ b/go.sum @@ -5,12 +5,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.9.0 h1:pTK/l/3qYIKaRXuHnEnIf7Y5NxfRPfpb7dis6/gdlVI= -github.com/dlclark/regexp2 v1.9.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dop251/goja v0.0.0-20240516125602-ccbae20bcec2 h1:OFTHt+yJDo/uaIKMGjEKzc3DGhrpQZoqvMUIloZv6ZY= -github.com/dop251/goja v0.0.0-20240516125602-ccbae20bcec2/go.mod h1:o31y53rb/qiIAONF7w3FHJZRqqP3fzHUr1HqanthByw= -github.com/evanw/esbuild v0.21.2 h1:CLplcGi794CfHLVmUbvVfTMKkykm+nyIHU8SU60KUTA= -github.com/evanw/esbuild v0.21.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -18,16 +12,8 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= -github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/pprof v0.0.0-20230728192033-2ba5b33183c6 h1:ZgoomqkdjGbQ3+qQXCkvYMCDvGDNg2k5JJDjjdTB6jY= -github.com/google/pprof v0.0.0-20230728192033-2ba5b33183c6/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/grafana/sobek v0.0.0-20240606091932-2da0e9e5f3e7 h1:Ed0df3dkkPsjL0RKagJAv/821vrTBiB6GBk+198pxi4= -github.com/grafana/sobek v0.0.0-20240606091932-2da0e9e5f3e7/go.mod h1:6ZH0b0iOxyigeTh+/IlGoL0Hd3lVXA94xoXf0ldNgCM= -github.com/grafana/sobek v0.0.0-20240607083612-4f0cd64f4e78 h1:rVCZdB+13G+aQoGm3CBVaDGl0uxZxfjvQgEJy4IeHTA= -github.com/grafana/sobek v0.0.0-20240607083612-4f0cd64f4e78/go.mod h1:6ZH0b0iOxyigeTh+/IlGoL0Hd3lVXA94xoXf0ldNgCM= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= @@ -126,7 +112,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/guregu/null.v3 v3.3.0 h1:8j3ggqq+NgKt/O7mbFVUFKUMWN+l1AmT5jQmJ6nPh2c= gopkg.in/guregu/null.v3 v3.3.0/go.mod h1:E4tX2Qe3h7QdL+uZ3a0vqvYwKQsRSQKM5V4YltdgH9Y= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From 6f74cc1673aaaeb31db8ff0ff52a55e5b21a0ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20SZKIBA?= Date: Mon, 1 Jul 2024 18:38:29 +0200 Subject: [PATCH 3/3] docs: prepare v0.7.5 release notes --- releases/v0.7.5.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 releases/v0.7.5.md diff --git a/releases/v0.7.5.md b/releases/v0.7.5.md new file mode 100644 index 0000000..ff03150 --- /dev/null +++ b/releases/v0.7.5.md @@ -0,0 +1,8 @@ +xk6-dashboard `v0.7.5` is here 🎉! This is an internal maintenance release. +(no bug fixes, no new features) + +## Breaking changes + +### Configuration customization from JavaScript code has been removed + +Until now, the dashboard configuration could be customized with a JavaScript code. Supporting this feature after switching the JavaScript interpreter (goja to sobek) causes serious difficulties. Since this feature is rarely used, it is easier to drop it than to support it.