Skip to content

Commit 9add48f

Browse files
committed
SNOW-2306184: config refactor - improve resolution raport
1 parent 8e0bea0 commit 9add48f

File tree

4 files changed

+236
-36
lines changed

4 files changed

+236
-36
lines changed

src/snowflake/cli/_plugins/helpers/commands.py

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -375,10 +375,12 @@ def show_config_sources(
375375
Set SNOWFLAKE_CLI_CONFIG_V2_ENABLED=true to enable it.
376376
"""
377377
from snowflake.cli.api.config_ng import (
378-
explain_configuration,
379378
export_resolution_history,
380379
is_resolution_logging_available,
381380
)
381+
from snowflake.cli.api.config_ng.resolution_logger import (
382+
get_configuration_explanation_results,
383+
)
382384

383385
if not is_resolution_logging_available():
384386
return MessageResult(
@@ -401,18 +403,4 @@ def show_config_sources(
401403
f"and can be attached to support tickets."
402404
)
403405

404-
# Show resolution information
405-
explain_configuration(key=key, verbose=show_details)
406-
407-
if key:
408-
return MessageResult(
409-
f"\n✅ Showing resolution for key: {key}\n"
410-
f"Use --show-details to see the complete resolution chain."
411-
)
412-
else:
413-
return MessageResult(
414-
"\n✅ Configuration resolution summary displayed above.\n"
415-
"Use a specific key (e.g., 'snow helpers show-config-sources account') "
416-
"to see detailed resolution for that key.\n"
417-
"Use --show-details to see complete resolution chains for all keys."
418-
)
406+
return get_configuration_explanation_results(key=key, verbose=show_details)

src/snowflake/cli/api/config_ng/resolution_logger.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@
3131
get_config_provider_singleton,
3232
)
3333
from snowflake.cli.api.console import cli_console
34+
from snowflake.cli.api.output.types import (
35+
CollectionResult,
36+
CommandResult,
37+
MessageResult,
38+
MultipleResults,
39+
)
3440

3541
if TYPE_CHECKING:
3642
from snowflake.cli.api.config_ng.resolver import ConfigurationResolver
@@ -304,3 +310,35 @@ def explain_configuration(key: Optional[str] = None, verbose: bool = False) -> N
304310

305311
if verbose:
306312
resolver.print_all_chains()
313+
314+
315+
def get_configuration_explanation_results(
316+
key: Optional[str] = None, verbose: bool = False
317+
) -> CommandResult:
318+
"""
319+
Build CommandResult(s) representing a fixed-column sources table and optional
320+
masked history message, suitable for Snow's output formats.
321+
322+
Returns:
323+
- CollectionResult for the table (always)
324+
- If verbose is True, MultipleResults with the table and a MessageResult
325+
containing the masked resolution history (for the key or all keys)
326+
"""
327+
from snowflake.cli.api.config_provider import get_config_provider_singleton
328+
329+
provider = get_config_provider_singleton()
330+
provider.read_config()
331+
332+
resolver = get_resolver()
333+
if resolver is None:
334+
return MessageResult(
335+
"Configuration resolution logging is not available. "
336+
f"Set {ALTERNATIVE_CONFIG_ENV_VAR}=true to enable it."
337+
)
338+
339+
table_result: CollectionResult = resolver.build_sources_table(key)
340+
if not verbose:
341+
return table_result
342+
343+
history_message: MessageResult = resolver.format_history_message(key)
344+
return MultipleResults([table_result, history_message])

src/snowflake/cli/api/config_ng/resolver.py

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@
2727
from collections import defaultdict
2828
from datetime import datetime
2929
from pathlib import Path
30-
from typing import TYPE_CHECKING, Any, Dict, List, Optional
30+
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple
3131

3232
from snowflake.cli.api.config_ng.core import (
3333
ConfigValue,
3434
ResolutionEntry,
3535
ResolutionHistory,
3636
)
3737
from snowflake.cli.api.console import cli_console
38+
from snowflake.cli.api.output.types import CollectionResult, MessageResult
3839

3940
if TYPE_CHECKING:
4041
from snowflake.cli.api.config_ng.core import ValueSource
@@ -62,6 +63,40 @@
6263
"token_file_path",
6364
}
6465

66+
# Fixed table columns ordered from most important (left) to least (right)
67+
SourceColumn = Literal[
68+
"params",
69+
"global_envs",
70+
"connections_env",
71+
"snowsql_env",
72+
"connections.toml",
73+
"config.toml",
74+
"snowsql",
75+
]
76+
77+
TABLE_COLUMNS: Tuple[str, ...] = (
78+
"key",
79+
"value",
80+
"params",
81+
"global_envs",
82+
"connections_env",
83+
"snowsql_env",
84+
"connections.toml",
85+
"config.toml",
86+
"snowsql",
87+
)
88+
89+
# Mapping of internal source names to fixed table columns
90+
SOURCE_TO_COLUMN: Dict[str, SourceColumn] = {
91+
"cli_arguments": "params",
92+
"cli_env": "global_envs",
93+
"connection_specific_env": "connections_env",
94+
"snowsql_env": "snowsql_env",
95+
"connections_toml": "connections.toml",
96+
"cli_config_toml": "config.toml",
97+
"snowsql_config": "snowsql",
98+
}
99+
65100

66101
def _should_mask_value(key: str) -> bool:
67102
"""
@@ -508,6 +543,104 @@ def get_history_summary(self) -> dict:
508543
"""
509544
return self._history_tracker.get_summary()
510545

546+
def build_sources_table(self, key: Optional[str] = None) -> CollectionResult:
547+
"""
548+
Build a tabular view of configuration sources per key.
549+
550+
Columns (left to right): key, value, params, env, connections.toml, cli_config.toml, snowsql.
551+
- value: masked final selected value for the key
552+
- presence columns: "+" if a given source provided a value for the key, empty otherwise
553+
"""
554+
# Ensure history is populated
555+
if key is None and not self._history_tracker.get_all_histories():
556+
# Resolve all keys to populate history
557+
self.resolve()
558+
elif key is not None and self._history_tracker.get_history(key) is None:
559+
# Resolve only the specific key
560+
self.resolve(key=key)
561+
562+
histories = (
563+
{key: self._history_tracker.get_history(key)}
564+
if key is not None
565+
else self._history_tracker.get_all_histories()
566+
)
567+
568+
def _row_items():
569+
for k, history in histories.items():
570+
if history is None:
571+
continue
572+
# Initialize row with fixed columns
573+
row: Dict[str, Any] = {c: "" for c in TABLE_COLUMNS}
574+
row["key"] = k
575+
576+
# Final value (masked)
577+
masked_final = _mask_sensitive_value(k, history.final_value)
578+
row["value"] = masked_final
579+
580+
# Mark presence per source
581+
for entry in history.entries:
582+
source_column = SOURCE_TO_COLUMN.get(entry.config_value.source_name)
583+
if source_column is not None:
584+
row[source_column] = "+"
585+
586+
# Ensure result preserves the column order
587+
ordered_row = {column: row[column] for column in TABLE_COLUMNS}
588+
yield ordered_row
589+
590+
return CollectionResult(_row_items())
591+
592+
def format_history_message(self, key: Optional[str] = None) -> MessageResult:
593+
"""
594+
Build a masked, human-readable history of merging as a single message.
595+
If key is None, returns concatenated histories for all keys.
596+
"""
597+
histories = (
598+
{key: self.get_resolution_history(key)}
599+
if key is not None
600+
else self.get_all_histories()
601+
)
602+
603+
if not histories:
604+
return MessageResult("No resolution history available")
605+
606+
lines: List[str] = []
607+
for k in sorted(histories.keys()):
608+
history = histories[k]
609+
if history is None:
610+
continue
611+
lines.append(f"{k} resolution chain ({len(history.entries)} sources):")
612+
for i, entry in enumerate(history.entries, 1):
613+
cv = entry.config_value
614+
status_text = (
615+
"(SELECTED)"
616+
if entry.was_used
617+
else (
618+
f"(overridden by {entry.overridden_by})"
619+
if entry.overridden_by
620+
else "(not used)"
621+
)
622+
)
623+
624+
masked_value = _mask_sensitive_value(cv.key, cv.value)
625+
masked_raw = (
626+
_mask_sensitive_value(cv.key, cv.raw_value)
627+
if cv.raw_value is not None
628+
else None
629+
)
630+
value_display = f'"{masked_value}"'
631+
if masked_raw is not None and cv.raw_value != cv.value:
632+
value_display = f'"{masked_raw}" → {masked_value}'
633+
634+
lines.append(f" {i}. {cv.source_name}: {value_display} {status_text}")
635+
636+
if history.default_used:
637+
masked_default = _mask_sensitive_value(k, history.final_value)
638+
lines.append(f" Default value used: {masked_default}")
639+
640+
lines.append("")
641+
642+
return MessageResult("\n".join(lines).rstrip())
643+
511644
def format_resolution_chain(self, key: str) -> str:
512645
"""
513646
Format the resolution chain for a key (debugging helper).

tests/helpers/test_show_config_sources.py

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -71,65 +71,106 @@ def test_command_unavailable_message_when_logging_not_available(
7171

7272
@mock.patch.dict(os.environ, {ALTERNATIVE_CONFIG_ENV_VAR: "1"}, clear=True)
7373
@mock.patch("snowflake.cli.api.config_ng.is_resolution_logging_available")
74-
@mock.patch("snowflake.cli.api.config_ng.explain_configuration")
74+
@mock.patch(
75+
"snowflake.cli.api.config_ng.resolution_logger.get_configuration_explanation_results"
76+
)
7577
def test_command_shows_summary_without_arguments(
76-
self, mock_explain, mock_is_available, runner
78+
self, mock_get_results, mock_is_available, runner
7779
):
7880
"""Command should show configuration summary when called without arguments."""
81+
from snowflake.cli.api.output.types import CollectionResult
82+
7983
mock_is_available.return_value = True
84+
mock_get_results.return_value = CollectionResult([])
8085
result = runner.invoke(["helpers", COMMAND])
8186
assert result.exit_code == 0
82-
mock_explain.assert_called_once_with(key=None, verbose=False)
83-
assert "Configuration resolution summary displayed above" in result.output
87+
mock_get_results.assert_called_once_with(key=None, verbose=False)
8488

8589
@mock.patch.dict(os.environ, {ALTERNATIVE_CONFIG_ENV_VAR: "1"}, clear=True)
8690
@mock.patch("snowflake.cli.api.config_ng.is_resolution_logging_available")
87-
@mock.patch("snowflake.cli.api.config_ng.explain_configuration")
88-
def test_command_shows_specific_key(self, mock_explain, mock_is_available, runner):
91+
@mock.patch(
92+
"snowflake.cli.api.config_ng.resolution_logger.get_configuration_explanation_results"
93+
)
94+
def test_command_shows_specific_key(
95+
self, mock_get_results, mock_is_available, runner
96+
):
8997
"""Command should show resolution for specific key when provided."""
98+
from snowflake.cli.api.output.types import CollectionResult
99+
90100
mock_is_available.return_value = True
101+
mock_get_results.return_value = CollectionResult([])
91102
result = runner.invoke(["helpers", COMMAND, "account"])
92103
assert result.exit_code == 0
93-
mock_explain.assert_called_once_with(key="account", verbose=False)
94-
assert "Showing resolution for key: account" in result.output
104+
mock_get_results.assert_called_once_with(key="account", verbose=False)
95105

96106
@mock.patch.dict(os.environ, {ALTERNATIVE_CONFIG_ENV_VAR: "1"}, clear=True)
97107
@mock.patch("snowflake.cli.api.config_ng.is_resolution_logging_available")
98-
@mock.patch("snowflake.cli.api.config_ng.explain_configuration")
108+
@mock.patch(
109+
"snowflake.cli.api.config_ng.resolution_logger.get_configuration_explanation_results"
110+
)
99111
def test_command_shows_details_with_flag(
100-
self, mock_explain, mock_is_available, runner
112+
self, mock_get_results, mock_is_available, runner
101113
):
102114
"""Command should show detailed resolution when --show-details flag is used."""
115+
from snowflake.cli.api.output.types import (
116+
CollectionResult,
117+
MessageResult,
118+
MultipleResults,
119+
)
120+
103121
mock_is_available.return_value = True
122+
mock_get_results.return_value = MultipleResults(
123+
[CollectionResult([]), MessageResult("test history")]
124+
)
104125
result = runner.invoke(["helpers", COMMAND, "--show-details"])
105126
assert result.exit_code == 0
106-
mock_explain.assert_called_once_with(key=None, verbose=True)
107-
assert "Configuration resolution summary displayed above" in result.output
127+
mock_get_results.assert_called_once_with(key=None, verbose=True)
108128

109129
@mock.patch.dict(os.environ, {ALTERNATIVE_CONFIG_ENV_VAR: "1"}, clear=True)
110130
@mock.patch("snowflake.cli.api.config_ng.is_resolution_logging_available")
111-
@mock.patch("snowflake.cli.api.config_ng.explain_configuration")
131+
@mock.patch(
132+
"snowflake.cli.api.config_ng.resolution_logger.get_configuration_explanation_results"
133+
)
112134
def test_command_shows_details_with_short_flag(
113-
self, mock_explain, mock_is_available, runner
135+
self, mock_get_results, mock_is_available, runner
114136
):
115137
"""Command should show detailed resolution when -d flag is used."""
138+
from snowflake.cli.api.output.types import (
139+
CollectionResult,
140+
MessageResult,
141+
MultipleResults,
142+
)
143+
116144
mock_is_available.return_value = True
145+
mock_get_results.return_value = MultipleResults(
146+
[CollectionResult([]), MessageResult("test history")]
147+
)
117148
result = runner.invoke(["helpers", COMMAND, "-d"])
118149
assert result.exit_code == 0
119-
mock_explain.assert_called_once_with(key=None, verbose=True)
150+
mock_get_results.assert_called_once_with(key=None, verbose=True)
120151

121152
@mock.patch.dict(os.environ, {ALTERNATIVE_CONFIG_ENV_VAR: "1"}, clear=True)
122153
@mock.patch("snowflake.cli.api.config_ng.is_resolution_logging_available")
123-
@mock.patch("snowflake.cli.api.config_ng.explain_configuration")
154+
@mock.patch(
155+
"snowflake.cli.api.config_ng.resolution_logger.get_configuration_explanation_results"
156+
)
124157
def test_command_shows_key_with_details(
125-
self, mock_explain, mock_is_available, runner
158+
self, mock_get_results, mock_is_available, runner
126159
):
127160
"""Command should show detailed resolution for specific key."""
161+
from snowflake.cli.api.output.types import (
162+
CollectionResult,
163+
MessageResult,
164+
MultipleResults,
165+
)
166+
128167
mock_is_available.return_value = True
168+
mock_get_results.return_value = MultipleResults(
169+
[CollectionResult([]), MessageResult("test history")]
170+
)
129171
result = runner.invoke(["helpers", COMMAND, "user", "--show-details"])
130172
assert result.exit_code == 0
131-
mock_explain.assert_called_once_with(key="user", verbose=True)
132-
assert "Showing resolution for key: user" in result.output
173+
mock_get_results.assert_called_once_with(key="user", verbose=True)
133174

134175
@mock.patch.dict(os.environ, {ALTERNATIVE_CONFIG_ENV_VAR: "1"}, clear=True)
135176
@mock.patch("snowflake.cli.api.config_ng.is_resolution_logging_available")

0 commit comments

Comments
 (0)