Provisioing: Update provisioned folder with a manager identity (#110988)

This commit is contained in:
Ryan McKinley
2025-09-12 10:50:10 +03:00
committed by GitHub
parent 0b9e0ef4dc
commit 1944d2dd0e
12 changed files with 123 additions and 67 deletions
+2 -1
View File
@@ -65,7 +65,8 @@ type DashboardProvisioningService interface {
GetProvisionedDashboardData(ctx context.Context, name string) ([]*DashboardProvisioning, error)
GetProvisionedDashboardDataByDashboardID(ctx context.Context, dashboardID int64) (*DashboardProvisioning, error)
GetProvisionedDashboardDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*DashboardProvisioning, error)
SaveFolderForProvisionedDashboards(ctx context.Context, cmd *folder.CreateFolderCommand, readerName string) (*folder.Folder, error)
SaveFolderForProvisionedDashboards(ctx context.Context, cmd *folder.CreateFolderCommand, managerIdentity string) (*folder.Folder, error)
UpdateFolderWithManagedByAnnotation(ctx context.Context, folder *folder.Folder, managerIdentity string) (*folder.Folder, error)
SaveProvisionedDashboard(ctx context.Context, dto *SaveDashboardDTO, provisioning *DashboardProvisioning) (*Dashboard, error)
UnprovisionDashboard(ctx context.Context, dashboardID int64) error
}
@@ -5,9 +5,8 @@ package dashboards
import (
context "context"
mock "github.com/stretchr/testify/mock"
folder "github.com/grafana/grafana/pkg/services/folder"
mock "github.com/stretchr/testify/mock"
)
// FakeDashboardProvisioning is an autogenerated mock type for the DashboardProvisioningService type
@@ -141,9 +140,9 @@ func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardUID(c
return r0, r1
}
// SaveFolderForProvisionedDashboards provides a mock function with given fields: _a0, _a1
func (_m *FakeDashboardProvisioning) SaveFolderForProvisionedDashboards(_a0 context.Context, _a1 *folder.CreateFolderCommand, _ string) (*folder.Folder, error) {
ret := _m.Called(_a0, _a1)
// SaveFolderForProvisionedDashboards provides a mock function with given fields: ctx, cmd, readerName
func (_m *FakeDashboardProvisioning) SaveFolderForProvisionedDashboards(ctx context.Context, cmd *folder.CreateFolderCommand, readerName string) (*folder.Folder, error) {
ret := _m.Called(ctx, cmd, readerName)
if len(ret) == 0 {
panic("no return value specified for SaveFolderForProvisionedDashboards")
@@ -151,19 +150,19 @@ func (_m *FakeDashboardProvisioning) SaveFolderForProvisionedDashboards(_a0 cont
var r0 *folder.Folder
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *folder.CreateFolderCommand) (*folder.Folder, error)); ok {
return rf(_a0, _a1)
if rf, ok := ret.Get(0).(func(context.Context, *folder.CreateFolderCommand, string) (*folder.Folder, error)); ok {
return rf(ctx, cmd, readerName)
}
if rf, ok := ret.Get(0).(func(context.Context, *folder.CreateFolderCommand) *folder.Folder); ok {
r0 = rf(_a0, _a1)
if rf, ok := ret.Get(0).(func(context.Context, *folder.CreateFolderCommand, string) *folder.Folder); ok {
r0 = rf(ctx, cmd, readerName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*folder.Folder)
}
}
if rf, ok := ret.Get(1).(func(context.Context, *folder.CreateFolderCommand) error); ok {
r1 = rf(_a0, _a1)
if rf, ok := ret.Get(1).(func(context.Context, *folder.CreateFolderCommand, string) error); ok {
r1 = rf(ctx, cmd, readerName)
} else {
r1 = ret.Error(1)
}
@@ -219,6 +218,36 @@ func (_m *FakeDashboardProvisioning) UnprovisionDashboard(ctx context.Context, d
return r0
}
// UpdateFolderWithManagedByAnnotation provides a mock function with given fields: ctx, _a1, readerName
func (_m *FakeDashboardProvisioning) UpdateFolderWithManagedByAnnotation(ctx context.Context, _a1 *folder.Folder, readerName string) (*folder.Folder, error) {
ret := _m.Called(ctx, _a1, readerName)
if len(ret) == 0 {
panic("no return value specified for UpdateFolderWithManagedByAnnotation")
}
var r0 *folder.Folder
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *folder.Folder, string) (*folder.Folder, error)); ok {
return rf(ctx, _a1, readerName)
}
if rf, ok := ret.Get(0).(func(context.Context, *folder.Folder, string) *folder.Folder); ok {
r0 = rf(ctx, _a1, readerName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*folder.Folder)
}
}
if rf, ok := ret.Get(1).(func(context.Context, *folder.Folder, string) error); ok {
r1 = rf(ctx, _a1, readerName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewFakeDashboardProvisioning creates a new instance of FakeDashboardProvisioning. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewFakeDashboardProvisioning(t interface {
@@ -990,6 +990,27 @@ func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.C
return f, nil
}
// UpdateFolderWithManagedByAnnotation implements dashboards.DashboardProvisioningService.
func (dr *DashboardServiceImpl) UpdateFolderWithManagedByAnnotation(ctx context.Context, f *folder.Folder, readerName string) (*folder.Folder, error) {
ctx, span := tracer.Start(ctx, "dashboards.service.UpdateFolderWithManagedByAnnotation")
defer span.End()
ctx, ident := identity.WithServiceIdentity(ctx, f.OrgID)
updated, err := dr.folderService.Update(ctx, &folder.UpdateFolderCommand{
UID: f.UID,
OrgID: f.OrgID,
SignedInUser: ident,
ManagerKindClassicFP: readerName, // nolint:staticcheck
Overwrite: true,
Version: f.Version,
})
if err != nil {
dr.log.Error("failed to update folder for provisioned dashboards", "folder", f.Title, "org", f.OrgID, "err", err)
return nil, err
}
return updated, nil
}
func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO,
allowUiUpdate bool) (*dashboards.Dashboard, error) {
ctx, span := tracer.Start(ctx, "dashboards.service.SaveDashboard")
-31
View File
@@ -5,7 +5,6 @@ package dashboards
import (
context "context"
quota "github.com/grafana/grafana/pkg/services/quota"
mock "github.com/stretchr/testify/mock"
)
@@ -32,36 +31,6 @@ func (_m *FakeDashboardStore) CleanupAfterDelete(ctx context.Context, cmd *Delet
return r0
}
// Count provides a mock function with given fields: _a0, _a1
func (_m *FakeDashboardStore) Count(_a0 context.Context, _a1 *quota.ScopeParameters) (*quota.Map, error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for Count")
}
var r0 *quota.Map
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *quota.ScopeParameters) (*quota.Map, error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, *quota.ScopeParameters) *quota.Map); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*quota.Map)
}
}
if rf, ok := ret.Get(1).(func(context.Context, *quota.ScopeParameters) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountInOrg provides a mock function with given fields: ctx, orgID, isFolder
func (_m *FakeDashboardStore) CountInOrg(ctx context.Context, orgID int64, isFolder bool) (int64, error) {
ret := _m.Called(ctx, orgID, isFolder)
@@ -562,14 +562,15 @@ func (s *Service) updateOnApiServer(ctx context.Context, cmd *folder.UpdateFolde
user := cmd.SignedInUser
foldr, err := s.unifiedStore.Update(ctx, folder.UpdateFolderCommand{
UID: cmd.UID,
OrgID: cmd.OrgID,
NewTitle: cmd.NewTitle,
NewDescription: cmd.NewDescription,
SignedInUser: user,
Overwrite: cmd.Overwrite,
Version: cmd.Version,
folder, err := s.unifiedStore.Update(ctx, folder.UpdateFolderCommand{
UID: cmd.UID,
OrgID: cmd.OrgID,
NewTitle: cmd.NewTitle,
NewDescription: cmd.NewDescription,
SignedInUser: user,
Overwrite: cmd.Overwrite,
Version: cmd.Version,
ManagerKindClassicFP: cmd.ManagerKindClassicFP, // nolint:staticcheck
})
if err != nil {
@@ -579,7 +580,7 @@ func (s *Service) updateOnApiServer(ctx context.Context, cmd *folder.UpdateFolde
if cmd.NewTitle != nil {
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc()
if err := s.publishFolderFullPathUpdatedEventViaApiServer(ctx, foldr.Updated, cmd.OrgID, cmd.UID); err != nil {
if err := s.publishFolderFullPathUpdatedEventViaApiServer(ctx, folder.Updated, cmd.OrgID, cmd.UID); err != nil {
return nil, err
}
}
@@ -587,7 +588,7 @@ func (s *Service) updateOnApiServer(ctx context.Context, cmd *folder.UpdateFolde
// always expose the dashboard store sequential ID
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc()
return foldr, nil
return folder, nil
}
func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
+12 -4
View File
@@ -97,6 +97,10 @@ func (ss *FolderUnifiedStoreImpl) Update(ctx context.Context, cmd folder.UpdateF
return nil, err
}
updated := obj.DeepCopy()
meta, err := utils.MetaAccessor(updated)
if err != nil {
return nil, err
}
if cmd.NewTitle != nil {
err = unstructured.SetNestedField(updated.Object, *cmd.NewTitle, "spec", "title")
@@ -111,10 +115,6 @@ func (ss *FolderUnifiedStoreImpl) Update(ctx context.Context, cmd folder.UpdateF
}
}
if cmd.NewParentUID != nil {
meta, err := utils.MetaAccessor(updated)
if err != nil {
return nil, err
}
meta.SetFolder(*cmd.NewParentUID)
} else {
// only compare versions if not moving the folder
@@ -123,6 +123,14 @@ func (ss *FolderUnifiedStoreImpl) Update(ctx context.Context, cmd folder.UpdateF
}
}
// nolint:staticcheck
if cmd.ManagerKindClassicFP != "" {
meta.SetManagerProperties(utils.ManagerProperties{
Kind: utils.ManagerKindClassicFP,
Identity: cmd.ManagerKindClassicFP,
})
}
out, err := ss.k8sclient.Update(ctx, updated, cmd.OrgID, v1.UpdateOptions{
FieldValidation: v1.FieldValidationIgnore,
})
+7
View File
@@ -158,6 +158,13 @@ type UpdateFolderCommand struct {
Overwrite bool `json:"overwrite"`
SignedInUser identity.Requester `json:"-"`
// When running classic file provisioning with folders saved in kubernetes,
// folders will be marked with a manager of kind ManagerKindClassicFP
// NOTE: this is ignored when running legacy SQL storage
//
// Deprecated: this should only be used by the legacy file provisioning system
ManagerKindClassicFP string `json:"-"`
}
// MoveFolderCommand captures the information required by the folder service
@@ -6,7 +6,8 @@ import (
"os"
"time"
dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
folderV1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/folder"
@@ -57,8 +58,17 @@ func New(ctx context.Context, configDirectory string, provisioner dashboards.Das
return nil, fmt.Errorf("%v: %w", "Failed to initialize file readers", err)
}
if dual != nil && !dual.ShouldManage(dashboard.DashboardResourceInfo.GroupResource()) {
dual = nil // not actively managed
if dual != nil {
foldersInUnified, _ := dual.ReadFromUnified(context.Background(), folderV1.FolderResourceInfo.GroupResource())
if foldersInUnified {
for _, reader := range fileReaders {
reader.foldersInUnified = true
}
}
if !dual.ShouldManage(dashboardV1.DashboardResourceInfo.GroupResource()) {
dual = nil // not actively managed
}
}
d := &Provisioner{
@@ -78,7 +88,7 @@ func New(ctx context.Context, configDirectory string, provisioner dashboards.Das
func (provider *Provisioner) Provision(ctx context.Context) error {
// skip provisioning during migrations to prevent multi-replica instances from crashing when another replica is migrating
if provider.dual != nil {
status, _ := provider.dual.Status(context.Background(), dashboard.DashboardResourceInfo.GroupResource())
status, _ := provider.dual.Status(context.Background(), dashboardV1.DashboardResourceInfo.GroupResource())
if status.Migrating > 0 {
provider.log.Info("dashboard migrations are running, skipping provisioning", "elapsed", time.Since(time.UnixMilli(status.Migrating)))
return nil
@@ -40,6 +40,7 @@ type FileReader struct {
dashboardStore utils.DashboardStore
FoldersFromFilesStructure bool
folderService folder.Service
foldersInUnified bool
mux sync.RWMutex
usageTracker *usageTracker
@@ -382,6 +383,14 @@ func (fr *FileReader) getOrCreateFolder(ctx context.Context, cfg *config, servic
return 0, "", dashboards.ErrFolderInvalidUID
}
// When we expect folders in unified storage, they should have a manager indicated
if err == nil && result != nil && result.ManagedBy == "" && fr.foldersInUnified {
result, err = service.UpdateFolderWithManagedByAnnotation(ctx, result, fr.Cfg.Name)
if err != nil {
return 0, "", fmt.Errorf("unable to update provisioned folder")
}
}
// dashboard folder not found. create one.
if errors.Is(err, dashboards.ErrFolderNotFound) {
createCmd := &folder.CreateFolderCommand{
@@ -145,7 +145,7 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
cfg.Folder = "Team A"
fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once()
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&folder.Folder{ID: 1}, nil).Once()
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything, configName).Return(&folder.Folder{ID: 1}, nil).Once()
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{ID: 2}, nil).Times(2)
reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
reader.dashboardProvisioningService = fakeService
@@ -324,7 +324,7 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
cfg.Options["foldersFromFilesStructure"] = true
fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once()
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&folder.Folder{}, nil).Times(2)
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything, configName).Return(&folder.Folder{}, nil).Times(2)
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(3)
reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
@@ -362,7 +362,7 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
cfg2 := &config{Name: "2", Type: "file", OrgID: 1, Folder: "f2", Options: map[string]any{"path": containingID}}
fakeService.On("GetProvisionedDashboardData", mock.Anything, mock.AnythingOfType("string")).Return(nil, nil).Times(2)
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&folder.Folder{}, nil).Times(2)
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything, mock.Anything).Return(&folder.Folder{}, nil).Times(2)
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(2)
reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore, folderSvc)
@@ -410,7 +410,7 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
"folder": defaultDashboards,
},
}
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&folder.Folder{ID: 1}, nil).Once()
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything, cfg.Name).Return(&folder.Folder{ID: 1}, nil).Once()
r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
require.NoError(t, err)
@@ -66,7 +66,7 @@ func TestIntegrationDuplicatesValidator(t *testing.T) {
fakeStore := &fakeDashboardStore{}
r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
require.NoError(t, err)
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&folder.Folder{}, nil).Times(6)
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything, mock.Anything).Return(&folder.Folder{}, nil).Times(6)
fakeService.On("GetProvisionedDashboardData", mock.Anything, mock.AnythingOfType("string")).Return([]*dashboards.DashboardProvisioning{}, nil).Times(4)
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(5)
_, folderUID, err := r.getOrCreateFolder(ctx, cfg, fakeService, folderName)
@@ -183,7 +183,7 @@ func TestIntegrationDuplicatesValidator(t *testing.T) {
})
t.Run("Duplicates validator should restrict write access only for readers with duplicates", func(t *testing.T) {
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&folder.Folder{}, nil).Times(5)
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything, mock.Anything).Return(&folder.Folder{}, nil).Times(5)
fakeService.On("GetProvisionedDashboardData", mock.Anything, mock.AnythingOfType("string")).Return([]*dashboards.DashboardProvisioning{}, nil).Times(3)
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(5)
fakeStore := &fakeDashboardStore{}
@@ -88,6 +88,7 @@ func ProvideService(
resourcePermissions: resourcePermissions,
tracer: tracer,
migratePrometheusType: promTypeMigrationProvider.Run,
dual: dual,
}
if err := s.setDashboardProvisioner(); err != nil {