diff --git a/.changelog/21060.txt b/.changelog/21060.txt new file mode 100644 index 00000000000..6d2ed8db6ef --- /dev/null +++ b/.changelog/21060.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_s3control_multi_region_access_point +``` + +```release-note:new-resource +aws_s3control_multi_region_access_point_policy +``` \ No newline at end of file diff --git a/internal/acctest/acctest.go b/internal/acctest/acctest.go index 34a24835bd5..3d80b6ac7b4 100644 --- a/internal/acctest/acctest.go +++ b/internal/acctest/acctest.go @@ -375,6 +375,21 @@ func MatchResourceAttrRegionalHostname(resourceName, attributeName, serviceName } } +// MatchResourceAttrGlobalHostname ensures the Terraform state regexp matches a formatted DNS hostname with partition DNS suffix and without region +func MatchResourceAttrGlobalHostname(resourceName, attributeName, serviceName string, hostnamePrefixRegexp *regexp.Regexp) resource.TestCheckFunc { + return func(s *terraform.State) error { + hostnameRegexpPattern := fmt.Sprintf("%s\\.%s\\.%s$", hostnamePrefixRegexp.String(), serviceName, PartitionDNSSuffix()) + + hostnameRegexp, err := regexp.Compile(hostnameRegexpPattern) + + if err != nil { + return fmt.Errorf("Unable to compile hostname regexp (%s): %w", hostnameRegexp, err) + } + + return resource.TestMatchResourceAttr(resourceName, attributeName, hostnameRegexp)(s) + } +} + // CheckResourceAttrGlobalARN ensures the Terraform state exactly matches a formatted ARN without region func CheckResourceAttrGlobalARN(resourceName, attributeName, arnService, arnResource string) resource.TestCheckFunc { return func(s *terraform.State) error { diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 87db2d3e31b..3999b1ea157 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1478,11 +1478,13 @@ func Provider() *schema.Provider { "aws_s3_bucket_public_access_block": s3.ResourceBucketPublicAccessBlock(), "aws_s3_object_copy": s3.ResourceObjectCopy(), - "aws_s3_access_point": s3control.ResourceAccessPoint(), - "aws_s3_account_public_access_block": s3control.ResourceAccountPublicAccessBlock(), - "aws_s3control_bucket": s3control.ResourceBucket(), - "aws_s3control_bucket_lifecycle_configuration": s3control.ResourceBucketLifecycleConfiguration(), - "aws_s3control_bucket_policy": s3control.ResourceBucketPolicy(), + "aws_s3_access_point": s3control.ResourceAccessPoint(), + "aws_s3_account_public_access_block": s3control.ResourceAccountPublicAccessBlock(), + "aws_s3control_bucket": s3control.ResourceBucket(), + "aws_s3control_bucket_lifecycle_configuration": s3control.ResourceBucketLifecycleConfiguration(), + "aws_s3control_bucket_policy": s3control.ResourceBucketPolicy(), + "aws_s3control_multi_region_access_point": s3control.ResourceMultiRegionAccessPoint(), + "aws_s3control_multi_region_access_point_policy": s3control.ResourceMultiRegionAccessPointPolicy(), "aws_s3outposts_endpoint": s3outposts.ResourceEndpoint(), diff --git a/internal/service/s3control/access_point.go b/internal/service/s3control/access_point.go index 3d9581ff388..0a3a39cf81a 100644 --- a/internal/service/s3control/access_point.go +++ b/internal/service/s3control/access_point.go @@ -178,7 +178,7 @@ func resourceAccessPointCreate(d *schema.ResourceData, meta interface{}) error { func resourceAccessPointRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).S3ControlConn - accountId, name, err := AccessPointParseID(d.Id()) + accountId, name, err := AccessPointParseResourceID(d.Id()) if err != nil { return err } @@ -291,7 +291,7 @@ func resourceAccessPointRead(d *schema.ResourceData, meta interface{}) error { func resourceAccessPointUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).S3ControlConn - accountId, name, err := AccessPointParseID(d.Id()) + accountId, name, err := AccessPointParseResourceID(d.Id()) if err != nil { return err } @@ -327,7 +327,7 @@ func resourceAccessPointUpdate(d *schema.ResourceData, meta interface{}) error { func resourceAccessPointDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).S3ControlConn - accountId, name, err := AccessPointParseID(d.Id()) + accountId, name, err := AccessPointParseResourceID(d.Id()) if err != nil { return err } @@ -349,21 +349,31 @@ func resourceAccessPointDelete(d *schema.ResourceData, meta interface{}) error { return nil } -// AccessPointParseID returns the Account ID and Access Point Name (S3) or ARN (S3 on Outposts) -func AccessPointParseID(id string) (string, string, error) { - parsedARN, err := arn.Parse(id) +const accessPointResourceIDSeparator = ":" - if err == nil { - return parsedARN.AccountID, id, nil +func AccessPointCreateResourceID(accessPointARN, accountID, accessPointName string) string { + if v, err := arn.Parse(accessPointARN); err != nil && v.Service == "s3-outposts" { + return accessPointARN } - parts := strings.SplitN(id, ":", 2) + parts := []string{accountID, accessPointName} + id := strings.Join(parts, accessPointResourceIDSeparator) - if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - return "", "", fmt.Errorf("unexpected format of ID (%s), expected ACCOUNT_ID:NAME", id) + return id +} + +func AccessPointParseResourceID(id string) (string, string, error) { + if v, err := arn.Parse(id); err == nil { + return v.AccountID, id, nil + } + + parts := strings.Split(id, multiRegionAccessPointResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil } - return parts[0], parts[1], nil + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected account-id%[2]saccess-point-name", id, accessPointResourceIDSeparator) } func expandS3AccessPointVpcConfiguration(vConfig []interface{}) *s3control.VpcConfiguration { diff --git a/internal/service/s3control/access_point_test.go b/internal/service/s3control/access_point_test.go index 5333c0eb335..e2ad0348654 100644 --- a/internal/service/s3control/access_point_test.go +++ b/internal/service/s3control/access_point_test.go @@ -333,7 +333,7 @@ func testAccCheckAccessPointDisappears(n string) resource.TestCheckFunc { return fmt.Errorf("No S3 Access Point ID is set") } - accountId, name, err := tfs3control.AccessPointParseID(rs.Primary.ID) + accountId, name, err := tfs3control.AccessPointParseResourceID(rs.Primary.ID) if err != nil { return err } @@ -360,7 +360,7 @@ func testAccCheckAccessPointDestroy(s *terraform.State) error { continue } - accountId, name, err := tfs3control.AccessPointParseID(rs.Primary.ID) + accountId, name, err := tfs3control.AccessPointParseResourceID(rs.Primary.ID) if err != nil { return err } @@ -387,7 +387,7 @@ func testAccCheckAccessPointExists(n string, output *s3control.GetAccessPointOut return fmt.Errorf("No S3 Access Point ID is set") } - accountId, name, err := tfs3control.AccessPointParseID(rs.Primary.ID) + accountId, name, err := tfs3control.AccessPointParseResourceID(rs.Primary.ID) if err != nil { return err } @@ -419,7 +419,7 @@ func testAccCheckAccessPointHasPolicy(n string, fn func() string) resource.TestC return fmt.Errorf("No S3 Access Point ID is set") } - accountId, name, err := tfs3control.AccessPointParseID(rs.Primary.ID) + accountId, name, err := tfs3control.AccessPointParseResourceID(rs.Primary.ID) if err != nil { return err } diff --git a/internal/service/s3control/consts.go b/internal/service/s3control/consts.go new file mode 100644 index 00000000000..68a30d284a3 --- /dev/null +++ b/internal/service/s3control/consts.go @@ -0,0 +1,7 @@ +package s3control + +// AsyncOperation.RequestStatus values. +const ( + RequestStatusFailed = "FAILED" + RequestStatusSucceeded = "SUCCEEDED" +) diff --git a/internal/service/s3control/errors.go b/internal/service/s3control/errors.go index 3d60a955008..adc8d4e5ec4 100644 --- a/internal/service/s3control/errors.go +++ b/internal/service/s3control/errors.go @@ -4,6 +4,8 @@ package s3control // https://docs.aws.amazon.com/sdk-for-go/api/service/s3control/#pkg-constants //nolint:deadcode,varcheck // These constants are missing from the AWS SDK const ( - errCodeNoSuchAccessPoint = "NoSuchAccessPoint" - errCodeNoSuchAccessPointPolicy = "NoSuchAccessPointPolicy" + errCodeNoSuchAccessPoint = "NoSuchAccessPoint" + errCodeNoSuchAccessPointPolicy = "NoSuchAccessPointPolicy" + errCodeNoSuchAsyncRequest = "NoSuchAsyncRequest" + errCodeNoSuchMultiRegionAccessPoint = "NoSuchMultiRegionAccessPoint" ) diff --git a/internal/service/s3control/find.go b/internal/service/s3control/find.go index e8ce1e01b46..f9104f11100 100644 --- a/internal/service/s3control/find.go +++ b/internal/service/s3control/find.go @@ -3,6 +3,9 @@ package s3control import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/s3control" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) func findPublicAccessBlockConfiguration(conn *s3control.S3Control, accountID string) (*s3control.PublicAccessBlockConfiguration, error) { @@ -22,3 +25,81 @@ func findPublicAccessBlockConfiguration(conn *s3control.S3Control, accountID str return output.PublicAccessBlockConfiguration, nil } + +func FindMultiRegionAccessPointByAccountIDAndName(conn *s3control.S3Control, accountID string, name string) (*s3control.MultiRegionAccessPointReport, error) { + input := &s3control.GetMultiRegionAccessPointInput{ + AccountId: aws.String(accountID), + Name: aws.String(name), + } + + output, err := conn.GetMultiRegionAccessPoint(input) + + if tfawserr.ErrCodeEquals(err, errCodeNoSuchMultiRegionAccessPoint) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.AccessPoint == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.AccessPoint, nil +} + +func findMultiRegionAccessPointOperationByAccountIDAndTokenARN(conn *s3control.S3Control, accountID string, requestTokenARN string) (*s3control.AsyncOperation, error) { + input := &s3control.DescribeMultiRegionAccessPointOperationInput{ + AccountId: aws.String(accountID), + RequestTokenARN: aws.String(requestTokenARN), + } + + output, err := conn.DescribeMultiRegionAccessPointOperation(input) + + if tfawserr.ErrCodeEquals(err, errCodeNoSuchAsyncRequest) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.AsyncOperation == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.AsyncOperation, nil +} + +func FindMultiRegionAccessPointPolicyDocumentByAccountIDAndName(conn *s3control.S3Control, accountID string, name string) (*s3control.MultiRegionAccessPointPolicyDocument, error) { + input := &s3control.GetMultiRegionAccessPointPolicyInput{ + AccountId: aws.String(accountID), + Name: aws.String(name), + } + + output, err := conn.GetMultiRegionAccessPointPolicy(input) + + if tfawserr.ErrCodeEquals(err, errCodeNoSuchMultiRegionAccessPoint) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Policy == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.Policy, nil +} diff --git a/internal/service/s3control/multi_region_access_point.go b/internal/service/s3control/multi_region_access_point.go new file mode 100644 index 00000000000..829be111286 --- /dev/null +++ b/internal/service/s3control/multi_region_access_point.go @@ -0,0 +1,460 @@ +package s3control + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/aws/aws-sdk-go/service/s3control" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "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/internal/verify" +) + +func ResourceMultiRegionAccessPoint() *schema.Resource { + return &schema.Resource{ + Create: resourceMultiRegionAccessPointCreate, + Read: resourceMultiRegionAccessPointRead, + Delete: resourceMultiRegionAccessPointDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(60 * time.Minute), + Delete: schema.DefaultTimeout(15 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "account_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: verify.ValidAccountID, + }, + "alias": { + Type: schema.TypeString, + Computed: true, + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "details": { + Type: schema.TypeList, + Required: true, + MinItems: 1, + MaxItems: 1, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateS3MultiRegionAccessPointName, + }, + "public_access_block": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MinItems: 0, + MaxItems: 1, + DiffSuppressFunc: verify.SuppressMissingOptionalConfigurationBlock, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "block_public_acls": { + Type: schema.TypeBool, + Optional: true, + Default: true, + ForceNew: true, + }, + "block_public_policy": { + Type: schema.TypeBool, + Optional: true, + Default: true, + ForceNew: true, + }, + "ignore_public_acls": { + Type: schema.TypeBool, + Optional: true, + Default: true, + ForceNew: true, + }, + "restrict_public_buckets": { + Type: schema.TypeBool, + Optional: true, + Default: true, + ForceNew: true, + }, + }, + }, + }, + "region": { + Type: schema.TypeSet, + Required: true, + ForceNew: true, + MinItems: 1, + MaxItems: 20, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "bucket": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(3, 255), + }, + }, + }, + }, + }, + }, + }, + "domain_name": { + Type: schema.TypeString, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceMultiRegionAccessPointCreate(d *schema.ResourceData, meta interface{}) error { + conn, err := S3ControlConn(meta.(*conns.AWSClient)) + + if err != nil { + return err + } + + accountID := meta.(*conns.AWSClient).AccountID + if v, ok := d.GetOk("account_id"); ok { + accountID = v.(string) + } + + input := &s3control.CreateMultiRegionAccessPointInput{ + AccountId: aws.String(accountID), + } + + if v, ok := d.GetOk("details"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.Details = expandCreateMultiRegionAccessPointInput_(v.([]interface{})[0].(map[string]interface{})) + } + + resourceID := MultiRegionAccessPointCreateResourceID(accountID, aws.StringValue(input.Details.Name)) + + log.Printf("[DEBUG] Creating S3 Multi-Region Access Point: %s", input) + output, err := conn.CreateMultiRegionAccessPoint(input) + + if err != nil { + return fmt.Errorf("error creating S3 Multi-Region Access Point (%s): %w", resourceID, err) + } + + d.SetId(resourceID) + + _, err = waitMultiRegionAccessPointRequestSucceeded(conn, accountID, aws.StringValue(output.RequestTokenARN), d.Timeout(schema.TimeoutCreate)) + + if err != nil { + return fmt.Errorf("error waiting for Multi-Region Access Point (%s) create: %s", d.Id(), err) + } + + return resourceMultiRegionAccessPointRead(d, meta) +} + +func resourceMultiRegionAccessPointRead(d *schema.ResourceData, meta interface{}) error { + conn, err := S3ControlConn(meta.(*conns.AWSClient)) + + if err != nil { + return err + } + + accountID, name, err := MultiRegionAccessPointParseResourceID(d.Id()) + + if err != nil { + return err + } + + accessPoint, err := FindMultiRegionAccessPointByAccountIDAndName(conn, accountID, name) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] S3 Multi-Region Access Point (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading S3 Multi-Region Access Point (%s): %w", d.Id(), err) + } + + alias := aws.StringValue(accessPoint.Alias) + arn := arn.ARN{ + Partition: meta.(*conns.AWSClient).Partition, + Service: "s3", + AccountID: accountID, + Resource: fmt.Sprintf("accesspoint/%s", alias), + }.String() + d.Set("account_id", accountID) + d.Set("alias", alias) + d.Set("arn", arn) + if err := d.Set("details", []interface{}{flattenMultiRegionAccessPointReport(accessPoint)}); err != nil { + return fmt.Errorf("error setting details: %w", err) + } + // https://docs.aws.amazon.com/AmazonS3/latest/userguide//MultiRegionAccessPointRequests.html#MultiRegionAccessPointHostnames. + d.Set("domain_name", meta.(*conns.AWSClient).PartitionHostname(fmt.Sprintf("%s.accesspoint.s3-global", alias))) + d.Set("status", accessPoint.Status) + + return nil +} + +func resourceMultiRegionAccessPointDelete(d *schema.ResourceData, meta interface{}) error { + conn, err := S3ControlConn(meta.(*conns.AWSClient)) + + if err != nil { + return err + } + + accountID, name, err := MultiRegionAccessPointParseResourceID(d.Id()) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Deleting S3 Multi-Region Access Point: %s", d.Id()) + output, err := conn.DeleteMultiRegionAccessPoint(&s3control.DeleteMultiRegionAccessPointInput{ + AccountId: aws.String(accountID), + Details: &s3control.DeleteMultiRegionAccessPointInput_{ + Name: aws.String(name), + }, + }) + + if tfawserr.ErrCodeEquals(err, errCodeNoSuchMultiRegionAccessPoint) { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting S3 Multi-Region Access Point (%s): %w", d.Id(), err) + } + + _, err = waitMultiRegionAccessPointRequestSucceeded(conn, accountID, aws.StringValue(output.RequestTokenARN), d.Timeout(schema.TimeoutDelete)) + + if err != nil { + return fmt.Errorf("error waiting for S3 Multi-Region Access Point (%s) delete: %w", d.Id(), err) + } + + return nil +} + +func S3ControlConn(client *conns.AWSClient) (*s3control.S3Control, error) { + originalConn := client.S3ControlConn + // All Multi-Region Access Point actions are routed to the US West (Oregon) Region. + region := endpoints.UsWest2RegionID + + if originalConn.Config.Region != nil && aws.StringValue(originalConn.Config.Region) == region { + return originalConn, nil + } + + sess, err := conns.NewSessionForRegion(&originalConn.Config, region, client.TerraformVersion) + + if err != nil { + return nil, fmt.Errorf("error creating AWS session: %w", err) + } + + return s3control.New(sess), nil +} + +const multiRegionAccessPointResourceIDSeparator = ":" + +func MultiRegionAccessPointCreateResourceID(accountID, accessPointName string) string { + parts := []string{accountID, accessPointName} + id := strings.Join(parts, multiRegionAccessPointResourceIDSeparator) + + return id +} + +func MultiRegionAccessPointParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, multiRegionAccessPointResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected account-id%[2]saccess-point-name", id, multiRegionAccessPointResourceIDSeparator) +} + +func expandCreateMultiRegionAccessPointInput_(tfMap map[string]interface{}) *s3control.CreateMultiRegionAccessPointInput_ { + if tfMap == nil { + return nil + } + + apiObject := &s3control.CreateMultiRegionAccessPointInput_{} + + if v, ok := tfMap["name"].(string); ok { + apiObject.Name = aws.String(v) + } + + if v, ok := tfMap["public_access_block"].([]interface{}); ok && len(v) > 0 { + apiObject.PublicAccessBlock = expandPublicAccessBlockConfiguration(v[0].(map[string]interface{})) + } + + if v, ok := tfMap["region"].(*schema.Set); ok && v.Len() > 0 { + apiObject.Regions = expandRegions(v.List()) + } + + return apiObject +} + +func expandPublicAccessBlockConfiguration(tfMap map[string]interface{}) *s3control.PublicAccessBlockConfiguration { + if tfMap == nil { + return nil + } + + apiObject := &s3control.PublicAccessBlockConfiguration{} + + if v, ok := tfMap["block_public_acls"].(bool); ok { + apiObject.BlockPublicAcls = aws.Bool(v) + } + + if v, ok := tfMap["block_public_policy"].(bool); ok { + apiObject.BlockPublicPolicy = aws.Bool(v) + } + + if v, ok := tfMap["ignore_public_acls"].(bool); ok { + apiObject.IgnorePublicAcls = aws.Bool(v) + } + + if v, ok := tfMap["restrict_public_buckets"].(bool); ok { + apiObject.RestrictPublicBuckets = aws.Bool(v) + } + + return apiObject +} + +func expandRegion(tfMap map[string]interface{}) *s3control.Region { + if tfMap == nil { + return nil + } + + apiObject := &s3control.Region{} + + if v, ok := tfMap["bucket"].(string); ok { + apiObject.Bucket = aws.String(v) + } + + return apiObject +} + +func expandRegions(tfList []interface{}) []*s3control.Region { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*s3control.Region + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandRegion(tfMap) + + if apiObject == nil { + continue + } + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func flattenMultiRegionAccessPointReport(apiObject *s3control.MultiRegionAccessPointReport) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.Name; v != nil { + tfMap["name"] = aws.StringValue(v) + } + + if v := apiObject.PublicAccessBlock; v != nil { + tfMap["public_access_block"] = []interface{}{flattenPublicAccessBlockConfiguration(v)} + } + + if v := apiObject.Regions; v != nil { + tfMap["region"] = flattenRegionReports(v) + } + + return tfMap +} + +func flattenPublicAccessBlockConfiguration(apiObject *s3control.PublicAccessBlockConfiguration) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.BlockPublicAcls; v != nil { + tfMap["block_public_acls"] = aws.BoolValue(v) + } + + if v := apiObject.BlockPublicPolicy; v != nil { + tfMap["block_public_policy"] = aws.BoolValue(v) + } + + if v := apiObject.IgnorePublicAcls; v != nil { + tfMap["ignore_public_acls"] = aws.BoolValue(v) + } + + if v := apiObject.RestrictPublicBuckets; v != nil { + tfMap["restrict_public_buckets"] = aws.BoolValue(v) + } + + return tfMap +} + +func flattenRegionReport(apiObject *s3control.RegionReport) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.Bucket; v != nil { + tfMap["bucket"] = aws.StringValue(v) + } + + return tfMap +} + +func flattenRegionReports(apiObjects []*s3control.RegionReport) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenRegionReport(apiObject)) + } + + return tfList +} diff --git a/internal/service/s3control/multi_region_access_point_policy.go b/internal/service/s3control/multi_region_access_point_policy.go new file mode 100644 index 00000000000..704a8754700 --- /dev/null +++ b/internal/service/s3control/multi_region_access_point_policy.go @@ -0,0 +1,233 @@ +package s3control + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/s3control" + "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/internal/verify" +) + +func ResourceMultiRegionAccessPointPolicy() *schema.Resource { + return &schema.Resource{ + Create: resourceMultiRegionAccessPointPolicyCreate, + Read: resourceMultiRegionAccessPointPolicyRead, + Update: resourceMultiRegionAccessPointPolicyUpdate, + Delete: schema.Noop, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(15 * time.Minute), + Update: schema.DefaultTimeout(15 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "account_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: verify.ValidAccountID, + }, + "details": { + Type: schema.TypeList, + Required: true, + MinItems: 1, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateS3MultiRegionAccessPointName, + }, + "policy": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsJSON, + DiffSuppressFunc: verify.SuppressEquivalentPolicyDiffs, + }, + }, + }, + }, + "established": { + Type: schema.TypeString, + Computed: true, + }, + "proposed": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceMultiRegionAccessPointPolicyCreate(d *schema.ResourceData, meta interface{}) error { + conn, err := S3ControlConn(meta.(*conns.AWSClient)) + + if err != nil { + return err + } + + accountID := meta.(*conns.AWSClient).AccountID + if v, ok := d.GetOk("account_id"); ok { + accountID = v.(string) + } + + input := &s3control.PutMultiRegionAccessPointPolicyInput{ + AccountId: aws.String(accountID), + } + + if v, ok := d.GetOk("details"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.Details = expandPutMultiRegionAccessPointPolicyInput_(v.([]interface{})[0].(map[string]interface{})) + } + + resourceID := MultiRegionAccessPointCreateResourceID(accountID, aws.StringValue(input.Details.Name)) + + log.Printf("[DEBUG] Creating S3 Multi-Region Access Point Policy: %s", input) + output, err := conn.PutMultiRegionAccessPointPolicy(input) + + if err != nil { + return fmt.Errorf("error creating S3 Multi-Region Access Point (%s) Policy: %w", resourceID, err) + } + + d.SetId(resourceID) + + _, err = waitMultiRegionAccessPointRequestSucceeded(conn, accountID, aws.StringValue(output.RequestTokenARN), d.Timeout(schema.TimeoutCreate)) + + if err != nil { + return fmt.Errorf("error waiting for S3 Multi-Region Access Point Policy (%s) create: %w", d.Id(), err) + } + + return resourceMultiRegionAccessPointPolicyRead(d, meta) +} + +func resourceMultiRegionAccessPointPolicyRead(d *schema.ResourceData, meta interface{}) error { + conn, err := S3ControlConn(meta.(*conns.AWSClient)) + + if err != nil { + return err + } + + accountID, name, err := MultiRegionAccessPointParseResourceID(d.Id()) + + if err != nil { + return err + } + + policyDocument, err := FindMultiRegionAccessPointPolicyDocumentByAccountIDAndName(conn, accountID, name) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] S3 Multi-Region Access Point Policy (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading S3 Multi-Region Access Point Policy (%s): %w", d.Id(), err) + } + + d.Set("account_id", accountID) + if policyDocument != nil { + if err := d.Set("details", []interface{}{flattenMultiRegionAccessPointPolicyDocument(name, policyDocument)}); err != nil { + return fmt.Errorf("error setting details: %w", err) + } + } else { + d.Set("details", nil) + } + if v := policyDocument.Established; v != nil { + d.Set("established", v.Policy) + } else { + d.Set("established", nil) + } + if v := policyDocument.Proposed; v != nil { + d.Set("proposed", v.Policy) + } else { + d.Set("proposed", nil) + } + + return nil +} + +func resourceMultiRegionAccessPointPolicyUpdate(d *schema.ResourceData, meta interface{}) error { + conn, err := S3ControlConn(meta.(*conns.AWSClient)) + + if err != nil { + return err + } + + accountID, _, err := MultiRegionAccessPointParseResourceID(d.Id()) + + if err != nil { + return err + } + + input := &s3control.PutMultiRegionAccessPointPolicyInput{ + AccountId: aws.String(accountID), + } + + if v, ok := d.GetOk("details"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.Details = expandPutMultiRegionAccessPointPolicyInput_(v.([]interface{})[0].(map[string]interface{})) + } + + log.Printf("[DEBUG] Updating S3 Multi-Region Access Point Policy: %s", input) + output, err := conn.PutMultiRegionAccessPointPolicy(input) + + if err != nil { + return fmt.Errorf("error updating S3 Multi-Region Access Point Policy (%s): %w", d.Id(), err) + } + + _, err = waitMultiRegionAccessPointRequestSucceeded(conn, accountID, aws.StringValue(output.RequestTokenARN), d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return fmt.Errorf("error waiting for S3 Multi-Region Access Point Policy (%s) update: %w", d.Id(), err) + } + + return resourceMultiRegionAccessPointPolicyRead(d, meta) +} + +func expandPutMultiRegionAccessPointPolicyInput_(tfMap map[string]interface{}) *s3control.PutMultiRegionAccessPointPolicyInput_ { + if tfMap == nil { + return nil + } + + apiObject := &s3control.PutMultiRegionAccessPointPolicyInput_{} + + if v, ok := tfMap["name"].(string); ok { + apiObject.Name = aws.String(v) + } + + if v, ok := tfMap["policy"].(string); ok { + apiObject.Policy = aws.String(v) + } + + return apiObject +} + +func flattenMultiRegionAccessPointPolicyDocument(name string, apiObject *s3control.MultiRegionAccessPointPolicyDocument) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + tfMap["name"] = name + + if v := apiObject.Proposed; v != nil { + if v := v.Policy; v != nil { + tfMap["policy"] = aws.StringValue(v) + } + } + + return tfMap +} diff --git a/internal/service/s3control/multi_region_access_point_policy_test.go b/internal/service/s3control/multi_region_access_point_policy_test.go new file mode 100644 index 00000000000..4b90b3d29f1 --- /dev/null +++ b/internal/service/s3control/multi_region_access_point_policy_test.go @@ -0,0 +1,271 @@ +package s3control_test + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/s3control" + 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/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfs3control "github.com/hashicorp/terraform-provider-aws/internal/service/s3control" +) + +func TestAccS3ControlMultiRegionAccessPointPolicy_basic(t *testing.T) { + var v s3control.MultiRegionAccessPointPolicyDocument + resourceName := "aws_s3control_multi_region_access_point_policy.test" + bucketName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + multiRegionAccessPointName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + if acctest.Partition() == "aws-us-gov" { + t.Skip("S3 Multi-Region Access Point is not supported in GovCloud partition") + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3control.EndpointsID), + Providers: acctest.Providers, + // Multi-Region Access Point Policy cannot be deleted once applied. + // Ensure parent resource is destroyed instead. + CheckDestroy: testAccCheckMultiRegionAccessPointDestroy, + Steps: []resource.TestStep{ + { + Config: testAccMultiRegionAccessPointPolicyConfig_basic(bucketName, multiRegionAccessPointName), + Check: resource.ComposeTestCheckFunc( + testAccCheckMultiRegionAccessPointPolicyExists(resourceName, &v), + acctest.CheckResourceAttrAccountID(resourceName, "account_id"), + resource.TestCheckResourceAttr(resourceName, "details.#", "1"), + resource.TestCheckResourceAttr(resourceName, "details.0.name", multiRegionAccessPointName), + resource.TestCheckResourceAttrSet(resourceName, "details.0.policy"), + resource.TestCheckResourceAttrSet(resourceName, "established"), + resource.TestCheckResourceAttrSet(resourceName, "proposed"), + resource.TestCheckResourceAttrPair(resourceName, "details.0.policy", resourceName, "proposed"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccS3ControlMultiRegionAccessPointPolicy_disappears_MultiRegionAccessPoint(t *testing.T) { + var v s3control.MultiRegionAccessPointReport + parentResourceName := "aws_s3control_multi_region_access_point.test" + resourceName := "aws_s3control_multi_region_access_point_policy.test" + bucketName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + if acctest.Partition() == "aws-us-gov" { + t.Skip("S3 Multi-Region Access Point is not supported in GovCloud partition") + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3control.EndpointsID), + Providers: acctest.Providers, + // Multi-Region Access Point Policy cannot be deleted once applied. + // Ensure parent resource is destroyed instead. + CheckDestroy: testAccCheckMultiRegionAccessPointDestroy, + Steps: []resource.TestStep{ + { + Config: testAccMultiRegionAccessPointPolicyConfig_basic(bucketName, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckMultiRegionAccessPointExists(resourceName, &v), + acctest.CheckResourceDisappears(acctest.Provider, tfs3control.ResourceMultiRegionAccessPoint(), parentResourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccS3ControlMultiRegionAccessPointPolicy_details_policy(t *testing.T) { + var v1, v2 s3control.MultiRegionAccessPointPolicyDocument + resourceName := "aws_s3control_multi_region_access_point_policy.test" + multiRegionAccessPointName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + bucketName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + if acctest.Partition() == "aws-us-gov" { + t.Skip("S3 Multi-Region Access Point is not supported in GovCloud partition") + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3control.EndpointsID), + Providers: acctest.Providers, + // Multi-Region Access Point Policy cannot be deleted once applied. + // Ensure parent resource is destroyed instead. + CheckDestroy: testAccCheckMultiRegionAccessPointDestroy, + Steps: []resource.TestStep{ + { + Config: testAccMultiRegionAccessPointPolicyConfig_basic(bucketName, multiRegionAccessPointName), + Check: resource.ComposeTestCheckFunc( + testAccCheckMultiRegionAccessPointPolicyExists(resourceName, &v1), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccMultiRegionAccessPointPolicyConfig_updatedStatement(bucketName, multiRegionAccessPointName), + Check: resource.ComposeTestCheckFunc( + testAccCheckMultiRegionAccessPointPolicyExists(resourceName, &v2), + testAccCheckMultiRegionAccessPointPolicyChanged(&v1, &v2), + ), + }, + }, + }) +} + +func TestAccS3ControlMultiRegionAccessPointPolicy_details_name(t *testing.T) { + var v1, v2 s3control.MultiRegionAccessPointPolicyDocument + resourceName := "aws_s3control_multi_region_access_point_policy.test" + multiRegionAccessPointName1 := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + multiRegionAccessPointName2 := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + bucketName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + if acctest.Partition() == "aws-us-gov" { + t.Skip("S3 Multi-Region Access Point is not supported in GovCloud partition") + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3control.EndpointsID), + Providers: acctest.Providers, + // Multi-Region Access Point Policy cannot be deleted once applied. + // Ensure parent resource is destroyed instead. + CheckDestroy: testAccCheckMultiRegionAccessPointDestroy, + Steps: []resource.TestStep{ + { + Config: testAccMultiRegionAccessPointPolicyConfig_basic(bucketName, multiRegionAccessPointName1), + Check: resource.ComposeTestCheckFunc( + testAccCheckMultiRegionAccessPointPolicyExists(resourceName, &v1), + resource.TestCheckResourceAttr(resourceName, "details.0.name", multiRegionAccessPointName1), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccMultiRegionAccessPointPolicyConfig_basic(bucketName, multiRegionAccessPointName2), + Check: resource.ComposeTestCheckFunc( + testAccCheckMultiRegionAccessPointPolicyExists(resourceName, &v2), + resource.TestCheckResourceAttr(resourceName, "details.0.name", multiRegionAccessPointName2), + ), + }, + }, + }) +} + +func testAccCheckMultiRegionAccessPointPolicyExists(n string, v *s3control.MultiRegionAccessPointPolicyDocument) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No S3 Multi-Region Access Point Policy ID is set") + } + + accountID, name, err := tfs3control.MultiRegionAccessPointParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + conn, err := tfs3control.S3ControlConn(acctest.Provider.Meta().(*conns.AWSClient)) + + if err != nil { + return err + } + + output, err := tfs3control.FindMultiRegionAccessPointPolicyDocumentByAccountIDAndName(conn, accountID, name) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccCheckMultiRegionAccessPointPolicyChanged(i, j *s3control.MultiRegionAccessPointPolicyDocument) resource.TestCheckFunc { + return func(s *terraform.State) error { + if aws.StringValue(i.Proposed.Policy) == aws.StringValue(j.Proposed.Policy) { + return fmt.Errorf("S3 Multi-Region Access Point Policy did not change") + } + + return nil + } +} + +func testAccMultiRegionAccessPointPolicyConfig_basic(bucketName, multiRegionAccessPointName string) string { + return acctest.ConfigCompose( + testAccMultiRegionAccessPointConfig_basic(bucketName, multiRegionAccessPointName), + fmt.Sprintf(` +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} + +resource "aws_s3control_multi_region_access_point_policy" "test" { + details { + name = %[1]q + policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Sid" : "Test", + "Effect" : "Allow", + "Principal" : { + "AWS" : data.aws_caller_identity.current.account_id + }, + "Action" : "s3:GetObject", + "Resource" : "arn:${data.aws_partition.current.partition}:s3::${data.aws_caller_identity.current.account_id}:accesspoint/${aws_s3control_multi_region_access_point.test.alias}/object/*" + } + ] + }) + } +} +`, multiRegionAccessPointName)) +} + +func testAccMultiRegionAccessPointPolicyConfig_updatedStatement(bucketName, multiRegionAccessPointName string) string { + return acctest.ConfigCompose( + testAccMultiRegionAccessPointConfig_basic(bucketName, multiRegionAccessPointName), + fmt.Sprintf(` +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} + +resource "aws_s3control_multi_region_access_point_policy" "test" { + details { + name = %[1]q + policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Sid" : "Test", + "Effect" : "Allow", + "Principal" : { + "AWS" : data.aws_caller_identity.current.account_id + }, + "Action" : "s3:PutObject", + "Resource" : "arn:${data.aws_partition.current.partition}:s3::${data.aws_caller_identity.current.account_id}:accesspoint/${aws_s3control_multi_region_access_point.test.alias}/object/*" + } + ] + }) + } +} +`, multiRegionAccessPointName)) +} diff --git a/internal/service/s3control/multi_region_access_point_test.go b/internal/service/s3control/multi_region_access_point_test.go new file mode 100644 index 00000000000..a0aa5b83a4d --- /dev/null +++ b/internal/service/s3control/multi_region_access_point_test.go @@ -0,0 +1,389 @@ +package s3control_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/s3control" + 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" + tfs3control "github.com/hashicorp/terraform-provider-aws/internal/service/s3control" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccS3ControlMultiRegionAccessPoint_basic(t *testing.T) { + var v s3control.MultiRegionAccessPointReport + resourceName := "aws_s3control_multi_region_access_point.test" + bucketName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + if acctest.Partition() == "aws-us-gov" { + t.Skip("S3 Multi-Region Access Point is not supported in GovCloud partition") + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3control.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckMultiRegionAccessPointDestroy, + Steps: []resource.TestStep{ + { + Config: testAccMultiRegionAccessPointConfig_basic(bucketName, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckMultiRegionAccessPointExists(resourceName, &v), + acctest.CheckResourceAttrAccountID(resourceName, "account_id"), + resource.TestMatchResourceAttr(resourceName, "alias", regexp.MustCompile(`^[a-z][a-z0-9]*[.]mrap$`)), + acctest.MatchResourceAttrGlobalARN(resourceName, "arn", "s3", regexp.MustCompile(`accesspoint\/[a-z][a-z0-9]*[.]mrap$`)), + acctest.MatchResourceAttrGlobalHostname(resourceName, "domain_name", "accesspoint.s3-global", regexp.MustCompile(`^[a-z][a-z0-9]*[.]mrap`)), + resource.TestCheckResourceAttr(resourceName, "details.#", "1"), + resource.TestCheckResourceAttr(resourceName, "details.0.name", rName), + resource.TestCheckResourceAttr(resourceName, "details.0.public_access_block.#", "1"), + resource.TestCheckResourceAttr(resourceName, "details.0.public_access_block.0.block_public_acls", "true"), + resource.TestCheckResourceAttr(resourceName, "details.0.public_access_block.0.block_public_policy", "true"), + resource.TestCheckResourceAttr(resourceName, "details.0.public_access_block.0.ignore_public_acls", "true"), + resource.TestCheckResourceAttr(resourceName, "details.0.public_access_block.0.restrict_public_buckets", "true"), + resource.TestCheckResourceAttr(resourceName, "details.0.region.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "details.0.region.*", map[string]string{ + "bucket": bucketName, + }), + resource.TestCheckResourceAttr(resourceName, "status", s3control.MultiRegionAccessPointStatusReady), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccS3ControlMultiRegionAccessPoint_disappears(t *testing.T) { + var v s3control.MultiRegionAccessPointReport + resourceName := "aws_s3control_multi_region_access_point.test" + bucketName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + if acctest.Partition() == "aws-us-gov" { + t.Skip("S3 Multi-Region Access Point is not supported in GovCloud partition") + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3control.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckMultiRegionAccessPointDestroy, + Steps: []resource.TestStep{ + { + Config: testAccMultiRegionAccessPointConfig_basic(bucketName, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckMultiRegionAccessPointExists(resourceName, &v), + acctest.CheckResourceDisappears(acctest.Provider, tfs3control.ResourceMultiRegionAccessPoint(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccS3ControlMultiRegionAccessPoint_PublicAccessBlock(t *testing.T) { + var v s3control.MultiRegionAccessPointReport + resourceName := "aws_s3control_multi_region_access_point.test" + bucketName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + if acctest.Partition() == "aws-us-gov" { + t.Skip("S3 Multi-Region Access Point is not supported in GovCloud partition") + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3control.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckMultiRegionAccessPointDestroy, + Steps: []resource.TestStep{ + { + Config: testAccMultiRegionAccessPointConfig_publicAccessBlock(bucketName, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckMultiRegionAccessPointExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "details.0.public_access_block.#", "1"), + resource.TestCheckResourceAttr(resourceName, "details.0.public_access_block.0.block_public_acls", "false"), + resource.TestCheckResourceAttr(resourceName, "details.0.public_access_block.0.block_public_policy", "false"), + resource.TestCheckResourceAttr(resourceName, "details.0.public_access_block.0.ignore_public_acls", "false"), + resource.TestCheckResourceAttr(resourceName, "details.0.public_access_block.0.restrict_public_buckets", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccS3ControlMultiRegionAccessPoint_name(t *testing.T) { + var v1, v2 s3control.MultiRegionAccessPointReport + resourceName := "aws_s3control_multi_region_access_point.test" + rName1 := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName2 := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + bucketName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + if acctest.Partition() == "aws-us-gov" { + t.Skip("S3 Multi-Region Access Point is not supported in GovCloud partition") + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3control.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckMultiRegionAccessPointDestroy, + Steps: []resource.TestStep{ + { + Config: testAccMultiRegionAccessPointConfig_basic(bucketName, rName1), + Check: resource.ComposeTestCheckFunc( + testAccCheckMultiRegionAccessPointExists(resourceName, &v1), + resource.TestCheckResourceAttr(resourceName, "details.0.name", rName1), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccMultiRegionAccessPointConfig_basic(bucketName, rName2), + Check: resource.ComposeTestCheckFunc( + testAccCheckMultiRegionAccessPointExists(resourceName, &v2), + testAccCheckMultiRegionAccessPointRecreated(&v1, &v2), + resource.TestCheckResourceAttr(resourceName, "details.0.name", rName2), + ), + }, + }, + }) +} + +func TestAccS3ControlMultiRegionAccessPoint_threeRegions(t *testing.T) { + var providers []*schema.Provider + var v s3control.MultiRegionAccessPointReport + resourceName := "aws_s3control_multi_region_access_point.test" + bucket1Name := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + bucket2Name := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + bucket3Name := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + if acctest.Partition() == "aws-us-gov" { + t.Skip("S3 Multi-Region Access Point is not supported in GovCloud partition") + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckMultipleRegion(t, 3) }, + ErrorCheck: acctest.ErrorCheck(t, s3control.EndpointsID), + ProviderFactories: acctest.FactoriesMultipleRegion(&providers, 3), + CheckDestroy: testAccCheckMultiRegionAccessPointDestroy, + Steps: []resource.TestStep{ + { + Config: testAccMultiRegionAccessPointConfig_threeRegions(bucket1Name, bucket2Name, bucket3Name, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckMultiRegionAccessPointExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "details.0.region.#", "3"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "details.0.region.*", map[string]string{ + "bucket": bucket1Name, + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "details.0.region.*", map[string]string{ + "bucket": bucket2Name, + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "details.0.region.*", map[string]string{ + "bucket": bucket3Name, + }), + resource.TestCheckResourceAttr(resourceName, "status", s3control.MultiRegionAccessPointStatusReady), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckMultiRegionAccessPointDestroy(s *terraform.State) error { + conn, err := tfs3control.S3ControlConn(acctest.Provider.Meta().(*conns.AWSClient)) + + if err != nil { + return err + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_s3control_multi_region_access_point" { + continue + } + + accountID, name, err := tfs3control.MultiRegionAccessPointParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + _, err = tfs3control.FindMultiRegionAccessPointByAccountIDAndName(conn, accountID, name) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("S3 Multi-Region Access Point %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccCheckMultiRegionAccessPointExists(n string, v *s3control.MultiRegionAccessPointReport) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No S3 Multi-Region Access Point ID is set") + } + + accountID, name, err := tfs3control.MultiRegionAccessPointParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + conn, err := tfs3control.S3ControlConn(acctest.Provider.Meta().(*conns.AWSClient)) + + if err != nil { + return err + } + + output, err := tfs3control.FindMultiRegionAccessPointByAccountIDAndName(conn, accountID, name) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +// Multi-Region Access Point aliases are unique throughout time and aren’t based on the name or configuration of a Multi-Region Access Point. +// If you create a Multi-Region Access Point, and then delete it and create another one with the same name and configuration, the +// second Multi-Region Access Point will have a different alias than the first. (https://docs.aws.amazon.com/AmazonS3/latest/userguide/CreatingMultiRegionAccessPoints.html#multi-region-access-point-naming) +func testAccCheckMultiRegionAccessPointRecreated(before, after *s3control.MultiRegionAccessPointReport) resource.TestCheckFunc { + return func(s *terraform.State) error { + if before, after := aws.StringValue(before.Alias), aws.StringValue(after.Alias); before == after { + return fmt.Errorf("S3 Multi-Region Access Point (%s) not recreated", before) + } + + return nil + } +} + +func testAccMultiRegionAccessPointConfig_basic(bucketName, multiRegionAccessPointName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + force_destroy = true +} + +resource "aws_s3control_multi_region_access_point" "test" { + details { + name = %[2]q + + region { + bucket = aws_s3_bucket.test.id + } + } +} +`, bucketName, multiRegionAccessPointName) +} + +func testAccMultiRegionAccessPointConfig_publicAccessBlock(bucketName, multiRegionAccessPointName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + force_destroy = true +} + +resource "aws_s3control_multi_region_access_point" "test" { + details { + name = %[2]q + + public_access_block { + block_public_acls = false + block_public_policy = false + ignore_public_acls = false + restrict_public_buckets = false + } + + region { + bucket = aws_s3_bucket.test.id + } + } +} +`, bucketName, multiRegionAccessPointName) +} + +func testAccMultiRegionAccessPointConfig_threeRegions(bucketName1, bucketName2, bucketName3, multiRegionAccessPointName string) string { + return acctest.ConfigCompose( + acctest.ConfigMultipleRegionProvider(3), + fmt.Sprintf(` +resource "aws_s3_bucket" "test1" { + provider = aws + + bucket = %[1]q + force_destroy = true +} + +resource "aws_s3_bucket" "test2" { + provider = awsalternate + + bucket = %[2]q + force_destroy = true +} + +resource "aws_s3_bucket" "test3" { + provider = awsthird + + bucket = %[3]q + force_destroy = true +} + +resource "aws_s3control_multi_region_access_point" "test" { + provider = aws + + details { + name = %[4]q + + region { + bucket = aws_s3_bucket.test1.id + } + + region { + bucket = aws_s3_bucket.test2.id + } + + region { + bucket = aws_s3_bucket.test3.id + } + } +} +`, bucketName1, bucketName2, bucketName3, multiRegionAccessPointName)) +} diff --git a/internal/service/s3control/status.go b/internal/service/s3control/status.go index ca9268f2ac2..659ec34b685 100644 --- a/internal/service/s3control/status.go +++ b/internal/service/s3control/status.go @@ -6,6 +6,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/s3control" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) // statusPublicAccessBlockConfigurationBlockPublicACLs fetches the PublicAccessBlockConfiguration and its BlockPublicAcls @@ -75,3 +76,19 @@ func statusPublicAccessBlockConfigurationRestrictPublicBuckets(conn *s3control.S return publicAccessBlockConfiguration, strconv.FormatBool(aws.BoolValue(publicAccessBlockConfiguration.RestrictPublicBuckets)), nil } } + +func statusMultiRegionAccessPointRequest(conn *s3control.S3Control, accountID string, requestTokenARN string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := findMultiRegionAccessPointOperationByAccountIDAndTokenARN(conn, accountID, requestTokenARN) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.RequestStatus), nil + } +} diff --git a/internal/service/s3control/sweep.go b/internal/service/s3control/sweep.go index 0dad2aed52b..c8f712cd704 100644 --- a/internal/service/s3control/sweep.go +++ b/internal/service/s3control/sweep.go @@ -8,9 +8,8 @@ import ( "log" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/service/s3control" - "github.com/hashicorp/aws-sdk-go-base/tfawserr" - "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/sweep" @@ -21,6 +20,11 @@ func init() { Name: "aws_s3_access_point", F: sweepAccessPoints, }) + + resource.AddTestSweepers("aws_s3control_multi_region_access_point", &resource.Sweeper{ + Name: "aws_s3control_multi_region_access_point", + F: sweepMultiRegionAccessPoints, + }) } func sweepAccessPoints(region string) error { @@ -28,14 +32,12 @@ func sweepAccessPoints(region string) error { if err != nil { return fmt.Errorf("error getting client: %s", err) } - - accountId := client.(*conns.AWSClient).AccountID conn := client.(*conns.AWSClient).S3ControlConn - + accountID := client.(*conns.AWSClient).AccountID input := &s3control.ListAccessPointsInput{ - AccountId: aws.String(accountId), + AccountId: aws.String(accountID), } - var sweeperErrs *multierror.Error + sweepResources := make([]*sweep.SweepResource, 0) err = conn.ListAccessPointsPages(input, func(page *s3control.ListAccessPointsOutput, lastPage bool) bool { if page == nil { @@ -43,25 +45,11 @@ func sweepAccessPoints(region string) error { } for _, accessPoint := range page.AccessPointList { - input := &s3control.DeleteAccessPointInput{ - AccountId: aws.String(accountId), - Name: accessPoint.Name, - } - name := aws.StringValue(accessPoint.Name) - - log.Printf("[INFO] Deleting S3 Access Point: %s", name) - _, err := conn.DeleteAccessPoint(input) - - if tfawserr.ErrMessageContains(err, "NoSuchAccessPoint", "") { - continue - } - - if err != nil { - sweeperErr := fmt.Errorf("error deleting S3 Access Point (%s): %w", name, err) - log.Printf("[ERROR] %s", sweeperErr) - sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) - continue - } + r := ResourceAccessPoint() + d := r.Data(nil) + d.SetId(AccessPointCreateResourceID(aws.StringValue(accessPoint.AccessPointArn), accountID, aws.StringValue(accessPoint.Name))) + + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) } return !lastPage @@ -73,8 +61,64 @@ func sweepAccessPoints(region string) error { } if err != nil { - return fmt.Errorf("error listing S3 Access Points: %w", err) + return fmt.Errorf("error listing SS3 Access Points (%s): %w", region, err) + } + + err = sweep.SweepOrchestrator(sweepResources) + + if err != nil { + return fmt.Errorf("error sweeping S3 Access Points (%s): %w", region, err) + } + + return nil +} + +func sweepMultiRegionAccessPoints(region string) error { + client, err := sweep.SharedRegionalSweepClient(region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + if region != endpoints.UsWest2RegionID { + log.Printf("[WARN] Skipping S3 Multi-Region Access Point sweep for region: %s", region) + return nil + } + conn := client.(*conns.AWSClient).S3ControlConn + accountID := client.(*conns.AWSClient).AccountID + input := &s3control.ListMultiRegionAccessPointsInput{ + AccountId: aws.String(accountID), + } + sweepResources := make([]*sweep.SweepResource, 0) + + err = conn.ListMultiRegionAccessPointsPages(input, func(page *s3control.ListMultiRegionAccessPointsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, accessPoint := range page.AccessPoints { + r := ResourceMultiRegionAccessPoint() + d := r.Data(nil) + d.SetId(MultiRegionAccessPointCreateResourceID(accountID, aws.StringValue(accessPoint.Name))) + + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) + } + + return !lastPage + }) + + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping S3 Multi-Region Access Point sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("error listing S3 Multi-Region Access Points (%s): %w", region, err) + } + + err = sweep.SweepOrchestrator(sweepResources) + + if err != nil { + return fmt.Errorf("error sweeping S3 Multi-Region Access Points (%s): %w", region, err) } - return sweeperErrs.ErrorOrNil() + return nil } diff --git a/internal/service/s3control/validate.go b/internal/service/s3control/validate.go new file mode 100644 index 00000000000..b7f0af1d942 --- /dev/null +++ b/internal/service/s3control/validate.go @@ -0,0 +1,27 @@ +package s3control + +import ( + "fmt" + "regexp" +) + +func validateS3MultiRegionAccessPointName(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if len(value) < 3 || len(value) > 50 { + errors = append(errors, fmt.Errorf( + "%q cannot be less than 3 or longer than 50 characters", k)) + } + if regexp.MustCompile(`_|[A-Z]|\.`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "cannot contain underscores, uppercase letters, or periods. %q", k)) + } + if regexp.MustCompile(`^-`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q cannot begin with a hyphen", k)) + } + if regexp.MustCompile(`-$`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q cannot end with a hyphen", k)) + } + return +} diff --git a/internal/service/s3control/wait.go b/internal/service/s3control/wait.go index ae00f69715f..296b3a8d740 100644 --- a/internal/service/s3control/wait.go +++ b/internal/service/s3control/wait.go @@ -1,11 +1,14 @@ package s3control import ( + "fmt" "strconv" "time" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/s3control" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) const ( @@ -17,6 +20,10 @@ const ( // Maximum amount of time to wait for S3control changes to propagate propagationTimeout = 1 * time.Minute + + multiRegionAccessPointRequestSucceededMinTimeout = 5 * time.Second + + multiRegionAccessPointRequestSucceededDelay = 15 * time.Second ) func waitPublicAccessBlockConfigurationBlockPublicACLsUpdated(conn *s3control.S3Control, accountID string, expectedValue bool) (*s3control.PublicAccessBlockConfiguration, error) { @@ -90,3 +97,25 @@ func waitPublicAccessBlockConfigurationRestrictPublicBucketsUpdated(conn *s3cont return nil, err } + +func waitMultiRegionAccessPointRequestSucceeded(conn *s3control.S3Control, accountID string, requestTokenArn string, timeout time.Duration) (*s3control.AsyncOperation, error) { //nolint:unparam + stateConf := &resource.StateChangeConf{ + Target: []string{RequestStatusSucceeded}, + Timeout: timeout, + Refresh: statusMultiRegionAccessPointRequest(conn, accountID, requestTokenArn), + MinTimeout: multiRegionAccessPointRequestSucceededMinTimeout, + Delay: multiRegionAccessPointRequestSucceededDelay, // Wait 15 secs before starting + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*s3control.AsyncOperation); ok { + if status, responseDetails := aws.StringValue(output.RequestStatus), output.ResponseDetails; status == RequestStatusFailed && responseDetails != nil && responseDetails.ErrorDetails != nil { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(responseDetails.ErrorDetails.Code), aws.StringValue(responseDetails.ErrorDetails.Message))) + } + + return output, err + } + + return nil, err +} diff --git a/website/docs/r/s3control_multi_region_access_point.html.markdown b/website/docs/r/s3control_multi_region_access_point.html.markdown new file mode 100644 index 00000000000..1b19ee3edbc --- /dev/null +++ b/website/docs/r/s3control_multi_region_access_point.html.markdown @@ -0,0 +1,117 @@ +--- +subcategory: "S3 Control" +layout: "aws" +page_title: "AWS: aws_s3control_multi_region_access_point" +description: |- + Provides a resource to manage an S3 Multi-Region Access Point associated with specified buckets. +--- + +# Resource: aws_s3control_multi_region_access_point + +Provides a resource to manage an S3 Multi-Region Access Point associated with specified buckets. + +## Example Usage + +### Multiple AWS Buckets in Different Regions + +```terraform +provider "aws" { + region = "us-east-1" + alias = "primary_region" +} + +provider "aws" { + region = "us-west-2" + alias = "secondary_region" +} + +resource "aws_s3_bucket" "foo_bucket" { + provider = aws.primary_region + + bucket = "example-bucket-foo" +} + +resource "aws_s3_bucket" "bar_bucket" { + provider = aws.secondary_region + + bucket = "example-bucket-bar" +} + +resource "aws_s3control_multi_region_access_point" "example" { + details { + name = "example" + + region { + bucket = aws_s3_bucket.foo_bucket.id + } + + region { + bucket = aws_s3_bucket.bar_bucket.id + } + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `account_id` - (Optional) The AWS account ID for the owner of the buckets for which you want to create a Multi-Region Access Point. Defaults to automatically determined account ID of the Terraform AWS provider. +* `details` - (Required) A configuration block containing details about the Multi-Region Access Point. See [Details Configuration Block](#details-configuration) below for more details + +### Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/blocks/resources/syntax.html#operation-timeouts) for certain actions: + +* `create` - (Default `60 minutes`) Used when creating the Multi-Region Access Point. +* `delete` - (Default `15 minutes`) Used when deleting the Multi-Region Access Point. + +### Details Configuration + +The `details` block supports the following: + +* `name` - (Required) The name of the Multi-Region Access Point. +* `public_access_block` - (Optional) Configuration block to manage the `PublicAccessBlock` configuration that you want to apply to this Multi-Region Access Point. You can enable the configuration options in any combination. See [Public Access Block Configuration](#public-access-block-configuration) below for more details. +* `region` - (Required) The Region configuration block to specify the bucket associated with the Multi-Region Access Point. See [Region Configuration](#region-configuration) below for more details. + +For more information, see the documentation on [Multi-Region Access Points](https://docs.aws.amazon.com/AmazonS3/latest/userguide/MultiRegionAccessPoints.html). + +### Public Access Block Configuration + +The `public_access_block` block supports the following: + +* `block_public_acls` - (Optional) Whether Amazon S3 should block public ACLs for buckets in this account. Defaults to `true`. Enabling this setting does not affect existing policies or ACLs. When set to `true` causes the following behavior: + * PUT Bucket acl and PUT Object acl calls fail if the specified ACL is public. + * PUT Object calls fail if the request includes a public ACL. + * PUT Bucket calls fail if the request includes a public ACL. +* `block_public_policy` - (Optional) Whether Amazon S3 should block public bucket policies for buckets in this account. Defaults to `true`. Enabling this setting does not affect existing bucket policies. When set to `true` causes Amazon S3 to: + * Reject calls to PUT Bucket policy if the specified bucket policy allows public access. +* `ignore_public_acls` - (Optional) Whether Amazon S3 should ignore public ACLs for buckets in this account. Defaults to `true`. Enabling this setting does not affect the persistence of any existing ACLs and doesn't prevent new public ACLs from being set. When set to `true` causes Amazon S3 to: + * Ignore all public ACLs on buckets in this account and any objects that they contain. +* `restrict_public_buckets` - (Optional) Whether Amazon S3 should restrict public bucket policies for buckets in this account. Defaults to `true`. Enabling this setting does not affect previously stored bucket policies, except that public and cross-account access within any public bucket policy, including non-public delegation to specific accounts, is blocked. When set to `true`: + * Only the bucket owner and AWS Services can access buckets with public policies. + +### Region Configuration + +The `region` block supports the following: + +* `bucket` - (Required) The name of the associated bucket for the Region. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `alias` - The alias for the Multi-Region Access Point. +* `arn` - Amazon Resource Name (ARN) of the Multi-Region Access Point. +* `alias` - The alias for the Multi-Region Access Point. +* `domain_name` - The DNS domain name of the S3 Multi-Region Access Point in the format _`alias`_.accesspoint.s3-global.amazonaws.com. For more information, see the documentation on [Multi-Region Access Point Requests](https://docs.aws.amazon.com/AmazonS3/latest/userguide/MultiRegionAccessPointRequests.html). +* `id` - The AWS account ID and access point name separated by a colon (`:`). +* `status` - The current status of the Multi-Region Access Point. One of: `READY`, `INCONSISTENT_ACROSS_REGIONS`, `CREATING`, `PARTIALLY_CREATED`, `PARTIALLY_DELETED`, `DELETING`. + +## Import + +Multi-Region Access Points can be imported using the `account_id` and `name` of the Multi-Region Access Point separated by a colon (`:`), e.g. + +``` +$ terraform import aws_s3control_multi_region_access_point.example 123456789012:example +``` diff --git a/website/docs/r/s3control_multi_region_access_point_policy.html.markdown b/website/docs/r/s3control_multi_region_access_point_policy.html.markdown new file mode 100644 index 00000000000..eb26b78148a --- /dev/null +++ b/website/docs/r/s3control_multi_region_access_point_policy.html.markdown @@ -0,0 +1,93 @@ +--- +subcategory: "S3 Control" +layout: "aws" +page_title: "AWS: aws_s3control_multi_region_access_point_policy" +description: |- + Provides a resource to manage an S3 Multi-Region Access Point access control policy. +--- + +# Resource: aws_s3control_multi_region_access_point_policy + +Provides a resource to manage an S3 Multi-Region Access Point access control policy. + +## Example Usage + +### Basic Example + +```terraform +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} + +resource "aws_s3_bucket" "foo_bucket" { + bucket = "example-bucket-foo" +} + +resource "aws_s3control_multi_region_access_point" "example" { + details { + name = "example" + + region { + bucket = aws_s3_bucket.foo_bucket.id + } + } +} + +resource "aws_s3control_multi_region_access_point_policy" "example" { + details { + name = element(split(":", aws_s3control_multi_region_access_point.example.id), 1) + policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Sid" : "Example", + "Effect" : "Allow", + "Principal" : { + "AWS" : data.aws_caller_identity.current.account_id + }, + "Action" : ["s3:GetObject", "s3:PutObject"], + "Resource" : "arn:${data.aws_partition.current.partition}:s3::${data.aws_caller_identity.current.account_id}:accesspoint/${aws_s3control_multi_region_access_point.example.alias}/object/*" + } + ] + }) + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `account_id` - (Optional) The AWS account ID for the owner of the Multi-Region Access Point. Defaults to automatically determined account ID of the Terraform AWS provider. +* `details` - (Required) A configuration block containing details about the policy for the Multi-Region Access Point. See [Details Configuration Block](#details-configuration) below for more details + +### Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/blocks/resources/syntax.html#operation-timeouts) for certain actions: + +* `create` - (Default `15 minutes`) Used when creating the Multi-Region Access Point Policy. +* `update` - (Default `15 minutes`) Used when updating the Multi-Region Access Point Policy. + +### Details Configuration + +The `details` block supports the following: + +* `name` - (Required) The name of the Multi-Region Access Point. +* `policy` - (Required) A valid JSON document that specifies the policy that you want to associate with this Multi-Region Access Point. Once applied, the policy can be edited, but not deleted. For more information, see the documentation on [Multi-Region Access Point Permissions](https://docs.aws.amazon.com/AmazonS3/latest/userguide/MultiRegionAccessPointPermissions.html). + +-> **NOTE:** When you update the `policy`, the update is first listed as the proposed policy. After the update is finished and all Regions have been updated, the proposed policy is listed as the established policy. If both policies have the same version number, the proposed policy is the established policy. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `established` - The last established policy for the Multi-Region Access Point. +* `id` - The AWS account ID and access point name separated by a colon (`:`). +* `proposed` - The proposed policy for the Multi-Region Access Point. + +## Import + +Multi-Region Access Point Policies can be imported using the `account_id` and `name` of the Multi-Region Access Point separated by a colon (`:`), e.g. + +``` +$ terraform import aws_s3control_multi_region_access_point_policy.example 123456789012:example +```