Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/next-release/enhancement-gamelift-12377.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "enhancement",
"category": "``gamelift``",
"description": "Add ``--tags`` parameter to ``upload-build`` command."
}
35 changes: 24 additions & 11 deletions awscli/customizations/gamelift/uploadbuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@
from awscli.utils import create_nested_client


def parse_tags(raw_tags_list):
"""Parse tags from Key=Value format to GameLift API format."""
tags_list = []
if raw_tags_list:
for tag in raw_tags_list:
if '=' in tag:
key, value = tag.split('=', 1)
else:
key, value = tag, ''
tags_list.append({'Key': key, 'Value': value})
return tags_list


class UploadBuildCommand(BasicCommand):
NAME = 'upload-build'
DESCRIPTION = 'Upload a new build to AWS GameLift.'
Expand All @@ -40,7 +53,9 @@ class UploadBuildCommand(BasicCommand):
'The version of the GameLift server SDK used to '
'create the game server'},
{'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', 'required': False, 'nargs': '+',
'help_text': 'Tags to assign to the build. Format: Key=Value'}
]

def _run_main(self, args, parsed_globals):
Expand All @@ -52,9 +67,8 @@ def _run_main(self, args, parsed_globals):
# Validate a build directory
if not validate_directory(args.build_root):
sys.stderr.write(
'Fail to upload %s. '
f'Fail to upload {args.build_root}. '
'The build root directory is empty or does not exist.\n'
% (args.build_root)
)

return 255
Expand All @@ -67,6 +81,8 @@ def _run_main(self, args, parsed_globals):
create_build_kwargs['OperatingSystem'] = args.operating_system
if args.server_sdk_version:
create_build_kwargs['ServerSdkVersion'] = args.server_sdk_version
if args.tags:
create_build_kwargs['Tags'] = parse_tags(args.tags)
response = gamelift_client.create_build(**create_build_kwargs)
build_id = response['Build']['BuildId']

Expand Down Expand Up @@ -94,7 +110,7 @@ def _run_main(self, args, parsed_globals):
s3_transfer_mgr = S3Transfer(s3_client)

try:
fd, temporary_zipfile = tempfile.mkstemp('%s.zip' % build_id)
fd, temporary_zipfile = tempfile.mkstemp(f'{build_id}.zip')
zip_directory(temporary_zipfile, args.build_root)
s3_transfer_mgr.upload_file(
temporary_zipfile, bucket, key,
Expand All @@ -108,8 +124,8 @@ def _run_main(self, args, parsed_globals):
os.remove(temporary_zipfile)

sys.stdout.write(
'Successfully uploaded %s to AWS GameLift\n'
'Build ID: %s\n' % (args.build_root, build_id))
f'Successfully uploaded {args.build_root} to AWS GameLift\n'
f'Build ID: {build_id}\n')

return 0

Expand Down Expand Up @@ -142,7 +158,7 @@ def validate_directory(source_root):

# TODO: Remove this class once available to CLI from s3transfer
# docstring.
class ProgressPercentage(object):
class ProgressPercentage:
def __init__(self, filename, label=None):
self._filename = filename
self._label = label
Expand All @@ -158,9 +174,6 @@ def __call__(self, bytes_amount):
if self._size > 0:
percentage = (self._seen_so_far / self._size) * 100
sys.stdout.write(
"\r%s %s / %s (%.2f%%)" % (
self._label, human_readable_size(self._seen_so_far),
human_readable_size(self._size), percentage
)
f"\r{self._label} {human_readable_size(self._seen_so_far)} / {human_readable_size(self._size)} ({percentage:.2f}%)"
)
sys.stdout.flush()
94 changes: 74 additions & 20 deletions tests/functional/gamelift/test_upload_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ class TestUploadBuild(BaseAWSCommandParamsTest):
prefix = 'gamelift upload-build'

def setUp(self):
super(TestUploadBuild, self).setUp()
super().setUp()
self.files = FileCreator()

def tearDown(self):
super(TestUploadBuild, self).tearDown()
super().tearDown()
self.files.remove_all()

def test_upload_build(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 += f' --build-root {self.files.rootdir}'

self.parsed_responses = [
{'Build': {'BuildId': 'myid'}},
Expand Down Expand Up @@ -69,15 +69,15 @@ def test_upload_build(self):

# Check the output of the command.
self.assertIn(
'Successfully uploaded %s to AWS GameLift' % self.files.rootdir,
f'Successfully uploaded {self.files.rootdir} to AWS GameLift',
stdout)
self.assertIn('Build ID: myid', stdout)

def test_upload_build_with_operating_system_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 += f' --build-root {self.files.rootdir}'
cmdline += ' --operating-system WINDOWS_2012'

self.parsed_responses = [
Expand Down Expand Up @@ -118,14 +118,14 @@ def test_upload_build_with_operating_system_param(self):

# Check the output of the command.
self.assertIn(
'Successfully uploaded %s to AWS GameLift' % self.files.rootdir,
f'Successfully uploaded {self.files.rootdir} to AWS GameLift',
stdout)
self.assertIn('Build ID: myid', stdout)

def test_upload_build_with_empty_directory(self):
cmdline = self.prefix
cmdline += ' --name mybuild --build-version myversion'
cmdline += ' --build-root %s' % self.files.rootdir
cmdline += f' --build-root {self.files.rootdir}'

self.parsed_responses = [
{'Build': {'BuildId': 'myid'}},
Expand All @@ -142,17 +142,16 @@ def test_upload_build_with_empty_directory(self):
stdout, stderr, rc = self.run_cmd(cmdline, expected_rc=255)

self.assertIn(
'Fail to upload %s. '
'The build root directory is empty or does not exist.\n'
% self.files.rootdir,
f'Fail to upload {self.files.rootdir}. '
'The build root directory is empty or does not exist.\n',
stderr)

def test_upload_build_with_nonexistent_directory(self):
dir_not_exist = os.path.join(self.files.rootdir, 'does_not_exist')

cmdline = self.prefix
cmdline += ' --name mybuild --build-version myversion'
cmdline += ' --build-root %s' % dir_not_exist
cmdline += f' --build-root {dir_not_exist}'

self.parsed_responses = [
{'Build': {'BuildId': 'myid'}},
Expand All @@ -169,15 +168,14 @@ def test_upload_build_with_nonexistent_directory(self):
stdout, stderr, rc = self.run_cmd(cmdline, expected_rc=255)

self.assertIn(
'Fail to upload %s. '
'The build root directory is empty or does not exist.\n'
% dir_not_exist,
f'Fail to upload {dir_not_exist}. '
'The build root directory is empty or does not exist.\n',
stderr)

def test_upload_build_with_nonprovided_directory(self):
cmdline = self.prefix
cmdline += ' --name mybuild --build-version myversion'
cmdline += ' --build-root %s' % '""'
cmdline += ' --build-root {}'.format('""')

self.parsed_responses = [
{'Build': {'BuildId': 'myid'}},
Expand All @@ -194,16 +192,15 @@ def test_upload_build_with_nonprovided_directory(self):
stdout, stderr, rc = self.run_cmd(cmdline, expected_rc=255)

self.assertIn(
'Fail to upload %s. '
'The build root directory is empty or does not exist.\n'
% '""',
'Fail to upload {}. '
'The build root directory is empty or does not exist.\n'.format('""'),
stderr)

def test_upload_build_with_server_sdk_version_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 += f' --build-root {self.files.rootdir}'
cmdline += ' --server-sdk-version 4.0.2'

self.parsed_responses = [
Expand Down Expand Up @@ -244,6 +241,63 @@ def test_upload_build_with_server_sdk_version_param(self):

# Check the output of the command.
self.assertIn(
'Successfully uploaded %s to AWS GameLift' % self.files.rootdir,
f'Successfully uploaded {self.files.rootdir} to AWS GameLift',
stdout)
self.assertIn('Build ID: myid', stdout)

def test_upload_build_with_tags_param(self):
self.files.create_file('tmpfile', 'Some contents')

expected_tags = [
{'Key': 'Key1', 'Value': 'Value1'},
{'Key': 'Key2', 'Value': 'Value2'}
]

cmdline = self.prefix
cmdline += ' --name mybuild --build-version myversion'
cmdline += f' --build-root {self.files.rootdir}'
cmdline += ' --tags'
for tag in expected_tags:
cmdline += ' {}={}'.format(tag['Key'], tag['Value'])

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',
'Tags': expected_tags}
)

# 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(
f'Successfully uploaded {self.files.rootdir} to AWS GameLift',
stdout)
self.assertIn('Build ID: myid', stdout)
56 changes: 50 additions & 6 deletions tests/unit/customizations/gamelift/test_uploadbuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from awscli.customizations.gamelift.uploadbuild import UploadBuildCommand
from awscli.customizations.gamelift.uploadbuild import zip_directory
from awscli.customizations.gamelift.uploadbuild import validate_directory
from awscli.customizations.gamelift.uploadbuild import parse_tags
from awscli.compat import StringIO


Expand Down Expand Up @@ -146,9 +147,8 @@ def test_error_message_when_directory_is_empty(self):
self.cmd(self.args, self.global_args)
self.assertEqual(
mock_stderr.getvalue(),
'Fail to upload %s. '
f'Fail to upload {self.build_root}. '
'The build root directory is empty or does not exist.\n'
% (self.build_root)
)

def test_error_message_when_directory_is_not_provided(self):
Expand All @@ -162,8 +162,8 @@ def test_error_message_when_directory_is_not_provided(self):
self.cmd(self.args, self.global_args)
self.assertEqual(
mock_stderr.getvalue(),
'Fail to upload %s. '
'The build root directory is empty or does not exist.\n' % ('')
'Fail to upload {}. '
'The build root directory is empty or does not exist.\n'.format('')
)

def test_error_message_when_directory_does_not_exist(self):
Expand All @@ -179,9 +179,8 @@ def test_error_message_when_directory_does_not_exist(self):
self.cmd(self.args, self.global_args)
self.assertEqual(
mock_stderr.getvalue(),
'Fail to upload %s. '
f'Fail to upload {dir_not_exist}. '
'The build root directory is empty or does not exist.\n'
% (dir_not_exist)
)

def test_temporary_file_does_exist_when_fails(self):
Expand Down Expand Up @@ -209,6 +208,51 @@ def test_upload_build_when_server_sdk_version_is_provided(self):
Name=self.build_name, Version=self.build_version,
ServerSdkVersion=server_sdk_version)

def test_upload_build_when_tags_are_provided(self):
self.file_creator.create_file('tmpfile', 'Some contents')
self.args = [
'--name', self.build_name, '--build-version', self.build_version,
'--build-root', self.build_root,
'--tags', 'Environment=Production', 'Team=GameDev'
]
self.cmd(self.args, self.global_args)

self.gamelift_client.create_build.assert_called_once_with(
Name=self.build_name, Version=self.build_version,
Tags=[
{'Key': 'Environment', 'Value': 'Production'},
{'Key': 'Team', 'Value': 'GameDev'}
])


class TestParseTags(unittest.TestCase):
def test_parse_tags_with_key_value_pairs(self):
result = parse_tags(['Key1=Value1', 'Key2=Value2'])
self.assertEqual(result, [
{'Key': 'Key1', 'Value': 'Value1'},
{'Key': 'Key2', 'Value': 'Value2'}
])

def test_parse_tags_with_empty_value(self):
result = parse_tags(['Key1='])
self.assertEqual(result, [{'Key': 'Key1', 'Value': ''}])

def test_parse_tags_without_equals(self):
result = parse_tags(['Key1'])
self.assertEqual(result, [{'Key': 'Key1', 'Value': ''}])

def test_parse_tags_with_equals_in_value(self):
result = parse_tags(['Key1=Value=WithEquals'])
self.assertEqual(result, [{'Key': 'Key1', 'Value': 'Value=WithEquals'}])

def test_parse_tags_with_none(self):
result = parse_tags(None)
self.assertEqual(result, [])

def test_parse_tags_with_empty_list(self):
result = parse_tags([])
self.assertEqual(result, [])


class TestZipDirectory(unittest.TestCase):
def setUp(self):
Expand Down