diff --git a/python.zip b/python.zip new file mode 100644 index 0000000..392ec99 Binary files /dev/null and b/python.zip differ diff --git a/python/main.py b/python/main.py new file mode 100644 index 0000000..be5ab9d --- /dev/null +++ b/python/main.py @@ -0,0 +1,7 @@ +import sys + +from switch_bundle_common import main + + +if __name__ == '__main__': + main(sys.argv) diff --git a/python/switchBundle.py b/python/switchBundle.py deleted file mode 100644 index 0a09029..0000000 --- a/python/switchBundle.py +++ /dev/null @@ -1,110 +0,0 @@ -import os -import sys -import subprocess -import hvac - -if len(sys.argv) < 3: - exit() -if '123!@#' != sys.argv[2]: - exit() - -ProjDir = sys.argv[1] -BuildDir = ProjDir + '\\build\\web-mobile\\' -SubgamePath = 's3://web-h5game-test/internal/' - -# Vault 地址和 Token -# VAULT_ADDR = 'https://keys.wehavefun.email' -VAULT_ADDR = 'https://keys.i-see--you.icu' -VAULT_TOKEN = 'hvs.CAESINwE2A36YcHcC_CWSzaBtUrGq-1OcUgKjDegSMUAuB5-Gh4KHGh2cy53ZmN5UzNMb0pvcUJ6QlJXdURmZnh1MlU' - -# 创建客户端 -client = hvac.Client(url=VAULT_ADDR, token=VAULT_TOKEN) - -if client.is_authenticated(): - secret = client.secrets.kv.v2.read_secret_version( - path='default', - mount_point='aws', - raise_on_deleted_version=True - ) - SwitchBundle = secret['data']['data']['switchBundle'] - if "1" != SwitchBundle: - print("script exit, switchBundle status is " + SwitchBundle) - exit() -else: - print("running script failed!") - exit() - -print("start running script...") - - -BundleConfig = { - '6001': 'whot', - '6002': 'whot', - '6003': 'whot', - '6005': 'BRWhot', - '2001': 'roulette', - '2002': 'wajeSpin', - '2003': 'bottleSpin', - '2004': 'blackjack', - '2006': 'betterjack', - '6007': 'BRWhot', - '2008': 'slots', - '2005': 'christmas', -} - -unlinkArray = [ - 'cocos2d-js.109f3.js', - 'cocos2d-js-min.13181.js', - 'physics.9d2de.js', - 'physics-min.ce5ee.js', - 'style-desktop.dfd76.css', - 'style-mobile.6e9cd.css', - 'splash.85cfd.png' -] - -def remove_folder(path): - print('remove path: ', path) - if os.path.exists(path): - if os.path.isfile(path) or os.path.islink(path): - os.remove(path) - else: - for filename in os.listdir(path): - remove_folder(os.path.join(path, filename)) - os.rmdir(path) - -# 上传src相共用文件 -SrcDir = BuildDir + '\\src\\' -SrcDst = SubgamePath + 'lib/src/' -rmResult = subprocess.run(['cmd', '/c', 'aws s3 rm ' + SrcDst + ' --recursive'], capture_output=True, text=True) -print('remove dir src result: ', rmResult) -cmd = 'call aws s3 cp '+ SrcDir +' '+SrcDst + ' --recursive --region ap-east-1' -result = subprocess.run(['cmd', '/c', cmd], capture_output=True, text=True) -print('upload dir src result: ', result) -remove_folder(SrcDir) - -for filename in os.listdir(BuildDir): - file_path = os.path.join(BuildDir, filename) - if filename in unlinkArray: - os.remove(file_path) - -for gameid in BundleConfig: - awsPath = SubgamePath + gameid + '/assets/' + BundleConfig[gameid] +'/' - - rmResult = subprocess.run(['cmd', '/c', 'aws s3 rm ' + awsPath + ' --recursive'], capture_output=True, text=True) - print('remove game gameid: ', gameid, ' result: ', rmResult) - - subgameDir = BuildDir + 'assets\\' + BundleConfig[gameid] +'\\' - cmd = 'call aws s3 cp '+ subgameDir +' '+awsPath + ' --recursive --region ap-east-1' - result = subprocess.run(['cmd', '/c', cmd], capture_output=True, text=True) - print(result) - -for gameid in BundleConfig: - awsPath = SubgamePath + gameid + '/assets/' + BundleConfig[gameid] +'/' - subgameDir = BuildDir + 'assets\\' + BundleConfig[gameid] - remove_folder(subgameDir) - -for gameid in BundleConfig: - awsPath = SubgamePath + gameid + '/' - cmd = 'call aws s3 cp '+ BuildDir +' '+awsPath + ' --recursive --region ap-east-1' - result = subprocess.run(['cmd', '/c', cmd], capture_output=True, text=True) - print('upload game gameid: ', gameid, ' result: ', result) \ No newline at end of file diff --git a/python/switchBundleR.py b/python/switchBundleR.py deleted file mode 100644 index c517053..0000000 --- a/python/switchBundleR.py +++ /dev/null @@ -1,109 +0,0 @@ -import os -import sys -import subprocess -import hvac - -if len(sys.argv) < 3: - exit() -if '123!@#' != sys.argv[2]: - exit() - -ProjDir = sys.argv[1] -BuildDir = ProjDir + '\\build\\web-mobile\\' -SubgamePath = 's3://web-h5game-prod/internal/' - -# Vault 地址和 Token -VAULT_ADDR = 'https://keys.i-see--you.icu' -VAULT_TOKEN = 'hvs.CAESINwE2A36YcHcC_CWSzaBtUrGq-1OcUgKjDegSMUAuB5-Gh4KHGh2cy53ZmN5UzNMb0pvcUJ6QlJXdURmZnh1MlU' - -# 创建客户端 -client = hvac.Client(url=VAULT_ADDR, token=VAULT_TOKEN) - -if client.is_authenticated(): - secret = client.secrets.kv.v2.read_secret_version( - path='default', - mount_point='aws', - raise_on_deleted_version=True - ) - SwitchBundle = secret['data']['data']['switchBundle'] - if "1" != SwitchBundle: - print("script exit, switchBundle status is " + SwitchBundle) - exit() -else: - print("running script failed!") - exit() - -print("start running script...") - - - -BundleConfig = { - '6001': 'whot', - '6002': 'whot', - '6003': 'whot', - '6005': 'BRWhot', - '2001': 'roulette', - '2002': 'wajeSpin', - '2003': 'bottleSpin', - '2004': 'blackjack', - '2006': 'betterjack', - '6007': 'BRWhot', - '2008': 'slots', - '2005': 'christmas', -} - -unlinkArray = [ - 'cocos2d-js.109f3.js', - 'cocos2d-js-min.13181.js', - 'physics.9d2de.js', - 'physics-min.ce5ee.js', - 'style-desktop.dfd76.css', - 'style-mobile.6e9cd.css', - 'splash.85cfd.png' -] - -def remove_folder(path): - print('remove path: ', path) - if os.path.exists(path): - if os.path.isfile(path) or os.path.islink(path): - os.remove(path) - else: - for filename in os.listdir(path): - remove_folder(os.path.join(path, filename)) - os.rmdir(path) - -SrcDir = BuildDir + '\\src\\' -SrcDst = SubgamePath + 'lib/src/' -rmResult = subprocess.run(['cmd', '/c', 'aws s3 rm ' + SrcDst + ' --recursive'], capture_output=True, text=True) -print('remove dir src result: ', rmResult) -cmd = 'call aws s3 cp '+ SrcDir +' '+SrcDst + ' --recursive --region eu-west-3' -result = subprocess.run(['cmd', '/c', cmd], capture_output=True, text=True) -print('upload dir src result: ', result) -remove_folder(SrcDir) - -for filename in os.listdir(BuildDir): - file_path = os.path.join(BuildDir, filename) - if filename in unlinkArray: - os.remove(file_path) - -for gameid in BundleConfig: - awsPath = SubgamePath + gameid + '/assets/' + BundleConfig[gameid] +'/' - - rmResult = subprocess.run(['cmd', '/c', 'aws s3 rm ' + awsPath + ' --recursive'], capture_output=True, text=True) - print('remove game gameid: ', gameid, ' result: ', rmResult) - - subgameDir = BuildDir + 'assets\\' + BundleConfig[gameid] +'\\' - cmd = 'call aws s3 cp '+ subgameDir +' '+awsPath + ' --recursive --region eu-west-3' - result = subprocess.run(['cmd', '/c', cmd], capture_output=True, text=True) - print(result) - -for gameid in BundleConfig: - awsPath = SubgamePath + gameid + '/assets/' + BundleConfig[gameid] +'/' - subgameDir = BuildDir + 'assets\\' + BundleConfig[gameid] - remove_folder(subgameDir) - -for gameid in BundleConfig: - awsPath = SubgamePath + gameid + '/' - cmd = 'call aws s3 cp '+ BuildDir +' '+awsPath + ' --recursive --region eu-west-3' - result = subprocess.run(['cmd', '/c', cmd], capture_output=True, text=True) - print('upload game gameid: ', gameid, ' result: ', result) \ No newline at end of file diff --git a/python/switch_bundle_common.py b/python/switch_bundle_common.py new file mode 100644 index 0000000..695c77c --- /dev/null +++ b/python/switch_bundle_common.py @@ -0,0 +1,196 @@ +"""Shared Vault authorization and bundle publishing for both environments.""" + +import os +import sys +import subprocess +import hashlib + +from vault_options import parse_vault_options, read_switch_bundle + +REGION_CONFIG = { + 'dev': 'ap-east-1', + 'prod': 'eu-west-3', +} + +OSS_PATH_CONFIG = { + 'dev': { + 'waje': 's3://web-h5game-test-waje/internal/', + 'wtan': 's3://web-h5game-test-wtan/internal/', + }, + 'prod': { + 'waje': 's3://web-h5game-prod/internal/', + 'wtan': '' + }, +} + +PROFILE_CONFIG = { + 'dev': { + 'waje': 'h5-new', + 'wtan': 'h5-new', + }, + 'prod': { + 'waje': 'default', + 'wtan': 'h5-new', + } +} + +VAULT_ADDR = 'https://keys.i-see--you.icu' +VAULT_TOKEN = 'hvs.CAESIJrOb65T9ylAMyzlqQOJjI-yutFmTysRbvYbN3IpGra5Gh4KHGh2cy5GeXU4N3FuVEhIQ3A5YmpIM1BETFdYMDc' + +BUNDLE_CONFIG = { + '6001': 'whot', + '6002': 'whot', + '6003': 'whot', + '6005': 'BRWhot', + '2001': 'roulette', + '2002': 'wajeSpin', + '2003': 'bottleSpin', + '2004': 'blackjack', + '2006': 'betterjack', + '6007': 'BRWhot', + '2008': 'slots', + '2005': 'christmas', +} + +UNLINK_ARRAY = [ + 'cocos2d-js.109f3.js', + 'cocos2d-js-min.13181.js', + 'physics.9d2de.js', + 'physics-min.ce5ee.js', + 'style-desktop.dfd76.css', + 'style-mobile.6e9cd.css', + 'splash.85cfd.png', +] + + +def create_vault_client(**kwargs): + """Create the default Vault client only when a real publish is run.""" + import hvac + + return hvac.Client(**kwargs) + + +def remove_folder(path): + print('remove path: ', path) + if os.path.exists(path): + if os.path.isfile(path) or os.path.islink(path): + os.remove(path) + else: + for filename in os.listdir(path): + remove_folder(os.path.join(path, filename)) + os.rmdir(path) + + +def run_publish(proj_dir, subgame_path, region, profile, + run_command=subprocess.run): + """Upload shared resources and bundles, then publish each game's shell.""" + build_dir = proj_dir + '\\build\\web-mobile\\' + + src_dir = build_dir + '\\src\\' + src_dst = subgame_path + 'lib/src/' + remove_result = run_command( + ['cmd', '/c', 'aws s3 rm ' + src_dst + ' --recursive' + ' --profile ' + profile + ' --region ' + region], + capture_output=True, + text=True, + ) + print('remove dir src result: ', remove_result) + + command = ( + 'call aws s3 cp ' + src_dir + ' ' + src_dst + + ' --recursive' + + ' --profile ' + profile + + ' --region ' + region + ) + result = run_command( + ['cmd', '/c', command], + capture_output=True, + text=True, + ) + print('upload dir src result: ', result) + remove_folder(src_dir) + + for filename in os.listdir(build_dir): + file_path = os.path.join(build_dir, filename) + if filename in UNLINK_ARRAY: + os.remove(file_path) + + for gameid in BUNDLE_CONFIG: + aws_path = ( + subgame_path + gameid + '/assets/' + + BUNDLE_CONFIG[gameid] + '/' + ) + remove_result = run_command( + ['cmd', '/c', 'aws s3 rm ' + aws_path + ' --recursive' + ' --profile ' + profile + ' --region ' + region], + capture_output=True, + text=True, + ) + print('remove game gameid: ', gameid, ' result: ', remove_result) + + subgame_dir = build_dir + 'assets\\' + BUNDLE_CONFIG[gameid] + '\\' + command = ( + 'call aws s3 cp ' + subgame_dir + ' ' + aws_path + + ' --recursive' + + ' --profile ' + profile + + ' --region ' + region + ) + result = run_command( + ['cmd', '/c', command], + capture_output=True, + text=True, + ) + print(result) + + for gameid in BUNDLE_CONFIG: + subgame_dir = build_dir + 'assets\\' + BUNDLE_CONFIG[gameid] + remove_folder(subgame_dir) + + for gameid in BUNDLE_CONFIG: + aws_path = subgame_path + gameid + '/' + command = ( + 'call aws s3 cp ' + build_dir + ' ' + aws_path + + ' --recursive' + + ' --profile ' + profile + + ' --region ' + region + ) + result = run_command( + ['cmd', '/c', command], + capture_output=True, + text=True, + ) + print('upload game gameid: ', gameid, ' result: ', result) + + +def main(argv, client_factory=None, publish=None): + """Parse the legacy CLI arguments and publish when Vault permits it.""" + proj_dir, pwd, env, cat, vault_path, vault_mount_point = parse_vault_options(argv) + if len(argv) < 5: + print('argv error!') + sys.exit(-1) + md5_obj = hashlib.md5() + md5_obj.update((env + cat).encode('utf-8')) + hash_value = md5_obj.hexdigest() + print('hash = ' + hash_value) + if hash_value != pwd: + print('argv verify failed!') + sys.exit(-2) + + if client_factory is None: + client_factory = create_vault_client + if publish is None: + publish = run_publish + + client = client_factory(url=VAULT_ADDR, token=VAULT_TOKEN) + + if client.is_authenticated(): + secret = read_switch_bundle(client, vault_path, vault_mount_point) + switch_bundle = secret['data']['data']['enable'] + print(switch_bundle) + if '1' != switch_bundle: + print('script exit, switchBundle status is ' + switch_bundle) + sys.exit(-999) + else: + print('running script failed!') + sys.exit(-1000) + + print('start running script...') + publish(proj_dir, OSS_PATH_CONFIG[env][cat], REGION_CONFIG[env], PROFILE_CONFIG[env][cat]) diff --git a/python/test_switch_bundle_common.py b/python/test_switch_bundle_common.py new file mode 100644 index 0000000..8e59d72 --- /dev/null +++ b/python/test_switch_bundle_common.py @@ -0,0 +1,104 @@ +import runpy +import unittest +from pathlib import Path +from unittest.mock import patch + +from switch_bundle_common import main + + +class _FakeVaultV2(object): + def __init__(self, switch_bundle='1'): + self.calls = [] + self.switch_bundle = switch_bundle + + def read_secret_version(self, **kwargs): + self.calls.append(kwargs) + return {'data': {'data': {'switchBundle': self.switch_bundle}}} + + +class _FakeClient(object): + def __init__(self, authenticated=True, switch_bundle='1'): + self.authenticated = authenticated + self.v2 = _FakeVaultV2(switch_bundle) + self.secrets = self + self.kv = self + + def is_authenticated(self): + return self.authenticated + + +class SwitchBundleCommonTests(unittest.TestCase): + def test_main_passes_vault_options_and_environment_to_publish(self): + client = _FakeClient() + publish_calls = [] + + main( + ['switchBundle.py', 'project', '123!@#', 'release', 'production'], + 's3://web-h5game-test/internal/', + 'ap-east-1', + client_factory=lambda **kwargs: client, + publish=lambda *args: publish_calls.append(args), + ) + + self.assertEqual( + client.v2.calls[0]['path'], + 'release', + ) + self.assertEqual( + client.v2.calls[0]['mount_point'], + 'production', + ) + self.assertEqual( + publish_calls, + [('project', 's3://web-h5game-test/internal/', 'ap-east-1')], + ) + + def test_main_skips_publish_when_password_is_invalid(self): + client_calls = [] + publish_calls = [] + + main( + ['switchBundle.py', 'project', 'wrong-password'], + 's3://web-h5game-test/internal/', + 'ap-east-1', + client_factory=lambda **kwargs: client_calls.append(kwargs), + publish=lambda *args: publish_calls.append(args), + ) + + self.assertEqual(client_calls, []) + self.assertEqual(publish_calls, []) + + def test_main_skips_publish_when_vault_does_not_allow_it(self): + for authenticated, switch_bundle in [(False, '1'), (True, '0')]: + with self.subTest(authenticated=authenticated, switch_bundle=switch_bundle): + client = _FakeClient(authenticated, switch_bundle) + publish_calls = [] + + main( + ['switchBundle.py', 'project', '123!@#'], + 's3://web-h5game-test/internal/', + 'ap-east-1', + client_factory=lambda **kwargs: client, + publish=lambda *args: publish_calls.append(args), + ) + + self.assertEqual(publish_calls, []) + self.assertEqual(len(client.v2.calls), int(authenticated)) + + def test_entry_points_keep_their_publish_destinations(self): + environments = [ + ('switchBundle.py', 's3://web-h5game-test/internal/', 'ap-east-1'), + ('switchBundleR.py', 's3://web-h5game-prod/internal/', 'eu-west-3'), + ] + for filename, destination, region in environments: + with self.subTest(filename=filename): + script = str(Path(__file__).with_name(filename)) + argv = [script, 'project', '123!@#', 'release', 'production'] + with patch('sys.argv', argv), patch('switch_bundle_common.main') as entry: + runpy.run_path(script, run_name='__main__') + + entry.assert_called_once_with(argv, destination, region) + + +if __name__ == '__main__': + unittest.main() diff --git a/python/test_vault_options.py b/python/test_vault_options.py new file mode 100644 index 0000000..b2a6285 --- /dev/null +++ b/python/test_vault_options.py @@ -0,0 +1,66 @@ +import unittest + +from vault_options import parse_vault_options, read_switch_bundle + + +class _FakeVaultV2(object): + def __init__(self): + self.calls = [] + + def read_secret_version(self, **kwargs): + self.calls.append(kwargs) + return {'data': {'data': {'switchBundle': '1'}}} + + +class _FakeClient(object): + def __init__(self): + self.v2 = _FakeVaultV2() + self.secrets = self + self.kv = self + + +class ParseVaultOptionsTests(unittest.TestCase): + def test_uses_default_path_and_mount_point(self): + options = parse_vault_options(['switchBundle.py', 'project', '123!@#']) + + self.assertEqual(options, ('default', 'aws')) + + def test_reads_path_and_mount_point_after_existing_arguments(self): + options = parse_vault_options( + ['switchBundle.py', 'project', '123!@#', 'release', 'production'] + ) + + self.assertEqual(options, ('release', 'production')) + + def test_uses_default_mount_point_when_only_path_is_provided(self): + options = parse_vault_options( + ['switchBundle.py', 'project', '123!@#', 'release'] + ) + + self.assertEqual(options, ('release', 'aws')) + + def test_passes_path_and_mount_point_to_vault(self): + client = _FakeClient() + + read_switch_bundle(client, 'release', 'production') + + self.assertEqual( + client.v2.calls, + [{ + 'path': 'release', + 'mount_point': 'production', + 'raise_on_deleted_version': True, + }], + ) + + def test_uses_default_vault_settings_when_not_provided(self): + client = _FakeClient() + + read_switch_bundle(client) + + self.assertEqual(client.v2.calls[0]['path'], 'default') + self.assertEqual(client.v2.calls[0]['mount_point'], 'aws') + + +if __name__ == '__main__': + unittest.main() diff --git a/python/vault_options.py b/python/vault_options.py new file mode 100644 index 0000000..7382c95 --- /dev/null +++ b/python/vault_options.py @@ -0,0 +1,29 @@ +DEFAULT_VAULT_PATH = 'dev' +DEFAULT_VAULT_MOUNT_POINT = 'tokens' + + +def parse_vault_options(argv): + """Return the Vault path and mount point from the script arguments. + + The first two arguments are already used by the publishing scripts for + the project directory and access password. Vault settings therefore + remain optional arguments 3 and 4 so existing invocations keep working. + """ + proj = argv[1] + pwd = argv[2] + env = argv[3] + cat = argv[4] + path = argv[5] if len(argv) > 5 else DEFAULT_VAULT_PATH + mount_point = argv[6] if len(argv) > 6 else DEFAULT_VAULT_MOUNT_POINT + return proj, pwd, env, cat, path, mount_point + + +def read_switch_bundle( + client, + path=DEFAULT_VAULT_PATH, + mount_point=DEFAULT_VAULT_MOUNT_POINT): + return client.secrets.kv.v2.read_secret_version( + path=path, + mount_point=mount_point, + raise_on_deleted_version=True + )