Skip to content

Commit 06bd141

Browse files
authored
Add support for dashboard json resource (#950)
* add dashboard_json resource * refactor and use client exported request methods * lint and move helper functions to util * lint * handle diffs on computed fields * normalize json string * add tests and refactor * update go client * lint * update tests and lint * apply code review requested changes * apply code review suggestions and re-record cassettes * generate resource docs * fmt
1 parent 1c73dcf commit 06bd141

15 files changed

+4316
-4
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package utils
2+
3+
import (
4+
"context"
5+
"io/ioutil"
6+
"net/http"
7+
"net/url"
8+
9+
datadogV1 "github.com/DataDog/datadog-api-client-go/api/v1/datadog"
10+
)
11+
12+
// SendRequest send custom request
13+
func SendRequest(ctx context.Context, client *datadogV1.APIClient, method, path string, body interface{}) ([]byte, *http.Response, error) {
14+
req, err := buildRequest(ctx, client, method, path, body)
15+
if err != nil {
16+
return nil, nil, err
17+
}
18+
19+
httpRes, err := client.CallAPI(req)
20+
if err != nil {
21+
return nil, nil, err
22+
}
23+
24+
var bodyResByte []byte
25+
bodyResByte, err = ioutil.ReadAll(httpRes.Body)
26+
defer httpRes.Body.Close()
27+
if err != nil {
28+
return nil, httpRes, err
29+
}
30+
31+
if httpRes.StatusCode >= 300 {
32+
newErr := CustomRequestAPIError{
33+
body: bodyResByte,
34+
error: httpRes.Status,
35+
}
36+
return nil, httpRes, newErr
37+
}
38+
39+
return bodyResByte, httpRes, nil
40+
}
41+
42+
func buildRequest(ctx context.Context, client *datadogV1.APIClient, method, path string, body interface{}) (*http.Request, error) {
43+
var (
44+
localVarPostBody interface{}
45+
localVarFormFileName string
46+
localVarFileName string
47+
localVarPath string
48+
localVarFileBytes []byte
49+
localVarQueryParams url.Values
50+
localVarFormQueryParams url.Values
51+
)
52+
53+
localBasePath, err := client.GetConfig().ServerURLWithContext(ctx, "")
54+
if err != nil {
55+
return nil, err
56+
}
57+
localVarPath = localBasePath + path
58+
59+
localVarHeaderParams := make(map[string]string)
60+
localVarHeaderParams["Content-Type"] = "application/json"
61+
62+
localVarHTTPHeaderAccepts := make(map[string]string)
63+
localVarHTTPHeaderAccepts["Accept"] = "application/json"
64+
65+
if body != nil {
66+
localVarPostBody = body
67+
}
68+
69+
if ctx != nil {
70+
if auth, ok := ctx.Value(datadogV1.ContextAPIKeys).(map[string]datadogV1.APIKey); ok {
71+
if apiKey, ok := auth["apiKeyAuth"]; ok {
72+
var key string
73+
if apiKey.Prefix != "" {
74+
key = apiKey.Prefix + " " + apiKey.Key
75+
} else {
76+
key = apiKey.Key
77+
}
78+
localVarHeaderParams["DD-API-KEY"] = key
79+
}
80+
}
81+
}
82+
if ctx != nil {
83+
if auth, ok := ctx.Value(datadogV1.ContextAPIKeys).(map[string]datadogV1.APIKey); ok {
84+
if apiKey, ok := auth["appKeyAuth"]; ok {
85+
var key string
86+
if apiKey.Prefix != "" {
87+
key = apiKey.Prefix + " " + apiKey.Key
88+
} else {
89+
key = apiKey.Key
90+
}
91+
localVarHeaderParams["DD-APPLICATION-KEY"] = key
92+
}
93+
}
94+
}
95+
96+
req, err := client.PrepareRequest(ctx, localVarPath, method, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormQueryParams, localVarFormFileName, localVarFileName, localVarFileBytes)
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
return req, nil
102+
}
103+
104+
// CustomRequestAPIError Provides access to the body, and error on returned errors.
105+
type CustomRequestAPIError struct {
106+
body []byte
107+
error string
108+
}
109+
110+
// Error returns non-empty string if there was an error.
111+
func (e CustomRequestAPIError) Error() string {
112+
return e.error
113+
}
114+
115+
// Body returns the raw bytes of the response
116+
func (e CustomRequestAPIError) Body() []byte {
117+
return e.body
118+
}

datadog/internal/utils/utils.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import (
55
"crypto/sha256"
66
"encoding/json"
77
"fmt"
8-
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
98
"net/url"
109
"strings"
1110

1211
datadogV1 "github.com/DataDog/datadog-api-client-go/api/v1/datadog"
1312
datadogV2 "github.com/DataDog/datadog-api-client-go/api/v2/datadog"
13+
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
1414
"github.com/hashicorp/terraform-plugin-sdk/meta"
1515
"github.com/terraform-providers/terraform-provider-datadog/version"
1616
)
@@ -24,6 +24,9 @@ func TranslateClientError(err error, msg string) error {
2424
msg = "an error occurred"
2525
}
2626

27+
if apiErr, ok := err.(CustomRequestAPIError); ok {
28+
return fmt.Errorf(msg+": %v: %s", err, apiErr.Body())
29+
}
2730
if apiErr, ok := err.(datadogV1.GenericOpenAPIError); ok {
2831
return fmt.Errorf(msg+": %v: %s", err, apiErr.Body())
2932
}
@@ -108,3 +111,14 @@ func AccountNameAndChannelNameFromID(id string) (string, string, error) {
108111
}
109112
return result[0], result[1], nil
110113
}
114+
115+
// ConvertResponseByteToMap converts JSON []byte to map[string]interface{}
116+
func ConvertResponseByteToMap(b []byte) (map[string]interface{}, error) {
117+
convertedMap := make(map[string]interface{})
118+
err := json.Unmarshal(b, &convertedMap)
119+
if err != nil {
120+
return nil, err
121+
}
122+
123+
return convertedMap, nil
124+
}

datadog/internal/utils/utils_test.go

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ func TestAccountAndRoleFromID(t *testing.T) {
5151
accountID, roleName, err := AccountAndRoleFromID(tc.id)
5252

5353
if err != nil && tc.err != nil && err.Error() != tc.err.Error() {
54-
t.Errorf("%s: erros should be '%s', not `%s`", name, tc.err.Error(), err.Error())
54+
t.Errorf("%s: errors should be '%s', not `%s`", name, tc.err.Error(), err.Error())
5555
} else if err != nil && tc.err == nil {
56-
t.Errorf("%s: erros should be nil, not `%s`", name, err.Error())
56+
t.Errorf("%s: errors should be nil, not `%s`", name, err.Error())
5757
} else if err == nil && tc.err != nil {
58-
t.Errorf("%s: erros should be '%s', not nil", name, tc.err.Error())
58+
t.Errorf("%s: errors should be '%s', not nil", name, tc.err.Error())
5959
}
6060

6161
if accountID != tc.accountID {
@@ -96,3 +96,43 @@ func TestAccountNameAndChannelNameFromID(t *testing.T) {
9696
}
9797
}
9898
}
99+
100+
func TestConvertResponseByteToMap(t *testing.T) {
101+
cases := map[string]struct {
102+
js string
103+
errMsg string
104+
}{
105+
"validJSON": {validJSON(), ""},
106+
"invalidJSON": {invalidJSON(), "invalid character ':' after object key:value pair"},
107+
}
108+
for name, tc := range cases {
109+
_, err := ConvertResponseByteToMap([]byte(tc.js))
110+
if err != nil && tc.errMsg != "" && err.Error() != tc.errMsg {
111+
t.Fatalf("%s: error should be %s, not %s", name, tc.errMsg, err.Error())
112+
}
113+
if err != nil && tc.errMsg == "" {
114+
t.Fatalf("%s: error should be nil, not %s", name, err.Error())
115+
}
116+
}
117+
}
118+
119+
func validJSON() string {
120+
return fmt.Sprint(`
121+
{
122+
"test":"value",
123+
"test_two":{
124+
"nested_attr":"value"
125+
}
126+
}
127+
`)
128+
}
129+
func invalidJSON() string {
130+
return fmt.Sprint(`
131+
{
132+
"test":"value":"value",
133+
"test_two":{
134+
"nested_attr":"value"
135+
}
136+
}
137+
`)
138+
}

datadog/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ func Provider() terraform.ResourceProvider {
7676

7777
ResourcesMap: map[string]*schema.Resource{
7878
"datadog_dashboard": resourceDatadogDashboard(),
79+
"datadog_dashboard_json": resourceDatadogDashboardJSON(),
7980
"datadog_dashboard_list": resourceDatadogDashboardList(),
8081
"datadog_downtime": resourceDatadogDowntime(),
8182
"datadog_integration_aws": resourceDatadogIntegrationAws(),
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package datadog
2+
3+
import (
4+
"errors"
5+
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
6+
"github.com/hashicorp/terraform-plugin-sdk/helper/structure"
7+
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
8+
"github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils"
9+
)
10+
11+
var computedFields = []string{"id", "author_handle", "author_name", "created_at", "modified_at", "url"}
12+
13+
const path = "/api/v1/dashboard"
14+
15+
func resourceDatadogDashboardJSON() *schema.Resource {
16+
return &schema.Resource{
17+
Description: "Provides a Datadog dashboard JSON resource. This can be used to create and manage Datadog dashboards using the JSON definition.",
18+
Create: resourceDatadogDashboardJSONCreate,
19+
Read: resourceDatadogDashboardJSONRead,
20+
Update: resourceDatadogDashboardJSONUpdate,
21+
Delete: resourceDatadogDashboardJSONDelete,
22+
Importer: &schema.ResourceImporter{
23+
State: schema.ImportStatePassthrough,
24+
},
25+
Schema: map[string]*schema.Schema{
26+
"dashboard": {
27+
Type: schema.TypeString,
28+
Required: true,
29+
ValidateFunc: validation.StringIsJSON,
30+
StateFunc: func(v interface{}) string {
31+
// Remove computed fields when comparing diffs
32+
attrMap, _ := structure.ExpandJsonFromString(v.(string))
33+
for _, f := range computedFields {
34+
delete(attrMap, f)
35+
}
36+
res, _ := structure.FlattenJsonToString(attrMap)
37+
return res
38+
},
39+
Description: "The JSON formatted definition of the Dashboard.",
40+
},
41+
"url": {
42+
Type: schema.TypeString,
43+
Optional: true,
44+
Computed: true,
45+
Description: "The URL of the dashboard.",
46+
},
47+
},
48+
}
49+
}
50+
51+
func resourceDatadogDashboardJSONRead(d *schema.ResourceData, meta interface{}) error {
52+
providerConf := meta.(*ProviderConfiguration)
53+
datadogClientV1 := providerConf.DatadogClientV1
54+
authV1 := providerConf.AuthV1
55+
56+
id := d.Id()
57+
58+
respByte, httpResp, err := utils.SendRequest(authV1, datadogClientV1, "GET", path+"/"+id, nil)
59+
if err != nil {
60+
if httpResp != nil && httpResp.StatusCode == 404 {
61+
d.SetId("")
62+
return nil
63+
}
64+
return err
65+
}
66+
67+
respMap, err := utils.ConvertResponseByteToMap(respByte)
68+
if err != nil {
69+
return err
70+
}
71+
72+
return updateDashboardJSONState(d, respMap)
73+
}
74+
75+
func resourceDatadogDashboardJSONCreate(d *schema.ResourceData, meta interface{}) error {
76+
providerConf := meta.(*ProviderConfiguration)
77+
datadogClientV1 := providerConf.DatadogClientV1
78+
authV1 := providerConf.AuthV1
79+
80+
dashboard := d.Get("dashboard")
81+
82+
respByte, _, err := utils.SendRequest(authV1, datadogClientV1, "POST", path, dashboard)
83+
if err != nil {
84+
return utils.TranslateClientError(err, "error creating resource")
85+
}
86+
87+
respMap, err := utils.ConvertResponseByteToMap(respByte)
88+
if err != nil {
89+
return err
90+
}
91+
92+
id, ok := respMap["id"]
93+
if !ok {
94+
return errors.New("error retrieving id from response")
95+
}
96+
d.SetId(id.(string))
97+
98+
return updateDashboardJSONState(d, respMap)
99+
}
100+
101+
func resourceDatadogDashboardJSONUpdate(d *schema.ResourceData, meta interface{}) error {
102+
providerConf := meta.(*ProviderConfiguration)
103+
datadogClientV1 := providerConf.DatadogClientV1
104+
authV1 := providerConf.AuthV1
105+
106+
dashboard := d.Get("dashboard")
107+
id := d.Id()
108+
109+
respByte, _, err := utils.SendRequest(authV1, datadogClientV1, "PUT", path+"/"+id, dashboard)
110+
if err != nil {
111+
return utils.TranslateClientError(err, "error updating dashboard")
112+
}
113+
114+
respMap, err := utils.ConvertResponseByteToMap(respByte)
115+
if err != nil {
116+
return err
117+
}
118+
119+
return updateDashboardJSONState(d, respMap)
120+
}
121+
122+
func resourceDatadogDashboardJSONDelete(d *schema.ResourceData, meta interface{}) error {
123+
providerConf := meta.(*ProviderConfiguration)
124+
datadogClientV1 := providerConf.DatadogClientV1
125+
authV1 := providerConf.AuthV1
126+
127+
id := d.Id()
128+
129+
_, _, err := utils.SendRequest(authV1, datadogClientV1, "DELETE", path+"/"+id, nil)
130+
if err != nil {
131+
return utils.TranslateClientError(err, "error deleting dashboard")
132+
}
133+
134+
return nil
135+
}
136+
137+
func updateDashboardJSONState(d *schema.ResourceData, dashboard map[string]interface{}) error {
138+
if v, ok := dashboard["url"]; ok {
139+
if err := d.Set("url", v.(string)); err != nil {
140+
return err
141+
}
142+
}
143+
144+
// Remove computed fields from the object
145+
for _, f := range computedFields {
146+
delete(dashboard, f)
147+
}
148+
149+
dashboardString, err := structure.FlattenJsonToString(dashboard)
150+
if err != nil {
151+
return err
152+
}
153+
154+
if err = d.Set("dashboard", dashboardString); err != nil {
155+
return err
156+
}
157+
return nil
158+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2021-03-09T10:14:31.495603-05:00

0 commit comments

Comments
 (0)