From e6a5b9ee7f517c3e59c182a310e88270bc0e70dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 16 Jul 2022 17:44:16 +0200 Subject: [PATCH 1/9] TopNav: Store collapse state for chrome top search bar in local storage (#52300) --- .../core/components/AppChrome/AppChrome.tsx | 19 +++++++++---------- .../components/AppChrome/AppChromeService.tsx | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/public/app/core/components/AppChrome/AppChrome.tsx b/public/app/core/components/AppChrome/AppChrome.tsx index cfa37b4e79d..a9c94b907d1 100644 --- a/public/app/core/components/AppChrome/AppChrome.tsx +++ b/public/app/core/components/AppChrome/AppChrome.tsx @@ -1,6 +1,5 @@ import { css, cx } from '@emotion/css'; -import React, { PropsWithChildren, useState } from 'react'; -import { useToggle } from 'react-use'; +import React, { PropsWithChildren } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { config } from '@grafana/runtime'; @@ -17,8 +16,6 @@ export interface Props extends PropsWithChildren<{}> {} export function AppChrome({ children }: Props) { const styles = useStyles2(getStyles); - const [searchBarHidden, toggleSearchBar] = useToggle(false); // repace with local storage - const [megaMenuOpen, setMegaMenuOpen] = useState(false); const state = appChromeService.useState(); if (state.chromeless || !config.featureToggles.topnav) { @@ -28,18 +25,20 @@ export function AppChrome({ children }: Props) { return (
- {!searchBarHidden && } + {!state.searchBarHidden && } setMegaMenuOpen(!megaMenuOpen)} + onToggleSearchBar={appChromeService.toggleSearchBar} + onToggleMegaMenu={appChromeService.toggleMegaMenu} />
-
{children}
- {megaMenuOpen && setMegaMenuOpen(false)} />} +
{children}
+ {state.megaMenuOpen && ( + + )}
); } diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx index 0c113f7701d..bf6c0c031b6 100644 --- a/public/app/core/components/AppChrome/AppChromeService.tsx +++ b/public/app/core/components/AppChrome/AppChromeService.tsx @@ -2,6 +2,7 @@ import { useObservable } from 'react-use'; import { BehaviorSubject } from 'rxjs'; import { NavModelItem } from '@grafana/data'; +import store from 'app/core/store'; import { isShallowEqual } from 'app/core/utils/isShallowEqual'; import { RouteDescriptor } from '../../navigation/types'; @@ -11,14 +12,19 @@ export interface AppChromeState { sectionNav: NavModelItem; pageNav?: NavModelItem; actions?: React.ReactNode; + searchBarHidden?: boolean; + megaMenuOpen?: boolean; } const defaultSection: NavModelItem = { text: 'Grafana' }; export class AppChromeService { + searchBarStorageKey = 'SearchBar_Hidden'; + readonly state = new BehaviorSubject({ chromeless: true, // start out hidden to not flash it on pages without chrome sectionNav: defaultSection, + searchBarHidden: store.getBool(this.searchBarStorageKey, false), }); routeMounted(route: RouteDescriptor) { @@ -42,6 +48,16 @@ export class AppChromeService { } } + toggleMegaMenu = () => { + this.update({ megaMenuOpen: !this.state.getValue().megaMenuOpen }); + }; + + toggleSearchBar = () => { + const searchBarHidden = !this.state.getValue().searchBarHidden; + store.set(this.searchBarStorageKey, searchBarHidden); + this.update({ searchBarHidden }); + }; + useState() { // eslint-disable-next-line react-hooks/rules-of-hooks return useObservable(this.state, this.state.getValue()); From 6188526e1d2a841c633fa0f18f24a2fc00bb4607 Mon Sep 17 00:00:00 2001 From: Artur Wierzbicki Date: Sun, 17 Jul 2022 22:41:54 +0400 Subject: [PATCH 2/9] Storage: use static access rules (#52334) * Storage: use static access rules * Storage: use static access rules * Storage: add tests --- pkg/services/store/file_guardian.go | 121 ++++++++++++++++++++++++++++ pkg/services/store/http.go | 5 +- pkg/services/store/service.go | 104 ++++++++++++++++++------ pkg/services/store/service_test.go | 90 +++++++++++++++------ pkg/services/store/static_auth.go | 41 ++++++++++ pkg/services/store/tree.go | 3 +- pkg/services/store/types.go | 2 +- pkg/services/store/utils.go | 5 ++ 8 files changed, 319 insertions(+), 52 deletions(-) create mode 100644 pkg/services/store/file_guardian.go create mode 100644 pkg/services/store/static_auth.go diff --git a/pkg/services/store/file_guardian.go b/pkg/services/store/file_guardian.go new file mode 100644 index 00000000000..56a1d58c42e --- /dev/null +++ b/pkg/services/store/file_guardian.go @@ -0,0 +1,121 @@ +package store + +import ( + "context" + "strings" + + "github.com/grafana/grafana/pkg/infra/filestorage" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models" +) + +const ( + ActionFilesRead = "files:read" + ActionFilesWrite = "files:write" + ActionFilesDelete = "files:delete" +) + +var ( + denyAllPathFilter = filestorage.NewDenyAllPathFilter() + allowAllPathFilter = filestorage.NewAllowAllPathFilter() +) + +func isValidAction(action string) bool { + return action == ActionFilesRead || action == ActionFilesWrite || action == ActionFilesDelete +} + +type storageAuthService interface { + newGuardian(ctx context.Context, user *models.SignedInUser, prefix string) fileGuardian +} + +type fileGuardian interface { + canView(path string) bool + canWrite(path string) bool + canDelete(path string) bool + can(action string, path string) bool + + getPathFilter(action string) filestorage.PathFilter +} + +type pathFilterFileGuardian struct { + ctx context.Context + user *models.SignedInUser + prefix string + pathFilterByAction map[string]filestorage.PathFilter + log log.Logger +} + +func (a *pathFilterFileGuardian) getPathFilter(action string) filestorage.PathFilter { + if !isValidAction(action) { + a.log.Warn("Unsupported action", "action", action) + return denyAllPathFilter + } + + if filter, ok := a.pathFilterByAction[action]; ok { + return filter + } + + return denyAllPathFilter +} + +func (a *pathFilterFileGuardian) canWrite(path string) bool { + return a.can(ActionFilesWrite, path) +} + +func (a *pathFilterFileGuardian) canView(path string) bool { + return a.can(ActionFilesRead, path) +} + +func (a *pathFilterFileGuardian) canDelete(path string) bool { + return a.can(ActionFilesDelete, path) +} + +func (a *pathFilterFileGuardian) can(action string, path string) bool { + if path == a.prefix { + path = filestorage.Delimiter + } else { + path = strings.TrimPrefix(path, a.prefix) + } + allow := false + + if !isValidAction(action) { + a.log.Warn("Unsupported action", "action", action, "path", path) + return false + } + + pathFilter, ok := a.pathFilterByAction[action] + + if !ok { + a.log.Warn("Missing path filter", "action", action, "path", path) + return false + } + + allow = pathFilter.IsAllowed(path) + if !allow { + a.log.Warn("denying", "action", action, "path", path) + } + return allow +} + +type denyAllFileGuardian struct { +} + +func (d denyAllFileGuardian) canView(path string) bool { + return d.can(ActionFilesRead, path) +} + +func (d denyAllFileGuardian) canWrite(path string) bool { + return d.can(ActionFilesWrite, path) +} + +func (d denyAllFileGuardian) canDelete(path string) bool { + return d.can(ActionFilesDelete, path) +} + +func (d denyAllFileGuardian) can(action string, path string) bool { + return false +} + +func (d denyAllFileGuardian) getPathFilter(action string) filestorage.PathFilter { + return denyAllPathFilter +} diff --git a/pkg/services/store/http.go b/pkg/services/store/http.go index 43f2f69c8c3..9f207d9a7a1 100644 --- a/pkg/services/store/http.go +++ b/pkg/services/store/http.go @@ -37,7 +37,7 @@ func ProvideHTTPService(store StorageService) HTTPStorageService { func UploadErrorToStatusCode(err error) int { switch { - case errors.Is(err, ErrUploadFeatureDisabled): + case errors.Is(err, ErrStorageNotFound): return 404 case errors.Is(err, ErrUnsupportedStorage): @@ -49,6 +49,9 @@ func UploadErrorToStatusCode(err error) int { case errors.Is(err, ErrFileAlreadyExists): return 400 + case errors.Is(err, ErrAccessDenied): + return 403 + default: return 500 } diff --git a/pkg/services/store/service.go b/pkg/services/store/service.go index 3713843f11f..50ab7da657d 100644 --- a/pkg/services/store/service.go +++ b/pkg/services/store/service.go @@ -3,7 +3,6 @@ package store import ( "context" "errors" - "fmt" "os" "path/filepath" @@ -19,13 +18,16 @@ import ( var grafanaStorageLogger = log.New("grafanaStorageLogger") -var ErrUploadFeatureDisabled = errors.New("upload feature is disabled") var ErrUnsupportedStorage = errors.New("storage does not support this operation") var ErrUploadInternalError = errors.New("upload internal error") var ErrValidationFailed = errors.New("request validation failed") var ErrFileAlreadyExists = errors.New("file exists") +var ErrStorageNotFound = errors.New("storage not found") +var ErrAccessDenied = errors.New("access denied") const RootPublicStatic = "public-static" +const RootResources = "resources" +const RootDevenv = "devenv" const MAX_UPLOAD_SIZE = 1 * 1024 * 1024 // 3MB @@ -66,9 +68,10 @@ type storageServiceConfig struct { } type standardStorageService struct { - sql *sqlstore.SQLStore - tree *nestedTree - cfg storageServiceConfig + sql *sqlstore.SQLStore + tree *nestedTree + cfg storageServiceConfig + authService storageAuthService } func ProvideService(sql *sqlstore.SQLStore, features featuremgmt.FeatureToggles, cfg *setting.Cfg) StorageService { @@ -90,7 +93,7 @@ func ProvideService(sql *sqlstore.SQLStore, features featuremgmt.FeatureToggles, devenv := filepath.Join(cfg.StaticRootPath, "..", "devenv") if _, err := os.Stat(devenv); !os.IsNotExist(err) { // path/to/whatever exists - s := newDiskStorage("devenv", "Development Environment", &StorageLocalDiskConfig{ + s := newDiskStorage(RootDevenv, "Development Environment", &StorageLocalDiskConfig{ Path: devenv, Roots: []string{ "/dev-dashboards/", @@ -104,7 +107,7 @@ func ProvideService(sql *sqlstore.SQLStore, features featuremgmt.FeatureToggles, storages := make([]storageRuntime, 0) if features.IsEnabled(featuremgmt.FlagStorageLocalUpload) { storages = append(storages, - newSQLStorage("resources", + newSQLStorage(RootResources, "Resources", &StorageSQLConfig{orgId: orgId}, sql). setBuiltin(true). @@ -114,10 +117,39 @@ func ProvideService(sql *sqlstore.SQLStore, features featuremgmt.FeatureToggles, return storages } - return newStandardStorageService(sql, globalRoots, initializeOrgStorages) + authService := newStaticStorageAuthService(func(ctx context.Context, user *models.SignedInUser, storageName string) map[string]filestorage.PathFilter { + if user == nil || !user.IsGrafanaAdmin { + return nil + } + + switch storageName { + case RootPublicStatic: + return map[string]filestorage.PathFilter{ + ActionFilesRead: allowAllPathFilter, + ActionFilesWrite: denyAllPathFilter, + ActionFilesDelete: denyAllPathFilter, + } + case RootDevenv: + return map[string]filestorage.PathFilter{ + ActionFilesRead: allowAllPathFilter, + ActionFilesWrite: denyAllPathFilter, + ActionFilesDelete: denyAllPathFilter, + } + case RootResources: + return map[string]filestorage.PathFilter{ + ActionFilesRead: allowAllPathFilter, + ActionFilesWrite: allowAllPathFilter, + ActionFilesDelete: allowAllPathFilter, + } + default: + return nil + } + }) + + return newStandardStorageService(sql, globalRoots, initializeOrgStorages, authService) } -func newStandardStorageService(sql *sqlstore.SQLStore, globalRoots []storageRuntime, initializeOrgStorages func(orgId int64) []storageRuntime) *standardStorageService { +func newStandardStorageService(sql *sqlstore.SQLStore, globalRoots []storageRuntime, initializeOrgStorages func(orgId int64) []storageRuntime, authService storageAuthService) *standardStorageService { rootsByOrgId := make(map[int64][]storageRuntime) rootsByOrgId[ac.GlobalOrgID] = globalRoots @@ -127,8 +159,9 @@ func newStandardStorageService(sql *sqlstore.SQLStore, globalRoots []storageRunt } res.init() return &standardStorageService{ - sql: sql, - tree: res, + sql: sql, + tree: res, + authService: authService, cfg: storageServiceConfig{ allowUnsanitizedSvgUpload: false, }, @@ -149,12 +182,15 @@ func getOrgId(user *models.SignedInUser) int64 { } func (s *standardStorageService) List(ctx context.Context, user *models.SignedInUser, path string) (*StorageListFrame, error) { - // apply access control here - return s.tree.ListFolder(ctx, getOrgId(user), path) + guardian := s.authService.newGuardian(ctx, user, getFirstSegment(path)) + return s.tree.ListFolder(ctx, getOrgId(user), path, guardian.getPathFilter(ActionFilesRead)) } func (s *standardStorageService) Read(ctx context.Context, user *models.SignedInUser, path string) (*filestorage.File, error) { - // TODO: permission check! + guardian := s.authService.newGuardian(ctx, user, getFirstSegment(path)) + if !guardian.canView(path) { + return nil, ErrAccessDenied + } return s.tree.GetFile(ctx, getOrgId(user), path) } @@ -171,12 +207,17 @@ type UploadRequest struct { } func (s *standardStorageService) Upload(ctx context.Context, user *models.SignedInUser, req *UploadRequest) error { - upload, storagePath := s.tree.getRoot(getOrgId(user), req.Path) - if upload == nil { - return ErrUploadFeatureDisabled + guardian := s.authService.newGuardian(ctx, user, getFirstSegment(req.Path)) + if !guardian.canWrite(req.Path) { + return ErrAccessDenied } - if upload.Meta().ReadOnly { + root, storagePath := s.tree.getRoot(getOrgId(user), req.Path) + if root == nil { + return ErrStorageNotFound + } + + if root.Meta().ReadOnly { return ErrUnsupportedStorage } @@ -195,7 +236,7 @@ func (s *standardStorageService) Upload(ctx context.Context, user *models.Signed grafanaStorageLogger.Info("uploading a file", "filetype", req.MimeType, "path", req.Path) if !req.OverwriteExistingFile { - file, err := upload.Store().Get(ctx, storagePath) + file, err := root.Store().Get(ctx, storagePath) if err != nil { grafanaStorageLogger.Error("failed while checking file existence", "err", err, "path", req.Path) return ErrUploadInternalError @@ -206,7 +247,7 @@ func (s *standardStorageService) Upload(ctx context.Context, user *models.Signed } } - if err := upload.Store().Upsert(ctx, upsertCommand); err != nil { + if err := root.Store().Upsert(ctx, upsertCommand); err != nil { grafanaStorageLogger.Error("failed while uploading the file", "err", err, "path", req.Path) return ErrUploadInternalError } @@ -215,9 +256,14 @@ func (s *standardStorageService) Upload(ctx context.Context, user *models.Signed } func (s *standardStorageService) DeleteFolder(ctx context.Context, user *models.SignedInUser, cmd *DeleteFolderCmd) error { + guardian := s.authService.newGuardian(ctx, user, getFirstSegment(cmd.Path)) + if !guardian.canDelete(cmd.Path) { + return ErrAccessDenied + } + root, storagePath := s.tree.getRoot(getOrgId(user), cmd.Path) if root == nil { - return fmt.Errorf("resources storage is not enabled") + return ErrStorageNotFound } if root.Meta().ReadOnly { @@ -227,13 +273,18 @@ func (s *standardStorageService) DeleteFolder(ctx context.Context, user *models. if storagePath == "" { storagePath = filestorage.Delimiter } - return root.Store().DeleteFolder(ctx, storagePath, &filestorage.DeleteFolderOptions{Force: true}) + return root.Store().DeleteFolder(ctx, storagePath, &filestorage.DeleteFolderOptions{Force: true, AccessFilter: guardian.getPathFilter(ActionFilesDelete)}) } func (s *standardStorageService) CreateFolder(ctx context.Context, user *models.SignedInUser, cmd *CreateFolderCmd) error { + guardian := s.authService.newGuardian(ctx, user, getFirstSegment(cmd.Path)) + if !guardian.canWrite(cmd.Path) { + return ErrAccessDenied + } + root, storagePath := s.tree.getRoot(getOrgId(user), cmd.Path) if root == nil { - return fmt.Errorf("resources storage is not enabled") + return ErrStorageNotFound } if root.Meta().ReadOnly { @@ -248,9 +299,14 @@ func (s *standardStorageService) CreateFolder(ctx context.Context, user *models. } func (s *standardStorageService) Delete(ctx context.Context, user *models.SignedInUser, path string) error { + guardian := s.authService.newGuardian(ctx, user, getFirstSegment(path)) + if !guardian.canDelete(path) { + return ErrAccessDenied + } + root, storagePath := s.tree.getRoot(getOrgId(user), path) if root == nil { - return fmt.Errorf("resources storage is not enabled") + return ErrStorageNotFound } if root.Meta().ReadOnly { diff --git a/pkg/services/store/service_test.go b/pkg/services/store/service_test.go index 9c705621512..597d6a6bf93 100644 --- a/pkg/services/store/service_test.go +++ b/pkg/services/store/service_test.go @@ -16,29 +16,41 @@ import ( ) var ( - dummyUser = &models.SignedInUser{OrgId: 1} + dummyUser = &models.SignedInUser{OrgId: 1} + allowAllAuthService = newStaticStorageAuthService(func(ctx context.Context, user *models.SignedInUser, storageName string) map[string]filestorage.PathFilter { + return map[string]filestorage.PathFilter{ + ActionFilesDelete: allowAllPathFilter, + ActionFilesWrite: allowAllPathFilter, + ActionFilesRead: allowAllPathFilter, + } + }) + denyAllAuthService = newStaticStorageAuthService(func(ctx context.Context, user *models.SignedInUser, storageName string) map[string]filestorage.PathFilter { + return map[string]filestorage.PathFilter{ + ActionFilesDelete: denyAllPathFilter, + ActionFilesWrite: denyAllPathFilter, + ActionFilesRead: denyAllPathFilter, + } + }) + publicRoot, _ = filepath.Abs("../../../public") + publicStaticFilesStorage = newDiskStorage("public", "Public static files", &StorageLocalDiskConfig{ + Path: publicRoot, + Roots: []string{ + "/testdata/", + "/img/icons/", + "/img/bg/", + "/gazetteer/", + "/maps/", + "/upload/", + }, + }).setReadOnly(true).setBuiltin(true) ) func TestListFiles(t *testing.T) { - publicRoot, err := filepath.Abs("../../../public") - require.NoError(t, err) - roots := []storageRuntime{ - newDiskStorage("public", "Public static files", &StorageLocalDiskConfig{ - Path: publicRoot, - Roots: []string{ - "/testdata/", - "/img/icons/", - "/img/bg/", - "/gazetteer/", - "/maps/", - "/upload/", - }, - }).setReadOnly(true).setBuiltin(true), - } + roots := []storageRuntime{publicStaticFilesStorage} store := newStandardStorageService(sqlstore.InitTestDB(t), roots, func(orgId int64) []storageRuntime { return make([]storageRuntime, 0) - }) + }, allowAllAuthService) frame, err := store.List(context.Background(), dummyUser, "public/testdata") require.NoError(t, err) @@ -53,22 +65,38 @@ func TestListFiles(t *testing.T) { experimental.CheckGoldenJSONFrame(t, "testdata", "public_testdata_js_libraries.golden", testDsFrame, true) } -func setupUploadStore(t *testing.T) (StorageService, *filestorage.MockFileStorage, string) { +func TestListFilesWithoutPermissions(t *testing.T) { + roots := []storageRuntime{publicStaticFilesStorage} + + store := newStandardStorageService(sqlstore.InitTestDB(t), roots, func(orgId int64) []storageRuntime { + return make([]storageRuntime, 0) + }, denyAllAuthService) + frame, err := store.List(context.Background(), dummyUser, "public/testdata") + require.NoError(t, err) + rowLen, err := frame.RowLen() + require.NoError(t, err) + require.Equal(t, 0, rowLen) +} + +func setupUploadStore(t *testing.T, authService storageAuthService) (StorageService, *filestorage.MockFileStorage, string) { t.Helper() storageName := "resources" mockStorage := &filestorage.MockFileStorage{} sqlStorage := newSQLStorage(storageName, "Testing upload", &StorageSQLConfig{orgId: 1}, sqlstore.InitTestDB(t)) sqlStorage.store = mockStorage + if authService == nil { + authService = allowAllAuthService + } store := newStandardStorageService(sqlstore.InitTestDB(t), []storageRuntime{sqlStorage}, func(orgId int64) []storageRuntime { return make([]storageRuntime, 0) - }) + }, authService) return store, mockStorage, storageName } func TestShouldUploadWhenNoFileAlreadyExists(t *testing.T) { - service, mockStorage, storageName := setupUploadStore(t) + service, mockStorage, storageName := setupUploadStore(t, nil) mockStorage.On("Get", mock.Anything, "/myFile.jpg").Return(nil, nil) mockStorage.On("Upsert", mock.Anything, mock.Anything).Return(nil) @@ -82,8 +110,20 @@ func TestShouldUploadWhenNoFileAlreadyExists(t *testing.T) { require.NoError(t, err) } +func TestShouldFailUploadWithoutAccess(t *testing.T) { + service, _, storageName := setupUploadStore(t, denyAllAuthService) + + err := service.Upload(context.Background(), dummyUser, &UploadRequest{ + EntityType: EntityTypeImage, + Contents: make([]byte, 0), + Path: storageName + "/myFile.jpg", + MimeType: "image/jpg", + }) + require.ErrorIs(t, err, ErrAccessDenied) +} + func TestShouldFailUploadWhenFileAlreadyExists(t *testing.T) { - service, mockStorage, storageName := setupUploadStore(t) + service, mockStorage, storageName := setupUploadStore(t, nil) mockStorage.On("Get", mock.Anything, "/myFile.jpg").Return(&filestorage.File{Contents: make([]byte, 0)}, nil) @@ -97,7 +137,7 @@ func TestShouldFailUploadWhenFileAlreadyExists(t *testing.T) { } func TestShouldDelegateFileDeletion(t *testing.T) { - service, mockStorage, storageName := setupUploadStore(t) + service, mockStorage, storageName := setupUploadStore(t, nil) mockStorage.On("Delete", mock.Anything, "/myFile.jpg").Return(nil) @@ -106,7 +146,7 @@ func TestShouldDelegateFileDeletion(t *testing.T) { } func TestShouldDelegateFolderCreation(t *testing.T) { - service, mockStorage, storageName := setupUploadStore(t) + service, mockStorage, storageName := setupUploadStore(t, nil) mockStorage.On("CreateFolder", mock.Anything, "/nestedFolder/mostNestedFolder").Return(nil) @@ -115,9 +155,9 @@ func TestShouldDelegateFolderCreation(t *testing.T) { } func TestShouldDelegateFolderDeletion(t *testing.T) { - service, mockStorage, storageName := setupUploadStore(t) + service, mockStorage, storageName := setupUploadStore(t, nil) - mockStorage.On("DeleteFolder", mock.Anything, "/", &filestorage.DeleteFolderOptions{Force: true}).Return(nil) + mockStorage.On("DeleteFolder", mock.Anything, "/", mock.Anything).Return(nil) err := service.DeleteFolder(context.Background(), dummyUser, &DeleteFolderCmd{ Path: storageName, diff --git a/pkg/services/store/static_auth.go b/pkg/services/store/static_auth.go new file mode 100644 index 00000000000..e97db52c487 --- /dev/null +++ b/pkg/services/store/static_auth.go @@ -0,0 +1,41 @@ +package store + +import ( + "context" + + "github.com/grafana/grafana/pkg/infra/filestorage" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models" +) + +type createPathFilterByAction func(ctx context.Context, user *models.SignedInUser, storageName string) map[string]filestorage.PathFilter + +func newStaticStorageAuthService(createPathFilterByAction createPathFilterByAction) storageAuthService { + return &staticStorageAuth{ + denyAllFileGuardian: &denyAllFileGuardian{}, + createPathFilterByAction: createPathFilterByAction, + log: log.New("staticStorageAuthService"), + } +} + +type staticStorageAuth struct { + log log.Logger + denyAllFileGuardian fileGuardian + createPathFilterByAction createPathFilterByAction +} + +func (a *staticStorageAuth) newGuardian(ctx context.Context, user *models.SignedInUser, storageName string) fileGuardian { + pathFilter := a.createPathFilterByAction(ctx, user, storageName) + + if pathFilter == nil { + return a.denyAllFileGuardian + } + + return &pathFilterFileGuardian{ + ctx: ctx, + user: user, + log: a.log, + prefix: storageName, + pathFilterByAction: pathFilter, + } +} diff --git a/pkg/services/store/tree.go b/pkg/services/store/tree.go index 5748fe5adc3..a28a7689410 100644 --- a/pkg/services/store/tree.go +++ b/pkg/services/store/tree.go @@ -85,7 +85,7 @@ func (t *nestedTree) GetFile(ctx context.Context, orgId int64, path string) (*fi return root.Store().Get(ctx, path) } -func (t *nestedTree) ListFolder(ctx context.Context, orgId int64, path string) (*StorageListFrame, error) { +func (t *nestedTree) ListFolder(ctx context.Context, orgId int64, path string, accessFilter filestorage.PathFilter) (*StorageListFrame, error) { if path == "" || path == "/" { t.assureOrgIsInitialized(orgId) @@ -150,6 +150,7 @@ func (t *nestedTree) ListFolder(ctx context.Context, orgId int64, path string) ( Recursive: false, WithFolders: true, WithFiles: true, + Filter: accessFilter, }) if err != nil { diff --git a/pkg/services/store/types.go b/pkg/services/store/types.go index db9cdfa8202..c076a60e7d0 100644 --- a/pkg/services/store/types.go +++ b/pkg/services/store/types.go @@ -30,7 +30,7 @@ type WriteValueResponse struct { type storageTree interface { GetFile(ctx context.Context, orgId int64, path string) (*filestorage.File, error) - ListFolder(ctx context.Context, orgId int64, path string) (*StorageListFrame, error) + ListFolder(ctx context.Context, orgId int64, path string, accessFilter filestorage.PathFilter) (*StorageListFrame, error) } //------------------------------------------- diff --git a/pkg/services/store/utils.go b/pkg/services/store/utils.go index 70f47349f39..f7fd341ac47 100644 --- a/pkg/services/store/utils.go +++ b/pkg/services/store/utils.go @@ -28,3 +28,8 @@ func getPathAndScope(c *models.ReqContext) (string, string) { } return splitFirstSegment(path) } + +func getFirstSegment(path string) string { + firstSegment, _ := splitFirstSegment(path) + return firstSegment +} From a71b4f13e477fdb15674d93eaddfde060e3193a8 Mon Sep 17 00:00:00 2001 From: Alexander Gee Date: Mon, 18 Jul 2022 01:32:52 -0500 Subject: [PATCH 3/9] Dashboard: Add guidance about reload required after updating shared cursor/tooltip setting. (#52280) * Dashboard: Add guidance about reloaded needed for shared cursor/tooltip * Dashboard: Added todo note for author of (#46581) impl * Dashboard: prettier errors fixed for new text --- .../dashboard/components/DashboardSettings/GeneralSettings.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx index 1ddfc1a4cef..d76cedd757d 100644 --- a/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx @@ -143,10 +143,11 @@ export function GeneralSettingsUnconnected({ dashboard, updateTimeZone, updateWe liveNow={dashboard.liveNow} /> + {/* @todo: Update "Graph tooltip" description to remove prompt about reloading when resolving #46581 */} From 9abe9fa7029312d342a878de8ff13d87dd42134e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20L=C3=B3pez=20de=20la=20Franca=20Beltran?= <5459617+joanlopez@users.noreply.github.com> Date: Mon, 18 Jul 2022 08:57:58 +0200 Subject: [PATCH 4/9] Encryption: Expose secrets migrations through HTTP API (#51707) * Encryption: Move secrets migrations into secrets.Migrator * Encryption: Refactor secrets.Service initialization * Encryption: Add support to run secrets migrations even when EE is disabled * Encryption: Expose secrets migrations through HTTP API * Update docs * Fix docs links * Some adjustments to makes errors explicit through HTTP response --- docs/sources/developers/http_api/admin.md | 69 +++++++++++++++++-- .../configure-database-encryption/_index.md | 15 ++-- pkg/api/admin_encryption.go | 36 +++++++++- pkg/api/api.go | 3 + .../secretsmigrations/secretsmigrations.go | 6 +- pkg/services/secrets/migrator/migrator.go | 48 +++++++------ pkg/services/secrets/migrator/reencrypt.go | 24 ++++--- pkg/services/secrets/secrets.go | 12 +++- 8 files changed, 168 insertions(+), 45 deletions(-) diff --git a/docs/sources/developers/http_api/admin.md b/docs/sources/developers/http_api/admin.md index e83c7c941eb..d113a1280ad 100644 --- a/docs/sources/developers/http_api/admin.md +++ b/docs/sources/developers/http_api/admin.md @@ -718,11 +718,7 @@ Content-Type: application/json `POST /api/admin/encryption/rotate-data-keys` -Rotates data encryption keys, so all the active keys are disabled -and no longer used for encryption but kept for decryption operations. - -Secrets encrypted with one of the deactivated keys need to be re-encrypted -to actually stop using those keys for both encryption and decryption. +[Rotates]({{< relref "../../setup-grafana/configure-security/configure-database-encryption/#rotate-data-keys" >}}) data encryption keys. **Example Request**: @@ -738,3 +734,66 @@ Content-Type: application/json HTTP/1.1 204 Content-Type: application/json ``` + +## Re-encrypt data encryption keys + +`POST /api/admin/encryption/reencrypt-data-keys` + +[Re-encrypts]({{< relref "../../setup-grafana/configure-security/configure-database-encryption/#re-encrypt-data-keys" >}}) data encryption keys. + +**Example Request**: + +```http +POST /api/admin/encryption/reencrypt-data-keys HTTP/1.1 +Accept: application/json +Content-Type: application/json +``` + +**Example Response**: + +```http +HTTP/1.1 204 +Content-Type: application/json +``` + +## Re-encrypt secrets + +`POST /api/admin/encryption/reencrypt-secrets` + +[Re-encrypts]({{< relref "../../setup-grafana/configure-security/configure-database-encryption/#re-encrypt-secrets" >}}) secrets. + +**Example Request**: + +```http +POST /api/admin/encryption/reencrypt-secrets HTTP/1.1 +Accept: application/json +Content-Type: application/json +``` + +**Example Response**: + +```http +HTTP/1.1 204 +Content-Type: application/json +``` + +## Roll back secrets + +`POST /api/admin/encryption/rollback-secrets` + +[Rolls back]({{< relref "../../setup-grafana/configure-security/configure-database-encryption/#roll-back-secrets" >}}) secrets. + +**Example Request**: + +```http +POST /api/admin/encryption/rollback-secrets HTTP/1.1 +Accept: application/json +Content-Type: application/json +``` + +**Example Response**: + +```http +HTTP/1.1 204 +Content-Type: application/json +``` diff --git a/docs/sources/setup-grafana/configure-security/configure-database-encryption/_index.md b/docs/sources/setup-grafana/configure-security/configure-database-encryption/_index.md index 21251774e37..48864deb65f 100644 --- a/docs/sources/setup-grafana/configure-security/configure-database-encryption/_index.md +++ b/docs/sources/setup-grafana/configure-security/configure-database-encryption/_index.md @@ -18,7 +18,7 @@ Grafana encrypts these secrets before they are written to the database, by using Since Grafana v9.0, it uses [envelope encryption](#envelope-encryption) by default, which adds a layer of indirection to the encryption process that represents an [**implicit breaking change**](#implicit-breaking-change) for older versions of Grafana. -For further details about how to operate a Grafana instance with envelope encryption, see the [Operational work]({{< relref "/#operational-work" >}}) section below. +For further details about how to operate a Grafana instance with envelope encryption, see the [Operational work](#operational-work) section below. > **Note:** In Grafana Enterprise, you can also choose to [encrypt secrets in AES-GCM mode]({{< relref "#changing-your-encryption-mode-to-aes-gcm" >}}) instead of AES-CFB. @@ -31,7 +31,7 @@ Instead of encrypting all secrets with a single key, Grafana uses a set of keys encrypt them. These data encryption keys are themselves encrypted with a single key encryption key (KEK), configured through the `secret_key` attribute in your [Grafana configuration]({{< relref "../../configure-grafana/#secret_key" >}}) or with a -[KMS integration](#kms-integration). +[KMS integration](#encrypting-your-database-with-a-key-from-a-key-management-system-kms). ## Implicit breaking change @@ -67,7 +67,8 @@ Secrets re-encryption can be performed when a Grafana administrator wants to eit - Re-encrypt secrets after a [data keys rotation](#rotate-data-keys). > **Note:** This operation is available through Grafana CLI by running `grafana-cli admin secrets-migration re-encrypt` -> command. It's safe to run more than once. Recommended to run under maintenance mode. +> command and through Grafana [Admin API]({{< relref "../../../developers/http_api/admin/#re-encrypt-secrets" >}}). +> It's safe to run more than once. Recommended to run under maintenance mode. ## Roll back secrets @@ -75,16 +76,18 @@ Used to roll back secrets encrypted with envelope encryption to legacy encryptio a Grafana version earlier than Grafana v9.0 after an unsuccessful upgrade. > **Note:** This operation is available through Grafana CLI by running `grafana-cli admin secrets-migration rollback` -> command. It's safe to run more than once. Recommended to run under maintenance mode. +> command and through Grafana [Admin API]({{< relref "../../../developers/http_api/admin/#roll-back-secrets" >}}). +> It's safe to run more than once. Recommended to run under maintenance mode. ## Re-encrypt data keys Used to re-encrypt data keys encrypted with a specific key encryption key (KEK). It can be used to either re-encrypt -existing data keys with a new key encryption key version (see [KMS integration](#kms-integration) rotation) or to +existing data keys with a new key encryption key version (see [KMS integration](#encrypting-your-database-with-a-key-from-a-key-management-system-kms) rotation) or to re-encrypt them with a completely different key encryption key. > **Note:** This operation is available through Grafana CLI by running `grafana-cli admin secrets-migration re-encrypt-data-keys` -> command. It's safe to run more than once. Recommended to run under maintenance mode. +> command and through Grafana [Admin API]({{< relref "../../../developers/http_api/admin/#re-encrypt-data-encryption-keys" >}}). +> It's safe to run more than once. Recommended to run under maintenance mode. ## Rotate data keys diff --git a/pkg/api/admin_encryption.go b/pkg/api/admin_encryption.go index 6eb94022dd5..1e71b67aeae 100644 --- a/pkg/api/admin_encryption.go +++ b/pkg/api/admin_encryption.go @@ -9,8 +9,42 @@ import ( func (hs *HTTPServer) AdminRotateDataEncryptionKeys(c *models.ReqContext) response.Response { if err := hs.SecretsService.RotateDataKeys(c.Req.Context()); err != nil { - return response.Error(http.StatusInternalServerError, "Failed to rotate data key", err) + return response.Error(http.StatusInternalServerError, "Failed to rotate data keys", err) } return response.Respond(http.StatusNoContent, "") } + +func (hs *HTTPServer) AdminReEncryptEncryptionKeys(c *models.ReqContext) response.Response { + if err := hs.SecretsService.ReEncryptDataKeys(c.Req.Context()); err != nil { + return response.Error(http.StatusInternalServerError, "Failed to re-encrypt data keys", err) + } + + return response.Respond(http.StatusOK, "Data encryption keys re-encrypted successfully") +} + +func (hs *HTTPServer) AdminReEncryptSecrets(c *models.ReqContext) response.Response { + success, err := hs.secretsMigrator.ReEncryptSecrets(c.Req.Context()) + if err != nil { + return response.Error(http.StatusInternalServerError, "Failed to re-encrypt secrets", err) + } + + if !success { + return response.Error(http.StatusPartialContent, "Something unexpected happened, refer to the server logs for more details", err) + } + + return response.Respond(http.StatusOK, "Secrets re-encrypted successfully") +} + +func (hs *HTTPServer) AdminRollbackSecrets(c *models.ReqContext) response.Response { + success, err := hs.secretsMigrator.RollBackSecrets(c.Req.Context()) + if err != nil { + return response.Error(http.StatusInternalServerError, "Failed to rollback secrets", err) + } + + if !success { + return response.Error(http.StatusPartialContent, "Something unexpected happened, refer to the server logs for more details", err) + } + + return response.Respond(http.StatusOK, "Secrets rolled back successfully") +} diff --git a/pkg/api/api.go b/pkg/api/api.go index b5cacdb4915..619afbcfb73 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -575,6 +575,9 @@ func (hs *HTTPServer) registerRoutes() { } adminRoute.Post("/encryption/rotate-data-keys", reqGrafanaAdmin, routing.Wrap(hs.AdminRotateDataEncryptionKeys)) + adminRoute.Post("/encryption/reencrypt-data-keys", reqGrafanaAdmin, routing.Wrap(hs.AdminReEncryptEncryptionKeys)) + adminRoute.Post("/encryption/reencrypt-secrets", reqGrafanaAdmin, routing.Wrap(hs.AdminReEncryptSecrets)) + adminRoute.Post("/encryption/rollback-secrets", reqGrafanaAdmin, routing.Wrap(hs.AdminRollbackSecrets)) adminRoute.Post("/provisioning/dashboards/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersDashboards)), routing.Wrap(hs.AdminProvisioningReloadDashboards)) adminRoute.Post("/provisioning/plugins/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersPlugins)), routing.Wrap(hs.AdminProvisioningReloadPlugins)) diff --git a/pkg/cmd/grafana-cli/commands/secretsmigrations/secretsmigrations.go b/pkg/cmd/grafana-cli/commands/secretsmigrations/secretsmigrations.go index 3d6b84b22a1..540e7c92a59 100644 --- a/pkg/cmd/grafana-cli/commands/secretsmigrations/secretsmigrations.go +++ b/pkg/cmd/grafana-cli/commands/secretsmigrations/secretsmigrations.go @@ -12,9 +12,11 @@ func ReEncryptDEKS(_ utils.CommandLine, runner runner.Runner) error { } func ReEncryptSecrets(_ utils.CommandLine, runner runner.Runner) error { - return runner.SecretsMigrator.ReEncryptSecrets(context.Background()) + _, err := runner.SecretsMigrator.ReEncryptSecrets(context.Background()) + return err } func RollBackSecrets(_ utils.CommandLine, runner runner.Runner) error { - return runner.SecretsMigrator.RollBackSecrets(context.Background()) + _, err := runner.SecretsMigrator.RollBackSecrets(context.Background()) + return err } diff --git a/pkg/services/secrets/migrator/migrator.go b/pkg/services/secrets/migrator/migrator.go index 2fcba755d2d..07f29e84d8c 100644 --- a/pkg/services/secrets/migrator/migrator.go +++ b/pkg/services/secrets/migrator/migrator.go @@ -37,14 +37,14 @@ func ProvideSecretsMigrator( } } -func (m *SecretsMigrator) ReEncryptSecrets(ctx context.Context) error { +func (m *SecretsMigrator) ReEncryptSecrets(ctx context.Context) (bool, error) { err := m.initProvidersIfNeeded() if err != nil { - return err + return false, err } toReencrypt := []interface { - reencrypt(context.Context, *manager.SecretsService, *sqlstore.SQLStore) + reencrypt(context.Context, *manager.SecretsService, *sqlstore.SQLStore) bool }{ simpleSecret{tableName: "dashboard_snapshot", columnName: "dashboard_encrypted"}, b64Secret{simpleSecret: simpleSecret{tableName: "user_auth", columnName: "o_auth_access_token"}, encoding: base64.StdEncoding}, @@ -56,30 +56,21 @@ func (m *SecretsMigrator) ReEncryptSecrets(ctx context.Context) error { alertingSecret{}, } + var anyFailure bool + for _, r := range toReencrypt { - r.reencrypt(ctx, m.secretsSrv, m.sqlStore) - } - - return nil -} - -func (m *SecretsMigrator) initProvidersIfNeeded() error { - if m.features.IsEnabled(featuremgmt.FlagDisableEnvelopeEncryption) { - logger.Info("Envelope encryption is not enabled but trying to init providers anyway...") - - if err := m.secretsSrv.InitProviders(); err != nil { - logger.Error("Envelope encryption providers initialization failed", "error", err) - return err + if success := r.reencrypt(ctx, m.secretsSrv, m.sqlStore); !success { + anyFailure = true } } - return nil + return !anyFailure, nil } -func (m *SecretsMigrator) RollBackSecrets(ctx context.Context) error { +func (m *SecretsMigrator) RollBackSecrets(ctx context.Context) (bool, error) { err := m.initProvidersIfNeeded() if err != nil { - return err + return false, err } toRollback := []interface { @@ -110,11 +101,26 @@ func (m *SecretsMigrator) RollBackSecrets(ctx context.Context) error { if anyFailure { logger.Warn("Some errors happened, not cleaning up data keys table...") - return nil + return false, nil } - if _, sqlErr := m.sqlStore.NewSession(ctx).Exec("DELETE FROM data_keys"); sqlErr != nil { + _, sqlErr := m.sqlStore.NewSession(ctx).Exec("DELETE FROM data_keys") + if sqlErr != nil { logger.Warn("Error while cleaning up data keys table...", "error", sqlErr) + return false, nil + } + + return true, nil +} + +func (m *SecretsMigrator) initProvidersIfNeeded() error { + if m.features.IsEnabled(featuremgmt.FlagDisableEnvelopeEncryption) { + logger.Info("Envelope encryption is not enabled but trying to init providers anyway...") + + if err := m.secretsSrv.InitProviders(); err != nil { + logger.Error("Envelope encryption providers initialization failed", "error", err) + return err + } } return nil diff --git a/pkg/services/secrets/migrator/reencrypt.go b/pkg/services/secrets/migrator/reencrypt.go index acbacc90524..80a8adb6cde 100644 --- a/pkg/services/secrets/migrator/reencrypt.go +++ b/pkg/services/secrets/migrator/reencrypt.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" ) -func (s simpleSecret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore *sqlstore.SQLStore) { +func (s simpleSecret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore *sqlstore.SQLStore) bool { var rows []struct { Id int Secret []byte @@ -20,7 +20,7 @@ func (s simpleSecret) reencrypt(ctx context.Context, secretsSrv *manager.Secrets if err := sqlStore.NewSession(ctx).Table(s.tableName).Select(fmt.Sprintf("id, %s as secret", s.columnName)).Find(&rows); err != nil { logger.Warn("Could not find any secret to re-encrypt", "table", s.tableName) - return + return false } var anyFailure bool @@ -62,9 +62,11 @@ func (s simpleSecret) reencrypt(ctx context.Context, secretsSrv *manager.Secrets } else { logger.Info(fmt.Sprintf("Column %s from %s has been re-encrypted successfully", s.columnName, s.tableName)) } + + return !anyFailure } -func (s b64Secret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore *sqlstore.SQLStore) { +func (s b64Secret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore *sqlstore.SQLStore) bool { var rows []struct { Id int Secret string @@ -72,7 +74,7 @@ func (s b64Secret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsSer if err := sqlStore.NewSession(ctx).Table(s.tableName).Select(fmt.Sprintf("id, %s as secret", s.columnName)).Find(&rows); err != nil { logger.Warn("Could not find any secret to re-encrypt", "table", s.tableName) - return + return false } var anyFailure bool @@ -128,9 +130,11 @@ func (s b64Secret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsSer } else { logger.Info(fmt.Sprintf("Column %s from %s has been re-encrypted successfully", s.columnName, s.tableName)) } + + return !anyFailure } -func (s jsonSecret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore *sqlstore.SQLStore) { +func (s jsonSecret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore *sqlstore.SQLStore) bool { var rows []struct { Id int SecureJsonData map[string][]byte @@ -138,7 +142,7 @@ func (s jsonSecret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsSe if err := sqlStore.NewSession(ctx).Table(s.tableName).Cols("id", "secure_json_data").Find(&rows); err != nil { logger.Warn("Could not find any secret to re-encrypt", "table", s.tableName) - return + return false } var anyFailure bool @@ -184,9 +188,11 @@ func (s jsonSecret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsSe } else { logger.Info(fmt.Sprintf("Secure json data secrets from %s have been re-encrypted successfully", s.tableName)) } + + return !anyFailure } -func (s alertingSecret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore *sqlstore.SQLStore) { +func (s alertingSecret) reencrypt(ctx context.Context, secretsSrv *manager.SecretsService, sqlStore *sqlstore.SQLStore) bool { var results []struct { Id int AlertmanagerConfiguration string @@ -195,7 +201,7 @@ func (s alertingSecret) reencrypt(ctx context.Context, secretsSrv *manager.Secre selectSQL := "SELECT id, alertmanager_configuration FROM alert_configuration" if err := sqlStore.NewSession(ctx).SQL(selectSQL).Find(&results); err != nil { logger.Warn("Could not find any alert_configuration secret to re-encrypt") - return + return false } var anyFailure bool @@ -261,4 +267,6 @@ func (s alertingSecret) reencrypt(ctx context.Context, secretsSrv *manager.Secre } else { logger.Info("Alerting configuration secrets have been re-encrypted successfully") } + + return !anyFailure } diff --git a/pkg/services/secrets/secrets.go b/pkg/services/secrets/secrets.go index aa4fdb7b13a..4b4a554406f 100644 --- a/pkg/services/secrets/secrets.go +++ b/pkg/services/secrets/secrets.go @@ -72,6 +72,14 @@ type BackgroundProvider interface { // Migrator is responsible for secrets migrations like re-encrypting or rolling back secrets. type Migrator interface { - ReEncryptSecrets(ctx context.Context) error - RollBackSecrets(ctx context.Context) error + // ReEncryptSecrets decrypts and re-encrypts the secrets with most recent + // available data key. If a secret-specific decryption / re-encryption fails, + // it does not stop, but returns false as the first return (success or not) + // at the end of the process. + ReEncryptSecrets(ctx context.Context) (bool, error) + // RollBackSecrets decrypts and re-encrypts the secrets using the legacy + // encryption. If a secret-specific decryption / re-encryption fails, it + // does not stop, but returns false as the first return (success or not) + // at the end of the process. + RollBackSecrets(ctx context.Context) (bool, error) } From 689ae96a0e06c6e500591e02f5696a60c76ef089 Mon Sep 17 00:00:00 2001 From: Joe Blubaugh Date: Mon, 18 Jul 2022 15:08:08 +0800 Subject: [PATCH 5/9] Alerting: Refactor API types generation with different names. (#51785) This changes the API codegen template (controller-api.mustache) to simplify some names. When this package was created, most APIs "forked" to either a Grafana backend implementation or a "Lotex" remote implementation. As we have added APIs it's no longer the case. Provisioning, configuration, and testing APIs do not fork, and we are likely to add additional APIs that don't fork. This change replaces {{classname}}ForkingService with {{classname}} for interface names, and names the concrete implementation {{classname}}Handler. It changes the implied implementation of a route handler from fork{{nickname}} to handle{{nickname}}. So PrometheusApiForkingService becomes PrometheusApi, ForkedPrometheusApi becomes PrometheusApiHandler and forkRouteGetGrafanaAlertStatuses becomes handleRouteGetGrafanaAlertStatuses It also renames some files - APIs that do no forking go from forked_{{name}}.go to {{name}}.go and APIs that still fork go from forked_{{name}}.go to forking_{{name}}.go to capture the idea that those files a "doing forking" rather than "are a fork of something." Signed-off-by: Joe Blubaugh --- pkg/services/ngalert/api/api.go | 14 +- .../{api_admin.go => api_configuration.go} | 10 +- pkg/services/ngalert/api/configuration.go | 34 +++++ pkg/services/ngalert/api/forked_admin.go | 35 ----- .../ngalert/api/forked_provisioning.go | 108 ---------------- pkg/services/ngalert/api/forked_testing.go | 31 ----- .../{forked_am.go => forking_alertmanager.go} | 58 ++++----- .../{forked_prom.go => forking_prometheus.go} | 16 +-- .../api/{fork_ruler.go => forking_ruler.go} | 33 +++-- .../api/generated_base_api_alertmanager.go | 122 +++++++++++------- .../api/generated_base_api_configuration.go | 21 +-- .../api/generated_base_api_prometheus.go | 22 ++-- .../api/generated_base_api_provisioning.go | 114 +++++++++------- .../ngalert/api/generated_base_api_ruler.go | 65 ++++++---- .../ngalert/api/generated_base_api_testing.go | 20 +-- pkg/services/ngalert/api/provisioning.go | 105 +++++++++++++++ pkg/services/ngalert/api/testing_api.go | 30 +++++ pkg/services/ngalert/api/tooling/Makefile | 4 +- pkg/services/ngalert/api/tooling/api.json | 4 +- pkg/services/ngalert/api/tooling/spec.json | 4 +- .../templates/controller-api.mustache | 17 ++- 21 files changed, 464 insertions(+), 403 deletions(-) rename pkg/services/ngalert/api/{api_admin.go => api_configuration.go} (88%) create mode 100644 pkg/services/ngalert/api/configuration.go delete mode 100644 pkg/services/ngalert/api/forked_admin.go delete mode 100644 pkg/services/ngalert/api/forked_provisioning.go delete mode 100644 pkg/services/ngalert/api/forked_testing.go rename pkg/services/ngalert/api/{forked_am.go => forking_alertmanager.go} (50%) rename pkg/services/ngalert/api/{forked_prom.go => forking_prometheus.go} (60%) rename pkg/services/ngalert/api/{fork_ruler.go => forking_ruler.go} (64%) create mode 100644 pkg/services/ngalert/api/provisioning.go create mode 100644 pkg/services/ngalert/api/testing_api.go diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 64ac0ed1501..8056a453342 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -93,19 +93,19 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { } // Register endpoints for proxying to Alertmanager-compatible backends. - api.RegisterAlertmanagerApiEndpoints(NewForkedAM( + api.RegisterAlertmanagerApiEndpoints(NewForkingAM( api.DatasourceCache, NewLotexAM(proxy, logger), &AlertmanagerSrv{crypto: api.MultiOrgAlertmanager.Crypto, log: logger, ac: api.AccessControl, mam: api.MultiOrgAlertmanager}, ), m) // Register endpoints for proxying to Prometheus-compatible backends. - api.RegisterPrometheusApiEndpoints(NewForkedProm( + api.RegisterPrometheusApiEndpoints(NewForkingProm( api.DatasourceCache, NewLotexProm(proxy, logger), &PrometheusSrv{log: logger, manager: api.StateManager, store: api.RuleStore, ac: api.AccessControl}, ), m) // Register endpoints for proxying to Cortex Ruler-compatible backends. - api.RegisterRulerApiEndpoints(NewForkedRuler( + api.RegisterRulerApiEndpoints(NewForkingRuler( api.DatasourceCache, NewLotexRuler(proxy, logger), &RulerSrv{ @@ -120,7 +120,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { ac: api.AccessControl, }, ), m) - api.RegisterTestingApiEndpoints(NewForkedTestingApi( + api.RegisterTestingApiEndpoints(NewTestingApi( &TestingApiSrv{ AlertingProxy: proxy, DatasourceCache: api.DatasourceCache, @@ -128,15 +128,15 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { accessControl: api.AccessControl, evaluator: eval.NewEvaluator(api.Cfg, log.New("ngalert.eval"), api.DatasourceCache, api.SecretsService, api.ExpressionService), }), m) - api.RegisterConfigurationApiEndpoints(NewForkedConfiguration( - &AdminSrv{ + api.RegisterConfigurationApiEndpoints(NewConfiguration( + &ConfigSrv{ store: api.AdminConfigStore, log: logger, alertmanagerProvider: api.AlertsRouter, }, ), m) - api.RegisterProvisioningApiEndpoints(NewForkedProvisioningApi(&ProvisioningSrv{ + api.RegisterProvisioningApiEndpoints(NewProvisioningApi(&ProvisioningSrv{ log: logger, policies: api.Policies, contactPointService: api.ContactPointService, diff --git a/pkg/services/ngalert/api/api_admin.go b/pkg/services/ngalert/api/api_configuration.go similarity index 88% rename from pkg/services/ngalert/api/api_admin.go rename to pkg/services/ngalert/api/api_configuration.go index e3f33f221dd..3e1ab1dd84d 100644 --- a/pkg/services/ngalert/api/api_admin.go +++ b/pkg/services/ngalert/api/api_configuration.go @@ -15,13 +15,13 @@ import ( v1 "github.com/prometheus/client_golang/api/prometheus/v1" ) -type AdminSrv struct { +type ConfigSrv struct { alertmanagerProvider ExternalAlertmanagerProvider store store.AdminConfigurationStore log log.Logger } -func (srv AdminSrv) RouteGetAlertmanagers(c *models.ReqContext) response.Response { +func (srv ConfigSrv) RouteGetAlertmanagers(c *models.ReqContext) response.Response { urls := srv.alertmanagerProvider.AlertmanagersFor(c.OrgId) droppedURLs := srv.alertmanagerProvider.DroppedAlertmanagersFor(c.OrgId) ams := v1.AlertManagersResult{Active: make([]v1.AlertManager, len(urls)), Dropped: make([]v1.AlertManager, len(droppedURLs))} @@ -38,7 +38,7 @@ func (srv AdminSrv) RouteGetAlertmanagers(c *models.ReqContext) response.Respons }) } -func (srv AdminSrv) RouteGetNGalertConfig(c *models.ReqContext) response.Response { +func (srv ConfigSrv) RouteGetNGalertConfig(c *models.ReqContext) response.Response { if c.OrgRole != models.ROLE_ADMIN { return accessForbiddenResp() } @@ -61,7 +61,7 @@ func (srv AdminSrv) RouteGetNGalertConfig(c *models.ReqContext) response.Respons return response.JSON(http.StatusOK, resp) } -func (srv AdminSrv) RoutePostNGalertConfig(c *models.ReqContext, body apimodels.PostableNGalertConfig) response.Response { +func (srv ConfigSrv) RoutePostNGalertConfig(c *models.ReqContext, body apimodels.PostableNGalertConfig) response.Response { if c.OrgRole != models.ROLE_ADMIN { return accessForbiddenResp() } @@ -97,7 +97,7 @@ func (srv AdminSrv) RoutePostNGalertConfig(c *models.ReqContext, body apimodels. return response.JSON(http.StatusCreated, util.DynMap{"message": "admin configuration updated"}) } -func (srv AdminSrv) RouteDeleteNGalertConfig(c *models.ReqContext) response.Response { +func (srv ConfigSrv) RouteDeleteNGalertConfig(c *models.ReqContext) response.Response { if c.OrgRole != models.ROLE_ADMIN { return accessForbiddenResp() } diff --git a/pkg/services/ngalert/api/configuration.go b/pkg/services/ngalert/api/configuration.go new file mode 100644 index 00000000000..070025e51ea --- /dev/null +++ b/pkg/services/ngalert/api/configuration.go @@ -0,0 +1,34 @@ +package api + +import ( + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/models" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" +) + +// ConfigurationApiHandler always forwards requests to grafana backend +type ConfigurationApiHandler struct { + grafana *ConfigSrv +} + +func NewConfiguration(grafana *ConfigSrv) *ConfigurationApiHandler { + return &ConfigurationApiHandler{ + grafana: grafana, + } +} + +func (f *ConfigurationApiHandler) handleRouteGetAlertmanagers(c *models.ReqContext) response.Response { + return f.grafana.RouteGetAlertmanagers(c) +} + +func (f *ConfigurationApiHandler) handleRouteGetNGalertConfig(c *models.ReqContext) response.Response { + return f.grafana.RouteGetNGalertConfig(c) +} + +func (f *ConfigurationApiHandler) handleRoutePostNGalertConfig(c *models.ReqContext, body apimodels.PostableNGalertConfig) response.Response { + return f.grafana.RoutePostNGalertConfig(c, body) +} + +func (f *ConfigurationApiHandler) handleRouteDeleteNGalertConfig(c *models.ReqContext) response.Response { + return f.grafana.RouteDeleteNGalertConfig(c) +} diff --git a/pkg/services/ngalert/api/forked_admin.go b/pkg/services/ngalert/api/forked_admin.go deleted file mode 100644 index 00c7b421546..00000000000 --- a/pkg/services/ngalert/api/forked_admin.go +++ /dev/null @@ -1,35 +0,0 @@ -package api - -import ( - "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" -) - -// ForkedConfigurationApi always forwards requests to grafana backend -type ForkedConfigurationApi struct { - grafana *AdminSrv -} - -// NewForkedConfiguration creates a new ForkedConfigurationApi instance -func NewForkedConfiguration(grafana *AdminSrv) *ForkedConfigurationApi { - return &ForkedConfigurationApi{ - grafana: grafana, - } -} - -func (f *ForkedConfigurationApi) forkRouteGetAlertmanagers(c *models.ReqContext) response.Response { - return f.grafana.RouteGetAlertmanagers(c) -} - -func (f *ForkedConfigurationApi) forkRouteGetNGalertConfig(c *models.ReqContext) response.Response { - return f.grafana.RouteGetNGalertConfig(c) -} - -func (f *ForkedConfigurationApi) forkRoutePostNGalertConfig(c *models.ReqContext, body apimodels.PostableNGalertConfig) response.Response { - return f.grafana.RoutePostNGalertConfig(c, body) -} - -func (f *ForkedConfigurationApi) forkRouteDeleteNGalertConfig(c *models.ReqContext) response.Response { - return f.grafana.RouteDeleteNGalertConfig(c) -} diff --git a/pkg/services/ngalert/api/forked_provisioning.go b/pkg/services/ngalert/api/forked_provisioning.go deleted file mode 100644 index 28457980df8..00000000000 --- a/pkg/services/ngalert/api/forked_provisioning.go +++ /dev/null @@ -1,108 +0,0 @@ -package api - -import ( - "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" -) - -// ForkedProvisioningApi always forwards requests to a Grafana backend. -// We do not currently support provisioning of external systems through Grafana's API. -type ForkedProvisioningApi struct { - svc *ProvisioningSrv -} - -// NewForkedProvisioningApi creates a new ForkedProvisioningApi instance. -func NewForkedProvisioningApi(svc *ProvisioningSrv) *ForkedProvisioningApi { - return &ForkedProvisioningApi{ - svc: svc, - } -} - -func (f *ForkedProvisioningApi) forkRouteGetPolicyTree(ctx *models.ReqContext) response.Response { - return f.svc.RouteGetPolicyTree(ctx) -} - -func (f *ForkedProvisioningApi) forkRoutePutPolicyTree(ctx *models.ReqContext, route apimodels.Route) response.Response { - return f.svc.RoutePutPolicyTree(ctx, route) -} - -func (f *ForkedProvisioningApi) forkRouteResetPolicyTree(ctx *models.ReqContext) response.Response { - return f.svc.RouteResetPolicyTree(ctx) -} - -func (f *ForkedProvisioningApi) forkRouteGetContactpoints(ctx *models.ReqContext) response.Response { - return f.svc.RouteGetContactPoints(ctx) -} - -func (f *ForkedProvisioningApi) forkRoutePostContactpoints(ctx *models.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response { - return f.svc.RoutePostContactPoint(ctx, cp) -} - -func (f *ForkedProvisioningApi) forkRoutePutContactpoint(ctx *models.ReqContext, cp apimodels.EmbeddedContactPoint, UID string) response.Response { - return f.svc.RoutePutContactPoint(ctx, cp, UID) -} - -func (f *ForkedProvisioningApi) forkRouteDeleteContactpoints(ctx *models.ReqContext, UID string) response.Response { - return f.svc.RouteDeleteContactPoint(ctx, UID) -} - -func (f *ForkedProvisioningApi) forkRouteGetTemplates(ctx *models.ReqContext) response.Response { - return f.svc.RouteGetTemplates(ctx) -} - -func (f *ForkedProvisioningApi) forkRouteGetTemplate(ctx *models.ReqContext, name string) response.Response { - return f.svc.RouteGetTemplate(ctx, name) -} - -func (f *ForkedProvisioningApi) forkRoutePutTemplate(ctx *models.ReqContext, body apimodels.MessageTemplateContent, name string) response.Response { - return f.svc.RoutePutTemplate(ctx, body, name) -} - -func (f *ForkedProvisioningApi) forkRouteDeleteTemplate(ctx *models.ReqContext, name string) response.Response { - return f.svc.RouteDeleteTemplate(ctx, name) -} - -func (f *ForkedProvisioningApi) forkRouteGetMuteTiming(ctx *models.ReqContext, name string) response.Response { - return f.svc.RouteGetMuteTiming(ctx, name) -} - -func (f *ForkedProvisioningApi) forkRouteGetMuteTimings(ctx *models.ReqContext) response.Response { - return f.svc.RouteGetMuteTimings(ctx) -} - -func (f *ForkedProvisioningApi) forkRoutePostMuteTiming(ctx *models.ReqContext, mt apimodels.MuteTimeInterval) response.Response { - return f.svc.RoutePostMuteTiming(ctx, mt) -} - -func (f *ForkedProvisioningApi) forkRoutePutMuteTiming(ctx *models.ReqContext, mt apimodels.MuteTimeInterval, name string) response.Response { - return f.svc.RoutePutMuteTiming(ctx, mt, name) -} - -func (f *ForkedProvisioningApi) forkRouteDeleteMuteTiming(ctx *models.ReqContext, name string) response.Response { - return f.svc.RouteDeleteMuteTiming(ctx, name) -} - -func (f *ForkedProvisioningApi) forkRouteGetAlertRule(ctx *models.ReqContext, UID string) response.Response { - return f.svc.RouteRouteGetAlertRule(ctx, UID) -} - -func (f *ForkedProvisioningApi) forkRoutePostAlertRule(ctx *models.ReqContext, ar apimodels.ProvisionedAlertRule) response.Response { - return f.svc.RoutePostAlertRule(ctx, ar) -} - -func (f *ForkedProvisioningApi) forkRoutePutAlertRule(ctx *models.ReqContext, ar apimodels.ProvisionedAlertRule, UID string) response.Response { - return f.svc.RoutePutAlertRule(ctx, ar, UID) -} - -func (f *ForkedProvisioningApi) forkRouteDeleteAlertRule(ctx *models.ReqContext, UID string) response.Response { - return f.svc.RouteDeleteAlertRule(ctx, UID) -} - -func (f *ForkedProvisioningApi) forkRouteGetAlertRuleGroup(ctx *models.ReqContext, folder, group string) response.Response { - return f.svc.RouteGetAlertRuleGroup(ctx, folder, group) -} - -func (f *ForkedProvisioningApi) forkRoutePutAlertRuleGroup(ctx *models.ReqContext, ag apimodels.AlertRuleGroupMetadata, folder, group string) response.Response { - return f.svc.RoutePutAlertRuleGroup(ctx, ag, folder, group) -} diff --git a/pkg/services/ngalert/api/forked_testing.go b/pkg/services/ngalert/api/forked_testing.go deleted file mode 100644 index 6b41961e770..00000000000 --- a/pkg/services/ngalert/api/forked_testing.go +++ /dev/null @@ -1,31 +0,0 @@ -package api - -import ( - "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" -) - -// ForkedTestingApi always forwards requests to grafana backend -type ForkedTestingApi struct { - svc *TestingApiSrv -} - -// NewForkedTestingApi creates a new ForkedTestingApi instance -func NewForkedTestingApi(svc *TestingApiSrv) *ForkedTestingApi { - return &ForkedTestingApi{ - svc: svc, - } -} - -func (f *ForkedTestingApi) forkRouteTestRuleConfig(c *models.ReqContext, body apimodels.TestRulePayload, dsUID string) response.Response { - return f.svc.RouteTestRuleConfig(c, body, dsUID) -} - -func (f *ForkedTestingApi) forkRouteTestRuleGrafanaConfig(c *models.ReqContext, body apimodels.TestRulePayload) response.Response { - return f.svc.RouteTestGrafanaRuleConfig(c, body) -} - -func (f *ForkedTestingApi) forkRouteEvalQueries(c *models.ReqContext, body apimodels.EvalQueriesPayload) response.Response { - return f.svc.RouteEvalQueries(c, body) -} diff --git a/pkg/services/ngalert/api/forked_am.go b/pkg/services/ngalert/api/forking_alertmanager.go similarity index 50% rename from pkg/services/ngalert/api/forked_am.go rename to pkg/services/ngalert/api/forking_alertmanager.go index 28ad7442f7d..32c835217af 100644 --- a/pkg/services/ngalert/api/forked_am.go +++ b/pkg/services/ngalert/api/forking_alertmanager.go @@ -9,22 +9,22 @@ import ( apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) -type ForkedAlertmanagerApi struct { +type AlertmanagerApiHandler struct { AMSvc *LotexAM GrafanaSvc *AlertmanagerSrv DatasourceCache datasources.CacheService } -// NewForkedAM implements a set of routes that proxy to various Alertmanager-compatible backends. -func NewForkedAM(datasourceCache datasources.CacheService, proxy *LotexAM, grafana *AlertmanagerSrv) *ForkedAlertmanagerApi { - return &ForkedAlertmanagerApi{ +// NewForkingAM implements a set of routes that proxy to various Alertmanager-compatible backends. +func NewForkingAM(datasourceCache datasources.CacheService, proxy *LotexAM, grafana *AlertmanagerSrv) *AlertmanagerApiHandler { + return &AlertmanagerApiHandler{ AMSvc: proxy, GrafanaSvc: grafana, DatasourceCache: datasourceCache, } } -func (f *ForkedAlertmanagerApi) getService(ctx *models.ReqContext) (*LotexAM, error) { +func (f *AlertmanagerApiHandler) getService(ctx *models.ReqContext) (*LotexAM, error) { t, err := backendTypeByUID(ctx, f.DatasourceCache) if err != nil { return nil, err @@ -38,7 +38,7 @@ func (f *ForkedAlertmanagerApi) getService(ctx *models.ReqContext) (*LotexAM, er } } -func (f *ForkedAlertmanagerApi) forkRouteGetAMStatus(ctx *models.ReqContext, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetAMStatus(ctx *models.ReqContext, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return response.Error(400, err.Error(), nil) @@ -47,7 +47,7 @@ func (f *ForkedAlertmanagerApi) forkRouteGetAMStatus(ctx *models.ReqContext, dsU return s.RouteGetAMStatus(ctx) } -func (f *ForkedAlertmanagerApi) forkRouteCreateSilence(ctx *models.ReqContext, body apimodels.PostableSilence, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteCreateSilence(ctx *models.ReqContext, body apimodels.PostableSilence, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return ErrResp(400, err, "") @@ -56,7 +56,7 @@ func (f *ForkedAlertmanagerApi) forkRouteCreateSilence(ctx *models.ReqContext, b return s.RouteCreateSilence(ctx, body) } -func (f *ForkedAlertmanagerApi) forkRouteDeleteAlertingConfig(ctx *models.ReqContext, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteDeleteAlertingConfig(ctx *models.ReqContext, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return ErrResp(400, err, "") @@ -65,7 +65,7 @@ func (f *ForkedAlertmanagerApi) forkRouteDeleteAlertingConfig(ctx *models.ReqCon return s.RouteDeleteAlertingConfig(ctx) } -func (f *ForkedAlertmanagerApi) forkRouteDeleteSilence(ctx *models.ReqContext, silenceID string, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteDeleteSilence(ctx *models.ReqContext, silenceID string, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return ErrResp(400, err, "") @@ -74,7 +74,7 @@ func (f *ForkedAlertmanagerApi) forkRouteDeleteSilence(ctx *models.ReqContext, s return s.RouteDeleteSilence(ctx, silenceID) } -func (f *ForkedAlertmanagerApi) forkRouteGetAlertingConfig(ctx *models.ReqContext, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetAlertingConfig(ctx *models.ReqContext, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return ErrResp(400, err, "") @@ -83,7 +83,7 @@ func (f *ForkedAlertmanagerApi) forkRouteGetAlertingConfig(ctx *models.ReqContex return s.RouteGetAlertingConfig(ctx) } -func (f *ForkedAlertmanagerApi) forkRouteGetAMAlertGroups(ctx *models.ReqContext, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetAMAlertGroups(ctx *models.ReqContext, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return ErrResp(400, err, "") @@ -92,7 +92,7 @@ func (f *ForkedAlertmanagerApi) forkRouteGetAMAlertGroups(ctx *models.ReqContext return s.RouteGetAMAlertGroups(ctx) } -func (f *ForkedAlertmanagerApi) forkRouteGetAMAlerts(ctx *models.ReqContext, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetAMAlerts(ctx *models.ReqContext, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return ErrResp(400, err, "") @@ -101,7 +101,7 @@ func (f *ForkedAlertmanagerApi) forkRouteGetAMAlerts(ctx *models.ReqContext, dsU return s.RouteGetAMAlerts(ctx) } -func (f *ForkedAlertmanagerApi) forkRouteGetSilence(ctx *models.ReqContext, silenceID string, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetSilence(ctx *models.ReqContext, silenceID string, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return ErrResp(400, err, "") @@ -110,7 +110,7 @@ func (f *ForkedAlertmanagerApi) forkRouteGetSilence(ctx *models.ReqContext, sile return s.RouteGetSilence(ctx, silenceID) } -func (f *ForkedAlertmanagerApi) forkRouteGetSilences(ctx *models.ReqContext, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetSilences(ctx *models.ReqContext, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return ErrResp(400, err, "") @@ -119,7 +119,7 @@ func (f *ForkedAlertmanagerApi) forkRouteGetSilences(ctx *models.ReqContext, dsU return s.RouteGetSilences(ctx) } -func (f *ForkedAlertmanagerApi) forkRoutePostAlertingConfig(ctx *models.ReqContext, body apimodels.PostableUserConfig, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRoutePostAlertingConfig(ctx *models.ReqContext, body apimodels.PostableUserConfig, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return ErrResp(400, err, "") @@ -137,7 +137,7 @@ func (f *ForkedAlertmanagerApi) forkRoutePostAlertingConfig(ctx *models.ReqConte return s.RoutePostAlertingConfig(ctx, body) } -func (f *ForkedAlertmanagerApi) forkRoutePostAMAlerts(ctx *models.ReqContext, body apimodels.PostableAlerts, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRoutePostAMAlerts(ctx *models.ReqContext, body apimodels.PostableAlerts, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return ErrResp(400, err, "") @@ -146,7 +146,7 @@ func (f *ForkedAlertmanagerApi) forkRoutePostAMAlerts(ctx *models.ReqContext, bo return s.RoutePostAMAlerts(ctx, body) } -func (f *ForkedAlertmanagerApi) forkRoutePostTestReceivers(ctx *models.ReqContext, body apimodels.TestReceiversConfigBodyParams, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRoutePostTestReceivers(ctx *models.ReqContext, body apimodels.TestReceiversConfigBodyParams, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return ErrResp(400, err, "") @@ -155,50 +155,50 @@ func (f *ForkedAlertmanagerApi) forkRoutePostTestReceivers(ctx *models.ReqContex return s.RoutePostTestReceivers(ctx, body) } -func (f *ForkedAlertmanagerApi) forkRouteDeleteGrafanaSilence(ctx *models.ReqContext, id string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteDeleteGrafanaSilence(ctx *models.ReqContext, id string) response.Response { return f.GrafanaSvc.RouteDeleteSilence(ctx, id) } -func (f *ForkedAlertmanagerApi) forkRouteDeleteGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteDeleteGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { return f.GrafanaSvc.RouteDeleteAlertingConfig(ctx) } -func (f *ForkedAlertmanagerApi) forkRouteCreateGrafanaSilence(ctx *models.ReqContext, body apimodels.PostableSilence) response.Response { +func (f *AlertmanagerApiHandler) handleRouteCreateGrafanaSilence(ctx *models.ReqContext, body apimodels.PostableSilence) response.Response { return f.GrafanaSvc.RouteCreateSilence(ctx, body) } -func (f *ForkedAlertmanagerApi) forkRouteGetGrafanaAMStatus(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAMStatus(ctx *models.ReqContext) response.Response { return f.GrafanaSvc.RouteGetAMStatus(ctx) } -func (f *ForkedAlertmanagerApi) forkRouteGetGrafanaAMAlerts(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAMAlerts(ctx *models.ReqContext) response.Response { return f.GrafanaSvc.RouteGetAMAlerts(ctx) } -func (f *ForkedAlertmanagerApi) forkRouteGetGrafanaAMAlertGroups(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAMAlertGroups(ctx *models.ReqContext) response.Response { return f.GrafanaSvc.RouteGetAMAlertGroups(ctx) } -func (f *ForkedAlertmanagerApi) forkRouteGetGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { return f.GrafanaSvc.RouteGetAlertingConfig(ctx) } -func (f *ForkedAlertmanagerApi) forkRouteGetGrafanaSilence(ctx *models.ReqContext, id string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaSilence(ctx *models.ReqContext, id string) response.Response { return f.GrafanaSvc.RouteGetSilence(ctx, id) } -func (f *ForkedAlertmanagerApi) forkRouteGetGrafanaSilences(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaSilences(ctx *models.ReqContext) response.Response { return f.GrafanaSvc.RouteGetSilences(ctx) } -func (f *ForkedAlertmanagerApi) forkRoutePostGrafanaAMAlerts(ctx *models.ReqContext, conf apimodels.PostableAlerts) response.Response { +func (f *AlertmanagerApiHandler) handleRoutePostGrafanaAMAlerts(ctx *models.ReqContext, conf apimodels.PostableAlerts) response.Response { return f.GrafanaSvc.RoutePostAMAlerts(ctx, conf) } -func (f *ForkedAlertmanagerApi) forkRoutePostGrafanaAlertingConfig(ctx *models.ReqContext, conf apimodels.PostableUserConfig) response.Response { +func (f *AlertmanagerApiHandler) handleRoutePostGrafanaAlertingConfig(ctx *models.ReqContext, conf apimodels.PostableUserConfig) response.Response { return f.GrafanaSvc.RoutePostAlertingConfig(ctx, conf) } -func (f *ForkedAlertmanagerApi) forkRoutePostTestGrafanaReceivers(ctx *models.ReqContext, conf apimodels.TestReceiversConfigBodyParams) response.Response { +func (f *AlertmanagerApiHandler) handleRoutePostTestGrafanaReceivers(ctx *models.ReqContext, conf apimodels.TestReceiversConfigBodyParams) response.Response { return f.GrafanaSvc.RoutePostTestReceivers(ctx, conf) } diff --git a/pkg/services/ngalert/api/forked_prom.go b/pkg/services/ngalert/api/forking_prometheus.go similarity index 60% rename from pkg/services/ngalert/api/forked_prom.go rename to pkg/services/ngalert/api/forking_prometheus.go index 7009969c43a..c192e696de6 100644 --- a/pkg/services/ngalert/api/forked_prom.go +++ b/pkg/services/ngalert/api/forking_prometheus.go @@ -9,22 +9,22 @@ import ( apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) -type ForkedPrometheusApi struct { +type PrometheusApiHandler struct { ProxySvc *LotexProm GrafanaSvc *PrometheusSrv DatasourceCache datasources.CacheService } -// NewForkedProm implements a set of routes that proxy to various Prometheus-compatible backends. -func NewForkedProm(datasourceCache datasources.CacheService, proxy *LotexProm, grafana *PrometheusSrv) *ForkedPrometheusApi { - return &ForkedPrometheusApi{ +// NewForkingProm implements a set of routes that proxy to various Prometheus-compatible backends. +func NewForkingProm(datasourceCache datasources.CacheService, proxy *LotexProm, grafana *PrometheusSrv) *PrometheusApiHandler { + return &PrometheusApiHandler{ ProxySvc: proxy, GrafanaSvc: grafana, DatasourceCache: datasourceCache, } } -func (f *ForkedPrometheusApi) forkRouteGetAlertStatuses(ctx *models.ReqContext, dsUID string) response.Response { +func (f *PrometheusApiHandler) handleRouteGetAlertStatuses(ctx *models.ReqContext, dsUID string) response.Response { t, err := backendTypeByUID(ctx, f.DatasourceCache) if err != nil { return ErrResp(400, err, "") @@ -38,7 +38,7 @@ func (f *ForkedPrometheusApi) forkRouteGetAlertStatuses(ctx *models.ReqContext, } } -func (f *ForkedPrometheusApi) forkRouteGetRuleStatuses(ctx *models.ReqContext, dsUID string) response.Response { +func (f *PrometheusApiHandler) handleRouteGetRuleStatuses(ctx *models.ReqContext, dsUID string) response.Response { t, err := backendTypeByUID(ctx, f.DatasourceCache) if err != nil { return ErrResp(400, err, "") @@ -52,10 +52,10 @@ func (f *ForkedPrometheusApi) forkRouteGetRuleStatuses(ctx *models.ReqContext, d } } -func (f *ForkedPrometheusApi) forkRouteGetGrafanaAlertStatuses(ctx *models.ReqContext) response.Response { +func (f *PrometheusApiHandler) handleRouteGetGrafanaAlertStatuses(ctx *models.ReqContext) response.Response { return f.GrafanaSvc.RouteGetAlertStatuses(ctx) } -func (f *ForkedPrometheusApi) forkRouteGetGrafanaRuleStatuses(ctx *models.ReqContext) response.Response { +func (f *PrometheusApiHandler) handleRouteGetGrafanaRuleStatuses(ctx *models.ReqContext) response.Response { return f.GrafanaSvc.RouteGetRuleStatuses(ctx) } diff --git a/pkg/services/ngalert/api/fork_ruler.go b/pkg/services/ngalert/api/forking_ruler.go similarity index 64% rename from pkg/services/ngalert/api/fork_ruler.go rename to pkg/services/ngalert/api/forking_ruler.go index 739370d3a04..44b06c85a4f 100644 --- a/pkg/services/ngalert/api/fork_ruler.go +++ b/pkg/services/ngalert/api/forking_ruler.go @@ -9,23 +9,22 @@ import ( apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) -// ForkedRulerApi will validate and proxy requests to the correct backend type depending on the datasource. -type ForkedRulerApi struct { +// RulerApiHandler will validate and proxy requests to the correct backend type depending on the datasource. +type RulerApiHandler struct { LotexRuler *LotexRuler GrafanaRuler *RulerSrv DatasourceCache datasources.CacheService } -// NewForkedRuler implements a set of routes that proxy to various Cortex Ruler-compatible backends. -func NewForkedRuler(datasourceCache datasources.CacheService, lotex *LotexRuler, grafana *RulerSrv) *ForkedRulerApi { - return &ForkedRulerApi{ +func NewForkingRuler(datasourceCache datasources.CacheService, lotex *LotexRuler, grafana *RulerSrv) *RulerApiHandler { + return &RulerApiHandler{ LotexRuler: lotex, GrafanaRuler: grafana, DatasourceCache: datasourceCache, } } -func (f *ForkedRulerApi) forkRouteDeleteNamespaceRulesConfig(ctx *models.ReqContext, dsUID, namespace string) response.Response { +func (f *RulerApiHandler) handleRouteDeleteNamespaceRulesConfig(ctx *models.ReqContext, dsUID, namespace string) response.Response { t, err := backendTypeByUID(ctx, f.DatasourceCache) if err != nil { return ErrResp(400, err, "") @@ -38,7 +37,7 @@ func (f *ForkedRulerApi) forkRouteDeleteNamespaceRulesConfig(ctx *models.ReqCont } } -func (f *ForkedRulerApi) forkRouteDeleteRuleGroupConfig(ctx *models.ReqContext, dsUID, namespace, group string) response.Response { +func (f *RulerApiHandler) handleRouteDeleteRuleGroupConfig(ctx *models.ReqContext, dsUID, namespace, group string) response.Response { t, err := backendTypeByUID(ctx, f.DatasourceCache) if err != nil { return ErrResp(400, err, "") @@ -51,7 +50,7 @@ func (f *ForkedRulerApi) forkRouteDeleteRuleGroupConfig(ctx *models.ReqContext, } } -func (f *ForkedRulerApi) forkRouteGetNamespaceRulesConfig(ctx *models.ReqContext, dsUID, namespace string) response.Response { +func (f *RulerApiHandler) handleRouteGetNamespaceRulesConfig(ctx *models.ReqContext, dsUID, namespace string) response.Response { t, err := backendTypeByUID(ctx, f.DatasourceCache) if err != nil { return ErrResp(400, err, "") @@ -64,7 +63,7 @@ func (f *ForkedRulerApi) forkRouteGetNamespaceRulesConfig(ctx *models.ReqContext } } -func (f *ForkedRulerApi) forkRouteGetRulegGroupConfig(ctx *models.ReqContext, dsUID, namespace, group string) response.Response { +func (f *RulerApiHandler) handleRouteGetRulegGroupConfig(ctx *models.ReqContext, dsUID, namespace, group string) response.Response { t, err := backendTypeByUID(ctx, f.DatasourceCache) if err != nil { return ErrResp(400, err, "") @@ -77,7 +76,7 @@ func (f *ForkedRulerApi) forkRouteGetRulegGroupConfig(ctx *models.ReqContext, ds } } -func (f *ForkedRulerApi) forkRouteGetRulesConfig(ctx *models.ReqContext, dsUID string) response.Response { +func (f *RulerApiHandler) handleRouteGetRulesConfig(ctx *models.ReqContext, dsUID string) response.Response { t, err := backendTypeByUID(ctx, f.DatasourceCache) if err != nil { return ErrResp(400, err, "") @@ -90,7 +89,7 @@ func (f *ForkedRulerApi) forkRouteGetRulesConfig(ctx *models.ReqContext, dsUID s } } -func (f *ForkedRulerApi) forkRoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig, dsUID, namespace string) response.Response { +func (f *RulerApiHandler) handleRoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig, dsUID, namespace string) response.Response { backendType, err := backendTypeByUID(ctx, f.DatasourceCache) if err != nil { return ErrResp(400, err, "") @@ -109,27 +108,27 @@ func (f *ForkedRulerApi) forkRoutePostNameRulesConfig(ctx *models.ReqContext, co } } -func (f *ForkedRulerApi) forkRouteDeleteNamespaceGrafanaRulesConfig(ctx *models.ReqContext, namespace string) response.Response { +func (f *RulerApiHandler) handleRouteDeleteNamespaceGrafanaRulesConfig(ctx *models.ReqContext, namespace string) response.Response { return f.GrafanaRuler.RouteDeleteAlertRules(ctx, namespace, "") } -func (f *ForkedRulerApi) forkRouteDeleteGrafanaRuleGroupConfig(ctx *models.ReqContext, namespace, groupName string) response.Response { +func (f *RulerApiHandler) handleRouteDeleteGrafanaRuleGroupConfig(ctx *models.ReqContext, namespace, groupName string) response.Response { return f.GrafanaRuler.RouteDeleteAlertRules(ctx, namespace, groupName) } -func (f *ForkedRulerApi) forkRouteGetNamespaceGrafanaRulesConfig(ctx *models.ReqContext, namespace string) response.Response { +func (f *RulerApiHandler) handleRouteGetNamespaceGrafanaRulesConfig(ctx *models.ReqContext, namespace string) response.Response { return f.GrafanaRuler.RouteGetNamespaceRulesConfig(ctx, namespace) } -func (f *ForkedRulerApi) forkRouteGetGrafanaRuleGroupConfig(ctx *models.ReqContext, namespace, group string) response.Response { +func (f *RulerApiHandler) handleRouteGetGrafanaRuleGroupConfig(ctx *models.ReqContext, namespace, group string) response.Response { return f.GrafanaRuler.RouteGetRulesGroupConfig(ctx, namespace, group) } -func (f *ForkedRulerApi) forkRouteGetGrafanaRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) handleRouteGetGrafanaRulesConfig(ctx *models.ReqContext) response.Response { return f.GrafanaRuler.RouteGetRulesConfig(ctx) } -func (f *ForkedRulerApi) forkRoutePostNameGrafanaRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig, namespace string) response.Response { +func (f *RulerApiHandler) handleRoutePostNameGrafanaRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig, namespace string) response.Response { payloadType := conf.Type() if payloadType != apimodels.GrafanaBackend { return ErrResp(400, fmt.Errorf("unexpected backend type (%v) vs payload type (%v)", apimodels.GrafanaBackend, payloadType), "") diff --git a/pkg/services/ngalert/api/generated_base_api_alertmanager.go b/pkg/services/ngalert/api/generated_base_api_alertmanager.go index c983f3f85fd..21f1c3cdc64 100644 --- a/pkg/services/ngalert/api/generated_base_api_alertmanager.go +++ b/pkg/services/ngalert/api/generated_base_api_alertmanager.go @@ -18,7 +18,7 @@ import ( "github.com/grafana/grafana/pkg/web" ) -type AlertmanagerApiForkingService interface { +type AlertmanagerApi interface { RouteCreateGrafanaSilence(*models.ReqContext) response.Response RouteCreateSilence(*models.ReqContext) response.Response RouteDeleteAlertingConfig(*models.ReqContext) response.Response @@ -45,128 +45,150 @@ type AlertmanagerApiForkingService interface { RoutePostTestReceivers(*models.ReqContext) response.Response } -func (f *ForkedAlertmanagerApi) RouteCreateGrafanaSilence(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteCreateGrafanaSilence(ctx *models.ReqContext) response.Response { + // Parse Request Body conf := apimodels.PostableSilence{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRouteCreateGrafanaSilence(ctx, conf) + return f.handleRouteCreateGrafanaSilence(ctx, conf) } -func (f *ForkedAlertmanagerApi) RouteCreateSilence(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteCreateSilence(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] + // Parse Request Body conf := apimodels.PostableSilence{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRouteCreateSilence(ctx, conf, datasourceUIDParam) + return f.handleRouteCreateSilence(ctx, conf, datasourceUIDParam) } -func (f *ForkedAlertmanagerApi) RouteDeleteAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteDeleteAlertingConfig(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] - return f.forkRouteDeleteAlertingConfig(ctx, datasourceUIDParam) + return f.handleRouteDeleteAlertingConfig(ctx, datasourceUIDParam) } -func (f *ForkedAlertmanagerApi) RouteDeleteGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { - return f.forkRouteDeleteGrafanaAlertingConfig(ctx) +func (f *AlertmanagerApiHandler) RouteDeleteGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { + return f.handleRouteDeleteGrafanaAlertingConfig(ctx) } -func (f *ForkedAlertmanagerApi) RouteDeleteGrafanaSilence(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteDeleteGrafanaSilence(ctx *models.ReqContext) response.Response { + // Parse Path Parameters silenceIdParam := web.Params(ctx.Req)[":SilenceId"] - return f.forkRouteDeleteGrafanaSilence(ctx, silenceIdParam) + return f.handleRouteDeleteGrafanaSilence(ctx, silenceIdParam) } -func (f *ForkedAlertmanagerApi) RouteDeleteSilence(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteDeleteSilence(ctx *models.ReqContext) response.Response { + // Parse Path Parameters silenceIdParam := web.Params(ctx.Req)[":SilenceId"] datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] - return f.forkRouteDeleteSilence(ctx, silenceIdParam, datasourceUIDParam) + return f.handleRouteDeleteSilence(ctx, silenceIdParam, datasourceUIDParam) } -func (f *ForkedAlertmanagerApi) RouteGetAMAlertGroups(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetAMAlertGroups(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] - return f.forkRouteGetAMAlertGroups(ctx, datasourceUIDParam) + return f.handleRouteGetAMAlertGroups(ctx, datasourceUIDParam) } -func (f *ForkedAlertmanagerApi) RouteGetAMAlerts(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetAMAlerts(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] - return f.forkRouteGetAMAlerts(ctx, datasourceUIDParam) + return f.handleRouteGetAMAlerts(ctx, datasourceUIDParam) } -func (f *ForkedAlertmanagerApi) RouteGetAMStatus(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetAMStatus(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] - return f.forkRouteGetAMStatus(ctx, datasourceUIDParam) + return f.handleRouteGetAMStatus(ctx, datasourceUIDParam) } -func (f *ForkedAlertmanagerApi) RouteGetAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetAlertingConfig(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] - return f.forkRouteGetAlertingConfig(ctx, datasourceUIDParam) + return f.handleRouteGetAlertingConfig(ctx, datasourceUIDParam) } -func (f *ForkedAlertmanagerApi) RouteGetGrafanaAMAlertGroups(ctx *models.ReqContext) response.Response { - return f.forkRouteGetGrafanaAMAlertGroups(ctx) +func (f *AlertmanagerApiHandler) RouteGetGrafanaAMAlertGroups(ctx *models.ReqContext) response.Response { + return f.handleRouteGetGrafanaAMAlertGroups(ctx) } -func (f *ForkedAlertmanagerApi) RouteGetGrafanaAMAlerts(ctx *models.ReqContext) response.Response { - return f.forkRouteGetGrafanaAMAlerts(ctx) +func (f *AlertmanagerApiHandler) RouteGetGrafanaAMAlerts(ctx *models.ReqContext) response.Response { + return f.handleRouteGetGrafanaAMAlerts(ctx) } -func (f *ForkedAlertmanagerApi) RouteGetGrafanaAMStatus(ctx *models.ReqContext) response.Response { - return f.forkRouteGetGrafanaAMStatus(ctx) +func (f *AlertmanagerApiHandler) RouteGetGrafanaAMStatus(ctx *models.ReqContext) response.Response { + return f.handleRouteGetGrafanaAMStatus(ctx) } -func (f *ForkedAlertmanagerApi) RouteGetGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { - return f.forkRouteGetGrafanaAlertingConfig(ctx) +func (f *AlertmanagerApiHandler) RouteGetGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { + return f.handleRouteGetGrafanaAlertingConfig(ctx) } -func (f *ForkedAlertmanagerApi) RouteGetGrafanaSilence(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetGrafanaSilence(ctx *models.ReqContext) response.Response { + // Parse Path Parameters silenceIdParam := web.Params(ctx.Req)[":SilenceId"] - return f.forkRouteGetGrafanaSilence(ctx, silenceIdParam) + return f.handleRouteGetGrafanaSilence(ctx, silenceIdParam) } -func (f *ForkedAlertmanagerApi) RouteGetGrafanaSilences(ctx *models.ReqContext) response.Response { - return f.forkRouteGetGrafanaSilences(ctx) +func (f *AlertmanagerApiHandler) RouteGetGrafanaSilences(ctx *models.ReqContext) response.Response { + return f.handleRouteGetGrafanaSilences(ctx) } -func (f *ForkedAlertmanagerApi) RouteGetSilence(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetSilence(ctx *models.ReqContext) response.Response { + // Parse Path Parameters silenceIdParam := web.Params(ctx.Req)[":SilenceId"] datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] - return f.forkRouteGetSilence(ctx, silenceIdParam, datasourceUIDParam) + return f.handleRouteGetSilence(ctx, silenceIdParam, datasourceUIDParam) } -func (f *ForkedAlertmanagerApi) RouteGetSilences(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetSilences(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] - return f.forkRouteGetSilences(ctx, datasourceUIDParam) + return f.handleRouteGetSilences(ctx, datasourceUIDParam) } -func (f *ForkedAlertmanagerApi) RoutePostAMAlerts(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RoutePostAMAlerts(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] + // Parse Request Body conf := apimodels.PostableAlerts{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePostAMAlerts(ctx, conf, datasourceUIDParam) + return f.handleRoutePostAMAlerts(ctx, conf, datasourceUIDParam) } -func (f *ForkedAlertmanagerApi) RoutePostAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RoutePostAlertingConfig(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] + // Parse Request Body conf := apimodels.PostableUserConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePostAlertingConfig(ctx, conf, datasourceUIDParam) + return f.handleRoutePostAlertingConfig(ctx, conf, datasourceUIDParam) } -func (f *ForkedAlertmanagerApi) RoutePostGrafanaAMAlerts(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RoutePostGrafanaAMAlerts(ctx *models.ReqContext) response.Response { + // Parse Request Body conf := apimodels.PostableAlerts{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePostGrafanaAMAlerts(ctx, conf) + return f.handleRoutePostGrafanaAMAlerts(ctx, conf) } -func (f *ForkedAlertmanagerApi) RoutePostGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RoutePostGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { + // Parse Request Body conf := apimodels.PostableUserConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePostGrafanaAlertingConfig(ctx, conf) + return f.handleRoutePostGrafanaAlertingConfig(ctx, conf) } -func (f *ForkedAlertmanagerApi) RoutePostTestGrafanaReceivers(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RoutePostTestGrafanaReceivers(ctx *models.ReqContext) response.Response { + // Parse Request Body conf := apimodels.TestReceiversConfigBodyParams{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePostTestGrafanaReceivers(ctx, conf) + return f.handleRoutePostTestGrafanaReceivers(ctx, conf) } -func (f *ForkedAlertmanagerApi) RoutePostTestReceivers(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RoutePostTestReceivers(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] + // Parse Request Body conf := apimodels.TestReceiversConfigBodyParams{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePostTestReceivers(ctx, conf, datasourceUIDParam) + return f.handleRoutePostTestReceivers(ctx, conf, datasourceUIDParam) } -func (api *API) RegisterAlertmanagerApiEndpoints(srv AlertmanagerApiForkingService, m *metrics.API) { +func (api *API) RegisterAlertmanagerApiEndpoints(srv AlertmanagerApi, m *metrics.API) { api.RouteRegister.Group("", func(group routing.RouteRegister) { group.Post( toMacaronPath("/api/alertmanager/grafana/api/v2/silences"), diff --git a/pkg/services/ngalert/api/generated_base_api_configuration.go b/pkg/services/ngalert/api/generated_base_api_configuration.go index 084c27dec82..d4b1788082e 100644 --- a/pkg/services/ngalert/api/generated_base_api_configuration.go +++ b/pkg/services/ngalert/api/generated_base_api_configuration.go @@ -18,31 +18,32 @@ import ( "github.com/grafana/grafana/pkg/web" ) -type ConfigurationApiForkingService interface { +type ConfigurationApi interface { RouteDeleteNGalertConfig(*models.ReqContext) response.Response RouteGetAlertmanagers(*models.ReqContext) response.Response RouteGetNGalertConfig(*models.ReqContext) response.Response RoutePostNGalertConfig(*models.ReqContext) response.Response } -func (f *ForkedConfigurationApi) RouteDeleteNGalertConfig(ctx *models.ReqContext) response.Response { - return f.forkRouteDeleteNGalertConfig(ctx) +func (f *ConfigurationApiHandler) RouteDeleteNGalertConfig(ctx *models.ReqContext) response.Response { + return f.handleRouteDeleteNGalertConfig(ctx) } -func (f *ForkedConfigurationApi) RouteGetAlertmanagers(ctx *models.ReqContext) response.Response { - return f.forkRouteGetAlertmanagers(ctx) +func (f *ConfigurationApiHandler) RouteGetAlertmanagers(ctx *models.ReqContext) response.Response { + return f.handleRouteGetAlertmanagers(ctx) } -func (f *ForkedConfigurationApi) RouteGetNGalertConfig(ctx *models.ReqContext) response.Response { - return f.forkRouteGetNGalertConfig(ctx) +func (f *ConfigurationApiHandler) RouteGetNGalertConfig(ctx *models.ReqContext) response.Response { + return f.handleRouteGetNGalertConfig(ctx) } -func (f *ForkedConfigurationApi) RoutePostNGalertConfig(ctx *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) RoutePostNGalertConfig(ctx *models.ReqContext) response.Response { + // Parse Request Body conf := apimodels.PostableNGalertConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePostNGalertConfig(ctx, conf) + return f.handleRoutePostNGalertConfig(ctx, conf) } -func (api *API) RegisterConfigurationApiEndpoints(srv ConfigurationApiForkingService, m *metrics.API) { +func (api *API) RegisterConfigurationApiEndpoints(srv ConfigurationApi, m *metrics.API) { api.RouteRegister.Group("", func(group routing.RouteRegister) { group.Delete( toMacaronPath("/api/v1/ngalert/admin_config"), diff --git a/pkg/services/ngalert/api/generated_base_api_prometheus.go b/pkg/services/ngalert/api/generated_base_api_prometheus.go index f9018b7b6ff..05297027bc3 100644 --- a/pkg/services/ngalert/api/generated_base_api_prometheus.go +++ b/pkg/services/ngalert/api/generated_base_api_prometheus.go @@ -17,29 +17,31 @@ import ( "github.com/grafana/grafana/pkg/web" ) -type PrometheusApiForkingService interface { +type PrometheusApi interface { RouteGetAlertStatuses(*models.ReqContext) response.Response RouteGetGrafanaAlertStatuses(*models.ReqContext) response.Response RouteGetGrafanaRuleStatuses(*models.ReqContext) response.Response RouteGetRuleStatuses(*models.ReqContext) response.Response } -func (f *ForkedPrometheusApi) RouteGetAlertStatuses(ctx *models.ReqContext) response.Response { +func (f *PrometheusApiHandler) RouteGetAlertStatuses(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] - return f.forkRouteGetAlertStatuses(ctx, datasourceUIDParam) + return f.handleRouteGetAlertStatuses(ctx, datasourceUIDParam) } -func (f *ForkedPrometheusApi) RouteGetGrafanaAlertStatuses(ctx *models.ReqContext) response.Response { - return f.forkRouteGetGrafanaAlertStatuses(ctx) +func (f *PrometheusApiHandler) RouteGetGrafanaAlertStatuses(ctx *models.ReqContext) response.Response { + return f.handleRouteGetGrafanaAlertStatuses(ctx) } -func (f *ForkedPrometheusApi) RouteGetGrafanaRuleStatuses(ctx *models.ReqContext) response.Response { - return f.forkRouteGetGrafanaRuleStatuses(ctx) +func (f *PrometheusApiHandler) RouteGetGrafanaRuleStatuses(ctx *models.ReqContext) response.Response { + return f.handleRouteGetGrafanaRuleStatuses(ctx) } -func (f *ForkedPrometheusApi) RouteGetRuleStatuses(ctx *models.ReqContext) response.Response { +func (f *PrometheusApiHandler) RouteGetRuleStatuses(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] - return f.forkRouteGetRuleStatuses(ctx, datasourceUIDParam) + return f.handleRouteGetRuleStatuses(ctx, datasourceUIDParam) } -func (api *API) RegisterPrometheusApiEndpoints(srv PrometheusApiForkingService, m *metrics.API) { +func (api *API) RegisterPrometheusApiEndpoints(srv PrometheusApi, m *metrics.API) { api.RouteRegister.Group("", func(group routing.RouteRegister) { group.Get( toMacaronPath("/api/prometheus/{DatasourceUID}/api/v1/alerts"), diff --git a/pkg/services/ngalert/api/generated_base_api_provisioning.go b/pkg/services/ngalert/api/generated_base_api_provisioning.go index e3efd6a1713..484524672b1 100644 --- a/pkg/services/ngalert/api/generated_base_api_provisioning.go +++ b/pkg/services/ngalert/api/generated_base_api_provisioning.go @@ -18,7 +18,7 @@ import ( "github.com/grafana/grafana/pkg/web" ) -type ProvisioningApiForkingService interface { +type ProvisioningApi interface { RouteDeleteAlertRule(*models.ReqContext) response.Response RouteDeleteContactpoints(*models.ReqContext) response.Response RouteDeleteMuteTiming(*models.ReqContext) response.Response @@ -43,125 +43,147 @@ type ProvisioningApiForkingService interface { RouteResetPolicyTree(*models.ReqContext) response.Response } -func (f *ForkedProvisioningApi) RouteDeleteAlertRule(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteDeleteAlertRule(ctx *models.ReqContext) response.Response { + // Parse Path Parameters uIDParam := web.Params(ctx.Req)[":UID"] - return f.forkRouteDeleteAlertRule(ctx, uIDParam) + return f.handleRouteDeleteAlertRule(ctx, uIDParam) } -func (f *ForkedProvisioningApi) RouteDeleteContactpoints(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteDeleteContactpoints(ctx *models.ReqContext) response.Response { + // Parse Path Parameters uIDParam := web.Params(ctx.Req)[":UID"] - return f.forkRouteDeleteContactpoints(ctx, uIDParam) + return f.handleRouteDeleteContactpoints(ctx, uIDParam) } -func (f *ForkedProvisioningApi) RouteDeleteMuteTiming(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteDeleteMuteTiming(ctx *models.ReqContext) response.Response { + // Parse Path Parameters nameParam := web.Params(ctx.Req)[":name"] - return f.forkRouteDeleteMuteTiming(ctx, nameParam) + return f.handleRouteDeleteMuteTiming(ctx, nameParam) } -func (f *ForkedProvisioningApi) RouteDeleteTemplate(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteDeleteTemplate(ctx *models.ReqContext) response.Response { + // Parse Path Parameters nameParam := web.Params(ctx.Req)[":name"] - return f.forkRouteDeleteTemplate(ctx, nameParam) + return f.handleRouteDeleteTemplate(ctx, nameParam) } -func (f *ForkedProvisioningApi) RouteGetAlertRule(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetAlertRule(ctx *models.ReqContext) response.Response { + // Parse Path Parameters uIDParam := web.Params(ctx.Req)[":UID"] - return f.forkRouteGetAlertRule(ctx, uIDParam) + return f.handleRouteGetAlertRule(ctx, uIDParam) } -func (f *ForkedProvisioningApi) RouteGetAlertRuleGroup(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetAlertRuleGroup(ctx *models.ReqContext) response.Response { + // Parse Path Parameters folderUIDParam := web.Params(ctx.Req)[":FolderUID"] groupParam := web.Params(ctx.Req)[":Group"] - return f.forkRouteGetAlertRuleGroup(ctx, folderUIDParam, groupParam) + return f.handleRouteGetAlertRuleGroup(ctx, folderUIDParam, groupParam) } -func (f *ForkedProvisioningApi) RouteGetContactpoints(ctx *models.ReqContext) response.Response { - return f.forkRouteGetContactpoints(ctx) +func (f *ProvisioningApiHandler) RouteGetContactpoints(ctx *models.ReqContext) response.Response { + return f.handleRouteGetContactpoints(ctx) } -func (f *ForkedProvisioningApi) RouteGetMuteTiming(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetMuteTiming(ctx *models.ReqContext) response.Response { + // Parse Path Parameters nameParam := web.Params(ctx.Req)[":name"] - return f.forkRouteGetMuteTiming(ctx, nameParam) + return f.handleRouteGetMuteTiming(ctx, nameParam) } -func (f *ForkedProvisioningApi) RouteGetMuteTimings(ctx *models.ReqContext) response.Response { - return f.forkRouteGetMuteTimings(ctx) +func (f *ProvisioningApiHandler) RouteGetMuteTimings(ctx *models.ReqContext) response.Response { + return f.handleRouteGetMuteTimings(ctx) } -func (f *ForkedProvisioningApi) RouteGetPolicyTree(ctx *models.ReqContext) response.Response { - return f.forkRouteGetPolicyTree(ctx) +func (f *ProvisioningApiHandler) RouteGetPolicyTree(ctx *models.ReqContext) response.Response { + return f.handleRouteGetPolicyTree(ctx) } -func (f *ForkedProvisioningApi) RouteGetTemplate(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetTemplate(ctx *models.ReqContext) response.Response { + // Parse Path Parameters nameParam := web.Params(ctx.Req)[":name"] - return f.forkRouteGetTemplate(ctx, nameParam) + return f.handleRouteGetTemplate(ctx, nameParam) } -func (f *ForkedProvisioningApi) RouteGetTemplates(ctx *models.ReqContext) response.Response { - return f.forkRouteGetTemplates(ctx) +func (f *ProvisioningApiHandler) RouteGetTemplates(ctx *models.ReqContext) response.Response { + return f.handleRouteGetTemplates(ctx) } -func (f *ForkedProvisioningApi) RoutePostAlertRule(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePostAlertRule(ctx *models.ReqContext) response.Response { + // Parse Request Body conf := apimodels.ProvisionedAlertRule{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePostAlertRule(ctx, conf) + return f.handleRoutePostAlertRule(ctx, conf) } -func (f *ForkedProvisioningApi) RoutePostContactpoints(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePostContactpoints(ctx *models.ReqContext) response.Response { + // Parse Request Body conf := apimodels.EmbeddedContactPoint{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePostContactpoints(ctx, conf) + return f.handleRoutePostContactpoints(ctx, conf) } -func (f *ForkedProvisioningApi) RoutePostMuteTiming(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePostMuteTiming(ctx *models.ReqContext) response.Response { + // Parse Request Body conf := apimodels.MuteTimeInterval{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePostMuteTiming(ctx, conf) + return f.handleRoutePostMuteTiming(ctx, conf) } -func (f *ForkedProvisioningApi) RoutePutAlertRule(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePutAlertRule(ctx *models.ReqContext) response.Response { + // Parse Path Parameters uIDParam := web.Params(ctx.Req)[":UID"] + // Parse Request Body conf := apimodels.ProvisionedAlertRule{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePutAlertRule(ctx, conf, uIDParam) + return f.handleRoutePutAlertRule(ctx, conf, uIDParam) } -func (f *ForkedProvisioningApi) RoutePutAlertRuleGroup(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePutAlertRuleGroup(ctx *models.ReqContext) response.Response { + // Parse Path Parameters folderUIDParam := web.Params(ctx.Req)[":FolderUID"] groupParam := web.Params(ctx.Req)[":Group"] + // Parse Request Body conf := apimodels.AlertRuleGroupMetadata{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePutAlertRuleGroup(ctx, conf, folderUIDParam, groupParam) + return f.handleRoutePutAlertRuleGroup(ctx, conf, folderUIDParam, groupParam) } -func (f *ForkedProvisioningApi) RoutePutContactpoint(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePutContactpoint(ctx *models.ReqContext) response.Response { + // Parse Path Parameters uIDParam := web.Params(ctx.Req)[":UID"] + // Parse Request Body conf := apimodels.EmbeddedContactPoint{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePutContactpoint(ctx, conf, uIDParam) + return f.handleRoutePutContactpoint(ctx, conf, uIDParam) } -func (f *ForkedProvisioningApi) RoutePutMuteTiming(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePutMuteTiming(ctx *models.ReqContext) response.Response { + // Parse Path Parameters nameParam := web.Params(ctx.Req)[":name"] + // Parse Request Body conf := apimodels.MuteTimeInterval{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePutMuteTiming(ctx, conf, nameParam) + return f.handleRoutePutMuteTiming(ctx, conf, nameParam) } -func (f *ForkedProvisioningApi) RoutePutPolicyTree(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePutPolicyTree(ctx *models.ReqContext) response.Response { + // Parse Request Body conf := apimodels.Route{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePutPolicyTree(ctx, conf) + return f.handleRoutePutPolicyTree(ctx, conf) } -func (f *ForkedProvisioningApi) RoutePutTemplate(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePutTemplate(ctx *models.ReqContext) response.Response { + // Parse Path Parameters nameParam := web.Params(ctx.Req)[":name"] + // Parse Request Body conf := apimodels.MessageTemplateContent{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePutTemplate(ctx, conf, nameParam) + return f.handleRoutePutTemplate(ctx, conf, nameParam) } -func (f *ForkedProvisioningApi) RouteResetPolicyTree(ctx *models.ReqContext) response.Response { - return f.forkRouteResetPolicyTree(ctx) +func (f *ProvisioningApiHandler) RouteResetPolicyTree(ctx *models.ReqContext) response.Response { + return f.handleRouteResetPolicyTree(ctx) } -func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingService, m *metrics.API) { +func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApi, m *metrics.API) { api.RouteRegister.Group("", func(group routing.RouteRegister) { group.Delete( toMacaronPath("/api/v1/provisioning/alert-rules/{UID}"), diff --git a/pkg/services/ngalert/api/generated_base_api_ruler.go b/pkg/services/ngalert/api/generated_base_api_ruler.go index 3c2ac808b9b..ac584615874 100644 --- a/pkg/services/ngalert/api/generated_base_api_ruler.go +++ b/pkg/services/ngalert/api/generated_base_api_ruler.go @@ -18,7 +18,7 @@ import ( "github.com/grafana/grafana/pkg/web" ) -type RulerApiForkingService interface { +type RulerApi interface { RouteDeleteGrafanaRuleGroupConfig(*models.ReqContext) response.Response RouteDeleteNamespaceGrafanaRulesConfig(*models.ReqContext) response.Response RouteDeleteNamespaceRulesConfig(*models.ReqContext) response.Response @@ -33,72 +33,85 @@ type RulerApiForkingService interface { RoutePostNameRulesConfig(*models.ReqContext) response.Response } -func (f *ForkedRulerApi) RouteDeleteGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteDeleteGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response { + // Parse Path Parameters namespaceParam := web.Params(ctx.Req)[":Namespace"] groupnameParam := web.Params(ctx.Req)[":Groupname"] - return f.forkRouteDeleteGrafanaRuleGroupConfig(ctx, namespaceParam, groupnameParam) + return f.handleRouteDeleteGrafanaRuleGroupConfig(ctx, namespaceParam, groupnameParam) } -func (f *ForkedRulerApi) RouteDeleteNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteDeleteNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response { + // Parse Path Parameters namespaceParam := web.Params(ctx.Req)[":Namespace"] - return f.forkRouteDeleteNamespaceGrafanaRulesConfig(ctx, namespaceParam) + return f.handleRouteDeleteNamespaceGrafanaRulesConfig(ctx, namespaceParam) } -func (f *ForkedRulerApi) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] namespaceParam := web.Params(ctx.Req)[":Namespace"] - return f.forkRouteDeleteNamespaceRulesConfig(ctx, datasourceUIDParam, namespaceParam) + return f.handleRouteDeleteNamespaceRulesConfig(ctx, datasourceUIDParam, namespaceParam) } -func (f *ForkedRulerApi) RouteDeleteRuleGroupConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteDeleteRuleGroupConfig(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] namespaceParam := web.Params(ctx.Req)[":Namespace"] groupnameParam := web.Params(ctx.Req)[":Groupname"] - return f.forkRouteDeleteRuleGroupConfig(ctx, datasourceUIDParam, namespaceParam, groupnameParam) + return f.handleRouteDeleteRuleGroupConfig(ctx, datasourceUIDParam, namespaceParam, groupnameParam) } -func (f *ForkedRulerApi) RouteGetGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteGetGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response { + // Parse Path Parameters namespaceParam := web.Params(ctx.Req)[":Namespace"] groupnameParam := web.Params(ctx.Req)[":Groupname"] - return f.forkRouteGetGrafanaRuleGroupConfig(ctx, namespaceParam, groupnameParam) + return f.handleRouteGetGrafanaRuleGroupConfig(ctx, namespaceParam, groupnameParam) } -func (f *ForkedRulerApi) RouteGetGrafanaRulesConfig(ctx *models.ReqContext) response.Response { - return f.forkRouteGetGrafanaRulesConfig(ctx) +func (f *RulerApiHandler) RouteGetGrafanaRulesConfig(ctx *models.ReqContext) response.Response { + return f.handleRouteGetGrafanaRulesConfig(ctx) } -func (f *ForkedRulerApi) RouteGetNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteGetNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response { + // Parse Path Parameters namespaceParam := web.Params(ctx.Req)[":Namespace"] - return f.forkRouteGetNamespaceGrafanaRulesConfig(ctx, namespaceParam) + return f.handleRouteGetNamespaceGrafanaRulesConfig(ctx, namespaceParam) } -func (f *ForkedRulerApi) RouteGetNamespaceRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteGetNamespaceRulesConfig(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] namespaceParam := web.Params(ctx.Req)[":Namespace"] - return f.forkRouteGetNamespaceRulesConfig(ctx, datasourceUIDParam, namespaceParam) + return f.handleRouteGetNamespaceRulesConfig(ctx, datasourceUIDParam, namespaceParam) } -func (f *ForkedRulerApi) RouteGetRulegGroupConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteGetRulegGroupConfig(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] namespaceParam := web.Params(ctx.Req)[":Namespace"] groupnameParam := web.Params(ctx.Req)[":Groupname"] - return f.forkRouteGetRulegGroupConfig(ctx, datasourceUIDParam, namespaceParam, groupnameParam) + return f.handleRouteGetRulegGroupConfig(ctx, datasourceUIDParam, namespaceParam, groupnameParam) } -func (f *ForkedRulerApi) RouteGetRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteGetRulesConfig(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] - return f.forkRouteGetRulesConfig(ctx, datasourceUIDParam) + return f.handleRouteGetRulesConfig(ctx, datasourceUIDParam) } -func (f *ForkedRulerApi) RoutePostNameGrafanaRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RoutePostNameGrafanaRulesConfig(ctx *models.ReqContext) response.Response { + // Parse Path Parameters namespaceParam := web.Params(ctx.Req)[":Namespace"] + // Parse Request Body conf := apimodels.PostableRuleGroupConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePostNameGrafanaRulesConfig(ctx, conf, namespaceParam) + return f.handleRoutePostNameGrafanaRulesConfig(ctx, conf, namespaceParam) } -func (f *ForkedRulerApi) RoutePostNameRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RoutePostNameRulesConfig(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] namespaceParam := web.Params(ctx.Req)[":Namespace"] + // Parse Request Body conf := apimodels.PostableRuleGroupConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePostNameRulesConfig(ctx, conf, datasourceUIDParam, namespaceParam) + return f.handleRoutePostNameRulesConfig(ctx, conf, datasourceUIDParam, namespaceParam) } -func (api *API) RegisterRulerApiEndpoints(srv RulerApiForkingService, m *metrics.API) { +func (api *API) RegisterRulerApiEndpoints(srv RulerApi, m *metrics.API) { api.RouteRegister.Group("", func(group routing.RouteRegister) { group.Delete( toMacaronPath("/api/ruler/grafana/api/v1/rules/{Namespace}/{Groupname}"), diff --git a/pkg/services/ngalert/api/generated_base_api_testing.go b/pkg/services/ngalert/api/generated_base_api_testing.go index b181e73ba49..c25842a1e6f 100644 --- a/pkg/services/ngalert/api/generated_base_api_testing.go +++ b/pkg/services/ngalert/api/generated_base_api_testing.go @@ -18,36 +18,40 @@ import ( "github.com/grafana/grafana/pkg/web" ) -type TestingApiForkingService interface { +type TestingApi interface { RouteEvalQueries(*models.ReqContext) response.Response RouteTestRuleConfig(*models.ReqContext) response.Response RouteTestRuleGrafanaConfig(*models.ReqContext) response.Response } -func (f *ForkedTestingApi) RouteEvalQueries(ctx *models.ReqContext) response.Response { +func (f *TestingApiHandler) RouteEvalQueries(ctx *models.ReqContext) response.Response { + // Parse Request Body conf := apimodels.EvalQueriesPayload{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRouteEvalQueries(ctx, conf) + return f.handleRouteEvalQueries(ctx, conf) } -func (f *ForkedTestingApi) RouteTestRuleConfig(ctx *models.ReqContext) response.Response { +func (f *TestingApiHandler) RouteTestRuleConfig(ctx *models.ReqContext) response.Response { + // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] + // Parse Request Body conf := apimodels.TestRulePayload{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRouteTestRuleConfig(ctx, conf, datasourceUIDParam) + return f.handleRouteTestRuleConfig(ctx, conf, datasourceUIDParam) } -func (f *ForkedTestingApi) RouteTestRuleGrafanaConfig(ctx *models.ReqContext) response.Response { +func (f *TestingApiHandler) RouteTestRuleGrafanaConfig(ctx *models.ReqContext) response.Response { + // Parse Request Body conf := apimodels.TestRulePayload{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRouteTestRuleGrafanaConfig(ctx, conf) + return f.handleRouteTestRuleGrafanaConfig(ctx, conf) } -func (api *API) RegisterTestingApiEndpoints(srv TestingApiForkingService, m *metrics.API) { +func (api *API) RegisterTestingApiEndpoints(srv TestingApi, m *metrics.API) { api.RouteRegister.Group("", func(group routing.RouteRegister) { group.Post( toMacaronPath("/api/v1/eval"), diff --git a/pkg/services/ngalert/api/provisioning.go b/pkg/services/ngalert/api/provisioning.go new file mode 100644 index 00000000000..192f8c1c3f0 --- /dev/null +++ b/pkg/services/ngalert/api/provisioning.go @@ -0,0 +1,105 @@ +package api + +import ( + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/models" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" +) + +type ProvisioningApiHandler struct { + svc *ProvisioningSrv +} + +func NewProvisioningApi(svc *ProvisioningSrv) *ProvisioningApiHandler { + return &ProvisioningApiHandler{ + svc: svc, + } +} + +func (f *ProvisioningApiHandler) handleRouteGetPolicyTree(ctx *models.ReqContext) response.Response { + return f.svc.RouteGetPolicyTree(ctx) +} + +func (f *ProvisioningApiHandler) handleRoutePutPolicyTree(ctx *models.ReqContext, route apimodels.Route) response.Response { + return f.svc.RoutePutPolicyTree(ctx, route) +} + +func (f *ProvisioningApiHandler) handleRouteGetContactpoints(ctx *models.ReqContext) response.Response { + return f.svc.RouteGetContactPoints(ctx) +} + +func (f *ProvisioningApiHandler) handleRoutePostContactpoints(ctx *models.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response { + return f.svc.RoutePostContactPoint(ctx, cp) +} + +func (f *ProvisioningApiHandler) handleRoutePutContactpoint(ctx *models.ReqContext, cp apimodels.EmbeddedContactPoint, UID string) response.Response { + return f.svc.RoutePutContactPoint(ctx, cp, UID) +} + +func (f *ProvisioningApiHandler) handleRouteDeleteContactpoints(ctx *models.ReqContext, UID string) response.Response { + return f.svc.RouteDeleteContactPoint(ctx, UID) +} + +func (f *ProvisioningApiHandler) handleRouteGetTemplates(ctx *models.ReqContext) response.Response { + return f.svc.RouteGetTemplates(ctx) +} + +func (f *ProvisioningApiHandler) handleRouteGetTemplate(ctx *models.ReqContext, name string) response.Response { + return f.svc.RouteGetTemplate(ctx, name) +} + +func (f *ProvisioningApiHandler) handleRoutePutTemplate(ctx *models.ReqContext, body apimodels.MessageTemplateContent, name string) response.Response { + return f.svc.RoutePutTemplate(ctx, body, name) +} + +func (f *ProvisioningApiHandler) handleRouteDeleteTemplate(ctx *models.ReqContext, name string) response.Response { + return f.svc.RouteDeleteTemplate(ctx, name) +} + +func (f *ProvisioningApiHandler) handleRouteGetMuteTiming(ctx *models.ReqContext, name string) response.Response { + return f.svc.RouteGetMuteTiming(ctx, name) +} + +func (f *ProvisioningApiHandler) handleRouteGetMuteTimings(ctx *models.ReqContext) response.Response { + return f.svc.RouteGetMuteTimings(ctx) +} + +func (f *ProvisioningApiHandler) handleRoutePostMuteTiming(ctx *models.ReqContext, mt apimodels.MuteTimeInterval) response.Response { + return f.svc.RoutePostMuteTiming(ctx, mt) +} + +func (f *ProvisioningApiHandler) handleRoutePutMuteTiming(ctx *models.ReqContext, mt apimodels.MuteTimeInterval, name string) response.Response { + return f.svc.RoutePutMuteTiming(ctx, mt, name) +} + +func (f *ProvisioningApiHandler) handleRouteDeleteMuteTiming(ctx *models.ReqContext, name string) response.Response { + return f.svc.RouteDeleteMuteTiming(ctx, name) +} + +func (f *ProvisioningApiHandler) handleRouteGetAlertRule(ctx *models.ReqContext, UID string) response.Response { + return f.svc.RouteRouteGetAlertRule(ctx, UID) +} + +func (f *ProvisioningApiHandler) handleRoutePostAlertRule(ctx *models.ReqContext, ar apimodels.ProvisionedAlertRule) response.Response { + return f.svc.RoutePostAlertRule(ctx, ar) +} + +func (f *ProvisioningApiHandler) handleRoutePutAlertRule(ctx *models.ReqContext, ar apimodels.ProvisionedAlertRule, UID string) response.Response { + return f.svc.RoutePutAlertRule(ctx, ar, UID) +} + +func (f *ProvisioningApiHandler) handleRouteDeleteAlertRule(ctx *models.ReqContext, UID string) response.Response { + return f.svc.RouteDeleteAlertRule(ctx, UID) +} + +func (f *ProvisioningApiHandler) handleRouteResetPolicyTree(ctx *models.ReqContext) response.Response { + return f.svc.RouteResetPolicyTree(ctx) +} + +func (f *ProvisioningApiHandler) handleRouteGetAlertRuleGroup(ctx *models.ReqContext, folder, group string) response.Response { + return f.svc.RouteGetAlertRuleGroup(ctx, folder, group) +} + +func (f *ProvisioningApiHandler) handleRoutePutAlertRuleGroup(ctx *models.ReqContext, ag apimodels.AlertRuleGroupMetadata, folder, group string) response.Response { + return f.svc.RoutePutAlertRuleGroup(ctx, ag, folder, group) +} diff --git a/pkg/services/ngalert/api/testing_api.go b/pkg/services/ngalert/api/testing_api.go new file mode 100644 index 00000000000..7a84a1e1bf0 --- /dev/null +++ b/pkg/services/ngalert/api/testing_api.go @@ -0,0 +1,30 @@ +package api + +import ( + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/models" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" +) + +// TestingApiHandler always forwards requests to grafana backend +type TestingApiHandler struct { + svc *TestingApiSrv +} + +func NewTestingApi(svc *TestingApiSrv) *TestingApiHandler { + return &TestingApiHandler{ + svc: svc, + } +} + +func (f *TestingApiHandler) handleRouteTestRuleConfig(c *models.ReqContext, body apimodels.TestRulePayload, dsUID string) response.Response { + return f.svc.RouteTestRuleConfig(c, body, dsUID) +} + +func (f *TestingApiHandler) handleRouteTestRuleGrafanaConfig(c *models.ReqContext, body apimodels.TestRulePayload) response.Response { + return f.svc.RouteTestGrafanaRuleConfig(c, body) +} + +func (f *TestingApiHandler) handleRouteEvalQueries(c *models.ReqContext, body apimodels.EvalQueriesPayload) response.Response { + return f.svc.RouteEvalQueries(c, body) +} diff --git a/pkg/services/ngalert/api/tooling/Makefile b/pkg/services/ngalert/api/tooling/Makefile index 279f0632116..2cf406d766e 100644 --- a/pkg/services/ngalert/api/tooling/Makefile +++ b/pkg/services/ngalert/api/tooling/Makefile @@ -65,4 +65,6 @@ serve: post.json serve-stable: api.json docker run --rm -p 80:8080 -v $$(pwd):/tmp -e SWAGGER_FILE=/tmp/$(<) swaggerapi/swagger-editor -all: post.json validate api.json validate-stable swagger-codegen-api fix copy-files clean +gen: swagger-codegen-api fix copy-files clean + +all: post.json api.json gen diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 4b5b7c14432..e59a028322e 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -2702,6 +2702,7 @@ "type": "object" }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "properties": { "ForceQuery": { "type": "boolean" @@ -2734,7 +2735,7 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "type": "object" }, "Userinfo": { @@ -3295,6 +3296,7 @@ "type": "object" }, "receiver": { + "description": "Receiver receiver", "properties": { "name": { "description": "name", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 289e18c7b3f..19ec03f7f72 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -5289,6 +5289,7 @@ } }, "alertGroup": { + "description": "AlertGroup alert group", "type": "object", "required": [ "alerts", @@ -5487,7 +5488,6 @@ "$ref": "#/definitions/gettableAlerts" }, "gettableSilence": { - "description": "GettableSilence gettable silence", "type": "object", "required": [ "comment", @@ -5537,7 +5537,6 @@ "$ref": "#/definitions/gettableSilence" }, "gettableSilences": { - "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" @@ -5688,6 +5687,7 @@ "$ref": "#/definitions/postableSilence" }, "receiver": { + "description": "Receiver receiver", "type": "object", "required": [ "name" diff --git a/pkg/services/ngalert/api/tooling/swagger-codegen/templates/controller-api.mustache b/pkg/services/ngalert/api/tooling/swagger-codegen/templates/controller-api.mustache index 0ab26e5596a..34dfce4f1ad 100644 --- a/pkg/services/ngalert/api/tooling/swagger-codegen/templates/controller-api.mustache +++ b/pkg/services/ngalert/api/tooling/swagger-codegen/templates/controller-api.mustache @@ -13,26 +13,25 @@ import ( "github.com/grafana/grafana/pkg/middleware" ) -type {{classname}}ForkingService interface { {{#operation}} +type {{classname}} interface { {{#operation}} {{nickname}}(*models.ReqContext) response.Response{{/operation}} } {{#operations}}{{#operation}} -func (f *Forked{{classname}}) {{nickname}}(ctx *models.ReqContext) response.Response { - {{#pathParams}} - {{paramName}}Param := web.Params(ctx.Req)[":{{baseName}}"] - {{/pathParams}} +func (f *{{classname}}Handler) {{nickname}}(ctx *models.ReqContext) response.Response { {{#hasPathParams}} + // Parse Path Parameters{{/hasPathParams}}{{#pathParams}} + {{paramName}}Param := web.Params(ctx.Req)[":{{baseName}}"]{{/pathParams}} {{#bodyParams}} + // Parse Request Body conf := apimodels.{{dataType}}{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - {{/bodyParams}} - return f.fork{{nickname}}(ctx{{#bodyParams}}, conf{{/bodyParams}}{{#pathParams}}, {{paramName}}Param{{/pathParams}}) + {{/bodyParams}}return f.handle{{nickname}}(ctx{{#bodyParams}}, conf{{/bodyParams}}{{#pathParams}}, {{paramName}}Param{{/pathParams}}) } {{/operation}}{{/operations}} -func (api *API) Register{{classname}}Endpoints(srv {{classname}}ForkingService, m *metrics.API) { +func (api *API) Register{{classname}}Endpoints(srv {{classname}}, m *metrics.API) { api.RouteRegister.Group("", func(group routing.RouteRegister){ {{#operations}}{{#operation}} group.{{httpMethod}}( toMacaronPath("{{{path}}}"), @@ -46,4 +45,4 @@ func (api *API) Register{{classname}}Endpoints(srv {{classname}}ForkingService, ){{/operation}}{{/operations}} }, middleware.ReqSignedIn) }{{#operation}} -{{/operation}}{{/operations}} \ No newline at end of file +{{/operation}}{{/operations}} From 3617eac5f3d2895433c08448f536674343b55d0c Mon Sep 17 00:00:00 2001 From: Joey Tawadrous <90795735+joey-grafana@users.noreply.github.com> Date: Mon, 18 Jul 2022 08:08:35 +0100 Subject: [PATCH 6/9] Traces: Add more template variables in Tempo & Zipkin (#52306) * Add support for more vars in Tempo * Tests for Tempo vars * Tempo ds vars * Tempo ds vars test * Zipkin template var * Zipkin tests --- .../tempo/QueryEditor/NativeSearch.test.tsx | 45 ++++++++++++++++++- .../tempo/QueryEditor/NativeSearch.tsx | 12 ++++- .../datasource/tempo/datasource.test.ts | 8 +++- .../plugins/datasource/tempo/datasource.ts | 2 + .../datasource/zipkin/datasource.test.ts | 13 +++++- .../plugins/datasource/zipkin/datasource.ts | 34 ++++++++++++-- 6 files changed, 103 insertions(+), 11 deletions(-) diff --git a/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.test.tsx b/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.test.tsx index dc01b7262f4..c59b44d8a86 100644 --- a/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.test.tsx +++ b/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.test.tsx @@ -30,7 +30,17 @@ jest.mock('../language_provider', () => { }); }); -const mockQuery = { +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getTemplateSrv: () => ({ + replace: jest.fn(), + containsTemplate: (val: string): boolean => { + return val.includes('$'); + }, + }), +})); + +let mockQuery = { refId: 'A', queryType: 'nativeSearch', key: 'Q-595a9bbc-2a25-49a7-9249-a52a0a475d83-0', @@ -117,7 +127,38 @@ describe('NativeSearch', () => { expect(option).toBeDefined(); await user.type(select, 'a'); - option = await screen.findByText('No options found'); + option = await screen.findByText('Hit enter to add'); expect(option).toBeDefined(); }); + + it('should add variable to select menu options', async () => { + mockQuery = { + ...mockQuery, + refId: '121314', + serviceName: '$service', + spanName: '$span', + }; + + render( + {}} onRunQuery={() => {}} /> + ); + + const asyncServiceSelect = screen.getByRole('combobox', { name: 'select-service-name' }); + expect(asyncServiceSelect).toBeInTheDocument(); + await user.click(asyncServiceSelect); + jest.advanceTimersByTime(3000); + + await user.type(asyncServiceSelect, '$'); + var serviceOption = await screen.findByText('$service'); + expect(serviceOption).toBeDefined(); + + const asyncSpanSelect = screen.getByRole('combobox', { name: 'select-span-name' }); + expect(asyncSpanSelect).toBeInTheDocument(); + await user.click(asyncSpanSelect); + jest.advanceTimersByTime(3000); + + await user.type(asyncSpanSelect, '$'); + var operationOption = await screen.findByText('$span'); + expect(operationOption).toBeDefined(); + }); }); diff --git a/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx b/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx index 0fdb8e983fa..d73eae5d4f9 100644 --- a/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx +++ b/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx @@ -3,7 +3,7 @@ import Prism from 'prismjs'; import React, { useCallback, useState, useEffect, useMemo } from 'react'; import { Node } from 'slate'; -import { GrafanaTheme2, isValidGoDuration, SelectableValue } from '@grafana/data'; +import { GrafanaTheme2, isValidGoDuration, SelectableValue, toOption } from '@grafana/data'; import { FetchError, getTemplateSrv, isFetchError, TemplateSrv } from '@grafana/runtime'; import { InlineFieldRow, @@ -90,7 +90,13 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props const fetchOptions = async () => { try { const [services, spans] = await Promise.all([loadOptions('serviceName'), loadOptions('spanName')]); + if (query.serviceName && getTemplateSrv().containsTemplate(query.serviceName)) { + services.push(toOption(query.serviceName)); + } setServiceOptions(services); + if (query.spanName && getTemplateSrv().containsTemplate(query.spanName)) { + spans.push(toOption(query.spanName)); + } setSpanOptions(spans); } catch (error) { // Display message if Tempo is connected but search 404's @@ -102,7 +108,7 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props } }; fetchOptions(); - }, [languageProvider, loadOptions]); + }, [languageProvider, loadOptions, query.serviceName, query.spanName]); useEffect(() => { const fetchTags = async () => { @@ -161,6 +167,7 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props isClearable onKeyDown={onKeyDown} aria-label={'select-service-name'} + allowCustomValue={true} /> @@ -184,6 +191,7 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props isClearable onKeyDown={onKeyDown} aria-label={'select-span-name'} + allowCustomValue={true} /> diff --git a/public/app/plugins/datasource/tempo/datasource.test.ts b/public/app/plugins/datasource/tempo/datasource.test.ts index ec834e35a5b..704b408bea4 100644 --- a/public/app/plugins/datasource/tempo/datasource.test.ts +++ b/public/app/plugins/datasource/tempo/datasource.test.ts @@ -77,9 +77,11 @@ describe('Tempo data source', () => { const queries = ds.interpolateVariablesInQueries([getQuery()], { interpolationVar: { text: text, value: text }, }); - expect(templateSrv.replace).toBeCalledTimes(5); + expect(templateSrv.replace).toBeCalledTimes(7); expect(queries[0].linkedQuery?.expr).toBe(text); expect(queries[0].query).toBe(text); + expect(queries[0].serviceName).toBe(text); + expect(queries[0].spanName).toBe(text); expect(queries[0].search).toBe(text); expect(queries[0].minDuration).toBe(text); expect(queries[0].maxDuration).toBe(text); @@ -94,9 +96,11 @@ describe('Tempo data source', () => { const resp = ds.applyTemplateVariables(getQuery(), { interpolationVar: { text: text, value: text }, }); - expect(templateSrv.replace).toBeCalledTimes(5); + expect(templateSrv.replace).toBeCalledTimes(7); expect(resp.linkedQuery?.expr).toBe(text); expect(resp.query).toBe(text); + expect(resp.serviceName).toBe(text); + expect(resp.spanName).toBe(text); expect(resp.search).toBe(text); expect(resp.minDuration).toBe(text); expect(resp.maxDuration).toBe(text); diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index 93308043580..3fa28b7cd99 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -296,6 +296,8 @@ export class TempoDatasource extends DataSourceWithBackend ({ describe('ZipkinDatasource', () => { describe('query', () => { + const templateSrv: TemplateSrv = { + replace: jest.fn(), + getVariables: jest.fn(), + containsTemplate: jest.fn(), + updateTimeRange: jest.fn(), + }; + it('runs query', async () => { setupBackendSrv(zipkinResponse); - const ds = new ZipkinDatasource(defaultSettings); + const ds = new ZipkinDatasource(defaultSettings, templateSrv); await expect(ds.query({ targets: [{ query: '12345' }] } as any)).toEmitValuesWith((val) => { expect(val[0].data[0].fields).toMatchObject(traceFrameFields); }); }); + it('runs query with traceId that includes special characters', async () => { setupBackendSrv(zipkinResponse); - const ds = new ZipkinDatasource(defaultSettings); + const ds = new ZipkinDatasource(defaultSettings, templateSrv); await expect(ds.query({ targets: [{ query: 'a/b' }] } as any)).toEmitValuesWith((val) => { expect(val[0].data[0].fields).toMatchObject(traceFrameFields); }); diff --git a/public/app/plugins/datasource/zipkin/datasource.ts b/public/app/plugins/datasource/zipkin/datasource.ts index 4c088c4548c..cf9ed51c03f 100644 --- a/public/app/plugins/datasource/zipkin/datasource.ts +++ b/public/app/plugins/datasource/zipkin/datasource.ts @@ -9,8 +9,9 @@ import { DataSourceJsonData, FieldType, MutableDataFrame, + ScopedVars, } from '@grafana/data'; -import { BackendSrvRequest, FetchResponse, getBackendSrv } from '@grafana/runtime'; +import { BackendSrvRequest, FetchResponse, getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { SpanBarOptions } from '@jaegertracing/jaeger-ui-components'; import { NodeGraphOptions } from 'app/core/components/NodeGraphSettings'; @@ -29,7 +30,10 @@ export class ZipkinDatasource extends DataSourceApi uploadedJson: string | ArrayBuffer | null = null; nodeGraph?: NodeGraphOptions; spanBar?: SpanBarOptions; - constructor(private instanceSettings: DataSourceInstanceSettings) { + constructor( + private instanceSettings: DataSourceInstanceSettings, + private readonly templateSrv: TemplateSrv = getTemplateSrv() + ) { super(instanceSettings); this.nodeGraph = instanceSettings.jsonData.nodeGraph; } @@ -50,7 +54,8 @@ export class ZipkinDatasource extends DataSourceApi } if (target.query) { - return this.request(`${apiPrefix}/trace/${encodeURIComponent(target.query)}`).pipe( + const query = this.applyVariables(target, options.scopedVars); + return this.request(`${apiPrefix}/trace/${encodeURIComponent(query.query)}`).pipe( map((res) => responseToDataQueryResponse(res, this.nodeGraph?.enabled)) ); } @@ -71,6 +76,29 @@ export class ZipkinDatasource extends DataSourceApi return query.query; } + interpolateVariablesInQueries(queries: ZipkinQuery[], scopedVars: ScopedVars): ZipkinQuery[] { + if (!queries || queries.length === 0) { + return []; + } + + return queries.map((query) => { + return { + ...query, + datasource: this.getRef(), + ...this.applyVariables(query, scopedVars), + }; + }); + } + + applyVariables(query: ZipkinQuery, scopedVars: ScopedVars) { + const expandedQuery = { ...query }; + + return { + ...expandedQuery, + query: this.templateSrv.replace(query.query ?? '', scopedVars), + }; + } + private request( apiUrl: string, data?: any, From 332639ce43a51e3f226eb4d924a515a624438852 Mon Sep 17 00:00:00 2001 From: eledobleefe Date: Mon, 18 Jul 2022 11:02:18 +0200 Subject: [PATCH 7/9] PanelEdit: Hide multi-/all-select datasource variables in datasource picker (#52142) --- public/app/features/plugins/datasource_srv.ts | 13 +++++++------ public/app/features/variables/guard.ts | 5 +++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index a60ad1b8f91..41a4fc7b4a2 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -24,7 +24,7 @@ import { instanceSettings as expressionInstanceSettings, } from 'app/features/expressions/ExpressionDatasource'; -import { DataSourceVariableModel } from '../variables/types'; +import { isDataSource } from '../variables/guard'; import { importDataSourcePlugin } from './plugin_loader'; @@ -245,11 +245,12 @@ export class DatasourceSrv implements DataSourceService { }); if (filters.variables) { - for (const variable of this.templateSrv.getVariables().filter((variable) => variable.type === 'datasource')) { - const dsVar = variable as DataSourceVariableModel; - const first = dsVar.current.value === 'default' ? this.defaultName : dsVar.current.value; - const dsName = first as unknown as string; - const dsSettings = this.settingsMapByName[dsName]; + for (const variable of this.templateSrv.getVariables()) { + if (!isDataSource(variable) || variable.multi || variable.includeAll) { + continue; + } + const dsName = variable.current.value === 'default' ? this.defaultName : variable.current.value; + const dsSettings = !Array.isArray(dsName) && this.settingsMapByName[dsName]; if (dsSettings) { const key = `$\{${variable.name}\}`; diff --git a/public/app/features/variables/guard.ts b/public/app/features/variables/guard.ts index 0efb21229b5..5d8eecababe 100644 --- a/public/app/features/variables/guard.ts +++ b/public/app/features/variables/guard.ts @@ -25,6 +25,7 @@ import { VariableQueryEditorProps, VariableWithMultiSupport, VariableWithOptions, + DataSourceVariableModel, } from './types'; export const isQuery = (model: VariableModel): model is QueryVariableModel => { @@ -39,6 +40,10 @@ export const isConstant = (model: VariableModel): model is ConstantVariableModel return model.type === 'constant'; }; +export const isDataSource = (model: VariableModel): model is DataSourceVariableModel => { + return model.type === 'datasource'; +}; + export const isMulti = (model: VariableModel): model is VariableWithMultiSupport => { const withMulti = model as VariableWithMultiSupport; return withMulti.hasOwnProperty('multi') && typeof withMulti.multi === 'boolean'; From fb379ae43672e4775b26f997023ec232bfb17fe0 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Mon, 18 Jul 2022 12:26:35 +0300 Subject: [PATCH 8/9] Chore: Introduce playlist service (#52252) * Store: Introduce playlist service * Integrate playlist service * Update swagger --- pkg/api/docs/definitions/playlists.go | 16 +- pkg/api/http_server.go | 5 +- pkg/api/playlist.go | 67 +++--- pkg/server/wire.go | 2 + pkg/services/playlist/model.go | 95 ++++++++ pkg/services/playlist/playlist.go | 14 ++ .../playlist/playlistimpl/playlist.go | 44 ++++ pkg/services/playlist/playlistimpl/store.go | 226 ++++++++++++++++++ .../playlist/playlistimpl/store_test.go | 82 +++++++ pkg/services/playlist/playlisttest/fake.go | 43 ++++ pkg/services/sqlstore/store.go | 6 + public/api-merged.json | 37 ++- public/api-spec.json | 8 +- 13 files changed, 587 insertions(+), 58 deletions(-) create mode 100644 pkg/services/playlist/model.go create mode 100644 pkg/services/playlist/playlist.go create mode 100644 pkg/services/playlist/playlistimpl/playlist.go create mode 100644 pkg/services/playlist/playlistimpl/store.go create mode 100644 pkg/services/playlist/playlistimpl/store_test.go create mode 100644 pkg/services/playlist/playlisttest/fake.go diff --git a/pkg/api/docs/definitions/playlists.go b/pkg/api/docs/definitions/playlists.go index 321a137601b..03989c4d153 100644 --- a/pkg/api/docs/definitions/playlists.go +++ b/pkg/api/docs/definitions/playlists.go @@ -2,7 +2,7 @@ package definitions import ( "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/playlist" ) // swagger:route GET /playlists playlists searchPlaylists @@ -121,7 +121,7 @@ type DeletePlaylistParams struct { type UpdatePlaylistParams struct { // in:body // required:true - Body models.UpdatePlaylistCommand + Body playlist.UpdatePlaylistCommand // in:path // required:true UID string `json:"uid"` @@ -131,28 +131,28 @@ type UpdatePlaylistParams struct { type CreatePlaylistParams struct { // in:body // required:true - Body models.CreatePlaylistCommand + Body playlist.CreatePlaylistCommand } // swagger:response searchPlaylistsResponse type SearchPlaylistsResponse struct { // The response message // in: body - Body models.Playlists `json:"body"` + Body playlist.Playlists `json:"body"` } // swagger:response getPlaylistResponse type GetPlaylistResponse struct { // The response message // in: body - Body *models.PlaylistDTO `json:"body"` + Body *playlist.PlaylistDTO `json:"body"` } // swagger:response getPlaylistItemsResponse type GetPlaylistItemsResponse struct { // The response message // in: body - Body []models.PlaylistItemDTO `json:"body"` + Body []playlist.PlaylistItemDTO `json:"body"` } // swagger:response getPlaylistDashboardsResponse @@ -166,12 +166,12 @@ type GetPlaylistDashboardsResponse struct { type UpdatePlaylistResponseResponse struct { // The response message // in: body - Body *models.PlaylistDTO `json:"body"` + Body *playlist.PlaylistDTO `json:"body"` } // swagger:response createPlaylistResponse type CreatePlaylistResponse struct { // The response message // in: body - Body *models.Playlist `json:"body"` + Body *playlist.Playlist `json:"body"` } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 8152dbf0004..532a0b201f1 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -58,6 +58,7 @@ import ( "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/ngalert" "github.com/grafana/grafana/pkg/services/notifications" + "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/services/plugindashboards" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service" pref "github.com/grafana/grafana/pkg/services/preference" @@ -168,6 +169,7 @@ type HTTPServer struct { dashboardVersionService dashver.Service PublicDashboardsApi *publicdashboardsApi.Api starService star.Service + playlistService playlist.Service CoremodelRegistry *registry.Generic CoremodelStaticRegistry *registry.Static kvStore kvstore.KVStore @@ -206,7 +208,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service, entityEventsService store.EntityEventsService, teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService, dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service, - starService star.Service, csrfService csrf.Service, coremodelRegistry *registry.Generic, coremodelStaticRegistry *registry.Static, + starService star.Service, playlistService playlist.Service, csrfService csrf.Service, coremodelRegistry *registry.Generic, coremodelStaticRegistry *registry.Static, kvStore kvstore.KVStore, secretsMigrator secrets.Migrator, remoteSecretsCheck secretsKV.UseRemoteSecretsPluginCheck, publicDashboardsApi *publicdashboardsApi.Api, userService user.Service) (*HTTPServer, error) { web.Env = cfg.Env @@ -289,6 +291,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi dashboardPermissionsService: dashboardPermissionsService, dashboardVersionService: dashboardVersionService, starService: starService, + playlistService: playlistService, CoremodelRegistry: coremodelRegistry, CoremodelStaticRegistry: coremodelStaticRegistry, kvStore: kvStore, diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index 47889948e13..6c85aec19d8 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -6,25 +6,26 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/web" ) func (hs *HTTPServer) ValidateOrgPlaylist(c *models.ReqContext) { uid := web.Params(c.Req)[":uid"] - query := models.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgId} - err := hs.SQLStore.GetPlaylist(c.Req.Context(), &query) + query := playlist.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgId} + p, err := hs.playlistService.Get(c.Req.Context(), &query) if err != nil { c.JsonApiErr(404, "Playlist not found", err) return } - if query.Result.OrgId == 0 { + if p.OrgId == 0 { c.JsonApiErr(404, "Playlist not found", err) return } - if query.Result.OrgId != c.OrgId { + if p.OrgId != c.OrgId { c.JsonApiErr(403, "You are not allowed to edit/view playlist", nil) return } @@ -38,53 +39,54 @@ func (hs *HTTPServer) SearchPlaylists(c *models.ReqContext) response.Response { limit = 1000 } - searchQuery := models.GetPlaylistsQuery{ + searchQuery := playlist.GetPlaylistsQuery{ Name: query, Limit: limit, OrgId: c.OrgId, } - err := hs.SQLStore.SearchPlaylists(c.Req.Context(), &searchQuery) + playlists, err := hs.playlistService.Search(c.Req.Context(), &searchQuery) if err != nil { return response.Error(500, "Search failed", err) } - return response.JSON(http.StatusOK, searchQuery.Result) + return response.JSON(http.StatusOK, playlists) } func (hs *HTTPServer) GetPlaylist(c *models.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] - cmd := models.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgId} + cmd := playlist.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgId} - if err := hs.SQLStore.GetPlaylist(c.Req.Context(), &cmd); err != nil { + p, err := hs.playlistService.Get(c.Req.Context(), &cmd) + if err != nil { return response.Error(500, "Playlist not found", err) } playlistDTOs, _ := hs.LoadPlaylistItemDTOs(c.Req.Context(), uid, c.OrgId) - dto := &models.PlaylistDTO{ - Id: cmd.Result.Id, - UID: cmd.Result.UID, - Name: cmd.Result.Name, - Interval: cmd.Result.Interval, - OrgId: cmd.Result.OrgId, + dto := &playlist.PlaylistDTO{ + Id: p.Id, + UID: p.UID, + Name: p.Name, + Interval: p.Interval, + OrgId: p.OrgId, Items: playlistDTOs, } return response.JSON(http.StatusOK, dto) } -func (hs *HTTPServer) LoadPlaylistItemDTOs(ctx context.Context, uid string, orgId int64) ([]models.PlaylistItemDTO, error) { +func (hs *HTTPServer) LoadPlaylistItemDTOs(ctx context.Context, uid string, orgId int64) ([]playlist.PlaylistItemDTO, error) { playlistitems, err := hs.LoadPlaylistItems(ctx, uid, orgId) if err != nil { return nil, err } - playlistDTOs := make([]models.PlaylistItemDTO, 0) + playlistDTOs := make([]playlist.PlaylistItemDTO, 0) for _, item := range playlistitems { - playlistDTOs = append(playlistDTOs, models.PlaylistItemDTO{ + playlistDTOs = append(playlistDTOs, playlist.PlaylistItemDTO{ Id: item.Id, PlaylistId: item.PlaylistId, Type: item.Type, @@ -97,13 +99,14 @@ func (hs *HTTPServer) LoadPlaylistItemDTOs(ctx context.Context, uid string, orgI return playlistDTOs, nil } -func (hs *HTTPServer) LoadPlaylistItems(ctx context.Context, uid string, orgId int64) ([]models.PlaylistItem, error) { - itemQuery := models.GetPlaylistItemsByUidQuery{PlaylistUID: uid, OrgId: orgId} - if err := hs.SQLStore.GetPlaylistItem(ctx, &itemQuery); err != nil { +func (hs *HTTPServer) LoadPlaylistItems(ctx context.Context, uid string, orgId int64) ([]playlist.PlaylistItem, error) { + itemQuery := playlist.GetPlaylistItemsByUidQuery{PlaylistUID: uid, OrgId: orgId} + items, err := hs.playlistService.GetItems(ctx, &itemQuery) + if err != nil { return nil, err } - return *itemQuery.Result, nil + return items, nil } func (hs *HTTPServer) GetPlaylistItems(c *models.ReqContext) response.Response { @@ -132,8 +135,8 @@ func (hs *HTTPServer) GetPlaylistDashboards(c *models.ReqContext) response.Respo func (hs *HTTPServer) DeletePlaylist(c *models.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] - cmd := models.DeletePlaylistCommand{UID: uid, OrgId: c.OrgId} - if err := hs.SQLStore.DeletePlaylist(c.Req.Context(), &cmd); err != nil { + cmd := playlist.DeletePlaylistCommand{UID: uid, OrgId: c.OrgId} + if err := hs.playlistService.Delete(c.Req.Context(), &cmd); err != nil { return response.Error(500, "Failed to delete playlist", err) } @@ -141,28 +144,30 @@ func (hs *HTTPServer) DeletePlaylist(c *models.ReqContext) response.Response { } func (hs *HTTPServer) CreatePlaylist(c *models.ReqContext) response.Response { - cmd := models.CreatePlaylistCommand{} + cmd := playlist.CreatePlaylistCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } cmd.OrgId = c.OrgId - if err := hs.SQLStore.CreatePlaylist(c.Req.Context(), &cmd); err != nil { + p, err := hs.playlistService.Create(c.Req.Context(), &cmd) + if err != nil { return response.Error(500, "Failed to create playlist", err) } - return response.JSON(http.StatusOK, cmd.Result) + return response.JSON(http.StatusOK, p) } func (hs *HTTPServer) UpdatePlaylist(c *models.ReqContext) response.Response { - cmd := models.UpdatePlaylistCommand{} + cmd := playlist.UpdatePlaylistCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } cmd.OrgId = c.OrgId cmd.UID = web.Params(c.Req)[":uid"] - if err := hs.SQLStore.UpdatePlaylist(c.Req.Context(), &cmd); err != nil { + p, err := hs.playlistService.Update(c.Req.Context(), &cmd) + if err != nil { return response.Error(500, "Failed to save playlist", err) } @@ -171,6 +176,6 @@ func (hs *HTTPServer) UpdatePlaylist(c *models.ReqContext) response.Response { return response.Error(500, "Failed to save playlist", err) } - cmd.Result.Items = playlistDTOs - return response.JSON(http.StatusOK, cmd.Result) + p.Items = playlistDTOs + return response.JSON(http.StatusOK, p) } diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 7af7d59a278..70ef423e848 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -6,6 +6,7 @@ package server import ( "github.com/google/wire" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/services/playlist/playlistimpl" "github.com/grafana/grafana/pkg/services/store/sanitizer" "github.com/grafana/grafana/pkg/api" @@ -286,6 +287,7 @@ var wireBasicSet = wire.NewSet( ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), starimpl.ProvideService, + playlistimpl.ProvideService, dashverimpl.ProvideService, publicdashboardsService.ProvideService, wire.Bind(new(publicdashboards.Service), new(*publicdashboardsService.PublicDashboardServiceImpl)), diff --git a/pkg/services/playlist/model.go b/pkg/services/playlist/model.go new file mode 100644 index 00000000000..b310edba74c --- /dev/null +++ b/pkg/services/playlist/model.go @@ -0,0 +1,95 @@ +package playlist + +import ( + "errors" +) + +// Typed errors +var ( + ErrPlaylistNotFound = errors.New("Playlist not found") + ErrPlaylistFailedGenerateUniqueUid = errors.New("failed to generate unique playlist UID") + ErrCommandValidationFailed = errors.New("command missing required fields") +) + +// Playlist model +type Playlist struct { + Id int64 `json:"id"` + UID string `json:"uid" xorm:"uid"` + Name string `json:"name"` + Interval string `json:"interval"` + OrgId int64 `json:"-"` +} + +type PlaylistDTO struct { + Id int64 `json:"id"` + UID string `json:"uid"` + Name string `json:"name"` + Interval string `json:"interval"` + OrgId int64 `json:"-"` + Items []PlaylistItemDTO `json:"items"` +} + +type PlaylistItemDTO struct { + Id int64 `json:"id"` + PlaylistId int64 `json:"playlistid"` + Type string `json:"type"` + Title string `json:"title"` + Value string `json:"value"` + Order int `json:"order"` +} + +type PlaylistItem struct { + Id int64 + PlaylistId int64 + Type string + Value string + Order int + Title string +} + +type Playlists []*Playlist + +// +// COMMANDS +// + +type UpdatePlaylistCommand struct { + OrgId int64 `json:"-"` + UID string `json:"uid"` + Name string `json:"name" binding:"Required"` + Interval string `json:"interval"` + Items []PlaylistItemDTO `json:"items"` +} + +type CreatePlaylistCommand struct { + Name string `json:"name" binding:"Required"` + Interval string `json:"interval"` + Items []PlaylistItemDTO `json:"items"` + + OrgId int64 `json:"-"` +} + +type DeletePlaylistCommand struct { + UID string + OrgId int64 +} + +// +// QUERIES +// + +type GetPlaylistsQuery struct { + Name string + Limit int + OrgId int64 +} + +type GetPlaylistByUidQuery struct { + UID string + OrgId int64 +} + +type GetPlaylistItemsByUidQuery struct { + PlaylistUID string + OrgId int64 +} diff --git a/pkg/services/playlist/playlist.go b/pkg/services/playlist/playlist.go new file mode 100644 index 00000000000..fbea7a84817 --- /dev/null +++ b/pkg/services/playlist/playlist.go @@ -0,0 +1,14 @@ +package playlist + +import ( + "context" +) + +type Service interface { + Create(context.Context, *CreatePlaylistCommand) (*Playlist, error) + Update(context.Context, *UpdatePlaylistCommand) (*PlaylistDTO, error) + Get(context.Context, *GetPlaylistByUidQuery) (*Playlist, error) + GetItems(context.Context, *GetPlaylistItemsByUidQuery) ([]PlaylistItem, error) + Search(context.Context, *GetPlaylistsQuery) (Playlists, error) + Delete(ctx context.Context, cmd *DeletePlaylistCommand) error +} diff --git a/pkg/services/playlist/playlistimpl/playlist.go b/pkg/services/playlist/playlistimpl/playlist.go new file mode 100644 index 00000000000..de2a8588051 --- /dev/null +++ b/pkg/services/playlist/playlistimpl/playlist.go @@ -0,0 +1,44 @@ +package playlistimpl + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/playlist" + "github.com/grafana/grafana/pkg/services/sqlstore/db" +) + +type Service struct { + store store +} + +func ProvideService(db db.DB) playlist.Service { + return &Service{ + store: &sqlStore{ + db: db, + }, + } +} + +func (s *Service) Create(ctx context.Context, cmd *playlist.CreatePlaylistCommand) (*playlist.Playlist, error) { + return s.store.Insert(ctx, cmd) +} + +func (s *Service) Update(ctx context.Context, cmd *playlist.UpdatePlaylistCommand) (*playlist.PlaylistDTO, error) { + return s.store.Update(ctx, cmd) +} + +func (s *Service) Get(ctx context.Context, q *playlist.GetPlaylistByUidQuery) (*playlist.Playlist, error) { + return s.store.Get(ctx, q) +} + +func (s *Service) GetItems(ctx context.Context, q *playlist.GetPlaylistItemsByUidQuery) ([]playlist.PlaylistItem, error) { + return s.store.GetItems(ctx, q) +} + +func (s *Service) Search(ctx context.Context, q *playlist.GetPlaylistsQuery) (playlist.Playlists, error) { + return s.store.List(ctx, q) +} + +func (s *Service) Delete(ctx context.Context, cmd *playlist.DeletePlaylistCommand) error { + return s.store.Delete(ctx, cmd) +} diff --git a/pkg/services/playlist/playlistimpl/store.go b/pkg/services/playlist/playlistimpl/store.go new file mode 100644 index 00000000000..121e3144ffc --- /dev/null +++ b/pkg/services/playlist/playlistimpl/store.go @@ -0,0 +1,226 @@ +package playlistimpl + +import ( + "context" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/playlist" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/db" + "github.com/grafana/grafana/pkg/util" +) + +type store interface { + Insert(context.Context, *playlist.CreatePlaylistCommand) (*playlist.Playlist, error) + Delete(context.Context, *playlist.DeletePlaylistCommand) error + Get(context.Context, *playlist.GetPlaylistByUidQuery) (*playlist.Playlist, error) + GetItems(context.Context, *playlist.GetPlaylistItemsByUidQuery) ([]playlist.PlaylistItem, error) + List(context.Context, *playlist.GetPlaylistsQuery) (playlist.Playlists, error) + Update(context.Context, *playlist.UpdatePlaylistCommand) (*playlist.PlaylistDTO, error) +} + +type sqlStore struct { + db db.DB +} + +func (s *sqlStore) Insert(ctx context.Context, cmd *playlist.CreatePlaylistCommand) (*playlist.Playlist, error) { + p := playlist.Playlist{} + err := s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + uid, err := generateAndValidateNewPlaylistUid(sess, cmd.OrgId) + if err != nil { + return err + } + + p = playlist.Playlist{ + Name: cmd.Name, + Interval: cmd.Interval, + OrgId: cmd.OrgId, + UID: uid, + } + + _, err = sess.Insert(&p) + if err != nil { + return err + } + + playlistItems := make([]playlist.PlaylistItem, 0) + for _, item := range cmd.Items { + playlistItems = append(playlistItems, playlist.PlaylistItem{ + PlaylistId: p.Id, + Type: item.Type, + Value: item.Value, + Order: item.Order, + Title: item.Title, + }) + } + + _, err = sess.Insert(&playlistItems) + + return err + }) + return &p, err +} + +func (s *sqlStore) Update(ctx context.Context, cmd *playlist.UpdatePlaylistCommand) (*playlist.PlaylistDTO, error) { + dto := playlist.PlaylistDTO{} + err := s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + p := playlist.Playlist{ + UID: cmd.UID, + OrgId: cmd.OrgId, + Name: cmd.Name, + Interval: cmd.Interval, + } + + existingPlaylist := playlist.Playlist{UID: cmd.UID, OrgId: cmd.OrgId} + _, err := sess.Get(&existingPlaylist) + if err != nil { + return err + } + p.Id = existingPlaylist.Id + + dto = playlist.PlaylistDTO{ + + Id: p.Id, + UID: p.UID, + OrgId: p.OrgId, + Name: p.Name, + Interval: p.Interval, + } + + _, err = sess.Where("id=?", p.Id).Cols("name", "interval").Update(&p) + if err != nil { + return err + } + + rawSQL := "DELETE FROM playlist_item WHERE playlist_id = ?" + _, err = sess.Exec(rawSQL, p.Id) + + if err != nil { + return err + } + + playlistItems := make([]models.PlaylistItem, 0) + + for index, item := range cmd.Items { + playlistItems = append(playlistItems, models.PlaylistItem{ + PlaylistId: p.Id, + Type: item.Type, + Value: item.Value, + Order: index + 1, + Title: item.Title, + }) + } + + _, err = sess.Insert(&playlistItems) + return err + }) + return &dto, err +} + +func (s *sqlStore) Get(ctx context.Context, query *playlist.GetPlaylistByUidQuery) (*playlist.Playlist, error) { + if query.UID == "" || query.OrgId == 0 { + return nil, playlist.ErrCommandValidationFailed + } + + p := playlist.Playlist{} + err := s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + p = playlist.Playlist{UID: query.UID, OrgId: query.OrgId} + exists, err := sess.Get(&p) + if !exists { + return playlist.ErrPlaylistNotFound + } + + return err + }) + return &p, err +} + +func (s *sqlStore) Delete(ctx context.Context, cmd *playlist.DeletePlaylistCommand) error { + if cmd.UID == "" || cmd.OrgId == 0 { + return playlist.ErrCommandValidationFailed + } + + return s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + playlist := playlist.Playlist{UID: cmd.UID, OrgId: cmd.OrgId} + _, err := sess.Get(&playlist) + if err != nil { + return err + } + + var rawPlaylistSQL = "DELETE FROM playlist WHERE uid = ? and org_id = ?" + _, err = sess.Exec(rawPlaylistSQL, cmd.UID, cmd.OrgId) + if err != nil { + return err + } + + var rawItemSQL = "DELETE FROM playlist_item WHERE playlist_id = ?" + _, err = sess.Exec(rawItemSQL, playlist.Id) + + return err + }) +} + +func (s *sqlStore) List(ctx context.Context, query *playlist.GetPlaylistsQuery) (playlist.Playlists, error) { + playlists := make(playlist.Playlists, 0) + if query.OrgId == 0 { + return playlists, playlist.ErrCommandValidationFailed + } + + err := s.db.WithDbSession(ctx, func(dbSess *sqlstore.DBSession) error { + sess := dbSess.Limit(query.Limit) + + if query.Name != "" { + sess.Where("name LIKE ?", "%"+query.Name+"%") + } + + sess.Where("org_id = ?", query.OrgId) + err := sess.Find(&playlists) + + return err + }) + return playlists, err +} + +func (s *sqlStore) GetItems(ctx context.Context, query *playlist.GetPlaylistItemsByUidQuery) ([]playlist.PlaylistItem, error) { + var playlistItems = make([]playlist.PlaylistItem, 0) + err := s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + if query.PlaylistUID == "" || query.OrgId == 0 { + return models.ErrCommandValidationFailed + } + + // getQuery the playlist Id + getQuery := &playlist.GetPlaylistByUidQuery{UID: query.PlaylistUID, OrgId: query.OrgId} + p, err := s.Get(ctx, getQuery) + if err != nil { + return err + } + + err = sess.Where("playlist_id=?", p.Id).Find(&playlistItems) + + return err + }) + return playlistItems, err +} + +// generateAndValidateNewPlaylistUid generates a playlistUID and verifies that +// the uid isn't already in use. This is deliberately overly cautious, since users +// can also specify playlist uids during provisioning. +func generateAndValidateNewPlaylistUid(sess *sqlstore.DBSession, orgId int64) (string, error) { + for i := 0; i < 3; i++ { + uid := generateNewUid() + + playlist := models.Playlist{OrgId: orgId, UID: uid} + exists, err := sess.Get(&playlist) + if err != nil { + return "", err + } + + if !exists { + return uid, nil + } + } + + return "", models.ErrPlaylistFailedGenerateUniqueUid +} + +var generateNewUid func() string = util.GenerateShortUID diff --git a/pkg/services/playlist/playlistimpl/store_test.go b/pkg/services/playlist/playlistimpl/store_test.go new file mode 100644 index 00000000000..0eb1fe842aa --- /dev/null +++ b/pkg/services/playlist/playlistimpl/store_test.go @@ -0,0 +1,82 @@ +package playlistimpl + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/services/playlist" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/stretchr/testify/require" +) + +func TestIntegrationPlaylistDataAccess(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ss := sqlstore.InitTestDB(t) + playlistStore := sqlStore{db: ss} + + t.Run("Can create playlist", func(t *testing.T) { + items := []playlist.PlaylistItemDTO{ + {Title: "graphite", Value: "graphite", Type: "dashboard_by_tag"}, + {Title: "Backend response times", Value: "3", Type: "dashboard_by_id"}, + } + cmd := playlist.CreatePlaylistCommand{Name: "NYC office", Interval: "10m", OrgId: 1, Items: items} + p, err := playlistStore.Insert(context.Background(), &cmd) + require.NoError(t, err) + uid := p.UID + + t.Run("Can get playlist items", func(t *testing.T) { + get := &playlist.GetPlaylistItemsByUidQuery{PlaylistUID: uid, OrgId: 1} + storedPlaylistItems, err := playlistStore.GetItems(context.Background(), get) + require.NoError(t, err) + require.Equal(t, len(storedPlaylistItems), len(items)) + }) + + t.Run("Can update playlist", func(t *testing.T) { + items := []playlist.PlaylistItemDTO{ + {Title: "influxdb", Value: "influxdb", Type: "dashboard_by_tag"}, + {Title: "Backend response times", Value: "2", Type: "dashboard_by_id"}, + } + query := playlist.UpdatePlaylistCommand{Name: "NYC office ", OrgId: 1, UID: uid, Interval: "10s", Items: items} + _, err = playlistStore.Update(context.Background(), &query) + require.NoError(t, err) + }) + + t.Run("Can remove playlist", func(t *testing.T) { + deleteQuery := playlist.DeletePlaylistCommand{UID: uid, OrgId: 1} + err = playlistStore.Delete(context.Background(), &deleteQuery) + require.NoError(t, err) + + getQuery := playlist.GetPlaylistByUidQuery{UID: uid, OrgId: 1} + p, err := playlistStore.Get(context.Background(), &getQuery) + require.Error(t, err) + require.Equal(t, uid, p.UID, "playlist should've been removed") + require.ErrorIs(t, err, playlist.ErrPlaylistNotFound) + }) + }) + + t.Run("Delete playlist that doesn't exist", func(t *testing.T) { + deleteQuery := playlist.DeletePlaylistCommand{UID: "654312", OrgId: 1} + err := playlistStore.Delete(context.Background(), &deleteQuery) + require.NoError(t, err) + }) + + t.Run("Delete playlist with invalid command yields error", func(t *testing.T) { + testCases := []struct { + desc string + cmd playlist.DeletePlaylistCommand + }{ + {desc: "none", cmd: playlist.DeletePlaylistCommand{}}, + {desc: "no OrgId", cmd: playlist.DeletePlaylistCommand{UID: "1"}}, + {desc: "no Uid", cmd: playlist.DeletePlaylistCommand{OrgId: 1}}, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + err := playlistStore.Delete(context.Background(), &tc.cmd) + require.EqualError(t, err, playlist.ErrCommandValidationFailed.Error()) + }) + } + }) +} diff --git a/pkg/services/playlist/playlisttest/fake.go b/pkg/services/playlist/playlisttest/fake.go new file mode 100644 index 00000000000..2f99a88c5b1 --- /dev/null +++ b/pkg/services/playlist/playlisttest/fake.go @@ -0,0 +1,43 @@ +package playlisttest + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/playlist" +) + +type FakePlaylistService struct { + ExpectedPlaylist *playlist.Playlist + ExpectedPlaylistDTO *playlist.PlaylistDTO + ExpectedPlaylistItems []playlist.PlaylistItem + ExpectedPlaylists playlist.Playlists + ExpectedError error +} + +func NewPlaylistServiveFake() *FakePlaylistService { + return &FakePlaylistService{} +} + +func (f *FakePlaylistService) Create(context.Context, *playlist.CreatePlaylistCommand) (*playlist.Playlist, error) { + return f.ExpectedPlaylist, f.ExpectedError +} + +func (f *FakePlaylistService) Update(context.Context, *playlist.UpdatePlaylistCommand) (*playlist.PlaylistDTO, error) { + return f.ExpectedPlaylistDTO, f.ExpectedError +} + +func (f *FakePlaylistService) Get(context.Context, *playlist.GetPlaylistByUidQuery) (*playlist.Playlist, error) { + return f.ExpectedPlaylist, f.ExpectedError +} + +func (f *FakePlaylistService) GetItems(context.Context, *playlist.GetPlaylistItemsByUidQuery) ([]playlist.PlaylistItem, error) { + return f.ExpectedPlaylistItems, f.ExpectedError +} + +func (f *FakePlaylistService) Search(context.Context, *playlist.GetPlaylistsQuery) (playlist.Playlists, error) { + return f.ExpectedPlaylists, f.ExpectedError +} + +func (f *FakePlaylistService) Delete(ctx context.Context, cmd *playlist.DeletePlaylistCommand) error { + return f.ExpectedError +} diff --git a/pkg/services/sqlstore/store.go b/pkg/services/sqlstore/store.go index 55bc7f85a3d..bcba1b75781 100644 --- a/pkg/services/sqlstore/store.go +++ b/pkg/services/sqlstore/store.go @@ -72,11 +72,17 @@ type Store interface { GetGlobalQuotaByTarget(ctx context.Context, query *models.GetGlobalQuotaByTargetQuery) error WithTransactionalDbSession(ctx context.Context, callback DBTransactionFunc) error InTransaction(ctx context.Context, fn func(ctx context.Context) error) error + // deprecated CreatePlaylist(ctx context.Context, cmd *models.CreatePlaylistCommand) error + // deprecated UpdatePlaylist(ctx context.Context, cmd *models.UpdatePlaylistCommand) error + // deprecated GetPlaylist(ctx context.Context, query *models.GetPlaylistByUidQuery) error + // deprecated DeletePlaylist(ctx context.Context, cmd *models.DeletePlaylistCommand) error + // deprecated SearchPlaylists(ctx context.Context, query *models.GetPlaylistsQuery) error + // deprecated GetPlaylistItem(ctx context.Context, query *models.GetPlaylistItemsByUidQuery) error GetAlertById(ctx context.Context, query *models.GetAlertByIdQuery) error GetAllAlertQueryHandler(ctx context.Context, query *models.GetAllAlertsQuery) error diff --git a/public/api-merged.json b/public/api-merged.json index c83c8d61fb2..98ab31b0a08 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -8535,6 +8535,14 @@ "tags": ["provisioning"], "summary": "Get all the contact points.", "operationId": "RouteGetContactpoints", + "parameters": [ + { + "type": "string", + "description": "Filter by name", + "name": "name", + "in": "query" + } + ], "responses": { "200": { "description": "ContactPoints", @@ -8874,6 +8882,20 @@ } } } + }, + "delete": { + "consumes": ["application/json"], + "tags": ["provisioning"], + "summary": "Clears the notification policy tree.", + "operationId": "RouteResetPolicyTree", + "responses": { + "202": { + "description": "Ack", + "schema": { + "$ref": "#/definitions/Ack" + } + } + } } }, "/v1/provisioning/templates": { @@ -10250,7 +10272,7 @@ "$ref": "#/definitions/ScheduleDTO" }, "state": { - "type": "string" + "$ref": "#/definitions/State" }, "templateVars": { "type": "object" @@ -10268,9 +10290,6 @@ "CreatePlaylistCommand": { "type": "object", "properties": { - "Result": { - "$ref": "#/definitions/Playlist" - }, "interval": { "type": "string" }, @@ -15496,9 +15515,8 @@ "type": "string" }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "type": "object", - "title": "A URL represents a parsed URL (technically, a URI reference).", + "title": "URL is a custom URL type that allows validation at configuration load time.", "properties": { "ForceQuery": { "type": "boolean" @@ -15768,9 +15786,6 @@ "UpdatePlaylistCommand": { "type": "object", "properties": { - "Result": { - "$ref": "#/definitions/PlaylistDTO" - }, "interval": { "type": "string" }, @@ -16298,6 +16313,7 @@ } }, "alertGroup": { + "description": "AlertGroup alert group", "type": "object", "required": ["alerts", "labels", "receiver"], "properties": { @@ -16461,6 +16477,7 @@ } }, "gettableSilence": { + "description": "GettableSilence gettable silence", "type": "object", "required": ["comment", "createdBy", "endsAt", "matchers", "startsAt", "id", "status", "updatedAt"], "properties": { @@ -16500,7 +16517,6 @@ } }, "gettableSilences": { - "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" @@ -16634,7 +16650,6 @@ } }, "receiver": { - "description": "Receiver receiver", "type": "object", "required": ["name"], "properties": { diff --git a/public/api-spec.json b/public/api-spec.json index b438b6d5e39..e9275cccdff 100644 --- a/public/api-spec.json +++ b/public/api-spec.json @@ -9380,7 +9380,7 @@ "$ref": "#/definitions/ScheduleDTO" }, "state": { - "type": "string" + "$ref": "#/definitions/State" }, "templateVars": { "type": "object" @@ -9398,9 +9398,6 @@ "CreatePlaylistCommand": { "type": "object", "properties": { - "Result": { - "$ref": "#/definitions/Playlist" - }, "interval": { "type": "string" }, @@ -12676,9 +12673,6 @@ "UpdatePlaylistCommand": { "type": "object", "properties": { - "Result": { - "$ref": "#/definitions/PlaylistDTO" - }, "interval": { "type": "string" }, From 076851313ddb95cda03203e434c03a499fdede83 Mon Sep 17 00:00:00 2001 From: brendamuir <100768211+brendamuir@users.noreply.github.com> Date: Mon, 18 Jul 2022 11:09:29 +0100 Subject: [PATCH 9/9] Docs: fixes warning for enterprise customers (#52385) * Docs: fixes data source links * fixes unified alerting redirect * Docs: fixes enterprise customer warning --- docs/sources/alerting/migrating-alerts/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/alerting/migrating-alerts/_index.md b/docs/sources/alerting/migrating-alerts/_index.md index 83b714edd6a..8e7c2ce778c 100644 --- a/docs/sources/alerting/migrating-alerts/_index.md +++ b/docs/sources/alerting/migrating-alerts/_index.md @@ -13,7 +13,7 @@ weight: 101 Grafana Alerting is enabled by default for new installations or existing installations whether or not legacy alerting is configured. -> **Note**: We recommend that Grafana Enterprise customers with more than a dozen Grafana dashboard alert rules do not upgrade and remain on legacy alerting for now by [opting out]({{< relref "opt-out/" >}}). If you do want to upgrade to Grafana Alerting, contact customer support. +> **Note**: When upgrading, your dashboard alerts are migrated to a new format. This migration can be rolled back easily by [opting out]({{< relref "opt-out/" >}}). If you have any questions regarding this migration, please contact us. Existing installations that do not use legacy alerting will have Grafana Alerting enabled by default unless alerting is disabled in the configuration.