diff --git a/pkg/registry/apis/provisioning/controller/finalizers.go b/pkg/registry/apis/provisioning/controller/finalizers.go index 487e6d4d062..a3eb35376fb 100644 --- a/pkg/registry/apis/provisioning/controller/finalizers.go +++ b/pkg/registry/apis/provisioning/controller/finalizers.go @@ -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: diff --git a/pkg/tests/apis/provisioning/repository_test.go b/pkg/tests/apis/provisioning/repository_test.go index 7aee8dc5c73..36d60f9b353 100644 --- a/pkg/tests/apis/provisioning/repository_test.go +++ b/pkg/tests/apis/provisioning/repository_test.go @@ -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") +}