diff --git a/.gitignore b/.gitignore index fb7d5d30a9a..d6dd8fb6d83 100644 --- a/.gitignore +++ b/.gitignore @@ -250,6 +250,8 @@ public/mockServiceWorker.js /e2e-playwright/test-plugins/*/dist /apps/provisioning/cmd/job-controller/bin/ - # Ignore unified storage kv store files /grafana-kv-data + +# Ignore debug output from test library +/pkg/storage/secret/metadata/testdata/rapid/TestStateMachine/ diff --git a/apps/advisor/pkg/app/app.go b/apps/advisor/pkg/app/app.go index fae11df0c6c..b572f4b3866 100644 --- a/apps/advisor/pkg/app/app.go +++ b/apps/advisor/pkg/app/app.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/k8s" "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-app-sdk/operator" "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana-app-sdk/simple" advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" @@ -48,8 +49,10 @@ func New(cfg app.Config) (app.App, error) { Name: "advisor", KubeConfig: cfg.KubeConfig, InformerConfig: simple.AppInformerConfig{ - ErrorHandler: func(ctx context.Context, err error) { - log.WithContext(ctx).Error("Informer processing error", "error", err) + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(ctx context.Context, err error) { + log.WithContext(ctx).Error("Informer processing error", "error", err) + }, }, }, ManagedKinds: []simple.AppManagedKind{ diff --git a/apps/alerting/alertenrichment/go.mod b/apps/alerting/alertenrichment/go.mod index aa670ef15ed..48c93f1aa1e 100644 --- a/apps/alerting/alertenrichment/go.mod +++ b/apps/alerting/alertenrichment/go.mod @@ -3,14 +3,14 @@ module github.com/grafana/grafana/apps/alerting/alertenrichment go 1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28 k8s.io/apimachinery v0.34.1 k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b ) require ( - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect diff --git a/apps/alerting/alertenrichment/go.sum b/apps/alerting/alertenrichment/go.sum index 1ff737c6fbd..ead26b4a7b0 100644 --- a/apps/alerting/alertenrichment/go.sum +++ b/apps/alerting/alertenrichment/go.sum @@ -2,8 +2,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -21,8 +21,8 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28 h1:PgMfX4OPENz/iXmtDDIW9+poZY4UD0hhmXm7flVclDo= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28/go.mod h1:av5N0Naq+8VV9MLF7zAkihy/mVq5UbS2EvRSJukDHlY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= diff --git a/apps/alerting/notifications/definitions/alerting-notifications-manifest.yaml b/apps/alerting/notifications/definitions/alerting-notifications-manifest.yaml index 447c5d5f747..aa40d7c74b3 100644 --- a/apps/alerting/notifications/definitions/alerting-notifications-manifest.yaml +++ b/apps/alerting/notifications/definitions/alerting-notifications-manifest.yaml @@ -5,6 +5,7 @@ metadata: spec: appName: alerting-notifications group: notifications.alerting.grafana.app + preferredVersion: v0alpha1 versions: - kinds: - conversion: false diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index 4f5e6fc625c..f811c12d2f8 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/notifications go 1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana-app-sdk/logging v0.45.0 k8s.io/apimachinery v0.34.1 k8s.io/apiserver v0.34.1 @@ -19,7 +19,7 @@ require ( github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index 793458ef166..9166be2eae7 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -21,8 +21,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -69,8 +69,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go b/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go index 3537a6e3e8d..84850cb31a1 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go @@ -34,8 +34,9 @@ var ( ) var appManifestData = app.ManifestData{ - AppName: "alerting-notifications", - Group: "notifications.alerting.grafana.app", + AppName: "alerting-notifications", + Group: "notifications.alerting.grafana.app", + PreferredVersion: "v0alpha1", Versions: []app.ManifestVersion{ { Name: "v0alpha1", diff --git a/apps/alerting/notifications/pkg/app/app.go b/apps/alerting/notifications/pkg/app/app.go index b57ed0cd143..65b3882ac59 100644 --- a/apps/alerting/notifications/pkg/app/app.go +++ b/apps/alerting/notifications/pkg/app/app.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-app-sdk/operator" "github.com/grafana/grafana-app-sdk/simple" "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis" @@ -22,8 +23,10 @@ func New(cfg app.Config) (app.App, error) { Name: "alerting.notification", KubeConfig: cfg.KubeConfig, InformerConfig: simple.AppInformerConfig{ - ErrorHandler: func(ctx context.Context, err error) { - logging.DefaultLogger.With("error", err).Error("Informer processing error") + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(ctx context.Context, err error) { + logging.DefaultLogger.With("error", err).Error("Informer processing error") + }, }, }, ManagedKinds: managedKinds, diff --git a/apps/alerting/rules/definitions/alerting-manifest.yaml b/apps/alerting/rules/definitions/alerting-manifest.yaml index 22808e9deba..836bd2c9a94 100644 --- a/apps/alerting/rules/definitions/alerting-manifest.yaml +++ b/apps/alerting/rules/definitions/alerting-manifest.yaml @@ -5,6 +5,7 @@ metadata: spec: appName: alerting group: rules.alerting.grafana.app + preferredVersion: v0alpha1 versions: - kinds: - conversion: false diff --git a/apps/alerting/rules/go.mod b/apps/alerting/rules/go.mod index 99aab42dd9a..7678f821f3a 100644 --- a/apps/alerting/rules/go.mod +++ b/apps/alerting/rules/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/rules go 1.24.4 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana-app-sdk/logging v0.45.0 k8s.io/apimachinery v0.34.1 k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b @@ -15,7 +15,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect diff --git a/apps/alerting/rules/go.sum b/apps/alerting/rules/go.sum index 396ebebc9de..dadd72070fd 100644 --- a/apps/alerting/rules/go.sum +++ b/apps/alerting/rules/go.sum @@ -10,8 +10,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -46,8 +46,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= diff --git a/apps/alerting/rules/pkg/apis/alerting_manifest.go b/apps/alerting/rules/pkg/apis/alerting_manifest.go index abbc407ddf3..ea4f2530948 100644 --- a/apps/alerting/rules/pkg/apis/alerting_manifest.go +++ b/apps/alerting/rules/pkg/apis/alerting_manifest.go @@ -28,8 +28,9 @@ var ( ) var appManifestData = app.ManifestData{ - AppName: "alerting", - Group: "rules.alerting.grafana.app", + AppName: "alerting", + Group: "rules.alerting.grafana.app", + PreferredVersion: "v0alpha1", Versions: []app.ManifestVersion{ { Name: "v0alpha1", diff --git a/apps/alerting/rules/pkg/app/app.go b/apps/alerting/rules/pkg/app/app.go index 513bcc92d30..ef0ab1883cf 100644 --- a/apps/alerting/rules/pkg/app/app.go +++ b/apps/alerting/rules/pkg/app/app.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-app-sdk/operator" "github.com/grafana/grafana-app-sdk/simple" "github.com/grafana/grafana/apps/alerting/rules/pkg/apis" @@ -22,8 +23,10 @@ func New(cfg app.Config) (app.App, error) { Name: "alerting.rules", KubeConfig: cfg.KubeConfig, InformerConfig: simple.AppInformerConfig{ - ErrorHandler: func(ctx context.Context, err error) { - logging.DefaultLogger.With("error", err).Error("Informer processing error") + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(ctx context.Context, err error) { + logging.DefaultLogger.With("error", err).Error("Informer processing error") + }, }, }, ManagedKinds: managedKinds, diff --git a/apps/correlations/go.mod b/apps/correlations/go.mod index 7599bad88d5..2f6538b879d 100644 --- a/apps/correlations/go.mod +++ b/apps/correlations/go.mod @@ -5,7 +5,7 @@ go 1.24.0 toolchain go1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana-app-sdk/logging v0.45.0 k8s.io/apimachinery v0.34.1 k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b @@ -17,7 +17,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect diff --git a/apps/correlations/go.sum b/apps/correlations/go.sum index 396ebebc9de..dadd72070fd 100644 --- a/apps/correlations/go.sum +++ b/apps/correlations/go.sum @@ -10,8 +10,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -46,8 +46,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= diff --git a/apps/correlations/pkg/apis/correlation_manifest.go b/apps/correlations/pkg/apis/correlation_manifest.go index 4ef118d2216..3a784903323 100644 --- a/apps/correlations/pkg/apis/correlation_manifest.go +++ b/apps/correlations/pkg/apis/correlation_manifest.go @@ -25,8 +25,9 @@ var ( ) var appManifestData = app.ManifestData{ - AppName: "correlation", - Group: "correlations.grafana.app", + AppName: "correlation", + Group: "correlations.grafana.app", + PreferredVersion: "v0alpha1", Versions: []app.ManifestVersion{ { Name: "v0alpha1", diff --git a/apps/correlations/pkg/app/app.go b/apps/correlations/pkg/app/app.go index 99a1600bac6..5d62c6c3902 100644 --- a/apps/correlations/pkg/app/app.go +++ b/apps/correlations/pkg/app/app.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-app-sdk/operator" "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana-app-sdk/simple" "k8s.io/apimachinery/pkg/runtime/schema" @@ -17,8 +18,10 @@ func New(cfg app.Config) (app.App, error) { Name: "correlation", KubeConfig: cfg.KubeConfig, InformerConfig: simple.AppInformerConfig{ - ErrorHandler: func(ctx context.Context, err error) { - logging.FromContext(ctx).Error("Informer processing error", "error", err) + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(ctx context.Context, err error) { + logging.FromContext(ctx).Error("Informer processing error", "error", err) + }, }, }, ManagedKinds: []simple.AppManagedKind{ diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index 01ace00792d..adfae61e8d1 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -5,7 +5,7 @@ go 1.24.6 require ( cuelang.org/go v0.11.1 github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana-app-sdk/logging v0.45.0 github.com/grafana/grafana-plugin-sdk-go v0.279.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e @@ -31,7 +31,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/elazarl/goproxy v1.7.2 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index 13b5f650649..4358d7d815a 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -36,8 +36,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY= github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -101,8 +101,8 @@ github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbP github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-plugin-sdk-go v0.279.0 h1:/KCrsZkj9pEGwIGovqAz1A8rjI2A2YT+ZpvgfZN0LAA= diff --git a/apps/dashboard/pkg/apis/dashboard_manifest.go b/apps/dashboard/pkg/apis/dashboard_manifest.go index 1e8f261e65b..3ea4e0284b3 100644 --- a/apps/dashboard/pkg/apis/dashboard_manifest.go +++ b/apps/dashboard/pkg/apis/dashboard_manifest.go @@ -21,8 +21,9 @@ import ( ) var appManifestData = app.ManifestData{ - AppName: "dashboard", - Group: "dashboard.grafana.app", + AppName: "dashboard", + Group: "dashboard.grafana.app", + PreferredVersion: "v1beta1", Versions: []app.ManifestVersion{ { Name: "v0alpha1", diff --git a/apps/dashboard/pkg/migration/schemaversion/v16.go b/apps/dashboard/pkg/migration/schemaversion/v16.go index f360fe246ad..d8fb357c3ef 100644 --- a/apps/dashboard/pkg/migration/schemaversion/v16.go +++ b/apps/dashboard/pkg/migration/schemaversion/v16.go @@ -128,22 +128,18 @@ func upgradeToGridLayout(dashboard map[string]interface{}) { continue } - // Set default span (line 1063 in TS) - span := GetFloatValue(panel, "span", defaultPanelSpan) + // Check if panel already has gridPos but no valid span + // If span is missing or zero, and gridPos exists, preserve gridPos dimensions + var panelWidth, panelHeight int + span := GetFloatValue(panel, "span", 0) + existingGridPos, hasGridPos := panel["gridPos"].(map[string]interface{}) - // Handle minSpan conversion (lines 1064-1066 in TS) - if minSpan, hasMinSpan := panel["minSpan"]; hasMinSpan { - if minSpanFloat, ok := ConvertToFloat(minSpan); ok && minSpanFloat > 0 { - panel["minSpan"] = int(math.Min(float64(gridColumnCount), (float64(gridColumnCount)/12.0)*minSpanFloat)) - } - } - - panelWidth := int(math.Floor(span * widthFactor)) - panelHeight := rowGridHeight - if panelHeightValue, hasHeight := panel["height"]; hasHeight { - if h, ok := ConvertToFloat(panelHeightValue); ok { - panelHeight = getGridHeight(h) - } + if hasGridPos && span == 0 { + // Panel already has gridPos but no valid span - preserve its dimensions + panelWidth = GetIntValue(existingGridPos, "w", int(defaultPanelSpan*widthFactor)) + panelHeight = GetIntValue(existingGridPos, "h", rowGridHeight) + } else { + panelWidth, panelHeight = calculatePanelDimensionsFromSpan(span, panel, widthFactor, rowGridHeight) } panelPos := rowArea.getPanelPosition(panelHeight, panelWidth) @@ -315,3 +311,27 @@ func getGridHeight(height float64) int { } return int(math.Ceil(height / panelHeightStep)) } + +func calculatePanelDimensionsFromSpan(span float64, panel map[string]interface{}, widthFactor float64, defaultHeight int) (int, int) { + // Set default span if still 0 + if span == 0 { + span = defaultPanelSpan + } + + if minSpan, hasMinSpan := panel["minSpan"]; hasMinSpan { + if minSpanFloat, ok := ConvertToFloat(minSpan); ok && minSpanFloat > 0 { + panel["minSpan"] = int(math.Min(float64(gridColumnCount), (float64(gridColumnCount)/12.0)*minSpanFloat)) + } + } + + panelWidth := int(math.Floor(span * widthFactor)) + panelHeight := defaultHeight + + if panelHeightValue, hasHeight := panel["height"]; hasHeight { + if h, ok := ConvertToFloat(panelHeightValue); ok { + panelHeight = getGridHeight(h) + } + } + + return panelWidth, panelHeight +} diff --git a/apps/folder/go.mod b/apps/folder/go.mod index 2772136fc3b..6db44655eeb 100644 --- a/apps/folder/go.mod +++ b/apps/folder/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/folder go 1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e k8s.io/apimachinery v0.34.1 k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b @@ -13,7 +13,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect github.com/go-logr/logr v1.4.3 // indirect diff --git a/apps/folder/go.sum b/apps/folder/go.sum index 37e7a4c1159..ab3cbcecbb1 100644 --- a/apps/folder/go.sum +++ b/apps/folder/go.sum @@ -6,8 +6,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= @@ -31,8 +31,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= diff --git a/apps/folder/pkg/apis/folder_manifest.go b/apps/folder/pkg/apis/folder_manifest.go index 37e994e62c6..4fcb76837ed 100644 --- a/apps/folder/pkg/apis/folder_manifest.go +++ b/apps/folder/pkg/apis/folder_manifest.go @@ -18,8 +18,9 @@ import ( ) var appManifestData = app.ManifestData{ - AppName: "folder", - Group: "folder.grafana.app", + AppName: "folder", + Group: "folder.grafana.app", + PreferredVersion: "v1beta1", Versions: []app.ManifestVersion{ { Name: "v1beta1", diff --git a/apps/iam/go.mod b/apps/iam/go.mod index 9339a260a4b..b196d540f3f 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -22,7 +22,7 @@ replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-aler require ( github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana-app-sdk/logging v0.45.0 github.com/grafana/grafana/apps/folder v0.0.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0 @@ -145,7 +145,7 @@ require ( github.com/dolthub/vitess v0.0.0-20250410090211-143e6b272ad4 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elazarl/goproxy v1.7.2 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect @@ -201,7 +201,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.14.2 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grafana/alerting v0.0.0-20250925200825-7a889aa4934d // indirect + github.com/grafana/alerting v0.0.0-20251002001425-eeed80da0165 // indirect github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect diff --git a/apps/iam/go.sum b/apps/iam/go.sum index b9188d6bd5f..5cf6786bea4 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -438,8 +438,8 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY= github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= @@ -721,8 +721,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/alerting v0.0.0-20250925200825-7a889aa4934d h1:zzEty7HgfXbQ/RiBCJFMqaZiJlqiXuz/Zbc6/H6ksuM= -github.com/grafana/alerting v0.0.0-20250925200825-7a889aa4934d/go.mod h1:T5sitas9VhVj8/S9LeRLy6H75kTBdh/sCCqHo7gaQI8= +github.com/grafana/alerting v0.0.0-20251002001425-eeed80da0165 h1:wfehM99Xlpltl9MQx8SITkgFgHmPGqrXoBCVLk/Q6NA= +github.com/grafana/alerting v0.0.0-20251002001425-eeed80da0165/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= @@ -733,8 +733,8 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-aws-sdk v1.2.0 h1:LLR4/g91WBuCRwm2cbWfCREq565+GxIFe08nqqIcIuw= diff --git a/apps/iam/kinds/v0alpha1/teambindingspec.cue b/apps/iam/kinds/v0alpha1/teambindingspec.cue index 3bd130c902d..c20e592e458 100644 --- a/apps/iam/kinds/v0alpha1/teambindingspec.cue +++ b/apps/iam/kinds/v0alpha1/teambindingspec.cue @@ -4,12 +4,13 @@ TeamBindingSpec: { #Subject: { // uid of the identity name: string - // permission of the identity in the team - permission: TeamPermission } - subjects: [...#Subject] + subject: #Subject teamRef: TeamRef + + // permission of the identity in the team + permission: TeamPermission } TeamRef:{ diff --git a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_spec_gen.go index 0e58323533e..13abd605168 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_spec_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_spec_gen.go @@ -6,8 +6,6 @@ package v0alpha1 type TeamBindingspecSubject struct { // uid of the identity Name string `json:"name"` - // permission of the identity in the team - Permission TeamBindingTeamPermission `json:"permission"` } // NewTeamBindingspecSubject creates a new TeamBindingspecSubject object. @@ -15,14 +13,6 @@ func NewTeamBindingspecSubject() *TeamBindingspecSubject { return &TeamBindingspecSubject{} } -// +k8s:openapi-gen=true -type TeamBindingTeamPermission string - -const ( - TeamBindingTeamPermissionAdmin TeamBindingTeamPermission = "admin" - TeamBindingTeamPermissionMember TeamBindingTeamPermission = "member" -) - // +k8s:openapi-gen=true type TeamBindingTeamRef struct { // Name is the unique identifier for a team. @@ -34,16 +24,26 @@ func NewTeamBindingTeamRef() *TeamBindingTeamRef { return &TeamBindingTeamRef{} } +// +k8s:openapi-gen=true +type TeamBindingTeamPermission string + +const ( + TeamBindingTeamPermissionAdmin TeamBindingTeamPermission = "admin" + TeamBindingTeamPermissionMember TeamBindingTeamPermission = "member" +) + // +k8s:openapi-gen=true type TeamBindingSpec struct { - Subjects []TeamBindingspecSubject `json:"subjects"` - TeamRef TeamBindingTeamRef `json:"teamRef"` + Subject TeamBindingspecSubject `json:"subject"` + TeamRef TeamBindingTeamRef `json:"teamRef"` + // permission of the identity in the team + Permission TeamBindingTeamPermission `json:"permission"` } // NewTeamBindingSpec creates a new TeamBindingSpec object. func NewTeamBindingSpec() *TeamBindingSpec { return &TeamBindingSpec{ - Subjects: []TeamBindingspecSubject{}, - TeamRef: *NewTeamBindingTeamRef(), + Subject: *NewTeamBindingspecSubject(), + TeamRef: *NewTeamBindingTeamRef(), } } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go index cb2e31f9144..2532b5df8cc 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go @@ -2103,17 +2103,10 @@ func schema_pkg_apis_iam_v0alpha1_TeamBindingSpec(ref common.ReferenceCallback) SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ - "subjects": { + "subject": { SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingspecSubject"), - }, - }, - }, + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingspecSubject"), }, }, "teamRef": { @@ -2122,8 +2115,16 @@ func schema_pkg_apis_iam_v0alpha1_TeamBindingSpec(ref common.ReferenceCallback) Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingTeamRef"), }, }, + "permission": { + SchemaProps: spec.SchemaProps{ + Description: "permission of the identity in the team", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, }, - Required: []string{"subjects", "teamRef"}, + Required: []string{"subject", "teamRef", "permission"}, }, }, Dependencies: []string{ @@ -2210,16 +2211,8 @@ func schema_pkg_apis_iam_v0alpha1_TeamBindingspecSubject(ref common.ReferenceCal Format: "", }, }, - "permission": { - SchemaProps: spec.SchemaProps{ - Description: "permission of the identity in the team", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, }, - Required: []string{"name", "permission"}, + Required: []string{"name"}, }, }, } diff --git a/apps/iam/pkg/apis/iam_manifest.go b/apps/iam/pkg/apis/iam_manifest.go index fba76fbd7df..24aba9a1fd8 100644 --- a/apps/iam/pkg/apis/iam_manifest.go +++ b/apps/iam/pkg/apis/iam_manifest.go @@ -18,8 +18,9 @@ import ( ) var appManifestData = app.ManifestData{ - AppName: "iam", - Group: "iam.grafana.app", + AppName: "iam", + Group: "iam.grafana.app", + PreferredVersion: "v0alpha1", Versions: []app.ManifestVersion{ { Name: "v0alpha1", diff --git a/apps/iam/pkg/app/app.go b/apps/iam/pkg/app/app.go index c84128e76b4..05216220d7e 100644 --- a/apps/iam/pkg/app/app.go +++ b/apps/iam/pkg/app/app.go @@ -36,7 +36,7 @@ func Provider(appCfg app.SpecificConfig) app.Provider { } func generateInformerSupplier(informerConfig InformerConfig, metrics *reconcilers.ReconcilerMetrics) simple.InformerSupplier { - return func(kind resource.Kind, clients resource.ClientGenerator, options operator.ListWatchOptions) (operator.Informer, error) { + return func(kind resource.Kind, clients resource.ClientGenerator, options operator.InformerOptions) (operator.Informer, error) { client, err := clients.ClientFor(kind) if err != nil { return nil, err @@ -44,9 +44,7 @@ func generateInformerSupplier(informerConfig InformerConfig, metrics *reconciler informer, err := operator.NewKubernetesBasedInformer( kind, client, - operator.KubernetesBasedInformerOptions{ - ListWatchOptions: options, - }, + options, ) if err != nil { return nil, err @@ -92,12 +90,14 @@ func New(cfg app.Config) (app.App, error) { KubeConfig: cfg.KubeConfig, InformerConfig: simple.AppInformerConfig{ InformerSupplier: generateInformerSupplier(appSpecificConfig.InformerConfig, metrics), - ErrorHandler: func(ctx context.Context, err error) { - logging.FromContext(ctx).With("error", err).Error("Informer processing error") - if metrics != nil { - // Use "unknown" for action since top-level informer errors don't have specific actions - metrics.RecordReconcileFailure("unknown", "informer") - } + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(ctx context.Context, err error) { + logging.FromContext(ctx).With("error", err).Error("Informer processing error") + if metrics != nil { + // Use "unknown" for action since top-level informer errors don't have specific actions + metrics.RecordReconcileFailure("unknown", "informer") + } + }, }, }, UnmanagedKinds: []simple.AppUnmanagedKind{ diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index 6cf0836786d..1652941eba8 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/investigations go 1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 k8s.io/apimachinery v0.34.1 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b @@ -15,7 +15,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index 396ebebc9de..dadd72070fd 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -10,8 +10,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -46,8 +46,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= diff --git a/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_client_gen.go b/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_client_gen.go index 5a615356172..dc73a0301e9 100644 --- a/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_client_gen.go +++ b/apps/investigations/pkg/apis/investigations/v0alpha1/investigation_client_gen.go @@ -76,7 +76,7 @@ func (c *InvestigationClient) Patch(ctx context.Context, identifier resource.Ide return c.client.Patch(ctx, identifier, req, opts) } -func (c *InvestigationClient) UpdateStatus(ctx context.Context, newStatus InvestigationStatus, opts resource.UpdateOptions) (*Investigation, error) { +func (c *InvestigationClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus InvestigationStatus, opts resource.UpdateOptions) (*Investigation, error) { return c.client.Update(ctx, &Investigation{ TypeMeta: metav1.TypeMeta{ Kind: InvestigationKind().Kind(), @@ -84,6 +84,8 @@ func (c *InvestigationClient) UpdateStatus(ctx context.Context, newStatus Invest }, ObjectMeta: metav1.ObjectMeta{ ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, }, Status: newStatus, }, resource.UpdateOptions{ diff --git a/apps/investigations/pkg/apis/investigations/v0alpha1/investigationindex_client_gen.go b/apps/investigations/pkg/apis/investigations/v0alpha1/investigationindex_client_gen.go index 3b63abc5d09..573d743b3cf 100644 --- a/apps/investigations/pkg/apis/investigations/v0alpha1/investigationindex_client_gen.go +++ b/apps/investigations/pkg/apis/investigations/v0alpha1/investigationindex_client_gen.go @@ -76,7 +76,7 @@ func (c *InvestigationIndexClient) Patch(ctx context.Context, identifier resourc return c.client.Patch(ctx, identifier, req, opts) } -func (c *InvestigationIndexClient) UpdateStatus(ctx context.Context, newStatus InvestigationIndexStatus, opts resource.UpdateOptions) (*InvestigationIndex, error) { +func (c *InvestigationIndexClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus InvestigationIndexStatus, opts resource.UpdateOptions) (*InvestigationIndex, error) { return c.client.Update(ctx, &InvestigationIndex{ TypeMeta: metav1.TypeMeta{ Kind: InvestigationIndexKind().Kind(), @@ -84,6 +84,8 @@ func (c *InvestigationIndexClient) UpdateStatus(ctx context.Context, newStatus I }, ObjectMeta: metav1.ObjectMeta{ ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, }, Status: newStatus, }, resource.UpdateOptions{ diff --git a/apps/investigations/pkg/apis/investigations_manifest.go b/apps/investigations/pkg/apis/investigations_manifest.go index e4d0dc6fbd9..04c52dbd9ac 100644 --- a/apps/investigations/pkg/apis/investigations_manifest.go +++ b/apps/investigations/pkg/apis/investigations_manifest.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/resource" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kube-openapi/pkg/spec3" v0alpha1 "github.com/grafana/grafana/apps/investigations/pkg/apis/investigations/v0alpha1" ) @@ -27,8 +28,9 @@ var ( ) var appManifestData = app.ManifestData{ - AppName: "investigations", - Group: "investigations.grafana.app", + AppName: "investigations", + Group: "investigations.grafana.app", + PreferredVersion: "v0alpha1", Versions: []app.ManifestVersion{ { Name: "v0alpha1", @@ -50,6 +52,10 @@ var appManifestData = app.ManifestData{ Schema: &versionSchemaInvestigationIndexv0alpha1, }, }, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{}, + Cluster: map[string]spec3.PathProps{}, + }, }, }, } @@ -79,6 +85,7 @@ var customRouteToGoResponseType = map[string]any{} // ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. // kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. // If there is no association for the provided kind, version, custom route path, and method, exists will return false. +// Resource routes (those without a kind) should prefix their route with "/" if the route is namespaced (otherwise the route is assumed to be cluster-scope) func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) { if len(path) > 0 && path[0] == '/' { path = path[1:] @@ -97,8 +104,22 @@ func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goTyp return goType, exists } +var customRouteToGoRequestBodyType = map[string]any{} + +func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + type GoTypeAssociator struct{} +func NewGoTypeAssociator() *GoTypeAssociator { + return &GoTypeAssociator{} +} + func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) { return ManifestGoTypeAssociator(kind, version) } @@ -108,3 +129,6 @@ func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb str func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) { return ManifestCustomRouteQueryAssociator(kind, version, path, verb) } +func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb) +} diff --git a/apps/investigations/pkg/app/investigations_app.go b/apps/investigations/pkg/app/investigations_app.go index 3155eb88404..516d278788d 100644 --- a/apps/investigations/pkg/app/investigations_app.go +++ b/apps/investigations/pkg/app/investigations_app.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/operator" "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana-app-sdk/simple" "k8s.io/apimachinery/pkg/runtime/schema" @@ -18,8 +19,10 @@ func New(cfg app.Config) (app.App, error) { Name: "investigation", KubeConfig: cfg.KubeConfig, InformerConfig: simple.AppInformerConfig{ - ErrorHandler: func(_ context.Context, err error) { - klog.ErrorS(err, "Informer processing error") + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(_ context.Context, err error) { + klog.ErrorS(err, "Informer processing error") + }, }, }, ManagedKinds: []simple.AppManagedKind{ diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index 08cebc839ab..4419785540a 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/playlist go 1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 k8s.io/apimachinery v0.34.1 k8s.io/client-go v0.34.1 k8s.io/klog/v2 v2.130.1 @@ -16,7 +16,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index 396ebebc9de..dadd72070fd 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -10,8 +10,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -46,8 +46,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= diff --git a/apps/playlist/pkg/apis/playlist_manifest.go b/apps/playlist/pkg/apis/playlist_manifest.go index f27cf6dd635..1baede5a7a4 100644 --- a/apps/playlist/pkg/apis/playlist_manifest.go +++ b/apps/playlist/pkg/apis/playlist_manifest.go @@ -25,8 +25,9 @@ var ( ) var appManifestData = app.ManifestData{ - AppName: "playlist", - Group: "playlist.grafana.app", + AppName: "playlist", + Group: "playlist.grafana.app", + PreferredVersion: "v0alpha1", Versions: []app.ManifestVersion{ { Name: "v0alpha1", diff --git a/apps/playlist/pkg/app/app.go b/apps/playlist/pkg/app/app.go index 11cb921dba9..6c9f1762a22 100644 --- a/apps/playlist/pkg/app/app.go +++ b/apps/playlist/pkg/app/app.go @@ -50,8 +50,10 @@ func New(cfg app.Config) (app.App, error) { Name: "playlist", KubeConfig: cfg.KubeConfig, InformerConfig: simple.AppInformerConfig{ - ErrorHandler: func(ctx context.Context, err error) { - klog.ErrorS(err, "Informer processing error") + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(ctx context.Context, err error) { + klog.ErrorS(err, "Informer processing error") + }, }, }, ManagedKinds: []simple.AppManagedKind{ diff --git a/apps/plugins/go.mod b/apps/plugins/go.mod index 1ff6e32794c..35ed57d995b 100644 --- a/apps/plugins/go.mod +++ b/apps/plugins/go.mod @@ -4,7 +4,7 @@ go 1.24.4 require ( github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250428110029-a8ea72012bde k8s.io/apimachinery v0.34.1 k8s.io/apiserver v0.34.1 @@ -19,7 +19,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect diff --git a/apps/plugins/go.sum b/apps/plugins/go.sum index 1b98503c4a5..ca929862af5 100644 --- a/apps/plugins/go.sum +++ b/apps/plugins/go.sum @@ -12,8 +12,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -56,8 +56,8 @@ github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbP github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250428110029-a8ea72012bde h1:ydSrBIOCxJQ84+JU+cyYsOLL40QeXrB7rYfsY/ezU4w= diff --git a/apps/plugins/pkg/apis/plugins_manifest.go b/apps/plugins/pkg/apis/plugins_manifest.go index 2ebfb699452..3965a906139 100644 --- a/apps/plugins/pkg/apis/plugins_manifest.go +++ b/apps/plugins/pkg/apis/plugins_manifest.go @@ -28,8 +28,9 @@ var ( ) var appManifestData = app.ManifestData{ - AppName: "plugins", - Group: "plugins.grafana.app", + AppName: "plugins", + Group: "plugins.grafana.app", + PreferredVersion: "v0alpha1", Versions: []app.ManifestVersion{ { Name: "v0alpha1", diff --git a/apps/plugins/pkg/app/app.go b/apps/plugins/pkg/app/app.go index 99fc20a8d2c..5503126704b 100644 --- a/apps/plugins/pkg/app/app.go +++ b/apps/plugins/pkg/app/app.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/operator" "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana-app-sdk/simple" "k8s.io/apimachinery/pkg/runtime/schema" @@ -26,8 +27,10 @@ func New(cfg app.Config) (app.App, error) { Name: "plugins", KubeConfig: cfg.KubeConfig, InformerConfig: simple.AppInformerConfig{ - ErrorHandler: func(ctx context.Context, err error) { - klog.ErrorS(err, "Informer processing error") + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(ctx context.Context, err error) { + klog.ErrorS(err, "Informer processing error") + }, }, }, ManagedKinds: managedKinds, diff --git a/apps/preferences/go.mod b/apps/preferences/go.mod index 3024a0128df..f21e6a19526 100644 --- a/apps/preferences/go.mod +++ b/apps/preferences/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/preferences go 1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 k8s.io/apimachinery v0.34.1 k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b @@ -13,7 +13,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect github.com/go-logr/logr v1.4.3 // indirect diff --git a/apps/preferences/go.sum b/apps/preferences/go.sum index 8bcea32a115..112d7cb18ab 100644 --- a/apps/preferences/go.sum +++ b/apps/preferences/go.sum @@ -6,8 +6,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= @@ -31,8 +31,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= diff --git a/apps/preferences/pkg/apis/preferences_manifest.go b/apps/preferences/pkg/apis/preferences_manifest.go index 3e9e865df6d..3912bc12866 100644 --- a/apps/preferences/pkg/apis/preferences_manifest.go +++ b/apps/preferences/pkg/apis/preferences_manifest.go @@ -28,8 +28,9 @@ var ( ) var appManifestData = app.ManifestData{ - AppName: "preferences", - Group: "preferences.grafana.app", + AppName: "preferences", + Group: "preferences.grafana.app", + PreferredVersion: "v1alpha1", Versions: []app.ManifestVersion{ { Name: "v1alpha1", diff --git a/apps/provisioning/go.mod b/apps/provisioning/go.mod index 61137e897e5..9a3cd99c066 100644 --- a/apps/provisioning/go.mod +++ b/apps/provisioning/go.mod @@ -25,7 +25,7 @@ require ( github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect github.com/go-jose/go-jose/v4 v4.1.2 // indirect @@ -42,7 +42,7 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect - github.com/grafana/grafana-app-sdk v0.45.0 // indirect + github.com/grafana/grafana-app-sdk v0.46.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect diff --git a/apps/provisioning/go.sum b/apps/provisioning/go.sum index 74542a4123f..93819002c42 100644 --- a/apps/provisioning/go.sum +++ b/apps/provisioning/go.sum @@ -8,8 +8,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= @@ -58,8 +58,8 @@ github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbP github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f h1:f+Z5Xpfp1WNYjUe23ginerWsHWUsRgOWrr3WGu3SlWs= diff --git a/apps/sdk.mk b/apps/sdk.mk index 529fe145648..2b1f6c8b34d 100644 --- a/apps/sdk.mk +++ b/apps/sdk.mk @@ -1,4 +1,4 @@ -APP_SDK_VERSION = v0.45.0 +APP_SDK_VERSION = v0.46.0 APP_SDK_DIR = $(shell go env GOPATH)/bin/app-sdk-$(APP_SDK_VERSION) APP_SDK_BIN = $(APP_SDK_DIR)/grafana-app-sdk diff --git a/apps/secret/go.mod b/apps/secret/go.mod index e6510b74019..1a6b9ecc824 100644 --- a/apps/secret/go.mod +++ b/apps/secret/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/secret go 1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf github.com/stretchr/testify v1.11.1 google.golang.org/grpc v1.75.1 @@ -18,7 +18,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect github.com/go-logr/logr v1.4.3 // indirect diff --git a/apps/secret/go.sum b/apps/secret/go.sum index 1a9beecb06c..712a7a12807 100644 --- a/apps/secret/go.sum +++ b/apps/secret/go.sum @@ -6,8 +6,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= @@ -35,8 +35,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf h1:BBGDHffvVNLoYQlXEpbXcxE0vbpq7pm/8OWF5I+UDZg= diff --git a/apps/secret/pkg/apis/secret/v1beta1/keeper_client_gen.go b/apps/secret/pkg/apis/secret/v1beta1/keeper_client_gen.go new file mode 100644 index 00000000000..c54e3d6f682 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/keeper_client_gen.go @@ -0,0 +1,99 @@ +package v1beta1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type KeeperClient struct { + client *resource.TypedClient[*Keeper, *KeeperList] +} + +func NewKeeperClient(client resource.Client) *KeeperClient { + return &KeeperClient{ + client: resource.NewTypedClient[*Keeper, *KeeperList](client, KeeperKind()), + } +} + +func NewKeeperClientFromGenerator(generator resource.ClientGenerator) (*KeeperClient, error) { + c, err := generator.ClientFor(KeeperKind()) + if err != nil { + return nil, err + } + return NewKeeperClient(c), nil +} + +func (c *KeeperClient) Get(ctx context.Context, identifier resource.Identifier) (*Keeper, error) { + return c.client.Get(ctx, identifier) +} + +func (c *KeeperClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*KeeperList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *KeeperClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*KeeperList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *KeeperClient) Create(ctx context.Context, obj *Keeper, opts resource.CreateOptions) (*Keeper, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = KeeperKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *KeeperClient) Update(ctx context.Context, obj *Keeper, opts resource.UpdateOptions) (*Keeper, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *KeeperClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Keeper, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *KeeperClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus KeeperStatus, opts resource.UpdateOptions) (*Keeper, error) { + return c.client.Update(ctx, &Keeper{ + TypeMeta: metav1.TypeMeta{ + Kind: KeeperKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *KeeperClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/securevalue_client_gen.go b/apps/secret/pkg/apis/secret/v1beta1/securevalue_client_gen.go new file mode 100644 index 00000000000..241d4cac3c2 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/securevalue_client_gen.go @@ -0,0 +1,99 @@ +package v1beta1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type SecureValueClient struct { + client *resource.TypedClient[*SecureValue, *SecureValueList] +} + +func NewSecureValueClient(client resource.Client) *SecureValueClient { + return &SecureValueClient{ + client: resource.NewTypedClient[*SecureValue, *SecureValueList](client, SecureValueKind()), + } +} + +func NewSecureValueClientFromGenerator(generator resource.ClientGenerator) (*SecureValueClient, error) { + c, err := generator.ClientFor(SecureValueKind()) + if err != nil { + return nil, err + } + return NewSecureValueClient(c), nil +} + +func (c *SecureValueClient) Get(ctx context.Context, identifier resource.Identifier) (*SecureValue, error) { + return c.client.Get(ctx, identifier) +} + +func (c *SecureValueClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*SecureValueList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *SecureValueClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*SecureValueList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *SecureValueClient) Create(ctx context.Context, obj *SecureValue, opts resource.CreateOptions) (*SecureValue, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = SecureValueKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *SecureValueClient) Update(ctx context.Context, obj *SecureValue, opts resource.UpdateOptions) (*SecureValue, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *SecureValueClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*SecureValue, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *SecureValueClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus SecureValueStatus, opts resource.UpdateOptions) (*SecureValue, error) { + return c.client.Update(ctx, &SecureValue{ + TypeMeta: metav1.TypeMeta{ + Kind: SecureValueKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *SecureValueClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/secret/pkg/apis/secret_manifest.go b/apps/secret/pkg/apis/secret_manifest.go index 512e381f77d..e83afe68aee 100644 --- a/apps/secret/pkg/apis/secret_manifest.go +++ b/apps/secret/pkg/apis/secret_manifest.go @@ -11,13 +11,16 @@ import ( "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/resource" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kube-openapi/pkg/spec3" v1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" ) var appManifestData = app.ManifestData{ - AppName: "secret", - Group: "secret.grafana.app", + AppName: "secret", + Group: "secret.grafana.app", + PreferredVersion: "v1beta1", Versions: []app.ManifestVersion{ { Name: "v1beta1", @@ -37,6 +40,10 @@ var appManifestData = app.ManifestData{ Conversion: false, }, }, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{}, + Cluster: map[string]spec3.PathProps{}, + }, }, }, } @@ -66,6 +73,7 @@ var customRouteToGoResponseType = map[string]any{} // ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. // kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. // If there is no association for the provided kind, version, custom route path, and method, exists will return false. +// Resource routes (those without a kind) should prefix their route with "/" if the route is namespaced (otherwise the route is assumed to be cluster-scope) func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) { if len(path) > 0 && path[0] == '/' { path = path[1:] @@ -73,3 +81,42 @@ func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (g goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] return goType, exists } + +var customRouteToGoParamsType = map[string]runtime.Object{} + +func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoRequestBodyType = map[string]any{} + +func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +type GoTypeAssociator struct{} + +func NewGoTypeAssociator() *GoTypeAssociator { + return &GoTypeAssociator{} +} + +func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) { + return ManifestGoTypeAssociator(kind, version) +} +func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteResponsesAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) { + return ManifestCustomRouteQueryAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb) +} diff --git a/apps/shorturl/go.mod b/apps/shorturl/go.mod index 3fba5d3439e..7eb91409ca0 100644 --- a/apps/shorturl/go.mod +++ b/apps/shorturl/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/shorturl go 1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.45.0 + github.com/grafana/grafana-app-sdk v0.46.0 github.com/grafana/grafana-app-sdk/logging v0.45.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba k8s.io/apimachinery v0.34.1 @@ -17,7 +17,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect diff --git a/apps/shorturl/go.sum b/apps/shorturl/go.sum index 9f0fe4068f8..06c31439b6e 100644 --- a/apps/shorturl/go.sum +++ b/apps/shorturl/go.sum @@ -10,8 +10,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -54,8 +54,8 @@ github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbP github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba h1:Qam8QzVRsyZN39zgZ9Vj6e8PEfswvv2McnqCZ/v5NcI= diff --git a/apps/shorturl/kinds/shorturl.cue b/apps/shorturl/kinds/shorturl.cue index 13938b56b07..ea234173776 100644 --- a/apps/shorturl/kinds/shorturl.cue +++ b/apps/shorturl/kinds/shorturl.cue @@ -20,6 +20,7 @@ shorturl: { response: { url: string } + responseMetadata: typeMeta: false } } } diff --git a/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_getgoto_types_gen.go b/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_getgoto_response_types_gen.go similarity index 100% rename from apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_getgoto_types_gen.go rename to apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_getgoto_response_types_gen.go diff --git a/apps/shorturl/pkg/apis/shorturl_manifest.go b/apps/shorturl/pkg/apis/shorturl_manifest.go index 5d5b718a681..c4446d924e9 100644 --- a/apps/shorturl/pkg/apis/shorturl_manifest.go +++ b/apps/shorturl/pkg/apis/shorturl_manifest.go @@ -26,8 +26,9 @@ var ( ) var appManifestData = app.ManifestData{ - AppName: "shorturl", - Group: "shorturl.grafana.app", + AppName: "shorturl", + Group: "shorturl.grafana.app", + PreferredVersion: "v1alpha1", Versions: []app.ManifestVersion{ { Name: "v1alpha1", @@ -52,7 +53,7 @@ var appManifestData = app.ManifestData{ Get: &spec3.Operation{ OperationProps: spec3.OperationProps{ - OperationId: "GetGoto", + OperationId: "getGoto", Responses: &spec3.Responses{ ResponsesProps: spec3.ResponsesProps{ diff --git a/apps/shorturl/pkg/app/app.go b/apps/shorturl/pkg/app/app.go index ab9b3477ed2..4c85ef97435 100644 --- a/apps/shorturl/pkg/app/app.go +++ b/apps/shorturl/pkg/app/app.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/k8s" "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-app-sdk/operator" "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana-app-sdk/simple" shorturlv1alpha1 "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1alpha1" @@ -39,8 +40,10 @@ func New(cfg app.Config) (app.App, error) { Name: "shorturl", KubeConfig: cfg.KubeConfig, InformerConfig: simple.AppInformerConfig{ - ErrorHandler: func(ctx context.Context, err error) { - klog.ErrorS(err, "Informer processing error") + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(ctx context.Context, err error) { + klog.ErrorS(err, "Informer processing error") + }, }, }, ManagedKinds: []simple.AppManagedKind{ diff --git a/docs/sources/alerting/alerting-rules/templates/_index.md b/docs/sources/alerting/alerting-rules/templates/_index.md index a2afde85de7..f66781f01e5 100644 --- a/docs/sources/alerting/alerting-rules/templates/_index.md +++ b/docs/sources/alerting/alerting-rules/templates/_index.md @@ -220,6 +220,24 @@ To preview label values, select `Use notification policy`, and then click on `Pr {{< figure src="/media/docs/alerting/alert-instance-routing-preview.png" max-width="1200px" alt="Routing preview displays label values" >}} +## Grafana Cloud AI-generated templates + +Grafana Cloud users can use built-in AI tool to generate templates in the appropriate [alerting template language](/docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/language/) for you. + +To use AI to create your template, follow these steps: + +1. Go to **Alerting -> Contact points**. + +1. Click the Notification Templates tab then, click the **+ Add notification template group** button. + +1. Name your template. + +1. In the Template group section, click the **Generate with AI** button. + +1. Supply the AI tool with a prompt or select from one of the example prompts and edit that if necessary. + +1. Click **Save**. + ## More information For further details on how to template alert rules, refer to: diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md index f1d507bd526..6abdfb93dd4 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md @@ -12,14 +12,14 @@ labels: - cloud - enterprise - oss -menuTitle: Azure AD/Entra ID OAuth -title: Configure Azure AD/Entra ID OAuth authentication +menuTitle: Entra ID OAuth +title: Configure Entra ID OAuth authentication weight: 800 --- -# Configure Azure AD/Entra ID OAuth authentication +# Configure Entra ID OAuth authentication -The Azure AD authentication allows you to use a Microsoft Entra ID (formerly known as Azure Active Directory) tenant as an identity provider for Grafana. You can use Entra ID application roles to assign users and groups to Grafana roles from the Azure Portal. +The Entra ID authentication allows you to use a Microsoft Entra ID (formerly known as Azure Active Directory) tenant as an identity provider for Grafana. You can use Entra ID application roles to assign users and groups to Grafana roles from the Azure Portal. {{< admonition type="note" >}} If Users use the same email address in Microsoft Entra ID that they use with other authentication providers (such as Grafana.com), you need to do additional configuration to ensure that the users are matched correctly. Please refer to [Using the same email address to login with different identity providers](../#using-the-same-email-address-to-login-with-different-identity-providers) for more information. @@ -27,7 +27,7 @@ If Users use the same email address in Microsoft Entra ID that they use with oth ## Create the Microsoft Entra ID application -To enable the Azure AD/Entra ID OAuth, register your application with Entra ID. +To enable the Entra ID OAuth, register your application with Entra ID. 1. Log in to [Azure Portal](https://portal.azure.com), then click **Microsoft Entra ID** in the side menu. @@ -119,7 +119,7 @@ To enable the Azure AD/Entra ID OAuth, register your application with Entra ID. 1. Click **Add user/group** to add a user or group to the Grafana roles. {{< admonition type="note" >}} -When assigning a group to a Grafana role, ensure that users are direct members of the group. Users in nested groups will not have access to Grafana due to limitations within Azure AD/Entra ID side. For more information, see [Microsoft Entra service limits and restrictions](https://learn.microsoft.com/en-us/entra/identity/users/directory-service-limits-restrictions). +When assigning a group to a Grafana role, ensure that users are direct members of the group. Users in nested groups will not have access to Grafana due to limitations within Entra ID side. For more information, see [Microsoft Entra service limits and restrictions](https://learn.microsoft.com/en-us/entra/identity/users/directory-service-limits-restrictions). {{< /admonition >}} ### Configure application roles for Grafana in the Azure Portal @@ -226,9 +226,9 @@ If the setting is set to `false`, the user is assigned the role of `Admin` of th Ensure that you have followed the steps in [Create the Microsoft Entra ID application](#create-the-microsoft-entra-id-application) before you begin. -## Configure Azure AD authentication client using the Grafana UI +## Configure Entra ID authentication client using the Grafana UI -As a Grafana Admin, you can configure your Azure AD/Entra ID OAuth client from within Grafana using the Grafana UI. To do this, navigate to the **Administration > Authentication > Azure AD** page and fill in the form. If you have a current configuration in the Grafana configuration file, the form will be pre-populated with those values. Otherwise the form will contain default values. +As a Grafana Admin, you can configure your Entra ID OAuth client from within Grafana using the Grafana UI. To do this, navigate to the **Administration > Authentication > Azure AD** page and fill in the form. If you have a current configuration in the Grafana configuration file, the form will be pre-populated with those values. Otherwise the form will contain default values. After you have filled in the form, click **Save** to save the configuration. If the save was successful, Grafana will apply the new configurations. @@ -238,7 +238,7 @@ If you need to reset changes you made in the UI back to the default values, clic If you run Grafana in high availability mode, configuration changes may not get applied to all Grafana instances immediately. You may need to wait a few minutes for the configuration to propagate to all Grafana instances. {{< /admonition >}} -## Configure Azure AD authentication client using the Terraform provider +## Configure Entra ID authentication client using the Terraform provider ```terraform resource "grafana_sso_settings" "azuread_sso_settings" { @@ -270,17 +270,17 @@ resource "grafana_sso_settings" "azuread_sso_settings" { Refer to [Terraform Registry](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/sso_settings) for a complete reference on using the `grafana_sso_settings` resource. -## Configure Azure AD authentication client using the Grafana configuration file +## Configure Entra ID authentication client using the Grafana configuration file Ensure that you have access to the [Grafana configuration file](../../../configure-grafana/#configuration-file-location). -### Enable Azure AD OAuth in Grafana +### Enable Entra ID OAuth in Grafana Add the following to the [Grafana configuration file](../../../configure-grafana/#configuration-file-location): ``` [auth.azuread] -name = Azure AD +name = Entra ID enabled = true allow_sign_up = true auto_login = false @@ -321,7 +321,7 @@ When a user logs in using an OAuth provider, Grafana verifies that the access to Grafana uses a refresh token to obtain a new access token without requiring the user to log in again. If a refresh token doesn't exist, Grafana logs the user out of the system after the access token has expired. -Refresh token fetching and access token expiration check is enabled by default for the AzureAD provider since Grafana v10.1.0. If you would like to disable access token expiration check then set the `use_refresh_token` configuration value to `false`. +Refresh token fetching and access token expiration check is enabled by default for the Entra ID provider since Grafana v10.1.0. If you would like to disable access token expiration check then set the `use_refresh_token` configuration value to `false`. {{< admonition type="note" >}} The `accessTokenExpirationCheck` feature toggle has been removed in Grafana v10.3.0 and the `use_refresh_token` configuration value will be used instead for configuring refresh token fetching and access token expiration check. @@ -427,7 +427,7 @@ To learn more, refer to the [Team Sync](https://grafana.com/docs/grafana/::` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | | -| `allow_assign_grafana_admin` | No | No | Set to `true` to automatically sync the Grafana server administrator role. When enabled, if the Azure AD/Entra ID user's App role is `GrafanaAdmin`, Grafana grants the user server administrator privileges and the organization administrator role. If disabled, the user will only receive the organization administrator role. For more details on user role mapping, refer to [Map roles](#map-roles). | `false` | +| `allow_assign_grafana_admin` | No | No | Set to `true` to automatically sync the Grafana server administrator role. When enabled, if the Entra ID user's App role is `GrafanaAdmin`, Grafana grants the user server administrator privileges and the organization administrator role. If disabled, the user will only receive the organization administrator role. For more details on user role mapping, refer to [Map roles](#map-roles). | `false` | | `skip_org_role_sync` | No | Yes | Set to `true` to stop automatically syncing user roles. This will allow you to set organization roles for your users from within Grafana manually. | `false` | -| `allowed_groups` | No | Yes | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure Azure AD/Entra ID to include the `groups` claim following [Configure group membership claims on the Azure Portal](#configure-group-membership-claims-on-the-azure-portal). | | +| `allowed_groups` | No | Yes | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure Entra ID to include the `groups` claim following [Configure group membership claims on the Azure Portal](#configure-group-membership-claims-on-the-azure-portal). | | | `allowed_organizations` | No | Yes | List of comma- or space-separated Azure tenant identifiers. The user should be a member of at least one tenant to log in. | | | `allowed_domains` | No | Yes | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | | -| `domain_hint` | No | Yes | The realm of the user in a federated directory. This skips the email-based discovery process that the user goes through on the Azure AD/Entra ID sign-in page, for a slightly more streamlined user experience. More info [here](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#send-the-sign-in-request). | | +| `domain_hint` | No | Yes | The realm of the user in a federated directory. This skips the email-based discovery process that the user goes through on the Entra ID sign-in page, for a slightly more streamlined user experience. More info [here](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#send-the-sign-in-request). | | | `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` | | `tls_client_cert` | No | No | The path to the certificate. | | diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 15dbecf1ed4..227edc2fd71 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -833,16 +833,6 @@ "count": 13 } }, - "packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, - "packages/grafana-ui/src/components/Slider/Slider.story.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "packages/grafana-ui/src/components/Table/Cells/TableCell.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 3 @@ -1638,11 +1628,6 @@ "count": 8 } }, - "public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/ActiveTimingFields.tsx": { "no-restricted-syntax": { "count": 1 diff --git a/eslint.config.js b/eslint.config.js index 4aa4e15a0f9..a0245a55b48 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -361,7 +361,10 @@ module.exports = [ '**/mock*.{ts,tsx}', ], rules: { - '@grafana/i18n/no-untranslated-strings': ['error', { calleesToIgnore: ['^css$', 'use[A-Z].*'] }], + '@grafana/i18n/no-untranslated-strings': [ + 'error', + { calleesToIgnore: ['^css$', 'use[A-Z].*'], basePaths: ['public/app/features'] }, + ], '@grafana/i18n/no-translation-top-level': 'error', }, }, diff --git a/go.mod b/go.mod index d8769f1b0c7..3be003fd815 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/dolthub/go-mysql-server v0.19.1-0.20250410182021-5632d67cd46e // @grafana/grafana-datasources-core-services github.com/dolthub/vitess v0.0.0-20250410090211-143e6b272ad4 // @grafana/grafana-datasources-core-services github.com/dustin/go-humanize v1.0.1 // @grafana/observability-traces-and-profiling - github.com/emicklei/go-restful/v3 v3.12.2 // @grafana/grafana-app-platform-squad + github.com/emicklei/go-restful/v3 v3.13.0 // @grafana/grafana-app-platform-squad github.com/fatih/color v1.18.0 // @grafana/grafana-backend-group github.com/fullstorydev/grpchan v1.1.1 // @grafana/grafana-backend-group github.com/gchaincl/sqlhooks v1.3.0 // @grafana/grafana-search-and-storage @@ -86,7 +86,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.2 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20250925200825-7a889aa4934d // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20251002001425-eeed80da0165 // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // @grafana/identity-access-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics @@ -96,7 +96,7 @@ require ( github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f // @grafana/sharing-squad github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend - github.com/grafana/grafana-app-sdk v0.45.0 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-app-sdk v0.46.0 // @grafana/grafana-app-platform-squad github.com/grafana/grafana-app-sdk/logging v0.45.0 // @grafana/grafana-app-platform-squad github.com/grafana/grafana-aws-sdk v1.2.0 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 // @grafana/partner-datasources diff --git a/go.sum b/go.sum index 5b9714044a1..4f36a619014 100644 --- a/go.sum +++ b/go.sum @@ -1144,8 +1144,8 @@ github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkg github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY= github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= @@ -1585,8 +1585,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/alerting v0.0.0-20250925200825-7a889aa4934d h1:zzEty7HgfXbQ/RiBCJFMqaZiJlqiXuz/Zbc6/H6ksuM= -github.com/grafana/alerting v0.0.0-20250925200825-7a889aa4934d/go.mod h1:T5sitas9VhVj8/S9LeRLy6H75kTBdh/sCCqHo7gaQI8= +github.com/grafana/alerting v0.0.0-20251002001425-eeed80da0165 h1:wfehM99Xlpltl9MQx8SITkgFgHmPGqrXoBCVLk/Q6NA= +github.com/grafana/alerting v0.0.0-20251002001425-eeed80da0165/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= @@ -1605,8 +1605,8 @@ github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d h1:oXRJlb9UjVsl github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= 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.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9ZGwB2uuOyE= -github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= +github.com/grafana/grafana-app-sdk v0.46.0 h1:gvzQvCQgZJ/73BfAcbDt/6TAMhnVikVPxZt/UwDl+oc= +github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-aws-sdk v1.2.0 h1:LLR4/g91WBuCRwm2cbWfCREq565+GxIFe08nqqIcIuw= diff --git a/go.work.sum b/go.work.sum index 8a3cc4f67d2..e3bc1d10c55 100644 --- a/go.work.sum +++ b/go.work.sum @@ -882,6 +882,7 @@ github.com/elastic/lunes v0.1.0 h1:amRtLPjwkWtzDF/RKzcEPMvSsSseLDLW+bnhfNSLRe4= github.com/elastic/lunes v0.1.0/go.mod h1:xGphYIt3XdZRtyWosHQTErsQTd4OP1p9wsbVoHelrd4= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633 h1:H2pdYOb3KQ1/YsqVWoWNLQO+fusocsw354rqGTZtAgw= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw= github.com/envoyproxy/go-control-plane/envoy v1.32.3/go.mod h1:F6hWupPfh75TBXGKA++MCT/CZHFq5r9/uwt/kQYkZfE= @@ -1057,6 +1058,8 @@ github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2/go.mod h1:w/ github.com/grafana/cog v0.0.37/go.mod h1:UDstzYqMdgIROmbfkHL8fB9XWQO2lnf5z+4W/eJo4Dc= github.com/grafana/cog v0.0.38 h1:V7gRRn/mh7Bg1ptrCxo0bv6K0SnG9TiDZk+3Ppftn6s= github.com/grafana/cog v0.0.38/go.mod h1:UDstzYqMdgIROmbfkHL8fB9XWQO2lnf5z+4W/eJo4Dc= +github.com/grafana/cog v0.0.40/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38= +github.com/grafana/cog v0.0.41/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38= github.com/grafana/dskit v0.0.0-20250818234656-8ff9c6532e85/go.mod h1:kImsvJ1xnmeT9Z6StK+RdEKLzlpzBsKwJbEQfmBJdFs= github.com/grafana/go-gelf/v2 v2.0.1 h1:BOChP0h/jLeD+7F9mL7tq10xVkDG15he3T1zHuQaWak= github.com/grafana/go-gelf/v2 v2.0.1/go.mod h1:lexHie0xzYGwCgiRGcvZ723bSNyNI8ZRD4s0CLobh90= diff --git a/packages/grafana-alerting/src/grafana/api/util.test.ts b/packages/grafana-alerting/src/grafana/api/util.test.ts index 215cba04187..f8278bbdc72 100644 --- a/packages/grafana-alerting/src/grafana/api/util.test.ts +++ b/packages/grafana-alerting/src/grafana/api/util.test.ts @@ -1,6 +1,6 @@ import { config } from '@grafana/runtime'; -import { getAPIBaseURL, getAPINamespace, getAPIReducerPath } from './util'; +import { base64UrlEncode, getAPIBaseURL, getAPINamespace, getAPIReducerPath } from './util'; describe('API utilities', () => { const originalAppSubUrl = config.appSubUrl; @@ -64,4 +64,72 @@ describe('API utilities', () => { expect(result).toBe('notifications.alerting.grafana.app/v0alpha1'); }); }); + + describe('base64UrlEncode', () => { + it('should encode simple ASCII strings', () => { + expect(base64UrlEncode('hello')).toBe('aGVsbG8'); + }); + + it('should encode strings with special characters', () => { + expect(base64UrlEncode('hello world!')).toBe('aGVsbG8gd29ybGQh'); + }); + + it('should handle emoji characters correctly', () => { + // Single emoji + expect(base64UrlEncode('⛳')).toBe('4puz'); + // Multi-byte emoji + expect(base64UrlEncode('🧀')).toBe('8J-ngA'); + // Emoji with variant selector + expect(base64UrlEncode('❤️')).toBe('4p2k77iP'); + }); + + it('should handle mixed ASCII and Unicode characters', () => { + const input = 'hello⛳❤️🧀'; + const encoded = base64UrlEncode(input); + expect(encoded).toBe('aGVsbG_im7PinaTvuI_wn6eA'); + }); + + it('should convert to base64url format (no padding)', () => { + // Standard base64 would have padding with '=' + const result = base64UrlEncode('test'); + expect(result).not.toContain('='); + }); + + it('should replace + with - and / with _', () => { + // String that produces both + and / in standard base64 + const input = 'a??b'; // produces 'YT8/Yg==' in base64, which has / + const input2 = 'a?>b'; // produces 'YT8+Yg==' in base64, which has + + const encoded = base64UrlEncode(input); + const encoded2 = base64UrlEncode(input2); + expect(encoded).not.toContain('+'); + expect(encoded).not.toContain('/'); + expect(encoded2).not.toContain('+'); + expect(encoded2).not.toContain('/'); + expect(encoded).toContain('_'); // Should have _ instead of / + expect(encoded2).toContain('-'); // Should have - instead of + + }); + + it('should handle empty strings', () => { + expect(base64UrlEncode('')).toBe(''); + }); + + it('should handle contact point names with special characters', () => { + expect(base64UrlEncode('my-contact-point')).toBe('bXktY29udGFjdC1wb2ludA'); + expect(base64UrlEncode('Contact Point 🔔')).toBe('Q29udGFjdCBQb2ludCDwn5SU'); + }); + + it('should throw error for malformed UTF-16 strings with lone surrogates', () => { + // String with lone high surrogate + const malformedString = 'hello\uDE75'; + expect(() => base64UrlEncode(malformedString)).toThrow( + 'Cannot encode malformed UTF-16 string with lone surrogates' + ); + }); + + it('should handle well-formed strings with proper surrogate pairs', () => { + // Proper surrogate pair for emoji (U+1F9C0) + const wellFormedString = 'hello\uD83E\uDDC0'; + expect(() => base64UrlEncode(wellFormedString)).not.toThrow(); + }); + }); }); diff --git a/packages/grafana-alerting/src/grafana/api/util.ts b/packages/grafana-alerting/src/grafana/api/util.ts index a562e151366..cc7787716a4 100644 --- a/packages/grafana-alerting/src/grafana/api/util.ts +++ b/packages/grafana-alerting/src/grafana/api/util.ts @@ -13,3 +13,39 @@ export const getAPIBaseURL = (group: string, version: string) => { // By including the version in the reducer path we can prevent cache bugs when different versions of the API are used for the same entities export const getAPIReducerPath = (group: string, version: string) => `${group}/${version}` as const; + +/** + * Check if a string is well-formed UTF-16 (no lone surrogates). + * encodeURIComponent() throws an error for lone surrogates + */ +export const isWellFormed = (str: string): boolean => { + try { + encodeURIComponent(str); + return true; + } catch (error) { + return false; + } +}; + +/** + * Base64URL encode a string using native browser APIs. + * Handles Unicode characters correctly by using TextEncoder. + * Converts standard base64 to base64url by replacing + with -, / with _, and removing padding. + * @throws Error if the input string contains lone surrogates (malformed UTF-16) + */ +export const base64UrlEncode = (value: string): string => { + // Check if the string is well-formed UTF-16 + if (!isWellFormed(value)) { + throw new Error(`Cannot encode malformed UTF-16 string with lone surrogates: ${value}`); + } + + // Encode UTF-8 string to bytes + const bytes = new TextEncoder().encode(value); + + // Convert bytes to base64 + const binString = String.fromCodePoint(...bytes); + const base64 = btoa(binString); + + // Convert to base64url format + return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +}; diff --git a/packages/grafana-alerting/src/index.ts b/packages/grafana-alerting/src/index.ts index 288d2c776fd..e7e8f4b22d2 100644 --- a/packages/grafana-alerting/src/index.ts +++ b/packages/grafana-alerting/src/index.ts @@ -11,5 +11,8 @@ export { AlertLabels } from './grafana/rules/components/labels/AlertLabels'; export { AlertLabel } from './grafana/rules/components/labels/AlertLabel'; // keep label utils internal to the app for now +// Utilities +export { base64UrlEncode } from './grafana/api/util'; + // This is a dummy export so typescript doesn't error importing an "empty module" export const index = {}; diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 6f61a738fc0..3ba2c0b50bc 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1206,4 +1206,9 @@ export interface FeatureToggles { * @default false */ cdnPluginsLoadFirst?: boolean; + /** + * Enable loading plugins via declarative URLs + * @default false + */ + cdnPluginsUrls?: boolean; } diff --git a/packages/grafana-i18n/src/eslint/README.md b/packages/grafana-i18n/src/eslint/README.md index 4fb1abdf8f6..12b3a559f9b 100644 --- a/packages/grafana-i18n/src/eslint/README.md +++ b/packages/grafana-i18n/src/eslint/README.md @@ -10,6 +10,30 @@ Check if strings are marked for translation inside JSX Elements, in certain JSX ### Options +#### `basePaths` + +Allows specifying base paths that should be stripped when generating i18n keys. Defaults to `['src']`. + +#### Example + +```tsx +// For a file located at public/app/features/search/EmptyState.tsx + +// Specifying basePaths: +// { +// '@grafana/i18n/no-untranslated-strings': ['error', { basePaths: ['public/app/features'] }], +// } + +No results found + +// Without basePaths: +// { +// '@grafana/i18n/no-untranslated-strings': ['error'], +// } + +No results found +``` + #### `forceFix` Allows specifying directories that, if the file is present within, then the rule will automatically fix the errors. This is primarily a workaround to allow for automatic mark up of new violations as the rule evolves. diff --git a/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs b/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs index 7d741720bc2..2b058426d53 100644 --- a/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs +++ b/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs @@ -2,8 +2,8 @@ /** @typedef {import('@typescript-eslint/utils').TSESTree.Node} Node */ /** @typedef {import('@typescript-eslint/utils').TSESTree.JSXElement} JSXElement */ /** @typedef {import('@typescript-eslint/utils').TSESTree.JSXFragment} JSXFragment */ -/** @typedef {import('@typescript-eslint/utils').TSESLint.RuleModule<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{ forceFix: string[] , calleesToIgnore: string[] }]>} RuleDefinition */ -/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{forceFix: string[], calleesToIgnore: string[]}]>} RuleContextWithOptions */ +/** @typedef {import('@typescript-eslint/utils').TSESLint.RuleModule<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{ forceFix: string[] , calleesToIgnore: string[], basePaths: string[] }]>} RuleDefinition */ +/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{forceFix: string[], calleesToIgnore: string[], basePaths: string[]}]>} RuleContextWithOptions */ const { getNodeValue, @@ -301,12 +301,19 @@ const noUntranslatedStrings = createRule({ }, default: [], }, + basePaths: { + type: 'array', + items: { + type: 'string', + }, + default: ['src'], + }, }, additionalProperties: false, }, ], }, - defaultOptions: [{ forceFix: [], calleesToIgnore: [] }], + defaultOptions: [{ forceFix: [], calleesToIgnore: [], basePaths: ['src'] }], }); module.exports = noUntranslatedStrings; diff --git a/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.test.js b/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.test.js index 9913ab10f73..684823ae152 100644 --- a/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.test.js +++ b/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.test.js @@ -2,7 +2,7 @@ import { RuleTester } from 'eslint'; import noUntranslatedStrings from './no-untranslated-strings.cjs'; -const filename = 'public/app/features/some-feature/nested/SomeFile.tsx'; +const filename = 'src/some-feature/nested/SomeFile.tsx'; const packageName = '@grafana/i18n'; @@ -797,7 +797,7 @@ const Foo = () => { name: 'Auto fixes when options are configured', code: `const Foo = () =>
test
`, filename, - options: [{ forceFix: ['public/app/features/some-feature'] }], + options: [{ forceFix: ['src/some-feature'] }], output: `${TRANS_IMPORT} const Foo = () =>
test
`, errors: [ @@ -821,7 +821,7 @@ const Foo = () => { return
}`, filename, - options: [{ forceFix: ['public/app/features/some-feature'] }], + options: [{ forceFix: ['src/some-feature'] }], output: ` ${T_IMPORT} const Foo = () => { @@ -853,7 +853,94 @@ const Foo = () => { } }`, filename, - options: [{ forceFix: ['public/app/features/some-feature'] }], + options: [{ forceFix: ['src/some-feature'] }], + output: ` +${T_IMPORT} +const Foo = () => { + return { + label: t("some-feature.foo.label.test", "test"), + } +}`, + errors: [ + { + messageId: 'noUntranslatedStringsProperties', + suggestions: [ + { + messageId: 'wrapWithT', + output: ` +${T_IMPORT} +const Foo = () => { + return { + label: t("some-feature.foo.label.test", "test"), + } +}`, + }, + ], + }, + ], + }, + + { + name: 'Auto fixes when options are configured for a different basePath', + code: `const Foo = () =>
test
`, + filename: 'public/app/features/some-feature/nested/SomeFile.tsx', + options: [{ forceFix: ['public/app/features/some-feature'], basePaths: ['public/app/features'] }], + output: `${TRANS_IMPORT} +const Foo = () =>
test
`, + errors: [ + { + messageId: 'noUntranslatedStrings', + suggestions: [ + { + messageId: 'wrapWithTrans', + output: `${TRANS_IMPORT} +const Foo = () =>
test
`, + }, + ], + }, + ], + }, + + { + name: 'Auto fixes when options are configured for a different basePath - prop', + code: ` +const Foo = () => { + return
+}`, + filename: 'public/app/features/some-feature/nested/SomeFile.tsx', + options: [{ forceFix: ['public/app/features/some-feature'], basePaths: ['public/app/features'] }], + output: ` +${T_IMPORT} +const Foo = () => { + return
+}`, + errors: [ + { + messageId: 'noUntranslatedStringsProp', + suggestions: [ + { + messageId: 'wrapWithT', + output: ` +${T_IMPORT} +const Foo = () => { + return
+}`, + }, + ], + }, + ], + }, + + { + name: 'Auto fixes object property for a different basePath', + code: ` +const Foo = () => { + return { + label: 'test', + } +}`, + filename: 'public/app/features/some-feature/nested/SomeFile.tsx', + options: [{ forceFix: ['public/app/features/some-feature'], basePaths: ['public/app/features'] }], output: ` ${T_IMPORT} const Foo = () => { diff --git a/packages/grafana-i18n/src/eslint/no-untranslated-strings/translation-utils.cjs b/packages/grafana-i18n/src/eslint/no-untranslated-strings/translation-utils.cjs index 070e0c5ee53..8d7d4456ace 100644 --- a/packages/grafana-i18n/src/eslint/no-untranslated-strings/translation-utils.cjs +++ b/packages/grafana-i18n/src/eslint/no-untranslated-strings/translation-utils.cjs @@ -7,7 +7,7 @@ /** @typedef {import('@typescript-eslint/utils').TSESTree.JSXChild} JSXChild */ /** @typedef {import('@typescript-eslint/utils').TSESTree.Property} Property */ /** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleFixer} RuleFixer */ -/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT', [{forceFix: string[]}]>} RuleContextWithOptions */ +/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT', [{forceFix: string[], calleesToIgnore: string[], basePaths: string[]}]>} RuleContextWithOptions */ const { AST_NODE_TYPES } = require('@typescript-eslint/utils'); /** @@ -150,9 +150,12 @@ function getTDeclaration(node, context) { */ function getTranslationPrefix(context) { const filename = context.filename; - const match = filename.match(/public\/app\/features\/(.+?)\//); - if (match) { - return match[1]; + const basePaths = context.options[0]?.basePaths ?? ['src']; + for (const path of basePaths) { + const match = filename.match(new RegExp(`${path}/(.+?)/`)); + if (match) { + return match[1]; + } } return null; } diff --git a/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx b/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx index d00648bdfbe..eccb916483a 100644 --- a/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx +++ b/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx @@ -9,8 +9,6 @@ const meta: Meta = { controls: { exclude: ['tooltipAlwaysVisible'], }, - // TODO fix a11y issue in story and remove this - a11y: { test: 'off' }, }, argTypes: { orientation: { control: { type: 'select', options: ['horizontal', 'vertical'] } }, diff --git a/packages/grafana-ui/src/components/Slider/RangeSlider.tsx b/packages/grafana-ui/src/components/Slider/RangeSlider.tsx index 34b9d403085..794ea5b795f 100644 --- a/packages/grafana-ui/src/components/Slider/RangeSlider.tsx +++ b/packages/grafana-ui/src/components/Slider/RangeSlider.tsx @@ -3,6 +3,8 @@ import { Global } from '@emotion/react'; import Slider, { SliderProps } from 'rc-slider'; import { useCallback } from 'react'; +import { t } from '@grafana/i18n'; + import { useStyles2 } from '../../themes/ThemeContext'; import HandleTooltip from './HandleTooltip'; @@ -44,6 +46,7 @@ export const RangeSlider = ({ const isHorizontal = orientation === 'horizontal'; const styles = useStyles2(getStyles, isHorizontal); + const dragHandleAriaLabel = t('grafana-ui.range-slider.drag-handle-aria-label', 'Use arrow keys to change the value'); const tipHandleRender: SliderProps['handleRender'] = (node, handleProps) => { return ( @@ -73,6 +76,7 @@ export const RangeSlider = ({ vertical={!isHorizontal} reverse={reverse} handleRender={tipHandleRender} + ariaLabelForHandle={dragHandleAriaLabel} />
); diff --git a/packages/grafana-ui/src/components/Slider/Slider.story.tsx b/packages/grafana-ui/src/components/Slider/Slider.story.tsx index 770038dbc64..207a33ec5fe 100644 --- a/packages/grafana-ui/src/components/Slider/Slider.story.tsx +++ b/packages/grafana-ui/src/components/Slider/Slider.story.tsx @@ -1,4 +1,7 @@ import { StoryFn, Meta } from '@storybook/react'; +import { useId } from 'react'; + +import { Field } from '../Forms/Field'; import { Slider } from './Slider'; @@ -12,8 +15,6 @@ const meta: Meta = { knobs: { disabled: true, }, - // TODO fix a11y issue in story and remove this - a11y: { test: 'off' }, }, argTypes: { orientation: { control: { type: 'select', options: ['horizontal', 'vertical'] } }, @@ -31,17 +32,25 @@ const meta: Meta = { }; export const Basic: StoryFn = (args) => { + const id = useId(); + return (
- + + +
); }; export const WithMarks: StoryFn = (args) => { + const id = useId(); + return (
- + + +
); }; diff --git a/packages/grafana-ui/src/components/Slider/Slider.test.tsx b/packages/grafana-ui/src/components/Slider/Slider.test.tsx index 2723a180b3c..e24ccd7e751 100644 --- a/packages/grafana-ui/src/components/Slider/Slider.test.tsx +++ b/packages/grafana-ui/src/components/Slider/Slider.test.tsx @@ -7,6 +7,7 @@ import { SliderProps } from './types'; const sliderProps: SliderProps = { min: 10, max: 20, + inputId: 'slider-test', }; describe('Slider', () => { diff --git a/packages/grafana-ui/src/components/Slider/Slider.tsx b/packages/grafana-ui/src/components/Slider/Slider.tsx index c677d402dc9..e8c377070bf 100644 --- a/packages/grafana-ui/src/components/Slider/Slider.tsx +++ b/packages/grafana-ui/src/components/Slider/Slider.tsx @@ -3,6 +3,8 @@ import { Global } from '@emotion/react'; import SliderComponent from 'rc-slider'; import { useState, useCallback, ChangeEvent, FocusEvent } from 'react'; +import { t } from '@grafana/i18n'; + import { useStyles2 } from '../../themes/ThemeContext'; import { Input } from '../Input/Input'; @@ -24,11 +26,14 @@ export const Slider = ({ ariaLabelForHandle, marks, included, + inputId, }: SliderProps) => { const isHorizontal = orientation === 'horizontal'; const styles = useStyles2(getStyles, isHorizontal, Boolean(marks)); const SliderWithTooltip = SliderComponent; const [sliderValue, setSliderValue] = useState(value ?? min); + const dragHandleAriaLabel = + ariaLabelForHandle ?? t('grafana-ui.slider.drag-handle-aria-label', 'Use arrow keys to change the value'); const onSliderChange = useCallback( (v: number | number[]) => { @@ -102,7 +107,7 @@ export const Slider = ({ onChangeComplete={handleChangeComplete} vertical={!isHorizontal} reverse={reverse} - ariaLabelForHandle={ariaLabelForHandle} + ariaLabelForHandle={dragHandleAriaLabel} marks={marks} included={included} /> @@ -116,6 +121,7 @@ export const Slider = ({ onBlur={onSliderInputBlur} min={min} max={max} + id={inputId} />
diff --git a/packages/grafana-ui/src/components/Slider/types.ts b/packages/grafana-ui/src/components/Slider/types.ts index 2551ac84937..812a8bed7d7 100644 --- a/packages/grafana-ui/src/components/Slider/types.ts +++ b/packages/grafana-ui/src/components/Slider/types.ts @@ -21,6 +21,7 @@ export interface SliderProps extends CommonSliderProps { onAfterChange?: (value?: number) => void; formatTooltipResult?: (value: number) => number; ariaLabelForHandle?: string; + inputId: string; } export interface RangeSliderProps extends CommonSliderProps { diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index f7a3cdc5c93..543ace09235 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/aggregator go 1.24.6 require ( - github.com/emicklei/go-restful/v3 v3.12.2 + github.com/emicklei/go-restful/v3 v3.13.0 github.com/grafana/grafana-plugin-sdk-go v0.279.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e github.com/grafana/grafana/pkg/semconv v0.0.0-20250514132646-acbc7b54ed9e diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index f294a97f777..2f6756ed544 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -46,8 +46,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= diff --git a/pkg/api/user.go b/pkg/api/user.go index 3d84e2cffd6..311f80e49ae 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -117,6 +117,7 @@ func (hs *HTTPServer) GetUserByLoginOrEmail(c *contextmodel.ReqContext) response } result := user.UserProfileDTO{ ID: usr.ID, + UID: usr.UID, Name: usr.Name, Email: usr.Email, Login: usr.Login, diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index e084fdf1fad..fe9637801e2 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -17,7 +17,7 @@ require github.com/go-jose/go-jose/v4 v4.1.2 require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 2cd16740dd0..11948a2d6ef 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -2,8 +2,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index c039fb3b21c..a6e649494bb 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -28,7 +28,7 @@ require ( github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-jose/go-jose/v4 v4.1.2 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index a28bdfada90..1025c416476 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -19,8 +19,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= diff --git a/pkg/codegen/go.mod b/pkg/codegen/go.mod index 4b1e087e044..91b8f538f49 100644 --- a/pkg/codegen/go.mod +++ b/pkg/codegen/go.mod @@ -6,7 +6,7 @@ require ( cuelang.org/go v0.11.1 github.com/dave/dst v0.27.3 github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d - github.com/grafana/cog v0.0.40 + github.com/grafana/cog v0.0.41 github.com/grafana/cuetsy v0.1.11 github.com/matryer/is v1.4.1 golang.org/x/tools v0.37.0 diff --git a/pkg/codegen/go.sum b/pkg/codegen/go.sum index 8b4652fb9a0..5fd86180488 100644 --- a/pkg/codegen/go.sum +++ b/pkg/codegen/go.sum @@ -31,8 +31,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk= github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= -github.com/grafana/cog v0.0.40 h1:rPNqOZBV2jXKpi6nJCY5VaoSM4BMSxkN7yuskV4lFxM= -github.com/grafana/cog v0.0.40/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38= +github.com/grafana/cog v0.0.41 h1:wszX7YmFkohvLgDy7VDU2XDFNghTdKFvz54zhq9Z+zU= +github.com/grafana/cog v0.0.41/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38= github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ= github.com/grafana/cue v0.0.0-20230926092038-971951014e3f/go.mod h1:okjJBHFQFer+a41sAe2SaGm1glWS8oEb6CmJvn5Zdws= github.com/grafana/cuetsy v0.1.11 h1:I3IwBhF+UaQxRM79HnImtrAn8REGdb5M3+C4QrYHoWk= diff --git a/pkg/plugins/codegen/go.mod b/pkg/plugins/codegen/go.mod index ace49eacb59..6f7a75648fe 100644 --- a/pkg/plugins/codegen/go.mod +++ b/pkg/plugins/codegen/go.mod @@ -7,7 +7,7 @@ replace github.com/grafana/grafana/pkg/codegen => ../../codegen require ( cuelang.org/go v0.11.1 github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d - github.com/grafana/cog v0.0.40 + github.com/grafana/cog v0.0.41 github.com/grafana/cuetsy v0.1.11 github.com/grafana/grafana/pkg/codegen v0.0.0-20250514132646-acbc7b54ed9e ) diff --git a/pkg/plugins/codegen/go.sum b/pkg/plugins/codegen/go.sum index 7418051cd4f..c8fff7e7441 100644 --- a/pkg/plugins/codegen/go.sum +++ b/pkg/plugins/codegen/go.sum @@ -30,8 +30,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk= github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= -github.com/grafana/cog v0.0.40 h1:rPNqOZBV2jXKpi6nJCY5VaoSM4BMSxkN7yuskV4lFxM= -github.com/grafana/cog v0.0.40/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38= +github.com/grafana/cog v0.0.41 h1:wszX7YmFkohvLgDy7VDU2XDFNghTdKFvz54zhq9Z+zU= +github.com/grafana/cog v0.0.41/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38= github.com/grafana/cuetsy v0.1.11 h1:I3IwBhF+UaQxRM79HnImtrAn8REGdb5M3+C4QrYHoWk= github.com/grafana/cuetsy v0.1.11/go.mod h1:Ix97+CPD8ws9oSSxR3/Lf4ahU1I4Np83kjJmDVnLZvc= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index dc5c76c2f29..6b046cd9a75 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -33,7 +33,7 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/elazarl/goproxy v1.7.2 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 2b4eab7619c..7c97cf52cdc 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -57,8 +57,8 @@ github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= diff --git a/pkg/registry/apis/iam/legacy/team.go b/pkg/registry/apis/iam/legacy/team.go index 0ad11584a7f..6f49707d5d0 100644 --- a/pkg/registry/apis/iam/legacy/team.go +++ b/pkg/registry/apis/iam/legacy/team.go @@ -421,7 +421,7 @@ type ListTeamBindingsQuery struct { } type ListTeamBindingsResult struct { - Bindings []TeamBinding + Bindings []TeamMember Continue int64 RV int64 } @@ -445,11 +445,6 @@ func (m TeamMember) MemberID() string { return claims.NewTypeID(claims.TypeUser, m.UserUID) } -type TeamBinding struct { - TeamUID string - Members []TeamMember -} - var sqlQueryTeamBindingsTemplate = mustTemplate("team_bindings_query.sql") type listTeamBindingsQuery struct { @@ -505,11 +500,11 @@ func (s *legacySQLStore) ListTeamBindings(ctx context.Context, ns claims.Namespa return nil, err } - res := &ListTeamBindingsResult{} - grouped := map[string][]TeamMember{} + res := &ListTeamBindingsResult{ + Bindings: make([]TeamMember, 0, int(query.Pagination.Limit)), + } var lastID int64 - var atTeamLimit bool for rows.Next() { m := TeamMember{} @@ -518,16 +513,11 @@ func (s *legacySQLStore) ListTeamBindings(ctx context.Context, ns claims.Namespa return res, err } - lastID = m.TeamID - members, ok := grouped[m.TeamUID] - if ok { - grouped[m.TeamUID] = append(members, m) - } else if !atTeamLimit { - grouped[m.TeamUID] = []TeamMember{m} - } + res.Bindings = append(res.Bindings, m) - if len(grouped) >= int(query.Pagination.Limit)-1 { - atTeamLimit = true + lastID = m.ID + + if len(res.Bindings) >= int(query.Pagination.Limit)-1 { res.Continue = lastID } } @@ -536,14 +526,6 @@ func (s *legacySQLStore) ListTeamBindings(ctx context.Context, ns claims.Namespa res.RV, err = sql.GetResourceVersion(ctx, "team_member", "updated") } - res.Bindings = make([]TeamBinding, 0, len(grouped)) - for uid, members := range grouped { - res.Bindings = append(res.Bindings, TeamBinding{ - TeamUID: uid, - Members: members, - }) - } - return res, err } diff --git a/pkg/registry/apis/iam/legacy/team_bindings_query.sql b/pkg/registry/apis/iam/legacy/team_bindings_query.sql index fb8930c9fde..a64687a7e8f 100644 --- a/pkg/registry/apis/iam/legacy/team_bindings_query.sql +++ b/pkg/registry/apis/iam/legacy/team_bindings_query.sql @@ -3,18 +3,13 @@ FROM {{ .Ident .TeamMemberTable }} tm INNER JOIN {{ .Ident .TeamTable }} t ON tm.team_id = t.id INNER JOIN {{ .Ident .UserTable }} u ON tm.user_id = u.id WHERE -{{ if .Query.UID }} - t.uid = {{ .Arg .Query.UID }} -{{ else }} - t.uid IN( - SELECT uid - FROM {{ .Ident .TeamTable }} t - {{ if .Query.Pagination.Continue }} - WHERE t.id >= {{ .Arg .Query.Pagination.Continue }} - {{ end }} - ORDER BY t.id ASC LIMIT {{ .Arg .Query.Pagination.Limit }} - ) -{{ end }} -AND tm.org_id = {{ .Arg .Query.OrgID}} + tm.org_id = {{ .Arg .Query.OrgID}} + {{ if .Query.UID }} + AND t.uid = {{ .Arg .Query.UID }} + {{ end }} + {{- if .Query.Pagination.Continue }} + AND tm.id >= {{ .Arg .Query.Pagination.Continue }} + {{- end }} AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT {{ .Arg .Query.Pagination.Limit }}; diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_1_bindings.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_1_bindings.sql index 4dc43b4f936..1b3c18c20cc 100755 --- a/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_1_bindings.sql +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_1_bindings.sql @@ -3,7 +3,8 @@ FROM `grafana`.`team_member` tm INNER JOIN `grafana`.`team` t ON tm.team_id = t.id INNER JOIN `grafana`.`user` u ON tm.user_id = u.id WHERE - t.uid = 'team-1' -AND tm.org_id = 1 + tm.org_id = 1 + AND t.uid = 'team-1' AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_bindings_page_1.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_bindings_page_1.sql index be3d212eb9e..2180553e3a0 100755 --- a/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_bindings_page_1.sql +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_bindings_page_1.sql @@ -3,11 +3,7 @@ FROM `grafana`.`team_member` tm INNER JOIN `grafana`.`team` t ON tm.team_id = t.id INNER JOIN `grafana`.`user` u ON tm.user_id = u.id WHERE - t.uid IN( - SELECT uid - FROM `grafana`.`team` t - ORDER BY t.id ASC LIMIT 5 - ) -AND tm.org_id = 1 + tm.org_id = 1 AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT 5; diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_bindings_page_2.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_bindings_page_2.sql index ecbef2e0c01..767f1fe4c82 100755 --- a/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_bindings_page_2.sql +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--team_bindings_query-team_bindings_page_2.sql @@ -3,12 +3,8 @@ FROM `grafana`.`team_member` tm INNER JOIN `grafana`.`team` t ON tm.team_id = t.id INNER JOIN `grafana`.`user` u ON tm.user_id = u.id WHERE - t.uid IN( - SELECT uid - FROM `grafana`.`team` t - WHERE t.id >= 2 - ORDER BY t.id ASC LIMIT 1 - ) -AND tm.org_id = 1 + tm.org_id = 1 + AND tm.id >= 2 AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_1_bindings.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_1_bindings.sql index 82d10b5fd51..50e968de4a1 100755 --- a/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_1_bindings.sql +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_1_bindings.sql @@ -3,7 +3,8 @@ FROM "grafana"."team_member" tm INNER JOIN "grafana"."team" t ON tm.team_id = t.id INNER JOIN "grafana"."user" u ON tm.user_id = u.id WHERE - t.uid = 'team-1' -AND tm.org_id = 1 + tm.org_id = 1 + AND t.uid = 'team-1' AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_bindings_page_1.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_bindings_page_1.sql index 77e1182cde8..76f9e5a8cca 100755 --- a/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_bindings_page_1.sql +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_bindings_page_1.sql @@ -3,11 +3,7 @@ FROM "grafana"."team_member" tm INNER JOIN "grafana"."team" t ON tm.team_id = t.id INNER JOIN "grafana"."user" u ON tm.user_id = u.id WHERE - t.uid IN( - SELECT uid - FROM "grafana"."team" t - ORDER BY t.id ASC LIMIT 5 - ) -AND tm.org_id = 1 + tm.org_id = 1 AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT 5; diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_bindings_page_2.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_bindings_page_2.sql index 2cec052d9da..c6608dd7968 100755 --- a/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_bindings_page_2.sql +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--team_bindings_query-team_bindings_page_2.sql @@ -3,12 +3,8 @@ FROM "grafana"."team_member" tm INNER JOIN "grafana"."team" t ON tm.team_id = t.id INNER JOIN "grafana"."user" u ON tm.user_id = u.id WHERE - t.uid IN( - SELECT uid - FROM "grafana"."team" t - WHERE t.id >= 2 - ORDER BY t.id ASC LIMIT 1 - ) -AND tm.org_id = 1 + tm.org_id = 1 + AND tm.id >= 2 AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_1_bindings.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_1_bindings.sql index 82d10b5fd51..50e968de4a1 100755 --- a/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_1_bindings.sql +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_1_bindings.sql @@ -3,7 +3,8 @@ FROM "grafana"."team_member" tm INNER JOIN "grafana"."team" t ON tm.team_id = t.id INNER JOIN "grafana"."user" u ON tm.user_id = u.id WHERE - t.uid = 'team-1' -AND tm.org_id = 1 + tm.org_id = 1 + AND t.uid = 'team-1' AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_bindings_page_1.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_bindings_page_1.sql index 77e1182cde8..76f9e5a8cca 100755 --- a/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_bindings_page_1.sql +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_bindings_page_1.sql @@ -3,11 +3,7 @@ FROM "grafana"."team_member" tm INNER JOIN "grafana"."team" t ON tm.team_id = t.id INNER JOIN "grafana"."user" u ON tm.user_id = u.id WHERE - t.uid IN( - SELECT uid - FROM "grafana"."team" t - ORDER BY t.id ASC LIMIT 5 - ) -AND tm.org_id = 1 + tm.org_id = 1 AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT 5; diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_bindings_page_2.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_bindings_page_2.sql index 2cec052d9da..c6608dd7968 100755 --- a/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_bindings_page_2.sql +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--team_bindings_query-team_bindings_page_2.sql @@ -3,12 +3,8 @@ FROM "grafana"."team_member" tm INNER JOIN "grafana"."team" t ON tm.team_id = t.id INNER JOIN "grafana"."user" u ON tm.user_id = u.id WHERE - t.uid IN( - SELECT uid - FROM "grafana"."team" t - WHERE t.id >= 2 - ORDER BY t.id ASC LIMIT 1 - ) -AND tm.org_id = 1 + tm.org_id = 1 + AND tm.id >= 2 AND NOT tm.external -ORDER BY t.id ASC; +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/iam/team/store_binding.go b/pkg/registry/apis/iam/team/store_binding.go index cdb61777039..38b2a23237f 100644 --- a/pkg/registry/apis/iam/team/store_binding.go +++ b/pkg/registry/apis/iam/team/store_binding.go @@ -117,46 +117,36 @@ func (l *LegacyBindingStore) List(ctx context.Context, options *internalversion. return &list, nil } -func mapToBindingObject(ns claims.NamespaceInfo, b legacy.TeamBinding) iamv0alpha1.TeamBinding { +func mapToBindingObject(ns claims.NamespaceInfo, tm legacy.TeamMember) iamv0alpha1.TeamBinding { rv := time.Time{} ct := time.Now() - for _, m := range b.Members { - if m.Updated.After(rv) { - rv = m.Updated - } - if m.Created.Before(ct) { - ct = m.Created - } + if tm.Updated.After(rv) { + rv = tm.Updated + } + if tm.Created.Before(ct) { + ct = tm.Created } return iamv0alpha1.TeamBinding{ ObjectMeta: metav1.ObjectMeta{ - Name: b.TeamUID, + Name: tm.TeamUID, Namespace: ns.Value, ResourceVersion: strconv.FormatInt(rv.UnixMilli(), 10), CreationTimestamp: metav1.NewTime(ct), }, Spec: iamv0alpha1.TeamBindingSpec{ TeamRef: iamv0alpha1.TeamBindingTeamRef{ - Name: b.TeamUID, + Name: tm.TeamUID, }, - Subjects: mapToSubjects(b.Members), + Subject: iamv0alpha1.TeamBindingspecSubject{ + Name: tm.UserUID, + }, + Permission: common.MapTeamPermission(tm.Permission), }, } } -func mapToSubjects(members []legacy.TeamMember) []iamv0alpha1.TeamBindingspecSubject { - out := make([]iamv0alpha1.TeamBindingspecSubject, 0, len(members)) - for _, m := range members { - out = append(out, iamv0alpha1.TeamBindingspecSubject{ - Name: m.UserUID, - Permission: common.MapTeamPermission(m.Permission), - }) - } - return out -} - func mapPermisson(p team.PermissionType) iamv0.TeamPermission { if p == team.PermissionTypeAdmin { return iamv0.TeamPermissionAdmin diff --git a/pkg/registry/apis/provisioning/controller/health.go b/pkg/registry/apis/provisioning/controller/health.go index 5e5953b4117..ed5f19c6ed0 100644 --- a/pkg/registry/apis/provisioning/controller/health.go +++ b/pkg/registry/apis/provisioning/controller/health.go @@ -12,6 +12,13 @@ import ( "github.com/prometheus/client_golang/prometheus" ) +const ( + // recentHealthyDuration defines how recent a health check must be to be considered "recent" when healthy + recentHealthyDuration = 5 * time.Minute + // recentHealthyDuration defines how recent a health check must be to be considered "recent" when unhealthy + recentUnhealthyDuration = 1 * time.Minute +) + // StatusPatcher defines the interface for updating repository status // //go:generate mockery --name=StatusPatcher @@ -60,9 +67,9 @@ func (hc *HealthChecker) hasRecentHealthCheck(healthStatus provisioning.HealthSt age := time.Since(time.UnixMilli(healthStatus.Checked)) if healthStatus.Healthy { - return age <= time.Minute*5 // Recent if checked within 5 minutes when healthy + return age <= recentHealthyDuration } - return age <= time.Minute // Recent if checked within 1 minute when unhealthy + return age <= recentUnhealthyDuration // Recent if checked within 1 minute when unhealthy } // HasRecentFailure checks if there's a recent failure of a specific type @@ -72,7 +79,7 @@ func (hc *HealthChecker) HasRecentFailure(healthStatus provisioning.HealthStatus } age := time.Since(time.UnixMilli(healthStatus.Checked)) - return age <= time.Minute // Recent if within 1 minute + return age <= recentUnhealthyDuration } // RecordFailureAndUpdate records a failure and updates the repository status @@ -111,7 +118,11 @@ func (hc *HealthChecker) hasHealthStatusChanged(old, new provisioning.HealthStat return true } - if old.Checked != new.Checked { + recent := recentUnhealthyDuration + if new.Healthy { + recent = recentHealthyDuration + } + if time.UnixMilli(new.Checked).Sub(time.UnixMilli(old.Checked)) > recent { return true } diff --git a/pkg/registry/apis/provisioning/controller/health_test.go b/pkg/registry/apis/provisioning/controller/health_test.go index 722500d5a94..91a0a67a131 100644 --- a/pkg/registry/apis/provisioning/controller/health_test.go +++ b/pkg/registry/apis/provisioning/controller/health_test.go @@ -400,6 +400,26 @@ func TestRefreshHealth(t *testing.T) { expectedHealth: true, expectPatch: false, }, + { + name: "no status change - no patch needed for unhealthy repo (recent check)", + testResult: &provisioning.TestResults{ + Success: false, + Code: 500, + Errors: []provisioning.ErrorDetails{ + {Detail: "connection failed"}, + {Detail: "timeout"}, + }, + }, + testError: nil, + existingStatus: provisioning.HealthStatus{ + Healthy: false, + Checked: time.Now().Add(-15 * time.Second).UnixMilli(), + Message: []string{"connection failed", "timeout"}, + }, + expectError: false, + expectedHealth: false, + expectPatch: false, + }, { name: "status unchanged but timestamp needs update (old check)", testResult: &provisioning.TestResults{ @@ -415,6 +435,26 @@ func TestRefreshHealth(t *testing.T) { expectedHealth: true, expectPatch: true, }, + { + name: "status unchanged but timestamp needs update (old unhealthy check)", + testResult: &provisioning.TestResults{ + Success: false, + Code: 500, + Errors: []provisioning.ErrorDetails{ + {Detail: "connection failed"}, + {Detail: "timeout"}, + }, + }, + testError: nil, + existingStatus: provisioning.HealthStatus{ + Healthy: false, + Checked: time.Now().Add(-2 * time.Minute).UnixMilli(), + Message: []string{"connection failed", "timeout"}, + }, + expectError: false, + expectedHealth: false, + expectPatch: true, + }, { name: "patch error", testResult: &provisioning.TestResults{ diff --git a/pkg/registry/apis/provisioning/jobs/sync/worker.go b/pkg/registry/apis/provisioning/jobs/sync/worker.go index 0b0139941c0..09ed5859cc7 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/worker.go +++ b/pkg/registry/apis/provisioning/jobs/sync/worker.go @@ -110,6 +110,10 @@ func (r *SyncWorker) Process(ctx context.Context, repo repository.Repository, jo lastRef := repo.Config().Status.Sync.LastRef syncStatus.LastRef = lastRef + if syncStatus.State == "" { + syncStatus.State = provisioning.JobStateWorking + } + // Update sync status at start using JSON patch patchOperations := []map[string]interface{}{ { diff --git a/pkg/registry/apis/secret/contracts/encryption.go b/pkg/registry/apis/secret/contracts/encryption.go index f24fe73a544..73a4952cac2 100644 --- a/pkg/registry/apis/secret/contracts/encryption.go +++ b/pkg/registry/apis/secret/contracts/encryption.go @@ -1,6 +1,10 @@ package contracts -import "context" +import ( + "context" + + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" +) // EncryptionManager is an envelope encryption service in charge of encrypting/decrypting secrets. type EncryptionManager interface { @@ -8,17 +12,23 @@ type EncryptionManager interface { // For those specific use cases where the encryption operation cannot be moved outside // the database transaction, look at database-specific methods present at the specific // implementation present at manager.EncryptionService. - Encrypt(ctx context.Context, namespace string, payload []byte) ([]byte, error) - Decrypt(ctx context.Context, namespace string, payload []byte) ([]byte, error) + Encrypt(ctx context.Context, namespace xkube.Namespace, payload []byte) (EncryptedPayload, error) + Decrypt(ctx context.Context, namespace xkube.Namespace, payload EncryptedPayload) ([]byte, error) +} + +type EncryptedPayload struct { + DataKeyID string + EncryptedData []byte } type EncryptedValue struct { - Namespace string - Name string - Version int64 - EncryptedData []byte - Created int64 - Updated int64 + EncryptedPayload + + Namespace string + Name string + Version int64 + Created int64 + Updated int64 } // ListOpts defines pagination options for listing encrypted values. @@ -28,10 +38,10 @@ type ListOpts struct { } type EncryptedValueStorage interface { - Create(ctx context.Context, namespace, name string, version int64, encryptedData []byte) (*EncryptedValue, error) - Update(ctx context.Context, namespace, name string, version int64, encryptedData []byte) error - Get(ctx context.Context, namespace, name string, version int64) (*EncryptedValue, error) - Delete(ctx context.Context, namespace, name string, version int64) error + Create(ctx context.Context, namespace xkube.Namespace, name string, version int64, encryptedData EncryptedPayload) (*EncryptedValue, error) + Update(ctx context.Context, namespace xkube.Namespace, name string, version int64, encryptedData EncryptedPayload) error + Get(ctx context.Context, namespace xkube.Namespace, name string, version int64) (*EncryptedValue, error) + Delete(ctx context.Context, namespace xkube.Namespace, name string, version int64) error } type GlobalEncryptedValueStorage interface { @@ -39,6 +49,10 @@ type GlobalEncryptedValueStorage interface { CountAll(ctx context.Context, untilTime *int64) (int64, error) } +type EncryptedValueMigrationExecutor interface { + Execute(ctx context.Context) (int, error) +} + type ConsolidationService interface { Consolidate(ctx context.Context) error } diff --git a/pkg/registry/apis/secret/contracts/keeper.go b/pkg/registry/apis/secret/contracts/keeper.go index 5558a4db3b5..a204bb5b1a7 100644 --- a/pkg/registry/apis/secret/contracts/keeper.go +++ b/pkg/registry/apis/secret/contracts/keeper.go @@ -96,10 +96,10 @@ func (s ExternalID) String() string { // Keeper is the interface for secret keepers. type Keeper interface { - Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) (ExternalID, error) - Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) error - Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) (secretv1beta1.ExposedSecureValue, error) - Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) error + Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) (ExternalID, error) + Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) error + Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) (secretv1beta1.ExposedSecureValue, error) + Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) error } // Service is the interface for secret keeper services. diff --git a/pkg/registry/apis/secret/encryption/manager/manager.go b/pkg/registry/apis/secret/encryption/manager/manager.go index f48b350c1bd..024c8aca05a 100644 --- a/pkg/registry/apis/secret/encryption/manager/manager.go +++ b/pkg/registry/apis/secret/encryption/manager/manager.go @@ -1,10 +1,8 @@ package manager import ( - "bytes" "context" "crypto/rand" - "encoding/base64" "errors" "fmt" "strconv" @@ -20,13 +18,10 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/util" ) -const ( - keyIdDelimiter = '#' -) - type EncryptionManager struct { tracer trace.Tracer store contracts.DataKeyStorage @@ -99,12 +94,9 @@ func (s *EncryptionManager) registerUsageMetrics() { }) } -// TODO: Why do we need to use a global variable for this? -var b64 = base64.RawStdEncoding - -func (s *EncryptionManager) Encrypt(ctx context.Context, namespace string, payload []byte) ([]byte, error) { +func (s *EncryptionManager) Encrypt(ctx context.Context, namespace xkube.Namespace, payload []byte) (contracts.EncryptedPayload, error) { ctx, span := s.tracer.Start(ctx, "EnvelopeEncryptionManager.Encrypt", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), )) defer span.End() @@ -128,34 +120,30 @@ func (s *EncryptionManager) Encrypt(ctx context.Context, namespace string, paylo id, dataKey, err = s.currentDataKey(ctx, namespace, label) if err != nil { s.log.Error("Failed to get current data key", "error", err, "label", label) - return nil, err + return contracts.EncryptedPayload{}, err } var encrypted []byte encrypted, err = s.cipher.Encrypt(ctx, payload, string(dataKey)) if err != nil { s.log.Error("Failed to encrypt secret", "error", err) - return nil, err + return contracts.EncryptedPayload{}, err } - prefix := make([]byte, b64.EncodedLen(len(id))+2) - b64.Encode(prefix[1:], []byte(id)) - prefix[0] = keyIdDelimiter - prefix[len(prefix)-1] = keyIdDelimiter + encryptedPayload := contracts.EncryptedPayload{ + DataKeyID: id, + EncryptedData: encrypted, + } - blob := make([]byte, len(prefix)+len(encrypted)) - copy(blob, prefix) - copy(blob[len(prefix):], encrypted) - - return blob, nil + return encryptedPayload, nil } // currentDataKey looks up for current data key in cache or database by name, and decrypts it. // If there's no current data key in cache nor in database it generates a new random data key, // and stores it into both the in-memory cache and database (encrypted by the encryption provider). -func (s *EncryptionManager) currentDataKey(ctx context.Context, namespace string, label string) (string, []byte, error) { +func (s *EncryptionManager) currentDataKey(ctx context.Context, namespace xkube.Namespace, label string) (string, []byte, error) { ctx, span := s.tracer.Start(ctx, "EnvelopeEncryptionManager.CurrentDataKey", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("label", label), )) defer span.End() @@ -166,14 +154,14 @@ func (s *EncryptionManager) currentDataKey(ctx context.Context, namespace string defer s.mtx.Unlock() // We try to fetch the data key, either from cache or database - id, dataKey, err := s.dataKeyByLabel(ctx, namespace, label) + id, dataKey, err := s.dataKeyByLabel(ctx, namespace.String(), label) if err != nil { return "", nil, err } // If no existing data key was found, create a new one if dataKey == nil { - id, dataKey, err = s.newDataKey(ctx, namespace, label) + id, dataKey, err = s.newDataKey(ctx, namespace.String(), label) if err != nil { return "", nil, err } @@ -264,9 +252,9 @@ func newRandomDataKey() ([]byte, error) { return rawDataKey, nil } -func (s *EncryptionManager) Decrypt(ctx context.Context, namespace string, payload []byte) ([]byte, error) { +func (s *EncryptionManager) Decrypt(ctx context.Context, namespace xkube.Namespace, payload contracts.EncryptedPayload) ([]byte, error) { ctx, span := s.tracer.Start(ctx, "EnvelopeEncryptionManager.Decrypt", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), )) defer span.End() @@ -285,50 +273,28 @@ func (s *EncryptionManager) Decrypt(ctx context.Context, namespace string, paylo } }() - if len(payload) == 0 { + if len(payload.EncryptedData) == 0 { err = fmt.Errorf("unable to decrypt empty payload") return nil, err } - payload = payload[1:] - endOfKey := bytes.Index(payload, []byte{keyIdDelimiter}) - if endOfKey == -1 { - err = fmt.Errorf("could not find valid key id in encrypted payload") - return nil, err - } - b64Key := payload[:endOfKey] - payload = payload[endOfKey+1:] - keyId := make([]byte, b64.DecodedLen(len(b64Key))) - _, err = b64.Decode(keyId, b64Key) - if err != nil { + if payload.DataKeyID == "" { + err = fmt.Errorf("unable to decrypt empty data key id") return nil, err } - dataKey, err := s.dataKeyById(ctx, namespace, string(keyId)) + dataKey, err := s.dataKeyById(ctx, namespace.String(), payload.DataKeyID) if err != nil { - s.log.FromContext(ctx).Error("Failed to lookup data key by id", "id", string(keyId), "error", err) + s.log.FromContext(ctx).Error("Failed to lookup data key by id", "id", payload.DataKeyID, "error", err) return nil, err } var decrypted []byte - decrypted, err = s.cipher.Decrypt(ctx, payload, string(dataKey)) + decrypted, err = s.cipher.Decrypt(ctx, payload.EncryptedData, string(dataKey)) return decrypted, err } -func (s *EncryptionManager) GetDecryptedValue(ctx context.Context, namespace string, sjd map[string][]byte, key, fallback string) string { - if value, ok := sjd[key]; ok { - decryptedData, err := s.Decrypt(ctx, namespace, value) - if err != nil { - return fallback - } - - return string(decryptedData) - } - - return fallback -} - // dataKeyById looks up for data key in the database and returns it decrypted. func (s *EncryptionManager) dataKeyById(ctx context.Context, namespace, id string) ([]byte, error) { ctx, span := s.tracer.Start(ctx, "EnvelopeEncryptionManager.GetDataKey", trace.WithAttributes( diff --git a/pkg/registry/apis/secret/encryption/manager/manager_test.go b/pkg/registry/apis/secret/encryption/manager/manager_test.go index a6c1c06bcfa..dc63fb461c9 100644 --- a/pkg/registry/apis/secret/encryption/manager/manager_test.go +++ b/pkg/registry/apis/secret/encryption/manager/manager_test.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service" osskmsproviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/secret/database" @@ -34,7 +35,7 @@ func TestMain(m *testing.M) { func TestEncryptionService_EnvelopeEncryption(t *testing.T) { svc := setupTestService(t) ctx := context.Background() - namespace := "test-namespace" + namespace := xkube.Namespace("test-namespace") t.Run("encrypting should create DEK", func(t *testing.T) { plaintext := []byte("very secret string") @@ -46,7 +47,7 @@ func TestEncryptionService_EnvelopeEncryption(t *testing.T) { require.NoError(t, err) assert.Equal(t, plaintext, decrypted) - keys, err := svc.store.ListDataKeys(ctx, namespace) + keys, err := svc.store.ListDataKeys(ctx, namespace.String()) require.NoError(t, err) assert.Equal(t, len(keys), 1) }) @@ -61,7 +62,7 @@ func TestEncryptionService_EnvelopeEncryption(t *testing.T) { require.NoError(t, err) assert.Equal(t, plaintext, decrypted) - keys, err := svc.store.ListDataKeys(ctx, namespace) + keys, err := svc.store.ListDataKeys(ctx, namespace.String()) require.NoError(t, err) assert.Equal(t, len(keys), 1) }) @@ -212,7 +213,7 @@ func TestEncryptionService_UseCurrentProvider(t *testing.T) { } encryptionManager.providerConfig.CurrentProvider = encryption.ProviderID("fakeProvider.v1") - namespace := "test-namespace" + namespace := xkube.Namespace("test-namespace") encrypted, _ := encryptionManager.Encrypt(context.Background(), namespace, []byte{}) assert.True(t, fake.encryptCalled) assert.False(t, fake.decryptCalled) @@ -241,7 +242,7 @@ func TestEncryptionService_UseCurrentProvider(t *testing.T) { func TestEncryptionService_SecretKeyVersionUpgrade(t *testing.T) { ctx := context.Background() - namespace := "test-namespace" + namespace := xkube.Namespace("test-namespace") // Generate random keys for testing oldKey := util.GenerateShortUID() + util.GenerateShortUID() // 32 chars @@ -416,16 +417,30 @@ func (p *fakeProvider) Decrypt(_ context.Context, _ []byte) ([]byte, error) { func TestEncryptionService_Decrypt(t *testing.T) { ctx := context.Background() - namespace := "test-namespace" + namespace := xkube.Namespace("test-namespace") t.Run("empty payload should fail", func(t *testing.T) { svc := setupTestService(t) - _, err := svc.Decrypt(context.Background(), namespace, []byte("")) + _, err := svc.Decrypt(context.Background(), namespace, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte(""), + }) require.Error(t, err) assert.Equal(t, "unable to decrypt empty payload", err.Error()) }) + t.Run("empty data key id should fail", func(t *testing.T) { + svc := setupTestService(t) + _, err := svc.Decrypt(context.Background(), namespace, contracts.EncryptedPayload{ + DataKeyID: "", + EncryptedData: []byte("some payload"), + }) + require.Error(t, err) + + assert.Equal(t, "unable to decrypt empty data key id", err.Error()) + }) + t.Run("ee encrypted payload with ee enabled should work", func(t *testing.T) { svc := setupTestService(t) ciphertext, err := svc.Encrypt(ctx, namespace, []byte("grafana")) @@ -442,7 +457,7 @@ func TestIntegration_SecretsService(t *testing.T) { ctx := context.Background() someData := []byte(`some-data`) - namespace := "test-namespace" + namespace := xkube.Namespace("test-namespace") tcs := map[string]func(*testing.T, db.DB, contracts.EncryptionManager){ "regular": func(t *testing.T, _ db.DB, svc contracts.EncryptionManager) { @@ -562,7 +577,7 @@ func TestIntegration_SecretsService(t *testing.T) { require.NoError(t, err) ctx := context.Background() - namespace := "test-namespace" + namespace := xkube.Namespace("test-namespace") // Here's what actually matters and varies on each test: look at the test case name. // diff --git a/pkg/registry/apis/secret/garbagecollectionworker/worker.go b/pkg/registry/apis/secret/garbagecollectionworker/worker.go index 4754f06c02d..e10bbed600e 100644 --- a/pkg/registry/apis/secret/garbagecollectionworker/worker.go +++ b/pkg/registry/apis/secret/garbagecollectionworker/worker.go @@ -104,7 +104,7 @@ func (w *Worker) Cleanup(ctx context.Context, sv *secretv1beta1.SecureValue) err } // Keeper deletion is idempotent - if err := keeper.Delete(ctx, keeperCfg, sv.Namespace, sv.Name, sv.Status.Version); err != nil { + if err := keeper.Delete(ctx, keeperCfg, xkube.Namespace(sv.Namespace), sv.Name, sv.Status.Version); err != nil { return fmt.Errorf("deleting secure value from keeper: %w", err) } diff --git a/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go b/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go index 344d2bcf605..23467d69b39 100644 --- a/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go +++ b/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go @@ -9,6 +9,7 @@ import ( secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/storage/secret/encryption" "github.com/mitchellh/copystructure" "github.com/stretchr/testify/require" @@ -58,7 +59,7 @@ func TestBasic(t *testing.T) { require.NoError(t, err) // Get the secret value once to make sure it's reachable - exposedValue, err := keeper.Expose(t.Context(), keeperCfg, sv.Namespace, sv.Name, sv.Status.Version) + exposedValue, err := keeper.Expose(t.Context(), keeperCfg, xkube.Namespace(sv.Namespace), sv.Name, sv.Status.Version) require.NoError(t, err) require.NotEmpty(t, exposedValue.DangerouslyExposeAndConsumeValue()) @@ -78,7 +79,7 @@ func TestBasic(t *testing.T) { require.Empty(t, svs) // Try to get the secreet value again to make sure it's been deleted from the keeper - exposedValue, err = keeper.Expose(t.Context(), keeperCfg, sv.Namespace, sv.Name, sv.Status.Version) + exposedValue, err = keeper.Expose(t.Context(), keeperCfg, xkube.Namespace(sv.Namespace), sv.Name, sv.Status.Version) require.ErrorIs(t, err, encryption.ErrEncryptedValueNotFound) require.Empty(t, exposedValue) }) diff --git a/pkg/registry/apis/secret/secretkeeper/secretkeeper.go b/pkg/registry/apis/secret/secretkeeper/secretkeeper.go index fc65573ac61..dc51d5b3160 100644 --- a/pkg/registry/apis/secret/secretkeeper/secretkeeper.go +++ b/pkg/registry/apis/secret/secretkeeper/secretkeeper.go @@ -1,9 +1,12 @@ package secretkeeper import ( + "fmt" + "go.opentelemetry.io/otel/trace" secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper" "github.com/prometheus/client_golang/prometheus" @@ -20,11 +23,17 @@ func ProvideService( tracer trace.Tracer, store contracts.EncryptedValueStorage, encryptionManager contracts.EncryptionManager, + migrationExecutor contracts.EncryptedValueMigrationExecutor, reg prometheus.Registerer, + _ *secret.DependencyRegisterer, // noop import so wire runs DB migrations before instantiating this service -- can be nil when manually instantiating ) (*OSSKeeperService, error) { + systemKeeper, err := sqlkeeper.NewSQLKeeper(tracer, encryptionManager, store, migrationExecutor, reg) + if err != nil { + return nil, fmt.Errorf("failed to create system keeper: %w", err) + } + return &OSSKeeperService{ - // TODO: rename to system keeper or something like that - systemKeeper: sqlkeeper.NewSQLKeeper(tracer, encryptionManager, store, reg), + systemKeeper: systemKeeper, }, nil } diff --git a/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go b/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go index 84baaf207b0..43193befcea 100644 --- a/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go +++ b/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go @@ -12,6 +12,7 @@ import ( osskmsproviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper" + "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/secret/database" @@ -65,7 +66,8 @@ func setupTestService(t *testing.T, cfg *setting.Cfg) (*OSSKeeperService, error) require.NoError(t, err) // Initialize the keeper service - keeperService, err := ProvideService(tracer, encValueStore, encryptionManager, nil) + keeperService, err := ProvideService(tracer, encValueStore, encryptionManager, &testutils.NoopMigrationExecutor{}, nil, nil) + require.NoError(t, err) return keeperService, err } diff --git a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go index bfe3972f72b..a6f54d5674a 100644 --- a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go +++ b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go @@ -5,9 +5,11 @@ import ( "fmt" "time" + "github.com/grafana/grafana-app-sdk/logging" secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/metrics" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -26,20 +28,32 @@ func NewSQLKeeper( tracer trace.Tracer, encryptionManager contracts.EncryptionManager, store contracts.EncryptedValueStorage, + migrationExecutor contracts.EncryptedValueMigrationExecutor, reg prometheus.Registerer, -) *SQLKeeper { +) (*SQLKeeper, error) { + // Run the encrypted value store migration before anything else, otherwise operations may fail + // TODO: This does not need to be here forever, but we may currently have on-prem deployments using GSM, so it needs to be here for now. + // Periodically assess whether it is safe to remove - most likely for G13 should be fine. + log := logging.FromContext(context.Background()) + log.Debug("sqlkeeper: executing encrypted value store migration") + rowsAffected, err := migrationExecutor.Execute(context.Background()) + log.Debug("sqlkeeper: encrypted value store migration completed", "rows_affected", rowsAffected) + if err != nil { + return nil, fmt.Errorf("error encountered during encrypted value store migration: %w", err) + } + return &SQLKeeper{ tracer: tracer, encryptionManager: encryptionManager, store: store, metrics: metrics.NewKeeperMetrics(reg), - } + }, nil } -func (s *SQLKeeper) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) (contracts.ExternalID, error) { +func (s *SQLKeeper) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) (contracts.ExternalID, error) { ctx, span := s.tracer.Start(ctx, "SQLKeeper.Store", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version)), ) @@ -63,9 +77,9 @@ func (s *SQLKeeper) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, n return contracts.ExternalID(""), nil } -func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) (secretv1beta1.ExposedSecureValue, error) { +func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) (secretv1beta1.ExposedSecureValue, error) { ctx, span := s.tracer.Start(ctx, "SQLKeeper.Expose", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version), )) @@ -77,7 +91,7 @@ func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, return "", fmt.Errorf("unable to get encrypted value: %w", err) } - exposedBytes, err := s.encryptionManager.Decrypt(ctx, namespace, encryptedValue.EncryptedData) + exposedBytes, err := s.encryptionManager.Decrypt(ctx, namespace, encryptedValue.EncryptedPayload) if err != nil { return "", fmt.Errorf("unable to decrypt value: %w", err) } @@ -88,9 +102,9 @@ func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, return exposedValue, nil } -func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) error { +func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) error { ctx, span := s.tracer.Start(ctx, "SQLKeeper.Delete", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version), )) @@ -107,9 +121,9 @@ func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, return nil } -func (s *SQLKeeper) Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) error { +func (s *SQLKeeper) Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) error { ctx, span := s.tracer.Start(ctx, "SQLKeeper.Update", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version), )) diff --git a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go index 1333cf894c7..db5139c50ff 100644 --- a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go +++ b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go @@ -8,6 +8,7 @@ import ( secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/tests/testsuite" ) @@ -16,10 +17,10 @@ func TestMain(m *testing.M) { } func Test_SQLKeeperSetup(t *testing.T) { - namespace1 := "namespace1" + namespace1 := xkube.Namespace("namespace1") name1 := "name1" version1 := int64(1) - namespace2 := "namespace2" + namespace2 := xkube.Namespace("namespace2") name2 := "name2" plaintext1 := "very secret string in namespace 1" plaintext2 := "very secret string in namespace 2" diff --git a/pkg/registry/apis/secret/service/consolidation.go b/pkg/registry/apis/secret/service/consolidation.go index b0baea659c4..bbe7d9084e6 100644 --- a/pkg/registry/apis/secret/service/consolidation.go +++ b/pkg/registry/apis/secret/service/consolidation.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" otelcodes "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" ) @@ -60,21 +61,21 @@ func (s *ConsolidationService) Consolidate(ctx context.Context) (err error) { for _, ev := range encryptedValues { // Decrypt the value using its old data key. - decryptedValue, err := s.encryptionManager.Decrypt(ctx, ev.Namespace, ev.EncryptedData) + decryptedValue, err := s.encryptionManager.Decrypt(ctx, xkube.Namespace(ev.Namespace), ev.EncryptedPayload) if err != nil { logging.FromContext(ctx).Error("Failed to decrypt value", "namespace", ev.Namespace, "name", ev.Name, "error", err) continue } // Re-encrypt the value using a new data key. - reEncryptedValue, err := s.encryptionManager.Encrypt(ctx, ev.Namespace, decryptedValue) + reEncryptedValue, err := s.encryptionManager.Encrypt(ctx, xkube.Namespace(ev.Namespace), decryptedValue) if err != nil { logging.FromContext(ctx).Error("Failed to re-encrypt value", "namespace", ev.Namespace, "name", ev.Name, "error", err) continue } // Update the encrypted value in the store. - err = s.encryptedValueStore.Update(ctx, ev.Namespace, ev.Name, ev.Version, reEncryptedValue) + err = s.encryptedValueStore.Update(ctx, xkube.Namespace(ev.Namespace), ev.Name, ev.Version, reEncryptedValue) if err != nil { logging.FromContext(ctx).Error("Failed to update encrypted value", "namespace", ev.Namespace, "name", ev.Name, "error", err) continue diff --git a/pkg/registry/apis/secret/service/consolidation_test.go b/pkg/registry/apis/secret/service/consolidation_test.go index 638ced19a81..381cf956436 100644 --- a/pkg/registry/apis/secret/service/consolidation_test.go +++ b/pkg/registry/apis/secret/service/consolidation_test.go @@ -97,7 +97,7 @@ func TestConsolidation(t *testing.T) { require.NoError(t, err) originalDecryptedValues = append(originalDecryptedValues, decryptedValue.DangerouslyExposeAndConsumeValue()) - encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, xkube.Namespace(tc.namespace), tc.name, 1) require.NoError(t, err) require.NotNil(t, encryptedValue) originalEncryptedData = append(originalEncryptedData, encryptedValue.EncryptedData) @@ -115,7 +115,7 @@ func TestConsolidation(t *testing.T) { require.Equal(t, originalDecryptedValues[i], decryptedValue.DangerouslyExposeAndConsumeValue()) // Verify that the encrypted data has changed (indicating re-encryption) - encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, xkube.Namespace(tc.namespace), tc.name, 1) require.NoError(t, err) require.NotEqual(t, originalEncryptedData[i], encryptedValue.EncryptedData) } @@ -174,7 +174,7 @@ func TestConsolidation(t *testing.T) { require.NoError(t, err) initialDecryptedValues = append(initialDecryptedValues, decryptedValue.DangerouslyExposeAndConsumeValue()) - encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, xkube.Namespace(tc.namespace), tc.name, 1) require.NoError(t, err) initialEncryptedData = append(initialEncryptedData, encryptedValue.EncryptedData) } @@ -223,7 +223,7 @@ func TestConsolidation(t *testing.T) { require.NoError(t, err) newSecretDecryptedValues = append(newSecretDecryptedValues, decryptedValue.DangerouslyExposeAndConsumeValue()) - encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, xkube.Namespace(tc.namespace), tc.name, 1) require.NoError(t, err) newSecretEncryptedData = append(newSecretEncryptedData, encryptedValue.EncryptedData) } @@ -252,7 +252,7 @@ func TestConsolidation(t *testing.T) { require.Equal(t, initialDecryptedValues[i], decryptedValue.DangerouslyExposeAndConsumeValue()) // Verify that the encrypted data has changed (indicating re-encryption) - encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, xkube.Namespace(tc.namespace), tc.name, 1) require.NoError(t, err) require.NotEqual(t, initialEncryptedData[i], encryptedValue.EncryptedData) } @@ -275,7 +275,7 @@ func TestConsolidation(t *testing.T) { // Verify that the encrypted data has changed from what it was when first created // (indicating it was re-encrypted during consolidation) - encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, xkube.Namespace(tc.namespace), tc.name, 1) require.NoError(t, err) require.NotEqual(t, newSecretEncryptedData[i], encryptedValue.EncryptedData) } diff --git a/pkg/registry/apis/secret/service/secure_value.go b/pkg/registry/apis/secret/service/secure_value.go index 2ad69b759db..cc4e6c80c72 100644 --- a/pkg/registry/apis/secret/service/secure_value.go +++ b/pkg/registry/apis/secret/service/secure_value.go @@ -146,7 +146,7 @@ func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv } logging.FromContext(ctx).Debug("retrieved keeper", "namespace", newSecureValue.Namespace, "keeperName", newSecureValue.Spec.Keeper, "type", keeperCfg.Type()) - secret, err := keeper.Expose(ctx, keeperCfg, newSecureValue.Namespace, newSecureValue.Name, currentVersion.Status.Version) + secret, err := keeper.Expose(ctx, keeperCfg, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, currentVersion.Status.Version) if err != nil { return nil, false, fmt.Errorf("reading secret value from keeper: %w", err) } @@ -191,7 +191,7 @@ func (s *SecureValueService) createNewVersion(ctx context.Context, sv *secretv1b // TODO: can we stop using external id? // TODO: store uses only the namespace and returns and id. It could be a kv instead. // TODO: check that the encrypted store works with multiple versions - externalID, err := keeper.Store(ctx, keeperCfg, createdSv.Namespace, createdSv.Name, createdSv.Status.Version, sv.Spec.Value.DangerouslyExposeAndConsumeValue()) + externalID, err := keeper.Store(ctx, keeperCfg, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version, sv.Spec.Value.DangerouslyExposeAndConsumeValue()) if err != nil { return nil, fmt.Errorf("storing secure value in keeper: %w", err) } diff --git a/pkg/registry/apis/secret/testutils/testutils.go b/pkg/registry/apis/secret/testutils/testutils.go index 50190dbb3ff..2d1c94c2ea3 100644 --- a/pkg/registry/apis/secret/testutils/testutils.go +++ b/pkg/registry/apis/secret/testutils/testutils.go @@ -126,7 +126,14 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { globalEncryptedValueStorage, err := encryptionstorage.ProvideGlobalEncryptedValueStorage(database, tracer) require.NoError(t, err) - sqlKeeper := sqlkeeper.NewSQLKeeper(tracer, encryptionManager, encryptedValueStorage, nil) + // Initialize a noop migration executor for the sql keeper so it doesn't interfere with initialization + noopMigrationExecutor := &NoopMigrationExecutor{} + sqlKeeper, err := sqlkeeper.NewSQLKeeper(tracer, encryptionManager, encryptedValueStorage, noopMigrationExecutor, nil) + require.NoError(t, err) + + // Initialize a real migration executor for test + realMigrationExecutor, err := encryptionstorage.ProvideEncryptedValueMigrationExecutor(database, tracer, encryptedValueStorage, globalEncryptedValueStorage) + require.NoError(t, err) var keeperService contracts.KeeperService = newKeeperServiceWrapper(sqlKeeper) @@ -158,39 +165,41 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { keeperService) return Sut{ - SecureValueService: secureValueService, - SecureValueMetadataStorage: secureValueMetadataStorage, - DecryptStorage: decryptStorage, - DecryptService: decryptService, - EncryptedValueStorage: encryptedValueStorage, - GlobalEncryptedValueStorage: globalEncryptedValueStorage, - SQLKeeper: sqlKeeper, - Database: database, - AccessClient: accessClient, - ConsolidationService: consolidationService, - EncryptionManager: encryptionManager, - GlobalDataKeyStore: globalDataKeyStore, - GarbageCollectionWorker: garbageCollectionWorker, - Clock: clock, - KeeperService: keeperService, - KeeperMetadataStorage: keeperMetadataStorage, + SecureValueService: secureValueService, + SecureValueMetadataStorage: secureValueMetadataStorage, + DecryptStorage: decryptStorage, + DecryptService: decryptService, + EncryptedValueStorage: encryptedValueStorage, + GlobalEncryptedValueStorage: globalEncryptedValueStorage, + EncryptedValueMigrationExecutor: realMigrationExecutor, + SQLKeeper: sqlKeeper, + Database: database, + AccessClient: accessClient, + ConsolidationService: consolidationService, + EncryptionManager: encryptionManager, + GlobalDataKeyStore: globalDataKeyStore, + GarbageCollectionWorker: garbageCollectionWorker, + Clock: clock, + KeeperService: keeperService, + KeeperMetadataStorage: keeperMetadataStorage, } } type Sut struct { - SecureValueService contracts.SecureValueService - SecureValueMetadataStorage contracts.SecureValueMetadataStorage - DecryptStorage contracts.DecryptStorage - DecryptService decryptcontracts.DecryptService - EncryptedValueStorage contracts.EncryptedValueStorage - GlobalEncryptedValueStorage contracts.GlobalEncryptedValueStorage - SQLKeeper *sqlkeeper.SQLKeeper - Database *database.Database - AccessClient types.AccessClient - ConsolidationService contracts.ConsolidationService - EncryptionManager contracts.EncryptionManager - GlobalDataKeyStore contracts.GlobalDataKeyStorage - GarbageCollectionWorker *garbagecollectionworker.Worker + SecureValueService contracts.SecureValueService + SecureValueMetadataStorage contracts.SecureValueMetadataStorage + DecryptStorage contracts.DecryptStorage + DecryptService decryptcontracts.DecryptService + EncryptedValueStorage contracts.EncryptedValueStorage + GlobalEncryptedValueStorage contracts.GlobalEncryptedValueStorage + EncryptedValueMigrationExecutor contracts.EncryptedValueMigrationExecutor + SQLKeeper *sqlkeeper.SQLKeeper + Database *database.Database + AccessClient types.AccessClient + ConsolidationService contracts.ConsolidationService + EncryptionManager contracts.EncryptionManager + GlobalDataKeyStore contracts.GlobalDataKeyStorage + GarbageCollectionWorker *garbagecollectionworker.Worker // The fake clock passed to implementations to make testing easier Clock *FakeClock KeeperService contracts.KeeperService @@ -366,3 +375,10 @@ func (c *FakeClock) Now() time.Time { func (c *FakeClock) AdvanceBy(duration time.Duration) { c.Current = c.Current.Add(duration) } + +type NoopMigrationExecutor struct { +} + +func (e *NoopMigrationExecutor) Execute(ctx context.Context) (int, error) { + return 0, nil +} diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 9ee5cfeaf68..5422ac453ae 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -444,6 +444,7 @@ var wireBasicSet = wire.NewSet( secretencryption.ProvideGlobalDataKeyStorage, secretencryption.ProvideEncryptedValueStorage, secretencryption.ProvideGlobalEncryptedValueStorage, + secretencryption.ProvideEncryptedValueMigrationExecutor, secretsecurevalueservice.ProvideSecureValueService, secretvalidator.ProvideKeeperValidator, secretvalidator.ProvideSecureValueValidator, diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index ee58cbe1d21..eba2e7b9ff5 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -217,7 +217,7 @@ import ( kvstore2 "github.com/grafana/grafana/pkg/services/secrets/kvstore" migrations2 "github.com/grafana/grafana/pkg/services/secrets/kvstore/migrations" "github.com/grafana/grafana/pkg/services/secrets/manager" - "github.com/grafana/grafana/pkg/services/secrets/migrator" + migrator2 "github.com/grafana/grafana/pkg/services/secrets/migrator" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/extsvcaccounts" manager3 "github.com/grafana/grafana/pkg/services/serviceaccounts/manager" @@ -255,7 +255,7 @@ import ( database4 "github.com/grafana/grafana/pkg/storage/secret/database" "github.com/grafana/grafana/pkg/storage/secret/encryption" "github.com/grafana/grafana/pkg/storage/secret/metadata" - migrator2 "github.com/grafana/grafana/pkg/storage/secret/migrator" + "github.com/grafana/grafana/pkg/storage/secret/migrator" "github.com/grafana/grafana/pkg/storage/unified" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/search" @@ -482,7 +482,20 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - ossKeeperService, err := secretkeeper.ProvideService(tracer, encryptedValueStorage, encryptionManager, registerer) + globalEncryptedValueStorage, err := encryption.ProvideGlobalEncryptedValueStorage(databaseDatabase, tracer) + if err != nil { + return nil, err + } + encryptedValueMigrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor(databaseDatabase, tracer, encryptedValueStorage, globalEncryptedValueStorage) + if err != nil { + return nil, err + } + secretDBMigrator := migrator.NewWithEngine(sqlStore) + dependencyRegisterer, err := secret.RegisterDependencies(featureToggles, cfg, secretDBMigrator, acimplService) + if err != nil { + return nil, err + } + ossKeeperService, err := secretkeeper.ProvideService(tracer, encryptedValueStorage, encryptionManager, encryptedValueMigrationExecutor, registerer, dependencyRegisterer) if err != nil { return nil, err } @@ -687,7 +700,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api } csrfCSRF := csrf.ProvideCSRFFilter(cfg) playlistService := playlistimpl.ProvideService(sqlStore, tracingService) - secretsMigrator := migrator.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) + secretsMigrator := migrator2.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service15, kvStore, featureToggles) secretMigrationProviderImpl := migrations2.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) publicDashboardServiceImpl := service3.ProvideService(cfg, featureToggles, publicDashboardStoreImpl, queryServiceImpl, repositoryImpl, accessControl, publicDashboardServiceWrapperImpl, dashboardService, ossLicensingService) @@ -858,11 +871,6 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - secretDBMigrator := migrator2.NewWithEngine(sqlStore) - dependencyRegisterer, err := secret.RegisterDependencies(featureToggles, cfg, secretDBMigrator, acimplService) - if err != nil { - return nil, err - } apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer) teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService) if err != nil { @@ -1088,7 +1096,20 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - ossKeeperService, err := secretkeeper.ProvideService(tracer, encryptedValueStorage, encryptionManager, registerer) + globalEncryptedValueStorage, err := encryption.ProvideGlobalEncryptedValueStorage(databaseDatabase, tracer) + if err != nil { + return nil, err + } + encryptedValueMigrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor(databaseDatabase, tracer, encryptedValueStorage, globalEncryptedValueStorage) + if err != nil { + return nil, err + } + secretDBMigrator := migrator.NewWithEngine(sqlStore) + dependencyRegisterer, err := secret.RegisterDependencies(featureToggles, cfg, secretDBMigrator, acimplService) + if err != nil { + return nil, err + } + ossKeeperService, err := secretkeeper.ProvideService(tracer, encryptedValueStorage, encryptionManager, encryptedValueMigrationExecutor, registerer, dependencyRegisterer) if err != nil { return nil, err } @@ -1295,7 +1316,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac } csrfCSRF := csrf.ProvideCSRFFilter(cfg) playlistService := playlistimpl.ProvideService(sqlStore, tracingService) - secretsMigrator := migrator.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) + secretsMigrator := migrator2.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service15, kvStore, featureToggles) secretMigrationProviderImpl := migrations2.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) publicDashboardServiceImpl := service3.ProvideService(cfg, featureToggles, publicDashboardStoreImpl, queryServiceImpl, repositoryImpl, accessControl, publicDashboardServiceWrapperImpl, dashboardService, ossLicensingService) @@ -1466,11 +1487,6 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - secretDBMigrator := migrator2.NewWithEngine(sqlStore) - dependencyRegisterer, err := secret.RegisterDependencies(featureToggles, cfg, secretDBMigrator, acimplService) - if err != nil { - return nil, err - } apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer) teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService) if err != nil { @@ -1540,7 +1556,7 @@ func InitializeForCLI(ctx context.Context, cfg *setting.Cfg) (Runner, error) { if err != nil { return Runner{}, err } - secretsMigrator := migrator.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) + secretsMigrator := migrator2.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) configProvider, err := configprovider.ProvideService(cfg) if err != nil { return Runner{}, err @@ -1678,7 +1694,7 @@ var withOTelSet = wire.NewSet( otelTracer, grpcserver.ProvideService, interceptors.ProvideAuthenticator, ) -var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator3.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service12.ProvideService, wire.Bind(new(service12.LDAP), new(*service12.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service9.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service9.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption2.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), garbagecollectionworker.ProvideWorker, grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database5.DashboardSnapshotStore)), database5.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service10.ServiceImpl)), service10.ProvideService, service9.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service9.Service)), service9.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager3.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), dsquerierclient.NewNullQSDatasourceClientBuilder, expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service7.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service7.DashboardServiceImpl)), service7.ProvideDashboardService, service7.ProvideDashboardProvisioningService, service7.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), wire.Bind(new(folder.LegacyService), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), service11.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service11.ImportDashboardService)), service8.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service8.Service)), service8.ProvideDashboardUpdater, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), promtypemigration.ProvideAzurePromMigrationService, promtypemigration.ProvideAmazonPromMigrationService, promtypemigration.ProvidePromTypeMigrationProvider, wire.Bind(new(promtypemigration.PromTypeMigrationProvider), new(*promtypemigration.PromTypeMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, wire.Value([]decrypt.ExtraOwnerDecrypter(nil)), decrypt.ProvideDecryptService, inline.ProvideInlineSecureValueService, encryption.ProvideDataKeyStorage, encryption.ProvideGlobalDataKeyStorage, encryption.ProvideEncryptedValueStorage, encryption.ProvideGlobalEncryptedValueStorage, service5.ProvideSecureValueService, validator.ProvideKeeperValidator, validator.ProvideSecureValueValidator, mutator.ProvideKeeperMutator, mutator.ProvideSecureValueMutator, migrator2.NewWithEngine, database4.ProvideDatabase, clock.ProvideClock, wire.Bind(new(contracts.Database), new(*database4.Database)), wire.Bind(new(contracts.Clock), new(*clock.Clock)), manager2.ProvideEncryptionManager, service4.ProvideAESGCMCipherService, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet, client.ProvideK8sClientWithFallback) +var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator3.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service12.ProvideService, wire.Bind(new(service12.LDAP), new(*service12.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service9.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service9.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption2.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), garbagecollectionworker.ProvideWorker, grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database5.DashboardSnapshotStore)), database5.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service10.ServiceImpl)), service10.ProvideService, service9.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service9.Service)), service9.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager3.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), dsquerierclient.NewNullQSDatasourceClientBuilder, expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service7.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service7.DashboardServiceImpl)), service7.ProvideDashboardService, service7.ProvideDashboardProvisioningService, service7.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), wire.Bind(new(folder.LegacyService), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), service11.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service11.ImportDashboardService)), service8.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service8.Service)), service8.ProvideDashboardUpdater, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), promtypemigration.ProvideAzurePromMigrationService, promtypemigration.ProvideAmazonPromMigrationService, promtypemigration.ProvidePromTypeMigrationProvider, wire.Bind(new(promtypemigration.PromTypeMigrationProvider), new(*promtypemigration.PromTypeMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, wire.Value([]decrypt.ExtraOwnerDecrypter(nil)), decrypt.ProvideDecryptService, inline.ProvideInlineSecureValueService, encryption.ProvideDataKeyStorage, encryption.ProvideGlobalDataKeyStorage, encryption.ProvideEncryptedValueStorage, encryption.ProvideGlobalEncryptedValueStorage, encryption.ProvideEncryptedValueMigrationExecutor, service5.ProvideSecureValueService, validator.ProvideKeeperValidator, validator.ProvideSecureValueValidator, mutator.ProvideKeeperMutator, mutator.ProvideSecureValueMutator, migrator.NewWithEngine, database4.ProvideDatabase, clock.ProvideClock, wire.Bind(new(contracts.Database), new(*database4.Database)), wire.Bind(new(contracts.Clock), new(*clock.Clock)), manager2.ProvideEncryptionManager, service4.ProvideAESGCMCipherService, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet, client.ProvideK8sClientWithFallback) var wireSet = wire.NewSet( wireBasicSet, metrics.WireSet, sqlstore.ProvideService, metrics2.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationService)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), prefimpl.ProvideService, oauthtoken.ProvideService, wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), wire.Bind(new(cleanup.AlertRuleService), new(*store2.DBstore)), diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index c87d65d25ea..85e883db58f 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -2091,6 +2091,14 @@ var ( Owner: grafanaPluginsPlatformSquad, Expression: "false", }, + { + Name: "cdnPluginsUrls", + Description: "Enable loading plugins via declarative URLs", + Stage: FeatureStageExperimental, + FrontendOnly: false, + Owner: grafanaPluginsPlatformSquad, + Expression: "false", + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 9be69256740..20b5b3d9255 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -268,3 +268,4 @@ pluginContainers,privatePreview,@grafana/plugins-platform-backend,false,true,fal tempoSearchBackendMigration,GA,@grafana/oss-big-tent,false,true,false filterOutBotsFromFrontendLogs,experimental,@grafana/plugins-platform-backend,false,false,true cdnPluginsLoadFirst,experimental,@grafana/plugins-platform-backend,false,false,false +cdnPluginsUrls,experimental,@grafana/plugins-platform-backend,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index f0ea79a2da1..055bd3d6ad0 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -1081,4 +1081,8 @@ const ( // FlagCdnPluginsLoadFirst // Prioritize loading plugins from the CDN before other sources FlagCdnPluginsLoadFirst = "cdnPluginsLoadFirst" + + // FlagCdnPluginsUrls + // Enable loading plugins via declarative URLs + FlagCdnPluginsUrls = "cdnPluginsUrls" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 178c845fed8..5b550b39a55 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -903,6 +903,19 @@ "expression": "false" } }, + { + "metadata": { + "name": "cdnPluginsUrls", + "resourceVersion": "1759489886228", + "creationTimestamp": "2025-10-03T11:11:26Z" + }, + "spec": { + "description": "Enable loading plugins via declarative URLs", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend", + "expression": "false" + } + }, { "metadata": { "name": "cloudRBACRoles", diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index 27a31a5bcff..0d9229a533c 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -376,7 +376,7 @@ func (s *ServiceImpl) readNavigationSettings() { "grafana-asserts-app": {SectionID: navtree.NavIDObservability, SortWeight: 1, Icon: "asserts"}, "grafana-kowalski-app": {SectionID: navtree.NavIDObservability, SortWeight: 2, Text: "Frontend"}, "grafana-app-observability-app": {SectionID: navtree.NavIDObservability, SortWeight: 3, Text: "Application"}, - "grafana-dbo11y-app": {SectionID: navtree.NavIDObservability, SortWeight: 4, Text: "Database"}, + "grafana-dbo11y-app": {SectionID: navtree.NavIDObservability, SortWeight: 4, Text: "Database", IsNew: true}, "grafana-k8s-app": {SectionID: navtree.NavIDObservability, SortWeight: 5, Text: "Kubernetes"}, "grafana-csp-app": {SectionID: navtree.NavIDObservability, SortWeight: 6, Icon: "cloud-provider"}, "grafana-metricsdrilldown-app": {SectionID: navtree.NavIDDrilldown, SortWeight: 1, Text: "Metrics"}, diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index a7dd8f1f2fe..db380834c4a 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -14,8 +14,7 @@ import ( "testing" "time" - alertingNotify "github.com/grafana/alerting/notify" - "github.com/grafana/alerting/receivers/schema" + "github.com/grafana/alerting/notify/notifytest" prometheus "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/pkg/labels" "github.com/prometheus/alertmanager/timeinterval" @@ -2033,12 +2032,12 @@ func TestApiContactPointExportSnapshot(t *testing.T) { t.Run(fmt.Sprintf("exportType=%s", exportType), func(t *testing.T) { for _, redacted := range []bool{true, false} { t.Run(fmt.Sprintf("redacted=%t", redacted), func(t *testing.T) { - allIntegrations := make([]models.Integration, 0, len(alertingNotify.AllKnownConfigsForTesting)) - for integrationType := range alertingNotify.AllKnownConfigsForTesting { + allIntegrations := make([]models.Integration, 0, len(notifytest.AllKnownV1ConfigsForTesting)) + for integrationType := range notifytest.AllKnownV1ConfigsForTesting { integration := models.IntegrationGen( models.IntegrationMuts.WithName(allIntegrationsName), - models.IntegrationMuts.WithUID(fmt.Sprintf("%s-uid", integrationType)), - models.IntegrationMuts.WithValidConfig(schema.IntegrationType(integrationType)), + models.IntegrationMuts.WithUID(fmt.Sprintf("%s-uid", strings.ToLower(string(integrationType)))), + models.IntegrationMuts.WithValidConfig(integrationType), )() integration.DisableResolveMessage = redacted allIntegrations = append(allIntegrations, integration) diff --git a/pkg/services/ngalert/api/compat_contact_points.go b/pkg/services/ngalert/api/compat_contact_points.go index e18b5c0a74a..5a55bc9ca95 100644 --- a/pkg/services/ngalert/api/compat_contact_points.go +++ b/pkg/services/ngalert/api/compat_contact_points.go @@ -7,6 +7,7 @@ import ( "strings" "unsafe" + alertingModels "github.com/grafana/alerting/models" "github.com/grafana/alerting/notify" "github.com/grafana/alerting/receivers" jsoniter "github.com/json-iterator/go" @@ -53,7 +54,7 @@ func ContactPointToContactPointExport(cp definitions.ContactPoint) (notify.APIRe len(cp.Threema) + len(cp.Victorops) + len(cp.Webhook) + len(cp.Wecom) + len(cp.Webex) + len(cp.Mqtt) - integration := make([]*notify.GrafanaIntegrationConfig, 0, contactPointsLength) + integration := make([]*alertingModels.IntegrationConfig, 0, contactPointsLength) var errs []error for _, i := range cp.Alertmanager { @@ -222,20 +223,20 @@ func ContactPointToContactPointExport(cp definitions.ContactPoint) (notify.APIRe return notify.APIReceiver{}, errors.Join(errs...) } contactPoint := notify.APIReceiver{ - ConfigReceiver: notify.ConfigReceiver{Name: cp.Name}, - GrafanaIntegrations: notify.GrafanaIntegrations{Integrations: integration}, + ConfigReceiver: notify.ConfigReceiver{Name: cp.Name}, + ReceiverConfig: alertingModels.ReceiverConfig{Integrations: integration}, } return contactPoint, nil } // marshallIntegration converts the API model integration to the storage model that contains settings in the JSON format. // The secret fields are not encrypted. -func marshallIntegration(json jsoniter.API, integrationType string, integration interface{}, disableResolveMessage *bool) (*notify.GrafanaIntegrationConfig, error) { +func marshallIntegration(json jsoniter.API, integrationType string, integration interface{}, disableResolveMessage *bool) (*alertingModels.IntegrationConfig, error) { data, err := json.Marshal(integration) if err != nil { return nil, fmt.Errorf("failed to marshall integration '%s' to JSON: %w", integrationType, err) } - e := ¬ify.GrafanaIntegrationConfig{ + e := &alertingModels.IntegrationConfig{ Type: integrationType, Settings: data, } diff --git a/pkg/services/ngalert/api/compat_contact_points_test.go b/pkg/services/ngalert/api/compat_contact_points_test.go index bda8336b0bf..ef178dd085f 100644 --- a/pkg/services/ngalert/api/compat_contact_points_test.go +++ b/pkg/services/ngalert/api/compat_contact_points_test.go @@ -7,8 +7,12 @@ import ( "testing" "github.com/google/go-cmp/cmp" + alertingmodels "github.com/grafana/alerting/models" "github.com/grafana/alerting/notify" + "github.com/grafana/alerting/notify/notifytest" + "github.com/grafana/alerting/receivers/line" receiversTesting "github.com/grafana/alerting/receivers/testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" apicompat "github.com/grafana/grafana/pkg/services/ngalert/api/compat" @@ -53,12 +57,12 @@ func TestContactPointFromContactPointExports(t *testing.T) { } // use the configs for testing because they have all fields supported by integrations - for integrationType, cfg := range notify.AllKnownConfigsForTesting { - t.Run(integrationType, func(t *testing.T) { + for integrationType, cfg := range notifytest.AllKnownV1ConfigsForTesting { + t.Run(string(integrationType), func(t *testing.T) { recCfg := ¬ify.APIReceiver{ ConfigReceiver: notify.ConfigReceiver{Name: "test-receiver"}, - GrafanaIntegrations: notify.GrafanaIntegrations{ - Integrations: []*notify.GrafanaIntegrationConfig{ + ReceiverConfig: alertingmodels.ReceiverConfig{ + Integrations: []*alertingmodels.IntegrationConfig{ cfg.GetRawNotifierConfig("test"), }, }, @@ -87,9 +91,15 @@ func TestContactPointFromContactPointExports(t *testing.T) { } if integrationType != "webhook" { // Many notifiers now support HTTPClientConfig but only Webhook currently has it enabled in schema. - //TODO: Remove this once HTTPClientConfig is added to other schemas. + // TODO: Remove this once HTTPClientConfig is added to other schemas. pathFilters = append(pathFilters, "HTTPClientConfig") } + if integrationType == line.Type { + for _, l := range actual.LineConfigs { + assert.Equal(t, "line", l.Type) + l.Type = string(line.Type) + } + } pathFilter := cmp.FilterPath(func(path cmp.Path) bool { for _, filter := range pathFilters { if strings.Contains(path.String(), filter) { diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index 5c33ad658fe..530264dc287 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -603,15 +603,15 @@ type AlertGroups = amv2.AlertGroups type AlertGroup = amv2.AlertGroup -type Receiver = alertingmodels.Receiver +type Receiver = alertingmodels.ReceiverStatus // swagger:response receiversResponse type ReceiversResponse struct { // in:body - Body []alertingmodels.Receiver + Body []alertingmodels.ReceiverStatus } -type Integration = alertingmodels.Integration +type Integration = alertingmodels.IntegrationStatus // swagger:parameters RouteGetAMAlerts RouteGetAMAlertGroups RouteGetGrafanaAMAlerts RouteGetGrafanaAMAlertGroups type AlertsParams struct { diff --git a/pkg/services/ngalert/models/receivers.go b/pkg/services/ngalert/models/receivers.go index 1f892147938..08734b0f56b 100644 --- a/pkg/services/ngalert/models/receivers.go +++ b/pkg/services/ngalert/models/receivers.go @@ -12,6 +12,7 @@ import ( "sort" "strings" + "github.com/grafana/alerting/models" alertingNotify "github.com/grafana/alerting/notify" "github.com/grafana/alerting/receivers/schema" ) @@ -572,7 +573,7 @@ func (integration *Integration) Validate(decryptFn DecryptFn) error { return err } - return ValidateIntegration(context.Background(), alertingNotify.GrafanaIntegrationConfig{ + return ValidateIntegration(context.Background(), models.IntegrationConfig{ UID: decrypted.UID, Name: decrypted.Name, Type: decrypted.Config.Type, @@ -582,7 +583,7 @@ func (integration *Integration) Validate(decryptFn DecryptFn) error { }, alertingNotify.NoopDecrypt) } -func ValidateIntegration(ctx context.Context, integration alertingNotify.GrafanaIntegrationConfig, decryptFunc alertingNotify.GetDecryptedValueFn) error { +func ValidateIntegration(ctx context.Context, integration models.IntegrationConfig, decryptFunc alertingNotify.GetDecryptedValueFn) error { if integration.Type == "" { return fmt.Errorf("type should not be an empty string") } @@ -591,8 +592,8 @@ func ValidateIntegration(ctx context.Context, integration alertingNotify.Grafana } _, err := alertingNotify.BuildReceiverConfiguration(ctx, &alertingNotify.APIReceiver{ - GrafanaIntegrations: alertingNotify.GrafanaIntegrations{ - Integrations: []*alertingNotify.GrafanaIntegrationConfig{&integration}, + ReceiverConfig: models.ReceiverConfig{ + Integrations: []*models.IntegrationConfig{&integration}, }, }, alertingNotify.DecodeSecretsFromBase64, decryptFunc) if err != nil { diff --git a/pkg/services/ngalert/models/receivers_test.go b/pkg/services/ngalert/models/receivers_test.go index 0c6c08c3d93..785fffd49be 100644 --- a/pkg/services/ngalert/models/receivers_test.go +++ b/pkg/services/ngalert/models/receivers_test.go @@ -6,6 +6,7 @@ import ( "testing" alertingNotify "github.com/grafana/alerting/notify" + "github.com/grafana/alerting/notify/notifytest" "github.com/grafana/alerting/receivers/schema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -40,8 +41,7 @@ func TestReceiver_EncryptDecrypt(t *testing.T) { encryptFn := Base64Enrypt decryptnFn := Base64Decrypt // Test that all known integration types encrypt and decrypt their secrets. - for it := range alertingNotify.AllKnownConfigsForTesting { - integrationType := schema.IntegrationType(it) + for integrationType := range notifytest.AllKnownV1ConfigsForTesting { t.Run(string(integrationType), func(t *testing.T) { decrypedIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))() encrypted := decrypedIntegration.Clone() @@ -76,8 +76,7 @@ func TestIntegration_Redact(t *testing.T) { return "TESTREDACTED" } // Test that all known integration types redact their secrets. - for it := range alertingNotify.AllKnownConfigsForTesting { - integrationType := schema.IntegrationType(it) + for integrationType := range notifytest.AllKnownV1ConfigsForTesting { t.Run(string(integrationType), func(t *testing.T) { validIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))() @@ -106,8 +105,7 @@ func TestIntegration_Validate(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) // Test that all known integration types are valid. - for it := range alertingNotify.AllKnownConfigsForTesting { - integrationType := schema.IntegrationType(it) + for integrationType := range notifytest.AllKnownV1ConfigsForTesting { t.Run(string(integrationType), func(t *testing.T) { validIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))() assert.NoError(t, validIntegration.Encrypt(Base64Enrypt)) @@ -242,8 +240,7 @@ func TestIntegration_WithExistingSecureFields(t *testing.T) { func TestSecretsIntegrationConfig(t *testing.T) { // Test that all known integration types have a config and correctly mark their secrets as secure. - for it := range alertingNotify.AllKnownConfigsForTesting { - integrationType := schema.IntegrationType(it) + for integrationType := range notifytest.AllKnownV1ConfigsForTesting { t.Run(string(integrationType), func(t *testing.T) { schemaType, ok := alertingNotify.GetSchemaForIntegration(integrationType) require.True(t, ok) @@ -272,8 +269,8 @@ func TestSecretsIntegrationConfig(t *testing.T) { } t.Run("Unknown version returns error", func(t *testing.T) { - for s := range maps.Keys(alertingNotify.AllKnownConfigsForTesting) { - schemaType, _ := alertingNotify.GetSchemaForIntegration(schema.IntegrationType(s)) + for s := range maps.Keys(notifytest.AllKnownV1ConfigsForTesting) { + schemaType, _ := alertingNotify.GetSchemaForIntegration(s) _, err := IntegrationConfigFromSchema(schemaType, "unknown") require.Error(t, err) return @@ -285,8 +282,8 @@ func TestIntegration_SecureFields(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) // Test that all known integration types have a config and correctly mark their secrets as secure. - for it := range alertingNotify.AllKnownConfigsForTesting { - integrationType := schema.IntegrationType(it) + for it := range notifytest.AllKnownV1ConfigsForTesting { + integrationType := it t.Run(string(integrationType), func(t *testing.T) { t.Run("contains SecureSettings", func(t *testing.T) { validIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))() diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index 2164ca8f86f..0fd681b693c 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -13,6 +13,7 @@ import ( "github.com/go-openapi/strfmt" "github.com/google/uuid" alertingNotify "github.com/grafana/alerting/notify" + "github.com/grafana/alerting/notify/notifytest" "github.com/grafana/alerting/receivers/schema" "github.com/grafana/alerting/receivers/webex" "github.com/grafana/grafana-plugin-sdk-go/data" @@ -1270,7 +1271,7 @@ func CopyIntegrationWith(r Integration, mutators ...Mutator[Integration]) Integr func IntegrationGen(mutators ...Mutator[Integration]) func() Integration { return func() Integration { name := util.GenerateShortUID() - randomIntegrationType, _ := randomMapKey(alertingNotify.AllKnownConfigsForTesting) + randomIntegrationType, _ := randomMapKey(notifytest.AllKnownV1ConfigsForTesting) c := Integration{ UID: util.GenerateShortUID(), @@ -1280,7 +1281,7 @@ func IntegrationGen(mutators ...Mutator[Integration]) func() Integration { SecureSettings: make(map[string]string), } - IntegrationMuts.WithValidConfig(schema.IntegrationType(randomIntegrationType))(&c) + IntegrationMuts.WithValidConfig(randomIntegrationType)(&c) for _, mutator := range mutators { mutator(&c) @@ -1317,7 +1318,11 @@ func (n IntegrationMutators) WithName(name string) Mutator[Integration] { func (n IntegrationMutators) WithValidConfig(integrationType schema.IntegrationType) Mutator[Integration] { return func(c *Integration) { // TODO add support for v0 integrations - config := alertingNotify.AllKnownConfigsForTesting[string(integrationType)].GetRawNotifierConfig(c.Name) + ncfg, ok := notifytest.AllKnownV1ConfigsForTesting[integrationType] + if !ok { + panic(fmt.Sprintf("unknown integration type: %s", integrationType)) + } + config := ncfg.GetRawNotifierConfig(c.Name) typeSchema, _ := alertingNotify.GetSchemaForIntegration(integrationType) integrationConfig, _ := IntegrationConfigFromSchema(typeSchema, schema.V1) c.Config = integrationConfig @@ -1337,7 +1342,10 @@ func (n IntegrationMutators) WithValidConfig(integrationType schema.IntegrationT func (n IntegrationMutators) WithInvalidConfig(integrationType schema.IntegrationType) Mutator[Integration] { return func(c *Integration) { - typeSchema, _ := alertingNotify.GetSchemaForIntegration(integrationType) + typeSchema, ok := alertingNotify.GetSchemaForIntegration(integrationType) + if !ok { + panic(fmt.Sprintf("unknown integration type: %s", integrationType)) + } c.Config, _ = IntegrationConfigFromSchema(typeSchema, schema.V1) c.Settings = map[string]interface{}{} c.SecureSettings = map[string]string{} diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 06c5b5e0cce..f4454d87c5f 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -9,6 +9,7 @@ import ( "strconv" "time" + "github.com/grafana/alerting/models" alertingNotify "github.com/grafana/alerting/notify" "github.com/grafana/alerting/notify/nfstatus" "github.com/prometheus/alertmanager/config" @@ -365,7 +366,7 @@ func (am *alertmanager) applyConfig(ctx context.Context, cfg *apimodels.Postable return false, nil } - receivers := PostableApiAlertingConfigToApiReceivers(amConfig) + receivers := alertingNotify.PostableAPIReceiversToAPIReceivers(amConfig.Receivers) for _, recv := range receivers { err = patchNewSecureFields(ctx, recv, alertingNotify.DecodeSecretsFromBase64, am.decryptFn) if err != nil { @@ -406,7 +407,7 @@ func patchNewSecureFields(ctx context.Context, api *alertingNotify.APIReceiver, return nil } -func patchSettingsFromSecureSettings(ctx context.Context, integration *alertingNotify.GrafanaIntegrationConfig, key string, decode alertingNotify.DecodeSecretsFn, decrypt alertingNotify.GetDecryptedValueFn) error { +func patchSettingsFromSecureSettings(ctx context.Context, integration *models.IntegrationConfig, key string, decode alertingNotify.DecodeSecretsFn, decrypt alertingNotify.GetDecryptedValueFn) error { if _, ok := integration.SecureSettings[key]; !ok { return nil } diff --git a/pkg/services/ngalert/notifier/alertmanager_mock/Alertmanager.go b/pkg/services/ngalert/notifier/alertmanager_mock/Alertmanager.go index 20f4e14d0ab..e4ec918e59d 100644 --- a/pkg/services/ngalert/notifier/alertmanager_mock/Alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager_mock/Alertmanager.go @@ -309,23 +309,23 @@ func (_c *AlertmanagerMock_GetAlerts_Call) RunAndReturn(run func(context.Context } // GetReceivers provides a mock function with given fields: ctx -func (_m *AlertmanagerMock) GetReceivers(ctx context.Context) ([]alertingmodels.Receiver, error) { +func (_m *AlertmanagerMock) GetReceivers(ctx context.Context) ([]alertingmodels.ReceiverStatus, error) { ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for GetReceivers") } - var r0 []alertingmodels.Receiver + var r0 []alertingmodels.ReceiverStatus var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]alertingmodels.Receiver, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context) ([]alertingmodels.ReceiverStatus, error)); ok { return rf(ctx) } - if rf, ok := ret.Get(0).(func(context.Context) []alertingmodels.Receiver); ok { + if rf, ok := ret.Get(0).(func(context.Context) []alertingmodels.ReceiverStatus); ok { r0 = rf(ctx) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]alertingmodels.Receiver) + r0 = ret.Get(0).([]alertingmodels.ReceiverStatus) } } @@ -356,12 +356,12 @@ func (_c *AlertmanagerMock_GetReceivers_Call) Run(run func(ctx context.Context)) return _c } -func (_c *AlertmanagerMock_GetReceivers_Call) Return(_a0 []alertingmodels.Receiver, _a1 error) *AlertmanagerMock_GetReceivers_Call { +func (_c *AlertmanagerMock_GetReceivers_Call) Return(_a0 []alertingmodels.ReceiverStatus, _a1 error) *AlertmanagerMock_GetReceivers_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *AlertmanagerMock_GetReceivers_Call) RunAndReturn(run func(context.Context) ([]alertingmodels.Receiver, error)) *AlertmanagerMock_GetReceivers_Call { +func (_c *AlertmanagerMock_GetReceivers_Call) RunAndReturn(run func(context.Context) ([]alertingmodels.ReceiverStatus, error)) *AlertmanagerMock_GetReceivers_Call { _c.Call.Return(run) return _c } diff --git a/pkg/services/ngalert/notifier/compat.go b/pkg/services/ngalert/notifier/compat.go index 9b2dccb8099..63c6ed7d7bb 100644 --- a/pkg/services/ngalert/notifier/compat.go +++ b/pkg/services/ngalert/notifier/compat.go @@ -1,47 +1,11 @@ package notifier import ( - "encoding/json" - alertingNotify "github.com/grafana/alerting/notify" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" ) -func PostableGrafanaReceiverToGrafanaIntegrationConfig(p *apimodels.PostableGrafanaReceiver) *alertingNotify.GrafanaIntegrationConfig { - return &alertingNotify.GrafanaIntegrationConfig{ - UID: p.UID, - Name: p.Name, - Type: p.Type, - DisableResolveMessage: p.DisableResolveMessage, - Settings: json.RawMessage(p.Settings), - SecureSettings: p.SecureSettings, - } -} - -func PostableApiReceiverToApiReceiver(r *apimodels.PostableApiReceiver) *alertingNotify.APIReceiver { - integrations := alertingNotify.GrafanaIntegrations{ - Integrations: make([]*alertingNotify.GrafanaIntegrationConfig, 0, len(r.GrafanaManagedReceivers)), - } - for _, cfg := range r.GrafanaManagedReceivers { - integrations.Integrations = append(integrations.Integrations, PostableGrafanaReceiverToGrafanaIntegrationConfig(cfg)) - } - - return &alertingNotify.APIReceiver{ - ConfigReceiver: r.Receiver, - GrafanaIntegrations: integrations, - } -} - -func PostableApiAlertingConfigToApiReceivers(c apimodels.PostableApiAlertingConfig) []*alertingNotify.APIReceiver { - apiReceivers := make([]*alertingNotify.APIReceiver, 0, len(c.Receivers)) - for _, receiver := range c.Receivers { - apiReceivers = append(apiReceivers, PostableApiReceiverToApiReceiver(receiver)) - } - return apiReceivers -} - // Silence-specific compat functions to convert between grafana/alerting and model types. func GettableSilenceToSilence(s alertingNotify.GettableSilence) *models.Silence { diff --git a/pkg/services/ngalert/notifier/compat_test.go b/pkg/services/ngalert/notifier/compat_test.go deleted file mode 100644 index b3a2ff8508a..00000000000 --- a/pkg/services/ngalert/notifier/compat_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package notifier - -import ( - "encoding/json" - "testing" - - alertingNotify "github.com/grafana/alerting/notify" - "github.com/prometheus/alertmanager/config" - "github.com/stretchr/testify/require" - - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" -) - -func TestPostableGrafanaReceiverToGrafanaIntegrationConfig(t *testing.T) { - r := &apimodels.PostableGrafanaReceiver{ - UID: "test-uid", - Name: "test-name", - Type: "slack", - DisableResolveMessage: false, - Settings: apimodels.RawMessage(`{ "data" : "test" }`), - SecureSettings: map[string]string{ - "test": "data", - }, - } - actual := PostableGrafanaReceiverToGrafanaIntegrationConfig(r) - require.Equal(t, alertingNotify.GrafanaIntegrationConfig{ - UID: "test-uid", - Name: "test-name", - Type: "slack", - DisableResolveMessage: false, - Settings: json.RawMessage(`{ "data" : "test" }`), - SecureSettings: map[string]string{ - "test": "data", - }, - }, *actual) -} - -func TestPostableApiReceiverToApiReceiver(t *testing.T) { - t.Run("returns empty when no receivers", func(t *testing.T) { - r := &apimodels.PostableApiReceiver{ - Receiver: config.Receiver{ - Name: "test-receiver", - }, - } - actual := PostableApiReceiverToApiReceiver(r) - require.Empty(t, actual.Integrations) - require.Equal(t, r.Receiver, actual.ConfigReceiver) - }) - t.Run("converts receivers", func(t *testing.T) { - r := &apimodels.PostableApiReceiver{ - Receiver: config.Receiver{ - Name: "test-receiver", - }, - PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{ - { - UID: "test-uid", - Name: "test-name", - Type: "slack", - DisableResolveMessage: false, - Settings: apimodels.RawMessage(`{ "data" : "test" }`), - SecureSettings: map[string]string{ - "test": "data", - }, - }, - { - UID: "test-uid2", - Name: "test-name2", - Type: "webhook", - DisableResolveMessage: false, - Settings: apimodels.RawMessage(`{ "data2" : "test2" }`), - SecureSettings: map[string]string{ - "test2": "data2", - }, - }, - }, - }, - } - actual := PostableApiReceiverToApiReceiver(r) - require.Len(t, actual.Integrations, 2) - require.Equal(t, r.Receiver, actual.ConfigReceiver) - require.Equal(t, *PostableGrafanaReceiverToGrafanaIntegrationConfig(r.GrafanaManagedReceivers[0]), *actual.Integrations[0]) - require.Equal(t, *PostableGrafanaReceiverToGrafanaIntegrationConfig(r.GrafanaManagedReceivers[1]), *actual.Integrations[1]) - }) -} - -func TestPostableApiAlertingConfigToApiReceivers(t *testing.T) { - t.Run("returns empty when no receivers", func(t *testing.T) { - r := apimodels.PostableApiAlertingConfig{ - Config: apimodels.Config{}, - } - actual := PostableApiAlertingConfigToApiReceivers(r) - require.Empty(t, actual) - }) - c := apimodels.PostableApiAlertingConfig{ - Config: apimodels.Config{}, - Receivers: []*apimodels.PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "test-receiver", - }, - PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{ - { - UID: "test-uid", - Name: "test-name", - Type: "slack", - DisableResolveMessage: false, - Settings: apimodels.RawMessage(`{ "data" : "test" }`), - SecureSettings: map[string]string{ - "test": "data", - }, - }, - }, - }, - }, - { - Receiver: config.Receiver{ - Name: "test-receiver2", - }, - PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{ - { - UID: "test-uid2", - Name: "test-name1", - Type: "slack", - DisableResolveMessage: false, - Settings: apimodels.RawMessage(`{ "data" : "test" }`), - SecureSettings: map[string]string{ - "test": "data", - }, - }, - }, - }, - }, - }, - } - actual := PostableApiAlertingConfigToApiReceivers(c) - - require.Len(t, actual, 2) - require.Equal(t, PostableApiReceiverToApiReceiver(c.Receivers[0]), actual[0]) - require.Equal(t, PostableApiReceiverToApiReceiver(c.Receivers[1]), actual[1]) -} diff --git a/pkg/services/ngalert/notifier/legacy_storage/receivers_test.go b/pkg/services/ngalert/notifier/legacy_storage/receivers_test.go index 72ce37763aa..a020b833de8 100644 --- a/pkg/services/ngalert/notifier/legacy_storage/receivers_test.go +++ b/pkg/services/ngalert/notifier/legacy_storage/receivers_test.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/alerting/definition" "github.com/grafana/alerting/notify" + "github.com/grafana/alerting/notify/notifytest" "github.com/grafana/alerting/receivers/schema" "github.com/grafana/alerting/receivers/webhook" "github.com/prometheus/alertmanager/config" @@ -92,7 +93,7 @@ func TestDeleteReceiver(t *testing.T) { } func TestCreateReceiver(t *testing.T) { - rawCfg := notify.AllKnownConfigsForTesting[string(webhook.Type)] + rawCfg := notifytest.AllKnownV1ConfigsForTesting[webhook.Type] typeSchema, _ := notify.GetSchemaForIntegration(webhook.Type) cfgSchema, err := models.IntegrationConfigFromSchema(typeSchema, schema.V1) require.NoError(t, err) @@ -199,7 +200,7 @@ func TestCreateReceiver(t *testing.T) { } func TestUpdateReceiver(t *testing.T) { - rawCfg := notify.AllKnownConfigsForTesting[string(webhook.Type)] + rawCfg := notifytest.AllKnownV1ConfigsForTesting[webhook.Type] typeSchema, _ := notify.GetSchemaForIntegration(webhook.Type) cfgSchema, err := models.IntegrationConfigFromSchema(typeSchema, schema.V1) require.NoError(t, err) @@ -300,7 +301,7 @@ func TestUpdateReceiver(t *testing.T) { } func TestGetReceiver(t *testing.T) { - rawCfg := notify.AllKnownConfigsForTesting[string(webhook.Type)] + rawCfg := notifytest.AllKnownV1ConfigsForTesting[webhook.Type] typeSchema, _ := notify.GetSchemaForIntegration(webhook.Type) cfgSchema, err := models.IntegrationConfigFromSchema(typeSchema, schema.V1) require.NoError(t, err) @@ -491,7 +492,7 @@ func getConfigRevisionForTest() *ConfigRevision { { UID: "integration-uid-1", Type: "webhook", - Settings: definitions.RawMessage(notify.AllKnownConfigsForTesting["webhook"].Config), + Settings: definitions.RawMessage(notifytest.AllKnownV1ConfigsForTesting["webhook"].Config), }, }, }, @@ -503,7 +504,7 @@ func getConfigRevisionForTest() *ConfigRevision { { UID: "integration-uid-2", Type: "webhook", - Settings: definitions.RawMessage(notify.AllKnownConfigsForTesting["webhook"].Config), + Settings: definitions.RawMessage(notifytest.AllKnownV1ConfigsForTesting["webhook"].Config), }, }, }, @@ -515,7 +516,7 @@ func getConfigRevisionForTest() *ConfigRevision { { UID: "integration-uid-3", Type: "email", - Settings: definitions.RawMessage(notify.AllKnownConfigsForTesting["email"].Config), + Settings: definitions.RawMessage(notifytest.AllKnownV1ConfigsForTesting["email"].Config), }, }, }, diff --git a/pkg/services/ngalert/notifier/receiver_svc_test.go b/pkg/services/ngalert/notifier/receiver_svc_test.go index 5cf9146bb1c..8816124334d 100644 --- a/pkg/services/ngalert/notifier/receiver_svc_test.go +++ b/pkg/services/ngalert/notifier/receiver_svc_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "github.com/grafana/alerting/receivers/line" "github.com/prometheus/alertmanager/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -352,7 +353,7 @@ func TestReceiverService_Create(t *testing.T) { slackIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("slack"))() emailIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("email"))() - lineIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("line"))() + lineIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig(line.Type))() baseReceiver := models.ReceiverGen(models.ReceiverMuts.WithName("test receiver"), models.ReceiverMuts.WithIntegrations(slackIntegration))() for _, tc := range []struct { diff --git a/pkg/services/ngalert/notifier/testreceivers.go b/pkg/services/ngalert/notifier/testreceivers.go index 187caab480b..41bd2a181c7 100644 --- a/pkg/services/ngalert/notifier/testreceivers.go +++ b/pkg/services/ngalert/notifier/testreceivers.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" + "github.com/grafana/alerting/models" alertingNotify "github.com/grafana/alerting/notify" v2 "github.com/prometheus/alertmanager/api/v2" @@ -13,9 +14,9 @@ import ( func (am *alertmanager) TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigBodyParams) (*alertingNotify.TestReceiversResult, int, error) { receivers := make([]*alertingNotify.APIReceiver, 0, len(c.Receivers)) for _, r := range c.Receivers { - integrations := make([]*alertingNotify.GrafanaIntegrationConfig, 0, len(r.GrafanaManagedReceivers)) + integrations := make([]*models.IntegrationConfig, 0, len(r.GrafanaManagedReceivers)) for _, gr := range r.GrafanaManagedReceivers { - integrations = append(integrations, &alertingNotify.GrafanaIntegrationConfig{ + integrations = append(integrations, &models.IntegrationConfig{ UID: gr.UID, Name: gr.Name, Type: gr.Type, @@ -26,7 +27,7 @@ func (am *alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei } recv := &alertingNotify.APIReceiver{ ConfigReceiver: r.Receiver, - GrafanaIntegrations: alertingNotify.GrafanaIntegrations{ + ReceiverConfig: models.ReceiverConfig{ Integrations: integrations, }, } @@ -52,5 +53,5 @@ func (am *alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei } func (am *alertmanager) GetReceivers(_ context.Context) ([]apimodels.Receiver, error) { - return am.Base.GetReceivers(), nil + return am.Base.GetReceiversStatus(), nil } diff --git a/pkg/services/ngalert/notifier/testreceivers_test.go b/pkg/services/ngalert/notifier/testreceivers_test.go index 6ee01b9b9fe..b367a8500ea 100644 --- a/pkg/services/ngalert/notifier/testreceivers_test.go +++ b/pkg/services/ngalert/notifier/testreceivers_test.go @@ -6,13 +6,14 @@ import ( "net/url" "testing" + "github.com/grafana/alerting/models" alertingNotify "github.com/grafana/alerting/notify" "github.com/stretchr/testify/require" ) func TestInvalidReceiverError_Error(t *testing.T) { e := alertingNotify.IntegrationValidationError{ - Integration: &alertingNotify.GrafanaIntegrationConfig{ + Integration: &models.IntegrationConfig{ Name: "test", Type: "test-type", UID: "uid", @@ -24,7 +25,7 @@ func TestInvalidReceiverError_Error(t *testing.T) { func TestReceiverTimeoutError_Error(t *testing.T) { e := alertingNotify.IntegrationTimeoutError{ - Integration: &alertingNotify.GrafanaIntegrationConfig{ + Integration: &models.IntegrationConfig{ Name: "test", UID: "uid", }, @@ -45,7 +46,7 @@ func (e timeoutError) Timeout() bool { func TestProcessNotifierError(t *testing.T) { t.Run("assert ReceiverTimeoutError is returned for context deadline exceeded", func(t *testing.T) { - r := &alertingNotify.GrafanaIntegrationConfig{ + r := &models.IntegrationConfig{ Name: "test", UID: "uid", } @@ -56,7 +57,7 @@ func TestProcessNotifierError(t *testing.T) { }) t.Run("assert ReceiverTimeoutError is returned for *url.Error timeout", func(t *testing.T) { - r := &alertingNotify.GrafanaIntegrationConfig{ + r := &models.IntegrationConfig{ Name: "test", UID: "uid", } @@ -72,7 +73,7 @@ func TestProcessNotifierError(t *testing.T) { }) t.Run("assert unknown error is returned unmodified", func(t *testing.T) { - r := &alertingNotify.GrafanaIntegrationConfig{ + r := &models.IntegrationConfig{ Name: "test", UID: "uid", } diff --git a/pkg/services/ngalert/provisioning/compat.go b/pkg/services/ngalert/provisioning/compat.go index 13d0bba88a9..a4b3ea50922 100644 --- a/pkg/services/ngalert/provisioning/compat.go +++ b/pkg/services/ngalert/provisioning/compat.go @@ -3,19 +3,19 @@ package provisioning import ( "strings" - alertingNotify "github.com/grafana/alerting/notify" + alertingModels "github.com/grafana/alerting/models" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" ) -func EmbeddedContactPointToGrafanaIntegrationConfig(e definitions.EmbeddedContactPoint) (alertingNotify.GrafanaIntegrationConfig, error) { +func EmbeddedContactPointToGrafanaIntegrationConfig(e definitions.EmbeddedContactPoint) (alertingModels.IntegrationConfig, error) { data, err := e.Settings.MarshalJSON() if err != nil { - return alertingNotify.GrafanaIntegrationConfig{}, err + return alertingModels.IntegrationConfig{}, err } - return alertingNotify.GrafanaIntegrationConfig{ + return alertingModels.IntegrationConfig{ UID: e.UID, Name: e.Name, Type: e.Type, diff --git a/pkg/services/ngalert/provisioning/contactpoints_test.go b/pkg/services/ngalert/provisioning/contactpoints_test.go index b85dfcd3108..1a281f912b6 100644 --- a/pkg/services/ngalert/provisioning/contactpoints_test.go +++ b/pkg/services/ngalert/provisioning/contactpoints_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/grafana/alerting/notify" + "github.com/grafana/alerting/notify/notifytest" "github.com/grafana/alerting/receivers/schema" "github.com/prometheus/alertmanager/config" "github.com/stretchr/testify/assert" @@ -425,7 +426,7 @@ func TestIntegrationContactPointServiceDecryptRedact(t *testing.T) { } func TestRemoveSecretsForContactPoint(t *testing.T) { - overrides := map[string]func(settings map[string]any){ + overrides := map[schema.IntegrationType]func(settings map[string]any){ "webhook": func(settings map[string]any) { // add additional field to the settings because valid config does not allow it to be specified along with password settings["authorization_credentials"] = "test-authz-creds" }, @@ -437,23 +438,23 @@ func TestRemoveSecretsForContactPoint(t *testing.T) { }, } - configs := notify.AllKnownConfigsForTesting + configs := notifytest.AllKnownV1ConfigsForTesting keys := maps.Keys(configs) slices.Sort(keys) for _, integrationType := range keys { - integration := models.IntegrationGen(models.IntegrationMuts.WithValidConfig(schema.IntegrationType(integrationType)))() + integration := models.IntegrationGen(models.IntegrationMuts.WithValidConfig(integrationType))() if f, ok := overrides[integrationType]; ok { f(integration.Settings) } settingsRaw, err := json.Marshal(integration.Settings) require.NoError(t, err) - typeSchema, _ := notify.GetSchemaVersionForIntegration(schema.IntegrationType(integrationType), schema.V1) + typeSchema, _ := notify.GetSchemaVersionForIntegration(integrationType, schema.V1) expectedFields := typeSchema.GetSecretFieldsPaths() - t.Run(integrationType, func(t *testing.T) { + t.Run(string(integrationType), func(t *testing.T) { cp := definitions.EmbeddedContactPoint{ - Name: "integration-" + integrationType, - Type: integrationType, + Name: "integration-" + string(integrationType), + Type: string(integrationType), Settings: simplejson.MustJson(settingsRaw), } secureFields, err := RemoveSecretsForContactPoint(&cp) diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 7048810195f..8cde9ee198f 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -609,10 +609,7 @@ func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei return nil, 0, fmt.Errorf("failed to decrypt receivers: %w", err) } - apiReceivers := make([]*alertingNotify.APIReceiver, 0, len(c.Receivers)) - for _, r := range decryptedReceivers { - apiReceivers = append(apiReceivers, notifier.PostableApiReceiverToApiReceiver(r)) - } + apiReceivers := alertingNotify.PostableAPIReceiversToAPIReceivers(decryptedReceivers) var alert *alertingNotify.TestReceiversConfigAlertParams if c.Alert != nil { alert = &alertingNotify.TestReceiversConfigAlertParams{Annotations: c.Alert.Annotations, Labels: c.Alert.Labels} diff --git a/pkg/services/ngalert/remote/mock/remoteAlertmanager.go b/pkg/services/ngalert/remote/mock/remoteAlertmanager.go index 1199b65ae9c..8f8e506c765 100644 --- a/pkg/services/ngalert/remote/mock/remoteAlertmanager.go +++ b/pkg/services/ngalert/remote/mock/remoteAlertmanager.go @@ -358,23 +358,23 @@ func (_c *RemoteAlertmanagerMock_GetAlerts_Call) RunAndReturn(run func(context.C } // GetReceivers provides a mock function with given fields: ctx -func (_m *RemoteAlertmanagerMock) GetReceivers(ctx context.Context) ([]alertingmodels.Receiver, error) { +func (_m *RemoteAlertmanagerMock) GetReceivers(ctx context.Context) ([]alertingmodels.ReceiverStatus, error) { ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for GetReceivers") } - var r0 []alertingmodels.Receiver + var r0 []alertingmodels.ReceiverStatus var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]alertingmodels.Receiver, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context) ([]alertingmodels.ReceiverStatus, error)); ok { return rf(ctx) } - if rf, ok := ret.Get(0).(func(context.Context) []alertingmodels.Receiver); ok { + if rf, ok := ret.Get(0).(func(context.Context) []alertingmodels.ReceiverStatus); ok { r0 = rf(ctx) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]alertingmodels.Receiver) + r0 = ret.Get(0).([]alertingmodels.ReceiverStatus) } } @@ -405,12 +405,12 @@ func (_c *RemoteAlertmanagerMock_GetReceivers_Call) Run(run func(ctx context.Con return _c } -func (_c *RemoteAlertmanagerMock_GetReceivers_Call) Return(_a0 []alertingmodels.Receiver, _a1 error) *RemoteAlertmanagerMock_GetReceivers_Call { +func (_c *RemoteAlertmanagerMock_GetReceivers_Call) Return(_a0 []alertingmodels.ReceiverStatus, _a1 error) *RemoteAlertmanagerMock_GetReceivers_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *RemoteAlertmanagerMock_GetReceivers_Call) RunAndReturn(run func(context.Context) ([]alertingmodels.Receiver, error)) *RemoteAlertmanagerMock_GetReceivers_Call { +func (_c *RemoteAlertmanagerMock_GetReceivers_Call) RunAndReturn(run func(context.Context) ([]alertingmodels.ReceiverStatus, error)) *RemoteAlertmanagerMock_GetReceivers_Call { _c.Call.Return(run) return _c } diff --git a/pkg/services/provisioning/alerting/rules_types.go b/pkg/services/provisioning/alerting/rules_types.go index b5b28072beb..bac091ababa 100644 --- a/pkg/services/provisioning/alerting/rules_types.go +++ b/pkg/services/provisioning/alerting/rules_types.go @@ -151,10 +151,6 @@ func (rule *AlertRuleV1) mapToModel(orgID int64) (models.AlertRule, error) { noDataState = models.NoData } alertRule.NoDataState = noDataState - alertRule.Condition = rule.Condition.Value() - if alertRule.Condition == "" { - return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: no condition set", alertRule.Title) - } alertRule.Annotations = rule.Annotations.Raw alertRule.Labels = rule.Labels.Value() for _, queryV1 := range rule.Data { @@ -182,6 +178,10 @@ func (rule *AlertRuleV1) mapToModel(orgID int64) (models.AlertRule, error) { } alertRule.Record = &record } + alertRule.Condition = rule.Condition.Value() + if alertRule.Condition == "" && alertRule.Record == nil { + return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: no condition set", alertRule.Title) + } return alertRule, nil } diff --git a/pkg/services/provisioning/alerting/rules_types_test.go b/pkg/services/provisioning/alerting/rules_types_test.go index 16c510707b9..9c4e819ff73 100644 --- a/pkg/services/provisioning/alerting/rules_types_test.go +++ b/pkg/services/provisioning/alerting/rules_types_test.go @@ -202,6 +202,14 @@ func TestRules(t *testing.T) { }) } +func TestRecordingRules(t *testing.T) { + t.Run("a valid rule should not error", func(t *testing.T) { + rule := validRecordingRuleV1(t) + _, err := rule.mapToModel(1) + require.NoError(t, err) + }) +} + func TestNotificationsSettingsV1MapToModel(t *testing.T) { tests := []struct { name string @@ -347,6 +355,37 @@ func validRuleV1(t *testing.T) AlertRuleV1 { } } +func validRecordingRuleV1(t *testing.T) AlertRuleV1 { + t.Helper() + var ( + title values.StringValue + uid values.StringValue + forDuration values.StringValue + metric values.StringValue + from values.StringValue + ) + err := yaml.Unmarshal([]byte("test"), &title) + require.NoError(t, err) + err = yaml.Unmarshal([]byte("test_uid"), &uid) + require.NoError(t, err) + err = yaml.Unmarshal([]byte("10s"), &forDuration) + require.NoError(t, err) + err = yaml.Unmarshal([]byte("test_metric"), &metric) + require.NoError(t, err) + err = yaml.Unmarshal([]byte("A"), &from) + require.NoError(t, err) + return AlertRuleV1{ + Title: title, + UID: uid, + For: forDuration, + Record: &RecordV1{ + Metric: metric, + From: from, + }, + Data: []QueryV1{{}}, + } +} + func stringToStringValue(s string) values.StringValue { result := values.StringValue{} err := yaml.Unmarshal([]byte(s), &result) diff --git a/pkg/services/team/team.go b/pkg/services/team/team.go index 49db0c53734..8d55fb908a8 100644 --- a/pkg/services/team/team.go +++ b/pkg/services/team/team.go @@ -19,7 +19,7 @@ type Service interface { GetTeamIDsByUser(ctx context.Context, query *GetTeamIDsByUserQuery) ([]int64, error) IsTeamMember(ctx context.Context, orgId int64, teamId int64, userId int64) (bool, error) RemoveUsersMemberships(tx context.Context, userID int64) error - GetUserTeamMemberships(ctx context.Context, orgID, userID int64, external bool) ([]*TeamMemberDTO, error) + GetUserTeamMemberships(ctx context.Context, orgID, userID int64, external bool, bypassCache bool) ([]*TeamMemberDTO, error) GetTeamMembers(ctx context.Context, query *GetTeamMembersQuery) ([]*TeamMemberDTO, error) RegisterDelete(query string) } diff --git a/pkg/services/team/teamimpl/team.go b/pkg/services/team/teamimpl/team.go index 245ee98ddae..94fcf6d2186 100644 --- a/pkg/services/team/teamimpl/team.go +++ b/pkg/services/team/teamimpl/team.go @@ -114,18 +114,20 @@ func (s *Service) RemoveUsersMemberships(ctx context.Context, userID int64) erro return s.store.RemoveUsersMemberships(ctx, userID) } -func (s *Service) GetUserTeamMemberships(ctx context.Context, orgID, userID int64, external bool) ([]*team.TeamMemberDTO, error) { +func (s *Service) GetUserTeamMemberships(ctx context.Context, orgID, userID int64, external bool, bypassCache bool) ([]*team.TeamMemberDTO, error) { ctx, span := s.tracer.Start(ctx, "team.GetUserTeamMemberships", trace.WithAttributes( attribute.Int64("orgID", orgID), attribute.Int64("userID", userID), )) defer span.End() cacheKey := fmt.Sprintf("teams:%d:%d:%t", orgID, userID, external) - if cached, found := s.cache.Get(cacheKey); found { - if teams, ok := cached.([]*team.TeamMemberDTO); ok { - return teams, nil + if !bypassCache { + if cached, found := s.cache.Get(cacheKey); found { + if teams, ok := cached.([]*team.TeamMemberDTO); ok { + return teams, nil + } + s.cache.Delete(cacheKey) } - s.cache.Delete(cacheKey) } teams, err := s.store.GetMemberships(ctx, orgID, userID, external) if err != nil { @@ -137,7 +139,9 @@ func (s *Service) GetUserTeamMemberships(ctx context.Context, orgID, userID int6 return []*team.TeamMemberDTO{}, nil } - s.cache.Set(cacheKey, teams, defaultCacheDuration) + if !bypassCache { + s.cache.Set(cacheKey, teams, defaultCacheDuration) + } return teams, nil } diff --git a/pkg/services/team/teamtest/team.go b/pkg/services/team/teamtest/team.go index 3caefb9562e..3b88d05d5d3 100644 --- a/pkg/services/team/teamtest/team.go +++ b/pkg/services/team/teamtest/team.go @@ -58,7 +58,7 @@ func (s *FakeService) RemoveUsersMemberships(ctx context.Context, userID int64) return s.ExpectedError } -func (s *FakeService) GetUserTeamMemberships(ctx context.Context, orgID, userID int64, external bool) ([]*team.TeamMemberDTO, error) { +func (s *FakeService) GetUserTeamMemberships(ctx context.Context, orgID, userID int64, external bool, bypassCache bool) ([]*team.TeamMemberDTO, error) { return s.ExpectedMembers, s.ExpectedError } diff --git a/pkg/storage/secret/encryption/data/encrypted_value_create.sql b/pkg/storage/secret/encryption/data/encrypted_value_create.sql index 7bce6a19bd4..7aa749e01a3 100644 --- a/pkg/storage/secret/encryption/data/encrypted_value_create.sql +++ b/pkg/storage/secret/encryption/data/encrypted_value_create.sql @@ -3,6 +3,7 @@ INSERT INTO {{ .Ident "secret_encrypted_value" }} ( {{ .Ident "name" }}, {{ .Ident "version" }}, {{ .Ident "encrypted_data" }}, + {{ .Ident "data_key_id" }}, {{ .Ident "created" }}, {{ .Ident "updated" }} ) VALUES ( @@ -10,6 +11,7 @@ INSERT INTO {{ .Ident "secret_encrypted_value" }} ( {{ .Arg .Row.Name }}, {{ .Arg .Row.Version }}, {{ .Arg .Row.EncryptedData }}, + {{ .Arg .Row.DataKeyID }}, {{ .Arg .Row.Created }}, {{ .Arg .Row.Updated }} ); diff --git a/pkg/storage/secret/encryption/data/encrypted_value_list_all.sql b/pkg/storage/secret/encryption/data/encrypted_value_list_all.sql index d318517346d..2a0446a3a9d 100644 --- a/pkg/storage/secret/encryption/data/encrypted_value_list_all.sql +++ b/pkg/storage/secret/encryption/data/encrypted_value_list_all.sql @@ -3,6 +3,7 @@ SELECT {{ .Ident "name" }}, {{ .Ident "version" }}, {{ .Ident "encrypted_data" }}, + {{ .Ident "data_key_id" }}, {{ .Ident "created" }}, {{ .Ident "updated" }} FROM diff --git a/pkg/storage/secret/encryption/data/encrypted_value_read.sql b/pkg/storage/secret/encryption/data/encrypted_value_read.sql index 6a672554efb..3ff20a641f0 100644 --- a/pkg/storage/secret/encryption/data/encrypted_value_read.sql +++ b/pkg/storage/secret/encryption/data/encrypted_value_read.sql @@ -3,6 +3,7 @@ SELECT {{ .Ident "name" }}, {{ .Ident "version" }}, {{ .Ident "encrypted_data" }}, + {{ .Ident "data_key_id" }}, {{ .Ident "created" }}, {{ .Ident "updated" }} FROM diff --git a/pkg/storage/secret/encryption/data/encrypted_value_update.sql b/pkg/storage/secret/encryption/data/encrypted_value_update.sql index 08e985297a1..c939c092c1c 100644 --- a/pkg/storage/secret/encryption/data/encrypted_value_update.sql +++ b/pkg/storage/secret/encryption/data/encrypted_value_update.sql @@ -2,6 +2,7 @@ UPDATE {{ .Ident "secret_encrypted_value" }} SET {{ .Ident "encrypted_data" }} = {{ .Arg .EncryptedData }}, + {{ .Ident "data_key_id" }} = {{ .Arg .DataKeyID }}, {{ .Ident "updated" }} = {{ .Arg .Updated }} WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND diff --git a/pkg/storage/secret/encryption/encrypted_value_model.go b/pkg/storage/secret/encryption/encrypted_value_model.go index eb66247c021..3c4ba7be905 100644 --- a/pkg/storage/secret/encryption/encrypted_value_model.go +++ b/pkg/storage/secret/encryption/encrypted_value_model.go @@ -6,6 +6,7 @@ type EncryptedValue struct { Namespace string Name string Version int64 + DataKeyID string EncryptedData []byte Created int64 Updated int64 diff --git a/pkg/storage/secret/encryption/encrypted_value_store.go b/pkg/storage/secret/encryption/encrypted_value_store.go index 3a10d01b957..af48a651868 100644 --- a/pkg/storage/secret/encryption/encrypted_value_store.go +++ b/pkg/storage/secret/encryption/encrypted_value_store.go @@ -1,7 +1,9 @@ package encryption import ( + "bytes" "context" + "encoding/base64" "errors" "fmt" "time" @@ -10,6 +12,7 @@ import ( "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/storage/unified/sql" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) @@ -37,9 +40,9 @@ type encryptedValStorage struct { tracer trace.Tracer } -func (s *encryptedValStorage) Create(ctx context.Context, namespace, name string, version int64, encryptedData []byte) (ev *contracts.EncryptedValue, err error) { +func (s *encryptedValStorage) Create(ctx context.Context, namespace xkube.Namespace, name string, version int64, encryptedData contracts.EncryptedPayload) (ev *contracts.EncryptedValue, err error) { ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Create", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), )) defer span.End() @@ -56,10 +59,11 @@ func (s *encryptedValStorage) Create(ctx context.Context, namespace, name string createdTime := time.Now().Unix() encryptedValue := &EncryptedValue{ - Namespace: namespace, + Namespace: namespace.String(), Name: name, Version: version, - EncryptedData: encryptedData, + EncryptedData: encryptedData.EncryptedData, + DataKeyID: encryptedData.DataKeyID, Created: createdTime, Updated: createdTime, } @@ -88,18 +92,21 @@ func (s *encryptedValStorage) Create(ctx context.Context, namespace, name string } return &contracts.EncryptedValue{ - Namespace: encryptedValue.Namespace, - Name: encryptedValue.Name, - Version: encryptedValue.Version, - EncryptedData: encryptedValue.EncryptedData, - Created: encryptedValue.Created, - Updated: encryptedValue.Updated, + Namespace: encryptedValue.Namespace, + Name: encryptedValue.Name, + Version: encryptedValue.Version, + EncryptedPayload: contracts.EncryptedPayload{ + DataKeyID: encryptedValue.DataKeyID, + EncryptedData: encryptedValue.EncryptedData, + }, + Created: encryptedValue.Created, + Updated: encryptedValue.Updated, }, nil } -func (s *encryptedValStorage) Update(ctx context.Context, namespace, name string, version int64, encryptedData []byte) error { +func (s *encryptedValStorage) Update(ctx context.Context, namespace xkube.Namespace, name string, version int64, encryptedData contracts.EncryptedPayload) error { ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Update", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version), )) @@ -107,10 +114,11 @@ func (s *encryptedValStorage) Update(ctx context.Context, namespace, name string req := updateEncryptedValue{ SQLTemplate: sqltemplate.New(s.dialect), - Namespace: namespace, + Namespace: namespace.String(), Name: name, Version: version, - EncryptedData: encryptedData, + EncryptedData: encryptedData.EncryptedData, + DataKeyID: encryptedData.DataKeyID, Updated: time.Now().Unix(), } @@ -133,9 +141,9 @@ func (s *encryptedValStorage) Update(ctx context.Context, namespace, name string return nil } -func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, version int64) (*contracts.EncryptedValue, error) { +func (s *encryptedValStorage) Get(ctx context.Context, namespace xkube.Namespace, name string, version int64) (*contracts.EncryptedValue, error) { ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Get", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version), )) @@ -143,7 +151,7 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, v req := &readEncryptedValue{ SQLTemplate: sqltemplate.New(s.dialect), - Namespace: namespace, + Namespace: namespace.String(), Name: name, Version: version, } @@ -163,7 +171,7 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, v } var encryptedValue EncryptedValue - err = rows.Scan(&encryptedValue.Namespace, &encryptedValue.Name, &encryptedValue.Version, &encryptedValue.EncryptedData, &encryptedValue.Created, &encryptedValue.Updated) + err = rows.Scan(&encryptedValue.Namespace, &encryptedValue.Name, &encryptedValue.Version, &encryptedValue.EncryptedData, &encryptedValue.DataKeyID, &encryptedValue.Created, &encryptedValue.Updated) if err != nil { return nil, fmt.Errorf("failed to scan encrypted value row: %w", err) } @@ -172,18 +180,21 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, v } return &contracts.EncryptedValue{ - Namespace: encryptedValue.Namespace, - Name: encryptedValue.Name, - Version: encryptedValue.Version, - EncryptedData: encryptedValue.EncryptedData, - Created: encryptedValue.Created, - Updated: encryptedValue.Updated, + Namespace: encryptedValue.Namespace, + Name: encryptedValue.Name, + Version: encryptedValue.Version, + EncryptedPayload: contracts.EncryptedPayload{ + DataKeyID: encryptedValue.DataKeyID, + EncryptedData: encryptedValue.EncryptedData, + }, + Created: encryptedValue.Created, + Updated: encryptedValue.Updated, }, nil } -func (s *encryptedValStorage) Delete(ctx context.Context, namespace, name string, version int64) error { +func (s *encryptedValStorage) Delete(ctx context.Context, namespace xkube.Namespace, name string, version int64) error { ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Delete", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version), )) @@ -191,7 +202,7 @@ func (s *encryptedValStorage) Delete(ctx context.Context, namespace, name string req := deleteEncryptedValue{ SQLTemplate: sqltemplate.New(s.dialect), - Namespace: namespace, + Namespace: namespace.String(), Name: name, Version: version, } @@ -264,6 +275,7 @@ func (s *globalEncryptedValStorage) ListAll(ctx context.Context, opts contracts. &row.Name, &row.Version, &row.EncryptedData, + &row.DataKeyID, &row.Created, &row.Updated, ) @@ -272,12 +284,15 @@ func (s *globalEncryptedValStorage) ListAll(ctx context.Context, opts contracts. } encryptedValues = append(encryptedValues, &contracts.EncryptedValue{ - Namespace: row.Namespace, - Name: row.Name, - Version: row.Version, - EncryptedData: row.EncryptedData, - Created: row.Created, - Updated: row.Updated, + Namespace: row.Namespace, + Name: row.Name, + Version: row.Version, + EncryptedPayload: contracts.EncryptedPayload{ + DataKeyID: row.DataKeyID, + EncryptedData: row.EncryptedData, + }, + Created: row.Created, + Updated: row.Updated, }) } if err := rows.Err(); err != nil { @@ -329,3 +344,77 @@ func (s *globalEncryptedValStorage) CountAll(ctx context.Context, untilTime *int return count, nil } + +type encryptedValMigrationExecutor struct { + db contracts.Database + dialect sqltemplate.Dialect + tracer trace.Tracer + encryptedValueStore contracts.EncryptedValueStorage + globalStore contracts.GlobalEncryptedValueStorage +} + +func ProvideEncryptedValueMigrationExecutor( + db contracts.Database, + tracer trace.Tracer, + encryptedValueStore contracts.EncryptedValueStorage, + globalStore contracts.GlobalEncryptedValueStorage, +) (contracts.EncryptedValueMigrationExecutor, error) { + return &encryptedValMigrationExecutor{ + db: db, + dialect: sqltemplate.DialectForDriver(db.DriverName()), + tracer: tracer, + encryptedValueStore: encryptedValueStore, + globalStore: globalStore, + }, nil +} + +func (s *encryptedValMigrationExecutor) Execute(ctx context.Context) (int, error) { + ctx, span := s.tracer.Start(ctx, "EncryptedValueMigrationExecutor.Execute") + defer span.End() + + // 1. Retrieve all encrypted values + encryptedValues, err := s.globalStore.ListAll(ctx, contracts.ListOpts{}, nil) + if err != nil { + return 0, fmt.Errorf("listing all encrypted values: %w", err) + } + + // This doesn't need to be done in a single transaction because there's no risk to successful rows if other rows fail + rowsAffected := 0 + for _, encryptedValue := range encryptedValues { + // 2. If the value already has the data key id broken out, skip it + if encryptedValue.DataKeyID != "" { + continue + } + + // 3. Split the data key id and the encrypted data out from the encoded payload + payload := encryptedValue.EncryptedData + const keyIdDelimiter = '#' + payload = payload[1:] + endOfKey := bytes.Index(payload, []byte{keyIdDelimiter}) + if endOfKey == -1 { + return rowsAffected, fmt.Errorf("could not find valid key id in encrypted payload with namespace %s and name %s and version %d", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version) + } + b64Key := payload[:endOfKey] + encryptedData := payload[endOfKey+1:] + if len(encryptedData) == 0 { + return rowsAffected, fmt.Errorf("encrypted data is empty with namespace %s and name %s and version %d", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version) + } + keyId := make([]byte, base64.RawStdEncoding.DecodedLen(len(b64Key))) + _, err := base64.RawStdEncoding.Decode(keyId, b64Key) + if err != nil { + return rowsAffected, fmt.Errorf("decoding key id with namespace %s and name %s and version %d: %w", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version, err) + } + + // 4. Update the encrypted value with the data key id and the encrypted data + err = s.encryptedValueStore.Update(ctx, xkube.Namespace(encryptedValue.Namespace), encryptedValue.Name, encryptedValue.Version, contracts.EncryptedPayload{ + DataKeyID: string(keyId), + EncryptedData: encryptedData, + }) + if err != nil { + return rowsAffected, fmt.Errorf("updating encrypted value with namespace %s and name %s and version %d: %w", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version, err) + } + rowsAffected++ + } + + return rowsAffected, nil +} diff --git a/pkg/storage/secret/encryption/encrypted_value_store_test.go b/pkg/storage/secret/encryption/encrypted_value_store_test.go index 620a034f22c..2f84e1f8575 100644 --- a/pkg/storage/secret/encryption/encrypted_value_store_test.go +++ b/pkg/storage/secret/encryption/encrypted_value_store_test.go @@ -2,15 +2,25 @@ package encryption_test import ( "bytes" + "context" + "encoding/base64" "errors" + "fmt" "slices" "testing" + "text/template" "time" + "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" + cipherService "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service" "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/storage/secret/encryption" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" "pgregory.net/rapid" ) @@ -21,7 +31,10 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("test-data")) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) require.NotEmpty(t, createdEV.Namespace) require.NotEmpty(t, createdEV.Name) @@ -36,10 +49,13 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("test-data")) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) - obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) + obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version) require.NoError(t, err) require.Equal(t, createdEV.Namespace, obtainedEV.Namespace) @@ -47,6 +63,7 @@ func TestEncryptedValueStoreImpl(t *testing.T) { require.Equal(t, createdEV.Created, obtainedEV.Created) require.Equal(t, createdEV.Updated, obtainedEV.Updated) require.Equal(t, createdEV.EncryptedData, obtainedEV.EncryptedData) + require.Equal(t, createdEV.DataKeyID, obtainedEV.DataKeyID) require.Equal(t, createdEV.Namespace, obtainedEV.Namespace) }) @@ -54,7 +71,10 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "ns1", "test-name", 1, []byte("test-data")) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "ns1", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), "ns2", createdEV.Name, createdEV.Version) @@ -78,16 +98,23 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("test-data")) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) - err = sut.EncryptedValueStorage.Update(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version, []byte("test-data-updated")) + err = sut.EncryptedValueStorage.Update(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id-updated", + EncryptedData: []byte("test-data-updated"), + }) require.NoError(t, err) - updatedEV, err := sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) + updatedEV, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version) require.NoError(t, err) require.Equal(t, []byte("test-data-updated"), updatedEV.EncryptedData) + require.Equal(t, "test-data-key-id-updated", updatedEV.DataKeyID) require.Equal(t, createdEV.Created, updatedEV.Created) require.Equal(t, createdEV.Namespace, updatedEV.Namespace) }) @@ -96,7 +123,10 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - err := sut.EncryptedValueStorage.Update(t.Context(), "test-namespace", "test-uid", 1, []byte("test-data")) + err := sut.EncryptedValueStorage.Update(t.Context(), "test-namespace", "test-uid", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.Error(t, err) }) @@ -104,16 +134,19 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("ttttest-data")) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("ttttest-data"), + }) require.NoError(t, err) - _, err = sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) + _, err = sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version) require.NoError(t, err) - err = sut.EncryptedValueStorage.Delete(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) + err = sut.EncryptedValueStorage.Delete(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version) require.NoError(t, err) - obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) + obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version) require.Error(t, err) require.Nil(t, obtainedEV) }) @@ -130,10 +163,16 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - createdEvA, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-a", "test-name", 1, []byte("test-data")) + createdEvA, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-a", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) - createdEvB, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-b", "test-name", 1, []byte("test-data")) + createdEvB, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-b", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) // List all encrypted values, without pagination @@ -180,10 +219,16 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - _, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-a", "test-name", 1, []byte("test-data")) + _, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-a", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) - _, err = sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-b", "test-name", 1, []byte("test-data")) + _, err = sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-b", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) count, err := sut.GlobalEncryptedValueStorage.CountAll(t.Context(), nil) @@ -198,6 +243,281 @@ func TestEncryptedValueStoreImpl(t *testing.T) { }) } +func TestEncryptedValueMigration(t *testing.T) { + t.Parallel() + + t.Run("golden path - successful migration of legacy format", func(t *testing.T) { + t.Parallel() + + sut := testutils.Setup(t) + tracer := noop.NewTracerProvider().Tracer("test") + usageStats := &usagestats.UsageStatsMock{T: t} + enc, err := cipherService.ProvideAESGCMCipherService(tracer, usageStats) + require.NoError(t, err) + + testCases := []struct { + namespace string + name string + version int64 + plaintext string + dataKeyId string + }{ + { + namespace: "test-namespace-1", + name: "test-name-1", + version: 1, + plaintext: "test-plaintext-1", + dataKeyId: "test-data-key-id-1", + }, + { + namespace: "test-namespace-1", + name: "test-name-2", + version: 1, + plaintext: "test-plaintext-2", + dataKeyId: "test-data-key-id-1", + }, + { + namespace: "test-namespace-2", + name: "test-name-3", + version: 1, + plaintext: "test-plaintext-3", + dataKeyId: "test-data-key-id-2", + }, + } + + // Seed with data in the legacy format + for _, tc := range testCases { + err := createLegacyEncryptedData(t, sut, enc, tc.namespace, tc.name, tc.version, tc.plaintext, tc.dataKeyId) + require.NoError(t, err) + } + + // Run the migration and blindy trust it + rowsAffected, err := sut.EncryptedValueMigrationExecutor.Execute(t.Context()) + require.NoError(t, err) + require.Equal(t, len(testCases), rowsAffected) + + // Now validate that the data is in the new format + encryptedValues, err := sut.GlobalEncryptedValueStorage.ListAll(t.Context(), contracts.ListOpts{}, nil) + require.NoError(t, err) + require.Len(t, encryptedValues, 3) + + for _, tc := range testCases { + ev, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(tc.namespace), tc.name, tc.version) + require.NoError(t, err) + + // Decrypt the encrypted data and check for equality + decrypted, err := enc.Decrypt(t.Context(), ev.EncryptedData, tc.dataKeyId) + require.NoError(t, err) + require.Equal(t, tc.dataKeyId, ev.DataKeyID) + require.Equal(t, tc.plaintext, string(decrypted)) + } + }) + + t.Run("error conditions - handles corrupt data gracefully", func(t *testing.T) { + t.Parallel() + + tracer := noop.NewTracerProvider().Tracer("test") + sut := testutils.Setup(t) + + t.Run("global store list error", func(t *testing.T) { + mockGlobalStore := &mockGlobalEncryptedValueStorage{ + listAllError: errors.New("database connection failed"), + } + + migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor( + sut.Database, + tracer, + sut.EncryptedValueStorage, + mockGlobalStore, + ) + require.NoError(t, err) + + rowsAffected, err := migrationExecutor.Execute(t.Context()) + require.Error(t, err) + require.Contains(t, err.Error(), "listing all encrypted values") + require.Equal(t, 0, rowsAffected) + }) + + t.Run("corrupt data - missing key delimiter", func(t *testing.T) { + mockGlobalStore := &mockGlobalEncryptedValueStorage{ + encryptedValues: []*contracts.EncryptedValue{ + { + Namespace: "test-ns", + Name: "test-name", + Version: 1, + EncryptedPayload: contracts.EncryptedPayload{ + EncryptedData: []byte("corrupt-data-without-delimiter"), + DataKeyID: "", // Empty to trigger migration + }, + }, + }, + } + + migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor( + sut.Database, + tracer, + sut.EncryptedValueStorage, + mockGlobalStore, + ) + require.NoError(t, err) + + rowsAffected, err := migrationExecutor.Execute(t.Context()) + require.Error(t, err) + require.Contains(t, err.Error(), "could not find valid key id in encrypted payload") + require.Equal(t, 0, rowsAffected) + }) + + t.Run("corrupt data - empty encrypted data", func(t *testing.T) { + mockGlobalStore := &mockGlobalEncryptedValueStorage{ + encryptedValues: []*contracts.EncryptedValue{ + { + Namespace: "test-ns", + Name: "test-name", + Version: 1, + EncryptedPayload: contracts.EncryptedPayload{ + EncryptedData: []byte("#dGVzdA#"), // Valid key but no encrypted data after delimiter + DataKeyID: "", // Empty to trigger migration + }, + }, + }, + } + + migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor( + sut.Database, + tracer, + sut.EncryptedValueStorage, + mockGlobalStore, + ) + require.NoError(t, err) + + rowsAffected, err := migrationExecutor.Execute(t.Context()) + require.Error(t, err) + require.Contains(t, err.Error(), "encrypted data is empty") + require.Equal(t, 0, rowsAffected) + }) + + t.Run("corrupt data - invalid base64 key", func(t *testing.T) { + mockGlobalStore := &mockGlobalEncryptedValueStorage{ + encryptedValues: []*contracts.EncryptedValue{ + { + Namespace: "test-ns", + Name: "test-name", + Version: 1, + EncryptedPayload: contracts.EncryptedPayload{ + EncryptedData: []byte("#invalid-base64!@#$%^&*()#somedata"), + DataKeyID: "", // Empty to trigger migration + }, + }, + }, + } + + migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor( + sut.Database, + tracer, + sut.EncryptedValueStorage, + mockGlobalStore, + ) + require.NoError(t, err) + + rowsAffected, err := migrationExecutor.Execute(t.Context()) + require.Error(t, err) + require.Contains(t, err.Error(), "decoding key id") + require.Equal(t, 0, rowsAffected) + }) + + t.Run("update failure", func(t *testing.T) { + mockGlobalStore := &mockGlobalEncryptedValueStorage{ + encryptedValues: []*contracts.EncryptedValue{ + { + Namespace: "nonexistent-ns", + Name: "nonexistent-name", + Version: 999, + EncryptedPayload: contracts.EncryptedPayload{ + EncryptedData: []byte("#dGVzdA#someencrypteddata"), + DataKeyID: "", // Empty to trigger migration + }, + }, + }, + } + + migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor( + sut.Database, + tracer, + sut.EncryptedValueStorage, + mockGlobalStore, + ) + require.NoError(t, err) + + rowsAffected, err := migrationExecutor.Execute(t.Context()) + require.Error(t, err) + require.Contains(t, err.Error(), "updating encrypted value") + require.Equal(t, 0, rowsAffected) + }) + }) +} + +// Helper function that bypasses interfaces and creates data in the legacy format directly in the database. +// The format is "#{encoded_key_id}#{encrypted_data}". +func createLegacyEncryptedData(t *testing.T, sut testutils.Sut, enc cipher.Cipher, namespace, name string, version int64, plaintext string, dataKeyId string) error { + t.Helper() + + encryptedData, err := enc.Encrypt(t.Context(), []byte(plaintext), dataKeyId) + require.NoError(t, err) + + // Encode using the legacy format + const keyIdDelimiter = '#' + prefix := make([]byte, base64.RawStdEncoding.EncodedLen(len(dataKeyId))+2) + base64.RawStdEncoding.Encode(prefix[1:], []byte(dataKeyId)) + prefix[0] = keyIdDelimiter + prefix[len(prefix)-1] = keyIdDelimiter + + blob := make([]byte, len(prefix)+len(encryptedData)) + copy(blob, prefix) + copy(blob[len(prefix):], encryptedData) + + createdTime := time.Now().Unix() + + encryptedValue := &encryption.EncryptedValue{ + Namespace: namespace, + Name: name, + Version: version, + EncryptedData: blob, + DataKeyID: "", + Created: createdTime, + Updated: createdTime, + } + + req := struct { + sqltemplate.SQLTemplate + Row *encryption.EncryptedValue + }{ + SQLTemplate: sqltemplate.New(sqltemplate.DialectForDriver(sut.Database.DriverName())), + Row: encryptedValue, + } + tmpl, err := template.ParseFiles("data/encrypted_value_create.sql") + if err != nil { + return fmt.Errorf("parsing template: %w", err) + } + + query, err := sqltemplate.Execute(tmpl, req) + if err != nil { + return fmt.Errorf("executing template: %w", err) + } + + res, err := sut.Database.ExecContext(t.Context(), query, req.GetArgs()...) + if err != nil { + return fmt.Errorf("inserting row: %w", err) + } + + if rowsAffected, err := res.RowsAffected(); err != nil { + return fmt.Errorf("getting rows affected: %w", err) + } else if rowsAffected != 1 { + return fmt.Errorf("expected 1 row affected, got %d", rowsAffected) + } + + return nil +} + func TestStateMachine(t *testing.T) { t.Parallel() @@ -212,10 +532,14 @@ func TestStateMachine(t *testing.T) { ns := namespaceGen.Draw(t, "ns") name := nameGen.Draw(t, "name") version := versionGen.Draw(t, "version") + dataKeyId := rapid.String().Draw(t, "dataKeyId") plaintext := rapid.String().Draw(t, "plaintext") - _, modelErr := m.create(ns, name, version, []byte(plaintext)) - _, err := sut.EncryptedValueStorage.Create(t.Context(), ns, name, version, []byte(plaintext)) + _, modelErr := m.create(ns, name, version, []byte(plaintext), dataKeyId) + _, err := sut.EncryptedValueStorage.Create(t.Context(), xkube.Namespace(ns), name, version, contracts.EncryptedPayload{ + DataKeyID: dataKeyId, + EncryptedData: []byte(plaintext), + }) if modelErr != nil || err != nil { require.ErrorIs(t, err, modelErr) return @@ -225,10 +549,14 @@ func TestStateMachine(t *testing.T) { ns := namespaceGen.Draw(t, "ns") name := nameGen.Draw(t, "name") version := versionGen.Draw(t, "version") + dataKeyId := rapid.String().Draw(t, "dataKeyId") plaintext := rapid.String().Draw(t, "plaintext") - modelErr := m.update(ns, name, version, []byte(plaintext)) - err := sut.EncryptedValueStorage.Update(t.Context(), ns, name, version, []byte(plaintext)) + modelErr := m.update(ns, name, version, []byte(plaintext), dataKeyId) + err := sut.EncryptedValueStorage.Update(t.Context(), xkube.Namespace(ns), name, version, contracts.EncryptedPayload{ + DataKeyID: dataKeyId, + EncryptedData: []byte(plaintext), + }) if modelErr != nil || err != nil { require.ErrorIs(t, err, modelErr) return @@ -240,7 +568,7 @@ func TestStateMachine(t *testing.T) { version := versionGen.Draw(t, "version") modelValue, modelErr := m.get(ns, name, version) - value, err := sut.EncryptedValueStorage.Get(t.Context(), ns, name, version) + value, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(ns), name, version) if modelErr != nil || err != nil { require.ErrorIs(t, err, modelErr) return @@ -258,7 +586,7 @@ func TestStateMachine(t *testing.T) { version := versionGen.Draw(t, "version") modelErr := m.delete(ns, name, version) - err := sut.EncryptedValueStorage.Delete(t.Context(), ns, name, version) + err := sut.EncryptedValueStorage.Delete(t.Context(), xkube.Namespace(ns), name, version) if modelErr != nil || err != nil { require.ErrorIs(t, err, modelErr) return @@ -290,18 +618,19 @@ type entry struct { name string version int64 encryptedData []byte + dataKeyId string } func newModel() *model { return &model{} } -func (m *model) create(namespace, name string, version int64, encryptedData []byte) (*contracts.EncryptedValue, error) { +func (m *model) create(namespace, name string, version int64, encryptedData []byte, dataKeyId string) (*contracts.EncryptedValue, error) { v, err := m.get(namespace, name, version) if err != nil && !errors.Is(err, encryption.ErrEncryptedValueNotFound) { return nil, err } - // The entry being creted already exists + // The entry being created already exists if v != nil { return nil, encryption.ErrEncryptedValueAlreadyExists } @@ -311,20 +640,25 @@ func (m *model) create(namespace, name string, version int64, encryptedData []by name: name, version: version, encryptedData: encryptedData, + dataKeyId: dataKeyId, }) return &contracts.EncryptedValue{ - Namespace: namespace, - Name: name, - Version: version, - EncryptedData: encryptedData, - Created: 1, - Updated: 1, + Namespace: namespace, + Name: name, + Version: version, + EncryptedPayload: contracts.EncryptedPayload{ + DataKeyID: dataKeyId, + EncryptedData: encryptedData, + }, + Created: 1, + Updated: 1, }, nil } -func (m *model) update(namespace, name string, version int64, encryptedData []byte) error { +func (m *model) update(namespace, name string, version int64, encryptedData []byte, dataKeyId string) error { for _, v := range m.entries { if v.namespace == namespace && v.name == name && v.version == version { v.encryptedData = encryptedData + v.dataKeyId = dataKeyId return nil } } @@ -336,12 +670,15 @@ func (m *model) get(namespace, name string, version int64) (*contracts.Encrypted for _, v := range m.entries { if v.namespace == namespace && v.name == name && v.version == version { return &contracts.EncryptedValue{ - Namespace: namespace, - Name: name, - Version: version, - EncryptedData: v.encryptedData, - Created: 1, - Updated: 1, + Namespace: namespace, + Name: name, + Version: version, + EncryptedPayload: contracts.EncryptedPayload{ + DataKeyID: v.dataKeyId, + EncryptedData: v.encryptedData, + }, + Created: 1, + Updated: 1, }, nil } } @@ -354,3 +691,25 @@ func (m *model) delete(namespace, name string, version int64) error { }) return nil } + +// mockGlobalEncryptedValueStorage is a mock implementation of contracts.GlobalEncryptedValueStorage +// used for testing error conditions in the migration executor +type mockGlobalEncryptedValueStorage struct { + encryptedValues []*contracts.EncryptedValue + listAllError error + countAllError error +} + +func (m *mockGlobalEncryptedValueStorage) ListAll(ctx context.Context, opts contracts.ListOpts, untilTime *int64) ([]*contracts.EncryptedValue, error) { + if m.listAllError != nil { + return nil, m.listAllError + } + return m.encryptedValues, nil +} + +func (m *mockGlobalEncryptedValueStorage) CountAll(ctx context.Context, untilTime *int64) (int64, error) { + if m.countAllError != nil { + return 0, m.countAllError + } + return int64(len(m.encryptedValues)), nil +} diff --git a/pkg/storage/secret/encryption/query.go b/pkg/storage/secret/encryption/query.go index 47ea83ce0cd..21e6081a7c7 100644 --- a/pkg/storage/secret/encryption/query.go +++ b/pkg/storage/secret/encryption/query.go @@ -74,6 +74,7 @@ type updateEncryptedValue struct { Name string Version int64 EncryptedData []byte + DataKeyID string Updated int64 } diff --git a/pkg/storage/secret/encryption/query_test.go b/pkg/storage/secret/encryption/query_test.go index a93ff0b77e6..4cbb830f0af 100644 --- a/pkg/storage/secret/encryption/query_test.go +++ b/pkg/storage/secret/encryption/query_test.go @@ -24,6 +24,7 @@ func TestEncryptedValueQueries(t *testing.T) { Name: "n1", Version: 1, EncryptedData: []byte("secret"), + DataKeyID: "test-data-key-id", Created: 1234, Updated: 5678, }, @@ -50,6 +51,7 @@ func TestEncryptedValueQueries(t *testing.T) { Name: "n1", Version: 1, EncryptedData: []byte("secret"), + DataKeyID: "test-data-key-id", Updated: 5679, }, }, diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_create-create.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_create-create.sql index 7a886b1c6ee..952c04730a8 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_create-create.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_create-create.sql @@ -3,6 +3,7 @@ INSERT INTO `secret_encrypted_value` ( `name`, `version`, `encrypted_data`, + `data_key_id`, `created`, `updated` ) VALUES ( @@ -10,6 +11,7 @@ INSERT INTO `secret_encrypted_value` ( 'n1', 1, '[115 101 99 114 101 116]', + 'test-data-key-id', 1234, 5678 ); diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all.sql index 74b699d45f1..e41d58588d4 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all.sql @@ -3,6 +3,7 @@ SELECT `name`, `version`, `encrypted_data`, + `data_key_id`, `created`, `updated` FROM diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all_until_time.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all_until_time.sql index b34496aaf93..c51a40a9a5a 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all_until_time.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all_until_time.sql @@ -3,6 +3,7 @@ SELECT `name`, `version`, `encrypted_data`, + `data_key_id`, `created`, `updated` FROM diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_0.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_0.sql index 9c33fd6bdbf..de82acacbbd 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_0.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_0.sql @@ -3,6 +3,7 @@ SELECT `name`, `version`, `encrypted_data`, + `data_key_id`, `created`, `updated` FROM diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_2.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_2.sql index d7066395a78..f21f7122646 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_2.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_2.sql @@ -3,6 +3,7 @@ SELECT `name`, `version`, `encrypted_data`, + `data_key_id`, `created`, `updated` FROM diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_read-read.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_read-read.sql index bb9faddb8ff..bca020a26d3 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_read-read.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_read-read.sql @@ -3,6 +3,7 @@ SELECT `name`, `version`, `encrypted_data`, + `data_key_id`, `created`, `updated` FROM diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_update-update.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_update-update.sql index cf3d6897a64..b454cb5165a 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_update-update.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_update-update.sql @@ -2,6 +2,7 @@ UPDATE `secret_encrypted_value` SET `encrypted_data` = '[115 101 99 114 101 116]', + `data_key_id` = 'test-data-key-id', `updated` = 5679 WHERE `namespace` = 'ns' AND diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_create-create.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_create-create.sql index 23d9a0f2331..17576f07232 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_create-create.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_create-create.sql @@ -3,6 +3,7 @@ INSERT INTO "secret_encrypted_value" ( "name", "version", "encrypted_data", + "data_key_id", "created", "updated" ) VALUES ( @@ -10,6 +11,7 @@ INSERT INTO "secret_encrypted_value" ( 'n1', 1, '[115 101 99 114 101 116]', + 'test-data-key-id', 1234, 5678 ); diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all.sql index 74432ebbf69..ee8b9c0a399 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all_until_time.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all_until_time.sql index 1d7089f751e..661d7f9ea0a 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all_until_time.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all_until_time.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_0.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_0.sql index 6f2bbd0b90f..06782d7e40d 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_0.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_0.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_2.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_2.sql index b9f326c8bf0..a64d45afaec 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_2.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_2.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_read-read.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_read-read.sql index 4bbe1000ce6..437685ae4e7 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_read-read.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_read-read.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_update-update.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_update-update.sql index b2b5e30ecac..0f710f7456d 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_update-update.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_update-update.sql @@ -2,6 +2,7 @@ UPDATE "secret_encrypted_value" SET "encrypted_data" = '[115 101 99 114 101 116]', + "data_key_id" = 'test-data-key-id', "updated" = 5679 WHERE "namespace" = 'ns' AND diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_create-create.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_create-create.sql index 23d9a0f2331..17576f07232 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_create-create.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_create-create.sql @@ -3,6 +3,7 @@ INSERT INTO "secret_encrypted_value" ( "name", "version", "encrypted_data", + "data_key_id", "created", "updated" ) VALUES ( @@ -10,6 +11,7 @@ INSERT INTO "secret_encrypted_value" ( 'n1', 1, '[115 101 99 114 101 116]', + 'test-data-key-id', 1234, 5678 ); diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all.sql index 74432ebbf69..ee8b9c0a399 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all_until_time.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all_until_time.sql index 1d7089f751e..661d7f9ea0a 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all_until_time.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all_until_time.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_0.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_0.sql index 6f2bbd0b90f..06782d7e40d 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_0.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_0.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_2.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_2.sql index b9f326c8bf0..a64d45afaec 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_2.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_2.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_read-read.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_read-read.sql index 4bbe1000ce6..437685ae4e7 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_read-read.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_read-read.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_update-update.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_update-update.sql index b2b5e30ecac..0f710f7456d 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_update-update.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_update-update.sql @@ -2,6 +2,7 @@ UPDATE "secret_encrypted_value" SET "encrypted_data" = '[115 101 99 114 101 116]', + "data_key_id" = 'test-data-key-id', "updated" = 5679 WHERE "namespace" = 'ns' AND diff --git a/pkg/storage/secret/metadata/decrypt_store.go b/pkg/storage/secret/metadata/decrypt_store.go index d148e365925..bd8ad2f73b9 100644 --- a/pkg/storage/secret/metadata/decrypt_store.go +++ b/pkg/storage/secret/metadata/decrypt_store.go @@ -134,7 +134,7 @@ func (s *decryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace, return "", fmt.Errorf("failed to get keeper for config: %v (%w)", err, contracts.ErrDecryptFailed) } - exposedValue, err := keeper.Expose(ctx, keeperConfig, namespace.String(), name, sv.Status.Version) + exposedValue, err := keeper.Expose(ctx, keeperConfig, namespace, name, sv.Status.Version) if err != nil { return "", fmt.Errorf("failed to expose secret: %v (%w)", err, contracts.ErrDecryptFailed) } diff --git a/pkg/storage/secret/migrator/migrator.go b/pkg/storage/secret/migrator/migrator.go index 34b56a905df..b08fb766e96 100644 --- a/pkg/storage/secret/migrator/migrator.go +++ b/pkg/storage/secret/migrator/migrator.go @@ -200,4 +200,15 @@ func (*SecretDB) AddMigration(mg *migrator.Migrator) { mg.AddMigration("add lease_created index to "+TableNameSecureValue, migrator.NewAddIndexMigration(secureValueTable, &migrator.Index{ Cols: []string{"lease_created"}, })) + + mg.AddMigration("add data_key_id column to "+TableNameEncryptedValue, migrator.NewAddColumnMigration(encryptedValueTable, &migrator.Column{ + Name: "data_key_id", + Type: migrator.DB_NVarchar, + Length: 100, + Nullable: false, + Default: "''", + })) + mg.AddMigration("add data_key_id index to "+TableNameEncryptedValue, migrator.NewAddIndexMigration(encryptedValueTable, &migrator.Index{ + Cols: []string{"data_key_id"}, + })) } diff --git a/pkg/storage/unified/apistore/permissions.go b/pkg/storage/unified/apistore/permissions.go index 95d2099cb86..395ac65f4f2 100644 --- a/pkg/storage/unified/apistore/permissions.go +++ b/pkg/storage/unified/apistore/permissions.go @@ -39,11 +39,6 @@ func afterCreatePermissionCreator(ctx context.Context, return nil, errors.New("missing auth info") } - idtype := auth.GetIdentityType() - if idtype != authtypes.TypeUser && idtype != authtypes.TypeServiceAccount && idtype != authtypes.TypeAccessPolicy { - return nil, fmt.Errorf("only users, service accounts, and access policies may grant permissions using an annotation") - } - return func(ctx context.Context) error { return setter(ctx, key, auth, val) }, nil diff --git a/pkg/storage/unified/apistore/permissions_test.go b/pkg/storage/unified/apistore/permissions_test.go index ff42a4cd189..fab28e6dc37 100644 --- a/pkg/storage/unified/apistore/permissions_test.go +++ b/pkg/storage/unified/apistore/permissions_test.go @@ -9,7 +9,6 @@ import ( authtypes "github.com/grafana/authlib/types" "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" - "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" ) @@ -46,85 +45,4 @@ func TestAfterCreatePermissionCreator(t *testing.T) { require.Nil(t, creator) require.Contains(t, err.Error(), "missing auth info") }) - - t.Run("should succeed for user identity", func(t *testing.T) { - ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{ - Type: authtypes.TypeUser, - OrgID: 1, - OrgRole: "Admin", - UserID: 1, - }) - obj := &v0alpha1.Dashboard{} - key := &resourcepb.ResourceKey{ - Group: "test", - Resource: "test", - Namespace: "test", - Name: "test", - } - - creator, err := afterCreatePermissionCreator(ctx, key, utils.AnnoGrantPermissionsDefault, obj, mockSetter) - require.NoError(t, err) - require.NotNil(t, creator) - - err = creator(ctx) - require.NoError(t, err) - }) - - t.Run("should succeed for service account identity", func(t *testing.T) { - ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{ - Type: authtypes.TypeServiceAccount, - OrgID: 1, - OrgRole: "Admin", - UserID: 1, - }) - obj := &v0alpha1.Dashboard{} - key := &resourcepb.ResourceKey{ - Group: "test", - Resource: "test", - Namespace: "test", - Name: "test", - } - - creator, err := afterCreatePermissionCreator(ctx, key, utils.AnnoGrantPermissionsDefault, obj, mockSetter) - require.NoError(t, err) - require.NotNil(t, creator) - - err = creator(ctx) - require.NoError(t, err) - }) - - t.Run("should succeed for access policy identity", func(t *testing.T) { - ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{ - Type: authtypes.TypeAccessPolicy, - OrgID: 1, - OrgRole: "Admin", - UserID: 1, - }) - obj := &v0alpha1.Dashboard{} - key := &resourcepb.ResourceKey{ - Group: "test", - Resource: "test", - Namespace: "test", - Name: "test", - } - - creator, err := afterCreatePermissionCreator(ctx, key, utils.AnnoGrantPermissionsDefault, obj, mockSetter) - require.NoError(t, err) - require.NotNil(t, creator) - - err = creator(ctx) - require.NoError(t, err) - }) - - t.Run("should error for non-user/non-service-account identity", func(t *testing.T) { - ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{ - Type: authtypes.TypeAnonymous, - }) - obj := &v0alpha1.Dashboard{} - - creator, err := afterCreatePermissionCreator(ctx, nil, utils.AnnoGrantPermissionsDefault, obj, mockSetter) - require.Error(t, err) - require.Nil(t, creator) - require.Contains(t, err.Error(), "only users, service accounts, and access policies may grant permissions") - }) } diff --git a/pkg/tests/api/alerting/test-data/provisioning-mixed-set.yaml b/pkg/tests/api/alerting/test-data/provisioning-mixed-set.yaml index b8845d29548..41b63fc2ed6 100644 --- a/pkg/tests/api/alerting/test-data/provisioning-mixed-set.yaml +++ b/pkg/tests/api/alerting/test-data/provisioning-mixed-set.yaml @@ -144,16 +144,17 @@ policies: - label_keys_not_$$escaped # a list of prometheus-like matchers that an alert rule has to fulfill to match the node (allowed chars # [a-zA-Z_:]) - matchers: - - alertname = Watchdog - - service_id_X = serviceX - - severity =~ "warning|critical" - # a list of grafana-like matchers that an alert rule has to fulfill to match the node - object_matchers: - - ["alertname", "=", "CPUUsage"] - - ["service_id-X", "=", "serviceX"] - - ["severity", "=~", "warning|critical"] group_wait: 30s group_interval: 5m repeat_interval: 4h - routes: [] + routes: + - matchers: + - alertname = Watchdog + - service_id_X = serviceX + - severity =~ "warning|critical" + # a list of grafana-like matchers that an alert rule has to fulfill to match the node + object_matchers: + - [ "alertname", "=", "CPUUsage" ] + - [ "service_id-X", "=", "serviceX" ] + - [ "severity", "=~", "warning|critical" ] + diff --git a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go index 1d34b5106c8..da834b690e8 100644 --- a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go +++ b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go @@ -9,10 +9,11 @@ import ( "net/http" "path" "slices" - "sort" "strings" "testing" + "github.com/grafana/alerting/notify/notifytest" + "github.com/grafana/alerting/receivers/line" "github.com/grafana/alerting/receivers/schema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -1225,7 +1226,7 @@ func TestIntegrationCRUD(t *testing.T) { t.Run("should be able to update default receiver", func(t *testing.T) { require.NotNil(t, defaultReceiver) newDefault := defaultReceiver.Copy().(*v0alpha1.Receiver) - newDefault.Spec.Integrations = append(newDefault.Spec.Integrations, createIntegration(t, "line")) + newDefault.Spec.Integrations = append(newDefault.Spec.Integrations, createIntegration(t, line.Type)) updatedReceiver, err := adminClient.Update(ctx, newDefault, v1.UpdateOptions{}) require.NoError(t, err) @@ -1266,10 +1267,10 @@ func TestIntegrationCRUD(t *testing.T) { var receiver *v0alpha1.Receiver t.Run("should correctly persist all known integrations", func(t *testing.T) { - integrations := make([]v0alpha1.ReceiverIntegration, 0, len(notify.AllKnownConfigsForTesting)) - keysIter := maps.Keys(notify.AllKnownConfigsForTesting) + integrations := make([]v0alpha1.ReceiverIntegration, 0, len(notifytest.AllKnownV1ConfigsForTesting)) + keysIter := maps.Keys(notifytest.AllKnownV1ConfigsForTesting) keys := slices.Collect(keysIter) - sort.Strings(keys) + slices.Sort(keys) for _, key := range keys { integrations = append(integrations, createIntegration(t, key)) } @@ -1300,7 +1301,7 @@ func TestIntegrationCRUD(t *testing.T) { export := legacyCli.ExportReceiverTyped(t, receiver.Spec.Title, true) for _, integration := range export.Receivers { - expected := notify.AllKnownConfigsForTesting[strings.ToLower(integration.Type)] // to lower because there is LINE that is in different casing in API + expected := notifytest.AllKnownV1ConfigsForTesting[schema.IntegrationType(integration.Type)] assert.JSONEqf(t, expected.Config, string(integration.Settings), "integration %s", integration.Type) } }) @@ -1313,7 +1314,7 @@ func TestIntegrationCRUD(t *testing.T) { for _, integration := range get.Spec.Integrations { integrationType := schema.IntegrationType(integration.Type) t.Run(integration.Type, func(t *testing.T) { - expected := notify.AllKnownConfigsForTesting[strings.ToLower(integration.Type)] + expected := notifytest.AllKnownV1ConfigsForTesting[schema.IntegrationType(integration.Type)] var fields map[string]any require.NoError(t, json.Unmarshal([]byte(expected.Config), &fields)) typeSchema, ok := notify.GetSchemaVersionForIntegration(integrationType, schema.V1) @@ -1336,11 +1337,11 @@ func TestIntegrationCRUD(t *testing.T) { }) t.Run("should fail to persist receiver with invalid config", func(t *testing.T) { - keysIter := maps.Keys(notify.AllKnownConfigsForTesting) + keysIter := maps.Keys(notifytest.AllKnownV1ConfigsForTesting) keys := slices.Collect(keysIter) - sort.Strings(keys) + slices.Sort(keys) for _, key := range keys { - t.Run(key, func(t *testing.T) { + t.Run(string(key), func(t *testing.T) { integration := createIntegration(t, key) // Make the integration invalid, so it fails to create. This is usually done by sending empty settings. clear(integration.Settings) @@ -1503,18 +1504,18 @@ func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig) require.NoError(t, err) } -func createIntegration(t *testing.T, integrationType string) v0alpha1.ReceiverIntegration { - cfg, ok := notify.AllKnownConfigsForTesting[integrationType] +func createIntegration(t *testing.T, integrationType schema.IntegrationType) v0alpha1.ReceiverIntegration { + cfg, ok := notifytest.AllKnownV1ConfigsForTesting[integrationType] require.Truef(t, ok, "no known config for integration type %s", integrationType) - return createIntegrationWithSettings(t, integrationType, "v1", cfg.Config) + return createIntegrationWithSettings(t, integrationType, schema.V1, cfg.Config) } -func createIntegrationWithSettings(t *testing.T, integrationType string, integrationVersion string, settingsJson string) v0alpha1.ReceiverIntegration { +func createIntegrationWithSettings(t *testing.T, integrationType schema.IntegrationType, integrationVersion schema.Version, settingsJson string) v0alpha1.ReceiverIntegration { settings := common.Unstructured{} require.NoError(t, settings.UnmarshalJSON([]byte(settingsJson))) return v0alpha1.ReceiverIntegration{ Settings: settings.Object, - Type: integrationType, - Version: integrationVersion, + Type: string(integrationType), + Version: string(integrationVersion), DisableResolveMessage: util.Pointer(false), } } diff --git a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json index 9a7b8ef0481..3561b54c453 100644 --- a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json @@ -3737,20 +3737,23 @@ "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingSpec": { "type": "object", "required": [ - "subjects", - "teamRef" + "subject", + "teamRef", + "permission" ], "properties": { - "subjects": { - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingspecSubject" - } - ] - } + "permission": { + "description": "permission of the identity in the team", + "type": "string", + "default": "" + }, + "subject": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingspecSubject" + } + ] }, "teamRef": { "default": {}, @@ -3778,19 +3781,13 @@ "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingspecSubject": { "type": "object", "required": [ - "name", - "permission" + "name" ], "properties": { "name": { "description": "uid of the identity", "type": "string", "default": "" - }, - "permission": { - "description": "permission of the identity in the team", - "type": "string", - "default": "" } } }, @@ -5594,15 +5591,18 @@ "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingSpec": { "type": "object", "required": [ - "subjects", - "teamRef" + "subject", + "teamRef", + "permission" ], "properties": { - "subjects": { - "type": "array", - "items": { - "default": {} - } + "permission": { + "description": "permission of the identity in the team", + "type": "string", + "default": "" + }, + "subject": { + "default": {} }, "teamRef": { "default": {} @@ -5644,19 +5644,13 @@ "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingspecSubject": { "type": "object", "required": [ - "name", - "permission" + "name" ], "properties": { "name": { "description": "uid of the identity", "type": "string", "default": "" - }, - "permission": { - "description": "permission of the identity in the team", - "type": "string", - "default": "" } } }, diff --git a/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json index 4816ea0580f..7973627042e 100644 --- a/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json @@ -4520,6 +4520,12 @@ "schemas": { "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver": { "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -4708,6 +4714,12 @@ }, "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree": { "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -4967,6 +4979,12 @@ }, "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup": { "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -5116,6 +5134,12 @@ }, "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval": { "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", diff --git a/pkg/tests/apis/openapi_snapshots/playlist.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/playlist.grafana.app-v0alpha1.json index 96ff9719372..dd1b486823e 100644 --- a/pkg/tests/apis/openapi_snapshots/playlist.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/playlist.grafana.app-v0alpha1.json @@ -1160,6 +1160,12 @@ "schemas": { "com.github.grafana.grafana.apps.playlist.pkg.apis.playlist.v0alpha1.Playlist": { "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", diff --git a/pkg/tests/apis/openapi_snapshots/rules.alerting.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/rules.alerting.grafana.app-v0alpha1.json index 0d12c909329..0e646c08d01 100644 --- a/pkg/tests/apis/openapi_snapshots/rules.alerting.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/rules.alerting.grafana.app-v0alpha1.json @@ -2280,6 +2280,12 @@ "schemas": { "com.github.grafana.grafana.apps.alerting.rules.pkg.apis.alerting.v0alpha1.AlertRule": { "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -2603,6 +2609,12 @@ }, "com.github.grafana.grafana.apps.alerting.rules.pkg.apis.alerting.v0alpha1.RecordingRule": { "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", diff --git a/pkg/tests/apis/openapi_snapshots/shorturl.grafana.app-v1alpha1.json b/pkg/tests/apis/openapi_snapshots/shorturl.grafana.app-v1alpha1.json index 9807d9c13b9..574ce2f474a 100644 --- a/pkg/tests/apis/openapi_snapshots/shorturl.grafana.app-v1alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/shorturl.grafana.app-v1alpha1.json @@ -1220,6 +1220,12 @@ }, "com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL": { "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", diff --git a/public/app/api/clients/playlist/v0alpha1/endpoints.gen.ts b/public/app/api/clients/playlist/v0alpha1/endpoints.gen.ts index 347f6d2b86a..7a42a89de07 100644 --- a/public/app/api/clients/playlist/v0alpha1/endpoints.gen.ts +++ b/public/app/api/clients/playlist/v0alpha1/endpoints.gen.ts @@ -308,11 +308,11 @@ export type PlaylistStatus = { }; export type Playlist = { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; + apiVersion: string; /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: ObjectMeta; - spec?: PlaylistSpec; + kind: string; + metadata: ObjectMeta; + spec: PlaylistSpec; status?: PlaylistStatus; }; export type ListMeta = { diff --git a/public/app/api/clients/rules/v0alpha1/endpoints.gen.ts b/public/app/api/clients/rules/v0alpha1/endpoints.gen.ts index 1555dae0e46..6537a5061fc 100644 --- a/public/app/api/clients/rules/v0alpha1/endpoints.gen.ts +++ b/public/app/api/clients/rules/v0alpha1/endpoints.gen.ts @@ -931,11 +931,11 @@ export type AlertRuleStatus = { }; export type AlertRule = { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; + apiVersion: string; /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: ObjectMeta; - spec?: AlertRuleSpec; + kind: string; + metadata: ObjectMeta; + spec: AlertRuleSpec; status?: AlertRuleStatus; }; export type ListMeta = { @@ -1070,11 +1070,11 @@ export type RecordingRuleStatus = { }; export type RecordingRule = { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; + apiVersion: string; /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: ObjectMeta; - spec?: RecordingRuleSpec; + kind: string; + metadata: ObjectMeta; + spec: RecordingRuleSpec; status?: RecordingRuleStatus; }; export type RecordingRuleList = { diff --git a/public/app/api/clients/shorturl/v1alpha1/endpoints.gen.ts b/public/app/api/clients/shorturl/v1alpha1/endpoints.gen.ts index cfb88c9224b..9cabcb8ab7b 100644 --- a/public/app/api/clients/shorturl/v1alpha1/endpoints.gen.ts +++ b/public/app/api/clients/shorturl/v1alpha1/endpoints.gen.ts @@ -524,11 +524,11 @@ export type ShortUrlStatus = { }; export type ShortUrl = { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; + apiVersion: string; /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: ObjectMeta; - spec?: ShortUrlSpec; + kind: string; + metadata: ObjectMeta; + spec: ShortUrlSpec; status?: ShortUrlStatus; }; export type ListMeta = { diff --git a/public/app/core/utils/shortLinks.ts b/public/app/core/utils/shortLinks.ts index b9187f7ca4c..eeaceaa298f 100644 --- a/public/app/core/utils/shortLinks.ts +++ b/public/app/core/utils/shortLinks.ts @@ -48,6 +48,9 @@ export const createShortLink = async function (path: string) { const result = await dispatch( generatedAPI.endpoints.createShortUrl.initiate({ shortUrl: { + apiVersion: 'shorturl.grafana.app/v1alpha1', + kind: 'Playlist', + metadata: {}, spec: { path: getRelativeURLPath(path), }, diff --git a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx index d9d611d7a00..0bce93870f9 100644 --- a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx @@ -1,5 +1,6 @@ import { render, screen, userEvent, within } from 'test/test-utils'; +import { base64UrlEncode } from '@grafana/alerting'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { setMuteTimingsListError, @@ -10,7 +11,7 @@ import { captureRequests } from 'app/features/alerting/unified/mocks/server/even import { AccessControlAction } from 'app/types/accessControl'; import { grantUserPermissions } from '../../mocks'; -import { TIME_INTERVAL_UID_HAPPY_PATH } from '../../mocks/server/handlers/k8s/timeIntervals.k8s'; +import { TIME_INTERVAL_NAME_HAPPY_PATH } from '../../mocks/server/handlers/k8s/timeIntervals.k8s'; import { AlertmanagerProvider } from '../../state/AlertmanagerContext'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; @@ -113,8 +114,9 @@ describe('MuteTimingsTable', () => { await user.click(await screen.findByRole('button', { name: /delete/i })); const requests = await capture; + const encodedName = base64UrlEncode(TIME_INTERVAL_NAME_HAPPY_PATH); const deleteRequest = requests.find( - (r) => r.url.includes(`timeintervals/${TIME_INTERVAL_UID_HAPPY_PATH}`) && r.method === 'DELETE' + (r) => r.url.includes(`timeintervals/${encodedName}`) && r.method === 'DELETE' ); expect(deleteRequest).toBeDefined(); diff --git a/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx b/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx index aad08e47548..94e290a2087 100644 --- a/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx @@ -1,5 +1,6 @@ import { useEffect } from 'react'; +import { base64UrlEncode } from '@grafana/alerting'; import { alertmanagerApi } from 'app/features/alerting/unified/api/alertmanagerApi'; import { timeIntervalsApi } from 'app/features/alerting/unified/api/timeIntervalsApi'; import { mergeTimeIntervals } from 'app/features/alerting/unified/components/mute-timings/util'; @@ -10,9 +11,9 @@ import { import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks'; import { PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; import { - encodeFieldSelector, isK8sEntityProvisioned, shouldUseK8sApi, + stringifyFieldSelector, } from 'app/features/alerting/unified/utils/k8s/utils'; import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types'; @@ -203,8 +204,10 @@ export const useGetMuteTiming = ({ alertmanager, name: nameToFind }: BaseAlertma useEffect(() => { if (useK8sApi) { const namespace = getAPINamespace(); - const entityName = encodeFieldSelector(nameToFind); - getGrafanaTimeInterval({ namespace, fieldSelector: `spec.name=${entityName}` }, true); + getGrafanaTimeInterval( + { namespace, fieldSelector: stringifyFieldSelector([['metadata.name', base64UrlEncode(nameToFind)]]) }, + true + ); } else { getAlertmanagerTimeInterval(alertmanager, true); } diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx index c512615ce78..a71930c8b88 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx @@ -3,10 +3,12 @@ import { isEmpty } from 'lodash'; import { useEffect } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; +import { base64UrlEncode } from '@grafana/alerting'; import { ContactPointSelector as GrafanaManagedContactPointSelector, alertingAPI } from '@grafana/alerting/unstable'; import { Trans, t } from '@grafana/i18n'; import { Field, FieldValidationMessage, Stack, TextLink } from '@grafana/ui'; import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form'; +import { stringifyFieldSelector } from 'app/features/alerting/unified/utils/k8s/utils'; import { createRelativeUrl } from 'app/features/alerting/unified/utils/url'; export interface ContactPointSelectorProps { @@ -21,9 +23,13 @@ export function ContactPointSelector({ alertManager }: ContactPointSelectorProps // check if the contact point still exists, we'll use listReceiver to check if the contact point exists because getReceiver doesn't work with // contact point titles but with UUIDs (which is not what we store on the alert rule definition) - const { currentData, status } = alertingAPI.endpoints.listReceiver.useQuery({ - fieldSelector: `spec.title=${contactPointInForm}`, - }); + const encodedContactPoint = contactPointInForm ? base64UrlEncode(contactPointInForm) : ''; + const { currentData, status } = alertingAPI.endpoints.listReceiver.useQuery( + { + fieldSelector: stringifyFieldSelector([['metadata.name', encodedContactPoint]]), + }, + { skip: !contactPointInForm } + ); const contactPointNotFound = contactPointInForm && status === QueryStatus.fulfilled && isEmpty(currentData?.items); @@ -37,6 +43,7 @@ export function ContactPointSelector({ alertManager }: ContactPointSelectorProps return ( diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/ContactPointGroup.tsx b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/ContactPointGroup.tsx index ba69db35766..9d4510a2ac1 100644 --- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/ContactPointGroup.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/ContactPointGroup.tsx @@ -3,6 +3,7 @@ import { PropsWithChildren, ReactNode } from 'react'; import Skeleton from 'react-loading-skeleton'; import { useToggle } from 'react-use'; +import { base64UrlEncode } from '@grafana/alerting'; import { alertingAPI, getContactPointDescription } from '@grafana/alerting/unstable'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; @@ -23,8 +24,10 @@ interface ContactPointGroupProps extends PropsWithChildren { export function GrafanaContactPointGroup({ name, matchedInstancesCount, children }: ContactPointGroupProps) { // find receiver by name – since this is what we store in the alert rule definition + const encodedName = base64UrlEncode(name); + const { data, isLoading } = alertingAPI.endpoints.listReceiver.useQuery({ - fieldSelector: stringifyFieldSelector([['spec.title', name]]), + fieldSelector: stringifyFieldSelector([['metadata.name', encodedName]]), }); // grab the first result from the fieldSelector result diff --git a/public/app/features/alerting/unified/components/rule-viewer/ContactPointLink.tsx b/public/app/features/alerting/unified/components/rule-viewer/ContactPointLink.tsx index b3c9196e664..741708860e0 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/ContactPointLink.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/ContactPointLink.tsx @@ -1,9 +1,11 @@ import { ComponentProps } from 'react'; import Skeleton from 'react-loading-skeleton'; +import { base64UrlEncode } from '@grafana/alerting'; import { alertingAPI } from '@grafana/alerting/unstable'; import { TextLink } from '@grafana/ui'; +import { stringifyFieldSelector } from '../../utils/k8s/utils'; import { makeEditContactPointLink } from '../../utils/misc'; interface ContactPointLinkProps extends Omit, 'href' | 'children'> { @@ -11,9 +13,11 @@ interface ContactPointLinkProps extends Omit, 'h } export const ContactPointLink = ({ name, ...props }: ContactPointLinkProps) => { - // find receiver by name – since this is what we store in the alert rule definition + const encodedName = base64UrlEncode(name); + + // find receiver by name using metadata.name field selector const { currentData, isLoading, isSuccess } = alertingAPI.endpoints.listReceiver.useQuery({ - fieldSelector: `spec.title=${name}`, + fieldSelector: stringifyFieldSelector([['metadata.name', encodedName]]), }); // grab the first result from the fieldSelector result diff --git a/public/app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s.ts b/public/app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s.ts index 126423077d7..84503c2ce13 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s.ts @@ -1,9 +1,10 @@ import { HttpResponse, http } from 'msw'; +import { base64UrlEncode } from '@grafana/alerting'; import { filterBySelector } from 'app/features/alerting/unified/mocks/server/handlers/k8s/utils'; import { ALERTING_API_SERVER_BASE_URL, getK8sResponse } from 'app/features/alerting/unified/mocks/server/utils'; import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval } from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen'; -import { PROVENANCE_ANNOTATION, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; +import { K8sAnnotations, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; /** UID of a time interval that we expect to follow all happy paths within tests/mocks */ export const TIME_INTERVAL_UID_HAPPY_PATH = 'f4eae7a4895fa786'; @@ -20,9 +21,9 @@ const allTimeIntervals = getK8sResponse ); } - // Rudimentary filter support for `spec.name` + // Rudimentary filter support for `metadata.name` const url = new URL(request.url); const fieldSelector = url.searchParams.get('fieldSelector'); - if (fieldSelector && fieldSelector.includes('spec.name')) { + if (fieldSelector && fieldSelector.includes('metadata.name')) { const filteredItems = filterBySelector(allTimeIntervals.items, fieldSelector); return HttpResponse.json({ items: filteredItems }); diff --git a/public/app/features/alerting/unified/mocks/server/utils.ts b/public/app/features/alerting/unified/mocks/server/utils.ts index 5f01e038225..0ff0618a5f2 100644 --- a/public/app/features/alerting/unified/mocks/server/utils.ts +++ b/public/app/features/alerting/unified/mocks/server/utils.ts @@ -1,5 +1,6 @@ import { DefaultBodyType, HttpResponse, HttpResponseResolver, PathParams } from 'msw'; +import { base64UrlEncode } from '@grafana/alerting'; import { PromRuleGroupDTO, PromRulesResponse } from 'app/types/unified-alerting-dto'; /** Helper method to help generate a kubernetes-style response with a list of items */ @@ -20,7 +21,7 @@ export function paginatedHandlerFor( ): HttpResponseResolver { const orderedGroupsWithCursor = groups.map((group) => ({ ...group, - id: Buffer.from(`${group.file}-${group.name}`).toString('base64url'), + id: base64UrlEncode(`${group.file}-${group.name}`), })); return ({ request }) => { diff --git a/public/app/features/alerting/unified/utils/k8s/utils.ts b/public/app/features/alerting/unified/utils/k8s/utils.ts index e75818cde66..8aecb9dd46b 100644 --- a/public/app/features/alerting/unified/utils/k8s/utils.ts +++ b/public/app/features/alerting/unified/utils/k8s/utils.ts @@ -52,5 +52,7 @@ export const encodeFieldSelector = (value: string): string => { type FieldSelector = [string, string] | [string, string, '=' | '!=']; export const stringifyFieldSelector = (fieldSelectors: FieldSelector[]): string => { - return fieldSelectors.map(([key, value, operator = '=']) => `${key}${operator}${value}`).join(','); + return fieldSelectors + .map(([key, value, operator = '=']) => `${key}${operator}${encodeFieldSelector(value)}`) + .join(','); }; diff --git a/public/app/features/auth-config/constants.ts b/public/app/features/auth-config/constants.ts index 954f35e7be5..3a98f08ba18 100644 --- a/public/app/features/auth-config/constants.ts +++ b/public/app/features/auth-config/constants.ts @@ -9,7 +9,7 @@ export const UIMap: Record = { google: ['google', 'Google'], generic_oauth: ['lock', 'Generic OAuth'], grafana_com: ['grafana', 'Grafana.com'], - azuread: ['microsoft', 'Azure AD'], + azuread: ['microsoft', 'Entra ID'], okta: ['okta', 'Okta'], scim: ['scim', 'SCIM'], }; diff --git a/public/app/features/auth-config/fields.tsx b/public/app/features/auth-config/fields.tsx index 029e67b581a..96bb2e3ccea 100644 --- a/public/app/features/auth-config/fields.tsx +++ b/public/app/features/auth-config/fields.tsx @@ -906,7 +906,7 @@ export function fieldMap(provider: string): Record { label: t('auth-config.fields.domain-hint-label', 'Domain hint'), description: t( 'auth-config.fields.domain-hint-description', - 'Parameter to indicate the realm of the user in the Azure AD/Entra ID tenant and streamline the login process.' + 'Parameter to indicate the realm of the user in the Entra ID tenant and streamline the login process.' ), type: 'text', validation: { diff --git a/public/app/features/playlist/PlaylistForm.test.tsx b/public/app/features/playlist/PlaylistForm.test.tsx index a08012a81c3..657a97560c3 100644 --- a/public/app/features/playlist/PlaylistForm.test.tsx +++ b/public/app/features/playlist/PlaylistForm.test.tsx @@ -12,6 +12,8 @@ jest.mock('app/core/components/TagFilter/TagFilter', () => ({ })); const mockPlaylist: Playlist = { + apiVersion: 'playlist.grafana.app/v0alpha1', + kind: 'Playlist', spec: { title: 'A test playlist', interval: '10m', @@ -28,6 +30,8 @@ const mockPlaylist: Playlist = { }; const mockEmptyPlaylist: Playlist = { + apiVersion: 'playlist.grafana.app/v0alpha1', + kind: 'Playlist', spec: { title: 'A test playlist', interval: '10m', @@ -102,6 +106,8 @@ describe('PlaylistForm', () => { await userEvent.click(screen.getByRole('button', { name: /save/i })); expect(onSubmitMock).toHaveBeenCalledTimes(1); expect(onSubmitMock).toHaveBeenCalledWith({ + apiVersion: 'playlist.grafana.app/v0alpha1', + kind: 'Playlist', spec: { title: 'A test playlist', interval: '10m', diff --git a/public/app/features/playlist/PlaylistSrv.test.ts b/public/app/features/playlist/PlaylistSrv.test.ts index 9b7bed662a7..8cb7559bbf4 100644 --- a/public/app/features/playlist/PlaylistSrv.test.ts +++ b/public/app/features/playlist/PlaylistSrv.test.ts @@ -22,6 +22,8 @@ jest.mock('./utils', () => ({ })); const mockPlaylist: Playlist = { + apiVersion: 'playlist.grafana.app/v0alpha1', + kind: 'Playlist', spec: { interval: '1s', title: 'The display', diff --git a/public/app/features/playlist/utils.ts b/public/app/features/playlist/utils.ts index d9c9635ee70..2b46984787c 100644 --- a/public/app/features/playlist/utils.ts +++ b/public/app/features/playlist/utils.ts @@ -71,6 +71,8 @@ export async function loadDashboards(items: PlaylistItemUI[]): Promise