Skip to content

Commit 25aeda6

Browse files
dprokopclaude
andcommitted
Dashboard: Add v2 AppPlatform resource
Add grafana_apps_dashboard_dashboard_v2 resource for the stable v2 Dashboard API (grafana/grafana#118646), mirroring the existing v2beta1 resource pattern with JSON spec + optional title/tags overrides. Requires Grafana >=13.0.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c31319e commit 25aeda6

File tree

7 files changed

+380
-8
lines changed

7 files changed

+380
-8
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "grafana_apps_dashboard_dashboard_v2 Resource - terraform-provider-grafana"
4+
subcategory: "Grafana Apps"
5+
description: |-
6+
Manages Grafana dashboards using the v2 (Dynamic Dashboards) schema.
7+
Official documentation https://grafana.com/docs/grafana/latest/dashboards/HTTP API https://grafana.com/docs/grafana/latest/developers/http_api/dashboard/#new-dashboard-apis
8+
---
9+
10+
# grafana_apps_dashboard_dashboard_v2 (Resource)
11+
12+
Manages Grafana dashboards using the v2 (Dynamic Dashboards) schema.
13+
14+
* [Official documentation](https://grafana.com/docs/grafana/latest/dashboards/)
15+
* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/dashboard/#new-dashboard-apis)
16+
17+
## Example Usage
18+
19+
```terraform
20+
resource "grafana_apps_dashboard_dashboard_v2" "example" {
21+
metadata {
22+
uid = "example-dashboard-v2"
23+
}
24+
25+
spec {
26+
title = "Example Dashboard V2"
27+
json = jsonencode({
28+
title = "Example Dashboard V2"
29+
cursorSync = "Off"
30+
elements = {}
31+
layout = { kind = "GridLayout", spec = { items = [] } }
32+
links = []
33+
preload = false
34+
annotations = []
35+
variables = []
36+
timeSettings = {
37+
timezone = "browser"
38+
from = "now-6h"
39+
to = "now"
40+
}
41+
})
42+
}
43+
}
44+
```
45+
46+
<!-- schema generated by tfplugindocs -->
47+
## Schema
48+
49+
### Optional
50+
51+
- `metadata` (Block, Optional) The metadata of the resource. (see [below for nested schema](#nestedblock--metadata))
52+
- `options` (Block, Optional) Options for applying the resource. (see [below for nested schema](#nestedblock--options))
53+
- `spec` (Block, Optional) The spec of the resource. (see [below for nested schema](#nestedblock--spec))
54+
55+
### Read-Only
56+
57+
- `id` (String) The ID of the resource derived from UUID.
58+
59+
<a id="nestedblock--metadata"></a>
60+
### Nested Schema for `metadata`
61+
62+
Required:
63+
64+
- `uid` (String) The unique identifier of the resource.
65+
66+
Optional:
67+
68+
- `folder_uid` (String) The UID of the folder to save the resource in.
69+
70+
Read-Only:
71+
72+
- `annotations` (Map of String) Annotations of the resource.
73+
- `url` (String) The full URL of the resource.
74+
- `uuid` (String) The globally unique identifier of a resource, used by the API for tracking.
75+
- `version` (String) The version of the resource.
76+
77+
78+
<a id="nestedblock--options"></a>
79+
### Nested Schema for `options`
80+
81+
Optional:
82+
83+
- `allow_ui_updates` (Boolean) Set to true to allow editing the resource from the Grafana UI. By default, resources managed by Terraform cannot be edited in the UI. Enabling this option will cause divergence between the Terraform configuration and the resource in Grafana.
84+
- `overwrite` (Boolean) Set to true if you want to overwrite existing resource with newer version, same resource title in folder or same resource uid.
85+
86+
87+
<a id="nestedblock--spec"></a>
88+
### Nested Schema for `spec`
89+
90+
Required:
91+
92+
- `json` (String) The JSON representation of the dashboard v2 spec.
93+
94+
Optional:
95+
96+
- `tags` (List of String) The tags of the dashboard. If not set, the tags will be derived from the JSON spec.
97+
- `title` (String) The title of the dashboard. If not set, the title will be derived from the JSON spec.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
resource "grafana_apps_dashboard_dashboard_v2" "example" {
2+
metadata {
3+
uid = "example-dashboard-v2"
4+
}
5+
6+
spec {
7+
title = "Example Dashboard V2"
8+
json = jsonencode({
9+
title = "Example Dashboard V2"
10+
cursorSync = "Off"
11+
elements = {}
12+
layout = { kind = "GridLayout", spec = { items = [] } }
13+
links = []
14+
preload = false
15+
annotations = []
16+
variables = []
17+
timeSettings = {
18+
timezone = "browser"
19+
from = "now-6h"
20+
to = "now"
21+
}
22+
})
23+
}
24+
}

internal/resources/appplatform/catalog-resource.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@ spec:
2727
---
2828
apiVersion: backstage.io/v1alpha1
2929
kind: Component
30+
metadata:
31+
name: resource-grafana_apps_dashboard_dashboard_v2
32+
title: grafana_apps_dashboard_dashboard_v2 (resource)
33+
description: |
34+
resource `grafana_apps_dashboard_dashboard_v2` in Grafana Labs' Terraform Provider
35+
spec:
36+
subcomponentOf: component:default/terraform-provider-grafana
37+
type: terraform-resource
38+
owner: group:default/grafana-app-platform-squad
39+
lifecycle: production
40+
---
41+
apiVersion: backstage.io/v1alpha1
42+
kind: Component
3043
metadata:
3144
name: resource-grafana_apps_playlist_playlist_v0alpha1
3245
title: grafana_apps_playlist_playlist_v0alpha1 (resource)
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package appplatform
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
7+
v2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2"
8+
"github.com/grafana/terraform-provider-grafana/v4/internal/common"
9+
"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
10+
"github.com/hashicorp/terraform-plugin-framework/attr"
11+
"github.com/hashicorp/terraform-plugin-framework/diag"
12+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
13+
"github.com/hashicorp/terraform-plugin-framework/types"
14+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
15+
)
16+
17+
// DashboardV2StableSpecModel is a model for the dashboard v2 spec.
18+
type DashboardV2StableSpecModel struct {
19+
JSON jsontypes.Normalized `tfsdk:"json"`
20+
Title types.String `tfsdk:"title"`
21+
Tags types.List `tfsdk:"tags"`
22+
}
23+
24+
// DashboardV2Stable creates a new Grafana Dashboard v2 resource.
25+
func DashboardV2Stable() NamedResource {
26+
return NewNamedResource[*v2.Dashboard, *v2.DashboardList](
27+
common.CategoryGrafanaApps,
28+
ResourceConfig[*v2.Dashboard]{
29+
Kind: v2.DashboardKind(),
30+
Schema: ResourceSpecSchema{
31+
Description: "Manages Grafana dashboards using the v2 schema (Dynamic Dashboards).",
32+
MarkdownDescription: `
33+
Manages Grafana dashboards using the v2 (Dynamic Dashboards) schema.
34+
35+
* [Official documentation](https://grafana.com/docs/grafana/latest/dashboards/)
36+
* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/dashboard/#new-dashboard-apis)
37+
`,
38+
SpecAttributes: map[string]schema.Attribute{
39+
"json": schema.StringAttribute{
40+
Required: true,
41+
Description: "The JSON representation of the dashboard v2 spec.",
42+
CustomType: jsontypes.NormalizedType{},
43+
},
44+
"title": schema.StringAttribute{
45+
Optional: true,
46+
Description: "The title of the dashboard. If not set, the title will be derived from the JSON spec.",
47+
},
48+
"tags": schema.ListAttribute{
49+
Optional: true,
50+
Description: "The tags of the dashboard. If not set, the tags will be derived from the JSON spec.",
51+
ElementType: types.StringType,
52+
},
53+
},
54+
OptionsAttributes: map[string]schema.Attribute{
55+
"allow_ui_updates": schema.BoolAttribute{
56+
Optional: true,
57+
Description: "Set to true to allow editing the resource from the Grafana UI. By default, resources managed by Terraform cannot be edited in the UI. Enabling this option will cause divergence between the Terraform configuration and the resource in Grafana.",
58+
},
59+
},
60+
},
61+
SpecParser: func(ctx context.Context, spec types.Object, dst *v2.Dashboard) diag.Diagnostics {
62+
var data DashboardV2StableSpecModel
63+
if diag := spec.As(ctx, &data, basetypes.ObjectAsOptions{
64+
UnhandledNullAsEmpty: true,
65+
UnhandledUnknownAsEmpty: true,
66+
}); diag.HasError() {
67+
return diag
68+
}
69+
70+
var res v2.DashboardSpec
71+
if diag := data.JSON.Unmarshal(&res); diag.HasError() {
72+
return diag
73+
}
74+
75+
if !data.Title.IsNull() && !data.Title.IsUnknown() {
76+
res.Title = data.Title.ValueString()
77+
}
78+
79+
if tags, ok := getTagsFromV2StableModel(data); ok {
80+
res.Tags = tags
81+
}
82+
83+
if err := dst.SetSpec(res); err != nil {
84+
return diag.Diagnostics{
85+
diag.NewErrorDiagnostic("failed to set spec", err.Error()),
86+
}
87+
}
88+
89+
return diag.Diagnostics{}
90+
},
91+
SpecSaver: func(ctx context.Context, obj *v2.Dashboard, dst *ResourceModel) diag.Diagnostics {
92+
jsonBytes, err := json.Marshal(obj.Spec)
93+
if err != nil {
94+
return diag.Diagnostics{
95+
diag.NewErrorDiagnostic("failed to marshal dashboard v2 spec", err.Error()),
96+
}
97+
}
98+
99+
var data DashboardV2StableSpecModel
100+
if diag := dst.Spec.As(ctx, &data, basetypes.ObjectAsOptions{
101+
UnhandledNullAsEmpty: true,
102+
UnhandledUnknownAsEmpty: true,
103+
}); diag.HasError() {
104+
return diag
105+
}
106+
data.JSON = jsontypes.NewNormalizedValue(string(jsonBytes))
107+
108+
// SpecSaver is only called during import — always populate title and tags
109+
// from the spec so that imported state reflects the actual resource.
110+
if obj.Spec.Title != "" {
111+
data.Title = types.StringValue(obj.Spec.Title)
112+
} else {
113+
data.Title = types.StringNull()
114+
}
115+
116+
if len(obj.Spec.Tags) > 0 {
117+
tags, diags := types.ListValueFrom(ctx, types.StringType, obj.Spec.Tags)
118+
if diags.HasError() {
119+
return diags
120+
}
121+
data.Tags = tags
122+
} else {
123+
data.Tags = types.ListNull(types.StringType)
124+
}
125+
126+
spec, diags := types.ObjectValueFrom(ctx, map[string]attr.Type{
127+
"json": jsontypes.NormalizedType{},
128+
"title": types.StringType,
129+
"tags": types.ListType{ElemType: types.StringType},
130+
}, &data)
131+
if diags.HasError() {
132+
return diags
133+
}
134+
dst.Spec = spec
135+
136+
return diag.Diagnostics{}
137+
},
138+
})
139+
}
140+
141+
func getTagsFromV2StableModel(data DashboardV2StableSpecModel) ([]string, bool) {
142+
if data.Tags.IsNull() || data.Tags.IsUnknown() {
143+
return nil, false
144+
}
145+
146+
tags := make([]string, 0, len(data.Tags.Elements()))
147+
for _, tag := range data.Tags.Elements() {
148+
if tag, ok := tag.(types.String); ok {
149+
tags = append(tags, tag.ValueString())
150+
}
151+
}
152+
153+
return tags, true
154+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package appplatform_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/grafana/terraform-provider-grafana/v4/internal/testutils"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
9+
terraformresource "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
10+
)
11+
12+
const dashboardV2StableResourceName = "grafana_apps_dashboard_dashboard_v2.test"
13+
14+
func TestAccDashboardV2Stable_basic(t *testing.T) {
15+
testutils.CheckOSSTestsEnabled(t, ">=13.0.0")
16+
17+
randSuffix := acctest.RandString(6)
18+
19+
terraformresource.ParallelTest(t, terraformresource.TestCase{
20+
ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
21+
Steps: []terraformresource.TestStep{
22+
{
23+
Config: testAccDashboardV2StableBasic(randSuffix),
24+
Check: terraformresource.ComposeTestCheckFunc(
25+
terraformresource.TestCheckResourceAttrSet(dashboardV2StableResourceName, "id"),
26+
terraformresource.TestCheckResourceAttr(dashboardV2StableResourceName, "spec.title", "Test Dashboard V2"),
27+
),
28+
},
29+
{
30+
ResourceName: dashboardV2StableResourceName,
31+
ImportState: true,
32+
ImportStateVerify: true,
33+
ImportStateVerifyIgnore: []string{
34+
"options.%",
35+
"options.overwrite",
36+
"options.allow_ui_updates",
37+
"spec.json",
38+
},
39+
ImportStateIdFunc: importStateIDFunc(dashboardV2StableResourceName),
40+
},
41+
},
42+
})
43+
}
44+
45+
func testAccDashboardV2StableBasic(randSuffix string) string {
46+
return fmt.Sprintf(`
47+
resource "grafana_apps_dashboard_dashboard_v2" "test" {
48+
metadata {
49+
uid = "test-v2-dashboard-%s"
50+
}
51+
52+
spec {
53+
title = "Test Dashboard V2"
54+
json = jsonencode({
55+
title = "Test Dashboard V2"
56+
cursorSync = "Off"
57+
elements = {}
58+
layout = { kind = "GridLayout", spec = { items = [] } }
59+
links = []
60+
preload = false
61+
annotations = []
62+
variables = []
63+
timeSettings = {
64+
timezone = "browser"
65+
from = "now-6h"
66+
to = "now"
67+
}
68+
})
69+
}
70+
71+
options {
72+
overwrite = true
73+
}
74+
}
75+
`, randSuffix)
76+
}

0 commit comments

Comments
 (0)