Merge remote-tracking branch 'origin/main' into ds-apiserver-with-configs

This commit is contained in:
Ryan McKinley
2025-06-30 13:47:39 -07:00
330 changed files with 8259 additions and 4001 deletions
@@ -49,7 +49,7 @@ func setupTestEnv(t testing.TB) *Service {
return ac
}
func TestUsageMetrics(t *testing.T) {
func TestIntegrationUsageMetrics(t *testing.T) {
tests := []struct {
name string
expectedValue int
@@ -80,7 +80,7 @@ func TestUsageMetrics(t *testing.T) {
}
}
func TestService_DeclareFixedRoles(t *testing.T) {
func TestIntegrationService_DeclareFixedRoles(t *testing.T) {
tests := []struct {
name string
registrations []accesscontrol.RoleRegistration
@@ -165,7 +165,7 @@ func TestService_DeclareFixedRoles(t *testing.T) {
}
}
func TestService_DeclarePluginRoles(t *testing.T) {
func TestIntegrationService_DeclarePluginRoles(t *testing.T) {
tests := []struct {
name string
pluginID string
@@ -278,7 +278,7 @@ func TestService_DeclarePluginRoles(t *testing.T) {
}
}
func TestService_RegisterFixedRoles(t *testing.T) {
func TestIntegrationService_RegisterFixedRoles(t *testing.T) {
tests := []struct {
name string
token licensing.Licensing
@@ -380,7 +380,7 @@ func TestService_RegisterFixedRoles(t *testing.T) {
}
}
func TestService_SearchUsersPermissions(t *testing.T) {
func TestIntegrationService_SearchUsersPermissions(t *testing.T) {
searchOption := accesscontrol.SearchOptions{ActionPrefix: "teams"}
ctx := context.Background()
listAllPerms := map[string][]string{accesscontrol.ActionUsersPermissionsRead: {"users:*"}}
@@ -601,7 +601,7 @@ func TestService_SearchUsersPermissions(t *testing.T) {
}
}
func TestService_SearchUserPermissions(t *testing.T) {
func TestIntegrationService_SearchUserPermissions(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
@@ -832,7 +832,7 @@ func TestService_SearchUserPermissions(t *testing.T) {
}
}
func TestService_SaveExternalServiceRole(t *testing.T) {
func TestIntegrationService_SaveExternalServiceRole(t *testing.T) {
type run struct {
cmd accesscontrol.SaveExternalServiceRoleCommand
wantErr bool
@@ -918,7 +918,7 @@ func TestService_SaveExternalServiceRole(t *testing.T) {
}
}
func TestService_DeleteExternalServiceRole(t *testing.T) {
func TestIntegrationService_DeleteExternalServiceRole(t *testing.T) {
tests := []struct {
name string
initCmd *accesscontrol.SaveExternalServiceRoleCommand
@@ -971,7 +971,7 @@ func TestService_DeleteExternalServiceRole(t *testing.T) {
}
}
func TestService_GetRoleByName(t *testing.T) {
func TestIntegrationService_GetRoleByName(t *testing.T) {
t.Parallel()
ctx := context.Background()
@@ -44,7 +44,7 @@ type getUserPermissionsTestCase struct {
policyCount int
}
func TestAccessControlStore_GetUserPermissions(t *testing.T) {
func TestIntegrationAccessControlStore_GetUserPermissions(t *testing.T) {
tests := []getUserPermissionsTestCase{
{
desc: "should successfully get user, team and builtin permissions",
@@ -158,7 +158,7 @@ type getTeamsPermissionsTestCase struct {
expected int
}
func TestAccessControlStore_GetTeamsPermissions(t *testing.T) {
func TestIntegrationAccessControlStore_GetTeamsPermissions(t *testing.T) {
tests := []getTeamsPermissionsTestCase{
{
desc: "should successfully get team permissions",
@@ -230,7 +230,7 @@ func TestAccessControlStore_GetTeamsPermissions(t *testing.T) {
}
}
func TestAccessControlStore_DeleteUserPermissions(t *testing.T) {
func TestIntegrationAccessControlStore_DeleteUserPermissions(t *testing.T) {
t.Run("expect permissions in all orgs to be deleted", func(t *testing.T) {
store, permissionsStore, usrSvc, teamSvc, _, sql := setupTestEnv(t)
user, _ := createUserAndTeam(t, sql, usrSvc, teamSvc, 1)
@@ -312,7 +312,7 @@ func TestAccessControlStore_DeleteUserPermissions(t *testing.T) {
})
}
func TestAccessControlStore_DeleteTeamPermissions(t *testing.T) {
func TestIntegrationAccessControlStore_DeleteTeamPermissions(t *testing.T) {
t.Run("expect permissions related to team to be deleted", func(t *testing.T) {
store, permissionsStore, usrSvc, teamSvc, _, sql := setupTestEnv(t)
user, team := createUserAndTeam(t, sql, usrSvc, teamSvc, 1)
@@ -767,7 +767,7 @@ func TestIntegrationAccessControlStore_SearchUsersPermissions(t *testing.T) {
}
}
func TestAccessControlStore_GetUsersBasicRoles(t *testing.T) {
func TestIntegrationAccessControlStore_GetUsersBasicRoles(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
@@ -12,7 +12,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestAccessControlStore_SaveExternalServiceRole(t *testing.T) {
func TestIntegrationAccessControlStore_SaveExternalServiceRole(t *testing.T) {
type run struct {
cmd accesscontrol.SaveExternalServiceRoleCommand
wantErr bool
@@ -152,7 +152,7 @@ func TestAccessControlStore_SaveExternalServiceRole(t *testing.T) {
}
}
func TestAccessControlStore_DeleteExternalServiceRole(t *testing.T) {
func TestIntegrationAccessControlStore_DeleteExternalServiceRole(t *testing.T) {
extID := "app1"
tests := []struct {
name string
+1 -1
View File
@@ -32,7 +32,7 @@ func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestFilter_Datasources(t *testing.T) {
func TestIntegrationFilter_Datasources(t *testing.T) {
tests := []filterDatasourcesTestCase{
{
desc: "expect all data sources to be returned",
@@ -30,7 +30,7 @@ type getDescriptionTestCase struct {
expectedStatus int
}
func TestApi_getDescription(t *testing.T) {
func TestIntegrationApi_getDescription(t *testing.T) {
tests := []getDescriptionTestCase{
{
desc: "should return description",
@@ -135,7 +135,7 @@ type getPermissionsTestCase struct {
expectedStatus int
}
func TestApi_getPermissions(t *testing.T) {
func TestIntegrationApi_getPermissions(t *testing.T) {
tests := []getPermissionsTestCase{
{
desc: "expect permissions for resource with id 1",
@@ -181,7 +181,7 @@ type setBuiltinPermissionTestCase struct {
permissions []accesscontrol.Permission
}
func TestApi_setBuiltinRolePermission(t *testing.T) {
func TestIntegrationApi_setBuiltinRolePermission(t *testing.T) {
tests := []setBuiltinPermissionTestCase{
{
desc: "should set Edit permission for Viewer",
@@ -260,7 +260,7 @@ type setTeamPermissionTestCase struct {
byUID bool
}
func TestApi_setTeamPermission(t *testing.T) {
func TestIntegrationApi_setTeamPermission(t *testing.T) {
tests := []setTeamPermissionTestCase{
{
desc: "should set Edit permission for team 1",
@@ -367,7 +367,7 @@ type setUserPermissionTestCase struct {
permissions []accesscontrol.Permission
}
func TestApi_setUserPermission(t *testing.T) {
func TestIntegrationApi_setUserPermission(t *testing.T) {
tests := []setUserPermissionTestCase{
{
desc: "should set Edit permission for user 1",
@@ -442,7 +442,7 @@ func TestApi_setUserPermission(t *testing.T) {
}
}
func TestApi_setUserPermissionForTeams(t *testing.T) {
func TestIntegrationApi_setUserPermissionForTeams(t *testing.T) {
type setUserPermissionForTeamsTestCase struct {
setUserPermissionTestCase
teamCmd *team.CreateTeamCommand
@@ -31,7 +31,7 @@ type setUserPermissionTest struct {
callHook bool
}
func TestService_SetUserPermission(t *testing.T) {
func TestIntegrationService_SetUserPermission(t *testing.T) {
tests := []setUserPermissionTest{
{
desc: "should call hook when updating user permissions",
@@ -75,7 +75,7 @@ type setTeamPermissionTest struct {
callHook bool
}
func TestService_SetTeamPermission(t *testing.T) {
func TestIntegrationService_SetTeamPermission(t *testing.T) {
tests := []setTeamPermissionTest{
{
desc: "should call hook when updating user permissions",
@@ -124,7 +124,7 @@ type setBuiltInRolePermissionTest struct {
callHook bool
}
func TestService_SetBuiltInRolePermission(t *testing.T) {
func TestIntegrationService_SetBuiltInRolePermission(t *testing.T) {
tests := []setBuiltInRolePermissionTest{
{
desc: "should call hook when updating user permissions",
@@ -166,7 +166,7 @@ type setPermissionsTest struct {
expectErr bool
}
func TestService_SetPermissions(t *testing.T) {
func TestIntegrationService_SetPermissions(t *testing.T) {
tests := []setPermissionsTest{
{
desc: "should set all permissions",
@@ -235,7 +235,7 @@ func TestService_SetPermissions(t *testing.T) {
}
}
func TestService_RegisterActionSets(t *testing.T) {
func TestIntegrationService_RegisterActionSets(t *testing.T) {
type registerActionSetsTest struct {
desc string
options Options
@@ -823,7 +823,7 @@ func TestIntegrationTokenCount(t *testing.T) {
require.Equal(t, int64(0), count)
}
func TestRevokeAllUserTokens(t *testing.T) {
func TestIntegrationRevokeAllUserTokens(t *testing.T) {
t.Run("should not fail if the external sessions could not be removed", func(t *testing.T) {
ctx := createTestContext(t)
usr := &user.User{ID: int64(10)}
@@ -856,7 +856,7 @@ func TestRevokeAllUserTokens(t *testing.T) {
})
}
func TestRevokeToken(t *testing.T) {
func TestIntegrationRevokeToken(t *testing.T) {
t.Run("should not fail if the external sessions could not be removed", func(t *testing.T) {
ctx := createTestContext(t)
usr := &user.User{ID: int64(10)}
@@ -887,7 +887,7 @@ func TestRevokeToken(t *testing.T) {
})
}
func TestBatchRevokeAllUserTokens(t *testing.T) {
func TestIntegrationBatchRevokeAllUserTokens(t *testing.T) {
t.Run("should not fail if the external sessions could not be removed", func(t *testing.T) {
ctx := createTestContext(t)
userIds := []int64{1, 2, 3}
+7 -7
View File
@@ -44,7 +44,7 @@ func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestVerifyUsingPKIXPublicKeyFile(t *testing.T) {
func TestIntegrationVerifyUsingPKIXPublicKeyFile(t *testing.T) {
key := rsaKeys[0]
unknownKey := rsaKeys[1]
@@ -79,7 +79,7 @@ func TestVerifyUsingPKIXPublicKeyFile(t *testing.T) {
})
}
func TestVerifyUsingJWKSetFile(t *testing.T) {
func TestIntegrationVerifyUsingJWKSetFile(t *testing.T) {
configure := func(t *testing.T, cfg *setting.Cfg) {
t.Helper()
@@ -118,7 +118,7 @@ func TestVerifyUsingJWKSetFile(t *testing.T) {
}, configure)
}
func TestVerifyUsingJWKSetURL(t *testing.T) {
func TestIntegrationVerifyUsingJWKSetURL(t *testing.T) {
t.Run("should refuse to start with non-https URL", func(t *testing.T) {
var err error
@@ -160,7 +160,7 @@ func TestVerifyUsingJWKSetURL(t *testing.T) {
})
}
func TestCachingJWKHTTPResponse(t *testing.T) {
func TestIntegrationCachingJWKHTTPResponse(t *testing.T) {
jwkCachingScenario(t, "caches the jwk response", func(t *testing.T, sc cachingScenarioContext) {
for i := 0; i < 5; i++ {
token := sign(t, &jwKeys[0], jwt.Claims{Subject: subject}, nil)
@@ -200,7 +200,7 @@ func TestCachingJWKHTTPResponse(t *testing.T) {
})
}
func TestSignatureWithNoneAlgorithm(t *testing.T) {
func TestIntegrationSignatureWithNoneAlgorithm(t *testing.T) {
scenario(t, "rejects a token signed with \"none\" algorithm", func(t *testing.T, sc scenarioContext) {
token := signNone(t, jwt.Claims{Subject: "foo"})
_, err := sc.authJWTSvc.Verify(sc.ctx, token)
@@ -208,7 +208,7 @@ func TestSignatureWithNoneAlgorithm(t *testing.T) {
}, configurePKIXPublicKeyFile)
}
func TestClaimValidation(t *testing.T) {
func TestIntegrationClaimValidation(t *testing.T) {
key := rsaKeys[0]
scenario(t, "validates iss field for equality", func(t *testing.T, sc scenarioContext) {
@@ -368,7 +368,7 @@ func jwkCachingScenario(t *testing.T, desc string, fn cachingScenarioFunc, cbs .
})
}
func TestBase64Paddings(t *testing.T) {
func TestIntegrationBase64Paddings(t *testing.T) {
key := rsaKeys[0]
scenario(t, "verifies a token with base64 padding (non compliant rfc7515#section-2 but accepted)", func(t *testing.T, sc scenarioContext) {
@@ -2643,7 +2643,7 @@ func TestCleanUpDashboard(t *testing.T) {
}
}
func TestK8sDashboardCleanupJob(t *testing.T) {
func TestIntegrationK8sDashboardCleanupJob(t *testing.T) {
tests := []struct {
name string
featureEnabled bool
@@ -38,7 +38,7 @@ func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestDashboardSnapshotsService(t *testing.T) {
func TestIntegrationDashboardSnapshotsService(t *testing.T) {
sqlStore := db.InitTestDB(t)
cfg := setting.NewCfg()
dsStore := dashsnapdb.ProvideStore(sqlStore, cfg)
@@ -97,7 +97,7 @@ func TestDashboardSnapshotsService(t *testing.T) {
})
}
func TestValidateDashboardExists(t *testing.T) {
func TestIntegrationValidateDashboardExists(t *testing.T) {
sqlStore := db.InitTestDB(t)
cfg := setting.NewCfg()
dsStore := dashsnapdb.ProvideStore(sqlStore, cfg)
@@ -63,7 +63,7 @@ func (d *dataSourceMockRetriever) GetDataSource(ctx context.Context, query *data
return nil, datasources.ErrDataSourceNotFound
}
func TestService_AddDataSource(t *testing.T) {
func TestIntegrationService_AddDataSource(t *testing.T) {
t.Run("should not fail if the plugin is not installed", func(t *testing.T) {
dsService := initDSService(t)
dsService.pluginStore = &pluginstore.FakePluginStore{
@@ -354,7 +354,7 @@ func TestService_getAvailableName(t *testing.T) {
}
}
func TestService_UpdateDataSource(t *testing.T) {
func TestIntegrationService_UpdateDataSource(t *testing.T) {
t.Run("should return not found error if datasource not found", func(t *testing.T) {
dsService := initDSService(t)
@@ -761,7 +761,7 @@ func TestService_UpdateDataSource(t *testing.T) {
})
}
func TestService_DeleteDataSource(t *testing.T) {
func TestIntegrationService_DeleteDataSource(t *testing.T) {
t.Run("should not return an error if data source doesn't exist", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
@@ -1038,7 +1038,7 @@ func TestService_awsServiceNamespace(t *testing.T) {
}
//nolint:goconst
func TestService_GetHttpTransport(t *testing.T) {
func TestIntegrationService_GetHttpTransport(t *testing.T) {
cfg := &setting.Cfg{}
t.Run("Should use cached proxy", func(t *testing.T) {
@@ -1450,7 +1450,7 @@ func TestService_GetHttpTransport(t *testing.T) {
})
}
func TestService_getProxySettings(t *testing.T) {
func TestIntegrationService_getProxySettings(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
@@ -1528,7 +1528,7 @@ func TestService_getProxySettings(t *testing.T) {
})
}
func TestService_getTimeout(t *testing.T) {
func TestIntegrationService_getTimeout(t *testing.T) {
cfg := &setting.Cfg{}
originalTimeout := sdkhttpclient.DefaultTimeoutOptions.Timeout
sdkhttpclient.DefaultTimeoutOptions.Timeout = time.Minute
@@ -1562,7 +1562,7 @@ func TestService_getTimeout(t *testing.T) {
}
}
func TestService_GetDecryptedValues(t *testing.T) {
func TestIntegrationService_GetDecryptedValues(t *testing.T) {
t.Run("should migrate and retrieve values from secure json data", func(t *testing.T) {
ds := &datasources.DataSource{
ID: 1,
@@ -1621,7 +1621,7 @@ func TestService_GetDecryptedValues(t *testing.T) {
})
}
func TestDataSource_CustomHeaders(t *testing.T) {
func TestIntegrationDataSource_CustomHeaders(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
+10 -22
View File
@@ -163,13 +163,6 @@ var (
Expression: "true", // turned on by default
AllowSelfServe: true,
},
{
Name: "lokiQuerySplittingConfig",
Description: "Give users the option to configure split durations for Loki queries",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaObservabilityLogsSquad,
},
{
Name: "individualCookiePreferences",
Description: "Support overriding cookie preferences per user",
@@ -262,13 +255,6 @@ var (
Stage: FeatureStageExperimental,
Owner: grafanaFrontendPlatformSquad,
},
{
Name: "lokiPredefinedOperations",
Description: "Adds predefined query operations to Loki query editor",
FrontendOnly: true,
Stage: FeatureStageExperimental,
Owner: grafanaObservabilityLogsSquad,
},
{
Name: "pluginsFrontendSandbox",
Description: "Enables the plugins frontend sandbox",
@@ -570,14 +556,6 @@ var (
Stage: FeatureStagePublicPreview,
Owner: awsDatasourcesSquad,
},
{
Name: "lokiStructuredMetadata",
Description: "Enables the loki data source to request structured metadata from the Loki server",
Stage: FeatureStageGeneralAvailability,
FrontendOnly: false,
Owner: grafanaObservabilityLogsSquad,
Expression: "true",
},
{
Name: "cachingOptimizeSerializationMemoryUsage",
Description: "If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses.",
@@ -1771,6 +1749,16 @@ var (
Owner: grafanaPartnerPluginsSquad,
Expression: "false",
},
{
Name: "enableAppChromeExtensions",
Description: "Set this to true to enable all app chrome extensions registered by plugins.",
Stage: FeatureStageExperimental,
Owner: grafanaPluginsPlatformSquad,
HideFromAdminPage: true,
HideFromDocs: true,
FrontendOnly: true,
Expression: "false", // extensions will be disabled by default
},
}
)
+1 -3
View File
@@ -19,7 +19,6 @@ editPanelCSVDragAndDrop,experimental,@grafana/dataviz-squad,false,false,true
logsContextDatasourceUi,GA,@grafana/observability-logs,false,false,true
lokiShardSplitting,experimental,@grafana/observability-logs,false,false,true
lokiQuerySplitting,GA,@grafana/observability-logs,false,false,true
lokiQuerySplittingConfig,experimental,@grafana/observability-logs,false,false,true
individualCookiePreferences,experimental,@grafana/grafana-backend-group,false,false,false
influxdbBackendMigration,GA,@grafana/partner-datasources,false,false,true
influxqlStreamingParser,experimental,@grafana/partner-datasources,false,false,false
@@ -33,7 +32,6 @@ refactorVariablesTimeRange,preview,@grafana/dashboards-squad,false,false,false
faroDatasourceSelector,preview,@grafana/app-o11y,false,false,true
enableDatagridEditing,preview,@grafana/dataviz-squad,false,false,true
extraThemes,experimental,@grafana/grafana-frontend-platform,false,false,true
lokiPredefinedOperations,experimental,@grafana/observability-logs,false,false,true
pluginsFrontendSandbox,privatePreview,@grafana/plugins-platform-backend,false,false,false
pluginsDetailsRightPanel,GA,@grafana/plugins-platform-backend,false,false,true
sqlDatasourceDatabaseSelection,preview,@grafana/oss-big-tent,false,false,true
@@ -75,7 +73,6 @@ queryServiceRewrite,experimental,@grafana/grafana-datasources-core-services,fals
queryServiceFromUI,experimental,@grafana/grafana-datasources-core-services,false,false,true
queryServiceFromExplore,experimental,@grafana/grafana-datasources-core-services,false,false,true
cloudWatchBatchQueries,preview,@grafana/aws-datasources,false,false,false
lokiStructuredMetadata,GA,@grafana/observability-logs,false,false,false
cachingOptimizeSerializationMemoryUsage,experimental,@grafana/grafana-operator-experience-squad,false,false,false
prometheusCodeModeMetricNamesSearch,experimental,@grafana/oss-big-tent,false,false,true
addFieldFromCalculationStatFunctions,GA,@grafana/dataviz-squad,false,false,true
@@ -231,3 +228,4 @@ alertingImportAlertmanagerAPI,experimental,@grafana/alerting-squad,false,false,f
preferLibraryPanelTitle,privatePreview,@grafana/dashboards-squad,false,false,false
tabularNumbers,GA,@grafana/grafana-frontend-platform,false,false,false
newInfluxDSConfigPageDesign,privatePreview,@grafana/partner-datasources,false,false,false
enableAppChromeExtensions,experimental,@grafana/plugins-platform-backend,false,false,true
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
19 logsContextDatasourceUi GA @grafana/observability-logs false false true
20 lokiShardSplitting experimental @grafana/observability-logs false false true
21 lokiQuerySplitting GA @grafana/observability-logs false false true
lokiQuerySplittingConfig experimental @grafana/observability-logs false false true
22 individualCookiePreferences experimental @grafana/grafana-backend-group false false false
23 influxdbBackendMigration GA @grafana/partner-datasources false false true
24 influxqlStreamingParser experimental @grafana/partner-datasources false false false
32 faroDatasourceSelector preview @grafana/app-o11y false false true
33 enableDatagridEditing preview @grafana/dataviz-squad false false true
34 extraThemes experimental @grafana/grafana-frontend-platform false false true
lokiPredefinedOperations experimental @grafana/observability-logs false false true
35 pluginsFrontendSandbox privatePreview @grafana/plugins-platform-backend false false false
36 pluginsDetailsRightPanel GA @grafana/plugins-platform-backend false false true
37 sqlDatasourceDatabaseSelection preview @grafana/oss-big-tent false false true
73 queryServiceFromUI experimental @grafana/grafana-datasources-core-services false false true
74 queryServiceFromExplore experimental @grafana/grafana-datasources-core-services false false true
75 cloudWatchBatchQueries preview @grafana/aws-datasources false false false
lokiStructuredMetadata GA @grafana/observability-logs false false false
76 cachingOptimizeSerializationMemoryUsage experimental @grafana/grafana-operator-experience-squad false false false
77 prometheusCodeModeMetricNamesSearch experimental @grafana/oss-big-tent false false true
78 addFieldFromCalculationStatFunctions GA @grafana/dataviz-squad false false true
228 preferLibraryPanelTitle privatePreview @grafana/dashboards-squad false false false
229 tabularNumbers GA @grafana/grafana-frontend-platform false false false
230 newInfluxDSConfigPageDesign privatePreview @grafana/partner-datasources false false false
231 enableAppChromeExtensions experimental @grafana/plugins-platform-backend false false true
+4 -12
View File
@@ -87,10 +87,6 @@ const (
// Split large interval queries into subqueries with smaller time intervals
FlagLokiQuerySplitting = "lokiQuerySplitting"
// FlagLokiQuerySplittingConfig
// Give users the option to configure split durations for Loki queries
FlagLokiQuerySplittingConfig = "lokiQuerySplittingConfig"
// FlagIndividualCookiePreferences
// Support overriding cookie preferences per user
FlagIndividualCookiePreferences = "individualCookiePreferences"
@@ -143,10 +139,6 @@ const (
// Enables extra themes
FlagExtraThemes = "extraThemes"
// FlagLokiPredefinedOperations
// Adds predefined query operations to Loki query editor
FlagLokiPredefinedOperations = "lokiPredefinedOperations"
// FlagPluginsFrontendSandbox
// Enables the plugins frontend sandbox
FlagPluginsFrontendSandbox = "pluginsFrontendSandbox"
@@ -311,10 +303,6 @@ const (
// Runs CloudWatch metrics queries as separate batches
FlagCloudWatchBatchQueries = "cloudWatchBatchQueries"
// FlagLokiStructuredMetadata
// Enables the loki data source to request structured metadata from the Loki server
FlagLokiStructuredMetadata = "lokiStructuredMetadata"
// FlagCachingOptimizeSerializationMemoryUsage
// If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses.
FlagCachingOptimizeSerializationMemoryUsage = "cachingOptimizeSerializationMemoryUsage"
@@ -934,4 +922,8 @@ const (
// FlagNewInfluxDSConfigPageDesign
// Enables new design for the InfluxDB data source configuration page
FlagNewInfluxDSConfigPageDesign = "newInfluxDSConfigPageDesign"
// FlagEnableAppChromeExtensions
// Set this to true to enable all app chrome extensions registered by plugins.
FlagEnableAppChromeExtensions = "enableAppChromeExtensions"
)
+22 -3
View File
@@ -943,6 +943,22 @@
"codeowner": "@grafana/aws-datasources"
}
},
{
"metadata": {
"name": "enableAppChromeExtensions",
"resourceVersion": "1750235111726",
"creationTimestamp": "2025-06-18T08:25:11Z"
},
"spec": {
"description": "Set this to true to enable all app chrome extensions registered by plugins.",
"stage": "experimental",
"codeowner": "@grafana/plugins-platform-backend",
"frontend": true,
"hideFromAdminPage": true,
"hideFromDocs": true,
"expression": "false"
}
},
{
"metadata": {
"name": "enableDatagridEditing",
@@ -1830,7 +1846,8 @@
"metadata": {
"name": "lokiPredefinedOperations",
"resourceVersion": "1750434297879",
"creationTimestamp": "2023-06-02T10:52:36Z"
"creationTimestamp": "2023-06-02T10:52:36Z",
"deletionTimestamp": "2025-06-27T08:08:44Z"
},
"spec": {
"description": "Adds predefined query operations to Loki query editor",
@@ -1872,7 +1889,8 @@
"metadata": {
"name": "lokiQuerySplittingConfig",
"resourceVersion": "1750434297879",
"creationTimestamp": "2023-03-20T15:51:36Z"
"creationTimestamp": "2023-03-20T15:51:36Z",
"deletionTimestamp": "2025-06-27T09:32:43Z"
},
"spec": {
"description": "Give users the option to configure split durations for Loki queries",
@@ -1910,7 +1928,8 @@
"metadata": {
"name": "lokiStructuredMetadata",
"resourceVersion": "1750434297879",
"creationTimestamp": "2023-11-16T16:06:14Z"
"creationTimestamp": "2023-11-16T16:06:14Z",
"deletionTimestamp": "2025-06-27T09:00:53Z"
},
"spec": {
"description": "Enables the loki data source to request structured metadata from the Loki server",
+10 -10
View File
@@ -358,7 +358,7 @@ func TestIntegrationFolderService(t *testing.T) {
})
}
func TestIntegrationNestedFolderService(t *testing.T) {
func TestIntegrationNestedFolderServiceBasicOperations(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
@@ -438,7 +438,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, dashSrv, ac, b)
require.NoError(t, err)
elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOn, featuresFlagOn, ac, dashSrv)
elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOn, featuresFlagOn, ac, dashSrv, nil, nil)
lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOn)
require.NoError(t, err)
@@ -517,7 +517,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac, b)
require.NoError(t, err)
elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOff, featuresFlagOff, ac, dashSrv)
elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOff, featuresFlagOff, ac, dashSrv, nil, nil)
lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOff)
require.NoError(t, err)
@@ -655,7 +655,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
require.NoError(t, err)
dashSrv.RegisterDashboardPermissions(dashboardPermissions)
elementService := libraryelements.ProvideService(cfg, db, routeRegister, tc.service, tc.featuresFlag, ac, dashSrv)
elementService := libraryelements.ProvideService(cfg, db, routeRegister, tc.service, tc.featuresFlag, ac, dashSrv, nil, nil)
lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, tc.service)
require.NoError(t, err)
@@ -726,7 +726,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
})
}
func TestNestedFolderServiceFeatureToggle(t *testing.T) {
func TestIntegrationNestedFolderServiceFeatureToggle(t *testing.T) {
nestedFolderStore := folder.NewFakeStore()
dashStore := dashboards.FakeDashboardStore{}
@@ -757,7 +757,7 @@ func TestNestedFolderServiceFeatureToggle(t *testing.T) {
})
}
func TestFolderServiceDualWrite(t *testing.T) {
func TestIntegrationFolderServiceDualWrite(t *testing.T) {
db, _ := sqlstore.InitTestDB(t)
cfg := setting.NewCfg()
features := featuremgmt.WithFeatures()
@@ -816,7 +816,7 @@ func TestFolderServiceDualWrite(t *testing.T) {
})
}
func TestNestedFolderService(t *testing.T) {
func TestIntegrationNestedFolderService(t *testing.T) {
t.Run("with feature flag unset", func(t *testing.T) {
t.Run("Should create a folder in both dashboard and folders tables", func(t *testing.T) {
// dash is needed here because folderSvc.Create expects SaveDashboard to return it
@@ -1690,7 +1690,7 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
})
}
func TestFolderServiceGetFolder(t *testing.T) {
func TestIntegrationFolderServiceGetFolder(t *testing.T) {
db, _ := sqlstore.InitTestDB(t)
signedInAdminUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{
@@ -1801,7 +1801,7 @@ func TestFolderServiceGetFolder(t *testing.T) {
}
}
func TestFolderServiceGetFolders(t *testing.T) {
func TestIntegrationFolderServiceGetFolders(t *testing.T) {
db, cfg := sqlstore.InitTestDB(t)
folderStore := ProvideDashboardFolderStore(db)
@@ -1872,7 +1872,7 @@ func TestFolderServiceGetFolders(t *testing.T) {
// TODO replace it with an API test under /pkg/tests/api/folders
// whenever the golang client with get updated to allow filtering child folders by permission
func TestGetChildrenFilterByPermission(t *testing.T) {
func TestIntegrationGetChildrenFilterByPermission(t *testing.T) {
db, cfg := sqlstore.InitTestDB(t)
signedInAdminUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{
@@ -847,7 +847,7 @@ func TestGetFoldersFromApiServer(t *testing.T) {
})
}
func TestDeleteFoldersFromApiServer(t *testing.T) {
func TestIntegrationDeleteFoldersFromApiServer(t *testing.T) {
fakeK8sClient := new(client.MockK8sHandler)
fakeK8sClient.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
dashboardK8sclient := new(client.MockK8sHandler)
+227 -5
View File
@@ -1,23 +1,38 @@
package libraryelements
import (
"encoding/json"
"errors"
"fmt"
"hash/fnv"
"net/http"
"strings"
dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/kinds/librarypanel"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/libraryelements/model"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errhttp"
"github.com/grafana/grafana/pkg/web"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
)
func (l *LibraryElementService) registerAPIEndpoints() {
@@ -25,13 +40,13 @@ func (l *LibraryElementService) registerAPIEndpoints() {
l.RouteRegister.Group("/api/library-elements", func(entities routing.RouteRegister) {
uidScope := ScopeLibraryPanelsProvider.GetResourceScopeUID(ac.Parameter(":uid"))
entities.Post("/", authorize(ac.EvalPermission(ActionLibraryPanelsCreate)), routing.Wrap(l.createHandler))
entities.Delete("/:uid", authorize(ac.EvalPermission(ActionLibraryPanelsDelete, uidScope)), routing.Wrap(l.deleteHandler))
entities.Get("/", authorize(ac.EvalPermission(ActionLibraryPanelsRead)), routing.Wrap(l.getAllHandler))
entities.Post("/", authorize(ac.EvalPermission(ActionLibraryPanelsCreate)), routing.Wrap(l.createHandler)) // TODO: add wrapper for k8s
entities.Delete("/:uid", authorize(ac.EvalPermission(ActionLibraryPanelsDelete, uidScope)), routing.Wrap(l.deleteHandler)) // TODO: add wrapper for k8s
entities.Get("/", authorize(ac.EvalPermission(ActionLibraryPanelsRead)), routing.Wrap(l.getAllHandler)) // TODO: add wrapper for k8s - requires search
entities.Get("/:uid", authorize(ac.EvalPermission(ActionLibraryPanelsRead)), routing.Wrap(l.getHandler))
entities.Get("/:uid/connections/", authorize(ac.EvalPermission(ActionLibraryPanelsRead, uidScope)), routing.Wrap(l.getConnectionsHandler))
entities.Get("/name/:name", routing.Wrap(l.getByNameHandler))
entities.Patch("/:uid", authorize(ac.EvalPermission(ActionLibraryPanelsWrite, uidScope)), routing.Wrap(l.patchHandler))
entities.Get("/name/:name", routing.Wrap(l.getByNameHandler)) // TODO: add wrapper for k8s - requires search
entities.Patch("/:uid", authorize(ac.EvalPermission(ActionLibraryPanelsWrite, uidScope)), routing.Wrap(l.patchHandler)) // TODO: add wrapper for k8s
})
}
@@ -133,6 +148,11 @@ func (l *LibraryElementService) deleteHandler(c *contextmodel.ReqContext) respon
// 404: notFoundError
// 500: internalServerError
func (l *LibraryElementService) getHandler(c *contextmodel.ReqContext) response.Response {
if l.features.IsEnabled(c.Req.Context(), featuremgmt.FlagKubernetesLibraryPanels) {
l.k8sHandler.getK8sLibraryElement(c)
return nil // already handled in the k8s handler
}
ctx := c.Req.Context()
element, err := l.getLibraryElementByUid(ctx, c.SignedInUser,
model.GetLibraryElementCommand{
@@ -529,3 +549,205 @@ type GetLibraryElementConnectionsResponse struct {
// in: body
Body model.LibraryElementConnectionsResponse `json:"body"`
}
//-----------------------------------------------------------------------------------------
// Library Elements k8s wrapper functions
//-----------------------------------------------------------------------------------------
type libraryElementsK8sHandler struct {
cfg *setting.Cfg
namespacer request.NamespaceMapper
gvr schema.GroupVersionResource
clientConfigProvider grafanaapiserver.DirectRestConfigProvider
folderService folder.Service
dashboardsService dashboards.DashboardService
userService user.Service
}
func newLibraryElementsK8sHandler(cfg *setting.Cfg, clientConfigProvider grafanaapiserver.DirectRestConfigProvider, folderService folder.Service, userService user.Service, dashboardsService dashboards.DashboardService) *libraryElementsK8sHandler {
gvr := schema.GroupVersionResource{
Group: dashboardV0.APIGroup,
Version: dashboardV0.APIVersion,
Resource: dashboardV0.LIBRARY_PANEL_RESOURCE,
}
return &libraryElementsK8sHandler{
cfg: cfg,
gvr: gvr,
namespacer: request.GetNamespaceMapper(cfg),
clientConfigProvider: clientConfigProvider,
folderService: folderService,
dashboardsService: dashboardsService,
userService: userService,
}
}
func (lk8s *libraryElementsK8sHandler) getK8sLibraryElement(c *contextmodel.ReqContext) {
client, ok := lk8s.getClient(c)
if !ok {
return
}
uid := web.Params(c.Req)[":uid"]
out, err := client.Get(c.Req.Context(), uid, v1.GetOptions{})
if err != nil {
lk8s.writeError(c, err)
return
}
dto, err := lk8s.unstructuredToLegacyLibraryPanelDTO(c, *out)
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "conversion error", err)
return
}
c.JSON(http.StatusOK, model.LibraryElementResponse{Result: *dto})
}
func (lk8s *libraryElementsK8sHandler) unstructuredToLegacyLibraryPanelDTO(c *contextmodel.ReqContext, item unstructured.Unstructured) (*model.LibraryElementDTO, error) {
spec, exists := item.Object["spec"].(map[string]interface{})
if !exists {
return nil, fmt.Errorf("spec not found in unstructured object")
}
id := int64(0)
folderUID := ""
meta, err := utils.MetaAccessor(&item)
if err == nil {
id = meta.GetDeprecatedInternalID() // nolint:staticcheck
folderUID = meta.GetFolder()
}
var libraryPanelSpec dashboardV0.LibraryPanelSpec
specJSON, err := json.Marshal(spec)
if err != nil {
return nil, fmt.Errorf("failed to marshal spec: %w", err)
}
err = json.Unmarshal(specJSON, &libraryPanelSpec)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal spec into LibraryPanelSpec: %w", err)
}
// need to reconstruct this section from what we have in the k8s object
legacyModel := map[string]any{}
legacyModel["datasource"] = libraryPanelSpec.Datasource
legacyModel["description"] = libraryPanelSpec.Description
legacyModel["fieldConfig"] = libraryPanelSpec.FieldConfig.Object
legacyModel["gridPos"] = libraryPanelSpec.GridPos
legacyModel["id"] = id
legacyModel["options"] = libraryPanelSpec.Options.Object
legacyModel["pluginVersion"] = libraryPanelSpec.PluginVersion
legacyModel["type"] = libraryPanelSpec.Type
legacyModel["title"] = libraryPanelSpec.PanelTitle // this is the title of the panel when displayed in the dashboard
legacyModel["libraryPanel"] = map[string]string{
"name": libraryPanelSpec.Title, // this is the title of the actual library panel, when displayed in the library panel list
"uid": item.GetName(),
}
if len(libraryPanelSpec.Links) > 0 {
legacyModel["links"] = libraryPanelSpec.Links
}
if len(libraryPanelSpec.Targets) > 0 {
legacyModel["targets"] = libraryPanelSpec.Targets
}
if libraryPanelSpec.Transparent {
legacyModel["transparent"] = libraryPanelSpec.Transparent
}
finalModel, err := json.Marshal(legacyModel)
if err != nil {
return nil, fmt.Errorf("failed to marshal model: %w", err)
}
dto := &model.LibraryElementDTO{
ID: id,
OrgID: c.OrgID,
FolderUID: folderUID,
UID: item.GetName(),
Name: libraryPanelSpec.Title,
Kind: int64(model.PanelElement),
Type: libraryPanelSpec.Type,
Description: libraryPanelSpec.Description,
Model: finalModel,
Version: item.GetGeneration(),
Meta: model.LibraryElementDTOMeta{
FolderUID: folderUID,
Created: meta.GetCreationTimestamp().Time,
},
}
if folderUID != "" {
folder, err := lk8s.folderService.Get(c.Req.Context(), &folder.GetFolderQuery{
OrgID: c.OrgID,
UID: &folderUID,
SignedInUser: c.SignedInUser,
})
if err != nil {
return nil, err
}
dto.Meta.FolderName = folder.Title
dto.FolderID = folder.ID // nolint:staticcheck
}
dashboards, err := lk8s.dashboardsService.GetDashboardsByLibraryPanelUID(c.Req.Context(), item.GetName(), c.OrgID)
if err != nil {
return nil, err
}
dto.Meta.ConnectedDashboards = int64(len(dashboards))
createdBy := meta.GetCreatedBy()
updatedBy := createdBy // the old /api returns the same user for updated if it was never updated
userUIDs := []string{meta.GetCreatedBy()}
if timestamp, err := meta.GetUpdatedTimestamp(); err == nil && timestamp != nil {
dto.Meta.Updated = *timestamp
updatedBy = meta.GetUpdatedBy()
userUIDs = append(userUIDs, updatedBy)
} else {
// if never updated, the old /api returns the same timestamp for updated as for created
dto.Meta.Updated = dto.Meta.Created
}
users, err := lk8s.userService.ListByIdOrUID(c.Req.Context(), userUIDs, []int64{c.OrgID})
if err != nil {
return nil, err
}
for _, user := range users {
if user.UID == createdBy {
dto.Meta.CreatedBy = librarypanel.LibraryElementDTOMetaUser{
Id: user.ID,
Name: user.Login,
AvatarUrl: dtos.GetGravatarUrl(lk8s.cfg, user.Email),
}
}
// not else because /api returns the same user for updated if it was never updated
if user.UID == updatedBy {
dto.Meta.UpdatedBy = librarypanel.LibraryElementDTOMetaUser{
Id: user.ID,
Name: user.Login,
AvatarUrl: dtos.GetGravatarUrl(lk8s.cfg, user.Email),
}
}
}
return dto, nil
}
//-----------------------------------------------------------------------------------------
// Utility functions
//-----------------------------------------------------------------------------------------
func (lk8s *libraryElementsK8sHandler) getClient(c *contextmodel.ReqContext) (dynamic.ResourceInterface, bool) {
dyn, err := dynamic.NewForConfig(lk8s.clientConfigProvider.GetDirectRestConfig(c))
if err != nil {
c.JsonApiErr(500, "client", err)
return nil, false
}
return dyn.Resource(lk8s.gvr).Namespace(lk8s.namespacer(c.OrgID)), true
}
func (lk8s *libraryElementsK8sHandler) writeError(c *contextmodel.ReqContext, err error) {
//nolint:errorlint
statusError, ok := err.(*k8serrors.StatusError)
if ok {
c.JsonApiErr(int(statusError.Status().Code), statusError.Status().Message, err)
return
}
errhttp.Write(c.Req.Context(), err, c.Resp)
}
@@ -0,0 +1,180 @@
package libraryelements
import (
"context"
"encoding/json"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/apimachinery/utils"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/libraryelements/model"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestUnstructuredToLegacyLibraryPanelDTO(t *testing.T) {
cfg := setting.NewCfg()
userSvc := &usertest.FakeUserService{}
testUser := &user.User{
ID: 1,
UID: "test-user-uid",
Login: "testuser",
Email: "test@example.com",
}
userSvc.ExpectedListUsersByIdOrUid = []*user.User{testUser}
testFolder := &folder.Folder{
ID: 1,
UID: "test-folder-uid",
Title: "Test Folder",
}
folderSvc := &foldertest.FakeService{
ExpectedFolder: testFolder,
}
dashboardsSvc := &dashboards.FakeDashboardService{}
testDashboard := &dashboards.DashboardRef{
ID: 1,
UID: "test-dashboard-uid",
FolderUID: testFolder.UID,
}
dashboardsSvc.On("GetDashboardsByLibraryPanelUID", mock.Anything, "test-panel-uid", int64(1)).Return([]*dashboards.DashboardRef{testDashboard}, nil)
handler := &libraryElementsK8sHandler{
cfg: cfg,
folderService: folderSvc,
dashboardsService: dashboardsSvc,
userService: userSvc,
}
unstructuredObj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "dashboard.grafana.app/v0alpha1",
"kind": "LibraryPanel",
"metadata": map[string]any{
"name": "test-panel-uid",
},
"spec": map[string]any{
"type": "text",
"pluginVersion": "1.0.0",
"title": "Test Library Panel",
"panelTitle": "Test Panel Title",
"description": "Test description",
"options": map[string]interface{}{
"content": "Test content",
},
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"color": map[string]interface{}{
"mode": "palette-classic",
},
},
},
"gridPos": map[string]interface{}{
"h": 8,
"w": 12,
"x": 0,
"y": 0,
},
"datasource": map[string]interface{}{
"type": "testdata",
"uid": "test-datasource",
},
"transparent": true,
"links": []interface{}{
map[string]interface{}{
"title": "Test Link",
"url": "https://example.com",
},
},
"targets": []interface{}{
map[string]interface{}{
"refId": "A",
"expr": "test_query",
},
},
},
},
}
meta, err := utils.MetaAccessor(unstructuredObj)
require.NoError(t, err)
meta.SetFolder(testFolder.UID)
meta.SetGeneration(2)
creationTimestamp := metav1.NewTime(time.Now())
meta.SetCreationTimestamp(creationTimestamp)
meta.SetCreatedBy(testUser.UID)
meta.SetDeprecatedInternalID(123) // nolint:staticcheck
reqContext := &contextmodel.ReqContext{
Context: &web.Context{
Req: httptest.NewRequest("GET", "/", nil).WithContext(context.Background()),
},
SignedInUser: &user.SignedInUser{
UserID: 1,
OrgID: 1,
OrgRole: org.RoleAdmin,
},
}
result, err := handler.unstructuredToLegacyLibraryPanelDTO(reqContext, *unstructuredObj)
require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, int64(123), result.ID)
require.Equal(t, int64(1), result.OrgID)
require.Equal(t, testFolder.UID, result.FolderUID)
require.Equal(t, "test-panel-uid", result.UID)
require.Equal(t, "Test Library Panel", result.Name)
require.Equal(t, int64(model.PanelElement), result.Kind)
require.Equal(t, "text", result.Type)
require.Equal(t, "Test description", result.Description)
require.Equal(t, int64(2), result.Version)
require.Equal(t, testFolder.UID, result.Meta.FolderUID)
require.Equal(t, testFolder.Title, result.Meta.FolderName)
require.Equal(t, int64(1), result.Meta.ConnectedDashboards)
require.Equal(t, int64(1), result.FolderID) // nolint:staticcheck
require.Equal(t, creationTimestamp.Format(time.RFC3339), result.Meta.Created.Format(time.RFC3339))
require.Equal(t, testUser.ID, result.Meta.CreatedBy.Id)
require.Equal(t, testUser.Login, result.Meta.CreatedBy.Name)
require.Equal(t, dtos.GetGravatarUrl(cfg, testUser.Email), result.Meta.CreatedBy.AvatarUrl)
require.Equal(t, creationTimestamp.Format(time.RFC3339), result.Meta.Updated.Format(time.RFC3339))
require.Equal(t, testUser.ID, result.Meta.UpdatedBy.Id)
require.Equal(t, testUser.Login, result.Meta.UpdatedBy.Name)
require.Equal(t, dtos.GetGravatarUrl(cfg, testUser.Email), result.Meta.UpdatedBy.AvatarUrl)
var modelMap map[string]interface{}
err = json.Unmarshal(result.Model, &modelMap)
require.NoError(t, err)
require.Equal(t, "testdata", modelMap["datasource"].(map[string]interface{})["type"])
require.Equal(t, "test-datasource", modelMap["datasource"].(map[string]interface{})["uid"])
require.Equal(t, "Test description", modelMap["description"])
require.Equal(t, float64(123), modelMap["id"])
require.Equal(t, "text", modelMap["type"])
require.Equal(t, "Test Panel Title", modelMap["title"])
require.Equal(t, "Test content", modelMap["options"].(map[string]interface{})["content"])
require.Equal(t, true, modelMap["transparent"])
require.Equal(t, "Test Library Panel", modelMap["libraryPanel"].(map[string]interface{})["name"])
require.Equal(t, "test-panel-uid", modelMap["libraryPanel"].(map[string]interface{})["uid"])
links := modelMap["links"].([]interface{})
require.Len(t, links, 1)
require.Equal(t, "Test Link", links[0].(map[string]interface{})["title"])
targets := modelMap["targets"].([]interface{})
require.Len(t, targets, 1)
require.Equal(t, "A", targets[0].(map[string]interface{})["refId"])
dashboardsSvc.AssertExpectations(t)
}
@@ -9,14 +9,16 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/libraryelements/model"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.RouteRegister, folderService folder.Service, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, dashboardsService dashboards.DashboardService) *LibraryElementService {
func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.RouteRegister, folderService folder.Service, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, dashboardsService dashboards.DashboardService, clientConfigProvider grafanaapiserver.DirectRestConfigProvider, userService user.Service) *LibraryElementService {
l := &LibraryElementService{
Cfg: cfg,
SQLStore: sqlStore,
@@ -26,6 +28,7 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.Rout
log: log.New("library-elements"),
features: features,
AccessControl: ac,
k8sHandler: newLibraryElementsK8sHandler(cfg, clientConfigProvider, folderService, userService, dashboardsService),
}
l.registerAPIEndpoints()
@@ -55,6 +58,7 @@ type LibraryElementService struct {
log log.Logger
features featuremgmt.FeatureToggles
AccessControl accesscontrol.AccessControl
k8sHandler *libraryElementsK8sHandler
}
var _ Service = (*LibraryElementService)(nil)
@@ -531,6 +531,7 @@ func setupTestScenario(t *testing.T) scenarioContext {
features := featuremgmt.WithFeatures()
tracer := tracing.InitializeTracerForTest()
sqlStore, cfg := db.InitTestDBWithCfg(t)
t.Cleanup(db.CleanupTestDB)
quotaService := quotatest.New(false, nil)
dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
@@ -55,7 +55,7 @@ func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestConnectLibraryPanelsForDashboard(t *testing.T) {
func TestIntegrationConnectLibraryPanelsForDashboard(t *testing.T) {
scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with a library panel, it should connect the two",
func(t *testing.T, sc scenarioContext) {
dashJSON := map[string]any{
@@ -348,7 +348,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) {
})
}
func TestImportLibraryPanelsForDashboard(t *testing.T) {
func TestIntegrationImportLibraryPanelsForDashboard(t *testing.T) {
testScenario(t, "When an admin tries to import a dashboard with a library panel that does not exist, it should import the library panel",
func(t *testing.T, sc scenarioContext) {
var missingUID = "jL6MrxCMz"
@@ -850,7 +850,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig)
elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, features, ac, dashService)
elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, features, ac, dashService, nil, nil)
service := LibraryPanelService{
Cfg: cfg,
SQLStore: sqlStore,
+1 -1
View File
@@ -24,7 +24,7 @@ func TestMain(m *testing.M) {
testsuite.Run(m)
}
func Test_provideLiveService_RedisUnavailable(t *testing.T) {
func TestIntegration_provideLiveService_RedisUnavailable(t *testing.T) {
cfg := setting.NewCfg()
cfg.LiveHAEngine = "testredisunavailable"
@@ -86,7 +86,7 @@ func TestService_Validate(t *testing.T) {
}
}
func TestUserLoginAttempts(t *testing.T) {
func TestIntegrationUserLoginAttempts(t *testing.T) {
ctx := context.Background()
cfg := setting.NewCfg()
cfg.DisableBruteForceLoginProtection = false
@@ -184,7 +184,7 @@ func TestService_ValidateIPAddress(t *testing.T) {
}
}
func TestIPLoginAttempts(t *testing.T) {
func TestIntegrationIPLoginAttempts(t *testing.T) {
ctx := context.Background()
cfg := setting.NewCfg()
cfg.DisableIPAddressLoginProtection = false
@@ -61,7 +61,7 @@ func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestProvisioningApi(t *testing.T) {
func TestIntegrationProvisioningApi(t *testing.T) {
t.Run("policies", func(t *testing.T) {
t.Run("successful GET returns 200", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
@@ -1619,7 +1619,7 @@ func TestProvisioningApi(t *testing.T) {
})
}
func TestProvisioningApiContactPointExport(t *testing.T) {
func TestIntegrationProvisioningApiContactPointExport(t *testing.T) {
createTestEnv := func(t *testing.T, testConfig string) testEnvironment {
env := createTestEnv(t, testConfig)
env.ac = &recordingAccessControlFake{
@@ -63,7 +63,7 @@ func setupAMTest(t *testing.T) *alertmanager {
return am
}
func TestAlertmanager_newAlertmanager(t *testing.T) {
func TestIntegrationAlertmanager_newAlertmanager(t *testing.T) {
am := setupAMTest(t)
require.False(t, am.Ready())
}
@@ -33,7 +33,7 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func TestReceiverService_GetReceiver(t *testing.T) {
func TestIntegrationReceiverService_GetReceiver(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
@@ -61,7 +61,7 @@ func TestReceiverService_GetReceiver(t *testing.T) {
})
}
func TestReceiverService_GetReceivers(t *testing.T) {
func TestIntegrationReceiverService_GetReceivers(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
@@ -91,7 +91,7 @@ func TestReceiverService_GetReceivers(t *testing.T) {
})
}
func TestReceiverService_DecryptRedact(t *testing.T) {
func TestIntegrationReceiverService_DecryptRedact(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
@@ -57,7 +57,7 @@ var (
}
)
func TestTemplateDefaultData(t *testing.T) {
func TestIntegrationTemplateDefaultData(t *testing.T) {
am := setupAMTest(t)
tests := []struct {
@@ -41,7 +41,7 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
func TestAlertRuleService(t *testing.T) {
func TestIntegrationAlertRuleService(t *testing.T) {
ruleService := createAlertRuleService(t, nil)
var orgID int64 = 1
u := &user.SignedInUser{
@@ -754,7 +754,7 @@ func TestAlertRuleService(t *testing.T) {
})
}
func TestCreateAlertRule(t *testing.T) {
func TestIntegrationCreateAlertRule(t *testing.T) {
orgID := rand.Int63()
u := &user.SignedInUser{OrgID: orgID, UserUID: util.GenerateShortUID()}
groupKey := models.GenerateGroupKey(orgID)
@@ -1983,7 +1983,7 @@ func TestDeleteRuleGroups(t *testing.T) {
})
}
func TestProvisiongWithFullpath(t *testing.T) {
func TestIntegrationProvisiongWithFullpath(t *testing.T) {
tracer := tracing.InitializeTracerForTest()
inProcBus := bus.ProvideBus(tracer)
sqlStore, cfg := db.InitTestDBWithCfg(t)
@@ -35,7 +35,7 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func TestContactPointService(t *testing.T) {
func TestIntegrationContactPointService(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
@@ -360,7 +360,7 @@ func TestContactPointService(t *testing.T) {
})
}
func TestContactPointServiceDecryptRedact(t *testing.T) {
func TestIntegrationContactPointServiceDecryptRedact(t *testing.T) {
secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(db.InitTestDB(t)))
redactedUser := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
@@ -128,7 +128,7 @@ func TestNewAlertmanager(t *testing.T) {
}
}
func TestApplyConfig(t *testing.T) {
func TestIntegrationApplyConfig(t *testing.T) {
const tenantID = "test"
// errorHandler returns an error response for the readiness check and state sync.
errorHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+1 -1
View File
@@ -1066,7 +1066,7 @@ func TestStop_DrainingEnabled(t *testing.T) {
require.Equal(t, int64(2), alertsReceived.Load())
}
func TestApplyConfig(t *testing.T) {
func TestIntegrationApplyConfig(t *testing.T) {
targetURL := "alertmanager:9093"
targetGroup := &targetgroup.Group{
Targets: []model.LabelSet{
+5 -5
View File
@@ -46,7 +46,7 @@ func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestWarmStateCache(t *testing.T) {
func TestIntegrationWarmStateCache(t *testing.T) {
evaluationTime, err := time.Parse("2006-01-02", "2021-03-25")
require.NoError(t, err)
ctx := context.Background()
@@ -271,7 +271,7 @@ func TestWarmStateCache(t *testing.T) {
})
}
func TestDashboardAnnotations(t *testing.T) {
func TestIntegrationDashboardAnnotations(t *testing.T) {
evaluationTime, err := time.Parse("2006-01-02", "2022-01-01")
require.NoError(t, err)
@@ -1450,7 +1450,7 @@ func printAllAnnotations(annos map[int64]annotations.Item) string {
return b.String()
}
func TestStaleResultsHandler(t *testing.T) {
func TestIntegrationStaleResultsHandler(t *testing.T) {
evaluationTime := time.Now().Truncate(time.Second).UTC() // Truncate to the second since we don't store sub-second precision.
interval := time.Minute
@@ -1737,7 +1737,7 @@ func TestStaleResults(t *testing.T) {
})
}
func TestDeleteStateByRuleUID(t *testing.T) {
func TestIntegrationDeleteStateByRuleUID(t *testing.T) {
interval := time.Minute
ctx := context.Background()
ng, dbstore := tests.SetupTestEnv(t, 1)
@@ -1883,7 +1883,7 @@ func TestDeleteStateByRuleUID(t *testing.T) {
}
}
func TestResetStateByRuleUID(t *testing.T) {
func TestIntegrationResetStateByRuleUID(t *testing.T) {
interval := time.Minute
ctx := context.Background()
ng, dbstore := tests.SetupTestEnv(t, 1)
@@ -1447,7 +1447,7 @@ func TestIntegrationRuleGroupsCaseSensitive(t *testing.T) {
})
}
func TestIncreaseVersionForAllRulesInNamespaces(t *testing.T) {
func TestIntegrationIncreaseVersionForAllRulesInNamespaces(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
@@ -1496,7 +1496,7 @@ func TestIncreaseVersionForAllRulesInNamespaces(t *testing.T) {
})
}
func TestGetRuleVersions(t *testing.T) {
func TestIntegrationGetRuleVersions(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
func TestFetchOrgIds(t *testing.T) {
func TestIntegrationFetchOrgIds(t *testing.T) {
ctx := context.Background()
t.Run("returns empty result when no orgs exist", func(t *testing.T) {
@@ -145,7 +145,7 @@ func TestCanBeInstant(t *testing.T) {
}
}
func TestMigrateLokiQueryToInstant(t *testing.T) {
func TestIntegrationMigrateLokiQueryToInstant(t *testing.T) {
original := createMigrateableLokiRule(t)
migrated := createMigrateableLokiRule(t, func(r *models.AlertRule) {
r.Data[0] = lokiQuery(t, "A", "instant", "grafanacloud-logs")
@@ -168,7 +168,7 @@ func TestMigrateLokiQueryToInstant(t *testing.T) {
require.False(t, canBeOptimized)
}
func TestMigrateMultiLokiQueryToInstant(t *testing.T) {
func TestIntegrationMigrateMultiLokiQueryToInstant(t *testing.T) {
original := createMultiQueryMigratableLokiRule(t)
migrated := createMultiQueryMigratableLokiRule(t, func(r *models.AlertRule) {
r.Data[0] = lokiQuery(t, "TotalRequests", "instant", "grafanacloud-logs")
@@ -205,7 +205,7 @@ func TestMigrateMultiLokiQueryToInstant(t *testing.T) {
require.False(t, canBeOptimized)
}
func TestMigratePromQueryToInstant(t *testing.T) {
func TestIntegrationMigratePromQueryToInstant(t *testing.T) {
original := createMigratablePromRule(t)
migrated := createMigratablePromRule(t, func(r *models.AlertRule) {
r.Data[0] = prometheusQuery(t, "A", promExternalDS, promIsInstant)
@@ -226,7 +226,7 @@ func TestMigratePromQueryToInstant(t *testing.T) {
require.False(t, canBeOptimized)
}
func TestMigrateMultiPromQueryToInstant(t *testing.T) {
func TestIntegrationMigrateMultiPromQueryToInstant(t *testing.T) {
original := createMultiQueryMigratablePromRule(t)
migrated := createMultiQueryMigratablePromRule(t, func(r *models.AlertRule) {
r.Data[0] = prometheusQuery(t, "TotalRequests", promExternalDS, promIsInstant)
+2 -1
View File
@@ -3,6 +3,7 @@ package orgimpl
import (
"context"
"errors"
"fmt"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/db"
@@ -59,7 +60,7 @@ func (s *DeletionService) Delete(ctx context.Context, cmd *org.DeleteOrgCommand)
ctx, _ = identity.WithServiceIdentity(ctx, cmd.ID)
err = s.dashSvc.DeleteAllDashboards(ctx, cmd.ID)
if err != nil {
return err
return fmt.Errorf("failed to delete dashboards for org %d: %w", cmd.ID, err)
}
return s.store.Delete(ctx, cmd)
@@ -2,8 +2,13 @@ package clientmiddleware
import (
"context"
"fmt"
"strings"
"unicode/utf8"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/transform"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/query"
@@ -50,6 +55,9 @@ func (m *TracingHeaderMiddleware) applyHeaders(ctx context.Context, req backend.
if gotVal == "" {
continue
}
if !utf8.ValidString(gotVal) {
gotVal = sanitizeHTTPHeaderValueForGRPC(gotVal)
}
req.SetHTTPHeader(headerName, gotVal)
}
}
@@ -102,3 +110,28 @@ func (m *TracingHeaderMiddleware) RunStream(ctx context.Context, req *backend.Ru
m.applyHeaders(ctx, req)
return m.BaseHandler.RunStream(ctx, req, sender)
}
// sanitizeHTTPHeaderValueForGRPC sanitizes header values according to HTTP/2 gRPC specification.
// The spec defines that header values must consist of printable ASCII characters 0x20 (space) - 0x7E(tilde) inclusive.
// First attempts to decode any percent-encoded characters, then encodes invalid characters.
func sanitizeHTTPHeaderValueForGRPC(value string) string {
// First try to decode characters that were encoded by the frontend
decoder := charmap.ISO8859_1.NewDecoder()
decoded, _, err := transform.Bytes(decoder, []byte(value))
// If decoding fails, work with the original value
if err != nil {
decoded = []byte(value)
}
var sanitized strings.Builder
sanitized.Grow(len(decoded)) // Pre-allocate reasonable capacity
// Then encode invalid characters
for _, b := range decoded {
if b >= 0x20 && b <= 0x7E {
sanitized.WriteByte(b)
} else {
sanitized.WriteString(fmt.Sprintf("%%%02X", b))
}
}
return sanitized.String()
}
@@ -238,5 +238,115 @@ func TestTracingHeaderMiddleware(t *testing.T) {
require.Equal(t, `d26e337d-cb53-481a-9212-0112537b3c1a`, cdt.RunStreamReq.GetHTTPHeader(`X-Query-Group-Id`))
require.Equal(t, `true`, cdt.RunStreamReq.GetHTTPHeader(`X-Grafana-From-Expr`))
})
t.Run("sanitizes grpc header values for invalid utf-8", func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/some/thing", nil)
require.NoError(t, err)
// Create invalid UTF-8 strings
invalidUTF8Dashboard := string([]byte{'d', 'a', 's', 'h', 0xFF, 0xFE, 'u', 'i', 'd'})
invalidUTF8Panel := string([]byte{'p', 'a', 'n', 'e', 'l', 0x80, 'i', 'd'})
// Set headers with various characters that need to be sanitization
req.Header[`X-Dashboard-Title`] = []string{invalidUTF8Dashboard} // invalid UTF-8
req.Header[`X-Panel-Title`] = []string{invalidUTF8Panel} // invalid UTF-8
// Set headers that don't need sanitization
req.Header[`X-Dashboard-Uid`] = []string{"dashboard\x00uid"} // control character
req.Header[`X-Datasource-Uid`] = []string{"datasource\tuid"} // tab character
req.Header[`X-Query-Group-Id`] = []string{"valid-text-123"} // valid characters
req.Header[`X-Grafana-From-Expr`] = []string{"café résumé"} // extended characters
pluginCtx := backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
}
cdt := handlertest.NewHandlerMiddlewareTest(t,
WithReqContext(req, &user.SignedInUser{
IsAnonymous: true,
Login: "anonymous"},
),
handlertest.WithMiddlewares(NewTracingHeaderMiddleware()),
)
_, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{
PluginContext: pluginCtx,
Headers: map[string]string{},
})
require.NoError(t, err)
// Invalid UTF-8 should be sanitized
require.Equal(t, "dash%C3%BF%C3%BEuid", cdt.QueryDataReq.GetHTTPHeader(`X-Dashboard-Title`))
require.Equal(t, "panel%C2%80id", cdt.QueryDataReq.GetHTTPHeader(`X-Panel-Title`))
// Valid characters should remain unchanged
require.Equal(t, "valid-text-123", cdt.QueryDataReq.GetHTTPHeader(`X-Query-Group-Id`))
require.Equal(t, "café résumé", cdt.QueryDataReq.GetHTTPHeader(`X-Grafana-From-Expr`))
require.Equal(t, "dashboard\x00uid", cdt.QueryDataReq.GetHTTPHeader(`X-Dashboard-Uid`))
require.Equal(t, "datasource\tuid", cdt.QueryDataReq.GetHTTPHeader(`X-Datasource-Uid`))
})
})
}
func TestSanitizeHTTPHeaderValueForGRPC(t *testing.T) {
testCases := []struct {
name string
input string
expected string
}{
{
name: "Valid printable ASCII characters remain unchanged",
input: "Hello World! 123 @#$%^&*()",
expected: "Hello World! 123 @#$%^&*()",
},
{
name: "Extended characters remain unchanged",
// %C3%A9 is encoded é
input: "naiv%C3%A9",
expected: "naiv%C3%A9",
},
{
name: "naivé coming in as an iso8859-1 string",
input: string([]byte{110, 97, 105, 118, 233}),
expected: "naiv%C3%A9",
},
{
name: "Control characters are percent-encoded",
input: "hello\x00\x01\x1Fworld",
expected: "hello%00%01%1Fworld",
},
{
name: "Tab character is percent-encoded",
input: "hello\tworld",
expected: "hello%09world",
},
{
name: "Newline character is percent-encoded",
input: "hello\nworld",
expected: "hello%0Aworld",
},
{
name: "Carriage return is percent-encoded",
input: "hello\rworld",
expected: "hello%0Dworld",
},
{
name: "Mixed valid and invalid characters",
// %F0%9F%9A%80 is encoded 🚀
input: "Valid text\x00invalid\x1Fmore valid %F0%9F%9A%80",
expected: "Valid text%00invalid%1Fmore valid %F0%9F%9A%80",
},
{
name: "Empty string remains empty",
input: "",
expected: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := sanitizeHTTPHeaderValueForGRPC(tc.input)
require.Equal(t, tc.expected, result)
})
}
}
@@ -108,7 +108,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
})
}
func TestDashboardFileReader(t *testing.T) {
func TestIntegrationDashboardFileReader(t *testing.T) {
logger := log.New("test-logger")
cfg := &config{}
@@ -31,7 +31,7 @@ const (
twoDashboardsWithUID = "testdata/test-dashboards/two-dashboards-with-uid"
)
func TestDuplicatesValidator(t *testing.T) {
func TestIntegrationDuplicatesValidator(t *testing.T) {
fakeService := &dashboards.FakeDashboardProvisioning{}
defer fakeService.AssertExpectations(t)
@@ -682,7 +682,7 @@ func TestIntegrationDelete(t *testing.T) {
})
}
func TestDeleteByDashboardUIDs(t *testing.T) {
func TestIntegrationDeleteByDashboardUIDs(t *testing.T) {
var sqlStore db.DB
var cfg *setting.Cfg
var dashboardStore dashboards.Store
@@ -732,7 +732,7 @@ func TestDeleteByDashboardUIDs(t *testing.T) {
})
}
func TestGetMetrics(t *testing.T) {
func TestIntegrationGetMetrics(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
@@ -307,7 +307,7 @@ const (
}`
)
func TestGetQueryDataResponse(t *testing.T) {
func TestIntegrationGetQueryDataResponse(t *testing.T) {
fakeDashboardService := &dashboards.FakeDashboardService{}
service, sqlStore, _ := newPublicDashboardServiceImpl(t, nil, nil, nil, fakeDashboardService, nil)
fakeQueryService := &query.FakeQueryService{}
@@ -361,7 +361,7 @@ func TestGetQueryDataResponse(t *testing.T) {
})
}
func TestFindAnnotations(t *testing.T) {
func TestIntegrationFindAnnotations(t *testing.T) {
color := "red"
name := "annoName"
t.Run("service identity has correct permissions to get annotations dashboards and query datasources", func(t *testing.T) {
@@ -717,7 +717,7 @@ func TestFindAnnotations(t *testing.T) {
})
}
func TestGetMetricRequest(t *testing.T) {
func TestIntegrationGetMetricRequest(t *testing.T) {
service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, nil, nil, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
@@ -756,7 +756,7 @@ func TestGetMetricRequest(t *testing.T) {
})
}
func TestBuildMetricRequest(t *testing.T) {
func TestIntegrationBuildMetricRequest(t *testing.T) {
fakeDashboardService := &dashboards.FakeDashboardService{}
service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, nil, fakeDashboardService, nil)
@@ -60,7 +60,7 @@ func TestLogPrefix(t *testing.T) {
assert.Equal(t, LogPrefix, "publicdashboards.service")
}
func TestGetPublicDashboardForView(t *testing.T) {
func TestIntegrationGetPublicDashboardForView(t *testing.T) {
type storeResp struct {
pd *PublicDashboard
d *dashboards.Dashboard
@@ -452,7 +452,7 @@ func TestGetPublicDashboardForView(t *testing.T) {
}
}
func TestGetPublicDashboard(t *testing.T) {
func TestIntegrationGetPublicDashboard(t *testing.T) {
type storeResp struct {
pd *PublicDashboard
d *dashboards.Dashboard
@@ -529,7 +529,7 @@ func TestGetPublicDashboard(t *testing.T) {
}
}
func TestGetEnabledPublicDashboard(t *testing.T) {
func TestIntegrationGetEnabledPublicDashboard(t *testing.T) {
type storeResp struct {
pd *PublicDashboard
d *dashboards.Dashboard
@@ -594,7 +594,7 @@ func TestGetEnabledPublicDashboard(t *testing.T) {
// We're using sqlite here because testing all of the behaviors with mocks in
// the correct order is convoluted.
func TestCreatePublicDashboard(t *testing.T) {
func TestIntegrationCreatePublicDashboard(t *testing.T) {
t.Run("Create public dashboard", func(t *testing.T) {
fakeDashboardService := &dashboards.FakeDashboardService{}
service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, nil, fakeDashboardService, nil)
@@ -975,7 +975,7 @@ func assertFalseIfNull(t *testing.T, expectedValue bool, nullableValue *bool) {
}
}
func TestUpdatePublicDashboard(t *testing.T) {
func TestIntegrationUpdatePublicDashboard(t *testing.T) {
fakeDashboardService := &dashboards.FakeDashboardService{}
service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, nil, fakeDashboardService, nil)
@@ -1219,7 +1219,7 @@ func assertOldValueIfNull(t *testing.T, expectedValue bool, oldValue bool, nulla
}
}
func TestDeletePublicDashboard(t *testing.T) {
func TestIntegrationDeletePublicDashboard(t *testing.T) {
pubdash := &PublicDashboard{Uid: "2", OrgId: 1, DashboardUid: "uid"}
type mockFindResponse struct {
@@ -1389,7 +1389,7 @@ func TestDashboardEnabledChanged(t *testing.T) {
})
}
func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) {
func TestIntegrationPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) {
features := featuremgmt.WithFeatures()
testDB, cfg := db.InitTestDBWithCfg(t)
dashStore, err := dashboardsDB.ProvideDashboardStore(testDB, cfg, features, tagimpl.ProvideService(testDB))
+2 -2
View File
@@ -47,7 +47,7 @@ func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestParseMetricRequest(t *testing.T) {
func TestIntegrationParseMetricRequest(t *testing.T) {
t.Run("Test a simple single datasource query", func(t *testing.T) {
tc := setup(t)
mr := metricRequestWithQueries(t, `{
@@ -268,7 +268,7 @@ func TestParseMetricRequest(t *testing.T) {
})
}
func TestQueryDataMultipleSources(t *testing.T) {
func TestIntegrationQueryDataMultipleSources(t *testing.T) {
t.Run("can query multiple datasources", func(t *testing.T) {
tc := setup(t)
query1, err := simplejson.NewJson([]byte(`
@@ -44,7 +44,7 @@ func SetupTestDataSourceSecretMigrationService(t *testing.T, sqlStore db.DB, kvS
return migService
}
func TestMigrate(t *testing.T) {
func TestIntegrationMigrate(t *testing.T) {
t.Run("should migrate from legacy to unified with compatibility", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
kvStore := kvstore.ProvideService(sqlStore)
+1 -1
View File
@@ -30,7 +30,7 @@ func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestSecretsKVStoreSQL(t *testing.T) {
func TestIntegrationSecretsKVStoreSQL(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := manager.SetupTestService(t, fakes.NewFakeSecretsStore())
kv := NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
+6 -6
View File
@@ -29,7 +29,7 @@ func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestSecretsService_EnvelopeEncryption(t *testing.T) {
func TestIntegrationSecretsService_EnvelopeEncryption(t *testing.T) {
testDB := db.InitTestDB(t)
store := database.ProvideSecretsStore(testDB)
svc := SetupTestService(t, store)
@@ -90,7 +90,7 @@ func TestSecretsService_EnvelopeEncryption(t *testing.T) {
})
}
func TestSecretsService_DataKeys(t *testing.T) {
func TestIntegrationSecretsService_DataKeys(t *testing.T) {
testDB := db.InitTestDB(t)
store := database.ProvideSecretsStore(testDB)
ctx := context.Background()
@@ -168,7 +168,7 @@ func TestSecretsService_DataKeys(t *testing.T) {
})
}
func TestSecretsService_UseCurrentProvider(t *testing.T) {
func TestIntegrationSecretsService_UseCurrentProvider(t *testing.T) {
t.Run("When encryption_provider is not specified explicitly, should use 'secretKey' as a current provider", func(t *testing.T) {
testDB := db.InitTestDB(t)
svc := SetupTestService(t, database.ProvideSecretsStore(testDB))
@@ -273,7 +273,7 @@ func (f *fakeKMS) Provide() (map[secrets.ProviderID]secrets.Provider, error) {
return providers, nil
}
func TestSecretsService_Run(t *testing.T) {
func TestIntegrationSecretsService_Run(t *testing.T) {
ctx := context.Background()
testDB := db.InitTestDB(t)
store := database.ProvideSecretsStore(testDB)
@@ -323,7 +323,7 @@ func TestSecretsService_Run(t *testing.T) {
})
}
func TestSecretsService_ReEncryptDataKeys(t *testing.T) {
func TestIntegrationSecretsService_ReEncryptDataKeys(t *testing.T) {
ctx := context.Background()
testDB := db.InitTestDB(t)
store := database.ProvideSecretsStore(testDB)
@@ -370,7 +370,7 @@ func TestSecretsService_ReEncryptDataKeys(t *testing.T) {
})
}
func TestSecretsService_Decrypt(t *testing.T) {
func TestIntegrationSecretsService_Decrypt(t *testing.T) {
ctx := context.Background()
testDB := db.InitTestDB(t)
store := database.ProvideSecretsStore(testDB)
@@ -117,7 +117,7 @@ func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestProvideServiceAccount_DeleteServiceAccount(t *testing.T) {
func TestIntegrationProvideServiceAccount_DeleteServiceAccount(t *testing.T) {
storeMock := newServiceAccountStoreFake()
acSvc := actest.FakeService{}
pSvc := &actest.FakePermissionsService{}
@@ -17,7 +17,7 @@ func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestShortURLService(t *testing.T) {
func TestIntegrationShortURLService(t *testing.T) {
user := &user.SignedInUser{UserID: 1}
store := db.InitTestDB(t)
@@ -44,4 +44,9 @@ func addLoginAttemptMigrations(mg *Migrator) {
mg.AddMigration("increase login_attempt.ip_address column length for IPv6 support", NewRawSQLMigration("").
Postgres("ALTER TABLE login_attempt ALTER COLUMN ip_address TYPE VARCHAR(50);").
Mysql("ALTER TABLE login_attempt MODIFY ip_address VARCHAR(50);"))
// Use bigint for "created" column to store timestamps (INT is too small)
mg.AddMigration("alter table login_attempt alter column created type to bigint", NewRawSQLMigration("").
Mysql("ALTER TABLE login_attempt MODIFY created BIGINT;").
Postgres("ALTER TABLE login_attempt ALTER COLUMN created TYPE BIGINT;"))
}
@@ -300,6 +300,10 @@ func addAlertRuleMigrations(mg *migrator.Migrator, defaultIntervalSeconds int64)
mg.AddMigration("fix is_paused column for alert_rule table", migrator.NewRawSQLMigration("").
Postgres(`ALTER TABLE alert_rule ALTER COLUMN is_paused SET DEFAULT false;
UPDATE alert_rule SET is_paused = false;`))
mg.AddMigration("alter table alert_rule alter column rule_group_idx type to bigint", migrator.NewRawSQLMigration("").
Mysql("ALTER TABLE alert_rule MODIFY rule_group_idx BIGINT;").
Postgres("ALTER TABLE alert_rule ALTER COLUMN rule_group_idx TYPE BIGINT;"))
}
var alertRuleVersionUDX_OrgIdRuleUIDVersion = &migrator.Index{Cols: []string{"rule_org_id", "rule_uid", "version"}, Type: migrator.UniqueIndex}
@@ -371,6 +375,10 @@ func addAlertRuleVersionMigrations(mg *migrator.Migrator) {
mg.AddMigration("fix is_paused column for alert_rule_version table", migrator.NewRawSQLMigration("").
Postgres(`ALTER TABLE alert_rule_version ALTER COLUMN is_paused SET DEFAULT false;
UPDATE alert_rule_version SET is_paused = false;`))
mg.AddMigration("alter table alert_rule_version alter column rule_group_idx type to bigint", migrator.NewRawSQLMigration("").
Mysql("ALTER TABLE alert_rule_version MODIFY rule_group_idx BIGINT;").
Postgres("ALTER TABLE alert_rule_version ALTER COLUMN rule_group_idx TYPE BIGINT;"))
}
func addAlertmanagerConfigMigrations(mg *migrator.Migrator) {
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestFolderUIDFilter(t *testing.T) {
func TestIntegrationFolderUIDFilter(t *testing.T) {
testCases := []struct {
description string
uids []string
@@ -31,7 +31,7 @@ func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestBuilder_EqualResults_Basic(t *testing.T) {
func TestIntegrationBuilder_EqualResults_Basic(t *testing.T) {
user := &user.SignedInUser{
UserID: 1,
OrgID: 1,
@@ -75,7 +75,7 @@ func TestBuilder_EqualResults_Basic(t *testing.T) {
}, res)
}
func TestBuilder_Pagination(t *testing.T) {
func TestIntegrationBuilder_Pagination(t *testing.T) {
user := &user.SignedInUser{
UserID: 1,
OrgID: 1,
@@ -122,7 +122,7 @@ func TestBuilder_Pagination(t *testing.T) {
assert.Equal(t, "P", resPg2[0].Title, "page 2 should start with the 16th dashboard")
}
func TestBuilder_RBAC(t *testing.T) {
func TestIntegrationBuilder_RBAC(t *testing.T) {
testsCases := []struct {
desc string
userPermissions []accesscontrol.Permission
+14 -14
View File
@@ -73,7 +73,7 @@ func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestListFiles(t *testing.T) {
func TestIntegrationListFiles(t *testing.T) {
roots := []storageRuntime{publicStaticFilesStorage}
store := newStandardStorageService(db.InitTestDB(t), roots, func(orgId int64) []storageRuntime {
@@ -94,7 +94,7 @@ func TestListFiles(t *testing.T) {
experimental.CheckGoldenJSONFrame(t, "testdata", "public_testdata_js_libraries.golden", testDsFrame, true)
}
func TestListFilesWithoutPermissions(t *testing.T) {
func TestIntegrationListFilesWithoutPermissions(t *testing.T) {
roots := []storageRuntime{publicStaticFilesStorage}
store := newStandardStorageService(db.InitTestDB(t), roots, func(orgId int64) []storageRuntime {
@@ -128,7 +128,7 @@ func setupUploadStore(t *testing.T, authService storageAuthService) (StorageServ
return store, mockStorage, storageName
}
func TestShouldUploadWhenNoFileAlreadyExists(t *testing.T) {
func TestIntegrationShouldUploadWhenNoFileAlreadyExists(t *testing.T) {
service, mockStorage, storageName := setupUploadStore(t, nil)
fileName := "/myFile.jpg"
@@ -147,7 +147,7 @@ func TestShouldUploadWhenNoFileAlreadyExists(t *testing.T) {
require.NoError(t, err)
}
func TestShouldFailUploadWithoutAccess(t *testing.T) {
func TestIntegrationShouldFailUploadWithoutAccess(t *testing.T) {
service, _, storageName := setupUploadStore(t, denyAllAuthService)
err := service.Upload(context.Background(), dummyUser, &UploadRequest{
@@ -158,7 +158,7 @@ func TestShouldFailUploadWithoutAccess(t *testing.T) {
require.ErrorIs(t, err, ErrAccessDenied)
}
func TestShouldFailUploadWhenFileAlreadyExists(t *testing.T) {
func TestIntegrationShouldFailUploadWhenFileAlreadyExists(t *testing.T) {
service, mockStorage, storageName := setupUploadStore(t, nil)
mockStorage.On("Get", mock.Anything, "/myFile.jpg", &filestorage.GetFileOptions{WithContents: false}).Return(&filestorage.File{Contents: make([]byte, 0)}, true, nil)
@@ -171,7 +171,7 @@ func TestShouldFailUploadWhenFileAlreadyExists(t *testing.T) {
require.ErrorIs(t, err, ErrFileAlreadyExists)
}
func TestShouldDelegateFileDeletion(t *testing.T) {
func TestIntegrationShouldDelegateFileDeletion(t *testing.T) {
service, mockStorage, storageName := setupUploadStore(t, nil)
mockStorage.On("Delete", mock.Anything, "/myFile.jpg").Return(nil)
@@ -180,7 +180,7 @@ func TestShouldDelegateFileDeletion(t *testing.T) {
require.NoError(t, err)
}
func TestShouldDelegateFolderCreation(t *testing.T) {
func TestIntegrationShouldDelegateFolderCreation(t *testing.T) {
service, mockStorage, storageName := setupUploadStore(t, nil)
mockStorage.On("CreateFolder", mock.Anything, "/nestedFolder/mostNestedFolder").Return(nil)
@@ -189,7 +189,7 @@ func TestShouldDelegateFolderCreation(t *testing.T) {
require.NoError(t, err)
}
func TestShouldDelegateFolderDeletion(t *testing.T) {
func TestIntegrationShouldDelegateFolderDeletion(t *testing.T) {
service, mockStorage, storageName := setupUploadStore(t, nil)
cmds := []*DeleteFolderCmd{
{
@@ -213,7 +213,7 @@ func TestShouldDelegateFolderDeletion(t *testing.T) {
}
}
func TestShouldUploadSvg(t *testing.T) {
func TestIntegrationShouldUploadSvg(t *testing.T) {
service, mockStorage, storageName := setupUploadStore(t, nil)
fileName := "/myFile.svg"
@@ -232,7 +232,7 @@ func TestShouldUploadSvg(t *testing.T) {
require.NoError(t, err)
}
func TestShouldNotUploadHtmlDisguisedAsSvg(t *testing.T) {
func TestIntegrationShouldNotUploadHtmlDisguisedAsSvg(t *testing.T) {
service, mockStorage, storageName := setupUploadStore(t, nil)
fileName := "/myFile.svg"
@@ -246,7 +246,7 @@ func TestShouldNotUploadHtmlDisguisedAsSvg(t *testing.T) {
require.ErrorIs(t, err, ErrValidationFailed)
}
func TestShouldNotUploadJpgDisguisedAsSvg(t *testing.T) {
func TestIntegrationShouldNotUploadJpgDisguisedAsSvg(t *testing.T) {
service, mockStorage, storageName := setupUploadStore(t, nil)
fileName := "/myFile.svg"
@@ -260,7 +260,7 @@ func TestShouldNotUploadJpgDisguisedAsSvg(t *testing.T) {
require.ErrorIs(t, err, ErrValidationFailed)
}
func TestSetupWithNonUniqueStoragePrefixes(t *testing.T) {
func TestIntegrationSetupWithNonUniqueStoragePrefixes(t *testing.T) {
prefix := "resources"
sqlStorage := newSQLStorage(RootStorageMeta{}, prefix, "Testing upload", "dummy descr", &StorageSQLConfig{}, db.InitTestDB(t), 1, false)
sqlStorage2 := newSQLStorage(RootStorageMeta{}, prefix, "Testing upload", "dummy descr", &StorageSQLConfig{}, db.InitTestDB(t), 1, false)
@@ -276,7 +276,7 @@ func TestSetupWithNonUniqueStoragePrefixes(t *testing.T) {
}, allowAllAuthService, cfg, nil)
}
func TestContentRootWithNestedStorage(t *testing.T) {
func TestIntegrationContentRootWithNestedStorage(t *testing.T) {
globalOrgID := int64(accesscontrol.GlobalOrgID)
testDB := db.InitTestDB(t)
orgedUser := &user.SignedInUser{OrgID: 1}
@@ -522,7 +522,7 @@ func TestContentRootWithNestedStorage(t *testing.T) {
}
}
func TestShadowingExistingFolderByNestedContentRoot(t *testing.T) {
func TestIntegrationShadowingExistingFolderByNestedContentRoot(t *testing.T) {
db := db.InitTestDB(t)
ctx := context.Background()
nestedStorage := newSQLStorage(RootStorageMeta{}, "nested", "Testing upload", "dummy descr", &StorageSQLConfig{}, db, accesscontrol.GlobalOrgID, true)
+1
View File
@@ -15,6 +15,7 @@ func (f *HelpFlags1) AddFlag(flag HelpFlags1) { *f |= flag }
const (
HelpFlagGettingStartedPanelDismissed HelpFlags1 = 1 << iota
HelpFlagDashboardHelp1
HelpFlagEnterpriseAuth1
)
type UpdateEmailActionType string
+15 -7
View File
@@ -274,7 +274,10 @@ func (ss *sqlStore) Update(ctx context.Context, cmd *user.UpdateUserCommand) err
q = q.UseBool("is_admin")
usr.IsAdmin = v
})
setOptional(cmd.HelpFlags1, func(v user.HelpFlags1) { usr.HelpFlags1 = *cmd.HelpFlags1 })
setOptional(cmd.HelpFlags1, func(v user.HelpFlags1) {
q = q.MustCols("help_flags1")
usr.HelpFlags1 = *cmd.HelpFlags1
})
if _, err := q.Update(&usr); err != nil {
return err
@@ -420,12 +423,17 @@ func (ss *sqlStore) Count(ctx context.Context) (int64, error) {
func (ss *sqlStore) CountUserAccountsWithEmptyRole(ctx context.Context) (int64, error) {
sb := &db.SQLBuilder{}
sb.Write("SELECT ")
sb.Write(`(SELECT COUNT (*) from ` + ss.dialect.Quote("org_user") + ` AS ou ` +
`LEFT JOIN ` + ss.dialect.Quote("user") + ` AS u ON u.id = ou.user_id ` +
`WHERE ou.role =? ` +
`AND u.is_service_account = ` + ss.dialect.BooleanStr(false) + ` ` +
`AND u.is_disabled = ` + ss.dialect.BooleanStr(false) + `) AS user_accounts_with_no_role`)
sb.Write(`
SELECT sub.user_accounts_with_no_role
FROM (
SELECT COUNT(*) AS user_accounts_with_no_role
FROM ` + ss.dialect.Quote("org_user") + ` AS ou
LEFT JOIN ` + ss.dialect.Quote("user") + ` AS u ON u.id = ou.user_id
WHERE ou.role = ?
AND u.is_service_account = ` + ss.dialect.BooleanStr(false) + `
AND u.is_disabled = ` + ss.dialect.BooleanStr(false) + `
) AS sub
`)
sb.AddParams("None")
var countStats int64
+1 -1
View File
@@ -1021,7 +1021,7 @@ func createFiveTestUsers(t *testing.T, svc user.Service, fn func(i int) *user.Cr
return users
}
func TestMetricsUsage(t *testing.T) {
func TestIntegrationMetricsUsage(t *testing.T) {
ss, cfg := db.InitTestDBWithCfg(t)
userStore := ProvideStore(ss, setting.NewCfg())
quotaService := quotaimpl.ProvideService(ss, cfg)