305 lines
10 KiB
Go
305 lines
10 KiB
Go
package migrate
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/client-go/dynamic"
|
|
|
|
"github.com/grafana/grafana/apps/provisioning/pkg/repository"
|
|
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
|
|
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
|
|
)
|
|
|
|
type mockClients struct {
|
|
mock.Mock
|
|
}
|
|
|
|
func (m *mockClients) ForResource(ctx context.Context, gvr schema.GroupVersionResource) (dynamic.ResourceInterface, schema.GroupVersionKind, error) {
|
|
args := m.Called(ctx, gvr)
|
|
var ri dynamic.ResourceInterface
|
|
if args.Get(0) != nil {
|
|
ri = args.Get(0).(dynamic.ResourceInterface)
|
|
}
|
|
return ri, args.Get(1).(schema.GroupVersionKind), args.Error(2)
|
|
}
|
|
|
|
func (m *mockClients) ForKind(ctx context.Context, gvk schema.GroupVersionKind) (dynamic.ResourceInterface, schema.GroupVersionResource, error) {
|
|
args := m.Called(ctx, gvk)
|
|
var ri dynamic.ResourceInterface
|
|
if args.Get(0) != nil {
|
|
ri = args.Get(0).(dynamic.ResourceInterface)
|
|
}
|
|
return ri, args.Get(1).(schema.GroupVersionResource), args.Error(2)
|
|
}
|
|
|
|
func (m *mockClients) Folder(ctx context.Context) (dynamic.ResourceInterface, error) {
|
|
args := m.Called(ctx)
|
|
var ri dynamic.ResourceInterface
|
|
if args.Get(0) != nil {
|
|
ri = args.Get(0).(dynamic.ResourceInterface)
|
|
}
|
|
return ri, args.Error(1)
|
|
}
|
|
|
|
func (m *mockClients) User(ctx context.Context) (dynamic.ResourceInterface, error) {
|
|
args := m.Called(ctx)
|
|
var ri dynamic.ResourceInterface
|
|
if args.Get(0) != nil {
|
|
ri = args.Get(0).(dynamic.ResourceInterface)
|
|
}
|
|
return ri, args.Error(1)
|
|
}
|
|
|
|
func TestNamespaceCleaner_Clean(t *testing.T) {
|
|
t.Run("should fail when getting clients fails", func(t *testing.T) {
|
|
mockClientFactory := resources.NewMockClientFactory(t)
|
|
mockClientFactory.On("Clients", mock.Anything, "test-namespace").
|
|
Return(nil, errors.New("failed to get clients"))
|
|
|
|
cleaner := NewNamespaceCleaner(mockClientFactory)
|
|
progress := jobs.NewMockJobProgressRecorder(t)
|
|
|
|
err := cleaner.Clean(context.Background(), "test-namespace", progress)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "get clients: failed to get clients")
|
|
|
|
mockClientFactory.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("should fail when getting resource client fails", func(t *testing.T) {
|
|
clients := &mockClients{}
|
|
clients.On("ForResource", mock.Anything, resources.SupportedProvisioningResources[0]).
|
|
Return(nil, schema.GroupVersionKind{}, errors.New("failed to get resource client"))
|
|
|
|
mockClientFactory := resources.NewMockClientFactory(t)
|
|
mockClientFactory.On("Clients", mock.Anything, "test-namespace").
|
|
Return(clients, nil)
|
|
|
|
cleaner := NewNamespaceCleaner(mockClientFactory)
|
|
progress := jobs.NewMockJobProgressRecorder(t)
|
|
progress.On("SetMessage", mock.Anything, mock.Anything).Return()
|
|
|
|
err := cleaner.Clean(context.Background(), "test-namespace", progress)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "get resource client: failed to get resource client")
|
|
|
|
mockClientFactory.AssertExpectations(t)
|
|
clients.AssertExpectations(t)
|
|
progress.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("should fail when delete operation fails", func(t *testing.T) {
|
|
// Create a mock dynamic client that returns a list with one item
|
|
mockDynamicClient := &mockDynamicInterface{
|
|
items: []unstructured.Unstructured{
|
|
{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": "folder.grafana.app/v1alpha1",
|
|
"kind": "Folder",
|
|
"metadata": map[string]interface{}{
|
|
"name": "test-folder",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
deleteError: errors.New("delete failed"),
|
|
}
|
|
|
|
clients := &mockClients{}
|
|
clients.On("ForResource", mock.Anything, mock.Anything).
|
|
Return(mockDynamicClient, schema.GroupVersionKind{}, nil)
|
|
|
|
mockClientFactory := resources.NewMockClientFactory(t)
|
|
mockClientFactory.On("Clients", mock.Anything, "test-namespace").
|
|
Return(clients, nil)
|
|
|
|
cleaner := NewNamespaceCleaner(mockClientFactory)
|
|
progress := jobs.NewMockJobProgressRecorder(t)
|
|
progress.On("SetMessage", mock.Anything, mock.Anything).Return()
|
|
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Action == repository.FileActionDeleted &&
|
|
result.Name == "test-folder" &&
|
|
result.Error != nil &&
|
|
result.Error.Error() == "deleting resource folder.grafana.app/Folder test-folder: delete failed"
|
|
})).Return()
|
|
|
|
err := cleaner.Clean(context.Background(), "test-namespace", progress)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "delete resource: delete failed")
|
|
|
|
mockClientFactory.AssertExpectations(t)
|
|
clients.AssertExpectations(t)
|
|
progress.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("should only delete unprovisioned resources", func(t *testing.T) {
|
|
// Create a mock dynamic client that returns a list with mixed provisioned and unprovisioned items
|
|
mockDynamicClient := &mockDynamicInterface{
|
|
items: []unstructured.Unstructured{
|
|
{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": "folder.grafana.app/v1alpha1",
|
|
"kind": "Folder",
|
|
"metadata": map[string]interface{}{
|
|
"name": "unprovisioned-folder",
|
|
// No manager annotations - this is unprovisioned
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": "dashboard.grafana.app/v1alpha1",
|
|
"kind": "Dashboard",
|
|
"metadata": map[string]interface{}{
|
|
"name": "provisioned-dashboard",
|
|
"annotations": map[string]interface{}{
|
|
"grafana.app/managerKind": "repo",
|
|
"grafana.app/managerId": "test-repo",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": "dashboard.grafana.app/v1alpha1",
|
|
"kind": "Dashboard",
|
|
"metadata": map[string]interface{}{
|
|
"name": "unprovisioned-dashboard",
|
|
// No manager annotations - this is unprovisioned
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
clients := &mockClients{}
|
|
clients.On("ForResource", mock.Anything, mock.Anything).
|
|
Return(mockDynamicClient, schema.GroupVersionKind{}, nil)
|
|
|
|
mockClientFactory := resources.NewMockClientFactory(t)
|
|
mockClientFactory.On("Clients", mock.Anything, "test-namespace").
|
|
Return(clients, nil)
|
|
|
|
cleaner := NewNamespaceCleaner(mockClientFactory)
|
|
progress := jobs.NewMockJobProgressRecorder(t)
|
|
progress.On("SetMessage", mock.Anything, "remove unprovisioned folders").Return()
|
|
progress.On("SetMessage", mock.Anything, "remove unprovisioned dashboards").Return()
|
|
|
|
// Expect only unprovisioned resources to be deleted (2 deletions)
|
|
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Action == repository.FileActionDeleted &&
|
|
result.Name == "unprovisioned-folder" &&
|
|
result.Error == nil
|
|
})).Return()
|
|
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Action == repository.FileActionDeleted &&
|
|
result.Name == "unprovisioned-dashboard" &&
|
|
result.Error == nil
|
|
})).Return()
|
|
|
|
// Expect provisioned resource to be ignored (1 ignore)
|
|
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Action == repository.FileActionIgnored &&
|
|
result.Name == "provisioned-dashboard" &&
|
|
result.Error == nil
|
|
})).Return()
|
|
|
|
err := cleaner.Clean(context.Background(), "test-namespace", progress)
|
|
require.NoError(t, err)
|
|
|
|
mockClientFactory.AssertExpectations(t)
|
|
clients.AssertExpectations(t)
|
|
progress.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("should skip all provisioned resources", func(t *testing.T) {
|
|
// Create a mock dynamic client with only provisioned resources
|
|
mockDynamicClient := &mockDynamicInterface{
|
|
items: []unstructured.Unstructured{
|
|
{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": "dashboard.grafana.app/v1alpha1",
|
|
"kind": "Dashboard",
|
|
"metadata": map[string]interface{}{
|
|
"name": "repo-managed-dashboard",
|
|
"annotations": map[string]interface{}{
|
|
"grafana.app/managerKind": "repo",
|
|
"grafana.app/managerId": "my-repo",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": "folder.grafana.app/v1alpha1",
|
|
"kind": "Folder",
|
|
"metadata": map[string]interface{}{
|
|
"name": "file-provisioned-folder",
|
|
"annotations": map[string]interface{}{
|
|
"grafana.app/managerKind": "classic-file-provisioning",
|
|
"grafana.app/managerId": "file-provisioner",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
clients := &mockClients{}
|
|
clients.On("ForResource", mock.Anything, mock.Anything).
|
|
Return(mockDynamicClient, schema.GroupVersionKind{}, nil)
|
|
|
|
mockClientFactory := resources.NewMockClientFactory(t)
|
|
mockClientFactory.On("Clients", mock.Anything, "test-namespace").
|
|
Return(clients, nil)
|
|
|
|
cleaner := NewNamespaceCleaner(mockClientFactory)
|
|
progress := jobs.NewMockJobProgressRecorder(t)
|
|
progress.On("SetMessage", mock.Anything, "remove unprovisioned folders").Return()
|
|
progress.On("SetMessage", mock.Anything, "remove unprovisioned dashboards").Return()
|
|
|
|
// Expect both resources to be ignored (no deletions)
|
|
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Action == repository.FileActionIgnored &&
|
|
result.Name == "repo-managed-dashboard" &&
|
|
result.Error == nil
|
|
})).Return()
|
|
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
|
return result.Action == repository.FileActionIgnored &&
|
|
result.Name == "file-provisioned-folder" &&
|
|
result.Error == nil
|
|
})).Return()
|
|
|
|
err := cleaner.Clean(context.Background(), "test-namespace", progress)
|
|
require.NoError(t, err)
|
|
|
|
mockClientFactory.AssertExpectations(t)
|
|
clients.AssertExpectations(t)
|
|
progress.AssertExpectations(t)
|
|
})
|
|
}
|
|
|
|
// mockDynamicInterface implements a simplified version of the dynamic.ResourceInterface
|
|
type mockDynamicInterface struct {
|
|
dynamic.ResourceInterface
|
|
items []unstructured.Unstructured
|
|
deleteError error
|
|
}
|
|
|
|
func (m *mockDynamicInterface) List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) {
|
|
return &unstructured.UnstructuredList{
|
|
Items: m.items,
|
|
}, nil
|
|
}
|
|
|
|
func (m *mockDynamicInterface) Delete(ctx context.Context, name string, opts metav1.DeleteOptions, subresources ...string) error {
|
|
return m.deleteError
|
|
}
|