From 4697645351eff3c457d35bec6e169a349b8de229 Mon Sep 17 00:00:00 2001 From: Arani Sen Date: Wed, 30 Nov 2022 11:51:16 -0600 Subject: [PATCH] feat: Adding Webhooks to Index Rate Alert adding the ability to add webhooks for Index Rate Alerts Ref:[LOG-9667](https://mezmo.atlassian.net/browse/LOG-9667) --- docs/resources/logdna_index_rate_alert.md | 11 +++ examples/index_rate_alert.tf | 12 +++- logdna/request_types.go | 83 +++++++++++++++++++++-- logdna/resource_index_rate_alert.go | 50 ++++++++++++++ logdna/resource_index_rate_alert_test.go | 56 +++++++++++++++ logdna/response_types.go | 29 +++++++- 6 files changed, 231 insertions(+), 10 deletions(-) diff --git a/docs/resources/logdna_index_rate_alert.md b/docs/resources/logdna_index_rate_alert.md index ae680b6..e860866 100644 --- a/docs/resources/logdna_index_rate_alert.md +++ b/docs/resources/logdna_index_rate_alert.md @@ -25,7 +25,18 @@ resource "logdna_index_rate_alert" "config" { slack = ["https://slack_url/key"] pagerduty = ["service_key"] } + webhook_channel { + url = "https:/testurl.com" + method = "POST" + headers = { + header1 = "value1" + } + bodytemplate = jsonencode({ + something = "something" + }) + } } + ``` ## Destroy diff --git a/examples/index_rate_alert.tf b/examples/index_rate_alert.tf index 6c53f9d..83a1ea7 100644 --- a/examples/index_rate_alert.tf +++ b/examples/index_rate_alert.tf @@ -13,4 +13,14 @@ resource "logdna_index_rate_alert" "config" { slack = ["https://slack_url/key"] pagerduty = ["service_key"] } -} \ No newline at end of file + webhook_channel { + url = "https://something.com" + method = "PUT" + headers = { + field2 = "value2" + } + bodytemplate = `jsonencode({ + something = "!something" + })` + } +} diff --git a/logdna/request_types.go b/logdna/request_types.go index fc628bf..8d9de7e 100644 --- a/logdna/request_types.go +++ b/logdna/request_types.go @@ -55,10 +55,18 @@ type keyRequest struct { Name string `json:"name,omitempty"` } +type indexRateAlertWebhookRequest struct { + URL string `json:"url,omitempty"` + Method string `json:"method,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + BodyTemplate map[string]interface{} `json:"bodyTemplate,omitempty"` +} + type indexRateAlertChannelRequest struct { - Email []string `json:"email,omitempty"` - Pagerduty []string `json:"pagerduty,omitempty"` - Slack []string `json:"slack,omitempty"` + Email []string `json:"email,omitempty"` + Pagerduty []string `json:"pagerduty,omitempty"` + Slack []string `json:"slack,omitempty"` + Webhook []indexRateAlertWebhookRequest `json:"webhook,omitempty"` } type indexRateAlertRequest struct { @@ -139,7 +147,6 @@ func (doc *indexRateAlertRequest) CreateRequestBody(d *schema.ResourceData) diag var diags diag.Diagnostics var channels = d.Get("channels").([]interface{}) - if len(channels) > 1 { return diag.FromErr( errors.New("Index rate alert resource supports only one channels object"), @@ -158,7 +165,7 @@ func (doc *indexRateAlertRequest) CreateRequestBody(d *schema.ResourceData) diag indexRateAlertChannel.Email = listToStrings(channel["email"].([]interface{})) indexRateAlertChannel.Pagerduty = listToStrings(channel["pagerduty"].([]interface{})) indexRateAlertChannel.Slack = listToStrings(channel["slack"].([]interface{})) - + indexRateAlertChannel.Webhook = *aggregateIndexRateAlertWebhookFromSchema(d, &diags) doc.Channels = indexRateAlertChannel return diags @@ -185,6 +192,24 @@ func (member *memberPutRequest) CreateRequestBody(d *schema.ResourceData) diag.D return diags } +func aggregateIndexRateAlertWebhookFromSchema( + d *schema.ResourceData, + diags *diag.Diagnostics, +) *[]indexRateAlertWebhookRequest { + + allWebhookEntries := make([]indexRateAlertWebhookRequest, 0) + + allWebhookEntries = append( + allWebhookEntries, + *iterateIndexRateAlertWebhookType( + d.Get("webhook_channel").([]interface{}), + diags, + )..., + ) + + return &allWebhookEntries +} + func aggregateAllChannelsFromSchema( d *schema.ResourceData, diags *diag.Diagnostics, @@ -269,6 +294,49 @@ func iterateIntegrationType( return &channelRequests } +func iterateIndexRateAlertWebhookType( + listEntries []interface{}, + diags *diag.Diagnostics, +) *[]indexRateAlertWebhookRequest { + webhookRequests := []indexRateAlertWebhookRequest{} + + for _, entry := range listEntries { + e := entry.(map[string]interface{}) + headersMap := make(map[string]string) + + for k, v := range e["headers"].(map[string]interface{}) { + headersMap[k] = v.(string) + } + + var c interface{} + var bt map[string]interface{} + + if bodyTemplate := e["bodytemplate"].(string); bodyTemplate != "" { + // See if the JSON is valid, but don't use the value or it will double encode + err := json.Unmarshal([]byte(bodyTemplate), &bt) + + if err != nil { + *diags = append(*diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "bodytemplate is not a valid JSON string", + Detail: err.Error(), + }) + } + } + + c = indexRateAlertWebhookRequest{ + Headers: headersMap, + Method: e["method"].(string), + URL: e["url"].(string), + BodyTemplate: bt, + } + + webhookRequests = append(webhookRequests, c.(indexRateAlertWebhookRequest)) + } + + return &webhookRequests +} + func emailChannelRequest(s map[string]interface{}) channelRequest { var emails []string for _, email := range s["emails"].([]interface{}) { @@ -317,7 +385,10 @@ func slackChannelRequest(s map[string]interface{}) channelRequest { return c } -func webHookChannelRequest(s map[string]interface{}, diags *diag.Diagnostics) channelRequest { +func webHookChannelRequest( + s map[string]interface{}, + diags *diag.Diagnostics, +) channelRequest { headersMap := make(map[string]string) for k, v := range s["headers"].(map[string]interface{}) { diff --git a/logdna/resource_index_rate_alert.go b/logdna/resource_index_rate_alert.go index 8088221..585022f 100644 --- a/logdna/resource_index_rate_alert.go +++ b/logdna/resource_index_rate_alert.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "log" + "reflect" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -100,6 +101,9 @@ func resourceIndexRateAlertRead(ctx context.Context, d *schema.ResourceData, m i integrations["email"] = indexRateAlert.Channels.Email integrations["pagerduty"] = indexRateAlert.Channels.Pagerduty integrations["slack"] = indexRateAlert.Channels.Slack + webhooks := mapIndexRateAlertWebhookToSchema(indexRateAlert) + + appendError(d.Set("webhook_channel", webhooks), &diags) channels = append(channels, integrations) @@ -214,6 +218,52 @@ func resourceIndexRateAlert() *schema.Resource { }, }, }, + "webhook_channel":{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "url": { + Type: schema.TypeString, + Required: true, + }, + "method": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"GET", "POST","PUT","DELETE"}, false), + }, + "headers": &schema.Schema{ + Type: schema.TypeMap, + Optional:true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + }, + "bodytemplate": { + Type: schema.TypeString, + Optional: true, + // This function compares JSON, ignoring whitespace that can occur in a .tf config. + // Without this, `terraform apply` will think values are different from remote to state. + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + var jsonOld, jsonNew interface{} + var err error + err = json.Unmarshal([]byte(old), &jsonOld) + if err != nil { + return false + } + err = json.Unmarshal([]byte(new), &jsonNew) + if err != nil { + return false + } + shouldSuppress := reflect.DeepEqual(jsonNew, jsonOld) + log.Println("[DEBUG] Does view 'bodytemplate' value in state appear the same as remote?", shouldSuppress) + return shouldSuppress + }, + }, + }, + }, + }, "enabled": { Type: schema.TypeBool, Required: true, diff --git a/logdna/resource_index_rate_alert_test.go b/logdna/resource_index_rate_alert_test.go index 96a5390..d15e2ad 100644 --- a/logdna/resource_index_rate_alert_test.go +++ b/logdna/resource_index_rate_alert_test.go @@ -164,6 +164,42 @@ func TestIndexRateAlert_ErrorChannels(t *testing.T) { }) } +func TestIndexRateAlert_ErrorInvalidTokenWebhookBodyTemplate(t *testing.T) { + iraArgs := map[string]string{ + "max_lines": `3`, + "max_z_score": `3`, + "threshold_alert": `"separate"`, + "frequency": `"hourly"`, + "enabled": `false`, + } + + chArgs := map[string]map[string]string{ + "channels": { + "email": `["test@logdna.com"]`, + }, + "webhook_channel": { + "url": `"https://something.com"`, + "method": `"POST"`, + "headers": `{ + field2 = "value2" + }`, + "bodytemplate": `jsonencode({ + something = "{{maxLines}}" + })`, + }, + } + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmtTestConfigResource("index_rate_alert", "test_config", globalPcArgs, iraArgs, chArgs, nilLst), + ExpectError: regexp.MustCompile("Invalid bodyTemplate: {{maxLines}} is not a valid token"), + }, + }, + }) +} + func TestIndexRateAlert_Basic(t *testing.T) { iraArgs := map[string]string{ "max_lines": `3`, @@ -179,6 +215,16 @@ func TestIndexRateAlert_Basic(t *testing.T) { "slack": `["https://hooks.slack.com/KEY"]`, "pagerduty": `["ndt3k75rsw520d8t55dv35decdyt3mkcb3r"]`, }, + "webhook_channel": { + "url" : `"https://something.com"`, + "method": `"POST"`, + "headers" : `{ + field2 = "value2" + }`, + "bodytemplate" :`jsonencode({ + something = "something" + })`, + }, } iraUpdArgs := map[string]string{ @@ -195,6 +241,16 @@ func TestIndexRateAlert_Basic(t *testing.T) { "slack": `["https://hooks.slack.com/UPDATED_KEY", "https://hooks.slack.com/KEY_2"]`, "pagerduty": `["new3k75rsw520d8t55dv35decdyt3mkcnew"]`, }, + "webhook_channel": { + "url" : `"https://something.com"`, + "method": `"PUT"`, + "headers" : `{ + field2 = "value2" + }`, + "bodytemplate" :`jsonencode({ + something = "!something" + })`, + }, } createdEmails := strings.Split( diff --git a/logdna/response_types.go b/logdna/response_types.go index 34439f9..af0019b 100644 --- a/logdna/response_types.go +++ b/logdna/response_types.go @@ -90,10 +90,18 @@ type categoryResponse struct { Id string `json:"id"` } +type indexRateAlertWebhookResponse struct { + URL string `json:"url,omitempty"` + Method string `json:"method,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + BodyTemplate string `json:"bodyTemplate,omitempty"` +} + type indexRateAlertChannelResponse struct { - Email []string `json:"email,omitempty"` - Pagerduty []string `json:"pagerduty,omitempty"` - Slack []string `json:"slack,omitempty"` + Email []string `json:"email,omitempty"` + Pagerduty []string `json:"pagerduty,omitempty"` + Slack []string `json:"slack,omitempty"` + Webhook []indexRateAlertWebhookResponse `json:"webhook,omitempty"` } type indexRateAlertResponse struct { @@ -105,6 +113,21 @@ type indexRateAlertResponse struct { Enabled bool `json:"enabled,omitempty"` } +func mapIndexRateAlertWebhookToSchema(indexRateAlert indexRateAlertResponse) []interface{} { + webhooks := make([]interface{}, 0) + + for _, webhook := range indexRateAlert.Channels.Webhook { + w := make(map[string]interface{}) + + w["bodytemplate"] = webhook.BodyTemplate + w["headers"] = webhook.Headers + w["method"] = webhook.Method + w["url"] = webhook.URL + + webhooks = append(webhooks, w) + } + return webhooks +} func (view *viewResponse) MapChannelsToSchema() (map[string][]interface{}, diag.Diagnostics) { channels := view.Channels channelIntegrations, diags := mapAllChannelsToSchema("view", &channels)