Skip to content

Commit 91a8ab6

Browse files
Fix airflowctl pools export/import dropping pool team ownership (#73132)
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. Co-authored-by: Eason09053360 <185830721+Eason09053360@users.noreply.github.com>
1 parent 6997dc7 commit 91a8ab6

2 files changed

Lines changed: 47 additions & 0 deletions

File tree

‎airflow-ctl/src/airflowctl/ctl/commands/pool_command.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def export(args, api_client: Client = NEW_API_CLIENT) -> None:
6868
"scheduled_slots": pool.scheduled_slots,
6969
"open_slots": pool.open_slots,
7070
"deferred_slots": pool.deferred_slots,
71+
"team_name": pool.team_name,
7172
}
7273
for pool in pools_response.pools
7374
]
@@ -105,6 +106,7 @@ def _import_helper(api_client: Client, filepath: Path, action_on_existence: Bulk
105106
slots=pool_config["slots"],
106107
description=pool_config.get("description", ""),
107108
include_deferred=pool_config.get("include_deferred", False),
109+
team_name=pool_config.get("team_name"),
108110
)
109111
)
110112

‎airflow-ctl/tests/airflow_ctl/ctl/commands/test_pool_command.py‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,23 @@ def test_import_action_on_existing_key(
136136
action = call_args["pools"].actions[0]
137137
assert action.action_on_existence == expected_enum
138138

139+
def test_import_forwards_team_name(self, mock_client, tmp_path):
140+
"""Test that team_name in the file reaches the server instead of being sent as null."""
141+
pools_file = tmp_path / "pools.json"
142+
pools_file.write_text(json.dumps([{"name": "test_pool", "slots": 1, "team_name": "data-team"}]))
143+
144+
mock_response = mock.MagicMock()
145+
mock_response.success = ["test_pool"]
146+
mock_response.errors = []
147+
mock_bulk_builder = mock.MagicMock()
148+
mock_bulk_builder.create = mock_response
149+
mock_client.pools.bulk.return_value = mock_bulk_builder
150+
151+
pool_command.import_(mock.MagicMock(file=pools_file, action_on_existing_key="overwrite"))
152+
153+
sent = mock_client.pools.bulk.call_args[1]["pools"].model_dump(mode="json")
154+
assert sent["actions"][0]["entities"][0]["team_name"] == "data-team"
155+
139156

140157
class TestPoolExportCommand:
141158
"""Test cases for pool export command."""
@@ -155,6 +172,7 @@ def test_export_json_to_file(self, mock_client, tmp_path, capsys):
155172
"scheduled_slots": 0,
156173
"open_slots": 1,
157174
"deferred_slots": 0,
175+
"team_name": None,
158176
}
159177
# Create a mock response with proper dictionary attributes
160178
mock_pools = mock.MagicMock()
@@ -191,6 +209,7 @@ def test_export_non_json_uses_airflow_console(self, mock_client, tmp_path, outpu
191209
"scheduled_slots": 0,
192210
"open_slots": 5,
193211
"deferred_slots": 0,
212+
"team_name": None,
194213
}
195214
mock_pools = mock.MagicMock()
196215
mock_pools.pools = [type("Pool", (), pool_attrs)()]
@@ -204,6 +223,32 @@ def test_export_non_json_uses_airflow_console(self, mock_client, tmp_path, outpu
204223
output=output_format,
205224
)
206225

226+
def test_export_includes_team_name(self, mock_client, tmp_path):
227+
"""Test that export writes team_name so the file can be imported back without losing it."""
228+
export_file = tmp_path / "export.json"
229+
pool_attrs = {
230+
"name": "test_pool",
231+
"slots": 1,
232+
"description": "Test pool",
233+
"include_deferred": True,
234+
"occupied_slots": 0,
235+
"running_slots": 0,
236+
"queued_slots": 0,
237+
"scheduled_slots": 0,
238+
"open_slots": 1,
239+
"deferred_slots": 0,
240+
"team_name": "data-team",
241+
}
242+
mock_pools = mock.MagicMock()
243+
mock_pools.pools = [type("Pool", (), pool_attrs)()]
244+
mock_pools.total_entries = 1
245+
mock_client.pools.list.return_value = mock_pools
246+
247+
pool_command.export(mock.MagicMock(file=export_file, output="json"))
248+
249+
exported_data = json.loads(export_file.read_text())
250+
assert exported_data[0]["team_name"] == "data-team"
251+
207252
def test_export_failure(self, mock_client, tmp_path):
208253
"""Test pool export with API failure."""
209254
export_file = tmp_path / "export.json"

0 commit comments

Comments
 (0)