Provisioing: Update provisioned folder with a manager identity (#110988)
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user