Skip to content

Commit e3ab576

Browse files
Fix alerting contact point issues (#1284)
* Fix alerting contact point issues Closes #1277 Terraform state management is extremely weird and the updates didn't work correctly for contact points. Updates would be pushed with invalid payloads (all fields zeroed out except UID) In this PR, rather than using an extra GET call when updating, we use the state read during the refresh step to build up what receivers need to be updated/added/deleted I also added a test with the failing case posted in the issue * Add comments
1 parent c9d3343 commit e3ab576

File tree

2 files changed

+142
-29
lines changed

2 files changed

+142
-29
lines changed

internal/resources/grafana/resource_alerting_contact_point.go

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package grafana
33
import (
44
"context"
55
"fmt"
6+
"reflect"
67
"strconv"
78
"strings"
89
"time"
@@ -100,7 +101,7 @@ func readContactPoint(ctx context.Context, data *schema.ResourceData, meta inter
100101
return diag.FromErr(err)
101102
}
102103
points := resp.Payload
103-
if len(points) == 0 {
104+
if len(points) == 0 && !strings.Contains(name, ":") {
104105
// If the contact point was not found by name, try to fetch it by UID.
105106
// This is a deprecated ID format (uid;uid2;uid3)
106107
// TODO: Remove on the next major version
@@ -149,22 +150,17 @@ func updateContactPoint(ctx context.Context, data *schema.ResourceData, meta int
149150

150151
ps := unpackContactPoints(data)
151152

152-
// If the contact point already exists, we need to fetch its current state so that we can compare it to the proposed state.
153-
var currentPoints models.ContactPoints
154-
if !data.IsNewResource() {
155-
name := data.Get("name").(string)
156-
resp, err := client.Provisioning.GetContactpoints(provisioning.NewGetContactpointsParams().WithName(&name))
157-
if err != nil && !common.IsNotFoundError(err) {
158-
return diag.FromErr(err)
159-
}
160-
if resp != nil {
161-
currentPoints = resp.Payload
162-
}
163-
}
164-
165-
processedUIDs := map[string]bool{}
166153
for i := range ps {
167154
p := ps[i]
155+
if p.deleted {
156+
uid := p.tfState["uid"].(string)
157+
// If the contact point is not in the proposed state, delete it.
158+
if _, err := client.Provisioning.DeleteContactpoints(uid); err != nil {
159+
return diag.Errorf("failed to remove contact point notifier with UID %s from contact point %s: %v", uid, data.Id(), err)
160+
}
161+
continue
162+
}
163+
168164
var uid string
169165
if uid = p.tfState["uid"].(string); uid != "" {
170166
// If the contact point already has a UID, update it.
@@ -194,16 +190,6 @@ func updateContactPoint(ctx context.Context, data *schema.ResourceData, meta int
194190
// Since this is a new resource, the proposed state won't have a UID.
195191
// We need the UID so that we can later associate it with the config returned in the api response.
196192
ps[i].tfState["uid"] = uid
197-
processedUIDs[uid] = true
198-
}
199-
200-
for _, p := range currentPoints {
201-
if _, ok := processedUIDs[p.UID]; !ok {
202-
// If the contact point is not in the proposed state, delete it.
203-
if _, err := client.Provisioning.DeleteContactpoints(p.UID); err != nil {
204-
return diag.Errorf("failed to remove contact point notifier with UID %s from contact point %s: %v", p.UID, p.Name, err)
205-
}
206-
}
207193
}
208194

209195
data.SetId(MakeOrgResourceID(orgID, data.Get("name").(string)))
@@ -227,15 +213,55 @@ func deleteContactPoint(ctx context.Context, data *schema.ResourceData, meta int
227213
return nil
228214
}
229215

216+
// unpackContactPoints unpacks the contact points from the Terraform state.
217+
// It returns a slice of statePairs, which contain the Terraform state and the Grafana state for each contact point.
218+
// It also tracks receivers that should be deleted. There are two cases where a receiver should be deleted:
219+
// - The receiver is present in the "new" part of the diff, but all fields are zeroed out (except UID).
220+
// - The receiver is present in the "old" part of the diff, but not in the "new" part.
230221
func unpackContactPoints(data *schema.ResourceData) []statePair {
231222
result := make([]statePair, 0)
232223
name := data.Get("name").(string)
233224
for _, n := range notifiers {
234-
if points, ok := data.GetOk(n.meta().field); ok {
235-
for _, p := range points.(*schema.Set).List() {
225+
oldPoints, newPoints := data.GetChange(n.meta().field)
226+
oldPointsList := oldPoints.(*schema.Set).List()
227+
newPointsList := newPoints.(*schema.Set).List()
228+
if len(oldPointsList) == 0 && len(newPointsList) == 0 {
229+
continue
230+
}
231+
processedUIDs := map[string]bool{}
232+
for _, p := range newPointsList {
233+
// Checking if the point/receiver should be deleted
234+
// If all fields are zeroed out, except UID, then the receiver should be deleted
235+
deleted := false
236+
pointMap := p.(map[string]interface{})
237+
if uid, ok := pointMap["uid"]; ok && uid != "" {
238+
deleted = true
239+
processedUIDs[uid.(string)] = true
240+
}
241+
for fieldName, fieldSchema := range n.schema().Schema {
242+
if !fieldSchema.Computed && fieldSchema.Required && !reflect.ValueOf(pointMap[fieldName]).IsZero() {
243+
deleted = false
244+
break
245+
}
246+
}
247+
248+
// Add the point/receiver to the result
249+
// If it's not deleted, it will either be created or updated
250+
result = append(result, statePair{
251+
tfState: pointMap,
252+
gfState: unpackPointConfig(n, p, name),
253+
deleted: deleted,
254+
})
255+
}
256+
// Checking if the point/receiver should be deleted
257+
// If the point is not present in the "new" part of the diff, but is present in the "old" part, then the receiver should be deleted
258+
for _, p := range oldPointsList {
259+
pointMap := p.(map[string]interface{})
260+
if uid, ok := pointMap["uid"]; ok && uid != "" && !processedUIDs[uid.(string)] {
236261
result = append(result, statePair{
237262
tfState: p.(map[string]interface{}),
238-
gfState: unpackPointConfig(n, p, name),
263+
gfState: nil,
264+
deleted: true,
239265
})
240266
}
241267
}
@@ -294,7 +320,7 @@ func packCommonNotifierFields(p *models.EmbeddedContactPoint) map[string]interfa
294320
func packSettings(p *models.EmbeddedContactPoint) map[string]interface{} {
295321
settings := map[string]interface{}{}
296322
for k, v := range p.Settings.(map[string]interface{}) {
297-
settings[k] = fmt.Sprintf("%#v", v)
323+
settings[k] = fmt.Sprintf("%s", v)
298324
}
299325
return settings
300326
}
@@ -344,6 +370,7 @@ type notifierMeta struct {
344370
type statePair struct {
345371
tfState map[string]interface{}
346372
gfState *models.EmbeddedContactPoint
373+
deleted bool
347374
}
348375

349376
func packNotifierStringField(gfSettings, tfSettings *map[string]interface{}, gfKey, tfKey string) {

internal/resources/grafana/resource_alerting_contact_point_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,52 @@ func TestAccContactPoint_notifiers10_3(t *testing.T) {
405405
})
406406
}
407407

408+
func TestAccContactPoint_sensitiveData(t *testing.T) {
409+
testutils.CheckOSSTestsEnabled(t, ">=9.1.0")
410+
411+
var points models.ContactPoints
412+
name := acctest.RandString(10)
413+
414+
resource.ParallelTest(t, resource.TestCase{
415+
ProviderFactories: testutils.ProviderFactories,
416+
CheckDestroy: alertingContactPointCheckExists.destroyed(&points, nil),
417+
Steps: []resource.TestStep{
418+
{
419+
Config: testAccContactPointWithSensitiveData(name, "https://api.eu.opsgenie.com/v2/alerts", "mykey"),
420+
Check: resource.ComposeTestCheckFunc(
421+
checkAlertingContactPointExistsWithLength("grafana_contact_point.test", &points, 1),
422+
resource.TestCheckResourceAttr("grafana_contact_point.test", "name", name),
423+
resource.TestCheckResourceAttr("grafana_contact_point.test", "opsgenie.#", "1"),
424+
resource.TestCheckResourceAttr("grafana_contact_point.test", "opsgenie.0.url", "https://api.eu.opsgenie.com/v2/alerts"),
425+
resource.TestCheckResourceAttr("grafana_contact_point.test", "opsgenie.0.api_key", "mykey"),
426+
),
427+
},
428+
// Update non-sensitive data
429+
{
430+
Config: testAccContactPointWithSensitiveData(name, "http://my-url", "mykey"),
431+
Check: resource.ComposeTestCheckFunc(
432+
checkAlertingContactPointExistsWithLength("grafana_contact_point.test", &points, 1),
433+
resource.TestCheckResourceAttr("grafana_contact_point.test", "name", name),
434+
resource.TestCheckResourceAttr("grafana_contact_point.test", "opsgenie.#", "1"),
435+
resource.TestCheckResourceAttr("grafana_contact_point.test", "opsgenie.0.url", "http://my-url"),
436+
resource.TestCheckResourceAttr("grafana_contact_point.test", "opsgenie.0.api_key", "mykey"),
437+
),
438+
},
439+
// Update sensitive data
440+
{
441+
Config: testAccContactPointWithSensitiveData(name, "http://my-url", "mykey2"),
442+
Check: resource.ComposeTestCheckFunc(
443+
checkAlertingContactPointExistsWithLength("grafana_contact_point.test", &points, 1),
444+
resource.TestCheckResourceAttr("grafana_contact_point.test", "name", name),
445+
resource.TestCheckResourceAttr("grafana_contact_point.test", "opsgenie.#", "1"),
446+
resource.TestCheckResourceAttr("grafana_contact_point.test", "opsgenie.0.url", "http://my-url"),
447+
resource.TestCheckResourceAttr("grafana_contact_point.test", "opsgenie.0.api_key", "mykey2"),
448+
),
449+
},
450+
},
451+
})
452+
}
453+
408454
func TestAccContactPoint_inOrg(t *testing.T) {
409455
testutils.CheckOSSTestsEnabled(t, ">=9.1.0")
410456

@@ -495,3 +541,43 @@ func testAccContactPointInOrg(name string) string {
495541
}
496542
`, name)
497543
}
544+
545+
func testAccContactPointWithSensitiveData(name, url, apiKey string) string {
546+
return fmt.Sprintf(`
547+
resource "grafana_contact_point" "test" {
548+
name = "%[1]s"
549+
opsgenie {
550+
url = "%[2]s"
551+
api_key = "%[3]s"
552+
message = "{{ .CommonAnnotations.summary }}"
553+
send_tags_as = "tags"
554+
override_priority = true
555+
settings = {
556+
tags = <<EOT
557+
{{- range .Alerts -}}
558+
{{- range .Labels.SortedPairs -}}
559+
{{- if and (ne .Name "severity") (ne .Name "destination") -}}
560+
{{ .Name }}={{ .Value }},
561+
{{- end -}}
562+
{{- end -}}
563+
{{- end -}}
564+
EOT
565+
og_priority = <<EOT
566+
{{- range .Alerts -}}
567+
{{- range .Labels.SortedPairs -}}
568+
{{- if eq .Name "severity" -}}
569+
{{- if eq .Value "warning" -}}
570+
P5
571+
{{- else if eq .Value "critical" -}}
572+
P3
573+
{{- else -}}
574+
{{ .Value }}
575+
{{- end -}}
576+
{{- end -}}
577+
{{- end -}}
578+
{{- end -}}
579+
EOT
580+
}
581+
}
582+
}`, name, url, apiKey)
583+
}

0 commit comments

Comments
 (0)