From 49d3d90ca55fc33c3a609a1f31c1ed49d747b031 Mon Sep 17 00:00:00 2001 From: Brad Klein Date: Mon, 1 May 2023 15:15:11 -0600 Subject: [PATCH] Add tenant name/id header when scoped to tenant (CASMPET-6319) --- cray/cli.py | 21 ++++++++++++-- cray/config.py | 2 +- cray/rest.py | 25 +++++++++------- cray/tests/conftest.py | 7 +++-- cray/tests/test_functional/test_init.py | 38 +++++++++++++++++++++---- cray/tests/test_modules/test_config.py | 3 +- cray/tests/test_unit/test_generator.py | 7 +++-- cray/tests/utils.py | 8 ++++-- cray/utils.py | 17 +++++++++++ 9 files changed, 101 insertions(+), 27 deletions(-) diff --git a/cray/cli.py b/cray/cli.py index 90c4e34..e89cd1c 100644 --- a/cray/cli.py +++ b/cray/cli.py @@ -95,6 +95,10 @@ def cli(ctx, *args, **kwargs): "--hostname", default=None, no_global=True, help='Hostname of cray system.' ) +@option( + "--tenant", default=None, no_global=True, + help='Tenant name to scope requests for.' +) @option( "--no-auth", is_flag=True, help='Do not attempt to authenticate.' @@ -103,7 +107,7 @@ def cli(ctx, *args, **kwargs): "--overwrite", is_flag=True, help="Overwrite existing configuration if it exists" ) -def init(ctx, hostname, no_auth, overwrite, **kwargs): +def init(ctx, hostname, no_auth, overwrite, tenant, **kwargs): """ Initialize/reinitialize the Cray CLI """ # pylint: disable=line-too-long config_dir = ctx.obj.get('config_dir') @@ -137,9 +141,21 @@ def init(ctx, hostname, no_auth, overwrite, **kwargs): if not re.match("^http(s)?://", hostname): hostname = f'https://{hostname}' + if tenant is None: + tenant = ctx.obj.get( + 'config', + {} + ).get( + 'core.tenant', + click.prompt('Tenant Name (leave blank for global scope):', + default="", + type=str) + ) + initialize_dirs(config_dir) # No error if directories already exist config = Config(config_dir, configuration, raise_err=False) config.set_deep('core.hostname', hostname) + config.set_deep('core.tenant', tenant) config.save() config.set_active() ctx.obj['config'] = config @@ -153,7 +169,8 @@ def init(ctx, hostname, no_auth, overwrite, **kwargs): echo( ctx.forward( login, hostname=hostname, username=username, - password=password, rsa_token=rsa_token, **kwargs + password=password, rsa_token=rsa_token, + tenant=tenant, **kwargs ), level=LOG_FORCE, ctx=ctx ) return "Initialization complete." diff --git a/cray/config.py b/cray/config.py index dee97e7..ec0b7cd 100644 --- a/cray/config.py +++ b/cray/config.py @@ -73,7 +73,7 @@ class Config(NestedDict): Use ctx.obj.config instead. """ - _CORE_KEYS = ['hostname', 'quiet', 'format'] + _CORE_KEYS = ['hostname', 'tenant', 'quiet', 'format'] def __init__(self, path, config, raise_err=False): # pylint: disable=super-init-not-called diff --git a/cray/rest.py b/cray/rest.py index 29a19cf..fa15b07 100644 --- a/cray/rest.py +++ b/cray/rest.py @@ -41,6 +41,7 @@ from cray.errors import InsecureError from cray.errors import UnauthorizedError from cray.utils import get_hostname +from cray.utils import get_headers def make_url(route, url=None, default_scheme='https', ctx=None): @@ -90,34 +91,36 @@ def request(method, route, callback=None, **kwargs): try: url = make_url(route) + headers = get_headers(ctx=ctx) echo(f'REQUEST: {method} to {url}', ctx=ctx, level=LOG_DEBUG) echo(f'OPTIONS: {opts}', ctx=ctx, level=LOG_RAW) # TODO: Find solution for this. with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=InsecureRequestWarning) - response = requester.request(method, url, **opts) + response = requester.request(method, url, **opts, headers=headers) if not response.ok: _log_request_error(response.text, ctx) raise BadResponseError(response, ctx=ctx) - except InsecureTransportError as err: # pragma: NO COVER - _log_request_error(err, ctx) - # pylint: disable=raise-missing-from + except InsecureTransportError as err: + # pragma: NO COVER + _log_request_error(err, ctx) # pylint: disable=raise-missing-from raise InsecureError(ctx=ctx) - except InvalidGrantError as err: # pragma: NO COVER - _log_request_error(err, ctx) - # pylint: disable=raise-missing-from + except InvalidGrantError as err: + # pragma: NO COVER + _log_request_error(err, ctx) # pylint: disable=raise-missing-from raise UnauthorizedError(ctx=ctx) - except requests.exceptions.HTTPError as err: # pragma: NO COVER + except requests.exceptions.HTTPError as err: + # pragma: NO COVER _log_request_error(err, ctx) if err.response.status_code == 401: # pylint: disable=raise-missing-from raise UnauthorizedError(ctx=ctx) raise click.UsageError(str(err)) - except requests.exceptions.Timeout as err: # pragma: NO COVER + except requests.exceptions.Timeout as err: + # pragma: NO COVER _log_request_error(err, ctx) raise click.UsageError('Timed out trying to connect to cray', ctx=ctx) - except click.ClickException: - # Don't log click specific exceptions + except click.ClickException: # Don't log click specific exceptions raise except Exception as err: _log_request_error(err, ctx) diff --git a/cray/tests/conftest.py b/cray/tests/conftest.py index 669fd9e..237cca3 100644 --- a/cray/tests/conftest.py +++ b/cray/tests/conftest.py @@ -42,6 +42,7 @@ from cray.tests.utils import new_username from cray.tests.utils import new_configname from cray.tests.utils import new_hostname +from cray.tests.utils import new_tenant from cray.tests.utils import create_config_file @@ -95,11 +96,13 @@ def cli_runner(request): 'configname': 'default', 'username': new_username(), 'hostname': new_hostname(), + 'tenant': '', } config = { 'configname': new_configname(), 'username': new_username(), 'hostname': new_hostname(), + 'tenant': new_tenant(), } opts = { 'default': default, @@ -120,11 +123,11 @@ def cli_runner(request): initialize_dirs(os.path.join(os.getcwd(), '.config/cray/')) create_config_file( default['configname'], default['hostname'], - default['username'] + default['tenant'], default['username'] ) create_config_file( config['configname'], config['hostname'], - config['username'] + config['tenant'], config['username'] ) yield runner, cli.cli, opts diff --git a/cray/tests/test_functional/test_init.py b/cray/tests/test_functional/test_init.py index 766dca2..fea568c 100644 --- a/cray/tests/test_functional/test_init.py +++ b/cray/tests/test_functional/test_init.py @@ -42,8 +42,9 @@ def test_cray_init_no_hostname(cli_runner): config = opts['default'] hostname = config['hostname'] configname = config['configname'] + tenant = config['tenant'] result = runner.invoke( - cli, ['init', '--no-auth'], input=f'{hostname}\n' + cli, ['init', '--no-auth', '--tenant', tenant], input=f'{hostname}\n' ) assert result.exit_code == 0 assert "Initialization complete." in result.output @@ -54,6 +55,29 @@ def test_cray_init_no_hostname(cli_runner): assert data['core']['hostname'] == hostname +@pytest.mark.parametrize( + 'cli_runner', [{'is_init': True}], indirect=['cli_runner'] +) +def test_cray_init_no_tenant(cli_runner): + """ Test `cray init --configuration {config}` + for validating a new configuration is created. """ + runner, cli, opts = cli_runner + config = opts['default'] + hostname = config['hostname'] + configname = config['configname'] + tenant = config['tenant'] + result = runner.invoke( + cli, ['init', '--no-auth', '--hostname', hostname], input=f'{tenant}\n' + ) + assert result.exit_code == 0 + assert "Initialization complete." in result.output + filep = f'.config/cray/configurations/{configname}' + assert os.path.isfile(filep) + with open(filep, encoding='utf-8') as f: + data = toml.load(f) + assert data['core']['tenant'] == tenant + + @pytest.mark.parametrize( 'cli_runner', [{'is_init': True}], indirect=['cli_runner'] ) @@ -62,8 +86,9 @@ def test_cray_init(cli_runner): runner, cli, opts = cli_runner config = opts['default'] hostname = config['hostname'] + tenant = config['tenant'] configname = config['configname'] - result = runner.invoke(cli, ['init', '--hostname', hostname, '--no-auth']) + result = runner.invoke(cli, ['init', '--hostname', hostname, '--no-auth', '--tenant', tenant]) assert result.exit_code == 0 assert "Initialization complete." in result.output @@ -72,6 +97,7 @@ def test_cray_init(cli_runner): with open(filep, encoding='utf-8') as f: data = toml.load(f) assert data['core']['hostname'] == hostname + assert data['core']['tenant'] == tenant @pytest.mark.parametrize( @@ -82,7 +108,8 @@ def test_cray_init_verify_no_auth(cli_runner, rest_mock): runner, cli, opts = cli_runner config = opts['default'] hostname = config['hostname'] - result = runner.invoke(cli, ['init', '--hostname', hostname, '--no-auth']) + tenant = config['tenant'] + result = runner.invoke(cli, ['init', '--hostname', hostname, '--no-auth', '--tenant', tenant]) assert result.exit_code == 0 result = runner.invoke(cli, ['uas', 'list']) print(result.output) @@ -98,11 +125,12 @@ def test_cray_init_w_config(cli_runner): runner, cli, opts = cli_runner config = opts['config'] hostname = config['hostname'] + tenant = config['tenant'] configname = config['configname'] result = runner.invoke( cli, - ['init', '--hostname', hostname, '--no-auth', '--configuration', - configname] + ['init', '--hostname', hostname, '--tenant', tenant, '--no-auth', + '--configuration', configname] ) assert result.exit_code == 0 diff --git a/cray/tests/test_modules/test_config.py b/cray/tests/test_modules/test_config.py index edcbbdd..17f0584 100644 --- a/cray/tests/test_modules/test_config.py +++ b/cray/tests/test_modules/test_config.py @@ -349,10 +349,11 @@ def test_cray_config_get_shallow(cli_runner): runner, cli, opts = cli_runner config = opts['default'] hostname = config['hostname'] + tenant = config['tenant'] result = runner.invoke(cli, ['config', 'get', 'core']) assert result.exit_code == 0 data = json.loads(result.output) - assert data == {'hostname': hostname} + assert data == {'hostname': hostname, 'tenant': tenant} def test_cray_config_get_missing_param(cli_runner): diff --git a/cray/tests/test_unit/test_generator.py b/cray/tests/test_unit/test_generator.py index e5c3685..b41ade7 100644 --- a/cray/tests/test_unit/test_generator.py +++ b/cray/tests/test_unit/test_generator.py @@ -125,12 +125,13 @@ def test_generator_danger_tag_default_confirm(cli_runner, rest_mock, pets): runner, cli, opts = cli_runner config = opts['default'] hostname = config['hostname'] - petId = "1" - result = runner.invoke(cli, ['pets', 'pet', 'delete', petId], input='y') + orderId = "1" + result = runner.invoke(cli, ['pets', 'store', 'order', 'delete', orderId], 'y') + data = json.loads(strip_confirmation(result.output)) print(result.output) assert result.exit_code == 0 data = json.loads(strip_confirmation(result.output)) - assert data['url'] == f'{hostname}/v2/pet/{petId}' + assert data['url'] == f'{hostname}/v2/store/order/{orderId}' assert data['method'].lower() == 'delete' diff --git a/cray/tests/utils.py b/cray/tests/utils.py index a341c84..abc7403 100644 --- a/cray/tests/utils.py +++ b/cray/tests/utils.py @@ -42,6 +42,10 @@ def new_hostname(): return f"https://{_uuid().replace('-', '')}" +def new_tenant(): + return f"vcluster-{_uuid().replace('-', '')}" + + def new_configname(): return _uuid().replace('-', '') @@ -50,9 +54,9 @@ def new_random_string(): return _uuid().replace('-', '') -def create_config_file(filename, hostname, username): +def create_config_file(filename, hostname, tenant, username): data = { - 'core': {'hostname': hostname}, + 'core': {'hostname': hostname, 'tenant': tenant}, 'auth': {'login': {'username': username}} } path = '.config/cray/configurations' diff --git a/cray/utils.py b/cray/utils.py index c47a07d..d3b835c 100644 --- a/cray/utils.py +++ b/cray/utils.py @@ -74,6 +74,23 @@ def get_hostname(ctx=None): return hostname +def get_tenant(ctx=None): + """ Get the tenant requests are scoped to (None if not scoped to a tenant) """ + ctx = ctx or click.get_current_context() + config = ctx.obj['config'] + tenant = config.get('core.tenant', None) + return tenant + +def get_headers(ctx=None): + """ Get the headers which may or may not contain a tenant """ + headers={} + ctx = ctx or click.get_current_context() + tenant = get_tenant(ctx=ctx) + if tenant: + headers={"Cray-Tenant-Name":tenant} + return headers + + def hostname_to_name(hostname=None, ctx=None): """ Convert hostname to name value for saving as filename""" hostname = hostname or get_hostname(ctx=ctx)