Provisioning: Migrate use history only with github (#104219)
This commit is contained in:
@@ -5,12 +5,13 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
|
||||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
|
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/jobs"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
"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"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
||||||
"k8s.io/client-go/dynamic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ExportResources(ctx context.Context, options provisioning.ExportJobOptions, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error {
|
func ExportResources(ctx context.Context, options provisioning.ExportJobOptions, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error {
|
||||||
@@ -37,7 +38,7 @@ func ExportResources(ctx context.Context, options provisioning.ExportJobOptions,
|
|||||||
|
|
||||||
func exportResource(ctx context.Context, options provisioning.ExportJobOptions, client dynamic.ResourceInterface, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error {
|
func exportResource(ctx context.Context, options provisioning.ExportJobOptions, client dynamic.ResourceInterface, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error {
|
||||||
return resources.ForEach(ctx, client, func(item *unstructured.Unstructured) error {
|
return resources.ForEach(ctx, client, func(item *unstructured.Unstructured) error {
|
||||||
fileName, err := repositoryResources.CreateResourceFileFromObject(ctx, item, resources.WriteOptions{
|
fileName, err := repositoryResources.WriteResourceFileFromObject(ctx, item, resources.WriteOptions{
|
||||||
Path: options.Path,
|
Path: options.Path,
|
||||||
Ref: options.Branch,
|
Ref: options.Branch,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,10 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
v0alpha1 "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"
|
|
||||||
mock "github.com/stretchr/testify/mock"
|
mock "github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@@ -17,6 +13,11 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
dynamicfake "k8s.io/client-go/dynamic/fake"
|
dynamicfake "k8s.io/client-go/dynamic/fake"
|
||||||
k8testing "k8s.io/client-go/testing"
|
k8testing "k8s.io/client-go/testing"
|
||||||
|
|
||||||
|
provisioningV0 "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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestExportResources(t *testing.T) {
|
func TestExportResources(t *testing.T) {
|
||||||
@@ -78,11 +79,11 @@ func TestExportResources(t *testing.T) {
|
|||||||
Ref: "feature/branch",
|
Ref: "feature/branch",
|
||||||
}
|
}
|
||||||
|
|
||||||
repoResources.On("CreateResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||||
return obj.GetName() == "dashboard-1"
|
return obj.GetName() == "dashboard-1"
|
||||||
}), options).Return("dashboard-1.json", nil)
|
}), options).Return("dashboard-1.json", nil)
|
||||||
|
|
||||||
repoResources.On("CreateResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||||
return obj.GetName() == "dashboard-2"
|
return obj.GetName() == "dashboard-2"
|
||||||
}), options).Return("dashboard-2.json", nil)
|
}), options).Return("dashboard-2.json", nil)
|
||||||
},
|
},
|
||||||
@@ -165,11 +166,11 @@ func TestExportResources(t *testing.T) {
|
|||||||
Ref: "feature/branch",
|
Ref: "feature/branch",
|
||||||
}
|
}
|
||||||
|
|
||||||
repoResources.On("CreateResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||||
return obj.GetName() == "dashboard-1"
|
return obj.GetName() == "dashboard-1"
|
||||||
}), options).Return("", fmt.Errorf("failed to export dashboard"))
|
}), options).Return("", fmt.Errorf("failed to export dashboard"))
|
||||||
|
|
||||||
repoResources.On("CreateResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||||
return obj.GetName() == "dashboard-2"
|
return obj.GetName() == "dashboard-2"
|
||||||
}), options).Return("dashboard-2.json", nil)
|
}), options).Return("dashboard-2.json", nil)
|
||||||
},
|
},
|
||||||
@@ -211,7 +212,7 @@ func TestExportResources(t *testing.T) {
|
|||||||
Ref: "feature/branch",
|
Ref: "feature/branch",
|
||||||
}
|
}
|
||||||
|
|
||||||
repoResources.On("CreateResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||||
return obj.GetName() == "dashboard-1"
|
return obj.GetName() == "dashboard-1"
|
||||||
}), options).Return("", fmt.Errorf("failed to export dashboard"))
|
}), options).Return("", fmt.Errorf("failed to export dashboard"))
|
||||||
},
|
},
|
||||||
@@ -254,7 +255,7 @@ func TestExportResources(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return true to indicate the file already exists, and provide the updated path
|
// Return true to indicate the file already exists, and provide the updated path
|
||||||
repoResources.On("CreateResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||||
return obj.GetName() == "existing-dashboard"
|
return obj.GetName() == "existing-dashboard"
|
||||||
}), options).Return("", resources.ErrAlreadyInRepository)
|
}), options).Return("", resources.ErrAlreadyInRepository)
|
||||||
},
|
},
|
||||||
@@ -291,7 +292,7 @@ func TestExportResources(t *testing.T) {
|
|||||||
repoResources := resources.NewMockRepositoryResources(t)
|
repoResources := resources.NewMockRepositoryResources(t)
|
||||||
tt.setupResources(repoResources, resourceClients, fakeDynamicClient, listGVK)
|
tt.setupResources(repoResources, resourceClients, fakeDynamicClient, listGVK)
|
||||||
|
|
||||||
options := v0alpha1.ExportJobOptions{
|
options := provisioningV0.ExportJobOptions{
|
||||||
Path: "grafana",
|
Path: "grafana",
|
||||||
Branch: "feature/branch",
|
Branch: "feature/branch",
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-1
@@ -108,6 +108,7 @@ type legacyResourceResourceMigrator struct {
|
|||||||
options provisioning.MigrateJobOptions
|
options provisioning.MigrateJobOptions
|
||||||
resources resources.RepositoryResources
|
resources resources.RepositoryResources
|
||||||
signer signature.Signer
|
signer signature.Signer
|
||||||
|
history map[string]string // UID >> file path
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLegacyResourceMigrator(
|
func NewLegacyResourceMigrator(
|
||||||
@@ -120,6 +121,10 @@ func NewLegacyResourceMigrator(
|
|||||||
kind schema.GroupResource,
|
kind schema.GroupResource,
|
||||||
signer signature.Signer,
|
signer signature.Signer,
|
||||||
) *legacyResourceResourceMigrator {
|
) *legacyResourceResourceMigrator {
|
||||||
|
var history map[string]string
|
||||||
|
if options.History {
|
||||||
|
history = make(map[string]string)
|
||||||
|
}
|
||||||
return &legacyResourceResourceMigrator{
|
return &legacyResourceResourceMigrator{
|
||||||
legacy: legacy,
|
legacy: legacy,
|
||||||
parser: parser,
|
parser: parser,
|
||||||
@@ -129,6 +134,7 @@ func NewLegacyResourceMigrator(
|
|||||||
kind: kind,
|
kind: kind,
|
||||||
resources: resources,
|
resources: resources,
|
||||||
signer: signer,
|
signer: signer,
|
||||||
|
history: history,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,11 +171,22 @@ func (r *legacyResourceResourceMigrator) Write(ctx context.Context, key *resourc
|
|||||||
|
|
||||||
// TODO: this seems to be same logic as the export job
|
// TODO: this seems to be same logic as the export job
|
||||||
// TODO: we should use a kind safe manager here
|
// TODO: we should use a kind safe manager here
|
||||||
fileName, err := r.resources.CreateResourceFileFromObject(ctx, parsed.Obj, resources.WriteOptions{
|
fileName, err := r.resources.WriteResourceFileFromObject(ctx, parsed.Obj, resources.WriteOptions{
|
||||||
Path: "",
|
Path: "",
|
||||||
Ref: "",
|
Ref: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// When replaying history, the path to the file may change over time
|
||||||
|
// This happens when the title or folder change
|
||||||
|
if r.history != nil {
|
||||||
|
name := parsed.Meta.GetName()
|
||||||
|
previous := r.history[name]
|
||||||
|
if previous != "" && previous != fileName {
|
||||||
|
_, _, err = r.resources.RemoveResourceFromFile(ctx, previous, "")
|
||||||
|
}
|
||||||
|
r.history[name] = fileName
|
||||||
|
}
|
||||||
|
|
||||||
result := jobs.JobResourceResult{
|
result := jobs.JobResourceResult{
|
||||||
Name: parsed.Meta.GetName(),
|
Name: parsed.Meta.GetName(),
|
||||||
Resource: r.kind.Resource,
|
Resource: r.kind.Resource,
|
||||||
+69
-14
@@ -290,8 +290,8 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
|
|||||||
t.Run("records error when create resource file fails", func(t *testing.T) {
|
t.Run("records error when create resource file fails", func(t *testing.T) {
|
||||||
mockParser := resources.NewMockParser(t)
|
mockParser := resources.NewMockParser(t)
|
||||||
obj := &unstructured.Unstructured{
|
obj := &unstructured.Unstructured{
|
||||||
Object: map[string]interface{}{
|
Object: map[string]any{
|
||||||
"metadata": map[string]interface{}{
|
"metadata": map[string]any{
|
||||||
"name": "test",
|
"name": "test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -306,7 +306,7 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
|
|||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
mockRepoResources := resources.NewMockRepositoryResources(t)
|
mockRepoResources := resources.NewMockRepositoryResources(t)
|
||||||
mockRepoResources.On("CreateResourceFileFromObject", mock.Anything, mock.Anything, mock.Anything).
|
mockRepoResources.On("WriteResourceFileFromObject", mock.Anything, mock.Anything, mock.Anything).
|
||||||
Return("", errors.New("create file error"))
|
Return("", errors.New("create file error"))
|
||||||
|
|
||||||
progress := jobs.NewMockJobProgressRecorder(t)
|
progress := jobs.NewMockJobProgressRecorder(t)
|
||||||
@@ -340,8 +340,8 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
|
|||||||
t.Run("should fail when signer fails", func(t *testing.T) {
|
t.Run("should fail when signer fails", func(t *testing.T) {
|
||||||
mockParser := resources.NewMockParser(t)
|
mockParser := resources.NewMockParser(t)
|
||||||
obj := &unstructured.Unstructured{
|
obj := &unstructured.Unstructured{
|
||||||
Object: map[string]interface{}{
|
Object: map[string]any{
|
||||||
"metadata": map[string]interface{}{
|
"metadata": map[string]any{
|
||||||
"name": "test",
|
"name": "test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -383,8 +383,8 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
|
|||||||
t.Run("should successfully add author signature", func(t *testing.T) {
|
t.Run("should successfully add author signature", func(t *testing.T) {
|
||||||
mockParser := resources.NewMockParser(t)
|
mockParser := resources.NewMockParser(t)
|
||||||
obj := &unstructured.Unstructured{
|
obj := &unstructured.Unstructured{
|
||||||
Object: map[string]interface{}{
|
Object: map[string]any{
|
||||||
"metadata": map[string]interface{}{
|
"metadata": map[string]any{
|
||||||
"name": "test",
|
"name": "test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -407,7 +407,7 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
|
|||||||
Return(signedCtx, nil)
|
Return(signedCtx, nil)
|
||||||
|
|
||||||
mockRepoResources := resources.NewMockRepositoryResources(t)
|
mockRepoResources := resources.NewMockRepositoryResources(t)
|
||||||
mockRepoResources.On("CreateResourceFileFromObject", signedCtx, mock.Anything, mock.Anything).
|
mockRepoResources.On("WriteResourceFileFromObject", signedCtx, mock.Anything, mock.Anything).
|
||||||
Return("test/path", nil)
|
Return("test/path", nil)
|
||||||
|
|
||||||
progress := jobs.NewMockJobProgressRecorder(t)
|
progress := jobs.NewMockJobProgressRecorder(t)
|
||||||
@@ -439,11 +439,66 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
|
|||||||
progress.AssertExpectations(t)
|
progress.AssertExpectations(t)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("should maintain history", func(t *testing.T) {
|
||||||
|
progress := jobs.NewMockJobProgressRecorder(t)
|
||||||
|
progress.On("Record", mock.Anything, mock.Anything).Return()
|
||||||
|
progress.On("TooManyErrors").Return(nil)
|
||||||
|
|
||||||
|
mockParser := resources.NewMockParser(t)
|
||||||
|
obj := &unstructured.Unstructured{
|
||||||
|
Object: map[string]any{
|
||||||
|
"metadata": map[string]any{
|
||||||
|
"name": "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
meta, _ := utils.MetaAccessor(obj)
|
||||||
|
mockParser.On("Parse", mock.Anything, mock.Anything).
|
||||||
|
Return(&resources.ParsedResource{
|
||||||
|
Meta: meta,
|
||||||
|
Obj: obj,
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
mockRepoResources := resources.NewMockRepositoryResources(t)
|
||||||
|
writeResourceFileFromObject := mockRepoResources.On("WriteResourceFileFromObject", mock.Anything, mock.Anything, mock.Anything)
|
||||||
|
|
||||||
|
migrator := NewLegacyResourceMigrator(
|
||||||
|
nil,
|
||||||
|
mockParser,
|
||||||
|
mockRepoResources,
|
||||||
|
progress,
|
||||||
|
provisioning.MigrateJobOptions{
|
||||||
|
History: true,
|
||||||
|
},
|
||||||
|
"test-namespace",
|
||||||
|
schema.GroupResource{Group: "test.grafana.app", Resource: "tests"},
|
||||||
|
signature.NewGrafanaSigner(),
|
||||||
|
)
|
||||||
|
|
||||||
|
writeResourceFileFromObject.Return("aaaa.json", nil)
|
||||||
|
err := migrator.Write(context.Background(), &resource.ResourceKey{}, []byte(""))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "aaaa.json", migrator.history["test"], "kept track of the old files")
|
||||||
|
|
||||||
|
// Change the result file name
|
||||||
|
writeResourceFileFromObject.Return("bbbb.json", nil)
|
||||||
|
mockRepoResources.On("RemoveResourceFromFile", mock.Anything, "aaaa.json", "").
|
||||||
|
Return("", schema.GroupVersionKind{}, nil).Once()
|
||||||
|
|
||||||
|
err = migrator.Write(context.Background(), &resource.ResourceKey{}, []byte(""))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "bbbb.json", migrator.history["test"], "kept track of the old files")
|
||||||
|
|
||||||
|
mockParser.AssertExpectations(t)
|
||||||
|
mockRepoResources.AssertExpectations(t)
|
||||||
|
progress.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("should successfully write resource", func(t *testing.T) {
|
t.Run("should successfully write resource", func(t *testing.T) {
|
||||||
mockParser := resources.NewMockParser(t)
|
mockParser := resources.NewMockParser(t)
|
||||||
obj := &unstructured.Unstructured{
|
obj := &unstructured.Unstructured{
|
||||||
Object: map[string]interface{}{
|
Object: map[string]any{
|
||||||
"metadata": map[string]interface{}{
|
"metadata": map[string]any{
|
||||||
"name": "test",
|
"name": "test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -471,7 +526,7 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
|
|||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
mockRepoResources := resources.NewMockRepositoryResources(t)
|
mockRepoResources := resources.NewMockRepositoryResources(t)
|
||||||
mockRepoResources.On("CreateResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
mockRepoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||||
if obj == nil {
|
if obj == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -524,8 +579,8 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
|
|||||||
t.Run("should fail when too many errors", func(t *testing.T) {
|
t.Run("should fail when too many errors", func(t *testing.T) {
|
||||||
mockParser := resources.NewMockParser(t)
|
mockParser := resources.NewMockParser(t)
|
||||||
obj := &unstructured.Unstructured{
|
obj := &unstructured.Unstructured{
|
||||||
Object: map[string]interface{}{
|
Object: map[string]any{
|
||||||
"metadata": map[string]interface{}{
|
"metadata": map[string]any{
|
||||||
"name": "test",
|
"name": "test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -540,7 +595,7 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
|
|||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
mockRepoResources := resources.NewMockRepositoryResources(t)
|
mockRepoResources := resources.NewMockRepositoryResources(t)
|
||||||
mockRepoResources.On("CreateResourceFileFromObject", mock.Anything, mock.Anything, resources.WriteOptions{}).
|
mockRepoResources.On("WriteResourceFileFromObject", mock.Anything, mock.Anything, resources.WriteOptions{}).
|
||||||
Return("test/path", nil)
|
Return("test/path", nil)
|
||||||
|
|
||||||
progress := jobs.NewMockJobProgressRecorder(t)
|
progress := jobs.NewMockJobProgressRecorder(t)
|
||||||
@@ -50,9 +50,19 @@ func (w *MigrationWorker) Process(ctx context.Context, repo repository.Repositor
|
|||||||
return errors.New("migration job submitted targeting repository that is not a ReaderWriter")
|
return errors.New("migration job submitted targeting repository that is not a ReaderWriter")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if options.History {
|
||||||
|
if repo.Config().Spec.Type != provisioning.GitHubRepositoryType {
|
||||||
|
return errors.New("history is only supported for github repositories")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, w.storageStatus) {
|
if dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, w.storageStatus) {
|
||||||
return w.legacyMigrator.Migrate(ctx, rw, *options, progress)
|
return w.legacyMigrator.Migrate(ctx, rw, *options, progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if options.History {
|
||||||
|
return errors.New("history is not yet supported in unified storage")
|
||||||
|
}
|
||||||
|
|
||||||
return w.unifiedMigrator.Migrate(ctx, rw, *options, progress)
|
return w.unifiedMigrator.Migrate(ctx, rw, *options, progress)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,50 @@ func TestMigrationWorker_ProcessNotReaderWriter(t *testing.T) {
|
|||||||
require.EqualError(t, err, "migration job submitted targeting repository that is not a ReaderWriter")
|
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()
|
||||||
|
progressRecorder.On("Strict").Return()
|
||||||
|
|
||||||
|
repo := repository.NewLocal(&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()
|
||||||
|
progressRecorder.On("Strict").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) {
|
func TestMigrationWorker_Process(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ func (r *localRepository) Validate() field.ErrorList {
|
|||||||
// Test implements provisioning.Repository.
|
// Test implements provisioning.Repository.
|
||||||
// NOTE: Validate has been called (and passed) before this function should be called
|
// NOTE: Validate has been called (and passed) before this function should be called
|
||||||
func (r *localRepository) Test(ctx context.Context) (*provisioning.TestResults, error) {
|
func (r *localRepository) Test(ctx context.Context) (*provisioning.TestResults, error) {
|
||||||
path := field.NewPath("spec", "localhost", "path")
|
path := field.NewPath("spec", "local", "path")
|
||||||
if r.config.Spec.Local.Path == "" {
|
if r.config.Spec.Local.Path == "" {
|
||||||
return fromFieldError(field.Required(path, "no path is configured")), nil
|
return fromFieldError(field.Required(path, "no path is configured")), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
|
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
|
||||||
|
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
|
||||||
|
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate mockery --name RepositoryResourcesFactory --structname MockRepositoryResourcesFactory --inpackage --filename repository_resources_factory_mock.go --with-expecter
|
//go:generate mockery --name RepositoryResourcesFactory --structname MockRepositoryResourcesFactory --inpackage --filename repository_resources_factory_mock.go --with-expecter
|
||||||
@@ -23,7 +24,7 @@ type RepositoryResources interface {
|
|||||||
EnsureFolderExists(ctx context.Context, folder Folder, parentID string) error
|
EnsureFolderExists(ctx context.Context, folder Folder, parentID string) error
|
||||||
EnsureFolderTreeExists(ctx context.Context, ref, path string, tree FolderTree, fn func(folder Folder, created bool, err error) error) error
|
EnsureFolderTreeExists(ctx context.Context, ref, path string, tree FolderTree, fn func(folder Folder, created bool, err error) error) error
|
||||||
// File from Resource
|
// File from Resource
|
||||||
CreateResourceFileFromObject(ctx context.Context, obj *unstructured.Unstructured, options WriteOptions) (string, error)
|
WriteResourceFileFromObject(ctx context.Context, obj *unstructured.Unstructured, options WriteOptions) (string, error)
|
||||||
// Resource from file
|
// Resource from file
|
||||||
WriteResourceFromFile(ctx context.Context, path, ref string) (string, schema.GroupVersionKind, error)
|
WriteResourceFromFile(ctx context.Context, path, ref string) (string, schema.GroupVersionKind, error)
|
||||||
RemoveResourceFromFile(ctx context.Context, path, ref string) (string, schema.GroupVersionKind, error)
|
RemoveResourceFromFile(ctx context.Context, path, ref string) (string, schema.GroupVersionKind, error)
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ func (_m *MockRepositoryResources) EXPECT() *MockRepositoryResources_Expecter {
|
|||||||
return &MockRepositoryResources_Expecter{mock: &_m.Mock}
|
return &MockRepositoryResources_Expecter{mock: &_m.Mock}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateResourceFileFromObject provides a mock function with given fields: ctx, obj, options
|
// WriteResourceFileFromObject provides a mock function with given fields: ctx, obj, options
|
||||||
func (_m *MockRepositoryResources) CreateResourceFileFromObject(ctx context.Context, obj *unstructured.Unstructured, options WriteOptions) (string, error) {
|
func (_m *MockRepositoryResources) WriteResourceFileFromObject(ctx context.Context, obj *unstructured.Unstructured, options WriteOptions) (string, error) {
|
||||||
ret := _m.Called(ctx, obj, options)
|
ret := _m.Called(ctx, obj, options)
|
||||||
|
|
||||||
if len(ret) == 0 {
|
if len(ret) == 0 {
|
||||||
panic("no return value specified for CreateResourceFileFromObject")
|
panic("no return value specified for WriteResourceFileFromObject")
|
||||||
}
|
}
|
||||||
|
|
||||||
var r0 string
|
var r0 string
|
||||||
@@ -54,32 +54,32 @@ func (_m *MockRepositoryResources) CreateResourceFileFromObject(ctx context.Cont
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// MockRepositoryResources_CreateResourceFileFromObject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateResourceFileFromObject'
|
// MockRepositoryResources_WriteResourceFileFromObject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteResourceFileFromObject'
|
||||||
type MockRepositoryResources_CreateResourceFileFromObject_Call struct {
|
type MockRepositoryResources_WriteResourceFileFromObject_Call struct {
|
||||||
*mock.Call
|
*mock.Call
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateResourceFileFromObject is a helper method to define mock.On call
|
// WriteResourceFileFromObject is a helper method to define mock.On call
|
||||||
// - ctx context.Context
|
// - ctx context.Context
|
||||||
// - obj *unstructured.Unstructured
|
// - obj *unstructured.Unstructured
|
||||||
// - options WriteOptions
|
// - options WriteOptions
|
||||||
func (_e *MockRepositoryResources_Expecter) CreateResourceFileFromObject(ctx interface{}, obj interface{}, options interface{}) *MockRepositoryResources_CreateResourceFileFromObject_Call {
|
func (_e *MockRepositoryResources_Expecter) WriteResourceFileFromObject(ctx interface{}, obj interface{}, options interface{}) *MockRepositoryResources_WriteResourceFileFromObject_Call {
|
||||||
return &MockRepositoryResources_CreateResourceFileFromObject_Call{Call: _e.mock.On("CreateResourceFileFromObject", ctx, obj, options)}
|
return &MockRepositoryResources_WriteResourceFileFromObject_Call{Call: _e.mock.On("WriteResourceFileFromObject", ctx, obj, options)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_c *MockRepositoryResources_CreateResourceFileFromObject_Call) Run(run func(ctx context.Context, obj *unstructured.Unstructured, options WriteOptions)) *MockRepositoryResources_CreateResourceFileFromObject_Call {
|
func (_c *MockRepositoryResources_WriteResourceFileFromObject_Call) Run(run func(ctx context.Context, obj *unstructured.Unstructured, options WriteOptions)) *MockRepositoryResources_WriteResourceFileFromObject_Call {
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
run(args[0].(context.Context), args[1].(*unstructured.Unstructured), args[2].(WriteOptions))
|
run(args[0].(context.Context), args[1].(*unstructured.Unstructured), args[2].(WriteOptions))
|
||||||
})
|
})
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_c *MockRepositoryResources_CreateResourceFileFromObject_Call) Return(_a0 string, _a1 error) *MockRepositoryResources_CreateResourceFileFromObject_Call {
|
func (_c *MockRepositoryResources_WriteResourceFileFromObject_Call) Return(_a0 string, _a1 error) *MockRepositoryResources_WriteResourceFileFromObject_Call {
|
||||||
_c.Call.Return(_a0, _a1)
|
_c.Call.Return(_a0, _a1)
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_c *MockRepositoryResources_CreateResourceFileFromObject_Call) RunAndReturn(run func(context.Context, *unstructured.Unstructured, WriteOptions) (string, error)) *MockRepositoryResources_CreateResourceFileFromObject_Call {
|
func (_c *MockRepositoryResources_WriteResourceFileFromObject_Call) RunAndReturn(run func(context.Context, *unstructured.Unstructured, WriteOptions) (string, error)) *MockRepositoryResources_WriteResourceFileFromObject_Call {
|
||||||
_c.Call.Return(run)
|
_c.Call.Return(run)
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package resources
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -54,7 +53,7 @@ func NewResourcesManager(repo repository.ReaderWriter, folders *FolderManager, p
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateResource writes an object to the repository
|
// CreateResource writes an object to the repository
|
||||||
func (r *ResourcesManager) CreateResourceFileFromObject(ctx context.Context, obj *unstructured.Unstructured, options WriteOptions) (string, error) {
|
func (r *ResourcesManager) WriteResourceFileFromObject(ctx context.Context, obj *unstructured.Unstructured, options WriteOptions) (string, error) {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return "", fmt.Errorf("context error: %w", err)
|
return "", fmt.Errorf("context error: %w", err)
|
||||||
}
|
}
|
||||||
@@ -81,7 +80,7 @@ func (r *ResourcesManager) CreateResourceFileFromObject(ctx context.Context, obj
|
|||||||
}
|
}
|
||||||
|
|
||||||
manager, _ := meta.GetManagerProperties()
|
manager, _ := meta.GetManagerProperties()
|
||||||
// TODO: how we should handle this?
|
// TODO: how should we handle this?
|
||||||
if manager.Identity == r.repo.Config().GetName() {
|
if manager.Identity == r.repo.Config().GetName() {
|
||||||
// If it's already in the repository, we don't need to write it
|
// If it's already in the repository, we don't need to write it
|
||||||
return "", ErrAlreadyInRepository
|
return "", ErrAlreadyInRepository
|
||||||
@@ -100,17 +99,6 @@ func (r *ResourcesManager) CreateResourceFileFromObject(ctx context.Context, obj
|
|||||||
return "", fmt.Errorf("folder not found in tree: %s", folder)
|
return "", fmt.Errorf("folder not found in tree: %s", folder)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the metadata
|
|
||||||
delete(obj.Object, "metadata")
|
|
||||||
|
|
||||||
// Always write the identifier
|
|
||||||
meta.SetName(name)
|
|
||||||
|
|
||||||
body, err := json.MarshalIndent(obj.Object, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to marshal dashboard: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileName := slugify.Slugify(title) + ".json"
|
fileName := slugify.Slugify(title) + ".json"
|
||||||
if fid.Path != "" {
|
if fid.Path != "" {
|
||||||
fileName = safepath.Join(fid.Path, fileName)
|
fileName = safepath.Join(fid.Path, fileName)
|
||||||
@@ -119,6 +107,18 @@ func (r *ResourcesManager) CreateResourceFileFromObject(ctx context.Context, obj
|
|||||||
fileName = safepath.Join(options.Path, fileName)
|
fileName = safepath.Join(options.Path, fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parsed := ParsedResource{
|
||||||
|
Info: &repository.FileInfo{
|
||||||
|
Path: fileName,
|
||||||
|
Ref: options.Ref,
|
||||||
|
},
|
||||||
|
Obj: obj,
|
||||||
|
}
|
||||||
|
body, err := parsed.ToSaveBytes()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
err = r.repo.Write(ctx, fileName, options.Ref, body, commitMessage)
|
err = r.repo.Write(ctx, fileName, options.Ref, body, commitMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to write file: %w", err)
|
return "", fmt.Errorf("failed to write file: %w", err)
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ export interface SynchronizeStepProps {
|
|||||||
export function SynchronizeStep({ onStepStatusUpdate, requiresMigration }: SynchronizeStepProps) {
|
export function SynchronizeStep({ onStepStatusUpdate, requiresMigration }: SynchronizeStepProps) {
|
||||||
const [createJob] = useCreateRepositoryJobsMutation();
|
const [createJob] = useCreateRepositoryJobsMutation();
|
||||||
const { getValues, register, watch } = useFormContext<WizardFormData>();
|
const { getValues, register, watch } = useFormContext<WizardFormData>();
|
||||||
const target = watch('repository.sync.target');
|
const repoType = watch('repository.type');
|
||||||
|
const supportsHistory = requiresMigration && repoType === 'github';
|
||||||
const [job, setJob] = useState<Job>();
|
const [job, setJob] = useState<Job>();
|
||||||
|
|
||||||
const startSynchronization = async () => {
|
const startSynchronization = async () => {
|
||||||
@@ -35,7 +36,7 @@ export function SynchronizeStep({ onStepStatusUpdate, requiresMigration }: Synch
|
|||||||
const jobSpec = requiresMigration
|
const jobSpec = requiresMigration
|
||||||
? {
|
? {
|
||||||
migrate: {
|
migrate: {
|
||||||
history,
|
history: history && supportsHistory,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
@@ -111,7 +112,7 @@ export function SynchronizeStep({ onStepStatusUpdate, requiresMigration }: Synch
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Alert>
|
</Alert>
|
||||||
{requiresMigration && target !== 'folder' && (
|
{supportsHistory && (
|
||||||
<>
|
<>
|
||||||
<Text element="h3">
|
<Text element="h3">
|
||||||
<Trans i18nKey="provisioning.synchronize-step.synchronization-options">Synchronization options</Trans>
|
<Trans i18nKey="provisioning.synchronize-step.synchronization-options">Synchronization options</Trans>
|
||||||
|
|||||||
Reference in New Issue
Block a user