Provisioning finalisers fix 2 (#111679)
* adding some logs to better understand what might be happening * only focus this PR on improve logging in finalizer handling * debug log before calling finalizers * working on finalizers * removing last todos, adding unit tests * better use SupportedFinalizers name * addressing comments * wip: fix tests and add delete error in status * chore: codegen * chore: codegen openapi * Merge remote-tracking branch 'origin/main' into provisioning-finalisers-fix-2 * update frontend client * fix: errors in testing * fix: breaking test --------- Co-authored-by: Daniele Ferru <daniele.ferru@grafana.com> Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
co-authored by
Daniele Ferru
Ryan McKinley
parent
893523dd7c
commit
1b766b9c9f
@@ -1,12 +1,508 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/repository"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
|
||||
)
|
||||
|
||||
var (
|
||||
_ dynamic.ResourceInterface = (*mockDynamicClient)(nil)
|
||||
_ repository.Repository = (*mockRepo)(nil)
|
||||
_ repository.Hooks = (*mockRepo)(nil)
|
||||
)
|
||||
|
||||
type mockDynamicClient struct {
|
||||
deleteFunc func(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error
|
||||
patchFunc func(ctx context.Context, name string, pt types.PatchType, data []byte, options metav1.PatchOptions, subresources ...string) (*unstructured.Unstructured, error)
|
||||
}
|
||||
|
||||
func (m mockDynamicClient) Create(ctx context.Context, obj *unstructured.Unstructured, options metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) {
|
||||
panic("not needed for testing")
|
||||
}
|
||||
|
||||
func (m mockDynamicClient) Update(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error) {
|
||||
panic("not needed for testing")
|
||||
}
|
||||
|
||||
func (m mockDynamicClient) UpdateStatus(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions) (*unstructured.Unstructured, error) {
|
||||
panic("not needed for testing")
|
||||
}
|
||||
|
||||
func (m mockDynamicClient) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error {
|
||||
if m.deleteFunc != nil {
|
||||
return m.deleteFunc(ctx, name, options, subresources...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m mockDynamicClient) DeleteCollection(ctx context.Context, options metav1.DeleteOptions, listOptions metav1.ListOptions) error {
|
||||
panic("not needed for testing")
|
||||
}
|
||||
|
||||
func (m mockDynamicClient) Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) {
|
||||
panic("not needed for testing")
|
||||
}
|
||||
|
||||
func (m mockDynamicClient) List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) {
|
||||
panic("not needed for testing")
|
||||
}
|
||||
|
||||
func (m mockDynamicClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
|
||||
panic("not needed for testing")
|
||||
}
|
||||
|
||||
func (m mockDynamicClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, options metav1.PatchOptions, subresources ...string) (*unstructured.Unstructured, error) {
|
||||
if m.patchFunc != nil {
|
||||
return m.patchFunc(ctx, name, pt, data, options, subresources...)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m mockDynamicClient) Apply(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions, subresources ...string) (*unstructured.Unstructured, error) {
|
||||
panic("not needed for testing")
|
||||
}
|
||||
|
||||
func (m mockDynamicClient) ApplyStatus(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions) (*unstructured.Unstructured, error) {
|
||||
panic("not needed for testing")
|
||||
}
|
||||
|
||||
type mockRepo struct {
|
||||
name string
|
||||
namespace string
|
||||
onDeleteFunc func(ctx context.Context) error
|
||||
}
|
||||
|
||||
func (m mockRepo) OnCreate(ctx context.Context) ([]map[string]interface{}, error) {
|
||||
panic("not needed for testing")
|
||||
}
|
||||
|
||||
func (m mockRepo) OnUpdate(ctx context.Context) ([]map[string]interface{}, error) {
|
||||
panic("not needed for testing")
|
||||
}
|
||||
|
||||
func (m mockRepo) OnDelete(ctx context.Context) error {
|
||||
if m.onDeleteFunc != nil {
|
||||
return m.onDeleteFunc(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m mockRepo) Config() *provisioning.Repository {
|
||||
return &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: m.name,
|
||||
Namespace: m.namespace,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m mockRepo) Validate() field.ErrorList {
|
||||
panic("not needed for testing")
|
||||
}
|
||||
|
||||
func (m mockRepo) Test(ctx context.Context) (*provisioning.TestResults, error) {
|
||||
panic("not needed for testing")
|
||||
}
|
||||
|
||||
func TestFinalizer_process(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
lister resources.ResourceLister
|
||||
clientFactory resources.ClientFactory
|
||||
repo repository.Repository
|
||||
finalizers []string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "No finalizers",
|
||||
lister: nil,
|
||||
clientFactory: nil,
|
||||
repo: nil,
|
||||
finalizers: []string{},
|
||||
},
|
||||
{
|
||||
name: "Successfully releases resources and cleanup hooks",
|
||||
lister: func() resources.ResourceLister {
|
||||
resourceLister := resources.NewMockResourceLister(t)
|
||||
|
||||
resourceLister.
|
||||
On("List", context.Background(), "default", "my-repo").
|
||||
Once().
|
||||
Return(&provisioning.ResourceList{
|
||||
Items: []provisioning.ResourceListItem{
|
||||
{
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Name: "my-dashboard",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
return resourceLister
|
||||
}(),
|
||||
clientFactory: func() resources.ClientFactory {
|
||||
clientFactory := resources.NewMockClientFactory(t)
|
||||
clients := resources.NewMockResourceClients(t)
|
||||
client := &mockDynamicClient{
|
||||
patchFunc: func(ctx context.Context, name string, pt types.PatchType, data []byte, options metav1.PatchOptions, subresources ...string) (*unstructured.Unstructured, error) {
|
||||
return &unstructured.Unstructured{}, nil
|
||||
},
|
||||
}
|
||||
|
||||
clientFactory.
|
||||
On("Clients", context.Background(), "default").
|
||||
Once().
|
||||
Return(clients, nil)
|
||||
|
||||
clients.
|
||||
On("ForResource", context.Background(), schema.GroupVersionResource{
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
}).
|
||||
Once().
|
||||
Return(client, schema.GroupVersionKind{}, nil)
|
||||
|
||||
return clientFactory
|
||||
}(),
|
||||
repo: mockRepo{
|
||||
name: "my-repo",
|
||||
namespace: "default",
|
||||
onDeleteFunc: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
finalizers: []string{
|
||||
repository.ReleaseOrphanResourcesFinalizer,
|
||||
repository.CleanFinalizer,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Successfully removes resources and cleanup hooks",
|
||||
lister: func() resources.ResourceLister {
|
||||
resourceLister := resources.NewMockResourceLister(t)
|
||||
|
||||
resourceLister.
|
||||
On("List", context.Background(), "default", "my-repo").
|
||||
Once().
|
||||
Return(&provisioning.ResourceList{
|
||||
Items: []provisioning.ResourceListItem{
|
||||
{
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Name: "my-dashboard",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
return resourceLister
|
||||
}(),
|
||||
clientFactory: func() resources.ClientFactory {
|
||||
clientFactory := resources.NewMockClientFactory(t)
|
||||
clients := resources.NewMockResourceClients(t)
|
||||
client := &mockDynamicClient{
|
||||
deleteFunc: func(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
clientFactory.
|
||||
On("Clients", context.Background(), "default").
|
||||
Once().
|
||||
Return(clients, nil)
|
||||
|
||||
clients.
|
||||
On("ForResource", context.Background(), schema.GroupVersionResource{
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
}).
|
||||
Once().
|
||||
Return(client, schema.GroupVersionKind{}, nil)
|
||||
|
||||
return clientFactory
|
||||
}(),
|
||||
repo: mockRepo{
|
||||
name: "my-repo",
|
||||
namespace: "default",
|
||||
onDeleteFunc: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
finalizers: []string{
|
||||
repository.RemoveOrphanResourcesFinalizer,
|
||||
repository.CleanFinalizer,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Issue getting the namespace clients",
|
||||
lister: nil,
|
||||
clientFactory: func() resources.ClientFactory {
|
||||
clientFactory := resources.NewMockClientFactory(t)
|
||||
|
||||
clientFactory.
|
||||
On("Clients", context.Background(), "default").
|
||||
Once().
|
||||
Return(nil, assert.AnError)
|
||||
|
||||
return clientFactory
|
||||
}(),
|
||||
repo: mockRepo{
|
||||
name: "my-repo",
|
||||
namespace: "default",
|
||||
},
|
||||
finalizers: []string{
|
||||
repository.RemoveOrphanResourcesFinalizer,
|
||||
repository.CleanFinalizer,
|
||||
},
|
||||
expectedErr: "remove resources: " + assert.AnError.Error(),
|
||||
},
|
||||
{
|
||||
name: "Issue listing items",
|
||||
lister: func() resources.ResourceLister {
|
||||
resourceLister := resources.NewMockResourceLister(t)
|
||||
|
||||
resourceLister.
|
||||
On("List", context.Background(), "default", "my-repo").
|
||||
Once().
|
||||
Return(nil, assert.AnError)
|
||||
|
||||
return resourceLister
|
||||
}(),
|
||||
clientFactory: func() resources.ClientFactory {
|
||||
clientFactory := resources.NewMockClientFactory(t)
|
||||
clients := resources.NewMockResourceClients(t)
|
||||
|
||||
clientFactory.
|
||||
On("Clients", context.Background(), "default").
|
||||
Once().
|
||||
Return(clients, nil)
|
||||
|
||||
return clientFactory
|
||||
}(),
|
||||
repo: mockRepo{
|
||||
name: "my-repo",
|
||||
namespace: "default",
|
||||
},
|
||||
finalizers: []string{
|
||||
repository.RemoveOrphanResourcesFinalizer,
|
||||
repository.CleanFinalizer,
|
||||
},
|
||||
expectedErr: "remove resources: " + assert.AnError.Error(),
|
||||
},
|
||||
{
|
||||
name: "Issue getting client for resource",
|
||||
lister: func() resources.ResourceLister {
|
||||
resourceLister := resources.NewMockResourceLister(t)
|
||||
|
||||
resourceLister.
|
||||
On("List", context.Background(), "default", "my-repo").
|
||||
Once().
|
||||
Return(&provisioning.ResourceList{
|
||||
Items: []provisioning.ResourceListItem{
|
||||
{
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Name: "my-dashboard",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
return resourceLister
|
||||
}(),
|
||||
clientFactory: func() resources.ClientFactory {
|
||||
clientFactory := resources.NewMockClientFactory(t)
|
||||
clients := resources.NewMockResourceClients(t)
|
||||
|
||||
clientFactory.
|
||||
On("Clients", context.Background(), "default").
|
||||
Once().
|
||||
Return(clients, nil)
|
||||
|
||||
clients.
|
||||
On("ForResource", context.Background(), schema.GroupVersionResource{
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
}).
|
||||
Once().
|
||||
Return(nil, schema.GroupVersionKind{}, assert.AnError)
|
||||
|
||||
return clientFactory
|
||||
}(),
|
||||
repo: mockRepo{
|
||||
name: "my-repo",
|
||||
namespace: "default",
|
||||
},
|
||||
finalizers: []string{
|
||||
repository.RemoveOrphanResourcesFinalizer,
|
||||
repository.CleanFinalizer,
|
||||
},
|
||||
expectedErr: "remove resources: " + assert.AnError.Error(),
|
||||
},
|
||||
{
|
||||
name: "Error deleting items",
|
||||
lister: func() resources.ResourceLister {
|
||||
resourceLister := resources.NewMockResourceLister(t)
|
||||
|
||||
resourceLister.
|
||||
On("List", context.Background(), "default", "my-repo").
|
||||
Once().
|
||||
Return(&provisioning.ResourceList{
|
||||
Items: []provisioning.ResourceListItem{
|
||||
{
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Name: "my-dashboard",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
return resourceLister
|
||||
}(),
|
||||
clientFactory: func() resources.ClientFactory {
|
||||
clientFactory := resources.NewMockClientFactory(t)
|
||||
clients := resources.NewMockResourceClients(t)
|
||||
client := &mockDynamicClient{
|
||||
deleteFunc: func(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error {
|
||||
return assert.AnError
|
||||
},
|
||||
}
|
||||
|
||||
clientFactory.
|
||||
On("Clients", context.Background(), "default").
|
||||
Once().
|
||||
Return(clients, nil)
|
||||
|
||||
clients.
|
||||
On("ForResource", context.Background(), schema.GroupVersionResource{
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
}).
|
||||
Once().
|
||||
Return(client, schema.GroupVersionKind{}, nil)
|
||||
|
||||
return clientFactory
|
||||
}(),
|
||||
repo: mockRepo{
|
||||
name: "my-repo",
|
||||
namespace: "default",
|
||||
onDeleteFunc: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
finalizers: []string{
|
||||
repository.RemoveOrphanResourcesFinalizer,
|
||||
repository.CleanFinalizer,
|
||||
},
|
||||
expectedErr: "remove resources",
|
||||
},
|
||||
{
|
||||
name: "Error releasing items",
|
||||
lister: func() resources.ResourceLister {
|
||||
resourceLister := resources.NewMockResourceLister(t)
|
||||
|
||||
resourceLister.
|
||||
On("List", context.Background(), "default", "my-repo").
|
||||
Once().
|
||||
Return(&provisioning.ResourceList{
|
||||
Items: []provisioning.ResourceListItem{
|
||||
{
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Name: "my-dashboard",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
return resourceLister
|
||||
}(),
|
||||
clientFactory: func() resources.ClientFactory {
|
||||
clientFactory := resources.NewMockClientFactory(t)
|
||||
clients := resources.NewMockResourceClients(t)
|
||||
client := &mockDynamicClient{
|
||||
patchFunc: func(ctx context.Context, name string, pt types.PatchType, data []byte, options metav1.PatchOptions, subresources ...string) (*unstructured.Unstructured, error) {
|
||||
return nil, assert.AnError
|
||||
},
|
||||
}
|
||||
|
||||
clientFactory.
|
||||
On("Clients", context.Background(), "default").
|
||||
Once().
|
||||
Return(clients, nil)
|
||||
|
||||
clients.
|
||||
On("ForResource", context.Background(), schema.GroupVersionResource{
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
}).
|
||||
Once().
|
||||
Return(client, schema.GroupVersionKind{}, nil)
|
||||
|
||||
return clientFactory
|
||||
}(),
|
||||
repo: mockRepo{
|
||||
name: "my-repo",
|
||||
namespace: "default",
|
||||
onDeleteFunc: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
finalizers: []string{
|
||||
repository.ReleaseOrphanResourcesFinalizer,
|
||||
repository.CleanFinalizer,
|
||||
},
|
||||
expectedErr: "release resources",
|
||||
},
|
||||
{
|
||||
name: "Error deleting hooks",
|
||||
lister: nil,
|
||||
clientFactory: nil,
|
||||
repo: mockRepo{
|
||||
name: "my-repo",
|
||||
namespace: "default",
|
||||
onDeleteFunc: func(ctx context.Context) error {
|
||||
return assert.AnError
|
||||
},
|
||||
},
|
||||
finalizers: []string{
|
||||
repository.RemoveOrphanResourcesFinalizer,
|
||||
repository.CleanFinalizer,
|
||||
},
|
||||
expectedErr: "execute deletion hooks: " + assert.AnError.Error(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
metrics := registerFinalizerMetrics(prometheus.NewRegistry())
|
||||
f := &finalizer{
|
||||
lister: tc.lister,
|
||||
clientFactory: tc.clientFactory,
|
||||
metrics: &metrics,
|
||||
}
|
||||
err := f.process(context.Background(), tc.repo, tc.finalizers)
|
||||
if tc.expectedErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortResourceListForDeletion(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user