merge main
This commit is contained in:
+8
-1
@@ -491,6 +491,9 @@ exports[`better eslint`] = {
|
|||||||
"packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.ts:5381": [
|
"packages/grafana-runtime/src/services/pluginExtensions/usePluginExtensions.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
],
|
],
|
||||||
|
"packages/grafana-runtime/src/services/pluginExtensions/usePluginFunctions.ts:5381": [
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
|
],
|
||||||
"packages/grafana-runtime/src/utils/DataSourceWithBackend.ts:5381": [
|
"packages/grafana-runtime/src/utils/DataSourceWithBackend.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
@@ -5541,7 +5544,8 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
|
||||||
],
|
],
|
||||||
"public/app/features/plugins/admin/components/PluginDetailsPanel.tsx:5381": [
|
"public/app/features/plugins/admin/components/PluginDetailsPanel.tsx:5381": [
|
||||||
[0, 0, 0, "\'@grafana/runtime/src/components/PluginPage\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]
|
[0, 0, 0, "\'@grafana/runtime/src/components/PluginPage\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
|
||||||
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||||
],
|
],
|
||||||
"public/app/features/plugins/admin/components/PluginDetailsSignature.tsx:5381": [
|
"public/app/features/plugins/admin/components/PluginDetailsSignature.tsx:5381": [
|
||||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||||
@@ -5645,6 +5649,9 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "\'@grafana/runtime/src/services/pluginExtensions/getPluginExtensions\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
|
[0, 0, 0, "\'@grafana/runtime/src/services/pluginExtensions/getPluginExtensions\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||||
],
|
],
|
||||||
|
"public/app/features/plugins/extensions/usePluginFunctions.tsx:5381": [
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
|
],
|
||||||
"public/app/features/plugins/extensions/usePluginLinks.tsx:5381": [
|
"public/app/features/plugins/extensions/usePluginLinks.tsx:5381": [
|
||||||
[0, 0, 0, "\'@grafana/runtime/src/services/pluginExtensions/getPluginExtensions\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]
|
[0, 0, 0, "\'@grafana/runtime/src/services/pluginExtensions/getPluginExtensions\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -244,6 +244,7 @@
|
|||||||
/devenv/dev-dashboards/extensions/ @grafana/plugins-platform-frontend
|
/devenv/dev-dashboards/extensions/ @grafana/plugins-platform-frontend
|
||||||
|
|
||||||
/devenv/docker/blocks/alert_webhook_listener/ @grafana/alerting-backend
|
/devenv/docker/blocks/alert_webhook_listener/ @grafana/alerting-backend
|
||||||
|
/devenv/docker/blocks/stateful_webhook/ @grafana/alerting-backend
|
||||||
/devenv/docker/blocks/caddy_tls/ @grafana/alerting-backend
|
/devenv/docker/blocks/caddy_tls/ @grafana/alerting-backend
|
||||||
/devenv/docker/blocks/clickhouse/ @grafana/partner-datasources
|
/devenv/docker/blocks/clickhouse/ @grafana/partner-datasources
|
||||||
/devenv/docker/blocks/collectd/ @grafana/observability-metrics
|
/devenv/docker/blocks/collectd/ @grafana/observability-metrics
|
||||||
|
|||||||
@@ -271,6 +271,14 @@ test-go-integration-alertmanager: ## Run integration tests for the remote alertm
|
|||||||
AM_URL=http://localhost:8080 AM_TENANT_ID=test \
|
AM_URL=http://localhost:8080 AM_TENANT_ID=test \
|
||||||
$(GO) test $(GO_RACE_FLAG) -count=1 -run "^TestIntegrationRemoteAlertmanager" -covermode=atomic -timeout=5m ./pkg/services/ngalert/...
|
$(GO) test $(GO_RACE_FLAG) -count=1 -run "^TestIntegrationRemoteAlertmanager" -covermode=atomic -timeout=5m ./pkg/services/ngalert/...
|
||||||
|
|
||||||
|
.PHONY: test-go-integration-grafana-alertmanager
|
||||||
|
test-go-integration-grafana-alertmanager: ## Run integration tests for the grafana alertmanager
|
||||||
|
@echo "test grafana alertmanager integration tests"
|
||||||
|
@export GRAFANA_VERSION=11.5.0-81938; \
|
||||||
|
$(GO) run tools/setup_grafana_alertmanager_integration_test_images.go; \
|
||||||
|
$(GO) clean -testcache; \
|
||||||
|
$(GO) test $(GO_RACE_FLAG) -count=1 -run "^TestAlertmanagerIntegration" -covermode=atomic -timeout=10m ./pkg/tests/alertmanager/...
|
||||||
|
|
||||||
.PHONY: test-go-integration-postgres
|
.PHONY: test-go-integration-postgres
|
||||||
test-go-integration-postgres: devenv-postgres ## Run integration tests for postgres backend with flags.
|
test-go-integration-postgres: devenv-postgres ## Run integration tests for postgres backend with flags.
|
||||||
@echo "test backend integration postgres tests"
|
@echo "test backend integration postgres tests"
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
FROM golang:1.23.5
|
||||||
|
|
||||||
|
ADD main.go /go/src/webhook/main.go
|
||||||
|
|
||||||
|
WORKDIR /go/src/webhook
|
||||||
|
|
||||||
|
RUN mkdir /tmp/logs
|
||||||
|
RUN go build -o /bin main.go
|
||||||
|
|
||||||
|
ENV PORT=8080
|
||||||
|
|
||||||
|
ENTRYPOINT [ "/bin/main" ]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
stateful_webhook:
|
||||||
|
build:
|
||||||
|
context: docker/blocks/stateful_webhook
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
TimeNow time.Time `json:"timeNow"`
|
||||||
|
StartsAt time.Time `json:"startsAt"`
|
||||||
|
Node string `json:"node"`
|
||||||
|
DeltaLastSeconds float64 `json:"deltaLastSeconds"`
|
||||||
|
DeltaStartSeconds float64 `json:"deltaStartSeconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Notification struct {
|
||||||
|
Alerts []Alert `json:"alerts"`
|
||||||
|
CommonAnnotations map[string]string `json:"commonAnnotations"`
|
||||||
|
CommonLabels map[string]string `json:"commonLabels"`
|
||||||
|
ExternalURL string `json:"externalURL"`
|
||||||
|
GroupKey string `json:"groupKey"`
|
||||||
|
GroupLabels map[string]string `json:"groupLabels"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
OrgID int `json:"orgId"`
|
||||||
|
Receiver string `json:"receiver"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
TruncatedAlerts int `json:"truncatedAlerts"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Alert struct {
|
||||||
|
Annotations map[string]string `json:"annotations"`
|
||||||
|
DashboardURL string `json:"dashboardURL"`
|
||||||
|
StartsAt time.Time `json:"startsAt"`
|
||||||
|
EndsAt time.Time `json:"endsAt"`
|
||||||
|
Fingerprint string `json:"fingerprint"`
|
||||||
|
GeneratorURL string `json:"generatorURL"`
|
||||||
|
Labels map[string]string `json:"labels"`
|
||||||
|
PanelURL string `json:"panelURL"`
|
||||||
|
SilenceURL string `json:"silenceURL"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
ValueString string `json:"valueString"`
|
||||||
|
Values map[string]any `json:"values"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationHandler struct {
|
||||||
|
startedAt time.Time
|
||||||
|
stats map[string]int
|
||||||
|
hist []Event
|
||||||
|
m sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationHandler() *NotificationHandler {
|
||||||
|
return &NotificationHandler{
|
||||||
|
startedAt: time.Now(),
|
||||||
|
stats: make(map[string]int),
|
||||||
|
hist: make([]Event, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ah *NotificationHandler) Notify(w http.ResponseWriter, r *http.Request) {
|
||||||
|
b, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n := Notification{}
|
||||||
|
if err := json.Unmarshal(b, &n); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("got notification from: %s. a: %v", r.RemoteAddr, n)
|
||||||
|
|
||||||
|
ah.m.Lock()
|
||||||
|
defer ah.m.Unlock()
|
||||||
|
|
||||||
|
addr := r.RemoteAddr
|
||||||
|
if split := strings.Split(r.RemoteAddr, ":"); len(split) > 0 {
|
||||||
|
addr = split[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
a := n.Alerts[0]
|
||||||
|
|
||||||
|
timeNow := time.Now()
|
||||||
|
|
||||||
|
ah.stats[n.Status]++
|
||||||
|
|
||||||
|
var d time.Duration
|
||||||
|
if len(ah.hist) > 0 {
|
||||||
|
last := ah.hist[len(ah.hist)-1]
|
||||||
|
d = timeNow.Sub(last.TimeNow)
|
||||||
|
}
|
||||||
|
|
||||||
|
ah.hist = append(ah.hist, Event{
|
||||||
|
Status: n.Status,
|
||||||
|
StartsAt: a.StartsAt,
|
||||||
|
TimeNow: timeNow,
|
||||||
|
Node: addr,
|
||||||
|
DeltaLastSeconds: d.Seconds(),
|
||||||
|
DeltaStartSeconds: timeNow.Sub(ah.startedAt).Seconds(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ah *NotificationHandler) GetNotifications(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
ah.m.Lock()
|
||||||
|
defer ah.m.Unlock()
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
res, err := json.MarshalIndent(map[string]any{"stats": ah.stats, "history": ah.hist}, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
//nolint:errcheck
|
||||||
|
w.Write([]byte(`{"error":"failed to marshal alerts"}`))
|
||||||
|
log.Printf("failed to marshal alerts: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("requested current state\n%v\n", string(res))
|
||||||
|
|
||||||
|
_, err = w.Write(res)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to write response: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ah := NewNotificationHandler()
|
||||||
|
|
||||||
|
http.HandleFunc("/ready", func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/notify", ah.Notify)
|
||||||
|
http.HandleFunc("/notifications", ah.GetNotifications)
|
||||||
|
|
||||||
|
log.Println("Listening")
|
||||||
|
//nolint:errcheck
|
||||||
|
http.ListenAndServe("0.0.0.0:8080", nil)
|
||||||
|
}
|
||||||
@@ -221,6 +221,8 @@ require (
|
|||||||
github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-search-and-storage
|
github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-search-and-storage
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cel.dev/expr v0.19.0 // indirect
|
cel.dev/expr v0.19.0 // indirect
|
||||||
cloud.google.com/go v0.116.0 // indirect
|
cloud.google.com/go v0.116.0 // indirect
|
||||||
|
|||||||
@@ -1310,6 +1310,8 @@ github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk
|
|||||||
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM=
|
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM=
|
||||||
github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I=
|
github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I=
|
||||||
github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
|
github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
|
||||||
|
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA=
|
||||||
|
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
|
||||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
@@ -1544,6 +1546,8 @@ github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 h1:jxJJ5z0GxqhWFbQU
|
|||||||
github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447/go.mod h1:IxsY6mns6Q5sAnWcrptrgUrSglTZJXH/kXr9nbpb/9I=
|
github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447/go.mod h1:IxsY6mns6Q5sAnWcrptrgUrSglTZJXH/kXr9nbpb/9I=
|
||||||
github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e h1:UlEET0InuoFautfaFp8lDrNF7rPHYXuBMrzwWx9XqFY=
|
github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e h1:UlEET0InuoFautfaFp8lDrNF7rPHYXuBMrzwWx9XqFY=
|
||||||
github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e/go.mod h1:IGRj8oOoxwJbHBYl1+OhS9UjQR0dv6SQOep7HqmtyFU=
|
github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e/go.mod h1:IGRj8oOoxwJbHBYl1+OhS9UjQR0dv6SQOep7HqmtyFU=
|
||||||
|
github.com/grafana/grafana-api-golang-client v0.27.0 h1:zIwMXcbCB4n588i3O2N6HfNcQogCNTd/vPkEXTr7zX8=
|
||||||
|
github.com/grafana/grafana-api-golang-client v0.27.0/go.mod h1:uNLZEmgKtTjHBtCQMwNn3qsx2mpMb8zU+7T4Xv3NR9Y=
|
||||||
github.com/grafana/grafana-app-sdk v0.31.0 h1:/mFCcx+YqG8cWAi9hePDJQxIdtXDClDIDRgZwHkksFk=
|
github.com/grafana/grafana-app-sdk v0.31.0 h1:/mFCcx+YqG8cWAi9hePDJQxIdtXDClDIDRgZwHkksFk=
|
||||||
github.com/grafana/grafana-app-sdk v0.31.0/go.mod h1:Xw00NL7qpRLo5r3Gn48Bl1Xn2n4eUDI5pYf/wMufKWs=
|
github.com/grafana/grafana-app-sdk v0.31.0/go.mod h1:Xw00NL7qpRLo5r3Gn48Bl1Xn2n4eUDI5pYf/wMufKWs=
|
||||||
github.com/grafana/grafana-app-sdk/logging v0.30.0 h1:K/P/bm7Cp7Di4tqIJ3EQz2+842JozQGRaz62r95ApME=
|
github.com/grafana/grafana-app-sdk/logging v0.30.0 h1:K/P/bm7Cp7Di4tqIJ3EQz2+842JozQGRaz62r95ApME=
|
||||||
|
|||||||
@@ -1456,6 +1456,7 @@ github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDs
|
|||||||
github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
|
github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
|
||||||
github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg=
|
github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg=
|
||||||
github.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
|
github.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
|
||||||
|
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
|
||||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
|
|||||||
@@ -549,6 +549,7 @@ export {
|
|||||||
type PluginExtensionLink,
|
type PluginExtensionLink,
|
||||||
type PluginExtensionComponent,
|
type PluginExtensionComponent,
|
||||||
type PluginExtensionConfig,
|
type PluginExtensionConfig,
|
||||||
|
type PluginExtensionFunction,
|
||||||
type PluginExtensionLinkConfig,
|
type PluginExtensionLinkConfig,
|
||||||
type PluginExtensionComponentConfig,
|
type PluginExtensionComponentConfig,
|
||||||
type PluginExtensionEventHelpers,
|
type PluginExtensionEventHelpers,
|
||||||
@@ -559,6 +560,7 @@ export {
|
|||||||
type PluginExtensionExposedComponentConfig,
|
type PluginExtensionExposedComponentConfig,
|
||||||
type PluginExtensionAddedComponentConfig,
|
type PluginExtensionAddedComponentConfig,
|
||||||
type PluginExtensionAddedLinkConfig,
|
type PluginExtensionAddedLinkConfig,
|
||||||
|
type PluginExtensionAddedFunctionConfig,
|
||||||
} from './types/pluginExtensions';
|
} from './types/pluginExtensions';
|
||||||
export {
|
export {
|
||||||
type ScopeDashboardBindingSpec,
|
type ScopeDashboardBindingSpec,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
PluginExtensionExposedComponentConfig,
|
PluginExtensionExposedComponentConfig,
|
||||||
PluginExtensionAddedComponentConfig,
|
PluginExtensionAddedComponentConfig,
|
||||||
PluginExtensionAddedLinkConfig,
|
PluginExtensionAddedLinkConfig,
|
||||||
|
PluginExtensionAddedFunctionConfig,
|
||||||
} from './pluginExtensions';
|
} from './pluginExtensions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,6 +61,7 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
|
|||||||
private _exposedComponentConfigs: PluginExtensionExposedComponentConfig[] = [];
|
private _exposedComponentConfigs: PluginExtensionExposedComponentConfig[] = [];
|
||||||
private _addedComponentConfigs: PluginExtensionAddedComponentConfig[] = [];
|
private _addedComponentConfigs: PluginExtensionAddedComponentConfig[] = [];
|
||||||
private _addedLinkConfigs: PluginExtensionAddedLinkConfig[] = [];
|
private _addedLinkConfigs: PluginExtensionAddedLinkConfig[] = [];
|
||||||
|
private _addedFunctionConfigs: PluginExtensionAddedFunctionConfig[] = [];
|
||||||
|
|
||||||
// Content under: /a/${plugin-id}/*
|
// Content under: /a/${plugin-id}/*
|
||||||
root?: ComponentType<AppRootProps<T>>;
|
root?: ComponentType<AppRootProps<T>>;
|
||||||
@@ -113,6 +115,10 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
|
|||||||
return this._addedLinkConfigs;
|
return this._addedLinkConfigs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get addedFunctionConfigs() {
|
||||||
|
return this._addedFunctionConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
addLink<Context extends object>(linkConfig: PluginExtensionAddedLinkConfig<Context>) {
|
addLink<Context extends object>(linkConfig: PluginExtensionAddedLinkConfig<Context>) {
|
||||||
this._addedLinkConfigs.push(linkConfig as PluginExtensionAddedLinkConfig);
|
this._addedLinkConfigs.push(linkConfig as PluginExtensionAddedLinkConfig);
|
||||||
|
|
||||||
@@ -125,6 +131,12 @@ export class AppPlugin<T extends KeyValue = KeyValue> extends GrafanaPlugin<AppP
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addFunction<Signature>(addedFunctionConfig: PluginExtensionAddedFunctionConfig<Signature>) {
|
||||||
|
this._addedFunctionConfigs.push(addedFunctionConfig);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
exposeComponent<Props = {}>(componentConfig: PluginExtensionExposedComponentConfig<Props>) {
|
exposeComponent<Props = {}>(componentConfig: PluginExtensionExposedComponentConfig<Props>) {
|
||||||
this._exposedComponentConfigs.push(componentConfig as PluginExtensionExposedComponentConfig);
|
this._exposedComponentConfigs.push(componentConfig as PluginExtensionExposedComponentConfig);
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,8 @@ export interface PluginExtensions {
|
|||||||
// The component extensions that the plugin registers
|
// The component extensions that the plugin registers
|
||||||
addedComponents: ExtensionInfo[];
|
addedComponents: ExtensionInfo[];
|
||||||
|
|
||||||
|
addedFunctions: ExtensionInfo[];
|
||||||
|
|
||||||
// The link extensions that the plugin registers
|
// The link extensions that the plugin registers
|
||||||
addedLinks: ExtensionInfo[];
|
addedLinks: ExtensionInfo[];
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { RawTimeRange, TimeZone } from './time';
|
|||||||
export enum PluginExtensionTypes {
|
export enum PluginExtensionTypes {
|
||||||
link = 'link',
|
link = 'link',
|
||||||
component = 'component',
|
component = 'component',
|
||||||
|
function = 'function',
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginExtensionBase = {
|
type PluginExtensionBase = {
|
||||||
@@ -36,7 +37,12 @@ export type PluginExtensionComponent<Props = {}> = PluginExtensionBase & {
|
|||||||
component: React.ComponentType<Props>;
|
component: React.ComponentType<Props>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PluginExtension = PluginExtensionLink | PluginExtensionComponent;
|
export type PluginExtensionFunction<Signature = () => void> = PluginExtensionBase & {
|
||||||
|
type: PluginExtensionTypes.function;
|
||||||
|
fn: Signature;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PluginExtension = PluginExtensionLink | PluginExtensionComponent | PluginExtensionFunction;
|
||||||
|
|
||||||
// Objects used for registering extensions (in app plugins)
|
// Objects used for registering extensions (in app plugins)
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -74,6 +80,17 @@ export type PluginExtensionAddedComponentConfig<Props = {}> = PluginExtensionCon
|
|||||||
*/
|
*/
|
||||||
component: React.ComponentType<Props>;
|
component: React.ComponentType<Props>;
|
||||||
};
|
};
|
||||||
|
export type PluginExtensionAddedFunctionConfig<Signature = unknown> = PluginExtensionConfigBase & {
|
||||||
|
/**
|
||||||
|
* The target extension points where the component will be added
|
||||||
|
*/
|
||||||
|
targets: string | string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The function to be executed
|
||||||
|
*/
|
||||||
|
fn: Signature;
|
||||||
|
};
|
||||||
|
|
||||||
export type PluginAddedLinksConfigureFunc<Context extends object> = (context: Readonly<Context> | undefined) =>
|
export type PluginAddedLinksConfigureFunc<Context extends object> = (context: Readonly<Context> | undefined) =>
|
||||||
| Partial<{
|
| Partial<{
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export {
|
|||||||
type UsePluginExtensions,
|
type UsePluginExtensions,
|
||||||
type UsePluginExtensionsResult,
|
type UsePluginExtensionsResult,
|
||||||
type UsePluginComponentResult,
|
type UsePluginComponentResult,
|
||||||
|
type UsePluginFunctionsOptions,
|
||||||
|
type UsePluginFunctionsResult,
|
||||||
} from './pluginExtensions/getPluginExtensions';
|
} from './pluginExtensions/getPluginExtensions';
|
||||||
export {
|
export {
|
||||||
setPluginExtensionsHook,
|
setPluginExtensionsHook,
|
||||||
@@ -33,6 +35,7 @@ export {
|
|||||||
export { setPluginComponentHook, usePluginComponent } from './pluginExtensions/usePluginComponent';
|
export { setPluginComponentHook, usePluginComponent } from './pluginExtensions/usePluginComponent';
|
||||||
export { setPluginComponentsHook, usePluginComponents } from './pluginExtensions/usePluginComponents';
|
export { setPluginComponentsHook, usePluginComponents } from './pluginExtensions/usePluginComponents';
|
||||||
export { setPluginLinksHook, usePluginLinks } from './pluginExtensions/usePluginLinks';
|
export { setPluginLinksHook, usePluginLinks } from './pluginExtensions/usePluginLinks';
|
||||||
|
export { setPluginFunctionsHook, usePluginFunctions } from './pluginExtensions/usePluginFunctions';
|
||||||
|
|
||||||
export { isPluginExtensionLink, isPluginExtensionComponent } from './pluginExtensions/utils';
|
export { isPluginExtensionLink, isPluginExtensionComponent } from './pluginExtensions/utils';
|
||||||
export { setCurrentUser } from './user';
|
export { setCurrentUser } from './user';
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import type { PluginExtension, PluginExtensionLink, PluginExtensionComponent } from '@grafana/data';
|
import type {
|
||||||
|
PluginExtension,
|
||||||
|
PluginExtensionLink,
|
||||||
|
PluginExtensionComponent,
|
||||||
|
PluginExtensionFunction,
|
||||||
|
} from '@grafana/data';
|
||||||
|
|
||||||
import { isPluginExtensionComponent, isPluginExtensionLink } from './utils';
|
import { isPluginExtensionComponent, isPluginExtensionLink } from './utils';
|
||||||
|
|
||||||
@@ -52,6 +57,16 @@ export type UsePluginLinksResult = {
|
|||||||
links: PluginExtensionLink[];
|
links: PluginExtensionLink[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UsePluginFunctionsOptions = {
|
||||||
|
extensionPointId: string;
|
||||||
|
limitPerPlugin?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UsePluginFunctionsResult<Signature> = {
|
||||||
|
isLoading: boolean;
|
||||||
|
functions: Array<PluginExtensionFunction<Signature>>;
|
||||||
|
};
|
||||||
|
|
||||||
let singleton: GetPluginExtensions | undefined;
|
let singleton: GetPluginExtensions | undefined;
|
||||||
|
|
||||||
export function setPluginExtensionGetter(instance: GetPluginExtensions): void {
|
export function setPluginExtensionGetter(instance: GetPluginExtensions): void {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { UsePluginFunctionsOptions, UsePluginFunctionsResult } from './getPluginExtensions';
|
||||||
|
|
||||||
|
export type UsePluginFunctions<T> = (options: UsePluginFunctionsOptions) => UsePluginFunctionsResult<T>;
|
||||||
|
|
||||||
|
let singleton: UsePluginFunctions<unknown> | undefined;
|
||||||
|
|
||||||
|
export function setPluginFunctionsHook(hook: UsePluginFunctions<unknown>): void {
|
||||||
|
// We allow overriding the registry in tests
|
||||||
|
if (singleton && process.env.NODE_ENV !== 'test') {
|
||||||
|
throw new Error('setUsePluginFunctionsHook() function should only be called once, when Grafana is starting.');
|
||||||
|
}
|
||||||
|
singleton = hook;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePluginFunctions<T>(options: UsePluginFunctionsOptions): UsePluginFunctionsResult<T> {
|
||||||
|
if (!singleton) {
|
||||||
|
throw new Error('usePluginFunctions(options) can only be used after the Grafana instance has started.');
|
||||||
|
}
|
||||||
|
return singleton(options) as UsePluginFunctionsResult<T>;
|
||||||
|
}
|
||||||
@@ -156,8 +156,8 @@ describe('MultiCombobox', () => {
|
|||||||
await user.type(input, 'D');
|
await user.type(input, 'D');
|
||||||
await user.keyboard('{arrowdown}{enter}');
|
await user.keyboard('{arrowdown}{enter}');
|
||||||
expect(onChange).toHaveBeenCalledWith([
|
expect(onChange).toHaveBeenCalledWith([
|
||||||
{ value: 'a' },
|
{ label: 'A', value: 'a' },
|
||||||
{ value: 'c' },
|
{ label: 'C', value: 'c' },
|
||||||
{ label: 'D', value: 'D', description: 'Use custom value' },
|
{ label: 'D', value: 'D', description: 'Use custom value' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -235,6 +235,19 @@ describe('MultiCombobox', () => {
|
|||||||
await user.click(await screen.findByRole('option', { name: 'All' }));
|
await user.click(await screen.findByRole('option', { name: 'All' }));
|
||||||
expect(onChange).toHaveBeenCalledWith([]);
|
expect(onChange).toHaveBeenCalledWith([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should keep label names on selected items when searching', async () => {
|
||||||
|
const options = [
|
||||||
|
{ label: 'A', value: 'a' },
|
||||||
|
{ label: 'B', value: 'b' },
|
||||||
|
{ label: 'C', value: 'c' },
|
||||||
|
];
|
||||||
|
render(<MultiCombobox width={200} options={options} value={['a']} onChange={jest.fn()} enableAllOption />);
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
await user.click(input);
|
||||||
|
await user.type(input, 'b');
|
||||||
|
expect(screen.getByText('A')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('async', () => {
|
describe('async', () => {
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ export const MultiCombobox = <T extends string | number>(props: MultiComboboxPro
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return getSelectedItemsFromValue<T>(value, baseOptions);
|
return getSelectedItemsFromValue<T>(value, typeof props.options !== 'function' ? props.options : baseOptions);
|
||||||
}, [value, baseOptions]);
|
}, [value, props.options, baseOptions]);
|
||||||
|
|
||||||
const { measureRef, counterMeasureRef, suffixMeasureRef, shownItems } = useMeasureMulti(
|
const { measureRef, counterMeasureRef, suffixMeasureRef, shownItems } = useMeasureMulti(
|
||||||
selectedItems,
|
selectedItems,
|
||||||
|
|||||||
@@ -207,7 +207,6 @@ type HTTPServer struct {
|
|||||||
tempUserService tempUser.Service
|
tempUserService tempUser.Service
|
||||||
loginAttemptService loginAttempt.Service
|
loginAttemptService loginAttempt.Service
|
||||||
orgService org.Service
|
orgService org.Service
|
||||||
idService auth.IDService
|
|
||||||
orgDeletionService org.DeletionService
|
orgDeletionService org.DeletionService
|
||||||
TeamService team.Service
|
TeamService team.Service
|
||||||
accesscontrolService accesscontrol.Service
|
accesscontrolService accesscontrol.Service
|
||||||
@@ -273,7 +272,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
|||||||
annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService,
|
annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService,
|
||||||
statsService stats.Service, authnService authn.Service, pluginsCDNService *pluginscdn.Service, promGatherer prometheus.Gatherer,
|
statsService stats.Service, authnService authn.Service, pluginsCDNService *pluginscdn.Service, promGatherer prometheus.Gatherer,
|
||||||
starApi *starApi.API, promRegister prometheus.Registerer, clientConfigProvider grafanaapiserver.DirectRestConfigProvider, anonService anonymous.Service,
|
starApi *starApi.API, promRegister prometheus.Registerer, clientConfigProvider grafanaapiserver.DirectRestConfigProvider, anonService anonymous.Service,
|
||||||
userVerifier user.Verifier, pluginPreinstall plugininstaller.Preinstall, idService auth.IDService,
|
userVerifier user.Verifier, pluginPreinstall plugininstaller.Preinstall,
|
||||||
) (*HTTPServer, error) {
|
) (*HTTPServer, error) {
|
||||||
web.Env = cfg.Env
|
web.Env = cfg.Env
|
||||||
m := web.New()
|
m := web.New()
|
||||||
@@ -361,7 +360,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
|||||||
tempUserService: tempUserService,
|
tempUserService: tempUserService,
|
||||||
loginAttemptService: loginAttemptService,
|
loginAttemptService: loginAttemptService,
|
||||||
orgService: orgService,
|
orgService: orgService,
|
||||||
idService: idService,
|
|
||||||
orgDeletionService: orgDeletionService,
|
orgDeletionService: orgDeletionService,
|
||||||
TeamService: teamService,
|
TeamService: teamService,
|
||||||
navTreeService: navTreeService,
|
navTreeService: navTreeService,
|
||||||
|
|||||||
@@ -7,11 +7,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
claims "github.com/grafana/authlib/types"
|
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/api/response"
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/authn"
|
|
||||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||||
"github.com/grafana/grafana/pkg/services/login"
|
"github.com/grafana/grafana/pkg/services/login"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
@@ -434,10 +432,6 @@ func (hs *HTTPServer) updateOrgUserHelper(c *contextmodel.ReqContext, cmd org.Up
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := hs.idService.RemoveIDToken(c.Req.Context(), &authn.Identity{ID: strconv.FormatInt(cmd.UserID, 10), Type: claims.TypeUser, OrgID: cmd.OrgID}); err != nil {
|
|
||||||
return response.Error(http.StatusInternalServerError, "Failed to invalidate the ID token cache", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := hs.orgService.UpdateOrgUser(c.Req.Context(), &cmd); err != nil {
|
if err := hs.orgService.UpdateOrgUser(c.Req.Context(), &cmd); err != nil {
|
||||||
if errors.Is(err, org.ErrLastOrgAdmin) {
|
if errors.Is(err, org.ErrLastOrgAdmin) {
|
||||||
return response.Error(http.StatusBadRequest, "Cannot change role so that there is no organization admin left", nil)
|
return response.Error(http.StatusBadRequest, "Cannot change role so that there is no organization admin left", nil)
|
||||||
|
|||||||
+24
-50
@@ -9,12 +9,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/authlib/types"
|
|
||||||
"github.com/grafana/grafana/pkg/services/auth/idtest"
|
|
||||||
"github.com/grafana/grafana/pkg/services/authn"
|
"github.com/grafana/grafana/pkg/services/authn"
|
||||||
"github.com/grafana/grafana/pkg/services/authn/authntest"
|
"github.com/grafana/grafana/pkg/services/authn/authntest"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
@@ -205,12 +202,11 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
|
|||||||
|
|
||||||
func TestOrgUsersAPIEndpoint_updateOrgRole(t *testing.T) {
|
func TestOrgUsersAPIEndpoint_updateOrgRole(t *testing.T) {
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
desc string
|
desc string
|
||||||
SkipOrgRoleSync bool
|
SkipOrgRoleSync bool
|
||||||
AuthEnabled bool
|
AuthEnabled bool
|
||||||
AuthModule string
|
AuthModule string
|
||||||
shouldInvalidateIDToken bool
|
expectedCode int
|
||||||
expectedCode int
|
|
||||||
}
|
}
|
||||||
permissions := []accesscontrol.Permission{
|
permissions := []accesscontrol.Permission{
|
||||||
{Action: accesscontrol.ActionOrgUsersRead, Scope: "users:*"},
|
{Action: accesscontrol.ActionOrgUsersRead, Scope: "users:*"},
|
||||||
@@ -220,12 +216,11 @@ func TestOrgUsersAPIEndpoint_updateOrgRole(t *testing.T) {
|
|||||||
}
|
}
|
||||||
tests := []testCase{
|
tests := []testCase{
|
||||||
{
|
{
|
||||||
desc: "should be able to change basicRole when skip_org_role_sync true",
|
desc: "should be able to change basicRole when skip_org_role_sync true",
|
||||||
SkipOrgRoleSync: true,
|
SkipOrgRoleSync: true,
|
||||||
AuthEnabled: true,
|
AuthEnabled: true,
|
||||||
AuthModule: login.LDAPAuthModule,
|
AuthModule: login.LDAPAuthModule,
|
||||||
shouldInvalidateIDToken: true,
|
expectedCode: http.StatusOK,
|
||||||
expectedCode: http.StatusOK,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "should not be able to change basicRole when skip_org_role_sync false",
|
desc: "should not be able to change basicRole when skip_org_role_sync false",
|
||||||
@@ -242,20 +237,18 @@ func TestOrgUsersAPIEndpoint_updateOrgRole(t *testing.T) {
|
|||||||
expectedCode: http.StatusForbidden,
|
expectedCode: http.StatusForbidden,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "should be able to change basicRole with a basic Auth",
|
desc: "should be able to change basicRole with a basic Auth",
|
||||||
SkipOrgRoleSync: false,
|
SkipOrgRoleSync: false,
|
||||||
AuthEnabled: false,
|
AuthEnabled: false,
|
||||||
AuthModule: "",
|
AuthModule: "",
|
||||||
shouldInvalidateIDToken: true,
|
expectedCode: http.StatusOK,
|
||||||
expectedCode: http.StatusOK,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "should be able to change basicRole with a basic Auth",
|
desc: "should be able to change basicRole with a basic Auth",
|
||||||
SkipOrgRoleSync: true,
|
SkipOrgRoleSync: true,
|
||||||
AuthEnabled: true,
|
AuthEnabled: true,
|
||||||
AuthModule: "",
|
AuthModule: "",
|
||||||
shouldInvalidateIDToken: true,
|
expectedCode: http.StatusOK,
|
||||||
expectedCode: http.StatusOK,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,11 +279,6 @@ func TestOrgUsersAPIEndpoint_updateOrgRole(t *testing.T) {
|
|||||||
}
|
}
|
||||||
hs.userService = &usertest.FakeUserService{ExpectedSignedInUser: userWithPermissions}
|
hs.userService = &usertest.FakeUserService{ExpectedSignedInUser: userWithPermissions}
|
||||||
hs.orgService = &orgtest.FakeOrgService{}
|
hs.orgService = &orgtest.FakeOrgService{}
|
||||||
idService := &idtest.MockService{}
|
|
||||||
if tt.shouldInvalidateIDToken {
|
|
||||||
idService.On("RemoveIDToken", mock.Anything, mock.Anything).Return(nil)
|
|
||||||
}
|
|
||||||
hs.idService = idService
|
|
||||||
hs.SocialService = &socialtest.FakeSocialService{
|
hs.SocialService = &socialtest.FakeSocialService{
|
||||||
ExpectedAuthInfoProvider: &social.OAuthInfo{Enabled: tt.AuthEnabled, SkipOrgRoleSync: tt.SkipOrgRoleSync},
|
ExpectedAuthInfoProvider: &social.OAuthInfo{Enabled: tt.AuthEnabled, SkipOrgRoleSync: tt.SkipOrgRoleSync},
|
||||||
}
|
}
|
||||||
@@ -627,7 +615,6 @@ func TestOrgUsersAPIEndpointWithSetPerms_AccessControl(t *testing.T) {
|
|||||||
ExpectedUser: &user.User{},
|
ExpectedUser: &user.User{},
|
||||||
ExpectedSignedInUser: userWithPermissions(1, tt.permissions),
|
ExpectedSignedInUser: userWithPermissions(1, tt.permissions),
|
||||||
}
|
}
|
||||||
hs.idService = &idtest.FakeService{}
|
|
||||||
hs.accesscontrolService = &actest.FakeService{}
|
hs.accesscontrolService = &actest.FakeService{}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -650,24 +637,16 @@ func TestPatchOrgUsersAPIEndpoint_AccessControl(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
role org.RoleType
|
role org.RoleType
|
||||||
permissions []accesscontrol.Permission
|
permissions []accesscontrol.Permission
|
||||||
setup func(*testing.T, *idtest.MockService)
|
|
||||||
input string
|
input string
|
||||||
expectedCode int
|
expectedCode int
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []testCase{
|
tests := []testCase{
|
||||||
{
|
{
|
||||||
name: "user with permissions can update org role",
|
name: "user with permissions can update org role",
|
||||||
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersWrite, Scope: "users:*"}},
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersWrite, Scope: "users:*"}},
|
||||||
role: org.RoleAdmin,
|
role: org.RoleAdmin,
|
||||||
input: `{"role": "Viewer"}`,
|
input: `{"role": "Viewer"}`,
|
||||||
setup: func(t *testing.T, idService *idtest.MockService) {
|
|
||||||
idService.On("RemoveIDToken", mock.Anything, mock.MatchedBy(func(id *authn.Identity) bool {
|
|
||||||
return id.GetIdentityType() == types.TypeUser &&
|
|
||||||
id.GetID() == "user:1" &&
|
|
||||||
id.GetOrgID() == int64(1)
|
|
||||||
})).Return(nil)
|
|
||||||
},
|
|
||||||
expectedCode: http.StatusOK,
|
expectedCode: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -694,11 +673,6 @@ func TestPatchOrgUsersAPIEndpoint_AccessControl(t *testing.T) {
|
|||||||
AuthModule: "",
|
AuthModule: "",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
idService := &idtest.MockService{}
|
|
||||||
if tt.setup != nil {
|
|
||||||
tt.setup(t, idService)
|
|
||||||
}
|
|
||||||
hs.idService = idService
|
|
||||||
hs.accesscontrolService = &actest.FakeService{}
|
hs.accesscontrolService = &actest.FakeService{}
|
||||||
hs.userService = &usertest.FakeUserService{
|
hs.userService = &usertest.FakeUserService{
|
||||||
ExpectedUser: &user.User{},
|
ExpectedUser: &user.User{},
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func RequestStatusFromError(err error) RequestStatus {
|
|||||||
status = RequestStatusError
|
status = RequestStatusError
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
status = RequestStatusCancelled
|
status = RequestStatusCancelled
|
||||||
} else if s, ok := grpcstatus.FromError(err); ok && s.Code() == grpccodes.Canceled {
|
} else if s, ok := grpcstatus.FromError(err); ok && s.Code() == grpccodes.Canceled || s.Code() == grpccodes.DeadlineExceeded {
|
||||||
status = RequestStatusCancelled
|
status = RequestStatusCancelled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -96,8 +97,10 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -127,8 +130,10 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -200,8 +205,10 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
{Name: "Nginx Datasource", Type: "datasource", Role: "Viewer", Action: "plugins.app:access"},
|
{Name: "Nginx Datasource", Type: "datasource", Role: "Viewer", Action: "plugins.app:access"},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -238,8 +245,10 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -269,8 +278,10 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -300,8 +311,10 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -340,8 +353,10 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -201,8 +202,10 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -249,8 +252,10 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -304,8 +309,10 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -398,8 +405,10 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
{Name: "Root Page (react)", Type: "page", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Path: "/a/my-simple-app", DefaultNav: true, AddToNav: true, Slug: "root-page-react"},
|
{Name: "Root Page (react)", Type: "page", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Path: "/a/my-simple-app", DefaultNav: true, AddToNav: true, Slug: "root-page-react"},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ type ExtensionsV2 struct {
|
|||||||
AddedComponents []AddedComponent `json:"addedComponents"`
|
AddedComponents []AddedComponent `json:"addedComponents"`
|
||||||
ExposedComponents []ExposedComponent `json:"exposedComponents"`
|
ExposedComponents []ExposedComponent `json:"exposedComponents"`
|
||||||
ExtensionPoints []ExtensionPoint `json:"extensionPoints"`
|
ExtensionPoints []ExtensionPoint `json:"extensionPoints"`
|
||||||
|
AddedFunctions []AddedFunction `json:"addedFunctions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Extensions ExtensionsV2
|
type Extensions ExtensionsV2
|
||||||
@@ -76,6 +77,7 @@ func (e *Extensions) UnmarshalJSON(data []byte) error {
|
|||||||
e.AddedLinks = extensionsV2.AddedLinks
|
e.AddedLinks = extensionsV2.AddedLinks
|
||||||
e.ExposedComponents = extensionsV2.ExposedComponents
|
e.ExposedComponents = extensionsV2.ExposedComponents
|
||||||
e.ExtensionPoints = extensionsV2.ExtensionPoints
|
e.ExtensionPoints = extensionsV2.ExtensionPoints
|
||||||
|
e.AddedFunctions = extensionsV2.AddedFunctions
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -123,6 +125,11 @@ type AddedComponent struct {
|
|||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AddedFunction struct {
|
||||||
|
Targets []string `json:"targets"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
type ExposedComponent struct {
|
type ExposedComponent struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
@@ -267,6 +274,7 @@ type PluginMetaDTO struct {
|
|||||||
Angular AngularMeta `json:"angular"`
|
Angular AngularMeta `json:"angular"`
|
||||||
MultiValueFilterOperators bool `json:"multiValueFilterOperators"`
|
MultiValueFilterOperators bool `json:"multiValueFilterOperators"`
|
||||||
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
|
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
|
||||||
|
Extensions Extensions `json:"extensions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataSourceDTO struct {
|
type DataSourceDTO struct {
|
||||||
|
|||||||
@@ -167,6 +167,10 @@ func ReadPluginJSON(reader io.Reader) (JSONData, error) {
|
|||||||
plugin.Extensions.AddedComponents = []AddedComponent{}
|
plugin.Extensions.AddedComponents = []AddedComponent{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if plugin.Extensions.AddedFunctions == nil {
|
||||||
|
plugin.Extensions.AddedFunctions = []AddedFunction{}
|
||||||
|
}
|
||||||
|
|
||||||
if plugin.Extensions.ExposedComponents == nil {
|
if plugin.Extensions.ExposedComponents == nil {
|
||||||
plugin.Extensions.ExposedComponents = []ExposedComponent{}
|
plugin.Extensions.ExposedComponents = []ExposedComponent{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
Extensions: Extensions{
|
Extensions: Extensions{
|
||||||
AddedLinks: []AddedLink{},
|
AddedLinks: []AddedLink{},
|
||||||
AddedComponents: []AddedComponent{},
|
AddedComponents: []AddedComponent{},
|
||||||
|
AddedFunctions: []AddedFunction{},
|
||||||
ExposedComponents: []ExposedComponent{},
|
ExposedComponents: []ExposedComponent{},
|
||||||
ExtensionPoints: []ExtensionPoint{},
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -108,8 +109,10 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
Name: "Pie Chart (old)",
|
Name: "Pie Chart (old)",
|
||||||
|
|
||||||
Extensions: Extensions{
|
Extensions: Extensions{
|
||||||
AddedLinks: []AddedLink{},
|
AddedLinks: []AddedLink{},
|
||||||
AddedComponents: []AddedComponent{},
|
AddedComponents: []AddedComponent{},
|
||||||
|
AddedFunctions: []AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []ExposedComponent{},
|
ExposedComponents: []ExposedComponent{},
|
||||||
ExtensionPoints: []ExtensionPoint{},
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -143,8 +146,10 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
Type: TypeDataSource,
|
Type: TypeDataSource,
|
||||||
|
|
||||||
Extensions: Extensions{
|
Extensions: Extensions{
|
||||||
AddedLinks: []AddedLink{},
|
AddedLinks: []AddedLink{},
|
||||||
AddedComponents: []AddedComponent{},
|
AddedComponents: []AddedComponent{},
|
||||||
|
AddedFunctions: []AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []ExposedComponent{},
|
ExposedComponents: []ExposedComponent{},
|
||||||
ExtensionPoints: []ExtensionPoint{},
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -188,6 +193,9 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
"id": "myorg-extensions-app/component-1/v1"
|
"id": "myorg-extensions-app/component-1/v1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"addedFunctions": [
|
||||||
|
{"targets": ["foo/bar"], "title":"some hook"}
|
||||||
|
],
|
||||||
"extensionPoints": [
|
"extensionPoints": [
|
||||||
{
|
{
|
||||||
"title": "Extension point 1",
|
"title": "Extension point 1",
|
||||||
@@ -209,6 +217,7 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
{Title: "Added link 1", Description: "Added link 1 description", Targets: []string{"grafana/dashboard/panel/menu"}},
|
{Title: "Added link 1", Description: "Added link 1 description", Targets: []string{"grafana/dashboard/panel/menu"}},
|
||||||
},
|
},
|
||||||
AddedComponents: []AddedComponent{
|
AddedComponents: []AddedComponent{
|
||||||
|
|
||||||
{Title: "Added component 1", Description: "Added component 1 description", Targets: []string{"grafana/user/profile/tab"}},
|
{Title: "Added component 1", Description: "Added component 1 description", Targets: []string{"grafana/user/profile/tab"}},
|
||||||
},
|
},
|
||||||
ExposedComponents: []ExposedComponent{
|
ExposedComponents: []ExposedComponent{
|
||||||
@@ -217,6 +226,9 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
ExtensionPoints: []ExtensionPoint{
|
ExtensionPoints: []ExtensionPoint{
|
||||||
{Id: "myorg-extensions-app/extensions-point-1/v1", Title: "Extension point 1", Description: "Extension points 1 description"},
|
{Id: "myorg-extensions-app/extensions-point-1/v1", Title: "Extension point 1", Description: "Extension points 1 description"},
|
||||||
},
|
},
|
||||||
|
AddedFunctions: []AddedFunction{
|
||||||
|
{Targets: []string{"foo/bar"}, Title: "some hook"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Dependencies: Dependencies{
|
Dependencies: Dependencies{
|
||||||
@@ -271,6 +283,7 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
AddedComponents: []AddedComponent{
|
AddedComponents: []AddedComponent{
|
||||||
{Title: "Added component 1", Description: "Added component 1 description", Targets: []string{"grafana/user/profile/tab"}},
|
{Title: "Added component 1", Description: "Added component 1 description", Targets: []string{"grafana/user/profile/tab"}},
|
||||||
},
|
},
|
||||||
|
AddedFunctions: []AddedFunction{},
|
||||||
ExposedComponents: []ExposedComponent{},
|
ExposedComponents: []ExposedComponent{},
|
||||||
ExtensionPoints: []ExtensionPoint{},
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -301,8 +314,10 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
Type: TypeApp,
|
Type: TypeApp,
|
||||||
|
|
||||||
Extensions: Extensions{
|
Extensions: Extensions{
|
||||||
AddedLinks: []AddedLink{},
|
AddedLinks: []AddedLink{},
|
||||||
AddedComponents: []AddedComponent{},
|
AddedComponents: []AddedComponent{},
|
||||||
|
AddedFunctions: []AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []ExposedComponent{},
|
ExposedComponents: []ExposedComponent{},
|
||||||
ExtensionPoints: []ExtensionPoint{},
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -332,8 +347,10 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
Type: TypeApp,
|
Type: TypeApp,
|
||||||
|
|
||||||
Extensions: Extensions{
|
Extensions: Extensions{
|
||||||
AddedLinks: []AddedLink{},
|
AddedLinks: []AddedLink{},
|
||||||
AddedComponents: []AddedComponent{},
|
AddedComponents: []AddedComponent{},
|
||||||
|
AddedFunctions: []AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []ExposedComponent{},
|
ExposedComponents: []ExposedComponent{},
|
||||||
ExtensionPoints: []ExtensionPoint{},
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -371,6 +388,7 @@ func Test_ReadPluginJSON(t *testing.T) {
|
|||||||
Extensions: Extensions{
|
Extensions: Extensions{
|
||||||
AddedLinks: []AddedLink{},
|
AddedLinks: []AddedLink{},
|
||||||
AddedComponents: []AddedComponent{},
|
AddedComponents: []AddedComponent{},
|
||||||
|
AddedFunctions: []AddedFunction{},
|
||||||
ExposedComponents: []ExposedComponent{},
|
ExposedComponents: []ExposedComponent{},
|
||||||
ExtensionPoints: []ExtensionPoint{},
|
ExtensionPoints: []ExtensionPoint{},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri
|
|||||||
s.metrics.tokenSigningDurationHistogram.Observe(time.Since(t).Seconds())
|
s.metrics.tokenSigningDurationHistogram.Observe(time.Since(t).Seconds())
|
||||||
}(time.Now())
|
}(time.Now())
|
||||||
|
|
||||||
cacheKey := prefixCacheKey(id.GetCacheKey())
|
cacheKey := getCacheKey(id)
|
||||||
|
|
||||||
type resultType struct {
|
type resultType struct {
|
||||||
token string
|
token string
|
||||||
@@ -140,7 +140,7 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) RemoveIDToken(ctx context.Context, id identity.Requester) error {
|
func (s *Service) RemoveIDToken(ctx context.Context, id identity.Requester) error {
|
||||||
return s.cache.Delete(ctx, prefixCacheKey(id.GetCacheKey()))
|
return s.cache.Delete(ctx, getCacheKey(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) hook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error {
|
func (s *Service) hook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error {
|
||||||
@@ -181,8 +181,8 @@ func getAudience(orgID int64) jwt.Audience {
|
|||||||
return jwt.Audience{fmt.Sprintf("org:%d", orgID)}
|
return jwt.Audience{fmt.Sprintf("org:%d", orgID)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func prefixCacheKey(key string) string {
|
func getCacheKey(ident identity.Requester) string {
|
||||||
return fmt.Sprintf("%s-%s", cachePrefix, key)
|
return cachePrefix + ident.GetCacheKey() + string(ident.GetOrgRole())
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldLogErr(err error) bool {
|
func shouldLogErr(err error) bool {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/authn"
|
"github.com/grafana/grafana/pkg/services/authn"
|
||||||
"github.com/grafana/grafana/pkg/services/authn/authntest"
|
"github.com/grafana/grafana/pkg/services/authn/authntest"
|
||||||
"github.com/grafana/grafana/pkg/services/login"
|
"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/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -101,4 +102,34 @@ func TestService_SignIdentity(t *testing.T) {
|
|||||||
assert.Equal(t, claims.TypeUser, gotClaims.Rest.Type)
|
assert.Equal(t, claims.TypeUser, gotClaims.Rest.Type)
|
||||||
assert.Equal(t, "edpu3nnt61se8e", gotClaims.Rest.Identifier)
|
assert.Equal(t, "edpu3nnt61se8e", gotClaims.Rest.Identifier)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("should sign new token if org role has changed", func(t *testing.T) {
|
||||||
|
s := ProvideService(
|
||||||
|
setting.NewCfg(), signer, remotecache.NewFakeCacheStorage(),
|
||||||
|
&authntest.FakeService{}, nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
ident := &authn.Identity{
|
||||||
|
ID: "1",
|
||||||
|
Type: claims.TypeUser,
|
||||||
|
AuthenticatedBy: login.AzureADAuthModule,
|
||||||
|
Login: "U1",
|
||||||
|
UID: "edpu3nnt61se8e",
|
||||||
|
OrgID: 1,
|
||||||
|
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
|
||||||
|
}
|
||||||
|
|
||||||
|
first, _, err := s.SignIdentity(context.Background(), ident)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
second, _, err := s.SignIdentity(context.Background(), ident)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, first, second)
|
||||||
|
|
||||||
|
ident.OrgRoles[1] = org.RoleEditor
|
||||||
|
third, _, err := s.SignIdentity(context.Background(), ident)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEqual(t, first, third)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,29 +86,38 @@ func (s *UserSync) SyncUserHook(ctx context.Context, id *authn.Identity, _ *auth
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Does user exist in the database?
|
// Does user exist in the database?
|
||||||
usr, userAuth, errUserInDB := s.getUser(ctx, id)
|
usr, userAuth, err := s.getUser(ctx, id)
|
||||||
if errUserInDB != nil && !errors.Is(errUserInDB, user.ErrUserNotFound) {
|
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
|
||||||
s.log.FromContext(ctx).Error("Failed to fetch user", "error", errUserInDB, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID)
|
s.log.FromContext(ctx).Error("Failed to fetch user", "error", err, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID)
|
||||||
return errSyncUserInternal.Errorf("unable to retrieve user")
|
return errSyncUserInternal.Errorf("unable to retrieve user")
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(errUserInDB, user.ErrUserNotFound) {
|
if errors.Is(err, user.ErrUserNotFound) {
|
||||||
if !id.ClientParams.AllowSignUp {
|
if !id.ClientParams.AllowSignUp {
|
||||||
s.log.FromContext(ctx).Warn("Failed to create user, signup is not allowed for module", "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID)
|
s.log.FromContext(ctx).Warn("Failed to create user, signup is not allowed for module", "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID)
|
||||||
return errUserSignupDisabled.Errorf("%w", errSignupNotAllowed)
|
return errUserSignupDisabled.Errorf("%w", errSignupNotAllowed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// create user
|
// create user
|
||||||
var errCreate error
|
usr, err = s.createUser(ctx, id)
|
||||||
usr, errCreate = s.createUser(ctx, id)
|
|
||||||
if errCreate != nil {
|
// There is a possibility for a race condition when creating a user. Most clients will probably not hit this
|
||||||
s.log.FromContext(ctx).Error("Failed to create user", "error", errCreate, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID)
|
// case but others will. The one we have seen this issue for is auth proxy. First time a new user loads grafana
|
||||||
return errSyncUserInternal.Errorf("unable to create user: %w", errCreate)
|
// several requests can get "user.ErrUserNotFound" at the same time but only one of the request will be allowed
|
||||||
|
// to actually create the user, resulting in all other requests getting "user.ErrUserAlreadyExists". So we can
|
||||||
|
// just try to fetch the user one more to make the other request work.
|
||||||
|
if errors.Is(err, user.ErrUserAlreadyExists) {
|
||||||
|
usr, _, err = s.getUser(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.log.FromContext(ctx).Error("Failed to create user", "error", err, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID)
|
||||||
|
return errSyncUserInternal.Errorf("unable to create user: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// update user
|
// update user
|
||||||
if errUpdate := s.updateUserAttributes(ctx, usr, id, userAuth); errUpdate != nil {
|
if err := s.updateUserAttributes(ctx, usr, id, userAuth); err != nil {
|
||||||
s.log.FromContext(ctx).Error("Failed to update user", "error", errUpdate, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID)
|
s.log.FromContext(ctx).Error("Failed to update user", "error", err, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID)
|
||||||
return errSyncUserInternal.Errorf("unable to update user")
|
return errSyncUserInternal.Errorf("unable to update user")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,6 +320,7 @@ func (s *UserSync) updateUserAttributes(ctx context.Context, usr *user.User, id
|
|||||||
func (s *UserSync) createUser(ctx context.Context, id *authn.Identity) (*user.User, error) {
|
func (s *UserSync) createUser(ctx context.Context, id *authn.Identity) (*user.User, error) {
|
||||||
ctx, span := s.tracer.Start(ctx, "user.sync.createUser")
|
ctx, span := s.tracer.Start(ctx, "user.sync.createUser")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
// FIXME(jguer): this should be done in the user service
|
// FIXME(jguer): this should be done in the user service
|
||||||
// quota check: we can have quotas on both global and org level
|
// quota check: we can have quotas on both global and org level
|
||||||
// therefore we need to query check quota for both user and org services
|
// therefore we need to query check quota for both user and org services
|
||||||
@@ -330,19 +340,18 @@ func (s *UserSync) createUser(ctx context.Context, id *authn.Identity) (*user.Us
|
|||||||
isAdmin = *id.IsGrafanaAdmin
|
isAdmin = *id.IsGrafanaAdmin
|
||||||
}
|
}
|
||||||
|
|
||||||
usr, errCreateUser := s.userService.Create(ctx, &user.CreateUserCommand{
|
usr, err := s.userService.Create(ctx, &user.CreateUserCommand{
|
||||||
Login: id.Login,
|
Login: id.Login,
|
||||||
Email: id.Email,
|
Email: id.Email,
|
||||||
Name: id.Name,
|
Name: id.Name,
|
||||||
IsAdmin: isAdmin,
|
IsAdmin: isAdmin,
|
||||||
SkipOrgSetup: len(id.OrgRoles) > 0,
|
SkipOrgSetup: len(id.OrgRoles) > 0,
|
||||||
})
|
})
|
||||||
if errCreateUser != nil {
|
if err != nil {
|
||||||
return nil, errCreateUser
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.upsertAuthConnection(ctx, usr.ID, id, true)
|
if err := s.upsertAuthConnection(ctx, usr.ID, id, true); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
claims "github.com/grafana/authlib/types"
|
claims "github.com/grafana/authlib/types"
|
||||||
@@ -451,6 +452,35 @@ func TestUserSync_SyncUserHook(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUserSync_SyncUserRetryFetch(t *testing.T) {
|
||||||
|
userSrv := usertest.NewMockService(t)
|
||||||
|
userSrv.On("GetByEmail", mock.Anything, mock.Anything).Return(nil, user.ErrUserNotFound).Once()
|
||||||
|
userSrv.On("Create", mock.Anything, mock.Anything).Return(nil, user.ErrUserAlreadyExists).Once()
|
||||||
|
userSrv.On("GetByEmail", mock.Anything, mock.Anything).Return(&user.User{ID: 1}, nil).Once()
|
||||||
|
|
||||||
|
s := ProvideUserSync(
|
||||||
|
userSrv,
|
||||||
|
authinfoimpl.ProvideOSSUserProtectionService(),
|
||||||
|
&authinfotest.FakeService{},
|
||||||
|
"atest.FakeQuotaService{},
|
||||||
|
tracing.NewNoopTracerService(),
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
|
)
|
||||||
|
|
||||||
|
email := "test@test.com"
|
||||||
|
|
||||||
|
err := s.SyncUserHook(context.Background(), &authn.Identity{
|
||||||
|
ClientParams: authn.ClientParams{
|
||||||
|
SyncUser: true,
|
||||||
|
AllowSignUp: true,
|
||||||
|
LookUpParams: login.UserLookupParams{
|
||||||
|
Email: &email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestUserSync_FetchSyncedUserHook(t *testing.T) {
|
func TestUserSync_FetchSyncedUserHook(t *testing.T) {
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
desc string
|
desc string
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package authz
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fullstorydev/grpchan"
|
"github.com/fullstorydev/grpchan"
|
||||||
@@ -11,6 +13,7 @@ import (
|
|||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
|
||||||
authnlib "github.com/grafana/authlib/authn"
|
authnlib "github.com/grafana/authlib/authn"
|
||||||
authzlib "github.com/grafana/authlib/authz"
|
authzlib "github.com/grafana/authlib/authz"
|
||||||
@@ -22,6 +25,8 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
|
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||||
|
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||||
"github.com/grafana/grafana/pkg/services/authz/rbac"
|
"github.com/grafana/grafana/pkg/services/authz/rbac"
|
||||||
"github.com/grafana/grafana/pkg/services/authz/rbac/store"
|
"github.com/grafana/grafana/pkg/services/authz/rbac/store"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
@@ -64,6 +69,10 @@ func ProvideAuthZClient(
|
|||||||
// Register the server
|
// Register the server
|
||||||
server := rbac.NewService(
|
server := rbac.NewService(
|
||||||
sql,
|
sql,
|
||||||
|
// When running in-proc we get a injection cycle between
|
||||||
|
// authz client, resource client and apiserver so we need to use
|
||||||
|
// package level function to get rest config
|
||||||
|
store.NewAPIFolderStore(tracer, apiserver.GetRestConfig),
|
||||||
legacy.NewLegacySQLStores(sql),
|
legacy.NewLegacySQLStores(sql),
|
||||||
store.NewUnionPermissionStore(
|
store.NewUnionPermissionStore(
|
||||||
store.NewStaticPermissionStore(acService),
|
store.NewStaticPermissionStore(acService),
|
||||||
@@ -201,3 +210,67 @@ func newCloudLegacyClient(authCfg *Cfg, tracer tracing.Tracer) (authlib.AccessCl
|
|||||||
|
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RegisterRBACAuthZService(
|
||||||
|
handler grpcserver.Provider,
|
||||||
|
db legacysql.LegacyDatabaseProvider,
|
||||||
|
tracer tracing.Tracer,
|
||||||
|
reg prometheus.Registerer,
|
||||||
|
cache cache.Cache,
|
||||||
|
exchangeClient authnlib.TokenExchanger,
|
||||||
|
folderAPIURL string,
|
||||||
|
) {
|
||||||
|
var folderStore store.FolderStore
|
||||||
|
// FIXME: for now we default to using database read proxy for folders if the api url is not configured.
|
||||||
|
// we should remove this and the sql implementation once we have verified that is works correctly
|
||||||
|
if folderAPIURL == "" {
|
||||||
|
folderStore = store.NewSQLFolderStore(db, tracer)
|
||||||
|
} else {
|
||||||
|
folderStore = store.NewAPIFolderStore(tracer, func(ctx context.Context) *rest.Config {
|
||||||
|
return &rest.Config{
|
||||||
|
Host: folderAPIURL,
|
||||||
|
WrapTransport: func(rt http.RoundTripper) http.RoundTripper {
|
||||||
|
return &tokenExhangeRoundTripper{te: exchangeClient, rt: rt}
|
||||||
|
},
|
||||||
|
QPS: 50,
|
||||||
|
Burst: 100,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
server := rbac.NewService(
|
||||||
|
db,
|
||||||
|
folderStore,
|
||||||
|
legacy.NewLegacySQLStores(db),
|
||||||
|
store.NewSQLPermissionStore(db, tracer),
|
||||||
|
log.New("authz-grpc-server"),
|
||||||
|
tracer,
|
||||||
|
reg,
|
||||||
|
cache,
|
||||||
|
)
|
||||||
|
|
||||||
|
srv := handler.GetServer()
|
||||||
|
authzv1.RegisterAuthzServiceServer(srv, server)
|
||||||
|
authzextv1.RegisterAuthzExtentionServiceServer(srv, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ http.RoundTripper = tokenExhangeRoundTripper{}
|
||||||
|
|
||||||
|
type tokenExhangeRoundTripper struct {
|
||||||
|
te authnlib.TokenExchanger
|
||||||
|
rt http.RoundTripper
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t tokenExhangeRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||||
|
res, err := t.te.Exchange(r.Context(), authnlib.TokenExchangeRequest{
|
||||||
|
Namespace: "*",
|
||||||
|
Audiences: []string{"folder.grafana.app"},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create access token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Set("X-Access-Token", "Bearer "+res.Token)
|
||||||
|
return t.rt.RoundTrip(r)
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
|
|
||||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
||||||
"github.com/grafana/authlib/cache"
|
"github.com/grafana/authlib/cache"
|
||||||
claims "github.com/grafana/authlib/types"
|
"github.com/grafana/authlib/types"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
@@ -41,6 +41,7 @@ type Service struct {
|
|||||||
authzextv1.UnimplementedAuthzExtentionServiceServer
|
authzextv1.UnimplementedAuthzExtentionServiceServer
|
||||||
|
|
||||||
store store.Store
|
store store.Store
|
||||||
|
folderStore store.FolderStore
|
||||||
permissionStore store.PermissionStore
|
permissionStore store.PermissionStore
|
||||||
identityStore legacy.LegacyIdentityStore
|
identityStore legacy.LegacyIdentityStore
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ type Service struct {
|
|||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
sql legacysql.LegacyDatabaseProvider,
|
sql legacysql.LegacyDatabaseProvider,
|
||||||
|
folderStore store.FolderStore,
|
||||||
identityStore legacy.LegacyIdentityStore,
|
identityStore legacy.LegacyIdentityStore,
|
||||||
permissionStore store.PermissionStore,
|
permissionStore store.PermissionStore,
|
||||||
logger log.Logger,
|
logger log.Logger,
|
||||||
@@ -72,6 +74,7 @@ func NewService(
|
|||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
store: store.NewStore(sql, tracer),
|
store: store.NewStore(sql, tracer),
|
||||||
|
folderStore: folderStore,
|
||||||
permissionStore: permissionStore,
|
permissionStore: permissionStore,
|
||||||
identityStore: identityStore,
|
identityStore: identityStore,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
@@ -209,40 +212,42 @@ func (s *Service) validateListRequest(ctx context.Context, req *authzv1.ListRequ
|
|||||||
return listReq, nil
|
return listReq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateNamespace(ctx context.Context, nameSpace string) (claims.NamespaceInfo, error) {
|
func validateNamespace(ctx context.Context, nameSpace string) (types.NamespaceInfo, error) {
|
||||||
if nameSpace == "" {
|
if nameSpace == "" {
|
||||||
return claims.NamespaceInfo{}, status.Error(codes.InvalidArgument, "namespace is required")
|
return types.NamespaceInfo{}, status.Error(codes.InvalidArgument, "namespace is required")
|
||||||
}
|
}
|
||||||
authInfo, has := claims.AuthInfoFrom(ctx)
|
authInfo, has := types.AuthInfoFrom(ctx)
|
||||||
if !has {
|
if !has {
|
||||||
return claims.NamespaceInfo{}, status.Error(codes.Internal, "could not get auth info from context")
|
return types.NamespaceInfo{}, status.Error(codes.Internal, "could not get auth info from context")
|
||||||
}
|
}
|
||||||
if !claims.NamespaceMatches(authInfo.GetNamespace(), nameSpace) {
|
if !types.NamespaceMatches(authInfo.GetNamespace(), nameSpace) {
|
||||||
return claims.NamespaceInfo{}, status.Error(codes.PermissionDenied, "namespace does not match")
|
return types.NamespaceInfo{}, status.Error(codes.PermissionDenied, "namespace does not match")
|
||||||
}
|
}
|
||||||
|
|
||||||
ns, err := claims.ParseNamespace(nameSpace)
|
ns, err := types.ParseNamespace(nameSpace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return claims.NamespaceInfo{}, err
|
return types.NamespaceInfo{}, err
|
||||||
}
|
}
|
||||||
return ns, nil
|
return ns, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) validateSubject(ctx context.Context, subject string) (string, claims.IdentityType, error) {
|
func (s *Service) validateSubject(ctx context.Context, subject string) (string, types.IdentityType, error) {
|
||||||
if subject == "" {
|
if subject == "" {
|
||||||
return "", "", status.Error(codes.InvalidArgument, "subject is required")
|
return "", "", status.Error(codes.InvalidArgument, "subject is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctxLogger := s.logger.FromContext(ctx)
|
ctxLogger := s.logger.FromContext(ctx)
|
||||||
identityType, userUID, err := claims.ParseTypeID(subject)
|
identityType, userUID, err := types.ParseTypeID(subject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Permission check currently only checks user, anonymous user, service account and renderer permissions
|
// Permission check currently only checks user, anonymous user, service account and renderer permissions
|
||||||
if !(identityType == claims.TypeUser || identityType == claims.TypeServiceAccount || identityType == claims.TypeAnonymous || identityType == claims.TypeRenderService) {
|
if !types.IsIdentityType(identityType, types.TypeUser, types.TypeServiceAccount, types.TypeAnonymous, types.TypeRenderService) {
|
||||||
ctxLogger.Error("unsupported identity type", "type", identityType)
|
ctxLogger.Error("unsupported identity type", "type", identityType)
|
||||||
return "", "", status.Error(codes.PermissionDenied, "unsupported identity type")
|
return "", "", status.Error(codes.PermissionDenied, "unsupported identity type")
|
||||||
}
|
}
|
||||||
|
|
||||||
return userUID, identityType, nil
|
return userUID, identityType, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,30 +269,29 @@ func (s *Service) validateAction(ctx context.Context, group, resource, verb stri
|
|||||||
return action, nil
|
return action, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) getIdentityPermissions(ctx context.Context, ns claims.NamespaceInfo, idType claims.IdentityType, userID, action string) (map[string]bool, error) {
|
func (s *Service) getIdentityPermissions(ctx context.Context, ns types.NamespaceInfo, idType types.IdentityType, userID, action string) (map[string]bool, error) {
|
||||||
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getIdentityPermissions")
|
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getIdentityPermissions")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
// When checking folder creation permissions, also check edit and admin action sets for folder, as the scoped folder create actions aren't stored in the DB separately
|
// When checking folder creation permissions, also check edit and admin action sets for folder, as the scoped folder create actions aren't stored in the DB separately
|
||||||
var actionSets []string
|
var actionSets []string
|
||||||
if action == "folders:create" {
|
if action == "folders:create" {
|
||||||
actionSets = append(actionSets, "folders:edit")
|
actionSets = append(actionSets, "folders:edit", "folders:admin")
|
||||||
actionSets = append(actionSets, "folders:admin")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch idType {
|
switch idType {
|
||||||
case claims.TypeAnonymous:
|
case types.TypeAnonymous:
|
||||||
return s.getAnonymousPermissions(ctx, ns, action, actionSets)
|
return s.getAnonymousPermissions(ctx, ns, action, actionSets)
|
||||||
case claims.TypeRenderService:
|
case types.TypeRenderService:
|
||||||
return s.getRendererPermissions(ctx, action)
|
return s.getRendererPermissions(ctx, action)
|
||||||
case claims.TypeUser, claims.TypeServiceAccount:
|
case types.TypeUser, types.TypeServiceAccount:
|
||||||
return s.getUserPermissions(ctx, ns, userID, action, actionSets)
|
return s.getUserPermissions(ctx, ns, userID, action, actionSets)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported identity type: %s", idType)
|
return nil, fmt.Errorf("unsupported identity type: %s", idType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) getUserPermissions(ctx context.Context, ns claims.NamespaceInfo, userID, action string, actionSets []string) (map[string]bool, error) {
|
func (s *Service) getUserPermissions(ctx context.Context, ns types.NamespaceInfo, userID, action string, actionSets []string) (map[string]bool, error) {
|
||||||
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getUserPermissions")
|
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getUserPermissions")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
@@ -342,7 +346,7 @@ func (s *Service) getUserPermissions(ctx context.Context, ns claims.NamespaceInf
|
|||||||
return res.(map[string]bool), nil
|
return res.(map[string]bool), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) getAnonymousPermissions(ctx context.Context, ns claims.NamespaceInfo, action string, actionSets []string) (map[string]bool, error) {
|
func (s *Service) getAnonymousPermissions(ctx context.Context, ns types.NamespaceInfo, action string, actionSets []string) (map[string]bool, error) {
|
||||||
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getAnonymousPermissions")
|
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getAnonymousPermissions")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
@@ -378,7 +382,7 @@ func (s *Service) getRendererPermissions(ctx context.Context, action string) (ma
|
|||||||
return map[string]bool{}, nil
|
return map[string]bool{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetUserIdentifiers(ctx context.Context, ns claims.NamespaceInfo, userUID string) (*store.UserIdentifiers, error) {
|
func (s *Service) GetUserIdentifiers(ctx context.Context, ns types.NamespaceInfo, userUID string) (*store.UserIdentifiers, error) {
|
||||||
uidCacheKey := userIdentifierCacheKey(ns.Value, userUID)
|
uidCacheKey := userIdentifierCacheKey(ns.Value, userUID)
|
||||||
if cached, ok := s.idCache.Get(ctx, uidCacheKey); ok {
|
if cached, ok := s.idCache.Get(ctx, uidCacheKey); ok {
|
||||||
return &cached, nil
|
return &cached, nil
|
||||||
@@ -397,7 +401,7 @@ func (s *Service) GetUserIdentifiers(ctx context.Context, ns claims.NamespaceInf
|
|||||||
userIDQuery = store.UserIdentifierQuery{UserUID: userUID}
|
userIDQuery = store.UserIdentifierQuery{UserUID: userUID}
|
||||||
}
|
}
|
||||||
userIdentifiers, err := s.store.GetUserIdentifiers(ctx, userIDQuery)
|
userIdentifiers, err := s.store.GetUserIdentifiers(ctx, userIDQuery)
|
||||||
if err != nil || userIdentifiers == nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not get user internal id: %w", err)
|
return nil, fmt.Errorf("could not get user internal id: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,7 +411,7 @@ func (s *Service) GetUserIdentifiers(ctx context.Context, ns claims.NamespaceInf
|
|||||||
return userIdentifiers, nil
|
return userIdentifiers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) getUserTeams(ctx context.Context, ns claims.NamespaceInfo, userIdentifiers *store.UserIdentifiers) ([]int64, error) {
|
func (s *Service) getUserTeams(ctx context.Context, ns types.NamespaceInfo, userIdentifiers *store.UserIdentifiers) ([]int64, error) {
|
||||||
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getUserTeams")
|
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getUserTeams")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
@@ -441,7 +445,7 @@ func (s *Service) getUserTeams(ctx context.Context, ns claims.NamespaceInfo, use
|
|||||||
return teamIDs, nil
|
return teamIDs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) getUserBasicRole(ctx context.Context, ns claims.NamespaceInfo, userIdentifiers *store.UserIdentifiers) (store.BasicRole, error) {
|
func (s *Service) getUserBasicRole(ctx context.Context, ns types.NamespaceInfo, userIdentifiers *store.UserIdentifiers) (store.BasicRole, error) {
|
||||||
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getUserBasicRole")
|
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.getUserBasicRole")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
@@ -535,7 +539,7 @@ func (s *Service) checkInheritedPermissions(ctx context.Context, scopeMap map[st
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) buildFolderTree(ctx context.Context, ns claims.NamespaceInfo) (folderTree, error) {
|
func (s *Service) buildFolderTree(ctx context.Context, ns types.NamespaceInfo) (folderTree, error) {
|
||||||
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.buildFolderTree")
|
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.buildFolderTree")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
@@ -545,7 +549,7 @@ func (s *Service) buildFolderTree(ctx context.Context, ns claims.NamespaceInfo)
|
|||||||
}
|
}
|
||||||
|
|
||||||
res, err, _ := s.sf.Do(ns.Value+"_buildFolderTree", func() (interface{}, error) {
|
res, err, _ := s.sf.Do(ns.Value+"_buildFolderTree", func() (interface{}, error) {
|
||||||
folders, err := s.store.GetFolders(ctx, ns)
|
folders, err := s.folderStore.ListFolders(ctx, ns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not get folders: %w", err)
|
return nil, fmt.Errorf("could not get folders: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -620,6 +620,7 @@ func setupService() *Service {
|
|||||||
folderCache: newCacheWrap[folderTree](cache, logger, shortCacheTTL),
|
folderCache: newCacheWrap[folderTree](cache, logger, shortCacheTTL),
|
||||||
store: fStore,
|
store: fStore,
|
||||||
permissionStore: fStore,
|
permissionStore: fStore,
|
||||||
|
folderStore: fStore,
|
||||||
identityStore: &fakeIdentityStore{},
|
identityStore: &fakeIdentityStore{},
|
||||||
sf: new(singleflight.Group),
|
sf: new(singleflight.Group),
|
||||||
}
|
}
|
||||||
@@ -663,7 +664,7 @@ func (f *fakeStore) GetUserPermissions(ctx context.Context, namespace claims.Nam
|
|||||||
return f.userPermissions, nil
|
return f.userPermissions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeStore) GetFolders(ctx context.Context, namespace claims.NamespaceInfo) ([]store.Folder, error) {
|
func (f *fakeStore) ListFolders(ctx context.Context, namespace claims.NamespaceInfo) ([]store.Folder, error) {
|
||||||
f.calls++
|
f.calls++
|
||||||
if f.err {
|
if f.err {
|
||||||
return nil, fmt.Errorf("store error")
|
return nil, fmt.Errorf("store error")
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/grafana/authlib/types"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
"k8s.io/client-go/tools/pager"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||||
|
folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
|
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||||
|
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FolderStore interface {
|
||||||
|
ListFolders(ctx context.Context, ns types.NamespaceInfo) ([]Folder, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Folder struct {
|
||||||
|
UID string
|
||||||
|
ParentUID *string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ FolderStore = (*SQLFolderStore)(nil)
|
||||||
|
|
||||||
|
func NewSQLFolderStore(sql legacysql.LegacyDatabaseProvider, tracer tracing.Tracer) *SQLFolderStore {
|
||||||
|
return &SQLFolderStore{sql, tracer}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SQLFolderStore struct {
|
||||||
|
sql legacysql.LegacyDatabaseProvider
|
||||||
|
tracer tracing.Tracer
|
||||||
|
}
|
||||||
|
|
||||||
|
var sqlFolders = mustTemplate("folder_query.sql")
|
||||||
|
|
||||||
|
type listFoldersQuery struct {
|
||||||
|
sqltemplate.SQLTemplate
|
||||||
|
|
||||||
|
Query *FolderQuery
|
||||||
|
FolderTable string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FolderQuery struct {
|
||||||
|
OrgID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r listFoldersQuery) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newListFolders(sql *legacysql.LegacyDatabaseHelper, query *FolderQuery) listFoldersQuery {
|
||||||
|
return listFoldersQuery{
|
||||||
|
SQLTemplate: sqltemplate.New(sql.DialectForDriver()),
|
||||||
|
Query: query,
|
||||||
|
FolderTable: sql.Table("folder"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SQLFolderStore) ListFolders(ctx context.Context, ns types.NamespaceInfo) ([]Folder, error) {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "authz_direct_db.database.ListFolders")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
sql, err := s.sql(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := newListFolders(sql, &FolderQuery{OrgID: ns.OrgID})
|
||||||
|
q, err := sqltemplate.Execute(sqlFolders, query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := sql.DB.GetSqlxSession().Query(ctx, q, query.GetArgs()...)
|
||||||
|
defer func() {
|
||||||
|
if rows != nil {
|
||||||
|
_ = rows.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var folders []Folder
|
||||||
|
for rows.Next() {
|
||||||
|
var folder Folder
|
||||||
|
if err := rows.Scan(&folder.UID, &folder.ParentUID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
folders = append(folders, folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
return folders, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ FolderStore = (*APIFolderStore)(nil)
|
||||||
|
|
||||||
|
func NewAPIFolderStore(tracer tracing.Tracer, configProvider func(ctx context.Context) *rest.Config) *APIFolderStore {
|
||||||
|
return &APIFolderStore{tracer, configProvider}
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIFolderStore struct {
|
||||||
|
tracer tracing.Tracer
|
||||||
|
configProvider func(ctx context.Context) *rest.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *APIFolderStore) ListFolders(ctx context.Context, ns types.NamespaceInfo) ([]Folder, error) {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "authz.apistore.ListFolders")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
client, err := s.client(ctx, ns.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create resource client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := pager.New(func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) {
|
||||||
|
return client.List(ctx, opts)
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultPageSize = 500
|
||||||
|
folders := make([]Folder, 0, defaultPageSize)
|
||||||
|
err = p.EachListItem(ctx, metav1.ListOptions{Limit: defaultPageSize}, func(obj runtime.Object) error {
|
||||||
|
object, err := utils.MetaAccessor(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
folder := Folder{UID: object.GetName()}
|
||||||
|
parent := object.GetFolder()
|
||||||
|
if parent != "" {
|
||||||
|
folder.ParentUID = &parent
|
||||||
|
}
|
||||||
|
|
||||||
|
folders = append(folders, folder)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetching folders: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return folders, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *APIFolderStore) client(ctx context.Context, namespace string) (dynamic.ResourceInterface, error) {
|
||||||
|
client, err := dynamic.NewForConfig(s.configProvider(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return client.Resource(folderv0alpha1.FolderResourceInfo.GroupVersionResource()).Namespace(namespace), nil
|
||||||
|
}
|
||||||
@@ -19,21 +19,3 @@ type UserIdentifierQuery struct {
|
|||||||
UserID int64
|
UserID int64
|
||||||
UserUID string
|
UserUID string
|
||||||
}
|
}
|
||||||
|
|
||||||
type FolderQuery struct {
|
|
||||||
OrgID int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type DashboardQuery struct {
|
|
||||||
OrgID int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type Folder struct {
|
|
||||||
UID string
|
|
||||||
ParentUID *string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Dashboard struct {
|
|
||||||
UID string
|
|
||||||
ParentUID *string
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ var (
|
|||||||
|
|
||||||
sqlQueryBasicRoles = mustTemplate("basic_role_query.sql")
|
sqlQueryBasicRoles = mustTemplate("basic_role_query.sql")
|
||||||
sqlUserIdentifiers = mustTemplate("user_identifier_query.sql")
|
sqlUserIdentifiers = mustTemplate("user_identifier_query.sql")
|
||||||
sqlFolders = mustTemplate("folder_query.sql")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func mustTemplate(filename string) *template.Template {
|
func mustTemplate(filename string) *template.Template {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
type Store interface {
|
type Store interface {
|
||||||
GetUserIdentifiers(ctx context.Context, query UserIdentifierQuery) (*UserIdentifiers, error)
|
GetUserIdentifiers(ctx context.Context, query UserIdentifierQuery) (*UserIdentifiers, error)
|
||||||
GetBasicRoles(ctx context.Context, ns claims.NamespaceInfo, query BasicRoleQuery) (*BasicRole, error)
|
GetBasicRoles(ctx context.Context, ns claims.NamespaceInfo, query BasicRoleQuery) (*BasicRole, error)
|
||||||
GetFolders(ctx context.Context, ns claims.NamespaceInfo) ([]Folder, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type StoreImpl struct {
|
type StoreImpl struct {
|
||||||
@@ -104,41 +103,3 @@ func (s *StoreImpl) GetBasicRoles(ctx context.Context, ns claims.NamespaceInfo,
|
|||||||
|
|
||||||
return &role, nil
|
return &role, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StoreImpl) GetFolders(ctx context.Context, ns claims.NamespaceInfo) ([]Folder, error) {
|
|
||||||
ctx, span := s.tracer.Start(ctx, "authz_direct_db.database.GetFolders")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
sql, err := s.sql(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
query := FolderQuery{OrgID: ns.OrgID}
|
|
||||||
req := newGetFolders(sql, &query)
|
|
||||||
q, err := sqltemplate.Execute(sqlFolders, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...)
|
|
||||||
defer func() {
|
|
||||||
if rows != nil {
|
|
||||||
_ = rows.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var folders []Folder
|
|
||||||
for rows.Next() {
|
|
||||||
var folder Folder
|
|
||||||
if err := rows.Scan(&folder.UID, &folder.ParentUID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
folders = append(folders, folder)
|
|
||||||
}
|
|
||||||
|
|
||||||
return folders, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
package authz
|
|
||||||
|
|
||||||
import (
|
|
||||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
|
||||||
cache "github.com/grafana/authlib/cache"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
|
|
||||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
|
||||||
"github.com/grafana/grafana/pkg/services/authz/rbac"
|
|
||||||
"github.com/grafana/grafana/pkg/services/authz/rbac/store"
|
|
||||||
"github.com/grafana/grafana/pkg/services/grpcserver"
|
|
||||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RegisterRBACAuthZService(
|
|
||||||
handler grpcserver.Provider,
|
|
||||||
db legacysql.LegacyDatabaseProvider,
|
|
||||||
tracer tracing.Tracer,
|
|
||||||
reg prometheus.Registerer,
|
|
||||||
cache cache.Cache) {
|
|
||||||
server := rbac.NewService(
|
|
||||||
db,
|
|
||||||
legacy.NewLegacySQLStores(db),
|
|
||||||
store.NewSQLPermissionStore(db, tracer),
|
|
||||||
log.New("authz-grpc-server"),
|
|
||||||
tracer,
|
|
||||||
reg,
|
|
||||||
cache,
|
|
||||||
)
|
|
||||||
|
|
||||||
srv := handler.GetServer()
|
|
||||||
authzv1.RegisterAuthzServiceServer(srv, server)
|
|
||||||
authzextv1.RegisterAuthzExtentionServiceServer(srv, server)
|
|
||||||
}
|
|
||||||
@@ -105,6 +105,7 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -200,8 +201,10 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -248,8 +251,10 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -309,8 +314,10 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -423,8 +430,10 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -504,8 +513,10 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -615,8 +626,10 @@ func TestLoader_Load_CustomSource(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -696,8 +709,10 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -801,8 +816,10 @@ func TestLoader_Load_RBACReady(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -883,8 +900,10 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) {
|
|||||||
ExposedComponents: []string{},
|
ExposedComponents: []string{},
|
||||||
}},
|
}},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -964,8 +983,10 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -1060,8 +1081,10 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) {
|
|||||||
{Name: "Nginx Datasource", Type: "datasource", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-datasource"},
|
{Name: "Nginx Datasource", Type: "datasource", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-datasource"},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -1272,8 +1295,10 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -1314,8 +1339,10 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -1463,8 +1490,10 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
@@ -1512,8 +1541,10 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extensions: plugins.Extensions{
|
Extensions: plugins.Extensions{
|
||||||
AddedLinks: []plugins.AddedLink{},
|
AddedLinks: []plugins.AddedLink{},
|
||||||
AddedComponents: []plugins.AddedComponent{},
|
AddedComponents: []plugins.AddedComponent{},
|
||||||
|
AddedFunctions: []plugins.AddedFunction{},
|
||||||
|
|
||||||
ExposedComponents: []plugins.ExposedComponent{},
|
ExposedComponents: []plugins.ExposedComponent{},
|
||||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/apikey"
|
"github.com/grafana/grafana/pkg/services/apikey"
|
||||||
"github.com/grafana/grafana/pkg/services/auth"
|
|
||||||
"github.com/grafana/grafana/pkg/services/authn"
|
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
|
||||||
@@ -44,7 +42,6 @@ type ServiceAccountsService struct {
|
|||||||
secretScanService secretscan.Checker
|
secretScanService secretscan.Checker
|
||||||
orgService org.Service
|
orgService org.Service
|
||||||
serverLock *serverlock.ServerLockService
|
serverLock *serverlock.ServerLockService
|
||||||
idService auth.IDService
|
|
||||||
|
|
||||||
secretScanEnabled bool
|
secretScanEnabled bool
|
||||||
secretScanInterval time.Duration
|
secretScanInterval time.Duration
|
||||||
@@ -61,7 +58,6 @@ func ProvideServiceAccountsService(
|
|||||||
acService accesscontrol.Service,
|
acService accesscontrol.Service,
|
||||||
permissions accesscontrol.ServiceAccountPermissionsService,
|
permissions accesscontrol.ServiceAccountPermissionsService,
|
||||||
serverLockService *serverlock.ServerLockService,
|
serverLockService *serverlock.ServerLockService,
|
||||||
idService auth.IDService,
|
|
||||||
) (*ServiceAccountsService, error) {
|
) (*ServiceAccountsService, error) {
|
||||||
serviceAccountsStore := database.ProvideServiceAccountsStore(
|
serviceAccountsStore := database.ProvideServiceAccountsStore(
|
||||||
cfg,
|
cfg,
|
||||||
@@ -81,7 +77,6 @@ func ProvideServiceAccountsService(
|
|||||||
backgroundLog: log.New("serviceaccounts.background"),
|
backgroundLog: log.New("serviceaccounts.background"),
|
||||||
orgService: orgService,
|
orgService: orgService,
|
||||||
serverLock: serverLockService,
|
serverLock: serverLockService,
|
||||||
idService: idService,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := RegisterRoles(acService); err != nil {
|
if err := RegisterRoles(acService); err != nil {
|
||||||
@@ -271,10 +266,6 @@ func (sa *ServiceAccountsService) UpdateServiceAccount(ctx context.Context, orgI
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := sa.idService.RemoveIDToken(ctx, &authn.Identity{ID: strconv.FormatInt(serviceAccountID, 10), Type: claims.TypeServiceAccount, OrgID: orgID}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return sa.store.UpdateServiceAccount(ctx, orgID, serviceAccountID, saForm)
|
return sa.store.UpdateServiceAccount(ctx, orgID, serviceAccountID, saForm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,13 +45,16 @@ require (
|
|||||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 // indirect
|
||||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||||
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect
|
||||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||||
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect
|
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
|
github.com/Masterminds/semver v1.5.0 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||||
|
github.com/NYTimes/gziphandler v1.1.1 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.1.5 // indirect
|
github.com/ProtonMail/go-crypto v1.1.5 // indirect
|
||||||
github.com/RoaringBitmap/roaring v1.9.3 // indirect
|
github.com/RoaringBitmap/roaring v1.9.3 // indirect
|
||||||
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // indirect
|
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // indirect
|
||||||
@@ -84,6 +87,7 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
|
||||||
github.com/aws/smithy-go v1.20.3 // indirect
|
github.com/aws/smithy-go v1.20.3 // indirect
|
||||||
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bits-and-blooms/bitset v1.12.0 // indirect
|
github.com/bits-and-blooms/bitset v1.12.0 // indirect
|
||||||
@@ -106,7 +110,9 @@ require (
|
|||||||
github.com/blevesearch/zapx/v14 v14.3.10 // indirect
|
github.com/blevesearch/zapx/v14 v14.3.10 // indirect
|
||||||
github.com/blevesearch/zapx/v15 v15.3.16 // indirect
|
github.com/blevesearch/zapx/v15 v15.3.16 // indirect
|
||||||
github.com/blevesearch/zapx/v16 v16.1.8 // indirect
|
github.com/blevesearch/zapx/v16 v16.1.8 // indirect
|
||||||
|
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect
|
||||||
github.com/bufbuild/protocompile v0.4.0 // indirect
|
github.com/bufbuild/protocompile v0.4.0 // indirect
|
||||||
|
github.com/buger/jsonparser v1.1.1 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cheekybits/genny v1.0.0 // indirect
|
github.com/cheekybits/genny v1.0.0 // indirect
|
||||||
@@ -116,6 +122,8 @@ require (
|
|||||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/dennwc/varint v1.0.0 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/dlmiddlecote/sqlstats v1.0.2 // indirect
|
github.com/dlmiddlecote/sqlstats v1.0.2 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
@@ -136,8 +144,10 @@ require (
|
|||||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||||
github.com/gchaincl/sqlhooks v1.3.0 // indirect
|
github.com/gchaincl/sqlhooks v1.3.0 // indirect
|
||||||
github.com/getkin/kin-openapi v0.129.0 // indirect
|
github.com/getkin/kin-openapi v0.129.0 // indirect
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
|
||||||
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
||||||
github.com/go-kit/log v0.2.1 // indirect
|
github.com/go-kit/log v0.2.1 // indirect
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.4 // indirect
|
||||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
@@ -151,6 +161,7 @@ require (
|
|||||||
github.com/go-openapi/strfmt v0.23.0 // indirect
|
github.com/go-openapi/strfmt v0.23.0 // indirect
|
||||||
github.com/go-openapi/swag v0.23.0 // indirect
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
github.com/go-openapi/validate v0.24.0 // indirect
|
github.com/go-openapi/validate v0.24.0 // indirect
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
github.com/go-stack/stack v1.8.1 // indirect
|
github.com/go-stack/stack v1.8.1 // indirect
|
||||||
github.com/gobwas/glob v0.2.3 // indirect
|
github.com/gobwas/glob v0.2.3 // indirect
|
||||||
@@ -159,10 +170,12 @@ require (
|
|||||||
github.com/gogo/googleapis v1.4.1 // indirect
|
github.com/gogo/googleapis v1.4.1 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/gogo/status v1.1.1 // indirect
|
github.com/gogo/status v1.1.1 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||||
github.com/golang-migrate/migrate/v4 v4.7.0 // indirect
|
github.com/golang-migrate/migrate/v4 v4.7.0 // indirect
|
||||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
|
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
|
github.com/golang/mock v1.7.0-rc.1 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/google/btree v1.1.3 // indirect
|
github.com/google/btree v1.1.3 // indirect
|
||||||
@@ -177,6 +190,7 @@ require (
|
|||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||||
github.com/gorilla/mux v1.8.1 // indirect
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/grafana/alerting v0.0.0-20250207161551-04c87cf39038 // indirect
|
github.com/grafana/alerting v0.0.0-20250207161551-04c87cf39038 // indirect
|
||||||
github.com/grafana/authlib v0.0.0-20250206063954-bf4600a17569 // indirect
|
github.com/grafana/authlib v0.0.0-20250206063954-bf4600a17569 // indirect
|
||||||
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
||||||
@@ -185,8 +199,12 @@ require (
|
|||||||
github.com/grafana/grafana-aws-sdk v0.31.5 // indirect
|
github.com/grafana/grafana-aws-sdk v0.31.5 // indirect
|
||||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // indirect
|
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // indirect
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.265.0 // indirect
|
github.com/grafana/grafana-plugin-sdk-go v0.265.0 // indirect
|
||||||
|
github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d // indirect
|
||||||
|
github.com/grafana/grafana/pkg/promlib v0.0.8 // indirect
|
||||||
|
github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d // indirect
|
||||||
github.com/grafana/otel-profiling-go v0.5.1 // indirect
|
github.com/grafana/otel-profiling-go v0.5.1 // indirect
|
||||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
|
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
|
||||||
|
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||||
github.com/grafana/sqlds/v4 v4.1.3 // indirect
|
github.com/grafana/sqlds/v4 v4.1.3 // indirect
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
|
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect
|
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect
|
||||||
@@ -207,6 +225,7 @@ require (
|
|||||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.7.2 // indirect
|
github.com/jackc/pgx/v5 v5.7.2 // indirect
|
||||||
@@ -231,6 +250,7 @@ require (
|
|||||||
github.com/magefile/mage v1.15.0 // indirect
|
github.com/magefile/mage v1.15.0 // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
|
github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 // indirect
|
||||||
github.com/mattetti/filebuffer v1.0.1 // indirect
|
github.com/mattetti/filebuffer v1.0.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
@@ -252,12 +272,14 @@ require (
|
|||||||
github.com/mithrandie/go-file/v2 v2.1.0 // indirect
|
github.com/mithrandie/go-file/v2 v2.1.0 // indirect
|
||||||
github.com/mithrandie/go-text v1.6.0 // indirect
|
github.com/mithrandie/go-text v1.6.0 // indirect
|
||||||
github.com/mithrandie/ternary v1.1.1 // indirect
|
github.com/mithrandie/ternary v1.1.1 // indirect
|
||||||
|
github.com/moby/spdystream v0.5.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||||
github.com/mschoch/smat v0.2.0 // indirect
|
github.com/mschoch/smat v0.2.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
|
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
|
||||||
|
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||||
github.com/natefinch/wrap v0.2.0 // indirect
|
github.com/natefinch/wrap v0.2.0 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 // indirect
|
github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 // indirect
|
||||||
@@ -286,6 +308,7 @@ require (
|
|||||||
github.com/prometheus/common/sigv4 v0.1.0 // indirect
|
github.com/prometheus/common/sigv4 v0.1.0 // indirect
|
||||||
github.com/prometheus/exporter-toolkit v0.13.2 // indirect
|
github.com/prometheus/exporter-toolkit v0.13.2 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
|
github.com/prometheus/prometheus v0.301.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/rs/cors v1.11.1 // indirect
|
github.com/rs/cors v1.11.1 // indirect
|
||||||
@@ -298,7 +321,6 @@ require (
|
|||||||
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
|
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
|
||||||
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect
|
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
github.com/spf13/afero v1.11.0 // indirect
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
github.com/spf13/cast v1.7.0 // indirect
|
github.com/spf13/cast v1.7.0 // indirect
|
||||||
@@ -316,6 +338,7 @@ require (
|
|||||||
github.com/unknwon/com v1.0.1 // indirect
|
github.com/unknwon/com v1.0.1 // indirect
|
||||||
github.com/unknwon/log v0.0.0-20200308114134-929b1006e34a // indirect
|
github.com/unknwon/log v0.0.0-20200308114134-929b1006e34a // indirect
|
||||||
github.com/urfave/cli v1.22.16 // indirect
|
github.com/urfave/cli v1.22.16 // indirect
|
||||||
|
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||||
go.etcd.io/bbolt v1.3.11 // indirect
|
go.etcd.io/bbolt v1.3.11 // indirect
|
||||||
@@ -365,11 +388,14 @@ require (
|
|||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/mail.v2 v2.3.1 // indirect
|
gopkg.in/mail.v2 v2.3.1 // indirect
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect
|
gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
k8s.io/api v0.32.1 // indirect
|
k8s.io/api v0.32.1 // indirect
|
||||||
k8s.io/component-base v0.32.1 // indirect
|
k8s.io/component-base v0.32.1 // indirect
|
||||||
|
k8s.io/kms v0.32.1 // indirect
|
||||||
|
k8s.io/kube-aggregator v0.32.0 // indirect
|
||||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
|
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
|
||||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
||||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJ
|
|||||||
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
||||||
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
||||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||||
github.com/at-wat/mqtt-go v0.19.4 h1:R2cbCU7O5PHQ38unbe1Y51ncG3KsFEJV6QeipDoqdLQ=
|
github.com/at-wat/mqtt-go v0.19.4 h1:R2cbCU7O5PHQ38unbe1Y51ncG3KsFEJV6QeipDoqdLQ=
|
||||||
@@ -184,6 +186,8 @@ github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
|
|||||||
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||||
|
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=
|
||||||
|
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0=
|
||||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
@@ -341,6 +345,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
|
|||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
|
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
|
||||||
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
|
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
|
||||||
|
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
|
||||||
|
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||||
@@ -385,6 +391,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
|||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
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/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-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
|
github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
|
||||||
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
|
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
|
||||||
github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
|
github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
|
||||||
@@ -547,7 +555,6 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
|
|||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
|
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
|
||||||
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
|
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
@@ -803,6 +810,8 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
|
|||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
||||||
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||||
|
github.com/mocktools/go-smtp-mock/v2 v2.3.1 h1:wq75NDSsOy5oHo/gEQQT0fRRaYKRqr1IdkjhIPXxagM=
|
||||||
|
github.com/mocktools/go-smtp-mock/v2 v2.3.1/go.mod h1:h9AOf/IXLSU2m/1u4zsjtOM/WddPwdOUBz56dV9f81M=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -827,6 +836,8 @@ 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/natefinch/wrap v0.2.0/go.mod h1:6gMHlAl12DwYEfKP3TkuykYUfLSEAvHw67itm4/KAS8=
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 h1:nZspmSkneBbtxU9TopEAE0CY+SBJLxO8LPUlw2vG4pU=
|
github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 h1:nZspmSkneBbtxU9TopEAE0CY+SBJLxO8LPUlw2vG4pU=
|
||||||
github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80/go.mod h1:7tFDb+Y51LcDpn26GccuUgQXUk6t0CXZsivKjyimYX8=
|
github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80/go.mod h1:7tFDb+Y51LcDpn26GccuUgQXUk6t0CXZsivKjyimYX8=
|
||||||
github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 h1:t05Ww3DxZutOqbMN+7OIuqDwXbhl32HiZGpLy26BAPc=
|
github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 h1:t05Ww3DxZutOqbMN+7OIuqDwXbhl32HiZGpLy26BAPc=
|
||||||
@@ -840,8 +851,9 @@ github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNs
|
|||||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
|
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||||
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
|
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
|
||||||
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
@@ -934,6 +946,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
|
|||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
github.com/prometheus/prometheus v0.301.0 h1:0z8dgegmILivNomCd79RKvVkIols8vBGPKmcIBc7OyY=
|
github.com/prometheus/prometheus v0.301.0 h1:0z8dgegmILivNomCd79RKvVkIols8vBGPKmcIBc7OyY=
|
||||||
github.com/prometheus/prometheus v0.301.0/go.mod h1:BJLjWCKNfRfjp7Q48DrAjARnCi7GhfUVvUFEAWTssZM=
|
github.com/prometheus/prometheus v0.301.0/go.mod h1:BJLjWCKNfRfjp7Q48DrAjARnCi7GhfUVvUFEAWTssZM=
|
||||||
|
github.com/prometheus/sigv4 v0.1.0 h1:FgxH+m1qf9dGQ4w8Dd6VkthmpFQfGTzUeavMoQeG1LA=
|
||||||
|
github.com/prometheus/sigv4 v0.1.0/go.mod h1:doosPW9dOitMzYe2I2BN0jZqUuBrGPbXrNsTScN18iU=
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||||
@@ -973,7 +987,6 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
|
|||||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
|
||||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
|
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
|
||||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||||
@@ -1042,7 +1055,6 @@ github.com/unknwon/log v0.0.0-20200308114134-929b1006e34a/go.mod h1:1xEUf2abjfP9
|
|||||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
|
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
|
||||||
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
|
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
|
||||||
github.com/wk8/go-ordered-map v1.0.0 h1:BV7z+2PaK8LTSd/mWgY12HyMAo5CEgkHqbkVq2thqr8=
|
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
@@ -1058,6 +1070,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
|||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
@@ -1153,6 +1166,7 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3
|
|||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
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=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
@@ -1190,6 +1204,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
|||||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
@@ -1235,6 +1250,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
|||||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
@@ -1262,6 +1279,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
@@ -1317,6 +1335,7 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/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-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -1369,7 +1388,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
|
|||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190424220101-1e8e1cfdf96b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190424220101-1e8e1cfdf96b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20190425222832-ad9eeb80039a/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190425222832-ad9eeb80039a/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
@@ -1410,6 +1428,7 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc
|
|||||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
@@ -1547,6 +1566,7 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST
|
|||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
gopkg.in/src-d/go-errors.v1 v1.0.0 h1:cooGdZnCjYbeS1zb1s6pVAAimTdKceRrpn7aKOnNIfc=
|
gopkg.in/src-d/go-errors.v1 v1.0.0 h1:cooGdZnCjYbeS1zb1s6pVAAimTdKceRrpn7aKOnNIfc=
|
||||||
gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMMNxPCbdYg=
|
gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMMNxPCbdYg=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
|||||||
@@ -13,9 +13,14 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/exp/rand"
|
"golang.org/x/exp/rand"
|
||||||
"k8s.io/apimachinery/pkg/api/apitesting"
|
"k8s.io/apimachinery/pkg/api/apitesting"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
"k8s.io/apiserver/pkg/storage"
|
"k8s.io/apiserver/pkg/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var scheme = runtime.NewScheme()
|
||||||
|
var codecs = serializer.NewCodecFactory(scheme)
|
||||||
|
|
||||||
func TestPrepareObjectForStorage(t *testing.T) {
|
func TestPrepareObjectForStorage(t *testing.T) {
|
||||||
_ = v0alpha1.AddToScheme(scheme)
|
_ = v0alpha1.AddToScheme(scheme)
|
||||||
node, err := snowflake.NewNode(rand.Int63n(1024))
|
node, err := snowflake.NewNode(rand.Int63n(1024))
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// Provenance-includes-license: Apache-2.0
|
// Provenance-includes-license: Apache-2.0
|
||||||
// Provenance-includes-copyright: The Kubernetes Authors.
|
// Provenance-includes-copyright: The Kubernetes Authors.
|
||||||
|
|
||||||
package apistore
|
package apistore_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@@ -19,7 +18,6 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||||
|
|
||||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
|
||||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -128,41 +126,3 @@ func isUnchanged(codec runtime.Codec, obj runtime.Object, newObj runtime.Object)
|
|||||||
|
|
||||||
return bytes.Equal(buf.Bytes(), newBuf.Bytes()), nil
|
return bytes.Equal(buf.Bytes(), newBuf.Bytes()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func testKeyParser(val string) (*resource.ResourceKey, error) {
|
|
||||||
k, err := grafanaregistry.ParseKey(val)
|
|
||||||
if err != nil {
|
|
||||||
if strings.HasPrefix(val, "pods/") {
|
|
||||||
parts := strings.Split(val, "/")
|
|
||||||
if len(parts) == 2 {
|
|
||||||
err = nil
|
|
||||||
k = &grafanaregistry.Key{
|
|
||||||
Resource: parts[0], // pods
|
|
||||||
Name: parts[1],
|
|
||||||
}
|
|
||||||
} else if len(parts) == 3 {
|
|
||||||
err = nil
|
|
||||||
k = &grafanaregistry.Key{
|
|
||||||
Resource: parts[0], // pods
|
|
||||||
Namespace: parts[1],
|
|
||||||
Name: parts[2],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if k.Group == "" {
|
|
||||||
k.Group = "example.apiserver.k8s.io"
|
|
||||||
}
|
|
||||||
if k.Resource == "" {
|
|
||||||
return nil, apierrors.NewInternalError(fmt.Errorf("missing resource in request"))
|
|
||||||
}
|
|
||||||
return &resource.ResourceKey{
|
|
||||||
Namespace: k.Namespace,
|
|
||||||
Group: k.Group,
|
|
||||||
Resource: k.Resource,
|
|
||||||
Name: k.Name,
|
|
||||||
}, err
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
// Provenance-includes-license: Apache-2.0
|
// Provenance-includes-license: Apache-2.0
|
||||||
// Provenance-includes-copyright: The Kubernetes Authors.
|
// Provenance-includes-copyright: The Kubernetes Authors.
|
||||||
|
|
||||||
package apistore
|
package apistore_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -16,6 +18,7 @@ import (
|
|||||||
"gocloud.dev/blob/fileblob"
|
"gocloud.dev/blob/fileblob"
|
||||||
"gocloud.dev/blob/memblob"
|
"gocloud.dev/blob/memblob"
|
||||||
"k8s.io/apimachinery/pkg/api/apitesting"
|
"k8s.io/apimachinery/pkg/api/apitesting"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
@@ -28,9 +31,11 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/storage/storagebackend"
|
"k8s.io/apiserver/pkg/storage/storagebackend"
|
||||||
"k8s.io/apiserver/pkg/storage/storagebackend/factory"
|
"k8s.io/apiserver/pkg/storage/storagebackend/factory"
|
||||||
|
|
||||||
|
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||||
storagetesting "github.com/grafana/grafana/pkg/apiserver/storage/testing"
|
storagetesting "github.com/grafana/grafana/pkg/apiserver/storage/testing"
|
||||||
infraDB "github.com/grafana/grafana/pkg/infra/db"
|
infraDB "github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||||
"github.com/grafana/grafana/pkg/storage/unified/sql"
|
"github.com/grafana/grafana/pkg/storage/unified/sql"
|
||||||
"github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl"
|
"github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl"
|
||||||
@@ -160,7 +165,7 @@ func testSetup(t testing.TB, opts ...setupOption) (context.Context, storage.Inte
|
|||||||
client := resource.NewLocalResourceClient(server)
|
client := resource.NewLocalResourceClient(server)
|
||||||
|
|
||||||
config := storagebackend.NewDefaultConfig(setupOpts.prefix, setupOpts.codec)
|
config := storagebackend.NewDefaultConfig(setupOpts.prefix, setupOpts.codec)
|
||||||
store, destroyFunc, err := NewStorage(
|
store, destroyFunc, err := apistore.NewStorage(
|
||||||
config.ForResource(setupOpts.groupResource),
|
config.ForResource(setupOpts.groupResource),
|
||||||
client,
|
client,
|
||||||
func(obj runtime.Object) (string, error) {
|
func(obj runtime.Object) (string, error) {
|
||||||
@@ -176,7 +181,7 @@ func testSetup(t testing.TB, opts ...setupOption) (context.Context, storage.Inte
|
|||||||
storage.DefaultNamespaceScopedAttr,
|
storage.DefaultNamespaceScopedAttr,
|
||||||
make(map[string]storage.IndexerFunc, 0),
|
make(map[string]storage.IndexerFunc, 0),
|
||||||
nil,
|
nil,
|
||||||
StorageOptions{},
|
apistore.StorageOptions{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
@@ -371,3 +376,41 @@ func newPod() runtime.Object {
|
|||||||
func newPodList() runtime.Object {
|
func newPodList() runtime.Object {
|
||||||
return &example.PodList{}
|
return &example.PodList{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testKeyParser(val string) (*resource.ResourceKey, error) {
|
||||||
|
k, err := grafanaregistry.ParseKey(val)
|
||||||
|
if err != nil {
|
||||||
|
if strings.HasPrefix(val, "pods/") {
|
||||||
|
parts := strings.Split(val, "/")
|
||||||
|
if len(parts) == 2 {
|
||||||
|
err = nil
|
||||||
|
k = &grafanaregistry.Key{
|
||||||
|
Resource: parts[0], // pods
|
||||||
|
Name: parts[1],
|
||||||
|
}
|
||||||
|
} else if len(parts) == 3 {
|
||||||
|
err = nil
|
||||||
|
k = &grafanaregistry.Key{
|
||||||
|
Resource: parts[0], // pods
|
||||||
|
Namespace: parts[1],
|
||||||
|
Name: parts[2],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if k.Group == "" {
|
||||||
|
k.Group = "example.apiserver.k8s.io"
|
||||||
|
}
|
||||||
|
if k.Resource == "" {
|
||||||
|
return nil, apierrors.NewInternalError(fmt.Errorf("missing resource in request"))
|
||||||
|
}
|
||||||
|
return &resource.ResourceKey{
|
||||||
|
Namespace: k.Namespace,
|
||||||
|
Group: k.Group,
|
||||||
|
Resource: k.Resource,
|
||||||
|
Name: k.Name,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
|
|
||||||
"github.com/grafana/dskit/instrument"
|
"github.com/grafana/dskit/instrument"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
onceIndex sync.Once
|
onceIndex sync.Once
|
||||||
|
onceSprinkles sync.Once
|
||||||
IndexMetrics *BleveIndexMetrics
|
IndexMetrics *BleveIndexMetrics
|
||||||
SprinklesIndexMetrics *SprinklesMetrics
|
SprinklesIndexMetrics *SprinklesMetrics
|
||||||
)
|
)
|
||||||
@@ -37,7 +37,7 @@ type SprinklesMetrics struct {
|
|||||||
var IndexCreationBuckets = []float64{1, 5, 10, 25, 50, 75, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000}
|
var IndexCreationBuckets = []float64{1, 5, 10, 25, 50, 75, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000}
|
||||||
|
|
||||||
func NewSprinklesMetrics() *SprinklesMetrics {
|
func NewSprinklesMetrics() *SprinklesMetrics {
|
||||||
onceIndex.Do(func() {
|
onceSprinkles.Do(func() {
|
||||||
SprinklesIndexMetrics = &SprinklesMetrics{
|
SprinklesIndexMetrics = &SprinklesMetrics{
|
||||||
SprinklesLatency: prometheus.NewHistogram(prometheus.HistogramOpts{
|
SprinklesLatency: prometheus.NewHistogram(prometheus.HistogramOpts{
|
||||||
Namespace: "index_server",
|
Namespace: "index_server",
|
||||||
@@ -104,12 +104,11 @@ func NewIndexMetrics(indexDir string, searchBackend SearchBackend) *BleveIndexMe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SprinklesMetrics) Collect(ch chan<- prometheus.Metric) {
|
func (s *SprinklesMetrics) Collect(ch chan<- prometheus.Metric) {
|
||||||
// s.SprinklesLatency.Collect(ch)
|
s.SprinklesLatency.Collect(ch)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SprinklesMetrics) Describe(ch chan<- *prometheus.Desc) {
|
func (s *SprinklesMetrics) Describe(ch chan<- *prometheus.Desc) {
|
||||||
// avoid starup panic
|
s.SprinklesLatency.Describe(ch)
|
||||||
// s.SprinklesLatency.Describe(ch)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BleveIndexMetrics) Collect(ch chan<- prometheus.Metric) {
|
func (s *BleveIndexMetrics) Collect(ch chan<- prometheus.Metric) {
|
||||||
|
|||||||
@@ -0,0 +1,386 @@
|
|||||||
|
package alertmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/e2e"
|
||||||
|
gapi "github.com/grafana/grafana-api-golang-client"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultNetworkName = "e2e-grafana-am"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AlertRuleConfig struct {
|
||||||
|
PendingPeriod string
|
||||||
|
GroupEvaluationIntervalSeconds int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationPolicyCfg struct {
|
||||||
|
GroupWait string
|
||||||
|
GroupInterval string
|
||||||
|
RepeatInterval string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProvisionCfg struct {
|
||||||
|
AlertRuleConfig
|
||||||
|
NotificationPolicyCfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlertmanagerScenario is a helper for writing tests which require some number of AM
|
||||||
|
// configured to communicate with some number of Grafana instances.
|
||||||
|
type AlertmanagerScenario struct {
|
||||||
|
*e2e.Scenario
|
||||||
|
|
||||||
|
Grafanas map[string]*GrafanaService
|
||||||
|
Webhook *WebhookService
|
||||||
|
Postgres *PostgresService
|
||||||
|
Loki *LokiService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAlertmanagerScenario() (*AlertmanagerScenario, error) {
|
||||||
|
s, err := e2e.NewScenario(getNetworkName())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AlertmanagerScenario{
|
||||||
|
Scenario: s,
|
||||||
|
Grafanas: make(map[string]*GrafanaService),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup starts a Grafana AM cluster of size n and all required dependencies
|
||||||
|
func (s *AlertmanagerScenario) Start(t *testing.T, n int, peerTimeout string, stopOnExtraDedup bool) {
|
||||||
|
is := getInstances(n)
|
||||||
|
ips := mapInstancePeers(is)
|
||||||
|
|
||||||
|
// start dependencies in one go
|
||||||
|
require.NoError(
|
||||||
|
t,
|
||||||
|
s.StartAndWaitReady([]e2e.Service{
|
||||||
|
s.NewWebhookService("webhook"),
|
||||||
|
s.NewLokiService("loki"),
|
||||||
|
s.NewPostgresService("postgres"),
|
||||||
|
}...),
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, ps := range ips {
|
||||||
|
require.NoError(t, s.StartAndWaitReady(s.NewGrafanaService(i, ps, peerTimeout, stopOnExtraDedup)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for instances to come online and cluster to be properly configured
|
||||||
|
time.Sleep(30 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provision provisions all required resources for the test
|
||||||
|
func (s *AlertmanagerScenario) Provision(t *testing.T, cfg ProvisionCfg) { //}*GrafanaClient {
|
||||||
|
c, err := s.NewGrafanaClient("grafana-1", 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dsUID := "integration-testdata"
|
||||||
|
|
||||||
|
// setup resources
|
||||||
|
_, err = c.NewDataSource(&gapi.DataSource{
|
||||||
|
Name: "grafana-testdata-datasource",
|
||||||
|
Type: "grafana-testdata-datasource",
|
||||||
|
Access: "proxy",
|
||||||
|
UID: dsUID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// setup loki for state history
|
||||||
|
_, err = c.NewDataSource(&gapi.DataSource{
|
||||||
|
Name: "loki",
|
||||||
|
Type: "loki",
|
||||||
|
URL: "http://loki:3100",
|
||||||
|
Access: "proxy",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = c.NewContactPoint(&gapi.ContactPoint{
|
||||||
|
Name: "webhook",
|
||||||
|
Type: "webhook",
|
||||||
|
Settings: map[string]any{
|
||||||
|
"url": "http://webhook:8080/notify",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, c.SetNotificationPolicyTree(&gapi.NotificationPolicyTree{
|
||||||
|
Receiver: "webhook",
|
||||||
|
GroupWait: cfg.GroupWait,
|
||||||
|
GroupInterval: cfg.GroupInterval,
|
||||||
|
RepeatInterval: cfg.RepeatInterval,
|
||||||
|
}))
|
||||||
|
|
||||||
|
f, err := c.NewFolder("integration_test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
r := &gapi.AlertRule{
|
||||||
|
Title: "integration rule",
|
||||||
|
Condition: "C",
|
||||||
|
FolderUID: f.UID,
|
||||||
|
ExecErrState: gapi.ErrError,
|
||||||
|
NoDataState: gapi.NoData,
|
||||||
|
For: cfg.PendingPeriod,
|
||||||
|
RuleGroup: "test",
|
||||||
|
Data: []*gapi.AlertQuery{
|
||||||
|
{
|
||||||
|
RefID: "A",
|
||||||
|
RelativeTimeRange: gapi.RelativeTimeRange{
|
||||||
|
From: 600,
|
||||||
|
To: 0,
|
||||||
|
},
|
||||||
|
DatasourceUID: dsUID,
|
||||||
|
Model: json.RawMessage(fmt.Sprintf(`{
|
||||||
|
"refId":"A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-testdata-datasource",
|
||||||
|
"uid": "%s"
|
||||||
|
},
|
||||||
|
"hide":false,
|
||||||
|
"range":false,
|
||||||
|
"instant":true,
|
||||||
|
"intervalMs":1000,
|
||||||
|
"maxDataPoints":43200,
|
||||||
|
"pulseWave": {
|
||||||
|
"offCount": 6,
|
||||||
|
"offValue": 0,
|
||||||
|
"onCount": 10,
|
||||||
|
"onValue": 10,
|
||||||
|
"timeStep": 10
|
||||||
|
},
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "predictable_pulse",
|
||||||
|
"seriesCount": 1
|
||||||
|
}`, dsUID)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
RefID: "B",
|
||||||
|
RelativeTimeRange: gapi.RelativeTimeRange{
|
||||||
|
From: 0,
|
||||||
|
To: 0,
|
||||||
|
},
|
||||||
|
DatasourceUID: "__expr__",
|
||||||
|
Model: json.RawMessage(`{
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"evaluator": {
|
||||||
|
"params": [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"type": "gt"
|
||||||
|
},
|
||||||
|
"operator": {
|
||||||
|
"type": "and"
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
"reducer": {
|
||||||
|
"params": [],
|
||||||
|
"type": "avg"
|
||||||
|
},
|
||||||
|
"type": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"datasource": {
|
||||||
|
"name": "Expression",
|
||||||
|
"type": "__expr__",
|
||||||
|
"uid": "__expr__"
|
||||||
|
},
|
||||||
|
"expression": "A",
|
||||||
|
"intervalMs": 1000,
|
||||||
|
"maxDataPoints": 43200,
|
||||||
|
"reducer": "last",
|
||||||
|
"refId": "B",
|
||||||
|
"type": "reduce"
|
||||||
|
}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
RefID: "C",
|
||||||
|
RelativeTimeRange: gapi.RelativeTimeRange{
|
||||||
|
From: 0,
|
||||||
|
To: 0,
|
||||||
|
},
|
||||||
|
DatasourceUID: "__expr__",
|
||||||
|
Model: json.RawMessage(`{
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"evaluator": {
|
||||||
|
"params": [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"type": "gt"
|
||||||
|
},
|
||||||
|
"operator": {
|
||||||
|
"type": "and"
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"params": [
|
||||||
|
"B"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reducer": {
|
||||||
|
"params": [],
|
||||||
|
"type": "last"
|
||||||
|
},
|
||||||
|
"type": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"datasource": {
|
||||||
|
"type": "__expr__",
|
||||||
|
"uid": "__expr__"
|
||||||
|
},
|
||||||
|
"hide": false,
|
||||||
|
"isPaused": false,
|
||||||
|
"intervalMs": 1000,
|
||||||
|
"maxDataPoints": 43200,
|
||||||
|
"refId": "C",
|
||||||
|
"expression": "B",
|
||||||
|
"type": "threshold"
|
||||||
|
}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err = c.NewAlertRule(r)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, c.SetAlertRuleGroup(gapi.RuleGroup{
|
||||||
|
Title: "test",
|
||||||
|
FolderUID: f.UID,
|
||||||
|
Interval: cfg.GroupEvaluationIntervalSeconds,
|
||||||
|
Rules: []gapi.AlertRule{*r},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGrafanaService creates a new Grafana instance.
|
||||||
|
func (s *AlertmanagerScenario) NewGrafanaService(name string, peers []string, peerTimeout string, stopOnExtraDedup bool) *GrafanaService {
|
||||||
|
flags := map[string]string{}
|
||||||
|
|
||||||
|
ft := []string{
|
||||||
|
"alertStateHistoryLokiSecondary",
|
||||||
|
"alertStateHistoryLokiPrimary",
|
||||||
|
"alertStateHistoryLokiOnly",
|
||||||
|
"alertingAlertmanagerExtraDedupStage",
|
||||||
|
}
|
||||||
|
if stopOnExtraDedup {
|
||||||
|
ft = append(ft, "alertingAlertmanagerExtraDedupStageStopPipeline")
|
||||||
|
}
|
||||||
|
envVars := map[string]string{
|
||||||
|
//"GF_LOG_MODE": "file", // disable console logging
|
||||||
|
"GF_LOG_LEVEL": "warn",
|
||||||
|
"GF_FEATURE_TOGGLES_ENABLE": strings.Join(ft, ","),
|
||||||
|
"GF_UNIFIED_ALERTING_ENABLED": "true",
|
||||||
|
"GF_UNIFIED_ALERTING_EXECUTE_ALERTS": "true",
|
||||||
|
"GF_UNIFIED_ALERTING_HA_PEER_TIMEOUT": peerTimeout,
|
||||||
|
"GF_UNIFIED_ALERTING_HA_RECONNECT_TIMEOUT": "2m",
|
||||||
|
"GF_UNIFIED_ALERTING_HA_LISTEN_ADDRESS": ":9094",
|
||||||
|
"GF_UNIFIED_ALERTING_HA_PEERS": strings.Join(peers, ","),
|
||||||
|
"GF_UNIFIED_ALERTING_STATE_HISTORY_ENABLED": "true",
|
||||||
|
"GF_UNIFIED_ALERTING_STATE_HISTORY_BACKEND": "loki",
|
||||||
|
"GF_UNIFIED_ALERTING_STATE_HISTORY_LOKI_REMOTE_URL": "http://loki:3100",
|
||||||
|
"GF_DATABASE_TYPE": "postgres",
|
||||||
|
"GF_DATABASE_HOST": "postgres:5432",
|
||||||
|
"GF_DATABASE_NAME": "grafana",
|
||||||
|
"GF_DATABASE_USER": "postgres",
|
||||||
|
"GF_DATABASE_PASSWORD": "password",
|
||||||
|
"GF_DATABASE_SSL_MODE": "disable",
|
||||||
|
}
|
||||||
|
|
||||||
|
g := NewGrafanaService(name, flags, envVars)
|
||||||
|
|
||||||
|
s.Grafanas[name] = g
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGrafanaService creates a new Grafana API client for the requested instance.
|
||||||
|
func (s *AlertmanagerScenario) NewGrafanaClient(grafanaName string, orgID int64) (*GrafanaClient, error) {
|
||||||
|
g, ok := s.Grafanas[grafanaName]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unknown grafana instance: %s", grafanaName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewGrafanaClient(g.HTTPEndpoint(), orgID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AlertmanagerScenario) NewWebhookClient() (*WebhookClient, error) {
|
||||||
|
return NewWebhookClient("http://" + s.Webhook.HTTPEndpoint())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AlertmanagerScenario) NewWebhookService(name string) *WebhookService {
|
||||||
|
ws := NewWebhookService(name, nil, nil)
|
||||||
|
s.Webhook = ws
|
||||||
|
|
||||||
|
return ws
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AlertmanagerScenario) NewLokiService(name string) *LokiService {
|
||||||
|
ls := NewLokiService(name, map[string]string{"--config.file": "/etc/loki/local-config.yaml"}, nil)
|
||||||
|
s.Loki = ls
|
||||||
|
|
||||||
|
return ls
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AlertmanagerScenario) NewPostgresService(name string) *PostgresService {
|
||||||
|
ps := NewPostgresService(name, map[string]string{"POSTGRES_PASSWORD": "password", "POSTGRES_DB": "grafana"})
|
||||||
|
s.Postgres = ps
|
||||||
|
|
||||||
|
return ps
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AlertmanagerScenario) NewLokiClient() (*LokiClient, error) {
|
||||||
|
return NewLokiClient("http://" + s.Loki.HTTPEndpoint())
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNetworkName() string {
|
||||||
|
// If the E2E_NETWORK_NAME is set, use that for the network name.
|
||||||
|
// Otherwise, return the default network name.
|
||||||
|
if os.Getenv("E2E_NETWORK_NAME") != "" {
|
||||||
|
return os.Getenv("E2E_NETWORK_NAME")
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultNetworkName
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInstances(n int) []string {
|
||||||
|
is := make([]string, n)
|
||||||
|
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
is[i] = "grafana-" + strconv.Itoa(i+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return is
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPeers(i string, is []string) []string {
|
||||||
|
peers := make([]string, 0, len(is)-1)
|
||||||
|
|
||||||
|
for _, p := range is {
|
||||||
|
if p != i {
|
||||||
|
peers = append(peers, p+":9094")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return peers
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapInstancePeers(is []string) map[string][]string {
|
||||||
|
mIs := make(map[string][]string, len(is))
|
||||||
|
|
||||||
|
for _, i := range is {
|
||||||
|
mIs[i] = getPeers(i, is)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mIs
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package alertmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAlertmanagerIntegration_ExtraDedupStage(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("assert no flapping alerts when stopOnExtraDedup is enabled", func(t *testing.T) {
|
||||||
|
s, err := NewAlertmanagerScenario()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
s.Start(t, 20, "15s", true)
|
||||||
|
s.Provision(t, ProvisionCfg{
|
||||||
|
AlertRuleConfig: AlertRuleConfig{
|
||||||
|
PendingPeriod: "30s",
|
||||||
|
GroupEvaluationIntervalSeconds: 10,
|
||||||
|
},
|
||||||
|
NotificationPolicyCfg: NotificationPolicyCfg{
|
||||||
|
GroupWait: "30s",
|
||||||
|
GroupInterval: "1m",
|
||||||
|
RepeatInterval: "30m",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
wc, err := s.NewWebhookClient()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
lc, err := s.NewLokiClient()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// notifications only start arriving after 2 to 3 minutes so we wait for that
|
||||||
|
time.Sleep(time.Minute * 2)
|
||||||
|
|
||||||
|
timeout := time.After(5 * time.Minute)
|
||||||
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
nr, err := wc.GetNotifications()
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("failed to get alert notifications: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the latest state for the alert from loki
|
||||||
|
st, err := lc.GetCurrentAlertState()
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("failed to get alert state: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the last state is not normal, ignore
|
||||||
|
// we might be missing other cases of flapping notifications but for now we are only interested in this one
|
||||||
|
// (alerting notification when state is already normal)
|
||||||
|
if st.State != AlertStateNormal {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// history is ordered - fetch the first notification that is after the last state change
|
||||||
|
var i int
|
||||||
|
for i = range nr.History {
|
||||||
|
if nr.History[i].TimeNow.After(st.Timestamp) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if all notifications are from before the last state change, we can wait a bit more
|
||||||
|
if nr.History[i].TimeNow.Before(st.Timestamp) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// for all notifications after the last state change, check if there is a firing one
|
||||||
|
for ; i < len(nr.History); i++ {
|
||||||
|
notification := nr.History[i]
|
||||||
|
if notification.Status == "firing" {
|
||||||
|
t.Errorf("flapping notifications - got firing notification when alert was resolved, state = %#v, notification = %#v", st, notification)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-timeout:
|
||||||
|
// if after the timeout there are no such cases, we assume there are no flapping notifications
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package alertmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/grafana/e2e"
|
||||||
|
gapi "github.com/grafana/grafana-api-golang-client"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
grafanaBinary = "/run.sh"
|
||||||
|
grafanaHTTPPort = 3000
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetDefaultImage returns the Docker image to use to run the Grafana..
|
||||||
|
func GetGrafanaImage() string {
|
||||||
|
if img := os.Getenv("GRAFANA_IMAGE"); img != "" {
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
|
||||||
|
if version := os.Getenv("GRAFANA_VERSION"); version != "" {
|
||||||
|
return "grafana/grafana-enterprise-dev:" + version
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("Provide GRAFANA_VERSION or GRAFANA_IMAGE")
|
||||||
|
}
|
||||||
|
|
||||||
|
type GrafanaService struct {
|
||||||
|
*e2e.HTTPService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGrafanaService(name string, flags, envVars map[string]string) *GrafanaService {
|
||||||
|
svc := &GrafanaService{
|
||||||
|
HTTPService: e2e.NewHTTPService(
|
||||||
|
name,
|
||||||
|
GetGrafanaImage(),
|
||||||
|
e2e.NewCommandWithoutEntrypoint(grafanaBinary, e2e.BuildArgs(flags)...),
|
||||||
|
e2e.NewHTTPReadinessProbe(grafanaHTTPPort, "/ready", 200, 299),
|
||||||
|
grafanaHTTPPort,
|
||||||
|
9094,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.SetEnvVars(envVars)
|
||||||
|
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
|
type GrafanaClient struct {
|
||||||
|
*gapi.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGrafanaClient creates a client for using the Grafana API. Note we don't bother
|
||||||
|
// wrapping the client library, and just use it as-is, until we find a reason not to.
|
||||||
|
func NewGrafanaClient(host string, orgID int64) (*GrafanaClient, error) {
|
||||||
|
cfg := gapi.Config{
|
||||||
|
BasicAuth: url.UserPassword("admin", "admin"),
|
||||||
|
OrgID: orgID,
|
||||||
|
HTTPHeaders: map[string]string{
|
||||||
|
"X-Disable-Provenance": "true",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gapi.New(fmt.Sprintf("http://%s/", host), cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &GrafanaClient{
|
||||||
|
Client: client,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package alertmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/e2e"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultLokiImage = "grafana/loki:latest"
|
||||||
|
lokiBinary = "/usr/bin/loki"
|
||||||
|
lokiHTTPPort = 3100
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetDefaultImage returns the Docker image to use to run the Loki..
|
||||||
|
func GetLokiImage() string {
|
||||||
|
if img := os.Getenv("LOKI_IMAGE"); img != "" {
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultLokiImage
|
||||||
|
}
|
||||||
|
|
||||||
|
type LokiService struct {
|
||||||
|
*e2e.HTTPService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLokiService(name string, flags, envVars map[string]string) *LokiService {
|
||||||
|
svc := &LokiService{
|
||||||
|
HTTPService: e2e.NewHTTPService(
|
||||||
|
name,
|
||||||
|
GetLokiImage(),
|
||||||
|
e2e.NewCommandWithoutEntrypoint(lokiBinary, e2e.BuildArgs(flags)...),
|
||||||
|
e2e.NewHTTPReadinessProbe(lokiHTTPPort, "/ready", 200, 299),
|
||||||
|
lokiHTTPPort,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.SetEnvVars(envVars)
|
||||||
|
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
|
type LokiClient struct {
|
||||||
|
c http.Client
|
||||||
|
u *url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLokiClient(u string) (*LokiClient, error) {
|
||||||
|
pu, err := url.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &LokiClient{
|
||||||
|
c: http.Client{},
|
||||||
|
u: pu,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type LokiQueryResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Data struct {
|
||||||
|
ResultType string `json:"resultType"`
|
||||||
|
Result []struct {
|
||||||
|
Stream struct {
|
||||||
|
Condition string `json:"condition"`
|
||||||
|
Current string `json:"current"`
|
||||||
|
DashboardUID string `json:"dashboardUID"`
|
||||||
|
Fingerprint string `json:"fingerprint"`
|
||||||
|
FolderUID string `json:"folderUID"`
|
||||||
|
From string `json:"from"`
|
||||||
|
Group string `json:"group"`
|
||||||
|
LabelsAlertname string `json:"labels_alertname"`
|
||||||
|
LabelsGrafanaFolder string `json:"labels_grafana_folder"`
|
||||||
|
OrgID string `json:"orgID"`
|
||||||
|
PanelID string `json:"panelID"`
|
||||||
|
Previous string `json:"previous"`
|
||||||
|
RuleID string `json:"ruleID"`
|
||||||
|
RuleTitle string `json:"ruleTitle"`
|
||||||
|
RuleUID string `json:"ruleUID"`
|
||||||
|
SchemaVersion string `json:"schemaVersion"`
|
||||||
|
ServiceName string `json:"service_name"`
|
||||||
|
ValuesB string `json:"values_B"`
|
||||||
|
ValuesC string `json:"values_C"`
|
||||||
|
} `json:"stream"`
|
||||||
|
Values [][]string `json:"values"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlertState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AlertStateNormal AlertState = "Normal"
|
||||||
|
AlertStatePending AlertState = "Pending"
|
||||||
|
AlertStateAlerting AlertState = "Alerting"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AlertStateResponse struct {
|
||||||
|
State AlertState
|
||||||
|
Timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentAlertState fetches the current alert state from loki
|
||||||
|
func (c *LokiClient) GetCurrentAlertState() (*AlertStateResponse, error) {
|
||||||
|
u := c.u.ResolveReference(&url.URL{Path: "/loki/api/v1/query_range"})
|
||||||
|
|
||||||
|
vs := url.Values{}
|
||||||
|
vs.Add("query", `{from="state-history"} | json`)
|
||||||
|
vs.Add("since", "60s")
|
||||||
|
|
||||||
|
u.RawQuery = vs.Encode()
|
||||||
|
|
||||||
|
resp, err := c.c.Get(u.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
//nolint:errcheck
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
res := LokiQueryResponse{}
|
||||||
|
|
||||||
|
if err = json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Status != "success" {
|
||||||
|
return nil, fmt.Errorf("failed to query state from loki")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res.Data.Result) == 0 {
|
||||||
|
return nil, fmt.Errorf("empty result from loki")
|
||||||
|
}
|
||||||
|
|
||||||
|
r := res.Data.Result[0]
|
||||||
|
it, err := strconv.ParseInt(r.Values[0][0], 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse timestamp: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AlertStateResponse{
|
||||||
|
State: AlertState(r.Stream.Current),
|
||||||
|
Timestamp: time.Unix(0, it),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package alertmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/grafana/e2e"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultPostgresImage = "postgres:16.4"
|
||||||
|
postgresHTTPPort = 5432
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetDefaultImage returns the Docker image to use to run the Postgres..
|
||||||
|
func GetPostgresImage() string {
|
||||||
|
if img := os.Getenv("POSTGRES_IMAGE"); img != "" {
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultPostgresImage
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostgresService struct {
|
||||||
|
*e2e.HTTPService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPostgresService(name string, envVars map[string]string) *PostgresService {
|
||||||
|
svc := &PostgresService{
|
||||||
|
HTTPService: e2e.NewHTTPService(
|
||||||
|
name,
|
||||||
|
GetPostgresImage(),
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
postgresHTTPPort,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.SetEnvVars(envVars)
|
||||||
|
|
||||||
|
return svc
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package alertmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/e2e"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultWebhookImage = "webhook-receiver"
|
||||||
|
webhookBinary = "/bin/main"
|
||||||
|
webhookHTTPPort = 8080
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebhookService struct {
|
||||||
|
*e2e.HTTPService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebhookService(name string, flags, envVars map[string]string) *WebhookService {
|
||||||
|
svc := &WebhookService{
|
||||||
|
HTTPService: e2e.NewHTTPService(
|
||||||
|
name,
|
||||||
|
"webhook-receiver",
|
||||||
|
e2e.NewCommandWithoutEntrypoint(webhookBinary, e2e.BuildArgs(flags)...),
|
||||||
|
e2e.NewHTTPReadinessProbe(webhookHTTPPort, "/ready", 200, 299),
|
||||||
|
webhookHTTPPort),
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.SetEnvVars(envVars)
|
||||||
|
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebhookClient struct {
|
||||||
|
c http.Client
|
||||||
|
u *url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebhookClient(u string) (*WebhookClient, error) {
|
||||||
|
pu, err := url.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &WebhookClient{
|
||||||
|
c: http.Client{},
|
||||||
|
u: pu,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetNotificationsResponse struct {
|
||||||
|
Stats map[string]int `json:"stats"`
|
||||||
|
History []struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
TimeNow time.Time `json:"timeNow"`
|
||||||
|
StartsAt time.Time `json:"startsAt"`
|
||||||
|
Node string `json:"node"`
|
||||||
|
DeltaLastSeconds float64 `json:"deltaLastSeconds"`
|
||||||
|
DeltaStartSeconds float64 `json:"deltaStartSeconds"`
|
||||||
|
} `json:"history"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotifications fetches notifications from the webhook server
|
||||||
|
func (c *WebhookClient) GetNotifications() (*GetNotificationsResponse, error) {
|
||||||
|
u := c.u.ResolveReference(&url.URL{Path: "/notifications"})
|
||||||
|
|
||||||
|
resp, err := c.c.Get(u.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
//nolint:errcheck
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
res := GetNotificationsResponse{}
|
||||||
|
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
+19
-16
@@ -5,7 +5,6 @@ import 'whatwg-fetch'; // fetch polyfill needed for PhantomJs rendering
|
|||||||
import 'file-saver';
|
import 'file-saver';
|
||||||
import 'jquery';
|
import 'jquery';
|
||||||
|
|
||||||
import _ from 'lodash'; // eslint-disable-line lodash/import-scope
|
|
||||||
import { createElement } from 'react';
|
import { createElement } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
@@ -40,12 +39,12 @@ import {
|
|||||||
setChromeHeaderHeightHook,
|
setChromeHeaderHeightHook,
|
||||||
setPluginLinksHook,
|
setPluginLinksHook,
|
||||||
setCorrelationsService,
|
setCorrelationsService,
|
||||||
|
setPluginFunctionsHook,
|
||||||
} from '@grafana/runtime';
|
} from '@grafana/runtime';
|
||||||
import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelDataErrorView';
|
import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelDataErrorView';
|
||||||
import { setPanelRenderer } from '@grafana/runtime/src/components/PanelRenderer';
|
import { setPanelRenderer } from '@grafana/runtime/src/components/PanelRenderer';
|
||||||
import { setPluginPage } from '@grafana/runtime/src/components/PluginPage';
|
import { setPluginPage } from '@grafana/runtime/src/components/PluginPage';
|
||||||
import config, { updateConfig } from 'app/core/config';
|
import config, { updateConfig } from 'app/core/config';
|
||||||
import { arrayMove } from 'app/core/utils/arrayMove';
|
|
||||||
import { getStandardTransformers } from 'app/features/transformers/standardTransformers';
|
import { getStandardTransformers } from 'app/features/transformers/standardTransformers';
|
||||||
|
|
||||||
import getDefaultMonacoLanguages from '../lib/monaco-languages';
|
import getDefaultMonacoLanguages from '../lib/monaco-languages';
|
||||||
@@ -66,13 +65,6 @@ import { backendSrv } from './core/services/backend_srv';
|
|||||||
import { contextSrv, RedirectToUrlKey } from './core/services/context_srv';
|
import { contextSrv, RedirectToUrlKey } from './core/services/context_srv';
|
||||||
import { Echo } from './core/services/echo/Echo';
|
import { Echo } from './core/services/echo/Echo';
|
||||||
import { reportPerformance } from './core/services/echo/EchoSrv';
|
import { reportPerformance } from './core/services/echo/EchoSrv';
|
||||||
import { PerformanceBackend } from './core/services/echo/backends/PerformanceBackend';
|
|
||||||
import { ApplicationInsightsBackend } from './core/services/echo/backends/analytics/ApplicationInsightsBackend';
|
|
||||||
import { BrowserConsoleBackend } from './core/services/echo/backends/analytics/BrowseConsoleBackend';
|
|
||||||
import { GA4EchoBackend } from './core/services/echo/backends/analytics/GA4Backend';
|
|
||||||
import { GAEchoBackend } from './core/services/echo/backends/analytics/GABackend';
|
|
||||||
import { RudderstackBackend } from './core/services/echo/backends/analytics/RudderstackBackend';
|
|
||||||
import { GrafanaJavascriptAgentBackend } from './core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend';
|
|
||||||
import { KeybindingSrv } from './core/services/keybindingSrv';
|
import { KeybindingSrv } from './core/services/keybindingSrv';
|
||||||
import { startMeasure, stopMeasure } from './core/utils/metrics';
|
import { startMeasure, stopMeasure } from './core/utils/metrics';
|
||||||
import { initDevFeatures } from './dev';
|
import { initDevFeatures } from './dev';
|
||||||
@@ -89,6 +81,7 @@ import { pluginExtensionRegistries } from './features/plugins/extensions/registr
|
|||||||
import { usePluginComponent } from './features/plugins/extensions/usePluginComponent';
|
import { usePluginComponent } from './features/plugins/extensions/usePluginComponent';
|
||||||
import { usePluginComponents } from './features/plugins/extensions/usePluginComponents';
|
import { usePluginComponents } from './features/plugins/extensions/usePluginComponents';
|
||||||
import { createUsePluginExtensions } from './features/plugins/extensions/usePluginExtensions';
|
import { createUsePluginExtensions } from './features/plugins/extensions/usePluginExtensions';
|
||||||
|
import { usePluginFunctions } from './features/plugins/extensions/usePluginFunctions';
|
||||||
import { usePluginLinks } from './features/plugins/extensions/usePluginLinks';
|
import { usePluginLinks } from './features/plugins/extensions/usePluginLinks';
|
||||||
import { getAppPluginsToAwait, getAppPluginsToPreload } from './features/plugins/extensions/utils';
|
import { getAppPluginsToAwait, getAppPluginsToPreload } from './features/plugins/extensions/utils';
|
||||||
import { importPanelPlugin, syncGetPanelPlugin } from './features/plugins/importPanelPlugin';
|
import { importPanelPlugin, syncGetPanelPlugin } from './features/plugins/importPanelPlugin';
|
||||||
@@ -111,10 +104,6 @@ import { createSystemVariableAdapter } from './features/variables/system/adapter
|
|||||||
import { createTextBoxVariableAdapter } from './features/variables/textbox/adapter';
|
import { createTextBoxVariableAdapter } from './features/variables/textbox/adapter';
|
||||||
import { configureStore } from './store/configureStore';
|
import { configureStore } from './store/configureStore';
|
||||||
|
|
||||||
// add move to lodash for backward compatabilty with plugins
|
|
||||||
// @ts-ignore
|
|
||||||
_.move = arrayMove;
|
|
||||||
|
|
||||||
// import symlinked extensions
|
// import symlinked extensions
|
||||||
const extensionsIndex = require.context('.', true, /extensions\/index.ts/);
|
const extensionsIndex = require.context('.', true, /extensions\/index.ts/);
|
||||||
const extensionsExports = extensionsIndex.keys().map((key) => {
|
const extensionsExports = extensionsIndex.keys().map((key) => {
|
||||||
@@ -137,7 +126,7 @@ export class GrafanaApp {
|
|||||||
initI18nPromise.then(({ language }) => updateConfig({ language }));
|
initI18nPromise.then(({ language }) => updateConfig({ language }));
|
||||||
|
|
||||||
setBackendSrv(backendSrv);
|
setBackendSrv(backendSrv);
|
||||||
initEchoSrv();
|
await initEchoSrv();
|
||||||
// This needs to be done after the `initEchoSrv` since it is being used under the hood.
|
// This needs to be done after the `initEchoSrv` since it is being used under the hood.
|
||||||
startMeasure('frontend_app_init');
|
startMeasure('frontend_app_init');
|
||||||
|
|
||||||
@@ -229,6 +218,7 @@ export class GrafanaApp {
|
|||||||
setPluginLinksHook(usePluginLinks);
|
setPluginLinksHook(usePluginLinks);
|
||||||
setPluginComponentHook(usePluginComponent);
|
setPluginComponentHook(usePluginComponent);
|
||||||
setPluginComponentsHook(usePluginComponents);
|
setPluginComponentsHook(usePluginComponents);
|
||||||
|
setPluginFunctionsHook(usePluginFunctions);
|
||||||
|
|
||||||
// initialize chrome service
|
// initialize chrome service
|
||||||
const queryParams = locationService.getSearchObject();
|
const queryParams = locationService.getSearchObject();
|
||||||
@@ -292,7 +282,7 @@ function initExtensions() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initEchoSrv() {
|
async function initEchoSrv() {
|
||||||
setEchoSrv(new Echo({ debug: process.env.NODE_ENV === 'development' }));
|
setEchoSrv(new Echo({ debug: process.env.NODE_ENV === 'development' }));
|
||||||
|
|
||||||
window.addEventListener('load', (e) => {
|
window.addEventListener('load', (e) => {
|
||||||
@@ -312,6 +302,7 @@ function initEchoSrv() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (contextSrv.user.orgRole !== '') {
|
if (contextSrv.user.orgRole !== '') {
|
||||||
|
const { PerformanceBackend } = await import('./core/services/echo/backends/PerformanceBackend');
|
||||||
registerEchoBackend(new PerformanceBackend({}));
|
registerEchoBackend(new PerformanceBackend({}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,6 +316,10 @@ function initEchoSrv() {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((url) => new RegExp(`${url}.*.`));
|
.map((url) => new RegExp(`${url}.*.`));
|
||||||
|
|
||||||
|
const { GrafanaJavascriptAgentBackend } = await import(
|
||||||
|
'./core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend'
|
||||||
|
);
|
||||||
|
|
||||||
registerEchoBackend(
|
registerEchoBackend(
|
||||||
new GrafanaJavascriptAgentBackend({
|
new GrafanaJavascriptAgentBackend({
|
||||||
...config.grafanaJavascriptAgent,
|
...config.grafanaJavascriptAgent,
|
||||||
@@ -343,6 +338,7 @@ function initEchoSrv() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.googleAnalyticsId) {
|
if (config.googleAnalyticsId) {
|
||||||
|
const { GAEchoBackend } = await import('./core/services/echo/backends/analytics/GABackend');
|
||||||
registerEchoBackend(
|
registerEchoBackend(
|
||||||
new GAEchoBackend({
|
new GAEchoBackend({
|
||||||
googleAnalyticsId: config.googleAnalyticsId,
|
googleAnalyticsId: config.googleAnalyticsId,
|
||||||
@@ -351,6 +347,7 @@ function initEchoSrv() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.googleAnalytics4Id) {
|
if (config.googleAnalytics4Id) {
|
||||||
|
const { GA4EchoBackend } = await import('./core/services/echo/backends/analytics/GA4Backend');
|
||||||
registerEchoBackend(
|
registerEchoBackend(
|
||||||
new GA4EchoBackend({
|
new GA4EchoBackend({
|
||||||
googleAnalyticsId: config.googleAnalytics4Id,
|
googleAnalyticsId: config.googleAnalytics4Id,
|
||||||
@@ -360,6 +357,7 @@ function initEchoSrv() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.rudderstackWriteKey && config.rudderstackDataPlaneUrl) {
|
if (config.rudderstackWriteKey && config.rudderstackDataPlaneUrl) {
|
||||||
|
const { RudderstackBackend } = await import('./core/services/echo/backends/analytics/RudderstackBackend');
|
||||||
registerEchoBackend(
|
registerEchoBackend(
|
||||||
new RudderstackBackend({
|
new RudderstackBackend({
|
||||||
writeKey: config.rudderstackWriteKey,
|
writeKey: config.rudderstackWriteKey,
|
||||||
@@ -374,6 +372,9 @@ function initEchoSrv() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.applicationInsightsConnectionString) {
|
if (config.applicationInsightsConnectionString) {
|
||||||
|
const { ApplicationInsightsBackend } = await import(
|
||||||
|
'./core/services/echo/backends/analytics/ApplicationInsightsBackend'
|
||||||
|
);
|
||||||
registerEchoBackend(
|
registerEchoBackend(
|
||||||
new ApplicationInsightsBackend({
|
new ApplicationInsightsBackend({
|
||||||
connectionString: config.applicationInsightsConnectionString,
|
connectionString: config.applicationInsightsConnectionString,
|
||||||
@@ -383,6 +384,7 @@ function initEchoSrv() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.analyticsConsoleReporting) {
|
if (config.analyticsConsoleReporting) {
|
||||||
|
const { BrowserConsoleBackend } = await import('./core/services/echo/backends/analytics/BrowseConsoleBackend');
|
||||||
registerEchoBackend(new BrowserConsoleBackend());
|
registerEchoBackend(new BrowserConsoleBackend());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,7 +394,7 @@ function initEchoSrv() {
|
|||||||
* like PerformanceMark or PerformancePaintTiming (e.g. created with performance.mark, or first-contentful-paint)
|
* like PerformanceMark or PerformancePaintTiming (e.g. created with performance.mark, or first-contentful-paint)
|
||||||
*/
|
*/
|
||||||
function reportMetricPerformanceMark(metricName: string, prefix = '', suffix = ''): void {
|
function reportMetricPerformanceMark(metricName: string, prefix = '', suffix = ''): void {
|
||||||
const metric = _.first(performance.getEntriesByName(metricName));
|
const metric = performance.getEntriesByName(metricName).at(0);
|
||||||
if (metric) {
|
if (metric) {
|
||||||
const metricName = metric.name.replace(/-/g, '_');
|
const metricName = metric.name.replace(/-/g, '_');
|
||||||
reportPerformance(`${prefix}${metricName}${suffix}`, Math.round(metric.startTime) / 1000);
|
reportPerformance(`${prefix}${metricName}${suffix}`, Math.round(metric.startTime) / 1000);
|
||||||
@@ -406,6 +408,7 @@ function handleRedirectTo(): void {
|
|||||||
if (queryParams.has('auth_token')) {
|
if (queryParams.has('auth_token')) {
|
||||||
// URL Login should not be redirected
|
// URL Login should not be redirected
|
||||||
window.sessionStorage.removeItem(RedirectToUrlKey);
|
window.sessionStorage.removeItem(RedirectToUrlKey);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryParams.has(redirectToParamKey) && window.location.pathname !== '/') {
|
if (queryParams.has(redirectToParamKey) && window.location.pathname !== '/') {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const getPluginsHandler = (pluginsArray: PluginMeta[] = plugins) => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
grafanaVersion: '',
|
grafanaVersion: '',
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ export function pluginMetaToPluginConfig(pluginMeta: PluginMeta): AppPluginConfi
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ describe('getRuleOrigin', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
grafanaVersion: '',
|
grafanaVersion: '',
|
||||||
|
|||||||
@@ -160,6 +160,51 @@ describe('RowRepeaterBehavior', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Given scene with variable with 15 values', () => {
|
||||||
|
let scene: DashboardScene, grid: SceneGridLayout;
|
||||||
|
let gridStateUpdates: unknown[];
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
({ scene, grid } = buildScene({ variableQueryTime: 0 }, [
|
||||||
|
{ label: 'A', value: 'A1' },
|
||||||
|
{ label: 'B', value: 'B1' },
|
||||||
|
{ label: 'C', value: 'C1' },
|
||||||
|
{ label: 'D', value: 'D1' },
|
||||||
|
{ label: 'E', value: 'E1' },
|
||||||
|
{ label: 'F', value: 'F1' },
|
||||||
|
{ label: 'G', value: 'G1' },
|
||||||
|
{ label: 'H', value: 'H1' },
|
||||||
|
{ label: 'I', value: 'I1' },
|
||||||
|
{ label: 'J', value: 'J1' },
|
||||||
|
{ label: 'K', value: 'K1' },
|
||||||
|
{ label: 'L', value: 'L1' },
|
||||||
|
{ label: 'M', value: 'M1' },
|
||||||
|
{ label: 'N', value: 'N1' },
|
||||||
|
{ label: 'O', value: 'O1' },
|
||||||
|
]));
|
||||||
|
|
||||||
|
gridStateUpdates = [];
|
||||||
|
grid.subscribeToState((state) => gridStateUpdates.push(state));
|
||||||
|
|
||||||
|
activateFullSceneTree(scene);
|
||||||
|
await new Promise((r) => setTimeout(r, 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should handle second repeat cycle and update remove old repeats', async () => {
|
||||||
|
// should have 15 repeated rows (and the panel above + the row at the bottom)
|
||||||
|
expect(grid.state.children.length).toBe(17);
|
||||||
|
|
||||||
|
// trigger another repeat cycle by changing the variable
|
||||||
|
const variable = scene.state.$variables!.state.variables[0] as TestVariable;
|
||||||
|
variable.changeValueTo(['B1', 'C1']);
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 1));
|
||||||
|
|
||||||
|
// should now only have 2 repeated rows (and the panel above + the row at the bottom)
|
||||||
|
expect(grid.state.children.length).toBe(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Given scene empty row', () => {
|
describe('Given scene empty row', () => {
|
||||||
let scene: DashboardScene;
|
let scene: DashboardScene;
|
||||||
let grid: SceneGridLayout;
|
let grid: SceneGridLayout;
|
||||||
|
|||||||
+45
@@ -104,6 +104,51 @@ describe('RowItemRepeaterBehavior', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Given scene with variable with 15 values', () => {
|
||||||
|
let scene: DashboardScene, layout: RowsLayoutManager;
|
||||||
|
let layoutStateUpdates: unknown[];
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
({ scene, layout } = buildScene({ variableQueryTime: 0 }, [
|
||||||
|
{ label: 'A', value: 'A1' },
|
||||||
|
{ label: 'B', value: 'B1' },
|
||||||
|
{ label: 'C', value: 'C1' },
|
||||||
|
{ label: 'D', value: 'D1' },
|
||||||
|
{ label: 'E', value: 'E1' },
|
||||||
|
{ label: 'F', value: 'F1' },
|
||||||
|
{ label: 'G', value: 'G1' },
|
||||||
|
{ label: 'H', value: 'H1' },
|
||||||
|
{ label: 'I', value: 'I1' },
|
||||||
|
{ label: 'J', value: 'J1' },
|
||||||
|
{ label: 'K', value: 'K1' },
|
||||||
|
{ label: 'L', value: 'L1' },
|
||||||
|
{ label: 'M', value: 'M1' },
|
||||||
|
{ label: 'N', value: 'N1' },
|
||||||
|
{ label: 'O', value: 'O1' },
|
||||||
|
]));
|
||||||
|
|
||||||
|
layoutStateUpdates = [];
|
||||||
|
layout.subscribeToState((state) => layoutStateUpdates.push(state));
|
||||||
|
|
||||||
|
activateFullSceneTree(scene);
|
||||||
|
await new Promise((r) => setTimeout(r, 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should handle second repeat cycle and update remove old repeats', async () => {
|
||||||
|
// should have 15 repeated rows (and the panel above)
|
||||||
|
expect(layout.state.rows.length).toBe(16);
|
||||||
|
|
||||||
|
// trigger another repeat cycle by changing the variable
|
||||||
|
const variable = scene.state.$variables!.state.variables[0] as TestVariable;
|
||||||
|
variable.changeValueTo(['B1', 'C1']);
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 1));
|
||||||
|
|
||||||
|
// should now only have 2 repeated rows (and the panel above)
|
||||||
|
expect(layout.state.rows.length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Given a scene with empty variable', () => {
|
describe('Given a scene with empty variable', () => {
|
||||||
it('Should preserve repeat row', async () => {
|
it('Should preserve repeat row', async () => {
|
||||||
const { scene, layout } = buildScene({ variableQueryTime: 0 }, []);
|
const { scene, layout } = buildScene({ variableQueryTime: 0 }, []);
|
||||||
|
|||||||
@@ -48,6 +48,20 @@ describe('clone', () => {
|
|||||||
expect(isClonedKey('tab-clone-1/row-clone-2/panel')).toBe(false);
|
expect(isClonedKey('tab-clone-1/row-clone-2/panel')).toBe(false);
|
||||||
expect(isClonedKey('row-clone-1/panel')).toBe(false);
|
expect(isClonedKey('row-clone-1/panel')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should properly handle indexes containing 0', () => {
|
||||||
|
expect(isClonedKey('tab-clone-0/row-clone-1/panel-clone-0')).toBe(false);
|
||||||
|
expect(isClonedKey('row-clone-0/panel-clone-0')).toBe(false);
|
||||||
|
expect(isClonedKey('panel-clone-0')).toBe(false);
|
||||||
|
|
||||||
|
expect(isClonedKey('tab-clone-0/row-clone-1/panel-clone-101')).toBe(true);
|
||||||
|
expect(isClonedKey('row-clone-0/panel-clone-101')).toBe(true);
|
||||||
|
expect(isClonedKey('panel-clone-1010')).toBe(true);
|
||||||
|
|
||||||
|
expect(isClonedKey('tab-clone-0/row-clone-1/panel-clone-10')).toBe(true);
|
||||||
|
expect(isClonedKey('row-clone-0/panel-clone-100')).toBe(true);
|
||||||
|
expect(isClonedKey('panel-clone-1000')).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isClonedKeyOf', () => {
|
describe('isClonedKeyOf', () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const CLONE_KEY = '-clone-';
|
const CLONE_KEY = '-clone-';
|
||||||
const CLONE_SEPARATOR = '/';
|
const CLONE_SEPARATOR = '/';
|
||||||
|
|
||||||
const CLONED_KEY_REGEX = new RegExp(`${CLONE_KEY}[1-9]+$`);
|
const CLONED_KEY_REGEX = new RegExp(`${CLONE_KEY}[1-9][0-9]*$`);
|
||||||
const ORIGINAL_REGEX = new RegExp(`${CLONE_KEY}\\d+$`);
|
const ORIGINAL_REGEX = new RegExp(`${CLONE_KEY}\\d+$`);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export async function getPluginDetails(id: string): Promise<CatalogPluginDetails
|
|||||||
iam: remote?.json?.iam,
|
iam: remote?.json?.iam,
|
||||||
lastCommitDate: remote?.lastCommitDate,
|
lastCommitDate: remote?.lastCommitDate,
|
||||||
changelog: remote?.changelog || localChangelog,
|
changelog: remote?.changelog || localChangelog,
|
||||||
|
licenseUrl: remote?.licenseUrl,
|
||||||
|
documentationUrl: remote?.documentationUrl,
|
||||||
signatureType: local?.signatureType || (remote?.signatureType !== '' ? remote?.signatureType : undefined),
|
signatureType: local?.signatureType || (remote?.signatureType !== '' ? remote?.signatureType : undefined),
|
||||||
signature: local?.signature,
|
signature: local?.signature,
|
||||||
};
|
};
|
||||||
@@ -94,6 +96,7 @@ async function getPluginVersions(id: string, isPublished: boolean): Promise<Vers
|
|||||||
return (versions.items || []).map((v) => ({
|
return (versions.items || []).map((v) => ({
|
||||||
version: v.version,
|
version: v.version,
|
||||||
createdAt: v.createdAt,
|
createdAt: v.createdAt,
|
||||||
|
updatedAt: v.updatedAt,
|
||||||
isCompatible: v.isCompatible,
|
isCompatible: v.isCompatible,
|
||||||
grafanaDependency: v.grafanaDependency,
|
grafanaDependency: v.grafanaDependency,
|
||||||
angularDetected: v.angularDetected,
|
angularDetected: v.angularDetected,
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ const plugin: CatalogPlugin = {
|
|||||||
],
|
],
|
||||||
grafanaDependency: '>=9.0.0',
|
grafanaDependency: '>=9.0.0',
|
||||||
statusContext: 'stable',
|
statusContext: 'stable',
|
||||||
|
changelog: 'Test changelog',
|
||||||
},
|
},
|
||||||
angularDetected: false,
|
angularDetected: false,
|
||||||
isFullyInstalled: true,
|
isFullyInstalled: true,
|
||||||
@@ -154,4 +155,28 @@ describe('PluginDetailsPage', () => {
|
|||||||
render(<PluginDetailsPage pluginId={plugin.id} />);
|
render(<PluginDetailsPage pluginId={plugin.id} />);
|
||||||
expect(screen.getByRole('tab', { name: 'Data source connections' })).toBeVisible();
|
expect(screen.getByRole('tab', { name: 'Data source connections' })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not show version and changelog tabs when plugin is core', () => {
|
||||||
|
mockUseGetSingle.mockReturnValue({ ...plugin, isCore: true });
|
||||||
|
render(<PluginDetailsPage pluginId={plugin.id} />);
|
||||||
|
expect(screen.queryByRole('tab', { name: 'Version history' })).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('tab', { name: 'Changelog' })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show last version in plugin details panel when plugin is core', () => {
|
||||||
|
config.featureToggles.pluginsDetailsRightPanel = true;
|
||||||
|
window.matchMedia = jest.fn().mockImplementation((query) => ({
|
||||||
|
matches: query !== '(max-width: 600px)',
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addEventListener: jest.fn(),
|
||||||
|
removeEventListener: jest.fn(),
|
||||||
|
dispatchEvent: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
mockUseGetSingle.mockReturnValue({ ...plugin, isCore: true, latestVersion: '1.2.0' });
|
||||||
|
|
||||||
|
render(<PluginDetailsPage pluginId={plugin.id} />);
|
||||||
|
expect(screen.queryByText('Latest Version:')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ describe('PluginDetailsPanel', () => {
|
|||||||
it('should render report abuse section for non-core plugins', () => {
|
it('should render report abuse section for non-core plugins', () => {
|
||||||
render(<PluginDetailsPanel plugin={mockPlugin} pluginExtentionsInfo={mockInfo} />);
|
render(<PluginDetailsPanel plugin={mockPlugin} pluginExtentionsInfo={mockInfo} />);
|
||||||
expect(screen.getByText('Report a concern')).toBeInTheDocument();
|
expect(screen.getByText('Report a concern')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Contact Grafana Labs')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render report abuse section for core plugins', () => {
|
it('should not render report abuse section for core plugins', () => {
|
||||||
@@ -117,6 +116,6 @@ describe('PluginDetailsPanel', () => {
|
|||||||
it('should respect custom width prop', () => {
|
it('should respect custom width prop', () => {
|
||||||
render(<PluginDetailsPanel plugin={mockPlugin} pluginExtentionsInfo={mockInfo} width="300px" />);
|
render(<PluginDetailsPanel plugin={mockPlugin} pluginExtentionsInfo={mockInfo} width="300px" />);
|
||||||
const panel = screen.getByTestId('plugin-details-panel');
|
const panel = screen.getByTestId('plugin-details-panel');
|
||||||
expect(panel).toHaveStyle({ maxWidth: '300px' });
|
expect(panel).toHaveStyle({ width: '300px' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,22 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
import { PageInfoItem } from '@grafana/runtime/src/components/PluginPage';
|
import { PageInfoItem } from '@grafana/runtime/src/components/PluginPage';
|
||||||
import { Stack, Text, LinkButton, Box, TextLink, useStyles2 } from '@grafana/ui';
|
import {
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
LinkButton,
|
||||||
|
Box,
|
||||||
|
TextLink,
|
||||||
|
CollapsableSection,
|
||||||
|
Tooltip,
|
||||||
|
Icon,
|
||||||
|
Modal,
|
||||||
|
Button,
|
||||||
|
useStyles2,
|
||||||
|
} from '@grafana/ui';
|
||||||
import { Trans } from 'app/core/internationalization';
|
import { Trans } from 'app/core/internationalization';
|
||||||
import { formatDate } from 'app/core/internationalization/dates';
|
import { formatDate } from 'app/core/internationalization/dates';
|
||||||
|
|
||||||
@@ -16,73 +30,203 @@ type Props = {
|
|||||||
|
|
||||||
export function PluginDetailsPanel(props: Props): React.ReactElement | null {
|
export function PluginDetailsPanel(props: Props): React.ReactElement | null {
|
||||||
const { pluginExtentionsInfo, plugin, width = '250px' } = props;
|
const { pluginExtentionsInfo, plugin, width = '250px' } = props;
|
||||||
|
const [reportAbuseModalOpen, setReportAbuseModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const normalizeURL = (url: string | undefined) => url?.replace(/\/$/, '');
|
||||||
|
|
||||||
|
const customLinks = plugin.details?.links?.filter((link) => {
|
||||||
|
const customLinksFiltered = ![plugin.url, plugin.details?.licenseUrl, plugin.details?.documentationUrl]
|
||||||
|
.map(normalizeURL)
|
||||||
|
.includes(normalizeURL(link.url));
|
||||||
|
return customLinksFiltered;
|
||||||
|
});
|
||||||
|
const shouldRenderLinks = plugin.url || plugin.details?.licenseUrl || plugin.details?.documentationUrl;
|
||||||
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
return (
|
const onClickReportConcern = (pluginId: string) => {
|
||||||
<Stack direction="column" gap={3} shrink={0} grow={0} maxWidth={width} data-testid="plugin-details-panel">
|
setReportAbuseModalOpen(true);
|
||||||
<Box padding={2} borderColor="medium" borderStyle="solid">
|
reportInteraction('plugin_detail_report_concern', {
|
||||||
<Stack direction="column" gap={2}>
|
plugin_id: pluginId,
|
||||||
{pluginExtentionsInfo.map((infoItem, index) => {
|
});
|
||||||
return (
|
};
|
||||||
<Stack key={index} wrap direction="column" gap={0.5}>
|
|
||||||
<Text color="secondary">{infoItem.label + ':'}</Text>
|
|
||||||
<div className={styles.pluginVersionDetails}>{infoItem.value}</div>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{plugin.updatedAt && (
|
|
||||||
<Stack direction="column" gap={0.5}>
|
|
||||||
<Text color="secondary">
|
|
||||||
<Trans i18nKey="plugins.details.labels.updatedAt">Last updated:</Trans>
|
|
||||||
</Text>{' '}
|
|
||||||
<Text>{formatDate(new Date(plugin.updatedAt), { day: 'numeric', month: 'short', year: 'numeric' })}</Text>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
{plugin?.details?.lastCommitDate && (
|
|
||||||
<Stack direction="column" gap={0.5}>
|
|
||||||
<Text color="secondary">
|
|
||||||
<Trans i18nKey="plugins.details.labels.lastCommitDate">Last commit date:</Trans>
|
|
||||||
</Text>{' '}
|
|
||||||
<Text>
|
|
||||||
{formatDate(new Date(plugin.details.lastCommitDate), {
|
|
||||||
day: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{plugin?.details?.links && plugin.details?.links?.length > 0 && (
|
return (
|
||||||
|
<>
|
||||||
|
<Stack direction="column" gap={3} shrink={0} grow={0} width={width} data-testid="plugin-details-panel">
|
||||||
<Box padding={2} borderColor="medium" borderStyle="solid">
|
<Box padding={2} borderColor="medium" borderStyle="solid">
|
||||||
<Stack direction="column" gap={2}>
|
<Stack direction="column" gap={2}>
|
||||||
<Text color="secondary">
|
{pluginExtentionsInfo.map((infoItem, index) => {
|
||||||
<Trans i18nKey="plugins.details.labels.links">Links </Trans>
|
return (
|
||||||
</Text>
|
<Stack key={index} wrap direction="column" gap={0.5}>
|
||||||
{plugin.details.links.map((link, index) => (
|
<Text color="secondary">{infoItem.label + ':'}</Text>
|
||||||
<TextLink key={index} href={link.url} external>
|
<div className={styles.pluginVersionDetails}>{infoItem.value}</div>
|
||||||
{link.name}
|
</Stack>
|
||||||
</TextLink>
|
);
|
||||||
))}
|
})}
|
||||||
|
{plugin.updatedAt && (
|
||||||
|
<Stack direction="column" gap={0.5}>
|
||||||
|
<Text color="secondary">
|
||||||
|
<Trans i18nKey="plugins.details.labels.updatedAt">Last updated:</Trans>
|
||||||
|
</Text>{' '}
|
||||||
|
<Text>
|
||||||
|
{formatDate(new Date(plugin.updatedAt), { day: 'numeric', month: 'short', year: 'numeric' })}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
{plugin?.details?.lastCommitDate && (
|
||||||
|
<Stack direction="column" gap={0.5}>
|
||||||
|
<Text color="secondary">
|
||||||
|
<Trans i18nKey="plugins.details.labels.lastCommitDate">Last commit date:</Trans>
|
||||||
|
</Text>{' '}
|
||||||
|
<Text>
|
||||||
|
{formatDate(new Date(plugin.details.lastCommitDate), {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
{shouldRenderLinks && (
|
||||||
|
<>
|
||||||
{!plugin?.isCore && (
|
<Box padding={2} borderColor="medium" borderStyle="solid">
|
||||||
<Box padding={2} borderColor="medium" borderStyle="solid">
|
<Stack direction="column" gap={2}>
|
||||||
<Stack direction="column">
|
{plugin.url && (
|
||||||
<Text color="secondary">
|
<LinkButton href={plugin.url} variant="secondary" fill="solid" icon="code-branch" target="_blank">
|
||||||
<Trans i18nKey="plugins.details.labels.reportAbuse">Report a concern </Trans>
|
<Trans i18nKey="plugins.details.labels.repository">Repository</Trans>
|
||||||
|
</LinkButton>
|
||||||
|
)}
|
||||||
|
{plugin.raiseAnIssueUrl && (
|
||||||
|
<LinkButton href={plugin.raiseAnIssueUrl} variant="secondary" fill="solid" icon="bug" target="_blank">
|
||||||
|
<Trans i18nKey="plugins.details.labels.raiseAnIssue">Raise an issue</Trans>
|
||||||
|
</LinkButton>
|
||||||
|
)}
|
||||||
|
{plugin.details?.licenseUrl && (
|
||||||
|
<LinkButton
|
||||||
|
href={plugin.details?.licenseUrl}
|
||||||
|
variant="secondary"
|
||||||
|
fill="solid"
|
||||||
|
icon={'document-info'}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Trans i18nKey="plugins.details.labels.license">License</Trans>
|
||||||
|
</LinkButton>
|
||||||
|
)}
|
||||||
|
{plugin.details?.documentationUrl && (
|
||||||
|
<LinkButton
|
||||||
|
href={plugin.details?.documentationUrl}
|
||||||
|
variant="secondary"
|
||||||
|
fill="solid"
|
||||||
|
icon={'list-ui-alt'}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Trans i18nKey="plugins.details.labels.documentation">Documentation</Trans>
|
||||||
|
</LinkButton>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{customLinks && customLinks?.length > 0 && (
|
||||||
|
<Box padding={2} borderColor="medium" borderStyle="solid">
|
||||||
|
<CollapsableSection
|
||||||
|
isOpen={true}
|
||||||
|
label={
|
||||||
|
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||||
|
<Text color="secondary" variant="body">
|
||||||
|
<Trans i18nKey="plugins.details.labels.customLinks">Custom links </Trans>
|
||||||
|
</Text>
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<Trans i18nKey="plugins.details.labels.customLinksTooltip">
|
||||||
|
These links are provided by the plugin developer to offer additional, developer-specific
|
||||||
|
resources and information
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
placement="right-end"
|
||||||
|
>
|
||||||
|
<Icon name="info-circle" size="xs" />
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack direction="column" gap={2}>
|
||||||
|
{customLinks.map((link, index) => (
|
||||||
|
<TextLink key={index} href={link.url} external>
|
||||||
|
{link.name}
|
||||||
|
</TextLink>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</CollapsableSection>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{!plugin?.isCore && (
|
||||||
|
<Box padding={2} borderColor="medium" borderStyle="solid">
|
||||||
|
<CollapsableSection
|
||||||
|
headerDataTestId="reportConcern"
|
||||||
|
isOpen={false}
|
||||||
|
label={
|
||||||
|
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||||
|
<Text color="secondary" variant="body">
|
||||||
|
<Trans i18nKey="plugins.details.labels.reportAbuse">Report a concern </Trans>
|
||||||
|
</Text>
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<Trans i18nKey="plugins.details.labels.reportAbuseTooltip">
|
||||||
|
Report issues related to malicious or harmful plugins directly to Grafana Labs.
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
placement="right-end"
|
||||||
|
>
|
||||||
|
<Icon name="info-circle" size="xs" />
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack direction="column">
|
||||||
|
<Button variant="secondary" fill="solid" icon="bell" onClick={() => onClickReportConcern(plugin.id)}>
|
||||||
|
<Trans i18nKey="plugins.details.labels.contactGrafanaLabs">Contact Grafana Labs</Trans>
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</CollapsableSection>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
{reportAbuseModalOpen && (
|
||||||
|
<Modal
|
||||||
|
title={<Trans i18nKey="plugins.details.modal.title">Report a plugin concern</Trans>}
|
||||||
|
isOpen
|
||||||
|
onDismiss={() => setReportAbuseModalOpen(false)}
|
||||||
|
>
|
||||||
|
<Stack direction="column" gap={2}>
|
||||||
|
<Text>
|
||||||
|
<Trans i18nKey="plugins.details.modal.description">
|
||||||
|
This feature is for reporting malicious or harmful behaviour within plugins. For plugin concerns, email
|
||||||
|
us at:{' '}
|
||||||
|
</Trans>
|
||||||
|
<TextLink href="mailto:integrations+report-plugin@grafana.com">integrations@grafana.com</TextLink>
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
<Trans i18nKey="plugins.details.modal.node">
|
||||||
|
Note: For general plugin issues like bugs or feature requests, please contact the plugin author using
|
||||||
|
the provided links.{' '}
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<LinkButton href="mailto:integrations@grafana.com" variant="secondary" fill="solid">
|
|
||||||
<Trans i18nKey="plugins.details.labels.contactGrafanaLabs">Contact Grafana Labs</Trans>
|
|
||||||
</LinkButton>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
<Modal.ButtonRow>
|
||||||
|
<Button variant="secondary" fill="outline" onClick={() => setReportAbuseModalOpen(false)}>
|
||||||
|
<Trans i18nKey="plugins.details.modal.cancel">Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
<Button icon="copy" onClick={() => navigator.clipboard.writeText('integrations@grafana.com')}>
|
||||||
|
<Trans i18nKey="plugins.details.modal.copyEmail">Copy email address</Trans>
|
||||||
|
</Button>
|
||||||
|
</Modal.ButtonRow>
|
||||||
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export const VersionList = ({ pluginId, versions = [], installedVersion, disable
|
|||||||
|
|
||||||
{/* Last updated */}
|
{/* Last updated */}
|
||||||
<td className={isInstalledVersion ? styles.currentVersion : ''}>
|
<td className={isInstalledVersion ? styles.currentVersion : ''}>
|
||||||
{dateTimeFormatTimeAgo(version.createdAt)}
|
{dateTimeFormatTimeAgo(version.updatedAt || version.createdAt)}
|
||||||
</td>
|
</td>
|
||||||
{/* Dependency */}
|
{/* Dependency */}
|
||||||
<td className={isInstalledVersion ? styles.currentVersion : ''}>{version.grafanaDependency || 'N/A'}</td>
|
<td className={isInstalledVersion ? styles.currentVersion : ''}>{version.grafanaDependency || 'N/A'}</td>
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ describe('Plugins/Helpers', () => {
|
|||||||
updatedAt: '2021-05-18T14:53:01.000Z',
|
updatedAt: '2021-05-18T14:53:01.000Z',
|
||||||
isFullyInstalled: false,
|
isFullyInstalled: false,
|
||||||
angularDetected: false,
|
angularDetected: false,
|
||||||
|
url: 'https://github.com/alexanderzobnin/grafana-zabbix',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -354,6 +355,7 @@ describe('Plugins/Helpers', () => {
|
|||||||
installedVersion: '4.2.2',
|
installedVersion: '4.2.2',
|
||||||
isFullyInstalled: true,
|
isFullyInstalled: true,
|
||||||
angularDetected: false,
|
angularDetected: false,
|
||||||
|
url: 'https://github.com/alexanderzobnin/grafana-zabbix',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C
|
|||||||
signatureType,
|
signatureType,
|
||||||
versionSignatureType,
|
versionSignatureType,
|
||||||
versionSignedByOrgName,
|
versionSignedByOrgName,
|
||||||
|
url,
|
||||||
|
raiseAnIssueUrl,
|
||||||
} = plugin;
|
} = plugin;
|
||||||
|
|
||||||
const isDisabled = !!error || isDisabledSecretsPlugin(typeCode);
|
const isDisabled = !!error || isDisabledSecretsPlugin(typeCode);
|
||||||
@@ -158,6 +160,8 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C
|
|||||||
angularDetected,
|
angularDetected,
|
||||||
isFullyInstalled: isDisabled,
|
isFullyInstalled: isDisabled,
|
||||||
latestVersion: plugin.version,
|
latestVersion: plugin.version,
|
||||||
|
url,
|
||||||
|
raiseAnIssueUrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +178,7 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat
|
|||||||
hasUpdate,
|
hasUpdate,
|
||||||
accessControl,
|
accessControl,
|
||||||
angularDetected,
|
angularDetected,
|
||||||
|
raiseAnIssueUrl,
|
||||||
} = plugin;
|
} = plugin;
|
||||||
|
|
||||||
const isDisabled = !!error || isDisabledSecretsPlugin(type);
|
const isDisabled = !!error || isDisabledSecretsPlugin(type);
|
||||||
@@ -208,6 +213,7 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat
|
|||||||
isFullyInstalled: true,
|
isFullyInstalled: true,
|
||||||
iam: plugin.iam,
|
iam: plugin.iam,
|
||||||
latestVersion: plugin.latestVersion,
|
latestVersion: plugin.latestVersion,
|
||||||
|
raiseAnIssueUrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +277,8 @@ export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, e
|
|||||||
isFullyInstalled: Boolean(local) || isDisabled,
|
isFullyInstalled: Boolean(local) || isDisabled,
|
||||||
iam: local?.iam,
|
iam: local?.iam,
|
||||||
latestVersion: local?.latestVersion || remote?.version || '',
|
latestVersion: local?.latestVersion || remote?.version || '',
|
||||||
|
url: remote?.url || '',
|
||||||
|
raiseAnIssueUrl: remote?.raiseAnIssueUrl || local?.raiseAnIssueUrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ export const usePluginDetailsTabs = (
|
|||||||
const navModelChildren = useMemo(() => {
|
const navModelChildren = useMemo(() => {
|
||||||
const canConfigurePlugins = plugin && contextSrv.hasPermissionInMetadata(AccessControlAction.PluginsWrite, plugin);
|
const canConfigurePlugins = plugin && contextSrv.hasPermissionInMetadata(AccessControlAction.PluginsWrite, plugin);
|
||||||
const navModelChildren: NavModelItem[] = [];
|
const navModelChildren: NavModelItem[] = [];
|
||||||
if (isPublished) {
|
// currently the versions available of core plugins are not consistent
|
||||||
|
if (isPublished && !plugin?.isCore) {
|
||||||
navModelChildren.push({
|
navModelChildren.push({
|
||||||
text: PluginTabLabels.VERSIONS,
|
text: PluginTabLabels.VERSIONS,
|
||||||
id: PluginTabIds.VERSIONS,
|
id: PluginTabIds.VERSIONS,
|
||||||
@@ -51,7 +52,8 @@ export const usePluginDetailsTabs = (
|
|||||||
active: PluginTabIds.VERSIONS === currentPageId,
|
active: PluginTabIds.VERSIONS === currentPageId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (isPublished && plugin?.details?.changelog) {
|
// currently there is not changelog available for core plugins
|
||||||
|
if (isPublished && plugin?.details?.changelog && !plugin.isCore) {
|
||||||
navModelChildren.push({
|
navModelChildren.push({
|
||||||
text: PluginTabLabels.CHANGELOG,
|
text: PluginTabLabels.CHANGELOG,
|
||||||
id: PluginTabIds.CHANGELOG,
|
id: PluginTabIds.CHANGELOG,
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ export const usePluginInfo = (plugin?: CatalogPlugin): PageInfoItem[] => {
|
|||||||
latestVersionValue = latestVersion;
|
latestVersionValue = latestVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
addInfo('latestVersion', latestVersionValue);
|
// latest versions of core plugins are not consistent
|
||||||
|
if (!plugin.isCore) {
|
||||||
|
addInfo('latestVersion', latestVersionValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Boolean(plugin.orgName)) {
|
if (Boolean(plugin.orgName)) {
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ export interface CatalogPlugin extends WithAccessControlMetadata {
|
|||||||
isUpdatingFromInstance?: boolean;
|
isUpdatingFromInstance?: boolean;
|
||||||
iam?: IdentityAccessManagement;
|
iam?: IdentityAccessManagement;
|
||||||
isProvisioned?: boolean;
|
isProvisioned?: boolean;
|
||||||
|
url?: string;
|
||||||
|
raiseAnIssueUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CatalogPluginDetails {
|
export interface CatalogPluginDetails {
|
||||||
@@ -79,6 +81,8 @@ export interface CatalogPluginDetails {
|
|||||||
iam?: IdentityAccessManagement;
|
iam?: IdentityAccessManagement;
|
||||||
changelog?: string;
|
changelog?: string;
|
||||||
lastCommitDate?: string;
|
lastCommitDate?: string;
|
||||||
|
licenseUrl?: string;
|
||||||
|
documentationUrl?: string;
|
||||||
signatureType?: PluginSignatureType;
|
signatureType?: PluginSignatureType;
|
||||||
signature?: PluginSignatureStatus;
|
signature?: PluginSignatureStatus;
|
||||||
}
|
}
|
||||||
@@ -143,6 +147,9 @@ export type RemotePlugin = {
|
|||||||
versionStatus: string;
|
versionStatus: string;
|
||||||
angularDetected?: boolean;
|
angularDetected?: boolean;
|
||||||
lastCommitDate?: string;
|
lastCommitDate?: string;
|
||||||
|
licenseUrl?: string;
|
||||||
|
documentationUrl?: string;
|
||||||
|
raiseAnIssueUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The available status codes on GCOM are available here:
|
// The available status codes on GCOM are available here:
|
||||||
@@ -190,6 +197,7 @@ export type LocalPlugin = WithAccessControlMetadata & {
|
|||||||
dependencies: PluginDependencies;
|
dependencies: PluginDependencies;
|
||||||
angularDetected: boolean;
|
angularDetected: boolean;
|
||||||
iam?: IdentityAccessManagement;
|
iam?: IdentityAccessManagement;
|
||||||
|
raiseAnIssueUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IdentityAccessManagement {
|
interface IdentityAccessManagement {
|
||||||
@@ -216,6 +224,7 @@ export interface Build {
|
|||||||
export interface Version {
|
export interface Version {
|
||||||
version: string;
|
version: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
updatedAt?: string;
|
||||||
isCompatible: boolean;
|
isCompatible: boolean;
|
||||||
grafanaDependency: string | null;
|
grafanaDependency: string | null;
|
||||||
angularDetected?: boolean;
|
angularDetected?: boolean;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Echo } from 'app/core/services/echo/Echo';
|
|||||||
|
|
||||||
import { ExtensionRegistriesProvider } from '../extensions/ExtensionRegistriesContext';
|
import { ExtensionRegistriesProvider } from '../extensions/ExtensionRegistriesContext';
|
||||||
import { AddedComponentsRegistry } from '../extensions/registry/AddedComponentsRegistry';
|
import { AddedComponentsRegistry } from '../extensions/registry/AddedComponentsRegistry';
|
||||||
|
import { AddedFunctionsRegistry } from '../extensions/registry/AddedFunctionsRegistry';
|
||||||
import { AddedLinksRegistry } from '../extensions/registry/AddedLinksRegistry';
|
import { AddedLinksRegistry } from '../extensions/registry/AddedLinksRegistry';
|
||||||
import { ExposedComponentsRegistry } from '../extensions/registry/ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from '../extensions/registry/ExposedComponentsRegistry';
|
||||||
import { getPluginSettings } from '../pluginSettings';
|
import { getPluginSettings } from '../pluginSettings';
|
||||||
@@ -93,6 +94,7 @@ function renderUnderRouter(page = '') {
|
|||||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||||
addedLinksRegistry: new AddedLinksRegistry(),
|
addedLinksRegistry: new AddedLinksRegistry(),
|
||||||
|
addedFunctionsRegistry: new AddedFunctionsRegistry(),
|
||||||
};
|
};
|
||||||
const pagePath = page ? `/${page}` : '';
|
const pagePath = page ? `/${page}` : '';
|
||||||
const route = {
|
const route = {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
useAddedLinksRegistry,
|
useAddedLinksRegistry,
|
||||||
useAddedComponentsRegistry,
|
useAddedComponentsRegistry,
|
||||||
useExposedComponentsRegistry,
|
useExposedComponentsRegistry,
|
||||||
|
useAddedFunctionsRegistry,
|
||||||
} from '../extensions/ExtensionRegistriesContext';
|
} from '../extensions/ExtensionRegistriesContext';
|
||||||
import { getPluginSettings } from '../pluginSettings';
|
import { getPluginSettings } from '../pluginSettings';
|
||||||
import { importAppPlugin } from '../plugin_loader';
|
import { importAppPlugin } from '../plugin_loader';
|
||||||
@@ -60,6 +61,7 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) {
|
|||||||
const addedLinksRegistry = useAddedLinksRegistry();
|
const addedLinksRegistry = useAddedLinksRegistry();
|
||||||
const addedComponentsRegistry = useAddedComponentsRegistry();
|
const addedComponentsRegistry = useAddedComponentsRegistry();
|
||||||
const exposedComponentsRegistry = useExposedComponentsRegistry();
|
const exposedComponentsRegistry = useExposedComponentsRegistry();
|
||||||
|
const addedFunctionsRegistry = useAddedFunctionsRegistry();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [state, dispatch] = useReducer(stateSlice.reducer, initialState);
|
const [state, dispatch] = useReducer(stateSlice.reducer, initialState);
|
||||||
const currentUrl = config.appSubUrl + location.pathname + location.search;
|
const currentUrl = config.appSubUrl + location.pathname + location.search;
|
||||||
@@ -104,6 +106,7 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) {
|
|||||||
addedLinksRegistry: addedLinksRegistry.readOnly(),
|
addedLinksRegistry: addedLinksRegistry.readOnly(),
|
||||||
addedComponentsRegistry: addedComponentsRegistry.readOnly(),
|
addedComponentsRegistry: addedComponentsRegistry.readOnly(),
|
||||||
exposedComponentsRegistry: exposedComponentsRegistry.readOnly(),
|
exposedComponentsRegistry: exposedComponentsRegistry.readOnly(),
|
||||||
|
addedFunctionsRegistry: addedFunctionsRegistry.readOnly(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<plugin.root
|
<plugin.root
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { PropsWithChildren, createContext, useContext } from 'react';
|
import { PropsWithChildren, createContext, useContext } from 'react';
|
||||||
|
|
||||||
import { AddedComponentsRegistry } from 'app/features/plugins/extensions/registry/AddedComponentsRegistry';
|
import { AddedComponentsRegistry } from 'app/features/plugins/extensions/registry/AddedComponentsRegistry';
|
||||||
|
import { AddedFunctionsRegistry } from 'app/features/plugins/extensions/registry/AddedFunctionsRegistry';
|
||||||
import { AddedLinksRegistry } from 'app/features/plugins/extensions/registry/AddedLinksRegistry';
|
import { AddedLinksRegistry } from 'app/features/plugins/extensions/registry/AddedLinksRegistry';
|
||||||
import { ExposedComponentsRegistry } from 'app/features/plugins/extensions/registry/ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from 'app/features/plugins/extensions/registry/ExposedComponentsRegistry';
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ export interface ExtensionRegistriesContextType {
|
|||||||
// Using a different context for each registry to avoid unnecessary re-renders
|
// Using a different context for each registry to avoid unnecessary re-renders
|
||||||
export const AddedLinksRegistryContext = createContext<AddedLinksRegistry | undefined>(undefined);
|
export const AddedLinksRegistryContext = createContext<AddedLinksRegistry | undefined>(undefined);
|
||||||
export const AddedComponentsRegistryContext = createContext<AddedComponentsRegistry | undefined>(undefined);
|
export const AddedComponentsRegistryContext = createContext<AddedComponentsRegistry | undefined>(undefined);
|
||||||
|
export const AddedFunctionsRegistryContext = createContext<AddedFunctionsRegistry | undefined>(undefined);
|
||||||
export const ExposedComponentsRegistryContext = createContext<ExposedComponentsRegistry | undefined>(undefined);
|
export const ExposedComponentsRegistryContext = createContext<ExposedComponentsRegistry | undefined>(undefined);
|
||||||
|
|
||||||
export function useAddedLinksRegistry(): AddedLinksRegistry {
|
export function useAddedLinksRegistry(): AddedLinksRegistry {
|
||||||
@@ -31,6 +33,14 @@ export function useAddedComponentsRegistry(): AddedComponentsRegistry {
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useAddedFunctionsRegistry(): AddedFunctionsRegistry {
|
||||||
|
const context = useContext(AddedFunctionsRegistryContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('No `AddedFunctionsRegistry` found.');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
export function useExposedComponentsRegistry(): ExposedComponentsRegistry {
|
export function useExposedComponentsRegistry(): ExposedComponentsRegistry {
|
||||||
const context = useContext(ExposedComponentsRegistryContext);
|
const context = useContext(ExposedComponentsRegistryContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
@@ -46,9 +56,11 @@ export const ExtensionRegistriesProvider = ({
|
|||||||
return (
|
return (
|
||||||
<AddedLinksRegistryContext.Provider value={registries.addedLinksRegistry}>
|
<AddedLinksRegistryContext.Provider value={registries.addedLinksRegistry}>
|
||||||
<AddedComponentsRegistryContext.Provider value={registries.addedComponentsRegistry}>
|
<AddedComponentsRegistryContext.Provider value={registries.addedComponentsRegistry}>
|
||||||
<ExposedComponentsRegistryContext.Provider value={registries.exposedComponentsRegistry}>
|
<AddedFunctionsRegistryContext.Provider value={registries.addedFunctionsRegistry}>
|
||||||
{children}
|
<ExposedComponentsRegistryContext.Provider value={registries.exposedComponentsRegistry}>
|
||||||
</ExposedComponentsRegistryContext.Provider>
|
{children}
|
||||||
|
</ExposedComponentsRegistryContext.Provider>
|
||||||
|
</AddedFunctionsRegistryContext.Provider>
|
||||||
</AddedComponentsRegistryContext.Provider>
|
</AddedComponentsRegistryContext.Provider>
|
||||||
</AddedLinksRegistryContext.Provider>
|
</AddedLinksRegistryContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export const TITLE_MISSING = 'Title is missing.';
|
|||||||
|
|
||||||
export const DESCRIPTION_MISSING = 'Description is missing.';
|
export const DESCRIPTION_MISSING = 'Description is missing.';
|
||||||
|
|
||||||
|
export const INVALID_EXTENSION_FUNCTION = 'The "fn" argument is invalid, it should be a function.';
|
||||||
|
|
||||||
export const INVALID_CONFIGURE_FUNCTION = 'The "configure" function is invalid. It should be a function.';
|
export const INVALID_CONFIGURE_FUNCTION = 'The "configure" function is invalid. It should be a function.';
|
||||||
|
|
||||||
export const INVALID_PATH_OR_ON_CLICK = 'Either "path" or "onClick" is required.';
|
export const INVALID_PATH_OR_ON_CLICK = 'Either "path" or "onClick" is required.';
|
||||||
@@ -33,6 +35,9 @@ export const TITLE_NOT_MATCHING_META_INFO = 'The "title" doesn\'t match the titl
|
|||||||
export const ADDED_LINK_META_INFO_MISSING =
|
export const ADDED_LINK_META_INFO_MISSING =
|
||||||
'The extension was not recorded in the plugin.json. Added link extensions must be listed in the section "extensions.addedLinks[]". Currently, this is only required in development but will be enforced also in production builds in the future.';
|
'The extension was not recorded in the plugin.json. Added link extensions must be listed in the section "extensions.addedLinks[]". Currently, this is only required in development but will be enforced also in production builds in the future.';
|
||||||
|
|
||||||
|
export const ADDED_FUNCTION_META_INFO_MISSING =
|
||||||
|
'The extension was not recorded in the plugin.json. Added function extensions must be listed in the section "extensions.addedFunction[]". Currently, this is only required in development but will be enforced also in production builds in the future.';
|
||||||
|
|
||||||
export const DESCRIPTION_NOT_MATCHING_META_INFO =
|
export const DESCRIPTION_NOT_MATCHING_META_INFO =
|
||||||
'The "description" doesn\'t match the description recorded in plugin.json.';
|
'The "description" doesn\'t match the description recorded in plugin.json.';
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type PluginExtension,
|
|
||||||
PluginExtensionTypes,
|
PluginExtensionTypes,
|
||||||
|
type PluginExtension,
|
||||||
type PluginExtensionLink,
|
type PluginExtensionLink,
|
||||||
type PluginExtensionComponent,
|
type PluginExtensionComponent,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ describe('AddedComponentsRegistry', () => {
|
|||||||
extensions: {
|
extensions: {
|
||||||
addedLinks: [],
|
addedLinks: [],
|
||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,677 @@
|
|||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
import { PluginLoadingStrategy } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { log } from '../logs/log';
|
||||||
|
import { resetLogMock } from '../logs/testUtils';
|
||||||
|
import { isGrafanaDevMode } from '../utils';
|
||||||
|
|
||||||
|
import { AddedFunctionsRegistry } from './AddedFunctionsRegistry';
|
||||||
|
import { MSG_CANNOT_REGISTER_READ_ONLY } from './Registry';
|
||||||
|
|
||||||
|
jest.mock('../utils', () => ({
|
||||||
|
...jest.requireActual('../utils'),
|
||||||
|
|
||||||
|
// Manually set the dev mode to false
|
||||||
|
// (to make sure that by default we are testing a production scneario)
|
||||||
|
isGrafanaDevMode: jest.fn().mockReturnValue(false),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../logs/log', () => {
|
||||||
|
const { createLogMock } = jest.requireActual('../logs/testUtils');
|
||||||
|
const original = jest.requireActual('../logs/log');
|
||||||
|
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
log: createLogMock(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addedFunctionsRegistry', () => {
|
||||||
|
const originalApps = config.apps;
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const appPluginConfig = {
|
||||||
|
id: pluginId,
|
||||||
|
path: '',
|
||||||
|
version: '',
|
||||||
|
preload: false,
|
||||||
|
angular: {
|
||||||
|
detected: false,
|
||||||
|
hideDeprecation: false,
|
||||||
|
},
|
||||||
|
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||||
|
dependencies: {
|
||||||
|
grafanaVersion: '8.0.0',
|
||||||
|
plugins: [],
|
||||||
|
extensions: {
|
||||||
|
exposedComponents: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
addedFunctions: [],
|
||||||
|
addedLinks: [],
|
||||||
|
addedComponents: [],
|
||||||
|
exposedComponents: [],
|
||||||
|
extensionPoints: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
resetLogMock(log);
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||||
|
config.apps = {
|
||||||
|
[pluginId]: appPluginConfig,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
config.apps = originalApps;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty registry when no extensions registered', async () => {
|
||||||
|
const addedFunctionsRegistry = new AddedFunctionsRegistry();
|
||||||
|
const observable = addedFunctionsRegistry.asObservable();
|
||||||
|
const registry = await firstValueFrom(observable);
|
||||||
|
expect(registry).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be possible to register function extensions in the registry', async () => {
|
||||||
|
const addedFunctionsRegistry = new AddedFunctionsRegistry();
|
||||||
|
|
||||||
|
addedFunctionsRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
targets: 'plugins/myorg-basic-app/start',
|
||||||
|
fn: jest.fn(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry = await addedFunctionsRegistry.getState();
|
||||||
|
|
||||||
|
expect(registry).toEqual({
|
||||||
|
'grafana/dashboard/panel/menu': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId,
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'plugins/myorg-basic-app/start': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId,
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
extensionPointId: 'plugins/myorg-basic-app/start',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should be possible to asynchronously register function extensions for the same placement (different plugins)', async () => {
|
||||||
|
const pluginId1 = 'grafana-basic-app';
|
||||||
|
const pluginId2 = 'grafana-basic-app2';
|
||||||
|
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||||
|
|
||||||
|
// Register extensions for the first plugin
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId1,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry1 = await reactiveRegistry.getState();
|
||||||
|
|
||||||
|
expect(registry1).toEqual({
|
||||||
|
'grafana/dashboard/panel/menu': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId1,
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register extensions for the second plugin to a different placement
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId2,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry2 = await reactiveRegistry.getState();
|
||||||
|
|
||||||
|
expect(registry2).toEqual({
|
||||||
|
'grafana/dashboard/panel/menu': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId1,
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pluginId: pluginId2,
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be possible to asynchronously register function extensions for a different placement (different plugin)', async () => {
|
||||||
|
const pluginId1 = 'grafana-basic-app';
|
||||||
|
const pluginId2 = 'grafana-basic-app2';
|
||||||
|
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||||
|
|
||||||
|
// Register extensions for the first plugin
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId1,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry1 = await reactiveRegistry.getState();
|
||||||
|
|
||||||
|
expect(registry1).toEqual({
|
||||||
|
'grafana/dashboard/panel/menu': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId1,
|
||||||
|
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register extensions for the second plugin to a different placement
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId2,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
targets: 'plugins/myorg-basic-app/start',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry2 = await reactiveRegistry.getState();
|
||||||
|
|
||||||
|
expect(registry2).toEqual({
|
||||||
|
'grafana/dashboard/panel/menu': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId1,
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'plugins/myorg-basic-app/start': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId2,
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
extensionPointId: 'plugins/myorg-basic-app/start',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be possible to asynchronously register function extensions for the same placement (same plugin)', async () => {
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||||
|
|
||||||
|
// Register extensions for the first extension point
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register extensions to a different extension point
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry2 = await reactiveRegistry.getState();
|
||||||
|
|
||||||
|
expect(registry2).toEqual({
|
||||||
|
'grafana/dashboard/panel/menu': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId,
|
||||||
|
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pluginId: pluginId,
|
||||||
|
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be possible to asynchronously register function extensions for a different placement (same plugin)', async () => {
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||||
|
|
||||||
|
// Register extensions for the first extension point
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register extensions to a different extension point
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
targets: 'plugins/myorg-basic-app/start',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry2 = await reactiveRegistry.getState();
|
||||||
|
|
||||||
|
expect(registry2).toEqual({
|
||||||
|
'grafana/dashboard/panel/menu': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId,
|
||||||
|
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'plugins/myorg-basic-app/start': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId,
|
||||||
|
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
extensionPointId: 'plugins/myorg-basic-app/start',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should notify subscribers when the registry changes', async () => {
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||||
|
const observable = reactiveRegistry.asObservable();
|
||||||
|
const subscribeCallback = jest.fn();
|
||||||
|
|
||||||
|
observable.subscribe(subscribeCallback);
|
||||||
|
|
||||||
|
// Register extensions for the first plugin
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(subscribeCallback).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
// Register extensions for the first plugin
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: 'another-plugin',
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(subscribeCallback).toHaveBeenCalledTimes(3);
|
||||||
|
|
||||||
|
const registry = subscribeCallback.mock.calls[2][0];
|
||||||
|
|
||||||
|
expect(registry).toEqual({
|
||||||
|
'grafana/dashboard/panel/menu': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId,
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pluginId: 'another-plugin',
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give the last version of the registry for new subscribers', async () => {
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||||
|
const observable = reactiveRegistry.asObservable();
|
||||||
|
const subscribeCallback = jest.fn();
|
||||||
|
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
observable.subscribe(subscribeCallback);
|
||||||
|
expect(subscribeCallback).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const registry = subscribeCallback.mock.calls[0][0];
|
||||||
|
|
||||||
|
expect(registry).toEqual({
|
||||||
|
'grafana/dashboard/panel/menu': [
|
||||||
|
{
|
||||||
|
pluginId: pluginId,
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: expect.any(Function),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not register a function extension if it has an invalid fn function', () => {
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||||
|
const observable = reactiveRegistry.asObservable();
|
||||||
|
const subscribeCallback = jest.fn();
|
||||||
|
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
//@ts-ignore
|
||||||
|
fn: '...',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(log.error).toHaveBeenCalled();
|
||||||
|
|
||||||
|
observable.subscribe(subscribeCallback);
|
||||||
|
expect(subscribeCallback).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const registry = subscribeCallback.mock.calls[0][0];
|
||||||
|
expect(registry).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not register a function extension if it has invalid properties (empty title)', () => {
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const reactiveRegistry = new AddedFunctionsRegistry();
|
||||||
|
const observable = reactiveRegistry.asObservable();
|
||||||
|
const subscribeCallback = jest.fn();
|
||||||
|
|
||||||
|
reactiveRegistry.register({
|
||||||
|
pluginId: pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(log.error).toHaveBeenCalled();
|
||||||
|
|
||||||
|
observable.subscribe(subscribeCallback);
|
||||||
|
expect(subscribeCallback).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const registry = subscribeCallback.mock.calls[0][0];
|
||||||
|
expect(registry).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be possible to register a function on a read-only registry', async () => {
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const registry = new AddedFunctionsRegistry();
|
||||||
|
const readOnlyRegistry = registry.readOnly();
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
readOnlyRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
targets: 'plugins/myorg-basic-app/start',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}).toThrow(MSG_CANNOT_REGISTER_READ_ONLY);
|
||||||
|
|
||||||
|
const currentState = await readOnlyRegistry.getState();
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass down fresh registrations to the read-only version of the registry', async () => {
|
||||||
|
const pluginId = 'grafana-basic-app';
|
||||||
|
const registry = new AddedFunctionsRegistry();
|
||||||
|
const readOnlyRegistry = registry.readOnly();
|
||||||
|
const subscribeCallback = jest.fn();
|
||||||
|
let readOnlyState;
|
||||||
|
|
||||||
|
// Should have no extensions registered in the beginning
|
||||||
|
readOnlyState = await readOnlyRegistry.getState();
|
||||||
|
expect(Object.keys(readOnlyState)).toHaveLength(0);
|
||||||
|
|
||||||
|
readOnlyRegistry.asObservable().subscribe(subscribeCallback);
|
||||||
|
|
||||||
|
// Register an extension to the original (writable) registry
|
||||||
|
registry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
title: 'Function 2',
|
||||||
|
description: 'Function 2 description',
|
||||||
|
targets: 'plugins/myorg-basic-app/start',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// The read-only registry should have received the new extension
|
||||||
|
readOnlyState = await readOnlyRegistry.getState();
|
||||||
|
expect(Object.keys(readOnlyState)).toHaveLength(1);
|
||||||
|
|
||||||
|
expect(subscribeCallback).toHaveBeenCalledTimes(2);
|
||||||
|
expect(Object.keys(subscribeCallback.mock.calls[1][0])).toEqual(['plugins/myorg-basic-app/start']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not register a function added by a plugin in dev-mode if the meta-info is missing from the plugin.json', async () => {
|
||||||
|
// Enabling dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
const registry = new AddedFunctionsRegistry();
|
||||||
|
const fnConfig = {
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure that the meta-info is empty
|
||||||
|
config.apps[pluginId].extensions.addedFunctions = [];
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [fnConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(0);
|
||||||
|
expect(log.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register a function added by core Grafana in dev-mode even if the meta-info is missing', async () => {
|
||||||
|
// Enabling dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
const registry = new AddedFunctionsRegistry();
|
||||||
|
const fnConfig = {
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId: 'grafana',
|
||||||
|
configs: [fnConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(1);
|
||||||
|
expect(log.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register a function added by a plugin in production mode even if the meta-info is missing', async () => {
|
||||||
|
// Production mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||||
|
|
||||||
|
const registry = new AddedFunctionsRegistry();
|
||||||
|
const fnConfig = {
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: 'grafana/dashboard/panel/menu',
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure that the meta-info is empty
|
||||||
|
config.apps[pluginId].extensions.addedFunctions = [];
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [fnConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(1);
|
||||||
|
expect(log.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register a function added by a plugin in dev-mode if the meta-info is present', async () => {
|
||||||
|
// Enabling dev mode
|
||||||
|
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||||
|
|
||||||
|
const registry = new AddedFunctionsRegistry();
|
||||||
|
const fnConfig = {
|
||||||
|
title: 'Function 1',
|
||||||
|
description: 'Function 1 description',
|
||||||
|
targets: ['grafana/dashboard/panel/menu'],
|
||||||
|
fn: jest.fn().mockReturnValue({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure that the meta-info is empty
|
||||||
|
config.apps[pluginId].extensions.addedFunctions = [fnConfig];
|
||||||
|
|
||||||
|
registry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: [fnConfig],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentState = await registry.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(currentState)).toHaveLength(1);
|
||||||
|
expect(log.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { isFunction } from 'lodash';
|
||||||
|
import { ReplaySubject } from 'rxjs';
|
||||||
|
|
||||||
|
import { PluginExtensionAddedFunctionConfig } from '@grafana/data';
|
||||||
|
|
||||||
|
import * as errors from '../errors';
|
||||||
|
import { isGrafanaDevMode } from '../utils';
|
||||||
|
import { isAddedFunctionMetaInfoMissing } from '../validators';
|
||||||
|
|
||||||
|
import { PluginExtensionConfigs, Registry, RegistryType } from './Registry';
|
||||||
|
|
||||||
|
const logPrefix = 'Could not register function extension. Reason:';
|
||||||
|
|
||||||
|
export type AddedFunctionsRegistryItem = {
|
||||||
|
pluginId: string;
|
||||||
|
title: string;
|
||||||
|
fn: unknown;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AddedFunctionsRegistry extends Registry<AddedFunctionsRegistryItem[], PluginExtensionAddedFunctionConfig> {
|
||||||
|
constructor(
|
||||||
|
options: {
|
||||||
|
registrySubject?: ReplaySubject<RegistryType<AddedFunctionsRegistryItem[]>>;
|
||||||
|
initialState?: RegistryType<AddedFunctionsRegistryItem[]>;
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
super(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
mapToRegistry(
|
||||||
|
registry: RegistryType<AddedFunctionsRegistryItem[]>,
|
||||||
|
item: PluginExtensionConfigs<PluginExtensionAddedFunctionConfig>
|
||||||
|
): RegistryType<AddedFunctionsRegistryItem[]> {
|
||||||
|
const { pluginId, configs } = item;
|
||||||
|
for (const config of configs) {
|
||||||
|
const configLog = this.logger.child({
|
||||||
|
title: config.title,
|
||||||
|
pluginId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!config.title) {
|
||||||
|
configLog.error(`${logPrefix} ${errors.TITLE_MISSING}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFunction(config.fn)) {
|
||||||
|
configLog.error(`${logPrefix} ${errors.INVALID_EXTENSION_FUNCTION}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginId !== 'grafana' && isGrafanaDevMode() && isAddedFunctionMetaInfoMissing(pluginId, config, configLog)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionPointIds = Array.isArray(config.targets) ? config.targets : [config.targets];
|
||||||
|
for (const extensionPointId of extensionPointIds) {
|
||||||
|
const pointIdLog = configLog.child({ extensionPointId });
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
pluginId,
|
||||||
|
fn: config.fn,
|
||||||
|
description: config.description,
|
||||||
|
title: config.title,
|
||||||
|
extensionPointId,
|
||||||
|
};
|
||||||
|
|
||||||
|
pointIdLog.debug('Added function extension successfully registered');
|
||||||
|
|
||||||
|
if (!(extensionPointId in registry)) {
|
||||||
|
registry[extensionPointId] = [result];
|
||||||
|
} else {
|
||||||
|
registry[extensionPointId].push(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a read-only version of the registry.
|
||||||
|
readOnly() {
|
||||||
|
return new AddedFunctionsRegistry({
|
||||||
|
registrySubject: this.registrySubject,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ describe('AddedLinksRegistry', () => {
|
|||||||
extensions: {
|
extensions: {
|
||||||
addedLinks: [],
|
addedLinks: [],
|
||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ describe('ExposedComponentsRegistry', () => {
|
|||||||
extensions: {
|
extensions: {
|
||||||
addedLinks: [],
|
addedLinks: [],
|
||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getCoreExtensionConfigurations } from '../getCoreExtensionConfigurations';
|
import { getCoreExtensionConfigurations } from '../getCoreExtensionConfigurations';
|
||||||
|
|
||||||
import { AddedComponentsRegistry } from './AddedComponentsRegistry';
|
import { AddedComponentsRegistry } from './AddedComponentsRegistry';
|
||||||
|
import { AddedFunctionsRegistry } from './AddedFunctionsRegistry';
|
||||||
import { AddedLinksRegistry } from './AddedLinksRegistry';
|
import { AddedLinksRegistry } from './AddedLinksRegistry';
|
||||||
import { ExposedComponentsRegistry } from './ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from './ExposedComponentsRegistry';
|
||||||
import { PluginExtensionRegistries } from './types';
|
import { PluginExtensionRegistries } from './types';
|
||||||
@@ -8,10 +9,12 @@ import { PluginExtensionRegistries } from './types';
|
|||||||
export const addedComponentsRegistry = new AddedComponentsRegistry();
|
export const addedComponentsRegistry = new AddedComponentsRegistry();
|
||||||
export const exposedComponentsRegistry = new ExposedComponentsRegistry();
|
export const exposedComponentsRegistry = new ExposedComponentsRegistry();
|
||||||
export const addedLinksRegistry = new AddedLinksRegistry();
|
export const addedLinksRegistry = new AddedLinksRegistry();
|
||||||
|
export const addedFunctionsRegistry = new AddedFunctionsRegistry();
|
||||||
export const pluginExtensionRegistries: PluginExtensionRegistries = {
|
export const pluginExtensionRegistries: PluginExtensionRegistries = {
|
||||||
addedComponentsRegistry,
|
addedComponentsRegistry,
|
||||||
exposedComponentsRegistry,
|
exposedComponentsRegistry,
|
||||||
addedLinksRegistry,
|
addedLinksRegistry,
|
||||||
|
addedFunctionsRegistry,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Registering core extensions
|
// Registering core extensions
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { AddedComponentsRegistry } from './AddedComponentsRegistry';
|
import { AddedComponentsRegistry } from './AddedComponentsRegistry';
|
||||||
|
import { AddedFunctionsRegistry } from './AddedFunctionsRegistry';
|
||||||
import { AddedLinksRegistry } from './AddedLinksRegistry';
|
import { AddedLinksRegistry } from './AddedLinksRegistry';
|
||||||
import { ExposedComponentsRegistry } from './ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from './ExposedComponentsRegistry';
|
||||||
|
|
||||||
export type PluginExtensionRegistries = {
|
export type PluginExtensionRegistries = {
|
||||||
addedComponentsRegistry: AddedComponentsRegistry;
|
addedComponentsRegistry: AddedComponentsRegistry;
|
||||||
exposedComponentsRegistry: ExposedComponentsRegistry;
|
exposedComponentsRegistry: ExposedComponentsRegistry;
|
||||||
|
addedFunctionsRegistry: AddedFunctionsRegistry;
|
||||||
addedLinksRegistry: AddedLinksRegistry;
|
addedLinksRegistry: AddedLinksRegistry;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
|||||||
import { log } from './logs/log';
|
import { log } from './logs/log';
|
||||||
import { resetLogMock } from './logs/testUtils';
|
import { resetLogMock } from './logs/testUtils';
|
||||||
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||||
|
import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry';
|
||||||
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
||||||
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
||||||
import { PluginExtensionRegistries } from './registry/types';
|
import { PluginExtensionRegistries } from './registry/types';
|
||||||
@@ -78,6 +79,7 @@ describe('usePluginComponent()', () => {
|
|||||||
extensions: {
|
extensions: {
|
||||||
addedLinks: [],
|
addedLinks: [],
|
||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
// This is necessary, so we can register exposed components to the registry during the tests
|
// This is necessary, so we can register exposed components to the registry during the tests
|
||||||
// (Otherwise the registry would reject it in the imitated production mode)
|
// (Otherwise the registry would reject it in the imitated production mode)
|
||||||
exposedComponents: [exposedComponentConfig],
|
exposedComponents: [exposedComponentConfig],
|
||||||
@@ -90,6 +92,7 @@ describe('usePluginComponent()', () => {
|
|||||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||||
addedLinksRegistry: new AddedLinksRegistry(),
|
addedLinksRegistry: new AddedLinksRegistry(),
|
||||||
|
addedFunctionsRegistry: new AddedFunctionsRegistry(),
|
||||||
};
|
};
|
||||||
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
||||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||||
@@ -122,6 +125,7 @@ describe('usePluginComponent()', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
grafanaVersion: '8.0.0',
|
grafanaVersion: '8.0.0',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
|||||||
import { log } from './logs/log';
|
import { log } from './logs/log';
|
||||||
import { resetLogMock } from './logs/testUtils';
|
import { resetLogMock } from './logs/testUtils';
|
||||||
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||||
|
import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry';
|
||||||
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
||||||
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
||||||
import { PluginExtensionRegistries } from './registry/types';
|
import { PluginExtensionRegistries } from './registry/types';
|
||||||
@@ -60,6 +61,7 @@ describe('usePluginComponents()', () => {
|
|||||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||||
addedLinksRegistry: new AddedLinksRegistry(),
|
addedLinksRegistry: new AddedLinksRegistry(),
|
||||||
|
addedFunctionsRegistry: new AddedFunctionsRegistry(),
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mocked(wrapWithPluginContext).mockClear();
|
jest.mocked(wrapWithPluginContext).mockClear();
|
||||||
@@ -89,6 +91,7 @@ describe('usePluginComponents()', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
grafanaVersion: '8.0.0',
|
grafanaVersion: '8.0.0',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { act, renderHook } from '@testing-library/react';
|
import { act, renderHook } from '@testing-library/react';
|
||||||
|
|
||||||
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||||
|
import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry';
|
||||||
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
||||||
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
||||||
import { PluginExtensionRegistries } from './registry/types';
|
import { PluginExtensionRegistries } from './registry/types';
|
||||||
@@ -19,6 +20,7 @@ describe('usePluginExtensions()', () => {
|
|||||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||||
addedLinksRegistry: new AddedLinksRegistry(),
|
addedLinksRegistry: new AddedLinksRegistry(),
|
||||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||||
|
addedFunctionsRegistry: new AddedFunctionsRegistry(),
|
||||||
};
|
};
|
||||||
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useObservable } from 'react-use';
|
||||||
|
|
||||||
|
import { usePluginContext, PluginExtensionFunction, PluginExtensionTypes } from '@grafana/data';
|
||||||
|
import { UsePluginFunctionsOptions, UsePluginFunctionsResult } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { useAddedFunctionsRegistry } from './ExtensionRegistriesContext';
|
||||||
|
import * as errors from './errors';
|
||||||
|
import { log } from './logs/log';
|
||||||
|
import { useLoadAppPlugins } from './useLoadAppPlugins';
|
||||||
|
import { generateExtensionId, getExtensionPointPluginDependencies, isGrafanaDevMode } from './utils';
|
||||||
|
import { isExtensionPointIdValid, isExtensionPointMetaInfoMissing } from './validators';
|
||||||
|
|
||||||
|
// Returns an array of component extensions for the given extension point
|
||||||
|
export function usePluginFunctions<Signature>({
|
||||||
|
limitPerPlugin,
|
||||||
|
extensionPointId,
|
||||||
|
}: UsePluginFunctionsOptions): UsePluginFunctionsResult<Signature> {
|
||||||
|
const registry = useAddedFunctionsRegistry();
|
||||||
|
const registryState = useObservable(registry.asObservable());
|
||||||
|
const pluginContext = usePluginContext();
|
||||||
|
const deps = getExtensionPointPluginDependencies(extensionPointId);
|
||||||
|
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(deps);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
// For backwards compatibility we don't enable restrictions in production or when the hook is used in core Grafana.
|
||||||
|
const enableRestrictions = isGrafanaDevMode() && pluginContext;
|
||||||
|
const results: Array<PluginExtensionFunction<Signature>> = [];
|
||||||
|
const extensionsByPlugin: Record<string, number> = {};
|
||||||
|
const pluginId = pluginContext?.meta.id ?? '';
|
||||||
|
const pointLog = log.child({
|
||||||
|
pluginId,
|
||||||
|
extensionPointId,
|
||||||
|
});
|
||||||
|
if (enableRestrictions && !isExtensionPointIdValid({ extensionPointId, pluginId })) {
|
||||||
|
pointLog.error(errors.INVALID_EXTENSION_POINT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) {
|
||||||
|
pointLog.error(errors.EXTENSION_POINT_META_INFO_MISSING);
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
functions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoadingAppPlugins) {
|
||||||
|
return {
|
||||||
|
isLoading: true,
|
||||||
|
functions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const registryItem of registryState?.[extensionPointId] ?? []) {
|
||||||
|
const { pluginId } = registryItem;
|
||||||
|
|
||||||
|
// Only limit if the `limitPerPlugin` is set
|
||||||
|
if (limitPerPlugin && extensionsByPlugin[pluginId] >= limitPerPlugin) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extensionsByPlugin[pluginId] === undefined) {
|
||||||
|
extensionsByPlugin[pluginId] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
id: generateExtensionId(pluginId, extensionPointId, registryItem.title),
|
||||||
|
type: PluginExtensionTypes.function,
|
||||||
|
title: registryItem.title,
|
||||||
|
description: registryItem.description ?? '',
|
||||||
|
pluginId: pluginId,
|
||||||
|
fn: registryItem.fn as Signature,
|
||||||
|
});
|
||||||
|
extensionsByPlugin[pluginId] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
functions: results,
|
||||||
|
};
|
||||||
|
}, [extensionPointId, limitPerPlugin, pluginContext, registryState, isLoadingAppPlugins]);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
|||||||
import { log } from './logs/log';
|
import { log } from './logs/log';
|
||||||
import { resetLogMock } from './logs/testUtils';
|
import { resetLogMock } from './logs/testUtils';
|
||||||
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
|
||||||
|
import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry';
|
||||||
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
|
||||||
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
|
||||||
import { PluginExtensionRegistries } from './registry/types';
|
import { PluginExtensionRegistries } from './registry/types';
|
||||||
@@ -57,6 +58,7 @@ describe('usePluginLinks()', () => {
|
|||||||
addedComponentsRegistry: new AddedComponentsRegistry(),
|
addedComponentsRegistry: new AddedComponentsRegistry(),
|
||||||
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
exposedComponentsRegistry: new ExposedComponentsRegistry(),
|
||||||
addedLinksRegistry: new AddedLinksRegistry(),
|
addedLinksRegistry: new AddedLinksRegistry(),
|
||||||
|
addedFunctionsRegistry: new AddedFunctionsRegistry(),
|
||||||
};
|
};
|
||||||
resetLogMock(log);
|
resetLogMock(log);
|
||||||
|
|
||||||
@@ -85,6 +87,7 @@ describe('usePluginLinks()', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
grafanaVersion: '8.0.0',
|
grafanaVersion: '8.0.0',
|
||||||
|
|||||||
@@ -475,6 +475,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
extensions: {
|
extensions: {
|
||||||
addedLinks: [],
|
addedLinks: [],
|
||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
},
|
},
|
||||||
@@ -553,6 +554,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
extensions: {
|
extensions: {
|
||||||
addedLinks: [],
|
addedLinks: [],
|
||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
},
|
},
|
||||||
@@ -584,6 +586,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'myorg-third-app': {
|
'myorg-third-app': {
|
||||||
@@ -623,6 +626,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
],
|
],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -679,6 +683,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
],
|
],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
...genereicAppPluginConfig.dependencies,
|
...genereicAppPluginConfig.dependencies,
|
||||||
@@ -705,6 +710,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
...genereicAppPluginConfig.dependencies,
|
...genereicAppPluginConfig.dependencies,
|
||||||
@@ -726,6 +732,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'myorg-sixth-app': {
|
'myorg-sixth-app': {
|
||||||
@@ -763,6 +770,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -791,6 +799,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'myorg-third-app': {
|
'myorg-third-app': {
|
||||||
@@ -825,6 +834,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
...genereicAppPluginConfig.dependencies,
|
...genereicAppPluginConfig.dependencies,
|
||||||
@@ -850,6 +860,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
...genereicAppPluginConfig.dependencies,
|
...genereicAppPluginConfig.dependencies,
|
||||||
@@ -871,6 +882,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -902,6 +914,7 @@ describe('Plugin Extensions / Utils', () => {
|
|||||||
extensions: {
|
extensions: {
|
||||||
addedLinks: [],
|
addedLinks: [],
|
||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
|
addedFunctions: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -271,6 +271,7 @@ describe('Plugin Extension Validators', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const extensionConfig = {
|
const extensionConfig = {
|
||||||
@@ -387,6 +388,7 @@ describe('Plugin Extension Validators', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const extensionConfig = {
|
const extensionConfig = {
|
||||||
@@ -503,6 +505,7 @@ describe('Plugin Extension Validators', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const exposedComponentConfig = {
|
const exposedComponentConfig = {
|
||||||
@@ -688,6 +691,7 @@ describe('Plugin Extension Validators', () => {
|
|||||||
addedComponents: [],
|
addedComponents: [],
|
||||||
exposedComponents: [],
|
exposedComponents: [],
|
||||||
extensionPoints: [],
|
extensionPoints: [],
|
||||||
|
addedFunctions: [],
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
grafanaVersion: '8.0.0',
|
grafanaVersion: '8.0.0',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
PluginContextType,
|
PluginContextType,
|
||||||
PluginExtensionAddedComponentConfig,
|
PluginExtensionAddedComponentConfig,
|
||||||
PluginExtensionExposedComponentConfig,
|
PluginExtensionExposedComponentConfig,
|
||||||
|
PluginExtensionAddedFunctionConfig,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { PluginAddedLinksConfigureFunc, PluginExtensionPoints } from '@grafana/data/src/types/pluginExtensions';
|
import { PluginAddedLinksConfigureFunc, PluginExtensionPoints } from '@grafana/data/src/types/pluginExtensions';
|
||||||
import { config, isPluginExtensionLink } from '@grafana/runtime';
|
import { config, isPluginExtensionLink } from '@grafana/runtime';
|
||||||
@@ -160,6 +161,38 @@ export const isAddedLinkMetaInfoMissing = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isAddedFunctionMetaInfoMissing = (
|
||||||
|
pluginId: string,
|
||||||
|
metaInfo: PluginExtensionAddedFunctionConfig,
|
||||||
|
log: ExtensionsLog
|
||||||
|
) => {
|
||||||
|
const logPrefix = 'Could not register function extension. Reason:';
|
||||||
|
const app = config.apps[pluginId];
|
||||||
|
const pluginJsonMetaInfo = app ? app.extensions.addedFunctions.find(({ title }) => title === metaInfo.title) : null;
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
log.error(`${logPrefix} ${errors.APP_NOT_FOUND(pluginId)}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pluginJsonMetaInfo) {
|
||||||
|
log.error(`${logPrefix} ${errors.ADDED_FUNCTION_META_INFO_MISSING}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets = Array.isArray(metaInfo.targets) ? metaInfo.targets : [metaInfo.targets];
|
||||||
|
if (!targets.every((target) => pluginJsonMetaInfo.targets.includes(target))) {
|
||||||
|
log.error(`${logPrefix} ${errors.TARGET_NOT_MATCHING_META_INFO}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginJsonMetaInfo.description !== metaInfo.description) {
|
||||||
|
log.warning(errors.DESCRIPTION_NOT_MATCHING_META_INFO);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
export const isAddedComponentMetaInfoMissing = (
|
export const isAddedComponentMetaInfoMissing = (
|
||||||
pluginId: string,
|
pluginId: string,
|
||||||
metaInfo: PluginExtensionAddedComponentConfig,
|
metaInfo: PluginExtensionAddedComponentConfig,
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ function getPanelPlugin(meta: PanelPluginMeta): Promise<PanelPlugin> {
|
|||||||
if (!plugin.panel && plugin.angularPanelCtrl) {
|
if (!plugin.panel && plugin.angularPanelCtrl) {
|
||||||
plugin.panel = getAngularPanelReactWrapper(plugin);
|
plugin.panel = getAngularPanelReactWrapper(plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
return plugin;
|
return plugin;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { appEvents, contextSrv } from 'app/core/core';
|
|||||||
import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv';
|
import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
import impressionSrv from 'app/core/services/impression_srv';
|
import impressionSrv from 'app/core/services/impression_srv';
|
||||||
import TimeSeries from 'app/core/time_series2';
|
import TimeSeries from 'app/core/time_series2';
|
||||||
|
import { arrayMove } from 'app/core/utils/arrayMove';
|
||||||
import * as flatten from 'app/core/utils/flatten';
|
import * as flatten from 'app/core/utils/flatten';
|
||||||
import kbn from 'app/core/utils/kbn';
|
import kbn from 'app/core/utils/kbn';
|
||||||
import * as ticks from 'app/core/utils/ticks';
|
import * as ticks from 'app/core/utils/ticks';
|
||||||
@@ -90,7 +91,8 @@ export const sharedDependenciesMap = {
|
|||||||
__useDefault: true,
|
__useDefault: true,
|
||||||
},
|
},
|
||||||
...jQueryFlotDeps,
|
...jQueryFlotDeps,
|
||||||
lodash: () => import('lodash').then((module) => ({ ...module, __useDefault: true })),
|
// add move to lodash for backward compatabilty with plugins
|
||||||
|
lodash: () => import('lodash').then((module) => ({ ...module, move: arrayMove, __useDefault: true })),
|
||||||
moment: () => import('moment').then((module) => ({ ...module, __useDefault: true })),
|
moment: () => import('moment').then((module) => ({ ...module, __useDefault: true })),
|
||||||
prismjs: () => import('prismjs'),
|
prismjs: () => import('prismjs'),
|
||||||
react: () => import('react'),
|
react: () => import('react'),
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ import { DataQuery } from '@grafana/schema';
|
|||||||
import { GenericDataSourcePlugin } from '../datasources/types';
|
import { GenericDataSourcePlugin } from '../datasources/types';
|
||||||
|
|
||||||
import builtInPlugins from './built_in_plugins';
|
import builtInPlugins from './built_in_plugins';
|
||||||
import { addedComponentsRegistry, addedLinksRegistry, exposedComponentsRegistry } from './extensions/registry/setup';
|
import {
|
||||||
|
addedComponentsRegistry,
|
||||||
|
addedFunctionsRegistry,
|
||||||
|
addedLinksRegistry,
|
||||||
|
exposedComponentsRegistry,
|
||||||
|
} from './extensions/registry/setup';
|
||||||
import { getPluginFromCache, registerPluginInCache } from './loader/cache';
|
import { getPluginFromCache, registerPluginInCache } from './loader/cache';
|
||||||
// SystemJS has to be imported before the sharedDependenciesMap
|
// SystemJS has to be imported before the sharedDependenciesMap
|
||||||
import { SystemJS } from './loader/systemjs';
|
import { SystemJS } from './loader/systemjs';
|
||||||
@@ -153,7 +158,6 @@ export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise<Gene
|
|||||||
dsPlugin.meta = meta;
|
dsPlugin.meta = meta;
|
||||||
return dsPlugin;
|
return dsPlugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pluginExports.Datasource) {
|
if (pluginExports.Datasource) {
|
||||||
const dsPlugin = new DataSourcePlugin<
|
const dsPlugin = new DataSourcePlugin<
|
||||||
DataSourceApi<DataQuery, DataSourceJsonData>,
|
DataSourceApi<DataQuery, DataSourceJsonData>,
|
||||||
@@ -205,6 +209,10 @@ export async function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> {
|
|||||||
pluginId,
|
pluginId,
|
||||||
configs: plugin.addedLinkConfigs || [],
|
configs: plugin.addedLinkConfigs || [],
|
||||||
});
|
});
|
||||||
|
addedFunctionsRegistry.register({
|
||||||
|
pluginId,
|
||||||
|
configs: plugin.addedFunctionConfigs || [],
|
||||||
|
});
|
||||||
|
|
||||||
importedAppPlugins[pluginId] = plugin;
|
importedAppPlugins[pluginId] = plugin;
|
||||||
|
|
||||||
|
|||||||
@@ -2781,17 +2781,30 @@
|
|||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"contactGrafanaLabs": "Contact Grafana Labs",
|
"contactGrafanaLabs": "Contact Grafana Labs",
|
||||||
|
"customLinks": "Custom links ",
|
||||||
|
"customLinksTooltip": "These links are provided by the plugin developer to offer additional, developer-specific resources and information",
|
||||||
"dependencies": "Dependencies",
|
"dependencies": "Dependencies",
|
||||||
|
"documentation": "Documentation",
|
||||||
"downloads": "Downloads",
|
"downloads": "Downloads",
|
||||||
"from": "From",
|
"from": "From",
|
||||||
"installedVersion": "Installed Version",
|
"installedVersion": "Installed Version",
|
||||||
"lastCommitDate": "Last commit date:",
|
"lastCommitDate": "Last commit date:",
|
||||||
"latestVersion": "Latest Version",
|
"latestVersion": "Latest Version",
|
||||||
"links": "Links ",
|
"license": "License",
|
||||||
|
"raiseAnIssue": "Raise an issue",
|
||||||
"reportAbuse": "Report a concern ",
|
"reportAbuse": "Report a concern ",
|
||||||
|
"reportAbuseTooltip": "Report issues related to malicious or harmful plugins directly to Grafana Labs.",
|
||||||
|
"repository": "Repository",
|
||||||
"signature": "Signature",
|
"signature": "Signature",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"updatedAt": "Last updated:"
|
"updatedAt": "Last updated:"
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"copyEmail": "Copy email address",
|
||||||
|
"description": "This feature is for reporting malicious or harmful behaviour within plugins. For plugin concerns, email us at: ",
|
||||||
|
"node": "Note: For general plugin issues like bugs or feature requests, please contact the plugin author using the provided links. ",
|
||||||
|
"title": "Report a plugin concern"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"empty-state": {
|
"empty-state": {
|
||||||
|
|||||||
@@ -2781,17 +2781,30 @@
|
|||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"contactGrafanaLabs": "Cőʼnŧäčŧ Ğřäƒäʼnä Ŀäþş",
|
"contactGrafanaLabs": "Cőʼnŧäčŧ Ğřäƒäʼnä Ŀäþş",
|
||||||
|
"customLinks": "Cūşŧőm ľįʼnĸş ",
|
||||||
|
"customLinksTooltip": "Ŧĥęşę ľįʼnĸş äřę přővįđęđ þy ŧĥę pľūģįʼn đęvęľőpęř ŧő őƒƒęř äđđįŧįőʼnäľ, đęvęľőpęř-şpęčįƒįč řęşőūřčęş äʼnđ įʼnƒőřmäŧįőʼn",
|
||||||
"dependencies": "Đępęʼnđęʼnčįęş",
|
"dependencies": "Đępęʼnđęʼnčįęş",
|
||||||
|
"documentation": "Đőčūmęʼnŧäŧįőʼn",
|
||||||
"downloads": "Đőŵʼnľőäđş",
|
"downloads": "Đőŵʼnľőäđş",
|
||||||
"from": "Fřőm",
|
"from": "Fřőm",
|
||||||
"installedVersion": "Ĩʼnşŧäľľęđ Vęřşįőʼn",
|
"installedVersion": "Ĩʼnşŧäľľęđ Vęřşįőʼn",
|
||||||
"lastCommitDate": "Ŀäşŧ čőmmįŧ đäŧę:",
|
"lastCommitDate": "Ŀäşŧ čőmmįŧ đäŧę:",
|
||||||
"latestVersion": "Ŀäŧęşŧ Vęřşįőʼn",
|
"latestVersion": "Ŀäŧęşŧ Vęřşįőʼn",
|
||||||
"links": "Ŀįʼnĸş ",
|
"license": "Ŀįčęʼnşę",
|
||||||
|
"raiseAnIssue": "Ŗäįşę äʼn įşşūę",
|
||||||
"reportAbuse": "Ŗępőřŧ ä čőʼnčęřʼn ",
|
"reportAbuse": "Ŗępőřŧ ä čőʼnčęřʼn ",
|
||||||
|
"reportAbuseTooltip": "Ŗępőřŧ įşşūęş řęľäŧęđ ŧő mäľįčįőūş őř ĥäřmƒūľ pľūģįʼnş đįřęčŧľy ŧő Ğřäƒäʼnä Ŀäþş.",
|
||||||
|
"repository": "Ŗępőşįŧőřy",
|
||||||
"signature": "Ŝįģʼnäŧūřę",
|
"signature": "Ŝįģʼnäŧūřę",
|
||||||
"status": "Ŝŧäŧūş",
|
"status": "Ŝŧäŧūş",
|
||||||
"updatedAt": "Ŀäşŧ ūpđäŧęđ:"
|
"updatedAt": "Ŀäşŧ ūpđäŧęđ:"
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"cancel": "Cäʼnčęľ",
|
||||||
|
"copyEmail": "Cőpy ęmäįľ äđđřęşş",
|
||||||
|
"description": "Ŧĥįş ƒęäŧūřę įş ƒőř řępőřŧįʼnģ mäľįčįőūş őř ĥäřmƒūľ þęĥävįőūř ŵįŧĥįʼn pľūģįʼnş. Főř pľūģįʼn čőʼnčęřʼnş, ęmäįľ ūş äŧ: ",
|
||||||
|
"node": "Ńőŧę: Főř ģęʼnęřäľ pľūģįʼn įşşūęş ľįĸę þūģş őř ƒęäŧūřę řęqūęşŧş, pľęäşę čőʼnŧäčŧ ŧĥę pľūģįʼn äūŧĥőř ūşįʼnģ ŧĥę přővįđęđ ľįʼnĸş. ",
|
||||||
|
"title": "Ŗępőřŧ ä pľūģįʼn čőʼnčęřʼn"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"empty-state": {
|
"empty-state": {
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
amtests "github.com/grafana/grafana/pkg/tests/alertmanager"
|
||||||
|
)
|
||||||
|
|
||||||
|
func docker(args []string) {
|
||||||
|
cmd := exec.Command("docker", args...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Printf("docker pull failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for _, cmd := range [][]string{
|
||||||
|
{"pull", amtests.GetGrafanaImage()},
|
||||||
|
{"pull", amtests.GetLokiImage()},
|
||||||
|
{"pull", amtests.GetPostgresImage()},
|
||||||
|
{"build", "-t", "webhook-receiver", "devenv/docker/blocks/stateful_webhook"},
|
||||||
|
} {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func(cmd []string) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
docker(cmd)
|
||||||
|
}(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user