Skip to content
Merged
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
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Features
---------
* Add `--ssh-options` CLI argument to pass extra options with `--ssh-jump`.
* Make `ssh_jump` a DSN query parameter.
* Warn on unknown DSN query parameters.


Bug Fixes
Expand Down
29 changes: 27 additions & 2 deletions mycli/cli_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@

ClientFactory = Callable[..., Any]
ENV_VAR_PATTERN = re.compile(r'^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$')
KNOWN_DSN_QUERY_PARAMS = {
'character_set',
'keepalive_ticks',
'socket',
'ssh_jump',
'ssl',
'ssl_ca',
'ssl_capath',
'ssl_cert',
'ssl_cipher',
'ssl_key',
'ssl_mode',
'ssl_verify_server_cert',
'tls_version',
}


class DsnAliasEnvVarError(ValueError):
Expand Down Expand Up @@ -79,7 +94,10 @@ def expand_dsn_alias_env_vars(
except ValueError as exc:
raise DsnAliasEnvVarError(f'Port in DSN alias {alias_name} must be an integer.') from exc

params = {key: [expand_dsn_alias_env_var(value, alias_name) or '' for value in values] for key, values in parse_qs(uri.query).items()}
params = {
key: [expand_dsn_alias_env_var(value, alias_name) or '' for value in values]
for key, values in parse_qs(uri.query, keep_blank_values=True).items()
}

return (
expand_dsn_alias_env_var(unquote(username) if username is not None else None, alias_name),
Expand Down Expand Up @@ -200,7 +218,7 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
dsn_host = uri.hostname
dsn_port = uri.port
dsn_database = uri.path[1:]
dsn_params = parse_qs(uri.query) if uri.query else {}
dsn_params = parse_qs(uri.query, keep_blank_values=True) if uri.query else {}

if not database:
database = dsn_database
Expand All @@ -214,6 +232,13 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
if not cli_args.port:
cli_args.port = dsn_port

if unknown_dsn_params := sorted(set(dsn_params) - KNOWN_DSN_QUERY_PARAMS):
click.secho(
f'Warning: Ignored unknown DSN URI query parameters: {", ".join(unknown_dsn_params)}.',
err=True,
fg='yellow',
)

if params := dsn_params.get('ssl'):
click.secho(
'Warning: The "ssl" DSN URI parameter is deprecated and will be removed in a future release. '
Expand Down
79 changes: 79 additions & 0 deletions test/pytests/test_cli_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,70 @@ def test_run_from_cli_args_does_not_expand_unbraced_dsn_alias_env_vars(
assert client.connect_calls[-1]['user'] == '$MYCLI_TEST_DSN_USER'


def test_run_from_cli_args_warns_about_unknown_dsn_query_parameter(
monkeypatch: pytest.MonkeyPatch,
) -> None:
cli_args = make_cli_args()
cli_args.dsn = 'mysql://user@host/db?unknown=value'
client = DummyMyCli()
secho_calls: list[tuple[str, dict[str, Any]]] = []
monkeypatch.setattr(cli_runner.click, 'secho', lambda text, **kwargs: secho_calls.append((text, kwargs)))

run_with_client(monkeypatch, cli_args, client)

assert client.connect_calls[-1]['database'] == 'db'
assert secho_calls == [
(
'Warning: Ignored unknown DSN URI query parameters: unknown.',
{'err': True, 'fg': 'yellow'},
)
]


def test_run_from_cli_args_warns_once_about_sorted_unknown_dsn_query_parameters(
monkeypatch: pytest.MonkeyPatch,
) -> None:
cli_args = make_cli_args()
cli_args.dsn = 'mysql://user@host/db?zzz=1&aaa&character_set=utf8&aaa=2'
client = DummyMyCli()
secho_calls: list[tuple[str, dict[str, Any]]] = []
monkeypatch.setattr(cli_runner.click, 'secho', lambda text, **kwargs: secho_calls.append((text, kwargs)))

run_with_client(monkeypatch, cli_args, client)

assert client.connect_calls[-1]['character_set'] == 'utf8'
assert secho_calls == [
(
'Warning: Ignored unknown DSN URI query parameters: aaa, zzz.',
{'err': True, 'fg': 'yellow'},
)
]


def test_run_from_cli_args_warns_about_unknown_alias_dsn_query_parameter_with_env_expansion(
monkeypatch: pytest.MonkeyPatch,
) -> None:
cli_args = make_cli_args()
cli_args.dsn = 'prod'
monkeypatch.setenv('MYCLI_TEST_DSN_CHARSET', 'utf8')
config = default_config()
config['main'] = {**config['main'], 'expand_dsn_alias_env_vars': 'true'}
config['alias_dsn'] = {'prod': 'mysql://user@host/db?unknown=value&character_set=${MYCLI_TEST_DSN_CHARSET}'}
client = DummyMyCli(config=config)
secho_calls: list[tuple[str, dict[str, Any]]] = []
monkeypatch.setattr(cli_runner.click, 'secho', lambda text, **kwargs: secho_calls.append((text, kwargs)))

run_with_client(monkeypatch, cli_args, client)

assert client.connect_calls[-1]['character_set'] == 'utf8'
assert secho_calls == [
(
'Warning: Ignored unknown DSN URI query parameters: unknown.',
{'err': True, 'fg': 'yellow'},
)
]


def test_run_from_cli_args_reports_missing_dsn_alias_env_var(
monkeypatch: pytest.MonkeyPatch,
) -> None:
Expand Down Expand Up @@ -380,6 +444,21 @@ def test_run_from_cli_args_maps_dsn_ssh_jump_parameter(monkeypatch: pytest.Monke
assert client.connect_calls[-1]['ssh_jump'] == 'bastion'


def test_run_from_cli_args_does_not_warn_about_known_dsn_query_parameter(
monkeypatch: pytest.MonkeyPatch,
) -> None:
cli_args = make_cli_args()
cli_args.dsn = 'mysql://user@host/db?ssh_jump=bastion'
client = DummyMyCli()
secho_calls: list[tuple[str, dict[str, Any]]] = []
monkeypatch.setattr(cli_runner.click, 'secho', lambda text, **kwargs: secho_calls.append((text, kwargs)))

run_with_client(monkeypatch, cli_args, client)

assert client.connect_calls[-1]['ssh_jump'] == 'bastion'
assert secho_calls == []


def test_run_from_cli_args_prefers_cli_ssh_jump_over_dsn_parameter(
monkeypatch: pytest.MonkeyPatch,
) -> None:
Expand Down
Loading