From 5a6f348c0d9c57907cb05532e040c82bcabf2805 Mon Sep 17 00:00:00 2001 From: Blai Peidro Date: Mon, 14 Sep 2026 01:53:39 +0200 Subject: [PATCH] feat: read the connection settings from ASCENDER_ environment variables Every connection setting the client takes from the environment now answers to an ASCENDER_ name: ASCENDER_HOST, ASCENDER_OAUTH_TOKEN, ASCENDER_TOKEN, ASCENDER_USERNAME, ASCENDER_PASSWORD, ASCENDER_VERIFY_SSL, ASCENDER_VERBOSE, ASCENDER_FORMAT and ASCENDER_COLOR. This is the same shape as ascender-manage: the new name is what the documentation says, and the old ones keep working. The nesting is what made this worth a helper rather than one more level of env.get. The token was already four deep, and adding two more spellings inside it would have put the default out of sight of the name it belongs to. env_default takes the suffix and the ordered prefixes instead, so the precedence is stated once and every argument reads the same way. Precedence is ASCENDER_, then CONTROLLER_, then TOWER_, prefix before suffix, so ASCENDER_TOKEN beats CONTROLLER_OAUTH_TOKEN: the prefix says which release the script was written against, where the suffix is only a spelling. Below the new prefix the old chain keeps the order it already had, and a name that is set but empty still wins over a later one, which is what the nested env.get calls did. ascender login -f human prints the ASCENDER_ name now. Both are read, so a shell that already sourced the old line keeps working. The integration tests that prove the token path works by stripping the username and password out of the environment now strip all three prefixes. With only one of them removed, a developer with the older spelling exported would have stayed authenticated by password and the tests would have passed without exercising the token at all. --- CHANGELOG.md | 10 +++ CONTRIBUTING.md | 6 +- README.md | 10 +-- ascenderkit/cli/docs/README.md | 6 +- .../cli/docs/source/authentication.rst | 18 +++--- ascenderkit/cli/docs/source/conf.py | 6 +- ascenderkit/cli/docs/source/output.rst | 2 +- ascenderkit/cli/docs/source/usage.rst | 15 +++-- ascenderkit/cli/format.py | 43 ++++++++++--- ascenderkit/cli/resource.py | 2 +- ascenderkit/cli/sphinx.py | 10 +-- tests/integration/conftest.py | 18 +++--- tests/integration/test_auth.py | 19 ++++-- tests/unit/cli/test_config.py | 16 +++++ tests/unit/cli/test_environment_prefixes.py | 64 +++++++++++++++++++ tests/unit/cli/test_sphinx.py | 13 ++-- 16 files changed, 193 insertions(+), 65 deletions(-) create mode 100644 tests/unit/cli/test_environment_prefixes.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 04812b7..d0771ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,9 +47,19 @@ and this project adheres to the versioning of Testing a lookup credential, or a lookup credential type, performs the lookup and reports what came back, with `--inputs` and `--metadata` to try values that are not saved yet. Both take JSON or YAML, or `@` a file holding either. +- Every connection setting now answers to an `ASCENDER_` environment variable: + `ASCENDER_HOST`, `ASCENDER_OAUTH_TOKEN`, `ASCENDER_TOKEN`, `ASCENDER_USERNAME`, + `ASCENDER_PASSWORD`, `ASCENDER_VERIFY_SSL`, `ASCENDER_VERBOSE`, + `ASCENDER_FORMAT` and `ASCENDER_COLOR`. The `CONTROLLER_` and `TOWER_` prefixes + still name the same settings, so nothing written against them breaks. Where more + than one is set, `ASCENDER_` wins, then `CONTROLLER_`, then `TOWER_`. ### Changed +- `ascender login -f human` prints `export ASCENDER_OAUTH_TOKEN=` rather than + `export CONTROLLER_OAUTH_TOKEN=`. Both variables are read, so a shell that + already sourced the old line keeps working. + - TLS certificates are now verified by default. Set `ASCENDERKIT_ASSUME_UNTRUSTED` to restore the previous behaviour when talking to an Ascender that presents a self-signed certificate. The CLI is unaffected: it already verified unless given diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7046665..0b03cb2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,9 +49,9 @@ imported back, that monitored jobs exit 0, 1 and 2 for success, failure and cancellation, and that token authentication works. ```bash -export CONTROLLER_HOST=https://ascender.example.org -export CONTROLLER_USERNAME=admin CONTROLLER_PASSWORD=... -export CONTROLLER_VERIFY_SSL=false # self-signed development servers +export ASCENDER_HOST=https://ascender.example.org +export ASCENDER_USERNAME=admin ASCENDER_PASSWORD=... +export ASCENDER_VERIFY_SSL=false # self-signed development servers pytest tests/integration -v ``` diff --git a/README.md b/README.md index eef6d17..5e5105b 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ pip install -e ascender-kit Point the client at your server and confirm it can authenticate: ```bash -export CONTROLLER_HOST=https://ascender.example.org -export CONTROLLER_USERNAME=alice -export CONTROLLER_PASSWORD=secret +export ASCENDER_HOST=https://ascender.example.org +export ASCENDER_USERNAME=alice +export ASCENDER_PASSWORD=secret ascender config ``` @@ -69,9 +69,11 @@ Connection settings resolve from highest to lowest precedence: | Precedence | Source | | ---------- | ------ | | 1 | Command line flags, such as `--conf.host` and `--conf.token` | -| 2 | Environment variables: `CONTROLLER_HOST`, `CONTROLLER_USERNAME`, `CONTROLLER_PASSWORD` | +| 2 | Environment variables: `ASCENDER_HOST`, `ASCENDER_USERNAME`, `ASCENDER_PASSWORD` | | 3 | The config file written by `ascender login` and `ascender config` | +The `CONTROLLER_` and `TOWER_` prefixes still name the same variables, so a script written against either keeps working. + For repeated use, generate a token instead of passing credentials each time: ```bash diff --git a/ascenderkit/cli/docs/README.md b/ascenderkit/cli/docs/README.md index a397abd..869d84c 100644 --- a/ascenderkit/cli/docs/README.md +++ b/ascenderkit/cli/docs/README.md @@ -14,8 +14,8 @@ wrong. To build the reference guide as well, point the same command at a real Ascender: - ~ CONTROLLER_HOST=https://ascender.example.org CONTROLLER_USERNAME=example \ - CONTROLLER_PASSWORD=secret sphinx-build -b html -W source build/html + ~ ASCENDER_HOST=https://ascender.example.org ASCENDER_USERNAME=example \ + ASCENDER_PASSWORD=secret sphinx-build -b html -W source build/html What needs a server, and why ---------------------------- @@ -28,5 +28,5 @@ version, settings and user access level. The extension itself imports without a server. `render()` runs only when the `autoprogram` directive asks for the parser, so that one page is what needs -`CONTROLLER_HOST`, not the Sphinx run. `conf.py` drops the page when no +`ASCENDER_HOST`, not the Sphinx run. `conf.py` drops the page when no credentials are present, which is what makes the offline build work. diff --git a/ascenderkit/cli/docs/source/authentication.rst b/ascenderkit/cli/docs/source/authentication.rst index 70d448c..ca6a09e 100644 --- a/ascenderkit/cli/docs/source/authentication.rst +++ b/ascenderkit/cli/docs/source/authentication.rst @@ -12,9 +12,9 @@ The preferred mechanism for authenticating with Ascender is by generating and st .. code:: bash - CONTROLLER_HOST=https://ascender.example.org \ - CONTROLLER_USERNAME=alice \ - CONTROLLER_PASSWORD=secret \ + ASCENDER_HOST=https://ascender.example.org \ + ASCENDER_USERNAME=alice \ + ASCENDER_PASSWORD=secret \ ascender login As a convenience, the ``ascender login -f human`` command prints a shell-formatted token @@ -22,15 +22,15 @@ value: .. code:: bash - export CONTROLLER_OAUTH_TOKEN=6E5SXhld7AMOhpRveZsLJQsfs9VS8U + export ASCENDER_OAUTH_TOKEN=6E5SXhld7AMOhpRveZsLJQsfs9VS8U By ingesting this token, you can run subsequent CLI commands without having to specify your username and password each time: .. code:: bash - export CONTROLLER_HOST=https://ascender.example.org - $(CONTROLLER_USERNAME=alice CONTROLLER_PASSWORD=secret ascender login -f human) + export ASCENDER_HOST=https://ascender.example.org + $(ASCENDER_USERNAME=alice ASCENDER_PASSWORD=secret ascender login -f human) ascender config Working with OAuth2.0 Applications @@ -43,7 +43,7 @@ application was created. .. code:: bash - CONTROLLER_USERNAME=alice CONTROLLER_PASSWORD=secret ascender login \ + ASCENDER_USERNAME=alice ASCENDER_PASSWORD=secret ascender login \ --conf.client_id --conf.client_secret @@ -55,7 +55,7 @@ a read-only token, specify ``--scope read``: .. code:: bash - CONTROLLER_USERNAME=alice CONTROLLER_PASSWORD=secret \ + ASCENDER_USERNAME=alice ASCENDER_PASSWORD=secret \ ascender login --conf.scope read Session Authentication @@ -65,5 +65,5 @@ specify your username and password on every invocation: .. code:: bash - CONTROLLER_USERNAME=alice CONTROLLER_PASSWORD=secret ascender jobs list + ASCENDER_USERNAME=alice ASCENDER_PASSWORD=secret ascender jobs list ascender --conf.username alice --conf.password secret jobs list diff --git a/ascenderkit/cli/docs/source/conf.py b/ascenderkit/cli/docs/source/conf.py index 515a97c..3de1087 100644 --- a/ascenderkit/cli/docs/source/conf.py +++ b/ascenderkit/cli/docs/source/conf.py @@ -63,16 +63,16 @@ # resource against a running Ascender. There is no static command table to fall # back on: the CLI discovers its own commands the same way. So that one page is # the only part of these docs that needs a server, and asking for it without one -# exits with a message naming CONTROLLER_HOST. +# exits with a message naming ASCENDER_HOST. # # Rather than fail the whole build, drop the page when there are no credentials. # The six hand-written pages then build offline, which is what CI does, and a -# build with CONTROLLER_HOST set still produces the complete documentation. +# build with ASCENDER_HOST set still produces the complete documentation. # # The toctree in index.rst names reference unconditionally. Moving that entry # behind an `only` directive does not help, because toctree entries resolve # while the source is read and `only` is evaluated later, so the warning is # suppressed by name instead. -if not (os.environ.get('CONTROLLER_HOST') or os.environ.get('TOWER_HOST')): +if not any(os.environ.get(prefix + 'HOST') for prefix in ('ASCENDER_', 'CONTROLLER_', 'TOWER_')): exclude_patterns = ['reference.rst'] suppress_warnings = ['toc.excluded'] diff --git a/ascenderkit/cli/docs/source/output.rst b/ascenderkit/cli/docs/source/output.rst index 6463c22..a1ffb39 100644 --- a/ascenderkit/cli/docs/source/output.rst +++ b/ascenderkit/cli/docs/source/output.rst @@ -54,4 +54,4 @@ Colorized Output By default, |prog| prints colorized output using ANSI color codes. To disable this functionality, specify ``--conf.color f`` or set the environment variable -``CONTROLLER_COLOR=f``. +``ASCENDER_COLOR=f``. diff --git a/ascenderkit/cli/docs/source/usage.rst b/ascenderkit/cli/docs/source/usage.rst index 843e936..73f0c5f 100644 --- a/ascenderkit/cli/docs/source/usage.rst +++ b/ascenderkit/cli/docs/source/usage.rst @@ -77,17 +77,22 @@ A few of the most important ones are: ``-f, --conf.format`` used to specify a custom output format (the default is json) -``--conf.host, CONTROLLER_HOST`` +``--conf.host, ASCENDER_HOST`` the full URL of the Ascender host (i.e., https://my.ascender.example.org) -``-k, --conf.insecure, CONTROLLER_VERIFY_SSL`` +``-k, --conf.insecure, ASCENDER_VERIFY_SSL`` allows insecure server connections when using SSL -``--conf.username, CONTROLLER_USERNAME`` +``--conf.username, ASCENDER_USERNAME`` the Ascender username to use for authentication -``--conf.password, CONTROLLER_PASSWORD`` +``--conf.password, ASCENDER_PASSWORD`` the Ascender password to use for authentication -``--conf.token, CONTROLLER_OAUTH_TOKEN`` +``--conf.token, ASCENDER_OAUTH_TOKEN`` an OAuth2.0 token to use for authentication + +Each of those variables answers to two older prefixes as well, ``CONTROLLER_`` +and ``TOWER_``, so a script written against either keeps working. Where more +than one is set the ``ASCENDER_`` name wins, then ``CONTROLLER_``, then +``TOWER_``. diff --git a/ascenderkit/cli/format.py b/ascenderkit/cli/format.py index e5c08c8..7caecfc 100644 --- a/ascenderkit/cli/format.py +++ b/ascenderkit/cli/format.py @@ -22,6 +22,31 @@ def strtobool(val): raise ValueError(f"invalid truth value {val!r}") +def env_default(env, suffix, default, extra=()): + """The first of the accepted names that `env` sets, else `default`. + + Three prefixes name the same setting, and all three stay readable so a + script written against any of them keeps working: `ASCENDER_` is what the + client documents, `CONTROLLER_` is what it read before the rebrand, and + `TOWER_` is what awxkit read before that. Precedence runs newest first. + + `extra` names further suffixes to try under the same prefix before moving + on to the next one, which is how `ASCENDER_OAUTH_TOKEN` and `ASCENDER_TOKEN` + both reach the token argument. The prefix is the outer loop because it says + which release the script was written against, where the suffix is only a + spelling of the same thing. + + A name that is set but empty wins over a later one, which is what the + nested `env.get()` calls this replaces did. + """ + for prefix in ('ASCENDER_', 'CONTROLLER_', 'TOWER_'): + for this_suffix in (suffix,) + tuple(extra): + name = prefix + this_suffix + if name in env: + return env[name] + return default + + def get_config_credentials(): """Load username and password from config.credentials.default. @@ -41,12 +66,12 @@ def add_authentication_arguments(parser, env): auth = parser.add_argument_group('authentication') auth.add_argument( '--conf.host', - default=env.get('CONTROLLER_HOST', env.get('TOWER_HOST', 'https://127.0.0.1:443')), + default=env_default(env, 'HOST', 'https://127.0.0.1:443'), metavar='https://example.ascender.org', ) auth.add_argument( '--conf.token', - default=env.get('CONTROLLER_OAUTH_TOKEN', env.get('CONTROLLER_TOKEN', env.get('TOWER_OAUTH_TOKEN', env.get('TOWER_TOKEN', '')))), + default=env_default(env, 'OAUTH_TOKEN', '', extra=('TOKEN',)), help='an OAuth2.0 token (get one by using `ascender login`)', metavar='TEXT', ) @@ -55,12 +80,12 @@ def add_authentication_arguments(parser, env): # options configured via cli args take higher precedence than those from the config auth.add_argument( '--conf.username', - default=env.get('CONTROLLER_USERNAME', env.get('TOWER_USERNAME', config_username)), + default=env_default(env, 'USERNAME', config_username), metavar='TEXT', ) auth.add_argument( '--conf.password', - default=env.get('CONTROLLER_PASSWORD', env.get('TOWER_PASSWORD', config_password)), + default=env_default(env, 'PASSWORD', config_password), metavar='TEXT', ) @@ -68,7 +93,7 @@ def add_authentication_arguments(parser, env): '-k', '--conf.insecure', help='Allow insecure server connections when using SSL', - default=not strtobool(env.get('CONTROLLER_VERIFY_SSL', env.get('TOWER_VERIFY_SSL', 'True'))), + default=not strtobool(env_default(env, 'VERIFY_SSL', 'True')), action='store_true', ) @@ -79,7 +104,7 @@ def add_verbose(formatting, env): '--verbose', dest='conf.verbose', help='print debug-level logs, including requests made', - default=strtobool(env.get('CONTROLLER_VERBOSE', env.get('TOWER_VERBOSE', 'f'))), + default=strtobool(env_default(env, 'VERBOSE', 'f')), action="store_true", ) @@ -91,7 +116,7 @@ def add_formatting_import_export(parser, env): '--conf.format', dest='conf.format', choices=['json', 'yaml'], - default=env.get('CONTROLLER_FORMAT', env.get('TOWER_FORMAT', 'json')), + default=env_default(env, 'FORMAT', 'json'), help=('specify a format for the input and output'), ) add_verbose(formatting, env) @@ -105,7 +130,7 @@ def add_output_formatting_arguments(parser, env): '--conf.format', dest='conf.format', choices=FORMATTERS.keys(), - default=env.get('CONTROLLER_FORMAT', env.get('TOWER_FORMAT', 'json')), + default=env_default(env, 'FORMAT', 'json'), help=('specify a format for the input and output'), ) formatting.add_argument( @@ -119,7 +144,7 @@ def add_output_formatting_arguments(parser, env): '--conf.color', metavar='BOOLEAN', help='Display colorized output. Defaults to True', - default=env.get('CONTROLLER_COLOR', env.get('TOWER_COLOR', 't')), + default=env_default(env, 'COLOR', 't'), type=strtobool, ) add_verbose(formatting, env) diff --git a/ascenderkit/cli/resource.py b/ascenderkit/cli/resource.py index bc95e04..c87b829 100644 --- a/ascenderkit/cli/resource.py +++ b/ascenderkit/cli/resource.py @@ -99,7 +99,7 @@ def handle(self, client, parser): else: fmt = client.get_config('format') if fmt == 'human': - print(f'export CONTROLLER_OAUTH_TOKEN={token}') + print(f'export ASCENDER_OAUTH_TOKEN={token}') else: print(to_str(FORMATTERS[fmt]({'token': token}, '.')).strip()) diff --git a/ascenderkit/cli/sphinx.py b/ascenderkit/cli/sphinx.py index 0cdd19e..5a9ea44 100644 --- a/ascenderkit/cli/sphinx.py +++ b/ascenderkit/cli/sphinx.py @@ -49,13 +49,9 @@ def render(): # The return value of this function is an argparse.ArgumentParser, which # the sphinxcontrib.autoprogram plugin crawls and generates an indexed # Sphinx document from. - for e in ( - ('CONTROLLER_HOST', 'TOWER_HOST'), - ('CONTROLLER_USERNAME', 'TOWER_USERNAME'), - ('CONTROLLER_PASSWORD', 'TOWER_PASSWORD'), - ): - if not os.environ.get(e[0]) and not os.environ.get(e[1]): - raise SystemExit('Please specify a valid {} for a real (running) installation.'.format(e[0])) # noqa + for suffix in ('HOST', 'USERNAME', 'PASSWORD'): + if not any(os.environ.get(prefix + suffix) for prefix in ('ASCENDER_', 'CONTROLLER_', 'TOWER_')): + raise SystemExit('Please specify a valid ASCENDER_{} for a real (running) installation.'.format(suffix)) # noqa cli = CLI() cli.parse_args(['ascender', '--help']) cli.connect() diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index c3476d9..e5a4e3d 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,12 +1,12 @@ """Tests that need a running Ascender. -The whole directory is skipped unless CONTROLLER_HOST is set, so `pytest +The whole directory is skipped unless a host is set, so `pytest tests/` stays offline by default and CI is unaffected until it is pointed at a server. To run them: - export CONTROLLER_HOST=https://ascender.example.org - export CONTROLLER_USERNAME=admin CONTROLLER_PASSWORD=... - export CONTROLLER_VERIFY_SSL=false # self-signed development servers + export ASCENDER_HOST=https://ascender.example.org + export ASCENDER_USERNAME=admin ASCENDER_PASSWORD=... + export ASCENDER_VERIFY_SSL=false # self-signed development servers pytest tests/integration -v """ @@ -15,7 +15,9 @@ import pytest -HOST = os.environ.get('CONTROLLER_HOST') +from ascenderkit.cli.format import env_default + +HOST = env_default(os.environ, 'HOST', None) # Without a server there is nothing here to run, so do not collect it at all. # A skip marker would still build the fixtures and report a wall of errors. @@ -31,7 +33,7 @@ def pytest_collection_modifyitems(items): @pytest.fixture(scope='session') def insecure(): """`-k` when the server presents a certificate we should not verify.""" - verify = os.environ.get('CONTROLLER_VERIFY_SSL', 'true').lower() + verify = env_default(os.environ, 'VERIFY_SSL', 'true').lower() return ['-k'] if verify in ('false', 'f', 'no', 'n', '0', 'off') else [] @@ -59,10 +61,10 @@ def api(): from ascenderkit.utils import PseudoNamespace config.base_url = HOST - config.assume_untrusted = os.environ.get('CONTROLLER_VERIFY_SSL', 'true').lower() in ('false', 'f', 'no', 'n', '0', 'off') + config.assume_untrusted = env_default(os.environ, 'VERIFY_SSL', 'true').lower() in ('false', 'f', 'no', 'n', '0', 'off') config.use_sessions = True config.credentials = PseudoNamespace( - {'default': {'username': os.environ.get('CONTROLLER_USERNAME', 'admin'), 'password': os.environ.get('CONTROLLER_PASSWORD', '')}} + {'default': {'username': env_default(os.environ, 'USERNAME', 'admin'), 'password': env_default(os.environ, 'PASSWORD', '')}} ) root = _api.Api() root.load_session().get() diff --git a/tests/integration/test_auth.py b/tests/integration/test_auth.py index 32427cc..1ba830a 100644 --- a/tests/integration/test_auth.py +++ b/tests/integration/test_auth.py @@ -9,6 +9,13 @@ import pytest +# Every prefix that reaches the same argument. A developer who exports one of +# the older spellings would otherwise still be authenticated by password, and +# these tests would pass without the token path being exercised at all. +_PREFIXES = ('ASCENDER_', 'CONTROLLER_', 'TOWER_') +_CREDENTIAL_NAMES = tuple(p + s for p in _PREFIXES for s in ('USERNAME', 'PASSWORD')) +_VERIFY_NAMES = tuple(p + 'VERIFY_SSL' for p in _PREFIXES) + @pytest.fixture def token(ascender): @@ -25,13 +32,13 @@ def test_login_human_format_is_shell_ready(ascender): rc, out, err = ascender('login', '-f', 'human') assert rc == 0, err - assert out.strip().startswith('export CONTROLLER_OAUTH_TOKEN=') + assert out.strip().startswith('export ASCENDER_OAUTH_TOKEN=') def test_a_token_authenticates(ascender, token, insecure): """No username or password, only the token.""" - env = {k: v for k, v in os.environ.items() if k not in ('CONTROLLER_USERNAME', 'CONTROLLER_PASSWORD')} - env['CONTROLLER_OAUTH_TOKEN'] = token + env = {k: v for k, v in os.environ.items() if k not in _CREDENTIAL_NAMES} + env['ASCENDER_OAUTH_TOKEN'] = token proc = subprocess.run(['ascender', *insecure, 'me', '-f', 'human'], capture_output=True, text=True, env=env, timeout=300) @@ -40,8 +47,8 @@ def test_a_token_authenticates(ascender, token, insecure): def test_a_bad_token_is_refused(ascender, insecure): - env = {k: v for k, v in os.environ.items() if k not in ('CONTROLLER_USERNAME', 'CONTROLLER_PASSWORD')} - env['CONTROLLER_OAUTH_TOKEN'] = 'not-a-real-token' + env = {k: v for k, v in os.environ.items() if k not in _CREDENTIAL_NAMES} + env['ASCENDER_OAUTH_TOKEN'] = 'not-a-real-token' proc = subprocess.run(['ascender', *insecure, 'me'], capture_output=True, text=True, env=env, timeout=300) @@ -55,7 +62,7 @@ def test_tls_is_verified_without_the_insecure_flag(insecure): # Drop the environment's opt-out as well as the flag; either one alone # still turns verification off, which is what this is checking for. - env = {k: v for k, v in os.environ.items() if k not in ('CONTROLLER_VERIFY_SSL', 'TOWER_VERIFY_SSL')} + env = {k: v for k, v in os.environ.items() if k not in _VERIFY_NAMES} proc = subprocess.run(['ascender', 'me'], capture_output=True, text=True, env=env, timeout=300) diff --git a/tests/unit/cli/test_config.py b/tests/unit/cli/test_config.py index 5c0e711..49f8d36 100644 --- a/tests/unit/cli/test_config.py +++ b/tests/unit/cli/test_config.py @@ -15,6 +15,22 @@ def test_host_from_environment(): assert config.base_url == 'https://xyz.local' +def test_host_from_the_ascender_environment_name(): + cli = CLI() + cli.parse_args(['ascender'], env={'ASCENDER_HOST': 'https://xyz.local'}) + with pytest.raises(ConnectionError): + cli.connect() + assert config.base_url == 'https://xyz.local' + + +def test_the_ascender_host_name_wins_over_the_older_ones(): + cli = CLI() + cli.parse_args(['ascender'], env={'ASCENDER_HOST': 'https://xyz.local', 'CONTROLLER_HOST': 'https://IGNORE', 'TOWER_HOST': 'https://IGNORE'}) + with pytest.raises(ConnectionError): + cli.connect() + assert config.base_url == 'https://xyz.local' + + def test_host_from_argv(): cli = CLI() cli.parse_args(['ascender', '--conf.host', 'https://xyz.local']) diff --git a/tests/unit/cli/test_environment_prefixes.py b/tests/unit/cli/test_environment_prefixes.py new file mode 100644 index 0000000..5b60920 --- /dev/null +++ b/tests/unit/cli/test_environment_prefixes.py @@ -0,0 +1,64 @@ +"""Which environment variable wins when more than one names the same setting. + +Three prefixes reach the same arguments. `ASCENDER_` is what the client +documents, `CONTROLLER_` is what it read before the rebrand, and `TOWER_` is +what awxkit read before that. A script written against any of them has to keep +working, so none of the three can simply be dropped, and the only question a +reader needs answered is which one wins when two are set at once. +""" + +import pytest + +from ascenderkit.cli.format import env_default + + +def test_the_ascender_name_wins(): + env = {'ASCENDER_HOST': 'new', 'CONTROLLER_HOST': 'middle', 'TOWER_HOST': 'old'} + assert env_default(env, 'HOST', 'fallback') == 'new' + + +def test_the_controller_name_wins_over_the_tower_one(): + env = {'CONTROLLER_HOST': 'middle', 'TOWER_HOST': 'old'} + assert env_default(env, 'HOST', 'fallback') == 'middle' + + +@pytest.mark.parametrize('prefix', ['ASCENDER_', 'CONTROLLER_', 'TOWER_']) +def test_any_one_of_the_three_on_its_own_is_read(prefix): + assert env_default({prefix + 'USERNAME': 'mary'}, 'USERNAME', 'admin') == 'mary' + + +def test_nothing_set_falls_back(): + assert env_default({}, 'USERNAME', 'admin') == 'admin' + + +def test_a_name_that_is_set_but_empty_still_wins(): + """`env.get(name, other)` returned an empty value rather than looking on, + and a deployment that exports an empty token to mean "no token" relies on + it, so the ordered lookup has to do the same. + """ + assert env_default({'ASCENDER_PASSWORD': ''}, 'PASSWORD', 'password') == '' + assert env_default({'TOWER_PASSWORD': '', 'ASCENDER_PASSWORD': 'set'}, 'PASSWORD', 'password') == 'set' + + +def test_the_oauth_token_name_is_tried_before_the_short_one(): + """Both spellings exist for the token, across all three prefixes, and the + full name is the one the login command prints. + """ + env = {'ASCENDER_OAUTH_TOKEN': 'full', 'ASCENDER_TOKEN': 'short'} + assert env_default(env, 'OAUTH_TOKEN', '', extra=('TOKEN',)) == 'full' + + +def test_a_newer_prefix_beats_a_longer_name(): + """`ASCENDER_TOKEN` is preferred over `CONTROLLER_OAUTH_TOKEN`: the prefix + says which release wrote the script, and the suffix is only a spelling. + """ + env = {'ASCENDER_TOKEN': 'short', 'CONTROLLER_OAUTH_TOKEN': 'full'} + assert env_default(env, 'OAUTH_TOKEN', '', extra=('TOKEN',)) == 'short' + + +def test_the_old_chain_keeps_its_own_order(): + """`CONTROLLER_TOKEN` came before `TOWER_OAUTH_TOKEN` before this change, + and still does, so adding a prefix on top reorders nothing below it. + """ + env = {'CONTROLLER_TOKEN': 'middle', 'TOWER_OAUTH_TOKEN': 'old'} + assert env_default(env, 'OAUTH_TOKEN', '', extra=('TOKEN',)) == 'middle' diff --git a/tests/unit/cli/test_sphinx.py b/tests/unit/cli/test_sphinx.py index 86e437f..42cec4e 100644 --- a/tests/unit/cli/test_sphinx.py +++ b/tests/unit/cli/test_sphinx.py @@ -7,11 +7,12 @@ def test_extension_imports_without_a_running_ascender(monkeypatch): """The module used to end in `parser = render()`, so importing it raised - SystemExit unless CONTROLLER_HOST pointed at a live server. Sphinx could + SystemExit unless ASCENDER_HOST pointed at a live server. Sphinx could not load the extension at all, which is why no workflow builds the docs. """ - for var in ('CONTROLLER_HOST', 'TOWER_HOST', 'CONTROLLER_USERNAME', 'TOWER_USERNAME', 'CONTROLLER_PASSWORD', 'TOWER_PASSWORD'): - monkeypatch.delenv(var, raising=False) + for prefix in ('ASCENDER_', 'CONTROLLER_', 'TOWER_'): + for suffix in ('HOST', 'USERNAME', 'PASSWORD'): + monkeypatch.delenv(prefix + suffix, raising=False) module = importlib.import_module('ascenderkit.cli.sphinx') importlib.reload(module) @@ -21,12 +22,12 @@ def test_extension_imports_without_a_running_ascender(monkeypatch): def test_asking_for_the_parser_is_what_needs_the_server(monkeypatch): - for var in ('CONTROLLER_HOST', 'TOWER_HOST'): - monkeypatch.delenv(var, raising=False) + for prefix in ('ASCENDER_', 'CONTROLLER_', 'TOWER_'): + monkeypatch.delenv(prefix + 'HOST', raising=False) module = importlib.reload(importlib.import_module('ascenderkit.cli.sphinx')) - with pytest.raises(SystemExit, match='CONTROLLER_HOST'): + with pytest.raises(SystemExit, match='ASCENDER_HOST'): module.parser