diff --git a/airflow-core/src/airflow/cli/commands/config_command.py b/airflow-core/src/airflow/cli/commands/config_command.py index 5f087fdfa4c8b..e762665ed07c3 100644 --- a/airflow-core/src/airflow/cli/commands/config_command.py +++ b/airflow-core/src/airflow/cli/commands/config_command.py @@ -933,8 +933,8 @@ def update_config(args) -> None: the breaking configuration changes by scanning the current configuration file for parameters that have been renamed, removed, or had their default values changed in Airflow 3.0. To see or fix all recommended changes, use the --all-recommendations argument. To automatically update your airflow.cfg file, use - the --fix argument. This command cleans up the existing comments in airflow.cfg but creates a backup of - the old airflow.cfg file. + the --fix argument. Applying --fix cleans up the existing comments in airflow.cfg, so a backup of the + old airflow.cfg file is written first. A dry-run leaves the filesystem untouched. CLI Arguments: --fix: flag (optional) @@ -1058,14 +1058,6 @@ def update_config(args) -> None: modifications.add_remove(conf_section, conf_option) changes_applied.append(f"{prefix} Removed '{conf_section}/{conf_option}' from configuration.") - backup_path = f"{AIRFLOW_CONFIG}.bak" - try: - shutil.copy2(AIRFLOW_CONFIG, backup_path) - console.print(f"Backup saved as '{backup_path}'.") - except Exception as e: - console.print(f"Failed to create backup: {e}") - raise AirflowConfigException("Backup creation failed. Aborting update_config operation.") - if dry_run: console.print("[blue]Dry-run mode enabled. No changes will be written to airflow.cfg.[/blue]") with StringIO() as config_output: @@ -1078,6 +1070,14 @@ def update_config(args) -> None: new_config = config_output.getvalue() console.print(new_config) else: + backup_path = f"{AIRFLOW_CONFIG}.bak" + try: + shutil.copy2(AIRFLOW_CONFIG, backup_path) + console.print(f"Backup saved as '{backup_path}'.") + except Exception as e: + console.print(f"Failed to create backup: {e}") + raise AirflowConfigException("Backup creation failed. Aborting update_config operation.") + with open(AIRFLOW_CONFIG, "w") as config_file: conf.write_custom_config( file=config_file, diff --git a/airflow-core/tests/unit/cli/commands/test_config_command.py b/airflow-core/tests/unit/cli/commands/test_config_command.py index ecbb31fbb1900..537267b452211 100644 --- a/airflow-core/tests/unit/cli/commands/test_config_command.py +++ b/airflow-core/tests/unit/cli/commands/test_config_command.py @@ -565,6 +565,29 @@ def fake_write_custom_config(file, **kwargs): current_cfg = cfg_file.read_text() assert initial_config in current_cfg, "Dry-run should not modify the config file." + @conf_vars({("core", "executor"): "SequentialExecutor"}) + def test_update_config_dry_run_does_not_touch_filesystem(self, tmp_path, monkeypatch, capsys): + cfg_file = tmp_path / "airflow.cfg" + cfg_file.write_text("[core]\nexecutor = SequentialExecutor\n") + + monkeypatch.setattr(config_command, "AIRFLOW_CONFIG", str(cfg_file)) + monkeypatch.setattr(conf, "write_custom_config", lambda file, **kwargs: file.write("preview_config")) + + def read_only_copy2(src, dst): + raise OSError("Read-only file system") + + monkeypatch.setattr(shutil, "copy2", read_only_copy2) + + parser = cli_parser.get_parser() + args = parser.parse_args(["config", "update", "--all-recommendations"]) + + config_command.update_config(args) + + output = capsys.readouterr().out + assert "preview_config" in output + assert "Backup saved as" not in output + assert not (tmp_path / "airflow.cfg.bak").exists() + @conf_vars({("core", "executor"): "SequentialExecutor"}) def test_update_config_all_options_fix(self, tmp_path, monkeypatch, capsys): cfg_file = tmp_path / "airflow.cfg"