diff --git a/.changelog/19859.txt b/.changelog/19859.txt new file mode 100644 index 00000000000..92976607dfc --- /dev/null +++ b/.changelog/19859.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_datasync_location_s3: Correctly parse S3 on Outposts location URI +``` \ No newline at end of file diff --git a/aws/datasync.go b/aws/datasync.go index 117ede5c4ef..ec7e10887b4 100644 --- a/aws/datasync.go +++ b/aws/datasync.go @@ -1,23 +1,11 @@ package aws import ( - "net/url" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/datasync" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func dataSyncParseLocationURI(uri string) (string, error) { - parsedURL, err := url.ParseRequestURI(uri) - - if err != nil { - return "", err - } - - return parsedURL.Path, nil -} - func expandDataSyncEc2Config(l []interface{}) *datasync.Ec2Config { if len(l) == 0 || l[0] == nil { return nil diff --git a/aws/datasync_test.go b/aws/datasync_test.go deleted file mode 100644 index f56cf7484fc..00000000000 --- a/aws/datasync_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package aws - -import ( - "testing" -) - -func TestDataSyncParseLocationURI(t *testing.T) { - testCases := []struct { - LocationURI string - Subdirectory string - }{ - { - LocationURI: "efs://us-east-2.fs-abcd1234/", // lintignore:AWSAT003 - Subdirectory: "/", - }, - { - LocationURI: "efs://us-east-2.fs-abcd1234/path", // lintignore:AWSAT003 - Subdirectory: "/path", - }, - { - LocationURI: "nfs://example.com/", - Subdirectory: "/", - }, - { - LocationURI: "nfs://example.com/path", - Subdirectory: "/path", - }, - { - LocationURI: "s3://myBucket/", - Subdirectory: "/", - }, - { - LocationURI: "s3://myBucket/path", - Subdirectory: "/path", - }, - } - - for i, tc := range testCases { - subdirectory, err := dataSyncParseLocationURI(tc.LocationURI) - if err != nil { - t.Fatalf("%d: received error parsing (%s): %s", i, tc.LocationURI, err) - } - if subdirectory != tc.Subdirectory { - t.Fatalf("%d: expected subdirectory (%s), received: %s", i, tc.Subdirectory, subdirectory) - } - } -} diff --git a/aws/internal/service/datasync/uri.go b/aws/internal/service/datasync/uri.go new file mode 100644 index 00000000000..bac9e606a8e --- /dev/null +++ b/aws/internal/service/datasync/uri.go @@ -0,0 +1,45 @@ +package datasync + +import ( + "fmt" + "regexp" + + "github.com/aws/aws-sdk-go/aws/arn" +) + +var ( + locationURIPattern = regexp.MustCompile(`^(efs|nfs|s3|smb|fsxw)://(.+)$`) + locationURIGlobalIDAndSubdirPattern = regexp.MustCompile(`^([a-zA-Z0-9.\-]+)(/.*)$`) + s3OutpostsAccessPointARNResourcePattern = regexp.MustCompile(`^outpost/.*/accesspoint/.*?(/.*)$`) +) + +// SubdirectoryFromLocationURI extracts the subdirectory from a location URI. +// https://docs.aws.amazon.com/datasync/latest/userguide/API_LocationListEntry.html#DataSync-Type-LocationListEntry-LocationUri +func SubdirectoryFromLocationURI(uri string) (string, error) { + submatches := locationURIPattern.FindStringSubmatch(uri) + + if len(submatches) != 3 { + return "", fmt.Errorf("location URI (%s) does not match pattern %q", uri, locationURIPattern) + } + + globalIDAndSubdir := submatches[2] + parsedARN, err := arn.Parse(globalIDAndSubdir) + + if err == nil { + submatches = s3OutpostsAccessPointARNResourcePattern.FindStringSubmatch(parsedARN.Resource) + + if len(submatches) != 2 { + return "", fmt.Errorf("location URI S3 on Outposts access point ARN resource (%s) does not match pattern %q", parsedARN.Resource, s3OutpostsAccessPointARNResourcePattern) + } + + return submatches[1], nil + } + + submatches = locationURIGlobalIDAndSubdirPattern.FindStringSubmatch(globalIDAndSubdir) + + if len(submatches) != 3 { + return "", fmt.Errorf("location URI global ID and subdirectory (%s) does not match pattern %q", globalIDAndSubdir, locationURIGlobalIDAndSubdirPattern) + } + + return submatches[2], nil +} diff --git a/aws/internal/service/datasync/uri_test.go b/aws/internal/service/datasync/uri_test.go new file mode 100644 index 00000000000..6c90cf99a04 --- /dev/null +++ b/aws/internal/service/datasync/uri_test.go @@ -0,0 +1,145 @@ +package datasync_test + +import ( + "testing" + + tfdatasync "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/datasync" +) + +func TestSubdirectoryFromLocationURI(t *testing.T) { + testCases := []struct { + TestName string + InputURI string + ExpectedError bool + ExpectedSubdirectory string + }{ + { + TestName: "empty URI", + InputURI: "", + ExpectedError: true, + }, + { + TestName: "invalid URI scheme", + InputURI: "test://testing/", + ExpectedError: true, + }, + { + TestName: "S3 bucket URI no bucket name (1)", + InputURI: "s3://", + ExpectedError: true, + }, + { + TestName: "S3 bucket URI no bucket name (2)", + InputURI: "s3:///", + ExpectedError: true, + }, + { + TestName: "S3 bucket URI top level", + InputURI: "s3://bucket/", + ExpectedSubdirectory: "/", + }, + { + TestName: "S3 bucket URI one level", + InputURI: "s3://bucket/my-folder-1/", + ExpectedSubdirectory: "/my-folder-1/", + }, + { + TestName: "S3 bucket URI two levels", + InputURI: "s3://bucket/my-folder-1/my-folder-2", + ExpectedSubdirectory: "/my-folder-1/my-folder-2", + }, + { + TestName: "S3 Outposts ARN URI top level", + InputURI: "s3://arn:aws:s3-outposts:eu-west-3:123456789012:outpost/op-YYYYYYYYYY/accesspoint/my-access-point/", + ExpectedSubdirectory: "/", + }, + { + TestName: "S3 Outposts ARN URI one level", + InputURI: "s3://arn:aws:s3-outposts:eu-west-3:123456789012:outpost/op-YYYYYYYYYY/accesspoint/my-access-point/my-folder-1/", + ExpectedSubdirectory: "/my-folder-1/", + }, + { + TestName: "S3 Outposts ARN URI two levels", + InputURI: "s3://arn:aws:s3-outposts:eu-west-3:123456789012:outpost/op-YYYYYYYYYY/accesspoint/my-access-point/my-folder-1/my-folder-2", + ExpectedSubdirectory: "/my-folder-1/my-folder-2", + }, + { + TestName: "EFS URI top level", + InputURI: "efs://us-west-2.fs-abcdef01/", + ExpectedSubdirectory: "/", + }, + { + TestName: "EFS URI one level", + InputURI: "efs://us-west-2.fs-abcdef01/my-folder-1/", + ExpectedSubdirectory: "/my-folder-1/", + }, + { + TestName: "EFS URI two levels", + InputURI: "efs://us-west-2.fs-abcdef01/my-folder-1/my-folder-2", + ExpectedSubdirectory: "/my-folder-1/my-folder-2", + }, + { + TestName: "NFS URI top level", + InputURI: "nfs://example.com/", + ExpectedSubdirectory: "/", + }, + { + TestName: "NFS URI one level", + InputURI: "nfs://example.com/my-folder-1/", + ExpectedSubdirectory: "/my-folder-1/", + }, + { + TestName: "NFS URI two levels", + InputURI: "nfs://example.com/my-folder-1/my-folder-2", + ExpectedSubdirectory: "/my-folder-1/my-folder-2", + }, + { + TestName: "SMB URI top level", + InputURI: "smb://192.168.1.1/", + ExpectedSubdirectory: "/", + }, + { + TestName: "SMB URI one level", + InputURI: "smb://192.168.1.1/my-folder-1/", + ExpectedSubdirectory: "/my-folder-1/", + }, + { + TestName: "SMB URI two levels", + InputURI: "smb://192.168.1.1/my-folder-1/my-folder-2", + ExpectedSubdirectory: "/my-folder-1/my-folder-2", + }, + { + TestName: "FSx Windows URI top level", + InputURI: "fsxw://us-west-2.fs-abcdef012345678901/", + ExpectedSubdirectory: "/", + }, + { + TestName: "FSx Windows URI one level", + InputURI: "fsxw://us-west-2.fs-abcdef012345678901/my-folder-1/", + ExpectedSubdirectory: "/my-folder-1/", + }, + { + TestName: "FSx Windows URI two levels", + InputURI: "fsxw://us-west-2.fs-abcdef012345678901/my-folder-1/my-folder-2", + ExpectedSubdirectory: "/my-folder-1/my-folder-2", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + got, err := tfdatasync.SubdirectoryFromLocationURI(testCase.InputURI) + + if err == nil && testCase.ExpectedError { + t.Fatalf("expected error") + } + + if err != nil && !testCase.ExpectedError { + t.Fatalf("unexpected error: %s", err) + } + + if got != testCase.ExpectedSubdirectory { + t.Errorf("got %s, expected %s", got, testCase.ExpectedSubdirectory) + } + }) + } +} diff --git a/aws/resource_aws_datasync_location_efs.go b/aws/resource_aws_datasync_location_efs.go index a8dc8f9782f..45bba360789 100644 --- a/aws/resource_aws_datasync_location_efs.go +++ b/aws/resource_aws_datasync_location_efs.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfdatasync "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/datasync" ) func resourceAwsDataSyncLocationEfs() *schema.Resource { @@ -128,10 +129,10 @@ func resourceAwsDataSyncLocationEfsRead(d *schema.ResourceData, meta interface{} return fmt.Errorf("error reading DataSync Location EFS (%s): %s", d.Id(), err) } - subdirectory, err := dataSyncParseLocationURI(aws.StringValue(output.LocationUri)) + subdirectory, err := tfdatasync.SubdirectoryFromLocationURI(aws.StringValue(output.LocationUri)) if err != nil { - return fmt.Errorf("error parsing Location EFS (%s) URI (%s): %s", d.Id(), aws.StringValue(output.LocationUri), err) + return err } d.Set("arn", output.LocationArn) diff --git a/aws/resource_aws_datasync_location_fsx_windows_file_system.go b/aws/resource_aws_datasync_location_fsx_windows_file_system.go index b2e1651d793..95b5ed9697c 100644 --- a/aws/resource_aws_datasync_location_fsx_windows_file_system.go +++ b/aws/resource_aws_datasync_location_fsx_windows_file_system.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfdatasync "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/datasync" ) func resourceAwsDataSyncLocationFsxWindowsFileSystem() *schema.Resource { @@ -155,10 +156,10 @@ func resourceAwsDataSyncLocationFsxWindowsFileSystemRead(d *schema.ResourceData, return fmt.Errorf("error reading DataSync Location Fsx Windows (%s): %w", d.Id(), err) } - subdirectory, err := dataSyncParseLocationURI(aws.StringValue(output.LocationUri)) + subdirectory, err := tfdatasync.SubdirectoryFromLocationURI(aws.StringValue(output.LocationUri)) if err != nil { - return fmt.Errorf("error parsing Location Fsx Windows File System (%s) URI (%s): %w", d.Id(), aws.StringValue(output.LocationUri), err) + return err } d.Set("arn", output.LocationArn) diff --git a/aws/resource_aws_datasync_location_nfs.go b/aws/resource_aws_datasync_location_nfs.go index 33a48542a06..fd00dceccb8 100644 --- a/aws/resource_aws_datasync_location_nfs.go +++ b/aws/resource_aws_datasync_location_nfs.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfdatasync "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/datasync" ) func resourceAwsDataSyncLocationNfs() *schema.Resource { @@ -144,10 +145,10 @@ func resourceAwsDataSyncLocationNfsRead(d *schema.ResourceData, meta interface{} return fmt.Errorf("error reading DataSync Location NFS (%s): %w", d.Id(), err) } - subdirectory, err := dataSyncParseLocationURI(aws.StringValue(output.LocationUri)) + subdirectory, err := tfdatasync.SubdirectoryFromLocationURI(aws.StringValue(output.LocationUri)) if err != nil { - return fmt.Errorf("error parsing Location NFS (%s) URI (%s): %w", d.Id(), aws.StringValue(output.LocationUri), err) + return err } d.Set("arn", output.LocationArn) diff --git a/aws/resource_aws_datasync_location_s3.go b/aws/resource_aws_datasync_location_s3.go index 8fa5bf11f88..27bb510e251 100644 --- a/aws/resource_aws_datasync_location_s3.go +++ b/aws/resource_aws_datasync_location_s3.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfdatasync "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/datasync" iamwaiter "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter" ) @@ -172,10 +173,10 @@ func resourceAwsDataSyncLocationS3Read(d *schema.ResourceData, meta interface{}) return fmt.Errorf("error reading DataSync Location S3 (%s): %s", d.Id(), err) } - subdirectory, err := dataSyncParseLocationURI(aws.StringValue(output.LocationUri)) + subdirectory, err := tfdatasync.SubdirectoryFromLocationURI(aws.StringValue(output.LocationUri)) if err != nil { - return fmt.Errorf("error parsing Location S3 (%s) URI (%s): %s", d.Id(), aws.StringValue(output.LocationUri), err) + return err } d.Set("agent_arns", flattenStringSet(output.AgentArns)) diff --git a/aws/resource_aws_datasync_location_smb.go b/aws/resource_aws_datasync_location_smb.go index ed0a76cb264..211520eb7d4 100644 --- a/aws/resource_aws_datasync_location_smb.go +++ b/aws/resource_aws_datasync_location_smb.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfdatasync "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/datasync" ) func resourceAwsDataSyncLocationSmb() *schema.Resource { @@ -164,10 +165,10 @@ func resourceAwsDataSyncLocationSmbRead(d *schema.ResourceData, meta interface{} return fmt.Errorf("error reading DataSync Location SMB (%s) tags: %w", d.Id(), err) } - subdirectory, err := dataSyncParseLocationURI(aws.StringValue(output.LocationUri)) + subdirectory, err := tfdatasync.SubdirectoryFromLocationURI(aws.StringValue(output.LocationUri)) if err != nil { - return fmt.Errorf("error parsing Location SMB (%s) URI (%s): %w", d.Id(), aws.StringValue(output.LocationUri), err) + return err } d.Set("agent_arns", flattenStringSet(output.AgentArns))