Skip to content

Commit 82318b2

Browse files
Leondon9ruvnet
authored andcommitted
Add --action-on-existing-key to pools import and connections import (apache#62702)
closes: apache#62695 Co-authored-by: claude-flow <ruv@ruv.net>
1 parent 62a493a commit 82318b2

5 files changed

Lines changed: 91 additions & 15 deletions

File tree

airflow-ctl/src/airflowctl/ctl/cli_config.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -254,12 +254,11 @@ def string_lower_type(val):
254254
help="The DAG ID of the DAG to pause or unpause",
255255
)
256256

257-
# Variable Commands Args
258-
ARG_VARIABLE_ACTION_ON_EXISTING_KEY = Arg(
257+
ARG_ACTION_ON_EXISTING_KEY = Arg(
259258
flags=("-a", "--action-on-existing-key"),
260259
type=str,
261260
default="overwrite",
262-
help="Action to take if we encounter a variable key that already exists.",
261+
help="Action to take if the entity already exists.",
263262
choices=("overwrite", "fail", "skip"),
264263
)
265264

@@ -865,7 +864,10 @@ def merge_commands(
865864
name="import",
866865
help="Import connections from a file exported with local CLI.",
867866
func=lazy_load_command("airflowctl.ctl.commands.connection_command.import_"),
868-
args=(Arg(flags=("file",), metavar="FILEPATH", help="Connections JSON file"),),
867+
args=(
868+
Arg(flags=("file",), metavar="FILEPATH", help="Connections JSON file"),
869+
ARG_ACTION_ON_EXISTING_KEY,
870+
),
869871
),
870872
)
871873

@@ -895,7 +897,7 @@ def merge_commands(
895897
name="import",
896898
help="Import pools",
897899
func=lazy_load_command("airflowctl.ctl.commands.pool_command.import_"),
898-
args=(ARG_FILE,),
900+
args=(ARG_FILE, ARG_ACTION_ON_EXISTING_KEY),
899901
),
900902
ActionCommand(
901903
name="export",
@@ -913,7 +915,7 @@ def merge_commands(
913915
name="import",
914916
help="Import variables from a file exported with local CLI.",
915917
func=lazy_load_command("airflowctl.ctl.commands.variable_command.import_"),
916-
args=(ARG_FILE, ARG_VARIABLE_ACTION_ON_EXISTING_KEY),
918+
args=(ARG_FILE, ARG_ACTION_ON_EXISTING_KEY),
917919
),
918920
)
919921

airflow-ctl/src/airflowctl/ctl/commands/connection_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def import_(args, api_client=NEW_API_CLIENT) -> None:
6262
connection_create_action = BulkCreateActionConnectionBody(
6363
action="create",
6464
entities=list(connections_data.values()),
65-
action_on_existence=BulkActionOnExistence("fail"),
65+
action_on_existence=BulkActionOnExistence(args.action_on_existing_key),
6666
)
6767
response = api_client.connections.bulk(BulkBodyConnectionBody(actions=[connection_create_action]))
6868
if response.create.errors:

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def import_(args, api_client: Client = NEW_API_CLIENT) -> None:
4141
if not filepath.exists():
4242
raise SystemExit(f"Missing pools file {args.file}")
4343

44-
success, errors = _import_helper(api_client, filepath)
44+
success, errors = _import_helper(api_client, filepath, BulkActionOnExistence(args.action_on_existing_key))
4545
if errors:
4646
raise SystemExit(f"Failed to update pool(s): {errors}")
4747
rich.print(success)
@@ -83,7 +83,7 @@ def export(args, api_client: Client = NEW_API_CLIENT) -> None:
8383
raise SystemExit(f"Failed to export pools: {e}")
8484

8585

86-
def _import_helper(api_client: Client, filepath: Path):
86+
def _import_helper(api_client: Client, filepath: Path, action_on_existence: BulkActionOnExistence):
8787
"""Help import pools from the json file."""
8888
try:
8989
with open(filepath) as f:
@@ -113,7 +113,7 @@ def _import_helper(api_client: Client, filepath: Path):
113113
BulkCreateActionPoolBody(
114114
action="create",
115115
entities=pools_to_update,
116-
action_on_existence=BulkActionOnExistence.FAIL,
116+
action_on_existence=action_on_existence,
117117
)
118118
]
119119
)

airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
from __future__ import annotations
1818

1919
import json
20+
from unittest import mock
2021
from unittest.mock import patch
2122

2223
import pytest
2324

24-
from airflowctl.api.client import ClientKind
25+
from airflowctl.api.client import Client, ClientKind
2526
from airflowctl.api.datamodels.generated import (
27+
BulkActionOnExistence,
2628
BulkActionResponse,
2729
BulkResponse,
2830
ConnectionBody,
@@ -176,3 +178,47 @@ def test_import_without_extra_field(self, api_client_maker, tmp_path, monkeypatc
176178
extra=None,
177179
description="",
178180
)
181+
182+
@pytest.mark.parametrize(
183+
("action_on_existing_key", "expected_enum"),
184+
[
185+
("overwrite", BulkActionOnExistence.OVERWRITE),
186+
("skip", BulkActionOnExistence.SKIP),
187+
("fail", BulkActionOnExistence.FAIL),
188+
],
189+
)
190+
def test_import_action_on_existing_key(self, tmp_path, action_on_existing_key, expected_enum):
191+
expected_json_path = tmp_path / self.export_file_name
192+
connection_file = {
193+
self.connection_id: {
194+
"conn_type": "test_type",
195+
"host": "test_host",
196+
"extra": "{}",
197+
"connection_id": self.connection_id,
198+
}
199+
}
200+
expected_json_path.write_text(json.dumps(connection_file))
201+
202+
mock_client = mock.MagicMock(spec=Client)
203+
mock_response = mock.MagicMock()
204+
mock_response.create.success = [self.connection_id]
205+
mock_response.create.errors = []
206+
mock_client.connections.bulk.return_value = mock_response
207+
208+
connection_command.import_(
209+
self.parser.parse_args(
210+
[
211+
"connections",
212+
"import",
213+
expected_json_path.as_posix(),
214+
"--action-on-existing-key",
215+
action_on_existing_key,
216+
]
217+
),
218+
api_client=mock_client,
219+
)
220+
221+
mock_client.connections.bulk.assert_called_once()
222+
bulk_body = mock_client.connections.bulk.call_args[0][0]
223+
action = bulk_body.actions[0]
224+
assert action.action_on_existence == expected_enum

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

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,21 +48,21 @@ def test_import_missing_file(self, mock_client, tmp_path):
4848
"""Test import with missing file."""
4949
non_existent = tmp_path / "non_existent.json"
5050
with pytest.raises(SystemExit, match=f"Missing pools file {non_existent}"):
51-
pool_command.import_(mock.MagicMock(file=non_existent))
51+
pool_command.import_(mock.MagicMock(file=non_existent, action_on_existing_key="fail"))
5252

5353
def test_import_invalid_json(self, mock_client, tmp_path):
5454
"""Test import with invalid JSON file."""
5555
invalid_json = tmp_path / "invalid.json"
5656
invalid_json.write_text("invalid json")
5757
with pytest.raises(SystemExit, match="Invalid json file"):
58-
pool_command.import_(mock.MagicMock(file=invalid_json))
58+
pool_command.import_(mock.MagicMock(file=invalid_json, action_on_existing_key="fail"))
5959

6060
def test_import_invalid_pool_config(self, mock_client, tmp_path):
6161
"""Test import with invalid pool configuration."""
6262
invalid_pool = tmp_path / "invalid_pool.json"
6363
invalid_pool.write_text(json.dumps([{"invalid": "config"}]))
6464
with pytest.raises(SystemExit, match="Invalid pool configuration: {'invalid': 'config'}"):
65-
pool_command.import_(mock.MagicMock(file=invalid_pool))
65+
pool_command.import_(mock.MagicMock(file=invalid_pool, action_on_existing_key="fail"))
6666

6767
def test_import_success(self, mock_client, tmp_path, capsys):
6868
"""Test successful pool import."""
@@ -87,7 +87,7 @@ def test_import_success(self, mock_client, tmp_path, capsys):
8787

8888
mock_client.pools.bulk.return_value = mock_bulk_builder
8989

90-
pool_command.import_(mock.MagicMock(file=pools_file))
90+
pool_command.import_(mock.MagicMock(file=pools_file, action_on_existing_key="fail"))
9191

9292
# Verify bulk operation was called with correct parameters
9393
mock_client.pools.bulk.assert_called_once()
@@ -108,6 +108,34 @@ def test_import_success(self, mock_client, tmp_path, capsys):
108108
captured = capsys.readouterr()
109109
assert str(["test_pool"]) in captured.out
110110

111+
@pytest.mark.parametrize(
112+
("action_on_existing_key", "expected_enum"),
113+
[
114+
("overwrite", BulkActionOnExistence.OVERWRITE),
115+
("skip", BulkActionOnExistence.SKIP),
116+
("fail", BulkActionOnExistence.FAIL),
117+
],
118+
)
119+
def test_import_action_on_existing_key(
120+
self, mock_client, tmp_path, action_on_existing_key, expected_enum
121+
):
122+
"""Test that --action-on-existing-key is passed through to the bulk API."""
123+
pools_file = tmp_path / "pools.json"
124+
pools_file.write_text(json.dumps([{"name": "test_pool", "slots": 1}]))
125+
126+
mock_response = mock.MagicMock()
127+
mock_response.success = ["test_pool"]
128+
mock_response.errors = []
129+
mock_bulk_builder = mock.MagicMock()
130+
mock_bulk_builder.create = mock_response
131+
mock_client.pools.bulk.return_value = mock_bulk_builder
132+
133+
pool_command.import_(mock.MagicMock(file=pools_file, action_on_existing_key=action_on_existing_key))
134+
135+
call_args = mock_client.pools.bulk.call_args[1]
136+
action = call_args["pools"].actions[0]
137+
assert action.action_on_existence == expected_enum
138+
111139

112140
class TestPoolExportCommand:
113141
"""Test cases for pool export command."""

0 commit comments

Comments
 (0)