Skip to content
Open
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
26 changes: 15 additions & 11 deletions airflow-core/src/airflow/cli/commands/config_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@
from typing import Any, NamedTuple

import pygments
from airflowctl.api.operations import ServerResponseError
from pygments.lexers.configs import IniLexer

from airflow.cli.api_client import NEW_API_CLIENT, Client, provide_api_client
from airflow.cli.simple_table import AirflowConsole
from airflow.cli.utils import deprecated_for_airflowctl
from airflow.configuration import AIRFLOW_CONFIG, ConfigModifications, conf
from airflow.exceptions import AirflowConfigException
from airflow.utils.cli import should_use_colors
from airflow.utils.cli import should_use_colors, suppress_logs_and_warning
from airflow.utils.code_utils import get_terminal_formatter
from airflow.utils.providers_configuration_loader import providers_configuration_loaded

Expand Down Expand Up @@ -63,19 +66,20 @@ def show_config(args):
print(code)


@deprecated_for_airflowctl("airflowctl config get")
@suppress_logs_and_warning
@providers_configuration_loaded
def get_value(args):
@provide_api_client
def get_value(args, api_client: Client = NEW_API_CLIENT):
"""Get one value from configuration."""
# while this will make get_value quite a bit slower we must initialize configuration
# for providers because we do not know what sections and options will be available after
# providers are initialized. Theoretically Providers might add new sections and options
# but also override defaults for existing options, so without loading all providers we
# cannot be sure what is the final value of the option.
try:
value = conf.get(args.section, args.option)
print(value)
except AirflowConfigException:
pass
config = api_client.configs.get(section=args.section, option=args.option)
except ServerResponseError as e:
# Preserve the historical behaviour of staying silent when the option is unknown.
if e.response.status_code == 404:
return
raise
print(config.sections[0].options[0].value)


class ConfigParameter(NamedTuple):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

import pytest

from airflow.cli.commands import asset_command, dag_command, pool_command
from airflow.cli.commands import asset_command, config_command, dag_command, pool_command
from airflow.exceptions import RemovedInAirflow4Warning

# (command callable, argv to parse, expected airflowctl replacement named in the warning)
Expand All @@ -52,6 +52,7 @@
["assets", "materialize", "--name=foo"],
"airflowctl assets materialize",
),
(config_command.get_value, ["config", "get-value", "core", "executor"], "airflowctl config get"),
]


Expand Down
44 changes: 28 additions & 16 deletions airflow-core/tests/unit/cli/commands/test_config_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
import shutil
from unittest import mock

import httpx
import pytest
from airflowctl.api.datamodels.generated import Config, ConfigOption, ConfigSection
from airflowctl.api.operations import ServerResponseError

from airflow.cli import cli_parser
from airflow.cli.commands import config_command
Expand All @@ -33,6 +36,12 @@
STATSD_CONFIG_BEGIN_WITH = "# `StatsD <https://github.com/statsd/statsd>`"


def _server_error(status_code: int) -> ServerResponseError:
request = httpx.Request("GET", "http://testserver/api/v2/config/section/core/option/executor")
response = httpx.Response(status_code, request=request, json={"detail": "boom"})
return ServerResponseError(message="boom", request=request, response=response)


class TestCliConfigList:
@classmethod
def setup_class(cls):
Expand Down Expand Up @@ -245,29 +254,32 @@ class TestCliConfigGetValue:
def setup_class(cls):
cls.parser = cli_parser.get_parser()

@conf_vars({("core", "test_key"): "test_value"})
def test_should_display_value(self, stdout_capture):
def test_should_display_value(self, mock_cli_api_client, stdout_capture):
mock_cli_api_client.configs.get.return_value = Config(
sections=[ConfigSection(name="core", options=[ConfigOption(key="test_key", value="test_value")])]
)

with stdout_capture as temp_stdout:
config_command.get_value(self.parser.parse_args(["config", "get-value", "core", "test_key"]))

assert temp_stdout.getvalue().strip() == "test_value"
mock_cli_api_client.configs.get.assert_called_once_with(section="core", option="test_key")

@mock.patch("airflow.cli.commands.config_command.conf")
def test_should_not_raise_exception_when_section_for_config_with_value_defined_elsewhere_is_missing(
self, mock_conf
):
# no section in config
mock_conf.has_section.return_value = False
# pretend that the option is defined by other means
mock_conf.has_option.return_value = True
def test_should_be_silent_when_option_is_missing(self, mock_cli_api_client, stdout_capture):
mock_cli_api_client.configs.get.side_effect = _server_error(404)

config_command.get_value(self.parser.parse_args(["config", "get-value", "some_section", "value"]))
with stdout_capture as temp_stdout:
config_command.get_value(
self.parser.parse_args(["config", "get-value", "missing-section", "dags_folder"])
)

def test_should_raise_exception_when_option_is_missing(self, caplog):
config_command.get_value(
self.parser.parse_args(["config", "get-value", "missing-section", "dags_folder"])
)
assert "section/key [missing-section/dags_folder] not found in config" in caplog.text
assert temp_stdout.getvalue() == ""

def test_should_reraise_non_404_error(self, mock_cli_api_client):
mock_cli_api_client.configs.get.side_effect = _server_error(403)

with pytest.raises(ServerResponseError):
config_command.get_value(self.parser.parse_args(["config", "get-value", "core", "executor"]))


class TestConfigLint:
Expand Down
Loading