This is an automated email from the ASF dual-hosted git repository.
henry3260 pushed a commit to branch airflow-ctl/v0-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/airflow-ctl/v0-1-test by this
push:
new 6abeae2e774 [airflow-ctl/v0-1-test] Fix airflowctl pools export/import
dropping pool team ownership (#73132) (#74846)
6abeae2e774 is described below
commit 6abeae2e77449366726cc9600f432ed5bb829ebf
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Oct 16 16:54:35 2026 +0800
[airflow-ctl/v0-1-test] Fix airflowctl pools export/import dropping pool
team ownership (#73132) (#74846)
Multi-team deployments assign pools to a team via team_name. Backing pools
up and restoring them silently lost that ownership: the exported file never
carried the field, and an import with --action-on-existing-key overwrite
sent it as null, which the server then wrote back over the existing pool.
The deprecated airflow pools export/import that this command supersedes does
round-trip team_name, so the gap only bites users who follow the migration
notice onto airflowctl.
(cherry picked from commit 91a8ab6950a1abc53828716b8826d671659cebf1)
Co-authored-by: Y-C <[email protected]>
Co-authored-by: Eason09053360
<[email protected]>
---
.../src/airflowctl/ctl/commands/pool_command.py | 2 +
.../airflow_ctl/ctl/commands/test_pool_command.py | 45 ++++++++++++++++++++++
2 files changed, 47 insertions(+)
diff --git a/airflow-ctl/src/airflowctl/ctl/commands/pool_command.py
b/airflow-ctl/src/airflowctl/ctl/commands/pool_command.py
index 08e56eed87b..8815827ab6f 100644
--- a/airflow-ctl/src/airflowctl/ctl/commands/pool_command.py
+++ b/airflow-ctl/src/airflowctl/ctl/commands/pool_command.py
@@ -68,6 +68,7 @@ def export(args, api_client: Client = NEW_API_CLIENT) -> None:
"scheduled_slots": pool.scheduled_slots,
"open_slots": pool.open_slots,
"deferred_slots": pool.deferred_slots,
+ "team_name": pool.team_name,
}
for pool in pools_response.pools
]
@@ -105,6 +106,7 @@ def _import_helper(api_client: Client, filepath: Path,
action_on_existence: Bulk
slots=pool_config["slots"],
description=pool_config.get("description", ""),
include_deferred=pool_config.get("include_deferred", False),
+ team_name=pool_config.get("team_name"),
)
)
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_pool_command.py
b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_pool_command.py
index 0bc24389294..15ce83e7af1 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_pool_command.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_pool_command.py
@@ -136,6 +136,23 @@ class TestPoolImportCommand:
action = call_args["pools"].actions[0]
assert action.action_on_existence == expected_enum
+ def test_import_forwards_team_name(self, mock_client, tmp_path):
+ """Test that team_name in the file reaches the server instead of being
sent as null."""
+ pools_file = tmp_path / "pools.json"
+ pools_file.write_text(json.dumps([{"name": "test_pool", "slots": 1,
"team_name": "data-team"}]))
+
+ mock_response = mock.MagicMock()
+ mock_response.success = ["test_pool"]
+ mock_response.errors = []
+ mock_bulk_builder = mock.MagicMock()
+ mock_bulk_builder.create = mock_response
+ mock_client.pools.bulk.return_value = mock_bulk_builder
+
+ pool_command.import_(mock.MagicMock(file=pools_file,
action_on_existing_key="overwrite"))
+
+ sent =
mock_client.pools.bulk.call_args[1]["pools"].model_dump(mode="json")
+ assert sent["actions"][0]["entities"][0]["team_name"] == "data-team"
+
class TestPoolExportCommand:
"""Test cases for pool export command."""
@@ -155,6 +172,7 @@ class TestPoolExportCommand:
"scheduled_slots": 0,
"open_slots": 1,
"deferred_slots": 0,
+ "team_name": None,
}
# Create a mock response with proper dictionary attributes
mock_pools = mock.MagicMock()
@@ -191,6 +209,7 @@ class TestPoolExportCommand:
"scheduled_slots": 0,
"open_slots": 5,
"deferred_slots": 0,
+ "team_name": None,
}
mock_pools = mock.MagicMock()
mock_pools.pools = [type("Pool", (), pool_attrs)()]
@@ -204,6 +223,32 @@ class TestPoolExportCommand:
output=output_format,
)
+ def test_export_includes_team_name(self, mock_client, tmp_path):
+ """Test that export writes team_name so the file can be imported back
without losing it."""
+ export_file = tmp_path / "export.json"
+ pool_attrs = {
+ "name": "test_pool",
+ "slots": 1,
+ "description": "Test pool",
+ "include_deferred": True,
+ "occupied_slots": 0,
+ "running_slots": 0,
+ "queued_slots": 0,
+ "scheduled_slots": 0,
+ "open_slots": 1,
+ "deferred_slots": 0,
+ "team_name": "data-team",
+ }
+ mock_pools = mock.MagicMock()
+ mock_pools.pools = [type("Pool", (), pool_attrs)()]
+ mock_pools.total_entries = 1
+ mock_client.pools.list.return_value = mock_pools
+
+ pool_command.export(mock.MagicMock(file=export_file, output="json"))
+
+ exported_data = json.loads(export_file.read_text())
+ assert exported_data[0]["team_name"] == "data-team"
+
def test_export_failure(self, mock_client, tmp_path):
"""Test pool export with API failure."""
export_file = tmp_path / "export.json"