Repository: apache/airflow

Issue:
Connections import and export should also support ".yml" file extensions
### Apache Airflow version

2.2.4 (latest released)

### What happened

Trying to export or import a yaml formatted connections file with ".yml" extension fails. 

### What you think should happen instead

While the "official recommended extension" for YAML files is .yaml, many pipeline are built around using the .yml file extension. Importing and exporting of .yml files should also be supported. 

### How to reproduce

Running airflow connections import or export with a file having a .yml file extension errors with:
`Unsupported file format. The file must have the extension .env or .json or .yaml`


### Operating System

debian 10 buster

### Versions of Apache Airflow Providers

_No response_

### Deployment

Docker-Compose

### Deployment details

_No response_

### Anything else

_No response_

### Are you willing to submit PR?

- [X] Yes I am willing to submit a PR!

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)



Discussion:
Thanks for opening your first issue here! Be sure to follow the issue template!

assigned you
@jackemuk @potiuk I will take this if no one minds
Feel free.

Resolving patch:
diff --git a/airflow/secrets/local_filesystem.py b/airflow/secrets/local_filesystem.py
--- a/airflow/secrets/local_filesystem.py
+++ b/airflow/secrets/local_filesystem.py
@@ -109,7 +109,6 @@ def _parse_yaml_file(file_path: str) -> Tuple[Dict[str, List[str]], List[FileSyn
         return {}, [FileSyntaxError(line_no=1, message="The file is empty.")]
     try:
         secrets = yaml.safe_load(content)
-
     except yaml.MarkedYAMLError as e:
         err_line_no = e.problem_mark.line if e.problem_mark else -1
         return {}, [FileSyntaxError(line_no=err_line_no, message=str(e))]
@@ -145,6 +144,7 @@ def _parse_json_file(file_path: str) -> Tuple[Dict[str, Any], List[FileSyntaxErr
     "env": _parse_env_file,
     "json": _parse_json_file,
     "yaml": _parse_yaml_file,
+    "yml": _parse_yaml_file,
 }
 
 
@@ -166,7 +166,8 @@ def _parse_secret_file(file_path: str) -> Dict[str, Any]:
 
     if ext not in FILE_PARSERS:
         raise AirflowException(
-            "Unsupported file format. The file must have the extension .env or .json or .yaml"
+            "Unsupported file format. The file must have one of the following extensions: "
+            ".env .json .yaml .yml"
         )
 
     secrets, parse_errors = FILE_PARSERS[ext](file_path)


Test patch:
diff --git a/tests/cli/commands/test_connection_command.py b/tests/cli/commands/test_connection_command.py
--- a/tests/cli/commands/test_connection_command.py
+++ b/tests/cli/commands/test_connection_command.py
@@ -630,7 +630,7 @@ def test_cli_connections_import_should_return_error_if_file_does_not_exist(self,
         with pytest.raises(SystemExit, match=r"Missing connections file."):
             connection_command.connections_import(self.parser.parse_args(["connections", "import", filepath]))
 
-    @pytest.mark.parametrize('filepath', ["sample.jso", "sample.yml", "sample.environ"])
+    @pytest.mark.parametrize('filepath', ["sample.jso", "sample.environ"])
     @mock.patch('os.path.exists')
     def test_cli_connections_import_should_return_error_if_file_format_is_invalid(
         self, mock_exists, filepath
@@ -638,7 +638,10 @@ def test_cli_connections_import_should_return_error_if_file_format_is_invalid(
         mock_exists.return_value = True
         with pytest.raises(
             AirflowException,
-            match=r"Unsupported file format. The file must have the extension .env or .json or .yaml",
+            match=(
+                "Unsupported file format. The file must have one of the following extensions: "
+                ".env .json .yaml .yml"
+            ),
         ):
             connection_command.connections_import(self.parser.parse_args(["connections", "import", filepath]))
 
diff --git a/tests/secrets/test_local_filesystem.py b/tests/secrets/test_local_filesystem.py
--- a/tests/secrets/test_local_filesystem.py
+++ b/tests/secrets/test_local_filesystem.py
@@ -122,8 +122,9 @@ def test_missing_file(self, mock_exists):
     )
     def test_yaml_file_should_load_variables(self, file_content, expected_variables):
         with mock_local_file(file_content):
-            variables = local_filesystem.load_variables('a.yaml')
-            assert expected_variables == variables
+            vars_yaml = local_filesystem.load_variables('a.yaml')
+            vars_yml = local_filesystem.load_variables('a.yml')
+            assert expected_variables == vars_yaml == vars_yml
 
 
 class TestLoadConnection(unittest.TestCase):
@@ -390,6 +391,21 @@ def test_ensure_unique_connection_yaml(self, file_content):
             with pytest.raises(ConnectionNotUnique):
                 local_filesystem.load_connections_dict("a.yaml")
 
+    @parameterized.expand(
+        (("conn_a: mysql://hosta"),),
+    )
+    def test_yaml_extension_parsers_return_same_result(self, file_content):
+        with mock_local_file(file_content):
+            conn_uri_by_conn_id_yaml = {
+                conn_id: conn.get_uri()
+                for conn_id, conn in local_filesystem.load_connections_dict("a.yaml").items()
+            }
+            conn_uri_by_conn_id_yml = {
+                conn_id: conn.get_uri()
+                for conn_id, conn in local_filesystem.load_connections_dict("a.yml").items()
+            }
+            assert conn_uri_by_conn_id_yaml == conn_uri_by_conn_id_yml
+
 
 class TestLocalFileBackend(unittest.TestCase):
     def test_should_read_variable(self):