Skip to content
Draft
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 RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
* Fixed `SELECT *` output being corrupted when joined tables share column names. Duplicate column names are now disambiguated by appending a numeric suffix (e.g. `NAME`, `NAME_2`).
* Fixed `snow connection generate-jwt` and `snow connection generate-workload-identity-token` failing with `Connection None is not configured` when used with `--temporary-connection`.
* The internal connection cache now remembers failed connect attempts and re-raises the original exception on subsequent accesses within the same process, instead of re-dialing Snowflake every time a command accesses the shared connection. This fixes, among other cases, the customer-visible duplicate `LOGIN_HISTORY` events (and `OVERFLOW_FAILURE_EVENTS_ELIDED`) previously emitted when a `snow` invocation was rejected by an authentication policy.
* `snow connection list` (and other commands that enumerate connections) now raises a clear, actionable error when the `[connections]` section of `config.toml` contains an entry that is not a table (for example, a stray `default = "something"` line at the top of the section). Previously these configurations produced an unhandled `AttributeError: 'String' object has no attribute 'items'` with no indication of which entry was wrong.


# v3.17.0
Expand Down
15 changes: 11 additions & 4 deletions src/snowflake/cli/api/config_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,14 +136,21 @@ def get_connection_dict(self, connection_name: str) -> dict:
)

def get_all_connections(self, include_env_connections: bool = False) -> dict:
from click import ClickException
from snowflake.cli.api.config import ConnectionConfig, get_config_section

# Legacy provider ignores the flag since it never had env connections
connections = get_config_section("connections")
return {
name: ConnectionConfig.from_dict(config)
for name, config in connections.items()
}
result = {}
for name, config in connections.items():
if not isinstance(config, dict):
raise ClickException(
f"Connection '{name}' in the configuration file is malformed: "
f"expected a table of connection parameters but found a {type(config).__name__}. "
f"Each connection must be defined as [connections.{name}] with key = value entries."
)
result[name] = ConnectionConfig.from_dict(config)
return result


class AlternativeConfigProvider(ConfigProvider):
Expand Down
26 changes: 26 additions & 0 deletions tests/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,32 @@ def test_mask_sensitive_parameters_masks_all_known_sensitive_keys():
assert params["password"] == "hunter2"


@mock.patch.dict(os.environ, {}, clear=True)
def test_connection_list_reports_malformed_connection_entry(runner):
with NamedTemporaryFile("w+", suffix=".toml") as tmp_file:
tmp_file.write(
dedent(
"""\
[connections]
default = "conn"

[connections.conn]
user = "foo"
"""
)
)
tmp_file.flush()
os.chmod(tmp_file.name, 0o600)
result = runner.invoke_with_config_file(
tmp_file.name, ["connection", "list", "--format", "json"]
)

assert result.exit_code != 0, result.output
assert "malformed" in result.output
assert "default" in result.output
assert "[connections.default]" in result.output


@mock.patch.dict(
os.environ,
{
Expand Down
Loading