Provisioning: Fix patching released resources when Repository is deleted (#110295)
* Provisioning: Use merge patch instead of json path to release orphan resources * rolling back to json Patch * adding TODO for testing * adding integration test * using struct * addressing comments on tests
This commit is contained in:
@@ -2,6 +2,7 @@ package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -52,12 +53,14 @@ func (f *finalizer) process(ctx context.Context,
|
||||
case ReleaseOrphanResourcesFinalizer:
|
||||
err := f.processExistingItems(ctx, repo.Config(),
|
||||
func(client dynamic.ResourceInterface, item *provisioning.ResourceListItem) error {
|
||||
_, err := client.Patch(ctx, item.Name, types.JSONPatchType, []byte(`[
|
||||
{"op": "remove", "path": "/metadata/annotations/`+utils.AnnoKeyManagerKind+`" },
|
||||
{"op": "remove", "path": "/metadata/annotations/`+utils.AnnoKeyManagerIdentity+`" },
|
||||
{"op": "remove", "path": "/metadata/annotations/`+utils.AnnoKeySourcePath+`" },
|
||||
{"op": "remove", "path": "/metadata/annotations/`+utils.AnnoKeySourceChecksum+`" }
|
||||
]`), v1.PatchOptions{})
|
||||
patchAnnotations, err := getPatchedAnnotations(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.Patch(
|
||||
ctx, item.Name, types.JSONPatchType, patchAnnotations, v1.PatchOptions{},
|
||||
)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
@@ -124,6 +127,43 @@ func (f *finalizer) processExistingItems(
|
||||
return nil
|
||||
}
|
||||
|
||||
type jsonPatchOperation struct {
|
||||
Op string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func getPatchedAnnotations(item *provisioning.ResourceListItem) ([]byte, error) {
|
||||
annotations := []jsonPatchOperation{
|
||||
{Op: "remove", Path: "/metadata/annotations/" + escapePatchString(utils.AnnoKeyManagerKind)},
|
||||
{Op: "remove", Path: "/metadata/annotations/" + escapePatchString(utils.AnnoKeyManagerIdentity)},
|
||||
}
|
||||
|
||||
if item.Path != "" {
|
||||
annotations = append(
|
||||
annotations,
|
||||
jsonPatchOperation{
|
||||
Op: "remove", Path: "/metadata/annotations/" + escapePatchString(utils.AnnoKeySourcePath),
|
||||
},
|
||||
)
|
||||
}
|
||||
if item.Hash != "" {
|
||||
annotations = append(
|
||||
annotations,
|
||||
jsonPatchOperation{
|
||||
Op: "remove", Path: "/metadata/annotations/" + escapePatchString(utils.AnnoKeySourceChecksum),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return json.Marshal(annotations)
|
||||
}
|
||||
|
||||
func escapePatchString(s string) string {
|
||||
s = strings.ReplaceAll(s, "~", "~0")
|
||||
s = strings.ReplaceAll(s, "/", "~1")
|
||||
return s
|
||||
}
|
||||
|
||||
func sortResourceListForDeletion(list *provisioning.ResourceList) {
|
||||
// FIXME: this code should be simplified once unified storage folders support recursive deletion
|
||||
// Sort by the following logic:
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
@@ -682,3 +683,78 @@ func TestIntegrationProvisioning_ImportAllPanelsFromLocalRepository(t *testing.T
|
||||
require.Error(t, err, "should delete the internal resource")
|
||||
require.True(t, apierrors.IsNotFound(err))
|
||||
}
|
||||
|
||||
func TestIntegrationProvisioning_DeleteRepositoryAndReleaseResources(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
helper := runGrafana(t)
|
||||
ctx := context.Background()
|
||||
|
||||
const repo = "gh-repo"
|
||||
testRepo := TestRepo{
|
||||
Name: repo,
|
||||
Template: "testdata/github-readonly.json.tmpl",
|
||||
Target: "folder",
|
||||
ExpectedDashboards: 3,
|
||||
ExpectedFolders: 3,
|
||||
}
|
||||
helper.CreateRepo(t, testRepo)
|
||||
|
||||
// Checking resources are there and are managed
|
||||
foundFolders, err := helper.Folders.Resource.List(ctx, metav1.ListOptions{})
|
||||
require.NoError(t, err, "can list folders")
|
||||
for _, v := range foundFolders.Items {
|
||||
assert.Contains(t, v.GetAnnotations(), utils.AnnoKeyManagerKind)
|
||||
assert.Contains(t, v.GetAnnotations(), utils.AnnoKeyManagerIdentity)
|
||||
}
|
||||
|
||||
foundDashboards, err := helper.DashboardsV1.Resource.List(ctx, metav1.ListOptions{})
|
||||
require.NoError(t, err, "can list dashboards")
|
||||
for _, v := range foundDashboards.Items {
|
||||
assert.Contains(t, v.GetAnnotations(), utils.AnnoKeyManagerKind)
|
||||
assert.Contains(t, v.GetAnnotations(), utils.AnnoKeyManagerIdentity)
|
||||
assert.Contains(t, v.GetAnnotations(), utils.AnnoKeySourcePath)
|
||||
assert.Contains(t, v.GetAnnotations(), utils.AnnoKeySourceChecksum)
|
||||
}
|
||||
|
||||
_, err = helper.Repositories.Resource.Patch(ctx, repo, types.JSONPatchType, []byte(`[
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/metadata/finalizers",
|
||||
"value": ["cleanup", "release-orphan-resources"]
|
||||
}
|
||||
]`), metav1.PatchOptions{})
|
||||
require.NoError(t, err, "should successfully patch finalizers")
|
||||
|
||||
err = helper.Repositories.Resource.Delete(ctx, repo, metav1.DeleteOptions{})
|
||||
require.NoError(t, err, "should delete repository")
|
||||
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
_, err := helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{})
|
||||
assert.True(collect, apierrors.IsNotFound(err), "repository should be deleted")
|
||||
}, time.Second*10, time.Millisecond*50, "repository should be deleted")
|
||||
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
foundDashboards, err := helper.DashboardsV1.Resource.List(ctx, metav1.ListOptions{})
|
||||
assert.NoError(t, err, "can list values")
|
||||
for _, v := range foundDashboards.Items {
|
||||
assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeyManagerKind)
|
||||
assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeyManagerIdentity)
|
||||
assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeySourcePath)
|
||||
assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeySourceChecksum)
|
||||
}
|
||||
}, time.Second*20, time.Millisecond*10, "Expected dashboards to be released")
|
||||
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
foundFolders, err := helper.Folders.Resource.List(ctx, metav1.ListOptions{})
|
||||
assert.NoError(t, err, "can list values")
|
||||
for _, v := range foundFolders.Items {
|
||||
assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeyManagerKind)
|
||||
assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeyManagerIdentity)
|
||||
assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeySourcePath)
|
||||
assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeySourceChecksum)
|
||||
}
|
||||
}, time.Second*20, time.Millisecond*10, "Expected folders to be released")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user