diff --git a/.changelog/24766.txt b/.changelog/24766.txt new file mode 100644 index 00000000000..fb2852a875d --- /dev/null +++ b/.changelog/24766.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_directory_service_shared_directory +``` + +```release-note:new-resource +aws_directory_service_shared_directory_accepter +``` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index e4fa86ab1ad..c24f096fb7f 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1267,9 +1267,11 @@ func Provider() *schema.Provider { "aws_docdb_global_cluster": docdb.ResourceGlobalCluster(), "aws_docdb_subnet_group": docdb.ResourceSubnetGroup(), - "aws_directory_service_conditional_forwarder": ds.ResourceConditionalForwarder(), - "aws_directory_service_directory": ds.ResourceDirectory(), - "aws_directory_service_log_subscription": ds.ResourceLogSubscription(), + "aws_directory_service_conditional_forwarder": ds.ResourceConditionalForwarder(), + "aws_directory_service_directory": ds.ResourceDirectory(), + "aws_directory_service_log_subscription": ds.ResourceLogSubscription(), + "aws_directory_service_shared_directory_accepter": ds.ResourceSharedDirectoryAccepter(), + "aws_directory_service_shared_directory": ds.ResourceSharedDirectory(), "aws_dynamodb_contributor_insights": dynamodb.ResourceContributorInsights(), "aws_dynamodb_global_table": dynamodb.ResourceGlobalTable(), diff --git a/internal/service/ds/directory.go b/internal/service/ds/directory.go index 7dd8c50c6b3..24fe615c589 100644 --- a/internal/service/ds/directory.go +++ b/internal/service/ds/directory.go @@ -395,8 +395,7 @@ func resourceDirectoryCreate(d *schema.ResourceData, meta interface{}) error { d.SetId(directoryId) - _, err = waitDirectoryCreated(conn, d.Id()) - + err = waitDirectoryCreated(conn, d.Id()) if err != nil { return fmt.Errorf("error waiting for Directory Service Directory (%s) to create: %w", d.Id(), err) } @@ -547,7 +546,7 @@ func resourceDirectoryDelete(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error deleting Directory Service Directory (%s): %w", d.Id(), err) } - _, err = waitDirectoryDeleted(conn, d.Id()) + err = waitDirectoryDeleted(conn, d.Id()) if err != nil { return fmt.Errorf("error waiting for Directory Service Directory (%s) to delete: %w", d.Id(), err) diff --git a/internal/service/ds/find.go b/internal/service/ds/find.go index deb6a58d8fe..b2e0c95bfa5 100644 --- a/internal/service/ds/find.go +++ b/internal/service/ds/find.go @@ -1,6 +1,8 @@ package ds import ( + "context" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/directoryservice" "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" @@ -45,3 +47,35 @@ func findDirectoryByID(conn *directoryservice.DirectoryService, id string) (*dir return directory, nil } + +func findSharedDirectoryByIDs(ctx context.Context, conn *directoryservice.DirectoryService, ownerDirectoryId string, sharedDirectoryId string) (*directoryservice.SharedDirectory, error) { // nosemgrep:ds-in-func-name + input := &directoryservice.DescribeSharedDirectoriesInput{ + OwnerDirectoryId: aws.String(ownerDirectoryId), + SharedDirectoryIds: []*string{aws.String(sharedDirectoryId)}, + } + + output, err := conn.DescribeSharedDirectoriesWithContext(ctx, input) + + if err != nil { + return nil, err + } + + if output == nil || len(output.SharedDirectories) == 0 || output.SharedDirectories[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output.SharedDirectories); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + sharedDirectory := output.SharedDirectories[0] + + if status := aws.StringValue(sharedDirectory.ShareStatus); status == directoryservice.ShareStatusDeleted { + return nil, &resource.NotFoundError{ + Message: status, + LastRequest: input, + } + } + + return sharedDirectory, nil +} diff --git a/internal/service/ds/shared_directory.go b/internal/service/ds/shared_directory.go new file mode 100644 index 00000000000..dffdc83caba --- /dev/null +++ b/internal/service/ds/shared_directory.go @@ -0,0 +1,239 @@ +package ds + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directoryservice" + "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" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +const ( + ResourceNameSharedDirectory = "Shared Directory" +) + +func ResourceSharedDirectory() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceSharedDirectoryCreate, + ReadContext: resourceSharedDirectoryRead, + DeleteContext: resourceSharedDirectoryDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "directory_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "method": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: directoryservice.ShareMethodHandshake, + ValidateFunc: validation.StringInSlice(directoryservice.ShareMethod_Values(), false), + }, + "notes": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Sensitive: true, + }, + "shared_directory_id": { + Type: schema.TypeString, + Computed: true, + }, + "target": { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "type": { + Type: schema.TypeString, + Optional: true, + Default: directoryservice.TargetTypeAccount, + ValidateFunc: validation.StringInSlice(directoryservice.TargetType_Values(), false), + }, + }, + }, + }, + }, + } +} + +func resourceSharedDirectoryCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).DSConn + + dirId := d.Get("directory_id").(string) + input := directoryservice.ShareDirectoryInput{ + DirectoryId: aws.String(dirId), + ShareMethod: aws.String(d.Get("method").(string)), + ShareTarget: expandShareTarget(d.Get("target").([]interface{})[0].(map[string]interface{})), + } + + if v, ok := d.GetOk("notes"); ok { + input.ShareNotes = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Creating Shared Directory: %s", input) + out, err := conn.ShareDirectoryWithContext(ctx, &input) + + if err != nil { + return names.DiagError(names.DS, names.ErrActionCreating, ResourceNameSharedDirectory, d.Id(), err) + } + + log.Printf("[DEBUG] Shared Directory created: %s", out) + d.SetId(sharedDirectoryID(dirId, aws.StringValue(out.SharedDirectoryId))) + d.Set("shared_directory_id", out.SharedDirectoryId) + + return nil +} + +func resourceSharedDirectoryRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).DSConn + + ownerDirID, sharedDirID, err := parseSharedDirectoryID(d.Id()) + + if err != nil { + return names.DiagError(names.DS, names.ErrActionReading, ResourceNameSharedDirectory, d.Id(), err) + } + + output, err := findSharedDirectoryByIDs(ctx, conn, ownerDirID, sharedDirID) + + if !d.IsNewResource() && tfresource.NotFound(err) { + names.LogNotFoundRemoveState(names.DS, names.ErrActionReading, ResourceNameSharedDirectory, d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return names.DiagError(names.DS, names.ErrActionReading, ResourceNameSharedDirectory, d.Id(), err) + } + + log.Printf("[DEBUG] Received DS shared directory: %s", output) + + d.Set("method", output.ShareMethod) + d.Set("notes", output.ShareNotes) + + if output.SharedAccountId != nil { + if err := d.Set("target", []interface{}{flattenShareTarget(output)}); err != nil { + return names.DiagError(names.DS, names.ErrActionSetting, ResourceNameSharedDirectory, d.Id(), err) + } + } else { + d.Set("target", nil) + } + + return nil +} + +func resourceSharedDirectoryDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).DSConn + + dirId := d.Get("directory_id").(string) + sharedId := d.Get("shared_directory_id").(string) + + input := directoryservice.UnshareDirectoryInput{ + DirectoryId: aws.String(dirId), + UnshareTarget: expandUnshareTarget(d.Get("target").([]interface{})[0].(map[string]interface{})), + } + + log.Printf("[DEBUG] Unsharing Directory Service Directory: %s", input) + output, err := conn.UnshareDirectoryWithContext(ctx, &input) + + if err != nil { + return names.DiagError(names.DS, names.ErrActionDeleting, ResourceNameSharedDirectory, d.Id(), err) + } + + _, err = waitSharedDirectoryDeleted(ctx, conn, dirId, sharedId) + + if err != nil { + return names.DiagError(names.DS, names.ErrActionWaitingForDeletion, ResourceNameSharedDirectory, d.Id(), err) + } + + log.Printf("[DEBUG] Unshared Directory Service Directory: %s", output) + + return nil +} + +func expandShareTarget(tfMap map[string]interface{}) *directoryservice.ShareTarget { // nosemgrep:ds-in-func-name + if tfMap == nil { + return nil + } + + apiObject := &directoryservice.ShareTarget{} + + if v, ok := tfMap["id"].(string); ok && len(v) > 0 { + apiObject.Id = aws.String(v) + } + + if v, ok := tfMap["type"].(string); ok && len(v) > 0 { + apiObject.Type = aws.String(v) + } + + return apiObject +} + +func expandUnshareTarget(tfMap map[string]interface{}) *directoryservice.UnshareTarget { + if tfMap == nil { + return nil + } + + apiObject := &directoryservice.UnshareTarget{} + + if v, ok := tfMap["id"].(string); ok && len(v) > 0 { + apiObject.Id = aws.String(v) + } + + if v, ok := tfMap["type"].(string); ok && len(v) > 0 { + apiObject.Type = aws.String(v) + } + + return apiObject +} + +// flattenShareTarget is not a mirror of expandShareTarget because the API data structures are +// different, with no ShareTarget returned +func flattenShareTarget(apiObject *directoryservice.SharedDirectory) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if apiObject.SharedAccountId != nil { + tfMap["id"] = aws.StringValue(apiObject.SharedAccountId) + } + + tfMap["type"] = directoryservice.TargetTypeAccount // only type available + + return tfMap +} + +func sharedDirectoryID(ownerDirectoryID, sharedDirectoryID string) string { + return fmt.Sprintf("%s/%s", ownerDirectoryID, sharedDirectoryID) +} + +func parseSharedDirectoryID(id string) (string, string, error) { + idParts := strings.SplitN(id, "/", 2) + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + return "", "", fmt.Errorf("unexpected format of ID (%q), expected /", id) + } + + return idParts[0], idParts[1], nil +} diff --git a/internal/service/ds/shared_directory_accepter.go b/internal/service/ds/shared_directory_accepter.go new file mode 100644 index 00000000000..f05f82f63cd --- /dev/null +++ b/internal/service/ds/shared_directory_accepter.go @@ -0,0 +1,144 @@ +package ds + +import ( + "context" + "errors" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directoryservice" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +const ( + ResourceNameSharedDirectoryAccepter = "Shared Directory Accepter" +) + +func ResourceSharedDirectoryAccepter() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceSharedDirectoryAccepterCreate, + ReadContext: resourceSharedDirectoryAccepterRead, + DeleteContext: resourceSharedDirectoryAccepterDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "method": { + Type: schema.TypeString, + Computed: true, + }, + "notes": { + Type: schema.TypeString, + Computed: true, + }, + "owner_account_id": { + Type: schema.TypeString, + Computed: true, + }, + "owner_directory_id": { + Type: schema.TypeString, + Computed: true, + }, + "shared_directory_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceSharedDirectoryAccepterCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).DSConn + + input := directoryservice.AcceptSharedDirectoryInput{ + SharedDirectoryId: aws.String(d.Get("shared_directory_id").(string)), + } + + log.Printf("[DEBUG] Accepting shared directory: %s", input) + + output, err := conn.AcceptSharedDirectoryWithContext(ctx, &input) + + if err != nil { + return names.DiagError(names.DS, names.ErrActionCreating, ResourceNameSharedDirectoryAccepter, d.Get("shared_directory_id").(string), err) + } + + if output == nil || output.SharedDirectory == nil { + return names.DiagError(names.DS, names.ErrActionCreating, ResourceNameSharedDirectoryAccepter, d.Get("shared_directory_id").(string), errors.New("empty output")) + } + + d.SetId(d.Get("shared_directory_id").(string)) + + d.Set("notes", output.SharedDirectory.ShareNotes) // only available in response to create + + _, err = waitDirectoryShared(ctx, conn, d.Id()) + + if err != nil { + return names.DiagError(names.DS, names.ErrActionWaitingForCreation, ResourceNameSharedDirectoryAccepter, d.Id(), err) + } + + return resourceSharedDirectoryAccepterRead(ctx, d, meta) +} + +func resourceSharedDirectoryAccepterRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).DSConn + + dir, err := findDirectoryByID(conn, d.Id()) + + if err != nil { + return names.DiagError(names.DS, names.ErrActionReading, ResourceNameSharedDirectoryAccepter, d.Id(), err) + } + + d.Set("method", dir.ShareMethod) + d.Set("owner_account_id", dir.OwnerDirectoryDescription.AccountId) + d.Set("owner_directory_id", dir.OwnerDirectoryDescription.DirectoryId) + d.Set("shared_directory_id", dir.DirectoryId) + return nil +} + +func resourceSharedDirectoryAccepterDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).DSConn + + input := &directoryservice.DeleteDirectoryInput{ + DirectoryId: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Deleting Directory Service Directory: (%s)", d.Id()) + err := resource.Retry(directoryApplicationDeauthorizedPropagationTimeout, func() *resource.RetryError { + _, err := conn.DeleteDirectory(input) + + if tfawserr.ErrCodeEquals(err, directoryservice.ErrCodeEntityDoesNotExistException) { + return nil + } + if tfawserr.ErrMessageContains(err, directoryservice.ErrCodeClientException, "authorized applications") { + return resource.RetryableError(err) + } + if err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) + if tfresource.TimedOut(err) { + _, err = conn.DeleteDirectory(input) + } + + if err != nil { + return names.DiagError(names.DS, names.ErrActionDeleting, ResourceNameSharedDirectoryAccepter, d.Id(), err) + } + + err = waitDirectoryDeleted(conn, d.Id()) + + if err != nil { + return names.DiagError(names.DS, names.ErrActionWaitingForDeletion, ResourceNameSharedDirectoryAccepter, d.Id(), err) + } + + return nil +} diff --git a/internal/service/ds/shared_directory_accepter_test.go b/internal/service/ds/shared_directory_accepter_test.go new file mode 100644 index 00000000000..e784af62127 --- /dev/null +++ b/internal/service/ds/shared_directory_accepter_test.go @@ -0,0 +1,129 @@ +package ds_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directoryservice" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfds "github.com/hashicorp/terraform-provider-aws/internal/service/ds" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccDSSharedDirectoryAccepter_basic(t *testing.T) { + var providers []*schema.Provider + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_directory_service_shared_directory_accepter.test" + + domainName := acctest.RandomDomainName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, directoryservice.EndpointsID), + ProviderFactories: acctest.FactoriesAlternate(&providers), + CheckDestroy: testAccCheckSharedDirectoryAccepterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSharedDirectoryAccepterConfig_basic(rName, domainName), + Check: resource.ComposeTestCheckFunc( + testAccCheckSharedDirectoryAccepterExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "method", directoryservice.ShareMethodHandshake), + resource.TestCheckResourceAttr(resourceName, "notes", "There were hints and allegations"), + resource.TestCheckResourceAttrPair(resourceName, "owner_account_id", "data.aws_caller_identity.current", "account_id"), + resource.TestCheckResourceAttrSet(resourceName, "owner_directory_id"), + resource.TestCheckResourceAttrSet(resourceName, "shared_directory_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "notes", + }, + }, + }, + }) + +} + +func testAccCheckSharedDirectoryAccepterExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return names.Error(names.DS, names.ErrActionCheckingExistence, tfds.ResourceNameSharedDirectoryAccepter, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return names.Error(names.DS, names.ErrActionCheckingExistence, tfds.ResourceNameSharedDirectoryAccepter, name, errors.New("no ID is set")) + } + + ownerId := rs.Primary.Attributes["owner_directory_id"] + sharedId := rs.Primary.Attributes["shared_directory_id"] + + conn := acctest.Provider.Meta().(*conns.AWSClient).DSConn + out, err := conn.DescribeSharedDirectories(&directoryservice.DescribeSharedDirectoriesInput{ + OwnerDirectoryId: aws.String(ownerId), + SharedDirectoryIds: aws.StringSlice([]string{sharedId}), + }) + + if err != nil { + return names.Error(names.DS, names.ErrActionCheckingExistence, tfds.ResourceNameSharedDirectoryAccepter, name, err) + } + + if len(out.SharedDirectories) < 1 { + return names.Error(names.DS, names.ErrActionCheckingExistence, tfds.ResourceNameSharedDirectoryAccepter, name, errors.New("not found")) + } + + if aws.StringValue(out.SharedDirectories[0].SharedDirectoryId) != sharedId { + return names.Error(names.DS, names.ErrActionCheckingExistence, tfds.ResourceNameSharedDirectoryAccepter, rs.Primary.ID, fmt.Errorf("shared directory ID mismatch - existing: %q, state: %q", aws.StringValue(out.SharedDirectories[0].SharedDirectoryId), sharedId)) + } + + if aws.StringValue(out.SharedDirectories[0].OwnerDirectoryId) != ownerId { + return names.Error(names.DS, names.ErrActionCheckingExistence, tfds.ResourceNameSharedDirectoryAccepter, rs.Primary.ID, fmt.Errorf("owner directory ID mismatch - existing: %q, state: %q", aws.StringValue(out.SharedDirectories[0].OwnerDirectoryId), ownerId)) + } + + return nil + } + +} + +func testAccCheckSharedDirectoryAccepterDestroy(s *terraform.State) error { + // cannot be destroyed from consumer account + return nil +} + +func testAccSharedDirectoryAccepterConfig_basic(rName, domain string) string { + return acctest.ConfigCompose( + acctest.ConfigAlternateAccountProvider(), + testAccDirectoryConfig_microsoftStandard(rName, domain), + ` +data "aws_caller_identity" "current" {} + +resource "aws_directory_service_shared_directory" "test" { + directory_id = aws_directory_service_directory.test.id + notes = "There were hints and allegations" + + target { + id = data.aws_caller_identity.consumer.account_id + } +} + +data "aws_caller_identity" "consumer" { + provider = "awsalternate" +} + +resource "aws_directory_service_shared_directory_accepter" "test" { + provider = "awsalternate" + + shared_directory_id = aws_directory_service_shared_directory.test.shared_directory_id +} +`) +} diff --git a/internal/service/ds/shared_directory_test.go b/internal/service/ds/shared_directory_test.go new file mode 100644 index 00000000000..5fc2948c999 --- /dev/null +++ b/internal/service/ds/shared_directory_test.go @@ -0,0 +1,146 @@ +package ds_test + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directoryservice" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" +) + +func TestAccDSSharedDirectory_basic(t *testing.T) { + var providers []*schema.Provider + var sharedDirectory directoryservice.SharedDirectory + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_directory_service_shared_directory.test" + + domainName := acctest.RandomDomainName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, directoryservice.EndpointsID), + ProviderFactories: acctest.FactoriesAlternate(&providers), + CheckDestroy: testAccCheckSharedDirectoryDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSharedDirectoryConfig_basic(rName, domainName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "shared_directory_id"), + testAccCheckSharedDirectoryExists(resourceName, &sharedDirectory), + resource.TestCheckResourceAttr(resourceName, "method", "HANDSHAKE"), + resource.TestCheckResourceAttr(resourceName, "notes", "test"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + +} + +func testAccCheckSharedDirectoryExists(name string, share *directoryservice.SharedDirectory) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + ownerId := rs.Primary.Attributes["directory_id"] + sharedId := rs.Primary.Attributes["shared_directory_id"] + + conn := acctest.Provider.Meta().(*conns.AWSClient).DSConn + out, err := conn.DescribeSharedDirectories(&directoryservice.DescribeSharedDirectoriesInput{ + OwnerDirectoryId: aws.String(ownerId), + SharedDirectoryIds: aws.StringSlice([]string{sharedId}), + }) + if err != nil { + return err + } + + if len(out.SharedDirectories) < 1 { + return fmt.Errorf("No Shared Directory found") + } + + if *out.SharedDirectories[0].SharedDirectoryId != sharedId { + return fmt.Errorf("Shared Directory mismatch - existing: %q, state: %q", + *out.SharedDirectories[0].SharedDirectoryId, sharedId) + } + + if *out.SharedDirectories[0].OwnerDirectoryId != ownerId { + return fmt.Errorf("Owner Directory ID mismatch - existing: %q, state: %q", + *out.SharedDirectories[0].OwnerDirectoryId, ownerId) + } + + *share = *out.SharedDirectories[0] + + return nil + } + +} + +func testAccCheckSharedDirectoryDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).DSConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_directory_service_shared_directory" { + continue + } + + ownerId := rs.Primary.Attributes["directory_id"] + sharedId := rs.Primary.Attributes["shared_directory_id"] + + input := directoryservice.DescribeSharedDirectoriesInput{ + OwnerDirectoryId: aws.String(ownerId), + SharedDirectoryIds: []*string{aws.String(sharedId)}, + } + out, err := conn.DescribeSharedDirectories(&input) + + if tfawserr.ErrCodeEquals(err, directoryservice.ErrCodeEntityDoesNotExistException) { + continue + } + + if err != nil { + return err + } + + if out != nil && len(out.SharedDirectories) > 0 { + return fmt.Errorf("Expected AWS Directory Service Shared Directory to be gone, but was still found") + } + } + + return nil +} + +func testAccSharedDirectoryConfig_basic(rName, domain string) string { + return acctest.ConfigCompose( + acctest.ConfigAlternateAccountProvider(), + testAccDirectoryConfig_microsoftStandard(rName, domain), + ` +resource "aws_directory_service_shared_directory" "test" { + directory_id = aws_directory_service_directory.test.id + notes = "test" + + target { + id = data.aws_caller_identity.receiver.account_id + } +} + +data "aws_caller_identity" "receiver" { + provider = "awsalternate" +} +`) +} diff --git a/internal/service/ds/status.go b/internal/service/ds/status.go index 61663ecb243..888fb5b6e51 100644 --- a/internal/service/ds/status.go +++ b/internal/service/ds/status.go @@ -1,6 +1,8 @@ package ds import ( + "context" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/directoryservice" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -22,3 +24,35 @@ func statusDirectoryStage(conn *directoryservice.DirectoryService, id string) re return output, aws.StringValue(output.Stage), nil } } + +func statusDirectoryShare(conn *directoryservice.DirectoryService, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := findDirectoryByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.ShareStatus), nil + } +} + +func statusSharedDirectory(ctx context.Context, conn *directoryservice.DirectoryService, ownerId, sharedId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := findSharedDirectoryByIDs(ctx, conn, ownerId, sharedId) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.ShareStatus), nil + } +} diff --git a/internal/service/ds/wait.go b/internal/service/ds/wait.go index 4023e51dfc4..eeb36a58072 100644 --- a/internal/service/ds/wait.go +++ b/internal/service/ds/wait.go @@ -1,6 +1,7 @@ package ds import ( + "context" "errors" "time" @@ -11,11 +12,12 @@ import ( ) const ( - directoryCreatedTimeout = 60 * time.Minute - directoryDeletedTimeout = 60 * time.Minute + directoryCreatedTimeout = 60 * time.Minute + directoryDeletedTimeout = 60 * time.Minute + sharedDirectoryDeletedTimeout = 60 * time.Minute ) -func waitDirectoryCreated(conn *directoryservice.DirectoryService, id string) (*directoryservice.DirectoryDescription, error) { +func waitDirectoryCreated(conn *directoryservice.DirectoryService, id string) error { stateConf := &resource.StateChangeConf{ Pending: []string{directoryservice.DirectoryStageRequested, directoryservice.DirectoryStageCreating, directoryservice.DirectoryStageCreated}, Target: []string{directoryservice.DirectoryStageActive}, @@ -23,10 +25,45 @@ func waitDirectoryCreated(conn *directoryservice.DirectoryService, id string) (* Timeout: directoryCreatedTimeout, } - outputRaw, err := stateConf.WaitForState() + _, err := stateConf.WaitForState() - if output, ok := outputRaw.(*directoryservice.DirectoryDescription); ok { - tfresource.SetLastError(err, errors.New(aws.StringValue(output.StageReason))) + return err +} + +func waitDirectoryDeleted(conn *directoryservice.DirectoryService, id string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{directoryservice.DirectoryStageActive, directoryservice.DirectoryStageDeleting}, + Target: []string{}, + Refresh: statusDirectoryStage(conn, id), + Timeout: directoryDeletedTimeout, + } + + _, err := stateConf.WaitForState() + + return err +} + +func waitSharedDirectoryDeleted(ctx context.Context, conn *directoryservice.DirectoryService, ownerId, sharedId string) (*directoryservice.SharedDirectory, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + directoryservice.ShareStatusDeleting, + directoryservice.ShareStatusShared, + directoryservice.ShareStatusPendingAcceptance, + directoryservice.ShareStatusRejectFailed, + directoryservice.ShareStatusRejected, + directoryservice.ShareStatusRejecting, + }, + Target: []string{}, + Refresh: statusSharedDirectory(ctx, conn, ownerId, sharedId), + Timeout: sharedDirectoryDeletedTimeout, + MinTimeout: 30 * time.Second, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*directoryservice.SharedDirectory); ok { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.ShareStatus))) return output, err } @@ -34,18 +71,22 @@ func waitDirectoryCreated(conn *directoryservice.DirectoryService, id string) (* return nil, err } -func waitDirectoryDeleted(conn *directoryservice.DirectoryService, id string) (*directoryservice.DirectoryDescription, error) { +func waitDirectoryShared(ctx context.Context, conn *directoryservice.DirectoryService, dirId string) (*directoryservice.SharedDirectory, error) { stateConf := &resource.StateChangeConf{ - Pending: []string{directoryservice.DirectoryStageActive, directoryservice.DirectoryStageDeleting}, - Target: []string{}, - Refresh: statusDirectoryStage(conn, id), - Timeout: directoryDeletedTimeout, + Pending: []string{ + directoryservice.ShareStatusSharing, + directoryservice.ShareStatusPendingAcceptance, + }, + Target: []string{directoryservice.ShareStatusShared}, + Refresh: statusDirectoryShare(conn, dirId), + Timeout: sharedDirectoryDeletedTimeout, + ContinuousTargetOccurence: 2, } - outputRaw, err := stateConf.WaitForState() + outputRaw, err := stateConf.WaitForStateContext(ctx) - if output, ok := outputRaw.(*directoryservice.DirectoryDescription); ok { - tfresource.SetLastError(err, errors.New(aws.StringValue(output.StageReason))) + if output, ok := outputRaw.(*directoryservice.SharedDirectory); ok { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.ShareStatus))) return output, err } diff --git a/names/errors.go b/names/errors.go index 04e1452036a..0fbc79cd928 100644 --- a/names/errors.go +++ b/names/errors.go @@ -9,13 +9,15 @@ import ( ) const ( - ErrActionReading = "reading" - ErrActionDeleting = "deleting" - ErrActionUpdating = "updating" - ErrActionCreating = "creating" - ErrActionSetting = "setting" - ErrActionCheckingExistence = "checking existence" - ErrActionCheckingDestroyed = "checking destroyed" + ErrActionCheckingDestroyed = "checking destroyed" + ErrActionCheckingExistence = "checking existence" + ErrActionCreating = "creating" + ErrActionDeleting = "deleting" + ErrActionReading = "reading" + ErrActionSetting = "setting" + ErrActionUpdating = "updating" + ErrActionWaitingForCreation = "waiting for creation" + ErrActionWaitingForDeletion = "waiting for delete" ) // ProblemStandardMessage is a standardized message for reporting errors and warnings @@ -60,6 +62,17 @@ func AddWarning(diags diag.Diagnostics, service, action, resource, id string, go ) } +// AddWarningNotFoundRemoveState returns diag.Diagnostics with an additional diag.Diagnostic containing +// a warning using a standardized problem message +func AddWarningNotFoundRemoveState(service, action, resource, id string) diag.Diagnostics { + return append(diag.Diagnostics{}, + diag.Diagnostic{ + Severity: diag.Warning, + Summary: ProblemStandardMessage(service, action, resource, id, errors.New("not found, removing from state")), + }, + ) +} + // WarnLog logs to the default logger a standardized problem message func WarnLog(service, action, resource, id string, gotError error) { log.Printf("[WARN] %s", ProblemStandardMessage(service, action, resource, id, gotError)) diff --git a/website/docs/r/directory_service_shared_directory.html.markdown b/website/docs/r/directory_service_shared_directory.html.markdown new file mode 100644 index 00000000000..b98d2b6097d --- /dev/null +++ b/website/docs/r/directory_service_shared_directory.html.markdown @@ -0,0 +1,68 @@ +--- +subcategory: "DS (Directory Service)" +layout: "aws" +page_title: "AWS: aws_directory_service_shared_directory" +description: |- + Manages a directory in your account (directory owner) shared with another account (directory consumer). +--- + +# Resource: aws_directory_service_shared_directory + +Manages a directory in your account (directory owner) shared with another account (directory consumer). + +## Example Usage + +```terraform +resource "aws_directory_service_directory" "example" { + name = "tf-example" + password = "SuperSecretPassw0rd" + type = "MicrosoftAD" + edition = "Standard" + + vpc_settings { + vpc_id = aws_vpc.example.id + subnet_ids = aws_subnet.example[*].id + } +} + +resource "aws_directory_service_shared_directory" "example" { + directory_id = aws_directory_service_directory.example.id + notes = "You wanna have a catch?" + + target { + id = data.aws_caller_identity.receiver.account_id + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `directory_id` - (Required) Identifier of the Managed Microsoft AD directory that you want to share with other accounts. +* `target` - (Required) Identifier for the directory consumer account with whom the directory is to be shared. See below. + +The following arguments are optional: + +* `method` - (Optional) Method used when sharing a directory. Valid values are `ORGANIZATIONS` and `HANDSHAKE`. Default is `HANDSHAKE`. +* `notes` - (Optional, Sensitive) Message sent by the directory owner to the directory consumer to help the directory consumer administrator determine whether to approve or reject the share invitation. + +### `target` + +* `id` - (Required) Identifier of the directory consumer account. +* `type` - (Optional) Type of identifier to be used in the `id` field. Valid value is `ACCOUNT`. Default is `ACCOUNT`. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - Identifier of the shared directory. +* `shared_directory_id` - Identifier of the directory that is stored in the directory consumer account that corresponds to the shared directory in the owner account. + +## Import + +Directory Service Shared Directories can be imported using the owner directory ID/shared directory ID, e.g., + +``` +$ terraform import aws_directory_service_shared_directory.example d-1234567890/d-9267633ece +``` diff --git a/website/docs/r/directory_service_shared_directory_accepter.html.markdown b/website/docs/r/directory_service_shared_directory_accepter.html.markdown new file mode 100644 index 00000000000..8df17c3579e --- /dev/null +++ b/website/docs/r/directory_service_shared_directory_accepter.html.markdown @@ -0,0 +1,56 @@ +--- +subcategory: "DS (Directory Service)" +layout: "aws" +page_title: "AWS: aws_directory_service_shared_directory_accepter" +description: |- + Accepts a shared directory in a consumer account. +--- + +# Resource: aws_directory_service_shared_directory_accepter + +Accepts a shared directory in a consumer account. + +~> **NOTE:** Destroying this resource removes the shared directory from the consumer account only. + +## Example Usage + +```terraform +resource "aws_directory_service_shared_directory" "example" { + directory_id = aws_directory_service_directory.example.id + notes = "example" + + target { + id = data.aws_caller_identity.receiver.account_id + } +} + +resource "aws_directory_service_shared_directory_accepter" "example" { + provider = "awsalternate" + + shared_directory_id = aws_directory_service_shared_directory.example.shared_directory_id +} +``` + +## Argument Reference + +The following arguments are required: + +* `shared_directory_id` - (Required) Identifier of the directory that is stored in the directory consumer account that corresponds to the shared directory in the owner account. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - Identifier of the shared directory. +* `method` - Method used when sharing a directory (i.e., `ORGANIZATIONS` or `HANDSHAKE`). +* `notes` - Message sent by the directory owner to the directory consumer to help the directory consumer administrator determine whether to approve or reject the share invitation. +* `owner_account_id` - Account identifier of the directory owner. +* `owner_directory_id` - Identifier of the Managed Microsoft AD directory from the perspective of the directory owner. + +## Import + +Directory Service Shared Directories can be imported using the shared directory ID, e.g., + +``` +$ terraform import aws_directory_service_shared_directory_accepter.example d-9267633ece +```