diff --git a/.betterer.results b/.betterer.results index 5c05d08f6a4..99d10079c92 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1320,10 +1320,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/core/services/echo/backends/analytics/ApplicationInsightsBackend.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] - ], "public/app/core/services/echo/backends/analytics/RudderstackBackend.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -1646,8 +1642,7 @@ exports[`better eslint`] = { ], "public/app/features/alerting/unified/components/AlertLabels.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "public/app/features/alerting/unified/components/AnnotationDetailsField.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -2876,6 +2871,9 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/saving/shared.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], + "public/app/features/dashboard-scene/scene/DashboardControls.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/.golangci.toml b/.golangci.toml index 7c8d17a625e..55c0e4573e2 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -129,14 +129,20 @@ max-func-lines = 60 [linters] disable-all = true +# try to keep this list sorted, please enable = [ + "asciicheck", "bodyclose", "depguard", "dogsled", "errcheck", + "errorlint", + "exhaustive", + "exportloopref", # "gochecknoinits", # "goconst", # "gocritic", # Temporarily disabled on 2022-09-09, running into weird bug "ruleguard: execution error: used Run() with an empty rule set; forgot to call Load() first?" + "gocyclo", "goimports", "goprintffuncname", "gosec", @@ -145,19 +151,14 @@ enable = [ "ineffassign", "misspell", "nakedret", - "exportloopref", + "prealloc", + "revive", "staticcheck", "stylecheck", "typecheck", "unconvert", "unused", "whitespace", - "gocyclo", - "exhaustive", - "typecheck", - "asciicheck", - "errorlint", - "revive", ] # Disabled linters (might want them later) diff --git a/cypress.config.js b/cypress.config.js index 52263a9e4d0..a03692e2ac0 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -4,6 +4,7 @@ const path = require('path'); const benchmarkPlugin = require('./e2e/cypress/plugins/benchmark/index'); const readProvisions = require('./e2e/cypress/plugins/readProvisions'); +const smtpTester = require('./e2e/cypress/plugins/smtpTester'); const typescriptPreprocessor = require('./e2e/cypress/plugins/typescriptPreprocessor'); module.exports = defineConfig({ @@ -27,6 +28,10 @@ module.exports = defineConfig({ benchmarkPlugin.initialize(on, config); } + if (config.env['SMTP_PLUGIN_ENABLED'] === true) { + smtpTester.initialize(on, config); + } + on('task', { readProvisions: (filePaths) => readProvisions({ CWD: process.cwd(), filePaths }), }); diff --git a/docs/sources/administration/feature-toggles/index.md b/docs/sources/administration/feature-toggles/index.md index 4257352637e..90d08353bec 100644 --- a/docs/sources/administration/feature-toggles/index.md +++ b/docs/sources/administration/feature-toggles/index.md @@ -50,6 +50,8 @@ To edit a feature toggle, follow these steps: 1. Navigate to the list of feature toggles and select your feature state overrides. 1. Click **Save changes** and wait for your Grafana instance to restart with the updated feature toggles. -{{% admonition type="note" %}} +{{< admonition type="note" >}} +If you don't have the feature toggle management page, enable the `featureToggleAdminPage` feature toggle. + Editing feature toggles with the feature toggle management page is available now in all tiers of [Grafana Cloud](/docs/grafana-cloud/). -{{% /admonition %}} +{{< /admonition >}} diff --git a/docs/sources/alerting/alerting-rules/templating-labels-annotations.md b/docs/sources/alerting/alerting-rules/templating-labels-annotations.md index f8773bce011..321d8bfd720 100644 --- a/docs/sources/alerting/alerting-rules/templating-labels-annotations.md +++ b/docs/sources/alerting/alerting-rules/templating-labels-annotations.md @@ -32,6 +32,10 @@ All templates should be written in [text/template](https://pkg.go.dev/text/templ Each template is evaluated whenever the alert rule is evaluated, and is evaluated for every alert separately. For example, if your alert rule has a templated summary annotation, and the alert rule has 10 firing alerts, then the template will be executed 10 times, once for each alert. You should try to avoid doing expensive computations in your templates as much as possible. +{{% admonition type="caution" %}} +Extra whitespace in label templates can break matches with notification policies. +{{% /admonition %}} + ## Examples The following examples attempt to show the most common use-cases we have seen for templates. You can use these examples verbatim, or adapt them as necessary for your use case. For more information on how to write text/template refer see [the beginner's guide to alert notification templates in Grafana](https://grafana.com/blog/2023/04/05/grafana-alerting-a-beginners-guide-to-templating-alert-notifications/). diff --git a/docs/sources/alerting/fundamentals/_index.md b/docs/sources/alerting/fundamentals/_index.md index b9dd88611e3..3ef02d5c1b5 100644 --- a/docs/sources/alerting/fundamentals/_index.md +++ b/docs/sources/alerting/fundamentals/_index.md @@ -48,6 +48,11 @@ refs: destination: /docs/grafana//alerting/fundamentals/alert-rules/rule-evaluation/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/rule-evaluation/ + group-alert-notifications: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/fundamentals/notifications/group-alert-notifications/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/notifications/group-alert-notifications/ --- # Introduction to Alerting @@ -56,14 +61,15 @@ Whether you’re just starting out or you're a more experienced user of Grafana The following diagram gives you an overview of Grafana Alerting and introduces you to some of the fundamental features that are the principles of how Grafana Alerting works. -{{< figure src="/media/docs/alerting/how-alerting-works.png" max-width="750px" caption="How Alerting works" >}} +
+ +{{< figure src="/media/docs/alerting/alerting-configure-notifications-v2.png" max-width="750px" alt="How Grafana Alerting works" >}} ## How it works at a glance -- Grafana alerting periodically queries data sources and evaluates the condition defined in the alert rule +- Grafana Alerting periodically queries data sources and evaluates the condition defined in the alert rule - If the condition is breached, an alert instance fires -- Firing and resolved alert instances are routed to notification policies based on matching labels -- Notifications are sent out to the contact points specified in the notification policy +- Firing (and resolved) alert instances are sent for notifications, either directly to a contact point or through notification policies for more flexibility ## Fundamentals @@ -73,9 +79,7 @@ The following concepts are key to your understanding of how Grafana Alerting wor An [alert rule](ref:alert-rules) consists of one or more queries and expressions that select the data you want to measure. It also contains a condition, which is the threshold that an alert rule must meet or exceed to fire. -Add labels to uniquely identify your alert rule and configure alert routing. Labels link alert rules to notification policies, so you can easily manage which policy should handle which alerts and who gets notified. - -After alert rules are created, they go through various states and transitions. +In the alert rule, choose the contact point or notification policies to determine how to receive the alert notifications. ### Alert instances @@ -91,23 +95,27 @@ A rule using the PromQL expression above creates as many alert instances as the {{< figure src="/static/img/docs/alerting/unified/multi-dimensional-alert.png" caption="Multiple alert instances from a single alert rule" >}} -[Alert rules are frequently evaluated](ref:alert-rule-evaluation) and the state of their alert instances is updated accordingly. Only alert instances that are in a firing or resolved state are routed to notification policies to be handled. - -### Notification policies - -[Notification policies](ref:notification-policies) group alerts and then route them to contact points. They determine when notifications are sent, and how often notifications should be repeated. - -Alert instances are matched to notification policies using label matchers. This provides a flexible way to organize and route alerts to different receivers. - -Each policy consists of a set of label matchers (0 or more) that specify which alert instances (identified by their labels) they handle. Notification policies are defined as a tree structure where the root of the notification policy tree is called the **Default notification policy**. Each policy can have child policies. - -{{< figure src="/media/docs/alerting/notification-routing.png" max-width="750px" caption="Notification policy routing" >}} +[Alert rules are frequently evaluated](ref:alert-rule-evaluation) and the state of their alert instances is updated accordingly. Only alert instances that are in a firing or resolved state are sent in notifications. ### Contact points -[Contact points](ref:contact-points) determine where notifications are sent. For example, you might have a contact point that sends notifications to an email address, to Slack, to an incident management system (IRM) such as Grafana OnCall or Pagerduty, or to a webhook. +[Contact points](ref:contact-points) determine the notification message and where notifications are sent. For example, you might have a contact point that sends notifications to an email address, to Slack, to an incident management system (IRM) such as Grafana OnCall or Pagerduty, or to a webhook. -Notifications sent from contact points are customizable with notification templates, which can be shared between contact points. +In the alert rule, you can choose a contact point to receive the alert notifications or use notification policies instead. + +### Notification policies + +[Notification policies](ref:notification-policies) provide a flexible method to handle alert notifications for larger systems. + +Notification policies routes alerts to contact points via label matching. Each notification policy consists of a set of label matchers (0 or more) that specify which alert instances (identified by their labels) they handle. Notification policies are defined in a tree structure, where the root of the notification policy tree is the **Default notification policy**, which ensures all alert instances are handled. + +{{< figure src="/media/docs/alerting/notification-routing.png" max-width="750px" alt="A diagram displaying how the notification policy tree routes alerts" caption="Routing firing alert instances through notification policies" >}} + +
+ +Each notification policy decides where to send the alert (contact point) and when to send the notification (timing options). Additionally, it can [group multiple firing alert instances into a single notification](ref:group-alert-notifications) to reduce alert noise. + +{{< figure src="/media/docs/alerting/alerting-notification-policy-diagram-v5.png" max-width="750px" alt="A diagram of the notification policy component" >}} ### Silences and mute timings @@ -115,16 +123,14 @@ Notifications sent from contact points are customizable with notification templa ### Architecture -Grafana Alerting is built on the Prometheus model of designing alerting systems. - -Prometheus-based alerting systems have two main components: +Grafana Alerting is built on the Prometheus model of designing alerting systems. Prometheus-based alerting systems have two main components: - An alert generator that evaluates alert rules and sends firing and resolved alerts to the alert receiver. - An alert receiver (also known as Alertmanager) that receives the alerts and is responsible for handling them and sending their notifications. -Grafana doesn’t use Prometheus as its default alert generator because Grafana Alerting needs to work with many other data sources in addition to Prometheus. +Grafana Alerting doesn’t use Prometheus as its default alert generator because it works with many other data sources; not just Prometheus. -However, Grafana can also use Prometheus as an alert generator as well as external Alertmanagers. For more information about how to use distinct alerting systems, refer to the [Grafana alert rule types](ref:alert-rules). +However, Grafana can also use Prometheus as an alert generator (refer to [alert rule types](ref:alert-rules)), as well as external Alertmanagers. ## Design your Alerting system diff --git a/docs/sources/alerting/fundamentals/notifications/_index.md b/docs/sources/alerting/fundamentals/notifications/_index.md index 92202f96cd9..2017bfef8d8 100644 --- a/docs/sources/alerting/fundamentals/notifications/_index.md +++ b/docs/sources/alerting/fundamentals/notifications/_index.md @@ -68,7 +68,9 @@ Choosing how, when, and where to send your alert notifications is an important p Start defining your [contact points](ref:contact-points) to specify how to receive your alert notifications. Then, configure your alert rules to send their alerts to either a contact point or use the [Notification Policy Tree](#notification-policies) to flexibly route alerts to contact points. -{{< figure src="/media/docs/alerting/alerting-configure-notifications.svg" max-width="750px" alt="Configure alerts to send notifications to a contact point or via notification policies" caption="Configure alerts to send notifications to a contact point or via notification policies" >}} +
+ +{{< figure src="/media/docs/alerting/alerting-configure-notifications-v2.png" max-width="750px" alt="Configure alert rules to forward firing alerts directly to a contact point or through notification policies" caption="Configure alert rules to forward firing alerts directly to a contact point or through notification policies" >}} ## How it works at a glance diff --git a/docs/sources/alerting/fundamentals/notifications/notification-policies.md b/docs/sources/alerting/fundamentals/notifications/notification-policies.md index ab1200a2f68..e9ca6e4ff03 100644 --- a/docs/sources/alerting/fundamentals/notifications/notification-policies.md +++ b/docs/sources/alerting/fundamentals/notifications/notification-policies.md @@ -72,6 +72,8 @@ By default, once a matching policy is found, the system does not continue to loo The default notification policy matches all alert instances. It always handles alert instances if there are no child policies or if none of the child policies match the alert instance's labels—this prevents any alerts from being missed. +If alerts use multiple labels, these labels must also be present in a notification policy to match and route notifications to a specific contact point. + {{% /admonition %}} {{< collapse title="Routing example" >}} @@ -84,6 +86,10 @@ The `team=security` policy is not a match and **Continue matching siblings** was **Disk Usage – 80%** has both a `team` and `severity` label, and matches a child policy of the operations team. +{{% admonition type="note" %}} +When an alert matches both a parent policy and a child policy (like it does in this case), the routing follows the child policy (`severity`) as it provides a more specific match. +{{% /admonition %}} + **Unauthorized log entry** has a `team` label but does not match the first policy (`team=operations`) since the values are not the same, so it will continue searching and match the `team=security` policy. It does not have any child policies, so the additional `severity=high` label is ignored. {{< /collapse >}} diff --git a/docs/sources/alerting/fundamentals/notifications/templates.md b/docs/sources/alerting/fundamentals/notifications/templates.md index 0866e7dc3a3..c0461d62b2c 100644 --- a/docs/sources/alerting/fundamentals/notifications/templates.md +++ b/docs/sources/alerting/fundamentals/notifications/templates.md @@ -106,6 +106,10 @@ Here are some commonly used built-in [variables](ref:variables-label-annotation) CPU usage for instance1 has exceeded 80% for the last 5 minutes: 81.2345 +{{% admonition type="caution" %}} +Extra whitespace in label templates can break matches with notification policies. +{{% /admonition %}} + ### Template annotations Both labels and annotations have the same structure: a set of named values; however their intended uses are different. The purpose of annotations is to add additional information to existing alerts. diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 9bb322b34fa..53578a6dfb7 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -62,7 +62,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `logRowsPopoverMenu` | Enable filtering menu displayed when text of a log line is selected | Yes | | `lokiQueryHints` | Enables query hints for Loki | Yes | | `alertingQueryOptimization` | Optimizes eligible queries in order to reduce load on datasources | | -| `betterPageScrolling` | Removes CustomScrollbar from the UI, relying on native browser scrollbars | Yes | | `cloudWatchNewLabelParsing` | Updates CloudWatch label parsing to be more accurate | Yes | | `pluginProxyPreserveTrailingSlash` | Preserve plugin proxy trailing slash. | | @@ -101,6 +100,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `groupToNestedTableTransformation` | Enables the group to nested table transformation | | `newPDFRendering` | New implementation for the dashboard-to-PDF rendering | | `ssoSettingsSAML` | Use the new SSO Settings API to configure the SAML connector | +| `openSearchBackendFlowEnabled` | Enables the backend query flow for Open Search datasource plugin | ## Experimental feature toggles @@ -190,6 +190,7 @@ Experimental features might be changed or removed without prior notice. | `alertingCentralAlertHistory` | Enables the new central alert history. | | `azureMonitorPrometheusExemplars` | Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars | | `pinNavItems` | Enables pinning of nav items | +| `databaseReadReplica` | Use a read replica for some database queries. | ## Development feature toggles diff --git a/docs/sources/setup-grafana/image-rendering/_index.md b/docs/sources/setup-grafana/image-rendering/_index.md index d9438fa0142..ed04d5cc64e 100644 --- a/docs/sources/setup-grafana/image-rendering/_index.md +++ b/docs/sources/setup-grafana/image-rendering/_index.md @@ -207,6 +207,47 @@ HTTP_PORT=0 } ``` +#### HTTP protocol + +{{% admonition type="note" %}} +HTTPS protocol is supported in the image renderer v3.11.0 and later. +{{% /admonition %}} + +Change the protocol of the server, it can be `http` or `https`. Default is `http`. + +```json +{ + "service": { + "protocol": "http" + } +} +``` + +#### HTTPS certificate and key file + +Path to the image renderer certificate and key file used to start an HTTPS server. + +```json +{ + "service": { + "certFile": "./path/to/cert", + "certKey": "./path/to/key" + } +} +``` + +#### HTTPS min TLS version + +Minimum TLS version allowed. Accepted values are: `TLSv1.2`, `TLSv1.3`. Default is `TLSv1.2`. + +```json +{ + "service": { + "minTLSVersion": "TLSv1.2" + } +} +``` + #### Enable Prometheus metrics You can enable [Prometheus](https://prometheus.io/) metrics endpoint `/metrics` using the environment variable `ENABLE_METRICS`. Node.js and render request duration metrics are included, see [Enable Prometheus metrics endpoint]({{< relref "./monitoring#enable-prometheus-metrics-endpoint" >}}) for details. diff --git a/docs/sources/shared/alerts/how_label_matching_works.md b/docs/sources/shared/alerts/how_label_matching_works.md index b7409a97fde..49eae1bfcd0 100644 --- a/docs/sources/shared/alerts/how_label_matching_works.md +++ b/docs/sources/shared/alerts/how_label_matching_works.md @@ -25,7 +25,9 @@ A label matchers consists of 3 distinct parts, the **label**, the **value** and | `=~` | Select labels that regex-match the value. | | `!~` | Select labels that do not regex-match the value. | +{{% admonition type="note" %}} If you are using multiple label matchers, they are combined using the AND logical operator. This means that all matchers must match in order to link a rule to a policy. +{{% /admonition %}} **Label matching example** diff --git a/e2e/cypress/plugins/index.js b/e2e/cypress/plugins/index.js index 596d58acca9..2f464a2770a 100644 --- a/e2e/cypress/plugins/index.js +++ b/e2e/cypress/plugins/index.js @@ -4,6 +4,7 @@ const path = require('path'); const benchmarkPlugin = require('./benchmark'); const extendConfig = require('./extendConfig'); const readProvisions = require('./readProvisions'); +const smtpTester = require('./smtpTester'); const typescriptPreprocessor = require('./typescriptPreprocessor'); module.exports = (on, config) => { @@ -11,6 +12,10 @@ module.exports = (on, config) => { benchmarkPlugin.initialize(on, config); } + if (config.env['SMTP_PLUGIN_ENABLED'] === true) { + smtpTester.initialize(on, config); + } + on('file:preprocessor', typescriptPreprocessor); on('task', { log({ message, optional }) { diff --git a/e2e/cypress/plugins/smtpTester.js b/e2e/cypress/plugins/smtpTester.js new file mode 100644 index 00000000000..67a8798a6aa --- /dev/null +++ b/e2e/cypress/plugins/smtpTester.js @@ -0,0 +1,34 @@ +const ms = require('smtp-tester'); + +const PORT = 7777; + +const initialize = (on, config) => { + // starts the SMTP server at localhost:7777 + const mailServer = ms.init(PORT); + console.log('mail server at port %d', PORT); + + let lastEmail = {}; + + // process all emails + mailServer.bind((addr, id, email) => { + lastEmail[email.headers.to] = email; + }); + + on('task', { + resetEmails(recipient) { + if (recipient) { + console.log('reset all emails for recipient %s', recipient); + delete lastEmail[recipient]; + } else { + console.log('reset all emails'); + lastEmail = {}; + } + }, + + getLastEmail(email) { + return lastEmail[email] || null; + }, + }); +}; + +exports.initialize = initialize; diff --git a/e2e/run-suite b/e2e/run-suite index d19c3bbfd1a..30cd8e95b55 100755 --- a/e2e/run-suite +++ b/e2e/run-suite @@ -66,6 +66,7 @@ case "$1" in ;; "enterprise") echo "Enterprise" + env[SMTP_PLUGIN_ENABLED]=true CLEANUP="rm -rf ./e2e/extensions-suite" SETUP="cp -Lr ./e2e/extensions ./e2e/extensions-suite" enterpriseSuite=$(basename "${args[1]}") @@ -105,6 +106,11 @@ case "$1" in ;; esac ;; + "enterprise-smtp") + env[SMTP_PLUGIN_ENABLED]=true + cypressConfig[specPattern]=./e2e/extensions/enterprise/smtp-suite/$testFilesForSingleSuite + cypressConfig[video]=${args[1]} + ;; *) cypressConfig[specPattern]=./e2e/"${args[0]}"/$testFilesForSingleSuite diff --git a/go.mod b/go.mod index 17af7f513e8..a186b0dc4dd 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,6 @@ module github.com/grafana/grafana go 1.22.4 -// Override docker/docker to avoid: -// go: github.com/drone-runners/drone-runner-docker@v1.8.2 requires -// github.com/docker/docker@v0.0.0-00010101000000-000000000000: invalid version: unknown revision 000000000000 -replace github.com/docker/docker => github.com/moby/moby v23.0.4+incompatible - // contains openapi encoder fixes. remove ASAP replace cuelang.org/go => github.com/grafana/cue v0.0.0-20230926092038-971951014e3f // @grafana/grafana-as-code @@ -67,7 +62,7 @@ require ( github.com/go-openapi/strfmt v0.23.0 // @grafana/alerting-backend github.com/go-redis/redis/v8 v8.11.5 // @grafana/grafana-backend-group github.com/go-sourcemap/sourcemap v2.1.3+incompatible // @grafana/grafana-backend-group - github.com/go-sql-driver/mysql v1.7.1 // @grafana/grafana-search-and-storage + github.com/go-sql-driver/mysql v1.8.1 // @grafana/grafana-search-and-storage github.com/go-stack/stack v1.8.1 // @grafana/grafana-backend-group github.com/gobwas/glob v0.2.3 // @grafana/grafana-backend-group github.com/gogo/protobuf v1.3.2 // @grafana/alerting-backend @@ -135,6 +130,8 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // @grafana/alerting-backend github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // @grafana/grafana-operator-experience-squad github.com/olekukonko/tablewriter v0.0.5 // @grafana/grafana-backend-group + github.com/openfga/api/proto v0.0.0-20240529184453-5b0b4941f3e0 // @grafana/identity-access-team + github.com/openfga/openfga v1.5.4 // @grafana/identity-access-team github.com/patrickmn/go-cache v2.1.0+incompatible // @grafana/alerting-backend github.com/prometheus/alertmanager v0.27.0 // @grafana/alerting-backend github.com/prometheus/client_golang v1.19.0 // @grafana/alerting-backend @@ -264,7 +261,6 @@ require ( github.com/dennwc/varint v1.0.0 // indirect github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect @@ -297,7 +293,7 @@ require ( github.com/golang/glog v1.2.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.1.2 // indirect - github.com/google/cel-go v0.17.7 // indirect + github.com/google/cel-go v0.20.1 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -390,7 +386,7 @@ require ( github.com/shopspring/decimal v1.2.0 // indirect github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect - github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/cast v1.6.0 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect @@ -415,7 +411,7 @@ require ( go.opentelemetry.io/otel/metric v1.26.0 // indirect go.opentelemetry.io/proto/otlp v1.2.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.26.0 // indirect + go.uber.org/zap v1.27.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/term v0.21.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect @@ -440,7 +436,25 @@ require ( modernc.org/token v1.1.0 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/yaml v1.3.0 // indirect; @grafana-app-platform-squad + sigs.k8s.io/yaml v1.4.0 // indirect; @grafana-app-platform-squad +) + +require ( + github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/karlseguin/ccache/v3 v3.0.5 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/natefinch/wrap v0.2.0 // indirect + github.com/oklog/ulid/v2 v2.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/viper v1.18.2 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/mock v0.4.0 // indirect ) // Use fork of crewjam/saml with fixes for some issues until changes get merged into upstream @@ -456,3 +470,9 @@ exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible // Use our fork xorm. go.work currently overrides this and points to the local ./pkg/util/xorm directory. replace xorm.io/xorm => github.com/grafana/grafana/pkg/util/xorm v0.0.1 + +// lock for k8s api server compat +replace github.com/google/cel-go => github.com/google/cel-go v0.17.7 + +// lock for mysql tsdb compat +replace github.com/go-sql-driver/mysql => github.com/go-sql-driver/mysql v1.7.1 diff --git a/go.sum b/go.sum index f5c3eec51ad..6d5235fff2a 100644 --- a/go.sum +++ b/go.sum @@ -1343,6 +1343,8 @@ cloud.google.com/go/workflows v1.12.4/go.mod h1:yQ7HUqOkdJK4duVtMeBCAOPiN1ZF1E9p contrib.go.opencensus.io/exporter/aws v0.0.0-20200617204711-c478e41e60e9/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= contrib.go.opencensus.io/exporter/stackdriver v0.13.10/go.mod h1:I5htMbyta491eUxufwwZPQdcKvvgzMB4O9ni41YnIM8= contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/age v1.1.1 h1:pIpO7l151hCnQ4BdyBujnGP2YlUo0uj6sAVNHGBvXHg= filippo.io/age v1.1.1/go.mod h1:l03SrzDUrBkdBx8+IILdnn2KZysqQdbEBUQ4p3sqEQE= @@ -1404,6 +1406,8 @@ github.com/Azure/go-amqp v0.16.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fw github.com/Azure/go-amqp v0.16.4/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= @@ -1469,9 +1473,13 @@ github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYr github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= +github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= @@ -1523,6 +1531,8 @@ github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/apache/arrow/go/arrow v0.0.0-20210223225224-5bea62493d91/go.mod h1:c9sxoIT3YgLxH4UhLOCKaBlEojuMhVYpk4Ntv3opUTQ= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= @@ -1753,6 +1763,9 @@ github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE github.com/containerd/cgroups/v3 v3.0.1/go.mod h1:/vtwk1VXrtoa5AaZLkypuOJgA/6DyPMZHJPGQNtlHnw= github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0= github.com/containerd/containerd v1.2.7/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.7.12 h1:+KQsnv4VnzyxWcfO9mlxxELaoztsDEjOuCMPAuPqgU0= +github.com/containerd/containerd v1.7.12/go.mod h1:/5OMpE1p0ylxtEUGY8kuCYkDRzJm9NO1TFMWjUpdevk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= @@ -1767,6 +1780,8 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= +github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -1818,6 +1833,7 @@ github.com/digitalocean/godo v1.113.0 h1:CLtCxlP4wDAjKIQ+Hshht/UNbgAp8/J/XBH1ZtD github.com/digitalocean/godo v1.113.0/go.mod h1:Z2mTP848Vi3IXXl5YbPekUgr4j4tOePomA+OE1Ag98w= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlmiddlecote/sqlstats v1.0.2 h1:gSU11YN23D/iY50A2zVYwgXgy072khatTsIW6UPjUtI= github.com/dlmiddlecote/sqlstats v1.0.2/go.mod h1:0CWaIh/Th+z2aI6Q9Jpfg/o21zmGxWhbByHgQSCUQvY= @@ -1825,10 +1841,14 @@ github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/ github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docker/distribution v2.7.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/docker v0.7.3-0.20190103212154-2b7e084dc98b/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v0.7.3-0.20190817195342-4760db040282/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v26.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v26.0.2+incompatible h1:yGVmKUFGgcxA6PXWAokO0sQL22BrQ67cgVjko8tGdXE= +github.com/docker/docker v26.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -1906,8 +1926,9 @@ github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2/go.mod h1:VzmDKDJVZ github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= -github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= @@ -1969,6 +1990,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/analysis v0.21.5/go.mod h1:25YcZosX9Lwz2wBsrFrrsL8bmjjXdlyP6zsr2AMy29M= github.com/go-openapi/analysis v0.22.0/go.mod h1:acDnkkCI2QxIo8sSIPgmp1wUlRohV7vfGtAIVae73b0= github.com/go-openapi/analysis v0.22.2 h1:ZBmNoP2h5omLKr/srIC9bfqrUGzT6g6gNv03HE9Vpj0= @@ -2021,9 +2044,6 @@ github.com/go-resty/resty/v2 v2.12.0 h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJ github.com/go-resty/resty/v2 v2.12.0/go.mod h1:o0yGPrkS3lOe1+eFajk6kBW8ScXzwU3hD69/gt2yB/0= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -2396,8 +2416,9 @@ github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOs github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= +github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= @@ -2423,6 +2444,7 @@ github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= @@ -2500,6 +2522,7 @@ github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bY github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= @@ -2510,21 +2533,29 @@ github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwX github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx v3.2.0+incompatible h1:0Vihzu20St42/UDsvZGdNE6jak7oi/UOeMzwMPHkgFY= github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.2.1 h1:gI8os0wpRXFd4FiAY2dWiqRK037tjj3t7rKFeO4X5iw= github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= @@ -2590,6 +2621,8 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8 github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/karlseguin/ccache/v3 v3.0.5 h1:hFX25+fxzNjsRlREYsoGNa2LoVEw5mPF8wkWq/UnevQ= +github.com/karlseguin/ccache/v3 v3.0.5/go.mod h1:qxC372+Qn+IBj8Pe3KvGjHPj0sWwEF7AeZVhsNPZ6uY= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -2635,6 +2668,10 @@ github.com/kshvakov/clickhouse v1.3.5/go.mod h1:DMzX7FxRymoNkVgizH0DWAL8Cur7wHLg github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353 h1:X/79QL0b4YJVO5+OsPH9rF2u428CIrGL/jLmPsoOQQ4= github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U= @@ -2652,6 +2689,8 @@ github.com/linkedin/goavro/v2 v2.10.0 h1:eTBIRoInBM88gITGXYtUSqqxLTFXfOsJBiX8ZMW github.com/linkedin/goavro/v2 v2.10.0/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA= github.com/linode/linodego v1.32.0 h1:OmZzB3iON6uu84VtLFf64uKmAQqJJarvmsVguroioPI= github.com/linode/linodego v1.32.0/go.mod h1:y8GDP9uLVH4jTB9qyrgw79qfKdYJmNCGUOJmfuiOcmI= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= @@ -2666,6 +2705,8 @@ github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= @@ -2716,6 +2757,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= +github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/microsoft/go-mssqldb v1.6.1-0.20240214161942-b65008136246 h1:KT4vTYcHqj5C5hMK5kSpyAk7MnFqfHVWLL4VqMq66S8= github.com/microsoft/go-mssqldb v1.6.1-0.20240214161942-b65008136246/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -2768,12 +2811,19 @@ github.com/mithrandie/go-text v1.6.0 h1:8gOXTMPbMY8DJbKMTv8kHhADcJlDWXqS/YQH4SyW github.com/mithrandie/go-text v1.6.0/go.mod h1:xCgj1xiNbI/d4xA9sLVvXkjh5B2tNx2ZT2/3rpmh8to= github.com/mithrandie/ternary v1.1.1 h1:k/joD6UGVYxHixYmSR8EGgDFNONBMqyD373xT4QRdC4= github.com/mithrandie/ternary v1.1.1/go.mod h1:0D9Ba3+09K2TdSZO7/bFCC0GjSXetCvYuYq0u8FY/1g= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/moby v23.0.4+incompatible h1:A/pe8vi9KIKhNbzR0G3wW4ACKDsMgXILBveMqiJNa8M= -github.com/moby/moby v23.0.4+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -2788,6 +2838,7 @@ github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJ github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de h1:D5x39vF5KCwKQaw+OC9ZPiLVHXz3UFw2+psEX+gYcto= github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de/go.mod h1:kJun4WP5gFuHZgRjZUWWuH1DTxCtxbHDOIJsudS8jzY= @@ -2802,6 +2853,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= +github.com/natefinch/wrap v0.2.0 h1:IXzc/pw5KqxJv55gV0lSOcKHYuEZPGbQrOOXr/bamRk= +github.com/natefinch/wrap v0.2.0/go.mod h1:6gMHlAl12DwYEfKP3TkuykYUfLSEAvHw67itm4/KAS8= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= github.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q= @@ -2828,6 +2881,8 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= @@ -2881,9 +2936,15 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.3-0.20220512140940-7b36cea86235 h1:DxS3bbeUSCpMQr3mTez5PIDrS+yBeBsoDsftOhqB1Fg= -github.com/opencontainers/image-spec v1.0.3-0.20220512140940-7b36cea86235/go.mod h1:K/JAU0m27RFhDRX4PcFdIKntROP6y5Ed6O91aZYDQfs= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/openfga/api/proto v0.0.0-20240529184453-5b0b4941f3e0 h1:tSJl/atdqsDACjRQQPCbXe2GfEvcOkhdrUNmDToAjTA= +github.com/openfga/api/proto v0.0.0-20240529184453-5b0b4941f3e0/go.mod h1:XnvYrdU//9i70Aou6n4H5DJ0bdRPB3IlmE/Vx6qhnm8= +github.com/openfga/language/pkg/go v0.0.0-20240409225820-a53ea2892d6d h1:n44DfITs+CLCYJIgsryJkG2ElwOZJ3huekPZKydPi7U= +github.com/openfga/language/pkg/go v0.0.0-20240409225820-a53ea2892d6d/go.mod h1:wkI4GcY3yNNuFMU2ncHPWqBaF7XylQTkJYfBi2pIpK8= +github.com/openfga/openfga v1.5.4 h1:mVrp0uB9jNWX/5+OtZLM6YOx5Y9Y4r/D/O+LNBF/FGQ= +github.com/openfga/openfga v1.5.4/go.mod h1:+PoZg9BJeq+h3L0eR52tqNTwghSapCFtmaVKHsUK7QM= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing-contrib/go-stdlib v1.0.0 h1:TBS7YuVotp8myLon4Pv7BtCBzOTo1DeZCld0Z63mW2w= github.com/opentracing-contrib/go-stdlib v1.0.0/go.mod h1:qtI1ogk+2JhVPIXVc6q+NHziSmy2W5GbdQZFUHADCBU= @@ -2907,10 +2968,13 @@ github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144T github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= @@ -2945,7 +3009,11 @@ 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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/pressly/goose/v3 v3.20.0 h1:uPJdOxF/Ipj7ABVNOAMJXSxwFXZGwMGHNqjC8e61VA0= +github.com/pressly/goose/v3 v3.20.0/go.mod h1:BRfF2GcG4FTG12QfdBVy3q1yveaf4ckL9vWwEcIO3lA= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= @@ -3059,6 +3127,10 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.2+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.26 h1:F+GIVtGqCFxPxO46ujf8cEOP574MBoRm3gNbPXECbxs= @@ -3074,6 +3146,12 @@ github.com/segmentio/encoding v0.3.6 h1:E6lVLyDPseWEulBmCmAKPanDd3jiyGDo5gMcugCR github.com/segmentio/encoding v0.3.6/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec= +github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v1.7.1/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= @@ -3102,6 +3180,8 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= @@ -3110,10 +3190,13 @@ github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= @@ -3126,6 +3209,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/spyzhov/ajson v0.9.0 h1:tF46gJGOenYVj+k9K1U1XpCxVWhmiyY5PsVCAs1+OJ0= github.com/spyzhov/ajson v0.9.0/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= @@ -3161,10 +3246,22 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/substrait-io/substrait-go v0.4.2/go.mod h1:qhpnLmrcvAnlZsUyPXZRqldiHapPTXC3t7xFgDi3aQg= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= +github.com/testcontainers/testcontainers-go v0.30.0 h1:jmn/XS22q4YRrcMwWg0pAwlClzs/abopbsBzrepyc4E= +github.com/testcontainers/testcontainers-go v0.30.0/go.mod h1:K+kHNGiM5zjklKjgTtcrEetF3uhWbMUyqAQoyoh8Pf0= +github.com/testcontainers/testcontainers-go/modules/mysql v0.30.0 h1:wrePvxfU/2HFALKyBqpNs6VoPPvThzHy9aN+PCxse9g= +github.com/testcontainers/testcontainers-go/modules/mysql v0.30.0/go.mod h1:Srnlf7wwA7s6K4sKKhjAoBHJcKorRINR/i5dCA4ZyGk= +github.com/testcontainers/testcontainers-go/modules/postgres v0.30.0 h1:D3HFqpZS90iRGAN7M85DFiuhPfvYvFNnx8urQ6mPAvo= +github.com/testcontainers/testcontainers-go/modules/postgres v0.30.0/go.mod h1:e1sKxwUOkqzvaqdHl/oV9mUtFmkDPTfBGp0po2tnWQU= github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= @@ -3234,6 +3331,8 @@ github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= @@ -3366,11 +3465,14 @@ go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= @@ -3384,10 +3486,11 @@ go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= gocloud.dev v0.25.0 h1:Y7vDq8xj7SyM848KXf32Krda2e6jQ4CLh/mTeCSqXtk= gocloud.dev v0.25.0/go.mod h1:7HegHVCYZrMiU3IE1qtnzf/vRrDwLYnRNR3EhWX8x9Y= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -3795,6 +3898,7 @@ golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -3937,6 +4041,7 @@ golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/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= @@ -4656,8 +4761,9 @@ sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+s sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= xorm.io/builder v0.3.6 h1:ha28mQ2M+TFx96Hxo+iq6tQgnkC9IZkM6D8w9sKHHF8= xorm.io/builder v0.3.6/go.mod h1:LEFAPISnRzG+zxaxj2vPicRwz67BdhFreKg8yv8/TgU= diff --git a/go.work.sum b/go.work.sum index 111a6af928b..917fda449c8 100644 --- a/go.work.sum +++ b/go.work.sum @@ -423,6 +423,7 @@ github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/KimMachineGun/automemlimit v0.6.0 h1:p/BXkH+K40Hax+PuWWPQ478hPjsp9h1CPDhLlA3Z37E= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= +github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= @@ -642,7 +643,6 @@ github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5A github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= github.com/containerd/imgcrypt v1.1.3/go.mod h1:/TPA1GIDXMzbj01yd8pIbQiLdQxed5ue1wb8bP7PQu4= github.com/containerd/imgcrypt v1.1.4/go.mod h1:LorQnPtzL/T0IyCeftcsMEO7AqxUDbdO8j/tSUpgxvo= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= @@ -743,7 +743,6 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dhui/dktest v0.3.0 h1:kwX5a7EkLcjo7VpsPQSYJcKGbXBXdjI9FGjuUj1jn6I= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= -github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/djherbis/atime v1.1.0/go.mod h1:28OF6Y8s3NQWwacXc5eZTsEsiMzp7LF8MbXE+XJPdBE= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= @@ -827,8 +826,6 @@ github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTg github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -858,6 +855,8 @@ github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJ github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd h1:hSkbZ9XSyjyBirMeqSqUrK+9HboWrweVlzRNqoBi2d4= github.com/gobuffalo/depgen v0.1.0 h1:31atYa/UW9V5q8vMJ+W6wd64OaaTHUrCUXER358zLM4= github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU= @@ -916,6 +915,7 @@ github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2c github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4= +github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= @@ -965,7 +965,6 @@ github.com/hashicorp/go-hclog v0.16.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39 github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/mdns v1.0.4 h1:sY0CMhFmjIPDMlTB+HfymFHCaYLhgifZ0QhjaYKD/UQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= @@ -1001,14 +1000,11 @@ github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGU github.com/jackc/pgconn v1.11.0 h1:HiHArx4yFbwl91X3qqIHtUFoiIfLNJXCQRsnzkiwwaQ= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38= -github.com/jackc/pgx v3.2.0+incompatible h1:0Vihzu20St42/UDsvZGdNE6jak7oi/UOeMzwMPHkgFY= github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w= -github.com/jackc/puddle v1.2.1 h1:gI8os0wpRXFd4FiAY2dWiqRK037tjj3t7rKFeO4X5iw= github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1 h1:9Xm8CKtMZIXgcopfdWk/qZ1rt0HjMgfMR9nxxSeK6vk= github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1/go.mod h1:zuHl3Hh+e9P6gmBPvcqR1HjkaWHC/csgyskg6IaFKFo= github.com/jaegertracing/jaeger v1.41.0 h1:vVNky8dP46M2RjGaZ7qRENqylW+tBFay3h57N16Ip7M= @@ -1025,6 +1021,7 @@ github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52Cu github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jon-whit/go-grpc-prometheus v1.4.0/go.mod h1:iTPm+Iuhh3IIqR0iGZ91JJEg5ax6YQEe1I0f6vtBuao= github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jsternberg/zap-logfmt v1.2.0 h1:1v+PK4/B48cy8cfQbxL4FmmNZrjnIMr2BsnyEmXqv2o= github.com/jsternberg/zap-logfmt v1.2.0/go.mod h1:kz+1CUmCutPWABnNkOu9hOHKdT2q3TDYCcsFy9hpqb0= @@ -1085,8 +1082,6 @@ github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmt github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 h1:143Bb8f8DuGWck/xpNUOckBVYfFbBTnLevfRZ1aVVqo= github.com/lightstep/lightstep-tracer-go v0.18.1 h1:vi1F1IQ8N7hNWytK9DpJsUfQhGuNSc19z330K6vl4zk= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lyft/protoc-gen-star v0.6.1 h1:erE0rdztuaDq3bpGifD95wfoPrSZc95nGA6tbiNYh6M= github.com/lyft/protoc-gen-star/v2 v2.0.3 h1:/3+/2sWyXeMLzKd1bX+ixWKgEMsULrIivpDsuaF441o= github.com/lyft/protoc-gen-validate v0.0.13 h1:KNt/RhmQTOLr7Aj8PsJ7mTronaFyx80mRTT9qF261dA= @@ -1132,7 +1127,6 @@ github.com/mithrandie/readline-csvq v1.2.1 h1:4cfeYeVSrqKEWi/1t7CjyhFD2yS6fm+l+o github.com/mithrandie/readline-csvq v1.2.1/go.mod h1:ydD9Eyp3/wn8KPSNbKmMZe4RQQauCuxi26yEo4N40dk= github.com/mithrandie/readline-csvq v1.3.0 h1:VTJEOGouJ8j27jJCD4kBBbNTxM0OdBvE1aY1tMhlqE8= github.com/mithrandie/readline-csvq v1.3.0/go.mod h1:FKyYqDgf/G4SNov7SMFXRWO6LQLXIOeTog/NB97FZl0= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= @@ -1153,15 +1147,15 @@ github.com/nats-io/jwt/v2 v2.5.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+v github.com/nats-io/nats-server/v2 v2.5.0 h1:wsnVaaXH9VRSg+A2MVg5Q727/CqxnmPLGFQ3YZYKTQg= github.com/nats-io/nats.go v1.12.1 h1:+0ndxwUPz3CmQ2vjbXdkC1fo3FdiOQDim4gl3Mge8Qo= github.com/nats-io/nats.go v1.28.0/go.mod h1:XpbWUlOElGwTYbMR7imivs7jJj9GtK7ypv321Wp6pjc= +github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8= github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= github.com/nats-io/nkeys v0.4.4/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64= +github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM= github.com/oapi-codegen/testutil v1.0.0/go.mod h1:ttCaYbHvJtHuiyeBF0tPIX+4uhEPTeizXKx28okijLw= github.com/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk= -github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= -github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -1260,9 +1254,8 @@ github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs= +github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= @@ -1293,12 +1286,15 @@ github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4 h1:BN/Nyn2nWMo github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= +github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY= github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/sagikazarmark/crypt v0.6.0 h1:REOEXCs/NFY/1jOCEouMuT4zEniE5YoXbvpC5X/TLF8= +github.com/sagikazarmark/crypt v0.17.0/go.mod h1:SMtHTvdmsZMuY/bpZoqokSoChIrcJ/epOxZN58PbZDg= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4= @@ -1323,7 +1319,6 @@ github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNX github.com/shirou/gopsutil/v3 v3.23.2 h1:PAWSuiAszn7IhPMBtXsbSCafej7PqUOvY6YywlQUExU= github.com/shirou/gopsutil/v3 v3.23.2/go.mod h1:gv0aQw33GLo3pG8SiWKiQrbDzbRY1K80RyZJ7V4Th1M= github.com/shirou/gopsutil/v3 v3.23.8/go.mod h1:7hmCaBn+2ZwaZOr6jmPBZDfawwMGuo1id3C6aM8EDqQ= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.6 h1:Oe8TPH9wAbv++YPNDKJWUnI8Q4PPWCx3UbOfH+FxiMU= github.com/shoenig/test v1.7.1 h1:UJcjSAI3aUKx52kfcfhblgyhZceouhvvs3OYdWgn+PY= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= @@ -1367,6 +1362,7 @@ github.com/tdewolff/parse/v2 v2.6.8 h1:mhNZXYCx//xG7Yq2e/kVLNZw4YfYmeHbhx+Zc0OvF github.com/tdewolff/parse/v2 v2.6.8/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM= github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= @@ -1376,10 +1372,8 @@ github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6 github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8= @@ -1432,9 +1426,12 @@ github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6 github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= 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/xhit/go-str2duration v1.2.0 h1:BcV5u025cITWxEQKGWr1URRzrcXtu7uk8+luz3Yuhwc= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= @@ -1444,7 +1441,6 @@ github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= diff --git a/package.json b/package.json index b999b4f1c53..9f32bc39ac5 100644 --- a/package.json +++ b/package.json @@ -165,6 +165,7 @@ "css-minimizer-webpack-plugin": "6.0.0", "cypress": "13.10.0", "cypress-file-upload": "5.0.8", + "cypress-recurse": "^1.35.3", "esbuild": "0.20.2", "esbuild-loader": "4.1.0", "esbuild-plugin-browserslist": "^0.12.0", @@ -217,6 +218,7 @@ "rudder-sdk-js": "2.48.8", "sass": "1.77.4", "sass-loader": "14.2.1", + "smtp-tester": "^2.1.0", "style-loader": "4.0.0", "stylelint": "16.6.1", "stylelint-config-sass-guidelines": "11.1.0", diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index fb7399368ca..7f5da38828d 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -169,7 +169,6 @@ export interface FeatureToggles { kubernetesAggregator?: boolean; expressionParser?: boolean; groupByVariable?: boolean; - betterPageScrolling?: boolean; authAPIAccessTokenAuth?: boolean; scopeFilters?: boolean; ssoSettingsSAML?: boolean; @@ -194,4 +193,7 @@ export interface FeatureToggles { azureMonitorPrometheusExemplars?: boolean; pinNavItems?: boolean; authZGRPCServer?: boolean; + openSearchBackendFlowEnabled?: boolean; + ssoSettingsLDAP?: boolean; + databaseReadReplica?: boolean; } diff --git a/packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.ts b/packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.ts index 801c1dcec0d..ff6bac57621 100644 --- a/packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.ts +++ b/packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.ts @@ -41,12 +41,14 @@ export function usePluginLinks(options: GetPluginExtensionsOptions): { export function usePluginComponents( options: GetPluginExtensionsOptions -): { components: Array>; isLoading: boolean } { +): { components: Array>; isLoading: boolean } { const { extensions, isLoading } = usePluginExtensions(options); return useMemo( () => ({ - components: extensions.filter(isPluginExtensionComponent) as Array>, + components: extensions + .filter(isPluginExtensionComponent) + .map(({ component }) => component as React.ComponentType), isLoading, }), [extensions, isLoading] diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts index 2c0f8acfc56..7172ae63740 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts @@ -231,9 +231,9 @@ class DataSourceWithBackend< if (request.dashboardUID) { headers[PluginRequestHeaders.DashboardUID] = request.dashboardUID; - } - if (request.panelId) { - headers[PluginRequestHeaders.PanelID] = `${request.panelId}`; + if (request.panelId) { + headers[PluginRequestHeaders.PanelID] = `${request.panelId}`; + } } if (request.panelPluginId) { headers[PluginRequestHeaders.PanelPluginId] = `${request.panelPluginId}`; diff --git a/packages/grafana-ui/src/components/Select/getSelectStyles.ts b/packages/grafana-ui/src/components/Select/getSelectStyles.ts index 0d89a3fa519..a75b79417a3 100644 --- a/packages/grafana-ui/src/components/Select/getSelectStyles.ts +++ b/packages/grafana-ui/src/components/Select/getSelectStyles.ts @@ -17,6 +17,7 @@ export const getSelectStyles = stylesFactory((theme: GrafanaTheme2) => { option: css({ label: 'grafana-select-option', padding: '8px', + position: 'relative', display: 'flex', alignItems: 'center', flexDirection: 'row', @@ -64,6 +65,17 @@ export const getSelectStyles = stylesFactory((theme: GrafanaTheme2) => { }), optionSelected: css({ background: theme.colors.action.selected, + '&::before': { + backgroundImage: theme.colors.gradients.brandVertical, + borderRadius: theme.shape.radius.default, + content: '" "', + display: 'block', + height: '100%', + position: 'absolute', + transform: 'translateX(-50%)', + width: theme.spacing(0.5), + left: 0, + }, }), optionDisabled: css({ label: 'grafana-select-option-disabled', diff --git a/packages/grafana-ui/src/components/Table/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/DefaultCell.tsx index 466e708de73..0a910f07eda 100644 --- a/packages/grafana-ui/src/components/Table/DefaultCell.tsx +++ b/packages/grafana-ui/src/components/Table/DefaultCell.tsx @@ -15,7 +15,7 @@ import { TableCellProps, CustomCellRendererProps, TableCellOptions } from './typ import { getCellColors, getCellOptions } from './utils'; export const DefaultCell = (props: TableCellProps) => { - const { field, cell, tableStyles, row, cellProps, frame, rowStyled, textWrapped, height } = props; + const { field, cell, tableStyles, row, cellProps, frame, rowStyled, rowExpanded, textWrapped, height } = props; const inspectEnabled = Boolean(field.config.custom?.inspect); const displayValue = field.display!(cell.value); @@ -60,7 +60,8 @@ export const DefaultCell = (props: TableCellProps) => { isStringValue, textShouldWrap, textWrapped, - rowStyled + rowStyled, + rowExpanded ); if (isStringValue) { @@ -122,7 +123,8 @@ function getCellStyle( isStringValue = false, shouldWrapText = false, textWrapped = false, - rowStyled = false + rowStyled = false, + rowExpanded = false ) { // Setup color variables let textColor: string | undefined = undefined; @@ -145,7 +147,8 @@ function getCellStyle( isStringValue, shouldWrapText, textWrapped, - rowStyled + rowStyled, + rowExpanded ); } diff --git a/packages/grafana-ui/src/components/Table/RowsList.tsx b/packages/grafana-ui/src/components/Table/RowsList.tsx index b994f18b541..aa061dd1bc1 100644 --- a/packages/grafana-ui/src/components/Table/RowsList.tsx +++ b/packages/grafana-ui/src/components/Table/RowsList.tsx @@ -267,6 +267,7 @@ export const RowsList = (props: RowsListProps) => { prepareRow(row); const expandedRowStyle = tableState.expanded[row.id] ? css({ '&:hover': { background: 'inherit' } }) : {}; + const rowExpanded = nestedDataField && tableState.expanded[row.id]; if (rowHighlightIndex !== undefined && row.index === rowHighlightIndex) { style = { ...style, backgroundColor: theme.components.table.rowHoverBackground }; @@ -304,7 +305,7 @@ export const RowsList = (props: RowsListProps) => { onMouseLeave={onRowLeave} > {/*add the nested data to the DOM first to prevent a 1px border CSS issue on the last cell of the row*/} - {nestedDataField && tableState.expanded[row.id] && ( + {rowExpanded && ( { timeRange={timeRange} frame={data} rowStyled={rowBg !== undefined} + rowExpanded={rowExpanded} textWrapped={textWrapField !== undefined} height={Number(style.height)} /> diff --git a/packages/grafana-ui/src/components/Table/TableCell.tsx b/packages/grafana-ui/src/components/Table/TableCell.tsx index 00986044a02..4c4b4a0e501 100644 --- a/packages/grafana-ui/src/components/Table/TableCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableCell.tsx @@ -16,6 +16,7 @@ export interface Props { userProps?: object; frame: DataFrame; rowStyled?: boolean; + rowExpanded?: boolean; textWrapped?: boolean; height?: number; } @@ -28,6 +29,7 @@ export const TableCell = ({ userProps, frame, rowStyled, + rowExpanded, textWrapped, height, }: Props) => { @@ -57,6 +59,7 @@ export const TableCell = ({ userProps, frame, rowStyled, + rowExpanded, textWrapped, height, })} diff --git a/packages/grafana-ui/src/components/Table/styles.ts b/packages/grafana-ui/src/components/Table/styles.ts index 60b673998f0..2a68ca23f87 100644 --- a/packages/grafana-ui/src/components/Table/styles.ts +++ b/packages/grafana-ui/src/components/Table/styles.ts @@ -19,14 +19,15 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell asCellText?: boolean, textShouldWrap?: boolean, textWrapped?: boolean, - rowStyled?: boolean + rowStyled?: boolean, + rowExpanded?: boolean ) => { return css({ label: overflowOnHover ? 'cellContainerOverflow' : 'cellContainerNoOverflow', padding: `${cellPadding}px`, width: '100%', // Cell height need to account for row border - height: `${rowHeight - 1}px`, + height: rowExpanded ? 'auto !important' : `${rowHeight - 1}px`, wordBreak: textWrapped ? 'break-all' : 'inherit', display: 'flex', diff --git a/pkg/api/api.go b/pkg/api/api.go index 76ff1a24c70..c6c1ee022cd 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -165,6 +165,10 @@ func (hs *HTTPServer) registerRoutes() { ) } + if hs.Features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) { + r.Get("/dashboard/recently-deleted", reqSignedIn, hs.Index) + } + r.Get("/explore", authorize(ac.EvalPermission(ac.ActionDatasourcesExplore)), hs.Index) r.Get("/playlists/", reqSignedIn, hs.Index) diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index 5b9f923e651..332f06abf0a 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -36,4 +36,5 @@ require ( k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 522786f0c93..433d2916653 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -33,4 +33,4 @@ k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7F k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 557d8653337..29ef6ee029a 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -22,7 +22,7 @@ require ( require ( github.com/BurntSushi/toml v1.3.2 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect - github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -53,7 +53,7 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.2 // indirect - github.com/google/cel-go v0.17.7 // indirect + github.com/google/cel-go v0.20.1 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -128,7 +128,7 @@ require ( go.opentelemetry.io/otel/sdk v1.26.0 // indirect go.opentelemetry.io/proto/otlp v1.2.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.26.0 // indirect + go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.24.0 // indirect golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect golang.org/x/net v0.26.0 // indirect @@ -154,5 +154,5 @@ require ( sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 2f96fd2f2c0..050f5aeac37 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -4,7 +4,7 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8 github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= -github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= @@ -95,8 +95,8 @@ github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaW github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 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/cel-go v0.17.7 h1:6ebJFzu1xO2n7TLtN+UBqShGBhlD85bhvglh5DpcfqQ= -github.com/google/cel-go v0.17.7/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= +github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= +github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= @@ -321,8 +321,7 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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= @@ -452,5 +451,4 @@ sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMm sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= diff --git a/pkg/build/cmd/grafanacom.go b/pkg/build/cmd/grafanacom.go index 9d44b9298c9..0627effcefa 100644 --- a/pkg/build/cmd/grafanacom.go +++ b/pkg/build/cmd/grafanacom.go @@ -156,8 +156,8 @@ func publishPackages(cfg packaging.PublishConfig) error { pth = path.Join(pth, product) baseArchiveURL := fmt.Sprintf("https://dl.grafana.com/%s", pth) - var builds []buildRepr - for _, ba := range packaging.ArtifactConfigs { + builds := make([]buildRepr, len(packaging.ArtifactConfigs)) + for i, ba := range packaging.ArtifactConfigs { u := ba.GetURL(baseArchiveURL, cfg) sha256, err := getSHA256(u) @@ -165,12 +165,12 @@ func publishPackages(cfg packaging.PublishConfig) error { return err } - builds = append(builds, buildRepr{ + builds[i] = buildRepr{ OS: ba.Os, URL: u, SHA256: string(sha256), Arch: ba.Arch, - }) + } } r := releaseRepr{ diff --git a/pkg/build/go.mod b/pkg/build/go.mod index 6b964b2c08c..91c7c5558c2 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -23,7 +23,7 @@ require ( github.com/Masterminds/semver/v3 v3.2.0 // @grafana/grafana-release-guild github.com/aws/aws-sdk-go v1.51.31 // @grafana/aws-datasources github.com/blang/semver/v4 v4.0.0 // @grafana/grafana-release-guild - github.com/docker/docker v26.0.1+incompatible // @grafana/grafana-release-guild + github.com/docker/docker v26.0.2+incompatible // @grafana/grafana-release-guild github.com/drone/drone-cli v1.6.1 // @grafana/grafana-release-guild github.com/gogo/protobuf v1.3.2 // indirect; @grafana/alerting-backend github.com/golang/protobuf v1.5.4 // indirect; @grafana/grafana-backend-group @@ -66,7 +66,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/docker/distribution v2.8.2+incompatible // indirect - github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/drone-runners/drone-runner-docker v1.8.2 // indirect github.com/drone/drone-go v1.7.1 // indirect @@ -81,7 +81,7 @@ require ( github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.3-0.20220512140940-7b36cea86235 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -98,8 +98,9 @@ require ( ) require ( + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/kr/pretty v0.3.1 // indirect - github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect + github.com/moby/term v0.5.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gotest.tools/v3 v3.5.1 // indirect diff --git a/pkg/build/go.sum b/pkg/build/go.sum index f7aea1be275..f44c496bd24 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -14,8 +14,7 @@ cloud.google.com/go/storage v1.38.0/go.mod h1:tlUADB0mAb9BgYls9lq+8MGkfzOXuLrnHX github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d/go.mod h1:3cARGAK9CfW3HoxCy1a0G4TKrdiKke8ftOMEOHyySYs= github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e/go.mod h1:Xa6lInWHNQnuWoF0YPSsx+INFA9qk7/7pTjwb3PInkY= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= @@ -53,8 +52,7 @@ github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4Kfc github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= @@ -154,8 +152,7 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/moby/moby v23.0.4+incompatible h1:A/pe8vi9KIKhNbzR0G3wW4ACKDsMgXILBveMqiJNa8M= github.com/moby/moby v23.0.4+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= -github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= -github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4/go.mod h1:cojhOHk1gbMeklOyDP2oKKLftefXoJreOQGOrXk+Z38= @@ -163,8 +160,7 @@ github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.3-0.20220512140940-7b36cea86235 h1:DxS3bbeUSCpMQr3mTez5PIDrS+yBeBsoDsftOhqB1Fg= -github.com/opencontainers/image-spec v1.0.3-0.20220512140940-7b36cea86235/go.mod h1:K/JAU0m27RFhDRX4PcFdIKntROP6y5Ed6O91aZYDQfs= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -176,7 +172,6 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -187,7 +182,6 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -198,9 +192,6 @@ github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM= github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0= github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -332,7 +323,6 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/pkg/expr/mathexp/exp.go b/pkg/expr/mathexp/exp.go index 9908cdc2cbc..055caffa27c 100644 --- a/pkg/expr/mathexp/exp.go +++ b/pkg/expr/mathexp/exp.go @@ -558,8 +558,9 @@ func (e *State) biSeriesSeries(labels data.Labels, op string, aSeries, bSeries S func (e *State) walkFunc(node *parse.FuncNode) (Results, error) { var res Results var err error - var in []reflect.Value - for _, a := range node.Args { + + in := make([]reflect.Value, len(node.Args)) + for i, a := range node.Args { var v any switch t := a.(type) { case *parse.StringNode: @@ -580,7 +581,8 @@ func (e *State) walkFunc(node *parse.FuncNode) (Results, error) { if err != nil { return res, err } - in = append(in, reflect.ValueOf(v)) + + in[i] = reflect.ValueOf(v) } f := reflect.ValueOf(node.F.F) diff --git a/pkg/infra/db/db.go b/pkg/infra/db/db.go index 574354fa605..48581f7d65d 100644 --- a/pkg/infra/db/db.go +++ b/pkg/infra/db/db.go @@ -62,6 +62,15 @@ func InitTestDB(t sqlutil.ITestDB, opts ...InitTestDBOpt) *sqlstore.SQLStore { return db } +func InitTestReplDBWithCfg(t sqlutil.ITestDB, opts ...InitTestDBOpt) (*sqlstore.ReplStore, *setting.Cfg) { + return sqlstore.InitTestReplDB(t, opts...) +} + +func InitTestReplDB(t sqlutil.ITestDB, opts ...InitTestDBOpt) *sqlstore.ReplStore { + db, _ := InitTestReplDBWithCfg(t, opts...) + return db +} + func InitTestDBWithCfg(t sqlutil.ITestDB, opts ...InitTestDBOpt) (*sqlstore.SQLStore, *setting.Cfg) { return sqlstore.InitTestDB(t, opts...) } diff --git a/pkg/infra/db/dbrepl.go b/pkg/infra/db/dbrepl.go new file mode 100644 index 00000000000..4b3bac0bb1d --- /dev/null +++ b/pkg/infra/db/dbrepl.go @@ -0,0 +1,9 @@ +package db + +import "github.com/grafana/grafana/pkg/services/sqlstore" + +type ReplDB interface { + // DB is the primary database connection. + DB() *sqlstore.SQLStore + ReadReplica() *sqlstore.SQLStore +} diff --git a/pkg/infra/log/log.go b/pkg/infra/log/log.go index f12009cfe3d..03623e5ee55 100644 --- a/pkg/infra/log/log.go +++ b/pkg/infra/log/log.go @@ -444,7 +444,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { defaultLevelName, _ := getLogLevelFromConfig("log", "info", cfg) defaultFilters := getFilters(util.SplitString(cfg.Section("log").Key("filters").String())) - var configLoggers []logWithFilters + configLoggers := make([]logWithFilters, 0, len(modes)) for _, mode := range modes { mode = strings.TrimSpace(mode) sec, err := cfg.GetSection("log." + mode) @@ -505,6 +505,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { handler.filters = modeFilters handler.maxLevel = leveloption + configLoggers = append(configLoggers, handler) } if len(configLoggers) > 0 { diff --git a/pkg/infra/usagestats/statscollector/concurrent_users_test.go b/pkg/infra/usagestats/statscollector/concurrent_users_test.go index decb1642657..919a74fa22e 100644 --- a/pkg/infra/usagestats/statscollector/concurrent_users_test.go +++ b/pkg/infra/usagestats/statscollector/concurrent_users_test.go @@ -24,7 +24,7 @@ func TestMain(m *testing.M) { } func TestConcurrentUsersMetrics(t *testing.T) { - sqlStore, cfg := db.InitTestDBWithCfg(t) + sqlStore, cfg := db.InitTestReplDBWithCfg(t) statsService := statsimpl.ProvideService(&setting.Cfg{}, sqlStore) s := createService(t, cfg, sqlStore, statsService) @@ -42,7 +42,7 @@ func TestConcurrentUsersMetrics(t *testing.T) { } func TestConcurrentUsersStats(t *testing.T) { - sqlStore, cfg := db.InitTestDBWithCfg(t) + sqlStore, cfg := db.InitTestReplDBWithCfg(t) statsService := statsimpl.ProvideService(&setting.Cfg{}, sqlStore) s := createService(t, cfg, sqlStore, statsService) diff --git a/pkg/login/social/socialimpl/service_test.go b/pkg/login/social/socialimpl/service_test.go index 6ac7811f3c8..1984704104f 100644 --- a/pkg/login/social/socialimpl/service_test.go +++ b/pkg/login/social/socialimpl/service_test.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/login/social/connectors" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/featuremgmt" - ldap "github.com/grafana/grafana/pkg/services/ldap/service" "github.com/grafana/grafana/pkg/services/licensing" secretsfake "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/services/ssosettings/ssosettingsimpl" @@ -82,7 +81,6 @@ func TestSocialService_ProvideService(t *testing.T) { nil, &setting.OSSImpl{Cfg: cfg}, &licensing.OSSLicensingService{}, - ldap.ProvideService(cfg), ) for _, tc := range testCases { @@ -195,7 +193,6 @@ func TestSocialService_ProvideService_GrafanaComGrafanaNet(t *testing.T) { nil, nil, &licensing.OSSLicensingService{}, - ldap.ProvideService(cfg), ) for _, tc := range testCases { diff --git a/pkg/modules/dependencies.go b/pkg/modules/dependencies.go index 01d54255440..f18b5f2c15f 100644 --- a/pkg/modules/dependencies.go +++ b/pkg/modules/dependencies.go @@ -7,12 +7,14 @@ const ( Core string = "core" GrafanaAPIServer string = "grafana-apiserver" StorageServer string = "storage-server" + ZanzanaServer string = "zanzana-server" InstrumentationServer string = "instrumentation-server" ) var dependencyMap = map[string][]string{ GrafanaAPIServer: {InstrumentationServer}, StorageServer: {InstrumentationServer}, + ZanzanaServer: {InstrumentationServer}, Core: {}, All: {Core}, } diff --git a/pkg/modules/modules.go b/pkg/modules/modules.go index c36cc395f78..8e1a7e995f7 100644 --- a/pkg/modules/modules.go +++ b/pkg/modules/modules.go @@ -71,7 +71,7 @@ func (m *service) Run(ctx context.Context) error { return nil } - var svcs []services.Service + svcs := make([]services.Service, 0, len(m.serviceMap)) for _, s := range m.serviceMap { svcs = append(svcs, s) } diff --git a/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go index d66fcf2b285..f45ae982d09 100644 --- a/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go +++ b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go @@ -93,6 +93,8 @@ func TestDefaultStaticDetectorsInspector(t *testing.T) { plugin *plugins.Plugin exp bool } + + //nolint:prealloc // just a test, and it'd require too much refactoring to preallocate var tcs []tc // Angular imports diff --git a/pkg/plugins/manager/signature/manifest_test.go b/pkg/plugins/manager/signature/manifest_test.go index 72425785889..1414f4d61b2 100644 --- a/pkg/plugins/manager/signature/manifest_test.go +++ b/pkg/plugins/manager/signature/manifest_test.go @@ -381,11 +381,13 @@ func TestFSPathSeparatorFiles(t *testing.T) { } func fileList(manifest *PluginManifest) []string { - var keys []string + keys := make([]string, 0, len(manifest.Files)) for k := range manifest.Files { keys = append(keys, k) } + sort.Strings(keys) + return keys } diff --git a/pkg/plugins/manager/sources/source_local_disk.go b/pkg/plugins/manager/sources/source_local_disk.go index 5f6ab1c3dd1..4fddc464fea 100644 --- a/pkg/plugins/manager/sources/source_local_disk.go +++ b/pkg/plugins/manager/sources/source_local_disk.go @@ -62,9 +62,10 @@ func DirAsLocalSources(pluginsPath string, class plugins.Class) ([]*LocalSource, } slices.Sort(pluginDirs) - var sources []*LocalSource - for _, dir := range pluginDirs { - sources = append(sources, NewLocalSource(class, []string{dir})) + sources := make([]*LocalSource, len(pluginDirs)) + for i, dir := range pluginDirs { + sources[i] = NewLocalSource(class, []string{dir}) } + return sources, nil } diff --git a/pkg/plugins/manager/sources/source_local_disk_test.go b/pkg/plugins/manager/sources/source_local_disk_test.go index e380b5d5388..0a903c4c4cc 100644 --- a/pkg/plugins/manager/sources/source_local_disk_test.go +++ b/pkg/plugins/manager/sources/source_local_disk_test.go @@ -46,7 +46,7 @@ func TestDirAsLocalSources(t *testing.T) { { name: "Directory with no subdirectories", pluginsPath: filepath.Join(testdataDir, "pluginRootWithDist", "datasource"), - expected: nil, + expected: []*LocalSource{}, }, { name: "Directory with a symlink to a directory", diff --git a/pkg/plugins/manager/sources/sources.go b/pkg/plugins/manager/sources/sources.go index a3bfe1d4112..353f6fe3046 100644 --- a/pkg/plugins/manager/sources/sources.go +++ b/pkg/plugins/manager/sources/sources.go @@ -38,22 +38,25 @@ func (s *Service) externalPluginSources() []plugins.PluginSource { return []plugins.PluginSource{} } - var srcs []plugins.PluginSource - for _, src := range localSrcs { - srcs = append(srcs, src) + srcs := make([]plugins.PluginSource, len(localSrcs)) + for i, src := range localSrcs { + srcs[i] = src } + return srcs } func (s *Service) pluginSettingSources() []plugins.PluginSource { - var sources []plugins.PluginSource + sources := make([]plugins.PluginSource, 0, len(s.cfg.PluginSettings)) for _, ps := range s.cfg.PluginSettings { path, exists := ps["path"] if !exists || path == "" { continue } + sources = append(sources, NewLocalSource(plugins.ClassExternal, []string{path})) } + return sources } diff --git a/pkg/plugins/repo/service_test.go b/pkg/plugins/repo/service_test.go index c2c0833e7ea..ba050893420 100644 --- a/pkg/plugins/repo/service_test.go +++ b/pkg/plugins/repo/service_test.go @@ -197,9 +197,8 @@ type versionArg struct { } func createPluginVersions(versions ...versionArg) []Version { - var vs []Version - - for _, version := range versions { + vs := make([]Version, len(versions)) + for i, version := range versions { ver := Version{ Version: version.version, } @@ -211,7 +210,8 @@ func createPluginVersions(versions ...versionArg) []Version { } } } - vs = append(vs, ver) + + vs[i] = ver } return vs diff --git a/pkg/server/module_server.go b/pkg/server/module_server.go index afafe5803f5..71ee65f3367 100644 --- a/pkg/server/module_server.go +++ b/pkg/server/module_server.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/modules" + "github.com/grafana/grafana/pkg/services/authz" "github.com/grafana/grafana/pkg/services/featuremgmt" storageServer "github.com/grafana/grafana/pkg/services/store/entity/server" "github.com/grafana/grafana/pkg/setting" @@ -133,6 +134,10 @@ func (s *ModuleServer) Run() error { return storageServer.ProvideService(s.cfg, s.features, s.log) }) + m.RegisterModule(modules.ZanzanaServer, func() (services.Service, error) { + return authz.ProvideZanzanaService(s.cfg, s.features) + }) + m.RegisterModule(modules.All, nil) return m.Run(s.context) diff --git a/pkg/server/test_env.go b/pkg/server/test_env.go index 7e4c39fa8c4..a2098c0a8b6 100644 --- a/pkg/server/test_env.go +++ b/pkg/server/test_env.go @@ -15,6 +15,7 @@ import ( func ProvideTestEnv( server *Server, db db.DB, + repldb db.ReplDB, cfg *setting.Cfg, ns *notifications.NotificationServiceMock, grpcServer grpcserver.Provider, @@ -26,6 +27,7 @@ func ProvideTestEnv( return &TestEnv{ Server: server, SQLStore: db, + ReadReplStore: repldb, Cfg: cfg, NotificationService: ns, GRPCServer: grpcServer, @@ -39,6 +41,7 @@ func ProvideTestEnv( type TestEnv struct { Server *Server SQLStore db.DB + ReadReplStore db.ReplDB Cfg *setting.Cfg NotificationService *notifications.NotificationServiceMock GRPCServer grpcserver.Provider diff --git a/pkg/server/wire.go b/pkg/server/wire.go index ccf410105ab..a8bf87483eb 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -390,6 +390,7 @@ var wireSet = wire.NewSet( wireBasicSet, metrics.WireSet, sqlstore.ProvideService, + sqlstore.ProvideServiceWithReadReplica, ngmetrics.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), @@ -405,6 +406,7 @@ var wireCLISet = wire.NewSet( wireBasicSet, metrics.WireSet, sqlstore.ProvideService, + sqlstore.ProvideServiceWithReadReplica, ngmetrics.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), @@ -420,12 +422,14 @@ var wireTestSet = wire.NewSet( ProvideTestEnv, metrics.WireSetForTest, sqlstore.ProvideServiceForTests, + sqlstore.ProvideServiceWithReadReplicaForTests, ngmetrics.ProvideServiceForTest, notifications.MockNotificationService, wire.Bind(new(notifications.Service), new(*notifications.NotificationServiceMock)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationServiceMock)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationServiceMock)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), + wire.Bind(new(db.ReplDB), new(*sqlstore.ReplStore)), prefimpl.ProvideService, oauthtoken.ProvideService, oauthtokentest.ProvideService, @@ -439,7 +443,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser func InitializeForTest(t sqlutil.ITestDB, cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*TestEnv, error) { wire.Build(wireExtsTestSet) - return &TestEnv{Server: &Server{}, SQLStore: &sqlstore.SQLStore{}, Cfg: &setting.Cfg{}}, nil + return &TestEnv{Server: &Server{}, SQLStore: &sqlstore.SQLStore{}, ReadReplStore: &sqlstore.ReplStore{}, Cfg: &setting.Cfg{}}, nil } func InitializeForCLI(cfg *setting.Cfg) (Runner, error) { diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index a9d9325a41c..02361171c82 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -326,7 +326,7 @@ func (e DatasourcePermissionsService) SetBuiltInRolePermission(ctx context.Conte // if an OSS/unlicensed instance is upgraded to Enterprise/licensed. // https://github.com/grafana/identity-access-team/issues/672 func (e DatasourcePermissionsService) SetPermissions(ctx context.Context, orgID int64, resourceID string, commands ...accesscontrol.SetResourcePermissionCommand) ([]accesscontrol.ResourcePermission, error) { - var dbCommands []resourcepermissions.SetResourcePermissionsCommand + dbCommands := make([]resourcepermissions.SetResourcePermissionsCommand, 0, len(commands)) for _, cmd := range commands { // Only set query permissions for built-in roles; do not set permissions for data sources with * as UID, as this would grant wildcard permissions if cmd.Permission != "Query" || cmd.BuiltinRole == "" || resourceID == "*" { diff --git a/pkg/services/annotations/annotationsimpl/annotations.go b/pkg/services/annotations/annotationsimpl/annotations.go index 3cf88288d27..b6a9e622899 100644 --- a/pkg/services/annotations/annotationsimpl/annotations.go +++ b/pkg/services/annotations/annotationsimpl/annotations.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/tag" @@ -27,6 +28,7 @@ func ProvideService( cfg *setting.Cfg, features featuremgmt.FeatureToggles, tagService tag.Service, + tracer tracing.Tracer, ) *RepositoryImpl { l := log.New("annotations") l.Debug("Initializing annotations service") @@ -35,7 +37,7 @@ func ProvideService( write := xormStore var read readStore - historianStore := loki.NewLokiHistorianStore(cfg.UnifiedAlerting.StateHistory, features, db, log.New("annotations.loki")) + historianStore := loki.NewLokiHistorianStore(cfg.UnifiedAlerting.StateHistory, features, db, log.New("annotations.loki"), tracer) if historianStore != nil { l.Debug("Using composite read store") read = NewCompositeStore(log.New("annotations.composite"), xormStore, historianStore) diff --git a/pkg/services/annotations/annotationsimpl/annotations_test.go b/pkg/services/annotations/annotationsimpl/annotations_test.go index c296ac4ab30..5cbc5ccb30a 100644 --- a/pkg/services/annotations/annotationsimpl/annotations_test.go +++ b/pkg/services/annotations/annotationsimpl/annotations_test.go @@ -48,7 +48,7 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) { features := featuremgmt.WithFeatures() tagService := tagimpl.ProvideService(sql) - repo := ProvideService(sql, cfg, features, tagService) + repo := ProvideService(sql, cfg, features, tagService, tracing.InitializeTracerForTest()) dashboard1 := testutil.CreateDashboard(t, sql, cfg, features, dashboards.SaveDashboardCommand{ UserID: 1, @@ -317,7 +317,7 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) { cfg := setting.NewCfg() cfg.AnnotationMaximumTagsLength = 60 - repo := ProvideService(sql, cfg, tc.features, tagimpl.ProvideService(sql)) + repo := ProvideService(sql, cfg, tc.features, tagimpl.ProvideService(sql), tracing.InitializeTracerForTest()) usr.Permissions = map[int64]map[string][]string{1: tc.permissions} testutil.SetupRBACPermission(t, sql, role, usr) diff --git a/pkg/services/annotations/annotationsimpl/loki/historian_store.go b/pkg/services/annotations/annotationsimpl/loki/historian_store.go index 1fca8d4f176..b4e411b4a54 100644 --- a/pkg/services/annotations/annotationsimpl/loki/historian_store.go +++ b/pkg/services/annotations/annotationsimpl/loki/historian_store.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" ngmetrics "github.com/grafana/grafana/pkg/services/ngalert/metrics" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" @@ -53,7 +54,7 @@ type LokiHistorianStore struct { log log.Logger } -func NewLokiHistorianStore(cfg setting.UnifiedAlertingStateHistorySettings, ft featuremgmt.FeatureToggles, db db.DB, log log.Logger) *LokiHistorianStore { +func NewLokiHistorianStore(cfg setting.UnifiedAlertingStateHistorySettings, ft featuremgmt.FeatureToggles, db db.DB, log log.Logger, tracer tracing.Tracer) *LokiHistorianStore { if !useStore(cfg, ft) { return nil } @@ -64,7 +65,7 @@ func NewLokiHistorianStore(cfg setting.UnifiedAlertingStateHistorySettings, ft f } return &LokiHistorianStore{ - client: historian.NewLokiClient(lokiCfg, historian.NewRequester(), ngmetrics.NewHistorianMetrics(prometheus.DefaultRegisterer, subsystem), log), + client: historian.NewLokiClient(lokiCfg, historian.NewRequester(), ngmetrics.NewHistorianMetrics(prometheus.DefaultRegisterer, subsystem), log, tracer), db: db, log: log, } diff --git a/pkg/services/authz/README.md b/pkg/services/authz/README.md index aee78e4c399..c230ba1bb51 100644 --- a/pkg/services/authz/README.md +++ b/pkg/services/authz/README.md @@ -1,3 +1,51 @@ # Authorization This package contains the authorization server implementation. + +## Feature toggles + +The following feature toggles need to be activated: + +```ini +[feature_toggles] +authZGRPCServer = true +grpcServer = true +``` + +## Configuration + +To configure the authorization server and client, use the "authorization" section of the configuration ini file. + +The `remote_address` setting, specifies the address where the authorization server is located (ex: `server.example.org:10000`). + +The `mode` setting can be set to either `grpc` or `inproc`. When set to `grpc`, the client will connect to the specified address. When set to `inproc` the client will use inprocgrpc (relying on go channels) to wrap a local instantiation of the server. + +The `listen` setting determines whether the authorization server should listen for incoming requests. When set to `true`, the authorization service will be registered to the Grafana GRPC server. + +The default configuration does not register the authorization service on the Grafana GRPC server and binds the client to it `inproc`: + +```ini +[authorization] +remote_address = "" +listen = false +mode = "inproc" +``` + +### Example + +Here is an example to connect the authorization client to a remote grpc server. + +```ini +[authorization] +remote_address = "server.example.org:10000" +mode = "grpc" +``` + +Here is an example to register the authorization service on the Grafana GRPC server and connect the client to it through grpc + +```ini +[authorization] +remote_address = "localhost:10000" +listen = true +mode = "grpc" +``` diff --git a/pkg/services/authz/client.go b/pkg/services/authz/client.go index f85afbdb56b..be0328aa284 100644 --- a/pkg/services/authz/client.go +++ b/pkg/services/authz/client.go @@ -1,10 +1,21 @@ package authz import ( + "context" + + "github.com/fullstorydev/grpchan" + "github.com/fullstorydev/grpchan/inprocgrpc" + grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + authzv1 "github.com/grafana/authlib/authz/proto/v1" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/grpcserver" + grpcUtils "github.com/grafana/grafana/pkg/services/store/entity/grpc" "github.com/grafana/grafana/pkg/setting" ) @@ -13,8 +24,10 @@ type Client interface { } type LegacyClient struct { + clientV1 authzv1.AuthzServiceClient } +// ProvideAuthZClient provides an AuthZ client and creates the AuthZ service. func ProvideAuthZClient( cfg *setting.Cfg, features featuremgmt.FeatureToggles, acSvc accesscontrol.Service, grpcServer grpcserver.Provider, tracer tracing.Tracer, @@ -23,11 +36,90 @@ func ProvideAuthZClient( return nil, nil } - _, err := newLegacyServer(acSvc, features, grpcServer, tracer) + authCfg, err := ReadCfg(cfg) if err != nil { return nil, err } - // TODO differentiate run local from run remote grpc - return &LegacyClient{}, nil + var client *LegacyClient + + // Register the server + server, err := newLegacyServer(acSvc, features, grpcServer, tracer, authCfg) + if err != nil { + return nil, err + } + + switch authCfg.mode { + case ModeInProc: + client = newInProcLegacyClient(server) + case ModeGRPC: + client, err = newGrpcLegacyClient(authCfg.remoteAddress) + if err != nil { + return nil, err + } + } + + return client, err +} + +// ProvideStandaloneAuthZClient provides a standalone AuthZ client, without registering the AuthZ service. +// You need to provide a remote address in the configuration +func ProvideStandaloneAuthZClient( + cfg *setting.Cfg, features featuremgmt.FeatureToggles, tracer tracing.Tracer, +) (Client, error) { + if !features.IsEnabledGlobally(featuremgmt.FlagAuthZGRPCServer) { + return nil, nil + } + + authCfg, err := ReadCfg(cfg) + if err != nil { + return nil, err + } + + return newGrpcLegacyClient(authCfg.remoteAddress) +} + +func newInProcLegacyClient(server *legacyServer) *LegacyClient { + channel := &inprocgrpc.Channel{} + + // TODO (gamab): change this once it's clear how to authenticate the client + // Choices are: + // - noAuth given it's in proc and we don't need the user + // - access_token verif only as it's consistent with when it's remote (we check the service is allowed to call the authz service) + // - access_token and id_token ? the id_token being only necessary when the user is trying to access the service straight away + // auth := grpcUtils.ProvideAuthenticator(cfg) + noAuth := func(ctx context.Context) (context.Context, error) { + return ctx, nil + } + + channel.RegisterService( + grpchan.InterceptServer( + &authzv1.AuthzService_ServiceDesc, + grpcAuth.UnaryServerInterceptor(noAuth), + grpcAuth.StreamServerInterceptor(noAuth), + ), + server, + ) + + conn := grpchan.InterceptClientConn(channel, grpcUtils.UnaryClientInterceptor, grpcUtils.StreamClientInterceptor) + + client := authzv1.NewAuthzServiceClient(conn) + + return &LegacyClient{ + clientV1: client, + } +} + +func newGrpcLegacyClient(address string) (*LegacyClient, error) { + // Create a connection to the gRPC server + conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + + client := authzv1.NewAuthzServiceClient(conn) + + return &LegacyClient{ + clientV1: client, + }, nil } diff --git a/pkg/services/authz/config.go b/pkg/services/authz/config.go new file mode 100644 index 00000000000..bf6c1b2c19a --- /dev/null +++ b/pkg/services/authz/config.go @@ -0,0 +1,43 @@ +package authz + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/setting" +) + +type Mode string + +func (s Mode) IsValid() bool { + switch s { + case ModeGRPC, ModeInProc: + return true + } + return false +} + +const ( + ModeGRPC Mode = "grpc" + ModeInProc Mode = "inproc" +) + +type Cfg struct { + remoteAddress string + listen bool + mode Mode +} + +func ReadCfg(cfg *setting.Cfg) (*Cfg, error) { + section := cfg.SectionWithEnvOverrides("authorization") + + mode := Mode(section.Key("mode").MustString(string(ModeInProc))) + if !mode.IsValid() { + return nil, fmt.Errorf("authorization: invalid mode %q", mode) + } + + return &Cfg{ + remoteAddress: section.Key("remote_address").MustString(""), + listen: section.Key("listen").MustBool(false), + mode: mode, + }, nil +} diff --git a/pkg/services/authz/server.go b/pkg/services/authz/server.go index 71b0ceb0140..8403ef4500a 100644 --- a/pkg/services/authz/server.go +++ b/pkg/services/authz/server.go @@ -24,7 +24,7 @@ type legacyServer struct { func newLegacyServer( acSvc accesscontrol.Service, features featuremgmt.FeatureToggles, - grpcServer grpcserver.Provider, tracer tracing.Tracer, + grpcServer grpcserver.Provider, tracer tracing.Tracer, cfg *Cfg, ) (*legacyServer, error) { if !features.IsEnabledGlobally(featuremgmt.FlagAuthZGRPCServer) { return nil, nil @@ -36,7 +36,9 @@ func newLegacyServer( tracer: tracer, } - grpcServer.GetServer().RegisterService(&authzv1.AuthzService_ServiceDesc, s) + if cfg.listen { + grpcServer.GetServer().RegisterService(&authzv1.AuthzService_ServiceDesc, s) + } return s, nil } diff --git a/pkg/services/authz/wireset.go b/pkg/services/authz/wireset.go index f16235fa4b4..4a46f15ea3b 100644 --- a/pkg/services/authz/wireset.go +++ b/pkg/services/authz/wireset.go @@ -6,4 +6,5 @@ import ( var WireSet = wire.NewSet( ProvideAuthZClient, + ProvideZanzana, ) diff --git a/pkg/services/authz/zanzana.go b/pkg/services/authz/zanzana.go new file mode 100644 index 00000000000..61097b39ad1 --- /dev/null +++ b/pkg/services/authz/zanzana.go @@ -0,0 +1,127 @@ +package authz + +import ( + "context" + "errors" + "fmt" + + "github.com/fullstorydev/grpchan/inprocgrpc" + "github.com/grafana/dskit/services" + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/authz/zanzana" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/grpcserver" + "github.com/grafana/grafana/pkg/setting" +) + +type ZanzanaClient interface{} + +func ProvideZanzana(cfg *setting.Cfg) (ZanzanaClient, error) { + var client *zanzana.Client + + switch cfg.Zanzana.Mode { + case setting.ZanzanaModeClient: + conn, err := grpc.NewClient(cfg.Zanzana.Addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, fmt.Errorf("failed to create zanzana client to remote server: %w", err) + } + client = zanzana.NewClient(openfgav1.NewOpenFGAServiceClient(conn)) + case setting.ZanzanaModeEmbedded: + srv, err := zanzana.NewServer(zanzana.NewStore()) + if err != nil { + return nil, fmt.Errorf("failed to start zanzana: %w", err) + } + channel := &inprocgrpc.Channel{} + openfgav1.RegisterOpenFGAServiceServer(channel, srv) + client = zanzana.NewClient(openfgav1.NewOpenFGAServiceClient(channel)) + default: + return nil, fmt.Errorf("unsupported zanzana mode: %s", cfg.Zanzana.Mode) + } + + return client, nil +} + +type ZanzanaService interface { + services.NamedService +} + +var _ ZanzanaService = (*Zanzana)(nil) + +// ProvideZanzanaService is used to register zanzana as a module so we can run it seperatly from grafana. +func ProvideZanzanaService(cfg *setting.Cfg, features featuremgmt.FeatureToggles) (*Zanzana, error) { + s := &Zanzana{ + cfg: cfg, + features: features, + logger: log.New("zanzana"), + } + + s.BasicService = services.NewBasicService(s.start, s.running, s.stopping).WithName("zanzana") + + return s, nil +} + +type Zanzana struct { + *services.BasicService + + cfg *setting.Cfg + + logger log.Logger + handle grpcserver.Provider + features featuremgmt.FeatureToggles +} + +func (z *Zanzana) start(ctx context.Context) error { + srv, err := zanzana.NewServer(zanzana.NewStore()) + if err != nil { + return fmt.Errorf("failed to start zanzana: %w", err) + } + + tracingCfg, err := tracing.ProvideTracingConfig(z.cfg) + if err != nil { + return err + } + tracingCfg.ServiceName = "zanzana" + + tracer, err := tracing.ProvideService(tracingCfg) + if err != nil { + return err + } + + // FIXME(kalleep): For now we use noopAuthenticator but we should create an authenticator that can be shared + // between different services. + z.handle, err = grpcserver.ProvideService(z.cfg, z.features, noopAuthenticator{}, tracer, prometheus.DefaultRegisterer) + if err != nil { + return fmt.Errorf("failed to create zanzana grpc server: %w", err) + } + + openfgav1.RegisterOpenFGAServiceServer(z.handle.GetServer(), srv) + if _, err := grpcserver.ProvideReflectionService(z.cfg, z.handle); err != nil { + return fmt.Errorf("failed to register reflection for zanzana: %w", err) + } + + return nil +} + +func (z *Zanzana) running(ctx context.Context) error { + // Run is blocking so we can just run it here + return z.handle.Run(ctx) +} + +func (z *Zanzana) stopping(err error) error { + if err != nil && !errors.Is(err, context.Canceled) { + z.logger.Error("Stopping zanzana due to unexpected error", "err", err) + } + return nil +} + +type noopAuthenticator struct{} + +func (n noopAuthenticator) Authenticate(ctx context.Context) (context.Context, error) { + return ctx, nil +} diff --git a/pkg/services/authz/zanzana/client.go b/pkg/services/authz/zanzana/client.go new file mode 100644 index 00000000000..4d3509a4257 --- /dev/null +++ b/pkg/services/authz/zanzana/client.go @@ -0,0 +1,14 @@ +package zanzana + +import ( + openfgav1 "github.com/openfga/api/proto/openfga/v1" +) + +// FIXME(kalleep): Build out our wrapper client for openFGA +type Client struct { + c openfgav1.OpenFGAServiceClient +} + +func NewClient(c openfgav1.OpenFGAServiceClient) *Client { + return &Client{c} +} diff --git a/pkg/services/authz/zanzana/server.go b/pkg/services/authz/zanzana/server.go new file mode 100644 index 00000000000..e00b2a22eb5 --- /dev/null +++ b/pkg/services/authz/zanzana/server.go @@ -0,0 +1,26 @@ +package zanzana + +import ( + "github.com/openfga/openfga/pkg/logger" + "github.com/openfga/openfga/pkg/server" + "github.com/openfga/openfga/pkg/storage" +) + +func NewServer(store storage.OpenFGADatastore) (*server.Server, error) { + // FIXME(kalleep): add support for more options, configure logging, tracing etc + opts := []server.OpenFGAServiceV1Option{ + server.WithDatastore(store), + // FIXME(kalleep): Write and log adapter for open fga logging interface + server.WithLogger(logger.NewNoopLogger()), + } + + // FIXME(kalleep): Interceptors + // We probably need to at least need to add store id interceptor also + // would be nice to inject our own requestid? + srv, err := server.NewServerWithOpts(opts...) + if err != nil { + return nil, err + } + + return srv, nil +} diff --git a/pkg/services/authz/zanzana/store.go b/pkg/services/authz/zanzana/store.go new file mode 100644 index 00000000000..d946954763a --- /dev/null +++ b/pkg/services/authz/zanzana/store.go @@ -0,0 +1,13 @@ +package zanzana + +import ( + "github.com/openfga/openfga/pkg/storage" + "github.com/openfga/openfga/pkg/storage/memory" +) + +// FIXME(kalleep): Add support for postgres, mysql and sqlite data stores. +// Postgres and mysql is already implmented by openFGA so we just need to hook up migartions for them. +// There is no support for sqlite atm but we are working on adding it: https://github.com/openfga/openfga/pull/1615 +func NewStore() storage.OpenFGADatastore { + return memory.New() +} diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go index de5982af442..e29844b5d7a 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go @@ -420,21 +420,12 @@ func (s *Service) RunMigration(ctx context.Context, uid string) (*cloudmigration } func (s *Service) getMigrationDataJSON(ctx context.Context) (*cloudmigration.MigrateDataRequest, error) { - var migrationDataSlice []cloudmigration.MigrateDataRequestItem // Data sources dataSources, err := s.getDataSources(ctx) if err != nil { s.log.Error("Failed to get datasources", "err", err) return nil, err } - for _, ds := range dataSources { - migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ - Type: cloudmigration.DatasourceDataType, - RefID: ds.UID, - Name: ds.Name, - Data: ds, - }) - } // Dashboards dashboards, err := s.getDashboards(ctx) @@ -443,6 +434,26 @@ func (s *Service) getMigrationDataJSON(ctx context.Context) (*cloudmigration.Mig return nil, err } + // Folders + folders, err := s.getFolders(ctx) + if err != nil { + s.log.Error("Failed to get folders", "err", err) + return nil, err + } + + migrationDataSlice := make( + []cloudmigration.MigrateDataRequestItem, 0, + len(dataSources)+len(dashboards)+len(folders), + ) + for _, ds := range dataSources { + migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ + Type: cloudmigration.DatasourceDataType, + RefID: ds.UID, + Name: ds.Name, + Data: ds, + }) + } + for _, dashboard := range dashboards { dashboard.Data.Del("id") migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ @@ -453,13 +464,6 @@ func (s *Service) getMigrationDataJSON(ctx context.Context) (*cloudmigration.Mig }) } - // Folders - folders, err := s.getFolders(ctx) - if err != nil { - s.log.Error("Failed to get folders", "err", err) - return nil, err - } - for _, f := range folders { migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ Type: cloudmigration.FolderDataType, @@ -468,6 +472,7 @@ func (s *Service) getMigrationDataJSON(ctx context.Context) (*cloudmigration.Mig Data: f, }) } + migrationData := &cloudmigration.MigrateDataRequest{ Items: migrationDataSlice, } @@ -521,9 +526,9 @@ func (s *Service) getFolders(ctx context.Context) ([]folder.Folder, error) { return nil, err } - var result []folder.Folder - for _, folder := range folders { - result = append(result, *folder) + result := make([]folder.Folder, len(folders)) + for i, folder := range folders { + result[i] = *folder } return result, nil @@ -535,10 +540,11 @@ func (s *Service) getDashboards(ctx context.Context) ([]dashboards.Dashboard, er return nil, err } - var result []dashboards.Dashboard - for _, dashboard := range dashs { - result = append(result, *dashboard) + result := make([]dashboards.Dashboard, len(dashs)) + for i, dashboard := range dashs { + result[i] = *dashboard } + return result, nil } diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index 8bbdea05fdb..c3ae1f976b3 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -536,6 +536,10 @@ func (d *dashboardStore) GetSoftDeletedDashboard(ctx context.Context, orgID int6 func (d *dashboardStore) RestoreDashboard(ctx context.Context, orgID int64, dashboardUID string, folder *folder.Folder) error { return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error { + if folder == nil || folder.UID == "" { + _, err := sess.Exec("UPDATE dashboard SET deleted=NULL, folder_id=0, folder_uid=NULL WHERE org_id=? AND uid=?", orgID, dashboardUID) + return err + } // nolint:staticcheck _, err := sess.Exec("UPDATE dashboard SET deleted=NULL, folder_id = ?, folder_uid=? WHERE org_id=? AND uid=?", folder.ID, folder.UID, orgID, dashboardUID) return err diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 7871776d224..c94d8b4df7f 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -741,7 +741,9 @@ func makeQueryResult(query *dashboards.FindPersistedDashboardsQuery, res []dashb hit.Tags = append(hit.Tags, item.Term) } if item.Deleted != nil { - hit.RemainingTrashAtAge = util.RemainingDaysUntil((*item.Deleted).Add(daysInTrash)) + deletedDate := (*item.Deleted).Add(daysInTrash) + hit.IsDeleted = true + hit.PermanentlyDeleteDate = &deletedDate } } return hitList diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 17485739de4..51bf6ea8828 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1126,14 +1126,6 @@ var ( HideFromDocs: true, HideFromAdminPage: true, }, - { - Name: "betterPageScrolling", - Description: "Removes CustomScrollbar from the UI, relying on native browser scrollbars", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Owner: grafanaFrontendPlatformSquad, - Expression: "true", // enabled by default - }, { Name: "authAPIAccessTokenAuth", Description: "Enables the use of Auth API access tokens for authentication", @@ -1317,6 +1309,27 @@ var ( HideFromAdminPage: true, HideFromDocs: true, }, + { + Name: "openSearchBackendFlowEnabled", + Description: "Enables the backend query flow for Open Search datasource plugin", + Stage: FeatureStagePublicPreview, + Owner: awsDatasourcesSquad, + }, + { + Name: "ssoSettingsLDAP", + Description: "Use the new SSO Settings API to configure LDAP", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, + HideFromAdminPage: true, + }, + { + Name: "databaseReadReplica", + Description: "Use a read replica for some database queries.", + Stage: FeatureStageExperimental, + Owner: grafanaBackendServicesSquad, + Expression: "false", // enabled by default + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 242c488b6fb..c0b5e76d4c1 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -150,7 +150,6 @@ tlsMemcached,experimental,@grafana/grafana-operator-experience-squad,false,false kubernetesAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false expressionParser,experimental,@grafana/grafana-app-platform-squad,false,true,false groupByVariable,experimental,@grafana/dashboards-squad,false,false,false -betterPageScrolling,GA,@grafana/grafana-frontend-platform,false,false,true authAPIAccessTokenAuth,experimental,@grafana/identity-access-team,false,false,false scopeFilters,experimental,@grafana/dashboards-squad,false,false,false ssoSettingsSAML,preview,@grafana/identity-access-team,false,false,false @@ -175,3 +174,6 @@ pluginProxyPreserveTrailingSlash,GA,@grafana/plugins-platform-backend,false,fals azureMonitorPrometheusExemplars,experimental,@grafana/partner-datasources,false,false,false pinNavItems,experimental,@grafana/grafana-frontend-platform,false,false,false authZGRPCServer,experimental,@grafana/identity-access-team,false,false,false +openSearchBackendFlowEnabled,preview,@grafana/aws-datasources,false,false,false +ssoSettingsLDAP,experimental,@grafana/identity-access-team,false,false,false +databaseReadReplica,experimental,@grafana/grafana-backend-services-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 9afc6b29e29..d7f10257ddf 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -611,10 +611,6 @@ const ( // Enable groupBy variable support in scenes dashboards FlagGroupByVariable = "groupByVariable" - // FlagBetterPageScrolling - // Removes CustomScrollbar from the UI, relying on native browser scrollbars - FlagBetterPageScrolling = "betterPageScrolling" - // FlagAuthAPIAccessTokenAuth // Enables the use of Auth API access tokens for authentication FlagAuthAPIAccessTokenAuth = "authAPIAccessTokenAuth" @@ -710,4 +706,16 @@ const ( // FlagAuthZGRPCServer // Enables the gRPC server for authorization FlagAuthZGRPCServer = "authZGRPCServer" + + // FlagOpenSearchBackendFlowEnabled + // Enables the backend query flow for Open Search datasource plugin + FlagOpenSearchBackendFlowEnabled = "openSearchBackendFlowEnabled" + + // FlagSsoSettingsLDAP + // Use the new SSO Settings API to configure LDAP + FlagSsoSettingsLDAP = "ssoSettingsLDAP" + + // FlagDatabaseReadReplica + // Use a read replica for some database queries. + FlagDatabaseReadReplica = "databaseReadReplica" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index de18acfb7be..02df39301fd 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -463,7 +463,8 @@ "metadata": { "name": "betterPageScrolling", "resourceVersion": "1717578796182", - "creationTimestamp": "2024-03-06T15:06:47Z" + "creationTimestamp": "2024-03-06T15:06:47Z", + "deletionTimestamp": "2024-06-18T09:25:56Z" }, "spec": { "description": "Removes CustomScrollbar from the UI, relying on native browser scrollbars", @@ -654,6 +655,18 @@ "frontend": true } }, + { + "metadata": { + "name": "databaseReadReplica", + "resourceVersion": "1718308641844", + "creationTimestamp": "2024-06-13T19:57:21Z" + }, + "spec": { + "description": "Use a read replica for some database queries.", + "stage": "experimental", + "codeowner": "@grafana/grafana-backend-services-squad" + } + }, { "metadata": { "name": "dataplaneFrontendFallback", @@ -1599,6 +1612,18 @@ "codeowner": "@grafana/grafana-operator-experience-squad" } }, + { + "metadata": { + "name": "openSearchBackendFlowEnabled", + "resourceVersion": "1718357852240", + "creationTimestamp": "2024-06-14T09:37:32Z" + }, + "spec": { + "description": "Enables the backend query flow for Open Search datasource plugin", + "stage": "preview", + "codeowner": "@grafana/aws-datasources" + } + }, { "metadata": { "name": "panelFilterVariable", @@ -2114,6 +2139,20 @@ "allowSelfServe": true } }, + { + "metadata": { + "name": "ssoSettingsLDAP", + "resourceVersion": "1718624569822", + "creationTimestamp": "2024-06-17T11:42:49Z" + }, + "spec": { + "description": "Use the new SSO Settings API to configure LDAP", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "ssoSettingsSAML", diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 8dd76b6505f..073e6ca239f 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -209,7 +209,10 @@ func (s *Service) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Fo var dashFolder *folder.Folder var err error switch { - case q.UID != nil && *q.UID != "": + case q.UID != nil: + if *q.UID == "" { + return &folder.GeneralFolder, nil + } dashFolder, err = s.getFolderByUID(ctx, q.OrgID, *q.UID) if err != nil { return nil, err diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 5e9485e4bc8..f47d4698769 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -395,6 +395,21 @@ func TestIntegrationFolderService(t *testing.T) { "For error '%s' expected error '%s', actual '%s'", tc.ActualError, tc.ExpectedError, actualError) } }) + + t.Run("Returns root folder", func(t *testing.T) { + t.Run("When the folder UID is blank should return the root folder", func(t *testing.T) { + emptyString := "" + actual, err := service.Get(context.Background(), &folder.GetFolderQuery{ + UID: &emptyString, + OrgID: 1, + SignedInUser: usr, + }) + + assert.NoError(t, err) + assert.Equal(t, folder.GeneralFolder.UID, actual.UID) + assert.Equal(t, folder.GeneralFolder.Title, actual.Title) + }) + }) }) } diff --git a/pkg/services/ldap/api/service.go b/pkg/services/ldap/api/service.go index 368e3acff6b..20080b10cb1 100644 --- a/pkg/services/ldap/api/service.go +++ b/pkg/services/ldap/api/service.go @@ -29,7 +29,8 @@ import ( ) type Service struct { - cfg *setting.Cfg + cfg *ldap.Config + adminUser string userService user.Service authInfoService login.AuthInfoService ldapGroupsService ldap.Groups @@ -47,7 +48,8 @@ func ProvideService( sessionService auth.UserTokenService, bundleRegistry supportbundles.Service, ) *Service { s := &Service{ - cfg: cfg, + cfg: ldap.GetLDAPConfig(cfg), + adminUser: cfg.AdminUser, userService: userService, authInfoService: authInfoService, ldapGroupsService: ldapGroupsService, @@ -96,7 +98,7 @@ func ProvideService( // 403: forbiddenError // 500: internalServerError func (s *Service) ReloadLDAPCfg(c *contextmodel.ReqContext) response.Response { - if !s.cfg.LDAPAuthEnabled { + if !s.cfg.Enabled { return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil) } @@ -122,7 +124,7 @@ func (s *Service) ReloadLDAPCfg(c *contextmodel.ReqContext) response.Response { // 403: forbiddenError // 500: internalServerError func (s *Service) GetLDAPStatus(c *contextmodel.ReqContext) response.Response { - if !s.cfg.LDAPAuthEnabled { + if !s.cfg.Enabled { return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil) } @@ -169,7 +171,7 @@ func (s *Service) GetLDAPStatus(c *contextmodel.ReqContext) response.Response { // 403: forbiddenError // 500: internalServerError func (s *Service) PostSyncUserWithLDAP(c *contextmodel.ReqContext) response.Response { - if !s.cfg.LDAPAuthEnabled { + if !s.cfg.Enabled { return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil) } @@ -206,7 +208,7 @@ func (s *Service) PostSyncUserWithLDAP(c *contextmodel.ReqContext) response.Resp userInfo, _, err := ldapClient.User(usr.Login) if err != nil { if errors.Is(err, multildap.ErrDidNotFindUser) { // User was not in the LDAP server - we need to take action: - if s.cfg.AdminUser == usr.Login { // User is *the* Grafana Admin. We cannot disable it. + if s.adminUser == usr.Login { // User is *the* Grafana Admin. We cannot disable it. errMsg := fmt.Sprintf(`Refusing to sync grafana super admin "%s" - it would be disabled`, usr.Login) s.log.Error(errMsg) return response.Error(http.StatusBadRequest, errMsg, err) @@ -250,7 +252,7 @@ func (s *Service) PostSyncUserWithLDAP(c *contextmodel.ReqContext) response.Resp // 403: forbiddenError // 500: internalServerError func (s *Service) GetUserFromLDAP(c *contextmodel.ReqContext) response.Response { - if !s.cfg.LDAPAuthEnabled { + if !s.cfg.Enabled { return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil) } @@ -330,8 +332,8 @@ func (s *Service) identityFromLDAPUser(user *login.ExternalUserInfo) *authn.Iden SyncUser: true, SyncTeams: true, EnableUser: true, - SyncOrgRoles: !s.cfg.LDAPSkipOrgRoleSync, - AllowSignUp: s.cfg.LDAPAllowSignup, + SyncOrgRoles: !s.cfg.SkipOrgRoleSync, + AllowSignUp: s.cfg.AllowSignUp, }, } } diff --git a/pkg/services/ldap/api/service_test.go b/pkg/services/ldap/api/service_test.go index 1e239a06e19..95fd54a0234 100644 --- a/pkg/services/ldap/api/service_test.go +++ b/pkg/services/ldap/api/service_test.go @@ -95,7 +95,7 @@ func TestGetUserFromLDAPAPIEndpoint_UserNotFound(t *testing.T) { ExpectedClient: &LDAPMock{ UserSearchResult: nil, }, - ExpectedConfig: &ldap.Config{}, + ExpectedConfig: &ldap.ServersConfig{}, } }) @@ -160,7 +160,7 @@ func TestGetUserFromLDAPAPIEndpoint_OrgNotfound(t *testing.T) { UserSearchResult: userSearchResult, UserSearchConfig: userSearchConfig, }, - ExpectedConfig: &ldap.Config{}, + ExpectedConfig: &ldap.ServersConfig{}, } }) @@ -229,7 +229,7 @@ func TestGetUserFromLDAPAPIEndpoint(t *testing.T) { UserSearchResult: userSearchResult, UserSearchConfig: userSearchConfig, }, - ExpectedConfig: &ldap.Config{}, + ExpectedConfig: &ldap.ServersConfig{}, } }) @@ -314,7 +314,7 @@ func TestGetUserFromLDAPAPIEndpoint_WithTeamHandler(t *testing.T) { UserSearchResult: userSearchResult, UserSearchConfig: userSearchConfig, }, - ExpectedConfig: &ldap.Config{}, + ExpectedConfig: &ldap.ServersConfig{}, } }) @@ -368,7 +368,7 @@ func TestGetLDAPStatusAPIEndpoint(t *testing.T) { _, server := setupAPITest(t, func(a *Service) { a.ldapService = &service.LDAPFakeService{ ExpectedClient: &LDAPMock{}, - ExpectedConfig: &ldap.Config{}, + ExpectedConfig: &ldap.ServersConfig{}, } }) @@ -407,7 +407,7 @@ func TestPostSyncUserWithLDAPAPIEndpoint_Success(t *testing.T) { ExpectedClient: &LDAPMock{UserSearchResult: &login.ExternalUserInfo{ Login: "ldap-daniel", }}, - ExpectedConfig: &ldap.Config{}, + ExpectedConfig: &ldap.ServersConfig{}, } }) @@ -442,7 +442,7 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotFound(t *testing.T) { a.userService = userServiceMock a.ldapService = &service.LDAPFakeService{ ExpectedClient: &LDAPMock{}, - ExpectedConfig: &ldap.Config{}, + ExpectedConfig: &ldap.ServersConfig{}, } }) @@ -475,10 +475,10 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenGrafanaAdmin(t *testing.T) { _, server := setupAPITest(t, func(a *Service) { a.userService = userServiceMock - a.cfg.AdminUser = "ldap-daniel" + a.adminUser = "ldap-daniel" a.ldapService = &service.LDAPFakeService{ ExpectedClient: &LDAPMock{UserSearchError: multildap.ErrDidNotFindUser}, - ExpectedConfig: &ldap.Config{}, + ExpectedConfig: &ldap.ServersConfig{}, } }) @@ -511,7 +511,7 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotInLDAP(t *testing.T) { a.authInfoService = &authinfotest.FakeService{ExpectedExternalUser: &login.ExternalUserInfo{IsDisabled: true, UserId: 34}} a.ldapService = &service.LDAPFakeService{ ExpectedClient: &LDAPMock{UserSearchError: multildap.ErrDidNotFindUser}, - ExpectedConfig: &ldap.Config{}, + ExpectedConfig: &ldap.ServersConfig{}, } }) @@ -641,12 +641,12 @@ search_base_dns = ["dc=grafana,dc=org"]`) t.Run(tt.desc, func(t *testing.T) { _, server := setupAPITest(t, func(a *Service) { a.userService = &usertest.FakeUserService{ExpectedUser: &user.User{Login: "ldap-daniel", ID: 1}} - a.cfg.LDAPConfigFilePath = ldapConfigFile + a.cfg.ConfigFilePath = ldapConfigFile a.ldapService = &service.LDAPFakeService{ ExpectedClient: &LDAPMock{UserSearchResult: &login.ExternalUserInfo{ Login: "ldap-daniel", }}, - ExpectedConfig: &ldap.Config{}, + ExpectedConfig: &ldap.ServersConfig{}, } }) // Add minimal setup to pass handler diff --git a/pkg/services/ldap/api/support_bundle.go b/pkg/services/ldap/api/support_bundle.go index f85784d6f43..88d71c53f13 100644 --- a/pkg/services/ldap/api/support_bundle.go +++ b/pkg/services/ldap/api/support_bundle.go @@ -73,12 +73,12 @@ func (s *Service) supportBundleCollector(context.Context) (*supportbundles.Suppo bWriter.WriteString("```ini\n") - bWriter.WriteString(fmt.Sprintf("enabled = %v\n", s.cfg.LDAPAuthEnabled)) - bWriter.WriteString(fmt.Sprintf("config_file = %s\n", s.cfg.LDAPConfigFilePath)) - bWriter.WriteString(fmt.Sprintf("allow_sign_up = %v\n", s.cfg.LDAPAllowSignup)) - bWriter.WriteString(fmt.Sprintf("sync_cron = %s\n", s.cfg.LDAPSyncCron)) - bWriter.WriteString(fmt.Sprintf("active_sync_enabled = %v\n", s.cfg.LDAPActiveSyncEnabled)) - bWriter.WriteString(fmt.Sprintf("skip_org_role_sync = %v\n", s.cfg.LDAPSkipOrgRoleSync)) + bWriter.WriteString(fmt.Sprintf("enabled = %v\n", s.cfg.Enabled)) + bWriter.WriteString(fmt.Sprintf("config_file = %s\n", s.cfg.ConfigFilePath)) + bWriter.WriteString(fmt.Sprintf("allow_sign_up = %v\n", s.cfg.AllowSignUp)) + bWriter.WriteString(fmt.Sprintf("sync_cron = %s\n", s.cfg.SyncCron)) + bWriter.WriteString(fmt.Sprintf("active_sync_enabled = %v\n", s.cfg.ActiveSyncEnabled)) + bWriter.WriteString(fmt.Sprintf("skip_org_role_sync = %v\n", s.cfg.SkipOrgRoleSync)) bWriter.WriteString("```\n\n") diff --git a/pkg/services/ldap/ldap.go b/pkg/services/ldap/ldap.go index e4d75119ebf..add301da595 100644 --- a/pkg/services/ldap/ldap.go +++ b/pkg/services/ldap/ldap.go @@ -18,7 +18,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -45,7 +44,7 @@ type IServer interface { // Server is basic struct of LDAP authorization type Server struct { - cfg *setting.Cfg + cfg *Config Config *ServerConfig Connection IConnection log log.Logger @@ -86,7 +85,7 @@ var ( ) // New creates the new LDAP connection -func New(config *ServerConfig, cfg *setting.Cfg) IServer { +func New(config *ServerConfig, cfg *Config) IServer { return &Server{ Config: config, cfg: cfg, @@ -414,7 +413,7 @@ func (server *Server) users(logins []string) ( // If there are no ldap group mappings access is true // otherwise a single group must match func (server *Server) validateGrafanaUser(user *login.ExternalUserInfo) error { - if !server.cfg.LDAPSkipOrgRoleSync && len(server.Config.Groups) > 0 && + if !server.cfg.SkipOrgRoleSync && len(server.Config.Groups) > 0 && (len(user.OrgRoles) == 0 && (user.IsGrafanaAdmin == nil || !*user.IsGrafanaAdmin)) { server.log.Warn( "User does not belong in any of the specified LDAP groups", @@ -499,7 +498,7 @@ func (server *Server) buildGrafanaUser(user *ldap.Entry) (*login.ExternalUserInf } // Skipping org role sync - if server.cfg.LDAPSkipOrgRoleSync { + if server.cfg.SkipOrgRoleSync { server.log.Debug("Skipping organization role mapping.") return extUser, nil } diff --git a/pkg/services/ldap/ldap_login_test.go b/pkg/services/ldap/ldap_login_test.go index a3feb6d9734..11d1d8a6cfc 100644 --- a/pkg/services/ldap/ldap_login_test.go +++ b/pkg/services/ldap/ldap_login_test.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/login" - "github.com/grafana/grafana/pkg/setting" ) var defaultLogin = &login.LoginUserQuery{ @@ -31,8 +30,9 @@ func TestServer_Login_UserBind_Fail(t *testing.T) { } } - cfg := setting.NewCfg() - cfg.LDAPAuthEnabled = true + cfg := &Config{ + Enabled: true, + } server := &Server{ cfg: cfg, Config: &ServerConfig{ @@ -105,8 +105,9 @@ func TestServer_Login_ValidCredentials(t *testing.T) { return nil } - cfg := setting.NewCfg() - cfg.LDAPAuthEnabled = true + cfg := &Config{ + Enabled: true, + } server := &Server{ cfg: cfg, @@ -142,8 +143,9 @@ func TestServer_Login_UnauthenticatedBind(t *testing.T) { return nil } - cfg := setting.NewCfg() - cfg.LDAPAuthEnabled = true + cfg := &Config{ + Enabled: true, + } server := &Server{ cfg: cfg, @@ -189,8 +191,9 @@ func TestServer_Login_AuthenticatedBind(t *testing.T) { return nil } - cfg := setting.NewCfg() - cfg.LDAPAuthEnabled = true + cfg := &Config{ + Enabled: true, + } server := &Server{ cfg: cfg, @@ -232,8 +235,9 @@ func TestServer_Login_UserWildcardBind(t *testing.T) { return nil } - cfg := setting.NewCfg() - cfg.LDAPAuthEnabled = true + cfg := &Config{ + Enabled: true, + } server := &Server{ cfg: cfg, diff --git a/pkg/services/ldap/ldap_private_test.go b/pkg/services/ldap/ldap_private_test.go index 24e1f3efe4c..6dc52c19ba7 100644 --- a/pkg/services/ldap/ldap_private_test.go +++ b/pkg/services/ldap/ldap_private_test.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/setting" ) func TestServer_getSearchRequest(t *testing.T) { @@ -53,8 +52,9 @@ func TestServer_getSearchRequest(t *testing.T) { func TestSerializeUsers(t *testing.T) { t.Run("simple case", func(t *testing.T) { - cfg := setting.NewCfg() - cfg.LDAPAuthEnabled = true + cfg := &Config{ + Enabled: true, + } server := &Server{ cfg: cfg, @@ -92,8 +92,9 @@ func TestSerializeUsers(t *testing.T) { }) t.Run("without lastname", func(t *testing.T) { - cfg := setting.NewCfg() - cfg.LDAPAuthEnabled = true + cfg := &Config{ + Enabled: true, + } server := &Server{ cfg: cfg, @@ -129,8 +130,9 @@ func TestSerializeUsers(t *testing.T) { }) t.Run("mark user without matching group as disabled", func(t *testing.T) { - cfg := setting.NewCfg() - cfg.LDAPAuthEnabled = true + cfg := &Config{ + Enabled: true, + } server := &Server{ cfg: cfg, @@ -163,8 +165,9 @@ func TestSerializeUsers(t *testing.T) { func TestServer_validateGrafanaUser(t *testing.T) { t.Run("no group config", func(t *testing.T) { - cfg := setting.NewCfg() - cfg.LDAPAuthEnabled = true + cfg := &Config{ + Enabled: true, + } server := &Server{ cfg: cfg, @@ -183,8 +186,9 @@ func TestServer_validateGrafanaUser(t *testing.T) { }) t.Run("user in group", func(t *testing.T) { - cfg := setting.NewCfg() - cfg.LDAPAuthEnabled = true + cfg := &Config{ + Enabled: true, + } server := &Server{ cfg: cfg, @@ -210,8 +214,9 @@ func TestServer_validateGrafanaUser(t *testing.T) { }) t.Run("user not in group", func(t *testing.T) { - cfg := setting.NewCfg() - cfg.LDAPAuthEnabled = true + cfg := &Config{ + Enabled: true, + } server := &Server{ cfg: cfg, diff --git a/pkg/services/ldap/ldap_test.go b/pkg/services/ldap/ldap_test.go index 11a38172608..b6768665418 100644 --- a/pkg/services/ldap/ldap_test.go +++ b/pkg/services/ldap/ldap_test.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/setting" ) const ( @@ -55,7 +54,7 @@ func TestNew(t *testing.T) { result := New(&ServerConfig{ Attr: AttributeMap{}, SearchBaseDNs: []string{"BaseDNHere"}, - }, &setting.Cfg{}) + }, &Config{}) assert.Implements(t, (*IServer)(nil), result) } @@ -68,7 +67,7 @@ func TestServer_Dial(t *testing.T) { ClientCert: "./testdata/parsable.cert", ClientKey: "./testdata/parsable.pem", } - server := New(serverConfig, &setting.Cfg{}) + server := New(serverConfig, &Config{}) err := server.Dial() require.Error(t, err) @@ -79,7 +78,7 @@ func TestServer_Dial(t *testing.T) { serverConfig := &ServerConfig{ RootCACert: "./testdata/invalid.cert", } - server := New(serverConfig, &setting.Cfg{}) + server := New(serverConfig, &Config{}) err := server.Dial() require.Error(t, err) @@ -90,7 +89,7 @@ func TestServer_Dial(t *testing.T) { serverConfig := &ServerConfig{ RootCACert: "./testdata/nofile.cert", } - server := New(serverConfig, &setting.Cfg{}) + server := New(serverConfig, &Config{}) err := server.Dial() require.Error(t, err) @@ -102,7 +101,7 @@ func TestServer_Dial(t *testing.T) { ClientCert: "./testdata/invalid.cert", ClientKey: "./testdata/invalid.pem", } - server := New(serverConfig, &setting.Cfg{}) + server := New(serverConfig, &Config{}) err := server.Dial() require.Error(t, err) @@ -114,7 +113,7 @@ func TestServer_Dial(t *testing.T) { ClientCert: "./testdata/nofile.cert", ClientKey: "./testdata/parsable.pem", } - server := New(serverConfig, &setting.Cfg{}) + server := New(serverConfig, &Config{}) err := server.Dial() require.Error(t, err) @@ -128,7 +127,7 @@ func TestServer_Dial(t *testing.T) { ClientCertValue: validCert, ClientKeyValue: validKey, } - server := New(serverConfig, &setting.Cfg{}) + server := New(serverConfig, &Config{}) err := server.Dial() require.Error(t, err) @@ -139,7 +138,7 @@ func TestServer_Dial(t *testing.T) { serverConfig := &ServerConfig{ RootCACertValue: []string{"invalid-certificate"}, } - server := New(serverConfig, &setting.Cfg{}) + server := New(serverConfig, &Config{}) err := server.Dial() require.Error(t, err) @@ -150,7 +149,7 @@ func TestServer_Dial(t *testing.T) { serverConfig := &ServerConfig{ RootCACertValue: []string{"aW52YWxpZC1jZXJ0aWZpY2F0ZQ=="}, } - server := New(serverConfig, &setting.Cfg{}) + server := New(serverConfig, &Config{}) err := server.Dial() require.Error(t, err) @@ -162,7 +161,7 @@ func TestServer_Dial(t *testing.T) { ClientCertValue: "invalid-certificate", ClientKeyValue: validKey, } - server := New(serverConfig, &setting.Cfg{}) + server := New(serverConfig, &Config{}) err := server.Dial() require.Error(t, err) @@ -174,7 +173,7 @@ func TestServer_Dial(t *testing.T) { ClientCertValue: validCert, ClientKeyValue: "aW52YWxpZC1rZXk=", } - server := New(serverConfig, &setting.Cfg{}) + server := New(serverConfig, &Config{}) err := server.Dial() require.Error(t, err) @@ -226,8 +225,9 @@ func TestServer_Users(t *testing.T) { conn.setSearchResult(&result) // Set up attribute map without surname and email - cfg := setting.NewCfg() - cfg.LDAPAuthEnabled = true + cfg := &Config{ + Enabled: true, + } server := &Server{ cfg: cfg, @@ -323,7 +323,7 @@ func TestServer_Users(t *testing.T) { }) server := &Server{ - cfg: setting.NewCfg(), + cfg: &Config{}, Config: &ServerConfig{ Attr: AttributeMap{ Username: "username", @@ -370,8 +370,9 @@ func TestServer_Users(t *testing.T) { } }) - cfg := setting.NewCfg() - cfg.LDAPAuthEnabled = true + cfg := &Config{ + Enabled: true, + } server := &Server{ cfg: cfg, @@ -464,8 +465,9 @@ func TestServer_Users(t *testing.T) { }) isGrafanaAdmin := true - cfg := setting.NewCfg() - cfg.LDAPAuthEnabled = true + cfg := &Config{ + Enabled: true, + } server := &Server{ cfg: cfg, @@ -506,7 +508,7 @@ func TestServer_Users(t *testing.T) { require.True(t, res[0].IsDisabled) }) t.Run("skip org role sync", func(t *testing.T) { - server.cfg.LDAPSkipOrgRoleSync = true + server.cfg.SkipOrgRoleSync = true res, err := server.Users([]string{"groot"}) require.NoError(t, err) @@ -517,7 +519,7 @@ func TestServer_Users(t *testing.T) { require.False(t, res[0].IsDisabled) }) t.Run("sync org role", func(t *testing.T) { - server.cfg.LDAPSkipOrgRoleSync = false + server.cfg.SkipOrgRoleSync = false res, err := server.Users([]string{"groot"}) require.NoError(t, err) require.Len(t, res, 1) diff --git a/pkg/services/ldap/multildap/multildap.go b/pkg/services/ldap/multildap/multildap.go index 2e70a8d5978..601a058da42 100644 --- a/pkg/services/ldap/multildap/multildap.go +++ b/pkg/services/ldap/multildap/multildap.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" - "github.com/grafana/grafana/pkg/setting" ) // GetConfig gets LDAP config @@ -54,12 +53,12 @@ type IMultiLDAP interface { // MultiLDAP is basic struct of LDAP authorization type MultiLDAP struct { configs []*ldap.ServerConfig - cfg *setting.Cfg + cfg *ldap.Config log log.Logger } // New creates the new LDAP auth -func New(configs []*ldap.ServerConfig, cfg *setting.Cfg) IMultiLDAP { +func New(configs []*ldap.ServerConfig, cfg *ldap.Config) IMultiLDAP { return &MultiLDAP{ configs: configs, cfg: cfg, diff --git a/pkg/services/ldap/multildap/multildap_test.go b/pkg/services/ldap/multildap/multildap_test.go index f76bdb9bab1..a04d0b1d037 100644 --- a/pkg/services/ldap/multildap/multildap_test.go +++ b/pkg/services/ldap/multildap/multildap_test.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" - "github.com/grafana/grafana/pkg/setting" //TODO(sh0rez): remove once import cycle resolved _ "github.com/grafana/grafana/pkg/api/response" @@ -19,7 +18,7 @@ func TestMultiLDAP(t *testing.T) { t.Run("Should return error for absent config list", func(t *testing.T) { setup() - multi := New([]*ldap.ServerConfig{}, setting.NewCfg()) + multi := New([]*ldap.ServerConfig{}, &ldap.Config{}) _, err := multi.Ping() require.Error(t, err) @@ -35,7 +34,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {Host: "10.0.0.1", Port: 361}, - }, setting.NewCfg()) + }, &ldap.Config{}) statuses, err := multi.Ping() @@ -53,7 +52,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {Host: "10.0.0.1", Port: 361}, - }, setting.NewCfg()) + }, &ldap.Config{}) statuses, err := multi.Ping() @@ -71,7 +70,7 @@ func TestMultiLDAP(t *testing.T) { t.Run("Should return error for absent config list", func(t *testing.T) { setup() - multi := New([]*ldap.ServerConfig{}, setting.NewCfg()) + multi := New([]*ldap.ServerConfig{}, &ldap.Config{}) _, err := multi.Login(&login.LoginUserQuery{}) require.Error(t, err) @@ -88,7 +87,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, - }, setting.NewCfg()) + }, &ldap.Config{}) _, err := multi.Login(&login.LoginUserQuery{}) @@ -104,7 +103,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, - }, setting.NewCfg()) + }, &ldap.Config{}) _, err := multi.Login(&login.LoginUserQuery{}) require.Equal(t, 2, mock.dialCalledTimes) @@ -125,7 +124,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, - }, setting.NewCfg()) + }, &ldap.Config{}) result, err := multi.Login(&login.LoginUserQuery{}) require.Equal(t, 1, mock.dialCalledTimes) @@ -145,7 +144,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, - }, setting.NewCfg()) + }, &ldap.Config{}) _, err := multi.Login(&login.LoginUserQuery{}) require.Equal(t, 2, mock.dialCalledTimes) @@ -164,7 +163,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, - }, setting.NewCfg()) + }, &ldap.Config{}) _, err := multi.Login(&login.LoginUserQuery{}) require.Equal(t, 2, mock.dialCalledTimes) @@ -184,7 +183,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, - }, setting.NewCfg()) + }, &ldap.Config{}) _, err := multi.Login(&login.LoginUserQuery{}) require.Equal(t, 2, mock.dialCalledTimes) @@ -202,7 +201,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, - }, setting.NewCfg()) + }, &ldap.Config{}) _, err := multi.Login(&login.LoginUserQuery{}) require.Equal(t, 1, mock.dialCalledTimes) @@ -219,7 +218,7 @@ func TestMultiLDAP(t *testing.T) { t.Run("Should return error for absent config list", func(t *testing.T) { setup() - multi := New([]*ldap.ServerConfig{}, setting.NewCfg()) + multi := New([]*ldap.ServerConfig{}, &ldap.Config{}) _, _, err := multi.User("test") require.Error(t, err) @@ -236,7 +235,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, - }, setting.NewCfg()) + }, &ldap.Config{}) _, _, err := multi.User("test") @@ -251,7 +250,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, - }, setting.NewCfg()) + }, &ldap.Config{}) _, _, err := multi.User("test") require.Equal(t, 2, mock.dialCalledTimes) @@ -271,7 +270,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, - }, setting.NewCfg()) + }, &ldap.Config{}) _, _, err := multi.User("test") require.Equal(t, 1, mock.dialCalledTimes) @@ -298,7 +297,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, - }, setting.NewCfg()) + }, &ldap.Config{}) user, _, err := multi.User("test") require.Equal(t, 1, mock.dialCalledTimes) @@ -319,7 +318,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, - }, setting.NewCfg()) + }, &ldap.Config{}) _, _, err := multi.User("test") require.Equal(t, 2, mock.dialCalledTimes) @@ -338,7 +337,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, - }, setting.NewCfg()) + }, &ldap.Config{}) _, err := multi.Users([]string{"test"}) require.Equal(t, 2, mock.dialCalledTimes) @@ -349,7 +348,7 @@ func TestMultiLDAP(t *testing.T) { t.Run("Should return error for absent config list", func(t *testing.T) { setup() - multi := New([]*ldap.ServerConfig{}, setting.NewCfg()) + multi := New([]*ldap.ServerConfig{}, &ldap.Config{}) _, err := multi.Users([]string{"test"}) require.Error(t, err) @@ -366,7 +365,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, - }, setting.NewCfg()) + }, &ldap.Config{}) _, err := multi.Users([]string{"test"}) @@ -381,7 +380,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, - }, setting.NewCfg()) + }, &ldap.Config{}) _, err := multi.Users([]string{"test"}) require.Equal(t, 2, mock.dialCalledTimes) @@ -401,7 +400,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, - }, setting.NewCfg()) + }, &ldap.Config{}) _, err := multi.Users([]string{"test"}) require.Equal(t, 1, mock.dialCalledTimes) @@ -434,7 +433,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, - }, setting.NewCfg()) + }, &ldap.Config{}) users, err := multi.Users([]string{"test"}) require.Equal(t, 2, mock.dialCalledTimes) @@ -512,7 +511,7 @@ func (mock *mockLDAP) Bind() error { func setup() *mockLDAP { mock := &mockLDAP{} - newLDAP = func(config *ldap.ServerConfig, cfg *setting.Cfg) ldap.IServer { + newLDAP = func(config *ldap.ServerConfig, cfg *ldap.Config) ldap.IServer { return mock } diff --git a/pkg/services/ldap/service/fake.go b/pkg/services/ldap/service/fake.go index 05940531038..c6aa168ae37 100644 --- a/pkg/services/ldap/service/fake.go +++ b/pkg/services/ldap/service/fake.go @@ -7,7 +7,7 @@ import ( ) type LDAPFakeService struct { - ExpectedConfig *ldap.Config + ExpectedConfig *ldap.ServersConfig ExpectedClient multildap.IMultiLDAP ExpectedError error ExpectedUser *login.ExternalUserInfo @@ -22,7 +22,7 @@ func (s *LDAPFakeService) ReloadConfig() error { return s.ExpectedError } -func (s *LDAPFakeService) Config() *ldap.Config { +func (s *LDAPFakeService) Config() *ldap.ServersConfig { return s.ExpectedConfig } diff --git a/pkg/services/ldap/service/helpers.go b/pkg/services/ldap/service/helpers.go index 245bddb9aa5..d2b487c4289 100644 --- a/pkg/services/ldap/service/helpers.go +++ b/pkg/services/ldap/service/helpers.go @@ -1,8 +1,10 @@ package service import ( + "encoding/json" "fmt" "os" + "strconv" "github.com/BurntSushi/toml" @@ -13,8 +15,8 @@ import ( const defaultTimeout = 10 -func readConfig(configFile string) (*ldap.Config, error) { - result := &ldap.Config{} +func readConfig(configFile string) (*ldap.ServersConfig, error) { + result := &ldap.ServersConfig{} logger.Info("LDAP enabled, reading config file", "file", configFile) @@ -85,3 +87,27 @@ func assertNotEmptyCfg(val any, propName string) error { } return nil } + +func resolveBool(input any, defaultValue bool) bool { + strInput := fmt.Sprintf("%v", input) + result, err := strconv.ParseBool(strInput) + if err != nil { + return defaultValue + } + return result +} + +func resolveServerConfig(input any) (*ldap.ServersConfig, error) { + var ldapCfg ldap.ServersConfig + + inputJson, err := json.Marshal(input) + if err != nil { + return nil, err + } + + if err = json.Unmarshal(inputJson, &ldapCfg); err != nil { + return nil, err + } + + return &ldapCfg, nil +} diff --git a/pkg/services/ldap/service/ldap.go b/pkg/services/ldap/service/ldap.go index 8378a9d5570..2125b322cb6 100644 --- a/pkg/services/ldap/service/ldap.go +++ b/pkg/services/ldap/service/ldap.go @@ -1,13 +1,19 @@ package service import ( + "context" "errors" "sync" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/ldap/multildap" "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/ssosettings" + "github.com/grafana/grafana/pkg/services/ssosettings/models" "github.com/grafana/grafana/pkg/setting" ) @@ -19,7 +25,7 @@ var ( // LDAP is the interface for the LDAP service. type LDAP interface { ReloadConfig() error - Config() *ldap.Config + Config() *ldap.ServersConfig Client() multildap.IMultiLDAP // Login authenticates the user against the LDAP server. @@ -29,48 +35,91 @@ type LDAP interface { } type LDAPImpl struct { - client multildap.IMultiLDAP - cfg *setting.Cfg - ldapCfg *ldap.Config - log log.Logger + client multildap.IMultiLDAP + cfg *ldap.Config + ldapCfg *ldap.ServersConfig + log log.Logger + features featuremgmt.FeatureToggles + ssoSettings ssosettings.Service // loadingMutex locks the reading of the config so multiple requests for reloading are sequential. loadingMutex *sync.Mutex } -func ProvideService(cfg *setting.Cfg) *LDAPImpl { +func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, ssoSettings ssosettings.Service) *LDAPImpl { s := &LDAPImpl{ - client: nil, - ldapCfg: nil, - cfg: cfg, log: log.New("ldap.service"), loadingMutex: &sync.Mutex{}, + features: features, + ssoSettings: ssoSettings, } - if !cfg.LDAPAuthEnabled { - return s - } + if s.features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) && s.features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsLDAP) { + s.ssoSettings.RegisterReloadable(social.LDAPProviderName, s) - ldapCfg, err := multildap.GetConfig(s.cfg) - if err != nil { - s.log.Error("Failed to get LDAP config", "error", err) + ldapSettings, err := s.ssoSettings.GetForProvider(context.Background(), social.LDAPProviderName) + if err != nil { + s.log.Error("Failed to retrieve LDAP settings from SSO settings service", "error", err) + return s + } + + err = s.Reload(context.Background(), *ldapSettings) + if err != nil { + s.log.Error("Failed to load LDAP settings", "error", err) + return s + } } else { - s.ldapCfg = ldapCfg - s.client = multildap.New(s.ldapCfg.Servers, s.cfg) + s.cfg = ldap.GetLDAPConfig(cfg) + if !cfg.LDAPAuthEnabled { + return s + } + + ldapCfg, err := multildap.GetConfig(s.cfg) + if err != nil { + s.log.Error("Failed to get LDAP config", "error", err) + } else { + s.ldapCfg = ldapCfg + s.client = multildap.New(s.ldapCfg.Servers, s.cfg) + } } return s } +func (s *LDAPImpl) Reload(ctx context.Context, settings models.SSOSettings) error { + cfg := &ldap.Config{} + cfg.Enabled = resolveBool(settings.Settings["enabled"], false) + cfg.SkipOrgRoleSync = resolveBool(settings.Settings["skip_org_role_sync"], false) + cfg.AllowSignUp = resolveBool(settings.Settings["allow_sign_up"], true) + + ldapCfg, err := resolveServerConfig(settings.Settings["config"]) + if err != nil { + return err + } + + s.loadingMutex.Lock() + defer s.loadingMutex.Unlock() + + s.cfg = cfg + s.ldapCfg = ldapCfg + s.client = multildap.New(s.ldapCfg.Servers, s.cfg) + + return nil +} + +func (s *LDAPImpl) Validate(ctx context.Context, settings models.SSOSettings, oldSettings models.SSOSettings, requester identity.Requester) error { + return nil +} + func (s *LDAPImpl) ReloadConfig() error { - if !s.cfg.LDAPAuthEnabled { + if !s.cfg.Enabled { return nil } s.loadingMutex.Lock() defer s.loadingMutex.Unlock() - config, err := readConfig(s.cfg.LDAPConfigFilePath) + config, err := readConfig(s.cfg.ConfigFilePath) if err != nil { return err } @@ -90,12 +139,12 @@ func (s *LDAPImpl) Client() multildap.IMultiLDAP { return s.client } -func (s *LDAPImpl) Config() *ldap.Config { +func (s *LDAPImpl) Config() *ldap.ServersConfig { return s.ldapCfg } func (s *LDAPImpl) Login(query *login.LoginUserQuery) (*login.ExternalUserInfo, error) { - if !s.cfg.LDAPAuthEnabled { + if !s.cfg.Enabled { return nil, ErrLDAPNotEnabled } @@ -108,7 +157,7 @@ func (s *LDAPImpl) Login(query *login.LoginUserQuery) (*login.ExternalUserInfo, } func (s *LDAPImpl) User(username string) (*login.ExternalUserInfo, error) { - if !s.cfg.LDAPAuthEnabled { + if !s.cfg.Enabled { return nil, ErrLDAPNotEnabled } diff --git a/pkg/services/ldap/service/ldap_test.go b/pkg/services/ldap/service/ldap_test.go new file mode 100644 index 00000000000..74cb4ab1a8d --- /dev/null +++ b/pkg/services/ldap/service/ldap_test.go @@ -0,0 +1,299 @@ +package service + +import ( + "context" + "sync" + "testing" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/ldap" + "github.com/grafana/grafana/pkg/services/ssosettings/models" + "github.com/stretchr/testify/require" +) + +const ( + validCert = `LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURYVENDQWtXZ0F3SUJBZ0lKQUxtVlZ1RFd1NE5ZTUEwR0NTcUdTSWIzRFFFQkN +3VUFNRVV4Q3pBSkJnTlYKQkFZVEFrRlZNUk13RVFZRFZRUUlEQXBUYjIxbExWTjBZWFJsTVNFd0h3WURWUVFLREJoSmJuUmxjbTVsZENCWAphV1JuYVhSekl +GQjBlU0JNZEdRd0hoY05NVFl4TWpNeE1UUXpORFEzV2hjTk5EZ3dOakkxTVRRek5EUTNXakJGCk1Rc3dDUVlEVlFRR0V3SkJWVEVUTUJFR0ExVUVDQXdLVTI +5dFpTMVRkR0YwWlRFaE1COEdBMVVFQ2d3WVNXNTAKWlhKdVpYUWdWMmxrWjJsMGN5QlFkSGtnVEhSa01JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUTh +BTUlJQgpDZ0tDQVFFQXpVQ0ZvemdOYjFoMU0wanpOUlNDamhPQm5SK3VWYlZwYVdmWFlJUitBaFdEZEVlNXJ5WStDZ2F2Ck9nOGJmTHlieXpGZGVobFlkRFJ +na2VkRUIvR2pHOGFKdzA2bDBxRjRqRE9BdzBrRXlnV0N1Mm1jSDdYT3hSdCsKWUFIM1RWSGEvSHUxVzNXanprb2JxcXFMUThna0tXV00yN2ZPZ0FaNkdpZWF +KQk42VkJTTU1jUGV5M0hXTEJtYworVFlKbXYxZGJhTzJqSGhLaDhwZkt3MFcxMlZNOFAxUElPOGd2NFBodS91dUpZaWVCV0tpeEJFeXkwbEhqeWl4CllGQ1I +xMnhkaDRDQTQ3cTk1OFpSR25uRFVHRlZFMVFoZ1JhY0pDT1o5YmQ1dDltcjhLTGFWQllUQ0pvNUVSRTgKanltYWI1ZFBxZTVxS2ZKc0NaaXFXZ2xialVvOXR +3SURBUUFCbzFBd1RqQWRCZ05WSFE0RUZnUVV4cHV3Y3MvQwpZUU95dWkrcjFHKzNLeEJOaHhrd0h3WURWUjBqQkJnd0ZvQVV4cHV3Y3MvQ1lRT3l1aStyMUc +rM0t4Qk5oeGt3CkRBWURWUjBUQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBQWlXVUtzLzJ4L3ZpTkNLaTNZNmIKbEV1Q3RBR2h6T09aOUV +qcnZKOCtDT0gzUmFnM3RWQldyY0JaMy91aGhQcTVneTlscXc0T2t2RXdzOTkvNWpGcwpYMUZKNk1LQmdxZnV5N3loNXMxWWZNMEFOSFljek1tWXBaZUFjUWY +yQ0dBYVZmd1RUZlNsek5Mc0YybFcvbHk3CnlhcEZ6bFlTSkxHb1ZFK09IRXU4ZzVTbE5BQ1VFZmtYdys1RWdoaCtLemxJTjdSNlE3cjJpeFdORkJDL2pXZjc +KTktVZkp5WDhxSUc1bWQxWVVlVDZHQlc5Qm0yLzEvUmlPMjRKVGFZbGZMZEtLOVRZYjhzRzVCK09MYWIyREltRwo5OUNKMjVSa0FjU29iV05GNXpEME82bGd +PbzNjRWRCL2tzQ3EzaG10bEMvRGxMWi9EOENKKzdWdVpuUzFyUjJuCmFRPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==` + validKey = `LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFdlFJQkFEQU5CZ2txaGtpRzl3MEJBUUVGQUFTQ0JLY3dnZ1NqQWdFQUFv +SUJBUUROUUlXak9BMXZXSFV6ClNQTTFGSUtPRTRHZEg2NVZ0V2xwWjlkZ2hINENGWU4wUjdtdkpqNEtCcTg2RHh0OHZKdkxNVjE2R1ZoME5HQ1IKNTBRSDhh +TWJ4b25EVHFYU29YaU1NNEREU1FUS0JZSzdhWndmdGM3RkczNWdBZmROVWRyOGU3VmJkYVBPU2h1cQpxb3REeUNRcFpZemJ0ODZBQm5vYUo1b2tFM3BVRkl3 +eHc5N0xjZFlzR1p6NU5nbWEvVjF0bzdhTWVFcUh5bDhyCkRSYlhaVXp3L1U4Zzd5Qy9nK0c3KzY0bGlKNEZZcUxFRVRMTFNVZVBLTEZnVUpIWGJGMkhnSURq +dXIzbnhsRWEKZWNOUVlWVVRWQ0dCRnB3a0k1bjF0M20zMmF2d290cFVGaE1JbWprUkVUeVBLWnB2bDArcDdtb3A4bXdKbUtwYQpDVnVOU2oyM0FnTUJBQUVD +Z2dFQUJuNEkvQjIweHhYY056QVNpVlpKdnVhOURkUkh0bXhUbGtMem5CajB4Mm9ZCnkxL05iczNkM29GUm41dUV1aEJaT1RjcGhzZ3dkUlNIRFhac1AzZ1VP +YmV3K2QyTi96aWVVSWo4aExEVmx2SlAKclUvczRVL2w1M1EwTGlOQnlFOVRodkwrekpMUENLSnRkNXVIWmpCNWZGbTY5K1E3Z3U4eGc0eEhJdWIrMHBQNQpQ +SGFubUhDRHJiZ05OL29xbGFyNEZaMk1YVGdla1c2QW15Yy9rb0U5aEluNEJhYTJLZS9CL0FVR1k0cE1STHFwClRBcnQrR1RWZVdlb0ZZOVFBQ1VwYUhwSmhH +Yi9QaW91NnRsVTU3ZTQyY0xva2kxZjArU0FSc0JCS3lYQTdCQjEKMWZNSDEwS1FZRkE2OGRUWVdsS3pRYXUvSzR4YXFnNEZLbXR3RjY2R1FRS0JnUUQ5T3BO +VVM3b1J4TUhWSmFCUgpUTldXK1YxRlh5Y3FvamVrRnBEaWpQYjJYNUNXVjE2b2VXZ2FYcDBuT0hGZHk5RVdzM0d0R3BmWmFzYVJWSHNYClNIdFBoNE5iOEpx +SGRHRTAvQ0Q2dDArNERuczhCbjljU3F0ZFFCN1IzSm43SU1YaTlYL1U4TERLbytBMTgvSnEKVjhWZ1VuZ01ueTlZak1rUUliSzhUUldrWVFLQmdRRFBmNG54 +TzZqdSt0T0hIT1JRdHkzYllERDArT1YzSTArTAoweXowdVByZXJ5QlZpOW5ZNDNLYWtINTJEN1VaRXd3c0JqakdYRCtXSDh4RXNtQldzR05YSnUwMjVQdnpJ +Sm96CmxBRWlYdk1wL05tWXArdFk0ckRtTzhSaHlWb2NCcVdIemgzOG0wSUZPZDRCeUZENW5MRURyQTNwRFZvMGFOZ1kKbjBHd1J5c1pGd0tCZ1FEa0NqM202 +Wk1Vc1VXRXR5K2FSMEVKaG1LeU9EQkRPblkwOUlWaEgyUy9GZXhWRnpVTgpMdGZLOTIwNmhwL0F3ZXozTG4ydVQ0WnpxcTVLN2ZNelVuaUpkQldkVkIwMDRs +OHZvZVhwSWU5T1p1d2ZjQko5CmdGaTF6eXB4L3VGRHY0MjFCelFwQk4rUWZPZEtidmJkUVZGam5xQ3hiU0RyODB5VmxHTXJJNWZid1FLQmdHMDkKb1JyZXBP +N0VJTzhHTi9HQ3J1TEsvcHRLR2t5aHkzUTZ4blZFbWRiNDdoWDduY0pBNUlvWlBtcmJsQ1ZTVU5zdwpuMTFYSGFia3NMOE9CZ2c5cnQ4b1FFVGhRdi9hRHpU +T1c5YURsSk5yYWdlamlCVHdxOTlhWWVaMWdqbzFDWnE0CjJqS3VicENmeVpDNHJHRHRySWZaWWkxcStTMlVjUWh0ZDhEZGh3UWJBb0dBQU00RXBEQTR5SEI1 +eWllazFwL28KQ2JxUkN0YS9EeDZFeW8wS2xOQXlQdUZQQXNodXBHNE5CeDdtVDJBU2ZMKzJWQkhvaTZtSFNyaStCRFg1cnlZRgpmTVl2cDdVUllvcTd3N3Fp +dlJsdnZFZzV5b1lySzEzRjIrR2o2eEo0akVOOW0wS2RNL2czbUpHcTBIQlRJUXJwClNtNzVXWHNmbE94dVRuMDhMYmdHYzRzPQotLS0tLUVORCBSU0EgUFJJ +VkFURSBLRVktLS0tLQ==` +) + +var ( + isAdmin = true +) + +func TestReload(t *testing.T) { + testCases := []struct { + description string + settings models.SSOSettings + expectedServersConfig *ldap.ServersConfig + expectedConfig *ldap.Config + }{ + { + description: "basic flow with minimal config", + settings: models.SSOSettings{ + Provider: "ldap", + Settings: map[string]any{ + "enabled": true, + "skip_org_role_sync": false, + "allow_sign_up": true, + "config": map[string]any{ + "servers": []any{ + map[string]any{ + "host": "127.0.0.1", + }, + }, + }, + }, + IsDeleted: false, + }, + expectedServersConfig: &ldap.ServersConfig{ + Servers: []*ldap.ServerConfig{ + { + Host: "127.0.0.1", + }, + }, + }, + expectedConfig: &ldap.Config{ + Enabled: true, + AllowSignUp: true, + SkipOrgRoleSync: false, + }, + }, + { + description: "complete set of server parameters", + settings: models.SSOSettings{ + Provider: "ldap", + Settings: map[string]any{ + "enabled": true, + "skip_org_role_sync": false, + "allow_sign_up": true, + "config": map[string]any{ + "servers": []any{ + map[string]any{ + "host": "127.0.0.1", + "port": 3389, + "bind_dn": "cn=ldapservice,ou=users,dc=ldap,dc=goauthentik,dc=io", + "bind_password": "grafana", + "search_filter": "(cn=%s)", + "ssl_skip_verify": false, + "use_ssl": true, + "start_tls": true, + "min_tls_version": "TLS1.3", + "timeout": 10, + "root_ca_cert": "/path/to/certificate.crt", + "root_ca_cert_value": []string{validCert}, + "client_cert": "/path/to/client.crt", + "client_cert_value": validCert, + "client_key": "/path/to/client.key", + "client_key_value": validKey, + "group_search_filter": "(&(objectClass=posixGroup)(memberUid=%s))", + "group_search_filter_user_attribute": "distinguishedName", + "group_search_base_dns": []string{"ou=groups,dc=grafana,dc=org"}, + "tls_ciphers": []string{ + "TLS_AES_256_GCM_SHA384", + }, + "attributes": map[string]string{ + "email": "mail", + "member_of": "memberOf", + "name": "displayName", + "surname": "sn", + "username": "cn", + }, + "search_base_dns": []string{ + "DC=ldap,DC=goauthentik,DC=io", + }, + "group_mappings": []any{ + map[string]any{ + "group_dn": "cn=admin,ou=groups,dc=ldap,dc=goauthentik,dc=io", + "org_id": 1, + "org_role": "Admin", + "grafana_admin": true, + }, + map[string]any{ + "group_dn": "cn=editor,ou=groups,dc=ldap,dc=goauthentik,dc=io", + "org_id": 1, + "org_role": "Editor", + }, + map[string]any{ + "group_dn": "cn=viewer,ou=groups,dc=ldap,dc=goauthentik,dc=io", + "org_id": 1, + "org_role": "Viewer", + }, + }, + }, + }, + }, + }, + IsDeleted: false, + }, + expectedServersConfig: &ldap.ServersConfig{ + Servers: []*ldap.ServerConfig{ + { + Host: "127.0.0.1", + Port: 3389, + UseSSL: true, + StartTLS: true, + SkipVerifySSL: false, + MinTLSVersion: "TLS1.3", + TLSCiphers: []string{ + "TLS_AES_256_GCM_SHA384", + }, + RootCACert: "/path/to/certificate.crt", + RootCACertValue: []string{validCert}, + ClientCert: "/path/to/client.crt", + ClientCertValue: validCert, + ClientKey: "/path/to/client.key", + ClientKeyValue: validKey, + BindDN: "cn=ldapservice,ou=users,dc=ldap,dc=goauthentik,dc=io", + BindPassword: "grafana", + Timeout: 10, + Attr: ldap.AttributeMap{ + Username: "cn", + Name: "displayName", + Surname: "sn", + Email: "mail", + MemberOf: "memberOf", + }, + SearchFilter: "(cn=%s)", + SearchBaseDNs: []string{"DC=ldap,DC=goauthentik,DC=io"}, + GroupSearchFilter: "(&(objectClass=posixGroup)(memberUid=%s))", + GroupSearchFilterUserAttribute: "distinguishedName", + GroupSearchBaseDNs: []string{"ou=groups,dc=grafana,dc=org"}, + Groups: []*ldap.GroupToOrgRole{ + { + GroupDN: "cn=admin,ou=groups,dc=ldap,dc=goauthentik,dc=io", + OrgId: 1, + OrgRole: "Admin", + IsGrafanaAdmin: &isAdmin, + }, + { + GroupDN: "cn=editor,ou=groups,dc=ldap,dc=goauthentik,dc=io", + OrgId: 1, + OrgRole: "Editor", + }, + { + GroupDN: "cn=viewer,ou=groups,dc=ldap,dc=goauthentik,dc=io", + OrgId: 1, + OrgRole: "Viewer", + }, + }, + }, + }, + }, + expectedConfig: &ldap.Config{ + Enabled: true, + AllowSignUp: true, + SkipOrgRoleSync: false, + }, + }, + { + description: "no servers config", + settings: models.SSOSettings{ + Provider: "ldap", + Settings: map[string]any{ + "enabled": true, + "skip_org_role_sync": false, + "allow_sign_up": true, + }, + IsDeleted: false, + }, + expectedServersConfig: &ldap.ServersConfig{Servers: nil}, + expectedConfig: &ldap.Config{ + Enabled: true, + AllowSignUp: true, + SkipOrgRoleSync: false, + }, + }, + { + description: "invalid settings", + settings: models.SSOSettings{ + Provider: "ldap", + Settings: map[string]any{ + "invalid": "some-value", + "another_one": true, + }, + IsDeleted: false, + }, + expectedServersConfig: &ldap.ServersConfig{Servers: nil}, + expectedConfig: &ldap.Config{ + AllowSignUp: true, + }, + }, + { + description: "config disabled", + settings: models.SSOSettings{ + Provider: "ldap", + Settings: map[string]any{ + "enabled": false, + "skip_org_role_sync": false, + "allow_sign_up": true, + "config": map[string]any{ + "servers": []any{ + map[string]any{ + "host": "127.0.0.1", + }, + }, + }, + }, + IsDeleted: false, + }, + expectedServersConfig: &ldap.ServersConfig{ + Servers: []*ldap.ServerConfig{ + { + Host: "127.0.0.1", + }, + }, + }, + expectedConfig: &ldap.Config{ + Enabled: false, + AllowSignUp: true, + SkipOrgRoleSync: false, + }, + }, + } + + for _, tt := range testCases { + t.Run(tt.description, func(t *testing.T) { + ldapImpl := &LDAPImpl{ + features: featuremgmt.WithManager(featuremgmt.FlagSsoSettingsApi), + loadingMutex: &sync.Mutex{}, + } + + err := ldapImpl.Reload(context.Background(), tt.settings) + require.NoError(t, err) + require.Equal(t, *tt.expectedServersConfig, *ldapImpl.ldapCfg) + require.Equal(t, *tt.expectedConfig, *ldapImpl.cfg) + }) + } +} diff --git a/pkg/services/ldap/settings.go b/pkg/services/ldap/settings.go index 459f6dadcf2..90f3732b235 100644 --- a/pkg/services/ldap/settings.go +++ b/pkg/services/ldap/settings.go @@ -15,15 +15,25 @@ import ( const defaultTimeout = 10 -// Config holds list of connections to LDAP +// Config holds parameters from the .ini config file type Config struct { + Enabled bool + ConfigFilePath string + AllowSignUp bool + SkipOrgRoleSync bool + SyncCron string + ActiveSyncEnabled bool +} + +// ServersConfig holds list of connections to LDAP +type ServersConfig struct { Servers []*ServerConfig `toml:"servers" json:"servers"` } // ServerConfig holds connection data to LDAP type ServerConfig struct { - Host string `toml:"host"` - Port int `toml:"port"` + Host string `toml:"host" json:"host"` + Port int `toml:"port" json:"port,omitempty"` UseSSL bool `toml:"use_ssl" json:"use_ssl,omitempty"` StartTLS bool `toml:"start_tls" json:"start_tls,omitempty"` @@ -83,16 +93,27 @@ var loadingMutex = &sync.Mutex{} // We need to define in this space so `GetConfig` fn // could be defined as singleton -var config *Config +var config *ServersConfig + +func GetLDAPConfig(cfg *setting.Cfg) *Config { + return &Config{ + Enabled: cfg.LDAPAuthEnabled, + ConfigFilePath: cfg.LDAPConfigFilePath, + AllowSignUp: cfg.LDAPAllowSignup, + SkipOrgRoleSync: cfg.LDAPSkipOrgRoleSync, + SyncCron: cfg.LDAPSyncCron, + ActiveSyncEnabled: cfg.LDAPActiveSyncEnabled, + } +} // GetConfig returns the LDAP config if LDAP is enabled otherwise it returns nil. It returns either cached value of // the config or it reads it and caches it first. -func GetConfig(cfg *setting.Cfg) (*Config, error) { +func GetConfig(cfg *Config) (*ServersConfig, error) { if cfg != nil { - if !cfg.LDAPAuthEnabled { + if !cfg.Enabled { return nil, nil } - } else if !cfg.LDAPAuthEnabled { + } else if !cfg.Enabled { return nil, nil } @@ -104,11 +125,11 @@ func GetConfig(cfg *setting.Cfg) (*Config, error) { loadingMutex.Lock() defer loadingMutex.Unlock() - return readConfig(cfg.LDAPConfigFilePath) + return readConfig(cfg.ConfigFilePath) } -func readConfig(configFile string) (*Config, error) { - result := &Config{} +func readConfig(configFile string) (*ServersConfig, error) { + result := &ServersConfig{} logger.Info("LDAP enabled, reading config file", "file", configFile) diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index cb0b656a142..25632794924 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -332,12 +332,14 @@ func setupRedisLiveEngine(g *GrafanaLive, node *centrifuge.Node) error { redisShardConfigs := []centrifuge.RedisShardConfig{ {Address: redisAddress, Password: redisPassword}, } - var redisShards []*centrifuge.RedisShard + + redisShards := make([]*centrifuge.RedisShard, 0, len(redisShardConfigs)) for _, redisConf := range redisShardConfigs { redisShard, err := centrifuge.NewRedisShard(node, redisConf) if err != nil { return fmt.Errorf("error connecting to Live Redis: %v", err) } + redisShards = append(redisShards, redisShard) } @@ -348,6 +350,7 @@ func setupRedisLiveEngine(g *GrafanaLive, node *centrifuge.Node) error { if err != nil { return fmt.Errorf("error creating Live Redis broker: %v", err) } + node.SetBroker(broker) presenceManager, err := centrifuge.NewRedisPresenceManager(node, centrifuge.RedisPresenceManagerConfig{ @@ -357,7 +360,9 @@ func setupRedisLiveEngine(g *GrafanaLive, node *centrifuge.Node) error { if err != nil { return fmt.Errorf("error creating Live Redis presence manager: %v", err) } + node.SetPresenceManager(presenceManager) + return nil } diff --git a/pkg/services/live/runstream/manager.go b/pkg/services/live/runstream/manager.go index 0d404ca1ead..e2a0033f1f8 100644 --- a/pkg/services/live/runstream/manager.go +++ b/pkg/services/live/runstream/manager.go @@ -116,17 +116,21 @@ func (s *Manager) handleDatasourceEvent(orgID int64, dsUID string, resubmit bool s.mu.RUnlock() return nil } - var resubmitRequests []streamRequest - var waitChannels []chan struct{} + + resubmitRequests := make([]streamRequest, 0, len(dsStreams)) + waitChannels := make([]chan struct{}, 0, len(dsStreams)) for channel := range dsStreams { streamCtx, ok := s.streams[channel] if !ok { continue } + streamCtx.cancelFn() + waitChannels = append(waitChannels, streamCtx.CloseCh) resubmitRequests = append(resubmitRequests, streamCtx.streamRequest) } + s.mu.RUnlock() // Wait for all streams to stop. diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go index a81542fbb18..80b45148c11 100644 --- a/pkg/services/navtree/navtreeimpl/admin.go +++ b/pkg/services/navtree/navtreeimpl/admin.go @@ -43,7 +43,7 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink } if s.features.IsEnabled(ctx, featuremgmt.FlagFeatureToggleAdminPage) && hasAccess(ac.EvalPermission(ac.ActionFeatureManagementRead)) { generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{ - Text: "Feature Toggles", + Text: "Feature toggles", SubTitle: "View and edit feature toggles", Id: "feature-toggles", Url: s.cfg.AppSubURL + "/admin/featuretoggles", diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 102e4fe953e..a9247c16877 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -358,8 +358,8 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navt dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{ Text: "Recently Deleted", SubTitle: "Any items listed here for more than 30 days will be automatically deleted.", - Id: "dashboards/recentlyDeleted", - Url: s.cfg.AppSubURL + "/dashboard/recentlyDeleted", + Id: "dashboards/recently-deleted", + Url: s.cfg.AppSubURL + "/dashboard/recently-deleted", }) } } @@ -408,6 +408,16 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Alert groups", SubTitle: "See grouped alerts from an Alertmanager instance", Id: "groups", Url: s.cfg.AppSubURL + "/alerting/groups", Icon: "layer-group"}) } + if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingCentralAlertHistory) { + alertChildNavs = append(alertChildNavs, &navtree.NavLink{ + Text: "History", + SubTitle: "History of events that were generated by your Grafana-managed alert rules. Silences and Mute timings are ignored.", + Id: "alerts-history", + Url: s.cfg.AppSubURL + "/alerting/history", + Icon: "history", + }) + } + if c.SignedInUser.GetOrgRole() == org.RoleAdmin { alertChildNavs = append(alertChildNavs, &navtree.NavLink{ Text: "Settings", Id: "alerting-admin", Url: s.cfg.AppSubURL + "/alerting/admin", diff --git a/pkg/services/ngalert/api/api_prometheus.go b/pkg/services/ngalert/api/api_prometheus.go index ac2bbad3718..d5c6343f780 100644 --- a/pkg/services/ngalert/api/api_prometheus.go +++ b/pkg/services/ngalert/api/api_prometheus.go @@ -97,8 +97,8 @@ func PrepareAlertStatuses(manager state.AlertInstanceManager, opts AlertStatuses } alertResponse.Data.Alerts = append(alertResponse.Data.Alerts, &apimodels.Alert{ - Labels: alertState.GetLabels(labelOptions...), - Annotations: alertState.Annotations, + Labels: apimodels.LabelsFromMap(alertState.GetLabels(labelOptions...)), + Annotations: apimodels.LabelsFromMap(alertState.Annotations), // TODO: or should we make this two fields? Using one field lets the // frontend use the same logic for parsing text on annotations and this. @@ -444,12 +444,12 @@ func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, groupKey ng Name: rule.Title, Query: ruleToQuery(log, rule), Duration: rule.For.Seconds(), - Annotations: rule.Annotations, + Annotations: apimodels.LabelsFromMap(rule.Annotations), } newRule := apimodels.Rule{ Name: rule.Title, - Labels: rule.GetLabels(labelOptions...), + Labels: apimodels.LabelsFromMap(rule.GetLabels(labelOptions...)), Health: "ok", Type: rule.Type().String(), LastEvaluation: time.Time{}, @@ -471,8 +471,8 @@ func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, groupKey ng totals["error"] += 1 } alert := apimodels.Alert{ - Labels: alertState.GetLabels(labelOptions...), - Annotations: alertState.Annotations, + Labels: apimodels.LabelsFromMap(alertState.GetLabels(labelOptions...)), + Annotations: apimodels.LabelsFromMap(alertState.Annotations), // TODO: or should we make this two fields? Using one field lets the // frontend use the same logic for parsing text on annotations and this. @@ -563,8 +563,8 @@ func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, groupKey ng // Returns the whole JSON model as a string if it fails to extract a minimum of 1 query. func ruleToQuery(logger log.Logger, rule *ngmodels.AlertRule) string { var queryErr error - var queries []string + queries := make([]string, 0, len(rule.Data)) for _, q := range rule.Data { q, err := q.GetQuery() if err != nil { diff --git a/pkg/services/ngalert/api/compat_contact_points.go b/pkg/services/ngalert/api/compat_contact_points.go index 26b13fbc5a2..60ff5f2902f 100644 --- a/pkg/services/ngalert/api/compat_contact_points.go +++ b/pkg/services/ngalert/api/compat_contact_points.go @@ -46,156 +46,164 @@ func ContactPointToContactPointExport(cp definitions.ContactPoint) (notify.APIRe // This is needed to keep the API models clean and convert from database model j.RegisterExtension(&contactPointsExtension{}) - var integration []*notify.GrafanaIntegrationConfig + contactPointsLength := len(cp.Alertmanager) + len(cp.Dingding) + len(cp.Discord) + len(cp.Email) + + len(cp.Googlechat) + len(cp.Kafka) + len(cp.Line) + len(cp.Opsgenie) + + len(cp.Pagerduty) + len(cp.OnCall) + len(cp.Pushover) + len(cp.Sensugo) + + len(cp.Sns) + len(cp.Slack) + len(cp.Teams) + len(cp.Telegram) + + len(cp.Threema) + len(cp.Victorops) + len(cp.Webhook) + len(cp.Wecom) + + len(cp.Webex) + + integration := make([]*notify.GrafanaIntegrationConfig, 0, contactPointsLength) var errs []error for _, i := range cp.Alertmanager { el, err := marshallIntegration(j, "prometheus-alertmanager", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Dingding { el, err := marshallIntegration(j, "dingding", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Discord { el, err := marshallIntegration(j, "discord", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Email { el, err := marshallIntegration(j, "email", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Googlechat { el, err := marshallIntegration(j, "googlechat", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Kafka { el, err := marshallIntegration(j, "kafka", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Line { el, err := marshallIntegration(j, "line", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Opsgenie { el, err := marshallIntegration(j, "opsgenie", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Pagerduty { el, err := marshallIntegration(j, "pagerduty", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.OnCall { el, err := marshallIntegration(j, "oncall", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Pushover { el, err := marshallIntegration(j, "pushover", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Sensugo { el, err := marshallIntegration(j, "sensugo", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Sns { el, err := marshallIntegration(j, "sns", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Slack { el, err := marshallIntegration(j, "slack", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Teams { el, err := marshallIntegration(j, "teams", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Telegram { el, err := marshallIntegration(j, "telegram", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Threema { el, err := marshallIntegration(j, "threema", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Victorops { el, err := marshallIntegration(j, "victorops", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Webhook { el, err := marshallIntegration(j, "webhook", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Wecom { el, err := marshallIntegration(j, "wecom", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } for _, i := range cp.Webex { el, err := marshallIntegration(j, "webex", i, i.DisableResolveMessage) - integration = append(integration, el) if err != nil { errs = append(errs, err) } + integration = append(integration, el) } + if len(errs) > 0 { return notify.APIReceiver{}, errors.Join(errs...) } diff --git a/pkg/services/ngalert/api/tooling/definitions/prom.go b/pkg/services/ngalert/api/tooling/definitions/prom.go index cb37533377c..f2ad8c4c110 100644 --- a/pkg/services/ngalert/api/tooling/definitions/prom.go +++ b/pkg/services/ngalert/api/tooling/definitions/prom.go @@ -9,6 +9,7 @@ import ( "time" v1 "github.com/prometheus/client_golang/api/prometheus/v1" + promlabels "github.com/prometheus/prometheus/model/labels" ) // swagger:route GET /prometheus/grafana/api/v1/rules prometheus RouteGetGrafanaRuleStatuses @@ -151,7 +152,7 @@ type AlertingRule struct { Query string `json:"query,omitempty"` Duration float64 `json:"duration,omitempty"` // required: true - Annotations overrideLabels `json:"annotations,omitempty"` + Annotations promlabels.Labels `json:"annotations,omitempty"` // required: true ActiveAt *time.Time `json:"activeAt,omitempty"` Alerts []Alert `json:"alerts,omitempty"` @@ -166,8 +167,8 @@ type Rule struct { // required: true Name string `json:"name"` // required: true - Query string `json:"query"` - Labels overrideLabels `json:"labels,omitempty"` + Query string `json:"query"` + Labels promlabels.Labels `json:"labels,omitempty"` // required: true Health string `json:"health"` LastError string `json:"lastError,omitempty"` @@ -181,9 +182,9 @@ type Rule struct { // swagger:model type Alert struct { // required: true - Labels overrideLabels `json:"labels"` + Labels promlabels.Labels `json:"labels"` // required: true - Annotations overrideLabels `json:"annotations"` + Annotations promlabels.Labels `json:"annotations"` // required: true State string `json:"state"` ActiveAt *time.Time `json:"activeAt"` @@ -300,31 +301,6 @@ func (by AlertsBy) TopK(alerts []Alert, k int) []Alert { // is more important than "normal". If two alerts have the same importance // then the ordering is based on their ActiveAt time and their labels. func AlertsByImportance(a1, a2 *Alert) bool { - // labelsForComparison concatenates each key/value pair into a string and - // sorts them. - labelsForComparison := func(m map[string]string) []string { - s := make([]string, 0, len(m)) - for k, v := range m { - s = append(s, k+v) - } - sort.Strings(s) - return s - } - - // compareLabels returns true if labels1 are less than labels2. This happens - // when labels1 has fewer labels than labels2, or if the next label from - // labels1 is lexicographically less than the next label from labels2. - compareLabels := func(labels1, labels2 []string) bool { - if len(labels1) == len(labels2) { - for i := range labels1 { - if labels1[i] != labels2[i] { - return labels1[i] < labels2[i] - } - } - } - return len(labels1) < len(labels2) - } - // The importance of an alert is first based on the importance of their states. // This ordering is intended to show the most important alerts first when // using pagination. @@ -345,9 +321,7 @@ func AlertsByImportance(a1, a2 *Alert) bool { return true } // Both alerts are active since the same time so compare their labels - labels1 := labelsForComparison(a1.Labels) - labels2 := labelsForComparison(a2.Labels) - return compareLabels(labels1, labels2) + return promlabels.Compare(a1.Labels, a2.Labels) < 0 } return importance1 < importance2 @@ -362,9 +336,16 @@ func (s AlertsSorter) Len() int { return len(s.alerts) } func (s AlertsSorter) Swap(i, j int) { s.alerts[i], s.alerts[j] = s.alerts[j], s.alerts[i] } func (s AlertsSorter) Less(i, j int) bool { return s.by(&s.alerts[i], &s.alerts[j]) } -// override the labels type with a map for generation. -// The custom marshaling for labels.Labels ends up doing this anyways. -type overrideLabels map[string]string +// LabelsFromMap creates Labels from a map. Note the Labels type requires the +// labels be sorted, so we make sure to do that. +func LabelsFromMap(m map[string]string) promlabels.Labels { + sb := promlabels.NewScratchBuilder(len(m)) + for k, v := range m { + sb.Add(k, v) + } + sb.Sort() + return sb.Labels() +} // swagger:parameters RouteGetGrafanaAlertStatuses type GetGrafanaAlertStatusesParams struct { diff --git a/pkg/services/ngalert/api/tooling/definitions/prom_bench_test.go b/pkg/services/ngalert/api/tooling/definitions/prom_bench_test.go index 64a051751cc..65d6fa39754 100644 --- a/pkg/services/ngalert/api/tooling/definitions/prom_bench_test.go +++ b/pkg/services/ngalert/api/tooling/definitions/prom_bench_test.go @@ -21,10 +21,11 @@ func makeAlerts(amount int) []Alert { alerts := make([]Alert, amount) for i := 0; i < len(alerts); i++ { - alerts[i].Labels = make(map[string]string) + lbls := make(map[string]string) for label := 0; label < numLabels; label++ { - alerts[i].Labels[fmt.Sprintf("label_%d", label)] = fmt.Sprintf("label_%d_value_%d", label, i%100) + lbls[fmt.Sprintf("label_%d", label)] = fmt.Sprintf("label_%d_value_%d", label, i%100) } + alerts[i].Labels = LabelsFromMap(lbls) if i%100 < percentAlerting { alerts[i].State = "alerting" diff --git a/pkg/services/ngalert/api/tooling/definitions/prom_test.go b/pkg/services/ngalert/api/tooling/definitions/prom_test.go index a2013541e2b..40c6dc66107 100644 --- a/pkg/services/ngalert/api/tooling/definitions/prom_test.go +++ b/pkg/services/ngalert/api/tooling/definitions/prom_test.go @@ -69,32 +69,42 @@ func TestSortAlertsByImportance(t *testing.T) { }, { name: "inactive alerts with same importance are ordered by labels", input: []Alert{ - {State: "normal", Labels: map[string]string{"c": "d"}}, - {State: "normal", Labels: map[string]string{"a": "b"}}, + {State: "normal", Labels: LabelsFromMap(map[string]string{"c": "d"})}, + {State: "normal", Labels: LabelsFromMap(map[string]string{"a": "b"})}, }, expected: []Alert{ - {State: "normal", Labels: map[string]string{"a": "b"}}, - {State: "normal", Labels: map[string]string{"c": "d"}}, + {State: "normal", Labels: LabelsFromMap(map[string]string{"a": "b"})}, + {State: "normal", Labels: LabelsFromMap(map[string]string{"c": "d"})}, }, }, { - name: "active alerts with same importance and active time are ordered fewest labels first", + name: "active alerts with same importance and active time are ordered by label names", input: []Alert{ - {State: "alerting", ActiveAt: &tm1, Labels: map[string]string{"a": "b", "c": "d"}}, - {State: "alerting", ActiveAt: &tm1, Labels: map[string]string{"e": "f"}}, + {State: "alerting", ActiveAt: &tm1, Labels: LabelsFromMap(map[string]string{"c": "d", "e": "f"})}, + {State: "alerting", ActiveAt: &tm1, Labels: LabelsFromMap(map[string]string{"a": "b"})}, }, expected: []Alert{ - {State: "alerting", ActiveAt: &tm1, Labels: map[string]string{"e": "f"}}, - {State: "alerting", ActiveAt: &tm1, Labels: map[string]string{"a": "b", "c": "d"}}, + {State: "alerting", ActiveAt: &tm1, Labels: LabelsFromMap(map[string]string{"a": "b"})}, + {State: "alerting", ActiveAt: &tm1, Labels: LabelsFromMap(map[string]string{"c": "d", "e": "f"})}, }, }, { name: "active alerts with same importance and active time are ordered by labels", input: []Alert{ - {State: "alerting", ActiveAt: &tm1, Labels: map[string]string{"c": "d"}}, - {State: "alerting", ActiveAt: &tm1, Labels: map[string]string{"a": "b"}}, + {State: "alerting", ActiveAt: &tm1, Labels: LabelsFromMap(map[string]string{"c": "d"})}, + {State: "alerting", ActiveAt: &tm1, Labels: LabelsFromMap(map[string]string{"a": "b"})}, }, expected: []Alert{ - {State: "alerting", ActiveAt: &tm1, Labels: map[string]string{"a": "b"}}, - {State: "alerting", ActiveAt: &tm1, Labels: map[string]string{"c": "d"}}, + {State: "alerting", ActiveAt: &tm1, Labels: LabelsFromMap(map[string]string{"a": "b"})}, + {State: "alerting", ActiveAt: &tm1, Labels: LabelsFromMap(map[string]string{"c": "d"})}, + }, + }, { + name: "active alerts with same importance and active time are ordered by label values", + input: []Alert{ + {State: "alerting", ActiveAt: &tm1, Labels: LabelsFromMap(map[string]string{"x": "b"})}, + {State: "alerting", ActiveAt: &tm1, Labels: LabelsFromMap(map[string]string{"x": "a"})}, + }, + expected: []Alert{ + {State: "alerting", ActiveAt: &tm1, Labels: LabelsFromMap(map[string]string{"x": "a"})}, + {State: "alerting", ActiveAt: &tm1, Labels: LabelsFromMap(map[string]string{"x": "b"})}, }, }} diff --git a/pkg/services/ngalert/client/client.go b/pkg/services/ngalert/client/client.go index 90b080c0bc6..b6c99d32be4 100644 --- a/pkg/services/ngalert/client/client.go +++ b/pkg/services/ngalert/client/client.go @@ -7,6 +7,11 @@ import ( "strconv" "github.com/grafana/dskit/instrument" + "github.com/grafana/grafana/pkg/infra/tracing" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" + "go.opentelemetry.io/otel/trace" ) // Requester executes an HTTP request. @@ -14,7 +19,7 @@ type Requester interface { Do(req *http.Request) (*http.Response, error) } -// TimedClient instruments a request. It implements Requester. +// TimedClient instruments a request with metrics. It implements Requester. type TimedClient struct { client Requester collector instrument.Collector @@ -70,3 +75,51 @@ func TimeRequest(ctx context.Context, operation string, coll instrument.Collecto coll, toStatusCode, doRequest) return response, err } + +// TracedClient instruments a request with tracing. It implements Requester. +type TracedClient struct { + client Requester + tracer tracing.Tracer + name string +} + +func NewTracedClient(client Requester, tracer tracing.Tracer, name string) *TracedClient { + return &TracedClient{ + client: client, + tracer: tracer, + name: name, + } +} + +// Do executes the request. +func (c TracedClient) Do(r *http.Request) (*http.Response, error) { + ctx, span := c.tracer.Start(r.Context(), c.name, trace.WithSpanKind(trace.SpanKindClient)) + defer span.End() + + span.SetAttributes(semconv.HTTPURL(r.URL.String())) + span.SetAttributes(semconv.HTTPMethod(r.Method)) + + c.tracer.Inject(ctx, r.Header, span) + + r = r.WithContext(ctx) + resp, err := c.client.Do(r) + if err != nil { + span.SetStatus(codes.Error, "request failed") + span.RecordError(err) + } else { + if resp.ContentLength > 0 { + span.SetAttributes(attribute.Int64("http.content_length", resp.ContentLength)) + } + span.SetAttributes(semconv.HTTPStatusCode(resp.StatusCode)) + if resp.StatusCode >= 400 && resp.StatusCode < 600 { + span.RecordError(fmt.Errorf("error with HTTP status code %d", resp.StatusCode)) + } + } + + return resp, err +} + +// RoundTrip implements the RoundTripper interface. +func (c TracedClient) RoundTrip(r *http.Request) (*http.Response, error) { + return c.Do(r) +} diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 5f87cafc251..ec86f1ed5f0 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -200,7 +200,7 @@ func (ng *AlertNG) init() error { PromoteConfig: true, SyncInterval: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.SyncInterval, } - remoteAM, err := createRemoteAlertmanager(cfg, ng.KVStore, ng.SecretsService.Decrypt, autogenFn, m) + remoteAM, err := createRemoteAlertmanager(cfg, ng.KVStore, ng.SecretsService.Decrypt, autogenFn, m, ng.tracer) if err != nil { moaLogger.Error("Failed to create remote Alertmanager", "err", err) return nil, err @@ -234,7 +234,7 @@ func (ng *AlertNG) init() error { TenantID: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.TenantID, URL: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.URL, } - remoteAM, err := createRemoteAlertmanager(cfg, ng.KVStore, ng.SecretsService.Decrypt, autogenFn, m) + remoteAM, err := createRemoteAlertmanager(cfg, ng.KVStore, ng.SecretsService.Decrypt, autogenFn, m, ng.tracer) if err != nil { moaLogger.Error("Failed to create remote Alertmanager, falling back to using only the internal one", "err", err) return internalAM, nil @@ -270,7 +270,7 @@ func (ng *AlertNG) init() error { URL: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.URL, SyncInterval: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.SyncInterval, } - remoteAM, err := createRemoteAlertmanager(cfg, ng.KVStore, ng.SecretsService.Decrypt, autogenFn, m) + remoteAM, err := createRemoteAlertmanager(cfg, ng.KVStore, ng.SecretsService.Decrypt, autogenFn, m, ng.tracer) if err != nil { moaLogger.Error("Failed to create remote Alertmanager, falling back to using only the internal one", "err", err) return internalAM, nil @@ -359,7 +359,7 @@ func (ng *AlertNG) init() error { // There are a set of feature toggles available that act as short-circuits for common configurations. // If any are set, override the config accordingly. ApplyStateHistoryFeatureToggles(&ng.Cfg.UnifiedAlerting.StateHistory, ng.FeatureToggles, ng.Log) - history, err := configureHistorianBackend(initCtx, ng.Cfg.UnifiedAlerting.StateHistory, ng.annotationsRepo, ng.dashboardService, ng.store, ng.Metrics.GetHistorianMetrics(), ng.Log) + history, err := configureHistorianBackend(initCtx, ng.Cfg.UnifiedAlerting.StateHistory, ng.annotationsRepo, ng.dashboardService, ng.store, ng.Metrics.GetHistorianMetrics(), ng.Log, ng.tracer) if err != nil { return err } @@ -523,7 +523,7 @@ type Historian interface { state.Historian } -func configureHistorianBackend(ctx context.Context, cfg setting.UnifiedAlertingStateHistorySettings, ar annotations.Repository, ds dashboards.DashboardService, rs historian.RuleStore, met *metrics.Historian, l log.Logger) (Historian, error) { +func configureHistorianBackend(ctx context.Context, cfg setting.UnifiedAlertingStateHistorySettings, ar annotations.Repository, ds dashboards.DashboardService, rs historian.RuleStore, met *metrics.Historian, l log.Logger, tracer tracing.Tracer) (Historian, error) { if !cfg.Enabled { met.Info.WithLabelValues("noop").Set(0) return historian.NewNopHistorian(), nil @@ -538,7 +538,7 @@ func configureHistorianBackend(ctx context.Context, cfg setting.UnifiedAlertingS if backend == historian.BackendTypeMultiple { primaryCfg := cfg primaryCfg.Backend = cfg.MultiPrimary - primary, err := configureHistorianBackend(ctx, primaryCfg, ar, ds, rs, met, l) + primary, err := configureHistorianBackend(ctx, primaryCfg, ar, ds, rs, met, l, tracer) if err != nil { return nil, fmt.Errorf("multi-backend target \"%s\" was misconfigured: %w", cfg.MultiPrimary, err) } @@ -547,7 +547,7 @@ func configureHistorianBackend(ctx context.Context, cfg setting.UnifiedAlertingS for _, b := range cfg.MultiSecondaries { secCfg := cfg secCfg.Backend = b - sec, err := configureHistorianBackend(ctx, secCfg, ar, ds, rs, met, l) + sec, err := configureHistorianBackend(ctx, secCfg, ar, ds, rs, met, l, tracer) if err != nil { return nil, fmt.Errorf("multi-backend target \"%s\" was miconfigured: %w", b, err) } @@ -569,7 +569,7 @@ func configureHistorianBackend(ctx context.Context, cfg setting.UnifiedAlertingS } req := historian.NewRequester() lokiBackendLogger := log.New("ngalert.state.historian", "backend", "loki") - backend := historian.NewRemoteLokiBackend(lokiBackendLogger, lcfg, req, met) + backend := historian.NewRemoteLokiBackend(lokiBackendLogger, lcfg, req, met, tracer) testConnCtx, cancelFunc := context.WithTimeout(ctx, 10*time.Second) defer cancelFunc() @@ -627,8 +627,8 @@ func ApplyStateHistoryFeatureToggles(cfg *setting.UnifiedAlertingStateHistorySet } } -func createRemoteAlertmanager(cfg remote.AlertmanagerConfig, kvstore kvstore.KVStore, decryptFn remote.DecryptFn, autogenFn remote.AutogenFn, m *metrics.RemoteAlertmanager) (*remote.Alertmanager, error) { - return remote.NewAlertmanager(cfg, notifier.NewFileStore(cfg.OrgID, kvstore), decryptFn, autogenFn, m) +func createRemoteAlertmanager(cfg remote.AlertmanagerConfig, kvstore kvstore.KVStore, decryptFn remote.DecryptFn, autogenFn remote.AutogenFn, m *metrics.RemoteAlertmanager, tracer tracing.Tracer) (*remote.Alertmanager, error) { + return remote.NewAlertmanager(cfg, notifier.NewFileStore(cfg.OrgID, kvstore), decryptFn, autogenFn, m, tracer) } func createRecordingWriter(featureToggles featuremgmt.FeatureToggles, settings setting.RecordingRuleSettings) (schedule.RecordingWriter, error) { diff --git a/pkg/services/ngalert/ngalert_test.go b/pkg/services/ngalert/ngalert_test.go index ed7bee52d7c..cb9b9863349 100644 --- a/pkg/services/ngalert/ngalert_test.go +++ b/pkg/services/ngalert/ngalert_test.go @@ -62,12 +62,13 @@ func TestConfigureHistorianBackend(t *testing.T) { t.Run("fail initialization if invalid backend", func(t *testing.T) { met := metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem) logger := log.NewNopLogger() + tracer := tracing.InitializeTracerForTest() cfg := setting.UnifiedAlertingStateHistorySettings{ Enabled: true, Backend: "invalid-backend", } - _, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger) + _, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer) require.ErrorContains(t, err, "unrecognized") }) @@ -75,13 +76,14 @@ func TestConfigureHistorianBackend(t *testing.T) { t.Run("fail initialization if invalid multi-backend primary", func(t *testing.T) { met := metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem) logger := log.NewNopLogger() + tracer := tracing.InitializeTracerForTest() cfg := setting.UnifiedAlertingStateHistorySettings{ Enabled: true, Backend: "multiple", MultiPrimary: "invalid-backend", } - _, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger) + _, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer) require.ErrorContains(t, err, "multi-backend target") require.ErrorContains(t, err, "unrecognized") @@ -90,6 +92,7 @@ func TestConfigureHistorianBackend(t *testing.T) { t.Run("fail initialization if invalid multi-backend secondary", func(t *testing.T) { met := metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem) logger := log.NewNopLogger() + tracer := tracing.InitializeTracerForTest() cfg := setting.UnifiedAlertingStateHistorySettings{ Enabled: true, Backend: "multiple", @@ -97,7 +100,7 @@ func TestConfigureHistorianBackend(t *testing.T) { MultiSecondaries: []string{"annotations", "invalid-backend"}, } - _, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger) + _, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer) require.ErrorContains(t, err, "multi-backend target") require.ErrorContains(t, err, "unrecognized") @@ -106,6 +109,7 @@ func TestConfigureHistorianBackend(t *testing.T) { t.Run("do not fail initialization if pinging Loki fails", func(t *testing.T) { met := metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem) logger := log.NewNopLogger() + tracer := tracing.InitializeTracerForTest() cfg := setting.UnifiedAlertingStateHistorySettings{ Enabled: true, Backend: "loki", @@ -114,7 +118,7 @@ func TestConfigureHistorianBackend(t *testing.T) { LokiWriteURL: "http://gone.invalid", } - h, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger) + h, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer) require.NotNil(t, h) require.NoError(t, err) @@ -124,12 +128,13 @@ func TestConfigureHistorianBackend(t *testing.T) { reg := prometheus.NewRegistry() met := metrics.NewHistorianMetrics(reg, metrics.Subsystem) logger := log.NewNopLogger() + tracer := tracing.InitializeTracerForTest() cfg := setting.UnifiedAlertingStateHistorySettings{ Enabled: true, Backend: "annotations", } - h, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger) + h, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer) require.NotNil(t, h) require.NoError(t, err) @@ -146,11 +151,12 @@ grafana_alerting_state_history_info{backend="annotations"} 1 reg := prometheus.NewRegistry() met := metrics.NewHistorianMetrics(reg, metrics.Subsystem) logger := log.NewNopLogger() + tracer := tracing.InitializeTracerForTest() cfg := setting.UnifiedAlertingStateHistorySettings{ Enabled: false, } - h, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger) + h, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer) require.NotNil(t, h) require.NoError(t, err) diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go b/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go index 35810085b11..81573c42061 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -66,7 +67,7 @@ func TestMultiorgAlertmanager_RemoteSecondaryMode(t *testing.T) { DefaultConfig: setting.GetAlertmanagerDefaultConfiguration(), } m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) - remoteAM, err := remote.NewAlertmanager(externalAMCfg, notifier.NewFileStore(orgID, kvStore), secretsService.Decrypt, remote.NoopAutogenFn, m) + remoteAM, err := remote.NewAlertmanager(externalAMCfg, notifier.NewFileStore(orgID, kvStore), secretsService.Decrypt, remote.NoopAutogenFn, m, tracing.InitializeTracerForTest()) require.NoError(t, err) // Use both Alertmanager implementations in the forked Alertmanager. diff --git a/pkg/services/ngalert/provisioning/contactpoints.go b/pkg/services/ngalert/provisioning/contactpoints.go index caf5cefc297..1a57046e526 100644 --- a/pkg/services/ngalert/provisioning/contactpoints.go +++ b/pkg/services/ngalert/provisioning/contactpoints.go @@ -88,13 +88,14 @@ func (ecp *ContactPointService) GetContactPoints(ctx context.Context, q ContactP } } - var contactPoints []apimodels.EmbeddedContactPoint - for _, gr := range grafanaReceivers { + contactPoints := make([]apimodels.EmbeddedContactPoint, len(grafanaReceivers)) + for i, gr := range grafanaReceivers { contactPoint, err := GettableGrafanaReceiverToEmbeddedContactPoint(gr) if err != nil { return nil, err } - contactPoints = append(contactPoints, contactPoint) + + contactPoints[i] = contactPoint } sort.SliceStable(contactPoints, func(i, j int) bool { diff --git a/pkg/services/ngalert/provisioning/templates.go b/pkg/services/ngalert/provisioning/templates.go index 2ed37ae3723..074934af988 100644 --- a/pkg/services/ngalert/provisioning/templates.go +++ b/pkg/services/ngalert/provisioning/templates.go @@ -31,17 +31,19 @@ func (t *TemplateService) GetTemplates(ctx context.Context, orgID int64) ([]defi return nil, err } - var templates []definitions.NotificationTemplate + templates := make([]definitions.NotificationTemplate, 0, len(revision.cfg.TemplateFiles)) for name, tmpl := range revision.cfg.TemplateFiles { tmpl := definitions.NotificationTemplate{ Name: name, Template: tmpl, } + provenance, err := t.provenanceStore.GetProvenance(ctx, &tmpl, orgID) if err != nil { return nil, err } tmpl.Provenance = definitions.Provenance(provenance) + templates = append(templates, tmpl) } diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 1c9c8afff27..4feca91ed4b 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -25,6 +25,7 @@ import ( "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -100,7 +101,7 @@ func (cfg *AlertmanagerConfig) Validate() error { return nil } -func NewAlertmanager(cfg AlertmanagerConfig, store stateStore, decryptFn DecryptFn, autogenFn AutogenFn, metrics *metrics.RemoteAlertmanager) (*Alertmanager, error) { +func NewAlertmanager(cfg AlertmanagerConfig, store stateStore, decryptFn DecryptFn, autogenFn AutogenFn, metrics *metrics.RemoteAlertmanager, tracer tracing.Tracer) (*Alertmanager, error) { if err := cfg.Validate(); err != nil { return nil, err } @@ -118,7 +119,7 @@ func NewAlertmanager(cfg AlertmanagerConfig, store stateStore, decryptFn Decrypt URL: u, PromoteConfig: cfg.PromoteConfig, } - mc, err := remoteClient.New(mcCfg, metrics) + mc, err := remoteClient.New(mcCfg, metrics, tracer) if err != nil { return nil, err } @@ -129,7 +130,7 @@ func NewAlertmanager(cfg AlertmanagerConfig, store stateStore, decryptFn Decrypt Password: cfg.BasicAuthPassword, Logger: logger, } - amc, err := remoteClient.NewAlertmanager(amcCfg, metrics) + amc, err := remoteClient.NewAlertmanager(amcCfg, metrics, tracer) if err != nil { return nil, err } @@ -492,13 +493,14 @@ func (am *Alertmanager) GetReceivers(ctx context.Context) ([]apimodels.Receiver, return []apimodels.Receiver{}, err } - var rcvs []apimodels.Receiver - for _, rcv := range res.Payload { - rcvs = append(rcvs, apimodels.Receiver{ + rcvs := make([]apimodels.Receiver, len(res.Payload)) + for i, rcv := range res.Payload { + rcvs[i] = apimodels.Receiver{ Name: *rcv.Name, Integrations: []apimodels.Integration{}, - }) + } } + return rcvs, nil } diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index dffb38e4644..5016ab49e38 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -23,6 +23,7 @@ import ( "github.com/grafana/alerting/definition" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -105,7 +106,7 @@ func TestNewAlertmanager(t *testing.T) { DefaultConfig: defaultGrafanaConfig, } m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) - am, err := NewAlertmanager(cfg, nil, secretsService.Decrypt, NoopAutogenFn, m) + am, err := NewAlertmanager(cfg, nil, secretsService.Decrypt, NoopAutogenFn, m, tracing.InitializeTracerForTest()) if test.expErr != "" { require.EqualError(tt, err, test.expErr) return @@ -177,7 +178,7 @@ func TestApplyConfig(t *testing.T) { // An error response from the remote Alertmanager should result in the readiness check failing. m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) - am, err := NewAlertmanager(cfg, fstore, secretsService.Decrypt, NoopAutogenFn, m) + am, err := NewAlertmanager(cfg, fstore, secretsService.Decrypt, NoopAutogenFn, m, tracing.InitializeTracerForTest()) require.NoError(t, err) config := &ngmodels.AlertConfiguration{ @@ -305,6 +306,7 @@ func TestCompareAndSendConfiguration(t *testing.T) { decryptFn, test.autogenFn, m, + tracing.InitializeTracerForTest(), ) require.NoError(t, err) @@ -364,7 +366,7 @@ func TestIntegrationRemoteAlertmanagerConfiguration(t *testing.T) { secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(db.InitTestDB(t))) m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) - am, err := NewAlertmanager(cfg, fstore, secretsService.Decrypt, NoopAutogenFn, m) + am, err := NewAlertmanager(cfg, fstore, secretsService.Decrypt, NoopAutogenFn, m, tracing.InitializeTracerForTest()) require.NoError(t, err) encodedFullState, err := am.getFullState(ctx) @@ -521,7 +523,7 @@ func TestIntegrationRemoteAlertmanagerGetStatus(t *testing.T) { secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) - am, err := NewAlertmanager(cfg, nil, secretsService.Decrypt, NoopAutogenFn, m) + am, err := NewAlertmanager(cfg, nil, secretsService.Decrypt, NoopAutogenFn, m, tracing.InitializeTracerForTest()) require.NoError(t, err) // We should get the default Cloud Alertmanager configuration. @@ -555,7 +557,7 @@ func TestIntegrationRemoteAlertmanagerSilences(t *testing.T) { secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) - am, err := NewAlertmanager(cfg, nil, secretsService.Decrypt, NoopAutogenFn, m) + am, err := NewAlertmanager(cfg, nil, secretsService.Decrypt, NoopAutogenFn, m, tracing.InitializeTracerForTest()) require.NoError(t, err) // We should have no silences at first. @@ -640,7 +642,7 @@ func TestIntegrationRemoteAlertmanagerAlerts(t *testing.T) { secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) - am, err := NewAlertmanager(cfg, nil, secretsService.Decrypt, NoopAutogenFn, m) + am, err := NewAlertmanager(cfg, nil, secretsService.Decrypt, NoopAutogenFn, m, tracing.InitializeTracerForTest()) require.NoError(t, err) // Wait until the Alertmanager is ready to send alerts. @@ -709,7 +711,7 @@ func TestIntegrationRemoteAlertmanagerReceivers(t *testing.T) { secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) - am, err := NewAlertmanager(cfg, nil, secretsService.Decrypt, NoopAutogenFn, m) + am, err := NewAlertmanager(cfg, nil, secretsService.Decrypt, NoopAutogenFn, m, tracing.InitializeTracerForTest()) require.NoError(t, err) // We should start with the default config. diff --git a/pkg/services/ngalert/remote/client/alertmanager.go b/pkg/services/ngalert/remote/client/alertmanager.go index 88a7ca7638b..52ab72b2723 100644 --- a/pkg/services/ngalert/remote/client/alertmanager.go +++ b/pkg/services/ngalert/remote/client/alertmanager.go @@ -9,6 +9,7 @@ import ( httptransport "github.com/go-openapi/runtime/client" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/ngalert/client" "github.com/grafana/grafana/pkg/services/ngalert/metrics" amclient "github.com/prometheus/alertmanager/api/v2/client" @@ -31,7 +32,7 @@ type Alertmanager struct { logger log.Logger } -func NewAlertmanager(cfg *AlertmanagerConfig, metrics *metrics.RemoteAlertmanager) (*Alertmanager, error) { +func NewAlertmanager(cfg *AlertmanagerConfig, metrics *metrics.RemoteAlertmanager, tracer tracing.Tracer) (*Alertmanager, error) { // First, add the authentication middleware. c := &http.Client{Transport: &MimirAuthRoundTripper{ TenantID: cfg.TenantID, @@ -40,14 +41,15 @@ func NewAlertmanager(cfg *AlertmanagerConfig, metrics *metrics.RemoteAlertmanage }} tc := client.NewTimedClient(c, metrics.RequestLatency) + trc := client.NewTracedClient(tc, tracer, "remote.alertmanager.client") apiEndpoint := *cfg.URL // Next, make sure you set the right path. u := apiEndpoint.JoinPath(alertmanagerAPIMountPath, amclient.DefaultBasePath) - // Create an Alertmanager client using the timed client as the transport. + // Create an Alertmanager client using the instrumented client as the transport. r := httptransport.New(u.Host, u.Path, []string{u.Scheme}) - r.Transport = tc + r.Transport = trc return &Alertmanager{ logger: cfg.Logger, diff --git a/pkg/services/ngalert/remote/client/mimir.go b/pkg/services/ngalert/remote/client/mimir.go index b99864af510..cf0a0941652 100644 --- a/pkg/services/ngalert/remote/client/mimir.go +++ b/pkg/services/ngalert/remote/client/mimir.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/client" "github.com/grafana/grafana/pkg/services/ngalert/metrics" @@ -68,7 +69,7 @@ func (e *errorResponse) Error() string { return e.Error2 } -func New(cfg *Config, metrics *metrics.RemoteAlertmanager) (*Mimir, error) { +func New(cfg *Config, metrics *metrics.RemoteAlertmanager, tracer tracing.Tracer) (*Mimir, error) { rt := &MimirAuthRoundTripper{ TenantID: cfg.TenantID, Password: cfg.Password, @@ -78,10 +79,12 @@ func New(cfg *Config, metrics *metrics.RemoteAlertmanager) (*Mimir, error) { c := &http.Client{ Transport: rt, } + tc := client.NewTimedClient(c, metrics.RequestLatency) + trc := client.NewTracedClient(tc, tracer, "remote.alertmanager.client") return &Mimir{ endpoint: cfg.URL, - client: client.NewTimedClient(c, metrics.RequestLatency), + client: trc, logger: cfg.Logger, metrics: metrics, promoteConfig: cfg.PromoteConfig, diff --git a/pkg/services/ngalert/sender/router.go b/pkg/services/ngalert/sender/router.go index 1fe82927bd6..cfb474f63be 100644 --- a/pkg/services/ngalert/sender/router.go +++ b/pkg/services/ngalert/sender/router.go @@ -205,15 +205,17 @@ func (d *AlertsRouter) SyncAndApplyConfigFromDatabase(ctx context.Context) error } func buildRedactedAMs(l log.Logger, alertmanagers []ExternalAMcfg, ordId int64) []string { - var redactedAMs []string + redactedAMs := make([]string, 0, len(alertmanagers)) for _, am := range alertmanagers { parsedAM, err := url.Parse(am.URL) if err != nil { l.Error("Failed to parse alertmanager string", "org", ordId, "error", err) continue } + redactedAMs = append(redactedAMs, parsedAM.Redacted()) } + return redactedAMs } @@ -225,9 +227,6 @@ func asSHA256(strings []string) string { } func (d *AlertsRouter) alertmanagersFromDatasources(orgID int64) ([]ExternalAMcfg, error) { - var ( - alertmanagers []ExternalAMcfg - ) // We might have alertmanager datasources that are acting as external // alertmanager, let's fetch them. query := &datasources.GetDataSourcesByTypeQuery{ @@ -240,6 +239,9 @@ func (d *AlertsRouter) alertmanagersFromDatasources(orgID int64) ([]ExternalAMcf if err != nil { return nil, fmt.Errorf("failed to fetch datasources for org: %w", err) } + + alertmanagers := make([]ExternalAMcfg, 0, len(dataSources)) + for _, ds := range dataSources { if !ds.JsonData.Get(definitions.HandleGrafanaManagedAlerts).MustBool(false) { continue @@ -262,11 +264,13 @@ func (d *AlertsRouter) alertmanagersFromDatasources(orgID int64) ([]ExternalAMcf "error", err) continue } + alertmanagers = append(alertmanagers, ExternalAMcfg{ URL: amURL, Headers: headers, }) } + return alertmanagers, nil } diff --git a/pkg/services/ngalert/sender/router_test.go b/pkg/services/ngalert/sender/router_test.go index 303fa6065d8..c10f88e6001 100644 --- a/pkg/services/ngalert/sender/router_test.go +++ b/pkg/services/ngalert/sender/router_test.go @@ -712,7 +712,7 @@ func TestAlertManagers_buildRedactedAMs(t *testing.T) { amUrls: []string{"1234://user:password@localhost:9094"}, errCalls: 1, errLog: "Failed to parse alertmanager string", - expected: nil, + expected: []string{}, }, } @@ -724,6 +724,7 @@ func TestAlertManagers_buildRedactedAMs(t *testing.T) { URL: url, }) } + require.Equal(t, tt.expected, buildRedactedAMs(&fakeLogger, cfgs, tt.orgId)) require.Equal(t, tt.errCalls, fakeLogger.ErrorLogs.Calls) require.Equal(t, tt.errLog, fakeLogger.ErrorLogs.Message) diff --git a/pkg/services/ngalert/state/compat.go b/pkg/services/ngalert/state/compat.go index a17b6637a02..673e9e56810 100644 --- a/pkg/services/ngalert/state/compat.go +++ b/pkg/services/ngalert/state/compat.go @@ -141,9 +141,9 @@ func errorAlert(labels, annotations data.Labels, alertState *State, urlStr strin func FromStateTransitionToPostableAlerts(firingStates []StateTransition, stateManager *Manager, appURL *url.URL) apimodels.PostableAlerts { alerts := apimodels.PostableAlerts{PostableAlerts: make([]models.PostableAlert, 0, len(firingStates))} - var sentAlerts []*State ts := time.Now() + sentAlerts := make([]*State, 0, len(firingStates)) for _, alertState := range firingStates { if !alertState.NeedsSending(stateManager.ResendDelay) { continue diff --git a/pkg/services/ngalert/state/historian/loki.go b/pkg/services/ngalert/state/historian/loki.go index 72add63518d..fd163a3a806 100644 --- a/pkg/services/ngalert/state/historian/loki.go +++ b/pkg/services/ngalert/state/historian/loki.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/ngalert/client" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" @@ -55,9 +56,9 @@ type RemoteLokiBackend struct { log log.Logger } -func NewRemoteLokiBackend(logger log.Logger, cfg LokiConfig, req client.Requester, metrics *metrics.Historian) *RemoteLokiBackend { +func NewRemoteLokiBackend(logger log.Logger, cfg LokiConfig, req client.Requester, metrics *metrics.Historian, tracer tracing.Tracer) *RemoteLokiBackend { return &RemoteLokiBackend{ - client: NewLokiClient(cfg, req, metrics, logger), + client: NewLokiClient(cfg, req, metrics, logger, tracer), externalLabels: cfg.ExternalLabels, clock: clock.New(), metrics: metrics, diff --git a/pkg/services/ngalert/state/historian/loki_http.go b/pkg/services/ngalert/state/historian/loki_http.go index 44a7c0044eb..2d9ca11ba13 100644 --- a/pkg/services/ngalert/state/historian/loki_http.go +++ b/pkg/services/ngalert/state/historian/loki_http.go @@ -12,6 +12,7 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/ngalert/client" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/setting" @@ -103,10 +104,11 @@ const ( NeqRegEx Operator = "!~" ) -func NewLokiClient(cfg LokiConfig, req client.Requester, metrics *metrics.Historian, logger log.Logger) *HttpLokiClient { +func NewLokiClient(cfg LokiConfig, req client.Requester, metrics *metrics.Historian, logger log.Logger, tracer tracing.Tracer) *HttpLokiClient { tc := client.NewTimedClient(req, metrics.WriteDuration) + trc := client.NewTracedClient(tc, tracer, "ngalert.historian.client") return &HttpLokiClient{ - client: tc, + client: trc, encoder: cfg.Encoder, cfg: cfg, metrics: metrics, diff --git a/pkg/services/ngalert/state/historian/loki_http_test.go b/pkg/services/ngalert/state/historian/loki_http_test.go index 961f3238097..0c81eca1ee4 100644 --- a/pkg/services/ngalert/state/historian/loki_http_test.go +++ b/pkg/services/ngalert/state/historian/loki_http_test.go @@ -18,6 +18,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" ) func TestLokiConfig(t *testing.T) { @@ -227,7 +228,7 @@ func TestLokiHTTPClient_Manual(t *testing.T) { ReadPathURL: url, WritePathURL: url, Encoder: JsonEncoder{}, - }, NewRequester(), metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem), log.NewNopLogger()) + }, NewRequester(), metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem), log.NewNopLogger(), tracing.InitializeTracerForTest()) // Unauthorized request should fail against Grafana Cloud. err = client.Ping(context.Background()) @@ -255,7 +256,7 @@ func TestLokiHTTPClient_Manual(t *testing.T) { BasicAuthUser: "", BasicAuthPassword: "", Encoder: JsonEncoder{}, - }, NewRequester(), metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem), log.NewNopLogger()) + }, NewRequester(), metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem), log.NewNopLogger(), tracing.InitializeTracerForTest()) // When running on prem, you might need to set the tenant id, // so the x-scope-orgid header is set. @@ -389,7 +390,7 @@ func createTestLokiClient(req client.Requester) *HttpLokiClient { Encoder: JsonEncoder{}, } met := metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem) - return NewLokiClient(cfg, req, met, log.NewNopLogger()) + return NewLokiClient(cfg, req, met, log.NewNopLogger(), tracing.InitializeTracerForTest()) } func reqBody(t *testing.T, req *http.Request) string { diff --git a/pkg/services/ngalert/state/historian/loki_test.go b/pkg/services/ngalert/state/historian/loki_test.go index dbdefd0c51c..dad1e0503a0 100644 --- a/pkg/services/ngalert/state/historian/loki_test.go +++ b/pkg/services/ngalert/state/historian/loki_test.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/ngalert/client" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" @@ -514,7 +515,7 @@ func createTestLokiBackend(req client.Requester, met *metrics.Historian) *Remote ExternalLabels: map[string]string{"externalLabelKey": "externalLabelValue"}, } lokiBackendLogger := log.New("ngalert.state.historian", "backend", "loki") - return NewRemoteLokiBackend(lokiBackendLogger, cfg, req, met) + return NewRemoteLokiBackend(lokiBackendLogger, cfg, req, met, tracing.InitializeTracerForTest()) } func singleFromNormal(st *state.State) []state.StateTransition { diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 266efc6eb48..d86a149c6e5 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -787,7 +787,8 @@ func (st DBstore) RenameReceiverInNotificationSettings(ctx context.Context, orgI if len(rules) == 0 { return 0, nil } - var updates []ngmodels.UpdateRule + + updates := make([]ngmodels.UpdateRule, 0, len(rules)) for _, rule := range rules { r := ngmodels.CopyRule(rule) for idx := range r.NotificationSettings { @@ -795,6 +796,7 @@ func (st DBstore) RenameReceiverInNotificationSettings(ctx context.Context, orgI r.NotificationSettings[idx].Receiver = newReceiver } } + updates = append(updates, ngmodels.UpdateRule{ Existing: rule, New: *r, diff --git a/pkg/services/plugindashboards/service/dashboard_updater_test.go b/pkg/services/plugindashboards/service/dashboard_updater_test.go index 16170c7ae26..6a8010c39f4 100644 --- a/pkg/services/plugindashboards/service/dashboard_updater_test.go +++ b/pkg/services/plugindashboards/service/dashboard_updater_test.go @@ -401,15 +401,15 @@ type pluginsSettingsServiceMock struct { func (s *pluginsSettingsServiceMock) GetPluginSettings(_ context.Context, args *pluginsettings.GetArgs) ([]*pluginsettings.InfoDTO, error) { s.getPluginSettingsArgs = append(s.getPluginSettingsArgs, args.OrgID) - var res []*pluginsettings.InfoDTO - for _, ps := range s.storedPluginSettings { - res = append(res, &pluginsettings.InfoDTO{ + res := make([]*pluginsettings.InfoDTO, len(s.storedPluginSettings)) + for i, ps := range s.storedPluginSettings { + res[i] = &pluginsettings.InfoDTO{ PluginID: ps.PluginID, OrgID: ps.OrgID, Enabled: ps.Enabled, Pinned: ps.Pinned, PluginVersion: ps.PluginVersion, - }) + } } return res, s.err diff --git a/pkg/services/pluginsintegration/pluginconfig/envvars.go b/pkg/services/pluginsintegration/pluginconfig/envvars.go index fe3579661e1..e29a75f6ec7 100644 --- a/pkg/services/pluginsintegration/pluginconfig/envvars.go +++ b/pkg/services/pluginsintegration/pluginconfig/envvars.go @@ -163,17 +163,23 @@ func (p *EnvVarsProvider) tracingEnvVars(plugin *plugins.Plugin) []string { func (p *EnvVarsProvider) pluginSettingsEnvVars(pluginID string) []string { const customConfigPrefix = "GF_PLUGIN" - var env []string - for k, v := range p.cfg.PluginSettings[pluginID] { + + pluginSettings := p.cfg.PluginSettings[pluginID] + + env := make([]string, 0, len(pluginSettings)) + for k, v := range pluginSettings { if k == "path" || strings.ToLower(k) == "id" { continue } + key := fmt.Sprintf("%s_%s", customConfigPrefix, strings.ToUpper(k)) if value := os.Getenv(key); value != "" { v = value } + env = append(env, fmt.Sprintf("%s=%s", key, v)) } + return env } diff --git a/pkg/services/publicdashboards/service/common_test.go b/pkg/services/publicdashboards/service/common_test.go index 1124b687288..959ed23849b 100644 --- a/pkg/services/publicdashboards/service/common_test.go +++ b/pkg/services/publicdashboards/service/common_test.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/annotationsimpl" "github.com/grafana/grafana/pkg/services/dashboards" @@ -29,7 +30,7 @@ func newPublicDashboardServiceImpl( db, cfg := db.InitTestDBWithCfg(t) tagService := tagimpl.ProvideService(db) if annotationsRepo == nil { - annotationsRepo = annotationsimpl.ProvideService(db, cfg, featuremgmt.WithFeatures(), tagService) + annotationsRepo = annotationsimpl.ProvideService(db, cfg, featuremgmt.WithFeatures(), tagService, tracing.InitializeTracerForTest()) } if publicDashboardStore == nil { diff --git a/pkg/services/search/model/model.go b/pkg/services/search/model/model.go index 5e3658c35a1..6b95d2a9448 100644 --- a/pkg/services/search/model/model.go +++ b/pkg/services/search/model/model.go @@ -2,6 +2,7 @@ package model import ( "strings" + "time" ) // FilterWhere limits the set of dashboard IDs to the dashboards for @@ -62,22 +63,23 @@ const ( ) type Hit struct { - ID int64 `json:"id"` - UID string `json:"uid"` - Title string `json:"title"` - URI string `json:"uri"` - URL string `json:"url"` - Slug string `json:"slug"` - Type HitType `json:"type"` - Tags []string `json:"tags"` - IsStarred bool `json:"isStarred"` - FolderID int64 `json:"folderId,omitempty"` // Deprecated: use FolderUID instead - FolderUID string `json:"folderUid,omitempty"` - FolderTitle string `json:"folderTitle,omitempty"` - FolderURL string `json:"folderUrl,omitempty"` - SortMeta int64 `json:"sortMeta"` - SortMetaName string `json:"sortMetaName,omitempty"` - RemainingTrashAtAge string `json:"remainingTrashAtAge,omitempty"` + ID int64 `json:"id"` + UID string `json:"uid"` + Title string `json:"title"` + URI string `json:"uri"` + URL string `json:"url"` + Slug string `json:"slug"` + Type HitType `json:"type"` + Tags []string `json:"tags"` + IsStarred bool `json:"isStarred"` + FolderID int64 `json:"folderId,omitempty"` // Deprecated: use FolderUID instead + FolderUID string `json:"folderUid,omitempty"` + FolderTitle string `json:"folderTitle,omitempty"` + FolderURL string `json:"folderUrl,omitempty"` + SortMeta int64 `json:"sortMeta"` + SortMetaName string `json:"sortMetaName,omitempty"` + IsDeleted bool `json:"isDeleted"` + PermanentlyDeleteDate *time.Time `json:"permanentlyDeleteDate,omitempty"` } type HitList []*Hit diff --git a/pkg/services/searchV2/bluge.go b/pkg/services/searchV2/bluge.go index dd290a2ed8a..d1fdeac5424 100644 --- a/pkg/services/searchV2/bluge.go +++ b/pkg/services/searchV2/bluge.go @@ -191,7 +191,9 @@ func getNonFolderDashboardDoc(dash dashboard, location string) *bluge.Document { func getDashboardPanelDocs(dash dashboard, location string) []*bluge.Document { dashURL := fmt.Sprintf("/d/%s/%s", dash.uid, slugify.Slugify(dash.summary.Name)) - var docs []*bluge.Document + // pre-allocating a little bit more than necessary, possibly + docs := make([]*bluge.Document, 0, len(dash.summary.Nested)) + for _, panel := range dash.summary.Nested { if panel.Fields["type"] == "row" { continue // skip rows @@ -239,7 +241,7 @@ func getDashboardPanelDocs(dash dashboard, location string) []*bluge.Document { } // Names need to be indexed a few ways to support key features -func newSearchDocument(uid string, name string, descr string, url string) *bluge.Document { +func newSearchDocument(uid, name, descr, url string) *bluge.Document { doc := bluge.NewDocument(uid) if name != "" { diff --git a/pkg/services/searchV2/ngram_test.go b/pkg/services/searchV2/ngram_test.go index be2d6646e9e..160a956db57 100644 --- a/pkg/services/searchV2/ngram_test.go +++ b/pkg/services/searchV2/ngram_test.go @@ -51,9 +51,11 @@ func Test_punctuationCharFilter_Filter(t1 *testing.T) { func TestNgramIndexAnalyzer(t *testing.T) { stream := ngramIndexAnalyzer.Analyze([]byte("x-rays.and.xRays, and НемногоКириллицы")) expectedTerms := []string{"x", "r", "ra", "ray", "rays", "a", "an", "and", "x", "r", "ra", "ray", "rays", "a", "an", "and", "н", "не", "нем", "немн", "немно", "немног", "немного", "к", "ки", "кир", "кири", "кирил", "кирилл", "кирилли"} - var actualTerms []string + + actualTerms := make([]string, 0, len(stream)) for _, t := range stream { actualTerms = append(actualTerms, string(t.Term)) } + require.Equal(t, expectedTerms, actualTerms) } diff --git a/pkg/services/sqlstore/database_config.go b/pkg/services/sqlstore/database_config.go index 171ff4972b9..8ffc69fcf66 100644 --- a/pkg/services/sqlstore/database_config.go +++ b/pkg/services/sqlstore/database_config.go @@ -65,9 +65,11 @@ func NewDatabaseConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles) (* return dbCfg, nil } -func (dbCfg *DatabaseConfig) readConfig(cfg *setting.Cfg) error { - sec := cfg.Raw.Section("database") - +// readConfigSection reads the database configuration from the given block of +// the configuration file. This method allows us to add a "database_replica" +// section to the configuration file while using the same cfg struct. +func (dbCfg *DatabaseConfig) readConfigSection(cfg *setting.Cfg, section string) error { + sec := cfg.Raw.Section(section) cfgURL := sec.Key("url").String() if len(cfgURL) != 0 { dbURL, err := url.Parse(cfgURL) @@ -101,7 +103,6 @@ func (dbCfg *DatabaseConfig) readConfig(cfg *setting.Cfg) error { dbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0) dbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(2) dbCfg.ConnMaxLifetime = sec.Key("conn_max_lifetime").MustInt(14400) - dbCfg.SslMode = sec.Key("ssl_mode").String() dbCfg.SSLSNI = sec.Key("ssl_sni").String() dbCfg.CaCertPath = sec.Key("ca_cert_path").String() @@ -110,21 +111,22 @@ func (dbCfg *DatabaseConfig) readConfig(cfg *setting.Cfg) error { dbCfg.ServerCertName = sec.Key("server_cert_name").String() dbCfg.Path = sec.Key("path").MustString("data/grafana.db") dbCfg.IsolationLevel = sec.Key("isolation_level").String() - dbCfg.CacheMode = sec.Key("cache_mode").MustString("private") dbCfg.WALEnabled = sec.Key("wal").MustBool(false) dbCfg.SkipMigrations = sec.Key("skip_migrations").MustBool() dbCfg.MigrationLock = sec.Key("migration_locking").MustBool(true) dbCfg.MigrationLockAttemptTimeout = sec.Key("locking_attempt_timeout_sec").MustInt() - dbCfg.QueryRetries = sec.Key("query_retries").MustInt() dbCfg.TransactionRetries = sec.Key("transaction_retries").MustInt(5) - dbCfg.LogQueries = sec.Key("log_queries").MustBool(false) - return nil } +// readConfig is a wrapper around readConfigSection that read the "database" configuration block. +func (dbCfg *DatabaseConfig) readConfig(cfg *setting.Cfg) error { + return dbCfg.readConfigSection(cfg, "database") +} + func (dbCfg *DatabaseConfig) buildConnectionString(cfg *setting.Cfg, features featuremgmt.FeatureToggles) error { if dbCfg.ConnectionString != "" { return nil diff --git a/pkg/services/sqlstore/migrations/external_alertmanagers.go b/pkg/services/sqlstore/migrations/external_alertmanagers.go index 067feee3a17..5f92210fa7f 100644 --- a/pkg/services/sqlstore/migrations/external_alertmanagers.go +++ b/pkg/services/sqlstore/migrations/external_alertmanagers.go @@ -97,9 +97,8 @@ func (e externalAlertmanagerToDatasources) Exec(sess *xorm.Session, mg *migrator } func removeDuplicates(strs []string) []string { - var res []string - found := map[string]bool{} - + found := make(map[string]bool, len(strs)) + res := make([]string, 0, len(strs)) for _, str := range strs { if found[str] { continue diff --git a/pkg/services/sqlstore/permissions/dashboard.go b/pkg/services/sqlstore/permissions/dashboard.go index 1dc5fe990ff..0b782348351 100644 --- a/pkg/services/sqlstore/permissions/dashboard.go +++ b/pkg/services/sqlstore/permissions/dashboard.go @@ -435,8 +435,10 @@ func (f *accessControlDashboardPermissionFilter) nestedFoldersSelectors(permSele } func getAllowedUIDs(action string, user identity.Requester, scopePrefix string) []any { - var args []any - for _, uidScope := range user.GetPermissions()[action] { + uidScopes := user.GetPermissions()[action] + + args := make([]any, 0, len(uidScopes)) + for _, uidScope := range uidScopes { if !strings.HasPrefix(uidScope, scopePrefix) { continue } diff --git a/pkg/services/sqlstore/replstore.go b/pkg/services/sqlstore/replstore.go new file mode 100644 index 00000000000..c3540d79c4f --- /dev/null +++ b/pkg/services/sqlstore/replstore.go @@ -0,0 +1,194 @@ +package sqlstore + +import ( + "errors" + "time" + + "github.com/dlmiddlecote/sqlstats" + "github.com/prometheus/client_golang/prometheus" + "xorm.io/xorm" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/sqlstore/migrations" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" + "github.com/grafana/grafana/pkg/setting" +) + +// ReplStore is a wrapper around a main SQLStore and a read-only SQLStore. The +// main SQLStore is anonymous, so the ReplStore may be used directly as a +// SQLStore. +type ReplStore struct { + *SQLStore + repl *SQLStore +} + +// DB returns the main SQLStore. +func (rs ReplStore) DB() *SQLStore { + return rs.SQLStore +} + +// ReadReplica returns the read-only SQLStore. If no read replica is configured, +// it returns the main SQLStore. +func (rs ReplStore) ReadReplica() *SQLStore { + if rs.repl == nil { + rs.log.Debug("ReadReplica not configured, using main SQLStore") + return rs.SQLStore + } + rs.log.Debug("Using ReadReplica") + return rs.repl +} + +// ProvideServiceWithReadReplica creates a new *SQLStore connection intended for +// use as a ReadReplica of the main SQLStore. The primary SQLStore must already +// be initialized. +func ProvideServiceWithReadReplica(primary *SQLStore, cfg *setting.Cfg, + features featuremgmt.FeatureToggles, migrations registry.DatabaseMigrator, + bus bus.Bus, tracer tracing.Tracer) (*ReplStore, error) { + // start with the initialized SQLStore + replStore := &ReplStore{primary, nil} + + // FeatureToggle fallback: If the FlagDatabaseReadReplica feature flag is not enabled, return a single SQLStore. + if !features.IsEnabledGlobally(featuremgmt.FlagDatabaseReadReplica) { + primary.log.Debug("ReadReplica feature flag not enabled, using main SQLStore") + return replStore, nil + } + + // This change will make xorm use an empty default schema for postgres and + // by that mimic the functionality of how it was functioning before + // xorm's changes above. + xorm.DefaultPostgresSchema = "" + s, err := newReadOnlySQLStore(cfg, features, bus, tracer) + if err != nil { + return nil, err + } + s.features = features + s.tracer = tracer + + // initialize and register metrics wrapper around the *sql.DB + db := s.engine.DB().DB + + // register the go_sql_stats_connections_* metrics + if err := prometheus.Register(sqlstats.NewStatsCollector("grafana_repl", db)); err != nil { + s.log.Warn("Failed to register sqlstore stats collector", "error", err) + } + + replStore.repl = s + return replStore, nil +} + +// newReadOnlySQLStore creates a new *SQLStore intended for use with a +// fully-populated read replica of the main Grafana Database. It provides no +// write capabilities and does not run migrations, but other tracing and logging +// features are enabled. +func newReadOnlySQLStore(cfg *setting.Cfg, features featuremgmt.FeatureToggles, bus bus.Bus, tracer tracing.Tracer) (*SQLStore, error) { + s := &SQLStore{ + cfg: cfg, + log: log.New("replstore"), + bus: bus, + tracer: tracer, + } + + s.features = features + s.tracer = tracer + + err := s.initReadOnlyEngine(s.engine) + if err != nil { + return nil, err + } + s.dialect = migrator.NewDialect(s.engine.DriverName()) + return s, nil +} + +// initReadOnlyEngine initializes ss.engine for read-only operations. The database must be a fully-populated read replica. +func (ss *SQLStore) initReadOnlyEngine(engine *xorm.Engine) error { + if ss.engine != nil { + ss.log.Debug("Already connected to database replica") + return nil + } + + dbCfg, err := NewRODatabaseConfig(ss.cfg, ss.features) + if err != nil { + return err + } + ss.dbCfg = dbCfg + + if ss.cfg.DatabaseInstrumentQueries { + ss.dbCfg.Type = WrapDatabaseDriverWithHooks(ss.dbCfg.Type, ss.tracer) + } + + if engine == nil { + var err error + engine, err = xorm.NewEngine(ss.dbCfg.Type, ss.dbCfg.ConnectionString) + if err != nil { + ss.log.Error("failed to connect to database replica", "error", err) + return err + } + // Only for MySQL or MariaDB, verify we can connect with the current connection string's system var for transaction isolation. + // If not, create a new engine with a compatible connection string. + if ss.dbCfg.Type == migrator.MySQL { + engine, err = ss.ensureTransactionIsolationCompatibility(engine, ss.dbCfg.ConnectionString) + if err != nil { + return err + } + } + } + + engine.SetMaxOpenConns(ss.dbCfg.MaxOpenConn) + engine.SetMaxIdleConns(ss.dbCfg.MaxIdleConn) + engine.SetConnMaxLifetime(time.Second * time.Duration(ss.dbCfg.ConnMaxLifetime)) + + // configure sql logging + debugSQL := ss.cfg.Raw.Section("database_replica").Key("log_queries").MustBool(false) + if !debugSQL { + engine.SetLogger(&xorm.DiscardLogger{}) + } else { + // add stack to database calls to be able to see what repository initiated queries. Top 7 items from the stack as they are likely in the xorm library. + engine.SetLogger(NewXormLogger(log.LvlInfo, log.WithSuffix(log.New("replsstore.xorm"), log.CallerContextKey, log.StackCaller(log.DefaultCallerDepth)))) + engine.ShowSQL(true) + engine.ShowExecTime(true) + } + + ss.engine = engine + return nil +} + +// NewRODatabaseConfig creates a new read-only database configuration. +func NewRODatabaseConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles) (*DatabaseConfig, error) { + if cfg == nil { + return nil, errors.New("cfg cannot be nil") + } + + dbCfg := &DatabaseConfig{} + if err := dbCfg.readConfigSection(cfg, "database_replica"); err != nil { + return nil, err + } + + if err := dbCfg.buildConnectionString(cfg, features); err != nil { + return nil, err + } + + return dbCfg, nil +} + +// ProvideServiceWithReadReplicaForTests wraps the SQLStore in a ReplStore, with the main sqlstore as both the primary and read replica. +// TODO: eventually this should be replaced with a more robust test setup which in +func ProvideServiceWithReadReplicaForTests(testDB *SQLStore, t sqlutil.ITestDB, cfg *setting.Cfg, features featuremgmt.FeatureToggles, migrations registry.DatabaseMigrator) (*ReplStore, error) { + return &ReplStore{testDB, testDB}, nil +} + +// InitTestReplDB initializes a test DB and returns it wrapped in a ReplStore with the main SQLStore as both the primary and read replica. +func InitTestReplDB(t sqlutil.ITestDB, opts ...InitTestDBOpt) (*ReplStore, *setting.Cfg) { + t.Helper() + features := getFeaturesForTesting(opts...) + cfg := getCfgForTesting(opts...) + ss, err := initTestDB(t, cfg, features, migrations.ProvideOSSMigrations(features), opts...) + if err != nil { + t.Fatalf("failed to initialize sql repl store: %s", err) + } + return &ReplStore{ss, ss}, cfg +} diff --git a/pkg/services/ssosettings/api/api.go b/pkg/services/ssosettings/api/api.go index c5d2540ebb1..e535699e07f 100644 --- a/pkg/services/ssosettings/api/api.go +++ b/pkg/services/ssosettings/api/api.go @@ -101,7 +101,7 @@ func (api *Api) getAuthorizedList(ctx context.Context, identity identity.Request return nil, err } - var authorizedProviders []*models.SSOSettings + authorizedProviders := make([]*models.SSOSettings, 0, len(allProviders)) for _, provider := range allProviders { ev := ac.EvalPermission(ac.ActionSettingsRead, ac.Scope("settings", "auth."+provider.Provider, "*")) hasAccess, err := api.AccessControl.Evaluate(ctx, identity, ev) diff --git a/pkg/services/ssosettings/ssosettingsimpl/service.go b/pkg/services/ssosettings/ssosettingsimpl/service.go index 0de3874e1fb..ad80c0dbfbb 100644 --- a/pkg/services/ssosettings/ssosettingsimpl/service.go +++ b/pkg/services/ssosettings/ssosettingsimpl/service.go @@ -18,7 +18,6 @@ import ( "github.com/grafana/grafana/pkg/login/social" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/ldap/service" "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/ssosettings" @@ -48,10 +47,10 @@ type Service struct { func ProvideService(cfg *setting.Cfg, sqlStore db.DB, ac ac.AccessControl, routeRegister routing.RouteRegister, features featuremgmt.FeatureToggles, secrets secrets.Service, usageStats usagestats.Service, registerer prometheus.Registerer, - settingsProvider setting.Provider, licensing licensing.Licensing, ldap service.LDAP) *Service { + settingsProvider setting.Provider, licensing licensing.Licensing) *Service { fbStrategies := []ssosettings.FallbackStrategy{ strategies.NewOAuthStrategy(cfg), - strategies.NewLDAPStrategy(cfg, ldap), + strategies.NewLDAPStrategy(cfg), } configurableProviders := make(map[string]bool) @@ -60,6 +59,11 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, ac ac.AccessControl, } providersList := ssosettings.AllOAuthProviders + + if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsLDAP) { + providersList = append(providersList, social.LDAPProviderName) + } + if licensing.FeatureEnabled(social.SAMLProviderName) { fbStrategies = append(fbStrategies, strategies.NewSAMLStrategy(settingsProvider)) diff --git a/pkg/services/ssosettings/ssosettingsimpl/service_test.go b/pkg/services/ssosettings/ssosettingsimpl/service_test.go index 4b2997c2ae1..094105c00b4 100644 --- a/pkg/services/ssosettings/ssosettingsimpl/service_test.go +++ b/pkg/services/ssosettings/ssosettingsimpl/service_test.go @@ -22,7 +22,6 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/ldap/service" "github.com/grafana/grafana/pkg/services/licensing/licensingtest" secretsFakes "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/services/ssosettings" @@ -1648,7 +1647,6 @@ func setupTestEnv(t *testing.T, isLicensingEnabled, keepFallbackStratergies, sam prometheus.NewRegistry(), &setting.OSSImpl{Cfg: cfg}, licensing, - service.ProvideService(cfg), ) // overriding values for exposed fields diff --git a/pkg/services/ssosettings/strategies/ldap_strategy.go b/pkg/services/ssosettings/strategies/ldap_strategy.go index afbde023a13..00447121682 100644 --- a/pkg/services/ssosettings/strategies/ldap_strategy.go +++ b/pkg/services/ssosettings/strategies/ldap_strategy.go @@ -6,22 +6,20 @@ import ( "encoding/json" "github.com/grafana/grafana/pkg/login/social" - "github.com/grafana/grafana/pkg/services/ldap/service" + "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/ssosettings" "github.com/grafana/grafana/pkg/setting" ) type LDAPStrategy struct { - cfg *setting.Cfg - ldap service.LDAP + cfg *setting.Cfg } var _ ssosettings.FallbackStrategy = (*LDAPStrategy)(nil) -func NewLDAPStrategy(cfg *setting.Cfg, ldap service.LDAP) *LDAPStrategy { +func NewLDAPStrategy(cfg *setting.Cfg) *LDAPStrategy { return &LDAPStrategy{ - cfg: cfg, - ldap: ldap, + cfg: cfg, } } @@ -52,8 +50,13 @@ func (s *LDAPStrategy) GetProviderConfig(_ context.Context, _ string) (map[strin func (s *LDAPStrategy) getLDAPConfig() (map[string]any, error) { var configMap map[string]any - config := s.ldap.Config() - configJson, err := json.Marshal(config) + config := ldap.GetLDAPConfig(s.cfg) + ldapConfig, err := ldap.GetConfig(config) + if err != nil { + return nil, err + } + + configJson, err := json.Marshal(ldapConfig) if err != nil { return nil, err } diff --git a/pkg/services/ssosettings/strategies/ldap_strategy_test.go b/pkg/services/ssosettings/strategies/ldap_strategy_test.go index 726411b44a0..ae4bb82405a 100644 --- a/pkg/services/ssosettings/strategies/ldap_strategy_test.go +++ b/pkg/services/ssosettings/strategies/ldap_strategy_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/ini.v1" - "github.com/grafana/grafana/pkg/services/ldap/service" "github.com/grafana/grafana/pkg/setting" ) @@ -29,8 +28,8 @@ var ( "config": map[string]interface{}{ "servers": []interface{}{ map[string]interface{}{ - "Host": "127.0.0.1", - "Port": int64(3389), + "host": "127.0.0.1", + "port": int64(3389), "attributes": map[string]interface{}{ "email": "mail", "member_of": "memberOf", @@ -76,9 +75,7 @@ func TestGetLDAPConfig(t *testing.T) { cfg, err := setting.NewCfgFromINIFile(iniFile) require.NoError(t, err) - ldap := service.ProvideService(cfg) - - strategy := NewLDAPStrategy(cfg, ldap) + strategy := NewLDAPStrategy(cfg) result, err := strategy.GetProviderConfig(context.Background(), "ldap") require.NoError(t, err) diff --git a/pkg/services/stats/statsimpl/stats.go b/pkg/services/stats/statsimpl/stats.go index e2139c80b57..7f580471212 100644 --- a/pkg/services/stats/statsimpl/stats.go +++ b/pkg/services/stats/statsimpl/stats.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/setting" @@ -17,12 +18,12 @@ import ( const activeUserTimeLimit = time.Hour * 24 * 30 const dailyActiveUserTimeLimit = time.Hour * 24 -func ProvideService(cfg *setting.Cfg, db db.DB) stats.Service { +func ProvideService(cfg *setting.Cfg, db *sqlstore.ReplStore) stats.Service { return &sqlStatsService{cfg: cfg, db: db} } type sqlStatsService struct { - db db.DB + db *sqlstore.ReplStore cfg *setting.Cfg } @@ -62,8 +63,8 @@ func notServiceAccount(dialect migrator.Dialect) string { } func (ss *sqlStatsService) GetSystemStats(ctx context.Context, query *stats.GetSystemStatsQuery) (result *stats.SystemStats, err error) { - dialect := ss.db.GetDialect() - err = ss.db.WithDbSession(ctx, func(dbSession *db.Session) error { + dialect := ss.db.ReadReplica().GetDialect() + err = ss.db.ReadReplica().WithDbSession(ctx, func(dbSession *db.Session) error { sb := &db.SQLBuilder{} sb.Write("SELECT ") sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("user") + ` WHERE ` + notServiceAccount(dialect) + `) AS users,`) @@ -148,8 +149,8 @@ func (ss *sqlStatsService) roleCounterSQL(ctx context.Context) string { } func (ss *sqlStatsService) GetAdminStats(ctx context.Context, query *stats.GetAdminStatsQuery) (result *stats.AdminStats, err error) { - err = ss.db.WithDbSession(ctx, func(dbSession *db.Session) error { - dialect := ss.db.GetDialect() + err = ss.db.ReadReplica().WithDbSession(ctx, func(dbSession *db.Session) error { + dialect := ss.db.ReadReplica().GetDialect() now := time.Now() activeEndDate := now.Add(-activeUserTimeLimit) dailyActiveEndDate := now.Add(-dailyActiveUserTimeLimit) diff --git a/pkg/services/stats/statsimpl/stats_test.go b/pkg/services/stats/statsimpl/stats_test.go index e63d51fddc0..15ec1c50334 100644 --- a/pkg/services/stats/statsimpl/stats_test.go +++ b/pkg/services/stats/statsimpl/stats_test.go @@ -32,9 +32,9 @@ func TestIntegrationStatsDataAccess(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - db, cfg := db.InitTestDBWithCfg(t) - statsService := &sqlStatsService{db: db} - populateDB(t, db, cfg) + store, cfg := db.InitTestReplDBWithCfg(t) + statsService := &sqlStatsService{db: store} + populateDB(t, store, cfg) t.Run("Get system stats should not results in error", func(t *testing.T) { query := stats.GetSystemStatsQuery{} @@ -49,7 +49,7 @@ func TestIntegrationStatsDataAccess(t *testing.T) { assert.Equal(t, int64(0), result.APIKeys) assert.Equal(t, int64(2), result.Correlations) assert.NotNil(t, result.DatabaseCreatedTime) - assert.Equal(t, db.GetDialect().DriverName(), result.DatabaseDriver) + assert.Equal(t, store.GetDialect().DriverName(), result.DatabaseDriver) }) t.Run("Get system user count stats should not results in error", func(t *testing.T) { @@ -157,8 +157,8 @@ func TestIntegration_GetAdminStats(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - db, cfg := db.InitTestDBWithCfg(t) - statsService := ProvideService(cfg, db) + store, cfg := db.InitTestReplDBWithCfg(t) + statsService := ProvideService(cfg, store) query := stats.GetAdminStatsQuery{} _, err := statsService.GetAdminStats(context.Background(), &query) diff --git a/pkg/services/store/entity/sqlstash/broadcaster.go b/pkg/services/store/entity/sqlstash/broadcaster.go index 5c805eb6473..56a010bf2a0 100644 --- a/pkg/services/store/entity/sqlstash/broadcaster.go +++ b/pkg/services/store/entity/sqlstash/broadcaster.go @@ -3,18 +3,97 @@ package sqlstash import ( "context" "fmt" + "io" ) -type ConnectFunc[T any] func(chan T) error +// Please, when reviewing or working on this file have the following cheat-sheet +// in mind: +// 1. A channel type in Go has one of three directions: send-only (chan<- T), +// receive-only (<-chan T) or bidirctional (chan T). Each of them are a +// different type. A bidirectional type can be converted to any of the other +// two types and is automatic, any other conversion attempt results in a +// panic. +// 2. There are three operations you can do on a channel: send, receive and +// close. Availability of operation for each channel direction: +// | Channel direction +// Operation | Receive-only | Send-only | Bidirectional +// ----------+--------------+------------+-------------- +// Receive | Yes | No (panic) | Yes +// Send | No (panic) | Yes | Yes +// Close | No (panic) | Yes | Yes +// 3. A channel of any type also has one of three states: nil (zero value), +// closed, or open (technically called "non-nil, not-closed channel", +// created with the `make` builtin). Nil and closed channels are also +// useful, but you have to know and care for how you use them. Outcome of +// each operation on a channel depending on its state, assuming the +// operation is available to the channel given its direction: +// | Channel state +// Operation | Nil | Closed | Open +// ----------+---------------+---------------+------------------ +// Receive | Block forever | Block forever | Receive/Block until receive +// Send | Block forever | Panic | Send/Block until send +// Close | Panic | Panic | Close the channel +// 4. A `select` statement has zero or more `case` branches, each one of them +// containing either a send or a receive channel operation. A `select` with +// no branches blocks forever. At most one branch will be executed, which +// means it behaves similar to a `switch`. If more than one branch can be +// executed then one of them is picked AT RANDOM (i.e. not the one first in +// the list). A `select` statement can also have a (single and optional) +// `default` branch that is executed if all the other branches are +// operations that are blocked at the time the `select` statement is +// reached. This means that having a `default` branch causes the `select` +// statement to never block. +// 5. A receive operation on a closed channel never blocks (as said before), +// but it will always yield a zero value. As it is also valid to send a zero +// value to the channel, you can receive from channels in two forms: +// v := <-c // get a zero value if closed +// v2, ok := <-c // `ok` is set to false iif the channel is closed +// 6. The `make` builtin is used to create open channels (and is the only way +// to get them). It has an optional second parameter to specify the amount +// of items that can buffered. After that, a send operation will block +// waiting for another goroutine to receive from it (which would make room +// for the new item). When the second argument is not passed to `make`, then +// all operations are fully synchronized, meaning that a send will block +// until a receive in another goroutine is performed, and vice versa. Less +// interestingly, `make` can also create send-only or receive-only channel. +// +// The sources are the Go Specs, Effective Go and Go 101, which are already +// linked in the contributing guide for the backend or elsewhere in Grafana, but +// this file exploits so many of these subtleties that it's worth keeping a +// refresher about them at all times. The above is unlikely to change in the +// foreseeable future, so it's zero maintenance as well. We exclude patterns for +// using channels and other concurrency patterns since that's a way longer +// topic for a refresher. + +// ConnectFunc is used to initialize the watch implementation. It should do very +// basic work and checks and it has the chance to return an error. After that, +// it should fork to a different goroutine with the provided channel and send to +// it all the new events from the backing database. It is also responsible for +// closing the provided channel under all circumstances, included returning an +// error. The caller of this function will only receive from this channel (i.e. +// it is guaranteed to never send to it or close it), hence providing a safe +// separation of concerns and preventing panics. +// +// FIXME: this signature suffers from inversion of control. It would also be +// much simpler if NewBroadcaster receives a context.Context and a <-chan T +// instead. That would also reduce the scope of the broadcaster to only +// broadcast to subscribers what it receives on the provided <-chan T. The +// context.Context is still needed to provide additional values in case we want +// to add observability into the broadcaster, which we want. The broadcaster +// should still terminate on either the context being done or the provided +// channel being closed. +type ConnectFunc[T any] func(chan<- T) error type Broadcaster[T any] interface { Subscribe(context.Context) (<-chan T, error) - Unsubscribe(chan T) + Unsubscribe(<-chan T) } func NewBroadcaster[T any](ctx context.Context, connect ConnectFunc[T]) (Broadcaster[T], error) { - b := &broadcaster[T]{} - err := b.start(ctx, connect) + b := &broadcaster[T]{ + started: make(chan struct{}), + } + err := b.init(ctx, connect) if err != nil { return nil, err } @@ -23,101 +102,132 @@ func NewBroadcaster[T any](ctx context.Context, connect ConnectFunc[T]) (Broadca } type broadcaster[T any] struct { - running bool // FIXME: race condition between `Subscribe`/`Unsubscribe` and `start` - ctx context.Context - subs map[chan T]struct{} + // lifecycle management + + started, terminated chan struct{} + shouldTerminate <-chan struct{} + + // subscription management + cache Cache[T] subscribe chan chan T - unsubscribe chan chan T + unsubscribe chan (<-chan T) + subs map[<-chan T]chan T } func (b *broadcaster[T]) Subscribe(ctx context.Context) (<-chan T, error) { - if !b.running { - return nil, fmt.Errorf("broadcaster not running") + select { + case <-ctx.Done(): // client canceled + return nil, ctx.Err() + case <-b.started: // wait for broadcaster to start } + // create the subscription sub := make(chan T, 100) - b.subscribe <- sub - go func() { - <-ctx.Done() - b.unsubscribe <- sub - }() - return sub, nil -} - -func (b *broadcaster[T]) Unsubscribe(sub chan T) { - b.unsubscribe <- sub -} - -func (b *broadcaster[T]) start(ctx context.Context, connect ConnectFunc[T]) error { - if b.running { - return fmt.Errorf("broadcaster already running") + select { + case <-ctx.Done(): // client canceled + return nil, ctx.Err() + case <-b.terminated: // no more data + return nil, io.EOF + case b.subscribe <- sub: // success submitting subscription + return sub, nil } +} +func (b *broadcaster[T]) Unsubscribe(sub <-chan T) { + // wait for broadcaster to start. In practice, the only way to reach + // Unsubscribe is by first having called Subscribe, which means we have + // already started. But a malfunctioning caller may call Unsubscribe freely, + // which would cause us to block forever the goroutine of the caller when + // trying to send to a nil `b.unsubscribe` or receive from a nil + // `b.terminated` if we haven't yet initialized those values. This would + // mean leaking that malfunctioninig caller's goroutine, so we rather make + // Unsubscribe safe in any possible case + if sub == nil { + return + } + <-b.started // wait for broadcaster to start + + select { + case b.unsubscribe <- sub: // success submitting unsubscription + case <-b.terminated: // broadcaster terminated, nothing to do + } +} + +// init initializes the broadcaster. It should not be run more than once. +func (b *broadcaster[T]) init(ctx context.Context, connect ConnectFunc[T]) error { + // create the stream that will connect us with the watch implementation and + // send it to them so they initialize and start sending data stream := make(chan T, 100) - - err := connect(stream) - if err != nil { + if err := connect(stream); err != nil { return err } - b.ctx = ctx - + // initialize our internal state + b.shouldTerminate = ctx.Done() b.cache = NewCache[T](ctx, 100) b.subscribe = make(chan chan T, 100) - b.unsubscribe = make(chan chan T, 100) - b.subs = make(map[chan T]struct{}) + b.unsubscribe = make(chan (<-chan T), 100) + b.subs = make(map[<-chan T]chan T) + b.terminated = make(chan struct{}) + // start handling incoming data from the watch implementation. If data came + // in until now, it will be buffered in `stream` go b.stream(stream) - b.running = true + // unblock any Subscribe/Unsubscribe calls since we are ready to handle them + close(b.started) + return nil } -func (b *broadcaster[T]) stream(input chan T) { +// stream acts a message broker between the watch implementation that receives a +// raw stream of events and the individual clients watching for those events. +// Thus, we hold the receive side of the watch implementation, and we are +// limited here to receive from it, whereas we are responsible for sending to +// watchers and closing their channels. The responsibility of closing `input` +// (as with any other channel) will always be of the sending side. Hence, the +// watch implementation should do it. +func (b *broadcaster[T]) stream(input <-chan T) { + // make sure we unconditionally cleanup upon return + defer func() { + // prevent new subscriptions and make sure to discard unsubscriptions + close(b.terminated) + // terminate all subscirptions and clean the map + for _, sub := range b.subs { + close(sub) + delete(b.subs, sub) + } + }() + for { select { - // context cancelled - case <-b.ctx.Done(): - close(input) - for sub := range b.subs { - close(sub) - delete(b.subs, sub) - } - b.running = false + case <-b.shouldTerminate: // service context cancelled return - // new subscriber - case sub := <-b.subscribe: + + case sub := <-b.subscribe: // subscribe // send initial batch of cached items err := b.cache.ReadInto(sub) if err != nil { close(sub) continue } + b.subs[sub] = sub - b.subs[sub] = struct{}{} - // unsubscribe - case sub := <-b.unsubscribe: - if _, ok := b.subs[sub]; ok { + case recv := <-b.unsubscribe: // unsubscribe + if sub, ok := b.subs[recv]; ok { close(sub) delete(b.subs, sub) } - // read item from input - case item, ok := <-input: + + case item, ok := <-input: // data arrived, send to subscribers // input closed, drain subscribers and exit if !ok { - for sub := range b.subs { - close(sub) - delete(b.subs, sub) - } - b.running = false return } - b.cache.Add(item) - - for sub := range b.subs { + for _, sub := range b.subs { select { case sub <- item: default: diff --git a/pkg/services/store/entity/sqlstash/sql_storage_server.go b/pkg/services/store/entity/sqlstash/sql_storage_server.go index 76024024dae..0a7d7cce617 100644 --- a/pkg/services/store/entity/sqlstash/sql_storage_server.go +++ b/pkg/services/store/entity/sqlstash/sql_storage_server.go @@ -63,6 +63,9 @@ func ProvideSQLEntityServer(db db.EntityDBInterface, tracer tracing.Tracer /*, c type SqlEntityServer interface { entity.EntityStoreServer + // FIXME: accpet a context.Context in the lifecycle methods, and Stop should + // also return an error. + Init() error Stop() } @@ -75,7 +78,6 @@ type sqlEntityServer struct { broadcaster Broadcaster[*entity.EntityWatchResponse] ctx context.Context // TODO: remove cancel context.CancelFunc - stream chan *entity.EntityWatchResponse tracer trace.Tracer once sync.Once @@ -139,9 +141,7 @@ func (s *sqlEntityServer) init() error { s.dialect = migrator.NewDialect(engine.DriverName()) // set up the broadcaster - s.broadcaster, err = NewBroadcaster(s.ctx, func(stream chan *entity.EntityWatchResponse) error { - s.stream = stream - + s.broadcaster, err = NewBroadcaster(s.ctx, func(stream chan<- *entity.EntityWatchResponse) error { // start the poller go s.poller(stream) @@ -994,13 +994,18 @@ func (s *sqlEntityServer) watchInit(ctx context.Context, r *entity.EntityWatchRe return lastRv, nil } -func (s *sqlEntityServer) poller(stream chan *entity.EntityWatchResponse) { +func (s *sqlEntityServer) poller(stream chan<- *entity.EntityWatchResponse) { var err error + // FIXME: we need a way to state startup of server from a (Group, Resource) + // standpoint, and consider that new (Group, Resource) may be added to + // `kind_version`, so we should probably also poll for changes in there since := int64(0) + interval := 1 * time.Second t := time.NewTicker(interval) + defer close(stream) defer t.Stop() for { @@ -1017,7 +1022,7 @@ func (s *sqlEntityServer) poller(stream chan *entity.EntityWatchResponse) { } } -func (s *sqlEntityServer) poll(since int64, out chan *entity.EntityWatchResponse) (int64, error) { +func (s *sqlEntityServer) poll(since int64, out chan<- *entity.EntityWatchResponse) (int64, error) { ctx, span := s.tracer.Start(s.ctx, "storage_server.poll") defer span.End() ctxLogger := s.log.FromContext(log.WithContextualAttributes(ctx, []any{"method", "poll"})) @@ -1182,26 +1187,25 @@ func (s *sqlEntityServer) watch(r *entity.EntityWatchRequest, w entity.EntitySto if err != nil { return err } + defer s.broadcaster.Unsubscribe(evts) stop := make(chan struct{}) since := r.Since go func() { + defer close(stop) for { r, err := w.Recv() if errors.Is(err, io.EOF) { s.log.Debug("watch client closed stream") - stop <- struct{}{} return } if err != nil { s.log.Error("error receiving message", "err", err) - stop <- struct{}{} return } if r.Action == entity.EntityWatchRequest_STOP { s.log.Debug("watch stop requested") - stop <- struct{}{} return } // handle any other message types @@ -1211,7 +1215,6 @@ func (s *sqlEntityServer) watch(r *entity.EntityWatchRequest, w entity.EntitySto for { select { - // stop signal case <-stop: s.log.Debug("watch stopped") return nil diff --git a/pkg/services/team/teamimpl/store.go b/pkg/services/team/teamimpl/store.go index 76658966c0b..7c422d8abf5 100644 --- a/pkg/services/team/teamimpl/store.go +++ b/pkg/services/team/teamimpl/store.go @@ -365,7 +365,7 @@ func getTeamMember(sess *db.Session, orgId int64, teamId int64, userId int64) (t func (ss *xormStore) IsMember(orgId int64, teamId int64, userId int64) (bool, error) { var isMember bool - err := ss.db.WithTransactionalDbSession(context.Background(), func(sess *db.Session) error { + err := ss.db.WithDbSession(context.Background(), func(sess *db.Session) error { var err error isMember, err = isTeamMember(sess, orgId, teamId, userId) return err diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index df89c95455e..ab0688afd66 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -16,7 +16,6 @@ import ( "path" "path/filepath" "regexp" - "runtime" "strconv" "strings" "time" @@ -475,6 +474,8 @@ type Cfg struct { // RBAC single organization. This configuration option is subject to change. RBACSingleOrganization bool + Zanzana ZanzanaSettings + // GRPC Server. GRPCServerNetwork string GRPCServerAddress string @@ -1022,9 +1023,9 @@ func (cfg *Cfg) validateStaticRootPath() error { func (cfg *Cfg) Load(args CommandLineArgs) error { cfg.setHomePath(args) - // Fix for missing IANA db on Windows + // Fix for missing IANA db on Windows or Alpine _, zoneInfoSet := os.LookupEnv(zoneInfo) - if runtime.GOOS == "windows" && !zoneInfoSet { + if !zoneInfoSet { if err := os.Setenv(zoneInfo, filepath.Join(cfg.HomePath, "tools", "zoneinfo.zip")); err != nil { cfg.Logger.Error("Can't set ZONEINFO environment variable", "err", err) } @@ -1113,6 +1114,9 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { readOAuth2ServerSettings(cfg) readAccessControlSettings(iniFile, cfg) + + cfg.readZanzanaSettings() + if err := cfg.readRenderingSettings(iniFile); err != nil { return err } @@ -1986,19 +1990,23 @@ func (cfg *Cfg) readLiveSettings(iniFile *ini.File) error { cfg.LiveHAEngineAddress = section.Key("ha_engine_address").MustString("127.0.0.1:6379") cfg.LiveHAEnginePassword = section.Key("ha_engine_password").MustString("") - var originPatterns []string allowedOrigins := section.Key("allowed_origins").MustString("") - for _, originPattern := range strings.Split(allowedOrigins, ",") { + origins := strings.Split(allowedOrigins, ",") + + originPatterns := make([]string, 0, len(origins)) + for _, originPattern := range origins { originPattern = strings.TrimSpace(originPattern) if originPattern == "" { continue } originPatterns = append(originPatterns, originPattern) } + _, err := GetAllowedOriginGlobs(originPatterns) if err != nil { return err } + cfg.LiveAllowedOrigins = originPatterns return nil } diff --git a/pkg/setting/setting_secure_socks_proxy.go b/pkg/setting/setting_secure_socks_proxy.go index 6e034cfa313..fe94e79e1c5 100644 --- a/pkg/setting/setting_secure_socks_proxy.go +++ b/pkg/setting/setting_secure_socks_proxy.go @@ -75,7 +75,7 @@ func readSecureSocksDSProxySettings(iniFile *ini.File) (SecureSocksDSProxySettin s.ClientKey = string(keyPEMBlock) } - var rootCAs []string + rootCAs := make([]string, 0, len(s.RootCAFilePaths)) for _, rootCAFile := range s.RootCAFilePaths { // nolint:gosec // The gosec G304 warning can be ignored because `rootCAFile` comes from config ini, and we check below if diff --git a/pkg/setting/settings_zanzana.go b/pkg/setting/settings_zanzana.go new file mode 100644 index 00000000000..f026a9036de --- /dev/null +++ b/pkg/setting/settings_zanzana.go @@ -0,0 +1,37 @@ +package setting + +import ( + "slices" +) + +type ZanzanaMode string + +const ( + ZanzanaModeClient ZanzanaMode = "client" + ZanzanaModeEmbedded ZanzanaMode = "embedded" +) + +type ZanzanaSettings struct { + // Addr is only used when mode is set to client + Addr string + // Mode can either be embedded or client + Mode ZanzanaMode +} + +func (cfg *Cfg) readZanzanaSettings() { + s := ZanzanaSettings{} + + sec := cfg.Raw.Section("zanzana") + s.Mode = ZanzanaMode(sec.Key("mode").MustString("embedded")) + + validModes := []ZanzanaMode{ZanzanaModeEmbedded, ZanzanaModeClient} + + if !slices.Contains(validModes, s.Mode) { + cfg.Logger.Warn("Invalid zanzana mode", "expected", validModes, "got", s.Mode) + s.Mode = "embedded" + } + + s.Addr = sec.Key("address").MustString("") + + cfg.Zanzana = s +} diff --git a/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go b/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go index ce3373bdbb4..8300a3c423a 100644 --- a/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go +++ b/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go @@ -88,9 +88,8 @@ type argJSONQuery struct { } func (e *AzureResourceGraphDatasource) buildQueries(queries []backend.DataQuery, dsInfo types.DatasourceInfo) ([]*AzureResourceGraphQuery, error) { - var azureResourceGraphQueries []*AzureResourceGraphQuery - - for _, query := range queries { + azureResourceGraphQueries := make([]*AzureResourceGraphQuery, len(queries)) + for i, query := range queries { queryJSONModel := argJSONQuery{} err := json.Unmarshal(query.JSON, &queryJSONModel) if err != nil { @@ -105,19 +104,18 @@ func (e *AzureResourceGraphDatasource) buildQueries(queries []backend.DataQuery, } interpolatedQuery, err := macros.KqlInterpolate(query, dsInfo, azureResourceGraphTarget.Query) - if err != nil { return nil, err } - azureResourceGraphQueries = append(azureResourceGraphQueries, &AzureResourceGraphQuery{ + azureResourceGraphQueries[i] = &AzureResourceGraphQuery{ RefID: query.RefID, ResultFormat: resultFormat, JSON: query.JSON, InterpolatedQuery: interpolatedQuery, TimeRange: query.TimeRange, QueryType: query.QueryType, - }) + } } return azureResourceGraphQueries, nil diff --git a/pkg/tsdb/azuremonitor/types/types.go b/pkg/tsdb/azuremonitor/types/types.go index 1e3e7f2073e..c0fd5b99095 100644 --- a/pkg/tsdb/azuremonitor/types/types.go +++ b/pkg/tsdb/azuremonitor/types/types.go @@ -129,8 +129,8 @@ type AzureMonitorDimensionFilterBackend struct { } func ConstructFiltersString(a dataquery.AzureMetricDimension) string { - var filterStrings []string - for _, filter := range a.Filters { + filterStrings := make([]string, len(a.Filters)) + for i, filter := range a.Filters { dimension := "" operator := "" if a.Dimension != nil { @@ -139,11 +139,14 @@ func ConstructFiltersString(a dataquery.AzureMetricDimension) string { if a.Operator != nil { operator = *a.Operator } - filterStrings = append(filterStrings, fmt.Sprintf("%v %v '%v'", dimension, operator, filter)) + + filterStrings[i] = fmt.Sprintf("%v %v '%v'", dimension, operator, filter) } + if a.Operator != nil && *a.Operator == "eq" { return strings.Join(filterStrings, " or ") } + return strings.Join(filterStrings, " and ") } diff --git a/pkg/tsdb/cloudwatch/services/utils.go b/pkg/tsdb/cloudwatch/services/utils.go index 4684aa2acad..3ba3fbfcba2 100644 --- a/pkg/tsdb/cloudwatch/services/utils.go +++ b/pkg/tsdb/cloudwatch/services/utils.go @@ -7,7 +7,7 @@ import ( ) func valuesToListMetricRespone[T any](values []T) []resources.ResourceResponse[T] { - var response []resources.ResourceResponse[T] + response := make([]resources.ResourceResponse[T], 0, len(values)) for _, value := range values { response = append(response, resources.ResourceResponse[T]{Value: value}) } diff --git a/pkg/tsdb/elasticsearch/querydata_test.go b/pkg/tsdb/elasticsearch/querydata_test.go index eeb3be175d7..1adf955af3a 100644 --- a/pkg/tsdb/elasticsearch/querydata_test.go +++ b/pkg/tsdb/elasticsearch/querydata_test.go @@ -84,15 +84,16 @@ func newFlowTestQueries(allJsonBytes []byte) ([]backend.DataQuery, error) { return nil, fmt.Errorf("error unmarshaling query-json: %w", err) } - var queries []backend.DataQuery - - for _, jsonBytes := range jsonBytesArray { + queries := make([]backend.DataQuery, len(jsonBytesArray)) + for i, jsonBytes := range jsonBytesArray { // we need to extract some fields from the json-array var jsonInfo queryDataTestQueryJSON + err = json.Unmarshal(jsonBytes, &jsonInfo) if err != nil { return nil, err } + // we setup the DataQuery, with values loaded from the json query := backend.DataQuery{ RefID: jsonInfo.RefID, @@ -101,7 +102,8 @@ func newFlowTestQueries(allJsonBytes []byte) ([]backend.DataQuery, error) { TimeRange: timeRange, JSON: jsonBytes, } - queries = append(queries, query) + + queries[i] = query } return queries, nil } diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index 822a655fb32..7701e8fee37 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -873,16 +873,16 @@ func trimDatapoints(queryResult backend.DataResponse, target *Query) { // we sort the label's pairs by the label-key, // and return the label-values func getSortedLabelValues(labels data.Labels) []string { - var keys []string + keys := make([]string, 0, len(labels)) for key := range labels { keys = append(keys, key) } sort.Strings(keys) - var values []string - for _, key := range keys { - values = append(values, labels[key]) + values := make([]string, len(keys)) + for i, key := range keys { + values[i] = labels[key] } return values diff --git a/pkg/tsdb/influxdb/fsql/arrow_test.go b/pkg/tsdb/influxdb/fsql/arrow_test.go index fa8844acf1d..3a7a27a0489 100644 --- a/pkg/tsdb/influxdb/fsql/arrow_test.go +++ b/pkg/tsdb/influxdb/fsql/arrow_test.go @@ -60,7 +60,7 @@ func TestNewQueryDataResponse(t *testing.T) { newJSONArray(`[0, 1, 2]`, &arrow.TimestampType{}), } - var arr []arrow.Array + arr := make([]arrow.Array, 0, len(strValues)) for _, v := range strValues { tarr, _, err := array.FromJSON( alloc, diff --git a/pkg/tsdb/influxdb/fsql/fsql_test.go b/pkg/tsdb/influxdb/fsql/fsql_test.go index b2eb94f5b0d..30ba7e516be 100644 --- a/pkg/tsdb/influxdb/fsql/fsql_test.go +++ b/pkg/tsdb/influxdb/fsql/fsql_test.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "encoding/json" + "net" "testing" "github.com/apache/arrow/go/v15/arrow/flight" @@ -21,9 +22,14 @@ type FSQLTestSuite struct { suite.Suite db *sql.DB server flight.Server + addr string } func (suite *FSQLTestSuite) SetupTest() { + addr, _ := freeport(suite.T()) + + suite.addr = addr + db, err := example.CreateDB() require.NoError(suite.T(), err) @@ -32,7 +38,7 @@ func (suite *FSQLTestSuite) SetupTest() { sqliteServer.Alloc = memory.NewCheckedAllocator(memory.DefaultAllocator) server := flight.NewServerWithMiddleware(nil) server.RegisterFlightService(flightsql.NewFlightServer(sqliteServer)) - err = server.Init("localhost:12345") + err = server.Init(suite.addr) require.NoError(suite.T(), err) go func() { err := server.Serve() @@ -59,7 +65,7 @@ func (suite *FSQLTestSuite) TestIntegration_QueryData() { &models.DatasourceInfo{ HTTPClient: nil, Token: "secret", - URL: "http://localhost:12345", + URL: "http://" + suite.addr, DbName: "influxdb", Version: "test", HTTPMode: "proxy", @@ -109,3 +115,15 @@ func mustQueryJSON(t *testing.T, refID, sql string) []byte { } return b } + +func freeport(t *testing.T) (addr string, err error) { + l, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("127.0.0.1")}) + if err != nil { + t.Fatal(err) + } + defer func() { + err = l.Close() + }() + a := l.Addr().(*net.TCPAddr) + return a.String(), nil +} diff --git a/pkg/tsdb/influxdb/influxql/buffered/response_parser.go b/pkg/tsdb/influxdb/influxql/buffered/response_parser.go index fbd93486511..91fc8b9967f 100644 --- a/pkg/tsdb/influxdb/influxql/buffered/response_parser.go +++ b/pkg/tsdb/influxdb/influxql/buffered/response_parser.go @@ -269,12 +269,12 @@ func transformRowsForTimeSeries(rows []models.Row, query models.Query) data.Fram } func newFrameWithTimeField(row models.Row, column string, colIndex int, query models.Query, frameName []byte) *data.Frame { - var timeArray []time.Time var floatArray []*float64 var stringArray []*string var boolArray []*bool valType := util.Typeof(row.Values, colIndex) + timeArray := make([]time.Time, 0, len(row.Values)) for _, valuePair := range row.Values { timestamp, timestampErr := util.ParseTimestamp(valuePair[0]) // we only add this row if the timestamp is valid diff --git a/pkg/tsdb/influxdb/influxql/influxql.go b/pkg/tsdb/influxdb/influxql/influxql.go index 5bba117aa78..660e245a940 100644 --- a/pkg/tsdb/influxdb/influxql/influxql.go +++ b/pkg/tsdb/influxdb/influxql/influxql.go @@ -23,7 +23,10 @@ import ( "github.com/grafana/grafana/pkg/tsdb/influxdb/models" ) -const defaultRetentionPolicy = "default" +const ( + defaultRetentionPolicy = "default" + metadataPrefix = "x-grafana-meta-add-" +) var ( ErrInvalidHttpMode = errors.New("'httpMode' should be either 'GET' or 'POST'") @@ -195,9 +198,27 @@ func execute(ctx context.Context, tracer trace.Tracer, dsInfo *models.Datasource } else { resp = buffered.ResponseParse(res.Body, res.StatusCode, query) } + + if resp.Frames != nil && len(resp.Frames) > 0 { + resp.Frames[0].Meta.Custom = readCustomMetadata(res) + } + return *resp, nil } +func readCustomMetadata(res *http.Response) map[string]any { + var result map[string]any + for k := range res.Header { + if key, found := strings.CutPrefix(strings.ToLower(k), metadataPrefix); found { + if result == nil { + result = make(map[string]any) + } + result[key] = res.Header.Get(k) + } + } + return result +} + // startTrace setups a trace but does not panic if tracer is nil which helps with testing func startTrace(ctx context.Context, tracer trace.Tracer, name string, attributes ...attribute.KeyValue) (context.Context, func()) { if tracer == nil { diff --git a/pkg/tsdb/influxdb/influxql/influxql_test.go b/pkg/tsdb/influxdb/influxql/influxql_test.go index fee920f1056..d34432403a0 100644 --- a/pkg/tsdb/influxdb/influxql/influxql_test.go +++ b/pkg/tsdb/influxdb/influxql/influxql_test.go @@ -3,6 +3,7 @@ package influxql import ( "context" "io" + "net/http" "net/url" "testing" @@ -60,3 +61,52 @@ func TestExecutor_createRequest(t *testing.T) { require.EqualError(t, err, ErrInvalidHttpMode.Error()) }) } + +func TestReadCustomMetadata(t *testing.T) { + t.Run("should read nothing if no X-Grafana-Meta-Add- header exists", func(t *testing.T) { + header := http.Header{} + header.Add("content-type", "text/html") + header.Add("content-encoding", "gzip") + res := &http.Response{ + Header: header, + } + result := readCustomMetadata(res) + require.Nil(t, result) + }) + + t.Run("should read X-Grafana-Meta-Add- header", func(t *testing.T) { + header := http.Header{} + header.Add("content-type", "text/html") + header.Add("content-encoding", "gzip") + header.Add("X-Grafana-Meta-Add-TestThing", "test1234") + res := &http.Response{ + Header: header, + } + result := readCustomMetadata(res) + expected := map[string]any{ + "testthing": "test1234", + } + require.NotNil(t, result) + require.Equal(t, expected, result) + }) + + t.Run("should read multiple X-Grafana-Meta-Add- header", func(t *testing.T) { + header := http.Header{} + header.Add("content-type", "text/html") + header.Add("content-encoding", "gzip") + header.Add("X-Grafana-Meta-Add-TestThing", "test111") + header.Add("X-Grafana-Meta-Add-TestThing2", "test222") + header.Add("X-Grafana-Meta-Add-Test-Other", "other") + res := &http.Response{ + Header: header, + } + result := readCustomMetadata(res) + expected := map[string]any{ + "testthing": "test111", + "testthing2": "test222", + "test-other": "other", + } + require.NotNil(t, result) + require.Equal(t, expected, result) + }) +} diff --git a/pkg/tsdb/tempo/trace_transform_test.go b/pkg/tsdb/tempo/trace_transform_test.go index 2dd60c07867..8b901c6c41c 100644 --- a/pkg/tsdb/tempo/trace_transform_test.go +++ b/pkg/tsdb/tempo/trace_transform_test.go @@ -136,10 +136,11 @@ func rootSpan(frame *BetterFrame) Row { } func fieldNames(frame *data.Frame) []string { - var names []string - for _, f := range frame.Fields { - names = append(names, f.Name) + names := make([]string, len(frame.Fields)) + for i, f := range frame.Fields { + names[i] = f.Name } + return names } diff --git a/pkg/util/strings.go b/pkg/util/strings.go index 9bb688e3ca5..f3a2d35540f 100644 --- a/pkg/util/strings.go +++ b/pkg/util/strings.go @@ -48,11 +48,13 @@ func SplitString(str string) []string { return res } - var result []string matches := stringListItemMatcher.FindAllString(str, -1) - for _, match := range matches { - result = append(result, strings.Trim(match, "\"")) + + result := make([]string, len(matches)) + for i, match := range matches { + result[i] = strings.Trim(match, "\"") } + return result } diff --git a/pkg/util/tls.go b/pkg/util/tls.go index 049d6cf12da..0a5b4227b27 100644 --- a/pkg/util/tls.go +++ b/pkg/util/tls.go @@ -30,22 +30,26 @@ func TlsCiphersToIDs(names []string) ([]uint16, error) { // no ciphers specified, use defaults return nil, nil } - var ids []uint16 - var missing []string ciphers := tls.CipherSuites() - var cipherMap = make(map[string]uint16, len(ciphers)) + + cipherMap := make(map[string]uint16, len(ciphers)) for _, cipher := range ciphers { cipherMap[cipher.Name] = cipher.ID } + missing := []string{} + ids := make([]uint16, 0, len(names)) + for _, name := range names { name = strings.ToUpper(name) + id, ok := cipherMap[name] if !ok { missing = append(missing, name) continue } + ids = append(ids, id) } diff --git a/pkg/util/xorm/go.mod b/pkg/util/xorm/go.mod index 78a1c390cf7..e981f997db3 100644 --- a/pkg/util/xorm/go.mod +++ b/pkg/util/xorm/go.mod @@ -11,7 +11,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/go-sql-driver/mysql v1.7.1 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect diff --git a/pkg/util/xorm/go.sum b/pkg/util/xorm/go.sum index 6a1b138bd40..c003657520e 100644 --- a/pkg/util/xorm/go.sum +++ b/pkg/util/xorm/go.sum @@ -1,10 +1,11 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= -github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y= github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index 4dea1f5f8ba..d8f4afb84c7 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -1556,6 +1556,49 @@ } } }, + "/reports/render/csvs": { + "get": { + "description": "Available to all users and with a valid license.", + "produces": [ + "application/zip" + ], + "tags": [ + "reports", + "enterprise" + ], + "summary": "Download a CSV report.", + "operationId": "renderReportCSVs", + "parameters": [ + { + "type": "string", + "name": "dashboards", + "in": "query" + }, + { + "type": "string", + "name": "title", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/contentResponse" + }, + "204": { + "$ref": "#/responses/noContentResponse" + }, + "400": { + "$ref": "#/responses/badRequestError" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } + }, "/reports/render/pdfs": { "get": { "description": "Available to all users and with a valid license.", @@ -1571,7 +1614,7 @@ "parameters": [ { "type": "string", - "name": "dashboardID", + "name": "dashboards", "in": "query" }, { @@ -4703,11 +4746,15 @@ "type": "integer", "format": "int64" }, + "isDeleted": { + "type": "boolean" + }, "isStarred": { "type": "boolean" }, - "remainingTrashAtAge": { - "type": "string" + "permanentlyDeleteDate": { + "type": "string", + "format": "date-time" }, "slug": { "type": "string" @@ -7049,6 +7096,7 @@ } }, "State": { + "description": "+enum", "type": "string" }, "Status": { @@ -7492,6 +7540,7 @@ } }, "Type": { + "description": "+enum", "type": "string" }, "TypeMeta": { @@ -9297,6 +9346,12 @@ } } }, + "noContentResponse": { + "description": "", + "schema": { + "type": "object" + } + }, "notFoundError": { "description": "NotFoundError is returned when the requested resource was not found.", "schema": { diff --git a/public/api-merged.json b/public/api-merged.json index 81135377b88..911dcfe26f1 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -7800,6 +7800,49 @@ } } }, + "/reports/render/csvs": { + "get": { + "description": "Available to all users and with a valid license.", + "produces": [ + "application/zip" + ], + "tags": [ + "reports", + "enterprise" + ], + "summary": "Download a CSV report.", + "operationId": "renderReportCSVs", + "parameters": [ + { + "type": "string", + "name": "dashboards", + "in": "query" + }, + { + "type": "string", + "name": "title", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/contentResponse" + }, + "204": { + "$ref": "#/responses/noContentResponse" + }, + "400": { + "$ref": "#/responses/badRequestError" + }, + "401": { + "$ref": "#/responses/unauthorisedError" + }, + "500": { + "$ref": "#/responses/internalServerError" + } + } + } + }, "/reports/render/pdfs": { "get": { "description": "Available to all users and with a valid license.", @@ -7815,7 +7858,7 @@ "parameters": [ { "type": "string", - "name": "dashboardID", + "name": "dashboards", "in": "query" }, { @@ -15815,11 +15858,15 @@ "type": "integer", "format": "int64" }, + "isDeleted": { + "type": "boolean" + }, "isStarred": { "type": "boolean" }, - "remainingTrashAtAge": { - "type": "string" + "permanentlyDeleteDate": { + "type": "string", + "format": "date-time" }, "slug": { "type": "string" @@ -23339,6 +23386,12 @@ } } }, + "noContentResponse": { + "description": "(empty)", + "schema": { + "type": "object" + } + }, "notFoundError": { "description": "NotFoundError is returned when the requested resource was not found.", "schema": { diff --git a/public/app/app.ts b/public/app/app.ts index 770653fe974..f48ad85af7a 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -42,7 +42,6 @@ import { import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelDataErrorView'; import { setPanelRenderer } from '@grafana/runtime/src/components/PanelRenderer'; import { setPluginPage } from '@grafana/runtime/src/components/PluginPage'; -import { getScrollbarWidth } from '@grafana/ui'; import config, { updateConfig } from 'app/core/config'; import { arrayMove } from 'app/core/utils/arrayMove'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; @@ -136,10 +135,6 @@ export class GrafanaApp { // This needs to be done after the `initEchoSrv` since it is being used under the hood. startMeasure('frontend_app_init'); - if (!config.featureToggles.betterPageScrolling) { - addClassIfNoOverlayScrollbar(); - } - setLocale(config.bootData.user.locale); setWeekStart(config.bootData.user.weekStart); setPanelRenderer(PanelRenderer); @@ -367,12 +362,6 @@ function initEchoSrv() { } } -function addClassIfNoOverlayScrollbar() { - if (getScrollbarWidth() > 0) { - document.body.classList.add('no-overlay-scrollbar'); - } -} - /** * Report when a metric of a given name was marked during the document lifecycle. Works for markers with no duration, * like PerformanceMark or PerformancePaintTiming (e.g. created with performance.mark, or first-contentful-paint) diff --git a/public/app/core/components/AccessControl/PermissionListItem.tsx b/public/app/core/components/AccessControl/PermissionListItem.tsx index 0d6eaf15cf7..1fdf97b7a55 100644 --- a/public/app/core/components/AccessControl/PermissionListItem.tsx +++ b/public/app/core/components/AccessControl/PermissionListItem.tsx @@ -60,7 +60,7 @@ export const PermissionListItem = ({ item, permissionLevels, canSet, onRemove, o /> ) : ( - diff --git a/public/app/core/components/Login/LoginForm.tsx b/public/app/core/components/Login/LoginForm.tsx index ffaa9d452ac..4524e997442 100644 --- a/public/app/core/components/Login/LoginForm.tsx +++ b/public/app/core/components/Login/LoginForm.tsx @@ -83,5 +83,9 @@ export const getStyles = (theme: GrafanaTheme2) => { justifyContent: 'center', width: '100%', }), + + skipButton: css({ + alignSelf: 'flex-start', + }), }; }; diff --git a/public/app/core/components/FlaggedScroller.tsx b/public/app/core/components/NativeScrollbar.tsx similarity index 72% rename from public/app/core/components/FlaggedScroller.tsx rename to public/app/core/components/NativeScrollbar.tsx index 9d9c3cc5043..76d447f4311 100644 --- a/public/app/core/components/FlaggedScroller.tsx +++ b/public/app/core/components/NativeScrollbar.tsx @@ -1,21 +1,12 @@ import { css, cx } from '@emotion/css'; import React, { useEffect, useRef } from 'react'; -import { config } from '@grafana/runtime'; import { CustomScrollbar, useStyles2 } from '@grafana/ui'; -type FlaggedScrollerProps = Parameters[0]; - -export default function FlaggedScrollbar(props: FlaggedScrollerProps) { - if (config.featureToggles.betterPageScrolling) { - return {props.children}; - } - - return ; -} +type Props = Parameters[0]; // Shim to provide API-compatibility for Page's scroll-related props -function NativeScrollbar({ children, scrollRefCallback, scrollTop, divId }: FlaggedScrollerProps) { +export default function NativeScrollbar({ children, scrollRefCallback, scrollTop, divId }: Props) { const styles = useStyles2(getStyles); const ref = useRef(null); diff --git a/public/app/core/components/Page/Page.tsx b/public/app/core/components/Page/Page.tsx index f88d0dd011b..c588d17be1e 100644 --- a/public/app/core/components/Page/Page.tsx +++ b/public/app/core/components/Page/Page.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; -import FlaggedScrollbar from '../FlaggedScroller'; +import NativeScrollbar from '../NativeScrollbar'; import { PageContents } from './PageContents'; import { PageHeader } from './PageHeader'; @@ -53,7 +53,7 @@ export const Page: PageType = ({ return (
{layout === PageLayoutType.Standard && ( - }
{children}
- + )} {layout === PageLayoutType.Canvas && ( -
{children}
-
+ )} {layout === PageLayoutType.Custom && children} diff --git a/public/app/core/services/echo/backends/analytics/ApplicationInsightsBackend.ts b/public/app/core/services/echo/backends/analytics/ApplicationInsightsBackend.ts index 433d4582aff..78db86015cd 100644 --- a/public/app/core/services/echo/backends/analytics/ApplicationInsightsBackend.ts +++ b/public/app/core/services/echo/backends/analytics/ApplicationInsightsBackend.ts @@ -7,8 +7,6 @@ import { PageviewEchoEvent, } from '@grafana/runtime'; -import { loadScript } from '../../utils'; - interface ApplicationInsights { trackPageView: () => void; trackEvent: (event: { name: string; properties?: Record }) => void; @@ -39,10 +37,12 @@ export class ApplicationInsightsBackend implements EchoBackend { - const init = new (window as any).Microsoft.ApplicationInsights.ApplicationInsights(applicationInsightsOpts); - window.applicationInsights = init.loadAppInsights(); - }); + System.import(url) + .then((m) => (m.default ? m.default : m)) + .then(({ ApplicationInsights }) => { + const init = new ApplicationInsights(applicationInsightsOpts); + window.applicationInsights = init.loadAppInsights(); + }); } addEvent = (e: PageviewEchoEvent | InteractionEchoEvent) => { diff --git a/public/app/core/utils/navBarItem-translations.ts b/public/app/core/utils/navBarItem-translations.ts index 9297cb4816c..6dcaa0ebf74 100644 --- a/public/app/core/utils/navBarItem-translations.ts +++ b/public/app/core/utils/navBarItem-translations.ts @@ -40,8 +40,8 @@ export function getNavTitle(navId: string | undefined) { return t('nav.reporting.title', 'Reporting'); case 'dashboards/public': return t('nav.public.title', 'Public dashboards'); - case 'dashboards/recentlyDeleted': - return t('nav.recentlyDeleted.title', 'Recently Deleted'); + case 'dashboards/recently-deleted': + return t('nav.recently-deleted.title', 'Recently Deleted'); case 'dashboards/new': return t('nav.new-dashboard.title', 'New dashboard'); case 'dashboards/folder/new': @@ -208,9 +208,9 @@ export function getNavSubTitle(navId: string | undefined) { ); case 'dashboards/library-panels': return t('nav.library-panels.subtitle', 'Reusable panels that can be added to multiple dashboards'); - case 'dashboards/recentlyDeleted': + case 'dashboards/recently-deleted': return t( - 'nav.recentlyDeleted.subtitle', + 'nav.recently-deleted.subtitle', 'Any items listed here for more than 30 days will be automatically deleted.' ); case 'alerting': diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index a481cade4ab..23031c03dc4 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -166,6 +166,19 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] { () => import(/* webpackChunkName: "AlertGroups" */ 'app/features/alerting/unified/AlertGroups') ), }, + { + path: '/alerting/history/', + roles: evaluateAccess([ + AccessControlAction.AlertingInstanceRead, + AccessControlAction.AlertingInstancesExternalRead, + ]), + component: importAlertingComponent( + () => + import( + /* webpackChunkName: "HistoryPage" */ 'app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryPage' + ) + ), + }, { path: '/alerting/new/:type?', pageClass: 'page-alerting', diff --git a/public/app/features/alerting/unified/Receivers.test.tsx b/public/app/features/alerting/unified/Receivers.test.tsx index ac966805830..982b4488eec 100644 --- a/public/app/features/alerting/unified/Receivers.test.tsx +++ b/public/app/features/alerting/unified/Receivers.test.tsx @@ -3,14 +3,13 @@ import { selectOptionInTest } from 'test/helpers/selectOptionInTest'; import { render, screen, waitFor, userEvent } from 'test/test-utils'; import { - EXTERNAL_VANILLA_ALERTMANAGER_UID, + PROVISIONED_MIMIR_ALERTMANAGER_UID, + mockDataSources, setupVanillaAlertmanagerServer, } from 'app/features/alerting/unified/components/settings/__mocks__/server'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; -import { grantUserPermissions, mockDataSource } from 'app/features/alerting/unified/mocks'; +import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; -import { DataSourceType } from 'app/features/alerting/unified/utils/datasource'; -import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; import ContactPoints from './Receivers'; @@ -19,15 +18,15 @@ import 'core-js/stable/structured-clone'; const server = setupMswServer(); -const mockDataSources = { - [EXTERNAL_VANILLA_ALERTMANAGER_UID]: mockDataSource({ - uid: EXTERNAL_VANILLA_ALERTMANAGER_UID, - name: EXTERNAL_VANILLA_ALERTMANAGER_UID, - type: DataSourceType.Alertmanager, - jsonData: { - implementation: AlertManagerImplementation.prometheus, - }, - }), +const assertSaveWasSuccessful = async () => { + // TODO: Have a better way to assert that the contact point was saved. This is instead asserting on some + // text that's present on the list page, as there's a lot of overlap in text between the form and the list page + return waitFor(() => expect(screen.getByText(/search by name or type/i)).toBeInTheDocument(), { timeout: 2000 }); +}; + +const saveContactPoint = async () => { + const user = userEvent.setup(); + return user.click(await screen.findByRole('button', { name: /save contact point/i })); }; beforeEach(() => { @@ -37,17 +36,22 @@ beforeEach(() => { AccessControlAction.AlertingNotificationsExternalRead, AccessControlAction.AlertingNotificationsExternalWrite, ]); + + setupVanillaAlertmanagerServer(server); + setupDataSources(mockDataSources[PROVISIONED_MIMIR_ALERTMANAGER_UID]); }); it('can save a contact point with a select dropdown', async () => { - setupVanillaAlertmanagerServer(server); - setupDataSources(mockDataSources[EXTERNAL_VANILLA_ALERTMANAGER_UID]); - const user = userEvent.setup(); render(, { historyOptions: { - initialEntries: [`/alerting/notifications/receivers/new?alertmanager=${EXTERNAL_VANILLA_ALERTMANAGER_UID}`], + initialEntries: [ + { + pathname: `/alerting/notifications/receivers/new`, + search: `?alertmanager=${PROVISIONED_MIMIR_ALERTMANAGER_UID}`, + }, + ], }, }); @@ -66,9 +70,28 @@ it('can save a contact point with a select dropdown', async () => { await user.type(botToken, 'sometoken'); await user.type(chatId, '-123'); - await user.click(await screen.findByRole('button', { name: /save contact point/i })); + await saveContactPoint(); - // TODO: Have a better way to assert that the contact point was saved. This is instead asserting on some - // text that's present on the list page, as there's a lot of overlap in text between the form and the list page - await waitFor(() => expect(screen.getByText(/search by name or type/i)).toBeInTheDocument(), { timeout: 2000 }); + await assertSaveWasSuccessful(); +}); + +it('can save existing Telegram contact point', async () => { + render(, { + historyOptions: { + initialEntries: [ + { + pathname: `/alerting/notifications/receivers/Telegram/edit`, + search: `?alertmanager=${PROVISIONED_MIMIR_ALERTMANAGER_UID}`, + }, + ], + }, + }); + + // Here, we're implicitly testing that our parsing of an existing Telegram integration works correctly + // Our mock server will reject a request if we've sent the Chat ID as `0`, + // so opening and trying to save an existing Telegram integration should + // trigger this error if it regresses + await saveContactPoint(); + + await assertSaveWasSuccessful(); }); diff --git a/public/app/features/alerting/unified/api/stateHistoryApi.ts b/public/app/features/alerting/unified/api/stateHistoryApi.ts index 3a78bcd8425..2f82fe6ef2d 100644 --- a/public/app/features/alerting/unified/api/stateHistoryApi.ts +++ b/public/app/features/alerting/unified/api/stateHistoryApi.ts @@ -4,7 +4,7 @@ import { alertingApi } from './alertingApi'; export const stateHistoryApi = alertingApi.injectEndpoints({ endpoints: (build) => ({ - getRuleHistory: build.query({ + getRuleHistory: build.query({ query: ({ ruleUid, from, to, limit = 100 }) => ({ url: '/api/v1/rules/history', params: { ruleUID: ruleUid, from, to, limit }, diff --git a/public/app/features/alerting/unified/components/AlertLabels.tsx b/public/app/features/alerting/unified/components/AlertLabels.tsx index 1e01be64cf4..40801357372 100644 --- a/public/app/features/alerting/unified/components/AlertLabels.tsx +++ b/public/app/features/alerting/unified/components/AlertLabels.tsx @@ -5,6 +5,7 @@ import React, { useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Button, getTagColorsFromName, useStyles2 } from '@grafana/ui'; +import { Trans, t } from 'app/core/internationalization'; import { isPrivateLabel } from '../utils/labels'; @@ -28,6 +29,7 @@ export const AlertLabels = ({ labels, commonLabels = {}, size }: Props) => { const commonLabelsCount = Object.keys(commonLabels).length; const hasCommonLabels = commonLabelsCount > 0; + const tooltip = t('alert-labels.button.show.tooltip', 'Show common labels'); return (
@@ -39,7 +41,7 @@ export const AlertLabels = ({ labels, commonLabels = {}, size }: Props) => { variant="secondary" fill="text" onClick={() => setShowCommonLabels(true)} - tooltip="Show common labels" + tooltip={tooltip} tooltipPlacement="top" size="sm" > @@ -54,7 +56,7 @@ export const AlertLabels = ({ labels, commonLabels = {}, size }: Props) => { tooltipPlacement="top" size="sm" > - Hide common labels + Hide common labels )}
diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistory.tsx b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistory.tsx new file mode 100644 index 00000000000..e01bb4f6a02 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistory.tsx @@ -0,0 +1,383 @@ +import { css } from '@emotion/css'; +import React, { useCallback, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { useMeasure } from 'react-use'; + +import { GrafanaTheme2, TimeRange } from '@grafana/data'; +import { isFetchError } from '@grafana/runtime'; +import { SceneComponentProps, SceneObjectBase, sceneGraph } from '@grafana/scenes'; +import { + Alert, + Button, + Field, + Icon, + Input, + Label, + LoadingBar, + Stack, + Text, + Tooltip, + useStyles2, + withErrorBoundary, +} from '@grafana/ui'; +import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; +import { Trans, t } from 'app/core/internationalization'; +import { + GrafanaAlertStateWithReason, + isAlertStateWithReason, + isGrafanaAlertState, + mapStateWithReasonToBaseState, + mapStateWithReasonToReason, +} from 'app/types/unified-alerting-dto'; + +import { stateHistoryApi } from '../../../api/stateHistoryApi'; +import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource'; +import { stringifyErrorLike } from '../../../utils/misc'; +import { hashLabelsOrAnnotations } from '../../../utils/rule-id'; +import { AlertLabels } from '../../AlertLabels'; +import { CollapseToggle } from '../../CollapseToggle'; +import { LogRecord } from '../state-history/common'; +import { useRuleHistoryRecords } from '../state-history/useRuleHistoryRecords'; + +const LIMIT_EVENTS = 250; + +const HistoryEventsList = ({ timeRange }: { timeRange?: TimeRange }) => { + const styles = useStyles2(getStyles); + + // Filter state + const [eventsFilter, setEventsFilter] = useState(''); + // form for filter fields + const { register, handleSubmit, reset } = useForm({ defaultValues: { query: '' } }); // form for search field + const from = timeRange?.from.unix(); + const to = timeRange?.to.unix(); + const onFilterCleared = useCallback(() => { + setEventsFilter(''); + reset(); + }, [setEventsFilter, reset]); + + const { + data: stateHistory, + isLoading, + isError, + error, + } = stateHistoryApi.endpoints.getRuleHistory.useQuery( + { + from: from, + to: to, + limit: LIMIT_EVENTS, + }, + { + refetchOnFocus: true, + refetchOnReconnect: true, + } + ); + + const { historyRecords } = useRuleHistoryRecords(stateHistory, eventsFilter); + + if (isError) { + return ; + } + + return ( + +
+
setEventsFilter(data.query))}> + + + +
+ + +
+ ); +}; + +// todo: this function has been copied from RuleList.v2.tsx, should be moved to a shared location +const LoadingIndicator = ({ visible = false }) => { + const [measureRef, { width }] = useMeasure(); + return
{visible && }
; +}; + +interface HistoryLogEventsProps { + logRecords: LogRecord[]; +} +function HistoryLogEvents({ logRecords }: HistoryLogEventsProps) { + // display log records + return ( +
    + {logRecords.map((record) => { + return ; + })} +
+ ); +} + +interface HistoryErrorMessageProps { + error: unknown; +} + +function HistoryErrorMessage({ error }: HistoryErrorMessageProps) { + if (isFetchError(error) && error.status === 404) { + return ; + } + const title = t('central-alert-history.error', 'Something went wrong loading the alert state history'); + + return {stringifyErrorLike(error)}; +} + +interface SearchFieldInputProps { + showClearFilterSuffix: boolean; + onClearFilterClick: () => void; +} +const SearchFieldInput = React.forwardRef( + ({ showClearFilterSuffix, onClearFilterClick, ...rest }: SearchFieldInputProps, ref) => { + const placeholder = t('central-alert-history.filter.placeholder', 'Filter events in the list with labels'); + return ( + + + + Filter events + + + + } + > + } + suffix={ + showClearFilterSuffix && ( + + ) + } + placeholder={placeholder} + ref={ref} + {...rest} + /> + + ); + } +); + +SearchFieldInput.displayName = 'SearchFieldInput'; + +function EventRow({ record }: { record: LogRecord }) { + const styles = useStyles2(getStyles); + const [isCollapsed, setIsCollapsed] = useState(true); + return ( +
+
+ + +
+ +
+
+ +
+
+ {record.line.labels ? : null} +
+
+ +
+
+
+
+ ); +} + +function AlertRuleName({ labels, ruleUID }: { labels: Record; ruleUID?: string }) { + const styles = useStyles2(getStyles); + const alertRuleName = labels['alertname']; + if (!ruleUID) { + return {alertRuleName}; + } + return ( + + + {alertRuleName} + + + ); +} + +interface EventTransitionProps { + previous: GrafanaAlertStateWithReason; + current: GrafanaAlertStateWithReason; +} +function EventTransition({ previous, current }: EventTransitionProps) { + return ( + + + + + + ); +} + +function EventState({ state }: { state: GrafanaAlertStateWithReason }) { + const styles = useStyles2(getStyles); + + if (!isGrafanaAlertState(state) && !isAlertStateWithReason(state)) { + return ( + + + + ); + } + const baseState = mapStateWithReasonToBaseState(state); + const reason = mapStateWithReasonToReason(state); + + switch (baseState) { + case 'Normal': + return ( + + + + ); + case 'Alerting': + return ( + + + + ); + case 'NoData': //todo:change icon + return ( + + + {/* no idea which icon to use */} + + ); + case 'Error': + return ( + + + + ); + + case 'Pending': + return ( + + + + ); + default: + return ; + } +} + +interface TimestampProps { + time: number; // epoch timestamp +} + +const Timestamp = ({ time }: TimestampProps) => { + const dateTime = new Date(time); + const formattedDate = dateTime.toLocaleString('en-US', { + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }); + + return ( + + {formattedDate} + + ); +}; + +export default withErrorBoundary(HistoryEventsList, { style: 'page' }); + +export const getStyles = (theme: GrafanaTheme2) => { + return { + header: css({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + padding: `${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)} 0`, + flexWrap: 'nowrap', + borderBottom: `1px solid ${theme.colors.border.weak}`, + + '&:hover': { + backgroundColor: theme.components.table.rowHoverBackground, + }, + }), + + collapseToggle: css({ + background: 'none', + border: 'none', + marginTop: `-${theme.spacing(1)}`, + marginBottom: `-${theme.spacing(1)}`, + + svg: { + marginBottom: 0, + }, + }), + normalColor: css({ + fill: theme.colors.success.text, + }), + warningColor: css({ + fill: theme.colors.warning.text, + }), + alertingColor: css({ + fill: theme.colors.error.text, + }), + timeCol: css({ + width: '150px', + }), + transitionCol: css({ + width: '80px', + }), + alertNameCol: css({ + width: '300px', + }), + labelsCol: css({ + display: 'flex', + overflow: 'hidden', + alignItems: 'center', + paddingRight: theme.spacing(2), + flex: 1, + }), + alertName: css({ + whiteSpace: 'nowrap', + cursor: 'pointer', + overflow: 'hidden', + textOverflow: 'ellipsis', + display: 'block', + color: theme.colors.text.link, + }), + labelsFilter: css({ + width: '100%', + paddingTop: theme.spacing(4), + }), + }; +}; + +export class HistoryEventsListObject extends SceneObjectBase { + public static Component = HistoryEventsListObjectRenderer; +} + +export function HistoryEventsListObjectRenderer({ model }: SceneComponentProps) { + const { value: timeRange } = sceneGraph.getTimeRange(model).useState(); // get time range from scene graph + + return ; +} diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryPage.tsx b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryPage.tsx new file mode 100644 index 00000000000..0ab57f508d4 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryPage.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import { withErrorBoundary } from '@grafana/ui'; + +import { AlertingPageWrapper } from '../../AlertingPageWrapper'; + +import { CentralAlertHistoryScene } from './CentralAlertHistoryScene'; + +const HistoryPage = () => { + return ( + + + + ); +}; +export default withErrorBoundary(HistoryPage, { style: 'page' }); diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx new file mode 100644 index 00000000000..0cd75c05a4b --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx @@ -0,0 +1,124 @@ +import React from 'react'; + +import { getDataSourceSrv } from '@grafana/runtime'; +import { + EmbeddedScene, + PanelBuilders, + SceneControlsSpacer, + SceneFlexItem, + SceneFlexLayout, + SceneQueryRunner, + SceneReactObject, + SceneRefreshPicker, + SceneTimePicker, +} from '@grafana/scenes'; +import { + GraphDrawStyle, + GraphGradientMode, + LegendDisplayMode, + LineInterpolation, + ScaleDistribution, + StackingMode, + TooltipDisplayMode, + VisibilityMode, +} from '@grafana/schema/dist/esm/index'; + +import { DataSourceInformation, PANEL_STYLES } from '../../../home/Insights'; +import { SectionSubheader } from '../../../insights/SectionSubheader'; + +import { HistoryEventsListObjectRenderer } from './CentralAlertHistory'; + +export const CentralAlertHistoryScene = () => { + const dataSourceSrv = getDataSourceSrv(); + const alertStateHistoryDatasource: DataSourceInformation = { + type: 'loki', + uid: 'grafanacloud-alert-state-history', + settings: undefined, + }; + + alertStateHistoryDatasource.settings = dataSourceSrv.getInstanceSettings(alertStateHistoryDatasource.uid); + + const scene = new EmbeddedScene({ + controls: [new SceneControlsSpacer(), new SceneTimePicker({}), new SceneRefreshPicker({})], + body: new SceneFlexLayout({ + direction: 'column', + children: [ + new SceneFlexItem({ + ySizing: 'content', + body: getEventsSceneObject(alertStateHistoryDatasource), + }), + new SceneFlexItem({ + body: new SceneReactObject({ + component: HistoryEventsListObjectRenderer, + }), + }), + ], + }), + }); + + return ; +}; + +function getEventsSceneObject(ashDs: DataSourceInformation) { + return new EmbeddedScene({ + controls: [ + new SceneReactObject({ + component: SectionSubheader, + }), + ], + body: new SceneFlexLayout({ + direction: 'column', + children: [ + new SceneFlexItem({ + ySizing: 'content', + body: new SceneFlexLayout({ + children: [getEventsScenesFlexItem(ashDs)], + }), + }), + ], + }), + }); +} + +function getSceneQuery(datasource: DataSourceInformation) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + expr: 'count_over_time({from="state-history"} |= `` [$__auto])', + queryType: 'range', + step: '10s', + }, + ], + }); + return query; +} + +export function getEventsScenesFlexItem(datasource: DataSourceInformation) { + return new SceneFlexItem({ + ...PANEL_STYLES, + body: PanelBuilders.timeseries() + .setTitle('Events') + .setDescription('Alert events during the period of time.') + .setData(getSceneQuery(datasource)) + .setColor({ mode: 'continuous-BlPu' }) + .setCustomFieldConfig('fillOpacity', 100) + .setCustomFieldConfig('drawStyle', GraphDrawStyle.Bars) + .setCustomFieldConfig('lineInterpolation', LineInterpolation.Linear) + .setCustomFieldConfig('lineWidth', 1) + .setCustomFieldConfig('barAlignment', 0) + .setCustomFieldConfig('spanNulls', false) + .setCustomFieldConfig('insertNulls', false) + .setCustomFieldConfig('showPoints', VisibilityMode.Auto) + .setCustomFieldConfig('pointSize', 5) + .setCustomFieldConfig('stacking', { mode: StackingMode.None, group: 'A' }) + .setCustomFieldConfig('gradientMode', GraphGradientMode.Hue) + .setCustomFieldConfig('scaleDistribution', { type: ScaleDistribution.Linear }) + .setOption('legend', { showLegend: false, displayMode: LegendDisplayMode.Hidden }) + .setOption('tooltip', { mode: TooltipDisplayMode.Single }) + + .setNoValue('No events found') + .build(), + }); +} diff --git a/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx b/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx index 062ff5b1820..9c9ea58eabe 100644 --- a/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx +++ b/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx @@ -4,7 +4,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; import { DataFrame, dateTime, GrafanaTheme2, TimeRange } from '@grafana/data'; -import { Alert, Button, Field, Icon, Input, Label, Tooltip, useStyles2, Stack } from '@grafana/ui'; +import { Alert, Button, Field, Icon, Input, Label, Stack, Tooltip, useStyles2 } from '@grafana/ui'; import { stateHistoryApi } from '../../../api/stateHistoryApi'; import { combineMatcherStrings } from '../../../utils/alertmanager'; diff --git a/public/app/features/alerting/unified/components/rules/state-history/common.ts b/public/app/features/alerting/unified/components/rules/state-history/common.ts index f797fa1f8a5..d6d924dccdf 100644 --- a/public/app/features/alerting/unified/components/rules/state-history/common.ts +++ b/public/app/features/alerting/unified/components/rules/state-history/common.ts @@ -7,6 +7,7 @@ export interface Line { current: GrafanaAlertStateWithReason; values?: Record; labels?: Record; + ruleUID?: string; } export interface LogRecord { diff --git a/public/app/features/alerting/unified/components/settings/__mocks__/api/alertmanager/provisioned/config/api/v1/alerts.json b/public/app/features/alerting/unified/components/settings/__mocks__/api/alertmanager/provisioned/config/api/v1/alerts.json new file mode 100644 index 00000000000..7d3a346d87d --- /dev/null +++ b/public/app/features/alerting/unified/components/settings/__mocks__/api/alertmanager/provisioned/config/api/v1/alerts.json @@ -0,0 +1,26 @@ +{ + "template_files": {}, + "alertmanager_config": { + "global": {}, + "receivers": [ + { + "name": "default" + }, + { + "name": "Telegram", + "telegram_configs": [ + { + "bot_token": "abc", + "chat_id": -123, + "disable_notifications": false, + "parse_mode": "MarkdownV2", + "send_resolved": true + } + ] + } + ], + "route": { + "receiver": "default" + } + } +} diff --git a/public/app/features/alerting/unified/components/settings/__mocks__/server.ts b/public/app/features/alerting/unified/components/settings/__mocks__/server.ts index 41ead147008..bb34baac74b 100644 --- a/public/app/features/alerting/unified/components/settings/__mocks__/server.ts +++ b/public/app/features/alerting/unified/components/settings/__mocks__/server.ts @@ -16,6 +16,7 @@ import { DataSourceType } from '../../../utils/datasource'; import internalAlertmanagerConfig from './api/alertmanager/grafana/config/api/v1/alerts.json'; import history from './api/alertmanager/grafana/config/history.json'; +import cloudAlertmanagerConfig from './api/alertmanager/provisioned/config/api/v1/alerts.json'; import vanillaAlertmanagerConfig from './api/alertmanager/vanilla prometheus/api/v2/status.json'; import datasources from './api/datasources.json'; import admin_config from './api/v1/ngalert/admin_config.json'; @@ -37,7 +38,7 @@ const mocks = { getAllDataSources: jest.mocked(config.getAllDataSources), }; -const mockDataSources = { +export const mockDataSources = { [EXTERNAL_VANILLA_ALERTMANAGER_UID]: mockDataSource({ uid: EXTERNAL_VANILLA_ALERTMANAGER_UID, name: EXTERNAL_VANILLA_ALERTMANAGER_UID, @@ -95,7 +96,12 @@ const createAlertmanagerConfigurationHandlers = () => { }; return [ - http.get(`/api/alertmanager/:name/config/api/v1/alerts`, () => HttpResponse.json(internalAlertmanagerConfig)), + http.get<{ name: string }>(`/api/alertmanager/:name/config/api/v1/alerts`, ({ params }) => { + if (params.name === 'grafana') { + return HttpResponse.json(internalAlertmanagerConfig); + } + return HttpResponse.json(cloudAlertmanagerConfig); + }), http.post(`/api/alertmanager/:name/config/api/v1/alerts`, async ({ request }) => { await delay(1000); // simulate some time @@ -108,7 +114,12 @@ const createAlertmanagerConfigurationHandlers = () => { return false; } - return (receiver.telegram_configs || []).some((config) => typeof config.parse_mode === 'object'); + const invalidParseMode = (receiver.telegram_configs || []).some( + (config) => typeof config.parse_mode === 'object' + ); + const invalidChatId = (receiver.telegram_configs || []).some((config) => Number(config.chat_id) >= 0); + + return invalidParseMode || invalidChatId; }); if (invalidConfig) { diff --git a/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts b/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts index 06ee6893057..0ff45bb14e9 100644 --- a/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts +++ b/public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts @@ -410,7 +410,7 @@ export const cloudNotifierTypes: Array> = [ }), option('chat_id', 'Chat ID', 'ID of the chat where to send the messages', { required: true, - setValueAs: (value) => (typeof value === 'string' ? parseInt(value, 10) : 0), + setValueAs: (value) => (typeof value === 'string' ? parseInt(value, 10) : value), }), option('message', 'Message', 'Message template', { placeholder: '{{ template "webex.default.message" .}}', diff --git a/public/app/features/alerting/unified/utils/rule-id.ts b/public/app/features/alerting/unified/utils/rule-id.ts index 3e6ebad8c64..b3785ffcaf5 100644 --- a/public/app/features/alerting/unified/utils/rule-id.ts +++ b/public/app/features/alerting/unified/utils/rule-id.ts @@ -240,7 +240,7 @@ export function hashRule(rule: Rule): string { throw new Error('only recording and alerting rules can be hashed'); } -function hashLabelsOrAnnotations(item: Labels | Annotations | undefined): string { +export function hashLabelsOrAnnotations(item: Labels | Annotations | undefined): string { return JSON.stringify(Object.entries(item || {}).sort((a, b) => a[0].localeCompare(b[0]))); } diff --git a/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx b/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx index 149ca72a03b..ee0579fbead 100644 --- a/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx +++ b/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx @@ -34,7 +34,7 @@ const RecentlyDeletedPage = memo(() => { }, [dispatch, stateManager]); return ( - + { - queryFulfilled.then(() => { - dispatch(refreshParents([dashboardUID])); - }); - }, }), }), }); diff --git a/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx b/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx index 8657244d90c..c1ceaa07289 100644 --- a/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx +++ b/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx @@ -3,16 +3,16 @@ import React, { useMemo } from 'react'; import { GrafanaTheme2 } from '@grafana/data/'; import { Button, useStyles2 } from '@grafana/ui'; +import { GENERAL_FOLDER_UID } from 'app/features/search/constants'; import appEvents from '../../../core/app_events'; import { Trans } from '../../../core/internationalization'; import { useDispatch } from '../../../types'; import { ShowModalReactEvent } from '../../../types/events'; -import { useRestoreDashboardMutation } from '../api/browseDashboardsAPI'; +import { useRestoreDashboardMutation } from '../../browse-dashboards/api/browseDashboardsAPI'; +import { clearFolders, setAllSelection, useActionSelectionState } from '../../browse-dashboards/state'; import { useRecentlyDeletedStateManager } from '../api/useRecentlyDeletedStateManager'; -import { setAllSelection, useActionSelectionState } from '../state'; - -import { RestoreModal } from './RestoreModal'; +import { RestoreModal } from '../components/RestoreModal'; export function RecentlyDeletedActions() { const styles = useStyles2(getStyles); @@ -36,9 +36,31 @@ export function RecentlyDeletedActions() { }; const onRestore = async () => { - const promises = selectedDashboards.map((uid) => restoreDashboard({ dashboardUID: uid })); + const resultsView = stateManager.state.result?.view.toArray(); + if (!resultsView) { + return; + } + + const promises = selectedDashboards.map((uid) => { + return restoreDashboard({ dashboardUID: uid }); + }); await Promise.all(promises); + + const parentUIDs = new Set(); + for (const uid of selectedDashboards) { + const foundItem = resultsView.find((v) => v.uid === uid); + if (!foundItem) { + continue; + } + + // Search API returns items with no parent with a location of 'general', so we + // need to convert that back to undefined + const folderUID = foundItem.location === GENERAL_FOLDER_UID ? undefined : foundItem.location; + parentUIDs.add(folderUID); + } + dispatch(clearFolders(Array.from(parentUIDs))); + onActionComplete(); }; diff --git a/public/app/features/browse-dashboards/state/reducers.ts b/public/app/features/browse-dashboards/state/reducers.ts index e506748dcd6..045be88a490 100644 --- a/public/app/features/browse-dashboards/state/reducers.ts +++ b/public/app/features/browse-dashboards/state/reducers.ts @@ -200,3 +200,18 @@ export function setAllSelection( } } } + +export function clearFolders(state: BrowseDashboardsState, action: PayloadAction>) { + const folderUIDs = Array.isArray(action.payload) ? action.payload : [action.payload]; + + for (const folderUID of folderUIDs) { + if (!folderUID) { + state.rootItems = undefined; + } else { + state.childrenByParentUID[folderUID] = undefined; + + // close the folder to require it to be refetched next time its opened + state.openFolders[folderUID] = false; + } + } +} diff --git a/public/app/features/browse-dashboards/state/slice.ts b/public/app/features/browse-dashboards/state/slice.ts index 55254eb78d1..8c2172e442f 100644 --- a/public/app/features/browse-dashboards/state/slice.ts +++ b/public/app/features/browse-dashboards/state/slice.ts @@ -32,7 +32,8 @@ const browseDashboardsSlice = createSlice({ export const browseDashboardsReducer = browseDashboardsSlice.reducer; -export const { setFolderOpenState, setItemSelectionState, setAllSelection } = browseDashboardsSlice.actions; +export const { setFolderOpenState, setItemSelectionState, setAllSelection, clearFolders } = + browseDashboardsSlice.actions; export default { browseDashboards: browseDashboardsReducer, diff --git a/public/app/features/connections/__mocks__/store.navIndex.mock.ts b/public/app/features/connections/__mocks__/store.navIndex.mock.ts index 217f9891655..f135bf3cf83 100644 --- a/public/app/features/connections/__mocks__/store.navIndex.mock.ts +++ b/public/app/features/connections/__mocks__/store.navIndex.mock.ts @@ -149,6 +149,13 @@ export const navIndex: NavIndex = { icon: 'layer-group', url: '/alerting/groups', }, + { + id: 'history', + text: 'History', + subTitle: 'Alert state history', + icon: 'history', + url: '/alerting/history', + }, { id: 'alerting-admin', text: 'Settings', diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx index 3fe7ba89db6..51777c2fd7a 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx @@ -11,9 +11,11 @@ import { config, getPluginLinkExtensions, locationService, setPluginImportUtils import { VizPanel } from '@grafana/scenes'; import { Dashboard } from '@grafana/schema'; import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; +import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import store from 'app/core/store'; import { DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { DASHBOARD_FROM_LS_KEY } from 'app/features/dashboard/state/initDashboard'; +import { DashboardRoutes } from 'app/types'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; @@ -24,6 +26,11 @@ jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), setPluginExtensionGetter: jest.fn(), getPluginLinkExtensions: jest.fn(), + getBackendSrv: () => { + return { + get: jest.fn().mockResolvedValue({ dashboard: simpleDashboard, meta: { url: '' } }), + }; + }, getDataSourceSrv: () => { return { get: jest.fn().mockResolvedValue({}), @@ -37,12 +44,19 @@ jest.mock('@grafana/runtime', () => ({ const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions); -function setup() { +function setup({ routeProps }: { routeProps?: Partial } = {}) { const context = getGrafanaContextMock(); + const defaultRouteProps = getRouteComponentProps(); const props: Props = { - ...getRouteComponentProps(), + ...defaultRouteProps, + match: { + ...defaultRouteProps.match, + params: { + uid: 'my-dash-uid', + }, + }, + ...routeProps, }; - props.match.params.uid = 'my-dash-uid'; const renderResult = render( @@ -258,14 +272,39 @@ describe('DashboardScenePage', () => { }); describe('home page', () => { - it('should not show controls', async () => { + it('should render the dashboard when the route is home', async () => { + setup({ + routeProps: { + route: { + ...getRouteComponentProps().route, + routeName: DashboardRoutes.Home, + }, + match: { + ...getRouteComponentProps().match, + path: '/', + params: {}, + }, + }, + }); + + await waitForDashbordToRender(); + + expect(await screen.findByTitle('Panel A')).toBeInTheDocument(); + expect(await screen.findByText('Content A')).toBeInTheDocument(); + + expect(await screen.findByTitle('Panel B')).toBeInTheDocument(); + expect(await screen.findByText('Content B')).toBeInTheDocument(); + }); + + it('should show controls', async () => { getDashboardScenePageStateManager().clearDashboardCache(); loadDashboardMock.mockClear(); loadDashboardMock.mockResolvedValue({ dashboard: { panels: [] }, meta: {} }); setup(); - await waitFor(() => expect(screen.queryByText('Refresh')).not.toBeInTheDocument()); + await waitFor(() => expect(screen.queryByText('Refresh')).toBeInTheDocument()); + await waitFor(() => expect(screen.queryByText('Last 6 hours')).toBeInTheDocument()); }); }); }); diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx index a1b4f0f46f3..7159235f8c1 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx @@ -19,7 +19,9 @@ export interface Props extends GrafanaRouteComponentProps { const loader = new DashboardScenePageStateManager({}); await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); - expect(loader.state.dashboard).toBeUndefined(); + expect(loader.state.dashboard).toBeDefined(); expect(loader.state.isLoading).toBe(false); - expect(loader.state.loadError).toBe('Error: Dashboard not found'); - }); - - it('should handle home dashboard redirect', async () => { - setBackendSrv({ - get: () => Promise.resolve({ redirectUri: '/d/asd' }), - } as unknown as BackendSrv); - - const loader = new DashboardScenePageStateManager({}); - await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home }); - - expect(loader.state.dashboard).toBeUndefined(); - expect(loader.state.loadError).toBeUndefined(); + expect(loader.state.loadError).toBe('Dashboard not found'); }); it('shoud fetch dashboard from local storage and remove it after if it exists', async () => { @@ -94,6 +82,37 @@ describe('DashboardScenePageStateManager', () => { expect(loader.state.isLoading).toBe(false); }); + describe('Home dashboard', () => { + it('should handle home dashboard redirect', async () => { + setBackendSrv({ + get: () => Promise.resolve({ redirectUri: '/d/asd' }), + } as unknown as BackendSrv); + + const loader = new DashboardScenePageStateManager({}); + await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home }); + + expect(loader.state.dashboard).toBeUndefined(); + expect(loader.state.loadError).toBeUndefined(); + }); + + it('should handle invalid home dashboard request', async () => { + setBackendSrv({ + get: () => + Promise.reject({ + status: 500, + data: { message: 'Failed to load home dashboard' }, + }), + } as unknown as BackendSrv); + + const loader = new DashboardScenePageStateManager({}); + await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home }); + + expect(loader.state.dashboard).toBeDefined(); + expect(loader.state.dashboard?.state.title).toEqual('Failed to load home dashboard'); + expect(loader.state.loadError).toEqual('Failed to load home dashboard'); + }); + }); + describe('New dashboards', () => { it('Should have new empty model with meta.isNew and should not be cached', async () => { const loader = new DashboardScenePageStateManager({}); diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index f0235de5a61..551d4208308 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -1,10 +1,14 @@ import { locationUtil } from '@grafana/data'; import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; +import { defaultDashboard } from '@grafana/schema'; import { StateManagerBase } from 'app/core/services/StateManagerBase'; import { default as localStorageStore } from 'app/core/store'; +import { getMessageFromError } from 'app/core/utils/errors'; import { startMeasure, stopMeasure } from 'app/core/utils/metrics'; import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; +import { DashboardModel } from 'app/features/dashboard/state'; +import { emitDashboardViewEvent } from 'app/features/dashboard/state/analyticsProcessor'; import { DASHBOARD_FROM_LS_KEY, removeDashboardToFetchFromLocalStorage, @@ -15,7 +19,10 @@ import { DashboardDTO, DashboardRoutes } from 'app/types'; import { PanelEditor } from '../panel-edit/PanelEditor'; import { DashboardScene } from '../scene/DashboardScene'; import { buildNewDashboardSaveModel } from '../serialization/buildNewDashboardSaveModel'; -import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; +import { + createDashboardSceneFromDashboardModel, + transformSaveModelToScene, +} from '../serialization/transformSaveModelToScene'; import { restoreDashboardStateFromLocalStorage } from '../utils/dashboardSessionState'; import { updateNavModel } from './utils'; @@ -142,7 +149,6 @@ export class DashboardScenePageStateManager extends StateManagerBase

${msg}

`, + mode: 'html', + }, + title: '', + transparent: true, + type: 'text', + }, + ], + }, + { canSave: false, canEdit: false } + ) + ); +} diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx new file mode 100644 index 00000000000..30b391e0aba --- /dev/null +++ b/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx @@ -0,0 +1,171 @@ +import { render } from '@testing-library/react'; +import React from 'react'; + +import { selectors } from '@grafana/e2e-selectors'; +import { SceneDataLayerControls, SceneVariableSet, TextBoxVariable, VariableValueSelectors } from '@grafana/scenes'; + +import { DashboardControls, DashboardControlsState } from './DashboardControls'; +import { DashboardScene } from './DashboardScene'; + +describe('DashboardControls', () => { + describe('Given a standard scene', () => { + it('should initialize with default values', () => { + const scene = buildTestScene(); + expect(scene.state.variableControls).toEqual([]); + expect(scene.state.timePicker).toBeDefined(); + expect(scene.state.refreshPicker).toBeDefined(); + }); + + it('should return if time controls are hidden', () => { + const scene = buildTestScene({ hideTimeControls: false, hideVariableControls: false, hideLinksControls: false }); + expect(scene.hasControls()).toBeTruthy(); + scene.setState({ hideTimeControls: true }); + expect(scene.hasControls()).toBeTruthy(); + scene.setState({ hideVariableControls: true, hideLinksControls: true }); + expect(scene.hasControls()).toBeFalsy(); + }); + }); + + describe('Component', () => { + it('should render', () => { + const scene = buildTestScene(); + expect(() => { + render(); + }).not.toThrow(); + }); + + it('should render visible controls', async () => { + const scene = buildTestScene({ + variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()], + }); + const renderer = render(); + + expect(await renderer.findByTestId(selectors.pages.Dashboard.Controls)).toBeInTheDocument(); + expect(await renderer.findByTestId(selectors.components.DashboardLinks.container)).toBeInTheDocument(); + expect(await renderer.findByTestId(selectors.components.TimePicker.openButton)).toBeInTheDocument(); + expect(await renderer.findByTestId(selectors.components.RefreshPicker.runButtonV2)).toBeInTheDocument(); + expect(await renderer.findByTestId(selectors.pages.Dashboard.SubMenu.submenuItem)).toBeInTheDocument(); + }); + + it('should render with hidden controls', async () => { + const scene = buildTestScene({ + hideTimeControls: true, + hideVariableControls: true, + hideLinksControls: true, + variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()], + }); + const renderer = render(); + + expect(await renderer.queryByTestId(selectors.pages.Dashboard.Controls)).not.toBeInTheDocument(); + }); + }); + + describe('UrlSync', () => { + it('should return keys', () => { + const scene = buildTestScene(); + // @ts-expect-error + expect(scene._urlSync.getKeys()).toEqual(['_dash.hideTimePicker', '_dash.hideVariables', '_dash.hideLinks']); + }); + + it('should return url state', () => { + const scene = buildTestScene(); + expect(scene.getUrlState()).toEqual({ + '_dash.hideTimePicker': undefined, + '_dash.hideVariables': undefined, + '_dash.hideLinks': undefined, + }); + scene.setState({ + hideTimeControls: true, + hideVariableControls: true, + hideLinksControls: true, + }); + expect(scene.getUrlState()).toEqual({ + '_dash.hideTimePicker': 'true', + '_dash.hideVariables': 'true', + '_dash.hideLinks': 'true', + }); + }); + + it('should update from url', () => { + const scene = buildTestScene(); + scene.updateFromUrl({ + '_dash.hideTimePicker': 'true', + '_dash.hideVariables': 'true', + '_dash.hideLinks': 'true', + }); + expect(scene.state.hideTimeControls).toBeTruthy(); + expect(scene.state.hideVariableControls).toBeTruthy(); + expect(scene.state.hideLinksControls).toBeTruthy(); + scene.updateFromUrl({ + '_dash.hideTimePicker': '', + '_dash.hideVariables': '', + '_dash.hideLinks': '', + }); + expect(scene.state.hideTimeControls).toBeTruthy(); + expect(scene.state.hideVariableControls).toBeTruthy(); + expect(scene.state.hideLinksControls).toBeTruthy(); + }); + + it('should not override state if no new state comes from url', () => { + const scene = buildTestScene({ hideTimeControls: true, hideVariableControls: true, hideLinksControls: true }); + scene.updateFromUrl({}); + expect(scene.state.hideTimeControls).toBeTruthy(); + expect(scene.state.hideVariableControls).toBeTruthy(); + expect(scene.state.hideLinksControls).toBeTruthy(); + }); + + it('should not call setState if no changes', () => { + const scene = buildTestScene(); + const setState = jest.spyOn(scene, 'setState'); + scene.updateFromUrl({ + '_dash.hideTimePicker': 'true', + '_dash.hideVariables': 'true', + '_dash.hideLinks': 'true', + }); + scene.updateFromUrl({ + '_dash.hideTimePicker': 'true', + '_dash.hideVariables': 'true', + '_dash.hideLinks': 'true', + }); + expect(setState).toHaveBeenCalledTimes(1); + }); + }); +}); + +function buildTestScene(state?: Partial): DashboardControls { + const variable = new TextBoxVariable({ + name: 'A', + label: 'A', + description: 'A', + type: 'textbox', + value: 'Text', + }); + const dashboard = new DashboardScene({ + uid: 'A', + links: [ + { + title: 'Link', + url: 'http://localhost:3000/$A', + type: 'link', + asDropdown: false, + icon: '', + includeVars: true, + keepTime: true, + tags: [], + targetBlank: false, + tooltip: 'Link', + }, + ], + $variables: new SceneVariableSet({ + variables: [variable], + }), + controls: new DashboardControls({ + ...state, + }), + }); + + dashboard.activate(); + variable.activate(); + + return dashboard.state.controls as DashboardControls; +} diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index fb46c138d4a..5614c57bfd4 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -1,7 +1,7 @@ import { css, cx } from '@emotion/css'; import React from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2, VariableHide } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { SceneObjectState, @@ -12,6 +12,9 @@ import { SceneRefreshPicker, SceneDebugger, VariableDependencyConfig, + sceneGraph, + SceneObjectUrlSyncConfig, + SceneObjectUrlValues, } from '@grafana/scenes'; import { Box, Stack, useStyles2 } from '@grafana/ui'; @@ -20,12 +23,15 @@ import { getDashboardSceneFor } from '../utils/utils'; import { DashboardLinksControls } from './DashboardLinksControls'; -interface DashboardControlsState extends SceneObjectState { +export interface DashboardControlsState extends SceneObjectState { variableControls: SceneObject[]; timePicker: SceneTimePicker; refreshPicker: SceneRefreshPicker; hideTimeControls?: boolean; + hideVariableControls?: boolean; + hideLinksControls?: boolean; } + export class DashboardControls extends SceneObjectBase { static Component = DashboardControlsRenderer; @@ -33,6 +39,35 @@ export class DashboardControls extends SceneObjectBase { onAnyVariableChanged: this._onAnyVariableChanged.bind(this), }); + protected _urlSync = new SceneObjectUrlSyncConfig(this, { + keys: ['_dash.hideTimePicker', '_dash.hideVariables', '_dash.hideLinks'], + }); + + getUrlState() { + return { + '_dash.hideTimePicker': this.state.hideTimeControls ? 'true' : undefined, + '_dash.hideVariables': this.state.hideVariableControls ? 'true' : undefined, + '_dash.hideLinks': this.state.hideLinksControls ? 'true' : undefined, + }; + } + + updateFromUrl(values: SceneObjectUrlValues) { + const update: Partial = {}; + + update.hideTimeControls = + values['_dash.hideTimePicker'] === 'true' || values['_dash.hideTimePicker'] === '' || this.state.hideTimeControls; + update.hideVariableControls = + values['_dash.hideVariables'] === 'true' || + values['_dash.hideVariables'] === '' || + this.state.hideVariableControls; + update.hideLinksControls = + values['_dash.hideLinks'] === 'true' || values['_dash.hideLinks'] === '' || this.state.hideLinksControls; + + if (Object.entries(update).some(([k, v]) => v !== this.state[k as keyof DashboardControlsState])) { + this.setState(update); + } + } + public constructor(state: Partial) { super({ variableControls: [], @@ -51,26 +86,42 @@ export class DashboardControls extends SceneObjectBase { this.forceRender(); } } + + public hasControls(): boolean { + const hasVariables = sceneGraph + .getVariables(this) + ?.state.variables.some((v) => v.state.hide !== VariableHide.hideVariable); + const hasAnnotations = sceneGraph.getDataLayers(this).some((d) => d.state.isEnabled && !d.state.isHidden); + const hasLinks = getDashboardSceneFor(this).state.links?.length > 0; + const hideLinks = this.state.hideLinksControls || !hasLinks; + const hideVariables = this.state.hideVariableControls || (!hasAnnotations && !hasVariables); + const hideTimePicker = this.state.hideTimeControls; + + return !(hideVariables && hideLinks && hideTimePicker); + } } function DashboardControlsRenderer({ model }: SceneComponentProps) { - const { variableControls, refreshPicker, timePicker, hideTimeControls } = model.useState(); + const { variableControls, refreshPicker, timePicker, hideTimeControls, hideVariableControls, hideLinksControls } = + model.useState(); const dashboard = getDashboardSceneFor(model); const { links, meta, editPanel } = dashboard.useState(); const styles = useStyles2(getStyles); const showDebugger = location.search.includes('scene-debugger'); + if (!model.hasControls()) { + return null; + } + return (
- {variableControls.map((c) => ( - - ))} + {!hideVariableControls && variableControls.map((c) => )} - {!editPanel && } + {!hideLinksControls && !editPanel && } {editPanel && } {!hideTimeControls && ( diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 13e6ec7dff7..83b0edbc9c3 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -35,7 +35,7 @@ import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { deleteDashboard } from 'app/features/manage-dashboards/state/actions'; import { VariablesChanged } from 'app/features/variables/types'; -import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types'; +import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types'; import { ShowConfirmModalEvent } from 'app/types/events'; import { PanelEditor } from '../panel-edit/PanelEditor'; @@ -125,6 +125,8 @@ export interface DashboardSceneState extends SceneObjectState { isEmpty?: boolean; /** Scene object that handles the scopes selector */ scopes?: ScopesScene; + /** Kiosk mode */ + kioskMode?: KioskMode; } export class DashboardScene extends SceneObjectBase { diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index 6e9a5d659bb..553710923cf 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -24,6 +24,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps; const withPanels = ( -
+
); @@ -49,14 +50,14 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps {scopes && } - {!isHomePage && controls && ( + {controls && hasControls && (
@@ -119,6 +120,9 @@ function getStyles(theme: GrafanaTheme2) { flexGrow: 0, gridArea: 'controls', padding: theme.spacing(2), + ':empty': { + display: 'none', + }, }), controlsWrapperWithScopes: css({ padding: theme.spacing(2, 2, 2, 0), @@ -139,7 +143,11 @@ function getStyles(theme: GrafanaTheme2) { flexGrow: 1, display: 'flex', gap: '8px', - marginBottom: theme.spacing(2), + paddingBottom: theme.spacing(2), + boxSizing: 'border-box', + }), + bodyWithoutControls: css({ + paddingTop: theme.spacing(2), }), }; } diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts index 984b7c488b0..c1a7172044f 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts @@ -1,6 +1,7 @@ import { AppEvents } from '@grafana/data'; import { SceneGridLayout, SceneQueryRunner, VizPanel } from '@grafana/scenes'; import appEvents from 'app/core/app_events'; +import { KioskMode } from 'app/types'; import { DashboardGridItem } from './DashboardGridItem'; import { DashboardScene } from './DashboardScene'; @@ -39,6 +40,29 @@ describe('DashboardSceneUrlSync', () => { (scene.state.body as SceneGridLayout).setState({ UNSAFE_fitPanels: true }); expect(scene.urlSync?.getUrlState().autofitpanels).toBe('true'); }); + + it('Should set kiosk mode when url has kiosk', () => { + const scene = buildTestScene(); + + scene.urlSync?.updateFromUrl({ kiosk: 'invalid' }); + expect(scene.state.kioskMode).toBe(undefined); + scene.urlSync?.updateFromUrl({ kiosk: '' }); + expect(scene.state.kioskMode).toBe(KioskMode.Full); + scene.urlSync?.updateFromUrl({ kiosk: 'tv' }); + expect(scene.state.kioskMode).toBe(KioskMode.TV); + scene.urlSync?.updateFromUrl({ kiosk: 'true' }); + expect(scene.state.kioskMode).toBe(KioskMode.Full); + }); + + it('Should get the kiosk mode from the scene state', () => { + const scene = buildTestScene(); + + expect(scene.urlSync?.getUrlState().kiosk).toBe(undefined); + scene.setState({ kioskMode: KioskMode.TV }); + expect(scene.urlSync?.getUrlState().kiosk).toBe(KioskMode.TV); + scene.setState({ kioskMode: KioskMode.Full }); + expect(scene.urlSync?.getUrlState().kiosk).toBe(''); + }); }); describe('entering edit mode', () => { diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index 49fbd51be06..fb891974cfe 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -11,6 +11,7 @@ import { VizPanel, } from '@grafana/scenes'; import appEvents from 'app/core/app_events'; +import { KioskMode } from 'app/types'; import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; @@ -28,7 +29,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { constructor(private _scene: DashboardScene) {} getKeys(): string[] { - return ['inspect', 'viewPanel', 'editPanel', 'editview', 'autofitpanels']; + return ['inspect', 'viewPanel', 'editPanel', 'editview', 'autofitpanels', 'kiosk']; } getUrlState(): SceneObjectUrlValues { @@ -39,6 +40,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { viewPanel: state.viewPanelScene?.getUrlKey(), editview: state.editview?.getUrlKey(), editPanel: state.editPanel?.getUrlKey() || undefined, + kiosk: state.kioskMode === KioskMode.Full ? '' : state.kioskMode === KioskMode.TV ? 'tv' : undefined, }; } @@ -159,6 +161,14 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { } } + if (typeof values.kiosk === 'string') { + if (values.kiosk === 'true' || values.kiosk === '') { + update.kioskMode = KioskMode.Full; + } else if (values.kiosk === 'tv') { + update.kioskMode = KioskMode.TV; + } + } + if (Object.keys(update).length > 0) { this._scene.setState(update); } diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 922d2d6543b..18a6a336ccf 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -566,7 +566,12 @@ export function ToolbarActions({ dashboard }: Props) { Save dashboard -