Files
Roberto Jiménez Sánchez 4eadc823a9 Provisioning: Move repository package to provisioning app (#110228)
* Move repository package to apps

* Move operators to grafana/grafana

* Go mod tidy

* Own package by git sync team for now

* Merged

* Do not use settings in local extra

* Remove dependency on webhook extra

* Hack to work around issue with secure contracts

* Sync Go modules

* Revert "Move operators to grafana/grafana"

This reverts commit 9f19b30a2e.
2025-09-02 09:45:44 +02:00

317 lines
10 KiB
Go

package migrate
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/apps/provisioning/pkg/repository"
"github.com/grafana/grafana/apps/provisioning/pkg/repository/local"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
)
func TestMigrationWorker_IsSupported(t *testing.T) {
tests := []struct {
name string
job provisioning.Job
want bool
}{
{
name: "should support migrate action",
job: provisioning.Job{
Spec: provisioning.JobSpec{
Action: provisioning.JobActionMigrate,
},
},
want: true,
},
{
name: "should not support other actions",
job: provisioning.Job{
Spec: provisioning.JobSpec{
Action: "other",
},
},
want: false,
},
}
worker := NewMigrationWorker(nil, nil, nil)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := worker.IsSupported(context.Background(), tt.job)
assert.Equal(t, tt.want, got)
})
}
}
func TestMigrationWorker_ProcessNotReaderWriter(t *testing.T) {
worker := NewMigrationWorker(nil, nil, nil)
job := provisioning.Job{
Spec: provisioning.JobSpec{
Action: provisioning.JobActionMigrate,
Migrate: &provisioning.MigrateJobOptions{},
},
}
progressRecorder := jobs.NewMockJobProgressRecorder(t)
progressRecorder.On("SetTotal", mock.Anything, 10).Return()
repo := repository.NewMockReader(t)
err := worker.Process(context.Background(), repo, job, progressRecorder)
require.EqualError(t, err, "migration job submitted targeting repository that is not a ReaderWriter")
}
func TestMigrationWorker_WithHistory(t *testing.T) {
fakeDualwrite := dualwrite.NewMockService(t)
fakeDualwrite.On("ReadFromUnified", mock.Anything, mock.Anything).
Maybe().Return(true, nil) // using unified storage
worker := NewMigrationWorker(nil, nil, fakeDualwrite)
job := provisioning.Job{
Spec: provisioning.JobSpec{
Action: provisioning.JobActionMigrate,
Migrate: &provisioning.MigrateJobOptions{
History: true,
},
},
}
t.Run("fail local", func(t *testing.T) {
progressRecorder := jobs.NewMockJobProgressRecorder(t)
progressRecorder.On("SetTotal", mock.Anything, 10).Return()
repo := local.NewRepository(&provisioning.Repository{}, nil)
err := worker.Process(context.Background(), repo, job, progressRecorder)
require.EqualError(t, err, "history is only supported for github repositories")
})
t.Run("fail unified", func(t *testing.T) {
progressRecorder := jobs.NewMockJobProgressRecorder(t)
progressRecorder.On("SetTotal", mock.Anything, 10).Return()
repo := repository.NewMockRepository(t)
repo.On("Config").Return(&provisioning.Repository{
Spec: provisioning.RepositorySpec{
Type: provisioning.GitHubRepositoryType,
GitHub: &provisioning.GitHubRepositoryConfig{
URL: "empty", // not valid
},
},
})
err := worker.Process(context.Background(), repo, job, progressRecorder)
require.EqualError(t, err, "history is not yet supported in unified storage")
})
}
func TestMigrationWorker_Process(t *testing.T) {
tests := []struct {
name string
setupMocks func(*MockMigrator, *MockMigrator, *dualwrite.MockService, *jobs.MockJobProgressRecorder)
setupRepo func(*repository.MockRepository)
job provisioning.Job
expectedError string
isLegacyActive bool
}{
{
name: "should fail when migrate settings are missing",
job: provisioning.Job{
Spec: provisioning.JobSpec{
Action: provisioning.JobActionMigrate,
Migrate: nil,
},
},
setupMocks: func(lm *MockMigrator, um *MockMigrator, ds *dualwrite.MockService, pr *jobs.MockJobProgressRecorder) {
},
setupRepo: func(repo *repository.MockRepository) {
// No Config() call expected since we fail before that
},
expectedError: "missing migrate settings",
},
{
name: "should use legacy migrator when legacy storage is active",
job: provisioning.Job{
Spec: provisioning.JobSpec{
Action: provisioning.JobActionMigrate,
Migrate: &provisioning.MigrateJobOptions{},
},
},
isLegacyActive: true,
setupMocks: func(lm *MockMigrator, um *MockMigrator, ds *dualwrite.MockService, pr *jobs.MockJobProgressRecorder) {
pr.On("SetTotal", mock.Anything, 10).Return()
ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(false, nil)
lm.On("Migrate", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
},
setupRepo: func(repo *repository.MockRepository) {
repo.On("Config").Return(&provisioning.Repository{
Spec: provisioning.RepositorySpec{
Sync: provisioning.SyncOptions{
Target: provisioning.SyncTargetTypeInstance,
},
},
})
},
},
{
name: "should use unified storage migrator when legacy storage is not active",
job: provisioning.Job{
Spec: provisioning.JobSpec{
Action: provisioning.JobActionMigrate,
Migrate: &provisioning.MigrateJobOptions{},
},
},
isLegacyActive: false,
setupMocks: func(lm *MockMigrator, um *MockMigrator, ds *dualwrite.MockService, pr *jobs.MockJobProgressRecorder) {
pr.On("SetTotal", mock.Anything, 10).Return()
ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil)
um.On("Migrate", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
},
setupRepo: func(repo *repository.MockRepository) {
repo.On("Config").Return(&provisioning.Repository{
Spec: provisioning.RepositorySpec{
Sync: provisioning.SyncOptions{
Target: provisioning.SyncTargetTypeInstance,
},
},
})
},
},
{
name: "should propagate migrator errors",
job: provisioning.Job{
Spec: provisioning.JobSpec{
Action: provisioning.JobActionMigrate,
Migrate: &provisioning.MigrateJobOptions{},
},
},
isLegacyActive: true,
setupMocks: func(lm *MockMigrator, um *MockMigrator, ds *dualwrite.MockService, pr *jobs.MockJobProgressRecorder) {
pr.On("SetTotal", mock.Anything, 10).Return()
ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(false, nil)
lm.On("Migrate", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("migration failed"))
},
setupRepo: func(repo *repository.MockRepository) {
repo.On("Config").Return(&provisioning.Repository{
Spec: provisioning.RepositorySpec{
Sync: provisioning.SyncOptions{
Target: provisioning.SyncTargetTypeInstance,
},
},
})
},
expectedError: "migration failed",
},
{
name: "should block migration of legacy resources for folder-type repositories",
job: provisioning.Job{
Spec: provisioning.JobSpec{
Action: provisioning.JobActionMigrate,
Migrate: &provisioning.MigrateJobOptions{},
},
},
isLegacyActive: true,
setupMocks: func(lm *MockMigrator, um *MockMigrator, ds *dualwrite.MockService, pr *jobs.MockJobProgressRecorder) {
pr.On("SetTotal", mock.Anything, 10).Return()
ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(false, nil)
// legacyMigrator should not be called as we block before reaching it
},
setupRepo: func(repo *repository.MockRepository) {
repo.On("Config").Return(&provisioning.Repository{
Spec: provisioning.RepositorySpec{
Sync: provisioning.SyncOptions{
Target: provisioning.SyncTargetTypeFolder,
},
},
})
},
expectedError: "migration of legacy resources is not supported for folder-type repositories",
},
{
name: "should allow migration of legacy resources for instance-type repositories",
job: provisioning.Job{
Spec: provisioning.JobSpec{
Action: provisioning.JobActionMigrate,
Migrate: &provisioning.MigrateJobOptions{},
},
},
isLegacyActive: true,
setupMocks: func(lm *MockMigrator, um *MockMigrator, ds *dualwrite.MockService, pr *jobs.MockJobProgressRecorder) {
pr.On("SetTotal", mock.Anything, 10).Return()
ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(false, nil)
lm.On("Migrate", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
},
setupRepo: func(repo *repository.MockRepository) {
repo.On("Config").Return(&provisioning.Repository{
Spec: provisioning.RepositorySpec{
Sync: provisioning.SyncOptions{
Target: provisioning.SyncTargetTypeInstance,
},
},
})
},
expectedError: "",
},
{
name: "should allow migration for folder-type repositories when legacy storage is not active",
job: provisioning.Job{
Spec: provisioning.JobSpec{
Action: provisioning.JobActionMigrate,
Migrate: &provisioning.MigrateJobOptions{},
},
},
isLegacyActive: false,
setupMocks: func(lm *MockMigrator, um *MockMigrator, ds *dualwrite.MockService, pr *jobs.MockJobProgressRecorder) {
pr.On("SetTotal", mock.Anything, 10).Return()
ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil)
um.On("Migrate", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
},
setupRepo: func(repo *repository.MockRepository) {
repo.On("Config").Return(&provisioning.Repository{
Spec: provisioning.RepositorySpec{
Sync: provisioning.SyncOptions{
Target: provisioning.SyncTargetTypeFolder,
},
},
})
},
expectedError: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
legacyMigrator := NewMockMigrator(t)
unifiedMigrator := NewMockMigrator(t)
dualWriteService := dualwrite.NewMockService(t)
progressRecorder := jobs.NewMockJobProgressRecorder(t)
worker := NewMigrationWorker(legacyMigrator, unifiedMigrator, dualWriteService)
if tt.setupMocks != nil {
tt.setupMocks(legacyMigrator, unifiedMigrator, dualWriteService, progressRecorder)
}
rw := repository.NewMockRepository(t)
if tt.setupRepo != nil {
tt.setupRepo(rw)
}
err := worker.Process(context.Background(), rw, tt.job, progressRecorder)
if tt.expectedError != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedError)
} else {
require.NoError(t, err)
}
mock.AssertExpectationsForObjects(t, legacyMigrator, unifiedMigrator, dualWriteService, progressRecorder, rw)
})
}
}