This is an automated email from the ASF dual-hosted git repository.
shahar1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 1bcc8e6bdbc Validate the file structure in airflowctl connections
import (#75176)
1bcc8e6bdbc is described below
commit 1bcc8e6bdbc11f151a38a064a6950156b7a5e78a
Author: Rishabh <[email protected]>
AuthorDate: Sat Oct 31 02:19:34 2026 +0530
Validate the file structure in airflowctl connections import (#75176)
`airflowctl connections import` assumed the file was a JSON object of
objects. A file holding a list, a scalar, or an entry that is not an object
failed with internal Python messages such as
"'list' object has no attribute 'items'" or
"'str' object has no attribute 'get'", which do not tell the user what is
wrong with the file.
Check the structure right after parsing, before building any request, and
exit with a message that names the file, the offending connection id and the
expected shape. `variables import` and `pools import` already validate their
input the same way. No request is sent for a malformed file.
---
.../airflowctl/ctl/commands/connection_command.py | 11 ++++++++
.../ctl/commands/test_connections_command.py | 30 ++++++++++++++++++++++
2 files changed, 41 insertions(+)
diff --git a/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py
b/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py
index 02958740a36..4c4067ffcb7 100644
--- a/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py
+++ b/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py
@@ -45,6 +45,17 @@ def import_(args, api_client=NEW_API_CLIENT) -> None:
connections_json = json.loads(file.read())
except Exception as e:
raise SystemExit(f"Error reading connections file {args.file}:
{e}")
+ if not isinstance(connections_json, dict):
+ raise SystemExit(
+ f"Invalid connections file {args.file}: expected a JSON object
mapping connection ids "
+ f"to connection details, got {type(connections_json).__name__}"
+ )
+ for connection_id, connection_details in connections_json.items():
+ if not isinstance(connection_details, dict):
+ raise SystemExit(
+ f"Invalid connection {connection_id!r} in {args.file}:
expected a JSON object "
+ f"of connection details, got
{type(connection_details).__name__}"
+ )
try:
connections_data = {
k: ConnectionBody(
diff --git
a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py
b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py
index 452d6182e7d..f400f07dcfd 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py
@@ -217,6 +217,36 @@ class TestCliConnectionCommands:
api_client=api_client,
)
+ @pytest.mark.parametrize(
+ ("file_content", "expected_message"),
+ [
+ ([{"conn_type": "postgres"}], "expected a JSON object mapping
connection ids"),
+ ("not-an-object", "expected a JSON object mapping connection ids"),
+ ({"my_conn": "postgres://host"}, "Invalid connection 'my_conn'"),
+ ({"my_conn": None}, "Invalid connection 'my_conn'"),
+ ],
+ ids=["list", "string", "string-entry", "null-entry"],
+ )
+ def test_import_rejects_malformed_file_structure(
+ self, tmp_path, monkeypatch, file_content, expected_message
+ ):
+ monkeypatch.chdir(tmp_path)
+ json_path = tmp_path / self.export_file_name
+ json_path.write_text(json.dumps(file_content))
+ mock_client = mock.MagicMock(spec=Client)
+
+ with pytest.raises(SystemExit) as exc_info:
+ connection_command.import_(
+ self.parser.parse_args(["connections", "import",
json_path.as_posix()]),
+ api_client=mock_client,
+ )
+
+ message = str(exc_info.value)
+ assert expected_message in message
+ assert json_path.as_posix() in message
+ assert "has no attribute" not in message
+ mock_client.connections.bulk.assert_not_called()
+
@pytest.mark.parametrize(
("action_on_existing_key", "expected_enum"),
[