Skip to content

Commit 0f27469

Browse files
Add a new datasource for Grafana datasource (#775)
Confusing, I know :) Closes #628 Supersedes #629 This is useful for alert rules which require the datasource UID
1 parent d67479d commit 0f27469

File tree

8 files changed

+260
-30
lines changed

8 files changed

+260
-30
lines changed

docs/data-sources/data_source.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "grafana_data_source Data Source - terraform-provider-grafana"
4+
subcategory: "Grafana OSS"
5+
description: |-
6+
Get details about a Grafana Datasource querying by either name, uid or ID
7+
---
8+
9+
# grafana_data_source (Data Source)
10+
11+
Get details about a Grafana Datasource querying by either name, uid or ID
12+
13+
## Example Usage
14+
15+
```terraform
16+
resource "grafana_data_source" "prometheus" {
17+
type = "prometheus"
18+
name = "prometheus-ds-test"
19+
uid = "prometheus-ds-test-uid"
20+
url = "https://my-instance.com"
21+
basic_auth_enabled = true
22+
basic_auth_username = "username"
23+
24+
json_data_encoded = jsonencode({
25+
httpMethod = "POST"
26+
prometheusType = "Mimir"
27+
prometheusVersion = "2.4.0"
28+
})
29+
30+
secure_json_data_encoded = jsonencode({
31+
basicAuthPassword = "password"
32+
})
33+
}
34+
35+
data "grafana_data_source" "from_name" {
36+
name = grafana_data_source.prometheus.name
37+
}
38+
39+
data "grafana_data_source" "from_id" {
40+
id = grafana_data_source.prometheus.id
41+
}
42+
43+
data "grafana_data_source" "from_uid" {
44+
uid = grafana_data_source.prometheus.uid
45+
}
46+
```
47+
48+
<!-- schema generated by tfplugindocs -->
49+
## Schema
50+
51+
### Optional
52+
53+
- `name` (String)
54+
- `uid` (String)
55+
56+
### Read-Only
57+
58+
- `access_mode` (String) The method by which Grafana will access the data source: `proxy` or `direct`.
59+
- `basic_auth_enabled` (Boolean) Whether to enable basic auth for the data source.
60+
- `basic_auth_username` (String) Basic auth username.
61+
- `database_name` (String) (Required by some data source types) The name of the database to use on the selected data source server.
62+
- `id` (String) The ID of this resource.
63+
- `is_default` (Boolean) Whether to set the data source as default. This should only be `true` to a single data source.
64+
- `json_data_encoded` (String) Serialized JSON string containing the json data. This attribute can be used to pass configuration options to the data source. To figure out what options a datasource has available, see its docs or inspect the network data when saving it from the Grafana UI. Note that keys in this map are usually camelCased.
65+
- `type` (String) The data source type. Must be one of the supported data source keywords.
66+
- `url` (String) The URL for the data source. The type of URL required varies depending on the chosen data source type.
67+
- `username` (String) (Required by some data source types) The username to use to authenticate to the data source.
68+
69+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
resource "grafana_data_source" "prometheus" {
2+
type = "prometheus"
3+
name = "prometheus-ds-test"
4+
uid = "prometheus-ds-test-uid"
5+
url = "https://my-instance.com"
6+
basic_auth_enabled = true
7+
basic_auth_username = "username"
8+
9+
json_data_encoded = jsonencode({
10+
httpMethod = "POST"
11+
prometheusType = "Mimir"
12+
prometheusVersion = "2.4.0"
13+
})
14+
15+
secure_json_data_encoded = jsonencode({
16+
basicAuthPassword = "password"
17+
})
18+
}
19+
20+
data "grafana_data_source" "from_name" {
21+
name = grafana_data_source.prometheus.name
22+
}
23+
24+
data "grafana_data_source" "from_id" {
25+
id = grafana_data_source.prometheus.id
26+
}
27+
28+
data "grafana_data_source" "from_uid" {
29+
uid = grafana_data_source.prometheus.uid
30+
}

grafana/data_source_data_source.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package grafana
2+
3+
import (
4+
"context"
5+
"strconv"
6+
7+
gapi "github.com/grafana/grafana-api-golang-client"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
)
11+
12+
func DatasourceDatasource() *schema.Resource {
13+
return &schema.Resource{
14+
Description: "Get details about a Grafana Datasource querying by either name, uid or ID",
15+
ReadContext: datasourceDatasourceRead,
16+
Schema: cloneResourceSchemaForDatasource(ResourceDataSource(), map[string]*schema.Schema{
17+
"id": {
18+
Type: schema.TypeString,
19+
Optional: true,
20+
Computed: true,
21+
AtLeastOneOf: []string{"id", "name", "uid"},
22+
},
23+
"name": {
24+
Type: schema.TypeString,
25+
Optional: true,
26+
Computed: true,
27+
AtLeastOneOf: []string{"id", "name", "uid"},
28+
},
29+
"uid": {
30+
Type: schema.TypeString,
31+
Optional: true,
32+
Computed: true,
33+
AtLeastOneOf: []string{"id", "name", "uid"},
34+
},
35+
"password": nil,
36+
"basic_auth_password": nil,
37+
"json_data": nil,
38+
"secure_json_data": nil,
39+
"secure_json_data_encoded": nil,
40+
"http_headers": nil,
41+
}),
42+
}
43+
}
44+
45+
func datasourceDatasourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
46+
client := meta.(*client).gapi
47+
48+
var (
49+
dataSource *gapi.DataSource
50+
err error
51+
)
52+
53+
if name, ok := d.GetOk("name"); ok {
54+
id, getIDErr := client.DataSourceIDByName(name.(string))
55+
if getIDErr != nil {
56+
return diag.FromErr(getIDErr)
57+
}
58+
dataSource, err = client.DataSource(id)
59+
} else if id, ok := d.GetOk("id"); ok {
60+
idInt, parseErr := strconv.ParseInt(id.(string), 10, 64)
61+
if parseErr != nil {
62+
return diag.FromErr(parseErr)
63+
}
64+
dataSource, err = client.DataSource(idInt)
65+
} else if uid, ok := d.GetOk("uid"); ok {
66+
dataSource, err = client.DataSourceByUID(uid.(string))
67+
}
68+
69+
if err != nil {
70+
return diag.FromErr(err)
71+
}
72+
73+
return readDatasource(d, dataSource)
74+
}
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+
"testing"
5+
6+
gapi "github.com/grafana/grafana-api-golang-client"
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
8+
)
9+
10+
func TestAccDatasourceDatasource(t *testing.T) {
11+
CheckOSSTestsEnabled(t)
12+
13+
var dataSource gapi.DataSource
14+
checks := []resource.TestCheckFunc{
15+
testAccDataSourceCheckExists("grafana_data_source.prometheus", &dataSource),
16+
17+
resource.TestMatchResourceAttr("data.grafana_data_source.from_name", "id", idRegexp),
18+
resource.TestCheckResourceAttr("data.grafana_data_source.from_name", "name", "prometheus-ds-test"),
19+
resource.TestCheckResourceAttr("data.grafana_data_source.from_name", "uid", "prometheus-ds-test-uid"),
20+
resource.TestCheckResourceAttr("data.grafana_data_source.from_name", "json_data_encoded", `{"httpMethod":"POST","prometheusType":"Mimir","prometheusVersion":"2.4.0"}`),
21+
22+
resource.TestMatchResourceAttr("data.grafana_data_source.from_uid", "id", idRegexp),
23+
resource.TestCheckResourceAttr("data.grafana_data_source.from_uid", "name", "prometheus-ds-test"),
24+
resource.TestCheckResourceAttr("data.grafana_data_source.from_uid", "uid", "prometheus-ds-test-uid"),
25+
resource.TestCheckResourceAttr("data.grafana_data_source.from_uid", "json_data_encoded", `{"httpMethod":"POST","prometheusType":"Mimir","prometheusVersion":"2.4.0"}`),
26+
27+
resource.TestMatchResourceAttr("data.grafana_data_source.from_id", "id", idRegexp),
28+
resource.TestCheckResourceAttr("data.grafana_data_source.from_id", "name", "prometheus-ds-test"),
29+
resource.TestCheckResourceAttr("data.grafana_data_source.from_id", "uid", "prometheus-ds-test-uid"),
30+
resource.TestCheckResourceAttr("data.grafana_data_source.from_id", "json_data_encoded", `{"httpMethod":"POST","prometheusType":"Mimir","prometheusVersion":"2.4.0"}`),
31+
}
32+
33+
resource.ParallelTest(t, resource.TestCase{
34+
ProviderFactories: testAccProviderFactories,
35+
CheckDestroy: testAccDataSourceCheckDestroy(&dataSource),
36+
Steps: []resource.TestStep{
37+
{
38+
Config: testAccExample(t, "data-sources/grafana_data_source/data-source.tf"),
39+
Check: resource.ComposeTestCheckFunc(checks...),
40+
},
41+
},
42+
})
43+
}

grafana/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ func Provider(version string) func() *schema.Provider {
112112
grafanaClientDatasources = addResourcesMetadataValidation(grafanaClientPresent, map[string]*schema.Resource{
113113
"grafana_dashboard": DatasourceDashboard(),
114114
"grafana_dashboards": DatasourceDashboards(),
115+
"grafana_data_source": DatasourceDatasource(),
115116
"grafana_folder": DatasourceFolder(),
116117
"grafana_folders": DatasourceFolders(),
117118
"grafana_library_panel": DatasourceLibraryPanel(),

grafana/resource_data_source.go

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,15 @@ source selected (via the 'type' argument).
578578
json, _ := structure.NormalizeJsonString(v)
579579
return json
580580
},
581-
DiffSuppressFunc: SuppressEquivalentJSONDiffs,
581+
DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool {
582+
// If the value wasn't directly changed, and the new value is empty, it means the value is computed and it should not be a diff
583+
// Ex: The data source is managed by the old `json_data` field
584+
if !d.HasChange("json_data_encoded") && newValue == "" {
585+
return true
586+
}
587+
588+
return SuppressEquivalentJSONDiffs(k, oldValue, newValue, d)
589+
},
582590
},
583591
"secure_json_data_encoded": {
584592
Type: schema.TypeString,
@@ -652,6 +660,27 @@ func ReadDataSource(ctx context.Context, d *schema.ResourceData, meta interface{
652660
return diag.FromErr(err)
653661
}
654662

663+
return readDatasource(d, dataSource)
664+
}
665+
666+
// DeleteDataSource deletes a Grafana datasource
667+
func DeleteDataSource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
668+
client := meta.(*client).gapi
669+
670+
idStr := d.Id()
671+
id, err := strconv.ParseInt(idStr, 10, 64)
672+
if err != nil {
673+
return diag.Errorf("Invalid id: %#v", idStr)
674+
}
675+
676+
if err = client.DeleteDataSource(id); err != nil {
677+
return diag.FromErr(err)
678+
}
679+
680+
return diag.Diagnostics{}
681+
}
682+
683+
func readDatasource(d *schema.ResourceData, dataSource *gapi.DataSource) diag.Diagnostics {
655684
d.SetId(strconv.FormatInt(dataSource.ID, 10))
656685
d.Set("access_mode", dataSource.Access)
657686
d.Set("database_name", dataSource.Database)
@@ -662,49 +691,31 @@ func ReadDataSource(ctx context.Context, d *schema.ResourceData, meta interface{
662691
d.Set("username", dataSource.User)
663692
d.Set("uid", dataSource.UID)
664693

665-
// If `json_data` is not set, then we'll use the new attribute: `json_data_encoded`. This allows support of imports.
666694
gottenJSONData, _, gottenHeaders := gapi.ExtractHeadersFromJSONData(dataSource.JSONData, dataSource.SecureJSONData)
667-
if _, ok := d.GetOk("json_data_encoded"); ok {
668-
encodedJSONData, err := json.Marshal(gottenJSONData)
669-
if err != nil {
670-
return diag.Errorf("Failed to marshal JSON data: %s", err)
671-
}
672-
d.Set("json_data_encoded", string(encodedJSONData))
695+
encodedJSONData, err := json.Marshal(gottenJSONData)
696+
if err != nil {
697+
return diag.Errorf("Failed to marshal JSON data: %s", err)
673698
}
699+
d.Set("json_data_encoded", string(encodedJSONData))
674700

675701
// For headers, we do not know the value (the API does not return secret data)
676702
// so we only remove keys from the state that are no longer present in the API.
677-
currentHeaders := d.Get("http_headers").(map[string]interface{})
678-
for key := range currentHeaders {
679-
if _, ok := gottenHeaders[key]; !ok {
680-
delete(currentHeaders, key)
703+
if currentHeadersInterface, ok := d.GetOk("http_headers"); ok {
704+
currentHeaders := currentHeadersInterface.(map[string]interface{})
705+
for key := range currentHeaders {
706+
if _, ok := gottenHeaders[key]; !ok {
707+
delete(currentHeaders, key)
708+
}
681709
}
710+
d.Set("http_headers", currentHeaders)
682711
}
683-
d.Set("http_headers", currentHeaders)
684712

685713
d.Set("basic_auth_enabled", dataSource.BasicAuth)
686714
d.Set("basic_auth_username", dataSource.BasicAuthUser)
687715

688716
return nil
689717
}
690718

691-
// DeleteDataSource deletes a Grafana datasource
692-
func DeleteDataSource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
693-
client := meta.(*client).gapi
694-
695-
idStr := d.Id()
696-
id, err := strconv.ParseInt(idStr, 10, 64)
697-
if err != nil {
698-
return diag.Errorf("Invalid id: %#v", idStr)
699-
}
700-
701-
if err = client.DeleteDataSource(id); err != nil {
702-
return diag.FromErr(err)
703-
}
704-
705-
return diag.Diagnostics{}
706-
}
707-
708719
func makeDataSource(d *schema.ResourceData) (*gapi.DataSource, error) {
709720
idStr := d.Id()
710721
var id int64

grafana/schema.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func cloneResourceSchemaForDatasource(r *schema.Resource, updates map[string]*sc
3535
clone[k].DiffSuppressFunc = nil
3636
clone[k].ValidateDiagFunc = nil
3737
clone[k].ValidateFunc = nil
38+
clone[k].ConflictsWith = nil
3839
}
3940
for k, v := range updates {
4041
if v == nil {

tools/subcategories.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"data-sources/cloud_stack": "Cloud",
5252
"data-sources/dashboard": "Grafana OSS",
5353
"data-sources/dashboards": "Grafana OSS",
54+
"data-sources/data_source": "Grafana OSS",
5455
"data-sources/folder": "Grafana OSS",
5556
"data-sources/folders": "Grafana OSS",
5657
"data-sources/library_panel": "Grafana OSS",

0 commit comments

Comments
 (0)