New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NestedFolders: Fix nested folder deletion #63572
Conversation
ec85cec
to
41fb115
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have tested and works as expected! Great work 🎉
I have left some minor suggestions.
How to better handle the corner case - deleting a parent folder (containing subfolders) with nested folders disabled - described by @suntala still an open question though. I'm tempted to suggest a common approach regardless of the feature flag as long as we discover nested folders.
pkg/kindsys/report.json
Outdated
@@ -2008,7 +2008,7 @@ | |||
"zipkindataquery", | |||
"zipkindatasourcecfg" | |||
], | |||
"count": 66 | |||
"count": 67 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this change seems irrelevant; do we actually need it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of the pipeline steps was failing, the one called verify-gen-cue
. There were instructions to run make gen-cue
(if I remember correctly) and add the resulting changes to source control. I see that, in the meantime, someone else added the change. I'm going to go ahead and rebase on main to eliminate this change.
In the future, I think a better way of handling it would have been for me to create a separate PR for this issue and rebase this PR on top of it. Happy to hear if you have another suggestion though.
_, err := serviceWithFlagOn.getFolderByUID(context.Background(), orgID, uid) | ||
require.ErrorIs(t, err, dashboards.ErrFolderNotFound) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this assumes prior knowledge of the folder service getFolderByUID()
internal implementation (that it searches in the dashboard store); it would be preferable to call the serviceWithFlagOn.dashboardFolderStore.GetFolderByUID()
or directly folderStore.GetFolderByUID()
(which is passed to the service)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do require that the dashboard store be searched and not some other store. To make sure I understand you fully, it's not the prior knowledge of which store is being searched that is of concern here. Rather it is the knowledge of the internal implementation. Is that correct? When using serviceWithFlagOn.dashboardFolderStore.GetFolderByUID()
, we don't require internal knowledge of which store is being searched because we know that dashboardFolderStore
is dedicated to the dashboard store.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, here we want to test if there is no entry in the dashboard
table but it requires knowledge of the folder service getFolderByUID()
internal implementation to know that it checks the specific store and raise the specific error if no entry is not found there.
@@ -508,10 +520,11 @@ func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*fol | |||
}) | |||
} | |||
|
|||
func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFolderCommand) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit; it would be nice to have a doc string describing what the function returns
Hi @papagian, thanks a lot for the review. All your comments make sense to me and I learned some things along the way. I have made the suggested changes.
This does seem like the way to go for me as well. I propose tackling it as a separate PR to not make this one bigger. WDYT? |
7156f4c
to
9a023c3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have some additional - minor - suggestions
func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFolderCommand) error { | ||
// nestedFolderDelete inspects the folder referenced by the cmd argument, deletes all the entries for | ||
// its descendant folders (folders which are nested within it either directly or indirectly) from | ||
// the folder store and returns the UIDs for all its descendants. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit
// the folder store and returns the UIDs for all its descendants. | |
// the folder store and returns the UIDs for all its direct descendants. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I can tell, because nestedFolderDelete
is recursive, it returns the UIDs of all its descendants, both direct and indirect. We're going progressively deeper into the nested folders and appending the UIDs of all the subfolders we find:
grafana/pkg/services/folder/folderimpl/folder.go
Lines 546 to 554 in 9a023c3
for _, f := range folders { | |
result = append(result, f.UID) | |
logger.Info("deleting subfolder", "org_id", f.OrgID, "uid", f.UID) | |
subfolders, err := s.nestedFolderDelete(ctx, &folder.DeleteFolderCommand{UID: f.UID, OrgID: f.OrgID, ForceDeleteRules: cmd.ForceDeleteRules, SignedInUser: cmd.SignedInUser}) | |
if err != nil { | |
logger.Error("failed deleting subfolder", "org_id", f.OrgID, "uid", f.UID, "error", err) | |
return result, err | |
} | |
result = append(result, subfolders...) |
Please let me know if I'm missing something though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you are right, I had in mind only the first iteration
@@ -183,6 +196,113 @@ func TestIntegrationDelete(t *testing.T) { | |||
}) | |||
} | |||
|
|||
func TestIntegrationDeleteNestedFolders(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file contains only tests for the nested folder store. Therefore I would suggest to move this to the folder service tests (since we test the folder service Delete() behaviour).
@@ -632,30 +756,35 @@ func CreateOrg(t *testing.T, db *sqlstore.SQLStore) int64 { | |||
return orgID | |||
} | |||
|
|||
func CreateSubTree(t *testing.T, store *sqlStore, orgID int64, parentUID string, depth int, prefix string) []string { | |||
func CreateSubTree(t *testing.T, store *sqlStore, service *Service, depth int, prefix string, cmd folder.CreateFolderCommand) []string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function has become over-complicated: I would suggest leave this one as as is and introduce a similar that uses the service instead (or the dashboard folder store).
33258a6
to
ee574dd
Compare
Update: we're going to hold off on merging this for one week (source). |
@suntala I talked to the User Essentials squad about this and we are all of the mind that we shouldn't do much work to support cases that will come up when users toggle nested folders on/off. Instead, we'll shift our efforts to documenting a recommendation for users to take a backup before playing around with the toggle and document that they might end up with orphaned items that are hidden from the UI if they turn the toggle off after created a nested structure. |
Co-authored-by: ying-jeanne <ying-jeanne@users.noreply.github.com> Co-authored-by: jeanne0731 <jeanne0731@users.noreply.github.com>
ee574dd
to
ede16ae
Compare
What is this feature?
It's a fix for a bug relating to the deletion of nested folders as described in the linked issue.
Which issue(s) does this PR fix?:
Fixes #63080
Special notes for your reviewer:
I (@suntala) still need to try out the fix by reproducing the issue, but hopefully the new integration test (TestIntegrationDeleteNestedFolders) makes it clear that the fix works. This integration test has also been intended as a replacement for two test cases (1, 2).
Additionally, I'd be curious to know if anyone has suggestions for how to better test deletion when the feature flag is off (old way, new way). The new approach is to create a folder with the feature flag on so that it gets added to both the
dashboard
table and thefolder
one. Then we proceed with deletion and check that the entry in thefolder
table hasn't been removed.When coming up with this approach, a corner case for this bug became apparent. Someone could theoretically turn the nested folder feature flag on, create some nested folders, turn the flag off and then delete the parent folder. This deletion wouldn't be complete. It would leave behind entries in the folder table. A fix for this would be to modify the legacyDelete method to check the folder table for nested folders when performing a deletion. I didn't implement this because hopefully people wouldn't use the nested folder flag in this way. However, please let me know if you think it would be valuable to do so.
I suggest not going through the commits individually because they aren't well structured and there was a change of approach halfway through.