Provisioning: Migrate use history only with github (#104219)

This commit is contained in:
Ryan McKinley
2025-04-21 21:10:37 +03:00
committed by GitHub
parent 536ff2fc3d
commit d6dbc0a421
13 changed files with 191 additions and 61 deletions
@@ -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",
} }
@@ -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,
@@ -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>