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:
Daniele Stefano Ferru
2025-09-02 21:13:43 +02:00
committed by GitHub
parent d97836f407
commit 451d6abe15
2 changed files with 122 additions and 6 deletions
@@ -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")
}