diff --git a/.changes/next-release/enhancement-s3-41977.json b/.changes/next-release/enhancement-s3-41977.json new file mode 100644 index 000000000000..646099011468 --- /dev/null +++ b/.changes/next-release/enhancement-s3-41977.json @@ -0,0 +1,5 @@ +{ + "type": "enhancement", + "category": "``S3``", + "description": "Add support for creating S3 account regional namespace buckets with ``aws s3 mb``. The command now automatically detects bucket names matching the account-regional naming pattern and sets the required ``x-amz-bucket-namespace`` header." +} diff --git a/awscli/customizations/s3/subcommands.py b/awscli/customizations/s3/subcommands.py index 1a4a90855c2c..388aca66d1f7 100644 --- a/awscli/customizations/s3/subcommands.py +++ b/awscli/customizations/s3/subcommands.py @@ -31,7 +31,7 @@ from awscli.customizations.s3.utils import find_bucket_key, AppendFilter, \ find_dest_path_comp_key, human_readable_size, \ RequestParamsMapper, split_s3_bucket_key, block_unsupported_resources, \ - S3PathResolver + S3PathResolver, is_account_regional_namespace_bucket from awscli.customizations.utils import uni_print from awscli.customizations.s3.syncstrategy.base import MissingFileSync, \ SizeAndLastModifiedSync, NeverSync, AlwaysSync @@ -910,6 +910,9 @@ def _run_main(self, parsed_args, parsed_globals): bucket_config = {} bucket_tags = self._create_bucket_tags(parsed_args) + if is_account_regional_namespace_bucket(bucket): + params['BucketNamespace'] = 'account-regional' + # Only set LocationConstraint when the region name is not us-east-1. # Sending LocationConstraint with value us-east-1 results in an error. if self.client.meta.region_name != 'us-east-1': diff --git a/awscli/customizations/s3/utils.py b/awscli/customizations/s3/utils.py index a2809755e2b1..9051ef69bc09 100644 --- a/awscli/customizations/s3/utils.py +++ b/awscli/customizations/s3/utils.py @@ -64,6 +64,10 @@ ) +def is_account_regional_namespace_bucket(bucket): + return bucket.endswith('-an') + + def human_readable_size(value): """Convert a size in bytes into a human readable format. diff --git a/tests/functional/s3/test_mb_command.py b/tests/functional/s3/test_mb_command.py index 7ea29c8d2b22..5f5675528b97 100644 --- a/tests/functional/s3/test_mb_command.py +++ b/tests/functional/s3/test_mb_command.py @@ -90,6 +90,40 @@ def test_make_bucket_with_multiple_tags(self): } self.assert_params_for_cmd(command, expected_params) + def test_account_regional_namespace_bucket(self): + bucket = 'amzn-s3-demo-bucket-111122223333-us-west-2-an' + command = self.prefix + f's3://{bucket} --region us-west-2' + self.parsed_responses = [{'Location': 'us-west-2'}] + expected_params = { + 'Bucket': bucket, + 'BucketNamespace': 'account-regional', + 'CreateBucketConfiguration': {'LocationConstraint': 'us-west-2'}, + } + self.assert_params_for_cmd(command, expected_params) + + def test_account_regional_namespace_bucket_us_east_1(self): + bucket = 'my-bucket-111122223333-us-east-1-an' + command = self.prefix + f's3://{bucket} --region us-east-1' + expected_params = { + 'Bucket': bucket, + 'BucketNamespace': 'account-regional', + } + self.assert_params_for_cmd(command, expected_params) + + def test_account_regional_namespace_short_bucket_name(self): + bucket = 'xyz-an' + command = self.prefix + f's3://{bucket} --region us-east-1' + expected_params = { + 'Bucket': bucket, + 'BucketNamespace': 'account-regional', + } + self.assert_params_for_cmd(command, expected_params) + + def test_regular_bucket_no_namespace(self): + command = self.prefix + 's3://my-regular-bucket --region us-east-1' + expected_params = {'Bucket': 'my-regular-bucket'} + self.assert_params_for_cmd(command, expected_params) + def test_tags_with_three_arguments_fails(self): command = self.prefix + 's3://bucket --tags Key1 Value1 ExtraArg' self.assert_params_for_cmd( diff --git a/tests/integration/customizations/s3/test_plugin.py b/tests/integration/customizations/s3/test_plugin.py index 4aab3dd74a04..95a71bc3a33d 100644 --- a/tests/integration/customizations/s3/test_plugin.py +++ b/tests/integration/customizations/s3/test_plugin.py @@ -190,6 +190,13 @@ def setUp(self): super(BaseS3IntegrationTest, self).setUp() +class TestMakeBucketAccountRegionalNamespace(BaseS3IntegrationTest): + def test_short_an_suffix_sends_namespace_header(self): + p = aws('s3 mb s3://xyz-an') + assert p.rc != 0 + assert 'InvalidBucketNamespace' in p.stderr + + class TestMoveCommand(BaseS3IntegrationTest): def assert_mv_local_to_s3(self, bucket_name): full_path = self.files.create_file('foo.txt', 'this is foo.txt') diff --git a/tests/unit/customizations/s3/test_utils.py b/tests/unit/customizations/s3/test_utils.py index 96d4e216c44c..7a29b8f01b9d 100644 --- a/tests/unit/customizations/s3/test_utils.py +++ b/tests/unit/customizations/s3/test_utils.py @@ -40,7 +40,8 @@ ProvideLastModifiedTimeSubscriber, DirectoryCreatorSubscriber, DeleteSourceObjectSubscriber, DeleteSourceFileSubscriber, DeleteCopySourceObjectSubscriber, NonSeekableStream, CreateDirectoryError, - S3PathResolver, CaseConflictCleanupSubscriber) + S3PathResolver, CaseConflictCleanupSubscriber, + is_account_regional_namespace_bucket) from awscli.customizations.s3.results import WarningResult from tests.unit.customizations.s3 import FakeTransferFuture from tests.unit.customizations.s3 import FakeTransferFutureMeta @@ -360,6 +361,44 @@ def test_outpost_bucket_arn_with_slash_raises_exception(self): ) +class TestIsAccountRegionalNamespaceBucket(unittest.TestCase): + def test_matches_standard_pattern(self): + self.assertTrue( + is_account_regional_namespace_bucket( + 'amzn-s3-demo-bucket-111122223333-us-west-2-an' + ) + ) + + def test_matches_different_region(self): + self.assertTrue( + is_account_regional_namespace_bucket( + 'my-bucket-123456789012-eu-central-1-an' + ) + ) + + def test_no_match_regular_bucket(self): + self.assertFalse( + is_account_regional_namespace_bucket('my-regular-bucket') + ) + + def test_no_match_missing_an_suffix(self): + self.assertFalse( + is_account_regional_namespace_bucket( + 'bucket-111122223333-us-west-2' + ) + ) + + def test_matches_short_bucket_name(self): + self.assertTrue( + is_account_regional_namespace_bucket('xyz-an') + ) + + def test_no_match_express_directory_bucket(self): + self.assertFalse( + is_account_regional_namespace_bucket('bucket--usw2-az1--x-s3') + ) + + class TestCreateWarning(unittest.TestCase): def test_create_warning(self): path = '/foo/'