diff --git a/.changelog/44636.txt b/.changelog/44636.txt new file mode 100644 index 000000000000..e9b5245392cb --- /dev/null +++ b/.changelog/44636.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_directory_service_directory: add `enable_directory_data_access` argument +``` diff --git a/internal/service/ds/directory.go b/internal/service/ds/directory.go index 02fd69b80366..8a2a89e6803b 100644 --- a/internal/service/ds/directory.go +++ b/internal/service/ds/directory.go @@ -199,6 +199,11 @@ func resourceDirectory() *schema.Resource { }, }, }, + "enable_directory_data_access": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, }, } } @@ -257,7 +262,6 @@ func resourceDirectoryCreate(ctx context.Context, d *schema.ResourceData, meta a return nil }, tfresource.WithPollInterval(1*time.Minute)) - if err != nil { return sdkdiag.AppendFromErr(diags, fmt.Errorf("creating Directory Service %s Directory (%s): %w", creator.TypeName(), name, err)) } @@ -280,6 +284,30 @@ func resourceDirectoryCreate(ctx context.Context, d *schema.ResourceData, meta a } } + if v, ok := d.GetOk("enable_directory_data_access"); ok && v.(bool) { + if err := enableDirectoryDataAccess(ctx, conn, d.Id()); err != nil { + sdkdiag.AppendFromErr(diags, err) + } + err := tfresource.Retry(ctx, d.Timeout(schema.TimeoutCreate), func(ctx context.Context) *tfresource.RetryError { + if err := waitDirectoryDataAccess(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + if use, ok := errs.As[*retry.UnexpectedStateError](err); ok { + if use.State == string(awstypes.DataAccessStatusFailed) { + tflog.Info(ctx, "retrying failed Directory Data Access enablement", map[string]any{ + "directory_id": d.Id(), + }) + return tfresource.RetryableError(err) + } + } + return tfresource.NonRetryableError(err) + } + + return nil + }, tfresource.WithPollInterval(1*time.Minute)) + if err != nil { + return sdkdiag.AppendFromErr(diags, fmt.Errorf("enabling directory service data access: %w", err)) + } + } + return append(diags, resourceDirectoryRead(ctx, d, meta)...) } @@ -334,6 +362,16 @@ func resourceDirectoryRead(ctx context.Context, d *schema.ResourceData, meta any d.Set("vpc_settings", nil) } + dda, err := getDirectoryDataAccess(ctx, conn, d.Id()) + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading directory data access: %s", err) + } + if dda.DataAccessStatus == awstypes.DataAccessStatusEnabled { + d.Set("enable_directory_data_access", true) + } else { + d.Set("enable_directory_data_access", false) + } + return diags } @@ -359,6 +397,37 @@ func resourceDirectoryUpdate(ctx context.Context, d *schema.ResourceData, meta a } } + if d.HasChange("enable_directory_data_access") { + if _, ok := d.GetOk("enable_directory_data_access"); ok { + if err := enableDirectoryDataAccess(ctx, conn, d.Id()); err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + } else { + if err := disableDirectoryDataAccess(ctx, conn, d.Id()); err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + } + + err := tfresource.Retry(ctx, d.Timeout(schema.TimeoutCreate), func(ctx context.Context) *tfresource.RetryError { + if err := waitDirectoryDataAccess(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + if use, ok := errs.As[*retry.UnexpectedStateError](err); ok { + if use.State == string(awstypes.DataAccessStatusFailed) { + tflog.Info(ctx, "retrying failed Directory Data Access enablement", map[string]any{ + "directory_id": d.Id(), + }) + return tfresource.RetryableError(err) + } + } + return tfresource.NonRetryableError(err) + } + + return nil + }, tfresource.WithPollInterval(1*time.Minute)) + if err != nil { + return sdkdiag.AppendFromErr(diags, fmt.Errorf("enabling directory service data access: %w", err)) + } + } + return append(diags, resourceDirectoryRead(ctx, d, meta)...) } @@ -426,7 +495,6 @@ func (c adConnectorCreator) Create(ctx context.Context, conn *directoryservice.C } output, err := conn.ConnectDirectory(ctx, input) - if err != nil { return err } @@ -466,7 +534,6 @@ func (c microsoftADCreator) Create(ctx context.Context, conn *directoryservice.C } output, err := conn.CreateMicrosoftAD(ctx, input) - if err != nil { return err } @@ -509,7 +576,6 @@ func (c simpleADCreator) Create(ctx context.Context, conn *directoryservice.Clie } output, err := conn.CreateDirectory(ctx, input) - if err != nil { return err } @@ -526,7 +592,6 @@ func createAlias(ctx context.Context, conn *directoryservice.Client, directoryID } _, err := conn.CreateAlias(ctx, input) - if err != nil { return fmt.Errorf("creating Directory Service Directory (%s) alias (%s): %w", directoryID, alias, err) } @@ -540,7 +605,6 @@ func disableSSO(ctx context.Context, conn *directoryservice.Client, directoryID } _, err := conn.DisableSso(ctx, input) - if err != nil { return fmt.Errorf("disabling Directory Service Directory (%s) SSO: %w", directoryID, err) } @@ -554,7 +618,6 @@ func enableSSO(ctx context.Context, conn *directoryservice.Client, directoryID s } _, err := conn.EnableSso(ctx, input) - if err != nil { return fmt.Errorf("enabling Directory Service Directory (%s) SSO: %w", directoryID, err) } @@ -562,11 +625,93 @@ func enableSSO(ctx context.Context, conn *directoryservice.Client, directoryID s return nil } +func enableDirectoryDataAccess(ctx context.Context, conn *directoryservice.Client, directoryID string) error { + input := &directoryservice.EnableDirectoryDataAccessInput{ + DirectoryId: aws.String(directoryID), + } + + _, err := conn.EnableDirectoryDataAccess(ctx, input) + if err != nil { + return fmt.Errorf("enabling Directory Data Access for Directory Service Directory (%s): %w", directoryID, err) + } + + return nil +} + +func disableDirectoryDataAccess(ctx context.Context, conn *directoryservice.Client, directoryID string) error { + input := &directoryservice.DisableDirectoryDataAccessInput{ + DirectoryId: aws.String(directoryID), + } + + _, err := conn.DisableDirectoryDataAccess(ctx, input) + if err != nil { + return fmt.Errorf("disabling Directory Data Access for Directory Service Directory (%s): %w", directoryID, err) + } + + return nil +} + +func getDirectoryDataAccess(ctx context.Context, conn *directoryservice.Client, directoryID string) (*directoryservice.DescribeDirectoryDataAccessOutput, error) { + input := directoryservice.DescribeDirectoryDataAccessInput{ + DirectoryId: aws.String(directoryID), + } + + dda, err := conn.DescribeDirectoryDataAccess(ctx, &input) + if err != nil { + return nil, fmt.Errorf("describing Directory Data Access for Directory Service Directory (%s): %w", directoryID, err) + } + + return dda, nil +} + +func statusDirectoryDataAccess(ctx context.Context, conn *directoryservice.Client, directoryID string) retry.StateRefreshFunc { + return func() (any, string, error) { + output, err := getDirectoryDataAccess(ctx, conn, directoryID) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, string(output.DataAccessStatus), nil + } +} + +func waitDirectoryDataAccess(ctx context.Context, conn *directoryservice.Client, directoryID string, timeout time.Duration) error { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(string(awstypes.DataAccessStatusEnabling), string(awstypes.DataAccessStatusDisabling)), + Target: enum.Slice(string(awstypes.DataAccessStatusEnabled), string(awstypes.DataAccessStatusDisabled)), + Refresh: statusDirectoryDataAccess(ctx, conn, directoryID), + Timeout: timeout, + Delay: 1 * time.Minute, + MinTimeout: 10 * time.Second, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + // Wrap any error returned with waiting message + defer func() { + if err != nil { + err = fmt.Errorf("waiting for completion: %w", err) + } + }() + + if output, ok := outputRaw.(*directoryservice.DescribeDirectoryDataAccessOutput); ok { + tfresource.SetLastError(err, errors.New(aws.ToString((*string)(&output.DataAccessStatus)))) + + return err + } + + return err +} + func updateNumberOfDomainControllers(ctx context.Context, conn *directoryservice.Client, directoryID string, desiredNumber int, timeout time.Duration, optFns ...func(*directoryservice.Options)) error { oldDomainControllers, err := findDomainControllers(ctx, conn, &directoryservice.DescribeDomainControllersInput{ DirectoryId: aws.String(directoryID), }, optFns...) - if err != nil { return fmt.Errorf("reading Directory Service Directory (%s) domain controllers: %w", directoryID, err) } @@ -577,7 +722,6 @@ func updateNumberOfDomainControllers(ctx context.Context, conn *directoryservice } _, err = conn.UpdateNumberOfDomainControllers(ctx, input, optFns...) - if err != nil { return fmt.Errorf("updating Directory Service Directory (%s) number of domain controllers (%d): %w", directoryID, desiredNumber, err) } @@ -585,7 +729,6 @@ func updateNumberOfDomainControllers(ctx context.Context, conn *directoryservice newDomainControllers, err := findDomainControllers(ctx, conn, &directoryservice.DescribeDomainControllersInput{ DirectoryId: aws.String(directoryID), }, optFns...) - if err != nil { return fmt.Errorf("reading Directory Service Directory (%s) domain controllers: %w", directoryID, err) } @@ -628,7 +771,6 @@ func updateNumberOfDomainControllers(ctx context.Context, conn *directoryservice func findDirectory(ctx context.Context, conn *directoryservice.Client, input *directoryservice.DescribeDirectoriesInput) (*awstypes.DirectoryDescription, error) { output, err := findDirectories(ctx, conn, input) - if err != nil { return nil, err } @@ -666,7 +808,6 @@ func findDirectoryByID(ctx context.Context, conn *directoryservice.Client, id st } output, err := findDirectory(ctx, conn, input) - if err != nil { return nil, err } @@ -753,7 +894,6 @@ func waitDirectoryDeleted(ctx context.Context, conn *directoryservice.Client, id func findDomainController(ctx context.Context, conn *directoryservice.Client, input *directoryservice.DescribeDomainControllersInput, optFns ...func(*directoryservice.Options)) (*awstypes.DomainController, error) { output, err := findDomainControllers(ctx, conn, input, optFns...) - if err != nil { return nil, err } @@ -792,7 +932,6 @@ func findDomainControllerByTwoPartKey(ctx context.Context, conn *directoryservic } output, err := findDomainController(ctx, conn, input, optFns...) - if err != nil { return nil, err } diff --git a/internal/service/ds/directory_test.go b/internal/service/ds/directory_test.go index 38beebfac26f..3bdd49b1e6c1 100644 --- a/internal/service/ds/directory_test.go +++ b/internal/service/ds/directory_test.go @@ -433,6 +433,69 @@ func TestAccDSDirectory_desiredNumberOfDomainControllers(t *testing.T) { }) } +func TestAccDSDirectory_enableDirectoryDataAccess(t *testing.T) { + ctx := acctest.Context(t) + var ds awstypes.DirectoryDescription + resourceName := "aws_directory_service_directory.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + domainName := acctest.RandomDomainName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckDirectoryService(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.DSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckDirectoryDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccDirectoryConfig_enableDirectoryDataAccess(rName, domainName, true), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckDirectoryExists(ctx, resourceName, &ds), + resource.TestCheckResourceAttrSet(resourceName, "access_url"), + resource.TestCheckResourceAttrSet(resourceName, names.AttrAlias), + resource.TestCheckResourceAttr(resourceName, "connect_settings.#", "0"), + resource.TestCheckResourceAttr(resourceName, names.AttrDescription, ""), + resource.TestCheckResourceAttr(resourceName, "enable_directory_data_access", acctest.CtTrue), + acctest.CheckResourceAttrGreaterThanValue(resourceName, "dns_ip_addresses.#", 0), + resource.TestCheckResourceAttr(resourceName, "edition", "Enterprise"), + resource.TestCheckResourceAttr(resourceName, "enable_sso", acctest.CtFalse), + resource.TestCheckResourceAttr(resourceName, names.AttrName, domainName), + resource.TestCheckResourceAttrSet(resourceName, "security_group_id"), + resource.TestCheckResourceAttrSet(resourceName, "short_name"), + resource.TestCheckResourceAttr(resourceName, names.AttrSize, "Large"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "0"), + resource.TestCheckResourceAttr(resourceName, names.AttrType, "MicrosoftAD"), + resource.TestCheckResourceAttr(resourceName, "vpc_settings.#", "1"), + resource.TestCheckResourceAttr(resourceName, "vpc_settings.0.availability_zones.#", "2"), + resource.TestCheckResourceAttr(resourceName, "vpc_settings.0.subnet_ids.#", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + names.AttrPassword, + }, + }, + { + Config: testAccDirectoryConfig_enableDirectoryDataAccess(rName, domainName, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckDirectoryExists(ctx, resourceName, &ds), + resource.TestCheckResourceAttr(resourceName, "enable_directory_data_access", acctest.CtFalse), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + names.AttrPassword, + }, + }, + }, + }) +} + func testAccCheckDirectoryDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).DSClient(ctx) @@ -469,7 +532,6 @@ func testAccCheckDirectoryExists(ctx context.Context, n string, v *awstypes.Dire conn := acctest.Provider.Meta().(*conns.AWSClient).DSClient(ctx) output, err := tfds.FindDirectoryByID(ctx, conn, rs.Primary.ID) - if err != nil { return err } @@ -688,3 +750,23 @@ resource "aws_directory_service_directory" "test" { `, domain, desiredNumber), ) } + +func testAccDirectoryConfig_enableDirectoryDataAccess(rName, domain string, enableDirectoryDataAccess bool) string { + return acctest.ConfigCompose( + acctest.ConfigVPCWithSubnets(rName, 2), + fmt.Sprintf(` +resource "aws_directory_service_directory" "test" { + name = %[1]q + password = "SuperSecretPassw0rd" + type = "MicrosoftAD" + + vpc_settings { + vpc_id = aws_vpc.test.id + subnet_ids = aws_subnet.test[*].id + } + + enable_directory_data_access = %[2]t +} +`, domain, enableDirectoryDataAccess), + ) +} diff --git a/website/docs/r/directory_service_directory.html.markdown b/website/docs/r/directory_service_directory.html.markdown index 1dfb9ad6089e..49d251474221 100644 --- a/website/docs/r/directory_service_directory.html.markdown +++ b/website/docs/r/directory_service_directory.html.markdown @@ -138,6 +138,7 @@ This resource supports the following arguments: * `type` (Optional) - The directory type (`SimpleAD`, `ADConnector` or `MicrosoftAD` are accepted values). Defaults to `SimpleAD`. * `edition` - (Optional, for type `MicrosoftAD` only) The MicrosoftAD edition (`Standard` or `Enterprise`). Defaults to `Enterprise`. * `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. +* `enable_directory_data_access` - (Optional) Enables access to directory data via the Directory Service Data API for the specified directory. For more information, see [Directory Service Data API Reference](https://docs.aws.amazon.com/directoryservicedata/latest/DirectoryServiceDataAPIReference/Welcome.html). **vpc_settings** supports the following: