From e8587fa2977a9452cf3d3b0cb7a0f114c55bc99f Mon Sep 17 00:00:00 2001 From: Jonny Stirling Date: Mon, 12 Apr 2021 12:32:21 +0100 Subject: [PATCH] Added support, and tests for, settings tags against the gamelift build resource when using upload-build command --- awscli/customizations/gamelift/uploadbuild.py | 54 ++++- .../functional/gamelift/test_upload_build.py | 220 ++++++++++++++++++ .../gamelift/test_uploadbuild.py | 118 ++++++++++ 3 files changed, 391 insertions(+), 1 deletion(-) diff --git a/awscli/customizations/gamelift/uploadbuild.py b/awscli/customizations/gamelift/uploadbuild.py index 931c8c2987f5..cef7aa34f90b 100644 --- a/awscli/customizations/gamelift/uploadbuild.py +++ b/awscli/customizations/gamelift/uploadbuild.py @@ -26,6 +26,24 @@ class UploadBuildCommand(BasicCommand): NAME = 'upload-build' DESCRIPTION = 'Upload a new build to AWS GameLift.' + TAGS_SCHEMA = { + "type": "array", + "items": { + "type": "object", + "properties": { + "Key": { + "description": "The tag key.", + "type": "string", + "required": True + }, + "Value": { + "description": "The tag value.", + "type": "string", + "required": True + } + } + } + } ARG_TABLE = [ {'name': 'name', 'required': True, 'help_text': 'The name of the build'}, @@ -35,7 +53,18 @@ class UploadBuildCommand(BasicCommand): 'help_text': 'The path to the directory containing the build to upload'}, {'name': 'operating-system', 'required': False, - 'help_text': 'The operating system the build runs on'} + 'help_text': 'The operating system the build runs on'}, + { + 'name': 'tags', + 'synopsis': '--tags ', + 'required': False, + 'nargs': '+', + 'schema': TAGS_SCHEMA, + 'help_text': ( + 'Optional. The list of key/value pairs to tag' + 'instance.' + ) + } ] def _run_main(self, args, parsed_globals): @@ -60,6 +89,18 @@ def _run_main(self, args, parsed_globals): } if args.operating_system: create_build_kwargs['OperatingSystem'] = args.operating_system + if args.tags: + # Validate tags if available + if not validate_tags(args.tags): + sys.stderr.write( + 'A maximum of 50 tags may be provided each containing a ' + '"Key" property value between 1 and 128 UTF-8 Unicode ' + 'characters and a "Value" property value between 0 and ' + '256 UTF-8 Unicode characters' + ) + + return 255 + create_build_kwargs['Tags'] = args.tags response = gamelift_client.create_build(**create_build_kwargs) build_id = response['Build']['BuildId'] @@ -132,6 +173,17 @@ def validate_directory(source_root): return True return False +def validate_tags(tags): + if tags: + if len(tags) > 50: + return False + for tag in tags: + if len(tag['Key']) > 128: + return False + if len(tag['Value']) > 256: + return False + return True + # TODO: Remove this class once available to CLI from s3transfer # docstring. diff --git a/tests/functional/gamelift/test_upload_build.py b/tests/functional/gamelift/test_upload_build.py index 7a52d0de271b..53d2d2d15ce1 100644 --- a/tests/functional/gamelift/test_upload_build.py +++ b/tests/functional/gamelift/test_upload_build.py @@ -122,6 +122,226 @@ def test_upload_build_with_operating_system_param(self): stdout) self.assertIn('Build ID: myid', stdout) + def test_upload_build_with_tags_param(self): + self.files.create_file('tmpfile', 'Some contents') + cmdline = self.prefix + cmdline += ' --name mybuild --build-version myversion' + cmdline += ' --build-root %s' % self.files.rootdir + cmdline += ' --operating-system WINDOWS_2012' + cmdline += ' --tags Key=k1,Value=v1 Key=k2,Value=v2' + + self.parsed_responses = [ + {'Build': {'BuildId': 'myid'}}, + {'StorageLocation': { + 'Bucket': 'mybucket', + 'Key': 'mykey'}, + 'UploadCredentials': { + 'AccessKeyId': 'myaccesskey', + 'SecretAccessKey': 'mysecretkey', + 'SessionToken': 'mytoken'}}, + {} + ] + + stdout, stderr, rc = self.run_cmd(cmdline, expected_rc=0) + + # First the build is created. + self.assertEqual(len(self.operations_called), 3) + self.assertEqual(self.operations_called[0][0].name, 'CreateBuild') + self.assertEqual( + self.operations_called[0][1], + {'Name': 'mybuild', 'Version': 'myversion', + 'OperatingSystem': 'WINDOWS_2012', + 'Tags': [{'Key': 'k1', 'Value': 'v1'}, {'Key': 'k2', 'Value': 'v2'}]} + ) + + # Second the credentials are requested. + self.assertEqual( + self.operations_called[1][0].name, 'RequestUploadCredentials') + self.assertEqual( + self.operations_called[1][1], {'BuildId': 'myid'}) + + # The build is then uploaded to S3. + self.assertEqual(self.operations_called[2][0].name, 'PutObject') + self.assertEqual( + self.operations_called[2][1], + {'Body': mock.ANY, 'Bucket': 'mybucket', 'Key': 'mykey'} + ) + + # Check the output of the command. + self.assertIn( + 'Successfully uploaded %s to AWS GameLift' % self.files.rootdir, + stdout) + self.assertIn('Build ID: myid', stdout) + + def test_upload_build_with_tags_missing_key(self): + self.files.create_file('tmpfile', 'Some contents') + cmdline = self.prefix + cmdline += ' --name mybuild --build-version myversion' + cmdline += ' --build-root %s' % self.files.rootdir + cmdline += ' --operating-system WINDOWS_2012' + cmdline += ' --tags Miss=k1,Value=v1 Key=k2,Value=v2' + + self.parsed_responses = [ + {'Build': {'BuildId': 'myid'}}, + {'StorageLocation': { + 'Bucket': 'mybucket', + 'Key': 'mykey'}, + 'UploadCredentials': { + 'AccessKeyId': 'myaccesskey', + 'SecretAccessKey': 'mysecretkey', + 'SessionToken': 'mytoken'}}, + {} + ] + + stdout, stderr, rc = self.run_cmd(cmdline, expected_rc=252) + + self.assertIn( + 'Missing required parameter in [0]: "Key"\n' + 'Unknown parameter in [0]: "Miss", must be one of: Key, Value', + stderr) + + def test_upload_build_with_tags_missing_value(self): + self.files.create_file('tmpfile', 'Some contents') + cmdline = self.prefix + cmdline += ' --name mybuild --build-version myversion' + cmdline += ' --build-root %s' % self.files.rootdir + cmdline += ' --operating-system WINDOWS_2012' + cmdline += ' --tags Key=k1,Miss=v1 Key=k2,Value=v2' + + self.parsed_responses = [ + {'Build': {'BuildId': 'myid'}}, + {'StorageLocation': { + 'Bucket': 'mybucket', + 'Key': 'mykey'}, + 'UploadCredentials': { + 'AccessKeyId': 'myaccesskey', + 'SecretAccessKey': 'mysecretkey', + 'SessionToken': 'mytoken'}}, + {} + ] + + stdout, stderr, rc = self.run_cmd(cmdline, expected_rc=252) + + self.assertIn( + 'Missing required parameter in [0]: "Value"\n' + 'Unknown parameter in [0]: "Miss", must be one of: Key, Value', + stderr) + + def test_upload_build_with_tags_empty_key(self): + self.files.create_file('tmpfile', 'Some contents') + cmdline = self.prefix + cmdline += ' --name mybuild --build-version myversion' + cmdline += ' --build-root %s' % self.files.rootdir + cmdline += ' --operating-system WINDOWS_2012' + cmdline += ' --tags Key=,Value=v1 Key=k2,Value=v2' + + self.parsed_responses = [ + {'Build': {'BuildId': 'myid'}}, + {'StorageLocation': { + 'Bucket': 'mybucket', + 'Key': 'mykey'}, + 'UploadCredentials': { + 'AccessKeyId': 'myaccesskey', + 'SecretAccessKey': 'mysecretkey', + 'SessionToken': 'mytoken'}}, + {} + ] + + stdout, stderr, rc = self.run_cmd(cmdline, expected_rc=252) + + self.assertIn( + 'Invalid length for parameter Tags[0].Key, value: 0, valid min ' + 'length: 1', stderr) + + def test_upload_build_with_tags_long_key(self): + self.files.create_file('tmpfile', 'Some contents') + cmdline = self.prefix + cmdline += ' --name mybuild --build-version myversion' + cmdline += ' --build-root %s' % self.files.rootdir + cmdline += ' --operating-system WINDOWS_2012' + cmdline += ' --tags Key=' + ('k' * 129) + ',Value=v1 Key=k2,Value=v2' + + self.parsed_responses = [ + {'Build': {'BuildId': 'myid'}}, + {'StorageLocation': { + 'Bucket': 'mybucket', + 'Key': 'mykey'}, + 'UploadCredentials': { + 'AccessKeyId': 'myaccesskey', + 'SecretAccessKey': 'mysecretkey', + 'SessionToken': 'mytoken'}}, + {} + ] + + stdout, stderr, rc = self.run_cmd(cmdline, expected_rc=255) + + self.assertIn( + 'A maximum of 50 tags may be provided each containing a "Key" ' + 'property value between 1 and 128 UTF-8 Unicode characters and a ' + '"Value" property value between 0 and 256 UTF-8 Unicode ' + 'characters', stderr) + + def test_upload_build_with_tags_long_value(self): + self.files.create_file('tmpfile', 'Some contents') + cmdline = self.prefix + cmdline += ' --name mybuild --build-version myversion' + cmdline += ' --build-root %s' % self.files.rootdir + cmdline += ' --operating-system WINDOWS_2012' + cmdline += ' --tags Key=k1,Value=' + ('v' * 257) + ' Key=k2,Value=v2' + + self.parsed_responses = [ + {'Build': {'BuildId': 'myid'}}, + {'StorageLocation': { + 'Bucket': 'mybucket', + 'Key': 'mykey'}, + 'UploadCredentials': { + 'AccessKeyId': 'myaccesskey', + 'SecretAccessKey': 'mysecretkey', + 'SessionToken': 'mytoken'}}, + {} + ] + + stdout, stderr, rc = self.run_cmd(cmdline, expected_rc=255) + + self.assertIn( + 'A maximum of 50 tags may be provided each containing a "Key" ' + 'property value between 1 and 128 UTF-8 Unicode characters and a ' + '"Value" property value between 0 and 256 UTF-8 Unicode ' + 'characters', stderr) + + def test_upload_build_with_too_many_tags(self): + self.files.create_file('tmpfile', 'Some contents') + + tags = [ + 'Key=k' + str(x) + ',Value=v' + str(x) for x in range(51) + ] + + cmdline = self.prefix + cmdline += ' --name mybuild --build-version myversion' + cmdline += ' --build-root %s' % self.files.rootdir + cmdline += ' --operating-system WINDOWS_2012' + cmdline += ' --tags %s' % ' '.join(tags) + + self.parsed_responses = [ + {'Build': {'BuildId': 'myid'}}, + {'StorageLocation': { + 'Bucket': 'mybucket', + 'Key': 'mykey'}, + 'UploadCredentials': { + 'AccessKeyId': 'myaccesskey', + 'SecretAccessKey': 'mysecretkey', + 'SessionToken': 'mytoken'}}, + {} + ] + + stdout, stderr, rc = self.run_cmd(cmdline, expected_rc=255) + + self.assertIn( + 'A maximum of 50 tags may be provided each containing a "Key" ' + 'property value between 1 and 128 UTF-8 Unicode characters and a ' + '"Value" property value between 0 and 256 UTF-8 Unicode ' + 'characters', stderr) + def test_upload_build_with_empty_directory(self): cmdline = self.prefix cmdline += ' --name mybuild --build-version myversion' diff --git a/tests/unit/customizations/gamelift/test_uploadbuild.py b/tests/unit/customizations/gamelift/test_uploadbuild.py index e9d368157c76..7772e4980069 100644 --- a/tests/unit/customizations/gamelift/test_uploadbuild.py +++ b/tests/unit/customizations/gamelift/test_uploadbuild.py @@ -11,12 +11,15 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. from argparse import Namespace +from collections import OrderedDict import contextlib +import json import os import zipfile from botocore.session import get_session from botocore.exceptions import ClientError +from botocore.exceptions import ParamValidationError from awscli.compat import six from awscli.testutils import unittest, mock, FileCreator @@ -141,6 +144,121 @@ def test_upload_build_when_operating_system_is_provided(self): Name=self.build_name, Version=self.build_version, OperatingSystem=operating_system) + def test_upload_build_when_tags_are_provided(self): + operating_system = 'WINDOWS_2012' + self.file_creator.create_file('tmpfile', 'Some contents') + self.args = [ + '--name', self.build_name, '--build-version', self.build_version, + '--build-root', self.build_root, + '--operating-system', operating_system, + '--tags', '[{"Key": "k1", "Value": "v1"}]' + ] + self.cmd(self.args, self.global_args) + + # Ensure the GameLift client was called correctly. + self.gamelift_client.create_build.assert_called_once_with( + Name=self.build_name, Version=self.build_version, + OperatingSystem=operating_system, + Tags=[OrderedDict([('Key', 'k1'), ('Value', 'v1')])]) + + def test_upload_build_throws_on_invalid_tag_count(self): + operating_system = 'WINDOWS_2012' + self.file_creator.create_file('tmpfile', 'Some contents') + self.args = [ + '--name', self.build_name, '--build-version', self.build_version, + '--build-root', self.build_root, + '--operating-system', operating_system, + '--tags', json.dumps([{ + 'Key': 'k' + str(x), 'Value': 'v' + str(x) + } for x in range(51)]) + ] + + with mock.patch('sys.stderr', six.StringIO()) as mock_stderr: + self.cmd(self.args, self.global_args) + self.assertEqual( + mock_stderr.getvalue(), + 'A maximum of 50 tags may be provided each containing a ' + '"Key" property value between 1 and 128 UTF-8 Unicode ' + 'characters and a "Value" property value between 0 and 256 ' + 'UTF-8 Unicode characters' + ) + + def test_upload_build_throws_on_missing_tag_key(self): + operating_system = 'WINDOWS_2012' + self.file_creator.create_file('tmpfile', 'Some contents') + self.args = [ + '--name', self.build_name, '--build-version', self.build_version, + '--build-root', self.build_root, + '--operating-system', operating_system, + '--tags', json.dumps([{'Miss': 'k1', 'Value': 'v1'}]) + ] + + with self.assertRaises(ParamValidationError) as err: + self.cmd(self.args, self.global_args) + + self.assertEquals(str(err.exception), + "Parameter validation failed:\n" + "Missing required parameter in [0]: \"Key\"\n" + "Unknown parameter in [0]: \"Miss\", must be one of: Key, Value") + + def test_upload_build_throws_on_missing_tag_value(self): + operating_system = 'WINDOWS_2012' + self.file_creator.create_file('tmpfile', 'Some contents') + self.args = [ + '--name', self.build_name, '--build-version', self.build_version, + '--build-root', self.build_root, + '--operating-system', operating_system, + '--tags', json.dumps([{'Key': 'k1', 'Miss': 'v1'}]) + ] + + with self.assertRaises(ParamValidationError) as err: + self.cmd(self.args, self.global_args) + + self.assertEquals(str(err.exception), + "Parameter validation failed:\n" + "Missing required parameter in [0]: \"Value\"\n" + "Unknown parameter in [0]: \"Miss\", must be one of: Key, Value") + + def test_upload_build_throws_on_long_tag_key_value(self): + operating_system = 'WINDOWS_2012' + self.file_creator.create_file('tmpfile', 'Some contents') + self.args = [ + '--name', self.build_name, '--build-version', self.build_version, + '--build-root', self.build_root, + '--operating-system', operating_system, + '--tags', json.dumps([{'Key': 'k' * 129, 'Value': 'v1'}]) + ] + + with mock.patch('sys.stderr', six.StringIO()) as mock_stderr: + self.cmd(self.args, self.global_args) + self.assertEqual( + mock_stderr.getvalue(), + 'A maximum of 50 tags may be provided each containing a ' + '"Key" property value between 1 and 128 UTF-8 Unicode ' + 'characters and a "Value" property value between 0 and 256 ' + 'UTF-8 Unicode characters' + ) + + def test_upload_build_throws_on_long_tag_value_value(self): + operating_system = 'WINDOWS_2012' + self.file_creator.create_file('tmpfile', 'Some contents') + self.args = [ + '--name', self.build_name, '--build-version', self.build_version, + '--build-root', self.build_root, + '--operating-system', operating_system, + '--tags', json.dumps([{'Key': 'k1', 'Value': 'v' * 257}]) + ] + + with mock.patch('sys.stderr', six.StringIO()) as mock_stderr: + self.cmd(self.args, self.global_args) + self.assertEqual( + mock_stderr.getvalue(), + 'A maximum of 50 tags may be provided each containing a ' + '"Key" property value between 1 and 128 UTF-8 Unicode ' + 'characters and a "Value" property value between 0 and 256 ' + 'UTF-8 Unicode characters' + ) + def test_error_message_when_directory_is_empty(self): with mock.patch('sys.stderr', six.StringIO()) as mock_stderr: self.cmd(self.args, self.global_args)