Skip to content

Commit 1304d6a

Browse files
Folder: Add option to prevent deletion if not empty (#897)
* Folder: Add option to prevent deletion if not empty Closes #855 This will allow Terraform to take on folders that potentially have manually-created dashboards in them, without the risk of destroying them without warning Also, in this condition we can handle the case of alert rules * Generate docs * Ignore import verify for the `prevent_destroy_if_not_empty` attribute * Use folder ID
1 parent dddd1a8 commit 1304d6a

File tree

5 files changed

+109
-10
lines changed

5 files changed

+109
-10
lines changed

docs/resources/folder.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ resource "grafana_folder" "test_folder_with_uid" {
4343

4444
### Optional
4545

46+
- `prevent_destroy_if_not_empty` (Boolean) Prevent deletion of the folder if it is not empty (contains dashboards or alert rules). Defaults to `false`.
4647
- `uid` (String) Unique identifier.
4748

4849
### Read-Only

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.18
55
require (
66
github.com/Masterminds/semver/v3 v3.2.1
77
github.com/grafana/amixr-api-go-client v0.0.7
8-
github.com/grafana/grafana-api-golang-client v0.19.0
8+
github.com/grafana/grafana-api-golang-client v0.19.1
99
github.com/grafana/machine-learning-go-client v0.5.0
1010
github.com/grafana/synthetic-monitoring-agent v0.14.4
1111
github.com/grafana/synthetic-monitoring-api-go-client v0.7.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
7272
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
7373
github.com/grafana/amixr-api-go-client v0.0.7 h1:U6W6yKxMMybI+Qz4zl+Vih48o6CczLaU/vjk2m7omvU=
7474
github.com/grafana/amixr-api-go-client v0.0.7/go.mod h1:N6x26XUrM5zGtK5zL5vNJnAn2JFMxLFPPLTw/6pDkFE=
75-
github.com/grafana/grafana-api-golang-client v0.19.0 h1:4z8voB2nv/bMiP4WbCKdhCym2mltSDFUzOrDqzohrw0=
76-
github.com/grafana/grafana-api-golang-client v0.19.0/go.mod h1:24W29gPe9yl0/3A9X624TPkAOR8DpHno490cPwnkv8E=
75+
github.com/grafana/grafana-api-golang-client v0.19.1 h1:eKekLtjX+dSYcxJIsMpz0HkTk2QxUkXf1pyLtZCuxNk=
76+
github.com/grafana/grafana-api-golang-client v0.19.1/go.mod h1:24W29gPe9yl0/3A9X624TPkAOR8DpHno490cPwnkv8E=
7777
github.com/grafana/machine-learning-go-client v0.5.0 h1:Q1K+MPSy8vfMm2jsk3WQ7O77cGr2fM5hxwtPSoPc5NU=
7878
github.com/grafana/machine-learning-go-client v0.5.0/go.mod h1:QFfZz8NkqVF8++skjkKQXJEZfpCYd8S0yTWJUpsLLTA=
7979
github.com/grafana/synthetic-monitoring-agent v0.14.4 h1:amLwPpBvWnqoYHg4Dn2IBdkDy9szcRLr7yCJHMXNhG8=

internal/resources/grafana/resource_folder.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"log"
9+
"net/url"
910
"strconv"
1011
"strings"
1112

@@ -67,6 +68,12 @@ func ResourceFolder() *schema.Resource {
6768
Computed: true,
6869
Description: "The full URL of the folder.",
6970
},
71+
"prevent_destroy_if_not_empty": {
72+
Type: schema.TypeBool,
73+
Optional: true,
74+
Default: false,
75+
Description: "Prevent deletion of the folder if it is not empty (contains dashboards or alert rules).",
76+
},
7077
},
7178
}
7279
}
@@ -138,7 +145,29 @@ func ReadFolder(ctx context.Context, d *schema.ResourceData, meta interface{}) d
138145
func DeleteFolder(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
139146
client := meta.(*common.Client).GrafanaAPI
140147

141-
if err := client.DeleteFolder(d.Get("uid").(string)); err != nil {
148+
deleteParams := []url.Values{}
149+
if d.Get("prevent_destroy_if_not_empty").(bool) {
150+
// Search for dashboards and fail if any are found
151+
dashboards, err := client.FolderDashboardSearch(url.Values{
152+
"type": []string{"dash-db"},
153+
"folderIds": []string{d.Id()},
154+
})
155+
if err != nil {
156+
return diag.FromErr(err)
157+
}
158+
if len(dashboards) > 0 {
159+
var dashboardNames []string
160+
for _, dashboard := range dashboards {
161+
dashboardNames = append(dashboardNames, dashboard.Title)
162+
}
163+
return diag.Errorf("folder %s is not empty and prevent_destroy_if_not_empty is set. It contains the following dashboards: %v", d.Get("uid").(string), dashboardNames)
164+
}
165+
} else {
166+
// If we're not preventing destroys, then we can force delete folders that have alert rules
167+
deleteParams = append(deleteParams, gapi.ForceDeleteFolderRules())
168+
}
169+
170+
if err := client.DeleteFolder(d.Get("uid").(string), deleteParams...); err != nil {
142171
return diag.FromErr(err)
143172
}
144173

internal/resources/grafana/resource_folder_test.go

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,16 @@ func TestAccFolder_basic(t *testing.T) {
4747
),
4848
},
4949
{
50-
ResourceName: "grafana_folder.test_folder",
51-
ImportState: true,
52-
ImportStateVerify: true,
50+
ResourceName: "grafana_folder.test_folder",
51+
ImportState: true,
52+
ImportStateVerify: true,
53+
ImportStateVerifyIgnore: []string{"prevent_destroy_if_not_empty"},
5354
},
5455
{
55-
ResourceName: "grafana_folder.test_folder_with_uid",
56-
ImportState: true,
57-
ImportStateVerify: true,
56+
ResourceName: "grafana_folder.test_folder_with_uid",
57+
ImportState: true,
58+
ImportStateVerify: true,
59+
ImportStateVerifyIgnore: []string{"prevent_destroy_if_not_empty"},
5860
},
5961
// Change the title of one folder, change the UID of the other. They shouldn't change IDs (the folder doesn't have to be recreated)
6062
{
@@ -99,6 +101,58 @@ func TestAccFolder_basic(t *testing.T) {
99101
})
100102
}
101103

104+
func TestAccFolder_PreventDeletion(t *testing.T) {
105+
testutils.CheckOSSTestsEnabled(t)
106+
107+
name := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)
108+
var folder gapi.Folder
109+
110+
resource.ParallelTest(t, resource.TestCase{
111+
ProviderFactories: testutils.ProviderFactories,
112+
Steps: []resource.TestStep{
113+
{
114+
Config: testAccFolderExample_PreventDeletion(name, true), // Create protected folder
115+
},
116+
{
117+
Config: testAccFolderExample_PreventDeletion(name, true), // Create protected folder
118+
Destroy: true,
119+
},
120+
{
121+
Config: testAccFolderExample_PreventDeletion(name, true), // Create protected folder again
122+
Check: resource.ComposeTestCheckFunc(
123+
testAccFolderCheckExists("grafana_folder.test_folder", &folder),
124+
// Create a dashboard in the protected folder
125+
func(s *terraform.State) error {
126+
client := testutils.Provider.Meta().(*common.Client).GrafanaAPI
127+
_, err := client.NewDashboard(gapi.Dashboard{
128+
FolderUID: folder.UID,
129+
FolderID: folder.ID,
130+
Model: map[string]interface{}{
131+
"uid": name + "-dashboard",
132+
"title": name + "-dashboard",
133+
}})
134+
return err
135+
},
136+
),
137+
},
138+
{
139+
Config: testAccFolderExample_PreventDeletion(name, true),
140+
Destroy: true, // Try to delete the protected folder
141+
ExpectError: regexp.MustCompile(
142+
fmt.Sprintf(`.+folder %s is not empty and prevent_destroy_if_not_empty is set.+`, name),
143+
), // Fail because it's protected
144+
},
145+
{
146+
Config: testAccFolderExample_PreventDeletion(name, false), // Remove protected flag
147+
},
148+
{
149+
Config: testAccFolderExample_PreventDeletion(name, false),
150+
Destroy: true, // No error if the folder is not protected
151+
},
152+
},
153+
})
154+
}
155+
102156
// This is a bug in Grafana, not the provider. It was fixed in 9.2.7+ and 9.3.0+, this test will check for regressions
103157
func TestAccFolder_createFromDifferentRoles(t *testing.T) {
104158
testutils.CheckOSSTestsEnabled(t)
@@ -218,3 +272,18 @@ func testAccFolderCheckDestroy(folder *gapi.Folder) resource.TestCheckFunc {
218272
return nil
219273
}
220274
}
275+
276+
func testAccFolderExample_PreventDeletion(name string, preventDeletion bool) string {
277+
preventDeletionStr := ""
278+
if preventDeletion {
279+
preventDeletionStr = "prevent_destroy_if_not_empty = true"
280+
}
281+
282+
return fmt.Sprintf(`
283+
resource "grafana_folder" "test_folder" {
284+
uid = "%[1]s"
285+
title = "%[1]s"
286+
%[2]s
287+
}
288+
`, name, preventDeletionStr)
289+
}

0 commit comments

Comments
 (0)