diff --git a/newrelic/data_source_newrelic_notifications_destination.go b/newrelic/data_source_newrelic_notifications_destination.go index 4df41bb2c..37d85648c 100644 --- a/newrelic/data_source_newrelic_notifications_destination.go +++ b/newrelic/data_source_newrelic_notifications_destination.go @@ -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 { @@ -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, @@ -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": { @@ -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)", }, }, @@ -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("") @@ -148,8 +170,10 @@ 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) @@ -157,11 +181,18 @@ func dataSourceNewRelicNotificationDestinationRead(ctx context.Context, d *schem 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("") @@ -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) @@ -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 +} diff --git a/newrelic/data_source_newrelic_notifications_destination_test.go b/newrelic/data_source_newrelic_notifications_destination_test.go index 16920db59..fd838375b 100644 --- a/newrelic/data_source_newrelic_notifications_destination_test.go +++ b/newrelic/data_source_newrelic_notifications_destination_test.go @@ -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) @@ -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"), ), }, }, @@ -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" @@ -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 { diff --git a/newrelic/resource_newrelic_notifications_destination.go b/newrelic/resource_newrelic_notifications_destination.go index 95836bba8..1996727be 100644 --- a/newrelic/resource_newrelic_notifications_destination.go +++ b/newrelic/resource_newrelic_notifications_destination.go @@ -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, }, }, }, @@ -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 { @@ -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 } @@ -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 } @@ -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 @@ -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 diff --git a/newrelic/resource_newrelic_notifications_destination_test.go b/newrelic/resource_newrelic_notifications_destination_test.go index f7cf44b0e..cd259fe4d 100644 --- a/newrelic/resource_newrelic_notifications_destination_test.go +++ b/newrelic/resource_newrelic_notifications_destination_test.go @@ -4,6 +4,7 @@ package newrelic import ( "fmt" + "strconv" "testing" "github.com/newrelic/newrelic-client-go/v2/pkg/ai" @@ -250,11 +251,12 @@ func TestNewRelicNotificationDestination_secureURL_update(t *testing.T) { }) } +// TODO: Uncomment when organization environment variables are available in GitHub Actions func TestNewRelicNotificationDestination_OrganizationScope(t *testing.T) { resourceName := "newrelic_notification_destination.foo" rand := acctest.RandString(5) rName := fmt.Sprintf("tf-notifications-test-%s", rand) - orgUUID := "f47ac10b-58cc-4372-a567-0e02b2c3d479" + orgUUID := "fb33fea3-4d7e-4736-9701-acb59a634fdf" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheckEnvVars(t) }, @@ -263,7 +265,7 @@ func TestNewRelicNotificationDestination_OrganizationScope(t *testing.T) { Steps: []resource.TestStep{ // Test: Create with ORGANIZATION scope (requires UUID) { - Config: testNewRelicNotificationDestinationConfigWithScope(testAccountID, rName, "ORGANIZATION", orgUUID), + Config: testNewRelicNotificationDestinationConfigWithScope(rName, "ORGANIZATION", orgUUID), Check: resource.ComposeTestCheckFunc( testAccCheckNewRelicNotificationDestinationExists(resourceName), resource.TestCheckResourceAttrSet(resourceName, "guid"), @@ -273,7 +275,7 @@ func TestNewRelicNotificationDestination_OrganizationScope(t *testing.T) { }, // Update name only (scope remains unchanged) { - Config: testNewRelicNotificationDestinationConfigWithScope(testAccountID, fmt.Sprintf("%s-updated", rName), "ORGANIZATION", orgUUID), + Config: testNewRelicNotificationDestinationConfigWithScope(fmt.Sprintf("%s-updated", rName), "ORGANIZATION", orgUUID), Check: resource.ComposeTestCheckFunc( testAccCheckNewRelicNotificationDestinationExists(resourceName), resource.TestCheckResourceAttrSet(resourceName, "guid"), @@ -298,7 +300,6 @@ func TestNewRelicNotificationDestination_AccountScope(t *testing.T) { resourceName := "newrelic_notification_destination.foo" rand := acctest.RandString(5) rName := fmt.Sprintf("tf-notifications-test-%s", rand) - accountNumber := "12345678" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheckEnvVars(t) }, @@ -307,20 +308,36 @@ func TestNewRelicNotificationDestination_AccountScope(t *testing.T) { Steps: []resource.TestStep{ // Test: Create with ACCOUNT scope (requires number) { - Config: testNewRelicNotificationDestinationConfigWithScope(testAccountID, rName, "ACCOUNT", accountNumber), + Config: testNewRelicNotificationDestinationConfigWithScope(rName, "ACCOUNT", strconv.Itoa(testAccountID)), Check: resource.ComposeTestCheckFunc( testAccCheckNewRelicNotificationDestinationExists(resourceName), resource.TestCheckResourceAttrSet(resourceName, "guid"), resource.TestCheckResourceAttr(resourceName, "scope.0.type", "ACCOUNT"), - resource.TestCheckResourceAttr(resourceName, "scope.0.id", accountNumber), + resource.TestCheckResourceAttr(resourceName, "scope.0.id", strconv.Itoa(testAccountID)), + ), + }, + // Update name only (scope remains unchanged) + { + Config: testNewRelicNotificationDestinationConfigWithScope(fmt.Sprintf("%s-updated", rName), "ACCOUNT", strconv.Itoa(testAccountID)), + Check: resource.ComposeTestCheckFunc( + testAccCheckNewRelicNotificationDestinationExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "guid"), + resource.TestCheckResourceAttr(resourceName, "scope.0.type", "ACCOUNT"), + resource.TestCheckResourceAttr(resourceName, "scope.0.id", strconv.Itoa(testAccountID)), ), }, // Import + // Note: Import uses backward-compatible flow (sets account_id instead of scope) + // so we ignore both scope and account_id differences { ResourceName: resourceName, ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{ + "account_id", + "scope.#", + "scope.0.%", + "scope.0.type", "scope.0.id", }, }, @@ -360,11 +377,10 @@ resource "newrelic_notification_destination" "foo" { return sprintf } -func testNewRelicNotificationDestinationConfigWithScope(accountID int, name string, scopeType string, scopeID string) string { +func testNewRelicNotificationDestinationConfigWithScope(name string, scopeType string, scopeID string) string { return fmt.Sprintf(` resource "newrelic_notification_destination" "foo" { - account_id = %[1]d - name = "%[2]s" + name = "%[1]s" type = "WEBHOOK" active = true @@ -373,18 +389,12 @@ resource "newrelic_notification_destination" "foo" { value = "https://webhook.site/" } - property { - key = "source" - value = "terraform" - label = "terraform-integration-test" - } - scope { - type = "%[3]s" - id = "%[4]s" + type = "%[2]s" + id = "%[3]s" } } -`, accountID, name, scopeType, scopeID) +`, name, scopeType, scopeID) } func testAccNewRelicNotificationDestinationDestroy(s *terraform.State) error { @@ -442,7 +452,7 @@ func testAccCheckNewRelicNotificationDestinationExists(n string) resource.TestCh } sorter := notifications.AiNotificationsDestinationSorter{} - found, err := client.Notifications.GetDestinations(accountID, "", filters, sorter) + found, err := client.Notifications.GetDestinationsWithScope(accountID, "", filters, sorter) if err != nil { return err diff --git a/newrelic/structures_newrelic_notifications_destination.go b/newrelic/structures_newrelic_notifications_destination.go index 8f3f95043..9a526a10a 100644 --- a/newrelic/structures_newrelic_notifications_destination.go +++ b/newrelic/structures_newrelic_notifications_destination.go @@ -194,11 +194,72 @@ func expandNotificationDestinationProperty(cfg map[string]interface{}) notificat return property } +func flattenNotificationDestinationWithScope(destination *notifications.AiNotificationsDestinationWithScope, d *schema.ResourceData) error { + if destination == nil { + return nil + } + + // Check if this is an org-scoped destination + isOrgScoped := destination.Scope != nil && destination.Scope.Type == notifications.EntityScopeTypeInputTypes.ORGANIZATION + + // Set scope based on the destination's scope info + // If org-scoped, use ORGANIZATION type with org ID; otherwise use ACCOUNT type with account ID + var scopeData []map[string]interface{} + if isOrgScoped { + scopeData = []map[string]interface{}{ + { + "type": string(notifications.EntityScopeTypeInputTypes.ORGANIZATION), + "id": destination.Scope.ID, + }, + } + } else { + scopeData = []map[string]interface{}{ + { + "type": string(notifications.EntityScopeTypeInputTypes.ACCOUNT), + "id": fmt.Sprintf("%d", destination.AccountID), + }, + } + } + if err := d.Set("scope", scopeData); err != nil { + return err + } + + // For ACCOUNT scope, also set account_id; for ORGANIZATION scope, don't set account_id + return flattenNotificationDestinationBase(&destination.AiNotificationsDestination, d, isOrgScoped) +} + func flattenNotificationDestination(destination *notifications.AiNotificationsDestination, d *schema.ResourceData) error { if destination == nil { return nil } + // Check if user configured scope in their config (new flow) + existingScope := expandNotificationDestinationScope(d) + + if existingScope != nil { + // User configured scope (ACCOUNT type) - set scope and account_id in state + scopeData := []map[string]interface{}{ + { + "type": string(notifications.EntityScopeTypeInputTypes.ACCOUNT), + "id": fmt.Sprintf("%d", destination.AccountID), + }, + } + if err := d.Set("scope", scopeData); err != nil { + return err + } + // For ACCOUNT scope, also set account_id + return flattenNotificationDestinationBase(destination, d, false) + } + + // Backward compatible flow: User used account_id - set account_id, don't set scope + return flattenNotificationDestinationBase(destination, d, false) +} + +func flattenNotificationDestinationBase(destination *notifications.AiNotificationsDestination, d *schema.ResourceData, isOrgScoped bool) error { + if destination == nil { + return nil + } + var err error if err = d.Set("name", destination.Name); err != nil { @@ -241,9 +302,8 @@ func flattenNotificationDestination(destination *notifications.AiNotificationsDe return err } - // Only set account_id for non-org-scoped destinations - scope := expandNotificationDestinationScope(d) - if scope == nil || scope.Type != notifications.EntityScopeTypeInputTypes.ORGANIZATION { + // Only set account_id for non-org-scoped destinations (preserves backward compatibility) + if !isOrgScoped { if err := d.Set("account_id", destination.AccountID); err != nil { return err } @@ -368,7 +428,7 @@ func flattenNotificationDestinationSecureURLForDataSource(url *notifications.AiN return secureURLResult } -func flattenNotificationDestinationDataSource(destination *notifications.AiNotificationsDestination, d *schema.ResourceData) error { +func flattenNotificationDestinationDataSourceWithScope(destination *notifications.AiNotificationsDestinationWithScope, d *schema.ResourceData) error { if destination == nil { return nil } @@ -397,14 +457,6 @@ func flattenNotificationDestinationDataSource(destination *notifications.AiNotif return err } - // Only set account_id for non-org-scoped destinations - scope := expandNotificationDestinationScope(d) - if scope == nil || scope.Type != notifications.EntityScopeTypeInputTypes.ORGANIZATION { - if err := d.Set("account_id", destination.AccountID); err != nil { - return err - } - } - if err := d.Set("status", destination.Status); err != nil { return err } @@ -413,6 +465,43 @@ func flattenNotificationDestinationDataSource(destination *notifications.AiNotif return err } + // Check if user explicitly configured scope in their data source config + userConfiguredScope := expandNotificationDestinationScope(d) + + // Only set scope if user explicitly configured it in their data source config + // This ensures backward compatibility - users who don't use scope won't see it in state + if userConfiguredScope != nil { + // Set scope based on the destination's scope info from API, or derive from account_id + if destination.Scope != nil { + scopeData := []map[string]interface{}{ + { + "type": string(destination.Scope.Type), + "id": destination.Scope.ID, + }, + } + if err := d.Set("scope", scopeData); err != nil { + return err + } + } else { + // Destination doesn't have scope from API - set ACCOUNT scope with account_id + scopeData := []map[string]interface{}{ + { + "type": string(notifications.EntityScopeTypeInputTypes.ACCOUNT), + "id": fmt.Sprintf("%d", destination.AccountID), + }, + } + if err := d.Set("scope", scopeData); err != nil { + return err + } + } + } + // If user didn't configure scope, don't set scope (backward compatible) + + // Set account_id for backward compatibility + if err := d.Set("account_id", destination.AccountID); err != nil { + return err + } + return nil } @@ -434,56 +523,3 @@ func expandNotificationDestinationScope(d *schema.ResourceData) *notifications.E ID: scopeMap["id"].(string), } } - -func flattenNotificationDestinationScope(scope *notifications.EntityScope) []map[string]interface{} { - if scope == nil || scope.Type == "" { - return nil - } - - return []map[string]interface{}{ - { - "type": string(scope.Type), - "id": scope.ID, - }, - } -} - -func flattenNotificationDestinationWithScope(destination *notifications.AiNotificationsDestinationWithScope, d *schema.ResourceData) error { - if destination == nil { - return nil - } - - // Flatten the base destination fields - if err := flattenNotificationDestination(&destination.AiNotificationsDestination, d); err != nil { - return err - } - - // Flatten the scope - if destination.Scope != nil { - if err := d.Set("scope", flattenNotificationDestinationScope(destination.Scope)); err != nil { - return err - } - } - - return nil -} - -func flattenNotificationDestinationWithScopeForDataSource(destination *notifications.AiNotificationsDestinationWithScope, d *schema.ResourceData) error { - if destination == nil { - return nil - } - - // Flatten the base destination fields - if err := flattenNotificationDestinationDataSource(&destination.AiNotificationsDestination, d); err != nil { - return err - } - - // Flatten the scope - if destination.Scope != nil { - if err := d.Set("scope", flattenNotificationDestinationScope(destination.Scope)); err != nil { - return err - } - } - - return nil -} diff --git a/newrelic/structures_newrelic_notifications_destination_test.go b/newrelic/structures_newrelic_notifications_destination_test.go index 8530a8de0..e942f3d08 100644 --- a/newrelic/structures_newrelic_notifications_destination_test.go +++ b/newrelic/structures_newrelic_notifications_destination_test.go @@ -136,7 +136,7 @@ func TestExpandNotificationDestination(t *testing.T) { "scope": []interface{}{ map[string]interface{}{ "type": "ORGANIZATION", - "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "id": "mock-organization-id", }, }, }, @@ -257,7 +257,7 @@ func TestFlattenNotificationDestinationDataSource(t *testing.T) { Data map[string]interface{} ExpectErr bool ExpectReason string - Flattened *notifications.AiNotificationsDestination + Flattened *notifications.AiNotificationsDestinationWithScope }{ "minimal": { Data: map[string]interface{}{ @@ -265,10 +265,12 @@ func TestFlattenNotificationDestinationDataSource(t *testing.T) { "type": "WEBHOOK", "guid": "testdestinationentityguid", }, - Flattened: ¬ifications.AiNotificationsDestination{ - Name: "testing123", - Type: "WEBHOOK", - GUID: guid, + Flattened: ¬ifications.AiNotificationsDestinationWithScope{ + AiNotificationsDestination: notifications.AiNotificationsDestination{ + Name: "testing123", + Type: "WEBHOOK", + GUID: guid, + }, }, }, } @@ -276,7 +278,7 @@ func TestFlattenNotificationDestinationDataSource(t *testing.T) { for _, tc := range cases { if tc.Flattened != nil { d := r.TestResourceData() - err := flattenNotificationDestinationDataSource(tc.Flattened, d) + err := flattenNotificationDestinationDataSourceWithScope(tc.Flattened, d) assert.NoError(t, err) for k, v := range tc.Data {