Copilot commented on code in PR #45931:
URL: https://github.com/apache/airflow/pull/45931#discussion_r3066497474


##########
airflow-core/src/airflow/configuration.py:
##########
@@ -741,35 +740,107 @@ def get_custom_secret_backend(worker_mode: bool = False) 
-> BaseSecretsBackend |
     return conf._get_custom_secret_backend(worker_mode=worker_mode)
 
 
+def get_importable_secret_backend(class_name: str | None) -> 
BaseSecretsBackend | None:
+    """Get secret backend defined in the given class name."""
+    if class_name is not None:
+        secrets_backend_cls = import_string(class_name)
+        return secrets_backend_cls()
+    return None
+
+
+class Backends(Enum):
+    """Type of the secrets backend."""
+
+    ENVIRONMENT_VARIABLE = "environment_variable"
+    EXECUTION_API = "execution_api"
+    CUSTOM = "custom"
+    METASTORE = "metastore"
+
+
 def initialize_secrets_backends(
-    default_backends: list[str] = DEFAULT_SECRETS_SEARCH_PATH,
+    default_backends: list[str] | None = None,
 ) -> list[BaseSecretsBackend]:
     """
     Initialize secrets backend.
 
     * import secrets backend classes
     * instantiate them and return them in a list
     """
-    backend_list = []
     worker_mode = False
-    if default_backends != DEFAULT_SECRETS_SEARCH_PATH:
+    search_section = "secrets"
+    environment_variable_args: str | None = (
+        "airflow.secrets.environment_variables.EnvironmentVariablesBackend"
+    )
+    metastore_args: str | None = "airflow.secrets.metastore.MetastoreBackend"
+    execution_args: str | None = None
+
+    if default_backends is not None:
         worker_mode = True
+        search_section = "workers"
+        environment_variable_args = (
+            environment_variable_args if environment_variable_args in 
default_backends else None
+        )
+        metastore_args = metastore_args if metastore_args in default_backends 
else None
+        execution_args = (
+            
"airflow.sdk.execution_time.secrets.execution_api.ExecutionAPISecretsBackend"
+            if 
"airflow.sdk.execution_time.secrets.execution_api.ExecutionAPISecretsBackend"
+            in default_backends
+            else None
+        )
+
+    backends_map: dict[str, dict[str, Any]] = {
+        "environment_variable": {
+            "callback": get_importable_secret_backend,
+            "args": (environment_variable_args,),
+        },
+        "metastore": {
+            "callback": get_importable_secret_backend,
+            "args": (metastore_args,),
+        },
+        "custom": {
+            "callback": get_custom_secret_backend,
+            "args": (worker_mode,),
+        },
+        "execution_api": {
+            "callback": get_importable_secret_backend,
+            "args": (execution_args,),
+        },
+    }
 
-    custom_secret_backend = get_custom_secret_backend(worker_mode)
+    backends_order = conf.getlist(search_section, "backends_order", 
delimiter=",")
 
-    if custom_secret_backend is not None:
-        from airflow.models import Connection
+    required_backends = (
+        [Backends.ENVIRONMENT_VARIABLE, Backends.EXECUTION_API]
+        if worker_mode
+        else [Backends.METASTORE, Backends.ENVIRONMENT_VARIABLE]
+    )
 
-        custom_secret_backend._set_connection_class(Connection)
-        backend_list.append(custom_secret_backend)
+    if missing_backends := [b.value for b in required_backends if b.value not 
in backends_order]:
+        raise AirflowConfigException(
+            f"The configuration option [{search_section}]backends_order is 
misconfigured. "
+            f"The following backend types are missing: {missing_backends}",
+            search_section,

Review Comment:
   In worker mode, this required-backends check only validates that the *type 
names* are present in `backends_order`, but it doesn’t validate that the 
corresponding backend classpaths are actually available (e.g. 
`execution_args`/`environment_variable_args` can be set to `None` when they 
aren’t in `default_backends`, causing the backend to be silently skipped). 
Please either (a) stop nulling out required backend args, or (b) extend 
validation to error when a required backend’s classpath is missing from 
`default_backends` so workers can’t start without required backends 
instantiated.



##########
airflow-core/src/airflow/ui/src/pages/Variables/BackendsOrderCard.tsx:
##########
@@ -0,0 +1,42 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Box, useDisclosure } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
+import { LuSettings } from "react-icons/lu";
+
+import { BackendsOrderButton } from "src/pages/Variables/BackendsOrderButton";
+import { BackendsOrderModal } from "src/pages/Variables/BackendsOrderModal";

Review Comment:
   These imports use an absolute `src/pages/...` path for components that live 
in the same directory. In this folder, other components are imported via 
relative paths (e.g. `./DeleteVariablesButton` in `Variables.tsx`), so this is 
inconsistent and makes refactors harder. Prefer `./BackendsOrderButton` and 
`./BackendsOrderModal` here.



##########
airflow-core/src/airflow/secrets/__init__.py:
##########
@@ -29,12 +29,11 @@
 
 from airflow.utils.deprecation_tools import add_deprecated_classes
 
-__all__ = ["BaseSecretsBackend", "DEFAULT_SECRETS_SEARCH_PATH"]
+__all__ = [
+    "BaseSecretsBackend",
+]
 
-from airflow.secrets.base_secrets import (
-    DEFAULT_SECRETS_SEARCH_PATH as DEFAULT_SECRETS_SEARCH_PATH,
-    BaseSecretsBackend,
-)
+from airflow.secrets.base_secrets import BaseSecretsBackend
 

Review Comment:
   Removing `DEFAULT_SECRETS_SEARCH_PATH` from `airflow.secrets`’ public 
exports is a backwards-incompatible change for any external code that imports 
it from this module. Consider keeping the re-export (potentially with a 
deprecation warning) since the constant still exists in 
`airflow.secrets.base_secrets` and this PR’s stated goal is adding a new config 
option, not removing a public symbol.



##########
airflow-core/src/airflow/ui/src/pages/Variables/BackendsOrderButton.tsx:
##########
@@ -0,0 +1,76 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Box, HStack, Skeleton, Text } from "@chakra-ui/react";
+import { FiChevronRight } from "react-icons/fi";
+import { Link as RouterLink } from "react-router-dom";
+
+import type { TaskInstanceState } from "openapi/requests/types.gen";
+import { StateBadge } from "src/components/StateBadge";
+
+export const BackendsOrderButton = ({
+  colorScheme,
+  icon,
+  isLoading = false,
+  label,
+  link,
+  onClick,
+  state,
+}: {
+  readonly colorScheme: string;
+  readonly icon?: React.ReactNode;
+  readonly isLoading?: boolean;
+  readonly label: string;
+  readonly link?: string;
+  readonly onClick?: () => void;
+  readonly state?: TaskInstanceState | null;
+}) => {
+  if (isLoading) {
+    return <Skeleton borderRadius="lg" height="42px" width="175px" />;
+  }
+
+  const content = (
+    <HStack
+      alignItems="center"
+      borderRadius="lg"
+      borderWidth={1}
+      color="fg.emphasized"
+      cursor="pointer"
+      p={2}
+    >
+      <StateBadge colorPalette={colorScheme} mr={2} state={state}>
+        {icon}
+      </StateBadge>
+
+      <Text color="fg" fontSize="sm" fontWeight="bold">

Review Comment:
   `StateBadge` expects a defined `state` (or `null`) to avoid generating 
invalid Chakra color tokens. When `state` is left `undefined` here (as in 
`BackendsOrderCard`), `StateBadge` computes `color="undefined.contrast"`, which 
can render incorrectly or trigger theme warnings. Use a neutral container (e.g. 
`Badge`/`Box`) for the icon, or pass `state={null}` explicitly (and adjust 
styling as needed).



##########
task-sdk/src/airflow/sdk/configuration.py:
##########
@@ -232,36 +252,92 @@ def initialize_secrets_backends(
 
     Uses SDK's conf instead of Core's conf.
     """
-    from airflow.sdk._shared.module_loading import import_string
-
-    backend_list = []
-    worker_mode = False
     # Determine worker mode - if default_backends is not the server default, 
it's worker mode
     # This is a simplified check; in practice, worker mode is determined by 
the caller
-    if default_backends != _SERVER_DEFAULT_SECRETS_SEARCH_PATH:
+    from airflow.sdk.configuration import conf
+

Review Comment:
   The comment and logic here are out of sync: worker mode is determined by 
`default_backends is not None`, not by comparing against a server-default list 
anymore. Also, `from airflow.sdk.configuration import conf` is a self-import 
used just to trigger `__getattr__`; consider refactoring to a local helper 
(e.g. `get_conf()`) or explicitly initializing/caching `conf` once, to avoid 
self-imports and make the control flow clearer.



##########
task-sdk/src/airflow/sdk/configuration.py:
##########
@@ -232,36 +252,92 @@ def initialize_secrets_backends(
 
     Uses SDK's conf instead of Core's conf.
     """
-    from airflow.sdk._shared.module_loading import import_string
-
-    backend_list = []
-    worker_mode = False
     # Determine worker mode - if default_backends is not the server default, 
it's worker mode
     # This is a simplified check; in practice, worker mode is determined by 
the caller
-    if default_backends != _SERVER_DEFAULT_SECRETS_SEARCH_PATH:
+    from airflow.sdk.configuration import conf
+
+    worker_mode = False
+    search_section = "secrets"
+    environment_variable_args: str | None = (
+        "airflow.secrets.environment_variables.EnvironmentVariablesBackend"
+    )
+    metastore_args: str | None = "airflow.secrets.metastore.MetastoreBackend"
+    execution_args: str | None = None
+
+    if default_backends is not None:
         worker_mode = True
+        search_section = "workers"
+        environment_variable_args = (
+            environment_variable_args if environment_variable_args in 
default_backends else None
+        )
+        metastore_args = metastore_args if metastore_args in default_backends 
else None
+        execution_args = (
+            
"airflow.sdk.execution_time.secrets.execution_api.ExecutionAPISecretsBackend"
+            if 
"airflow.sdk.execution_time.secrets.execution_api.ExecutionAPISecretsBackend"
+            in default_backends
+            else None
+        )
+
+    backends_map: dict[str, dict[str, Any]] = {
+        "environment_variable": {
+            "callback": get_importable_secret_backend,
+            "args": (environment_variable_args,),
+        },
+        "metastore": {
+            "callback": get_importable_secret_backend,
+            "args": (metastore_args,),
+        },
+        "custom": {
+            "callback": get_custom_secret_backend,
+            "args": (worker_mode,),
+        },
+        "execution_api": {
+            "callback": get_importable_secret_backend,
+            "args": (execution_args,),
+        },
+    }
 
-    custom_secret_backend = get_custom_secret_backend(worker_mode)
+    backends_order = conf.getlist(search_section, "backends_order", 
delimiter=",")
 
-    if custom_secret_backend is not None:
-        from airflow.sdk.definitions.connection import Connection
+    required_backends = (
+        [Backends.ENVIRONMENT_VARIABLE, Backends.EXECUTION_API]
+        if worker_mode
+        else [Backends.METASTORE, Backends.ENVIRONMENT_VARIABLE]
+    )
 
-        custom_secret_backend._set_connection_class(Connection)
-        backend_list.append(custom_secret_backend)
+    if missing_backends := [b.value for b in required_backends if b.value not 
in backends_order]:
+        raise AirflowConfigException(
+            f"The configuration option [{search_section}]backends_order is 
misconfigured. "
+            f"The following backend types are missing: {missing_backends}",
+            search_section,

Review Comment:
   Like in core, this required-backends validation only checks that the backend 
*type names* exist in `backends_order`, but it doesn’t ensure the required 
backend implementations are actually enabled/instantiable (e.g. 
`execution_args`/`environment_variable_args` can be set to `None` when they 
aren’t present in `default_backends`, and then the backend is silently 
skipped). Please validate that required backends have non-None classpaths in 
worker mode (or don’t null them out) so workers can’t run with required 
backends missing.



##########
task-sdk/src/airflow/sdk/configuration.py:
##########
@@ -270,10 +346,9 @@ def ensure_secrets_loaded(
     """
     # Check if the secrets_backend_list contains only 2 default backends.
 
-    # Check if we are loading the backends for worker too by checking if the 
default_backends is equal
-    # to _SERVER_DEFAULT_SECRETS_SEARCH_PATH.
+    # Check if we are loading the backends for worker too by checking if the 
default_backends is not None
     secrets_backend_list = initialize_secrets_backends()
-    if len(secrets_backend_list) == 2 or default_backends != 
_SERVER_DEFAULT_SECRETS_SEARCH_PATH:
+    if len(secrets_backend_list) == 2 or default_backends is not None:
         return initialize_secrets_backends(default_backends=default_backends)
     return secrets_backend_list

Review Comment:
   `ensure_secrets_loaded` always calls `initialize_secrets_backends()` once, 
and then calls it again when `default_backends` is provided (the common worker 
case), making the first initialization redundant. This can instantiate secret 
backends twice (potentially expensive / side-effectful). Consider passing 
`default_backends` through to the first call (or skipping the first call 
entirely when `default_backends` is not `None`).



##########
task-sdk/src/airflow/sdk/configuration.py:
##########
@@ -221,8 +222,27 @@ def get_custom_secret_backend(worker_mode: bool = False):
     return conf._get_custom_secret_backend(worker_mode=worker_mode)
 
 
+class Backends(Enum):
+    """Type of the secrets backend."""
+
+    ENVIRONMENT_VARIABLE = "environment_variable"
+    EXECUTION_API = "execution_api"
+    CUSTOM = "custom"
+    METASTORE = "metastore"
+
+
+def get_importable_secret_backend(class_name: str | None) -> Any | None:
+    """Get secret backend defined in the given class name."""
+    from airflow.sdk._shared.module_loading import import_string
+
+    if class_name is not None:

Review Comment:
   `get_importable_secret_backend` performs an import inside the function body. 
Unless this is strictly needed for circular-import avoidance or process 
isolation, prefer importing `import_string` at module scope to avoid repeated 
imports and to match the project’s import conventions.



##########
airflow-core/src/airflow/api_fastapi/core_api/routes/ui/config.py:
##########
@@ -67,3 +74,32 @@ def get_configs() -> ConfigResponse:
     config.update({key: value for key, value in additional_config.items()})
 
     return ConfigResponse.model_validate(config)
+
+
+@config_router.get(
+    "/backends_order",
+    responses={
+        **create_openapi_http_exception_doc(
+            [
+                status.HTTP_404_NOT_FOUND,
+                status.HTTP_406_NOT_ACCEPTABLE,
+            ]
+        ),
+    },
+    response_model=Config,
+    dependencies=[Depends(requires_authenticated())],
+)

Review Comment:
   This new `/backends_order` UI route isn’t covered by the existing FastAPI UI 
config route tests (see 
`airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py`, which 
currently only asserts `/config`). Please add tests for the new endpoint (at 
least: 200 JSON response, 200 text/plain via `Accept`, and 401 for 
unauthenticated).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to