Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alerting Contact Points: Refer by name #1247

Merged
merged 2 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions docs/resources/contact_point.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,26 @@ resource "grafana_contact_point" "my_contact_point" {

### Optional

- `alertmanager` (Block List) A contact point that sends notifications to other Alertmanager instances. (see [below for nested schema](#nestedblock--alertmanager))
- `dingding` (Block List) A contact point that sends notifications to DingDing. (see [below for nested schema](#nestedblock--dingding))
- `discord` (Block List) A contact point that sends notifications as Discord messages (see [below for nested schema](#nestedblock--discord))
- `email` (Block List) A contact point that sends notifications to an email address. (see [below for nested schema](#nestedblock--email))
- `googlechat` (Block List) A contact point that sends notifications to Google Chat. (see [below for nested schema](#nestedblock--googlechat))
- `kafka` (Block List) A contact point that publishes notifications to Apache Kafka topics. (see [below for nested schema](#nestedblock--kafka))
- `line` (Block List) A contact point that sends notifications to LINE.me. (see [below for nested schema](#nestedblock--line))
- `oncall` (Block List) A contact point that sends notifications to Grafana On-Call. (see [below for nested schema](#nestedblock--oncall))
- `opsgenie` (Block List) A contact point that sends notifications to OpsGenie. (see [below for nested schema](#nestedblock--opsgenie))
- `pagerduty` (Block List) A contact point that sends notifications to PagerDuty. (see [below for nested schema](#nestedblock--pagerduty))
- `pushover` (Block List) A contact point that sends notifications to Pushover. (see [below for nested schema](#nestedblock--pushover))
- `sensugo` (Block List) A contact point that sends notifications to SensuGo. (see [below for nested schema](#nestedblock--sensugo))
- `slack` (Block List) A contact point that sends notifications to Slack. (see [below for nested schema](#nestedblock--slack))
- `teams` (Block List) A contact point that sends notifications to Microsoft Teams. (see [below for nested schema](#nestedblock--teams))
- `telegram` (Block List) A contact point that sends notifications to Telegram. (see [below for nested schema](#nestedblock--telegram))
- `threema` (Block List) A contact point that sends notifications to Threema. (see [below for nested schema](#nestedblock--threema))
- `victorops` (Block List) A contact point that sends notifications to VictorOps (now known as Splunk OnCall). (see [below for nested schema](#nestedblock--victorops))
- `webex` (Block List) A contact point that sends notifications to Cisco Webex. (see [below for nested schema](#nestedblock--webex))
- `webhook` (Block List) A contact point that sends notifications to an arbitrary webhook, using the Prometheus webhook format defined here: https://prometheus.io/docs/alerting/latest/configuration/#webhook_config (see [below for nested schema](#nestedblock--webhook))
- `wecom` (Block List) A contact point that sends notifications to WeCom. (see [below for nested schema](#nestedblock--wecom))
- `alertmanager` (Block Set) A contact point that sends notifications to other Alertmanager instances. (see [below for nested schema](#nestedblock--alertmanager))
- `dingding` (Block Set) A contact point that sends notifications to DingDing. (see [below for nested schema](#nestedblock--dingding))
- `discord` (Block Set) A contact point that sends notifications as Discord messages (see [below for nested schema](#nestedblock--discord))
- `email` (Block Set) A contact point that sends notifications to an email address. (see [below for nested schema](#nestedblock--email))
- `googlechat` (Block Set) A contact point that sends notifications to Google Chat. (see [below for nested schema](#nestedblock--googlechat))
- `kafka` (Block Set) A contact point that publishes notifications to Apache Kafka topics. (see [below for nested schema](#nestedblock--kafka))
- `line` (Block Set) A contact point that sends notifications to LINE.me. (see [below for nested schema](#nestedblock--line))
- `oncall` (Block Set) A contact point that sends notifications to Grafana On-Call. (see [below for nested schema](#nestedblock--oncall))
- `opsgenie` (Block Set) A contact point that sends notifications to OpsGenie. (see [below for nested schema](#nestedblock--opsgenie))
- `pagerduty` (Block Set) A contact point that sends notifications to PagerDuty. (see [below for nested schema](#nestedblock--pagerduty))
- `pushover` (Block Set) A contact point that sends notifications to Pushover. (see [below for nested schema](#nestedblock--pushover))
- `sensugo` (Block Set) A contact point that sends notifications to SensuGo. (see [below for nested schema](#nestedblock--sensugo))
- `slack` (Block Set) A contact point that sends notifications to Slack. (see [below for nested schema](#nestedblock--slack))
- `teams` (Block Set) A contact point that sends notifications to Microsoft Teams. (see [below for nested schema](#nestedblock--teams))
- `telegram` (Block Set) A contact point that sends notifications to Telegram. (see [below for nested schema](#nestedblock--telegram))
- `threema` (Block Set) A contact point that sends notifications to Threema. (see [below for nested schema](#nestedblock--threema))
- `victorops` (Block Set) A contact point that sends notifications to VictorOps (now known as Splunk OnCall). (see [below for nested schema](#nestedblock--victorops))
- `webex` (Block Set) A contact point that sends notifications to Cisco Webex. (see [below for nested schema](#nestedblock--webex))
- `webhook` (Block Set) A contact point that sends notifications to an arbitrary webhook, using the Prometheus webhook format defined here: https://prometheus.io/docs/alerting/latest/configuration/#webhook_config (see [below for nested schema](#nestedblock--webhook))
- `wecom` (Block Set) A contact point that sends notifications to WeCom. (see [below for nested schema](#nestedblock--wecom))

### Read-Only

Expand Down
6 changes: 5 additions & 1 deletion internal/common/errcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ func CheckReadError(resourceType string, d *schema.ResourceData, err error) (ret
return diag.Errorf("error reading %s with ID`%s`: %v", resourceType, d.Id(), err), true
}

return WarnMissing(resourceType, d), true
}

func WarnMissing(resourceType string, d *schema.ResourceData) diag.Diagnostics {
log.Printf("[WARN] removing %s with ID %q from state because it no longer exists in grafana", resourceType, d.Id())
var diags diag.Diagnostics
diags = append(diags, diag.Diagnostic{
Expand All @@ -33,7 +37,7 @@ func CheckReadError(resourceType string, d *schema.ResourceData, err error) (ret
Detail: fmt.Sprintf("%q will be recreated when you apply", d.Id()),
})
d.SetId("")
return diags, true
return diags
}

func IsNotFoundError(err error) bool {
Expand Down
186 changes: 90 additions & 96 deletions internal/resources/grafana/resource_alerting_contact_point.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package grafana
import (
"context"
"fmt"
"log"
"strings"

"github.com/grafana/grafana-openapi-client-go/client/provisioning"
Expand Down Expand Up @@ -53,13 +52,14 @@ This resource requires Grafana 9.1.0 or later.
DeleteContext: deleteContactPoint,

Importer: &schema.ResourceImporter{
StateContext: importContactPoint,
StateContext: schema.ImportStatePassthroughContext,
},

SchemaVersion: 0,
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
ForceNew: true,
Required: true,
Description: "The name of the contact point.",
},
Expand All @@ -74,7 +74,7 @@ This resource requires Grafana 9.1.0 or later.

for _, n := range notifiers {
resource.Schema[n.meta().field] = &schema.Schema{
Type: schema.TypeList,
Type: schema.TypeSet,
Optional: true,
Description: n.meta().desc,
Elem: n.schema(),
Expand All @@ -85,62 +85,55 @@ This resource requires Grafana 9.1.0 or later.
return resource
}

func importContactPoint(ctx context.Context, data *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
name := data.Id()
client := OAPIGlobalClient(meta) // TODO: Support org-scoped contact points

params := provisioning.NewGetContactpointsParams().WithName(&name)
resp, err := client.Provisioning.GetContactpoints(params)
if err != nil {
return nil, err
}
ps := resp.Payload

if len(ps) == 0 {
return nil, fmt.Errorf("no contact points with the given name were found to import")
}

uids := make([]string, 0, len(ps))
for _, p := range ps {
uids = append(uids, p.UID)
}

data.SetId(packUIDs(uids))
return []*schema.ResourceData{data}, nil
}

func readContactPoint(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := OAPIGlobalClient(meta) // TODO: Support org-scoped contact points

uidsToFetch := unpackUIDs(data.Id())

resp, err := client.Provisioning.GetContactpoints(nil)
// First, try to fetch the contact point by name.
// If that fails, try to fetch it by the UID of its notifiers.
name := data.Id()
resp, err := client.Provisioning.GetContactpoints(provisioning.NewGetContactpointsParams().WithName(&name))
if err != nil {
return diag.FromErr(err)
}
contactPointByUID := map[string]*models.EmbeddedContactPoint{}
for _, p := range resp.Payload {
contactPointByUID[p.UID] = p
}
points := resp.Payload
if len(points) == 0 {
// If the contact point was not found by name, try to fetch it by UID.
// This is a deprecated ID format (uid;uid2;uid3)
// TODO: Remove on the next major version
uidsMap := map[string]bool{}
for _, uid := range strings.Split(data.Id(), ";") {
uidsMap[uid] = false
}
resp, err := client.Provisioning.GetContactpoints(provisioning.NewGetContactpointsParams())
if err != nil {
return diag.FromErr(err)
}
for i, p := range resp.Payload {
if _, ok := uidsMap[p.UID]; !ok {
continue
}
uidsMap[p.UID] = true
points = append(points, p)
if i > 0 && p.Name != points[0].Name {
return diag.FromErr(fmt.Errorf("contact point with UID %s has a different name (%s) than the contact point with UID %s (%s)", p.UID, p.Name, points[0].UID, points[0].Name))
}
}

points := []*models.EmbeddedContactPoint{}
for _, uid := range uidsToFetch {
p, ok := contactPointByUID[uid]
if !ok {
log.Printf("[WARN] removing contact point %s from state because it no longer exists in grafana", uid)
continue
for uid, found := range uidsMap {
if !found {
// Since this is an import, all UIDs should exist
return diag.FromErr(fmt.Errorf("contact point with UID %s was not found", uid))
}
}
points = append(points, p)
}

if len(points) == 0 {
return common.WarnMissing("contact point", data)
}

if err := packContactPoints(points, data); err != nil {
return diag.FromErr(err)
}
uids := make([]string, 0, len(points))
for _, p := range points {
uids = append(uids, p.UID)
}
data.SetId(packUIDs(uids))

return nil
}
Expand All @@ -151,42 +144,56 @@ func updateContactPoint(ctx context.Context, data *schema.ResourceData, meta int
defer lock.Unlock()
client := OAPIGlobalClient(meta) // TODO: Support org-scoped contact points

existingUIDs := unpackUIDs(data.Id())
ps := unpackContactPoints(data)

unprocessedUIDs := toUIDSet(existingUIDs)
newUIDs := make([]string, 0, len(ps))
for i := range ps {
p := ps[i].gfState
delete(unprocessedUIDs, p.UID)
params := provisioning.NewPutContactpointParams().WithUID(p.UID).WithBody(p)
_, err := client.Provisioning.PutContactpoint(params)
if err != nil {
if common.IsNotFoundError(err) {
params := provisioning.NewPostContactpointsParams().WithBody(p)
resp, err := client.Provisioning.PostContactpoints(params)
ps[i].tfState["uid"] = resp.Payload.UID
newUIDs = append(newUIDs, resp.Payload.UID)
if err != nil {
return diag.FromErr(err)
}
continue
}
// If the contact point already exists, we need to fetch its current state so that we can compare it to the proposed state.
var currentPoints models.ContactPoints
if !data.IsNewResource() {
name := data.Id()
resp, err := client.Provisioning.GetContactpoints(provisioning.NewGetContactpointsParams().WithName(&name))
if err != nil && !common.IsNotFoundError(err) {
return diag.FromErr(err)
}
newUIDs = append(newUIDs, p.UID)
if resp != nil {
currentPoints = resp.Payload
}
}

// Any UIDs still left in the state that we haven't seen must map to deleted receivers.
// Delete them on the server and drop them from state.
for u := range unprocessedUIDs {
if _, err := client.Provisioning.DeleteContactpoints(u); err != nil {
return diag.FromErr(err)
processedUIDs := map[string]bool{}
for i := range ps {
p := ps[i]
var uid string
if uid = p.tfState["uid"].(string); uid != "" {
// If the contact point already has a UID, update it.
params := provisioning.NewPutContactpointParams().WithUID(uid).WithBody(p.gfState)
if _, err := client.Provisioning.PutContactpoint(params); err != nil {
return diag.FromErr(err)
}
} else {
// If the contact point does not have a UID, create it.
resp, err := client.Provisioning.PostContactpoints(provisioning.NewPostContactpointsParams().WithBody(p.gfState))
if err != nil {
return diag.FromErr(err)
}
uid = resp.Payload.UID
}

// Since this is a new resource, the proposed state won't have a UID.
// We need the UID so that we can later associate it with the config returned in the api response.
ps[i].tfState["uid"] = uid
processedUIDs[uid] = true
}

data.SetId(packUIDs(newUIDs))
for _, p := range currentPoints {
if _, ok := processedUIDs[p.UID]; !ok {
// If the contact point is not in the proposed state, delete it.
if _, err := client.Provisioning.DeleteContactpoints(p.UID); err != nil {
return diag.Errorf("failed to remove contact point notifier with UID %s from contact point %s: %v", p.UID, p.Name, err)
}
}
}

data.SetId(data.Get("name").(string))
return readContactPoint(ctx, data, meta)
}

Expand All @@ -196,23 +203,27 @@ func deleteContactPoint(ctx context.Context, data *schema.ResourceData, meta int
defer lock.Unlock()
client := OAPIGlobalClient(meta) // TODO: Support org-scoped contact points

uids := unpackUIDs(data.Id())
name := data.Id()
resp, err := client.Provisioning.GetContactpoints(provisioning.NewGetContactpointsParams().WithName(&name))
if err, shouldReturn := common.CheckReadError("contact point", data, err); shouldReturn {
return err
}

for _, uid := range uids {
if _, err := client.Provisioning.DeleteContactpoints(uid); err != nil {
for _, cp := range resp.Payload {
if _, err := client.Provisioning.DeleteContactpoints(cp.UID); err != nil {
return diag.FromErr(err)
}
}

return diag.Diagnostics{}
return nil
}

func unpackContactPoints(data *schema.ResourceData) []statePair {
result := make([]statePair, 0)
name := data.Get("name").(string)
for _, n := range notifiers {
if points, ok := data.GetOk(n.meta().field); ok {
for _, p := range points.([]interface{}) {
for _, p := range points.(*schema.Set).List() {
result = append(result, statePair{
tfState: p.(map[string]interface{}),
gfState: unpackPointConfig(n, p, name),
Expand Down Expand Up @@ -240,6 +251,7 @@ func packContactPoints(ps []*models.EmbeddedContactPoint, data *schema.ResourceD
pointsPerNotifier := map[notifier][]interface{}{}
for _, p := range ps {
data.Set("name", p.Name)
data.SetId(p.Name)

for _, n := range notifiers {
if *p.Type == n.meta().typeStr {
Expand Down Expand Up @@ -307,24 +319,6 @@ func commonNotifierResource() *schema.Resource {
}
}

const UIDSeparator = ";"

func packUIDs(uids []string) string {
return strings.Join(uids, UIDSeparator)
}

func unpackUIDs(packed string) []string {
return strings.Split(packed, UIDSeparator)
}

func toUIDSet(uids []string) map[string]bool {
set := map[string]bool{}
for _, uid := range uids {
set[uid] = true
}
return set
}

type notifier interface {
meta() notifierMeta
schema() *schema.Resource
Expand Down Expand Up @@ -367,7 +361,7 @@ func unpackNotifierStringField(tfSettings, gfSettings *map[string]interface{}, t

func getNotifierConfigFromStateWithUID(data *schema.ResourceData, n notifier, uid string) map[string]interface{} {
if points, ok := data.GetOk(n.meta().field); ok {
for _, pt := range points.([]interface{}) {
for _, pt := range points.(*schema.Set).List() {
config := pt.(map[string]interface{})
if config["uid"] == uid {
return config
Expand Down
Loading