Merge remote-tracking branch 'origin/main' into ds-apiserver-with-configs
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -15,6 +15,7 @@ func (f *HelpFlags1) AddFlag(flag HelpFlags1) { *f |= flag }
|
||||
const (
|
||||
HelpFlagGettingStartedPanelDismissed HelpFlags1 = 1 << iota
|
||||
HelpFlagDashboardHelp1
|
||||
HelpFlagEnterpriseAuth1
|
||||
)
|
||||
|
||||
type UpdateEmailActionType string
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user