Compare commits

..
Author SHA1 Message Date
maicon d68451b70e provisioning: acquire server lock before provisioning dashboards+folders (#114488)
* provisioning: acquire server lock before provisioning dashboards+folders

Signed-off-by: Maicon Costa <maiconscosta@gmail.com>

---------

Signed-off-by: Maicon Costa <maiconscosta@gmail.com>
2025-12-05 12:59:22 -03:00
Stephanie Hingtgen 2be78f8e63 Dashboards: Prevent panic in validation (#114436) 2025-12-05 12:57:49 -03:00
Renato Costa 076d378adc fix: update search request for existing provisioned dashboards in modes 3+ (#114412)
Fix search for existing provisioned dashboards in modes 3+

The search query was not requesting the dashboard's "legacy ID". As a result,
the provisioning process would not find existing provisioned dashboards, making
copies of these dashboards every time there was a change in the provisioned
dashboard's definition.

Signed-off-by: Maicon Costa <maiconscosta@gmail.com>
2025-12-05 12:32:30 -03:00
Daniele Stefano Ferru 6b854bc57d Hotfix: Granting None role Viewer access for a fixed API group list (#114310) 2025-12-04 12:35:23 -05:00
71 changed files with 358 additions and 1249 deletions
@@ -66,6 +66,9 @@ func ValidateDashboardSpec(obj *Dashboard, forceValidation bool) (field.ErrorLis
}
func formatErrorPath(path []string) string {
if len(path) <= 4 {
return strings.Join(path, ".")
}
// omitting the "lineage.schemas[0].schema.spec" prefix here.
return strings.Join(path[4:], ".")
}
@@ -67,6 +67,9 @@ func ValidateDashboardSpec(obj *Dashboard, forceValidation bool) (field.ErrorLis
}
func formatErrorPath(path []string) string {
if len(path) <= 4 {
return strings.Join(path, ".")
}
// omitting the "lineage.schemas[0].schema.spec" prefix here.
return strings.Join(path[4:], ".")
}
@@ -29,7 +29,7 @@ refs:
- pattern: /docs/grafana/
destination: docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/custom-role-actions-scopes/#cloud-access-policies-action-definitions
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/custom-role-actions-scopes/#create-access-policies
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/custom-role-actions-scopes/#cloud-access-policies-action-definitions
rbac-role-definitions:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/
@@ -66,17 +66,16 @@ Please refer to plugin documentation to see what RBAC permissions the plugin has
The following list contains app plugins that have fine-grained RBAC support.
| App plugin | App plugin ID | App plugin permission documentation |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) | `grafana-auth-app` | [RBAC actions for Access Policies](ref:cloud-access-policies-action-definitions) |
| [Adaptive Metrics](https://grafana.com/docs/grafana-cloud/cost-management-and-billing/reduce-costs/metrics-costs/control-metrics-usage-via-adaptive-metrics/adaptive-metrics-plugin/) | `grafana-adaptive-metrics-app` | [RBAC actions for Adaptive Metrics](ref:adaptive-metrics-permissions) |
| [Cloud Provider](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/) | `grafana-csp-app` | [Cloud Provider Observability role-based access control](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/rbac/) |
| [Incident](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/incident/) | `grafana-incident-app` | n/a |
| [Kubernetes Monitoring](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/) | `grafana-k8s-app` | [Kubernetes Monitoring role-based access control](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/configuration/control-access/#precision-access-with-rbac-custom-plugin-roles) |
| [OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/) | `grafana-oncall-app` | [Configure RBAC for OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/manage/user-and-team-management/#manage-users-and-teams-for-grafana-oncall) |
| [Performance Testing (K6)](https://grafana.com/docs/grafana-cloud/testing/k6/) | `k6-app` | [Configure RBAC for K6](https://grafana.com/docs/grafana-cloud/testing/k6/projects-and-users/configure-rbac/) |
| [Private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) | `grafana-pdc-app` | n/a |
| [Service Level Objective (SLO)](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/) | `grafana-slo-app` | [Configure RBAC for SLO](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/set-up/rbac/) |
| App plugin | App plugin ID | App plugin permission documentation |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) | `grafana-auth-app` | [RBAC actions for Access Policies](ref:cloud-access-policies-action-definitions) |
| [Adaptive metrics](https://grafana.com/docs/grafana-cloud/cost-management-and-billing/reduce-costs/metrics-costs/control-metrics-usage-via-adaptive-metrics/adaptive-metrics-plugin/) | `grafana-adaptive-metrics-app` | [RBAC actions for Adaptive Metrics](ref:adaptive-metrics-permissions) |
| [Incident](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/incident/) | `grafana-incident-app` | n/a |
| [OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/) | `grafana-oncall-app` | [Configure RBAC for OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/manage/user-and-team-management/#manage-users-and-teams-for-grafana-oncall) |
| [Performance Testing (K6)](https://grafana.com/docs/grafana-cloud/testing/k6/) | `k6-app` | [Configure RBAC for K6](https://grafana.com/docs/grafana-cloud/testing/k6/projects-and-users/configure-rbac/) |
| [Private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) | `grafana-pdc-app` | n/a |
| [Service Level Objective (SLO)](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/) | `grafana-slo-app` | [Configure RBAC for SLO](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/set-up/rbac/) |
| [Cloud Provider](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/) | `grafana-csp-app` | [Cloud Provider Observability role-based access control](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/rbac/) |
### Revoke fine-grained access from app plugins
+10
View File
@@ -2136,6 +2136,11 @@
"count": 2
}
},
"public/app/features/dashboard-scene/sharing/ShareExportTab.tsx": {
"no-restricted-syntax": {
"count": 1
}
},
"public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx": {
"no-restricted-syntax": {
"count": 4
@@ -2937,6 +2942,11 @@
"count": 1
}
},
"public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx": {
"react-prefer-function-component/react-prefer-function-component": {
"count": 1
}
},
"public/app/features/manage-dashboards/state/actions.ts": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
-2
View File
@@ -313,7 +313,6 @@ func (hs *HTTPServer) declareFixedRoles() error {
Grants: []string{string(org.RoleEditor)},
}
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) {
// Keeping the name to avoid breaking changes (for users who have assigned this role to grant permissions on organization annotations)
annotationsReaderRole = ac.RoleRegistration{
@@ -620,7 +619,6 @@ func (hs *HTTPServer) declareFixedRoles() error {
libraryPanelsReaderRole, libraryPanelsWriterRole, libraryPanelsGeneralReaderRole, libraryPanelsGeneralWriterRole,
snapshotsCreatorRole, snapshotsDeleterRole, snapshotsReaderRole}
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) {
allAnnotationsReaderRole := ac.RoleRegistration{
Role: ac.RoleDTO{
-9
View File
@@ -126,7 +126,6 @@ func (hs *HTTPServer) PostAnnotation(c *contextmodel.ReqContext) response.Respon
}
if canSave, err := hs.canCreateAnnotation(c, cmd.DashboardUID); err != nil || !canSave {
//nolint:staticcheck // not yet migrated to OpenFeature
if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
return dashboardGuardianResponse(err)
} else if err != nil {
@@ -272,7 +271,6 @@ func (hs *HTTPServer) UpdateAnnotation(c *contextmodel.ReqContext) response.Resp
return resp
}
//nolint:staticcheck // not yet migrated to OpenFeature
if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
if canSave, err := hs.canSaveAnnotation(c, hs.AccessControl, annotation); err != nil || !canSave {
return dashboardGuardianResponse(err)
@@ -331,7 +329,6 @@ func (hs *HTTPServer) PatchAnnotation(c *contextmodel.ReqContext) response.Respo
return resp
}
//nolint:staticcheck // not yet migrated to OpenFeature
if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
if canSave, err := hs.canSaveAnnotation(c, hs.AccessControl, annotation); err != nil || !canSave {
return dashboardGuardianResponse(err)
@@ -442,7 +439,6 @@ func (hs *HTTPServer) MassDeleteAnnotations(c *contextmodel.ReqContext) response
canSave, err := hs.canMassDeleteAnnotations(c, dashboardUID)
if err != nil || !canSave {
//nolint:staticcheck // not yet migrated to OpenFeature
if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
return dashboardGuardianResponse(err)
} else if err != nil {
@@ -504,7 +500,6 @@ func (hs *HTTPServer) DeleteAnnotationByID(c *contextmodel.ReqContext) response.
return response.Error(http.StatusBadRequest, "annotationId is invalid", err)
}
//nolint:staticcheck // not yet migrated to OpenFeature
if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
annotation, resp := findAnnotationByID(c.Req.Context(), hs.annotationsRepo, annotationID, c.SignedInUser)
if resp != nil {
@@ -615,7 +610,6 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, feature
},
}
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) {
tempUser = &user.SignedInUser{
OrgID: orgID,
@@ -632,7 +626,6 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, feature
return nil, errors.New("could not resolve annotation type")
}
//nolint:staticcheck // not yet migrated to OpenFeature
if !features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) {
switch annotation.GetType() {
case annotations.Organization:
@@ -669,7 +662,6 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, feature
}
func (hs *HTTPServer) canCreateAnnotation(c *contextmodel.ReqContext, dashboardUID string) (bool, error) {
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
if dashboardUID != "" {
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashboardUID))
@@ -694,7 +686,6 @@ func (hs *HTTPServer) canCreateAnnotation(c *contextmodel.ReqContext, dashboardU
}
func (hs *HTTPServer) canMassDeleteAnnotations(c *contextmodel.ReqContext, dashboardUID string) (bool, error) {
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
if dashboardUID == "" {
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, accesscontrol.ScopeAnnotationsTypeOrganization)
-1
View File
@@ -166,7 +166,6 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response
}
annotationPermissions := &dashboardsV1.AnnotationPermission{}
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) {
hs.getAnnotationPermissionsByScope(c, &annotationPermissions.Dashboard, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash.UID))
} else {
-4
View File
@@ -150,7 +150,6 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
continue
}
//nolint:staticcheck // not yet migrated to OpenFeature
if panel.ID == "datagrid" && !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagEnableDatagridEditing) {
continue
}
@@ -191,7 +190,6 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
hasAccess := accesscontrol.HasAccess(hs.AccessControl, c)
trustedTypesDefaultPolicyEnabled := (hs.Cfg.CSPEnabled && strings.Contains(hs.Cfg.CSPTemplate, "require-trusted-types-for")) || (hs.Cfg.CSPReportOnlyEnabled && strings.Contains(hs.Cfg.CSPReportOnlyTemplate, "require-trusted-types-for"))
//nolint:staticcheck // not yet migrated to OpenFeature
isCloudMigrationTarget := hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagOnPremToCloudMigrations) && hs.Cfg.CloudMigration.IsTarget
featureToggles := hs.Features.GetEnabled(c.Req.Context())
// this is needed for backwards compatibility with external plugins
@@ -408,7 +406,6 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
DisableSignoutMenu: hs.Cfg.DisableSignoutMenu,
}
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
hasEnabledProviders := hs.samlEnabled() || hs.authnService.IsClientEnabled(authn.ClientLDAP)
@@ -447,7 +444,6 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
frontendSettings.Namespace = hs.namespacer(c.OrgID)
// experimental scope features
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagScopeFilters) {
frontendSettings.ListScopesEndpoint = hs.Cfg.ScopesListScopesURL
frontendSettings.ListDashboardScopesEndpoint = hs.Cfg.ScopesListDashboardsURL
-2
View File
@@ -62,7 +62,6 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
return nil, err
}
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagIndividualCookiePreferences) {
if !prefs.Cookies("analytics") {
settings.GoogleAnalytics4Id = ""
@@ -95,7 +94,6 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
}
var regionalFormat string
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagLocaleFormatPreference) {
regionalFormat = "en"
-1
View File
@@ -361,7 +361,6 @@ func (hs *HTTPServer) RedirectResponseWithError(c *contextmodel.ReqContext, err
func (hs *HTTPServer) redirectURLWithErrorCookie(c *contextmodel.ReqContext, err error) string {
setCookie := true
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagIndividualCookiePreferences) {
var userID int64
if c.SignedInUser != nil && !c.IsNil() {
-1
View File
@@ -81,7 +81,6 @@ func (proxy *PluginProxy) HandleRequest() {
hasSlash := strings.HasSuffix(proxy.proxyPath, "/")
proxy.proxyPath = path
//nolint:staticcheck // not yet migrated to OpenFeature
if hasSlash && !strings.HasSuffix(path, "/") && proxy.features.IsEnabled(proxy.ctx.Req.Context(), featuremgmt.FlagPluginProxyPreserveTrailingSlash) {
proxy.proxyPath += "/"
}
-2
View File
@@ -144,7 +144,6 @@ func (hs *HTTPServer) GetPluginList(c *contextmodel.ReqContext) response.Respons
AngularDetected: pluginDef.Angular.Detected,
}
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Cfg.ManagedServiceAccountsEnabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) {
listItem.IAM = pluginDef.IAM
}
@@ -491,7 +490,6 @@ func (hs *HTTPServer) InstallPlugin(c *contextmodel.ReqContext) response.Respons
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to install plugin", err)
}
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Cfg.ManagedServiceAccountsEnabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) {
// This is a non-blocking function that verifies that the installer has
// the permissions that the plugin requests to have on Grafana.
-1
View File
@@ -29,7 +29,6 @@ func (c *ResultConverter) Convert(ctx context.Context,
}
var dt data.FrameType
//nolint:staticcheck // not yet migrated to OpenFeature
dt, useDataplane, _ := shouldUseDataplane(frames, logger, c.Features.IsEnabled(ctx, featuremgmt.FlagDisableSSEDataplane))
if useDataplane {
logger.Debug("Handling SSE data source query through dataplane", "datatype", dt)
-1
View File
@@ -70,7 +70,6 @@ func handleDataplaneFrames(ctx context.Context, tracer tracing.Tracer, features
case data.KindTimeSeries:
return handleDataplaneTimeseries(frames)
case data.KindNumeric:
//nolint:staticcheck // not yet migrated to OpenFeature
sortMetrics := !features.IsEnabled(ctx, featuremgmt.FlagDisableNumericMetricsSortingInExpressions)
return handleDataplaneNumeric(frames, sortMetrics)
default:
+1 -1
View File
@@ -68,7 +68,7 @@ type DataPipeline []Node
// map of the refId of the of each command
func (dp *DataPipeline) execute(c context.Context, now time.Time, s *Service) (mathexp.Vars, error) {
vars := make(mathexp.Vars)
//nolint:staticcheck // not yet migrated to OpenFeature
groupByDSFlag := s.features.IsEnabled(c, featuremgmt.FlagSseGroupByDatasource)
// Execute datasource nodes first, and grouped by datasource.
if groupByDSFlag {
+3 -3
View File
@@ -15,7 +15,6 @@ import (
_ "github.com/blugelabs/bluge"
_ "github.com/blugelabs/bluge_segment_api"
_ "github.com/crewjam/saml"
_ "github.com/docker/go-connections/nat"
_ "github.com/go-jose/go-jose/v4"
_ "github.com/gobwas/glob"
_ "github.com/googleapis/gax-go/v2"
@@ -31,7 +30,6 @@ import (
_ "github.com/spf13/cobra" // used by the standalone apiserver cli
_ "github.com/spyzhov/ajson"
_ "github.com/stretchr/testify/require"
_ "github.com/testcontainers/testcontainers-go"
_ "gocloud.dev/secrets/awskms"
_ "gocloud.dev/secrets/azurekeyvault"
_ "gocloud.dev/secrets/gcpkms"
@@ -56,7 +54,9 @@ import (
_ "github.com/grafana/e2e"
_ "github.com/grafana/gofpdf"
_ "github.com/grafana/gomemcache/memcache"
_ "github.com/grafana/tempo/pkg/traceql"
_ "github.com/grafana/grafana/apps/alerting/alertenrichment/pkg/apis/alertenrichment/v1beta1"
_ "github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1"
_ "github.com/grafana/tempo/pkg/traceql"
_ "github.com/testcontainers/testcontainers-go"
)
-1
View File
@@ -64,7 +64,6 @@ func (l *loggerImpl) Middleware() web.Middleware {
// put the start time on context so we can measure it later.
r = r.WithContext(log.InitstartTime(r.Context(), time.Now()))
//nolint:staticcheck // not yet migrated to OpenFeature
if l.flags.IsEnabled(r.Context(), featuremgmt.FlagUnifiedRequestLog) {
r = r.WithContext(errutil.SetUnifiedLogging(r.Context()))
}
-1
View File
@@ -114,7 +114,6 @@ func RequestMetrics(features featuremgmt.FeatureToggles, cfg *setting.Cfg, promR
handler = "notfound"
} else {
// log requests where we could not identify handler so we can register them.
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabled(r.Context(), featuremgmt.FlagLogRequestsInstrumentedAsUnknown) {
log.Warn("request instrumented as unknown", "path", r.URL.Path, "status_code", status)
}
@@ -90,8 +90,3 @@ func (d *directResourceClient) Watch(ctx context.Context, in *resourcepb.WatchRe
func (d *directResourceClient) BulkProcess(ctx context.Context, opts ...grpc.CallOption) (resourcepb.BulkStore_BulkProcessClient, error) {
return nil, fmt.Errorf("BulkProcess not supported with direct resource client")
}
// RebuildIndexes implements resource.ResourceClient.
func (b *directResourceClient) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error) {
return nil, fmt.Errorf("not implemented")
}
@@ -109,14 +109,13 @@ func ProvideMigratorDashboardAccessor(
features featuremgmt.FeatureToggles,
) MigrationDashboardAccessor {
return &dashboardSqlAccess{
sql: sql,
namespacer: claims.OrgNamespaceFormatter,
dashStore: nil, // not needed for migration
provisioning: provisioning,
dashboardPermissionSvc: nil, // not needed for migration
libraryPanelSvc: nil, // not needed for migration
accessControl: accessControl,
//nolint:staticcheck // not yet migrated to OpenFeature
sql: sql,
namespacer: claims.OrgNamespaceFormatter,
dashStore: nil, // not needed for migration
provisioning: provisioning,
dashboardPermissionSvc: nil, // not needed for migration
libraryPanelSvc: nil, // not needed for migration
accessControl: accessControl,
invalidDashboardParseFallbackEnabled: features.IsEnabled(context.Background(), featuremgmt.FlagScanRowInvalidDashboardParseFallbackEnabled),
}
}
@@ -133,15 +132,14 @@ func NewDashboardSQLAccess(sql legacysql.LegacyDatabaseProvider,
) *dashboardSqlAccess {
dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
return &dashboardSqlAccess{
sql: sql,
namespacer: namespacer,
dashStore: dashStore,
provisioning: provisioning,
dashboardSearchClient: *dashboardSearchClient,
dashboardPermissionSvc: dashboardPermissionSvc,
libraryPanelSvc: libraryPanelSvc,
accessControl: accessControl,
//nolint:staticcheck // not yet migrated to OpenFeature
sql: sql,
namespacer: namespacer,
dashStore: dashStore,
provisioning: provisioning,
dashboardSearchClient: *dashboardSearchClient,
dashboardPermissionSvc: dashboardPermissionSvc,
libraryPanelSvc: libraryPanelSvc,
accessControl: accessControl,
invalidDashboardParseFallbackEnabled: features.IsEnabled(context.Background(), featuremgmt.FlagScanRowInvalidDashboardParseFallbackEnabled),
}
}
@@ -1048,7 +1046,3 @@ func parseLibraryPanelRow(p panel) (dashboardV0.LibraryPanel, error) {
return item, nil
}
func (b *dashboardSqlAccess) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest) (*resourcepb.RebuildIndexesResponse, error) {
return nil, fmt.Errorf("not implemented")
}
@@ -33,11 +33,9 @@ func (b *DashboardsAPIBuilder) ValidateDashboardSpec(ctx context.Context, obj ru
case *v0.Dashboard:
errorOnSchemaMismatches = false // Never error for v0
case *v1.Dashboard:
//nolint:staticcheck // not yet migrated to OpenFeature
errorOnSchemaMismatches = !b.features.IsEnabled(ctx, featuremgmt.FlagDashboardDisableSchemaValidationV1)
case *v2alpha1.Dashboard:
case *v2beta1.Dashboard:
//nolint:staticcheck // not yet migrated to OpenFeature
errorOnSchemaMismatches = !b.features.IsEnabled(ctx, featuremgmt.FlagDashboardDisableSchemaValidationV2)
default:
return nil, fmt.Errorf("invalid dashboard type: %T", obj)
@@ -47,7 +45,6 @@ func (b *DashboardsAPIBuilder) ValidateDashboardSpec(ctx context.Context, obj ru
return nil, apierrors.NewBadRequest("Not supported: FieldValidationMode: Warn")
}
//nolint:staticcheck // not yet migrated to OpenFeature
alwaysLogSchemaValidationErrors := b.features.IsEnabled(ctx, featuremgmt.FlagDashboardSchemaValidationLogging)
var errors field.ErrorList
@@ -581,8 +581,3 @@ func (m *mockSearchClient) GetStats(ctx context.Context, in *resourcepb.Resource
func (m *mockSearchClient) Search(ctx context.Context, in *resourcepb.ResourceSearchRequest, opts ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) {
return m.search, m.searchErr
}
// RebuildIndexes implements resourcepb.ResourceIndexClient.
func (m *mockSearchClient) RebuildIndexes(ctx context.Context, in *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error) {
return nil, fmt.Errorf("not implemented")
}
@@ -8,7 +8,6 @@ import (
"net"
"net/http"
"strconv"
"strings"
"sync"
"testing"
"time"
@@ -176,28 +175,6 @@ func TestIntegrationDistributor(t *testing.T) {
}
})
t.Run("RebuildIndexes", func(t *testing.T) {
instanceResponseCount := make(map[string]int)
// simulate RebuildIndexes for a single namespace
testNamespace := testNamespaces[0]
req := &resourcepb.RebuildIndexesRequest{
Namespace: testNamespace,
Keys: []*resourcepb.ResourceKey{{
Namespace: testNamespace,
Group: "folder.grafana.app",
Resource: "folders",
}},
}
distributorRes := getDistributorResponse(t, req, distributorServer.resourceClient.RebuildIndexes, instanceResponseCount)
require.Nil(t, distributorRes.Error)
// assert all instances got the response by looking at the merged details
count := strings.Count(distributorRes.Details, "{instance:")
require.Equal(t, len(testServers), count)
})
var wg sync.WaitGroup
for _, testServer := range testServers {
wg.Add(1)
+2 -2
View File
@@ -666,7 +666,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
azurePromMigrationService := promtypemigration.ProvideAzurePromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg)
amazonPromMigrationService := promtypemigration.ProvideAmazonPromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg)
promTypeMigrationProviderImpl := promtypemigration.ProvidePromTypeMigrationProvider(serverLockService, featureToggles, azurePromMigrationService, amazonPromMigrationService)
provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl)
provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl, serverLockService)
if err != nil {
return nil, err
}
@@ -1309,7 +1309,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
azurePromMigrationService := promtypemigration.ProvideAzurePromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg)
amazonPromMigrationService := promtypemigration.ProvideAmazonPromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg)
promTypeMigrationProviderImpl := promtypemigration.ProvidePromTypeMigrationProvider(serverLockService, featureToggles, azurePromMigrationService, amazonPromMigrationService)
provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl)
provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl, serverLockService)
if err != nil {
return nil, err
}
@@ -741,7 +741,6 @@ func (s *Service) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.SaveExternalServiceRole")
defer span.End()
//nolint:staticcheck // not yet migrated to OpenFeature
if !s.cfg.ManagedServiceAccountsEnabled || !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) {
s.log.Debug("Registering an external service role is behind a feature flag, enable it to use this feature.")
return nil
@@ -758,7 +757,6 @@ func (s *Service) DeleteExternalServiceRole(ctx context.Context, externalService
ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.DeleteExternalServiceRole")
defer span.End()
//nolint:staticcheck // not yet migrated to OpenFeature
if !s.cfg.ManagedServiceAccountsEnabled || !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) {
s.log.Debug("Deleting an external service role is behind a feature flag, enable it to use this feature.")
return nil
@@ -28,7 +28,6 @@ var DashboardEditActions = append(DashboardViewActions, []string{dashboards.Acti
var DashboardAdminActions = append(DashboardEditActions, []string{dashboards.ActionDashboardsPermissionsRead, dashboards.ActionDashboardsPermissionsWrite}...)
func getDashboardViewActions(features featuremgmt.FeatureToggles) []string {
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) {
return append(DashboardViewActions, accesscontrol.ActionAnnotationsRead)
}
@@ -36,7 +35,6 @@ func getDashboardViewActions(features featuremgmt.FeatureToggles) []string {
}
func getDashboardEditActions(features featuremgmt.FeatureToggles) []string {
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) {
return append(DashboardEditActions, []string{accesscontrol.ActionAnnotationsRead, accesscontrol.ActionAnnotationsWrite, accesscontrol.ActionAnnotationsDelete, accesscontrol.ActionAnnotationsCreate}...)
}
@@ -44,7 +42,6 @@ func getDashboardEditActions(features featuremgmt.FeatureToggles) []string {
}
func getDashboardAdminActions(features featuremgmt.FeatureToggles) []string {
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) {
return append(DashboardAdminActions, []string{accesscontrol.ActionAnnotationsRead, accesscontrol.ActionAnnotationsWrite, accesscontrol.ActionAnnotationsDelete, accesscontrol.ActionAnnotationsCreate}...)
}
@@ -61,7 +61,6 @@ func (authz *AuthService) Authorize(ctx context.Context, query annotations.ItemQ
scopeTypes := annotationScopeTypes(scopes)
_, canAccessOrgAnnotations := scopeTypes[annotations.Organization.String()]
_, canAccessDashAnnotations := scopeTypes[annotations.Dashboard.String()]
//nolint:staticcheck // not yet migrated to OpenFeature
if authz.features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) {
canAccessDashAnnotations = true
}
@@ -123,7 +122,6 @@ func (authz *AuthService) dashboardsWithVisibleAnnotations(ctx context.Context,
}
filterType := searchstore.TypeDashboard
//nolint:staticcheck // not yet migrated to OpenFeature
if authz.features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) {
filterType = searchstore.TypeAnnotation
}
@@ -83,7 +83,6 @@ func ProvideRegistration(
}
}
//nolint:staticcheck // not yet migrated to OpenFeature
if cfg.PasswordlessMagicLinkAuth.Enabled && features.IsEnabled(context.Background(), featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
hasEnabledProviders := authnSvc.IsClientEnabled(authn.ClientSAML) || authnSvc.IsClientEnabled(authn.ClientLDAP)
if !hasEnabledProviders {
-1
View File
@@ -210,7 +210,6 @@ func (c *CachingServiceClient) WithQueryDataCaching(ctx context.Context, req *ba
// Update the query cache with the result for this metrics request
if err == nil && cr.UpdateCacheFn != nil {
// If AWS async caching is not enabled, use the old code path
//nolint:staticcheck // not yet migrated to OpenFeature
if c.features == nil || !c.features.IsEnabled(ctx, featuremgmt.FlagAwsAsyncQueryCaching) {
cr.UpdateCacheFn(ctx, resp)
} else if reqCtx != nil {
@@ -2009,7 +2009,14 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex
request.Limit = query.Limit
request.Page = query.Page
request.Offset = (query.Page - 1) * query.Limit // only relevant when running in modes 3+
request.Fields = dashboardsearch.IncludeFields
request.Fields = append(
dashboardsearch.IncludeFields,
// Include the dashboard legacy ID in the results, as it is needed when
// determining whether a provisioned dashboard exists or not, see
// `(*DashboardServiceImpl).searchProvisionedDashboardsThroughK8s`.
resource.SEARCH_FIELD_LEGACY_ID,
resource.SEARCH_FIELD_LABELS+"."+resource.SEARCH_FIELD_LEGACY_ID,
)
namespace := dr.k8sclient.GetNamespace(query.OrgId)
var err error
@@ -2016,6 +2016,26 @@ func TestSearchDashboardsThroughK8sRaw(t *testing.T) {
_, err := service.searchDashboardsThroughK8s(ctx, query)
require.NoError(t, err)
})
t.Run("search will request legacy dashboard ID", func(t *testing.T) {
ctx := context.Background()
k8sCliMock := new(client.MockK8sHandler)
service := &DashboardServiceImpl{k8sclient: k8sCliMock}
query := &dashboards.FindPersistedDashboardsQuery{
ManagedBy: utils.ManagerKindClassicFP, //nolint:staticcheck
OrgId: 1,
}
k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool {
return slices.Contains(req.Fields, "grafana.app/deprecatedInternalID") &&
slices.Contains(req.Fields, "labels.grafana.app/deprecatedInternalID")
})).Return(&resourcepb.ResourceSearchResponse{
Results: &resourcepb.ResourceTable{},
TotalHits: 0,
}, nil)
_, err := service.searchDashboardsThroughK8s(ctx, query)
require.NoError(t, err)
})
}
func TestSearchProvisionedDashboardsThroughK8sRaw(t *testing.T) {
+1 -7
View File
@@ -10,10 +10,6 @@ import (
type FeatureToggles interface {
// IsEnabled checks if a feature is enabled for a given context.
// The settings may be per user, tenant, or globally set in the cloud
//
// Deprecated: FeatureToggles.IsEnabled is deprecated and will be removed in a future release.
// Evaluate with OpenFeature instead (see [github.com/open-feature/go-sdk/openfeature.Client]), for example:
// openfeature.NewDefaultClient().Boolean(ctx, "your-flag", false, openfeature.TransactionContext(ctx))
IsEnabled(ctx context.Context, flag string) bool
// IsEnabledGlobally checks if a flag is configured globally. For now, this is the same
@@ -23,9 +19,7 @@ type FeatureToggles interface {
// a full server restart for a change to take place.
//
// Deprecated: FeatureToggles.IsEnabledGlobally is deprecated and will be removed in a future release.
// Toggles that must be reliably evaluated at the service startup should be
// changed to settings (see setting.StartupSettings), and/or removed entirely.
// For app registration please use `grafana-apiserver.runtime_config` in settings.ini
// Evaluate with OpenFeature instead (see [github.com/open-feature/go-sdk/openfeature.Client])
IsEnabledGlobally(flag string) bool
// Get the enabled flags -- this *may* also include disabled flags (with value false)
-1
View File
@@ -148,7 +148,6 @@ func (l *LibraryElementService) deleteHandler(c *contextmodel.ReqContext) respon
// 404: notFoundError
// 500: internalServerError
func (l *LibraryElementService) getHandler(c *contextmodel.ReqContext) response.Response {
//nolint:staticcheck // not yet migrated to OpenFeature
if l.features.IsEnabled(c.Req.Context(), featuremgmt.FlagKubernetesLibraryPanels) {
l.k8sHandler.getK8sLibraryElement(c)
return nil // already handled in the k8s handler
@@ -44,7 +44,6 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
Text: "Organizations", SubTitle: "Isolated instances of Grafana running on the same server", Id: "global-orgs", Url: s.cfg.AppSubURL + "/admin/orgs", Icon: "building",
})
}
//nolint:staticcheck // not yet migrated to OpenFeature
if hasAccess(cloudmigration.MigrationAssistantAccess) && s.features.IsEnabled(ctx, featuremgmt.FlagOnPremToCloudMigrations) {
generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{
Text: "Migrate to Grafana Cloud",
@@ -100,7 +99,6 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
})
}
//nolint:staticcheck // not yet migrated to OpenFeature
if (s.cfg.Env == setting.Dev) || s.features.IsEnabled(ctx, featuremgmt.FlagEnableExtensionsAdminPage) && hasAccess(pluginaccesscontrol.AdminAccessEvaluator) {
pluginsNodeLinks = append(pluginsNodeLinks, &navtree.NavLink{
Text: "Extensions",
@@ -149,7 +147,6 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
})
}
//nolint:staticcheck // not yet migrated to OpenFeature
if s.license.FeatureEnabled("groupsync") &&
s.features.IsEnabled(ctx, featuremgmt.FlagGroupAttributeSync) &&
hasAccess(ac.EvalAny(
@@ -407,7 +407,6 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navt
})
}
//nolint:staticcheck // not yet migrated to OpenFeature
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagRestoreDashboards) && (c.GetOrgRole() == org.RoleAdmin || c.IsGrafanaAdmin) {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Recently deleted",
@@ -436,7 +435,6 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
hasAccess := ac.HasAccess(s.accessControl, c)
var alertChildNavs []*navtree.NavLink
//nolint:staticcheck // not yet migrated to OpenFeature
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingTriage) {
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(ac.ActionAlertingRuleExternalRead))) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
@@ -494,7 +492,6 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Alert groups", SubTitle: "See grouped alerts with active notifications", Id: "groups", Url: s.cfg.AppSubURL + "/alerting/groups", Icon: "layer-group"})
}
//nolint:staticcheck // not yet migrated to OpenFeature
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingCentralAlertHistory) {
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead))) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
@@ -506,7 +503,6 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
})
}
}
//nolint:staticcheck // not yet migrated to OpenFeature
if c.GetOrgRole() == org.RoleAdmin && s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertRuleRestore) && s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingRuleRecoverDeleted) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
Text: "Recently deleted",
@@ -77,7 +77,6 @@ func (srv ConfigSrv) RoutePostNGalertConfig(c *contextmodel.ReqContext, body api
return response.Error(http.StatusBadRequest, "Invalid alertmanager choice specified", err)
}
//nolint:staticcheck // not yet migrated to OpenFeature
disableExternal := srv.featureManager.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingDisableSendAlertsExternal)
if disableExternal && sendAlertsTo != ngmodels.InternalAlertmanager {
return response.Error(http.StatusBadRequest, "Sending alerts to external alertmanagers is disallowed on this instance", err)
-3
View File
@@ -79,7 +79,6 @@ func (srv TestingApiSrv) RouteTestGrafanaRuleConfig(c *contextmodel.ReqContext,
return response.ErrOrFallback(http.StatusInternalServerError, "failed to authorize access to rule group", err)
}
//nolint:staticcheck // not yet migrated to OpenFeature
if srv.featureManager.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingQueryOptimization) {
if _, err := store.OptimizeAlertQueries(rule.Data); err != nil {
return ErrResp(http.StatusInternalServerError, err, "Failed to optimize query")
@@ -179,7 +178,6 @@ func (srv TestingApiSrv) RouteEvalQueries(c *contextmodel.ReqContext, cmd apimod
}
var optimizations []store.Optimization
//nolint:staticcheck // not yet migrated to OpenFeature
if srv.featureManager.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingQueryOptimization) {
var err error
optimizations, err = store.OptimizeAlertQueries(cond.Data)
@@ -225,7 +223,6 @@ func addOptimizedQueryWarnings(evalResults *backend.QueryDataResponse, optimizat
}
func (srv TestingApiSrv) BacktestAlertRule(c *contextmodel.ReqContext, cmd apimodels.BacktestConfig) response.Response {
//nolint:staticcheck // not yet migrated to OpenFeature
if !srv.featureManager.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingBacktesting) {
return ErrResp(http.StatusNotFound, nil, "Backgtesting API is not enabled")
}
@@ -6,7 +6,6 @@ import (
"encoding/json"
"errors"
"fmt"
"maps"
"net/url"
"slices"
"sort"
@@ -27,9 +26,6 @@ import (
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/state"
"github.com/grafana/grafana/pkg/util"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
)
type RuleStoreReader interface {
@@ -58,9 +54,6 @@ type PrometheusSrv struct {
provenanceStore ProvenanceStore
}
// Package-level OpenTelemetry tracer per Grafana instrumentation conventions.
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/ngalert/api/prometheus")
func NewPrometheusSrv(log log.Logger, manager state.AlertInstanceManager, status StatusReader, store RuleStoreReader, authz RuleGroupAccessControlService, provenanceStore ProvenanceStore) *PrometheusSrv {
return &PrometheusSrv{
log,
@@ -226,14 +219,6 @@ func GetStatesFromQuery(v url.Values) (map[eval.State]struct{}, error) {
return states, nil
}
func MapStateSetToStrings(stateSet map[eval.State]struct{}) []string {
states := make([]string, 0, len(stateSet))
for state := range stateSet {
states = append(states, state.String())
}
return states
}
func GetHealthFromQuery(v url.Values) (map[string]struct{}, error) {
health := make(map[string]struct{})
for _, s := range v["health"] {
@@ -267,13 +252,6 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon
// As we are using req.Form directly, this triggers a call to ParseForm() if needed.
c.Query("")
ctx, span := tracer.Start(c.Req.Context(), "api.prometheus.RouteGetRuleStatuses")
defer span.End()
// Propagate the new context so child spans can attach to it.
c.Req = c.Req.WithContext(ctx)
orgID := c.GetOrgID()
span.SetAttributes(attribute.Int64("org_id", orgID))
ruleResponse := apimodels.RuleResponse{
DiscoveryBase: apimodels.DiscoveryBase{
Status: "success",
@@ -283,14 +261,13 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon
},
}
namespaceMap, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), orgID, c.SignedInUser)
namespaceMap, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), c.GetOrgID(), c.SignedInUser)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = fmt.Sprintf("failed to get namespaces visible to the user: %s", err.Error())
ruleResponse.ErrorType = apiv1.ErrServer
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse)
}
span.AddEvent("User visible namespaces retrieved")
allowedNamespaces := map[string]string{}
for namespaceUID, folder := range namespaceMap {
@@ -306,8 +283,6 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon
allowedNamespaces[namespaceUID] = folder.Fullpath
}
}
span.AddEvent("User permissions checked")
span.SetAttributes(attribute.Int("allowedNamespaces", len(allowedNamespaces)))
provenanceRecords, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.GetOrgID(), (&ngmodels.AlertRule{}).ResourceType())
if err != nil {
@@ -322,7 +297,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon
srv.store,
RuleGroupStatusesOptions{
Ctx: c.Req.Context(),
OrgID: orgID,
OrgID: c.OrgID,
Query: c.Req.Form,
AllowedNamespaces: allowedNamespaces,
},
@@ -430,10 +405,6 @@ func RuleAlertStateMutatorGenerator(manager state.AlertInstanceManager) RuleAler
}
func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opts RuleGroupStatusesOptions, ruleStatusMutator RuleStatusMutator, alertStateMutator RuleAlertStateMutator, provenanceRecords map[string]ngmodels.Provenance) apimodels.RuleResponse {
ctx, span := tracer.Start(opts.Ctx, "api.prometheus.PrepareRuleGroupStatusesV2")
defer span.End()
opts.Ctx = ctx
ruleResponse := apimodels.RuleResponse{
DiscoveryBase: apimodels.DiscoveryBase{
Status: "success",
@@ -457,17 +428,9 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
}
span.SetAttributes(
attribute.String("dashboard_uid", dashboardUID),
attribute.Int64("panel_id", panelID),
)
limitRulesPerGroup := getInt64WithDefault(opts.Query, "limit_rules", -1)
limitAlertsPerRule := getInt64WithDefault(opts.Query, "limit_alerts", -1)
span.SetAttributes(
attribute.Int64("limit_rules", limitRulesPerGroup),
attribute.Int64("limit_alerts", limitAlertsPerRule),
)
matchers, err := getMatchersFromQuery(opts.Query)
if err != nil {
ruleResponse.Status = "error"
@@ -475,8 +438,6 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
}
span.SetAttributes(attribute.Int("matcher_count", len(matchers)))
stateFilterSet, err := GetStatesFromQuery(opts.Query)
if err != nil {
ruleResponse.Status = "error"
@@ -484,10 +445,6 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
}
span.SetAttributes(
attribute.Int("state_filter_count", len(stateFilterSet)),
attribute.StringSlice("state_filter", MapStateSetToStrings(stateFilterSet)),
)
healthFilterSet, err := GetHealthFromQuery(opts.Query)
if err != nil {
@@ -496,18 +453,11 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
}
span.SetAttributes(
attribute.Int("health_filter_count", len(healthFilterSet)),
attribute.StringSlice("health_filter", slices.Collect(maps.Keys(healthFilterSet))),
)
var labelOptions []ngmodels.LabelOption
if !getBoolWithDefault(opts.Query, queryIncludeInternalLabels, false) {
labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels())
}
span.SetAttributes(
attribute.Bool("include_internal_labels", len(labelOptions) == 0),
)
if len(opts.AllowedNamespaces) == 0 {
log.Debug("User does not have access to any namespaces")
@@ -526,36 +476,19 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
}
}
span.SetAttributes(
attribute.Bool("folder_uid_set", folderUID != ""),
attribute.Int("namespace_count", len(namespaceUIDs)),
)
ruleGroups := opts.Query["rule_group"]
ruleUIDs := opts.Query["rule_uid"]
span.SetAttributes(
attribute.Int("rule_group_count", len(ruleGroups)),
attribute.Int("rule_uid_count", len(ruleUIDs)),
)
receiverName := opts.Query.Get("receiver_name")
span.SetAttributes(attribute.Bool("receiver_name_set", receiverName != ""))
title := opts.Query.Get("search.rule_name")
span.SetAttributes(attribute.Bool("search_rule_name_set", title != ""))
searchRuleGroup := opts.Query.Get("search.rule_group")
span.SetAttributes(attribute.Bool("search_rule_group_set", searchRuleGroup != ""))
var ruleType ngmodels.RuleTypeFilter
switch ngmodels.RuleType(opts.Query.Get("rule_type")) {
case ngmodels.RuleTypeAlerting:
ruleType = ngmodels.RuleTypeFilterAlerting
span.SetAttributes(attribute.Bool("alerting_only", true))
case ngmodels.RuleTypeRecording:
ruleType = ngmodels.RuleTypeFilterRecording
span.SetAttributes(attribute.Bool("recording_only", true))
default:
ruleType = ngmodels.RuleTypeFilterAll
}
@@ -574,23 +507,11 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
maxGroups := getInt64WithDefault(opts.Query, "group_limit", -1)
maxRules := getInt64WithDefault(opts.Query, "rule_limit", -1)
nextToken := opts.Query.Get("group_next_token")
span.SetAttributes(
attribute.Int64("group_limit", maxGroups),
attribute.Int64("rule_limit", maxRules),
attribute.Bool("group_next_token_set", nextToken != ""),
)
if maxGroups == 0 || maxRules == 0 {
return ruleResponse
}
ruleNames := opts.Query["rule_name"]
ruleNamesSet := make(map[string]struct{}, len(ruleNames))
for _, rn := range ruleNames {
ruleNamesSet[rn] = struct{}{}
}
span.SetAttributes(attribute.Int("rule_name_count", len(ruleNamesSet)))
byGroupQuery := ngmodels.ListAlertRulesExtendedQuery{
ListAlertRulesQuery: ngmodels.ListAlertRulesQuery{
OrgID: opts.OrgID,
@@ -615,11 +536,12 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
ruleResponse.ErrorType = apiv1.ErrServer
return ruleResponse
}
span.SetAttributes(
attribute.Int("store_rule_list_len", len(ruleList)),
attribute.Bool("store_continue_token_set", continueToken != ""),
)
span.AddEvent("Alert rules retrieved from store")
ruleNames := opts.Query["rule_name"]
ruleNamesSet := make(map[string]struct{}, len(ruleNames))
for _, rn := range ruleNames {
ruleNamesSet[rn] = struct{}{}
}
groupedRules := getGroupedRules(log, ruleList, ruleNamesSet, opts.AllowedNamespaces)
rulesTotals := make(map[string]int64, len(groupedRules))
-4
View File
@@ -194,11 +194,8 @@ func (ng *AlertNG) init() error {
var opts []notifier.Option
moaLogger := log.New("ngalert.multiorg.alertmanager")
crypto := notifier.NewCrypto(ng.SecretsService, ng.store, moaLogger)
//nolint:staticcheck // not yet migrated to OpenFeature
remotePrimary := ng.FeatureToggles.IsEnabled(initCtx, featuremgmt.FlagAlertmanagerRemotePrimary)
//nolint:staticcheck // not yet migrated to OpenFeature
remoteSecondary := ng.FeatureToggles.IsEnabled(initCtx, featuremgmt.FlagAlertmanagerRemoteSecondary)
//nolint:staticcheck // not yet migrated to OpenFeature
remoteSecondaryWithRemoteState := ng.FeatureToggles.IsEnabled(initCtx, featuremgmt.FlagAlertmanagerRemoteSecondaryWithRemoteState)
if remotePrimary || remoteSecondary || remoteSecondaryWithRemoteState {
m := ng.Metrics.GetRemoteAlertmanagerMetrics()
@@ -720,7 +717,6 @@ func configureNotificationHistorian(
l log.Logger,
tracer tracing.Tracer,
) (nfstatus.NotificationHistorian, error) {
//nolint:staticcheck // not yet migrated to OpenFeature
if !featureToggles.IsEnabled(ctx, featuremgmt.FlagAlertingNotificationHistory) || !cfg.Enabled {
met.Info.Set(0)
return nil, nil
-1
View File
@@ -89,7 +89,6 @@ func (d *AlertsRouter) SyncAndApplyConfigFromDatabase(ctx context.Context) error
d.logger.Debug("Attempting to sync admin configs", "count", len(cfgs))
//nolint:staticcheck // not yet migrated to OpenFeature
disableExternal := d.featureManager.IsEnabled(ctx, featuremgmt.FlagAlertingDisableSendAlertsExternal)
orgsFound := make(map[int64]struct{}, len(cfgs))
-1
View File
@@ -1097,7 +1097,6 @@ func (st DBstore) GetAlertRulesForScheduling(ctx context.Context, query *ngmodel
continue
}
}
//nolint:staticcheck // not yet migrated to OpenFeature
if st.FeatureToggles.IsEnabled(ctx, featuremgmt.FlagAlertingQueryOptimization) {
if optimizations, err := OptimizeAlertQueries(converted.Data); err != nil {
st.Logger.Error("Could not migrate rule from range to instant query", "rule", rule.UID, "err", err)
@@ -87,12 +87,10 @@ func ProvideSyncer(
}
func (s *syncer) Sync(ctx context.Context, source install.Source, installedPlugins []*plugins.Plugin) error {
//nolint:staticcheck // not yet migrated to OpenFeature
if !s.featureToggles.IsEnabled(ctx, featuremgmt.FlagPluginInstallAPISync) {
return nil
}
//nolint:staticcheck // not yet migrated to OpenFeature
if !s.featureToggles.IsEnabled(ctx, featuremgmt.FlagPluginStoreServiceLoading) {
logging.DefaultLogger.Warn("pluginInstallAPISync is enabled, but pluginStoreServiceLoading is disabled. skipping plugin sync.")
return nil
-2
View File
@@ -61,7 +61,6 @@ func UpdatePreferencesFor(ctx context.Context,
Navbar: dtoCmd.Navbar,
}
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabled(ctx, featuremgmt.FlagLocaleFormatPreference) {
saveCmd.RegionalFormat = dtoCmd.RegionalFormat
}
@@ -102,7 +101,6 @@ func GetPreferencesFor(ctx context.Context,
dto.Language = &preference.JSONData.Language
}
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabled(ctx, featuremgmt.FlagLocaleFormatPreference) {
if preference.JSONData.RegionalFormat != "" {
dto.RegionalFormat = &preference.JSONData.RegionalFormat
@@ -43,7 +43,6 @@ func ProvidePromTypeMigrationProvider(
}
func (s *PromTypeMigrationProviderImpl) Run(ctx context.Context) error {
//nolint:staticcheck // not yet migrated to OpenFeature
if !s.features.IsEnabled(ctx, featuremgmt.FlagPrometheusTypeMigration) {
return nil
}
@@ -2,6 +2,7 @@ package dashboards
import (
"context"
"errors"
"fmt"
"os"
"time"
@@ -9,10 +10,12 @@ import (
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
folderV1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/provisioning/utils"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
)
@@ -28,7 +31,7 @@ type DashboardProvisioner interface {
}
// DashboardProvisionerFactory creates DashboardProvisioners based on input
type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service) (DashboardProvisioner, error)
type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService, *setting.Cfg, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service, *serverlock.ServerLockService) (DashboardProvisioner, error)
// Provisioner is responsible for syncing dashboard from disk to Grafana's database.
type Provisioner struct {
@@ -38,6 +41,8 @@ type Provisioner struct {
duplicateValidator duplicateValidator
provisioner dashboards.DashboardProvisioningService
dual dualwrite.Service
serverLock *serverlock.ServerLockService
cfg *setting.Cfg
}
func (provider *Provisioner) HasDashboardSources() bool {
@@ -45,7 +50,7 @@ func (provider *Provisioner) HasDashboardSources() bool {
}
// New returns a new DashboardProvisioner
func New(ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, orgService org.Service, dashboardStore utils.DashboardStore, folderService folder.Service, dual dualwrite.Service) (DashboardProvisioner, error) {
func New(ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, cfg *setting.Cfg, orgService org.Service, dashboardStore utils.DashboardStore, folderService folder.Service, dual dualwrite.Service, serverLockService *serverlock.ServerLockService) (DashboardProvisioner, error) {
logger := log.New("provisioning.dashboard")
cfgReader := &configReader{path: configDirectory, log: logger, orgExists: utils.NewOrgExistsChecker(orgService)}
configs, err := cfgReader.readConfig(ctx)
@@ -78,6 +83,8 @@ func New(ctx context.Context, configDirectory string, provisioner dashboards.Das
duplicateValidator: newDuplicateValidator(logger, fileReaders),
provisioner: provisioner,
dual: dual,
serverLock: serverLockService,
cfg: cfg,
}
return d, nil
@@ -95,23 +102,53 @@ func (provider *Provisioner) Provision(ctx context.Context) error {
}
}
provider.log.Info("starting to provision dashboards")
var errProvisioning error
for _, reader := range provider.fileReaders {
if err := reader.walkDisk(ctx); err != nil {
if os.IsNotExist(err) {
// don't stop the provisioning service in case the folder is missing. The folder can appear after the startup
provider.log.Warn("Failed to provision config", "name", reader.Cfg.Name, "error", err)
return nil
}
return fmt.Errorf("failed to provision config %v: %w", reader.Cfg.Name, err)
// retry obtaining the lock for 20 attempts
retryOpt := func(attempts int) error {
if attempts < 20 {
return nil
}
return errors.New("retries exhausted")
}
provider.duplicateValidator.validate()
provider.log.Info("finished to provision dashboards")
return nil
lockTimeConfig := serverlock.LockTimeConfig{
// if a replica crashes while holding the lock, other replicas can obtain the
// lock after this duration (15s default value, might be configured via config file)
MaxInterval: time.Duration(provider.cfg.ClassicProvisioningDashboardsServerLockMaxIntervalSeconds) * time.Second,
// wait beetween 100ms and 1s before retrying to obtain the lock (default values, might be configured via config file)
MinWait: time.Duration(provider.cfg.ClassicProvisioningDashboardsServerLockMinWaitMs) * time.Millisecond,
MaxWait: time.Duration(provider.cfg.ClassicProvisioningDashboardsServerLockMaxWaitMs) * time.Millisecond,
}
// this means that if we fail to obtain the lock after ~10 seconds, we return an error
lockErr := provider.serverLock.LockExecuteAndReleaseWithRetries(ctx, "provisioning_dashboards", lockTimeConfig, func(ctx context.Context) {
provider.log.Info("starting to provision dashboards")
for _, reader := range provider.fileReaders {
if err := reader.walkDisk(ctx); err != nil {
if os.IsNotExist(err) {
// don't stop the provisioning service in case the folder is missing. The folder can appear after the startup
provider.log.Warn("Failed to provision config", "name", reader.Cfg.Name, "error", err)
return
}
errProvisioning = fmt.Errorf("failed to provision config %v: %w", reader.Cfg.Name, err)
return
}
}
provider.duplicateValidator.validate()
provider.log.Info("finished to provision dashboards")
}, retryOpt)
if lockErr != nil {
provider.log.Error("Failed to obtain dashboard provisioning lock", "error", lockErr)
return lockErr
}
return errProvisioning
}
// CleanUpOrphanedDashboards deletes provisioned dashboards missing a linked reader.
+5 -1
View File
@@ -10,6 +10,7 @@ import (
"github.com/grafana/dskit/services"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/accesscontrol"
@@ -64,6 +65,7 @@ func ProvideService(
tracer tracing.Tracer,
dual dualwrite.Service,
promTypeMigrationProvider promtypemigration.PromTypeMigrationProvider,
serverLockService *serverlock.ServerLockService,
) (*ProvisioningServiceImpl, error) {
s := &ProvisioningServiceImpl{
Cfg: cfg,
@@ -92,6 +94,7 @@ func ProvideService(
tracer: tracer,
migratePrometheusType: promTypeMigrationProvider.Run,
dual: dual,
serverLock: serverLockService,
}
s.NamedService = services.NewBasicService(s.starting, s.running, nil).WithName(ServiceName)
@@ -166,7 +169,7 @@ func (ps *ProvisioningServiceImpl) running(ctx context.Context) error {
func (ps *ProvisioningServiceImpl) setDashboardProvisioner() error {
dashboardPath := filepath.Join(ps.Cfg.ProvisioningPath, "dashboards")
dashProvisioner, err := ps.newDashboardProvisioner(context.Background(), dashboardPath, ps.dashboardProvisioningService, ps.orgService, ps.dashboardService, ps.folderService, ps.dual)
dashProvisioner, err := ps.newDashboardProvisioner(context.Background(), dashboardPath, ps.dashboardProvisioningService, ps.Cfg, ps.orgService, ps.dashboardService, ps.folderService, ps.dual, ps.serverLock)
if err != nil {
return fmt.Errorf("%v: %w", "Failed to create provisioner", err)
}
@@ -242,6 +245,7 @@ type ProvisioningServiceImpl struct {
resourcePermissions accesscontrol.ReceiverPermissionsService
tracer tracing.Tracer
dual dualwrite.Service
serverLock *serverlock.ServerLockService
migratePrometheusType func(context.Context) error
}
@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/serverlock"
dashboardstore "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/org"
@@ -20,6 +21,7 @@ import (
"github.com/grafana/grafana/pkg/services/provisioning/datasources"
"github.com/grafana/grafana/pkg/services/provisioning/utils"
"github.com/grafana/grafana/pkg/services/searchV2"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
)
@@ -160,7 +162,7 @@ func setup(t *testing.T) *serviceTestStruct {
searchStub := searchV2.NewStubSearchService()
service, err := newProvisioningServiceImpl(
func(context.Context, string, dashboardstore.DashboardProvisioningService, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service) (dashboards.DashboardProvisioner, error) {
func(context.Context, string, dashboardstore.DashboardProvisioningService, *setting.Cfg, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service, *serverlock.ServerLockService) (dashboards.DashboardProvisioner, error) {
serviceTest.dashboardProvisionerInstantiations++
return serviceTest.mock, nil
},
-1
View File
@@ -37,7 +37,6 @@ func (rs *RenderingService) GetRenderUser(ctx context.Context, key string) (*Ren
var renderUser *RenderUser
//nolint:staticcheck // not yet migrated to OpenFeature
if looksLikeJWT(key) && rs.features.IsEnabled(ctx, featuremgmt.FlagRenderAuthJWT) {
from = "jwt"
renderUser = rs.getRenderUserFromJWT(key)
-4
View File
@@ -122,7 +122,6 @@ func (s *SecretsService) registerUsageMetrics() {
// Enabled / disabled
usageMetrics["stats.encryption.envelope_encryption_enabled.count"] = 0
//nolint:staticcheck // not yet migrated to OpenFeature
if !s.features.IsEnabled(ctx, featuremgmt.FlagDisableEnvelopeEncryption) {
usageMetrics["stats.encryption.envelope_encryption_enabled.count"] = 1
}
@@ -168,7 +167,6 @@ func (s *SecretsService) Encrypt(ctx context.Context, payload []byte, opt secret
defer span.End()
// Use legacy encryption service if featuremgmt.FlagDisableEnvelopeEncryption toggle is on
//nolint:staticcheck // not yet migrated to OpenFeature
if s.features.IsEnabled(ctx, featuremgmt.FlagDisableEnvelopeEncryption) {
return s.enc.Encrypt(ctx, payload, s.cfg.SecretKey)
}
@@ -345,7 +343,6 @@ func (s *SecretsService) Decrypt(ctx context.Context, payload []byte) ([]byte, e
// If encrypted with envelope encryption, the feature is disabled and
// no provider is initialized, then we throw an error.
//nolint:staticcheck // not yet migrated to OpenFeature
if s.encryptedWithEnvelopeEncryption(payload) &&
s.features.IsEnabled(ctx, featuremgmt.FlagDisableEnvelopeEncryption) &&
!s.providersInitialized() {
@@ -483,7 +480,6 @@ func (s *SecretsService) RotateDataKeys(ctx context.Context) error {
func (s *SecretsService) ReEncryptDataKeys(ctx context.Context) error {
s.log.Info("Data keys re-encryption triggered")
//nolint:staticcheck // not yet migrated to OpenFeature
if s.features.IsEnabled(ctx, featuremgmt.FlagDisableEnvelopeEncryption) {
s.log.Info("Envelope encryption is not enabled but trying to init providers anyway...")
+13
View File
@@ -150,6 +150,11 @@ type Cfg struct {
PluginsPath string
EnterpriseLicensePath string
// Classic Provisioning settings
ClassicProvisioningDashboardsServerLockMaxIntervalSeconds int64
ClassicProvisioningDashboardsServerLockMinWaitMs int64
ClassicProvisioningDashboardsServerLockMaxWaitMs int64
// SMTP email settings
Smtp SmtpSettings
@@ -1221,6 +1226,8 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error {
return err
}
cfg.readClassicProvisioningSettings(iniFile)
// read dashboard settings
dashboards := iniFile.Section("dashboards")
cfg.DashboardVersionsToKeep = dashboards.Key("versions_to_keep").MustInt(20)
@@ -2107,6 +2114,12 @@ func (cfg *Cfg) readLiveSettings(iniFile *ini.File) error {
return nil
}
func (cfg *Cfg) readClassicProvisioningSettings(iniFile *ini.File) {
cfg.ClassicProvisioningDashboardsServerLockMinWaitMs = iniFile.Section("classic_provisioning").Key("dashboards_server_lock_min_wait_ms").MustInt64(100)
cfg.ClassicProvisioningDashboardsServerLockMaxWaitMs = iniFile.Section("classic_provisioning").Key("dashboards_server_lock_max_wait_ms").MustInt64(1000)
cfg.ClassicProvisioningDashboardsServerLockMaxIntervalSeconds = iniFile.Section("classic_provisioning").Key("dashboards_server_lock_max_interval_seconds").MustInt64(15)
}
func (cfg *Cfg) readProvisioningSettings(iniFile *ini.File) error {
provisioning := valueAsString(iniFile.Section("paths"), "provisioning", "")
cfg.ProvisioningPath = makeAbsolute(provisioning, cfg.HomePath)
-22
View File
@@ -13,8 +13,6 @@ service ResourceIndex {
// Get the resource stats
rpc GetStats(ResourceStatsRequest) returns (ResourceStatsResponse);
rpc RebuildIndexes(RebuildIndexesRequest) returns (RebuildIndexesResponse);
}
// Get statistics across multiple resources
@@ -140,23 +138,3 @@ message ResourceSearchResponse {
// Facet results
map<string,Facet> facet = 7;
}
message RebuildIndexesRequest {
// Namespace (tenant) must be the same as all keys' namespace
string namespace = 1;
// List of ResourceKeys (Namespace + Group + Resource)
repeated ResourceKey keys = 2;
}
message RebuildIndexesResponse {
// Total count of rebuilt indexes
int64 rebuildCount = 1;
// Result message
string details = 2;
// Error details
ErrorResult error = 3;
}
+1 -75
View File
@@ -1,4 +1,4 @@
// Code generated by mockery v2.53.5. DO NOT EDIT.
// Code generated by mockery v2.53.4. DO NOT EDIT.
package resource
@@ -838,80 +838,6 @@ func (_c *MockResourceClient_Read_Call) RunAndReturn(run func(context.Context, *
return _c
}
// RebuildIndexes provides a mock function with given fields: ctx, in, opts
func (_m *MockResourceClient) RebuildIndexes(ctx context.Context, in *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error) {
_va := make([]interface{}, len(opts))
for _i := range opts {
_va[_i] = opts[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, in)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
if len(ret) == 0 {
panic("no return value specified for RebuildIndexes")
}
var r0 *resourcepb.RebuildIndexesResponse
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *resourcepb.RebuildIndexesRequest, ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error)); ok {
return rf(ctx, in, opts...)
}
if rf, ok := ret.Get(0).(func(context.Context, *resourcepb.RebuildIndexesRequest, ...grpc.CallOption) *resourcepb.RebuildIndexesResponse); ok {
r0 = rf(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*resourcepb.RebuildIndexesResponse)
}
}
if rf, ok := ret.Get(1).(func(context.Context, *resourcepb.RebuildIndexesRequest, ...grpc.CallOption) error); ok {
r1 = rf(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockResourceClient_RebuildIndexes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RebuildIndexes'
type MockResourceClient_RebuildIndexes_Call struct {
*mock.Call
}
// RebuildIndexes is a helper method to define mock.On call
// - ctx context.Context
// - in *resourcepb.RebuildIndexesRequest
// - opts ...grpc.CallOption
func (_e *MockResourceClient_Expecter) RebuildIndexes(ctx interface{}, in interface{}, opts ...interface{}) *MockResourceClient_RebuildIndexes_Call {
return &MockResourceClient_RebuildIndexes_Call{Call: _e.mock.On("RebuildIndexes",
append([]interface{}{ctx, in}, opts...)...)}
}
func (_c *MockResourceClient_RebuildIndexes_Call) Run(run func(ctx context.Context, in *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption)) *MockResourceClient_RebuildIndexes_Call {
_c.Call.Run(func(args mock.Arguments) {
variadicArgs := make([]grpc.CallOption, len(args)-2)
for i, a := range args[2:] {
if a != nil {
variadicArgs[i] = a.(grpc.CallOption)
}
}
run(args[0].(context.Context), args[1].(*resourcepb.RebuildIndexesRequest), variadicArgs...)
})
return _c
}
func (_c *MockResourceClient_RebuildIndexes_Call) Return(_a0 *resourcepb.RebuildIndexesResponse, _a1 error) *MockResourceClient_RebuildIndexes_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockResourceClient_RebuildIndexes_Call) RunAndReturn(run func(context.Context, *resourcepb.RebuildIndexesRequest, ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error)) *MockResourceClient_RebuildIndexes_Call {
_c.Call.Return(run)
return _c
}
// Search provides a mock function with given fields: ctx, in, opts
func (_m *MockResourceClient) Search(ctx context.Context, in *resourcepb.ResourceSearchRequest, opts ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) {
_va := make([]interface{}, len(opts))
+9 -86
View File
@@ -234,9 +234,6 @@ func combineRebuildRequests(a, b rebuildRequest) (c rebuildRequest, ok bool) {
ret.lastImportTime = b.lastImportTime
}
// Combine complete channels
ret.completeChannels = append(a.completeChannels, b.completeChannels...)
return ret, true
}
@@ -515,52 +512,6 @@ func (s *searchSupport) GetStats(ctx context.Context, req *resourcepb.ResourceSt
return rsp, nil
}
func (s *searchSupport) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest) (*resourcepb.RebuildIndexesResponse, error) {
ctx, span := tracer.Start(ctx, "resource.searchSupport.RebuildIndexes")
defer span.End()
filterKeys := make([]NamespacedResource, len(req.Keys))
for _, key := range req.Keys {
if req.Namespace != key.Namespace {
return &resourcepb.RebuildIndexesResponse{
Error: NewBadRequestError("key namespace does not match request namespace"),
}, nil
}
filterKeys = append(filterKeys, NamespacedResource{
Namespace: key.Namespace,
Group: key.Group,
Resource: key.Resource,
})
}
importTimes, err := s.getLastImportTimes(ctx)
if err != nil {
return &resourcepb.RebuildIndexesResponse{
Error: AsErrorResult(err),
}, nil
}
completeChs := s.findIndexesToRebuild(importTimes, filterKeys, time.Now())
rebuildCount := len(completeChs)
for _, ch := range completeChs {
select {
case <-ch:
continue
case <-ctx.Done(): // request was done before all indexes rebuilt
return &resourcepb.RebuildIndexesResponse{
RebuildCount: int64(rebuildCount),
Details: fmt.Sprintf("returning before all index rebuilds completed for %d indexes", rebuildCount),
}, nil
}
}
// All rebuilds completed successfully
return &resourcepb.RebuildIndexesResponse{
RebuildCount: int64(rebuildCount),
Details: fmt.Sprintf("completed %d index rebuilds", rebuildCount),
}, nil
}
func (s *searchSupport) buildIndexes(ctx context.Context) (int, error) {
totalBatchesIndexed := 0
group := errgroup.Group{}
@@ -651,23 +602,16 @@ func (s *searchSupport) runPeriodicScanForIndexesToRebuild(ctx context.Context)
if err != nil {
s.log.Error("failed to get import times", "error", err)
}
s.findIndexesToRebuild(importTimes, nil, time.Now())
s.findIndexesToRebuild(importTimes, time.Now())
}
}
}
func (s *searchSupport) findIndexesToRebuild(lastImportTimes map[NamespacedResource]time.Time, filterKeys []NamespacedResource, now time.Time) []chan struct{} {
func (s *searchSupport) findIndexesToRebuild(lastImportTimes map[NamespacedResource]time.Time, now time.Time) {
// Check all open indexes and see if any of them need to be rebuilt.
// This is done periodically to make sure that the indexes are up to date.
var keys []NamespacedResource
if filterKeys != nil {
keys = filterKeys
} else {
keys = s.search.GetOpenIndexes()
}
var completeChs []chan struct{}
keys := s.search.GetOpenIndexes()
for _, key := range keys {
idx := s.search.GetIndex(key)
if idx == nil {
@@ -694,17 +638,18 @@ func (s *searchSupport) findIndexesToRebuild(lastImportTimes map[NamespacedResou
}
if shouldRebuildIndex(bi, s.minBuildVersion, minBuildTime, lastImportTime, nil) {
completeCh := make(chan struct{})
completeChs = append(completeChs, completeCh)
rebuildReq := newRebuildRequest(key, minBuildTime, lastImportTime, s.minBuildVersion, completeCh)
s.rebuildQueue.Add(rebuildReq)
s.rebuildQueue.Add(rebuildRequest{
NamespacedResource: key,
minBuildTime: minBuildTime,
minBuildVersion: s.minBuildVersion,
lastImportTime: lastImportTime,
})
if s.indexMetrics != nil {
s.indexMetrics.RebuildQueueLength.Set(float64(s.rebuildQueue.Len()))
}
}
}
return completeChs
}
func (s *searchSupport) getLastImportTimes(ctx context.Context) (map[NamespacedResource]time.Time, error) {
@@ -745,12 +690,6 @@ func (s *searchSupport) rebuildIndex(ctx context.Context, req rebuildRequest) {
l := s.log.New("namespace", req.Namespace, "group", req.Group, "resource", req.Resource)
defer func() {
for _, ch := range req.completeChannels {
close(ch)
}
}()
idx := s.search.GetIndex(req.NamespacedResource)
if idx == nil {
span.AddEvent("index not found")
@@ -843,22 +782,6 @@ type rebuildRequest struct {
minBuildTime time.Time // if not zero, rebuild index if it has been built before this timestamp
lastImportTime time.Time // if not zero, rebuild index if it has been built before this timestamp.
minBuildVersion *semver.Version // if not nil, rebuild index with build version older than this.
completeChannels []chan<- struct{} // signal rebuild index is complete
}
func newRebuildRequest(key NamespacedResource, minBuildTime, lastImportTime time.Time, minBuildVersion *semver.Version, completeCh chan<- struct{}) rebuildRequest {
var completeChannels []chan<- struct{} // setup a list as requests can be combined
if completeCh != nil {
completeChannels = []chan<- struct{}{completeCh}
}
return rebuildRequest{
NamespacedResource: key,
minBuildTime: minBuildTime,
minBuildVersion: minBuildVersion,
lastImportTime: lastImportTime,
completeChannels: completeChannels,
}
}
func (s *searchSupport) getOrCreateIndex(ctx context.Context, stats *SearchStats, key NamespacedResource, reason string) (ResourceIndex, error) {
@@ -211,11 +211,6 @@ func (s *searchWrapper) Search(ctx context.Context, in *resourcepb.ResourceSearc
return client.Search(ctx, in, opts...)
}
func (s *searchWrapper) RebuildIndexes(ctx context.Context, in *resourcepb.RebuildIndexesRequest,
opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error) {
return s.unifiedClient.RebuildIndexes(ctx, in, opts...)
}
// compareSearchResults compares legacy and unified search results and logs/metrics the outcome
func (s *searchWrapper) compareSearchResults(legacyResponse, unifiedResponse *resourcepb.ResourceSearchResponse, requestKey *resourcepb.ResourceKey) {
if legacyResponse == nil || unifiedResponse == nil {
@@ -112,11 +112,6 @@ func (m *MockResourceIndexClient) GetStats(ctx context.Context, in *resourcepb.R
return args.Get(0).(*resourcepb.ResourceStatsResponse), args.Error(1)
}
func (m *MockResourceIndexClient) RebuildIndexes(ctx context.Context, in *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error) {
args := m.Called(ctx, in, opts)
return args.Get(0).(*resourcepb.RebuildIndexesResponse), args.Error(1)
}
func setupTestSearchClient(t *testing.T) (schema.GroupResource, *MockResourceIndexClient, *MockResourceIndexClient, featuremgmt.FeatureToggles) {
t.Helper()
gr := schema.GroupResource{Group: "test", Resource: "items"}
@@ -2,12 +2,8 @@ package resource
import (
"context"
"errors"
"fmt"
"hash/fnv"
"math/rand"
"sync"
"sync/atomic"
"time"
"github.com/grafana/dskit/ring"
@@ -118,100 +114,6 @@ func (ds *distributorServer) GetStats(ctx context.Context, r *resourcepb.Resourc
return client.GetStats(ctx, r)
}
func (ds *distributorServer) RebuildIndexes(ctx context.Context, r *resourcepb.RebuildIndexesRequest) (*resourcepb.RebuildIndexesResponse, error) {
ctx, span := ds.tracing.Start(ctx, "distributor.RebuildIndexes")
defer span.End()
// validate input
for _, key := range r.Keys {
if r.Namespace != key.Namespace {
return &resourcepb.RebuildIndexesResponse{
Error: NewBadRequestError("key namespace does not match request namespace"),
}, nil
}
}
// distribute the request to all search pods to minimize risk of stale index
// it will not rebuild on those which don't have the index open
rs, err := ds.ring.GetAllHealthy(searchRingRead)
if err != nil {
return nil, fmt.Errorf("failed to get all healthy instances from the ring")
}
err = grpc.SetHeader(ctx, metadata.Pairs("proxied-instance-id", "all"))
if err != nil {
ds.log.Debug("error setting grpc header", "err", err)
}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
md = make(metadata.MD)
}
rCtx := userutils.InjectOrgID(metadata.NewOutgoingContext(ctx, md), r.Namespace)
var wg sync.WaitGroup
var totalRebuildCount atomic.Int64
detailsCh := make(chan string, len(rs.Instances))
errorCh := make(chan error, len(rs.Instances))
for _, inst := range rs.Instances {
wg.Add(1)
go func() {
defer wg.Done()
client, err := ds.clientPool.GetClientForInstance(inst)
if err != nil {
errorCh <- fmt.Errorf("instance %s: failed to get client, %w", inst.Id, err)
return
}
rsp, err := client.(*RingClient).Client.RebuildIndexes(rCtx, r)
if err != nil {
errorCh <- fmt.Errorf("instance %s: failed to distribute rebuild index request, %w", inst.Id, err)
return
}
if rsp.Error != nil {
errorCh <- fmt.Errorf("instance %s: rebuild index request returned the error %s", inst.Id, rsp.Error.Message)
return
}
if rsp.Details != "" {
detailsCh <- fmt.Sprintf("{instance: %s, details: %s}", inst.Id, rsp.Details)
}
totalRebuildCount.Add(rsp.RebuildCount)
}()
}
wg.Wait()
close(errorCh)
close(detailsCh)
errs := make([]error, 0, len(errorCh))
for err := range errorCh {
ds.log.Error("rebuild indexes call failed with %w", err)
errs = append(errs, err)
}
var details string
for d := range detailsCh {
if len(details) > 0 {
details += ", "
}
details += d
}
response := &resourcepb.RebuildIndexesResponse{
RebuildCount: totalRebuildCount.Load(),
Details: details,
}
if len(errs) > 0 {
response.Error = AsErrorResult(errors.Join(errs...))
}
return response, nil
}
func (ds *distributorServer) CountManagedObjects(ctx context.Context, r *resourcepb.CountManagedObjectsRequest) (*resourcepb.CountManagedObjectsResponse, error) {
ctx, span := ds.tracing.Start(ctx, "distributor.CountManagedObjects")
defer span.End()
+6 -94
View File
@@ -10,8 +10,6 @@ import (
"time"
"github.com/Masterminds/semver"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/grafana/authlib/types"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@@ -87,8 +85,7 @@ func (m *MockDocumentBuilder) BuildDocument(ctx context.Context, key *resourcepb
// mockStorageBackend implements StorageBackend for testing
type mockStorageBackend struct {
resourceStats []ResourceStats
lastImportTimes []ResourceLastImportTime
resourceStats []ResourceStats
}
func (m *mockStorageBackend) GetResourceStats(ctx context.Context, nsr NamespacedResource, minCount int) ([]ResourceStats, error) {
@@ -130,11 +127,7 @@ func (m *mockStorageBackend) ListModifiedSince(ctx context.Context, key Namespac
func (m *mockStorageBackend) GetResourceLastImportTimes(ctx context.Context) iter.Seq2[ResourceLastImportTime, error] {
return func(yield func(ResourceLastImportTime, error) bool) {
for _, ti := range m.lastImportTimes {
if !yield(ti, nil) {
return
}
}
yield(ResourceLastImportTime{}, errors.New("not implemented"))
}
}
@@ -612,14 +605,14 @@ func TestFindIndexesForRebuild(t *testing.T) {
{Namespace: "resource-v6", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}: lastImportTime,
}
support.findIndexesToRebuild(importTimes, nil, now)
support.findIndexesToRebuild(importTimes, now)
require.Equal(t, 7, support.rebuildQueue.Len())
now5m := now.Add(5 * time.Minute)
// Running findIndexesToRebuild again should not add any new indexes to the rebuild queue, and all existing
// ones should be "combined" with new ones (this will "bump" minBuildTime)
support.findIndexesToRebuild(importTimes, nil, now5m)
support.findIndexesToRebuild(importTimes, now5m)
require.Equal(t, 7, support.rebuildQueue.Len())
// Values that we expect to find in rebuild requests.
@@ -628,7 +621,7 @@ func TestFindIndexesForRebuild(t *testing.T) {
minBuildTimeDashboard := now5m.Add(-1 * time.Hour)
vals := support.rebuildQueue.Elements()
expected := []rebuildRequest{
require.ElementsMatch(t, vals, []rebuildRequest{
{NamespacedResource: NamespacedResource{Namespace: "resource-2h-v5", Group: "group", Resource: "folder"}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTime},
{NamespacedResource: NamespacedResource{Namespace: "resource-10h-v5", Group: "group", Resource: "folder"}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTime},
{NamespacedResource: NamespacedResource{Namespace: "resource-10h-v6", Group: "group", Resource: "folder"}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTime},
@@ -638,10 +631,7 @@ func TestFindIndexesForRebuild(t *testing.T) {
{NamespacedResource: NamespacedResource{Namespace: "resource-2h-v6", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTimeDashboard},
{NamespacedResource: NamespacedResource{Namespace: "resource-recently-imported", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTimeDashboard, lastImportTime: lastImportTime},
}
if diff := cmp.Diff(expected, vals, cmpopts.IgnoreFields(rebuildRequest{}, "completeChannels"), cmp.AllowUnexported(rebuildRequest{})); diff != "" {
t.Errorf("rebuildQueue mismatch (-want +got):\n%s", diff)
}
})
}
func TestRebuildIndexes(t *testing.T) {
@@ -758,81 +748,3 @@ func checkRebuildIndex(t *testing.T, support *searchSupport, req rebuildRequest,
require.Nil(t, idxAfter, "index should not exist after rebuildIndex")
}
}
func TestRebuildIndexesForResource(t *testing.T) {
key := NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}
storage := &mockStorageBackend{
resourceStats: []ResourceStats{
{NamespacedResource: key, Count: 50, ResourceVersion: 11111111},
},
lastImportTimes: []ResourceLastImportTime{{
NamespacedResource: key,
LastImportTime: time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC),
}},
}
search := &mockSearchBackend{}
supplier := &TestDocumentBuilderSupplier{
GroupsResources: map[string]string{
"group": "resource",
},
}
opts := SearchOptions{
Backend: search,
Resources: supplier,
InitMinCount: 1,
}
support, err := newSearchSupport(opts, storage, nil, nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, support)
err = support.init(t.Context())
require.NoError(t, err)
require.Equal(t, 0, support.rebuildQueue.Len())
// invalid request
rebuildReq := &resourcepb.RebuildIndexesRequest{
Namespace: "some-other-namespace",
Keys: []*resourcepb.ResourceKey{{
Namespace: key.Namespace,
Group: key.Group,
Resource: key.Resource,
}}}
rsp, err := support.RebuildIndexes(t.Context(), rebuildReq)
require.NoError(t, err)
require.Equal(t, "key namespace does not match request namespace", rsp.Error.Message)
rebuildReq.Namespace = key.Namespace
// cached index info
search.cache[key] = &MockResourceIndex{
buildInfo: IndexBuildInfo{BuildVersion: semver.MustParse("5.0.0"), BuildTime: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)},
}
// old import time will not be rebuilt
storage.lastImportTimes = []ResourceLastImportTime{{
NamespacedResource: key,
LastImportTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
}}
rsp, err = support.RebuildIndexes(t.Context(), rebuildReq)
require.NoError(t, err)
require.Equal(t, int64(0), rsp.RebuildCount)
require.Equal(t, 0, support.rebuildQueue.Len())
// recent import time gets added to rebuild queue and processed
storage.lastImportTimes = []ResourceLastImportTime{{
NamespacedResource: key,
LastImportTime: time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC),
}}
rsp, err = support.RebuildIndexes(t.Context(), rebuildReq)
require.NoError(t, err)
require.Equal(t, int64(1), rsp.RebuildCount)
// rebuild waited for rebuild queue to process
require.Equal(t, 0, support.rebuildQueue.Len())
}
-8
View File
@@ -1541,11 +1541,3 @@ func (s *server) runInQueue(ctx context.Context, tenantID string, runnable func(
return queueCtx.Err() // Timed out or canceled while waiting for execution.
}
}
func (s *server) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest) (*resourcepb.RebuildIndexesResponse, error) {
if s.search == nil {
return nil, fmt.Errorf("search index not configured")
}
return s.search.RebuildIndexes(ctx, req)
}
+61 -204
View File
@@ -386,123 +386,6 @@ func (x *ResourceSearchResponse) GetFacet() map[string]*ResourceSearchResponse_F
return nil
}
type RebuildIndexesRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Namespace (tenant)
Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"`
// List of ResourceKeys (Namespace + Group + Resource)
Keys []*ResourceKey `protobuf:"bytes,2,rep,name=keys,proto3" json:"keys,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RebuildIndexesRequest) Reset() {
*x = RebuildIndexesRequest{}
mi := &file_search_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RebuildIndexesRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RebuildIndexesRequest) ProtoMessage() {}
func (x *RebuildIndexesRequest) ProtoReflect() protoreflect.Message {
mi := &file_search_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RebuildIndexesRequest.ProtoReflect.Descriptor instead.
func (*RebuildIndexesRequest) Descriptor() ([]byte, []int) {
return file_search_proto_rawDescGZIP(), []int{4}
}
func (x *RebuildIndexesRequest) GetNamespace() string {
if x != nil {
return x.Namespace
}
return ""
}
func (x *RebuildIndexesRequest) GetKeys() []*ResourceKey {
if x != nil {
return x.Keys
}
return nil
}
type RebuildIndexesResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Total count of rebuilt indexes
RebuildCount int64 `protobuf:"varint,1,opt,name=rebuildCount,proto3" json:"rebuildCount,omitempty"`
// Result message
Details string `protobuf:"bytes,2,opt,name=details,proto3" json:"details,omitempty"`
// Error details
Error *ErrorResult `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RebuildIndexesResponse) Reset() {
*x = RebuildIndexesResponse{}
mi := &file_search_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RebuildIndexesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RebuildIndexesResponse) ProtoMessage() {}
func (x *RebuildIndexesResponse) ProtoReflect() protoreflect.Message {
mi := &file_search_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RebuildIndexesResponse.ProtoReflect.Descriptor instead.
func (*RebuildIndexesResponse) Descriptor() ([]byte, []int) {
return file_search_proto_rawDescGZIP(), []int{5}
}
func (x *RebuildIndexesResponse) GetRebuildCount() int64 {
if x != nil {
return x.RebuildCount
}
return 0
}
func (x *RebuildIndexesResponse) GetDetails() string {
if x != nil {
return x.Details
}
return ""
}
func (x *RebuildIndexesResponse) GetError() *ErrorResult {
if x != nil {
return x.Error
}
return nil
}
type ResourceStatsResponse_Stats struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Resource group
@@ -517,7 +400,7 @@ type ResourceStatsResponse_Stats struct {
func (x *ResourceStatsResponse_Stats) Reset() {
*x = ResourceStatsResponse_Stats{}
mi := &file_search_proto_msgTypes[6]
mi := &file_search_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -529,7 +412,7 @@ func (x *ResourceStatsResponse_Stats) String() string {
func (*ResourceStatsResponse_Stats) ProtoMessage() {}
func (x *ResourceStatsResponse_Stats) ProtoReflect() protoreflect.Message {
mi := &file_search_proto_msgTypes[6]
mi := &file_search_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -576,7 +459,7 @@ type ResourceSearchRequest_Sort struct {
func (x *ResourceSearchRequest_Sort) Reset() {
*x = ResourceSearchRequest_Sort{}
mi := &file_search_proto_msgTypes[7]
mi := &file_search_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -588,7 +471,7 @@ func (x *ResourceSearchRequest_Sort) String() string {
func (*ResourceSearchRequest_Sort) ProtoMessage() {}
func (x *ResourceSearchRequest_Sort) ProtoReflect() protoreflect.Message {
mi := &file_search_proto_msgTypes[7]
mi := &file_search_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -628,7 +511,7 @@ type ResourceSearchRequest_Facet struct {
func (x *ResourceSearchRequest_Facet) Reset() {
*x = ResourceSearchRequest_Facet{}
mi := &file_search_proto_msgTypes[8]
mi := &file_search_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -640,7 +523,7 @@ func (x *ResourceSearchRequest_Facet) String() string {
func (*ResourceSearchRequest_Facet) ProtoMessage() {}
func (x *ResourceSearchRequest_Facet) ProtoReflect() protoreflect.Message {
mi := &file_search_proto_msgTypes[8]
mi := &file_search_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -685,7 +568,7 @@ type ResourceSearchResponse_Facet struct {
func (x *ResourceSearchResponse_Facet) Reset() {
*x = ResourceSearchResponse_Facet{}
mi := &file_search_proto_msgTypes[10]
mi := &file_search_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -697,7 +580,7 @@ func (x *ResourceSearchResponse_Facet) String() string {
func (*ResourceSearchResponse_Facet) ProtoMessage() {}
func (x *ResourceSearchResponse_Facet) ProtoReflect() protoreflect.Message {
mi := &file_search_proto_msgTypes[10]
mi := &file_search_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -751,7 +634,7 @@ type ResourceSearchResponse_TermFacet struct {
func (x *ResourceSearchResponse_TermFacet) Reset() {
*x = ResourceSearchResponse_TermFacet{}
mi := &file_search_proto_msgTypes[11]
mi := &file_search_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -763,7 +646,7 @@ func (x *ResourceSearchResponse_TermFacet) String() string {
func (*ResourceSearchResponse_TermFacet) ProtoMessage() {}
func (x *ResourceSearchResponse_TermFacet) ProtoReflect() protoreflect.Message {
mi := &file_search_proto_msgTypes[11]
mi := &file_search_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -897,42 +780,22 @@ var file_search_proto_rawDesc = string([]byte{
0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x72,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46,
0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22,
0x60, 0x0a, 0x15, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65,
0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65,
0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d,
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x29, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x02,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x04, 0x6b, 0x65, 0x79,
0x73, 0x22, 0x83, 0x01, 0x0a, 0x16, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64,
0x65, 0x78, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c,
0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01,
0x28, 0x03, 0x52, 0x0c, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74,
0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72,
0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74,
0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x32, 0xfe, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x4b, 0x0a, 0x06, 0x53, 0x65, 0x61,
0x72, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61,
0x74, 0x73, 0x12, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x0e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e,
0x64, 0x65, 0x78, 0x65, 0x73, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x2e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x2e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67,
0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61,
0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32,
0xa9, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65,
0x78, 0x12, 0x4b, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53,
0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b,
0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x1e, 0x2e, 0x72, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74,
0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x72, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74,
0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3b, 0x5a, 0x39, 0x67,
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e,
0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74,
0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
})
var (
@@ -947,53 +810,47 @@ func file_search_proto_rawDescGZIP() []byte {
return file_search_proto_rawDescData
}
var file_search_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
var file_search_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
var file_search_proto_goTypes = []any{
(*ResourceStatsRequest)(nil), // 0: resource.ResourceStatsRequest
(*ResourceStatsResponse)(nil), // 1: resource.ResourceStatsResponse
(*ResourceSearchRequest)(nil), // 2: resource.ResourceSearchRequest
(*ResourceSearchResponse)(nil), // 3: resource.ResourceSearchResponse
(*RebuildIndexesRequest)(nil), // 4: resource.RebuildIndexesRequest
(*RebuildIndexesResponse)(nil), // 5: resource.RebuildIndexesResponse
(*ResourceStatsResponse_Stats)(nil), // 6: resource.ResourceStatsResponse.Stats
(*ResourceSearchRequest_Sort)(nil), // 7: resource.ResourceSearchRequest.Sort
(*ResourceSearchRequest_Facet)(nil), // 8: resource.ResourceSearchRequest.Facet
nil, // 9: resource.ResourceSearchRequest.FacetEntry
(*ResourceSearchResponse_Facet)(nil), // 10: resource.ResourceSearchResponse.Facet
(*ResourceSearchResponse_TermFacet)(nil), // 11: resource.ResourceSearchResponse.TermFacet
nil, // 12: resource.ResourceSearchResponse.FacetEntry
(*ErrorResult)(nil), // 13: resource.ErrorResult
(*ListOptions)(nil), // 14: resource.ListOptions
(*ResourceKey)(nil), // 15: resource.ResourceKey
(*ResourceTable)(nil), // 16: resource.ResourceTable
(*ResourceStatsResponse_Stats)(nil), // 4: resource.ResourceStatsResponse.Stats
(*ResourceSearchRequest_Sort)(nil), // 5: resource.ResourceSearchRequest.Sort
(*ResourceSearchRequest_Facet)(nil), // 6: resource.ResourceSearchRequest.Facet
nil, // 7: resource.ResourceSearchRequest.FacetEntry
(*ResourceSearchResponse_Facet)(nil), // 8: resource.ResourceSearchResponse.Facet
(*ResourceSearchResponse_TermFacet)(nil), // 9: resource.ResourceSearchResponse.TermFacet
nil, // 10: resource.ResourceSearchResponse.FacetEntry
(*ErrorResult)(nil), // 11: resource.ErrorResult
(*ListOptions)(nil), // 12: resource.ListOptions
(*ResourceKey)(nil), // 13: resource.ResourceKey
(*ResourceTable)(nil), // 14: resource.ResourceTable
}
var file_search_proto_depIdxs = []int32{
13, // 0: resource.ResourceStatsResponse.error:type_name -> resource.ErrorResult
6, // 1: resource.ResourceStatsResponse.stats:type_name -> resource.ResourceStatsResponse.Stats
14, // 2: resource.ResourceSearchRequest.options:type_name -> resource.ListOptions
15, // 3: resource.ResourceSearchRequest.federated:type_name -> resource.ResourceKey
7, // 4: resource.ResourceSearchRequest.sortBy:type_name -> resource.ResourceSearchRequest.Sort
9, // 5: resource.ResourceSearchRequest.facet:type_name -> resource.ResourceSearchRequest.FacetEntry
13, // 6: resource.ResourceSearchResponse.error:type_name -> resource.ErrorResult
15, // 7: resource.ResourceSearchResponse.key:type_name -> resource.ResourceKey
16, // 8: resource.ResourceSearchResponse.results:type_name -> resource.ResourceTable
12, // 9: resource.ResourceSearchResponse.facet:type_name -> resource.ResourceSearchResponse.FacetEntry
15, // 10: resource.RebuildIndexesRequest.keys:type_name -> resource.ResourceKey
13, // 11: resource.RebuildIndexesResponse.error:type_name -> resource.ErrorResult
8, // 12: resource.ResourceSearchRequest.FacetEntry.value:type_name -> resource.ResourceSearchRequest.Facet
11, // 13: resource.ResourceSearchResponse.Facet.terms:type_name -> resource.ResourceSearchResponse.TermFacet
10, // 14: resource.ResourceSearchResponse.FacetEntry.value:type_name -> resource.ResourceSearchResponse.Facet
2, // 15: resource.ResourceIndex.Search:input_type -> resource.ResourceSearchRequest
0, // 16: resource.ResourceIndex.GetStats:input_type -> resource.ResourceStatsRequest
4, // 17: resource.ResourceIndex.RebuildIndexes:input_type -> resource.RebuildIndexesRequest
3, // 18: resource.ResourceIndex.Search:output_type -> resource.ResourceSearchResponse
1, // 19: resource.ResourceIndex.GetStats:output_type -> resource.ResourceStatsResponse
5, // 20: resource.ResourceIndex.RebuildIndexes:output_type -> resource.RebuildIndexesResponse
18, // [18:21] is the sub-list for method output_type
15, // [15:18] is the sub-list for method input_type
15, // [15:15] is the sub-list for extension type_name
15, // [15:15] is the sub-list for extension extendee
0, // [0:15] is the sub-list for field type_name
11, // 0: resource.ResourceStatsResponse.error:type_name -> resource.ErrorResult
4, // 1: resource.ResourceStatsResponse.stats:type_name -> resource.ResourceStatsResponse.Stats
12, // 2: resource.ResourceSearchRequest.options:type_name -> resource.ListOptions
13, // 3: resource.ResourceSearchRequest.federated:type_name -> resource.ResourceKey
5, // 4: resource.ResourceSearchRequest.sortBy:type_name -> resource.ResourceSearchRequest.Sort
7, // 5: resource.ResourceSearchRequest.facet:type_name -> resource.ResourceSearchRequest.FacetEntry
11, // 6: resource.ResourceSearchResponse.error:type_name -> resource.ErrorResult
13, // 7: resource.ResourceSearchResponse.key:type_name -> resource.ResourceKey
14, // 8: resource.ResourceSearchResponse.results:type_name -> resource.ResourceTable
10, // 9: resource.ResourceSearchResponse.facet:type_name -> resource.ResourceSearchResponse.FacetEntry
6, // 10: resource.ResourceSearchRequest.FacetEntry.value:type_name -> resource.ResourceSearchRequest.Facet
9, // 11: resource.ResourceSearchResponse.Facet.terms:type_name -> resource.ResourceSearchResponse.TermFacet
8, // 12: resource.ResourceSearchResponse.FacetEntry.value:type_name -> resource.ResourceSearchResponse.Facet
2, // 13: resource.ResourceIndex.Search:input_type -> resource.ResourceSearchRequest
0, // 14: resource.ResourceIndex.GetStats:input_type -> resource.ResourceStatsRequest
3, // 15: resource.ResourceIndex.Search:output_type -> resource.ResourceSearchResponse
1, // 16: resource.ResourceIndex.GetStats:output_type -> resource.ResourceStatsResponse
15, // [15:17] is the sub-list for method output_type
13, // [13:15] is the sub-list for method input_type
13, // [13:13] is the sub-list for extension type_name
13, // [13:13] is the sub-list for extension extendee
0, // [0:13] is the sub-list for field type_name
}
func init() { file_search_proto_init() }
@@ -1008,7 +865,7 @@ func file_search_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_search_proto_rawDesc), len(file_search_proto_rawDesc)),
NumEnums: 0,
NumMessages: 13,
NumMessages: 11,
NumExtensions: 0,
NumServices: 1,
},
@@ -19,9 +19,8 @@ import (
const _ = grpc.SupportPackageIsVersion8
const (
ResourceIndex_Search_FullMethodName = "/resource.ResourceIndex/Search"
ResourceIndex_GetStats_FullMethodName = "/resource.ResourceIndex/GetStats"
ResourceIndex_RebuildIndexes_FullMethodName = "/resource.ResourceIndex/RebuildIndexes"
ResourceIndex_Search_FullMethodName = "/resource.ResourceIndex/Search"
ResourceIndex_GetStats_FullMethodName = "/resource.ResourceIndex/GetStats"
)
// ResourceIndexClient is the client API for ResourceIndex service.
@@ -34,7 +33,6 @@ type ResourceIndexClient interface {
Search(ctx context.Context, in *ResourceSearchRequest, opts ...grpc.CallOption) (*ResourceSearchResponse, error)
// Get the resource stats
GetStats(ctx context.Context, in *ResourceStatsRequest, opts ...grpc.CallOption) (*ResourceStatsResponse, error)
RebuildIndexes(ctx context.Context, in *RebuildIndexesRequest, opts ...grpc.CallOption) (*RebuildIndexesResponse, error)
}
type resourceIndexClient struct {
@@ -65,16 +63,6 @@ func (c *resourceIndexClient) GetStats(ctx context.Context, in *ResourceStatsReq
return out, nil
}
func (c *resourceIndexClient) RebuildIndexes(ctx context.Context, in *RebuildIndexesRequest, opts ...grpc.CallOption) (*RebuildIndexesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RebuildIndexesResponse)
err := c.cc.Invoke(ctx, ResourceIndex_RebuildIndexes_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// ResourceIndexServer is the server API for ResourceIndex service.
// All implementations should embed UnimplementedResourceIndexServer
// for forward compatibility
@@ -85,7 +73,6 @@ type ResourceIndexServer interface {
Search(context.Context, *ResourceSearchRequest) (*ResourceSearchResponse, error)
// Get the resource stats
GetStats(context.Context, *ResourceStatsRequest) (*ResourceStatsResponse, error)
RebuildIndexes(context.Context, *RebuildIndexesRequest) (*RebuildIndexesResponse, error)
}
// UnimplementedResourceIndexServer should be embedded to have forward compatible implementations.
@@ -98,9 +85,6 @@ func (UnimplementedResourceIndexServer) Search(context.Context, *ResourceSearchR
func (UnimplementedResourceIndexServer) GetStats(context.Context, *ResourceStatsRequest) (*ResourceStatsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetStats not implemented")
}
func (UnimplementedResourceIndexServer) RebuildIndexes(context.Context, *RebuildIndexesRequest) (*RebuildIndexesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RebuildIndexes not implemented")
}
// UnsafeResourceIndexServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ResourceIndexServer will
@@ -149,24 +133,6 @@ func _ResourceIndex_GetStats_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler)
}
func _ResourceIndex_RebuildIndexes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RebuildIndexesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ResourceIndexServer).RebuildIndexes(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ResourceIndex_RebuildIndexes_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ResourceIndexServer).RebuildIndexes(ctx, req.(*RebuildIndexesRequest))
}
return interceptor(ctx, in, info, handler)
}
// ResourceIndex_ServiceDesc is the grpc.ServiceDesc for ResourceIndex service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -182,10 +148,6 @@ var ResourceIndex_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetStats",
Handler: _ResourceIndex_GetStats_Handler,
},
{
MethodName: "RebuildIndexes",
Handler: _ResourceIndex_RebuildIndexes_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "search.proto",
+1 -7
View File
@@ -994,9 +994,7 @@ func (b *backend) GetResourceLastImportTimes(ctx context.Context) iter.Seq2[reso
b.lastImportTimeDeletionTime.Store(now)
}
rows, err := dbutil.QueryRows(ctx, b.db, sqlResourceLastImportTimeQuery, &sqlResourceLastImportTimeQueryRequest{
SQLTemplate: sqltemplate.New(b.dialect),
})
rows, err := dbutil.QueryRows(ctx, b.db, sqlResourceLastImportTimeQuery, &sqlResourceLastImportTimeQueryRequest{SQLTemplate: sqltemplate.New(b.dialect)})
if err != nil {
return func(yield func(resource.ResourceLastImportTime, error) bool) {
yield(resource.ResourceLastImportTime{}, err)
@@ -1039,7 +1037,3 @@ func (b *backend) GetResourceLastImportTimes(ctx context.Context) iter.Seq2[reso
}
}
}
func (b *backend) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest) (*resourcepb.RebuildIndexesResponse, error) {
return nil, fmt.Errorf("rebuild indexes not supported by unistore sql backend")
}
@@ -13,7 +13,6 @@ import {
SceneObjectState,
SceneObjectStateChangedEvent,
SceneQueryRunner,
sceneGraph,
sceneUtils,
VizPanel,
isSceneObject,
@@ -22,7 +21,6 @@ import { Panel } from '@grafana/schema';
import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions';
import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard';
import { saveLibPanel } from 'app/features/library-panels/state/api';
import { getAllSuggestions } from 'app/features/panel/suggestions/getAllSuggestions';
import { DashboardEditActionEvent } from '../edit-pane/shared';
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
@@ -31,7 +29,6 @@ import { UNCONFIGURED_PANEL_PLUGIN_ID } from '../scene/UnconfiguredPanel';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { DashboardLayoutItem, isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem';
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
import { PanelModelCompatibilityWrapper } from '../utils/PanelModelCompatibilityWrapper';
import {
activateSceneObjectAndParentTree,
getDashboardSceneFor,
@@ -87,11 +84,7 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
const panel = this.state.panelRef.resolve();
if (panel.state.pluginId === UNCONFIGURED_PANEL_PLUGIN_ID) {
if (config.featureToggles.newVizSuggestions) {
this._autoSelectVisualization(panel);
} else {
panel.changePluginType('timeseries');
}
panel.changePluginType('timeseries');
}
this._subs.add(
@@ -124,31 +117,6 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
};
}
private async _autoSelectVisualization(panel: VizPanel) {
const dataObject = sceneGraph.getData(panel);
this._subs.add(
dataObject.subscribeToState(async () => {
const { data } = dataObject.state;
const hasData = data && data.series && data.series.length > 0 && data.series.some((frame) => frame.length > 0);
if (hasData && panel.state.pluginId === UNCONFIGURED_PANEL_PLUGIN_ID) {
const panelModel = new PanelModelCompatibilityWrapper(panel);
const suggestions = await getAllSuggestions(data, panelModel);
if (suggestions.length > 0) {
const defaultFirstSuggestion = suggestions[0];
await panel.changePluginType(
defaultFirstSuggestion.pluginId,
defaultFirstSuggestion.options,
defaultFirstSuggestion.fieldConfig
);
}
}
})
);
}
private commitChanges() {
if (!this.state.isDirty && !this._changesHaveBeenMade) {
// Nothing to commit
@@ -260,17 +228,12 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
})
);
const isUnconfigured = Boolean(
config.featureToggles.newVizSuggestions && panel.state.pluginId === UNCONFIGURED_PANEL_PLUGIN_ID
);
// Setup options pane
this.setState({
optionsPane: new PanelOptionsPane({
panelRef: this.state.panelRef,
searchQuery: '',
listMode: OptionFilter.All,
isVizPickerOpen: isUnconfigured,
}),
isInitializing: false,
});
@@ -1,13 +1,11 @@
import { css } from '@emotion/css';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useState } from 'react';
import { CoreApp, GrafanaTheme2, PanelPlugin, PanelProps } from '@grafana/data';
import { PanelPlugin, PanelProps } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { config, locationService } from '@grafana/runtime';
import { locationService } from '@grafana/runtime';
import { sceneUtils } from '@grafana/scenes';
import { Box, Button, ButtonGroup, Dropdown, Icon, Menu, Stack, Text, usePanelContext, useStyles2 } from '@grafana/ui';
import { Box, Button, ButtonGroup, Dropdown, Menu, Stack } from '@grafana/ui';
import { NEW_PANEL_TITLE } from '../../dashboard/utils/dashboard';
import { DashboardInteractions } from '../utils/interactions';
import { findVizPanelByKey, getVizPanelKeyForPanelId } from '../utils/utils';
@@ -18,8 +16,6 @@ const UnconfiguredPanel = new PanelPlugin(UnconfiguredPanelComp);
function UnconfiguredPanelComp(props: PanelProps) {
const [isOpen, setIsOpen] = useState(false);
const panelContext = usePanelContext();
const styles = useStyles2(getStyles);
const onMenuClick = useCallback((isOpen: boolean) => {
setIsOpen(isOpen);
@@ -30,15 +26,14 @@ function UnconfiguredPanelComp(props: PanelProps) {
DashboardInteractions.panelActionClicked('configure', props.id, 'panel');
};
const dashboard = window.__grafanaSceneContext;
const panel =
dashboard instanceof DashboardScene ? findVizPanelByKey(dashboard, getVizPanelKeyForPanelId(props.id)) : null;
const onUseLibraryPanel = () => {
if (!dashboard || !(dashboard instanceof DashboardScene)) {
const dashboard = window.__grafanaSceneContext;
if (!(dashboard instanceof DashboardScene)) {
throw new Error('DashboardScene not found');
}
const panel = findVizPanelByKey(dashboard, getVizPanelKeyForPanelId(props.id));
if (!panel) {
throw new Error('Panel not found');
}
@@ -46,18 +41,6 @@ function UnconfiguredPanelComp(props: PanelProps) {
dashboard.onShowAddLibraryPanelDrawer(panel.getRef());
};
useEffect(() => {
if (!panel || !config.featureToggles.newVizSuggestions) {
return;
}
if (panelContext.app === CoreApp.PanelEditor) {
panel.setState({ title: '' });
} else if (!panel.state.title) {
panel.setState({ title: NEW_PANEL_TITLE });
}
}, [panel, panelContext.app]);
const MenuActions = () => (
<Menu>
<Menu.Item
@@ -73,25 +56,6 @@ function UnconfiguredPanelComp(props: PanelProps) {
</Menu>
);
const showEmptyState = config.featureToggles.newVizSuggestions && panelContext.app === CoreApp.PanelEditor;
if (showEmptyState) {
const defaultContent = (
<Trans i18nKey="dashboard.new-panel.empty-state-message">
Run a query to visualize it here or go to all visualizations to add other panel types
</Trans>
);
return (
<div className={styles.emptyStateWrapper}>
<Icon name="chart-line" size="xxxl" className={styles.emptyStateIcon} />
<Text element="p" textAlignment="center" color="secondary">
{defaultContent}
</Text>
</div>
);
}
return (
<Stack direction={'row'} alignItems={'center'} height={'100%'} justifyContent={'center'}>
<Box paddingBottom={2}>
@@ -115,20 +79,3 @@ sceneUtils.registerRuntimePanelPlugin({
pluginId: UNCONFIGURED_PANEL_PLUGIN_ID,
plugin: UnconfiguredPanel,
});
function getStyles(theme: GrafanaTheme2) {
return {
emptyStateWrapper: css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
textAlign: 'center',
}),
emptyStateIcon: css({
color: theme.colors.text.secondary,
marginBottom: theme.spacing(2),
}),
};
}
@@ -19,6 +19,7 @@ interface Props {
dashboardJson: AsyncState<{
json: Dashboard | DashboardJson | DashboardV2Spec | ExportableResource | { error: unknown };
hasLibraryPanels?: boolean;
initialSaveModelVersion: 'v1' | 'v2';
}>;
isSharingExternally: boolean;
exportMode: ExportMode;
@@ -40,27 +41,49 @@ export function ResourceExport({
onViewYAML,
}: Props) {
const hasLibraryPanels = dashboardJson.value?.hasLibraryPanels;
const initialSaveModelVersion = dashboardJson.value?.initialSaveModelVersion;
const isV2Dashboard =
dashboardJson.value?.json && 'spec' in dashboardJson.value.json && 'elements' in dashboardJson.value.json.spec;
const showV2LibPanelAlert = isV2Dashboard && isSharingExternally && hasLibraryPanels;
const switchExportLabel = isV2Dashboard
? t('export.json.export-remove-ds-refs', 'Remove deployment details')
: t('share-modal.export.share-externally-label', `Export for sharing externally`);
const switchExportLabel =
exportMode === ExportMode.V2Resource
? t('export.json.export-remove-ds-refs', 'Remove deployment details')
: t('share-modal.export.share-externally-label', `Export for sharing externally`);
const switchExportModeLabel = t('export.json.export-mode', 'Model');
const switchExportFormatLabel = t('export.json.export-format', 'Format');
return (
<Stack gap={2} direction="column">
<Stack gap={1} direction="column">
{!isV2Dashboard && (
{initialSaveModelVersion === 'v1' && (
<Stack alignItems="center">
<Label>{switchExportModeLabel}</Label>
<RadioButtonGroup
options={[
{ label: t('dashboard-scene.resource-export.label.classic', 'Classic'), value: ExportMode.Classic },
{
label: t('dashboard-scene.resource-export.label.v1-resource', 'V1 Resource'),
value: ExportMode.V1Resource,
},
{
label: t('dashboard-scene.resource-export.label.v2-resource', 'V2 Resource'),
value: ExportMode.V2Resource,
},
]}
value={exportMode}
onChange={(value) => onExportModeChange(value)}
/>
</Stack>
)}
{initialSaveModelVersion === 'v2' && (
<Stack alignItems="center">
<Label>{switchExportModeLabel}</Label>
<RadioButtonGroup
options={[
{
label: t('dashboard-scene.resource-export.label.classic', 'Classic'),
value: ExportMode.Classic,
label: t('dashboard-scene.resource-export.label.v2-resource', 'V2 Resource'),
value: ExportMode.V2Resource,
},
{
label: t('dashboard-scene.resource-export.label.v1-resource', 'V1 Resource'),
@@ -85,7 +108,9 @@ export function ResourceExport({
/>
</Stack>
)}
{exportMode !== ExportMode.V1Resource && (
{(isV2Dashboard ||
exportMode === ExportMode.Classic ||
(initialSaveModelVersion === 'v2' && exportMode === ExportMode.V1Resource)) && (
<Stack gap={1} alignItems="start">
<Label>{switchExportLabel}</Label>
<Switch
@@ -113,6 +113,9 @@ describe('ShareExportTab', () => {
// Should call transformSceneToV1 (not transform V2→V1)
expect(transformSceneToV1Spy).toHaveBeenCalled();
expect(transformV2ToV1Spy).not.toHaveBeenCalled();
// Should report correct initial version
expect(result.initialSaveModelVersion).toBe('v1');
});
// If V2 dashboard → V1 Resource should auto-transform with V1 apiVersion
@@ -133,6 +136,9 @@ describe('ShareExportTab', () => {
// Should auto-transform V2→V1
expect(transformSceneToV2Spy).toHaveBeenCalled(); // Get V2 spec first
expect(transformV2ToV1Spy).toHaveBeenCalled(); // Then transform to V1
// Should report correct initial version
expect(result.initialSaveModelVersion).toBe('v2');
});
// If V2 dashboard → V1 Resource with external sharing should transform and apply external sharing
@@ -158,6 +164,9 @@ describe('ShareExportTab', () => {
// Should call makeExportableV1 for external sharing
expect(makeExportableV1Spy).toHaveBeenCalled();
// Should report correct initial version
expect(result.initialSaveModelVersion).toBe('v2');
});
});
@@ -178,6 +187,9 @@ describe('ShareExportTab', () => {
// Should not call V2→V1 transformation since source is already V2
expect(transformV2ToV1Spy).not.toHaveBeenCalled();
// Should report correct initial version
expect(result.initialSaveModelVersion).toBe('v2');
});
// If V1 dashboard → V2 Resource should detect library panels correctly
@@ -189,6 +201,7 @@ describe('ShareExportTab', () => {
// Should detect library panels from V1 dashboard
expect(result.hasLibraryPanels).toBe(true);
expect(result.initialSaveModelVersion).toBe('v1');
});
// If V1 dashboard with dashboardNewLayouts disabled → V2 Resource should detect library panels correctly
@@ -200,6 +213,7 @@ describe('ShareExportTab', () => {
// Should detect library panels from V1 dashboard (first branch of the logic)
expect(result.hasLibraryPanels).toBe(true);
expect(result.initialSaveModelVersion).toBe('v1');
});
// If V1 dashboard without library panels → V2 Resource should return false
@@ -211,6 +225,7 @@ describe('ShareExportTab', () => {
// Should not detect library panels
expect(result.hasLibraryPanels).toBe(false);
expect(result.initialSaveModelVersion).toBe('v1');
});
});
@@ -232,6 +247,7 @@ describe('ShareExportTab', () => {
// Should detect library panels from V2 dashboard elements (second branch of the logic)
expect(result.hasLibraryPanels).toBe(true);
expect(result.initialSaveModelVersion).toBe('v2');
});
// Test the second branch: V2 dashboard with V1 initial save model
@@ -243,6 +259,7 @@ describe('ShareExportTab', () => {
// Should detect library panels from V2 dashboard elements (second branch of the logic)
expect(result.hasLibraryPanels).toBe(true);
expect(result.initialSaveModelVersion).toBe('v1');
});
// If V2 dashboard without library panels → V2 Resource should return false
@@ -254,6 +271,7 @@ describe('ShareExportTab', () => {
// Should not detect library panels
expect(result.hasLibraryPanels).toBe(false);
expect(result.initialSaveModelVersion).toBe('v2');
});
});
@@ -276,6 +294,9 @@ describe('ShareExportTab', () => {
expect(result.json).not.toHaveProperty('apiVersion');
expect(result.json).not.toHaveProperty('kind');
expect(result.json).not.toHaveProperty('status');
// Should report correct initial version
expect(result.initialSaveModelVersion).toBe('v1');
});
});
@@ -102,16 +102,28 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
public getExportableDashboardJson = async (): Promise<{
json: Dashboard | DashboardJson | DashboardV2Spec | ExportableResource | { error: unknown };
hasLibraryPanels?: boolean;
initialSaveModelVersion: 'v1' | 'v2';
}> => {
const { isSharingExternally, exportMode } = this.state;
const scene = getDashboardSceneFor(this);
const exportableDashboard = await scene.serializer.makeExportableExternally(scene);
const initialSaveModel = scene.getInitialSaveModel();
const initialSaveModelVersion = initialSaveModel && isDashboardV2Spec(initialSaveModel) ? 'v2' : 'v1';
const origDashboard = scene.serializer.getSaveModel(scene);
const exportable = isSharingExternally ? exportableDashboard : origDashboard;
const metadata = getMetadata(scene, Boolean(isSharingExternally));
if (isDashboardV2Spec(origDashboard) && 'elements' in exportable && exportMode !== ExportMode.V1Resource) {
if (
isDashboardV2Spec(origDashboard) &&
'elements' in exportable &&
initialSaveModelVersion === 'v2' &&
exportMode !== ExportMode.V1Resource
) {
this.setState({
exportMode: ExportMode.V2Resource,
});
// For automatic V2 path, also process library panels when sharing externally
let finalSpec = exportable;
if (isSharingExternally && isDashboardV2Spec(exportable)) {
@@ -120,6 +132,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
if ('error' in result) {
return {
json: { error: result.error },
initialSaveModelVersion,
hasLibraryPanels: Object.values(origDashboard.elements).some((element) => element.kind === 'LibraryPanel'),
};
}
@@ -134,13 +147,14 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
spec: finalSpec,
status: {},
},
initialSaveModelVersion,
hasLibraryPanels: Object.values(origDashboard.elements).some((element) => element.kind === 'LibraryPanel'),
};
}
if (exportMode === ExportMode.V1Resource) {
// Check if source is V2 and auto-transform to V1
if (isDashboardV2Spec(origDashboard)) {
if (isDashboardV2Spec(origDashboard) && initialSaveModelVersion === 'v2') {
try {
const spec = transformSceneToSaveModelSchemaV2(scene);
const metadata = getMetadata(scene, Boolean(isSharingExternally));
@@ -171,6 +185,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
spec: exportableV1,
status: {},
},
initialSaveModelVersion,
hasLibraryPanels: hasLibraryPanelsInV1Dashboard(spec1),
};
} catch (err) {
@@ -178,6 +193,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
json: {
error: `Failed to convert dashboard to v1. ${err}`,
},
initialSaveModelVersion,
hasLibraryPanels: undefined,
};
}
@@ -193,6 +209,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
spec,
status: {},
},
initialSaveModelVersion,
hasLibraryPanels: hasLibraryPanelsInV1Dashboard(spec),
};
}
@@ -206,7 +223,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
// Check if dashboard contains library panels based on dashboard version
let hasLibraryPanels = false;
// Case: V1 dashboard loaded (with kubernetesDashboards enabled and dashboardNewLayouts disabled), and user explicitly selected V2Resource export mode
if (!isDashboardV2Spec(origDashboard)) {
if (initialSaveModelVersion === 'v1' && !isDashboardV2Spec(origDashboard)) {
hasLibraryPanels = hasLibraryPanelsInV1Dashboard(origDashboard);
} else if (isDashboardV2Spec(origDashboard)) {
// Case: V2 dashboard (either originally V2 or transformed from V1) being exported as V2Resource
@@ -222,10 +239,35 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
spec: exportableV2,
status: {},
},
initialSaveModelVersion,
hasLibraryPanels,
};
}
// Classic mode
// This handles a case when:
// 1. dashboardNewLayouts feature toggle is enabled
// 2. v1 dashboard is loaded
// 3. dashboard hasn't been edited yet - if it was edited, user would be forced to save it in v2 version
if (
initialSaveModelVersion === 'v1' &&
isDashboardV2Spec(origDashboard) &&
initialSaveModel &&
'panels' in initialSaveModel
) {
const oldModel = new DashboardModel(initialSaveModel, undefined, {
getVariablesFromState: () => {
return getVariablesCompatibility(window.__grafanaSceneContext);
},
});
const exportableV1 = isSharingExternally ? await makeExportableV1(oldModel) : initialSaveModel;
return {
json: exportableV1,
hasLibraryPanels: hasLibraryPanelsInV1Dashboard(initialSaveModel),
initialSaveModelVersion,
};
}
// legacy mode or classic mode when dashboardNewLayouts is disabled
// At this point we know that dashboard should be V1 or could have produced an error
return {
@@ -234,6 +276,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
'error' in exportable || !isV1ClassicDashboard(origDashboard)
? false
: hasLibraryPanelsInV1Dashboard(origDashboard),
initialSaveModelVersion,
};
};
@@ -254,11 +297,9 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
const extension = isViewingYAML ? 'yaml' : 'json';
saveAs(blob, `${title}-${time}.${extension}`);
const isV2Dashboard = 'spec' in dashboard.json && 'elements' in dashboard.json.spec;
DashboardInteractions.exportDownloadJsonClicked({
externally: isSharingExternally,
dashboard_schema_version: isV2Dashboard ? 'v2' : 'v1',
dashboard_schema_version: dashboard.initialSaveModelVersion,
has_library_panels: Boolean(dashboard.hasLibraryPanels),
format: isViewingYAML ? 'yaml' : 'json',
action: 'download',
@@ -269,11 +310,9 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
const dashboard = await this.getExportableDashboardJson();
const { isSharingExternally, isViewingYAML, exportMode } = this.state;
const isV2Dashboard = 'spec' in dashboard.json && 'elements' in dashboard.json.spec;
DashboardInteractions.exportCopyJsonClicked({
externally: isSharingExternally,
dashboard_schema_version: isV2Dashboard ? 'v2' : 'v1',
dashboard_schema_version: dashboard.initialSaveModelVersion,
has_library_panels: Boolean(dashboard.hasLibraryPanels),
export_mode: exportMode || 'classic',
format: isViewingYAML ? 'yaml' : 'json',
@@ -363,7 +402,7 @@ function ShareExportTabRenderer({ model }: SceneComponentProps<ShareExportTab>)
/>
) : (
<Stack gap={2} direction="column">
<Field noMargin label={exportExternallyTranslation}>
<Field label={exportExternallyTranslation}>
<Switch
id="share-externally-toggle"
value={isSharingExternally}
@@ -96,7 +96,6 @@ function findVizPanelInternal(scene: SceneObject, key: string | undefined): VizP
return null;
}
export function findEditPanel(scene: SceneObject, key: string | undefined): VizPanel | null {
if (!key) {
return null;
@@ -259,15 +258,8 @@ export function getClosestVizPanel(sceneObject: SceneObject): VizPanel | null {
}
export function getDefaultVizPanel(): VizPanel {
const defaultPluginId =
config.featureToggles.dashboardNewLayouts || config.featureToggles.newVizSuggestions
? UNCONFIGURED_PANEL_PLUGIN_ID
: 'timeseries';
const newPanelTitle =
config.featureToggles.newVizSuggestions && defaultPluginId === UNCONFIGURED_PANEL_PLUGIN_ID
? ''
: t('dashboard.new-panel-title', 'New panel');
const defaultPluginId = config.featureToggles.dashboardNewLayouts ? UNCONFIGURED_PANEL_PLUGIN_ID : 'timeseries';
const newPanelTitle = t('dashboard.new-panel-title', 'New panel');
return new VizPanel({
title: newPanelTitle,
@@ -128,7 +128,6 @@ export class LogListModel implements LogRowModel {
clone._wrapLogMessage = true;
clone._body = undefined;
clone._highlightTokens = undefined;
clone.collapsed = false;
return clone;
}
@@ -1,21 +1,15 @@
import { PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { DataSourceInstanceSettings, dateTimeFormat, locationUtil, TypedVariableModel } from '@grafana/data';
import { dateTimeFormat } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { locationService, reportInteraction, config } from '@grafana/runtime';
import { Panel } from '@grafana/schema/dist/esm/raw/dashboard/x/dashboard_types.gen';
import { AnnotationQuery, Dashboard } from '@grafana/schema/dist/esm/veneer/dashboard.types';
import { locationService, reportInteraction } from '@grafana/runtime';
import { Box, Legend, TextLink } from '@grafana/ui';
import { Form } from 'app/core/components/Form/Form';
import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { addLibraryPanel } from 'app/features/library-panels/state/api';
import { StoreState } from 'app/types/store';
import { clearLoadedDashboard, importDashboard } from '../state/actions';
import { DashboardSource, DataSourceInput, ImportDashboardDTO, LibraryPanelInputState } from '../state/reducers';
import { DashboardSource, ImportDashboardDTO } from '../state/reducers';
import { ImportDashboardForm } from './ImportDashboardForm';
@@ -51,80 +45,9 @@ class ImportDashboardOverviewUnConnected extends PureComponent<Props, State> {
uidReset: false,
};
onSubmit = async (form: ImportDashboardDTO) => {
onSubmit = (form: ImportDashboardDTO) => {
reportInteraction(IMPORT_FINISHED_EVENT_NAME);
const { dashboard, inputs, folder } = this.props;
// when kubernetesDashboard are enabled, we bypass api/dashboard/import
// and hit the k8s dashboard API directly
if (config.featureToggles.kubernetesDashboards) {
// 1. process datasources so the template placeholder is replaced with the actual value user selected
const annotations = dashboard.annotations.list.map((annotation: AnnotationQuery) => {
return processAnnotation(annotation, inputs, form);
});
const panels = dashboard.panels.map((panel: Panel) => {
return processPanel(panel, inputs, form);
});
const variables = dashboard.templating.list.map((variable: TypedVariableModel) => {
return processVariable(variable, inputs, form);
});
const dashboardWithDataSources: Dashboard = {
...dashboard,
title: form.title,
annotations,
panels,
templating: {
list: variables,
},
uid: form.uid,
};
const newLibraryPanels = inputs.libraryPanels.filter((lp) => lp.state === LibraryPanelInputState.New);
// for library panels that don't exist in the instance, we create them by hitting the library panel API
for (const lp of newLibraryPanels) {
const libPanelWithPanelModel = new PanelModel(lp.model.model);
let { scopedVars, ...panelSaveModel } = libPanelWithPanelModel.getSaveModel();
panelSaveModel = {
libraryPanel: {
name: lp.model.name,
uid: lp.model.uid,
},
...panelSaveModel,
};
try {
await addLibraryPanel(panelSaveModel, folder.uid);
} catch (error) {
console.error('Error adding library panel during dashboard import', error);
}
}
const dashboardK8SPayload: SaveDashboardCommand<Dashboard> = {
dashboard: dashboardWithDataSources,
k8s: {
annotations: {
'grafana.app/folder': form.folder.uid,
},
},
};
// hit v1 API directly
const result = await getDashboardAPI('v1').saveDashboard(dashboardK8SPayload);
if (result.url) {
const dashboardUrl = locationUtil.stripBaseFromUrl(result.url);
locationService.push(dashboardUrl);
}
return;
}
this.props.importDashboard(form);
};
@@ -203,116 +126,3 @@ class ImportDashboardOverviewUnConnected extends PureComponent<Props, State> {
export const ImportDashboardOverview = connector(ImportDashboardOverviewUnConnected);
ImportDashboardOverview.displayName = 'ImportDashboardOverview';
function hasUid(query: Record<string, unknown> | {}): query is { uid: string } {
return 'uid' in query && typeof query['uid'] === 'string';
}
/*
Checks whether the templateized uid matches the user prodvided datasource input
*/
function checkUserInputMatch(
templateizedUid: string,
datasourceInputs: DataSourceInput[],
userDsInputs: DataSourceInstanceSettings[]
) {
const dsName = templateizedUid.replace(/\$\{(.*)\}/, '$1');
const input = datasourceInputs?.find((ds) => ds.name === dsName);
const userInput = input && userDsInputs.find((ds) => ds.type === input.pluginId);
return userInput;
}
function processAnnotation(
annotation: AnnotationQuery,
inputs: { dataSources: DataSourceInput[] },
form: ImportDashboardDTO
): AnnotationQuery {
if (annotation.datasource && annotation.datasource.uid && annotation.datasource.uid.startsWith('$')) {
const userInput = checkUserInputMatch(annotation.datasource.uid, inputs.dataSources, form.dataSources);
if (userInput) {
return {
...annotation,
datasource: {
...annotation.datasource,
uid: userInput.uid,
},
};
}
}
return annotation;
}
function processPanel(panel: Panel, inputs: { dataSources: DataSourceInput[] }, form: ImportDashboardDTO): Panel {
if (panel.datasource && panel.datasource.uid && panel.datasource.uid.startsWith('$')) {
const userInput = checkUserInputMatch(panel.datasource.uid, inputs.dataSources, form.dataSources);
const queries = panel.targets?.map((target) => {
if (target.datasource && hasUid(target.datasource) && target.datasource.uid.startsWith('$')) {
const userInput = checkUserInputMatch(target.datasource.uid, inputs.dataSources, form.dataSources);
if (userInput) {
return {
...target,
datasource: {
...target.datasource,
uid: userInput.uid,
},
};
}
}
return target;
});
if (userInput) {
return {
...panel,
targets: queries,
datasource: {
...panel.datasource,
uid: userInput.uid,
},
};
}
}
return panel;
}
function processVariable(
variable: TypedVariableModel,
inputs: { dataSources: DataSourceInput[] },
form: ImportDashboardDTO
): TypedVariableModel {
if (variable.type === 'query') {
if (variable.datasource && variable.datasource.uid?.startsWith('$')) {
const userInput = checkUserInputMatch(variable.datasource.uid, inputs.dataSources, form.dataSources);
if (userInput) {
return {
...variable,
datasource: {
...variable.datasource,
uid: userInput.uid,
},
};
}
}
}
if (variable.type === 'datasource') {
if (variable.current && variable.current.value && String(variable.current.value).startsWith('$')) {
const userInput = checkUserInputMatch(String(variable.current.value), inputs.dataSources, form.dataSources);
if (userInput) {
return {
...variable,
current: {
selected: variable.current.selected,
text: userInput.name,
value: userInput.uid,
},
};
}
}
}
return variable;
}
@@ -5,8 +5,7 @@ import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2, PanelData, PanelModel, PanelPluginVisualizationSuggestion } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { config } from '@grafana/runtime';
import { Icon, Text, useStyles2 } from '@grafana/ui';
import { useStyles2 } from '@grafana/ui';
import { getAllSuggestions } from '../../suggestions/getAllSuggestions';
@@ -32,21 +31,6 @@ export function VisualizationSuggestions({ searchQuery, onChange, data, panel, t
return result;
}, [searchQuery, suggestions, trackSearch]);
const hasData = data?.series && data.series.length > 0 && !data.series.every((frame) => frame.length === 0);
if (config.featureToggles.newVizSuggestions && !hasData && !searchQuery) {
return (
<div className={styles.emptyStateWrapper}>
<Icon name="chart-line" size="xxxl" className={styles.emptyStateIcon} />
<Text element="p" textAlignment="center" color="secondary">
<Trans i18nKey="dashboard.new-panel.suggestions.empty-state-message">
Run a query to start seeing suggested visualizations
</Trans>
</Text>
</div>
);
}
return (
// This div is needed in some places to make AutoSizer work
<div>
@@ -132,18 +116,5 @@ const getStyles = (theme: GrafanaTheme2) => {
marginBottom: theme.spacing(1),
justifyContent: 'space-evenly',
}),
emptyStateWrapper: css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: theme.spacing(4),
textAlign: 'center',
minHeight: '200px',
}),
emptyStateIcon: css({
color: theme.colors.text.secondary,
marginBottom: theme.spacing(2),
}),
};
};
+2 -5
View File
@@ -5100,12 +5100,8 @@
"new-panel": {
"configure-button": "Configure",
"configure-button-menu": "Toggle menu",
"empty-state-message": "Run a query to visualize it here or go to all visualizations to add other panel types",
"menu-open-panel-editor": "Configure",
"menu-use-library-panel": "Use library panel",
"suggestions": {
"empty-state-message": "Run a query to start seeing suggested visualizations"
}
"menu-use-library-panel": "Use library panel"
},
"new-panel-title": "New panel",
"on-create-new-row": {
@@ -6215,6 +6211,7 @@
"classic": "Classic",
"json": "JSON",
"v1-resource": "V1 Resource",
"v2-resource": "V2 Resource",
"yaml": "YAML"
}
},