Provisioning: Delete from repository, not the resource client on rename (#104291)

Co-authored-by: Roberto Jimenez Sanchez <roberto.jimenez@grafana.com>
This commit is contained in:
Ryan McKinley
2025-04-23 12:30:30 +02:00
committed by GitHub
co-authored by Roberto Jimenez Sanchez
parent 8cee546d67
commit 3923538ba4
12 changed files with 263 additions and 820 deletions
@@ -6,11 +6,16 @@ import (
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
"k8s.io/client-go/dynamic"
)
func ExportAll(ctx context.Context, repoName string, options provisioning.ExportJobOptions, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, folderClient dynamic.ResourceInterface, progress jobs.JobProgressRecorder) error {
func ExportAll(ctx context.Context, repoName string, options provisioning.ExportJobOptions, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error {
// FIXME: should we sign with grafana user?
folderClient, err := clients.Folder()
if err != nil {
return err
}
if err := ExportFolders(ctx, repoName, options, folderClient, repositoryResources, progress); err != nil {
return err
}
@@ -5,15 +5,17 @@ import (
"errors"
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/dynamic"
"github.com/grafana/grafana/pkg/apimachinery/utils"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/dynamic"
)
// ExportFolders will load the full folder tree into memory and update the repositoryResources tree
func ExportFolders(ctx context.Context, repoName string, options provisioning.ExportJobOptions, folderClient dynamic.ResourceInterface, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error {
// Load and write all folders
// FIXME: we load the entire tree in memory
@@ -6,8 +6,6 @@ import (
context "context"
jobs "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
dynamic "k8s.io/client-go/dynamic"
mock "github.com/stretchr/testify/mock"
resources "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
@@ -28,17 +26,17 @@ func (_m *MockExportFn) EXPECT() *MockExportFn_Expecter {
return &MockExportFn_Expecter{mock: &_m.Mock}
}
// Execute provides a mock function with given fields: ctx, repoName, options, clients, repositoryResources, folderClient, progress
func (_m *MockExportFn) Execute(ctx context.Context, repoName string, options v0alpha1.ExportJobOptions, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, folderClient dynamic.ResourceInterface, progress jobs.JobProgressRecorder) error {
ret := _m.Called(ctx, repoName, options, clients, repositoryResources, folderClient, progress)
// Execute provides a mock function with given fields: ctx, repoName, options, clients, repositoryResources, progress
func (_m *MockExportFn) Execute(ctx context.Context, repoName string, options v0alpha1.ExportJobOptions, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error {
ret := _m.Called(ctx, repoName, options, clients, repositoryResources, progress)
if len(ret) == 0 {
panic("no return value specified for Execute")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, v0alpha1.ExportJobOptions, resources.ResourceClients, resources.RepositoryResources, dynamic.ResourceInterface, jobs.JobProgressRecorder) error); ok {
r0 = rf(ctx, repoName, options, clients, repositoryResources, folderClient, progress)
if rf, ok := ret.Get(0).(func(context.Context, string, v0alpha1.ExportJobOptions, resources.ResourceClients, resources.RepositoryResources, jobs.JobProgressRecorder) error); ok {
r0 = rf(ctx, repoName, options, clients, repositoryResources, progress)
} else {
r0 = ret.Error(0)
}
@@ -57,15 +55,14 @@ type MockExportFn_Execute_Call struct {
// - options v0alpha1.ExportJobOptions
// - clients resources.ResourceClients
// - repositoryResources resources.RepositoryResources
// - folderClient dynamic.ResourceInterface
// - progress jobs.JobProgressRecorder
func (_e *MockExportFn_Expecter) Execute(ctx interface{}, repoName interface{}, options interface{}, clients interface{}, repositoryResources interface{}, folderClient interface{}, progress interface{}) *MockExportFn_Execute_Call {
return &MockExportFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repoName, options, clients, repositoryResources, folderClient, progress)}
func (_e *MockExportFn_Expecter) Execute(ctx interface{}, repoName interface{}, options interface{}, clients interface{}, repositoryResources interface{}, progress interface{}) *MockExportFn_Execute_Call {
return &MockExportFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repoName, options, clients, repositoryResources, progress)}
}
func (_c *MockExportFn_Execute_Call) Run(run func(ctx context.Context, repoName string, options v0alpha1.ExportJobOptions, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, folderClient dynamic.ResourceInterface, progress jobs.JobProgressRecorder)) *MockExportFn_Execute_Call {
func (_c *MockExportFn_Execute_Call) Run(run func(ctx context.Context, repoName string, options v0alpha1.ExportJobOptions, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder)) *MockExportFn_Execute_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(v0alpha1.ExportJobOptions), args[3].(resources.ResourceClients), args[4].(resources.RepositoryResources), args[5].(dynamic.ResourceInterface), args[6].(jobs.JobProgressRecorder))
run(args[0].(context.Context), args[1].(string), args[2].(v0alpha1.ExportJobOptions), args[3].(resources.ResourceClients), args[4].(resources.RepositoryResources), args[5].(jobs.JobProgressRecorder))
})
return _c
}
@@ -75,7 +72,7 @@ func (_c *MockExportFn_Execute_Call) Return(_a0 error) *MockExportFn_Execute_Cal
return _c
}
func (_c *MockExportFn_Execute_Call) RunAndReturn(run func(context.Context, string, v0alpha1.ExportJobOptions, resources.ResourceClients, resources.RepositoryResources, dynamic.ResourceInterface, jobs.JobProgressRecorder) error) *MockExportFn_Execute_Call {
func (_c *MockExportFn_Execute_Call) RunAndReturn(run func(context.Context, string, v0alpha1.ExportJobOptions, resources.ResourceClients, resources.RepositoryResources, jobs.JobProgressRecorder) error) *MockExportFn_Execute_Call {
_c.Call.Return(run)
return _c
}
@@ -11,11 +11,10 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
"k8s.io/client-go/dynamic"
)
//go:generate mockery --name ExportFn --structname MockExportFn --inpackage --filename mock_export_fn.go --with-expecter
type ExportFn func(ctx context.Context, repoName string, options provisioning.ExportJobOptions, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, folderClient dynamic.ResourceInterface, progress jobs.JobProgressRecorder) error
type ExportFn func(ctx context.Context, repoName string, options provisioning.ExportJobOptions, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error
//go:generate mockery --name WrapWithCloneFn --structname MockWrapWithCloneFn --inpackage --filename mock_wrap_with_clone_fn.go --with-expecter
type WrapWithCloneFn func(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repo repository.Repository, cloned bool) error) error
@@ -87,11 +86,6 @@ func (r *ExportWorker) Process(ctx context.Context, repo repository.Repository,
return fmt.Errorf("create clients: %w", err)
}
folderClient, err := clients.Folder()
if err != nil {
return fmt.Errorf("create folder client: %w", err)
}
rw, ok := repo.(repository.ReaderWriter)
if !ok {
return errors.New("export job submitted targeting repository that is not a ReaderWriter")
@@ -102,7 +96,7 @@ func (r *ExportWorker) Process(ctx context.Context, repo repository.Repository,
return fmt.Errorf("create repository resource client: %w", err)
}
return r.exportFn(ctx, cfg.Name, *options, clients, repositoryResources, folderClient, progress)
return r.exportFn(ctx, cfg.Name, *options, clients, repositoryResources, progress)
}
return r.wrapWithCloneFn(ctx, repo, cloneOptions, pushOptions, fn)
@@ -177,7 +177,6 @@ func TestExportWorker_ProcessNotReaderWriter(t *testing.T) {
resourceClients := resources.NewMockResourceClients(t)
mockClients := resources.NewMockClientFactory(t)
mockClients.On("Clients", context.Background(), "test-namespace").Return(resourceClients, nil)
resourceClients.On("Folder").Return(nil, nil)
mockProgress := jobs.NewMockJobProgressRecorder(t)
mockCloneFn := NewMockWrapWithCloneFn(t)
@@ -190,40 +189,6 @@ func TestExportWorker_ProcessNotReaderWriter(t *testing.T) {
require.EqualError(t, err, "export job submitted targeting repository that is not a ReaderWriter")
}
func TestExportWorker_ProcessFolderClientError(t *testing.T) {
job := v0alpha1.Job{
Spec: v0alpha1.JobSpec{
Action: v0alpha1.JobActionPush,
Push: &v0alpha1.ExportJobOptions{},
},
}
mockRepo := repository.NewMockRepository(t)
mockRepo.On("Config").Return(&v0alpha1.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
Namespace: "test-namespace",
},
Spec: v0alpha1.RepositorySpec{
Workflows: []v0alpha1.Workflow{v0alpha1.WriteWorkflow},
},
})
resourceClients := resources.NewMockResourceClients(t)
mockClients := resources.NewMockClientFactory(t)
mockClients.On("Clients", context.Background(), "test-namespace").Return(resourceClients, nil)
resourceClients.On("Folder").Return(nil, fmt.Errorf("failed to create folder client"))
mockProgress := jobs.NewMockJobProgressRecorder(t)
mockCloneFn := NewMockWrapWithCloneFn(t)
mockCloneFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error {
return fn(repo, true)
})
r := NewExportWorker(mockClients, nil, nil, mockCloneFn.Execute)
err := r.Process(context.Background(), mockRepo, job, mockProgress)
require.EqualError(t, err, "create folder client: failed to create folder client")
}
func TestExportWorker_ProcessRepositoryResourcesError(t *testing.T) {
job := v0alpha1.Job{
Spec: v0alpha1.JobSpec{
@@ -244,7 +209,6 @@ func TestExportWorker_ProcessRepositoryResourcesError(t *testing.T) {
})
resourceClients := resources.NewMockResourceClients(t)
resourceClients.On("Folder").Return(nil, nil)
mockClients := resources.NewMockClientFactory(t)
mockClients.On("Clients", context.Background(), "test-namespace").Return(resourceClients, nil)
@@ -288,7 +252,6 @@ func TestExportWorker_ProcessCloneAndPushOptions(t *testing.T) {
mockClients := resources.NewMockClientFactory(t)
mockResourceClients := resources.NewMockResourceClients(t)
mockClients.On("Clients", mock.Anything, "test-namespace").Return(mockResourceClients, nil)
mockResourceClients.On("Folder").Return(nil, nil)
mockRepoResources := resources.NewMockRepositoryResourcesFactory(t)
mockRepoResourcesClient := resources.NewMockRepositoryResources(t)
@@ -339,7 +302,6 @@ func TestExportWorker_ProcessExportFnError(t *testing.T) {
mockClients := resources.NewMockClientFactory(t)
mockResourceClients := resources.NewMockResourceClients(t)
mockClients.On("Clients", mock.Anything, "test-namespace").Return(mockResourceClients, nil)
mockResourceClients.On("Folder").Return(nil, nil)
mockRepoResources := resources.NewMockRepositoryResourcesFactory(t)
mockRepoResourcesClient := resources.NewMockRepositoryResources(t)
@@ -1,98 +0,0 @@
package migrate
import (
"context"
"errors"
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
"github.com/grafana/grafana/pkg/storage/unified/parquet"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
const maxFolders = 10000
//go:generate mockery --name LegacyFoldersMigrator --structname MockLegacyFoldersMigrator --inpackage --filename mock_legacy_folders_migrator.go --with-expecter
type LegacyFoldersMigrator interface {
resource.BulkResourceWriter
Migrate(ctx context.Context, namespace string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error
}
type legacyFoldersMigrator struct {
tree resources.FolderTree
legacyMigrator legacy.LegacyMigrator
}
func NewLegacyFoldersMigrator(legacyMigrator legacy.LegacyMigrator) LegacyFoldersMigrator {
return &legacyFoldersMigrator{
legacyMigrator: legacyMigrator,
tree: resources.NewEmptyFolderTree(),
}
}
// Close implements resource.BulkResourceWrite.
func (f *legacyFoldersMigrator) Close() error {
return nil
}
// CloseWithResults implements resource.BulkResourceWrite.
func (f *legacyFoldersMigrator) CloseWithResults() (*resource.BulkResponse, error) {
return &resource.BulkResponse{}, nil
}
// Write implements resource.BulkResourceWrite.
func (f *legacyFoldersMigrator) Write(ctx context.Context, key *resource.ResourceKey, value []byte) error {
item := &unstructured.Unstructured{}
err := item.UnmarshalJSON(value)
if err != nil {
return fmt.Errorf("unmarshal unstructured to JSON: %w", err)
}
if f.tree.Count() > maxFolders {
return errors.New("too many folders")
}
// TODO: should we check if managed already and abort migration?
return f.tree.AddUnstructured(item)
}
func (f *legacyFoldersMigrator) Migrate(ctx context.Context, namespace string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error {
progress.SetMessage(ctx, "read folders from SQL")
if _, err := f.legacyMigrator.Migrate(ctx, legacy.MigrateOptions{
Namespace: namespace,
Resources: []schema.GroupResource{resources.FolderResource.GroupResource()},
Store: parquet.NewBulkResourceWriterClient(f),
}); err != nil {
return fmt.Errorf("read folders from SQL: %w", err)
}
progress.SetMessage(ctx, "export folders from SQL")
// FIXME: we don't sign folders, not even with grafana user
if err := repositoryResources.EnsureFolderTreeExists(ctx, "", "", f.tree, func(folder resources.Folder, created bool, err error) error {
result := jobs.JobResourceResult{
Action: repository.FileActionCreated,
Name: folder.ID,
Resource: resources.FolderResource.Resource,
Group: resources.FolderResource.Group,
Path: folder.Path,
Error: err,
}
if !created {
result.Action = repository.FileActionIgnored
}
progress.Record(ctx, result)
return err
}); err != nil {
return fmt.Errorf("export folders from SQL: %w", err)
}
return nil
}
@@ -1,326 +0,0 @@
package migrate
import (
"context"
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
func TestLegacyFoldersMigrator_Write(t *testing.T) {
t.Run("should fail when json is invalid", func(t *testing.T) {
migrator := NewLegacyFoldersMigrator(legacy.NewMockLegacyMigrator(t))
err := migrator.Write(context.Background(), nil, []byte("invalid json"))
require.Error(t, err)
require.Contains(t, err.Error(), "unmarshal unstructured to JSON")
})
t.Run("should fail when too many folders", func(t *testing.T) {
migrator := NewLegacyFoldersMigrator(legacy.NewMockLegacyMigrator(t))
// Write more than maxFolders
for i := 0; i <= maxFolders+1; i++ {
folder := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "folder.grafana.app/v1alpha1",
"kind": "Folder",
"metadata": map[string]interface{}{
"name": fmt.Sprintf("test-folder-%d", i),
},
},
}
folder.SetKind("Folder")
folder.SetAPIVersion("folder.grafana.app/v1alpha1")
data, err := folder.MarshalJSON()
require.NoError(t, err)
if i == maxFolders+1 {
err = migrator.Write(context.Background(), nil, data)
require.Error(t, err)
require.Equal(t, "too many folders", err.Error())
return
}
err = migrator.Write(context.Background(), nil, data)
require.NoError(t, err)
}
})
t.Run("should add folder to tree", func(t *testing.T) {
migrator := NewLegacyFoldersMigrator(legacy.NewMockLegacyMigrator(t))
folder := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "folder.grafana.app/v1alpha1",
"kind": "Folder",
"metadata": map[string]interface{}{
"name": "test-folder",
"annotations": map[string]interface{}{
"folder.grafana.app/uid": "test-folder-uid",
},
},
},
}
folder.SetKind("Folder")
folder.SetAPIVersion("folder.grafana.app/v1alpha1")
data, err := folder.MarshalJSON()
require.NoError(t, err)
err = migrator.Write(context.Background(), nil, data)
require.NoError(t, err)
})
}
func TestLegacyFoldersMigrator_Migrate(t *testing.T) {
t.Run("should fail when legacy migrator fails", func(t *testing.T) {
mockLegacyMigrator := legacy.NewMockLegacyMigrator(t)
mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool {
return opts.Namespace == "test-namespace" &&
len(opts.Resources) == 1 &&
opts.Resources[0] == resources.FolderResource.GroupResource()
}), mock.Anything).Return(nil, errors.New("migration failed"))
migrator := NewLegacyFoldersMigrator(mockLegacyMigrator)
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, "read folders from SQL").Return()
err := migrator.Migrate(context.Background(), "test-namespace", nil, progress)
require.Error(t, err)
require.Contains(t, err.Error(), "read folders from SQL: migration failed")
progress.AssertExpectations(t)
})
t.Run("should fail when folder tree creation fails", func(t *testing.T) {
mockLegacyMigrator := legacy.NewMockLegacyMigrator(t)
mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool {
return opts.Namespace == "test-namespace" &&
len(opts.Resources) == 1 &&
opts.Resources[0] == resources.FolderResource.GroupResource()
}), mock.Anything).Return(&resource.BulkResponse{}, nil)
mockRepositoryResources := resources.NewMockRepositoryResources(t)
mockRepositoryResources.On("EnsureFolderTreeExists", mock.Anything, "", "", mock.Anything, mock.Anything).
Return(errors.New("folder tree creation failed"))
migrator := NewLegacyFoldersMigrator(mockLegacyMigrator)
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, "read folders from SQL").Return()
progress.On("SetMessage", mock.Anything, "export folders from SQL").Return()
err := migrator.Migrate(context.Background(), "test-namespace", mockRepositoryResources, progress)
require.Error(t, err)
require.Contains(t, err.Error(), "export folders from SQL: folder tree creation failed")
progress.AssertExpectations(t)
})
t.Run("should successfully migrate folders", func(t *testing.T) {
mockLegacyMigrator := legacy.NewMockLegacyMigrator(t)
mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool {
return opts.Namespace == "test-namespace" &&
len(opts.Resources) == 1 &&
opts.Resources[0] == resources.FolderResource.GroupResource()
}), mock.Anything).Run(func(args mock.Arguments) {
// Simulate writing a folder through the bulk writer
opts := args.Get(1).(legacy.MigrateOptions)
folder := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "folder.grafana.app/v1alpha1",
"kind": "Folder",
"metadata": map[string]interface{}{
"name": "test-folder",
"annotations": map[string]interface{}{
"folder.grafana.app/uid": "test-folder-uid",
},
},
},
}
folder.SetKind("Folder")
folder.SetAPIVersion("folder.grafana.app/v1alpha1")
data, err := folder.MarshalJSON()
require.NoError(t, err)
client, err := opts.Store.BulkProcess(context.Background())
require.NoError(t, err)
require.NoError(t, client.Send(&resource.BulkRequest{
Key: &resource.ResourceKey{Namespace: "test-namespace", Name: "test-folder"},
Value: data,
}))
}).Return(&resource.BulkResponse{}, nil)
mockRepositoryResources := resources.NewMockRepositoryResources(t)
mockRepositoryResources.On("EnsureFolderTreeExists", mock.Anything, "", "", mock.Anything, mock.Anything).
Run(func(args mock.Arguments) {
callback := args.Get(4).(func(folder resources.Folder, created bool, err error) error)
err := callback(resources.Folder{
ID: "test-folder-uid",
Path: "/test-folder",
}, true, nil)
require.NoError(t, err)
}).Return(nil)
migrator := NewLegacyFoldersMigrator(mockLegacyMigrator)
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, "read folders from SQL").Return()
progress.On("SetMessage", mock.Anything, "export folders from SQL").Return()
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
return result.Action == repository.FileActionCreated &&
result.Name == "test-folder-uid" &&
result.Resource == resources.FolderResource.Resource &&
result.Group == resources.FolderResource.Group &&
result.Path == "/test-folder" &&
result.Error == nil
})).Return()
err := migrator.Migrate(context.Background(), "test-namespace", mockRepositoryResources, progress)
require.NoError(t, err)
progress.AssertExpectations(t)
})
t.Run("should ignore folders that already exist", func(t *testing.T) {
mockLegacyMigrator := legacy.NewMockLegacyMigrator(t)
mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool {
return opts.Namespace == "test-namespace" &&
len(opts.Resources) == 1 &&
opts.Resources[0] == resources.FolderResource.GroupResource()
}), mock.Anything).Run(func(args mock.Arguments) {
// Simulate writing a folder through the bulk writer
opts := args.Get(1).(legacy.MigrateOptions)
folder := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "folder.grafana.app/v1alpha1",
"kind": "Folder",
"metadata": map[string]interface{}{
"name": "test-folder",
"annotations": map[string]interface{}{
"folder.grafana.app/uid": "test-folder-uid",
},
},
},
}
folder.SetKind("Folder")
folder.SetAPIVersion("folder.grafana.app/v1alpha1")
data, err := folder.MarshalJSON()
require.NoError(t, err)
client, err := opts.Store.BulkProcess(context.Background())
require.NoError(t, err)
require.NoError(t, client.Send(&resource.BulkRequest{
Key: &resource.ResourceKey{Namespace: "test-namespace", Name: "test-folder"},
Value: data,
}))
}).Return(&resource.BulkResponse{}, nil)
mockRepositoryResources := resources.NewMockRepositoryResources(t)
mockRepositoryResources.On("EnsureFolderTreeExists", mock.Anything, "", "", mock.Anything, mock.Anything).
Run(func(args mock.Arguments) {
callback := args.Get(4).(func(folder resources.Folder, created bool, err error) error)
err := callback(resources.Folder{
ID: "test-folder-uid",
Path: "/test-folder",
}, false, nil)
require.NoError(t, err)
}).Return(nil)
migrator := NewLegacyFoldersMigrator(mockLegacyMigrator)
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, "read folders from SQL").Return()
progress.On("SetMessage", mock.Anything, "export folders from SQL").Return()
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
return result.Action == repository.FileActionIgnored &&
result.Name == "test-folder-uid" &&
result.Resource == resources.FolderResource.Resource &&
result.Group == resources.FolderResource.Group &&
result.Path == "/test-folder" &&
result.Error == nil
})).Return()
err := migrator.Migrate(context.Background(), "test-namespace", mockRepositoryResources, progress)
require.NoError(t, err)
progress.AssertExpectations(t)
})
t.Run("should fail when folder creation fails", func(t *testing.T) {
mockLegacyMigrator := legacy.NewMockLegacyMigrator(t)
mockLegacyMigrator.On("Migrate", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
opts := args.Get(1).(legacy.MigrateOptions)
folder := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "folder.grafana.app/v1alpha1",
"kind": "Folder",
"metadata": map[string]interface{}{
"name": "test-folder",
"annotations": map[string]interface{}{
"folder.grafana.app/uid": "test-folder-uid",
},
},
},
}
folder.SetKind("Folder")
folder.SetAPIVersion("folder.grafana.app/v1alpha1")
data, err := folder.MarshalJSON()
require.NoError(t, err)
client, err := opts.Store.BulkProcess(context.Background())
require.NoError(t, err)
require.NoError(t, client.Send(&resource.BulkRequest{
Key: &resource.ResourceKey{Namespace: "test-namespace", Name: "test-folder"},
Value: data,
}))
}).Return(&resource.BulkResponse{}, nil)
mockRepositoryResources := resources.NewMockRepositoryResources(t)
expectedError := errors.New("folder creation failed")
mockRepositoryResources.On("EnsureFolderTreeExists", mock.Anything, "", "", mock.Anything, mock.Anything).
Run(func(args mock.Arguments) {
callback := args.Get(4).(func(folder resources.Folder, created bool, err error) error)
// Call the callback with an error and return its result
err := callback(resources.Folder{
ID: "test-folder-uid",
Path: "/test-folder",
}, true, expectedError)
require.Equal(t, expectedError, err)
}).Return(expectedError)
migrator := NewLegacyFoldersMigrator(mockLegacyMigrator)
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, "read folders from SQL").Return()
progress.On("SetMessage", mock.Anything, "export folders from SQL").Return()
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
return result.Action == repository.FileActionCreated &&
result.Name == "test-folder-uid" &&
result.Resource == resources.FolderResource.Resource &&
result.Group == resources.FolderResource.Group &&
result.Path == "/test-folder" &&
assert.Equal(t, expectedError, result.Error)
})).Return()
err := migrator.Migrate(context.Background(), "test-namespace", mockRepositoryResources, progress)
require.Error(t, err)
require.Contains(t, err.Error(), "export folders from SQL: folder creation failed")
progress.AssertExpectations(t)
})
}
func TestLegacyFoldersMigrator_Close(t *testing.T) {
t.Run("should close without error", func(t *testing.T) {
migrator := NewLegacyFoldersMigrator(legacy.NewMockLegacyMigrator(t))
err := migrator.Close()
require.NoError(t, err)
})
t.Run("should close with results without error", func(t *testing.T) {
migrator := NewLegacyFoldersMigrator(legacy.NewMockLegacyMigrator(t))
resp, err := migrator.CloseWithResults()
require.NoError(t, err)
require.NotNil(t, resp)
})
}
@@ -10,6 +10,7 @@ import (
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/export"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources/signature"
@@ -28,23 +29,26 @@ type legacyResourcesMigrator struct {
repositoryResources resources.RepositoryResourcesFactory
parsers resources.ParserFactory
legacyMigrator legacy.LegacyMigrator
folderMigrator LegacyFoldersMigrator
signerFactory signature.SignerFactory
clients resources.ClientFactory
exportFn export.ExportFn
}
func NewLegacyResourcesMigrator(
repositoryResources resources.RepositoryResourcesFactory,
parsers resources.ParserFactory,
legacyMigrator legacy.LegacyMigrator,
folderMigrator LegacyFoldersMigrator,
signerFactory signature.SignerFactory,
clients resources.ClientFactory,
exportFn export.ExportFn,
) LegacyResourcesMigrator {
return &legacyResourcesMigrator{
repositoryResources: repositoryResources,
parsers: parsers,
legacyMigrator: legacyMigrator,
folderMigrator: folderMigrator,
signerFactory: signerFactory,
clients: clients,
exportFn: exportFn,
}
}
@@ -70,17 +74,25 @@ func (m *legacyResourcesMigrator) Migrate(ctx context.Context, rw repository.Rea
}
progress.SetMessage(ctx, "migrate folders from SQL")
if err := m.folderMigrator.Migrate(ctx, namespace, repositoryResources, progress); err != nil {
clients, err := m.clients.Clients(ctx, namespace)
if err != nil {
return err
}
// nothing special for the export for now
exportOpts := provisioning.ExportJobOptions{}
if err = m.exportFn(ctx, rw.Config().Name, exportOpts, clients, repositoryResources, progress); err != nil {
return fmt.Errorf("migrate folders from SQL: %w", err)
}
progress.SetMessage(ctx, "migrate resources from SQL")
for _, kind := range resources.SupportedProvisioningResources {
if kind == resources.FolderResource {
continue
continue // folders have special handling
}
reader := NewLegacyResourceMigrator(
reader := newLegacyResourceMigrator(
rw,
m.legacyMigrator,
parser,
repositoryResources,
@@ -100,6 +112,7 @@ func (m *legacyResourcesMigrator) Migrate(ctx context.Context, rw repository.Rea
}
type legacyResourceResourceMigrator struct {
repo repository.ReaderWriter
legacy legacy.LegacyMigrator
parser resources.Parser
progress jobs.JobProgressRecorder
@@ -111,7 +124,8 @@ type legacyResourceResourceMigrator struct {
history map[string]string // UID >> file path
}
func NewLegacyResourceMigrator(
func newLegacyResourceMigrator(
repo repository.ReaderWriter,
legacy legacy.LegacyMigrator,
parser resources.Parser,
resources resources.RepositoryResources,
@@ -126,6 +140,7 @@ func NewLegacyResourceMigrator(
history = make(map[string]string)
}
return &legacyResourceResourceMigrator{
repo: repo,
legacy: legacy,
parser: parser,
progress: progress,
@@ -178,11 +193,11 @@ func (r *legacyResourceResourceMigrator) Write(ctx context.Context, key *resourc
// When replaying history, the path to the file may change over time
// This happens when the title or folder change
if r.history != nil {
if r.history != nil && err == nil {
name := parsed.Meta.GetName()
previous := r.history[name]
if previous != "" && previous != fileName {
_, _, err = r.resources.RemoveResourceFromFile(ctx, previous, "")
err = r.repo.Delete(ctx, previous, "", fmt.Sprintf("moved to: %s", fileName))
}
r.history[name] = fileName
}
@@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -16,6 +17,7 @@ import (
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/export"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources/signature"
@@ -29,13 +31,16 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) {
Return(nil, errors.New("parser factory error"))
signerFactory := signature.NewMockSignerFactory(t)
mockClientFactory := resources.NewMockClientFactory(t)
mockExportFn := export.NewMockExportFn(t)
migrator := NewLegacyResourcesMigrator(
nil,
mockParserFactory,
nil,
nil,
signerFactory,
mockClientFactory,
mockExportFn.Execute,
)
err := migrator.Migrate(context.Background(), nil, "test-namespace", provisioning.MigrateJobOptions{}, jobs.NewMockJobProgressRecorder(t))
@@ -43,6 +48,8 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) {
require.EqualError(t, err, "get parser: parser factory error")
mockParserFactory.AssertExpectations(t)
mockExportFn.AssertExpectations(t)
mockClientFactory.AssertExpectations(t)
})
t.Run("should fail when repository resources factory fails", func(t *testing.T) {
@@ -51,16 +58,19 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) {
Return(resources.NewMockParser(t), nil)
mockRepoResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything, mock.Anything).
mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything).
Return(nil, errors.New("repo resources factory error"))
signerFactory := signature.NewMockSignerFactory(t)
mockClientFactory := resources.NewMockClientFactory(t)
mockExportFn := export.NewMockExportFn(t)
migrator := NewLegacyResourcesMigrator(
mockRepoResourcesFactory,
mockParserFactory,
nil,
nil,
signerFactory,
mockClientFactory,
mockExportFn.Execute,
)
err := migrator.Migrate(context.Background(), nil, "test-namespace", provisioning.MigrateJobOptions{}, jobs.NewMockJobProgressRecorder(t))
@@ -69,46 +79,8 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) {
mockParserFactory.AssertExpectations(t)
mockRepoResourcesFactory.AssertExpectations(t)
})
t.Run("should fail when folder migrator fails", func(t *testing.T) {
mockParserFactory := resources.NewMockParserFactory(t)
mockParserFactory.On("GetParser", mock.Anything, mock.Anything).
Return(resources.NewMockParser(t), nil)
mockRepoResources := resources.NewMockRepositoryResources(t)
mockRepoResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything, mock.Anything).
Return(mockRepoResources, nil)
mockFolderMigrator := NewMockLegacyFoldersMigrator(t)
mockFolderMigrator.On("Migrate", mock.Anything, "test-namespace", mockRepoResources, mock.Anything).
Return(errors.New("folder migrator error"))
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, mock.Anything).Return()
signer := signature.NewMockSigner(t)
signerFactory := signature.NewMockSignerFactory(t)
signerFactory.On("New", mock.Anything, mock.Anything).
Return(signer, nil)
migrator := NewLegacyResourcesMigrator(
mockRepoResourcesFactory,
mockParserFactory,
nil,
mockFolderMigrator,
signerFactory,
)
err := migrator.Migrate(context.Background(), nil, "test-namespace", provisioning.MigrateJobOptions{}, progress)
require.Error(t, err)
require.Contains(t, err.Error(), "migrate folders from SQL")
mockParserFactory.AssertExpectations(t)
mockRepoResourcesFactory.AssertExpectations(t)
mockFolderMigrator.AssertExpectations(t)
progress.AssertExpectations(t)
mockExportFn.AssertExpectations(t)
mockClientFactory.AssertExpectations(t)
})
t.Run("should fail when resource migration fails", func(t *testing.T) {
@@ -118,13 +90,9 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) {
mockRepoResources := resources.NewMockRepositoryResources(t)
mockRepoResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything, mock.Anything).
mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything).
Return(mockRepoResources, nil)
mockFolderMigrator := NewMockLegacyFoldersMigrator(t)
mockFolderMigrator.On("Migrate", mock.Anything, "test-namespace", mockRepoResources, mock.Anything).
Return(nil)
mockLegacyMigrator := legacy.NewMockLegacyMigrator(t)
mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool {
return opts.OnlyCount && opts.Namespace == "test-namespace"
@@ -138,23 +106,88 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) {
signerFactory.On("New", mock.Anything, mock.Anything).
Return(signer, nil)
mockClients := resources.NewMockResourceClients(t)
mockClientFactory := resources.NewMockClientFactory(t)
mockClientFactory.On("Clients", mock.Anything, "test-namespace").
Return(mockClients, nil)
mockExportFn := export.NewMockExportFn(t)
migrator := NewLegacyResourcesMigrator(
mockRepoResourcesFactory,
mockParserFactory,
mockLegacyMigrator,
mockFolderMigrator,
signerFactory,
mockClientFactory,
mockExportFn.Execute,
)
repo := repository.NewMockRepository(t)
repo.On("Config").Return(&provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-namespace",
Name: "test-repo",
},
})
mockExportFn.On("Execute", mock.Anything, mock.Anything, provisioning.ExportJobOptions{}, mockClients, mockRepoResources, mock.Anything).
Return(nil)
err := migrator.Migrate(context.Background(), nil, "test-namespace", provisioning.MigrateJobOptions{}, progress)
err := migrator.Migrate(context.Background(), repo, "test-namespace", provisioning.MigrateJobOptions{}, progress)
require.Error(t, err)
require.Contains(t, err.Error(), "migrate resource")
mockParserFactory.AssertExpectations(t)
mockRepoResourcesFactory.AssertExpectations(t)
mockFolderMigrator.AssertExpectations(t)
mockLegacyMigrator.AssertExpectations(t)
progress.AssertExpectations(t)
mockExportFn.AssertExpectations(t)
mockClientFactory.AssertExpectations(t)
mockClients.AssertExpectations(t)
repo.AssertExpectations(t)
})
t.Run("should fail when client creation fails", func(t *testing.T) {
mockParserFactory := resources.NewMockParserFactory(t)
mockParserFactory.On("GetParser", mock.Anything, mock.Anything).
Return(resources.NewMockParser(t), nil)
mockRepoResources := resources.NewMockRepositoryResources(t)
mockRepoResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything).
Return(mockRepoResources, nil)
mockSigner := signature.NewMockSigner(t)
mockSignerFactory := signature.NewMockSignerFactory(t)
mockSignerFactory.On("New", mock.Anything, mock.Anything).
Return(mockSigner, nil)
mockClientFactory := resources.NewMockClientFactory(t)
mockClientFactory.On("Clients", mock.Anything, "test-namespace").
Return(nil, errors.New("client creation error"))
mockExportFn := export.NewMockExportFn(t)
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, "migrate folders from SQL").Return()
migrator := NewLegacyResourcesMigrator(
mockRepoResourcesFactory,
mockParserFactory,
nil,
mockSignerFactory,
mockClientFactory,
mockExportFn.Execute,
)
repo := repository.NewMockRepository(t)
err := migrator.Migrate(context.Background(), repo, "test-namespace", provisioning.MigrateJobOptions{}, progress)
require.Error(t, err)
require.EqualError(t, err, "client creation error")
mockParserFactory.AssertExpectations(t)
mockRepoResourcesFactory.AssertExpectations(t)
mockSignerFactory.AssertExpectations(t)
mockClientFactory.AssertExpectations(t)
progress.AssertExpectations(t)
mockExportFn.AssertExpectations(t)
repo.AssertExpectations(t)
})
t.Run("should fail when signer factory fails", func(t *testing.T) {
@@ -164,23 +197,26 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) {
mockRepoResources := resources.NewMockRepositoryResources(t)
mockRepoResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything, mock.Anything).
mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything).
Return(mockRepoResources, nil)
mockFolderMigrator := NewMockLegacyFoldersMigrator(t)
mockSignerFactory := signature.NewMockSignerFactory(t)
mockSignerFactory.On("New", mock.Anything, signature.SignOptions{
Namespace: "test-namespace",
History: true,
}).Return(nil, fmt.Errorf("signer factory error"))
mockClientFactory := resources.NewMockClientFactory(t)
mockExportFn := export.NewMockExportFn(t)
progress := jobs.NewMockJobProgressRecorder(t)
migrator := NewLegacyResourcesMigrator(
mockRepoResourcesFactory,
mockParserFactory,
nil,
mockFolderMigrator,
mockSignerFactory,
mockClientFactory,
mockExportFn.Execute,
)
err := migrator.Migrate(context.Background(), nil, "test-namespace", provisioning.MigrateJobOptions{
@@ -191,8 +227,66 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) {
mockParserFactory.AssertExpectations(t)
mockRepoResourcesFactory.AssertExpectations(t)
mockFolderMigrator.AssertExpectations(t)
mockSignerFactory.AssertExpectations(t)
mockClientFactory.AssertExpectations(t)
progress.AssertExpectations(t)
mockExportFn.AssertExpectations(t)
})
t.Run("should fail when folder export fails", func(t *testing.T) {
mockParser := resources.NewMockParser(t)
mockParserFactory := resources.NewMockParserFactory(t)
mockParserFactory.On("GetParser", mock.Anything, mock.Anything).
Return(mockParser, nil)
mockRepoResources := resources.NewMockRepositoryResources(t)
mockRepoResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything).
Return(mockRepoResources, nil)
mockSigner := signature.NewMockSigner(t)
mockSignerFactory := signature.NewMockSignerFactory(t)
mockSignerFactory.On("New", mock.Anything, signature.SignOptions{
Namespace: "test-namespace",
History: false,
}).Return(mockSigner, nil)
mockClients := resources.NewMockResourceClients(t)
mockClientFactory := resources.NewMockClientFactory(t)
mockClientFactory.On("Clients", mock.Anything, "test-namespace").
Return(mockClients, nil)
mockExportFn := export.NewMockExportFn(t)
mockExportFn.On("Execute", mock.Anything, mock.Anything, provisioning.ExportJobOptions{}, mockClients, mockRepoResources, mock.Anything).
Return(fmt.Errorf("export error"))
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, "migrate folders from SQL").Return()
migrator := NewLegacyResourcesMigrator(
mockRepoResourcesFactory,
mockParserFactory,
nil,
mockSignerFactory,
mockClientFactory,
mockExportFn.Execute,
)
repo := repository.NewMockRepository(t)
repo.On("Config").Return(&provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-namespace",
Name: "test-repo",
},
})
err := migrator.Migrate(context.Background(), repo, "test-namespace", provisioning.MigrateJobOptions{}, progress)
require.Error(t, err)
require.Contains(t, err.Error(), "migrate folders from SQL: export error")
mockParserFactory.AssertExpectations(t)
mockRepoResourcesFactory.AssertExpectations(t)
mockSignerFactory.AssertExpectations(t)
mockClientFactory.AssertExpectations(t)
mockExportFn.AssertExpectations(t)
progress.AssertExpectations(t)
})
@@ -204,13 +298,9 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) {
mockRepoResources := resources.NewMockRepositoryResources(t)
mockRepoResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything, mock.Anything).
mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything).
Return(mockRepoResources, nil)
mockFolderMigrator := NewMockLegacyFoldersMigrator(t)
mockFolderMigrator.On("Migrate", mock.Anything, "test-namespace", mockRepoResources, mock.Anything).
Return(nil)
mockSigner := signature.NewMockSigner(t)
mockSignerFactory := signature.NewMockSignerFactory(t)
mockSignerFactory.On("New", mock.Anything, signature.SignOptions{
@@ -235,6 +325,12 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) {
},
}, nil).Once() // Migration phase
mockClients := resources.NewMockResourceClients(t)
mockClientFactory := resources.NewMockClientFactory(t)
mockClientFactory.On("Clients", mock.Anything, "test-namespace").
Return(mockClients, nil)
mockExportFn := export.NewMockExportFn(t)
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, "migrate folders from SQL").Return()
progress.On("SetMessage", mock.Anything, "migrate resources from SQL").Return()
@@ -244,20 +340,33 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) {
mockRepoResourcesFactory,
mockParserFactory,
mockLegacyMigrator,
mockFolderMigrator,
mockSignerFactory,
mockClientFactory,
mockExportFn.Execute,
)
err := migrator.Migrate(context.Background(), nil, "test-namespace", provisioning.MigrateJobOptions{
repo := repository.NewMockRepository(t)
repo.On("Config").Return(&provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-namespace",
Name: "test-repo",
},
})
mockExportFn.On("Execute", mock.Anything, mock.Anything, provisioning.ExportJobOptions{}, mockClients, mockRepoResources, mock.Anything).
Return(nil)
err := migrator.Migrate(context.Background(), repo, "test-namespace", provisioning.MigrateJobOptions{
History: true,
}, progress)
require.NoError(t, err)
mockParserFactory.AssertExpectations(t)
mockRepoResourcesFactory.AssertExpectations(t)
mockFolderMigrator.AssertExpectations(t)
mockLegacyMigrator.AssertExpectations(t)
mockClientFactory.AssertExpectations(t)
mockExportFn.AssertExpectations(t)
progress.AssertExpectations(t)
mockClients.AssertExpectations(t)
})
}
@@ -269,7 +378,8 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
progress := jobs.NewMockJobProgressRecorder(t)
migrator := NewLegacyResourceMigrator(
migrator := newLegacyResourceMigrator(
nil,
nil,
mockParser,
nil,
@@ -318,7 +428,8 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
})).Return()
progress.On("TooManyErrors").Return(nil)
migrator := NewLegacyResourceMigrator(
migrator := newLegacyResourceMigrator(
nil,
nil,
mockParser,
mockRepoResources,
@@ -360,7 +471,8 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
Return(nil, errors.New("signing error"))
progress := jobs.NewMockJobProgressRecorder(t)
migrator := NewLegacyResourceMigrator(
migrator := newLegacyResourceMigrator(
nil,
nil,
mockParser,
nil,
@@ -419,7 +531,8 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
})).Return()
progress.On("TooManyErrors").Return(nil)
migrator := NewLegacyResourceMigrator(
migrator := newLegacyResourceMigrator(
nil,
nil,
mockParser,
mockRepoResources,
@@ -459,10 +572,13 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
Obj: obj,
}, nil)
mockRepo := repository.NewMockRepository(t)
mockRepoResources := resources.NewMockRepositoryResources(t)
writeResourceFileFromObject := mockRepoResources.On("WriteResourceFileFromObject", mock.Anything, mock.Anything, mock.Anything)
migrator := NewLegacyResourceMigrator(
migrator := newLegacyResourceMigrator(
mockRepo,
nil,
mockParser,
mockRepoResources,
@@ -482,8 +598,8 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
// Change the result file name
writeResourceFileFromObject.Return("bbbb.json", nil)
mockRepoResources.On("RemoveResourceFromFile", mock.Anything, "aaaa.json", "").
Return("", schema.GroupVersionKind{}, nil).Once()
mockRepo.On("Delete", mock.Anything, "aaaa.json", "", "moved to: bbbb.json").
Return(nil).Once()
err = migrator.Write(context.Background(), &resource.ResourceKey{}, []byte(""))
require.NoError(t, err)
@@ -557,7 +673,8 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
})).Return()
progress.On("TooManyErrors").Return(nil)
migrator := NewLegacyResourceMigrator(
migrator := newLegacyResourceMigrator(
nil,
nil,
mockParser,
mockRepoResources,
@@ -602,7 +719,8 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
progress.On("Record", mock.Anything, mock.Anything).Return()
progress.On("TooManyErrors").Return(errors.New("too many errors"))
migrator := NewLegacyResourceMigrator(
migrator := newLegacyResourceMigrator(
nil,
nil,
mockParser,
mockRepoResources,
@@ -632,7 +750,8 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) {
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, mock.Anything).Return()
migrator := NewLegacyResourceMigrator(
migrator := newLegacyResourceMigrator(
nil,
mockLegacyMigrator,
nil,
nil,
@@ -663,7 +782,8 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) {
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, mock.Anything).Return()
migrator := NewLegacyResourceMigrator(
migrator := newLegacyResourceMigrator(
nil,
mockLegacyMigrator,
nil,
nil,
@@ -694,7 +814,8 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) {
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, mock.Anything).Return()
migrator := NewLegacyResourceMigrator(
migrator := newLegacyResourceMigrator(
nil,
mockLegacyMigrator,
nil,
nil,
@@ -733,7 +854,8 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) {
progress.On("SetMessage", mock.Anything, mock.Anything).Return()
progress.On("SetTotal", mock.Anything, 100).Return()
migrator := NewLegacyResourceMigrator(
migrator := newLegacyResourceMigrator(
nil,
mockLegacyMigrator,
nil,
nil,
@@ -773,7 +895,8 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) {
progress.On("SetTotal", mock.Anything, 200).Return()
signer := signature.NewMockSigner(t)
migrator := NewLegacyResourceMigrator(
migrator := newLegacyResourceMigrator(
nil,
mockLegacyMigrator,
nil,
nil,
@@ -1,240 +0,0 @@
// Code generated by mockery v2.52.4. DO NOT EDIT.
package migrate
import (
context "context"
jobs "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
mock "github.com/stretchr/testify/mock"
resource "github.com/grafana/grafana/pkg/storage/unified/resource"
resources "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
)
// MockLegacyFoldersMigrator is an autogenerated mock type for the LegacyFoldersMigrator type
type MockLegacyFoldersMigrator struct {
mock.Mock
}
type MockLegacyFoldersMigrator_Expecter struct {
mock *mock.Mock
}
func (_m *MockLegacyFoldersMigrator) EXPECT() *MockLegacyFoldersMigrator_Expecter {
return &MockLegacyFoldersMigrator_Expecter{mock: &_m.Mock}
}
// Close provides a mock function with no fields
func (_m *MockLegacyFoldersMigrator) Close() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Close")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// MockLegacyFoldersMigrator_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type MockLegacyFoldersMigrator_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
func (_e *MockLegacyFoldersMigrator_Expecter) Close() *MockLegacyFoldersMigrator_Close_Call {
return &MockLegacyFoldersMigrator_Close_Call{Call: _e.mock.On("Close")}
}
func (_c *MockLegacyFoldersMigrator_Close_Call) Run(run func()) *MockLegacyFoldersMigrator_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockLegacyFoldersMigrator_Close_Call) Return(_a0 error) *MockLegacyFoldersMigrator_Close_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockLegacyFoldersMigrator_Close_Call) RunAndReturn(run func() error) *MockLegacyFoldersMigrator_Close_Call {
_c.Call.Return(run)
return _c
}
// CloseWithResults provides a mock function with no fields
func (_m *MockLegacyFoldersMigrator) CloseWithResults() (*resource.BulkResponse, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for CloseWithResults")
}
var r0 *resource.BulkResponse
var r1 error
if rf, ok := ret.Get(0).(func() (*resource.BulkResponse, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() *resource.BulkResponse); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*resource.BulkResponse)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockLegacyFoldersMigrator_CloseWithResults_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CloseWithResults'
type MockLegacyFoldersMigrator_CloseWithResults_Call struct {
*mock.Call
}
// CloseWithResults is a helper method to define mock.On call
func (_e *MockLegacyFoldersMigrator_Expecter) CloseWithResults() *MockLegacyFoldersMigrator_CloseWithResults_Call {
return &MockLegacyFoldersMigrator_CloseWithResults_Call{Call: _e.mock.On("CloseWithResults")}
}
func (_c *MockLegacyFoldersMigrator_CloseWithResults_Call) Run(run func()) *MockLegacyFoldersMigrator_CloseWithResults_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockLegacyFoldersMigrator_CloseWithResults_Call) Return(_a0 *resource.BulkResponse, _a1 error) *MockLegacyFoldersMigrator_CloseWithResults_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockLegacyFoldersMigrator_CloseWithResults_Call) RunAndReturn(run func() (*resource.BulkResponse, error)) *MockLegacyFoldersMigrator_CloseWithResults_Call {
_c.Call.Return(run)
return _c
}
// Migrate provides a mock function with given fields: ctx, namespace, repositoryResources, progress
func (_m *MockLegacyFoldersMigrator) Migrate(ctx context.Context, namespace string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error {
ret := _m.Called(ctx, namespace, repositoryResources, progress)
if len(ret) == 0 {
panic("no return value specified for Migrate")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, resources.RepositoryResources, jobs.JobProgressRecorder) error); ok {
r0 = rf(ctx, namespace, repositoryResources, progress)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockLegacyFoldersMigrator_Migrate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Migrate'
type MockLegacyFoldersMigrator_Migrate_Call struct {
*mock.Call
}
// Migrate is a helper method to define mock.On call
// - ctx context.Context
// - namespace string
// - repositoryResources resources.RepositoryResources
// - progress jobs.JobProgressRecorder
func (_e *MockLegacyFoldersMigrator_Expecter) Migrate(ctx interface{}, namespace interface{}, repositoryResources interface{}, progress interface{}) *MockLegacyFoldersMigrator_Migrate_Call {
return &MockLegacyFoldersMigrator_Migrate_Call{Call: _e.mock.On("Migrate", ctx, namespace, repositoryResources, progress)}
}
func (_c *MockLegacyFoldersMigrator_Migrate_Call) Run(run func(ctx context.Context, namespace string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder)) *MockLegacyFoldersMigrator_Migrate_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(resources.RepositoryResources), args[3].(jobs.JobProgressRecorder))
})
return _c
}
func (_c *MockLegacyFoldersMigrator_Migrate_Call) Return(_a0 error) *MockLegacyFoldersMigrator_Migrate_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockLegacyFoldersMigrator_Migrate_Call) RunAndReturn(run func(context.Context, string, resources.RepositoryResources, jobs.JobProgressRecorder) error) *MockLegacyFoldersMigrator_Migrate_Call {
_c.Call.Return(run)
return _c
}
// Write provides a mock function with given fields: ctx, key, value
func (_m *MockLegacyFoldersMigrator) Write(ctx context.Context, key *resource.ResourceKey, value []byte) error {
ret := _m.Called(ctx, key, value)
if len(ret) == 0 {
panic("no return value specified for Write")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *resource.ResourceKey, []byte) error); ok {
r0 = rf(ctx, key, value)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockLegacyFoldersMigrator_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
type MockLegacyFoldersMigrator_Write_Call struct {
*mock.Call
}
// Write is a helper method to define mock.On call
// - ctx context.Context
// - key *resource.ResourceKey
// - value []byte
func (_e *MockLegacyFoldersMigrator_Expecter) Write(ctx interface{}, key interface{}, value interface{}) *MockLegacyFoldersMigrator_Write_Call {
return &MockLegacyFoldersMigrator_Write_Call{Call: _e.mock.On("Write", ctx, key, value)}
}
func (_c *MockLegacyFoldersMigrator_Write_Call) Run(run func(ctx context.Context, key *resource.ResourceKey, value []byte)) *MockLegacyFoldersMigrator_Write_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(*resource.ResourceKey), args[2].([]byte))
})
return _c
}
func (_c *MockLegacyFoldersMigrator_Write_Call) Return(_a0 error) *MockLegacyFoldersMigrator_Write_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockLegacyFoldersMigrator_Write_Call) RunAndReturn(run func(context.Context, *resource.ResourceKey, []byte) error) *MockLegacyFoldersMigrator_Write_Call {
_c.Call.Return(run)
return _c
}
// NewMockLegacyFoldersMigrator creates a new instance of MockLegacyFoldersMigrator. 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 NewMockLegacyFoldersMigrator(t interface {
mock.TestingT
Cleanup(func())
}) *MockLegacyFoldersMigrator {
mock := &MockLegacyFoldersMigrator{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
+2 -2
View File
@@ -548,13 +548,13 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH
syncer,
)
signerFactory := signature.NewSignerFactory(b.clients)
legacyFolders := migrate.NewLegacyFoldersMigrator(b.legacyMigrator)
legacyResources := migrate.NewLegacyResourcesMigrator(
b.repositoryResources,
b.parsers,
b.legacyMigrator,
legacyFolders,
signerFactory,
b.clients,
export.ExportAll,
)
storageSwapper := migrate.NewStorageSwapper(b.unified, b.storageStatus)
legacyMigrator := migrate.NewLegacyMigrator(
@@ -336,7 +336,10 @@ func (g *GoGitRepo) Write(ctx context.Context, fpath string, ref string, data []
if err != nil {
return err
}
return g.maybeCommit(ctx, message)
}
func (g *GoGitRepo) maybeCommit(ctx context.Context, message string) error {
// Skip commit for each file
if !g.opts.PushOnWrites {
return nil
@@ -351,7 +354,7 @@ func (g *GoGitRepo) Write(ctx context.Context, fpath string, ref string, data []
When: sig.When,
}
}
_, err = g.tree.Commit(message, opts)
_, err := g.tree.Commit(message, opts)
if errors.Is(err, git.ErrEmptyCommit) {
return nil // empty commit is fine -- no change
}
@@ -359,16 +362,22 @@ func (g *GoGitRepo) Write(ctx context.Context, fpath string, ref string, data []
}
// Delete implements repository.Repository.
func (g *GoGitRepo) Delete(ctx context.Context, path string, ref string, message string) error {
if _, err := g.tree.Remove(safepath.Join(g.config.Spec.GitHub.Path, path)); err != nil {
func (g *GoGitRepo) Delete(ctx context.Context, fpath string, ref string, message string) error {
fpath = safepath.Join(g.config.Spec.GitHub.Path, fpath)
if err := verifyPathWithoutRef(fpath, ref); err != nil {
return err
}
return nil
if _, err := g.tree.Remove(fpath); err != nil {
return err
}
return g.maybeCommit(ctx, message)
}
// Read implements repository.Repository.
func (g *GoGitRepo) Read(ctx context.Context, path string, ref string) (*repository.FileInfo, error) {
if err := verifyPathWithoutRef(path, ref); err != nil {
return nil, err
}
readPath := safepath.Join(g.config.Spec.GitHub.Path, path)
stat, err := g.tree.Filesystem.Lstat(readPath)
if errors.Is(err, fs.ErrNotExist) {