Skip to content

Commit 3f91662

Browse files
authored
Add optional uid parameter to grafana_folder data source (#2301)
* Make title optional and add uid parameter for more flexible folder lookup. * Support finding folders by title only, uid only, or both title and uid * Add comprehensive error handling with specific error messages for different lookup scenarios * Add extensive test coverage for all lookup combinations and error cases * Update docs.
1 parent 9b73f04 commit 3f91662

File tree

3 files changed

+202
-13
lines changed

3 files changed

+202
-13
lines changed

docs/data-sources/folder.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,14 @@ data "grafana_folder" "from_title" {
2727
<!-- schema generated by tfplugindocs -->
2828
## Schema
2929

30-
### Required
31-
32-
- `title` (String) The title of the folder.
33-
3430
### Optional
3531

3632
- `org_id` (String) The Organization ID. If not set, the Org ID defined in the provider block will be used.
33+
- `title` (String) The title of the folder. If not set, only the uid is used to find the folder.
34+
- `uid` (String) The uid of the folder. If not set, only the title of the folder is used to find the folder.
3735

3836
### Read-Only
3937

4038
- `id` (String) The ID of this resource.
4139
- `parent_folder_uid` (String) The uid of the parent folder. If set, the folder will be nested. If not set, the folder will be created in the root folder. Note: This requires the nestedFolders feature flag to be enabled on your Grafana instance.
42-
- `uid` (String) Unique identifier.
4340
- `url` (String) The full URL of the folder.

internal/resources/grafana/data_source_folder.go

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ package grafana
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67

78
goapi "github.com/grafana/grafana-openapi-client-go/client"
89
"github.com/grafana/grafana-openapi-client-go/client/search"
9-
"github.com/grafana/terraform-provider-grafana/v4/internal/common"
1010
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1111
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
12+
13+
"github.com/grafana/terraform-provider-grafana/v4/internal/common"
1214
)
1315

1416
func datasourceFolder() *common.DataSource {
@@ -22,16 +24,33 @@ func datasourceFolder() *common.DataSource {
2224
"org_id": orgIDAttribute(),
2325
"title": {
2426
Type: schema.TypeString,
25-
Required: true,
26-
Description: "The title of the folder.",
27+
Optional: true,
28+
Description: "The title of the folder. If not set, only the uid is used to find the folder.",
29+
},
30+
"uid": {
31+
Type: schema.TypeString,
32+
Optional: true,
33+
Description: "The uid of the folder. If not set, only the title of the folder is used to find the folder.",
2734
},
2835
"prevent_destroy_if_not_empty": nil,
2936
}),
3037
}
3138
return common.NewLegacySDKDataSource(common.CategoryGrafanaOSS, "grafana_folder", schema)
3239
}
3340

34-
func findFolderWithTitle(client *goapi.GrafanaHTTPAPI, title string) (string, error) {
41+
// The following consts are only exported for usage in tests
42+
const (
43+
FolderTitleOrUIDMissing = "either title or uid must be set"
44+
FolderWithTitleNotFound = "folder with title %s not found"
45+
FolderWithUIDNotFound = "folder with uid %s not found"
46+
FolderWithTitleAndUIDNotFound = "folder with title %s and uid %s not found"
47+
)
48+
49+
func findFolderWithTitleAndUID(client *goapi.GrafanaHTTPAPI, title string, uid string) (string, error) {
50+
if title == "" && uid == "" {
51+
return "", errors.New(FolderTitleOrUIDMissing)
52+
}
53+
3554
var page int64 = 1
3655

3756
for {
@@ -42,11 +61,19 @@ func findFolderWithTitle(client *goapi.GrafanaHTTPAPI, title string) (string, er
4261
}
4362

4463
if len(resp.Payload) == 0 {
45-
return "", fmt.Errorf("folder with title %s not found", title)
64+
switch {
65+
case title != "" && uid == "":
66+
err = fmt.Errorf(FolderWithTitleNotFound, title)
67+
case title == "" && uid != "":
68+
err = fmt.Errorf(FolderWithUIDNotFound, uid)
69+
case title != "" && uid != "":
70+
err = fmt.Errorf(FolderWithTitleAndUIDNotFound, title, uid)
71+
}
72+
return "", err
4673
}
4774

4875
for _, folder := range resp.Payload {
49-
if folder.Title == title {
76+
if (title == "" || folder.Title == title) && (uid == "" || folder.UID == uid) {
5077
return folder.UID, nil
5178
}
5279
}
@@ -57,7 +84,7 @@ func findFolderWithTitle(client *goapi.GrafanaHTTPAPI, title string) (string, er
5784

5885
func dataSourceFolderRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
5986
client, orgID := OAPIClientFromNewOrgResource(meta, d)
60-
uid, err := findFolderWithTitle(client, d.Get("title").(string))
87+
uid, err := findFolderWithTitleAndUID(client, d.Get("title").(string), d.Get("uid").(string))
6188
if err != nil {
6289
return diag.FromErr(err)
6390
}

internal/resources/grafana/data_source_folder_test.go

Lines changed: 166 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ package grafana_test
33
import (
44
"fmt"
55
"os"
6+
"regexp"
67
"strings"
78
"testing"
89

910
"github.com/grafana/grafana-openapi-client-go/models"
10-
"github.com/grafana/terraform-provider-grafana/v4/internal/testutils"
1111
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
1212
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
13+
14+
"github.com/grafana/terraform-provider-grafana/v4/internal/resources/grafana"
15+
"github.com/grafana/terraform-provider-grafana/v4/internal/testutils"
1316
)
1417

1518
func TestAccDatasourceFolder_basic(t *testing.T) {
@@ -92,3 +95,165 @@ data "grafana_folder" "child" {
9295
}
9396
`, name)
9497
}
98+
99+
func TestAccDatasourceFolderByTitleAndUid(t *testing.T) {
100+
// This test uses duplicate folder names, a feature that was introduced in Grafana 11.4: https://github.com/grafana/grafana/pull/90687
101+
testutils.CheckOSSTestsEnabled(t, ">=11.4.0")
102+
103+
var folder1 models.Folder
104+
var folder2 models.Folder
105+
var folder3 models.Folder
106+
randomName := acctest.RandStringFromCharSet(6, acctest.CharSetAlpha)
107+
108+
resource.ParallelTest(t, resource.TestCase{
109+
ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
110+
CheckDestroy: resource.ComposeTestCheckFunc(
111+
folderCheckExists.destroyed(&folder1, nil),
112+
folderCheckExists.destroyed(&folder2, nil),
113+
folderCheckExists.destroyed(&folder3, nil),
114+
),
115+
Steps: []resource.TestStep{
116+
{
117+
Config: folderResources(randomName),
118+
Check: resource.ComposeTestCheckFunc(
119+
folderCheckExists.exists("grafana_folder.folder1", &folder1),
120+
folderCheckExists.exists("grafana_folder.folder2", &folder2),
121+
folderCheckExists.exists("grafana_folder.folder3", &folder3),
122+
),
123+
},
124+
{
125+
Config: folderResources(randomName) + folderData(randomName),
126+
Check: resource.ComposeTestCheckFunc(
127+
resource.TestCheckResourceAttr("data.grafana_folder.f1", "title", randomName),
128+
// f1 must be one of our folders, but we cannot guarantee which one.
129+
resource.TestMatchResourceAttr("data.grafana_folder.f1", "uid", regexp.MustCompile(regexp.QuoteMeta(randomName)+"[123]")),
130+
131+
resource.TestCheckResourceAttr("data.grafana_folder.f2", "title", randomName),
132+
resource.TestCheckResourceAttr("data.grafana_folder.f2", "uid", randomName+"2"),
133+
134+
resource.TestCheckResourceAttr("data.grafana_folder.f3", "title", randomName),
135+
resource.TestCheckResourceAttr("data.grafana_folder.f3", "uid", randomName+"3"),
136+
),
137+
},
138+
{
139+
Config: folderResources(randomName) + fmt.Sprintf(`
140+
data "grafana_folder" "unknown" {
141+
title = "unknown %[1]s"
142+
}
143+
`, randomName),
144+
ExpectError: regexp.MustCompile(regexp.QuoteMeta(fmt.Sprintf(grafana.FolderWithTitleNotFound, "unknown "+randomName))),
145+
},
146+
{
147+
Config: folderResources(randomName) + fmt.Sprintf(`
148+
data "grafana_folder" "unknown" {
149+
title = "unknown %[1]s"
150+
}
151+
`, randomName),
152+
ExpectError: regexp.MustCompile(regexp.QuoteMeta(fmt.Sprintf(grafana.FolderWithTitleNotFound, "unknown "+randomName))),
153+
},
154+
{
155+
Config: folderResources(randomName) + fmt.Sprintf(`
156+
data "grafana_folder" "unknown" {
157+
title = "unknown %[1]s"
158+
}
159+
`, randomName),
160+
ExpectError: regexp.MustCompile(regexp.QuoteMeta(fmt.Sprintf(grafana.FolderWithTitleNotFound, "unknown "+randomName))),
161+
},
162+
{
163+
Config: folderResources(randomName) + fmt.Sprintf(`
164+
data "grafana_folder" "unknown" {
165+
title = "unknown %[1]s"
166+
}
167+
`, randomName),
168+
ExpectError: regexp.MustCompile(regexp.QuoteMeta(fmt.Sprintf(grafana.FolderWithTitleNotFound, "unknown "+randomName))),
169+
},
170+
// Don't find the folder if neither title or uid is provided.
171+
{
172+
Config: folderResources(randomName) + `
173+
data "grafana_folder" "unknown" {
174+
}
175+
`,
176+
ExpectError: regexp.MustCompile(regexp.QuoteMeta(grafana.FolderTitleOrUIDMissing)),
177+
},
178+
// Don't find the folder if title is wrong.
179+
{
180+
Config: folderResources(randomName) + fmt.Sprintf(`
181+
data "grafana_folder" "unknown" {
182+
title = "unknown %[1]s"
183+
}
184+
`, randomName),
185+
ExpectError: regexp.MustCompile(regexp.QuoteMeta(fmt.Sprintf(grafana.FolderWithTitleNotFound, "unknown "+randomName))),
186+
},
187+
// Don't find the folder if uid is wrong.
188+
{
189+
Config: folderResources(randomName) + fmt.Sprintf(`
190+
data "grafana_folder" "unknown" {
191+
uid = "%[1]s9"
192+
}
193+
`, randomName),
194+
ExpectError: regexp.MustCompile(regexp.QuoteMeta(fmt.Sprintf(grafana.FolderWithUIDNotFound, randomName+"9"))),
195+
},
196+
// Don't find the folder if the title is wrong, even if uid matches.
197+
{
198+
Config: folderResources(randomName) + fmt.Sprintf(`
199+
data "grafana_folder" "unknown" {
200+
title = "unknown %[1]s"
201+
uid = "%[1]s1"
202+
}
203+
`, randomName),
204+
ExpectError: regexp.MustCompile(regexp.QuoteMeta(fmt.Sprintf(grafana.FolderWithTitleAndUIDNotFound, "unknown "+randomName, randomName+"1"))),
205+
},
206+
// Don't find the folder if uid is wrong, even if the title matches.
207+
{
208+
Config: folderResources(randomName) + fmt.Sprintf(`
209+
data "grafana_folder" "unknown" {
210+
title = "%[1]s"
211+
uid = "%[1]s9"
212+
}
213+
`, randomName),
214+
ExpectError: regexp.MustCompile(regexp.QuoteMeta(fmt.Sprintf(grafana.FolderWithTitleAndUIDNotFound, randomName, randomName+"9"))),
215+
},
216+
},
217+
})
218+
}
219+
220+
// Creates three folders with the same title, but different UIDs
221+
func folderResources(name string) string {
222+
return fmt.Sprintf(`
223+
resource "grafana_folder" "folder1" {
224+
title = "%[1]s"
225+
uid = "%[1]s1"
226+
}
227+
228+
resource "grafana_folder" "folder2" {
229+
title = "%[1]s"
230+
uid = "%[1]s2"
231+
}
232+
233+
resource "grafana_folder" "folder3" {
234+
title = "%[1]s"
235+
uid = "%[1]s3"
236+
}
237+
`, name)
238+
}
239+
240+
// Creates data sources that find folders by title and/or uid.
241+
func folderData(name string) string {
242+
return fmt.Sprintf(`
243+
# Find folder by title only -- random folder
244+
data "grafana_folder" "f1" {
245+
title = "%[1]s"
246+
}
247+
248+
# Find folder by uid only -- matches second folder
249+
data "grafana_folder" "f2" {
250+
uid = "%[1]s2"
251+
}
252+
253+
# Find folder by title and uid -- matches third folder
254+
data "grafana_folder" "f3" {
255+
title = "%[1]s"
256+
uid = "%[1]s3"
257+
}
258+
`, name)
259+
}

0 commit comments

Comments
 (0)