diff --git a/.changelog/45197.txt b/.changelog/45197.txt new file mode 100644 index 000000000000..d0a061abecc9 --- /dev/null +++ b/.changelog/45197.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_notifications_organizational_unit_association +``` \ No newline at end of file diff --git a/internal/service/notifications/exports_test.go b/internal/service/notifications/exports_test.go index 66c8caa28bb2..772a80928834 100644 --- a/internal/service/notifications/exports_test.go +++ b/internal/service/notifications/exports_test.go @@ -5,13 +5,15 @@ package notifications // Exports for use in tests only. var ( - ResourceChannelAssociation = newChannelAssociationResource - ResourceEventRule = newEventRuleResource - ResourceNotificationConfiguration = newNotificationConfigurationResource - ResourceNotificationHub = newNotificationHubResource + ResourceChannelAssociation = newChannelAssociationResource + ResourceEventRule = newEventRuleResource + ResourceNotificationConfiguration = newNotificationConfigurationResource + ResourceNotificationHub = newNotificationHubResource + ResourceOrganizationalUnitAssociation = newOrganizationalUnitAssociationResource - FindChannelAssociationByTwoPartKey = findChannelAssociationByTwoPartKey - FindEventRuleByARN = findEventRuleByARN - FindNotificationConfigurationByARN = findNotificationConfigurationByARN - FindNotificationHubByRegion = findNotificationHubByRegion + FindChannelAssociationByTwoPartKey = findChannelAssociationByTwoPartKey + FindEventRuleByARN = findEventRuleByARN + FindNotificationConfigurationByARN = findNotificationConfigurationByARN + FindNotificationHubByRegion = findNotificationHubByRegion + FindOrganizationalUnitAssociationByTwoPartKey = findOrganizationalUnitAssociationByTwoPartKey ) diff --git a/internal/service/notifications/organizational_unit_association.go b/internal/service/notifications/organizational_unit_association.go new file mode 100644 index 000000000000..c7f6fc7c3d17 --- /dev/null +++ b/internal/service/notifications/organizational_unit_association.go @@ -0,0 +1,193 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package notifications + +import ( + "context" + "fmt" + "slices" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/notifications" + awstypes "github.com/aws/aws-sdk-go-v2/service/notifications/types" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + intflex "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +// @FrameworkResource("aws_notifications_organizational_unit_association", name="Organizational Unit Association") +func newOrganizationalUnitAssociationResource(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &organizationalUnitAssociationResource{} + + return r, nil +} + +type organizationalUnitAssociationResource struct { + framework.ResourceWithModel[organizationalUnitAssociationResourceModel] + framework.WithNoUpdate +} + +func (r *organizationalUnitAssociationResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "notification_configuration_arn": schema.StringAttribute{ + CustomType: fwtypes.ARNType, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "organizational_unit_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +func (r *organizationalUnitAssociationResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var data organizationalUnitAssociationResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().NotificationsClient(ctx) + + var input notifications.AssociateOrganizationalUnitInput + response.Diagnostics.Append(fwflex.Expand(ctx, data, &input)...) + if response.Diagnostics.HasError() { + return + } + + _, err := conn.AssociateOrganizationalUnit(ctx, &input) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("creating User Notifications Organizational Unit Association (%s,%s)", data.NotificationConfigurationARN.ValueString(), data.OrganizationalUnitID.ValueString()), err.Error()) + + return + } + + response.Diagnostics.Append(response.State.Set(ctx, data)...) +} + +func (r *organizationalUnitAssociationResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var data organizationalUnitAssociationResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().NotificationsClient(ctx) + + notificationConfigurationARN, organizationalUnitID := fwflex.StringValueFromFramework(ctx, data.NotificationConfigurationARN), fwflex.StringValueFromFramework(ctx, data.OrganizationalUnitID) + err := findOrganizationalUnitAssociationByTwoPartKey(ctx, conn, notificationConfigurationARN, organizationalUnitID) + + if tfresource.NotFound(err) { + response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + response.State.RemoveResource(ctx) + + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading User Notifications Organizational Unit Association (%s,%s)", notificationConfigurationARN, organizationalUnitID), err.Error()) + + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *organizationalUnitAssociationResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var data organizationalUnitAssociationResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().NotificationsClient(ctx) + + var input notifications.DisassociateOrganizationalUnitInput + response.Diagnostics.Append(fwflex.Expand(ctx, data, &input)...) + if response.Diagnostics.HasError() { + return + } + + _, err := conn.DisassociateOrganizationalUnit(ctx, &input) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("deleting User Notifications Organizational Unit Association (%s,%s)", data.NotificationConfigurationARN.ValueString(), data.OrganizationalUnitID.ValueString()), err.Error()) + + return + } +} + +func (r *organizationalUnitAssociationResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + const ( + organizationalUnitAssociationIDParts = 2 + ) + parts, err := intflex.ExpandResourceId(request.ID, organizationalUnitAssociationIDParts, false) + + if err != nil { + response.Diagnostics.Append(fwdiag.NewParsingResourceIDErrorDiagnostic(err)) + + return + } + + response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("organizational_unit_id"), parts[1])...) + response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("notification_configuration_arn"), parts[0])...) +} + +func findOrganizationalUnitAssociationByTwoPartKey(ctx context.Context, conn *notifications.Client, notificationConfigurationArn, organizationalUnitID string) error { + input := notifications.ListOrganizationalUnitsInput{ + NotificationConfigurationArn: aws.String(notificationConfigurationArn), + } + + pages := notifications.NewListOrganizationalUnitsPaginator(conn, &input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return &retry.NotFoundError{ + LastError: err, + LastRequest: &input, + } + } + + if err != nil { + return err + } + + if slices.Contains(page.OrganizationalUnits, organizationalUnitID) { + return nil + } + } + + return &retry.NotFoundError{ + LastRequest: &input, + } +} + +type organizationalUnitAssociationResourceModel struct { + NotificationConfigurationARN fwtypes.ARN `tfsdk:"notification_configuration_arn"` + OrganizationalUnitID types.String `tfsdk:"organizational_unit_id"` +} diff --git a/internal/service/notifications/organizational_unit_association_test.go b/internal/service/notifications/organizational_unit_association_test.go new file mode 100644 index 000000000000..26118db09993 --- /dev/null +++ b/internal/service/notifications/organizational_unit_association_test.go @@ -0,0 +1,241 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package notifications_test + +import ( + "context" + "errors" + "fmt" + "testing" + + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + intflex "github.com/hashicorp/terraform-provider-aws/internal/flex" + tfnotifications "github.com/hashicorp/terraform-provider-aws/internal/service/notifications" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccNotificationsOrganizationalUnitAssociation_basic(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_notifications_organizational_unit_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.NotificationsEndpointID) + testAccPreCheck(ctx, t) + acctest.PreCheckOrganizationsEnabled(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.NotificationsServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ExternalProviders: map[string]resource.ExternalProvider{ + "time": { + Source: "hashicorp/time", + VersionConstraint: "0.12.1", + }, + }, + CheckDestroy: testAccCheckOrganizationalUnitAssociationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccOrganizationalUnitAssociationConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckOrganizationalUnitAssociationExists(ctx, resourceName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "organizational_unit_id", + ImportStateIdFunc: testAccOrganizationalUnitAssociationImportStateIDFunc(resourceName), + }, + }, + }) +} + +func TestAccNotificationsOrganizationalUnitAssociation_organization(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_notifications_organizational_unit_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.NotificationsEndpointID) + testAccPreCheck(ctx, t) + acctest.PreCheckOrganizationsEnabled(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.NotificationsServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ExternalProviders: map[string]resource.ExternalProvider{ + "time": { + Source: "hashicorp/time", + VersionConstraint: "0.12.1", + }, + }, + CheckDestroy: testAccCheckOrganizationalUnitAssociationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccOrganizationalUnitAssociationConfig_organization(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckOrganizationalUnitAssociationExists(ctx, resourceName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "organizational_unit_id", + ImportStateIdFunc: testAccOrganizationalUnitAssociationImportStateIDFunc(resourceName), + }, + }, + }) +} + +func TestAccNotificationsOrganizationalUnitAssociation_disappears(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_notifications_organizational_unit_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.NotificationsEndpointID) + testAccPreCheck(ctx, t) + acctest.PreCheckOrganizationsEnabled(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.NotificationsServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ExternalProviders: map[string]resource.ExternalProvider{ + "time": { + Source: "hashicorp/time", + VersionConstraint: "0.12.1", + }, + }, + CheckDestroy: testAccCheckOrganizationalUnitAssociationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccOrganizationalUnitAssociationConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckOrganizationalUnitAssociationExists(ctx, resourceName), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfnotifications.ResourceOrganizationalUnitAssociation, resourceName), + ), + ExpectNonEmptyPlan: true, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + }, + }) +} + +func testAccCheckOrganizationalUnitAssociationDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).NotificationsClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_notifications_organizational_unit_association" { + continue + } + + err := tfnotifications.FindOrganizationalUnitAssociationByTwoPartKey(ctx, conn, rs.Primary.Attributes["notification_configuration_arn"], rs.Primary.Attributes["organizational_unit_id"]) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return errors.New("User Notifications Organizational Unit Association still exists") + } + + return nil + } +} + +func testAccCheckOrganizationalUnitAssociationExists(ctx context.Context, n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).NotificationsClient(ctx) + + err := tfnotifications.FindOrganizationalUnitAssociationByTwoPartKey(ctx, conn, rs.Primary.Attributes["notification_configuration_arn"], rs.Primary.Attributes["organizational_unit_id"]) + + return err + } +} + +func testAccOrganizationalUnitAssociationImportStateIDFunc(n string) func(*terraform.State) (string, error) { + return func(state *terraform.State) (string, error) { + rs, ok := state.RootModule().Resources[n] + if !ok { + return "", fmt.Errorf("Not found: %s", n) + } + + return rs.Primary.Attributes["notification_configuration_arn"] + intflex.ResourceIdSeparator + rs.Primary.Attributes["organizational_unit_id"], nil + } +} + +func testAccOrganizationalUnitAssociationConfig_base(rName string) string { + return fmt.Sprintf(` +data "aws_organizations_organization" "test" {} + +resource "aws_notifications_notification_configuration" "test" { + name = %[1]q + description = "example" +} +`, rName) +} + +func testAccOrganizationalUnitAssociationConfig_basic(rName string) string { + return acctest.ConfigCompose( + testAccOrganizationalUnitAssociationConfig_base(rName), + fmt.Sprintf(` +resource "aws_organizations_organizational_unit" "test" { + name = %[1]q + parent_id = data.aws_organizations_organization.test.roots[0].id +} + +# Allow time for organizational unit creation to propagate +resource "time_sleep" "wait" { + depends_on = [ + aws_organizations_organizational_unit.test, + aws_notifications_notification_configuration.test, + ] + + create_duration = "5s" +} + +resource "aws_notifications_organizational_unit_association" "test" { + depends_on = [time_sleep.wait] + + organizational_unit_id = aws_organizations_organizational_unit.test.id + notification_configuration_arn = aws_notifications_notification_configuration.test.arn +} +`, rName)) +} + +func testAccOrganizationalUnitAssociationConfig_organization(rName string) string { + return acctest.ConfigCompose( + testAccOrganizationalUnitAssociationConfig_base(rName), + ` +resource "aws_notifications_organizational_unit_association" "test" { + organizational_unit_id = data.aws_organizations_organization.test.roots[0].id + notification_configuration_arn = aws_notifications_notification_configuration.test.arn +} +`) +} diff --git a/internal/service/notifications/service_package_gen.go b/internal/service/notifications/service_package_gen.go index 019f39a8c774..037b2fc8b4f3 100644 --- a/internal/service/notifications/service_package_gen.go +++ b/internal/service/notifications/service_package_gen.go @@ -51,6 +51,12 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*inttypes.Ser Name: "Notification Hub", Region: unique.Make(inttypes.ResourceRegionDisabled()), }, + { + Factory: newOrganizationalUnitAssociationResource, + TypeName: "aws_notifications_organizational_unit_association", + Name: "Organizational Unit Association", + Region: unique.Make(inttypes.ResourceRegionDisabled()), + }, } } diff --git a/website/docs/r/notifications_organizational_unit_association.html.markdown b/website/docs/r/notifications_organizational_unit_association.html.markdown new file mode 100644 index 000000000000..5d96215820bf --- /dev/null +++ b/website/docs/r/notifications_organizational_unit_association.html.markdown @@ -0,0 +1,89 @@ +--- +subcategory: "User Notifications" +layout: "aws" +page_title: "AWS: aws_notifications_organizational_unit_association" +description: |- + Terraform resource for managing an AWS User Notifications Organizational Unit Association. +--- +# Resource: aws_notifications_organizational_unit_association + +Terraform resource for managing an AWS User Notifications Organizational Unit Association. This resource associates an organizational unit with a notification configuration. + +## Example Usage + +### Basic Usage + +```terraform +data "aws_organizations_organization" "example" {} + +resource "aws_notifications_notification_configuration" "example" { + name = "example-notification-config" + description = "Example notification configuration" +} + +resource "aws_organizations_organizational_unit" "example" { + name = "example-ou" + parent_id = data.aws_organizations_organization.example.roots[0].id +} + +# Allow time for organizational unit creation to propagate +resource "time_sleep" "wait" { + depends_on = [ + aws_organizations_organizational_unit.example, + aws_notifications_notification_configuration.example, + ] + + create_duration = "5s" +} + +resource "aws_notifications_organizational_unit_association" "example" { + depends_on = [time_sleep.wait] + + organizational_unit_id = aws_organizations_organizational_unit.example.id + notification_configuration_arn = aws_notifications_notification_configuration.example.arn +} +``` + +### Associate with Organization Root + +```terraform +data "aws_organizations_organization" "example" {} + +resource "aws_notifications_notification_configuration" "example" { + name = "example-notification-config" + description = "Example notification configuration" +} + +resource "aws_notifications_organizational_unit_association" "example" { + organizational_unit_id = data.aws_organizations_organization.example.roots[0].id + notification_configuration_arn = aws_notifications_notification_configuration.example.arn +} +``` + +## Argument Reference + +The following arguments are required: + +* `organizational_unit_id` - (Required) ID of the organizational unit or ID of the root to associate with the notification configuration. Can be a root ID (e.g., `r-1234`), or an organization ID (e.g., `o-1234567890`). +* `notification_configuration_arn` - (Required) ARN of the notification configuration to associate the organizational unit with. + +## Attribute Reference + +This resource exports no additional attributes. + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import User Notifications Organizational Unit Association using the `notification_configuration_arn,organizational_unit_id` format. For example: + +```terraform +import { + to = aws_notifications_organizational_unit_association.example + id = "arn:aws:notifications:us-west-2:123456789012:configuration:example-notification-config,ou-1234-12345678" +} +``` + +Using `terraform import`, import User Notifications Organizational Unit Association using the `notification_configuration_arn,organizational_unit_id` format. For example: + +```console +% terraform import aws_notifications_organizational_unit_association.example arn:aws:notifications:us-west-2:123456789012:configuration:example-notification-config,ou-1234-12345678 +```