Copilot commented on code in PR #64754: URL: https://github.com/apache/airflow/pull/64754#discussion_r3066502782
########## providers/akeyless/src/airflow/providers/akeyless/hooks/akeyless.py: ########## @@ -0,0 +1,220 @@ +# 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. +"""Hook for Akeyless Vault Platform.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Any + +import akeyless + +from airflow.providers.common.compat.sdk import BaseHook + +VALID_AUTH_TYPES = ("api_key", "aws_iam", "gcp", "azure_ad", "uid", "jwt", "k8s", "certificate") Review Comment: If `access_type` is set to an unsupported value (or even a supported one but misspelled), the code currently falls through and calls `auth()` with an incomplete `Auth` body (e.g., missing `access_key` / missing cloud_id / etc.). This leads to a hard-to-debug runtime failure. Add explicit validation (e.g., `if access_type not in VALID_AUTH_TYPES: raise ValueError(...)`) and include a clear message listing allowed values. ########## providers/akeyless/src/airflow/providers/akeyless/hooks/akeyless.py: ########## @@ -0,0 +1,220 @@ +# 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. +"""Hook for Akeyless Vault Platform.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Any + +import akeyless + +from airflow.providers.common.compat.sdk import BaseHook + +VALID_AUTH_TYPES = ("api_key", "aws_iam", "gcp", "azure_ad", "uid", "jwt", "k8s", "certificate") + + +class AkeylessHook(BaseHook): + """ + Hook to interact with the Akeyless Vault Platform. + + Thin wrapper around the ``akeyless`` Python SDK. + + .. seealso:: + - https://docs.akeyless.io/ + - https://github.com/akeylesslabs/akeyless-python + + Connection fields: + + * **Host** -> API URL (default ``https://api.akeyless.io``) + * **Login** -> Access ID + * **Password** -> Access Key (for ``api_key`` auth) + * **Extra** -> JSON with ``access_type`` and auth-method-specific fields + + :param akeyless_conn_id: Airflow connection ID. + """ + + conn_name_attr = "akeyless_conn_id" + default_conn_name = "akeyless_default" + conn_type = "akeyless" + hook_name = "Akeyless" + + def __init__(self, akeyless_conn_id: str = default_conn_name, **kwargs: Any) -> None: + super().__init__() + self.akeyless_conn_id = akeyless_conn_id + self._conn = self.get_connection(akeyless_conn_id) + self._extra = self._conn.extra_dejson or {} + + @cached_property + def client(self) -> akeyless.V2Api: + """Return an ``akeyless.V2Api`` client (cached).""" + api_url = self._conn.host or self._extra.get("api_url", "https://api.akeyless.io") + if not api_url.startswith("http"): + api_url = f"https://{api_url}" + return akeyless.V2Api(akeyless.ApiClient(akeyless.Configuration(host=api_url))) + + def get_conn(self) -> akeyless.V2Api: + """Return the underlying ``akeyless.V2Api`` client.""" + return self.client + + def authenticate(self) -> str: + """ + Authenticate and return an API token. + + For ``uid`` auth the token is the UID token itself. + For all other methods, calls ``akeyless.Auth``. + """ + access_type = self._extra.get("access_type", "api_key") + + if access_type == "uid": + return self._extra["uid_token"] + + body = akeyless.Auth(access_id=self._conn.login) + + if access_type == "api_key": + body.access_key = self._conn.password + elif access_type in ("aws_iam", "gcp", "azure_ad"): + body.access_type = access_type + body.cloud_id = self._get_cloud_id(access_type) + elif access_type == "jwt": + body.access_type = "jwt" + body.jwt = self._extra.get("jwt") + elif access_type == "k8s": + body.access_type = "k8s" + body.k8s_auth_config_name = self._extra.get("k8s_auth_config_name") + elif access_type == "certificate": + body.access_type = "cert" + body.cert_data = self._extra.get("certificate_data") + body.key_data = self._extra.get("private_key_data") + + return self.client.auth(body).token Review Comment: If `access_type` is set to an unsupported value (or even a supported one but misspelled), the code currently falls through and calls `auth()` with an incomplete `Auth` body (e.g., missing `access_key` / missing cloud_id / etc.). This leads to a hard-to-debug runtime failure. Add explicit validation (e.g., `if access_type not in VALID_AUTH_TYPES: raise ValueError(...)`) and include a clear message listing allowed values. ########## providers/akeyless/src/airflow/providers/akeyless/hooks/akeyless.py: ########## @@ -0,0 +1,220 @@ +# 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. +"""Hook for Akeyless Vault Platform.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Any + +import akeyless + +from airflow.providers.common.compat.sdk import BaseHook + +VALID_AUTH_TYPES = ("api_key", "aws_iam", "gcp", "azure_ad", "uid", "jwt", "k8s", "certificate") + + +class AkeylessHook(BaseHook): + """ + Hook to interact with the Akeyless Vault Platform. + + Thin wrapper around the ``akeyless`` Python SDK. + + .. seealso:: + - https://docs.akeyless.io/ + - https://github.com/akeylesslabs/akeyless-python + + Connection fields: + + * **Host** -> API URL (default ``https://api.akeyless.io``) + * **Login** -> Access ID + * **Password** -> Access Key (for ``api_key`` auth) + * **Extra** -> JSON with ``access_type`` and auth-method-specific fields + + :param akeyless_conn_id: Airflow connection ID. + """ + + conn_name_attr = "akeyless_conn_id" + default_conn_name = "akeyless_default" + conn_type = "akeyless" + hook_name = "Akeyless" + + def __init__(self, akeyless_conn_id: str = default_conn_name, **kwargs: Any) -> None: + super().__init__() + self.akeyless_conn_id = akeyless_conn_id + self._conn = self.get_connection(akeyless_conn_id) + self._extra = self._conn.extra_dejson or {} + + @cached_property + def client(self) -> akeyless.V2Api: + """Return an ``akeyless.V2Api`` client (cached).""" + api_url = self._conn.host or self._extra.get("api_url", "https://api.akeyless.io") + if not api_url.startswith("http"): + api_url = f"https://{api_url}" + return akeyless.V2Api(akeyless.ApiClient(akeyless.Configuration(host=api_url))) + + def get_conn(self) -> akeyless.V2Api: + """Return the underlying ``akeyless.V2Api`` client.""" + return self.client + + def authenticate(self) -> str: + """ + Authenticate and return an API token. + + For ``uid`` auth the token is the UID token itself. + For all other methods, calls ``akeyless.Auth``. + """ + access_type = self._extra.get("access_type", "api_key") + + if access_type == "uid": + return self._extra["uid_token"] + + body = akeyless.Auth(access_id=self._conn.login) + + if access_type == "api_key": + body.access_key = self._conn.password + elif access_type in ("aws_iam", "gcp", "azure_ad"): + body.access_type = access_type + body.cloud_id = self._get_cloud_id(access_type) + elif access_type == "jwt": + body.access_type = "jwt" + body.jwt = self._extra.get("jwt") + elif access_type == "k8s": + body.access_type = "k8s" + body.k8s_auth_config_name = self._extra.get("k8s_auth_config_name") + elif access_type == "certificate": + body.access_type = "cert" + body.cert_data = self._extra.get("certificate_data") + body.key_data = self._extra.get("private_key_data") + + return self.client.auth(body).token + + def _get_cloud_id(self, access_type: str) -> str: + from akeyless_cloud_id import CloudId + + cid = CloudId() + if access_type == "aws_iam": + return cid.generate() + if access_type == "gcp": + return cid.generateGcp(self._extra.get("gcp_audience")) + if access_type == "azure_ad": + return cid.generateAzure(self._extra.get("azure_object_id")) + raise ValueError(f"No cloud-id generator for {access_type!r}") + + # ------------------------------------------------------------------ + # Secret operations — thin delegates to the SDK + # ------------------------------------------------------------------ + + def get_secret_value(self, name: str) -> str | None: + """Get a static secret value by path.""" + token = self.authenticate() + res = self.client.get_secret_value(akeyless.GetSecretValue(names=[name], token=token)) + return res.get(name) + + def get_secret_values(self, names: list[str]) -> dict[str, str]: + """Get multiple static secret values.""" + token = self.authenticate() + return dict(self.client.get_secret_value(akeyless.GetSecretValue(names=names, token=token))) + + def create_secret(self, name: str, value: str, description: str | None = None) -> None: + """Create a static secret.""" + token = self.authenticate() + body = akeyless.CreateSecret(name=name, value=value, token=token) + if description: + body.description = description + self.client.create_secret(body) + + def update_secret_value(self, name: str, value: str) -> None: + """Update a static secret's value.""" + token = self.authenticate() + self.client.update_secret_val(akeyless.UpdateSecretVal(name=name, value=value, token=token)) + + def delete_item(self, name: str) -> None: + """Delete a secret/item.""" + token = self.authenticate() + self.client.delete_item(akeyless.DeleteItem(name=name, token=token)) + + def describe_item(self, name: str) -> dict[str, Any] | None: + """Describe a secret/item (returns metadata).""" + token = self.authenticate() + return self.client.describe_item(akeyless.DescribeItem(name=name, token=token)).to_dict() + + def list_items(self, path: str = "/") -> list[dict[str, Any]]: + """List items under a path.""" + token = self.authenticate() + res = self.client.list_items(akeyless.ListItems(token=token, path=path)) + return [item.to_dict() for item in (res.items or [])] + + def get_dynamic_secret_value(self, name: str) -> dict[str, Any]: + """Generate a dynamic secret value (e.g. DB credentials).""" + token = self.authenticate() + res = self.client.get_dynamic_secret_value(akeyless.GetDynamicSecretValue(name=name, token=token)) + return res if isinstance(res, dict) else res.to_dict() + + def get_rotated_secret_value(self, name: str) -> dict[str, Any]: + """Retrieve a rotated secret value.""" + token = self.authenticate() + res = self.client.get_rotated_secret_value(akeyless.GetRotatedSecretValue(names=name, token=token)) Review Comment: `akeyless.GetRotatedSecretValue` is called with `names=name` (a string). In most Akeyless SDK request models, `names` is a list of secret names (similar to `GetSecretValue(names=[...])` above). Passing a string risks incorrect request serialization. Use a list for `names` (e.g. `[name]`) to match the SDK’s expected shape. ```suggestion res = self.client.get_rotated_secret_value(akeyless.GetRotatedSecretValue(names=[name], token=token)) ``` ########## providers/akeyless/src/airflow/providers/akeyless/hooks/akeyless.py: ########## @@ -0,0 +1,220 @@ +# 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. +"""Hook for Akeyless Vault Platform.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Any + +import akeyless + +from airflow.providers.common.compat.sdk import BaseHook + +VALID_AUTH_TYPES = ("api_key", "aws_iam", "gcp", "azure_ad", "uid", "jwt", "k8s", "certificate") + + +class AkeylessHook(BaseHook): + """ + Hook to interact with the Akeyless Vault Platform. + + Thin wrapper around the ``akeyless`` Python SDK. + + .. seealso:: + - https://docs.akeyless.io/ + - https://github.com/akeylesslabs/akeyless-python + + Connection fields: + + * **Host** -> API URL (default ``https://api.akeyless.io``) + * **Login** -> Access ID + * **Password** -> Access Key (for ``api_key`` auth) + * **Extra** -> JSON with ``access_type`` and auth-method-specific fields + + :param akeyless_conn_id: Airflow connection ID. + """ + + conn_name_attr = "akeyless_conn_id" + default_conn_name = "akeyless_default" + conn_type = "akeyless" + hook_name = "Akeyless" + + def __init__(self, akeyless_conn_id: str = default_conn_name, **kwargs: Any) -> None: + super().__init__() + self.akeyless_conn_id = akeyless_conn_id + self._conn = self.get_connection(akeyless_conn_id) + self._extra = self._conn.extra_dejson or {} + + @cached_property + def client(self) -> akeyless.V2Api: + """Return an ``akeyless.V2Api`` client (cached).""" + api_url = self._conn.host or self._extra.get("api_url", "https://api.akeyless.io") + if not api_url.startswith("http"): + api_url = f"https://{api_url}" + return akeyless.V2Api(akeyless.ApiClient(akeyless.Configuration(host=api_url))) + + def get_conn(self) -> akeyless.V2Api: + """Return the underlying ``akeyless.V2Api`` client.""" + return self.client + + def authenticate(self) -> str: + """ + Authenticate and return an API token. + + For ``uid`` auth the token is the UID token itself. + For all other methods, calls ``akeyless.Auth``. + """ + access_type = self._extra.get("access_type", "api_key") + + if access_type == "uid": + return self._extra["uid_token"] + + body = akeyless.Auth(access_id=self._conn.login) + + if access_type == "api_key": + body.access_key = self._conn.password + elif access_type in ("aws_iam", "gcp", "azure_ad"): + body.access_type = access_type + body.cloud_id = self._get_cloud_id(access_type) + elif access_type == "jwt": + body.access_type = "jwt" + body.jwt = self._extra.get("jwt") + elif access_type == "k8s": + body.access_type = "k8s" + body.k8s_auth_config_name = self._extra.get("k8s_auth_config_name") + elif access_type == "certificate": + body.access_type = "cert" + body.cert_data = self._extra.get("certificate_data") + body.key_data = self._extra.get("private_key_data") + + return self.client.auth(body).token + + def _get_cloud_id(self, access_type: str) -> str: + from akeyless_cloud_id import CloudId Review Comment: `akeyless_cloud_id` is an optional dependency, but importing it directly will raise a bare `ImportError` when users pick `aws_iam`/`gcp`/`azure_ad` auth without installing the extra. Catch `ImportError` and raise a clearer exception that explains how to install the dependency (e.g., `apache-airflow-providers-akeyless[cloud_id]`). ```suggestion try: from akeyless_cloud_id import CloudId except ImportError as exc: raise ImportError( "`akeyless_cloud_id` is required for `aws_iam`, `gcp`, and `azure_ad` " "authentication. Install it with " "`apache-airflow-providers-akeyless[cloud_id]`." ) from exc ``` ########## providers/akeyless/src/airflow/providers/akeyless/secrets/akeyless.py: ########## @@ -0,0 +1,156 @@ +# 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. +"""Secrets Backend for sourcing Connections, Variables, and Config from Akeyless.""" + +from __future__ import annotations + +import json +from functools import cached_property +from typing import TYPE_CHECKING, Any + +import akeyless + +from airflow.secrets import BaseSecretsBackend +from airflow.utils.log.logging_mixin import LoggingMixin + +if TYPE_CHECKING: + from airflow.models.connection import Connection + + +class AkeylessBackend(BaseSecretsBackend, LoggingMixin): + """ + Retrieve Connections, Variables, and Configuration from Akeyless. + + Configurable via ``airflow.cfg``: + + .. code-block:: ini + + [secrets] + backend = airflow.providers.akeyless.secrets.akeyless.AkeylessBackend + backend_kwargs = { + "connections_path": "/airflow/connections", + "variables_path": "/airflow/variables", + "api_url": "https://api.akeyless.io", + "access_id": "p-xxxx", + "access_key": "xxxx" + } + + Secrets are looked up by joining ``<base_path>/<key>``. + + :param connections_path: Akeyless path prefix for Connections (None to disable). + :param variables_path: Akeyless path prefix for Variables (None to disable). + :param config_path: Akeyless path prefix for Config (None to disable). + :param sep: Separator between base path and key. + :param api_url: Akeyless API endpoint. + :param access_id: Access ID. + :param access_key: Access Key (for ``api_key`` auth). + :param access_type: Auth type (default ``api_key``). + """ + + def __init__( + self, + connections_path: str | None = "/airflow/connections", + variables_path: str | None = "/airflow/variables", + config_path: str | None = "/airflow/config", + sep: str = "/", + api_url: str = "https://api.akeyless.io", + access_id: str | None = None, + access_key: str | None = None, + access_type: str = "api_key", + **kwargs: Any, + ) -> None: + super().__init__() + self.connections_path = connections_path.rstrip("/") if connections_path else None + self.variables_path = variables_path.rstrip("/") if variables_path else None + self.config_path = config_path.rstrip("/") if config_path else None + self.sep = sep + self._api_url = api_url + self._access_id = access_id + self._access_key = access_key + self._access_type = access_type + self._extra = kwargs + + @cached_property + def _client(self) -> akeyless.V2Api: + return akeyless.V2Api(akeyless.ApiClient(akeyless.Configuration(host=self._api_url))) + + def _authenticate(self) -> str: + """Return an API token via ``akeyless.Auth``.""" + if self._access_type == "uid": + return self._extra["uid_token"] + body = akeyless.Auth(access_id=self._access_id, access_key=self._access_key) + if self._access_type != "api_key": + body.access_type = self._access_type + return self._client.auth(body).token Review Comment: For non-`api_key` auth types (e.g. `aws_iam`, `gcp`, `azure_ad`, `jwt`, `k8s`, `certificate`), the backend sets `body.access_type` but does not populate the auth-method-specific fields that the Akeyless API typically requires (cloud id / jwt / k8s config / cert+key). This makes those `access_type` values effectively unusable in `AkeylessBackend`. Either implement the missing fields (mirroring `AkeylessHook.authenticate`) or explicitly restrict/validate `access_type` in the backend (e.g. only `api_key` and `uid`) and update docs accordingly. ########## providers/akeyless/src/airflow/providers/akeyless/secrets/akeyless.py: ########## @@ -0,0 +1,156 @@ +# 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. +"""Secrets Backend for sourcing Connections, Variables, and Config from Akeyless.""" + +from __future__ import annotations + +import json +from functools import cached_property +from typing import TYPE_CHECKING, Any + +import akeyless + +from airflow.secrets import BaseSecretsBackend +from airflow.utils.log.logging_mixin import LoggingMixin + +if TYPE_CHECKING: + from airflow.models.connection import Connection + + +class AkeylessBackend(BaseSecretsBackend, LoggingMixin): + """ + Retrieve Connections, Variables, and Configuration from Akeyless. + + Configurable via ``airflow.cfg``: + + .. code-block:: ini + + [secrets] + backend = airflow.providers.akeyless.secrets.akeyless.AkeylessBackend + backend_kwargs = { + "connections_path": "/airflow/connections", + "variables_path": "/airflow/variables", + "api_url": "https://api.akeyless.io", + "access_id": "p-xxxx", + "access_key": "xxxx" + } + + Secrets are looked up by joining ``<base_path>/<key>``. + + :param connections_path: Akeyless path prefix for Connections (None to disable). + :param variables_path: Akeyless path prefix for Variables (None to disable). + :param config_path: Akeyless path prefix for Config (None to disable). + :param sep: Separator between base path and key. + :param api_url: Akeyless API endpoint. + :param access_id: Access ID. + :param access_key: Access Key (for ``api_key`` auth). + :param access_type: Auth type (default ``api_key``). + """ + + def __init__( + self, + connections_path: str | None = "/airflow/connections", + variables_path: str | None = "/airflow/variables", + config_path: str | None = "/airflow/config", + sep: str = "/", + api_url: str = "https://api.akeyless.io", + access_id: str | None = None, + access_key: str | None = None, + access_type: str = "api_key", + **kwargs: Any, + ) -> None: + super().__init__() + self.connections_path = connections_path.rstrip("/") if connections_path else None + self.variables_path = variables_path.rstrip("/") if variables_path else None + self.config_path = config_path.rstrip("/") if config_path else None + self.sep = sep + self._api_url = api_url + self._access_id = access_id + self._access_key = access_key + self._access_type = access_type + self._extra = kwargs + + @cached_property + def _client(self) -> akeyless.V2Api: + return akeyless.V2Api(akeyless.ApiClient(akeyless.Configuration(host=self._api_url))) + + def _authenticate(self) -> str: + """Return an API token via ``akeyless.Auth``.""" + if self._access_type == "uid": + return self._extra["uid_token"] + body = akeyless.Auth(access_id=self._access_id, access_key=self._access_key) + if self._access_type != "api_key": + body.access_type = self._access_type + return self._client.auth(body).token + + def _get_secret(self, base_path: str | None, key: str) -> str | None: + if base_path is None: + return None + path = f"{base_path}{self.sep}{key}" + try: + token = self._authenticate() + res = self._client.get_secret_value(akeyless.GetSecretValue(names=[path], token=token)) + return res.get(path) Review Comment: Each secret lookup authenticates (`_authenticate()`) and fetches a new token. Secrets backends can be called frequently (e.g., during DAG parsing / task execution), so repeated auth requests can become a noticeable latency and rate-limit risk. Consider caching the token on the backend instance with a timestamp/TTL (and refreshing on auth failure), so multiple lookups reuse the same token when valid. ########## providers/akeyless/docs/index.rst: ########## @@ -0,0 +1,135 @@ + + .. 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. + Review Comment: This file starts with a blank line and then an indented comment block (` .. Licensed...`). In reStructuredText, leading indentation at the top level can trigger `Unexpected indentation` warnings (often treated as a block quote). Other new provider docs here use `.. Licensed...` without the extra leading space. Remove the initial blank line and align the license comment to column 1 for consistent, warning-free docs builds. ```suggestion .. 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. ``` ########## providers/akeyless/provider.yaml: ########## @@ -0,0 +1,104 @@ +# 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. + +--- +package-name: apache-airflow-providers-akeyless +name: Akeyless +description: | + `Akeyless <https://www.akeyless.io/>`__ Vault Platform provider for Apache Airflow. + Provides a Hook, Connection type, and Secrets Backend for managing static secrets, + dynamic secrets, rotated secrets, and more. + +state: ready +lifecycle: incubation +source-date-epoch: 1712361600 +versions: + - 1.0.0 + +integrations: + - integration-name: Akeyless + external-doc-url: https://docs.akeyless.io/ + tags: [software] Review Comment: Provider metadata is inconsistent: `provider.yaml` sets integration tags to `[software]`, while `get_provider_info.py` uses `['security', 'secrets']` and the PR description frames this as a secrets/security provider. Align tags across these two metadata sources to avoid confusing or conflicting categorization in generated provider docs/registry. ```suggestion tags: [security, secrets] ``` -- 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]
