diff --git a/Makefile b/Makefile index f9a54d4..f8595ba 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ intermediate-docs: @go run ./main.go -h > ./docs/_intermediate/help.txt @go run ./main.go completion -h > ./docs/_intermediate/completion.txt @go run ./main.go lint -h > ./docs/_intermediate/lint.txt - @go run ./main.go rules > ./docs/_intermediate/rules.txt + @go run ./main.go rules --experimental > ./docs/_intermediate/rules.txt @echo "Can't automate everything, please replace the #Rules section of index.md with the contents of ./docs/_intermediate/rules.txt" embedmd: diff --git a/docs/index.md b/docs/index.md index e781f51..b0eabee 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,6 +2,24 @@ All Commands: [embedmd]:# (_intermediate/help.txt) +```txt +A command-line application to lint Grafana dashboards. + +Usage: + dashboard-linter [flags] + dashboard-linter [command] + +Available Commands: + completion Generate the autocompletion script for the specified shell + help Help about any command + lint Lint a dashboard + rules Print documentation about each lint rule. + +Flags: + -h, --help help for dashboard-linter + +Use "dashboard-linter [command] --help" for more information about a command. +``` ```txt A command-line application to lint Grafana dashboards. @@ -25,6 +43,24 @@ Use "dashboard-linter [command] --help" for more information about a command. ## Completion [embedmd]:# (_intermediate/completion.txt) +```txt +Generate the autocompletion script for dashboard-linter for the specified shell. +See each sub-command's help for details on how to use the generated script. + +Usage: + dashboard-linter completion [command] + +Available Commands: + bash Generate the autocompletion script for bash + fish Generate the autocompletion script for fish + powershell Generate the autocompletion script for powershell + zsh Generate the autocompletion script for zsh + +Flags: + -h, --help help for completion + +Use "dashboard-linter completion [command] --help" for more information about a command. +``` ```txt Generate the autocompletion script for dashboard-linter for the specified shell. @@ -48,6 +84,21 @@ Use "dashboard-linter completion [command] --help" for more information about a ## Lint [embedmd]:# (_intermediate/lint.txt) +```txt +Returns warnings or errors for dashboard which do not adhere to accepted standards + +Usage: + dashboard-linter lint [dashboard.json] [flags] + +Flags: + -c, --config string path to a configuration file + --experimental enable experimental rules + --fix automatically fix problems if possible + -h, --help help for lint + --stdin read from stdin + --strict fail upon linting error or warning + --verbose show more information about linting +``` ```txt Returns warnings or errors for dashboard which do not adhere to accepted standards @@ -57,6 +108,7 @@ Usage: Flags: -c, --config string path to a configuration file + --experimental enable experimental rules --fix automatically fix problems if possible -h, --help help for lint --stdin read from stdin @@ -68,23 +120,34 @@ Flags: The linter implements the following rules: -* [template-datasource-rule](./rules/template-datasource-rule.md) - Checks that the dashboard has a templated datasource. -* [template-job-rule](./rules/template-job-rule.md) - Checks that the dashboard has a templated job. -* [template-instance-rule](./rules/template-instance-rule.md) - Checks that the dashboard has a templated instance. -* [template-label-promql-rule](./rules/template-label-promql-rule.md) - Checks that the dashboard templated labels have proper PromQL expressions. -* [template-on-time-change-reload-rule](./rules/template-on-time-change-reload-rule.md) - Checks that the dashboard template variables are configured to reload on time change. -* [panel-datasource-rule](./rules/panel-datasource-rule.md) - Checks that each panel uses the templated datasource. -* [panel-title-description-rule](./rules/panel-title-description-rule.md) - Checks that each panel has a title and description. -* [panel-units-rule](./rules/panel-units-rule.md) - Checks that each panel uses has valid units defined. -* `panel-no-targets-rule` - Checks that each panel has at least one target. -* [target-logql-rule](./rules/target-logql-rule.md) - Checks that each target uses a valid LogQL query. -* [target-logql-auto-rule](./rules/target-logql-auto-rule.md) - Checks that each Loki target uses $__auto for range vectors when appropriate. -* [target-promql-rule](./rules/target-promql-rule.md) - Checks that each target uses a valid PromQL query. -* [target-rate-interval-rule](./rules/target-rate-interval-rule.md) - Checks that each target uses $__rate_interval. -* [target-job-rule](./rules/target-job-rule.md) - Checks that every PromQL query has a job matcher. -* [target-instance-rule](./rules/target-instance-rule.md) - Checks that every PromQL query has a instance matcher. -* `target-counter-agg-rule` - Checks that any counter metric (ending in _total) is aggregated with rate, irate, or increase. -* `uneditable-dashboard` - Checks that the dashboard is not editable. +* [template-datasource-rule](./rules/template-datasource-rule) - ``stable`` - Checks that the dashboard has a templated datasource. +* [template-job-rule](./rules/template-job-rule) - `stable` - Checks that the dashboard has a templated job. +* [template-instance-rule](./rules/template-instance-rule) - `stable` - Checks that the dashboard has a templated instance. +* [template-label-promql-rule](./rules/template-label-promql-rule) - `stable` - Checks that the dashboard templated labels have proper PromQL expressions. +* [template-on-time-change-reload-rule](./rules/template-on-time-change-reload-rule) - `stable` - Checks that the dashboard template variables are configured to reload on time change. +* [panel-datasource-rule](./rules/panel-datasource-rule) - `stable` - Checks that each panel uses the templated datasource. +* [panel-title-description-rule](./rules/panel-title-description-rule) - `stable` - Checks that each panel has a title and description. +* [panel-units-rule](./rules/panel-units-rule) - `stable` - Checks that each panel uses has valid units defined. +* [panel-no-targets-rule](./rules/panel-no-targets-rule) - `stable` - Checks that each panel has at least one target. +* [target-promql-rule](./rules/target-promql-rule) - `stable` - Checks that each target uses a valid PromQL query. +* [target-rate-interval-rule](./rules/target-rate-interval-rule) - `stable` - Checks that each target uses $__rate_interval. +* [target-job-rule](./rules/target-job-rule) - `stable` - Checks that every PromQL query has a job matcher. +* [target-instance-rule](./rules/target-instance-rule) - `stable` - Checks that every PromQL query has a instance matcher. +* [target-counter-agg-rule](./rules/target-counter-agg-rule) - `stable` - Checks that any counter metric (ending in _total) is aggregated with rate, irate, or increase. +* [uneditable-dashboard-rule](./rules/uneditable-dashboard-rule) - `stable` - Checks that the dashboard is not editable. +* [target-logql-rule](./rules/target-logql-rule) - `experimental` - Checks that each target uses a valid LogQL query. +* [target-logql-auto-rule](./rules/target-logql-auto-rule) - `experimental` - Checks that each Loki target uses $__auto for range vectors when appropriate. +* [target-required-matchers-rule](./rules/target-required-matchers-rule) - `experimental` - Checks that target PromQL query has the required matchers +* [template-required-variables-rule](./rules/template-required-variables-rule) - `experimental` - Checks that the dashboard has a template variable for required variables or matchers that use variables + +## Rule stability +- **Stable** rules have gone through testing and been widely adopted. + +- **Experimental** rules are for new and experimental features. + These rules are not enabled by default, but can be enabled by providing the `experimental` flag. + Allowing early adopters to gain confidence with new features. + +- **Deprecated** rules may be removed or replaced when they are marked as deprecated. ## Related Rules diff --git a/docs/rules/target-required-matchers-rule.md b/docs/rules/target-required-matchers-rule.md new file mode 100644 index 0000000..51fd807 --- /dev/null +++ b/docs/rules/target-required-matchers-rule.md @@ -0,0 +1,20 @@ +# target-required-matchers-rule +Checks that each PromQL query has a the matchers specified in rule settings. This rule is experimental and is designed to work with Prometheus datasources. + +## Rule Settings + +```yaml +settings: + target-required-matchers-rule: + matchers: + - cluster=~"$cluster" + - someLabel="someValue" +``` +Legacy config example for job and instance +```yaml +settings: + target-required-matchers-rule: + matchers: + - job + - instance +``` \ No newline at end of file diff --git a/docs/rules/template-required-variables-rule.md b/docs/rules/template-required-variables-rule.md new file mode 100644 index 0000000..6c05500 --- /dev/null +++ b/docs/rules/template-required-variables-rule.md @@ -0,0 +1,31 @@ +# template-required-variables-rule +Checks that each dashboard has a templated variable based on provided rule settings and detected variable usage for the target-required-matchers-rule. + +# Best Practice +The rule ensures all of the following conditions. + +* The dashboard template exists. +* The dashboard template is named `xxx`. +* The dashboard template is labeled `xxx`. +* The dashboard template uses a templated datasource, specifically named `$datasource`. +* The dashboard template uses a Prometheus query to find available matching instances. +* The dashboard template is multi select +* The dashboard template has an allValue of `.+` + +## Rule Settings + +```yaml +settings: + template-required-variables-rule: + variables: + - cluster + - namespace +``` +Legacy config example for job and instance +```yaml +settings: + template-required-variables-rule: + variables: + - job + - instance +``` \ No newline at end of file diff --git a/go.mod b/go.mod index 2e91c84..a702f0a 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,10 @@ module github.com/grafana/dashboard-linter -go 1.21.8 - -toolchain go1.23.1 +go 1.23.1 require ( - github.com/grafana/loki/v3 v3.2.0 + github.com/grafana/grafana-foundation-sdk/go v0.0.0-20241106131137-6420b47bdc17 + github.com/grafana/loki/v3 v3.2.1 github.com/prometheus/prometheus v0.54.1 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 @@ -15,6 +14,27 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) +require ( + github.com/aws/aws-sdk-go v1.54.19 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/grafana/dskit v0.0.0-20240905221822-931a021fb06b // indirect + github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56 // indirect + github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 // indirect + github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608 // indirect + github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/prometheus/common/sigv4 v0.1.0 // indirect + github.com/prometheus/exporter-toolkit v0.11.0 // indirect + go.opentelemetry.io/collector/pdata v1.12.0 // indirect + go.uber.org/zap v1.21.0 // indirect + go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect + golang.org/x/mod v0.19.0 // indirect + golang.org/x/tools v0.23.0 // indirect +) + require ( cloud.google.com/go v0.115.1 // indirect cloud.google.com/go/auth v0.9.0 // indirect @@ -61,13 +81,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect - github.com/gorilla/mux v1.8.1 // indirect - github.com/grafana/dskit v0.0.0-20240905221822-931a021fb06b // indirect - github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56 // indirect - github.com/grafana/grafana-foundation-sdk/go v0.0.0-20241101005901-83e3491f2a70 // indirect - github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 // indirect - github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608 // indirect - github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/hashicorp/consul/api v1.29.4 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -85,7 +98,6 @@ require ( github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -98,8 +110,6 @@ require ( github.com/mitchellh/reflectwalk v1.0.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e // indirect github.com/opentracing-contrib/go-stdlib v1.0.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect @@ -107,10 +117,10 @@ require ( github.com/pires/go-proxyproto v0.7.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/alertmanager v0.27.0 github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect - github.com/prometheus/exporter-toolkit v0.11.0 // indirect + github.com/prometheus/common v0.55.0 github.com/prometheus/procfs v0.15.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect @@ -133,7 +143,6 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.12 // indirect go.etcd.io/etcd/client/v3 v3.5.12 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/collector/pdata v1.12.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect go.opentelemetry.io/otel v1.28.0 // indirect @@ -141,17 +150,13 @@ require ( go.opentelemetry.io/otel/trace v1.28.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.21.0 // indirect - go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect - golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/time v0.6.0 // indirect - golang.org/x/tools v0.23.0 // indirect google.golang.org/api v0.193.0 // indirect google.golang.org/genproto v0.0.0-20240820151423-278611b39280 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240820151423-278611b39280 // indirect diff --git a/go.sum b/go.sum index 36e9d2b..07d978a 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,47 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ= cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc= cloud.google.com/go/auth v0.9.0 h1:cYhKl1JUhynmxjXfrk4qdPc6Amw7i+GC9VLflgT0p5M= cloud.google.com/go/auth v0.9.0/go.mod h1:2HsApZBr9zGZhC9QAXsYVYaWk8kNUt37uny+XVKi7wM= cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/iam v1.2.0 h1:kZKMKVNk/IsSSc/udOb83K0hL/Yh/Gcqpz+oAkoIFN8= cloud.google.com/go/iam v1.2.0/go.mod h1:zITGuWgsLZxd8OwAlX+eMFgZDXzBm7icj1PVTYG766Q= cloud.google.com/go/longrunning v0.6.0 h1:mM1ZmaNsQsnb+5n1DNPeL0KwQd9jQRqSqSDEkBZr+aI= cloud.google.com/go/longrunning v0.6.0/go.mod h1:uHzSZqW89h7/pasCWNYdUpwGz3PcVWhrWupreVPYLts= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs= cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -44,6 +75,7 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 h1:t3eaIm0rUkzbrIewtiFmMK5RXHej2XnoXNhxVsAYUfg= github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= @@ -57,12 +89,14 @@ github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJ github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.54.19 h1:tyWV+07jagrNiCcGRzRhdtVjQs7Vy41NwsuOcl0IbVI= github.com/aws/aws-sdk-go v1.54.19/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -76,6 +110,9 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -119,12 +156,16 @@ github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyT github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -148,39 +189,66 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -189,6 +257,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -197,14 +267,14 @@ github.com/grafana/dskit v0.0.0-20240905221822-931a021fb06b h1:x2HCzk29I0o5pRPfq github.com/grafana/dskit v0.0.0-20240905221822-931a021fb06b/go.mod h1:SPLNCARd4xdjCkue0O6hvuoveuS1dGJjDnfxYe405YQ= github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56 h1:X8IKQ0wu40wpvYcKfBcc5T4QnhdQjUhtUtB/1CY89lE= github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU= -github.com/grafana/grafana-foundation-sdk/go v0.0.0-20241101005901-83e3491f2a70 h1:69GI3KsF851YnwYp6zHdsskcGp3ZnGsWc+ve8vMp1mc= -github.com/grafana/grafana-foundation-sdk/go v0.0.0-20241101005901-83e3491f2a70/go.mod h1:WtWosval1KCZP9BGa42b8aVoJmVXSg0EvQXi9LDSVZQ= +github.com/grafana/grafana-foundation-sdk/go v0.0.0-20241106131137-6420b47bdc17 h1:+QqxnP5g2QzomcXrhmwbpHDZUKxvKtwQjgI+fi/UL5Y= +github.com/grafana/grafana-foundation-sdk/go v0.0.0-20241106131137-6420b47bdc17/go.mod h1:WtWosval1KCZP9BGa42b8aVoJmVXSg0EvQXi9LDSVZQ= github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 h1:NznuPwItog+rwdVg8hAuGKP29ndRSzJAwhxKldkP8oQ= github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32/go.mod h1:796sq+UcONnSlzA3RtlBZ+b/hrerkZXiEmO8oMjyRwY= github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608 h1:ZYk42718kSXOiIKdjZKljWLgBpzL5z1yutKABksQCMg= github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608/go.mod h1:f3JSoxBTPXX5ec4FxxeC19nTBSxoTz+cBgS3cYLMcr0= -github.com/grafana/loki/v3 v3.2.0 h1:QFOERFfYtWDh4/9Z3Hi2x+h91HPLKvqMaIePEbfjhMM= -github.com/grafana/loki/v3 v3.2.0/go.mod h1:WvdLl6wOS+yahaeQY+xhD2m2XzkHDfKr5FZaX7D/X2Y= +github.com/grafana/loki/v3 v3.2.1 h1:VB7u+KHfvL5aHAxgoVBvz5wVhsdGuqKC7uuOFOOe7jw= +github.com/grafana/loki/v3 v3.2.1/go.mod h1:WvdLl6wOS+yahaeQY+xhD2m2XzkHDfKr5FZaX7D/X2Y= github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= @@ -248,6 +318,7 @@ github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -260,6 +331,7 @@ github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= @@ -267,19 +339,27 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -371,9 +451,13 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/alertmanager v0.27.0 h1:V6nTa2J5V4s8TG4C4HtrBP/WNSebCCTYGGv4qecA/+I= +github.com/prometheus/alertmanager v0.27.0/go.mod h1:8Ia/R3urPmbzJ8OsdvmZvIprDwvwmYCmUbwBL+jlPOE= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -384,6 +468,9 @@ github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= @@ -393,10 +480,13 @@ github.com/prometheus/exporter-toolkit v0.11.0/go.mod h1:BVnENhnNecpwoTLiABx7mrP github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/prometheus v0.54.1 h1:vKuwQNjnYN2/mDoWfHXDhAsz/68q/dQDb+YbcEqU7MQ= github.com/prometheus/prometheus v0.54.1/go.mod h1:xlLByHhk2g3ycakQGrMaU8K7OySZx98BzeCR99991NY= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -413,6 +503,7 @@ github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXY github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -463,7 +554,9 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -477,6 +570,11 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.12 h1:EYDL6pWwyOsylrQyLp2w+HkQ46ATiOvoEdMarin go.etcd.io/etcd/client/pkg/v3 v3.5.12/go.mod h1:seTzl2d9APP8R5Y2hFL3NVlD6qC/dOT+3kvrqPyTas4= go.etcd.io/etcd/client/v3 v3.5.12 h1:v5lCPXn1pf1Uu3M4laUE2hp/geOTc5uPcYYsNe1lDxg= go.etcd.io/etcd/client/v3 v3.5.12/go.mod h1:tSbBCakoWmmddL+BKVAJHa9km+O/E+bumDe9mSbPiqw= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/collector/pdata v1.12.0 h1:Xx5VK1p4VO0md8MWm2icwC1MnJ7f8EimKItMWw46BmA= @@ -509,6 +607,7 @@ go4.org/netipx v0.0.0-20230125063823-8449b0a6169f/go.mod h1:tgPU4N2u9RByaTN3NC2p golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -521,7 +620,14 @@ golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= @@ -529,10 +635,20 @@ golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMx golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -542,32 +658,61 @@ golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190921015927-1a5e07d1ff72/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= @@ -581,17 +726,42 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -607,7 +777,9 @@ golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -615,6 +787,9 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -623,12 +798,45 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190424220101-1e8e1cfdf96b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -642,14 +850,60 @@ gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJ gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.193.0 h1:eOGDoJFsLU+HpCBaDJex2fWiYujAw9KbXgpOAMePoUs= google.golang.org/api v0.193.0/go.mod h1:Po3YMV1XZx+mTku3cfJrlIYR03wiGrCOsdpC67hjZvw= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20240820151423-278611b39280 h1:oKt8r1ZvaPqBe3oeGTdyx1iNjuBS+VJcc9QdU1CD3d8= google.golang.org/genproto v0.0.0-20240820151423-278611b39280/go.mod h1:wxEc5TmU9JSLs1rSqG4z1YzeSNigp/9yIojIPuZVvKQ= google.golang.org/genproto/googleapis/api v0.0.0-20240820151423-278611b39280 h1:YDFM9oOjiFhaMAVgbDxfxW+66nRrsvzQzJ51wp3OxC0= @@ -658,10 +912,18 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240820151423-278611b39280 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20240820151423-278611b39280/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= @@ -673,7 +935,9 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -683,6 +947,7 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= @@ -700,7 +965,12 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU= k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU= k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= @@ -710,4 +980,7 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/lint/configuration.go b/lint/configuration.go index 5e84074..a131b25 100644 --- a/lint/configuration.go +++ b/lint/configuration.go @@ -11,10 +11,11 @@ import ( // ConfigurationFile contains a map for rule exclusions, and warnings, where the key is the // rule name to be excluded or downgraded to a warning type ConfigurationFile struct { - Exclusions map[string]*ConfigurationRuleEntries `yaml:"exclusions"` - Warnings map[string]*ConfigurationRuleEntries `yaml:"warnings"` - Verbose bool `yaml:"-"` - Autofix bool `yaml:"-"` + Exclusions map[string]*ConfigurationRuleEntries `yaml:"exclusions"` + Warnings map[string]*ConfigurationRuleEntries `yaml:"warnings"` + RuleSettings ConfigurationRuleSettings `yaml:"settings,omitempty"` + Verbose bool `yaml:"-"` + Autofix bool `yaml:"-"` } type ConfigurationRuleEntries struct { @@ -22,6 +23,11 @@ type ConfigurationRuleEntries struct { Entries []ConfigurationEntry `json:"entries,omitempty"` } +type ConfigurationRuleSettings struct { + TemplateRequiredVariablesRule *TemplateRequiredVariablesRuleSettings `yaml:"template-required-variables-rule,omitempty"` + TargetRequiredMatchersRule *TargetRequiredMatchersRuleSettings `yaml:"target-required-matchers-rule,omitempty"` +} + // ConfigurationEntry will exist precisely once for every instance of a rule violation you wish // exclude or downgrade to a warning. Each ConfigurationEntry will have to be an *exact* match // to the combination of attributes set. Reason will not be evaluated, and is an opportunity for @@ -124,6 +130,10 @@ func NewConfigurationFile() *ConfigurationFile { return &ConfigurationFile{ Exclusions: map[string]*ConfigurationRuleEntries{}, Warnings: map[string]*ConfigurationRuleEntries{}, + RuleSettings: ConfigurationRuleSettings{ + TemplateRequiredVariablesRule: &TemplateRequiredVariablesRuleSettings{}, + TargetRequiredMatchersRule: &TargetRequiredMatchersRuleSettings{}, + }, } } diff --git a/lint/constants.go b/lint/constants.go index ea96b9c..da9678d 100644 --- a/lint/constants.go +++ b/lint/constants.go @@ -10,3 +10,9 @@ const ( panelTypeTimeSeries = "timeseries" panelTypeTimeTable = "table" ) + +const ( + ruleStabilityStable = "stable" + ruleStabilityExperimental = "experimental" + ruleStabilityDeprecated = "deprecated" +) diff --git a/lint/lint.go b/lint/lint.go index 686be48..f9d4d69 100644 --- a/lint/lint.go +++ b/lint/lint.go @@ -265,7 +265,7 @@ type Dashboard struct { } `json:"annotations"` Rows []Row `json:"rows,omitempty"` Panels []Panel `json:"panels,omitempty"` - Editable bool `json:"editable,omitempty"` + Editable bool `json:"editable"` // Do not omitempty, since false is seen as empty, and if it is not included, it defaults to true. } // GetPanels returns the all panels whether they are nested in the (now deprecated) "rows" property or diff --git a/lint/results.go b/lint/results.go index 9d3832e..cdebfac 100644 --- a/lint/results.go +++ b/lint/results.go @@ -34,12 +34,26 @@ type TargetRuleResults struct { Results []TargetResult } +func TargetMessage(d Dashboard, p Panel, t Target, message string) string { + return fmt.Sprintf("Dashboard '%s', panel '%s', target idx '%d' %s", d.Title, p.Title, t.Idx, message) +} + func (r *TargetRuleResults) AddError(d Dashboard, p Panel, t Target, message string) { r.Results = append(r.Results, TargetResult{ Result: Result{ Severity: Error, - Message: fmt.Sprintf("Dashboard '%s', panel '%s', target idx '%d' %s", d.Title, p.Title, t.Idx, message), + Message: TargetMessage(d, p, t, message), + }, + }) +} + +func (r *TargetRuleResults) AddFixableError(d Dashboard, p Panel, t Target, message string, fix func(Dashboard, Panel, *Target)) { + r.Results = append(r.Results, TargetResult{ + Result: Result{ + Severity: Error, + Message: TargetMessage(d, p, t, message), }, + Fix: fix, }) } diff --git a/lint/rule_panel_datasource.go b/lint/rule_panel_datasource.go index ac0a9d2..55d51a5 100644 --- a/lint/rule_panel_datasource.go +++ b/lint/rule_panel_datasource.go @@ -8,6 +8,7 @@ func NewPanelDatasourceRule() *PanelRuleFunc { return &PanelRuleFunc{ name: "panel-datasource-rule", description: "Checks that each panel uses the templated datasource.", + stability: ruleStabilityStable, fn: func(d Dashboard, p Panel) PanelRuleResults { r := PanelRuleResults{} diff --git a/lint/rule_panel_no_targets.go b/lint/rule_panel_no_targets.go index 79a249a..dd512a8 100644 --- a/lint/rule_panel_no_targets.go +++ b/lint/rule_panel_no_targets.go @@ -4,6 +4,7 @@ func NewPanelNoTargetsRule() *PanelRuleFunc { return &PanelRuleFunc{ name: "panel-no-targets-rule", description: "Checks that each panel has at least one target.", + stability: ruleStabilityStable, fn: func(d Dashboard, p Panel) PanelRuleResults { r := PanelRuleResults{} switch p.Type { diff --git a/lint/rule_panel_title_description.go b/lint/rule_panel_title_description.go index eeb7506..76c6e7c 100644 --- a/lint/rule_panel_title_description.go +++ b/lint/rule_panel_title_description.go @@ -6,6 +6,7 @@ func NewPanelTitleDescriptionRule() *PanelRuleFunc { return &PanelRuleFunc{ name: "panel-title-description-rule", description: "Checks that each panel has a title and description.", + stability: ruleStabilityStable, fn: func(d Dashboard, p Panel) PanelRuleResults { r := PanelRuleResults{} switch p.Type { diff --git a/lint/rule_panel_units.go b/lint/rule_panel_units.go index 7fea86a..e1e273d 100644 --- a/lint/rule_panel_units.go +++ b/lint/rule_panel_units.go @@ -71,6 +71,7 @@ func NewPanelUnitsRule() *PanelRuleFunc { return &PanelRuleFunc{ name: "panel-units-rule", description: "Checks that each panel uses has valid units defined.", + stability: ruleStabilityStable, fn: func(d Dashboard, p Panel) PanelRuleResults { r := PanelRuleResults{} switch p.Type { diff --git a/lint/rule_target_counter_agg.go b/lint/rule_target_counter_agg.go index bb87d06..9f9b065 100644 --- a/lint/rule_target_counter_agg.go +++ b/lint/rule_target_counter_agg.go @@ -11,6 +11,7 @@ func NewTargetCounterAggRule() *TargetRuleFunc { return &TargetRuleFunc{ name: "target-counter-agg-rule", description: "Checks that any counter metric (ending in _total) is aggregated with rate, irate, or increase.", + stability: ruleStabilityStable, fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { r := TargetRuleResults{} expr, err := parsePromQL(t.Expr, d.Templating.List) diff --git a/lint/rule_target_job_instance.go b/lint/rule_target_job_instance.go index 968f679..4e52572 100644 --- a/lint/rule_target_job_instance.go +++ b/lint/rule_target_job_instance.go @@ -11,6 +11,7 @@ func newTargetRequiredMatcherRule(matcher string) *TargetRuleFunc { return &TargetRuleFunc{ name: fmt.Sprintf("target-%s-rule", matcher), description: fmt.Sprintf("Checks that every PromQL query has a %s matcher.", matcher), + stability: ruleStabilityStable, fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { r := TargetRuleResults{} // TODO: The RuleSet should be responsible for routing rule checks based on their query type (prometheus, loki, mysql, etc) diff --git a/lint/rule_target_logql.go b/lint/rule_target_logql.go index f8cb168..900fd40 100644 --- a/lint/rule_target_logql.go +++ b/lint/rule_target_logql.go @@ -8,6 +8,7 @@ func NewTargetLogQLRule() *TargetRuleFunc { return &TargetRuleFunc{ name: "target-logql-rule", description: "Checks that each target uses a valid LogQL query.", + stability: ruleStabilityExperimental, fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { r := TargetRuleResults{} diff --git a/lint/rule_target_logql_auto.go b/lint/rule_target_logql_auto.go index 52cdd46..65abfa2 100644 --- a/lint/rule_target_logql_auto.go +++ b/lint/rule_target_logql_auto.go @@ -17,14 +17,10 @@ func parseLogQL(expr string, variables []Template) (syntax.Expr, error) { } func NewTargetLogQLAutoRule() *TargetRuleFunc { - autoDuration, err := time.ParseDuration(globalVariables["__auto"].(string)) - if err != nil { - panic(err) - } - return &TargetRuleFunc{ name: "target-logql-auto-rule", description: "Checks that each Loki target uses $__auto for range vectors when appropriate.", + stability: ruleStabilityExperimental, fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { r := TargetRuleResults{} @@ -51,6 +47,11 @@ func NewTargetLogQLAutoRule() *TargetRuleFunc { return r } + autoDuration, err := time.ParseDuration(placeholderByVariable["$__auto"].value) + if err != nil { + panic(err) + } + parsedExpr, err := parseLogQL(t.Expr, d.Templating.List) if err != nil { r.AddError(d, p, t, fmt.Sprintf("Invalid LogQL query: %v", err)) diff --git a/lint/rule_target_promql.go b/lint/rule_target_promql.go index 2159b80..2a3ddbc 100644 --- a/lint/rule_target_promql.go +++ b/lint/rule_target_promql.go @@ -23,7 +23,7 @@ func panelHasQueries(p Panel) bool { // replacing eg [$__rate_interval] with [5m] so queries parse correctly. // We also replace various other Grafana global variables. func parsePromQL(expr string, variables []Template) (parser.Expr, error) { - expr, err := expandVariables(expr, variables) + expr, err := expandPromQlVariables(expr, variables) if err != nil { return nil, fmt.Errorf("could not expand variables: %w", err) } @@ -39,6 +39,7 @@ func NewTargetPromQLRule() *TargetRuleFunc { return &TargetRuleFunc{ name: "target-promql-rule", description: "Checks that each target uses a valid PromQL query.", + stability: ruleStabilityStable, fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { r := TargetRuleResults{} diff --git a/lint/rule_target_promql_test.go b/lint/rule_target_promql_test.go index 90554f8..2a15a0a 100644 --- a/lint/rule_target_promql_test.go +++ b/lint/rule_target_promql_test.go @@ -54,7 +54,7 @@ func TestTargetPromQLRule(t *testing.T) { { result: []Result{{ Severity: Error, - Message: "Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query 'foo(bar.baz)': 1:8: parse error: unexpected character: '.'", + Message: "Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query 'foo(bar.baz)': could not expand variables: failed to parse expression: foo(bar.baz)", }}, panel: Panel{ Title: "panel", @@ -113,7 +113,7 @@ func TestTargetPromQLRule(t *testing.T) { Type: "singlestat", Targets: []Target{ { - Expr: `sum (rate(foo[$interval:$resolution]))`, + Expr: `max by($var) (rate(cpu{}[$interval:$resolution]))`, }, }, }, @@ -134,7 +134,7 @@ func TestTargetPromQLRule(t *testing.T) { { result: []Result{{ Severity: Error, - Message: "Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query '': unknown position: parse error: no expression found in input", + Message: "Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query '': could not expand variables: failed to parse expression: ", }}, panel: Panel{ Title: "panel", @@ -155,7 +155,7 @@ func TestTargetPromQLRule(t *testing.T) { }, { Severity: Error, - Message: "Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query '': unknown position: parse error: no expression found in input", + Message: "Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query '': could not expand variables: failed to parse expression: ", }, }, panel: Panel{ @@ -194,6 +194,11 @@ func TestTargetPromQLRule(t *testing.T) { Name: "sampling", Current: map[string]interface{}{"value": "$__auto_interval_sampling"}, }, + { + Name: "var", + Type: "query", + Current: map[string]interface{}{"value": "value"}, + }, { Type: "resolution", Name: "resolution", diff --git a/lint/rule_target_rate_interval.go b/lint/rule_target_rate_interval.go index 55fbc6f..c5ca1e7 100644 --- a/lint/rule_target_rate_interval.go +++ b/lint/rule_target_rate_interval.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/prometheus/common/model" "github.com/prometheus/prometheus/promql/parser" ) @@ -19,14 +20,10 @@ func (f inspector) Visit(node parser.Node, path []parser.Node) (parser.Visitor, // NewTargetRateIntervalRule builds a lint rule for panels with Prometheus queries which checks // all range vector selectors use $__rate_interval. func NewTargetRateIntervalRule() *TargetRuleFunc { - rateIntervalMagicDuration, err := time.ParseDuration(globalVariables["__rate_interval"].(string)) - if err != nil { - // Will not happen - panic(err) - } return &TargetRuleFunc{ name: "target-rate-interval-rule", description: "Checks that each target uses $__rate_interval.", + stability: ruleStabilityStable, fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { r := TargetRuleResults{} if t := getTemplateDatasource(d); t == nil || t.Query != Prometheus { @@ -44,6 +41,11 @@ func NewTargetRateIntervalRule() *TargetRuleFunc { // Invalid PromQL is another rule return r } + rateIntervalMagicDuration, err := model.ParseDuration(placeholderByVariable["$__rate_interval"].value + "s") + if err != nil { + // Will not happen + panic(err) + } err = parser.Walk(inspector(func(node parser.Node, parents []parser.Node) error { selector, ok := node.(*parser.MatrixSelector) if !ok { @@ -51,7 +53,7 @@ func NewTargetRateIntervalRule() *TargetRuleFunc { return nil } - if selector.Range == rateIntervalMagicDuration { + if selector.Range == time.Duration(rateIntervalMagicDuration) { // Range vector selector is $__rate_interval return nil } diff --git a/lint/rule_target_rate_interval_test.go b/lint/rule_target_rate_interval_test.go index b7f84b6..bf0d5c1 100644 --- a/lint/rule_target_rate_interval_test.go +++ b/lint/rule_target_rate_interval_test.go @@ -6,7 +6,6 @@ import ( func TestTargetRateIntervalRule(t *testing.T) { linter := NewTargetRateIntervalRule() - for _, tc := range []struct { result Result panel Panel diff --git a/lint/rule_target_required_matchers.go b/lint/rule_target_required_matchers.go new file mode 100644 index 0000000..072cf14 --- /dev/null +++ b/lint/rule_target_required_matchers.go @@ -0,0 +1,97 @@ +package lint + +import ( + "fmt" + + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql/parser" +) + +type TargetRequiredMatchersRuleSettings struct { + Matchers config.Matchers `yaml:"matchers"` +} + +func NewTargetRequiredMatchersRule(config *TargetRequiredMatchersRuleSettings) *TargetRuleFunc { + return &TargetRuleFunc{ + name: "target-required-matchers-rule", + description: "Checks that target PromQL query has the required matchers", + stability: ruleStabilityExperimental, + fn: func(d Dashboard, p Panel, t Target) TargetRuleResults { + r := TargetRuleResults{} + // TODO: The RuleSet should be responsible for routing rule checks based on their query type (prometheus, loki, mysql, etc) + // and for ensuring that the datasource is set. + if t := getTemplateDatasource(d); t == nil || t.Query != Prometheus { + // Missing template datasource is a separate rule. + // Non prometheus datasources don't have rules yet + return r + } + + expr, err := parsePromQL(t.Expr, d.Templating.List) + if err != nil { + // Invalid PromQL is another rule + return r + } + if config != nil { + for _, m := range config.Matchers { + for _, selector := range parser.ExtractSelectors(expr) { + if err := checkForMatcher(selector, m.Name, labels.MatchType(m.Type), m.Value); err != nil { + r.AddFixableError(d, p, t, fmt.Sprintf("invalid PromQL query '%s': %v", t.Expr, err), fixTargetRequiredMatcherRule(m.Name, labels.MatchType(m.Type), m.Value)) + } + } + } + } + return r + }, + } +} + +func fixTargetRequiredMatcherRule(name string, ty labels.MatchType, value string) func(Dashboard, Panel, *Target) { + return func(d Dashboard, p Panel, t *Target) { + // using t.Expr to ensure matchers added earlier in the loop are not lost + // no need to check for errors here, as the expression was already parsed and validated + expr, _ := parsePromQL(t.Expr, d.Templating.List) + // Walk the expression tree and add the matcher to all vector selectors + err := parser.Walk(addMatchers(name, ty, value), expr, nil) + if err != nil { + return + } + e, err := revertExpandedVariables(expr.String()) + if err != nil { + return + } + t.Expr = e + } +} + +type matcherAdder func(node parser.Node) error + +func (f matcherAdder) Visit(node parser.Node, path []parser.Node) (w parser.Visitor, err error) { + err = f(node) + return f, err +} + +func addMatchers(name string, ty labels.MatchType, value string) matcherAdder { + return func(node parser.Node) error { + if n, ok := node.(*parser.VectorSelector); ok { + matcherfixed := false + for _, m := range n.LabelMatchers { + if m.Name == name { + if m.Type != ty || m.Value != value { + m.Type = ty + m.Value = value + } + matcherfixed = true + } + } + if !matcherfixed { + n.LabelMatchers = append(n.LabelMatchers, &labels.Matcher{ + Name: name, + Type: ty, + Value: value, + }) + } + } + return nil + } +} diff --git a/lint/rule_target_required_matchers_test.go b/lint/rule_target_required_matchers_test.go new file mode 100644 index 0000000..e4b5e72 --- /dev/null +++ b/lint/rule_target_required_matchers_test.go @@ -0,0 +1,223 @@ +package lint + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/pkg/labels" + "github.com/stretchr/testify/require" +) + +func TestTargetRequiredMatcherRule(t *testing.T) { + linter := NewTargetRequiredMatchersRule(&TargetRequiredMatchersRuleSettings{ + Matchers: config.Matchers{ + { + Name: "instance", + Type: labels.MatchRegexp, + Value: "$instance", + }, + }, + }) + + for _, tc := range []struct { + name string + result Result + target Target + fixed *Target + }{ + // Happy path + { + name: "OK", + result: ResultSuccess, + target: Target{ + Expr: fmt.Sprintf(`sum(rate(foo{%s=~"$%s"}[5m]))`, "instance", "instance"), + }, + }, + // Also happy when the promql is invalid + { + name: "OK-invalid-promql", + result: ResultSuccess, + target: Target{ + Expr: `foo(bar.baz))`, + }, + }, + // Missing matcher + { + name: "autofix-missing-matcher", + result: Result{ + Severity: Fixed, + Message: fmt.Sprintf("Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query 'sum(rate(foo[5m]))': %s selector not found", "instance"), + }, + target: Target{ + Expr: `sum(rate(foo[5m]))`, + }, + fixed: &Target{ + Expr: fmt.Sprintf(`sum(rate(foo{%s=~"$%s"}[5m]))`, "instance", "instance"), + }, + }, + // Not a regex matcher + { + name: "autofix-not-regex-matcher", + result: Result{ + Severity: Fixed, + Message: fmt.Sprintf("Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query 'sum(rate(foo{%s=\"$%s\"}[5m]))': %s selector is =, not =~", "instance", "instance", "instance"), + }, + target: Target{ + Expr: fmt.Sprintf(`sum(rate(foo{%s="$%s"}[5m]))`, "instance", "instance"), + }, + fixed: &Target{ + Expr: fmt.Sprintf(`sum(rate(foo{%s=~"$%s"}[5m]))`, "instance", "instance"), + }, + }, + // Wrong template variable + { + name: "autofix-wrong-template-variable", + result: Result{ + Severity: Fixed, + Message: fmt.Sprintf("Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query 'sum(rate(foo{%s=~\"$foo\"}[5m]))': %s selector is $foo, not $%s", "instance", "instance", "instance"), + }, + target: Target{ + Expr: fmt.Sprintf(`sum(rate(foo{%s=~"$foo"}[5m]))`, "instance"), + }, + fixed: &Target{ + Expr: fmt.Sprintf(`sum(rate(foo{%s=~"$%s"}[5m]))`, "instance", "instance"), + }, + }, + // Using Grafana global-variable + { + name: "autofix-reverse-expanded-variables", + result: Result{ + Severity: Fixed, + Message: fmt.Sprintf("Dashboard 'dashboard', panel 'panel', target idx '0' invalid PromQL query 'sum(rate(foo[$__rate_interval]))': %s selector not found", "instance"), + }, + target: Target{ + Expr: `sum(rate(foo[$__rate_interval]))`, + }, + fixed: &Target{ + Expr: fmt.Sprintf(`sum(rate(foo{%s=~"$%s"}[$__rate_interval]))`, "instance", "instance"), + }, + }, + } { + dashboard := Dashboard{ + Title: "dashboard", + Templating: struct { + List []Template `json:"list"` + }{ + List: []Template{ + { + Type: "datasource", + Query: "prometheus", + }, + }, + }, + Panels: []Panel{ + { + Title: "panel", + Type: "singlestat", + Targets: []Target{tc.target}, + }, + }, + } + t.Run(tc.name, func(t *testing.T) { + autofix := tc.fixed != nil + testRuleWithAutofix(t, linter, &dashboard, []Result{tc.result}, autofix) + if autofix { + fixedDashboard := Dashboard{ + Title: "dashboard", + Templating: struct { + List []Template `json:"list"` + }{ + List: []Template{ + { + Type: "datasource", + Query: "prometheus", + }, + }, + }, + Panels: []Panel{ + { + Title: "panel", + Type: "singlestat", + Targets: []Target{*tc.fixed}, + }, + }, + } + expected, _ := json.Marshal(fixedDashboard) + actual, _ := json.Marshal(dashboard) + require.Equal(t, string(expected), string(actual)) + } + }) + } +} + +func TestTargetRequiredMatcherRuleNilInput(t *testing.T) { + linter := NewTargetRequiredMatchersRule(nil) + + for _, tc := range []struct { + name string + result Result + target Target + fixed *Target + }{ + // Happy path + { + name: "OK", + result: ResultSuccess, + target: Target{ + Expr: fmt.Sprintf(`sum(rate(foo{%s=~"$%s"}[5m]))`, "instance", "instance"), + }, + }, + } { + dashboard := Dashboard{ + Title: "dashboard", + Templating: struct { + List []Template `json:"list"` + }{ + List: []Template{ + { + Type: "datasource", + Query: "prometheus", + }, + }, + }, + Panels: []Panel{ + { + Title: "panel", + Type: "singlestat", + Targets: []Target{tc.target}, + }, + }, + } + t.Run(tc.name, func(t *testing.T) { + autofix := tc.fixed != nil + testRuleWithAutofix(t, linter, &dashboard, []Result{tc.result}, autofix) + if autofix { + fixedDashboard := Dashboard{ + Title: "dashboard", + Templating: struct { + List []Template `json:"list"` + }{ + List: []Template{ + { + Type: "datasource", + Query: "prometheus", + }, + }, + }, + Panels: []Panel{ + { + Title: "panel", + Type: "singlestat", + Targets: []Target{*tc.fixed}, + }, + }, + } + expected, _ := json.Marshal(fixedDashboard) + actual, _ := json.Marshal(dashboard) + require.Equal(t, string(expected), string(actual)) + } + }) + } +} diff --git a/lint/rule_template_datasource.go b/lint/rule_template_datasource.go index 37bffab..d3df644 100644 --- a/lint/rule_template_datasource.go +++ b/lint/rule_template_datasource.go @@ -12,6 +12,7 @@ func NewTemplateDatasourceRule() *DashboardRuleFunc { return &DashboardRuleFunc{ name: "template-datasource-rule", description: "Checks that the dashboard has a templated datasource.", + stability: ruleStabilityStable, fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} diff --git a/lint/rule_template_instance.go b/lint/rule_template_instance.go index 01142e4..3b2df2d 100644 --- a/lint/rule_template_instance.go +++ b/lint/rule_template_instance.go @@ -4,6 +4,7 @@ func NewTemplateInstanceRule() *DashboardRuleFunc { return &DashboardRuleFunc{ name: "template-instance-rule", description: "Checks that the dashboard has a templated instance.", + stability: ruleStabilityStable, fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} diff --git a/lint/rule_template_job.go b/lint/rule_template_job.go index 6eff4ed..f034ef4 100644 --- a/lint/rule_template_job.go +++ b/lint/rule_template_job.go @@ -1,16 +1,10 @@ package lint -import ( - "fmt" - - "golang.org/x/text/cases" - "golang.org/x/text/language" -) - func NewTemplateJobRule() *DashboardRuleFunc { return &DashboardRuleFunc{ name: "template-job-rule", description: "Checks that the dashboard has a templated job.", + stability: ruleStabilityStable, fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} @@ -24,51 +18,3 @@ func NewTemplateJobRule() *DashboardRuleFunc { }, } } - -func checkTemplate(d Dashboard, name string, r *DashboardRuleResults) { - t := getTemplate(d, name) - if t == nil { - r.AddError(d, fmt.Sprintf("is missing the %s template", name)) - return - } - - // TODO: Adding the prometheus_datasource here is hacky. This check function also assumes that all template vars which it will - // ever check are only prometheus queries, which may not always be the case. - src, err := t.GetDataSource() - if err != nil { - r.AddError(d, fmt.Sprintf("%s template has invalid datasource %v", name, err)) - } - - srcUid := src.UID - if srcUid != "$datasource" && srcUid != "${datasource}" && srcUid != "$prometheus_datasource" && srcUid != "${prometheus_datasource}" { - r.AddError(d, fmt.Sprintf("%s template should use datasource '$datasource', is currently '%s'", name, srcUid)) - } - - if t.Type != targetTypeQuery { - r.AddError(d, fmt.Sprintf("%s template should be a Prometheus query, is currently '%s'", name, t.Type)) - } - - titleCaser := cases.Title(language.English) - labelTitle := titleCaser.String(name) - - if t.Label != labelTitle { - r.AddWarning(d, fmt.Sprintf("%s template should be a labeled '%s', is currently '%s'", name, labelTitle, t.Label)) - } - - if !t.Multi { - r.AddError(d, fmt.Sprintf("%s template should be a multi select", name)) - } - - if t.AllValue != ".+" { - r.AddError(d, fmt.Sprintf("%s template allValue should be '.+', is currently '%s'", name, t.AllValue)) - } -} - -func getTemplate(d Dashboard, name string) *Template { - for _, template := range d.Templating.List { - if template.Name == name { - return &template - } - } - return nil -} diff --git a/lint/rule_template_label_promql.go b/lint/rule_template_label_promql.go index 62c3357..eec0c5f 100644 --- a/lint/rule_template_label_promql.go +++ b/lint/rule_template_label_promql.go @@ -5,34 +5,45 @@ import ( "regexp" ) -var templatedLabelRegexp = regexp.MustCompile(`([a-z_]+)\((.+)\)`) +var ( + lvNoQueryRegexp = regexp.MustCompile(`(?s)label_values\((.+)\)`) // label_values(label) + lvRegexp = regexp.MustCompile(`(?s)label_values\((.+),.+\)`) // label_values(metric, label) + mRegexp = regexp.MustCompile(`(?s)metrics\((.+)\)`) // metrics(metric) + lnRegexp = regexp.MustCompile(`(?s)label_names\((.+)\)`) // label_names() + qrRegexp = regexp.MustCompile(`(?s)query_result\((.+)\)`) // query_result(query) +) -func labelHasValidDataSourceFunction(name string) bool { - // https://grafana.com/docs/grafana/v8.1/datasources/prometheus/#query-variable - names := []string{"label_names", "label_values", "metrics", "query_result"} - for _, n := range names { - if name == n { - return true - } +func extractPromQLQuery(q string) []string { + // label_values(query, label) + switch { + case lvRegexp.MatchString(q): + return lvRegexp.FindStringSubmatch(q) + case lvNoQueryRegexp.MatchString(q): + return nil // No query so no metric. + case mRegexp.MatchString(q): + return mRegexp.FindStringSubmatch(q) + case lnRegexp.MatchString(q): + return lnRegexp.FindStringSubmatch(q) + case qrRegexp.MatchString(q): + return qrRegexp.FindStringSubmatch(q) + default: + return nil } - return false } // parseTemplatedLabelPromQL returns error in case // 1) The given PromQL expressions is invalid // 2) Use of invalid label function func parseTemplatedLabelPromQL(t Template, variables []Template) error { - // regex capture must return slice of 3 strings. - // 1) given query 2) function name 3) function arg. - tokens := templatedLabelRegexp.FindStringSubmatch(t.Query) + // regex capture must return slice of 2 strings. + // 1) given query 2) function arg. + + tokens := extractPromQLQuery(t.Query) if tokens == nil { return fmt.Errorf("invalid 'query': %v", t.Query) } - if !labelHasValidDataSourceFunction(tokens[1]) { - return fmt.Errorf("invalid 'function': %v", tokens[1]) - } - expr, err := parsePromQL(tokens[2], variables) + expr, err := parsePromQL(tokens[1], variables) if expr != nil { return nil } @@ -43,6 +54,7 @@ func NewTemplateLabelPromQLRule() *DashboardRuleFunc { return &DashboardRuleFunc{ name: "template-label-promql-rule", description: "Checks that the dashboard templated labels have proper PromQL expressions.", + stability: ruleStabilityStable, fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} @@ -58,7 +70,6 @@ func NewTemplateLabelPromQLRule() *DashboardRuleFunc { r.AddError(d, fmt.Sprintf("template '%s' invalid templated label '%s': %v", template.Name, template.Query, err)) } } - return r }, } diff --git a/lint/rule_template_label_promql_test.go b/lint/rule_template_label_promql_test.go index 8ef33ac..aec7778 100644 --- a/lint/rule_template_label_promql_test.go +++ b/lint/rule_template_label_promql_test.go @@ -57,7 +57,7 @@ func TestTemplateLabelPromQLRule(t *testing.T) { name: "Error", result: Result{ Severity: Error, - Message: `Dashboard 'test' template 'namespaces' invalid templated label 'label_values(up{, namespace)': 1:4: parse error: unexpected "," in label matching, expected identifier or "}"`, + Message: `Dashboard 'test' template 'namespaces' invalid templated label 'label_values(up{, namespace)': could not expand variables: failed to parse expression: up{`, }, dashboard: Dashboard{ Title: "test", @@ -84,7 +84,7 @@ func TestTemplateLabelPromQLRule(t *testing.T) { name: "Invalid function.", result: Result{ Severity: Error, - Message: `Dashboard 'test' template 'namespaces' invalid templated label 'foo(up, namespace)': invalid 'function': foo`, + Message: `Dashboard 'test' template 'namespaces' invalid templated label 'foo(up, namespace)': invalid 'query': foo(up, namespace)`, }, dashboard: Dashboard{ Title: "test", diff --git a/lint/rule_template_on_time_change_reload.go b/lint/rule_template_on_time_change_reload.go index cef025a..75c6beb 100644 --- a/lint/rule_template_on_time_change_reload.go +++ b/lint/rule_template_on_time_change_reload.go @@ -8,6 +8,7 @@ func NewTemplateOnTimeRangeReloadRule() *DashboardRuleFunc { return &DashboardRuleFunc{ name: "template-on-time-change-reload-rule", description: "Checks that the dashboard template variables are configured to reload on time change.", + stability: ruleStabilityStable, fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} diff --git a/lint/rule_template_required_variables.go b/lint/rule_template_required_variables.go new file mode 100644 index 0000000..479475d --- /dev/null +++ b/lint/rule_template_required_variables.go @@ -0,0 +1,58 @@ +package lint + +import ( + "strings" +) + +type TemplateRequiredVariablesRuleSettings struct { + Variables []string `yaml:"variables"` +} + +func NewTemplateRequiredVariablesRule(config *TemplateRequiredVariablesRuleSettings, requiredMatchers *TargetRequiredMatchersRuleSettings) *DashboardRuleFunc { + return &DashboardRuleFunc{ + name: "template-required-variables-rule", + description: "Checks that the dashboard has a template variable for required variables or matchers that use variables", + stability: ruleStabilityExperimental, + fn: func(d Dashboard) DashboardRuleResults { + r := DashboardRuleResults{} + + template := getTemplateDatasource(d) + if template == nil || template.Query != Prometheus { + return r + } + + // Create a map and a slice, map for uniqueness and slice to keep the order... + var varMap = make(map[string]bool) + var varSlice = []string{} + + if config != nil { + // Convert the config.variables to a map to leverage uniqueness... + for _, v := range config.Variables { + if varMap[v] { + continue + } + varMap[v] = true + varSlice = append(varSlice, v) + } + } + + if requiredMatchers != nil { + // Check that all required matchers that use variables form target-required-matchers have a corresponding template variable + for _, m := range requiredMatchers.Matchers { + if strings.HasPrefix(m.Value, "$") { + if varMap[m.Value[1:]] { + continue + } + varMap[m.Value[1:]] = true + varSlice = append(varSlice, m.Value[1:]) + } + } + } + + for _, v := range varSlice { + checkTemplate(d, v, &r) + } + return r + }, + } +} diff --git a/lint/rule_template_required_variables_test.go b/lint/rule_template_required_variables_test.go new file mode 100644 index 0000000..7c6b386 --- /dev/null +++ b/lint/rule_template_required_variables_test.go @@ -0,0 +1,351 @@ +package lint + +import ( + "testing" + + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/pkg/labels" +) + +func TestTemplateRequiredVariable(t *testing.T) { + linter := NewTemplateRequiredVariablesRule( + &TemplateRequiredVariablesRuleSettings{ + Variables: []string{"job"}, + }, + &TargetRequiredMatchersRuleSettings{ + Matchers: config.Matchers{ + { + Name: "instance", + Type: labels.MatchRegexp, + Value: "$instance", + }, + }, + }) + + for _, tc := range []struct { + name string + result []Result + dashboard Dashboard + }{ + { + name: "Non-promtheus dashboards shouldn't fail.", + result: []Result{ResultSuccess}, + dashboard: Dashboard{ + Title: "test", + }, + }, + { + name: "Missing job/instance template.", + result: []Result{ + {Severity: Error, Message: "Dashboard 'test' is missing the job template"}, + {Severity: Error, Message: "Dashboard 'test' is missing the instance template"}, + }, + dashboard: Dashboard{ + Title: "test", + Templating: struct { + List []Template `json:"list"` + }{ + List: []Template{ + { + Type: "datasource", + Query: "prometheus", + }, + }, + }, + }, + }, + { + name: "Wrong datasource.", + result: []Result{ + {Severity: Error, Message: "Dashboard 'test' job template should use datasource '$datasource', is currently 'foo'"}, + {Severity: Error, Message: "Dashboard 'test' job template should be a Prometheus query, is currently ''"}, + {Severity: Warning, Message: "Dashboard 'test' job template should be a labeled 'Job', is currently ''"}, + {Severity: Error, Message: "Dashboard 'test' job template should be a multi select"}, + {Severity: Error, Message: "Dashboard 'test' job template allValue should be '.+', is currently ''"}, + {Severity: Error, Message: "Dashboard 'test' instance template should use datasource '$datasource', is currently 'foo'"}, + {Severity: Error, Message: "Dashboard 'test' instance template should be a Prometheus query, is currently ''"}, + {Severity: Warning, Message: "Dashboard 'test' instance template should be a labeled 'Instance', is currently ''"}, + {Severity: Error, Message: "Dashboard 'test' instance template should be a multi select"}, + {Severity: Error, Message: "Dashboard 'test' instance template allValue should be '.+', is currently ''"}, + }, + dashboard: Dashboard{ + Title: "test", + Templating: struct { + List []Template `json:"list"` + }{ + List: []Template{ + { + Type: "datasource", + Query: "prometheus", + }, + { + Name: "job", + Datasource: "foo", + }, + { + Name: "instance", + Datasource: "foo", + }, + }, + }, + }, + }, + { + name: "Wrong type.", + result: []Result{ + {Severity: Error, Message: "Dashboard 'test' job template should be a Prometheus query, is currently 'bar'"}, + {Severity: Warning, Message: "Dashboard 'test' job template should be a labeled 'Job', is currently ''"}, + {Severity: Error, Message: "Dashboard 'test' job template should be a multi select"}, + {Severity: Error, Message: "Dashboard 'test' job template allValue should be '.+', is currently ''"}, + {Severity: Error, Message: "Dashboard 'test' instance template should be a Prometheus query, is currently 'bar'"}, + {Severity: Warning, Message: "Dashboard 'test' instance template should be a labeled 'Instance', is currently ''"}, + {Severity: Error, Message: "Dashboard 'test' instance template should be a multi select"}, + {Severity: Error, Message: "Dashboard 'test' instance template allValue should be '.+', is currently ''"}, + }, + dashboard: Dashboard{ + Title: "test", + Templating: struct { + List []Template `json:"list"` + }{ + List: []Template{ + { + Type: "datasource", + Query: "prometheus", + }, + { + Name: "job", + Datasource: "$datasource", + Type: "bar", + }, + { + Name: "instance", + Datasource: "$datasource", + Type: "bar", + }, + }, + }, + }, + }, + { + name: "Wrong job/instance label.", + result: []Result{ + {Severity: Warning, Message: "Dashboard 'test' job template should be a labeled 'Job', is currently 'bar'"}, + {Severity: Error, Message: "Dashboard 'test' job template should be a multi select"}, + {Severity: Error, Message: "Dashboard 'test' job template allValue should be '.+', is currently ''"}, + {Severity: Warning, Message: "Dashboard 'test' instance template should be a labeled 'Instance', is currently 'bar'"}, + {Severity: Error, Message: "Dashboard 'test' instance template should be a multi select"}, + {Severity: Error, Message: "Dashboard 'test' instance template allValue should be '.+', is currently ''"}, + }, + dashboard: Dashboard{ + Title: "test", + Templating: struct { + List []Template `json:"list"` + }{ + List: []Template{ + { + Type: "datasource", + Query: "prometheus", + }, + { + Name: "job", + Datasource: "$datasource", + Type: "query", + Label: "bar", + }, + { + Name: "instance", + Datasource: "$datasource", + Type: "query", + Label: "bar", + }, + }, + }, + }, + }, + { + name: "OK", + result: []Result{ResultSuccess}, + dashboard: Dashboard{ + Title: "test", + Templating: struct { + List []Template `json:"list"` + }{ + List: []Template{ + { + Type: "datasource", + Query: "prometheus", + }, + { + Name: "job", + Datasource: "$datasource", + Type: "query", + Label: "Job", + Multi: true, + AllValue: ".+", + }, + { + Name: "instance", + Datasource: "${datasource}", + Type: "query", + Label: "Instance", + Multi: true, + AllValue: ".+", + }, + }, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + testMultiResultRule(t, linter, tc.dashboard, tc.result) + }) + } +} + +func TestTemplateRequiredVariableNilRequiredMatchers(t *testing.T) { + linter := NewTemplateRequiredVariablesRule( + nil, + &TargetRequiredMatchersRuleSettings{ + Matchers: config.Matchers{ + { + Name: "instance", + Type: labels.MatchRegexp, + Value: "$instance", + }, + }, + }) + + for _, tc := range []struct { + name string + result []Result + dashboard Dashboard + }{ + { + name: "Missing instance template.", + result: []Result{ + {Severity: Error, Message: "Dashboard 'test' is missing the instance template"}, + }, + dashboard: Dashboard{ + Title: "test", + Templating: struct { + List []Template `json:"list"` + }{ + List: []Template{ + { + Type: "datasource", + Query: "prometheus", + }, + { + Name: "job", + Datasource: "$datasource", + Type: "query", + Label: "Job", + Multi: true, + AllValue: ".+", + }, + }, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + testMultiResultRule(t, linter, tc.dashboard, tc.result) + }) + } +} + +func TestTemplateRequiredVariableNilConfig(t *testing.T) { + linter := NewTemplateRequiredVariablesRule( + &TemplateRequiredVariablesRuleSettings{ + Variables: []string{"job"}, + }, + nil) + + for _, tc := range []struct { + name string + result []Result + dashboard Dashboard + }{ + { + name: "Missing job template.", + result: []Result{ + {Severity: Error, Message: "Dashboard 'test' is missing the job template"}, + }, + dashboard: Dashboard{ + Title: "test", + Templating: struct { + List []Template `json:"list"` + }{ + List: []Template{ + { + Type: "datasource", + Query: "prometheus", + }, + { + Name: "instance", + Datasource: "${datasource}", + Type: "query", + Label: "Instance", + Multi: true, + AllValue: ".+", + }, + }, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + testMultiResultRule(t, linter, tc.dashboard, tc.result) + }) + } +} + +func TestTemplateRequiredVariableNilInput(t *testing.T) { + linter := NewTemplateRequiredVariablesRule( + nil, + nil) + + for _, tc := range []struct { + name string + result []Result + dashboard Dashboard + }{ + { + name: "OK", + result: []Result{ResultSuccess}, + dashboard: Dashboard{ + Title: "test", + Templating: struct { + List []Template `json:"list"` + }{ + List: []Template{ + { + Type: "datasource", + Query: "prometheus", + }, + { + Name: "job", + Datasource: "$datasource", + Type: "query", + Label: "Job", + Multi: true, + AllValue: ".+", + }, + { + Name: "instance", + Datasource: "${datasource}", + Type: "query", + Label: "Instance", + Multi: true, + AllValue: ".+", + }, + }, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + testMultiResultRule(t, linter, tc.dashboard, tc.result) + }) + } +} diff --git a/lint/rule_uneditable.go b/lint/rule_uneditable.go index ac5f410..c1d6194 100644 --- a/lint/rule_uneditable.go +++ b/lint/rule_uneditable.go @@ -2,8 +2,9 @@ package lint func NewUneditableRule() *DashboardRuleFunc { return &DashboardRuleFunc{ - name: "uneditable-dashboard", + name: "uneditable-dashboard-rule", description: "Checks that the dashboard is not editable.", + stability: ruleStabilityStable, fn: func(d Dashboard) DashboardRuleResults { r := DashboardRuleResults{} if d.Editable { diff --git a/lint/rules.go b/lint/rules.go index 5a55a9f..8ac117f 100644 --- a/lint/rules.go +++ b/lint/rules.go @@ -3,20 +3,22 @@ package lint type Rule interface { Description() string Name() string + Stability() string Lint(Dashboard, *ResultSet) } type DashboardRuleFunc struct { - name, description string - fn func(Dashboard) DashboardRuleResults + name, description, stability string + fn func(Dashboard) DashboardRuleResults } -func NewDashboardRuleFunc(name, description string, fn func(Dashboard) DashboardRuleResults) Rule { - return &DashboardRuleFunc{name, description, fn} +func NewDashboardRuleFunc(name, description, stability string, fn func(Dashboard) DashboardRuleResults) Rule { + return &DashboardRuleFunc{name, description, stability, fn} } func (f DashboardRuleFunc) Name() string { return f.name } func (f DashboardRuleFunc) Description() string { return f.description } +func (f DashboardRuleFunc) Stability() string { return f.stability } func (f DashboardRuleFunc) Lint(d Dashboard, s *ResultSet) { dashboardResults := f.fn(d).Results if len(dashboardResults) == 0 { @@ -50,16 +52,17 @@ func (f DashboardRuleFunc) Lint(d Dashboard, s *ResultSet) { } type PanelRuleFunc struct { - name, description string - fn func(Dashboard, Panel) PanelRuleResults + name, description, stability string + fn func(Dashboard, Panel) PanelRuleResults } -func NewPanelRuleFunc(name, description string, fn func(Dashboard, Panel) PanelRuleResults) Rule { - return &PanelRuleFunc{name, description, fn} +func NewPanelRuleFunc(name, description, stability string, fn func(Dashboard, Panel) PanelRuleResults) Rule { + return &PanelRuleFunc{name, description, stability, fn} } func (f PanelRuleFunc) Name() string { return f.name } func (f PanelRuleFunc) Description() string { return f.description } +func (f PanelRuleFunc) Stability() string { return f.stability } func (f PanelRuleFunc) Lint(d Dashboard, s *ResultSet) { for pi, p := range d.GetPanels() { p := p // capture loop variable @@ -105,16 +108,17 @@ func fixPanel(pi int, r PanelResult) func(dashboard *Dashboard) { } type TargetRuleFunc struct { - name, description string - fn func(Dashboard, Panel, Target) TargetRuleResults + name, description, stability string + fn func(Dashboard, Panel, Target) TargetRuleResults } -func NewTargetRuleFunc(name, description string, fn func(Dashboard, Panel, Target) TargetRuleResults) Rule { - return &TargetRuleFunc{name, description, fn} +func NewTargetRuleFunc(name, description, stability string, fn func(Dashboard, Panel, Target) TargetRuleResults) Rule { + return &TargetRuleFunc{name, description, stability, fn} } func (f TargetRuleFunc) Name() string { return f.name } func (f TargetRuleFunc) Description() string { return f.description } +func (f TargetRuleFunc) Stability() string { return f.stability } func (f TargetRuleFunc) Lint(d Dashboard, s *ResultSet) { for pi, p := range d.GetPanels() { p := p // capture loop variable @@ -170,28 +174,35 @@ type RuleSet struct { rules []Rule } -func NewRuleSet() RuleSet { - return RuleSet{ - rules: []Rule{ - NewTemplateDatasourceRule(), - NewTemplateJobRule(), - NewTemplateInstanceRule(), - NewTemplateLabelPromQLRule(), - NewTemplateOnTimeRangeReloadRule(), - NewPanelDatasourceRule(), - NewPanelTitleDescriptionRule(), - NewPanelUnitsRule(), - NewPanelNoTargetsRule(), +func NewRuleSet(experimental bool, ruleSettings ConfigurationRuleSettings) RuleSet { + // Add stable rules here + rules := []Rule{ + NewTemplateDatasourceRule(), + NewTemplateJobRule(), + NewTemplateInstanceRule(), + NewTemplateLabelPromQLRule(), + NewTemplateOnTimeRangeReloadRule(), + NewPanelDatasourceRule(), + NewPanelTitleDescriptionRule(), + NewPanelUnitsRule(), + NewPanelNoTargetsRule(), + NewTargetPromQLRule(), + NewTargetRateIntervalRule(), + NewTargetJobRule(), + NewTargetInstanceRule(), + NewTargetCounterAggRule(), + NewUneditableRule(), + } + // Add experimental rules here + if experimental { + rules = append(rules, NewTargetLogQLRule(), NewTargetLogQLAutoRule(), - NewTargetPromQLRule(), - NewTargetRateIntervalRule(), - NewTargetJobRule(), - NewTargetInstanceRule(), - NewTargetCounterAggRule(), - NewUneditableRule(), - }, + NewTargetRequiredMatchersRule(ruleSettings.TargetRequiredMatchersRule), + NewTemplateRequiredVariablesRule(ruleSettings.TemplateRequiredVariablesRule, ruleSettings.TargetRequiredMatchersRule), + ) } + return RuleSet{rules} } func (s *RuleSet) Rules() []Rule { diff --git a/lint/rules_test.go b/lint/rules_test.go index 8f711b9..7e1b5c6 100644 --- a/lint/rules_test.go +++ b/lint/rules_test.go @@ -19,7 +19,7 @@ func TestCustomRules(t *testing.T) { { desc: "Should allow addition of dashboard rule", rule: lint.NewDashboardRuleFunc( - "test-dashboard-rule", "Test dashboard rule", + "test-dashboard-rule", "Test dashboard rule", "stable", func(lint.Dashboard) lint.DashboardRuleResults { return lint.DashboardRuleResults{Results: []lint.DashboardResult{{ Result: lint.Result{Severity: lint.Error, Message: "Error found"}, @@ -30,7 +30,7 @@ func TestCustomRules(t *testing.T) { { desc: "Should allow addition of panel rule", rule: lint.NewPanelRuleFunc( - "test-panel-rule", "Test panel rule", + "test-panel-rule", "Test panel rule", "stable", func(d lint.Dashboard, p lint.Panel) lint.PanelRuleResults { return lint.PanelRuleResults{Results: []lint.PanelResult{{ Result: lint.Result{Severity: lint.Error, Message: "Error found"}, @@ -41,7 +41,7 @@ func TestCustomRules(t *testing.T) { { desc: "Should allow addition of target rule", rule: lint.NewTargetRuleFunc( - "test-target-rule", "Test target rule", + "test-target-rule", "Test target rule", "stable", func(lint.Dashboard, lint.Panel, lint.Target) lint.TargetRuleResults { return lint.TargetRuleResults{Results: []lint.TargetResult{{ Result: lint.Result{Severity: lint.Error, Message: "Error found"}, @@ -73,7 +73,7 @@ func TestFixableRules(t *testing.T) { assert.NoError(t, err) rule := lint.NewDashboardRuleFunc( - "test-fixable-rule", "Test fixable rule", + "test-fixable-rule", "Test fixable rule", "stable", func(d lint.Dashboard) lint.DashboardRuleResults { rr := lint.DashboardRuleResults{} rr.AddFixableError(d, "fixing first issue", func(d *lint.Dashboard) { diff --git a/lint/template_utils.go b/lint/template_utils.go new file mode 100644 index 0000000..ed75a6f --- /dev/null +++ b/lint/template_utils.go @@ -0,0 +1,56 @@ +package lint + +import ( + "fmt" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +func checkTemplate(d Dashboard, name string, r *DashboardRuleResults) { + t := getTemplate(d, name) + if t == nil { + r.AddError(d, fmt.Sprintf("is missing the %s template", name)) + return + } + + // TODO: Adding the prometheus_datasource here is hacky. This check function also assumes that all template vars which it will + // ever check are only prometheus queries, which may not always be the case. + src, err := t.GetDataSource() + if err != nil { + r.AddError(d, fmt.Sprintf("%s template has invalid datasource %v", name, err)) + } + + srcUid := src.UID + if srcUid != "$datasource" && srcUid != "${datasource}" && srcUid != "$prometheus_datasource" && srcUid != "${prometheus_datasource}" { + r.AddError(d, fmt.Sprintf("%s template should use datasource '$datasource', is currently '%s'", name, srcUid)) + } + + if t.Type != targetTypeQuery { + r.AddError(d, fmt.Sprintf("%s template should be a Prometheus query, is currently '%s'", name, t.Type)) + } + + titleCaser := cases.Title(language.English) + labelTitle := titleCaser.String(name) + + if t.Label != labelTitle { + r.AddWarning(d, fmt.Sprintf("%s template should be a labeled '%s', is currently '%s'", name, labelTitle, t.Label)) + } + + if !t.Multi { + r.AddError(d, fmt.Sprintf("%s template should be a multi select", name)) + } + + if t.AllValue != ".+" { + r.AddError(d, fmt.Sprintf("%s template allValue should be '.+', is currently '%s'", name, t.AllValue)) + } +} + +func getTemplate(d Dashboard, name string) *Template { + for _, template := range d.Templating.List { + if template.Name == name { + return &template + } + } + return nil +} diff --git a/lint/variables.go b/lint/variables.go index 7151bb8..d95b9e7 100644 --- a/lint/variables.go +++ b/lint/variables.go @@ -1,263 +1,423 @@ package lint import ( - "encoding/json" "fmt" - "net/url" "regexp" "strconv" "strings" - "time" + + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/promql/parser" +) + +const ( + auto = "__auto" + rateInterval = "__rate_interval" + interval = "__interval" + intervalMs = "__interval_ms" + rangeMs = "__range_ms" + rangeS = "__range_s" + rangeVar = "__range" + dashboardVar = "__dashboard" + from = "__from" + to = "__to" + name = "__name" + org = "__org" + orgName = "__org.name" + userID = "__user.id" + userLogin = "__user.login" + userEmail = "__user.email" + timeFilter = "timeFilter" + timeFilter2 = "__timeFilter" + // magicTimeRange = model.Duration(time.Hour*24*211 + time.Hour*12 + time.Minute*44 + time.Second*22 + time.Millisecond*50) // 211d12h44m22s50ms + magicTimeRange = 11277964 // seconds 130d12h46m4s + magicEpoch = float64(1294671549254) + magicString = "bgludgvy" +) + +const ( + valTypeString valType = iota + valTypeTimeRange + valTypeEpoch + // This is effectively a valTypeTimeRange, but it will always have 's' appended to it to identify that the duration unit is seconds. + // Initially used only for logql which is more strict about requiring the duration unit for range vectors, while promql implies 's' as the unit of duration if not specified. + valTypeDuration ) -// https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/ -var globalVariables = map[string]interface{}{ - "__rate_interval": "8869990787ms", - "__interval": "4867856611ms", - "__interval_ms": "7781188786", - "__range_ms": "6737667980", - "__range_s": "9397795485", - "__range": "6069770749ms", - "__dashboard": "AwREbnft", - "__from": time.Date(2020, 7, 13, 20, 19, 9, 254000000, time.UTC), - "__to": time.Date(2020, 7, 13, 20, 19, 9, 254000000, time.UTC), - "__name": "name", - "__org": 42, - "__org.name": "orgname", - "__user.id": 42, - "__user.login": "user", - "__user.email": "user@test.com", - "timeFilter": "time > now() - 7d", - "__timeFilter": "time > now() - 7d", - "__auto": "12345ms", +type valType int + +type placeholder struct { + variable string // variable including the "variable syntax" i.e. $var, ${var}, [[var]] + valType valType + value string +} + +var placeholderByVariable = make(map[string]*placeholder) +var placeholderByValue = make(map[string]*placeholder) + +var globalVariablesInit = false + +// list of global variables in the for om a list of placeholders +var globalVariables = []*placeholder{ + { + variable: rateInterval, + valType: valTypeTimeRange, + }, + { + variable: interval, + valType: valTypeTimeRange, + }, + { + variable: intervalMs, + valType: valTypeTimeRange, + }, + { + variable: rangeMs, + valType: valTypeTimeRange, + }, + { + variable: rangeS, + valType: valTypeTimeRange, + }, + { + variable: rangeVar, + valType: valTypeTimeRange, + }, + { + variable: dashboardVar, + valType: valTypeString, + }, + { + variable: from, + valType: valTypeEpoch, + }, + { + variable: to, + valType: valTypeEpoch, + }, + { + variable: name, + valType: valTypeString, + }, + { + variable: org, + valType: valTypeEpoch, // not really an epoch, but it is a float64 + }, + { + variable: orgName, + valType: valTypeString, + }, + { + variable: userID, + valType: valTypeEpoch, // not really an epoch, but it is a float64 + }, + { + variable: userLogin, + valType: valTypeString, + }, + { + variable: userEmail, + valType: valTypeString, + }, + { + variable: timeFilter, + valType: valTypeString, // not really a string, but currently we do only support prometheus queries, and this would not be a valid prometheus query... + }, + { + variable: timeFilter2, + valType: valTypeString, // not really a string, but currently we do only support prometheus queries, and this would not be a valid prometheus query... + }, + { + variable: auto, + valType: valTypeDuration, + }, } -func stringValue(name string, value interface{}, kind, format string) (string, error) { - switch val := value.(type) { - case int: - return strconv.Itoa(val), nil - case time.Time: - // Implements https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/#__from-and-__to - switch kind { - case "date": - switch format { - case "seconds": - return strconv.FormatInt(val.Unix(), 10), nil - case "iso": - return val.Format(time.RFC3339), nil - default: - return "", fmt.Errorf("Unsupported momentjs time format: %s", format) +// var supportedFormatOptions = []string{"csv", "distributed", "doublequote", "glob", "json", "lucene", "percentencode", "pipe", "raw", "regex", "singlequote", "sqlstring", "text", "queryparam"} + +var variableRegexp = regexp.MustCompile( + strings.Join([]string{ + `("\$|\$)([[:word:]]+)`, // $var syntax + `("\$|\$)\{([^}]+)\}`, // ${var} syntax + `\[\[([^\[\]]+)\]\]`, // [[var]] syntax + }, "|"), +) + +// Initializes the global variables if not already initialized. +// Creates placeholders for every variable found in the expression, returning the expression with placeholders +func expandVariables(expr string, variables []Template) string { + // initialize global variables if not already initialized + if !globalVariablesInit { + for _, v := range globalVariables { + // assign placeholder to global variable 3 times to account for the 3 different ways a variable can be defined + // $var, ${var}, [[var]] + p := []placeholder{ + { + variable: fmt.Sprintf("$%s", v.variable), + valType: v.valType, + }, + { + variable: fmt.Sprintf("${%s}", v.variable), + valType: v.valType, + }, + { + variable: fmt.Sprintf("[[%s]]", v.variable), + valType: v.valType, + }, } - default: - switch format { - case "date": - return val.Format(time.RFC3339), nil - default: - return strconv.FormatInt(val.UnixMilli(), 10), nil + for _, v := range p { + createPlaceholder(v.variable, v.valType) } } - default: - // Use variable name as sample value - svalue := fmt.Sprintf("%s", value) - // For list types, repeat it 3 times (arbitrary value) - svalueList := []string{svalue, svalue, svalue} - // Implements https://grafana.com/docs/grafana/latest/variables/advanced-variable-format-options/ - switch format { - case "csv": - return strings.Join(svalueList, ","), nil - case "doublequote": - return "\"" + strings.Join(svalueList, "\",\"") + "\"", nil - case "glob": - return "{" + strings.Join(svalueList, ",") + "}", nil - case "json": - data, err := json.Marshal(svalueList) + globalVariablesInit = true + } + // add template variables to placeholder maps + for _, v := range variables { + if v.Name != "" { + // create placeholder 3 times to account for the 3 different ways a variable can be defined + // at this point, we do not care about the value of the variable, we just need a placeholder for it. + valType := getValueType(getTemplateVariableValue(v)) + createPlaceholder(fmt.Sprintf("$%s", v.Name), valType) + createPlaceholder(fmt.Sprintf("${%s}", v.Name), valType) + createPlaceholder(fmt.Sprintf("[[%s]]", v.Name), valType) + } + } + + expr = variableRegexp.ReplaceAllStringFunc(expr, RegexpExpandVariables) + return expr +} + +func expandPromQlVariables(expr string, variables []Template) (string, error) { + expr = expandVariables(expr, variables) + + // Check if the expression can be parsed + _, err := parser.ParseExpr(expr) + if err != nil { + // not using promql parser error since it contains memory address which is hard to test... + return "", fmt.Errorf("failed to parse expression: %s", expr) + } + + return expr, nil +} + +func revertExpandedVariables(expr string) (string, error) { + for _, p := range placeholderByValue { + if p.valType == valTypeTimeRange { + // Replace all versions of time range placeholder + expr = strings.ReplaceAll(expr, p.value, p.variable) + + // Parse time duration + d, err := model.ParseDuration(p.value + "s") if err != nil { - return "", err + return "", fmt.Errorf("failed to parse duration: %s when reverting expanded variable: %s", p.value, p.variable) } - return string(data), nil - case "lucene": - return "(\"" + strings.Join(svalueList, "\" OR \"") + "\")", nil - case "percentencode": - return url.QueryEscape(strings.Join(svalueList, ",")), nil - case "pipe": - return strings.Join(svalueList, "|"), nil - case "raw": - return strings.Join(svalueList, ","), nil - case "regex": - return strings.Join(svalueList, "|"), nil - case "singlequote": - return "'" + strings.Join(svalueList, "','") + "'", nil - case "sqlstring": - return "'" + strings.Join(svalueList, "','") + "'", nil - case "text": - return strings.Join(svalueList, " + "), nil - case "queryparam": - values := url.Values{} - for _, svalue := range svalueList { - values.Add("var-"+name, svalue) + expr = strings.ReplaceAll(expr, d.String(), p.variable) + + // Parse as float64 + f, err := strconv.ParseFloat(p.value, 64) + if err != nil { + return "", fmt.Errorf("failed to parse float64: %s when reverting expanded variable: %s", p.value, p.variable) } - return values.Encode(), nil - default: - return svalue, nil + expr = strings.ReplaceAll(expr, fmt.Sprint(f), p.variable) + } else { + expr = strings.ReplaceAll(expr, p.value, p.variable) } } + return expr, nil } -func removeVariableByName(name string, variables []Template) []Template { - vars := make([]Template, 0, len(variables)) - for _, v := range variables { - if v.Name == name { - continue +// Should not replace variables inside double quotes +func RegexpExpandVariables(s string) string { + // check if string starts with a double quote + if s[0:1] == `"` { + return s + } + + if strings.Contains(s, ":") { + // check if variable is __from or __to with advanced formatting + if strings.HasPrefix(trimVariableSyntax(s), from) || strings.HasPrefix(trimVariableSyntax(s), to) { + if strings.Count(s, ":") > 2 { + // Should not replace variables with more than 2 colons returning the original string, promql parser will handle the error. + return s + } + return createPlaceholder(s, valTypeEpoch) + } + // check if variable contains more than 1 colon + if strings.Count(s, ":") > 1 { + // Should not replace variables with more than 1 colon returning the original string, promql parser will handle the error. + return s } - vars = append(vars, v) } - return vars + return createPlaceholder(s, valTypeString) } -func variableSampleValue(s string, variables []Template) (string, error) { - var name, kind, format string - parts := strings.Split(s, ":") - switch len(parts) { - case 1: - // No format - name = s - case 2: - // Could be __from:date, variable:csv, ... - name = parts[0] - format = parts[1] - case 3: - // Could be __from:date:iso, ... - name = parts[0] - kind = parts[1] - format = parts[2] - default: - return "", fmt.Errorf("unknown variable format: %s", s) +// getPlaceholder returns placeholder for a provided variable or value +func getPlaceholder(variable string, value string) *placeholder { + switch { + case variable != "" && value != "": + if p, ok := placeholderByVariable[variable]; ok { + if p.value == value { + return p + } + } + case variable != "": + if p, ok := placeholderByVariable[variable]; ok { + return p + } + case value != "": + if p, ok := placeholderByValue[value]; ok { + return p + } } - // If it is part of the globals, return a string representation of a sample value - if value, ok := globalVariables[name]; ok { - return stringValue(name, value, kind, format) + return nil +} + +// assignPlaceholder assigns a placeholder to a variable it ensures both placeholderByVariable and placeholderByValue are updated +func assignPlaceholder(placeholder placeholder) error { + if placeholder.variable == "" || placeholder.value == "" { + return fmt.Errorf("variable and value must not be empty") } - // If it is an auto interval variable, replace with a sample value of 10s - if strings.HasPrefix(name, "__auto_interval") { - return "10s", nil + // Check if variable and value combination already exists + if getPlaceholder(placeholder.variable, placeholder.value) != nil { + return nil } - // If it is a template variable and we have a value, we use it - for _, v := range variables { - if v.Name != name { - continue - } - // if it has a current value, use it - c, err := v.Current.Get() - if err != nil { - return "", err + // check if value already exists but with a different variable + p := getPlaceholder("", placeholder.value) + if p != nil { + if p.variable != placeholder.variable { + return fmt.Errorf("value %s already assigned to variable %s", placeholder.value, p.variable) } - if c.Value != "" { - // Recursively expand, without the current variable to avoid infinite recursion - return expandVariables(c.Value, removeVariableByName(name, variables)) - } - // If it has options, use the first option - if len(v.Options) > 0 { - // Recursively expand, without the current variable to avoid infinite recursion - o, err := v.Options[0].Get() - if err != nil { - return "", err - } - return expandVariables(o.Value, removeVariableByName(name, variables)) + } + // check if variable already exists but with a different value + p = getPlaceholder(placeholder.variable, "") + if p != nil { + if p.value != placeholder.value { + return fmt.Errorf("variable %s already assigned to value %s", placeholder.variable, p.value) } } - // Assume variable type is a string - return stringValue(name, name, kind, format) + // add placeholder to placeholderByVariable + placeholderByVariable[placeholder.variable] = &placeholder + // add placeholder to placeholderByValue + placeholderByValue[placeholder.value] = &placeholder + return nil } -var variableRegexp = regexp.MustCompile( - strings.Join([]string{ - `\$([[:word:]]+)`, // $var syntax - `\$\{([^}]+)\}`, // ${var} syntax - `\[\[([^\[\]]+)\]\]`, // [[var]] syntax - }, "|"), -) +func getValueType(value string) valType { + // value might be provided as an integer, so we need to check if it can be parsed as an integer and then add s to the end) + if _, err := strconv.Atoi(value); err == nil { + value = value + "s" + } + // check if variable is a time range + if _, err := model.ParseDuration(value); err == nil { + return valTypeTimeRange + } + // check if variable is epoch, this is used for promql @ modifier + if _, err := strconv.ParseFloat(value, 64); err == nil { + return valTypeEpoch + } + return valTypeString +} -func expandVariables(expr string, variables []Template) (string, error) { - parts := strings.Split(expr, "\"") - for i, part := range parts { - if i%2 == 1 { - // Inside a double quote string, just add it - continue +// createPlaceholder returns a placeholder for a variable. +func createPlaceholder(variable string, valType valType) string { + // check if variable already has a placeholder + if p := getPlaceholder(variable, ""); p != nil { + return p.value + } + // create placeholder + counter := 0 + var value string + for { + if valType == valTypeTimeRange { + // Using magicTimeRange as a seed for the placeholder + timeRange := magicTimeRange + counter + value = strconv.Itoa(timeRange) + } + if valType == valTypeDuration { + // Using magicTimeRange as a seed for the placeholder + duration := magicTimeRange + counter + value = strconv.Itoa(duration) + "s" + } + if valType == valTypeEpoch { + // Using magicEpoch as a seed for the placeholder + epoch := magicEpoch + float64(counter) + // trim epoch to 3 decimal places since that is the precision used in prometheus + value = fmt.Sprintf("%.3f", epoch) + } + if valType == valTypeString { + value = fmt.Sprintf("%s_%s_%d", magicString, trimVariableSyntax(variable), counter) } - // Accumulator to store the processed submatches - var subparts []string - // Cursor indicates where we are in the part being processed - cursor := 0 - for _, v := range variableRegexp.FindAllStringSubmatchIndex(part, -1) { - // Add all until match starts - subparts = append(subparts, part[cursor:v[0]]) - // Iterate on all the subgroups and find the one that matched - for j := 2; j < len(v); j += 2 { - if v[j] < 0 { - continue - } - // Replace the match with sample value - val, err := variableSampleValue(part[v[j]:v[j+1]], variables) - if err != nil { - return "", err - } - subparts = append(subparts, val) + if _, ok := placeholderByValue[value]; !ok { + err := assignPlaceholder(placeholder{variable: variable, valType: valType, value: value}) + if err == nil { + return value } - // Move the start cursor at the end of the current match - cursor = v[1] } - // Add rest of the string - subparts = append(subparts, parts[i][cursor:]) - // Merge all back into the parts - parts[i] = strings.Join(subparts, "") + counter++ + if counter > 10000 { + // this should never happen... but just in case... lets panic... + panic("createPlaceholder: counter > 10000 - this should never happen :(") + } } - return strings.Join(parts, "\""), nil } -func expandLogQLVariables(expr string, variables []Template) (string, error) { - lines := strings.Split(expr, "\n") - for i, line := range lines { - parts := strings.Split(line, "\"") - for j, part := range parts { - if j%2 == 1 { - // Inside a double quote string, just add it - continue - } +// Helper func to remove the variable syntax from a string +func trimVariableSyntax(s string) string { + s = strings.TrimPrefix(s, "[[") + s = strings.TrimPrefix(s, "${") + s = strings.TrimPrefix(s, "$") + + s = strings.TrimSuffix(s, "]]") + s = strings.TrimSuffix(s, "}") + + // replace all ":" with "_" + s = strings.ReplaceAll(s, ":", "_") + + return s +} - // Accumulator to store the processed submatches - var subparts []string - // Cursor indicates where we are in the part being processed - cursor := 0 - for _, v := range variableRegexp.FindAllStringSubmatchIndex(part, -1) { - // Add all until match starts - subparts = append(subparts, part[cursor:v[0]]) - // Iterate on all the subgroups and find the one that matched - for k := 2; k < len(v); k += 2 { - if v[k] < 0 { - continue - } - // Replace the match with sample value - val, err := variableSampleValue(part[v[k]:v[k+1]], variables) - if err != nil { - return "", err - } - // If the variable is within square brackets, remove the '$' prefix - if strings.HasPrefix(part[v[0]-1:v[0]], "[") && strings.HasSuffix(part[v[1]:v[1]+1], "]") { - val = strings.TrimPrefix(val, "$") - } - subparts = append(subparts, val) - } - // Move the start cursor at the end of the current match - cursor = v[1] +// Helper func to check if string has variable syntax +func checkVariableSyntax(s string) bool { + return strings.Contains(s, "$") || strings.Contains(s, "[[") || strings.Contains(s, "{") +} + +// Helper func to get the value of a template variable +func getTemplateVariableValue(v Template) string { + var value string + // do not handle error + c, _ := v.Current.Get() + // check if variable has a value + if c.Value == "" { + if len(v.Options) > 0 { + // Do not handle error + o, _ := v.Options[0].Get() + if o.Value != "" { + value = o.Value } - // Add rest of the string - subparts = append(subparts, part[cursor:]) - // Merge all back into the parts - parts[j] = strings.Join(subparts, "") } - lines[i] = strings.Join(parts, "\"") + } else { + value = c.Value } - result := strings.Join(lines, "\n") - return result, nil + // check value for variable syntax + if checkVariableSyntax(value) { + // lazy way of dealing with __auto_interval... + if strings.HasPrefix(trimVariableSyntax(value), "__auto_interval") { + // This will result in a placeholder with type timeRange + value = "9001s" + } else { + // try to expand variable + varValue := getPlaceholder(value, "") + if varValue != nil { + value = varValue.value + } + } + } + return value +} + +func expandLogQLVariables(expr string, variables []Template) (string, error) { + expr = expandVariables(expr, variables) + // TODO: Use the logql parsing to validate this is parsable logql. + return expr, nil } diff --git a/lint/variables_test.go b/lint/variables_test.go index 1f0e080..84ae467 100644 --- a/lint/variables_test.go +++ b/lint/variables_test.go @@ -20,128 +20,133 @@ func TestVariableExpansion(t *testing.T) { expr: "up{job=~\"$job\"}", result: "up{job=~\"$job\"}", }, - // https://grafana.com/docs/grafana/latest/variables/syntax/ + // https: //grafana.com/docs/grafana/latest/variables/syntax/ { desc: "Should replace variables in metric name", expr: "up$var{job=~\"$job\"}", - result: "upvar{job=~\"$job\"}", + result: "upbgludgvy_var_0{job=~\"$job\"}", }, { desc: "Should replace global rate/range variables", - expr: "rate(metric{}[$__rate_interval])", - result: "rate(metric{}[8869990787ms])", + expr: "rate(metric{}[11277964])", + result: "rate(metric{}[11277964])", }, { desc: "Should support ${...} syntax", expr: "rate(metric{}[${__rate_interval}])", - result: "rate(metric{}[8869990787ms])", + result: "rate(metric{}[11277965])", }, { desc: "Should support [[...]] syntax", expr: "rate(metric{}[[[__rate_interval]]])", - result: "rate(metric{}[8869990787ms])", + result: "rate(metric{}[11277966])", }, // https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/ { desc: "Should support ${__user.id}", expr: "sum(http_requests_total{method=\"GET\"} @ ${__user.id})", - result: "sum(http_requests_total{method=\"GET\"} @ 42)", + result: "sum(http_requests_total{method=\"GET\"} @ 1294671549264.000)", }, { desc: "Should support $__from/$__to", expr: "sum(http_requests_total{method=\"GET\"} @ $__from)", - result: "sum(http_requests_total{method=\"GET\"} @ 1594671549254)", + result: "sum(http_requests_total{method=\"GET\"} @ 1294671549254.000)", }, { desc: "Should support $__from/$__to with formatting option (unix seconds)", - expr: "sum(http_requests_total{method=\"GET\"} @ ${__from:date:seconds}000)", - result: "sum(http_requests_total{method=\"GET\"} @ 1594671549000)", + expr: "sum(http_requests_total{method=\"GET\"} @ ${__from:date:seconds})", + result: "sum(http_requests_total{method=\"GET\"} @ 1294671549266.000)", }, { desc: "Should support $__from/$__to with formatting option (iso default)", expr: "sum(http_requests_total{method=\"GET\"} @ ${__from:date})", - result: "sum(http_requests_total{method=\"GET\"} @ 2020-07-13T20:19:09Z)", + result: "sum(http_requests_total{method=\"GET\"} @ 1294671549267.000)", }, { desc: "Should support $__from/$__to with formatting option (iso)", expr: "sum(http_requests_total{method=\"GET\"} @ ${__from:date:iso})", - result: "sum(http_requests_total{method=\"GET\"} @ 2020-07-13T20:19:09Z)", + result: "sum(http_requests_total{method=\"GET\"} @ 1294671549268.000)", }, { - desc: "Should not support $__from/$__to with momentjs formatting option (iso)", - expr: "sum(http_requests_total{method=\"GET\"} @ ${__from:date:YYYY-MM})", - err: fmt.Errorf("Unsupported momentjs time format: YYYY-MM"), + desc: "Should not support $__from/$__to with momentjs formatting option (iso)", + expr: "sum(http_requests_total{method=\"GET\"} @ ${__from:date:YYYY-MM})", + result: "sum(http_requests_total{method=\"GET\"} @ 1294671549269.000)", }, // https://grafana.com/docs/grafana/latest/variables/advanced-variable-format-options/ { desc: "Should support ${variable:csv} syntax", expr: "max by(${variable:csv}) (rate(cpu{}[$__rate_interval]))", - result: "max by(variable,variable,variable) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_csv_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:doublequote} syntax", expr: "max by(${variable:doublequote}) (rate(cpu{}[$__rate_interval]))", - result: "max by(\"variable\",\"variable\",\"variable\") (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_doublequote_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:glob} syntax", expr: "max by(${variable:glob}) (rate(cpu{}[$__rate_interval]))", - result: "max by({variable,variable,variable}) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_glob_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:json} syntax", expr: "max by(${variable:json}) (rate(cpu{}[$__rate_interval]))", - result: "max by([\"variable\",\"variable\",\"variable\"]) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_json_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:lucene} syntax", expr: "max by(${variable:lucene}) (rate(cpu{}[$__rate_interval]))", - result: "max by((\"variable\" OR \"variable\" OR \"variable\")) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_lucene_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:percentencode} syntax", expr: "max by(${variable:percentencode}) (rate(cpu{}[$__rate_interval]))", - result: "max by(variable%2Cvariable%2Cvariable) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_percentencode_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:pipe} syntax", expr: "max by(${variable:pipe}) (rate(cpu{}[$__rate_interval]))", - result: "max by(variable|variable|variable) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_pipe_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:raw} syntax", expr: "max by(${variable:raw}) (rate(cpu{}[$__rate_interval]))", - result: "max by(variable,variable,variable) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_raw_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:regex} syntax", expr: "max by(${variable:regex}) (rate(cpu{}[$__rate_interval]))", - result: "max by(variable|variable|variable) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_regex_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:singlequote} syntax", expr: "max by(${variable:singlequote}) (rate(cpu{}[$__rate_interval]))", - result: "max by('variable','variable','variable') (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_singlequote_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:sqlstring} syntax", expr: "max by(${variable:sqlstring}) (rate(cpu{}[$__rate_interval]))", - result: "max by('variable','variable','variable') (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_sqlstring_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:text} syntax", expr: "max by(${variable:text}) (rate(cpu{}[$__rate_interval]))", - result: "max by(variable + variable + variable) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_text_0) (rate(cpu{}[11277964]))", }, { desc: "Should support ${variable:queryparam} syntax", expr: "max by(${variable:queryparam}) (rate(cpu{}[$__rate_interval]))", - result: "max by(var-variable=variable&var-variable=variable&var-variable=variable) (rate(cpu{}[8869990787ms]))", + result: "max by(bgludgvy_variable_queryparam_0) (rate(cpu{}[11277964]))", + }, + { + desc: "Should support using variables for multiplication", + expr: "sum(rate(foo[$__rate_interval])) * $__range_s", + result: "sum(rate(foo[11277964])) * 11277976", }, { desc: "Should return an error for unknown syntax", expr: "max by(${a:b:c:d}) (rate(cpu{}[$__rate_interval]))", - err: fmt.Errorf("unknown variable format: a:b:c:d"), + err: fmt.Errorf("failed to parse expression: max by(${a:b:c:d}) (rate(cpu{}[11277964]))"), }, { desc: "Should replace variables present in the templating", @@ -155,21 +160,31 @@ func TestVariableExpansion(t *testing.T) { }, }, }, + { + Name: "sampling", + Options: []RawTemplateValue{ + map[string]interface{}{ + "value": "1h", + }, + }, + }, { Name: "resolution", Options: []RawTemplateValue{ map[string]interface{}{ "value": "5m", }, - }}, + }, + }, { Name: "var", Type: "query", Current: map[string]interface{}{ "value": "value", - }}, + }, + }, }, - result: "max by(value) (rate(cpu{}[4h:5m]))", + result: "max by(bgludgvy_var_0) (rate(cpu{}[11277982:11277988]))", }, { desc: "Should recursively replace variables", @@ -177,7 +192,7 @@ func TestVariableExpansion(t *testing.T) { variables: []Template{ {Name: "interval", Current: map[string]interface{}{"value": "$__auto_interval_interval"}}, }, - result: "sum (rate(cpu{}[10s]))", + result: "sum (rate(cpu{}[11277982]))", }, { desc: "Should support plain $__auto_interval, generated by grafonnet-lib (https://github.com/grafana/grafonnet-lib/blob/master/grafonnet/template.libsonnet#L100)", @@ -185,19 +200,251 @@ func TestVariableExpansion(t *testing.T) { variables: []Template{ {Name: "interval", Current: map[string]interface{}{"value": "$__auto_interval"}}, }, - result: "sum (rate(cpu{}[10s]))", + result: "sum (rate(cpu{}[11277982]))", }, + } { + s, err := expandPromQlVariables(tc.expr, tc.variables) + require.Equal(t, tc.err, err) + require.Equal(t, tc.result, s, tc.desc) + } +} + +func TestReverseVariableExpansion(t *testing.T) { + placeholderByValue = map[string]*placeholder{ + "11277964": {variable: "$__rate_interval", valType: 1, value: "11277964"}, + "11277965": {variable: "${__rate_interval}", valType: 1, value: "11277965"}, + "11277966": {variable: "[[__rate_interval]]", valType: 1, value: "11277966"}, + "11277967": {variable: "$__interval", valType: 1, value: "11277967"}, + "11277968": {variable: "${__interval}", valType: 1, value: "11277968"}, + "11277969": {variable: "[[__interval]]", valType: 1, value: "11277969"}, + "11277970": {variable: "$__interval_ms", valType: 1, value: "11277970"}, + "11277971": {variable: "${__interval_ms}", valType: 1, value: "11277971"}, + "11277972": {variable: "[[__interval_ms]]", valType: 1, value: "11277972"}, + "11277973": {variable: "$__range_ms", valType: 1, value: "11277973"}, + "11277974": {variable: "${__range_ms}", valType: 1, value: "11277974"}, + "11277975": {variable: "[[__range_ms]]", valType: 1, value: "11277975"}, + "11277976": {variable: "$__range_s", valType: 1, value: "11277976"}, + "11277977": {variable: "${__range_s}", valType: 1, value: "11277977"}, + "11277978": {variable: "[[__range_s]]", valType: 1, value: "11277978"}, + "11277979": {variable: "$__range", valType: 1, value: "11277979"}, + "11277980": {variable: "${__range}", valType: 1, value: "11277980"}, + "11277981": {variable: "[[__range]]", valType: 1, value: "11277981"}, + "11277982": {variable: "$interval", valType: 1, value: "11277982"}, + "11277983": {variable: "${interval}", valType: 1, value: "11277983"}, + "11277984": {variable: "[[interval]]", valType: 1, value: "11277984"}, + "11277985": {variable: "$sampling", valType: 1, value: "11277985"}, + "11277986": {variable: "${sampling}", valType: 1, value: "11277986"}, + "11277987": {variable: "[[sampling]]", valType: 1, value: "11277987"}, + "11277988": {variable: "$resolution", valType: 1, value: "11277988"}, + "11277989": {variable: "${resolution}", valType: 1, value: "11277989"}, + "11277990": {variable: "[[resolution]]", valType: 1, value: "11277990"}, + "1294671549254.000": {variable: "$__from", valType: 2, value: "1294671549254.000"}, + "1294671549255.000": {variable: "${__from}", valType: 2, value: "1294671549255.000"}, + "1294671549256.000": {variable: "[[__from]]", valType: 2, value: "1294671549256.000"}, + "1294671549257.000": {variable: "$__to", valType: 2, value: "1294671549257.000"}, + "1294671549258.000": {variable: "${__to}", valType: 2, value: "1294671549258.000"}, + "1294671549259.000": {variable: "[[__to]]", valType: 2, value: "1294671549259.000"}, + "1294671549260.000": {variable: "$__org", valType: 2, value: "1294671549260.000"}, + "1294671549261.000": {variable: "${__org}", valType: 2, value: "1294671549261.000"}, + "1294671549262.000": {variable: "[[__org]]", valType: 2, value: "1294671549262.000"}, + "1294671549263.000": {variable: "$__user.id", valType: 2, value: "1294671549263.000"}, + "1294671549264.000": {variable: "${__user.id}", valType: 2, value: "1294671549264.000"}, + "1294671549265.000": {variable: "[[__user.id]]", valType: 2, value: "1294671549265.000"}, + "1294671549266.000": {variable: "${__from:date:seconds}", valType: 2, value: "1294671549266.000"}, + "1294671549267.000": {variable: "${__from:date}", valType: 2, value: "1294671549267.000"}, + "1294671549268.000": {variable: "${__from:date:iso}", valType: 2, value: "1294671549268.000"}, + "1294671549269.000": {variable: "${__from:date:YYYY-MM}", valType: 2, value: "1294671549269.000"}, + "bgludgvy___dashboard_0": {variable: "$__dashboard", valType: 0, value: "bgludgvy___dashboard_0"}, + "bgludgvy___dashboard_1": {variable: "${__dashboard}", valType: 0, value: "bgludgvy___dashboard_1"}, + "bgludgvy___dashboard_2": {variable: "[[__dashboard]]", valType: 0, value: "bgludgvy___dashboard_2"}, + "bgludgvy___name_0": {variable: "$__name", valType: 0, value: "bgludgvy___name_0"}, + "bgludgvy___name_1": {variable: "${__name}", valType: 0, value: "bgludgvy___name_1"}, + "bgludgvy___name_2": {variable: "[[__name]]", valType: 0, value: "bgludgvy___name_2"}, + "bgludgvy___org.name_0": {variable: "$__org.name", valType: 0, value: "bgludgvy___org.name_0"}, + "bgludgvy___org.name_1": {variable: "${__org.name}", valType: 0, value: "bgludgvy___org.name_1"}, + "bgludgvy___org.name_2": {variable: "[[__org.name]]", valType: 0, value: "bgludgvy___org.name_2"}, + "bgludgvy___timeFilter_0": {variable: "$__timeFilter", valType: 0, value: "bgludgvy___timeFilter_0"}, + "bgludgvy___timeFilter_1": {variable: "${__timeFilter}", valType: 0, value: "bgludgvy___timeFilter_1"}, + "bgludgvy___timeFilter_2": {variable: "[[__timeFilter]]", valType: 0, value: "bgludgvy___timeFilter_2"}, + "bgludgvy___user.email_0": {variable: "$__user.email", valType: 0, value: "bgludgvy___user.email_0"}, + "bgludgvy___user.email_1": {variable: "${__user.email}", valType: 0, value: "bgludgvy___user.email_1"}, + "bgludgvy___user.email_2": {variable: "[[__user.email]]", valType: 0, value: "bgludgvy___user.email_2"}, + "bgludgvy___user.login_0": {variable: "$__user.login", valType: 0, value: "bgludgvy___user.login_0"}, + "bgludgvy___user.login_1": {variable: "${__user.login}", valType: 0, value: "bgludgvy___user.login_1"}, + "bgludgvy___user.login_2": {variable: "[[__user.login]]", valType: 0, value: "bgludgvy___user.login_2"}, + "bgludgvy_namespaces_0": {variable: "$namespaces", valType: 0, value: "bgludgvy_namespaces_0"}, + "bgludgvy_namespaces_1": {variable: "${namespaces}", valType: 0, value: "bgludgvy_namespaces_1"}, + "bgludgvy_namespaces_2": {variable: "[[namespaces]]", valType: 0, value: "bgludgvy_namespaces_2"}, + "bgludgvy_timeFilter_0": {variable: "$timeFilter", valType: 0, value: "bgludgvy_timeFilter_0"}, + "bgludgvy_timeFilter_1": {variable: "${timeFilter}", valType: 0, value: "bgludgvy_timeFilter_1"}, + "bgludgvy_timeFilter_2": {variable: "[[timeFilter]]", valType: 0, value: "bgludgvy_timeFilter_2"}, + "bgludgvy_var_0": {variable: "$var", valType: 0, value: "bgludgvy_var_0"}, + "bgludgvy_var_1": {variable: "${var}", valType: 0, value: "bgludgvy_var_1"}, + "bgludgvy_var_2": {variable: "[[var]]", valType: 0, value: "bgludgvy_var_2"}, + "bgludgvy_variable_csv_0": {variable: "${variable:csv}", valType: 0, value: "bgludgvy_variable_csv_0"}, + "bgludgvy_variable_doublequote_0": {variable: "${variable:doublequote}", valType: 0, value: "bgludgvy_variable_doublequote_0"}, + "bgludgvy_variable_glob_0": {variable: "${variable:glob}", valType: 0, value: "bgludgvy_variable_glob_0"}, + "bgludgvy_variable_json_0": {variable: "${variable:json}", valType: 0, value: "bgludgvy_variable_json_0"}, + "bgludgvy_variable_lucene_0": {variable: "${variable:lucene}", valType: 0, value: "bgludgvy_variable_lucene_0"}, + "bgludgvy_variable_percentencode_0": {variable: "${variable:percentencode}", valType: 0, value: "bgludgvy_variable_percentencode_0"}, + "bgludgvy_variable_pipe_0": {variable: "${variable:pipe}", valType: 0, value: "bgludgvy_variable_pipe_0"}, + "bgludgvy_variable_queryparam_0": {variable: "${variable:queryparam}", valType: 0, value: "bgludgvy_variable_queryparam_0"}, + "bgludgvy_variable_raw_0": {variable: "${variable:raw}", valType: 0, value: "bgludgvy_variable_raw_0"}, + "bgludgvy_variable_regex_0": {variable: "${variable:regex}", valType: 0, value: "bgludgvy_variable_regex_0"}, + "bgludgvy_variable_singlequote_0": {variable: "${variable:singlequote}", valType: 0, value: "bgludgvy_variable_singlequote_0"}, + "bgludgvy_variable_sqlstring_0": {variable: "${variable:sqlstring}", valType: 0, value: "bgludgvy_variable_sqlstring_0"}, + "bgludgvy_variable_text_0": {variable: "${variable:text}", valType: 0, value: "bgludgvy_variable_text_0"}, + } + for _, tc := range []struct { + desc string + expr string + result string + }{ { - desc: "Should recursively replace variables, but not run into an infinite loop", - expr: "sum (rate(cpu{}[$interval]))", - variables: []Template{ - {Name: "interval", Current: map[string]interface{}{"value": "$interval"}}, - }, - result: "sum (rate(cpu{}[interval]))", + desc: "Should not replace variables in quoted strings", + expr: "up{job=~\"$job\"}", + result: "up{job=~\"$job\"}", + }, + // https: //grafana.com/docs/grafana/latest/variables/syntax/ + { + desc: "Should replace variables in metric name", + expr: "upbgludgvy_var_0{job=~\"$job\"}", + result: "up$var{job=~\"$job\"}", + }, + { + desc: "Should replace global rate/range variables", + expr: "rate(metric{}[130d12h46m4s])", + result: "rate(metric{}[$__rate_interval])", + }, + { + desc: "Should support ${...} syntax", + expr: "rate(metric{}[130d12h46m5s])", + result: "rate(metric{}[${__rate_interval}])", + }, + { + desc: "Should support [[...]] syntax", + expr: "rate(metric{}[130d12h46m6s])", + result: "rate(metric{}[[[__rate_interval]]])", + }, + // https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/ + { + desc: "Should support ${__user.id}", + expr: "sum(http_requests_total{method=\"GET\"} @ 1294671549264.000)", + result: "sum(http_requests_total{method=\"GET\"} @ ${__user.id})", + }, + { + desc: "Should support $__from/$__to", + expr: "sum(http_requests_total{method=\"GET\"} @ 1294671549254.000)", + result: "sum(http_requests_total{method=\"GET\"} @ $__from)", + }, + { + desc: "Should support $__from/$__to with formatting option (unix seconds)", + expr: "sum(http_requests_total{method=\"GET\"} @ 1294671549266.000)", + result: "sum(http_requests_total{method=\"GET\"} @ ${__from:date:seconds})", + }, + { + desc: "Should support $__from/$__to with formatting option (iso default)", + expr: "sum(http_requests_total{method=\"GET\"} @ 1294671549267.000)", + result: "sum(http_requests_total{method=\"GET\"} @ ${__from:date})", + }, + { + desc: "Should support $__from/$__to with formatting option (iso)", + expr: "sum(http_requests_total{method=\"GET\"} @ 1294671549268.000)", + result: "sum(http_requests_total{method=\"GET\"} @ ${__from:date:iso})", + }, + { + desc: "Should not support $__from/$__to with momentjs formatting option (iso)", + expr: "sum(http_requests_total{method=\"GET\"} @ 1294671549269.000)", + result: "sum(http_requests_total{method=\"GET\"} @ ${__from:date:YYYY-MM})", + }, + // https://grafana.com/docs/grafana/latest/variables/advanced-variable-format-options/ + { + desc: "Should support ${variable:csv} syntax", + expr: "max by(bgludgvy_variable_csv_0) (rate(cpu{}[130d12h46m4s]))", + result: "max by(${variable:csv}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:doublequote} syntax", + expr: "max by(bgludgvy_variable_doublequote_0) (rate(cpu{}[130d12h46m4s]))", + result: "max by(${variable:doublequote}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:glob} syntax", + expr: "max by(bgludgvy_variable_glob_0) (rate(cpu{}[130d12h46m4s]))", + result: "max by(${variable:glob}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:json} syntax", + expr: "max by(bgludgvy_variable_json_0) (rate(cpu{}[130d12h46m4s]))", + result: "max by(${variable:json}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:lucene} syntax", + expr: "max by(bgludgvy_variable_lucene_0) (rate(cpu{}[130d12h46m4s]))", + result: "max by(${variable:lucene}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:percentencode} syntax", + expr: "max by(bgludgvy_variable_percentencode_0) (rate(cpu{}[130d12h46m4s]))", + result: "max by(${variable:percentencode}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:pipe} syntax", + expr: "max by(bgludgvy_variable_pipe_0) (rate(cpu{}[130d12h46m4s]))", + result: "max by(${variable:pipe}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:raw} syntax", + expr: "max by(bgludgvy_variable_raw_0) (rate(cpu{}[130d12h46m4s]))", + result: "max by(${variable:raw}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:regex} syntax", + expr: "max by(bgludgvy_variable_regex_0) (rate(cpu{}[130d12h46m4s]))", + result: "max by(${variable:regex}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:singlequote} syntax", + expr: "max by(bgludgvy_variable_singlequote_0) (rate(cpu{}[130d12h46m4s]))", + result: "max by(${variable:singlequote}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:sqlstring} syntax", + expr: "max by(bgludgvy_variable_sqlstring_0) (rate(cpu{}[130d12h46m4s]))", + result: "max by(${variable:sqlstring}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:text} syntax", + expr: "max by(bgludgvy_variable_text_0) (rate(cpu{}[130d12h46m4s]))", + result: "max by(${variable:text}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should support ${variable:queryparam} syntax", + expr: "max by(bgludgvy_variable_queryparam_0) (rate(cpu{}[130d12h46m4s]))", + result: "max by(${variable:queryparam}) (rate(cpu{}[$__rate_interval]))", + }, + { + desc: "Should replace variables present in the templating", + expr: "max by(bgludgvy_var_0) (rate(cpu{}[130d12h46m22s:130d12h46m28s]))", + result: "max by($var) (rate(cpu{}[$interval:$resolution]))", + }, + { + desc: "Should support using variables for multiplication", + expr: "sum(rate(foo[130d12h46m4s])) * 11277976", + result: "sum(rate(foo[$__rate_interval])) * $__range_s", + }, + { + desc: "Should recursively replace variables", + expr: "sum (rate(cpu{}[130d12h46m22s]))", + result: "sum (rate(cpu{}[$interval]))", + }, + { + desc: "Should support plain $__auto_interval, generated by grafonnet-lib (https://github.com/grafana/grafonnet-lib/blob/master/grafonnet/template.libsonnet#L100)", + expr: "sum (rate(cpu{}[130d12h46m22s]))", + result: "sum (rate(cpu{}[$interval]))", }, } { - s, err := expandVariables(tc.expr, tc.variables) - require.Equal(t, tc.err, err) + s, _ := revertExpandedVariables(tc.expr) require.Equal(t, tc.result, s, tc.desc) } } diff --git a/main.go b/main.go index d85e7a3..20f0b1f 100644 --- a/main.go +++ b/main.go @@ -14,11 +14,14 @@ import ( "github.com/grafana/dashboard-linter/lint" ) -var lintStrictFlag bool -var lintVerboseFlag bool -var lintAutofixFlag bool -var lintReadFromStdIn bool -var lintConfigFlag string +var ( + lintStrictFlag bool + lintVerboseFlag bool + lintAutofixFlag bool + lintReadFromStdIn bool + lintConfigFlag string + ExperimentalFlag bool +) // lintCmd represents the lint command var lintCmd = &cobra.Command{ @@ -69,7 +72,7 @@ var lintCmd = &cobra.Command{ config.Verbose = lintVerboseFlag config.Autofix = lintAutofixFlag - rules := lint.NewRuleSet() + rules := lint.NewRuleSet(ExperimentalFlag, config.RuleSettings) results, err := rules.Lint([]lint.Dashboard{dashboard}) if err != nil { return fmt.Errorf("failed to lint dashboard: %v", err) @@ -119,9 +122,11 @@ var rulesCmd = &cobra.Command{ Short: "Print documentation about each lint rule.", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - rules := lint.NewRuleSet() + rules := lint.NewRuleSet(ExperimentalFlag, lint.ConfigurationRuleSettings{}) + fmt.Fprintf(os.Stdout, "%-40s %-15s %s\n", "Rule Name", "Stability", "Description") + fmt.Fprintf(os.Stdout, "%-40s %-15s %s\n", "---------", "---------", "-----------") for _, rule := range rules.Rules() { - fmt.Fprintf(os.Stdout, "* `%s` - %s\n", rule.Name(), rule.Description()) + fmt.Fprintf(os.Stdout, "%-40s %-15s %s\n", rule.Name(), rule.Stability(), rule.Description()) } return nil }, @@ -129,7 +134,6 @@ var rulesCmd = &cobra.Command{ func init() { rootCmd.AddCommand(lintCmd) - rootCmd.AddCommand(rulesCmd) lintCmd.Flags().BoolVar( &lintStrictFlag, "strict", @@ -161,6 +165,19 @@ func init() { false, "read from stdin", ) + lintCmd.Flags().BoolVar( + &ExperimentalFlag, + "experimental", + false, + "enable experimental rules", + ) + rootCmd.AddCommand(rulesCmd) + rulesCmd.Flags().BoolVar( + &ExperimentalFlag, + "experimental", + false, + "enable experimental rules", + ) } var rootCmd = &cobra.Command{