Skip to content
Open
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
124 changes: 106 additions & 18 deletions newrelic/data_source_newrelic_notifications_destination.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
nr "github.com/newrelic/newrelic-client-go/v2/newrelic"
)

func dataSourceNewRelicNotificationDestination() *schema.Resource {
Expand Down Expand Up @@ -43,10 +44,11 @@ func dataSourceNewRelicNotificationDestination() *schema.Resource {
Description: "The exact name of the destination. Uses an exact match.",
},
"account_id": {
Type: schema.TypeInt,
Optional: true,
Computed: true,
Description: "The account ID under which to put the destination.",
Type: schema.TypeInt,
Optional: true,
Computed: true,
ConflictsWith: []string{"scope"},
Description: "The account ID under which to put the destination.",
},
"type": {
Type: schema.TypeString,
Expand Down Expand Up @@ -84,10 +86,11 @@ func dataSourceNewRelicNotificationDestination() *schema.Resource {
},
},
"scope": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Description: "Scope of the destination",
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
ConflictsWith: []string{"account_id"},
Description: "Scope of the destination",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"type": {
Expand All @@ -99,7 +102,6 @@ func dataSourceNewRelicNotificationDestination() *schema.Resource {
"id": {
Type: schema.TypeString,
Required: true,
Sensitive: true,
Description: "The ID of the scope (Organization UUID for ORGANIZATION scope, Account ID for ACCOUNT scope)",
},
},
Expand Down Expand Up @@ -137,7 +139,27 @@ func dataSourceNewRelicNotificationDestinationRead(ctx context.Context, d *schem

// Case 1: No scope provided OR Case 2: ACCOUNT scope - use regular query (no scopeTypes filter)
if scope == nil || scope.Type == notifications.EntityScopeTypeInputTypes.ACCOUNT {
destinationResponse, err := client.Notifications.GetDestinationsWithContext(updatedContext, accountID, "", filters, sorter)
return getDestinationWithAccountScope(updatedContext, client, accountID, filters, sorter, idValue, nameValue, exactNameValue, scope, d)
}

// Case 3: ORGANIZATION scope - use scope-aware query
return getDestinationWithOrganizationScope(updatedContext, client, accountID, filters, sorter, idValue, nameValue, exactNameValue, scope, d)
}

// getDestinationWithAccountScope handles retrieval for no scope or ACCOUNT scope destinations
func getDestinationWithAccountScope(
ctx context.Context,
client *nr.NewRelic,
accountID int,
filters ai.AiNotificationsDestinationFilter,
sorter notifications.AiNotificationsDestinationSorter,
idValue, nameValue, exactNameValue string,
scope *notifications.EntityScopeInput,
d *schema.ResourceData,
) diag.Diagnostics {
// If ACCOUNT scope is explicitly provided, use scope-aware API to filter by scope type and id
if scope != nil && scope.Type == notifications.EntityScopeTypeInputTypes.ACCOUNT {
destinationResponse, err := client.Notifications.GetDestinationsWithScopeWithContext(ctx, accountID, "", filters, sorter)
if err != nil {
if _, ok := err.(*errors.NotFound); ok {
d.SetId("")
Expand All @@ -148,20 +170,29 @@ func dataSourceNewRelicNotificationDestinationRead(ctx context.Context, d *schem

if len(destinationResponse.Entities) == 0 {
d.SetId("")
return diag.FromErr(fmt.Errorf("no destination found. accountID=%d, filters={id=%s, name=%s, exactName=%s}",
accountID, filters.ID, filters.Name, filters.ExactName))
if err := getNotificationDestinationNotFoundError(idValue, nameValue, exactNameValue); err != nil {
return diag.FromErr(err)
}
return nil
}

respErrors := buildAiNotificationsResponseErrors(destinationResponse.Errors)
if len(respErrors) > 0 {
return respErrors
}

return diag.FromErr(flattenNotificationDestinationDataSource(&destinationResponse.Entities[0], d))
// Filter by ACCOUNT scope type and id
for _, dest := range destinationResponse.Entities {
if dest.Scope != nil && string(dest.Scope.Type) == string(scope.Type) && dest.Scope.ID == scope.ID {
return diag.FromErr(flattenNotificationDestinationDataSourceWithScope(&dest, d))
}
}
d.SetId("")
return diag.FromErr(fmt.Errorf("no destination found matching the provided scope type %s and id %s", scope.Type, scope.ID))
}

// Case 3: ORGANIZATION scope - use scope-aware query
destinationResponse, err := client.Notifications.GetDestinationsWithScopeWithContext(updatedContext, accountID, "", filters, sorter)
// No scope provided - use scope-aware API to get scope info for the destination
destinationResponse, err := client.Notifications.GetDestinationsWithScopeWithContext(ctx, accountID, "", filters, sorter)
if err != nil {
if _, ok := err.(*errors.NotFound); ok {
d.SetId("")
Expand All @@ -172,8 +203,46 @@ func dataSourceNewRelicNotificationDestinationRead(ctx context.Context, d *schem

if len(destinationResponse.Entities) == 0 {
d.SetId("")
return diag.FromErr(fmt.Errorf("no destination found. accountID=%d, filters={id=%s, name=%s, exactName=%s}, totalCount=%d",
accountID, filters.ID, filters.Name, filters.ExactName, destinationResponse.TotalCount))
if err := getNotificationDestinationNotFoundError(idValue, nameValue, exactNameValue); err != nil {
return diag.FromErr(err)
}
return nil
}

respErrors := buildAiNotificationsResponseErrors(destinationResponse.Errors)
if len(respErrors) > 0 {
return respErrors
}

return diag.FromErr(flattenNotificationDestinationDataSourceWithScope(&destinationResponse.Entities[0], d))
}

// getDestinationWithOrganizationScope handles retrieval for ORGANIZATION scope destinations
func getDestinationWithOrganizationScope(
ctx context.Context,
client *nr.NewRelic,
accountID int,
filters ai.AiNotificationsDestinationFilter,
sorter notifications.AiNotificationsDestinationSorter,
idValue, nameValue, exactNameValue string,
scope *notifications.EntityScopeInput,
d *schema.ResourceData,
) diag.Diagnostics {
destinationResponse, err := client.Notifications.GetDestinationsWithScopeWithContext(ctx, accountID, "", filters, sorter)
if err != nil {
if _, ok := err.(*errors.NotFound); ok {
d.SetId("")
return nil
}
return diag.FromErr(err)
}

if len(destinationResponse.Entities) == 0 {
d.SetId("")
if err := getNotificationDestinationNotFoundError(idValue, nameValue, exactNameValue); err != nil {
return diag.FromErr(err)
}
return nil
}

respErrors := buildAiNotificationsResponseErrors(destinationResponse.Errors)
Expand All @@ -184,10 +253,29 @@ func dataSourceNewRelicNotificationDestinationRead(ctx context.Context, d *schem
// Filter by ORGANIZATION scope
for _, dest := range destinationResponse.Entities {
if dest.Scope != nil && string(dest.Scope.Type) == string(scope.Type) && dest.Scope.ID == scope.ID {
return diag.FromErr(flattenNotificationDestinationDataSource(&dest.AiNotificationsDestination, d))
return diag.FromErr(flattenNotificationDestinationDataSourceWithScope(&dest, d))
}
}

d.SetId("")
return diag.FromErr(fmt.Errorf("no destination found matching the provided scope type %s and id %s", scope.Type, scope.ID))
}

// getNotificationDestinationNotFoundError returns an appropriate error message based on which filter attribute was provided
func getNotificationDestinationNotFoundError(idValue, nameValue, exactNameValue string) error {
filterAttributes := []struct {
value string
name string
}{
{idValue, "id"},
{nameValue, "name"},
{exactNameValue, "exact_name"},
}

for _, attr := range filterAttributes {
if attr.value != "" {
return fmt.Errorf("the %s provided does not match any New Relic notification destination", attr.name)
}
}
return nil
}
69 changes: 62 additions & 7 deletions newrelic/data_source_newrelic_notifications_destination_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,28 @@ func TestAccNewRelicNotificationDestinationDataSource_WithSecureURL(t *testing.T
})
}

func TestAccNewRelicNotificationDestinationDataSource_WithScope(t *testing.T) {
// TODO: Uncomment when organization environment variables are available in GitHub Actions
// func TestAccNewRelicNotificationDestinationDataSource_WithOrganizationScope(t *testing.T) {
// dataSourceName := "data.newrelic_notification_destination.foo"
// rand := acctest.RandString(5)
// rName := fmt.Sprintf("tf-notifications-test-%s", rand)
//
// resource.ParallelTest(t, resource.TestCase{
// PreCheck: func() { testAccPreCheckEnvVars(t) },
// Providers: testAccProviders,
// Steps: []resource.TestStep{
// {
// Config: testAccNewRelicNotificationsDestinationDataSourceConfigWithOrganizationScope(rName),
// Check: resource.ComposeTestCheckFunc(
// testAccNewRelicNotificationDestination(dataSourceName),
// resource.TestCheckResourceAttr(dataSourceName, "scope.0.type", "ORGANIZATION"),
// ),
// },
// },
// })
// }

func TestAccNewRelicNotificationDestinationDataSource_WithAccountScope(t *testing.T) {
dataSourceName := "data.newrelic_notification_destination.foo"
rand := acctest.RandString(5)
rName := fmt.Sprintf("tf-notifications-test-%s", rand)
Expand All @@ -105,10 +126,10 @@ func TestAccNewRelicNotificationDestinationDataSource_WithScope(t *testing.T) {
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccNewRelicNotificationsDestinationDataSourceConfigWithScope(rName),
Config: testAccNewRelicNotificationsDestinationDataSourceConfigWithAccountScope(rName),
Check: resource.ComposeTestCheckFunc(
testAccNewRelicNotificationDestination(dataSourceName),
resource.TestCheckResourceAttr(dataSourceName, "scope.0.type", "ORGANIZATION"),
resource.TestCheckResourceAttr(dataSourceName, "scope.0.type", "ACCOUNT"),
),
},
},
Expand Down Expand Up @@ -196,7 +217,37 @@ func testAccNewRelicNotificationsDestinationDataSourceConfigWithSecureURL(name s
`, name)
}

func testAccNewRelicNotificationsDestinationDataSourceConfigWithScope(name string) string {
// TODO: Uncomment when organization environment variables are available in GitHub Actions
// func testAccNewRelicNotificationsDestinationDataSourceConfigWithOrganizationScope(name string) string {
// return fmt.Sprintf(`
// resource "newrelic_notification_destination" "foo" {
// name = "%s"
// type = "WEBHOOK"
// active = true
//
// property {
// key = "url"
// value = "https://webhook.site/"
// }
//
// property {
// key = "source"
// value = "terraform"
// }
//
// scope {
// type = "ORGANIZATION"
// id = "fb33fea3-4d7e-4736-9701-acb59a634fdf"
// }
// }
//
// data "newrelic_notification_destination" "foo" {
// name = newrelic_notification_destination.foo.name
// }
// `, name)
// }

func testAccNewRelicNotificationsDestinationDataSourceConfigWithAccountScope(name string) string {
return fmt.Sprintf(`
resource "newrelic_notification_destination" "foo" {
name = "%s"
Expand All @@ -214,15 +265,19 @@ func testAccNewRelicNotificationsDestinationDataSourceConfigWithScope(name strin
}

scope {
type = "ORGANIZATION"
id = "f47ac10b-58cc-4372-a567-0e02b2c3d479"
type = "ACCOUNT"
id = "%d"
}
}

data "newrelic_notification_destination" "foo" {
name = newrelic_notification_destination.foo.name
scope {
type = "ACCOUNT"
id = "%d"
}
}
`, name)
`, name, testAccountID, testAccountID)
}

func testAccNewRelicNotificationDestination(n string) resource.TestCheckFunc {
Expand Down
47 changes: 35 additions & 12 deletions newrelic/resource_newrelic_notifications_destination.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,8 @@ func resourceNewRelicNotificationDestination() *schema.Resource {
Description: fmt.Sprintf("(Required) The scope type of the destination. One of: (%s).", strings.Join(listValidNotificationsScopeTypes(), ", ")),
},
"id": {
Type: schema.TypeString,
Required: true,
Sensitive: true,
Type: schema.TypeString,
Required: true,
},
},
},
Expand Down Expand Up @@ -313,7 +312,7 @@ func resourceNewRelicNotificationDestinationCreate(ctx context.Context, d *schem
providerConfig := meta.(*ProviderConfig)
accountID := selectAccountID(providerConfig, d)
updatedContext := updateContextWithAccountID(ctx, accountID)

scope := expandNotificationDestinationScope(d)
var destinationResponse *notifications.AiNotificationsDestinationResponse
if scope != nil {
Expand Down Expand Up @@ -379,10 +378,9 @@ func resourceNewRelicNotificationDestinationRead(ctx context.Context, d *schema.
if len(errs) > 0 {
return errs
}
return diag.FromErr(flattenNotificationDestination(&dest.AiNotificationsDestination, d))
return diag.FromErr(flattenNotificationDestinationWithScope(&dest, d))
}
}

d.SetId("")
return nil
}
Expand All @@ -398,7 +396,32 @@ func resourceNewRelicNotificationDestinationRead(ctx context.Context, d *schema.
return diag.FromErr(err)
}

// If regular API returns no results, check scope-aware API for org-scoped destinations
// This handles import of org-scoped destinations which aren't visible via regular API
if len(destinationResponse.Entities) == 0 {
scopeResponse, scopeErr := client.Notifications.GetDestinationsWithScopeWithContext(updatedContext, accountID, "", filters, sorter)
if scopeErr != nil {
if _, ok := scopeErr.(*errors.NotFound); ok {
d.SetId("")
return nil
}
return diag.FromErr(scopeErr)
}

// Look for an org-scoped destination with matching ID
for _, scopedDest := range scopeResponse.Entities {
if scopedDest.ID == d.Id() && scopedDest.Scope != nil &&
scopedDest.Scope.Type == notifications.EntityScopeTypeInputTypes.ORGANIZATION {
// Found org-scoped destination, set scope and flatten
errs := buildAiNotificationsResponseErrors(scopeResponse.Errors)
if len(errs) > 0 {
return errs
}
return diag.FromErr(flattenNotificationDestinationWithScope(&scopedDest, d))
}
}

// No matching destination found
d.SetId("")
return nil
}
Expand Down Expand Up @@ -435,9 +458,9 @@ func resourceNewRelicNotificationDestinationUpdate(ctx context.Context, d *schem
filters := ai.AiNotificationsDestinationFilter{ID: d.Id()}
sorter := notifications.AiNotificationsDestinationSorter{}

destResponse, err := client.Notifications.GetDestinationsWithScopeWithContext(updatedContext, accountID, "", filters, sorter)
if err != nil {
return diag.FromErr(err)
destResponse, lookupErr := client.Notifications.GetDestinationsWithScopeWithContext(updatedContext, accountID, "", filters, sorter)
if lookupErr != nil {
return diag.FromErr(lookupErr)
}

// Find destination matching the org scope
Expand Down Expand Up @@ -484,9 +507,9 @@ func resourceNewRelicNotificationDestinationDelete(ctx context.Context, d *schem
filters := ai.AiNotificationsDestinationFilter{ID: d.Id()}
sorter := notifications.AiNotificationsDestinationSorter{}

destResponse, err := client.Notifications.GetDestinationsWithScopeWithContext(updatedContext, accountID, "", filters, sorter)
if err != nil {
return diag.FromErr(err)
destResponse, lookupErr := client.Notifications.GetDestinationsWithScopeWithContext(updatedContext, accountID, "", filters, sorter)
if lookupErr != nil {
return diag.FromErr(lookupErr)
}

// Find destination matching the org scope
Expand Down
Loading
Loading