rollbar/resource_notification.go
/*
* Copyright (c) 2022 Rollbar, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package rollbar
import (
"context"
"errors"
"strconv"
"strings"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/rollbar/terraform-provider-rollbar/client"
"github.com/rs/zerolog/log"
)
var configMap = map[string][]string{
"email": {"users", "teams"},
"slack": {"message_template", "channel", "show_message_buttons"},
"pagerduty": {"service_key"},
"webhook": {"url", "format"},
}
var emailDailySummaryConfigList = []string{"summary_time", "environments", "send_only_if_data", "min_item_level"}
func CustomNotificationImport(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) {
splitID := strings.Split(d.Id(), ComplexImportSeparator)
if len(splitID) > 1 {
mustSet(d, "channel", splitID[0])
d.SetId(splitID[1])
}
return []*schema.ResourceData{d}, nil
}
// resourceNotification constructs a resource representing a Rollbar notification.
func resourceNotification() *schema.Resource {
return &schema.Resource{
CreateContext: resourceNotificationCreate,
UpdateContext: resourceNotificationUpdate,
ReadContext: resourceNotificationRead,
DeleteContext: resourceNotificationDelete,
Importer: &schema.ResourceImporter{
StateContext: CustomNotificationImport,
},
Schema: map[string]*schema.Schema{
// Required
"channel": {
Description: "Channel",
Type: schema.TypeString,
Required: true,
},
"rule": {
Description: "Human readable name for the rule",
Type: schema.TypeSet,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"trigger": {
Description: "Trigger",
Type: schema.TypeString,
Required: true,
},
"filters": {
Description: "Filters",
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"path": {
Description: "Path",
Type: schema.TypeString,
Optional: true,
},
"type": {
Description: "Type",
Type: schema.TypeString,
Required: true,
},
"operation": {
Description: "Operation",
Type: schema.TypeString,
Optional: true,
},
"value": {
Description: "Value",
Type: schema.TypeString,
Optional: true,
},
"period": {
Description: "Period",
Type: schema.TypeFloat,
Optional: true,
Default: 0,
},
"count": {
Description: "Count",
Type: schema.TypeFloat,
Optional: true,
Default: 0,
},
},
},
},
},
},
},
"config": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"users": {
Type: schema.TypeList,
Optional: true,
Description: "Users (email)",
Elem: &schema.Schema{Type: schema.TypeString},
},
"teams": {
Type: schema.TypeList,
Optional: true,
Description: "Teams (email)",
Elem: &schema.Schema{Type: schema.TypeString},
},
"summary_time": {
Type: schema.TypeFloat,
Optional: true,
Description: "Summary Time (email daily summary only)",
},
"send_only_if_data": {
Type: schema.TypeBool,
Optional: true,
Description: "Send only if data (email daily summary only)",
},
"environments": {
Type: schema.TypeList,
Optional: true,
Description: "Environments (email daily summary only)",
Elem: &schema.Schema{Type: schema.TypeString},
},
"min_item_level": {
Type: schema.TypeString,
Optional: true,
Description: "Min item level (email daily summary only)",
},
"message_template": {
Description: "Message template (slack)",
Type: schema.TypeString,
Optional: true,
},
"channel": {
Description: "Channel (slack)",
Type: schema.TypeString,
Optional: true,
},
"show_message_buttons": {
Description: "Show message buttons (slack)",
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"service_key": {
Description: "Service key (pagerduty)",
Type: schema.TypeString,
Optional: true,
},
"url": {
Description: "URL (webhook)",
Type: schema.TypeString,
Optional: true,
},
"format": {
Description: "Format (webhook)",
Type: schema.TypeString,
Optional: true,
},
},
},
},
},
}
}
func find(slice []string, val string) bool {
for _, item := range slice {
if item == val {
return true
}
}
return false
}
func parseSet(setName string, d *schema.ResourceData) map[string]interface{} {
setMap, ok := d.GetOk(setName)
var properSetMap map[string]interface{}
if ok {
set := setMap.(*schema.Set).List()
for _, s := range set {
properSetMap, ok = s.(map[string]interface{})
if ok {
return properSetMap
}
}
}
return map[string]interface{}{}
}
func parseRule(d *schema.ResourceData) (trigger string, filters interface{}) {
rule := parseSet("rule", d)
for key, value := range rule {
if key == "trigger" {
trigger = value.(string)
} else {
filters = value
}
}
return trigger, filters
}
func cleanConfig(channel, trigger string, config map[string]interface{}) map[string]interface{} {
returnSetMap := map[string]interface{}{}
for key, v := range config {
if find(configMap[channel], key) {
returnSetMap[key] = v
}
}
switch channel {
case "email":
if trigger == "daily_summary" {
for key, v := range config {
if find(emailDailySummaryConfigList, key) {
returnSetMap[key] = v
}
}
}
case "slack":
if trigger == "deploy" || trigger == "new_version" || trigger == "exp_repeat_item" {
delete(returnSetMap, "show_message_buttons")
}
}
return returnSetMap
}
func resourceNotificationCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
trigger, filters := parseRule(d)
channel := d.Get("channel").(string)
config := parseSet("config", d)
config = cleanConfig(channel, trigger, config)
l := log.With().Str("channel", channel).Logger()
l.Info().Msg("Creating rollbar_notification resource")
c := m.(map[string]*client.RollbarAPIClient)[projectKeyToken]
c.SetHeaderResource(rollbarNotification)
n, err := c.CreateNotification(channel, filters, trigger, config)
if err != nil {
l.Err(err).Send()
d.SetId("") // removing from the state
return diag.FromErr(err)
}
l = l.With().Int("id", n.ID).Logger()
d.SetId(strconv.Itoa(n.ID))
l.Debug().Msg("Successfully created rollbar_notification resource")
return nil
}
func resourceNotificationUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
id := mustGetID(d)
trigger, filters := parseRule(d)
channel := d.Get("channel").(string)
config := parseSet("config", d)
config = cleanConfig(channel, trigger, config)
l := log.With().Str("channel", channel).Logger()
l.Info().Msg("Creating rollbar_notification resource")
c := m.(map[string]*client.RollbarAPIClient)[projectKeyToken]
c.SetHeaderResource(rollbarNotification)
n, err := c.UpdateNotification(id, channel, filters, trigger, config)
if err != nil {
l.Err(err).Send()
d.SetId("") // removing from the state
return diag.FromErr(err)
}
if n.ID != id {
err = errors.New("IDs are not equal")
l.Err(err).Send()
d.SetId("") // removing from the state
return diag.FromErr(err)
}
l = l.With().Int("id", n.ID).Logger()
l.Debug().Msg("Successfully updated rollbar_notification resource")
return nil
}
func flattenConfig(config map[string]interface{}) *schema.Set {
var out = make([]interface{}, 0)
out = append(out, config)
specResource := resourceNotification().Schema["config"].Elem.(*schema.Resource)
f := schema.HashResource(specResource)
set := schema.NewSet(f, out)
return set
}
func flattenRule(filters []interface{}, trigger string) *schema.Set {
var out = make([]interface{}, 0)
m := make(map[string]interface{})
for _, filter := range filters {
filterConv := filter.(map[string]interface{})
filterValue := filterConv["value"]
switch v := filterValue.(type) {
case int:
filterConv["value"] = strconv.Itoa(v)
case int8:
filterConv["value"] = strconv.FormatInt(int64(v), 10)
case int16:
filterConv["value"] = strconv.FormatInt(int64(v), 10)
case int32:
filterConv["value"] = strconv.FormatInt(int64(v), 10)
case int64:
filterConv["value"] = strconv.FormatInt(v, 10)
case float32:
filterConv["value"] = strconv.FormatFloat(float64(v), 'f', -1, 32)
case float64:
filterConv["value"] = strconv.FormatFloat(v, 'f', -1, 64)
}
}
m["filters"] = filters
out = append(out, m)
m["trigger"] = trigger
specResource := resourceNotification().Schema["rule"].Elem.(*schema.Resource)
f := schema.HashResource(specResource)
set := schema.NewSet(f, out)
return set
}
func resourceNotificationRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
id := mustGetID(d)
channel := d.Get("channel").(string)
l := log.With().
Int("id", id).
Logger()
l.Info().Msg("Reading rollbar_notification resource")
c := m.(map[string]*client.RollbarAPIClient)[projectKeyToken]
c.SetHeaderResource(rollbarNotification)
n, err := c.ReadNotification(id, channel)
if err == client.ErrNotFound {
d.SetId("")
l.Info().Msg("Notification not found - removed from state")
return nil
}
if err != nil {
l.Err(err).Msg("error reading rollbar_notification resource")
return diag.FromErr(err)
}
mustSet(d, "config", flattenConfig(n.Config))
mustSet(d, "rule", flattenRule(n.Filters, n.Trigger))
l.Debug().Msg("Successfully read rollbar_notification resource")
return nil
}
func resourceNotificationDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
id := mustGetID(d)
channel := d.Get("channel").(string)
l := log.With().Int("id", id).Logger()
l.Info().Msg("Deleting rollbar_notification resource")
c := m.(map[string]*client.RollbarAPIClient)[projectKeyToken]
c.SetHeaderResource(rollbarNotification)
err := c.DeleteNotification(id, channel)
if err != nil {
l.Err(err).Msg("Error deleting rollbar_notification resource")
return diag.FromErr(err)
}
l.Debug().Msg("Successfully deleted rollbar_notification resource")
return nil
}