diff --git a/internal/provider/burn_alert_resource.go b/internal/provider/burn_alert_resource.go index 9f7a7ab4..665df9c8 100644 --- a/internal/provider/burn_alert_resource.go +++ b/internal/provider/burn_alert_resource.go @@ -314,7 +314,7 @@ func (r *burnAlertResource) Read(ctx context.Context, req resource.ReadRequest, state.ID = types.StringValue(burnAlert.ID) state.AlertType = types.StringValue(string(burnAlert.AlertType)) state.SLOID = types.StringValue(burnAlert.SLO.ID) - state.Recipients = reconcileNotificationRecipientState(burnAlert.Recipients, state.Recipients) + state.Recipients = reconcileReadNotificationRecipientState(burnAlert.Recipients, state.Recipients) // Process any attributes that could be nil and add them to the state values if burnAlert.ExhaustionMinutes != nil { diff --git a/internal/provider/notification_recipients.go b/internal/provider/notification_recipients.go index 2e45d850..e07597ed 100644 --- a/internal/provider/notification_recipients.go +++ b/internal/provider/notification_recipients.go @@ -85,14 +85,15 @@ func notificationRecipientSchema(allowedTypes []client.RecipientType) schema.Set } } -func reconcileNotificationRecipientState(remote []client.NotificationRecipient, state []models.NotificationRecipientModel) []models.NotificationRecipientModel { +func reconcileReadNotificationRecipientState(remote []client.NotificationRecipient, state []models.NotificationRecipientModel) []models.NotificationRecipientModel { if state == nil { // if we don't have any state, we can't reconcile anything so just return the remote recipients return flattenNotificationRecipients(remote) } recipients := make([]models.NotificationRecipientModel, len(remote)) - // match the remote recipients to those in the state sorting out type+target vs ID + // match the remote recipients to those in the state + // in an effort to preserve the id vs type+target distinction for i, r := range remote { idx := slices.IndexFunc(state, func(s models.NotificationRecipientModel) bool { if !s.ID.IsNull() { diff --git a/internal/provider/notification_recipients_test.go b/internal/provider/notification_recipients_test.go new file mode 100644 index 00000000..a3e727c1 --- /dev/null +++ b/internal/provider/notification_recipients_test.go @@ -0,0 +1,140 @@ +package provider + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stretchr/testify/assert" + + "github.com/honeycombio/terraform-provider-honeycombio/client" + "github.com/honeycombio/terraform-provider-honeycombio/internal/models" +) + +func Test_reconcileReadNotificationRecipientState(t *testing.T) { + type args struct { + remote []client.NotificationRecipient + state []models.NotificationRecipientModel + } + tests := []struct { + name string + args args + want []models.NotificationRecipientModel + }{ + { + name: "both empty", + args: args{}, + want: []models.NotificationRecipientModel{}, + }, + { + name: "empty state", + args: args{ + remote: []client.NotificationRecipient{ + {ID: "abcd12345", Type: client.RecipientTypeEmail, Target: "test@example.com"}, + }, + state: []models.NotificationRecipientModel{}, + }, + want: []models.NotificationRecipientModel{ + {ID: types.StringValue("abcd12345"), Type: types.StringValue("email"), Target: types.StringValue("test@example.com")}, + }, + }, + { + name: "empty remote", + args: args{ + remote: []client.NotificationRecipient{}, + state: []models.NotificationRecipientModel{ + {ID: types.StringValue("abcd12345"), Type: types.StringValue("email"), Target: types.StringValue("test@example.com")}, + }, + }, + want: []models.NotificationRecipientModel{}, + }, + { + name: "remote and state reconciled", + args: args{ + remote: []client.NotificationRecipient{ + {ID: "abcd12345", Type: client.RecipientTypeEmail, Target: "test@example.com"}, + {ID: "efgh67890", Type: client.RecipientTypeSlack, Target: "#test-channel"}, + }, + state: []models.NotificationRecipientModel{ + {ID: types.StringValue("abcd12345")}, // defined by ID + {Type: types.StringValue("slack"), Target: types.StringValue("#test-channel")}, // defined by type+target + }, + }, + want: []models.NotificationRecipientModel{ + {ID: types.StringValue("abcd12345")}, + {Type: types.StringValue("slack"), Target: types.StringValue("#test-channel")}, + }, + }, + { + name: "remote has additional recipients", + args: args{ + remote: []client.NotificationRecipient{ + {ID: "abcd12345", Type: client.RecipientTypeEmail, Target: "test@example.com"}, + {ID: "efgh67890", Type: client.RecipientTypeSlack, Target: "#test-channel"}, + {ID: "qrsty3847", Type: client.RecipientTypeSlack, Target: "#test-alerts"}, + { + ID: "ijkl13579", + Type: client.RecipientTypePagerDuty, + Target: "test-pagerduty", + Details: &client.NotificationRecipientDetails{ + PDSeverity: client.PDSeverityWARNING, + }}, + }, + state: []models.NotificationRecipientModel{ + {ID: types.StringValue("abcd12345")}, // defined by ID + {Type: types.StringValue("slack"), Target: types.StringValue("#test-channel")}, // defined by type+target + }, + }, + want: []models.NotificationRecipientModel{ + {ID: types.StringValue("abcd12345")}, + {Type: types.StringValue("slack"), Target: types.StringValue("#test-channel")}, + {ID: types.StringValue("qrsty3847"), Type: types.StringValue("slack"), Target: types.StringValue("#test-alerts")}, + { + ID: types.StringValue("ijkl13579"), + Type: types.StringValue("pagerduty"), + Target: types.StringValue("test-pagerduty"), + Details: []models.NotificationRecipientDetailsModel{ + {PDSeverity: types.StringValue("warning")}, + }, + }, + }, + }, + { + name: "state has additional recipients", + args: args{ + remote: []client.NotificationRecipient{ + {ID: "efgh67890", Type: client.RecipientTypeSlack, Target: "#test-foo"}, + }, + state: []models.NotificationRecipientModel{ + {ID: types.StringValue("abcd12345")}, + {Type: types.StringValue("slack"), Target: types.StringValue("#test-foo")}, + {ID: types.StringValue("ijkl13579"), Details: []models.NotificationRecipientDetailsModel{{PDSeverity: types.StringValue("warning")}}}, + }, + }, + want: []models.NotificationRecipientModel{ + {Type: types.StringValue("slack"), Target: types.StringValue("#test-foo")}, + }, + }, + { + name: "state has totally unmatched recipients", + args: args{ + remote: []client.NotificationRecipient{ + {ID: "efgh67890", Type: client.RecipientTypeSlack, Target: "#test-foo"}, + }, + state: []models.NotificationRecipientModel{ + {ID: types.StringValue("abcd12345")}, + {Type: types.StringValue("slack"), Target: types.StringValue("#test-channel")}, + {ID: types.StringValue("ijkl13579"), Details: []models.NotificationRecipientDetailsModel{{PDSeverity: types.StringValue("warning")}}}, + }, + }, + want: []models.NotificationRecipientModel{ + {ID: types.StringValue("efgh67890"), Type: types.StringValue("slack"), Target: types.StringValue("#test-foo")}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, reconcileReadNotificationRecipientState(tt.args.remote, tt.args.state)) + }) + } +} diff --git a/internal/provider/trigger_resource.go b/internal/provider/trigger_resource.go index 06018c33..d729e5e9 100644 --- a/internal/provider/trigger_resource.go +++ b/internal/provider/trigger_resource.go @@ -286,7 +286,7 @@ func (r *triggerResource) Read(ctx context.Context, req resource.ReadRequest, re state.Threshold = flattenTriggerThreshold(trigger.Threshold) state.Frequency = types.Int64Value(int64(trigger.Frequency)) state.EvaluationSchedule = flattenTriggerEvaluationSchedule(trigger) - state.Recipients = reconcileNotificationRecipientState(trigger.Recipients, state.Recipients) + state.Recipients = reconcileReadNotificationRecipientState(trigger.Recipients, state.Recipients) resp.Diagnostics.Append(resp.State.Set(ctx, state)...) }