diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py b/airflow-ctl/src/airflowctl/ctl/cli_config.py index 787a53ce074b6..019645ac9b291 100755 --- a/airflow-ctl/src/airflowctl/ctl/cli_config.py +++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py @@ -357,27 +357,27 @@ def _load_help_texts_yaml() -> dict[str, dict[str, str]]: # Config arguments ARG_CONFIG_SECTION = Arg( flags=("--section",), - type=str, + type=string_list_type, dest="section", - help="The section of the configuration", + help="The section name(s) of the configuration, comma separated", ) ARG_CONFIG_OPTION = Arg( flags=("--option",), - type=str, + type=string_list_type, dest="option", - help="The option of the configuration", + help="The option name(s) of the configuration, comma separated", ) ARG_CONFIG_IGNORE_SECTION = Arg( flags=("--ignore-section",), - type=str, + type=string_list_type, dest="ignore_section", - help="The configuration section being ignored", + help="The configuration section name(s) being ignored, comma separated", ) ARG_CONFIG_IGNORE_OPTION = Arg( flags=("--ignore-option",), - type=str, + type=string_list_type, dest="ignore_option", - help="The configuration option being ignored", + help="The configuration option name(s) being ignored, comma separated", ) ARG_CONFIG_VERBOSE = Arg( flags=( diff --git a/airflow-ctl/src/airflowctl/ctl/commands/config_command.py b/airflow-ctl/src/airflowctl/ctl/commands/config_command.py index b83600697ad37..8d93d0393de04 100644 --- a/airflow-ctl/src/airflowctl/ctl/commands/config_command.py +++ b/airflow-ctl/src/airflowctl/ctl/commands/config_command.py @@ -733,20 +733,20 @@ def lint(args, api_client=NEW_API_CLIENT) -> None: This function scans the Airflow configuration file for parameters that are removed or renamed in Airflow 3.0. It provides suggestions for alternative parameters or settings where applicable. CLI Arguments: - --section: str (optional) - The specific section of the configuration to lint. + --section: comma separated list (optional) + The specific section name(s) of the configuration to lint. Example: --section core - --option: str (optional) - The specific option within a section to lint. + --option: comma separated list (optional) + The specific option name(s) within a section to lint. Example: --option check_slas - --ignore-section: str (optional) - A section to ignore during linting. + --ignore-section: comma separated list (optional) + The section name(s) to ignore during linting. Example: --ignore-section webserver - --ignore-option: str (optional) - An option to ignore during linting. + --ignore-option: comma separated list (optional) + The option name(s) to ignore during linting. Example: --ignore-option smtp_user --verbose: flag (optional) diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_config_command.py b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_config_command.py index 77705a4c63cc8..d5a8f21e0dc72 100644 --- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_config_command.py +++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_config_command.py @@ -27,10 +27,50 @@ from airflowctl.ctl.commands import config_command from airflowctl.ctl.commands.config_command import ConfigChange, ConfigParameter +# Section and option names where one name is a prefix of another, mirroring real pairs in +# CONFIGS_CHANGES such as auth_backend/auth_backends and sql_alchemy_conn/sql_alchemy_connect_args. +CONFUSABLE_CONFIGS_CHANGES = [ + ConfigChange(config=ConfigParameter("api", "auth_backend")), + ConfigChange(config=ConfigParameter("api_auth", "auth_backends")), + ConfigChange(config=ConfigParameter("database", "sql_alchemy_conn")), +] + +CONFUSABLE_CONFIG_RESPONSE = Config( + sections=[ + ConfigSection(name=change.config.section, options=[ConfigOption(key=change.config.option, value="x")]) + for change in CONFUSABLE_CONFIGS_CHANGES + ] +) + + +def get_printed_lines(mock_rich_print) -> list[str]: + """Return the strings lint passed to rich.print.""" + return [call.args[0] for call in mock_rich_print.call_args_list] + + +def find_reported_options(mock_rich_print) -> list[str]: + """Return the CONFUSABLE_CONFIGS_CHANGES option names that lint reported, sorted.""" + printed = get_printed_lines(mock_rich_print) + return sorted( + change.config.option + for change in CONFUSABLE_CONFIGS_CHANGES + # Backticks delimit the name in the message, so `auth_backend` does not match `auth_backends`. + if any(f"`{change.config.option}` configuration parameter" in line for line in printed) + ) + class TestCliConfigCommands: parser = cli_parser.get_parser() + @pytest.fixture + def confusable_api_client(self, api_client_maker): + return api_client_maker( + path="/api/v2/config", + response_json=CONFUSABLE_CONFIG_RESPONSE.model_dump(), + expected_http_status_code=200, + kind=ClientKind.CLI, + ) + @patch("rich.print") def test_lint_no_issues(self, mock_rich_print, api_client_maker): response_config = Config( @@ -429,6 +469,67 @@ def test_lint_detects_configs_with_suggestions(self, mock_rich_print, api_client assert "[red]Found issues in your airflow.cfg:[/red]" in calls[0] assert "This is a test suggestion." in calls[1] + @pytest.mark.parametrize( + ("lint_args", "expected_options"), + [ + pytest.param( + ["--option", "auth_backends"], + ["auth_backends"], + id="option-matches-whole-name-not-prefix", + ), + pytest.param( + ["--option", "auth_backends,sql_alchemy_conn"], + ["auth_backends", "sql_alchemy_conn"], + id="option-comma-separated-list", + ), + pytest.param( + ["--ignore-option", "auth_backends"], + ["auth_backend", "sql_alchemy_conn"], + id="ignore-option-matches-whole-name-not-prefix", + ), + pytest.param( + ["--section", "api_auth"], + ["auth_backends"], + id="section-matches-whole-name-not-prefix", + ), + pytest.param( + ["--section", "api_auth,database"], + ["auth_backends", "sql_alchemy_conn"], + id="section-comma-separated-list", + ), + pytest.param( + ["--ignore-section", "api_auth"], + ["auth_backend", "sql_alchemy_conn"], + id="ignore-section-matches-whole-name-not-prefix", + ), + ], + ) + @patch("rich.print") + @patch("airflowctl.ctl.commands.config_command.CONFIGS_CHANGES", CONFUSABLE_CONFIGS_CHANGES) + def test_lint_filters_match_whole_names( + self, mock_rich_print, lint_args, expected_options, confusable_api_client + ): + config_command.lint( + self.parser.parse_args(["config", "lint", *lint_args]), + api_client=confusable_api_client, + ) + + assert find_reported_options(mock_rich_print) == expected_options + + @patch("rich.print") + @patch("airflowctl.ctl.commands.config_command.CONFIGS_CHANGES", CONFUSABLE_CONFIGS_CHANGES) + def test_lint_verbose_lists_ignored_names(self, mock_rich_print, confusable_api_client): + config_command.lint( + self.parser.parse_args( + ["config", "lint", "--ignore-section", "api", "--ignore-option", "auth_backend", "--verbose"] + ), + api_client=confusable_api_client, + ) + + printed = get_printed_lines(mock_rich_print) + assert "Ignored sections: [green]api[/green]" in printed + assert "Ignored options: [green]auth_backend[/green]" in printed + @patch("airflowctl.api.client.Credentials.load") @patch.dict(os.environ, {"AIRFLOW_CLI_TOKEN": "TEST_TOKEN"}) @patch.dict(os.environ, {"AIRFLOW_CLI_ENVIRONMENT": "TEST_CONFIG"})