Provisioning: refactor commit signature logic (#104055)
* WIP: Separate signature package * Add some unit tests * Test factory * Finish unit test coverage signature package * Fix register * Add FIXME * Add more coverage * Add more coverage * Fix migrate tests * Fix unit tests
This commit is contained in:
@@ -76,6 +76,8 @@ const AnnoKeyFullpathUIDs = "grafana.app/fullpathUIDs"
|
||||
const LabelKeyDeprecatedInternalID = "grafana.app/deprecatedInternalID"
|
||||
|
||||
// Accessor functions for k8s objects
|
||||
//
|
||||
//go:generate mockery --name GrafanaMetaAccessor --structname MockGrafanaMetaAccessor --inpackage --filename meta_mock.go --with-expecter
|
||||
type GrafanaMetaAccessor interface {
|
||||
metav1.Object
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func ExportAll(ctx context.Context, repoName string, options provisioning.ExportJobOptions, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, folderClient dynamic.ResourceInterface, progress jobs.JobProgressRecorder) error {
|
||||
// FIXME: should we sign with grafana user?
|
||||
if err := ExportFolders(ctx, repoName, options, folderClient, repositoryResources, progress); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ func (r *ExportWorker) Process(ctx context.Context, repo repository.Repository,
|
||||
return errors.New("export job submitted targeting repository that is not a ReaderWriter")
|
||||
}
|
||||
|
||||
repositoryResources, err := r.repositoryResources.Client(ctx, rw, resources.RepositoryResourcesOptions{})
|
||||
repositoryResources, err := r.repositoryResources.Client(ctx, rw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create repository resource client: %w", err)
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ func TestExportWorker_ProcessRepositoryResourcesError(t *testing.T) {
|
||||
mockClients.On("Clients", context.Background(), "test-namespace").Return(resourceClients, nil)
|
||||
|
||||
mockRepoResources := resources.NewMockRepositoryResourcesFactory(t)
|
||||
mockRepoResources.On("Client", context.Background(), mockRepo, resources.RepositoryResourcesOptions{}).Return(nil, fmt.Errorf("failed to create repository resources client"))
|
||||
mockRepoResources.On("Client", context.Background(), mockRepo).Return(nil, fmt.Errorf("failed to create repository resources client"))
|
||||
|
||||
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
||||
mockCloneFn := NewMockWrapWithCloneFn(t)
|
||||
@@ -292,7 +292,7 @@ func TestExportWorker_ProcessCloneAndPushOptions(t *testing.T) {
|
||||
|
||||
mockRepoResources := resources.NewMockRepositoryResourcesFactory(t)
|
||||
mockRepoResourcesClient := resources.NewMockRepositoryResources(t)
|
||||
mockRepoResources.On("Client", mock.Anything, mock.Anything, resources.RepositoryResourcesOptions{}).Return(mockRepoResourcesClient, nil)
|
||||
mockRepoResources.On("Client", mock.Anything, mock.Anything).Return(mockRepoResourcesClient, nil)
|
||||
|
||||
mockExportFn := NewMockExportFn(t)
|
||||
mockExportFn.On("Execute", mock.Anything, "test-repo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
@@ -343,7 +343,7 @@ func TestExportWorker_ProcessExportFnError(t *testing.T) {
|
||||
|
||||
mockRepoResources := resources.NewMockRepositoryResourcesFactory(t)
|
||||
mockRepoResourcesClient := resources.NewMockRepositoryResources(t)
|
||||
mockRepoResources.On("Client", mock.Anything, mock.Anything, resources.RepositoryResourcesOptions{}).Return(mockRepoResourcesClient, nil)
|
||||
mockRepoResources.On("Client", mock.Anything, mock.Anything).Return(mockRepoResourcesClient, nil)
|
||||
|
||||
mockExportFn := NewMockExportFn(t)
|
||||
mockExportFn.On("Execute", mock.Anything, "test-repo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("export failed"))
|
||||
|
||||
@@ -74,6 +74,7 @@ func (f *legacyFoldersMigrator) Migrate(ctx context.Context, namespace string, r
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -12,6 +12,7 @@ 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"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources/signature"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/parquet"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
@@ -28,6 +29,7 @@ type legacyResourcesMigrator struct {
|
||||
parsers resources.ParserFactory
|
||||
legacyMigrator legacy.LegacyMigrator
|
||||
folderMigrator LegacyFoldersMigrator
|
||||
signerFactory signature.SignerFactory
|
||||
}
|
||||
|
||||
func NewLegacyResourcesMigrator(
|
||||
@@ -35,12 +37,14 @@ func NewLegacyResourcesMigrator(
|
||||
parsers resources.ParserFactory,
|
||||
legacyMigrator legacy.LegacyMigrator,
|
||||
folderMigrator LegacyFoldersMigrator,
|
||||
signerFactory signature.SignerFactory,
|
||||
) LegacyResourcesMigrator {
|
||||
return &legacyResourcesMigrator{
|
||||
repositoryResources: repositoryResources,
|
||||
parsers: parsers,
|
||||
legacyMigrator: legacyMigrator,
|
||||
folderMigrator: folderMigrator,
|
||||
signerFactory: signerFactory,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,15 +54,21 @@ func (m *legacyResourcesMigrator) Migrate(ctx context.Context, rw repository.Rea
|
||||
return fmt.Errorf("get parser: %w", err)
|
||||
}
|
||||
|
||||
repoOpts := resources.RepositoryResourcesOptions{
|
||||
PreloadAllUserInfo: opts.History,
|
||||
}
|
||||
|
||||
repositoryResources, err := m.repositoryResources.Client(ctx, rw, repoOpts)
|
||||
repositoryResources, err := m.repositoryResources.Client(ctx, rw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get repository resources: %w", err)
|
||||
}
|
||||
|
||||
// FIXME: signature is only relevant for repositories which support signature
|
||||
// Not all repositories support history
|
||||
signer, err := m.signerFactory.New(ctx, signature.SignOptions{
|
||||
Namespace: namespace,
|
||||
History: opts.History,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get signer: %w", err)
|
||||
}
|
||||
|
||||
progress.SetMessage(ctx, "migrate folders from SQL")
|
||||
if err := m.folderMigrator.Migrate(ctx, namespace, repositoryResources, progress); err != nil {
|
||||
return fmt.Errorf("migrate folders from SQL: %w", err)
|
||||
@@ -70,7 +80,17 @@ func (m *legacyResourcesMigrator) Migrate(ctx context.Context, rw repository.Rea
|
||||
continue
|
||||
}
|
||||
|
||||
reader := NewLegacyResourceMigrator(m.legacyMigrator, parser, repositoryResources, progress, opts, namespace, kind.GroupResource())
|
||||
reader := NewLegacyResourceMigrator(
|
||||
m.legacyMigrator,
|
||||
parser,
|
||||
repositoryResources,
|
||||
progress,
|
||||
opts,
|
||||
namespace,
|
||||
kind.GroupResource(),
|
||||
signer,
|
||||
)
|
||||
|
||||
if err := reader.Migrate(ctx); err != nil {
|
||||
return fmt.Errorf("migrate resource %s: %w", kind, err)
|
||||
}
|
||||
@@ -87,9 +107,19 @@ type legacyResourceResourceMigrator struct {
|
||||
kind schema.GroupResource
|
||||
options provisioning.MigrateJobOptions
|
||||
resources resources.RepositoryResources
|
||||
signer signature.Signer
|
||||
}
|
||||
|
||||
func NewLegacyResourceMigrator(legacy legacy.LegacyMigrator, parser resources.Parser, resources resources.RepositoryResources, progress jobs.JobProgressRecorder, options provisioning.MigrateJobOptions, namespace string, kind schema.GroupResource) *legacyResourceResourceMigrator {
|
||||
func NewLegacyResourceMigrator(
|
||||
legacy legacy.LegacyMigrator,
|
||||
parser resources.Parser,
|
||||
resources resources.RepositoryResources,
|
||||
progress jobs.JobProgressRecorder,
|
||||
options provisioning.MigrateJobOptions,
|
||||
namespace string,
|
||||
kind schema.GroupResource,
|
||||
signer signature.Signer,
|
||||
) *legacyResourceResourceMigrator {
|
||||
return &legacyResourceResourceMigrator{
|
||||
legacy: legacy,
|
||||
parser: parser,
|
||||
@@ -98,6 +128,7 @@ func NewLegacyResourceMigrator(legacy legacy.LegacyMigrator, parser resources.Pa
|
||||
namespace: namespace,
|
||||
kind: kind,
|
||||
resources: resources,
|
||||
signer: signer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +157,12 @@ func (r *legacyResourceResourceMigrator) Write(ctx context.Context, key *resourc
|
||||
parsed.Meta.SetManagerProperties(utils.ManagerProperties{})
|
||||
parsed.Meta.SetSourceProperties(utils.SourceProperties{})
|
||||
|
||||
// Add author signature to the context
|
||||
ctx, err = r.signer.Sign(ctx, parsed.Meta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add author signature: %w", err)
|
||||
}
|
||||
|
||||
// TODO: this seems to be same logic as the export job
|
||||
// TODO: we should use a kind safe manager here
|
||||
fileName, err := r.resources.CreateResourceFileFromObject(ctx, parsed.Obj, resources.WriteOptions{
|
||||
|
||||
@@ -3,6 +3,7 @@ package migrate
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -17,6 +18,7 @@ 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"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources/signature"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
|
||||
@@ -26,11 +28,14 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) {
|
||||
mockParserFactory.On("GetParser", mock.Anything, mock.Anything).
|
||||
Return(nil, errors.New("parser factory error"))
|
||||
|
||||
signerFactory := signature.NewMockSignerFactory(t)
|
||||
|
||||
migrator := NewLegacyResourcesMigrator(
|
||||
nil,
|
||||
mockParserFactory,
|
||||
nil,
|
||||
nil,
|
||||
signerFactory,
|
||||
)
|
||||
|
||||
err := migrator.Migrate(context.Background(), nil, "test-namespace", provisioning.MigrateJobOptions{}, jobs.NewMockJobProgressRecorder(t))
|
||||
@@ -48,12 +53,14 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) {
|
||||
mockRepoResourcesFactory := resources.NewMockRepositoryResourcesFactory(t)
|
||||
mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything, mock.Anything).
|
||||
Return(nil, errors.New("repo resources factory error"))
|
||||
signerFactory := signature.NewMockSignerFactory(t)
|
||||
|
||||
migrator := NewLegacyResourcesMigrator(
|
||||
mockRepoResourcesFactory,
|
||||
mockParserFactory,
|
||||
nil,
|
||||
nil,
|
||||
signerFactory,
|
||||
)
|
||||
|
||||
err := migrator.Migrate(context.Background(), nil, "test-namespace", provisioning.MigrateJobOptions{}, jobs.NewMockJobProgressRecorder(t))
|
||||
@@ -81,11 +88,17 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) {
|
||||
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)
|
||||
@@ -120,11 +133,17 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) {
|
||||
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,
|
||||
mockLegacyMigrator,
|
||||
mockFolderMigrator,
|
||||
signerFactory,
|
||||
)
|
||||
|
||||
err := migrator.Migrate(context.Background(), nil, "test-namespace", provisioning.MigrateJobOptions{}, progress)
|
||||
@@ -138,6 +157,45 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) {
|
||||
progress.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("should fail when signer factory 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)
|
||||
mockSignerFactory := signature.NewMockSignerFactory(t)
|
||||
mockSignerFactory.On("New", mock.Anything, signature.SignOptions{
|
||||
Namespace: "test-namespace",
|
||||
History: true,
|
||||
}).Return(nil, fmt.Errorf("signer factory error"))
|
||||
|
||||
progress := jobs.NewMockJobProgressRecorder(t)
|
||||
migrator := NewLegacyResourcesMigrator(
|
||||
mockRepoResourcesFactory,
|
||||
mockParserFactory,
|
||||
nil,
|
||||
mockFolderMigrator,
|
||||
mockSignerFactory,
|
||||
)
|
||||
|
||||
err := migrator.Migrate(context.Background(), nil, "test-namespace", provisioning.MigrateJobOptions{
|
||||
History: true,
|
||||
}, progress)
|
||||
require.Error(t, err)
|
||||
require.EqualError(t, err, "get signer: signer factory error")
|
||||
|
||||
mockParserFactory.AssertExpectations(t)
|
||||
mockRepoResourcesFactory.AssertExpectations(t)
|
||||
mockFolderMigrator.AssertExpectations(t)
|
||||
mockSignerFactory.AssertExpectations(t)
|
||||
progress.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("should successfully migrate all resources", func(t *testing.T) {
|
||||
mockParser := resources.NewMockParser(t)
|
||||
mockParserFactory := resources.NewMockParserFactory(t)
|
||||
@@ -153,6 +211,13 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.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{
|
||||
Namespace: "test-namespace",
|
||||
History: true,
|
||||
}).Return(mockSigner, nil)
|
||||
|
||||
mockLegacyMigrator := legacy.NewMockLegacyMigrator(t)
|
||||
mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool {
|
||||
return opts.OnlyCount && opts.Namespace == "test-namespace"
|
||||
@@ -180,9 +245,12 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) {
|
||||
mockParserFactory,
|
||||
mockLegacyMigrator,
|
||||
mockFolderMigrator,
|
||||
mockSignerFactory,
|
||||
)
|
||||
|
||||
err := migrator.Migrate(context.Background(), nil, "test-namespace", provisioning.MigrateJobOptions{}, progress)
|
||||
err := migrator.Migrate(context.Background(), nil, "test-namespace", provisioning.MigrateJobOptions{
|
||||
History: true,
|
||||
}, progress)
|
||||
require.NoError(t, err)
|
||||
|
||||
mockParserFactory.AssertExpectations(t)
|
||||
@@ -209,6 +277,7 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
|
||||
provisioning.MigrateJobOptions{},
|
||||
"test-namespace",
|
||||
schema.GroupResource{Group: "test.grafana.app", Resource: "tests"},
|
||||
signature.NewGrafanaSigner(),
|
||||
)
|
||||
|
||||
err := migrator.Write(context.Background(), &resource.ResourceKey{}, []byte("test"))
|
||||
@@ -257,6 +326,7 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
|
||||
provisioning.MigrateJobOptions{},
|
||||
"test-namespace",
|
||||
schema.GroupResource{Group: "test.grafana.app", Resource: "tests"},
|
||||
signature.NewGrafanaSigner(),
|
||||
)
|
||||
|
||||
err = migrator.Write(context.Background(), &resource.ResourceKey{}, []byte("test"))
|
||||
@@ -267,6 +337,108 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
|
||||
progress.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("should fail when signer fails", func(t *testing.T) {
|
||||
mockParser := resources.NewMockParser(t)
|
||||
obj := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test",
|
||||
},
|
||||
},
|
||||
}
|
||||
meta, err := utils.MetaAccessor(obj)
|
||||
require.NoError(t, err)
|
||||
|
||||
mockParser.On("Parse", mock.Anything, mock.Anything).
|
||||
Return(&resources.ParsedResource{
|
||||
Meta: meta,
|
||||
Obj: obj,
|
||||
}, nil)
|
||||
|
||||
mockSigner := signature.NewMockSigner(t)
|
||||
mockSigner.On("Sign", mock.Anything, meta).
|
||||
Return(nil, errors.New("signing error"))
|
||||
|
||||
progress := jobs.NewMockJobProgressRecorder(t)
|
||||
migrator := NewLegacyResourceMigrator(
|
||||
nil,
|
||||
mockParser,
|
||||
nil,
|
||||
progress,
|
||||
provisioning.MigrateJobOptions{},
|
||||
"test-namespace",
|
||||
schema.GroupResource{Group: "test.grafana.app", Resource: "tests"},
|
||||
mockSigner,
|
||||
)
|
||||
|
||||
err = migrator.Write(context.Background(), &resource.ResourceKey{}, []byte("test"))
|
||||
require.Error(t, err)
|
||||
require.EqualError(t, err, "add author signature: signing error")
|
||||
|
||||
mockParser.AssertExpectations(t)
|
||||
mockSigner.AssertExpectations(t)
|
||||
progress.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("should successfully add author signature", func(t *testing.T) {
|
||||
mockParser := resources.NewMockParser(t)
|
||||
obj := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test",
|
||||
},
|
||||
},
|
||||
}
|
||||
meta, err := utils.MetaAccessor(obj)
|
||||
require.NoError(t, err)
|
||||
|
||||
mockParser.On("Parse", mock.Anything, mock.Anything).
|
||||
Return(&resources.ParsedResource{
|
||||
Meta: meta,
|
||||
Obj: obj,
|
||||
}, nil)
|
||||
|
||||
mockSigner := signature.NewMockSigner(t)
|
||||
signedCtx := repository.WithAuthorSignature(context.Background(), repository.CommitSignature{
|
||||
Name: "test-user",
|
||||
Email: "test@example.com",
|
||||
})
|
||||
mockSigner.On("Sign", mock.Anything, meta).
|
||||
Return(signedCtx, nil)
|
||||
|
||||
mockRepoResources := resources.NewMockRepositoryResources(t)
|
||||
mockRepoResources.On("CreateResourceFileFromObject", signedCtx, mock.Anything, mock.Anything).
|
||||
Return("test/path", nil)
|
||||
|
||||
progress := jobs.NewMockJobProgressRecorder(t)
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Action == repository.FileActionCreated &&
|
||||
result.Name == "test" &&
|
||||
result.Error == nil &&
|
||||
result.Path == "test/path"
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
|
||||
migrator := NewLegacyResourceMigrator(
|
||||
nil,
|
||||
mockParser,
|
||||
mockRepoResources,
|
||||
progress,
|
||||
provisioning.MigrateJobOptions{},
|
||||
"test-namespace",
|
||||
schema.GroupResource{Group: "test.grafana.app", Resource: "tests"},
|
||||
mockSigner,
|
||||
)
|
||||
|
||||
err = migrator.Write(context.Background(), &resource.ResourceKey{}, []byte("test"))
|
||||
require.NoError(t, err)
|
||||
|
||||
mockParser.AssertExpectations(t)
|
||||
mockSigner.AssertExpectations(t)
|
||||
mockRepoResources.AssertExpectations(t)
|
||||
progress.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("should successfully write resource", func(t *testing.T) {
|
||||
mockParser := resources.NewMockParser(t)
|
||||
obj := &unstructured.Unstructured{
|
||||
@@ -338,6 +510,7 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
|
||||
provisioning.MigrateJobOptions{},
|
||||
"test-namespace",
|
||||
schema.GroupResource{Group: "test.grafana.app", Resource: "tests"},
|
||||
signature.NewGrafanaSigner(),
|
||||
)
|
||||
|
||||
err = migrator.Write(context.Background(), &resource.ResourceKey{}, []byte("test"))
|
||||
@@ -382,6 +555,7 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) {
|
||||
provisioning.MigrateJobOptions{},
|
||||
"test-namespace",
|
||||
schema.GroupResource{Group: "test.grafana.app", Resource: "tests"},
|
||||
signature.NewGrafanaSigner(),
|
||||
)
|
||||
|
||||
err = migrator.Write(context.Background(), &resource.ResourceKey{}, []byte("test"))
|
||||
@@ -411,6 +585,7 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) {
|
||||
provisioning.MigrateJobOptions{},
|
||||
"test-namespace",
|
||||
schema.GroupResource{Group: "test.grafana.app", Resource: "tests"},
|
||||
signature.NewGrafanaSigner(),
|
||||
)
|
||||
|
||||
err := migrator.Migrate(context.Background())
|
||||
@@ -441,6 +616,7 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) {
|
||||
provisioning.MigrateJobOptions{},
|
||||
"test-namespace",
|
||||
schema.GroupResource{Group: "test.grafana.app", Resource: "test-resources"},
|
||||
signature.NewGrafanaSigner(),
|
||||
)
|
||||
|
||||
err := migrator.Migrate(context.Background())
|
||||
@@ -471,6 +647,7 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) {
|
||||
provisioning.MigrateJobOptions{},
|
||||
"test-namespace",
|
||||
schema.GroupResource{Group: "test.grafana.app", Resource: "tests"},
|
||||
signature.NewGrafanaSigner(),
|
||||
)
|
||||
|
||||
err := migrator.Migrate(context.Background())
|
||||
@@ -509,6 +686,7 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) {
|
||||
provisioning.MigrateJobOptions{},
|
||||
"test-namespace",
|
||||
schema.GroupResource{Group: "test.grafana.app", Resource: "tests"},
|
||||
signature.NewGrafanaSigner(),
|
||||
)
|
||||
|
||||
err := migrator.Migrate(context.Background())
|
||||
@@ -538,6 +716,7 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) {
|
||||
progress := jobs.NewMockJobProgressRecorder(t)
|
||||
progress.On("SetMessage", mock.Anything, mock.Anything).Return()
|
||||
progress.On("SetTotal", mock.Anything, 200).Return()
|
||||
signer := signature.NewMockSigner(t)
|
||||
|
||||
migrator := NewLegacyResourceMigrator(
|
||||
mockLegacyMigrator,
|
||||
@@ -547,6 +726,7 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) {
|
||||
provisioning.MigrateJobOptions{},
|
||||
"test-namespace",
|
||||
schema.GroupResource{Group: "test.grafana.app", Resource: "tests"},
|
||||
signer,
|
||||
)
|
||||
|
||||
err := migrator.Migrate(context.Background())
|
||||
|
||||
@@ -86,7 +86,7 @@ func (r *SyncWorker) Process(ctx context.Context, repo repository.Repository, jo
|
||||
return fmt.Errorf("update repo with job status at start: %w", err)
|
||||
}
|
||||
|
||||
repositoryResources, err := r.repositoryResources.Client(ctx, rw, resources.RepositoryResourcesOptions{})
|
||||
repositoryResources, err := r.repositoryResources.Client(ctx, rw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create repository resources client: %w", err)
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ func TestSyncWorker_Process(t *testing.T) {
|
||||
rpf.On("Execute", mock.Anything, repoConfig, mock.Anything).Return(nil)
|
||||
|
||||
// Repository resources creation fails
|
||||
rrf.On("Client", mock.Anything, mock.Anything, resources.RepositoryResourcesOptions{}).Return(nil, errors.New("failed to create repository resources client"))
|
||||
rrf.On("Client", mock.Anything, mock.Anything).Return(nil, errors.New("failed to create repository resources client"))
|
||||
},
|
||||
expectedError: "create repository resources client: failed to create repository resources client",
|
||||
},
|
||||
@@ -189,7 +189,7 @@ func TestSyncWorker_Process(t *testing.T) {
|
||||
rpf.On("Execute", mock.Anything, repoConfig, mock.Anything).Return(nil)
|
||||
|
||||
// Repository resources creation succeeds
|
||||
rrf.On("Client", mock.Anything, mock.Anything, resources.RepositoryResourcesOptions{}).Return(&resources.MockRepositoryResources{}, nil)
|
||||
rrf.On("Client", mock.Anything, mock.Anything).Return(&resources.MockRepositoryResources{}, nil)
|
||||
|
||||
// Getting clients for namespace fails
|
||||
cf.On("Clients", mock.Anything, "test-namespace").Return(nil, errors.New("failed to get clients"))
|
||||
@@ -222,7 +222,7 @@ func TestSyncWorker_Process(t *testing.T) {
|
||||
// Setup resources and clients
|
||||
mockRepoResources := resources.NewMockRepositoryResources(t)
|
||||
mockRepoResources.On("Stats", mock.Anything).Return(nil, nil)
|
||||
rrf.On("Client", mock.Anything, mock.Anything, resources.RepositoryResourcesOptions{}).Return(mockRepoResources, nil)
|
||||
rrf.On("Client", mock.Anything, mock.Anything).Return(mockRepoResources, nil)
|
||||
|
||||
mockClients := resources.NewMockResourceClients(t)
|
||||
cf.On("Clients", mock.Anything, "test-namespace").Return(mockClients, nil)
|
||||
@@ -277,7 +277,7 @@ func TestSyncWorker_Process(t *testing.T) {
|
||||
// Setup resources and clients
|
||||
mockRepoResources := resources.NewMockRepositoryResources(t)
|
||||
mockRepoResources.On("Stats", mock.Anything).Return(nil, nil)
|
||||
rrf.On("Client", mock.Anything, mock.Anything, resources.RepositoryResourcesOptions{}).Return(mockRepoResources, nil)
|
||||
rrf.On("Client", mock.Anything, mock.Anything).Return(mockRepoResources, nil)
|
||||
|
||||
mockClients := resources.NewMockResourceClients(t)
|
||||
cf.On("Clients", mock.Anything, "test-namespace").Return(mockClients, nil)
|
||||
@@ -321,7 +321,7 @@ func TestSyncWorker_Process(t *testing.T) {
|
||||
|
||||
mockRepoResources := resources.NewMockRepositoryResources(t)
|
||||
mockRepoResources.On("Stats", mock.Anything).Return(nil, errors.New("stats error"))
|
||||
rrf.On("Client", mock.Anything, mock.Anything, resources.RepositoryResourcesOptions{}).Return(mockRepoResources, nil)
|
||||
rrf.On("Client", mock.Anything, mock.Anything).Return(mockRepoResources, nil)
|
||||
|
||||
// Simple mocks for other calls
|
||||
mockClients := resources.NewMockResourceClients(t)
|
||||
@@ -347,7 +347,7 @@ func TestSyncWorker_Process(t *testing.T) {
|
||||
|
||||
mockRepoResources := resources.NewMockRepositoryResources(t)
|
||||
mockRepoResources.On("Stats", mock.Anything).Return(nil, nil)
|
||||
rrf.On("Client", mock.Anything, mock.Anything, resources.RepositoryResourcesOptions{}).Return(mockRepoResources, nil)
|
||||
rrf.On("Client", mock.Anything, mock.Anything).Return(mockRepoResources, nil)
|
||||
|
||||
// Verify only sync status is patched
|
||||
rpf.On("Execute", mock.Anything, mock.Anything, mock.MatchedBy(func(patch []map[string]interface{}) bool {
|
||||
@@ -391,7 +391,7 @@ func TestSyncWorker_Process(t *testing.T) {
|
||||
},
|
||||
}
|
||||
mockRepoResources.On("Stats", mock.Anything).Return(stats, nil)
|
||||
rrf.On("Client", mock.Anything, mock.Anything, resources.RepositoryResourcesOptions{}).Return(mockRepoResources, nil)
|
||||
rrf.On("Client", mock.Anything, mock.Anything).Return(mockRepoResources, nil)
|
||||
|
||||
// Verify both sync status and stats are patched
|
||||
rpf.On("Execute", mock.Anything, mock.Anything, mock.MatchedBy(func(patch []map[string]interface{}) bool {
|
||||
@@ -463,7 +463,7 @@ func TestSyncWorker_Process(t *testing.T) {
|
||||
},
|
||||
}
|
||||
mockRepoResources.On("Stats", mock.Anything).Return(stats, nil)
|
||||
rrf.On("Client", mock.Anything, mock.Anything, resources.RepositoryResourcesOptions{}).Return(mockRepoResources, nil)
|
||||
rrf.On("Client", mock.Anything, mock.Anything).Return(mockRepoResources, nil)
|
||||
|
||||
// Verify only sync status is patched (multiple stats should be ignored)
|
||||
rpf.On("Execute", mock.Anything, mock.Anything, mock.MatchedBy(func(patch []map[string]interface{}) bool {
|
||||
@@ -497,7 +497,7 @@ func TestSyncWorker_Process(t *testing.T) {
|
||||
// Setup resources and clients
|
||||
mockRepoResources := resources.NewMockRepositoryResources(t)
|
||||
mockRepoResources.On("Stats", mock.Anything).Return(nil, nil)
|
||||
rrf.On("Client", mock.Anything, mock.Anything, resources.RepositoryResourcesOptions{}).Return(mockRepoResources, nil)
|
||||
rrf.On("Client", mock.Anything, mock.Anything).Return(mockRepoResources, nil)
|
||||
|
||||
mockClients := resources.NewMockResourceClients(t)
|
||||
cf.On("Clients", mock.Anything, mock.Anything).Return(mockClients, nil)
|
||||
|
||||
@@ -49,6 +49,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
|
||||
gogit "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/go-git"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources/signature"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
@@ -546,13 +547,14 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH
|
||||
statusPatcher.Patch,
|
||||
syncer,
|
||||
)
|
||||
|
||||
signerFactory := signature.NewSignerFactory(b.clients)
|
||||
legacyFolders := migrate.NewLegacyFoldersMigrator(b.legacyMigrator)
|
||||
legacyResources := migrate.NewLegacyResourcesMigrator(
|
||||
b.repositoryResources,
|
||||
b.parsers,
|
||||
b.legacyMigrator,
|
||||
legacyFolders,
|
||||
signerFactory,
|
||||
)
|
||||
storageSwapper := migrate.NewStorageSwapper(b.unified, b.storageStatus)
|
||||
legacyMigrator := migrate.NewLegacyMigrator(
|
||||
|
||||
@@ -10,15 +10,9 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type RepositoryResourcesOptions struct {
|
||||
// FIXME: this is a temporary option to preload all user info
|
||||
// we should remove this once we have a better way to handle user info and commit signatures
|
||||
PreloadAllUserInfo bool
|
||||
}
|
||||
|
||||
//go:generate mockery --name RepositoryResourcesFactory --structname MockRepositoryResourcesFactory --inpackage --filename repository_resources_factory_mock.go --with-expecter
|
||||
type RepositoryResourcesFactory interface {
|
||||
Client(ctx context.Context, repo repository.ReaderWriter, opts RepositoryResourcesOptions) (RepositoryResources, error)
|
||||
Client(ctx context.Context, repo repository.ReaderWriter) (RepositoryResources, error)
|
||||
}
|
||||
|
||||
//go:generate mockery --name RepositoryResources --structname MockRepositoryResources --inpackage --filename repository_resources_mock.go --with-expecter
|
||||
@@ -64,7 +58,7 @@ func NewRepositoryResourcesFactory(parsers ParserFactory, clients ClientFactory,
|
||||
return &repositoryResourcesFactory{parsers, clients, lister}
|
||||
}
|
||||
|
||||
func (r *repositoryResourcesFactory) Client(ctx context.Context, repo repository.ReaderWriter, opts RepositoryResourcesOptions) (RepositoryResources, error) {
|
||||
func (r *repositoryResourcesFactory) Client(ctx context.Context, repo repository.ReaderWriter) (RepositoryResources, error) {
|
||||
clients, err := r.clients.Clients(ctx, repo.Config().Namespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create clients: %w", err)
|
||||
@@ -79,21 +73,8 @@ func (r *repositoryResourcesFactory) Client(ctx context.Context, repo repository
|
||||
return nil, fmt.Errorf("create parser: %w", err)
|
||||
}
|
||||
|
||||
signatures := map[string]repository.CommitSignature{}
|
||||
if opts.PreloadAllUserInfo {
|
||||
userClient, err := clients.User()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create user client: %w", err)
|
||||
}
|
||||
|
||||
signatures, err = loadUsers(ctx, userClient)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load users: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
folders := NewFolderManager(repo, folderClient, NewEmptyFolderTree())
|
||||
resources := NewResourcesManager(repo, folders, parser, clients, signatures)
|
||||
resources := NewResourcesManager(repo, folders, parser, clients)
|
||||
|
||||
return &repositoryResources{
|
||||
FolderManager: folders,
|
||||
|
||||
@@ -22,9 +22,9 @@ func (_m *MockRepositoryResourcesFactory) EXPECT() *MockRepositoryResourcesFacto
|
||||
return &MockRepositoryResourcesFactory_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Client provides a mock function with given fields: ctx, repo, opts
|
||||
func (_m *MockRepositoryResourcesFactory) Client(ctx context.Context, repo repository.ReaderWriter, opts RepositoryResourcesOptions) (RepositoryResources, error) {
|
||||
ret := _m.Called(ctx, repo, opts)
|
||||
// Client provides a mock function with given fields: ctx, repo
|
||||
func (_m *MockRepositoryResourcesFactory) Client(ctx context.Context, repo repository.ReaderWriter) (RepositoryResources, error) {
|
||||
ret := _m.Called(ctx, repo)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Client")
|
||||
@@ -32,19 +32,19 @@ func (_m *MockRepositoryResourcesFactory) Client(ctx context.Context, repo repos
|
||||
|
||||
var r0 RepositoryResources
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, repository.ReaderWriter, RepositoryResourcesOptions) (RepositoryResources, error)); ok {
|
||||
return rf(ctx, repo, opts)
|
||||
if rf, ok := ret.Get(0).(func(context.Context, repository.ReaderWriter) (RepositoryResources, error)); ok {
|
||||
return rf(ctx, repo)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, repository.ReaderWriter, RepositoryResourcesOptions) RepositoryResources); ok {
|
||||
r0 = rf(ctx, repo, opts)
|
||||
if rf, ok := ret.Get(0).(func(context.Context, repository.ReaderWriter) RepositoryResources); ok {
|
||||
r0 = rf(ctx, repo)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(RepositoryResources)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, repository.ReaderWriter, RepositoryResourcesOptions) error); ok {
|
||||
r1 = rf(ctx, repo, opts)
|
||||
if rf, ok := ret.Get(1).(func(context.Context, repository.ReaderWriter) error); ok {
|
||||
r1 = rf(ctx, repo)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
@@ -60,14 +60,13 @@ type MockRepositoryResourcesFactory_Client_Call struct {
|
||||
// Client is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - repo repository.ReaderWriter
|
||||
// - opts RepositoryResourcesOptions
|
||||
func (_e *MockRepositoryResourcesFactory_Expecter) Client(ctx interface{}, repo interface{}, opts interface{}) *MockRepositoryResourcesFactory_Client_Call {
|
||||
return &MockRepositoryResourcesFactory_Client_Call{Call: _e.mock.On("Client", ctx, repo, opts)}
|
||||
func (_e *MockRepositoryResourcesFactory_Expecter) Client(ctx interface{}, repo interface{}) *MockRepositoryResourcesFactory_Client_Call {
|
||||
return &MockRepositoryResourcesFactory_Client_Call{Call: _e.mock.On("Client", ctx, repo)}
|
||||
}
|
||||
|
||||
func (_c *MockRepositoryResourcesFactory_Client_Call) Run(run func(ctx context.Context, repo repository.ReaderWriter, opts RepositoryResourcesOptions)) *MockRepositoryResourcesFactory_Client_Call {
|
||||
func (_c *MockRepositoryResourcesFactory_Client_Call) Run(run func(ctx context.Context, repo repository.ReaderWriter)) *MockRepositoryResourcesFactory_Client_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(repository.ReaderWriter), args[2].(RepositoryResourcesOptions))
|
||||
run(args[0].(context.Context), args[1].(repository.ReaderWriter))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
@@ -77,7 +76,7 @@ func (_c *MockRepositoryResourcesFactory_Client_Call) Return(_a0 RepositoryResou
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockRepositoryResourcesFactory_Client_Call) RunAndReturn(run func(context.Context, repository.ReaderWriter, RepositoryResourcesOptions) (RepositoryResources, error)) *MockRepositoryResourcesFactory_Client_Call {
|
||||
func (_c *MockRepositoryResourcesFactory_Client_Call) RunAndReturn(run func(context.Context, repository.ReaderWriter) (RepositoryResources, error)) *MockRepositoryResourcesFactory_Client_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -41,17 +41,15 @@ type ResourcesManager struct {
|
||||
folders *FolderManager
|
||||
parser Parser
|
||||
clients ResourceClients
|
||||
userInfo map[string]repository.CommitSignature
|
||||
resourcesLookup map[resourceID]string // the path with this k8s name
|
||||
}
|
||||
|
||||
func NewResourcesManager(repo repository.ReaderWriter, folders *FolderManager, parser Parser, clients ResourceClients, userInfo map[string]repository.CommitSignature) *ResourcesManager {
|
||||
func NewResourcesManager(repo repository.ReaderWriter, folders *FolderManager, parser Parser, clients ResourceClients) *ResourcesManager {
|
||||
return &ResourcesManager{
|
||||
repo: repo,
|
||||
folders: folders,
|
||||
parser: parser,
|
||||
clients: clients,
|
||||
userInfo: userInfo,
|
||||
resourcesLookup: map[resourceID]string{},
|
||||
}
|
||||
}
|
||||
@@ -78,8 +76,6 @@ func (r *ResourcesManager) CreateResourceFileFromObject(ctx context.Context, obj
|
||||
}
|
||||
}
|
||||
|
||||
ctx = r.withAuthorSignature(ctx, meta)
|
||||
|
||||
name := meta.GetName()
|
||||
if name == "" {
|
||||
return "", ErrMissingName
|
||||
@@ -233,26 +229,3 @@ func (r *ResourcesManager) RemoveResourceFromFile(ctx context.Context, path stri
|
||||
|
||||
return objName, schema.GroupVersionKind{}, nil
|
||||
}
|
||||
|
||||
func (r *ResourcesManager) withAuthorSignature(ctx context.Context, item utils.GrafanaMetaAccessor) context.Context {
|
||||
id := item.GetUpdatedBy()
|
||||
if id == "" {
|
||||
id = item.GetCreatedBy()
|
||||
}
|
||||
if id == "" {
|
||||
id = "grafana"
|
||||
}
|
||||
|
||||
sig := r.userInfo[id] // lookup
|
||||
if sig.Name == "" && sig.Email == "" {
|
||||
sig.Name = id
|
||||
}
|
||||
t, err := item.GetUpdatedTimestamp()
|
||||
if err == nil && t != nil {
|
||||
sig.When = *t
|
||||
} else {
|
||||
sig.When = item.GetCreationTimestamp().Time
|
||||
}
|
||||
|
||||
return repository.WithAuthorSignature(ctx, sig)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package signature
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
)
|
||||
|
||||
type grafanaSigner struct{}
|
||||
|
||||
// FIXME: where should we use this default signature?
|
||||
// NewGrafanaSigner returns a Signer that uses the grafana user as the author
|
||||
func NewGrafanaSigner() Signer {
|
||||
return &grafanaSigner{}
|
||||
}
|
||||
|
||||
func (s *grafanaSigner) Sign(ctx context.Context, item utils.GrafanaMetaAccessor) (context.Context, error) {
|
||||
sig := repository.CommitSignature{
|
||||
Name: "grafana",
|
||||
// TODO: should we add email?
|
||||
// Email: "grafana@grafana.com",
|
||||
}
|
||||
|
||||
t, err := item.GetUpdatedTimestamp()
|
||||
if err == nil && t != nil {
|
||||
sig.When = *t
|
||||
} else {
|
||||
sig.When = item.GetCreationTimestamp().Time
|
||||
}
|
||||
|
||||
return repository.WithAuthorSignature(ctx, sig), nil
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package signature
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
)
|
||||
|
||||
func TestNewGrafanaSigner(t *testing.T) {
|
||||
signer := NewGrafanaSigner()
|
||||
require.NotNil(t, signer, "signer should not be nil")
|
||||
require.IsType(t, &grafanaSigner{}, signer, "signer should be of type *grafanaSigner")
|
||||
}
|
||||
|
||||
func TestGrafanaSigner_Sign(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
creationTimestamp time.Time
|
||||
updateTimestampErr error
|
||||
updatedTimestamp *time.Time
|
||||
expectedTime time.Time
|
||||
setupMocks func(meta *utils.MockGrafanaMetaAccessor)
|
||||
}{
|
||||
{
|
||||
name: "should use creation timestamp when no update timestamp",
|
||||
creationTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
updatedTimestamp: ptr(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)),
|
||||
updateTimestampErr: errors.New("failed"),
|
||||
expectedTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
setupMocks: func(meta *utils.MockGrafanaMetaAccessor) {
|
||||
meta.On("GetCreationTimestamp").Return(metav1.Time{Time: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should use creation timestamp when update timestamp is nil",
|
||||
creationTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
updatedTimestamp: nil,
|
||||
expectedTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
setupMocks: func(meta *utils.MockGrafanaMetaAccessor) {
|
||||
meta.On("GetCreationTimestamp").Return(metav1.Time{Time: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should use update timestamp when available",
|
||||
creationTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
updatedTimestamp: ptr(time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC)),
|
||||
expectedTime: time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
meta := utils.NewMockGrafanaMetaAccessor(t)
|
||||
var updatedTime *time.Time
|
||||
if tt.updatedTimestamp != nil {
|
||||
updatedTime = tt.updatedTimestamp
|
||||
}
|
||||
meta.On("GetUpdatedTimestamp").Return(updatedTime, tt.updateTimestampErr)
|
||||
|
||||
if tt.setupMocks != nil {
|
||||
tt.setupMocks(meta)
|
||||
}
|
||||
|
||||
signer := NewGrafanaSigner()
|
||||
ctx := context.Background()
|
||||
|
||||
signedCtx, err := signer.Sign(ctx, meta)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the signature in the context
|
||||
sig := repository.GetAuthorSignature(signedCtx)
|
||||
require.NotNil(t, sig, "signature should be present in context")
|
||||
require.Equal(t, "grafana", sig.Name)
|
||||
require.Equal(t, tt.expectedTime, sig.When)
|
||||
|
||||
meta.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package signature
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
|
||||
)
|
||||
|
||||
//go:generate mockery --name Signer --structname MockSigner --inpackage --filename signer_mock.go --with-expecter
|
||||
type Signer interface {
|
||||
Sign(ctx context.Context, item utils.GrafanaMetaAccessor) (context.Context, error)
|
||||
}
|
||||
|
||||
type SignOptions struct {
|
||||
Namespace string
|
||||
History bool
|
||||
}
|
||||
|
||||
// SignerFactory is a factory for creating Signers
|
||||
//
|
||||
//go:generate mockery --name SignerFactory --structname MockSignerFactory --inpackage --filename signature_factory_mock.go --with-expecter
|
||||
type SignerFactory interface {
|
||||
New(ctx context.Context, opts SignOptions) (Signer, error)
|
||||
}
|
||||
|
||||
type signerFactory struct {
|
||||
clients resources.ClientFactory
|
||||
}
|
||||
|
||||
func NewSignerFactory(clients resources.ClientFactory) SignerFactory {
|
||||
return &signerFactory{clients}
|
||||
}
|
||||
|
||||
func (f *signerFactory) New(ctx context.Context, opts SignOptions) (Signer, error) {
|
||||
if !opts.History {
|
||||
return NewGrafanaSigner(), nil
|
||||
}
|
||||
|
||||
clients, err := f.clients.Clients(ctx, opts.Namespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get clients: %w", err)
|
||||
}
|
||||
|
||||
userClient, err := clients.User()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get user client: %w", err)
|
||||
}
|
||||
|
||||
return NewLoadUsersOnceSigner(userClient), nil
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Code generated by mockery v2.52.4. DO NOT EDIT.
|
||||
|
||||
package signature
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockSignerFactory is an autogenerated mock type for the SignerFactory type
|
||||
type MockSignerFactory struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockSignerFactory_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockSignerFactory) EXPECT() *MockSignerFactory_Expecter {
|
||||
return &MockSignerFactory_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// New provides a mock function with given fields: ctx, opts
|
||||
func (_m *MockSignerFactory) New(ctx context.Context, opts SignOptions) (Signer, error) {
|
||||
ret := _m.Called(ctx, opts)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for New")
|
||||
}
|
||||
|
||||
var r0 Signer
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, SignOptions) (Signer, error)); ok {
|
||||
return rf(ctx, opts)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, SignOptions) Signer); ok {
|
||||
r0 = rf(ctx, opts)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(Signer)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, SignOptions) error); ok {
|
||||
r1 = rf(ctx, opts)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockSignerFactory_New_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'New'
|
||||
type MockSignerFactory_New_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// New is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - opts SignOptions
|
||||
func (_e *MockSignerFactory_Expecter) New(ctx interface{}, opts interface{}) *MockSignerFactory_New_Call {
|
||||
return &MockSignerFactory_New_Call{Call: _e.mock.On("New", ctx, opts)}
|
||||
}
|
||||
|
||||
func (_c *MockSignerFactory_New_Call) Run(run func(ctx context.Context, opts SignOptions)) *MockSignerFactory_New_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(SignOptions))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSignerFactory_New_Call) Return(_a0 Signer, _a1 error) *MockSignerFactory_New_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSignerFactory_New_Call) RunAndReturn(run func(context.Context, SignOptions) (Signer, error)) *MockSignerFactory_New_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockSignerFactory creates a new instance of MockSignerFactory. 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 NewMockSignerFactory(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockSignerFactory {
|
||||
mock := &MockSignerFactory{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package signature
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
|
||||
)
|
||||
|
||||
func TestSignerFactory_New(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts SignOptions
|
||||
setupMocks func(t *testing.T, clients *resources.MockClientFactory)
|
||||
expectedType interface{}
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "should return grafana signer when history is false",
|
||||
opts: SignOptions{
|
||||
History: false,
|
||||
},
|
||||
setupMocks: func(t *testing.T, clients *resources.MockClientFactory) {
|
||||
// No mocks needed as we shouldn't call any clients
|
||||
},
|
||||
expectedType: &grafanaSigner{},
|
||||
},
|
||||
{
|
||||
name: "should return load users once signer when history is true",
|
||||
opts: SignOptions{
|
||||
History: true,
|
||||
Namespace: "test-ns",
|
||||
},
|
||||
setupMocks: func(t *testing.T, clients *resources.MockClientFactory) {
|
||||
mockResourceClients := resources.NewMockResourceClients(t)
|
||||
clients.On("Clients", context.Background(), "test-ns").Return(mockResourceClients, nil)
|
||||
mockResourceClients.On("User").Return(nil, nil)
|
||||
},
|
||||
expectedType: &loadUsersOnceSigner{},
|
||||
},
|
||||
{
|
||||
name: "should return error when clients factory fails",
|
||||
opts: SignOptions{
|
||||
History: true,
|
||||
Namespace: "test-ns",
|
||||
},
|
||||
setupMocks: func(t *testing.T, clients *resources.MockClientFactory) {
|
||||
clients.On("Clients", context.Background(), "test-ns").Return(nil, fmt.Errorf("clients error"))
|
||||
},
|
||||
expectedError: "get clients: clients error",
|
||||
},
|
||||
{
|
||||
name: "should return error when user client fails",
|
||||
opts: SignOptions{
|
||||
History: true,
|
||||
Namespace: "test-ns",
|
||||
},
|
||||
setupMocks: func(t *testing.T, clients *resources.MockClientFactory) {
|
||||
mockResourceClients := resources.NewMockResourceClients(t)
|
||||
clients.On("Clients", context.Background(), "test-ns").Return(mockResourceClients, nil)
|
||||
mockResourceClients.On("User").Return(nil, fmt.Errorf("user client error"))
|
||||
},
|
||||
expectedError: "get user client: user client error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockClients := resources.NewMockClientFactory(t)
|
||||
tt.setupMocks(t, mockClients)
|
||||
|
||||
factory := NewSignerFactory(mockClients)
|
||||
signer, err := factory.New(context.Background(), tt.opts)
|
||||
|
||||
if tt.expectedError != "" {
|
||||
require.Error(t, err)
|
||||
require.EqualError(t, err, tt.expectedError)
|
||||
require.Nil(t, signer)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, signer)
|
||||
require.IsType(t, tt.expectedType, signer, "signer should be of expected type")
|
||||
}
|
||||
|
||||
mockClients.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Code generated by mockery v2.52.4. DO NOT EDIT.
|
||||
|
||||
package signature
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
utils "github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockSigner is an autogenerated mock type for the Signer type
|
||||
type MockSigner struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockSigner_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockSigner) EXPECT() *MockSigner_Expecter {
|
||||
return &MockSigner_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Sign provides a mock function with given fields: ctx, item
|
||||
func (_m *MockSigner) Sign(ctx context.Context, item utils.GrafanaMetaAccessor) (context.Context, error) {
|
||||
ret := _m.Called(ctx, item)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Sign")
|
||||
}
|
||||
|
||||
var r0 context.Context
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, utils.GrafanaMetaAccessor) (context.Context, error)); ok {
|
||||
return rf(ctx, item)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, utils.GrafanaMetaAccessor) context.Context); ok {
|
||||
r0 = rf(ctx, item)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(context.Context)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, utils.GrafanaMetaAccessor) error); ok {
|
||||
r1 = rf(ctx, item)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockSigner_Sign_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Sign'
|
||||
type MockSigner_Sign_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Sign is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - item utils.GrafanaMetaAccessor
|
||||
func (_e *MockSigner_Expecter) Sign(ctx interface{}, item interface{}) *MockSigner_Sign_Call {
|
||||
return &MockSigner_Sign_Call{Call: _e.mock.On("Sign", ctx, item)}
|
||||
}
|
||||
|
||||
func (_c *MockSigner_Sign_Call) Run(run func(ctx context.Context, item utils.GrafanaMetaAccessor)) *MockSigner_Sign_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(utils.GrafanaMetaAccessor))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSigner_Sign_Call) Return(_a0 context.Context, _a1 error) *MockSigner_Sign_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSigner_Sign_Call) RunAndReturn(run func(context.Context, utils.GrafanaMetaAccessor) (context.Context, error)) *MockSigner_Sign_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockSigner creates a new instance of MockSigner. 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 NewMockSigner(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockSigner {
|
||||
mock := &MockSigner{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package signature
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
|
||||
)
|
||||
|
||||
const maxUsers = 10000
|
||||
|
||||
type loadUsersOnceSigner struct {
|
||||
signatures map[string]repository.CommitSignature
|
||||
client dynamic.ResourceInterface
|
||||
once sync.Once
|
||||
onceErr error
|
||||
}
|
||||
|
||||
// NewLoadUsersOnceSigner returns a Signer that loads the signatures from users
|
||||
// it will only load the signatures once and cache them
|
||||
// if the user is not found, it will use the grafana user as the author
|
||||
func NewLoadUsersOnceSigner(client dynamic.ResourceInterface) Signer {
|
||||
return &loadUsersOnceSigner{
|
||||
client: client,
|
||||
once: sync.Once{},
|
||||
signatures: map[string]repository.CommitSignature{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *loadUsersOnceSigner) Sign(ctx context.Context, item utils.GrafanaMetaAccessor) (context.Context, error) {
|
||||
if s.onceErr != nil {
|
||||
return ctx, fmt.Errorf("load signatures: %w", s.onceErr)
|
||||
}
|
||||
|
||||
var err error
|
||||
s.once.Do(func() {
|
||||
s.signatures, err = s.load(ctx, s.client)
|
||||
s.onceErr = err
|
||||
})
|
||||
if err != nil {
|
||||
return ctx, fmt.Errorf("load signatures: %w", err)
|
||||
}
|
||||
|
||||
id := item.GetUpdatedBy()
|
||||
if id == "" {
|
||||
id = item.GetCreatedBy()
|
||||
}
|
||||
if id == "" {
|
||||
id = "grafana"
|
||||
}
|
||||
|
||||
sig := s.signatures[id] // lookup
|
||||
if sig.Name == "" && sig.Email == "" {
|
||||
sig.Name = id
|
||||
}
|
||||
t, err := item.GetUpdatedTimestamp()
|
||||
if err == nil && t != nil {
|
||||
sig.When = *t
|
||||
} else {
|
||||
sig.When = item.GetCreationTimestamp().Time
|
||||
}
|
||||
|
||||
return repository.WithAuthorSignature(ctx, sig), nil
|
||||
}
|
||||
|
||||
func (s *loadUsersOnceSigner) load(ctx context.Context, client dynamic.ResourceInterface) (map[string]repository.CommitSignature, error) {
|
||||
userInfo := make(map[string]repository.CommitSignature)
|
||||
var count int
|
||||
err := resources.ForEach(ctx, client, func(item *unstructured.Unstructured) error {
|
||||
count++
|
||||
if count > maxUsers {
|
||||
return errors.New("too many users")
|
||||
}
|
||||
|
||||
sig := repository.CommitSignature{}
|
||||
// FIXME: should we improve logging here?
|
||||
var (
|
||||
ok bool
|
||||
err error
|
||||
)
|
||||
|
||||
sig.Name, ok, err = unstructured.NestedString(item.Object, "spec", "login")
|
||||
if !ok || err != nil {
|
||||
return nil
|
||||
}
|
||||
sig.Email, ok, err = unstructured.NestedString(item.Object, "spec", "email")
|
||||
if !ok || err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if sig.Name == sig.Email {
|
||||
if sig.Name == "" {
|
||||
sig.Name = item.GetName()
|
||||
} else if strings.Contains(sig.Email, "@") {
|
||||
sig.Email = "" // don't use the same value for name+email
|
||||
}
|
||||
}
|
||||
|
||||
userInfo["user:"+item.GetName()] = sig
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
package signature
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
)
|
||||
|
||||
// mockDynamicInterface implements a simplified version of the dynamic.ResourceInterface
|
||||
type mockDynamicInterface struct {
|
||||
dynamic.ResourceInterface
|
||||
items []unstructured.Unstructured
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *mockDynamicInterface) List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) {
|
||||
if m.err != nil {
|
||||
return nil, m.err
|
||||
}
|
||||
return &unstructured.UnstructuredList{
|
||||
Items: m.items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type mockGrafanaMetaAccessor struct {
|
||||
utils.GrafanaMetaAccessor
|
||||
createdBy string
|
||||
updatedBy string
|
||||
creationTimestamp time.Time
|
||||
updatedTimestamp *time.Time
|
||||
updatedTimestampErr error
|
||||
}
|
||||
|
||||
func (m *mockGrafanaMetaAccessor) GetCreatedBy() string {
|
||||
return m.createdBy
|
||||
}
|
||||
|
||||
func (m *mockGrafanaMetaAccessor) GetUpdatedBy() string {
|
||||
return m.updatedBy
|
||||
}
|
||||
|
||||
func (m *mockGrafanaMetaAccessor) GetCreationTimestamp() metav1.Time {
|
||||
return metav1.Time{Time: m.creationTimestamp}
|
||||
}
|
||||
|
||||
func (m *mockGrafanaMetaAccessor) GetUpdatedTimestamp() (*time.Time, error) {
|
||||
if m.updatedTimestampErr != nil {
|
||||
return nil, m.updatedTimestampErr
|
||||
}
|
||||
return m.updatedTimestamp, nil
|
||||
}
|
||||
|
||||
func TestLoadUsersOnceSigner_Sign(t *testing.T) {
|
||||
baseTime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updateTime := time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
items []unstructured.Unstructured
|
||||
meta *mockGrafanaMetaAccessor
|
||||
clientErr error
|
||||
expectedSig repository.CommitSignature
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "should sign with user info when user exists",
|
||||
items: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "user1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"login": "johndoe",
|
||||
"email": "john@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
meta: &mockGrafanaMetaAccessor{
|
||||
updatedBy: "user:user1",
|
||||
creationTimestamp: baseTime,
|
||||
updatedTimestamp: &updateTime,
|
||||
},
|
||||
expectedSig: repository.CommitSignature{
|
||||
Name: "johndoe",
|
||||
Email: "john@example.com",
|
||||
When: updateTime,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should fallback to created by when updated by is empty",
|
||||
items: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "user1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"login": "johndoe",
|
||||
"email": "john@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
meta: &mockGrafanaMetaAccessor{
|
||||
createdBy: "user:user1",
|
||||
creationTimestamp: baseTime,
|
||||
},
|
||||
expectedSig: repository.CommitSignature{
|
||||
Name: "johndoe",
|
||||
Email: "john@example.com",
|
||||
When: baseTime,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should use grafana when no user info available",
|
||||
meta: &mockGrafanaMetaAccessor{
|
||||
creationTimestamp: baseTime,
|
||||
},
|
||||
expectedSig: repository.CommitSignature{
|
||||
Name: "grafana",
|
||||
When: baseTime,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should handle user with same login and email",
|
||||
items: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "user1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"login": "john@example.com",
|
||||
"email": "john@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
meta: &mockGrafanaMetaAccessor{
|
||||
updatedBy: "user:user1",
|
||||
creationTimestamp: baseTime,
|
||||
updatedTimestamp: &updateTime,
|
||||
},
|
||||
expectedSig: repository.CommitSignature{
|
||||
Name: "john@example.com",
|
||||
Email: "",
|
||||
When: updateTime,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should handle empty login and email",
|
||||
items: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "user1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"login": "",
|
||||
"email": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
meta: &mockGrafanaMetaAccessor{
|
||||
updatedBy: "user:user1",
|
||||
creationTimestamp: baseTime,
|
||||
updatedTimestamp: &updateTime,
|
||||
},
|
||||
expectedSig: repository.CommitSignature{
|
||||
Name: "user1",
|
||||
Email: "",
|
||||
When: updateTime,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should handle empty email",
|
||||
items: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "user1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"login": "johndoe",
|
||||
"email": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
meta: &mockGrafanaMetaAccessor{
|
||||
updatedBy: "user:user1",
|
||||
creationTimestamp: baseTime,
|
||||
updatedTimestamp: &updateTime,
|
||||
},
|
||||
expectedSig: repository.CommitSignature{
|
||||
Name: "johndoe",
|
||||
Email: "",
|
||||
When: updateTime,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should fail when too many users",
|
||||
items: func() []unstructured.Unstructured {
|
||||
items := make([]unstructured.Unstructured, maxUsers+1)
|
||||
for i := 0; i < maxUsers+1; i++ {
|
||||
items[i] = unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "user1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"login": "johndoe",
|
||||
"email": "john@example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return items
|
||||
}(),
|
||||
meta: &mockGrafanaMetaAccessor{
|
||||
updatedBy: "user:user1",
|
||||
creationTimestamp: baseTime,
|
||||
},
|
||||
expectedError: "load signatures: too many users",
|
||||
},
|
||||
{
|
||||
name: "should handle missing user fields gracefully",
|
||||
items: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "user1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
// missing login and email
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
meta: &mockGrafanaMetaAccessor{
|
||||
updatedBy: "user:user1",
|
||||
creationTimestamp: baseTime,
|
||||
},
|
||||
expectedSig: repository.CommitSignature{
|
||||
Name: "user:user1",
|
||||
When: baseTime,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should use creation timestamp when update timestamp has error",
|
||||
items: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "user1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"login": "johndoe",
|
||||
"email": "john@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
meta: &mockGrafanaMetaAccessor{
|
||||
updatedBy: "user:user1",
|
||||
creationTimestamp: baseTime,
|
||||
updatedTimestampErr: errors.New("update timestamp error"),
|
||||
},
|
||||
expectedSig: repository.CommitSignature{
|
||||
Name: "johndoe",
|
||||
Email: "john@example.com",
|
||||
When: baseTime,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should fail when listing users fails",
|
||||
meta: &mockGrafanaMetaAccessor{
|
||||
updatedBy: "user:user1",
|
||||
creationTimestamp: baseTime,
|
||||
},
|
||||
clientErr: fmt.Errorf("failed to list users"),
|
||||
expectedError: "load signatures: error executing list: failed to list users",
|
||||
},
|
||||
{
|
||||
name: "should handle empty user list",
|
||||
meta: &mockGrafanaMetaAccessor{
|
||||
updatedBy: "user:user1",
|
||||
creationTimestamp: baseTime,
|
||||
},
|
||||
items: []unstructured.Unstructured{},
|
||||
expectedSig: repository.CommitSignature{
|
||||
Name: "user:user1",
|
||||
When: baseTime,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should handle multiple calls with error",
|
||||
meta: &mockGrafanaMetaAccessor{
|
||||
updatedBy: "user:user1",
|
||||
creationTimestamp: baseTime,
|
||||
},
|
||||
clientErr: fmt.Errorf("failed to list users"),
|
||||
expectedError: "load signatures: error executing list: failed to list users",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := &mockDynamicInterface{
|
||||
items: tt.items,
|
||||
err: tt.clientErr,
|
||||
}
|
||||
|
||||
signer := NewLoadUsersOnceSigner(client)
|
||||
ctx := context.Background()
|
||||
|
||||
signedCtx, err := signer.Sign(ctx, tt.meta)
|
||||
|
||||
if tt.expectedError != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.expectedError)
|
||||
|
||||
// Test that subsequent calls also fail with the same error
|
||||
_, err2 := signer.Sign(ctx, tt.meta)
|
||||
require.Error(t, err2)
|
||||
require.Contains(t, err2.Error(), tt.expectedError)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
sig := repository.GetAuthorSignature(signedCtx)
|
||||
require.NotNil(t, sig)
|
||||
require.Equal(t, tt.expectedSig.Name, sig.Name)
|
||||
require.Equal(t, tt.expectedSig.Email, sig.Email)
|
||||
require.Equal(t, tt.expectedSig.When, sig.When)
|
||||
|
||||
// Test that subsequent calls use cached data
|
||||
signedCtx2, err := signer.Sign(ctx, tt.meta)
|
||||
require.NoError(t, err)
|
||||
sig2 := repository.GetAuthorSignature(signedCtx2)
|
||||
require.Equal(t, sig, sig2)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
)
|
||||
|
||||
const maxUsers = 10000
|
||||
|
||||
func loadUsers(ctx context.Context, client dynamic.ResourceInterface) (map[string]repository.CommitSignature, error) {
|
||||
userInfo := make(map[string]repository.CommitSignature)
|
||||
var count int
|
||||
err := ForEach(ctx, client, func(item *unstructured.Unstructured) error {
|
||||
count++
|
||||
if count > maxUsers {
|
||||
return errors.New("too many users")
|
||||
}
|
||||
|
||||
sig := repository.CommitSignature{}
|
||||
// FIXME: should we improve logging here?
|
||||
var (
|
||||
ok bool
|
||||
err error
|
||||
)
|
||||
|
||||
sig.Name, ok, err = unstructured.NestedString(item.Object, "spec", "login")
|
||||
if !ok || err != nil {
|
||||
return nil
|
||||
}
|
||||
sig.Email, ok, err = unstructured.NestedString(item.Object, "spec", "email")
|
||||
if !ok || err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if sig.Name == sig.Email {
|
||||
if sig.Name == "" {
|
||||
sig.Name = item.GetName()
|
||||
} else if strings.Contains(sig.Email, "@") {
|
||||
sig.Email = "" // don't use the same value for name+email
|
||||
}
|
||||
}
|
||||
|
||||
userInfo["user:"+item.GetName()] = sig
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
)
|
||||
|
||||
// mockDynamicInterface implements a simplified version of the dynamic.ResourceInterface
|
||||
type mockDynamicInterface struct {
|
||||
dynamic.ResourceInterface
|
||||
items []unstructured.Unstructured
|
||||
}
|
||||
|
||||
func (m *mockDynamicInterface) List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) {
|
||||
return &unstructured.UnstructuredList{
|
||||
Items: m.items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestLoadUsers(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
items []unstructured.Unstructured
|
||||
expectedUsers map[string]repository.CommitSignature
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "should load users successfully",
|
||||
items: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "user1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"login": "johndoe",
|
||||
"email": "john@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "user2",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"login": "janedoe",
|
||||
"email": "jane@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedUsers: map[string]repository.CommitSignature{
|
||||
"user:user1": {
|
||||
Name: "johndoe",
|
||||
Email: "john@example.com",
|
||||
},
|
||||
"user:user2": {
|
||||
Name: "janedoe",
|
||||
Email: "jane@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should handle missing email",
|
||||
items: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "user1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"login": "johndoe",
|
||||
// email missing
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedUsers: map[string]repository.CommitSignature{},
|
||||
},
|
||||
{
|
||||
name: "should handle missing login",
|
||||
items: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "user1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
// login missing
|
||||
"email": "john@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedUsers: map[string]repository.CommitSignature{},
|
||||
},
|
||||
{
|
||||
name: "should handle same login and email",
|
||||
items: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "user1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"login": "john@example.com",
|
||||
"email": "john@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedUsers: map[string]repository.CommitSignature{
|
||||
"user:user1": {
|
||||
Name: "john@example.com",
|
||||
Email: "", // Email should be empty when same as login
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should handle empty login and email",
|
||||
items: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "user1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"login": "",
|
||||
"email": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedUsers: map[string]repository.CommitSignature{
|
||||
"user:user1": {
|
||||
Name: "user1", // Should use metadata name when login is empty
|
||||
Email: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should fail when too many users",
|
||||
items: func() []unstructured.Unstructured {
|
||||
items := make([]unstructured.Unstructured, maxUsers+1)
|
||||
for i := 0; i < maxUsers+1; i++ {
|
||||
items[i] = unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "user1",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"login": "johndoe",
|
||||
"email": "john@example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return items
|
||||
}(),
|
||||
expectedError: "too many users",
|
||||
},
|
||||
{
|
||||
name: "should handle empty user list",
|
||||
items: []unstructured.Unstructured{},
|
||||
expectedUsers: map[string]repository.CommitSignature{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := &mockDynamicInterface{
|
||||
items: tt.items,
|
||||
}
|
||||
|
||||
userInfo, err := loadUsers(context.Background(), client)
|
||||
|
||||
if tt.expectedError != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.expectedError)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expectedUsers, userInfo)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user