Skip to content

Commit 8209847

Browse files
Add org_id to dashboards (#782)
Issue: #747 Follow-up to #776 This will allow such a config: ```terraform resource "grafana_organization" "test" { name = "test" } resource "grafana_dashboard" "test" { org_id = grafana_organization.test.id config_json = jsonencode({ title = "dashboard-test" }) } ``` The main change here is that the ID of resources becomes: `<org id>:<resource id>`. This can be changed transparently because if the first block isn't there, it means the currently configured org Also added some helpers for org IDs, as these will be used in every resource soon
1 parent 80874e1 commit 8209847

File tree

7 files changed

+133
-30
lines changed

7 files changed

+133
-30
lines changed

docs/resources/dashboard.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ resource "grafana_dashboard" "metrics" {
3333

3434
- `folder` (String) The id of the folder to save the dashboard in. This attribute is a string to reflect the type of the folder's id.
3535
- `message` (String) Set a commit message for the version history.
36+
- `org_id` (Number) The Organization ID. If not set, the Org ID defined in the provider block will be used.
3637
- `overwrite` (Boolean) Set to true if you want to overwrite existing dashboard with newer version, same dashboard title in folder or same dashboard uid.
3738

3839
### Read-Only
@@ -49,5 +50,6 @@ resource "grafana_dashboard" "metrics" {
4950
Import is supported using the following syntax:
5051

5152
```shell
52-
terraform import grafana_dashboard.dashboard_name {{dashboard_uid}}
53+
terraform import grafana_dashboard.dashboard_name {{dashboard_uid}} # To use the default provider org
54+
terraform import grafana_dashboard.dashboard_name {{org_id}}:{{dashboard_uid}} # When "org_id" is set on the resource
5355
```
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
terraform import grafana_dashboard.dashboard_name {{dashboard_uid}}
1+
terraform import grafana_dashboard.dashboard_name {{dashboard_uid}} # To use the default provider org
2+
terraform import grafana_dashboard.dashboard_name {{org_id}}:{{dashboard_uid}} # When "org_id" is set on the resource

grafana/data_source_dashboard_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func TestAccDatasourceDashboardBasicID(t *testing.T) {
4141

4242
resource.ParallelTest(t, resource.TestCase{
4343
ProviderFactories: testAccProviderFactories,
44-
CheckDestroy: testAccDashboardCheckDestroy(&dashboard),
44+
CheckDestroy: testAccDashboardCheckDestroy(&dashboard, 0),
4545
Steps: []resource.TestStep{
4646
{
4747
Config: testAccExample(t, "data-sources/grafana_dashboard/data-source.tf"),

grafana/oss_org_id.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package grafana
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
8+
gapi "github.com/grafana/grafana-api-golang-client"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
)
11+
12+
func makeOSSOrgID(orgID int64, resourceID interface{}) string {
13+
return fmt.Sprintf("%d:%s", orgID, fmt.Sprint(resourceID))
14+
}
15+
16+
func splitOSSOrgID(id string) (int64, string) {
17+
if strings.ContainsRune(id, ':') {
18+
parts := strings.SplitN(id, ":", 2)
19+
orgID, _ := strconv.ParseInt(parts[0], 10, 64)
20+
return orgID, parts[1]
21+
}
22+
23+
return 0, id
24+
}
25+
26+
// nolint: unparam
27+
func clientFromOSSOrgID(meta interface{}, id string) (*gapi.Client, int64, string) {
28+
orgID, restOfID := splitOSSOrgID(id)
29+
client := meta.(*client).gapi
30+
if orgID > 0 {
31+
client = client.WithOrgID(orgID)
32+
}
33+
return client, orgID, restOfID
34+
}
35+
36+
func clientFromOrgIDAttr(meta interface{}, d *schema.ResourceData) (*gapi.Client, int64) {
37+
orgID := int64(d.Get("org_id").(int))
38+
client := meta.(*client).gapi
39+
if orgID > 0 {
40+
client = client.WithOrgID(orgID)
41+
}
42+
return client, orgID
43+
}

grafana/resource_dashboard.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ Manages Grafana dashboards.
3535
},
3636

3737
Schema: map[string]*schema.Schema{
38+
"org_id": {
39+
Type: schema.TypeInt,
40+
Optional: true,
41+
Description: "The Organization ID. If not set, the Org ID defined in the provider block will be used.",
42+
ForceNew: true,
43+
},
3844
"uid": {
3945
Type: schema.TypeString,
4046
Computed: true,
@@ -176,7 +182,8 @@ func resourceDashboardStateUpgradeV0(ctx context.Context, rawState map[string]in
176182
}
177183

178184
func CreateDashboard(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
179-
client := meta.(*client).gapi
185+
client, orgID := clientFromOrgIDAttr(meta, d)
186+
180187
dashboard, err := makeDashboard(d)
181188
if err != nil {
182189
return diag.FromErr(err)
@@ -185,15 +192,14 @@ func CreateDashboard(ctx context.Context, d *schema.ResourceData, meta interface
185192
if err != nil {
186193
return diag.FromErr(err)
187194
}
188-
d.SetId(resp.UID)
189-
d.Set("uid", resp.UID)
195+
d.SetId(makeOSSOrgID(orgID, resp.UID))
190196
return ReadDashboard(ctx, d, meta)
191197
}
192198

193199
func ReadDashboard(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
194200
gapiURL := meta.(*client).gapiURL
195-
client := meta.(*client).gapi
196-
uid := d.Id()
201+
client, _, uid := clientFromOSSOrgID(meta, d.Id())
202+
197203
dashboard, err := client.DashboardByUID(uid)
198204
var diags diag.Diagnostics
199205
if err != nil {
@@ -210,7 +216,6 @@ func ReadDashboard(ctx context.Context, d *schema.ResourceData, meta interface{}
210216
}
211217
}
212218

213-
d.SetId(dashboard.Model["uid"].(string))
214219
d.Set("uid", dashboard.Model["uid"].(string))
215220
d.Set("slug", dashboard.Meta.Slug)
216221
d.Set("dashboard_id", int64(dashboard.Model["id"].(float64)))
@@ -255,7 +260,8 @@ func ReadDashboard(ctx context.Context, d *schema.ResourceData, meta interface{}
255260
}
256261

257262
func UpdateDashboard(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
258-
client := meta.(*client).gapi
263+
client, orgID := clientFromOrgIDAttr(meta, d)
264+
259265
dashboard, err := makeDashboard(d)
260266
if err != nil {
261267
return diag.FromErr(err)
@@ -266,14 +272,13 @@ func UpdateDashboard(ctx context.Context, d *schema.ResourceData, meta interface
266272
if err != nil {
267273
return diag.FromErr(err)
268274
}
269-
d.SetId(resp.UID)
270-
d.Set("uid", resp.UID)
275+
d.SetId(makeOSSOrgID(orgID, resp.UID))
271276
return ReadDashboard(ctx, d, meta)
272277
}
273278

274279
func DeleteDashboard(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
275-
client := meta.(*client).gapi
276-
uid := d.Id()
280+
client, _, uid := clientFromOSSOrgID(meta, d.Id())
281+
277282
err := client.DeleteDashboardByUID(uid)
278283
var diags diag.Diagnostics
279284
if err != nil && !strings.HasPrefix(err.Error(), "status: 404") {

grafana/resource_dashboard_test.go

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
gapi "github.com/grafana/grafana-api-golang-client"
1010

11+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
1112
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
1213
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
1314
)
@@ -34,14 +35,14 @@ func TestAccDashboard_basic(t *testing.T) {
3435
// TODO: Make parallelizable
3536
resource.Test(t, resource.TestCase{
3637
ProviderFactories: testAccProviderFactories,
37-
CheckDestroy: testAccDashboardCheckDestroy(&dashboard),
38+
CheckDestroy: testAccDashboardCheckDestroy(&dashboard, 0),
3839
Steps: []resource.TestStep{
3940
{
4041
// Test resource creation.
4142
Config: testAccExample(t, "resources/grafana_dashboard/_acc_basic.tf"),
4243
Check: resource.ComposeTestCheckFunc(
4344
testAccDashboardCheckExists("grafana_dashboard.test", &dashboard),
44-
resource.TestCheckResourceAttr("grafana_dashboard.test", "id", "basic"),
45+
resource.TestCheckResourceAttr("grafana_dashboard.test", "id", "0:basic"), // <org id>:<uid>
4546
resource.TestCheckResourceAttr("grafana_dashboard.test", "uid", "basic"),
4647
resource.TestCheckResourceAttr("grafana_dashboard.test", "url", strings.TrimRight(os.Getenv("GRAFANA_URL"), "/")+"/d/basic/terraform-acceptance-test"),
4748
resource.TestCheckResourceAttr(
@@ -54,7 +55,7 @@ func TestAccDashboard_basic(t *testing.T) {
5455
Config: testAccExample(t, "resources/grafana_dashboard/_acc_basic_update.tf"),
5556
Check: resource.ComposeTestCheckFunc(
5657
testAccDashboardCheckExists("grafana_dashboard.test", &dashboard),
57-
resource.TestCheckResourceAttr("grafana_dashboard.test", "id", "basic"),
58+
resource.TestCheckResourceAttr("grafana_dashboard.test", "id", "0:basic"), // <org id>:<uid>
5859
resource.TestCheckResourceAttr("grafana_dashboard.test", "uid", "basic"),
5960
resource.TestCheckResourceAttr(
6061
"grafana_dashboard.test", "config_json", expectedUpdatedTitleConfig,
@@ -68,7 +69,7 @@ func TestAccDashboard_basic(t *testing.T) {
6869
Config: testAccExample(t, "resources/grafana_dashboard/_acc_basic_update_uid.tf"),
6970
Check: resource.ComposeTestCheckFunc(
7071
testAccDashboardCheckExists("grafana_dashboard.test", &dashboard),
71-
resource.TestCheckResourceAttr("grafana_dashboard.test", "id", "basic-update"),
72+
resource.TestCheckResourceAttr("grafana_dashboard.test", "id", "0:basic-update"), // <org id>:<uid>
7273
resource.TestCheckResourceAttr("grafana_dashboard.test", "uid", "basic-update"),
7374
resource.TestCheckResourceAttr("grafana_dashboard.test", "url", strings.TrimRight(os.Getenv("GRAFANA_URL"), "/")+"/d/basic-update/updated-title"),
7475
resource.TestCheckResourceAttr(
@@ -96,7 +97,7 @@ func TestAccDashboard_uid_unset(t *testing.T) {
9697

9798
resource.ParallelTest(t, resource.TestCase{
9899
ProviderFactories: testAccProviderFactories,
99-
CheckDestroy: testAccDashboardCheckDestroy(&dashboard),
100+
CheckDestroy: testAccDashboardCheckDestroy(&dashboard, 0),
100101
Steps: []resource.TestStep{
101102
{
102103
// Create dashboard with no uid set.
@@ -140,7 +141,7 @@ func TestAccDashboard_computed_config(t *testing.T) {
140141

141142
resource.ParallelTest(t, resource.TestCase{
142143
ProviderFactories: testAccProviderFactories,
143-
CheckDestroy: testAccDashboardCheckDestroy(&dashboard),
144+
CheckDestroy: testAccDashboardCheckDestroy(&dashboard, 0),
144145
Steps: []resource.TestStep{
145146
{
146147
// Test resource creation.
@@ -170,7 +171,7 @@ func TestAccDashboard_folder(t *testing.T) {
170171
testAccDashboardCheckExists("grafana_dashboard.test_folder", &dashboard),
171172
testAccFolderCheckExists("grafana_folder.test_folder", &folder),
172173
testAccDashboardCheckExistsInFolder(&dashboard, &folder),
173-
resource.TestCheckResourceAttr("grafana_dashboard.test_folder", "id", "folder"),
174+
resource.TestCheckResourceAttr("grafana_dashboard.test_folder", "id", "0:folder"), // <org id>:<uid>
174175
resource.TestCheckResourceAttr("grafana_dashboard.test_folder", "uid", "folder"),
175176
resource.TestMatchResourceAttr(
176177
"grafana_dashboard.test_folder", "folder", idRegexp,
@@ -181,6 +182,40 @@ func TestAccDashboard_folder(t *testing.T) {
181182
})
182183
}
183184

185+
func TestAccDashboard_inOrg(t *testing.T) {
186+
CheckOSSTestsEnabled(t)
187+
188+
var dashboard gapi.Dashboard
189+
var org gapi.Org
190+
191+
orgName := acctest.RandString(10)
192+
193+
resource.ParallelTest(t, resource.TestCase{
194+
ProviderFactories: testAccProviderFactories,
195+
CheckDestroy: testAccDashboardCheckDestroy(&dashboard, org.ID),
196+
Steps: []resource.TestStep{
197+
{
198+
Config: testAccDashboardInOrganization(orgName),
199+
Check: resource.ComposeTestCheckFunc(
200+
testAccOrganizationCheckExists("grafana_organization.test", &org),
201+
testAccDashboardCheckExists("grafana_dashboard.test", &dashboard),
202+
resource.TestCheckResourceAttr("grafana_dashboard.test", "uid", "dashboard-"+orgName),
203+
// Check that the dashboard is not in the default org, from its ID
204+
func(s *terraform.State) error {
205+
expectedOrgID := org.ID
206+
if org.ID <= 1 {
207+
return fmt.Errorf("expected org ID higher than 1, got %d", org.ID)
208+
}
209+
expectedDashboardID := fmt.Sprintf("%d:dashboard-%s", expectedOrgID, orgName) // <org id>:<uid>
210+
checkFunc := resource.TestCheckResourceAttr("grafana_dashboard.test", "id", expectedDashboardID)
211+
return checkFunc(s)
212+
},
213+
),
214+
},
215+
},
216+
})
217+
}
218+
184219
func testAccDashboardCheckExists(rn string, dashboard *gapi.Dashboard) resource.TestCheckFunc {
185220
return func(s *terraform.State) error {
186221
rs, ok := s.RootModule().Resources[rn]
@@ -190,8 +225,9 @@ func testAccDashboardCheckExists(rn string, dashboard *gapi.Dashboard) resource.
190225
if rs.Primary.ID == "" {
191226
return fmt.Errorf("resource id not set")
192227
}
193-
client := testAccProvider.Meta().(*client).gapi
194-
gotDashboard, err := client.DashboardByUID(rs.Primary.ID)
228+
229+
client, _, dashboardID := clientFromOSSOrgID(testAccProvider.Meta(), rs.Primary.ID)
230+
gotDashboard, err := client.DashboardByUID(dashboardID)
195231
if err != nil {
196232
return fmt.Errorf("error getting dashboard: %s", err)
197233
}
@@ -209,9 +245,13 @@ func testAccDashboardCheckExistsInFolder(dashboard *gapi.Dashboard, folder *gapi
209245
}
210246
}
211247

212-
func testAccDashboardCheckDestroy(dashboard *gapi.Dashboard) resource.TestCheckFunc {
248+
func testAccDashboardCheckDestroy(dashboard *gapi.Dashboard, orgID int64) resource.TestCheckFunc {
213249
return func(s *terraform.State) error {
214250
client := testAccProvider.Meta().(*client).gapi
251+
if orgID != 0 {
252+
client = client.WithOrgID(orgID)
253+
}
254+
215255
_, err := client.DashboardByUID(dashboard.Model["uid"].(string))
216256
if err == nil {
217257
return fmt.Errorf("dashboard still exists")
@@ -294,3 +334,18 @@ func Test_normalizeDashboardConfigJSON(t *testing.T) {
294334
})
295335
}
296336
}
337+
338+
func testAccDashboardInOrganization(orgName string) string {
339+
return fmt.Sprintf(`
340+
resource "grafana_organization" "test" {
341+
name = "%[1]s"
342+
}
343+
344+
resource "grafana_dashboard" "test" {
345+
org_id = grafana_organization.test.id
346+
config_json = jsonencode({
347+
title = "dashboard-%[1]s"
348+
uid = "dashboard-%[1]s"
349+
})
350+
}`, orgName)
351+
}

grafana/resource_organization_preferences.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func ResourceOrganizationPreferences() *schema.Resource {
3131
Type: schema.TypeInt,
3232
Optional: true,
3333
Description: "The Organization ID. If not set, the Org ID defined in the provider block will be used.",
34+
ForceNew: true,
3435
},
3536
"theme": {
3637
Type: schema.TypeString,
@@ -65,11 +66,7 @@ func ResourceOrganizationPreferences() *schema.Resource {
6566
}
6667

6768
func CreateOrganizationPreferences(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
68-
client := meta.(*client).gapi
69-
id := d.Get("org_id").(int)
70-
if id > 0 {
71-
client = client.WithOrgID(int64(id))
72-
}
69+
client, orgID := clientFromOrgIDAttr(meta, d)
7370

7471
_, err := client.UpdateAllOrgPreferences(gapi.Preferences{
7572
Theme: d.Get("theme").(string),
@@ -82,7 +79,7 @@ func CreateOrganizationPreferences(ctx context.Context, d *schema.ResourceData,
8279
return diag.FromErr(err)
8380
}
8481

85-
d.SetId(strconv.Itoa(id))
82+
d.SetId(strconv.FormatInt(orgID, 10))
8683

8784
return ReadOrganizationPreferences(ctx, d, meta)
8885
}

0 commit comments

Comments
 (0)