This is an automated email from the ASF dual-hosted git repository.
jason810496 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 114d19c0aab Add `MskHook` (#69000)
114d19c0aab is described below
commit 114d19c0aab19c715d2f4d23527b17b73e9a1dc7
Author: Yi Lin <[email protected]>
AuthorDate: Wed Sep 16 01:24:10 2026 -0400
Add `MskHook` (#69000)
* feat: Add Hook for Amazon MSK
* feat: Add confluenttoken for Amazon MSK
* test: Update unit test for MskHook
---------
Co-authored-by: Arnold Lin <[email protected]>
---
providers/amazon/README.rst | 1 +
providers/amazon/docs/index.rst | 1 +
providers/amazon/provider.yaml | 7 ++-
providers/amazon/pyproject.toml | 3 +
.../src/airflow/providers/amazon/aws/hooks/msk.py | 66 ++++++++++++++++++++++
.../airflow/providers/amazon/get_provider_info.py | 6 ++
.../amazon/tests/unit/amazon/aws/hooks/test_msk.py | 63 +++++++++++++++++++++
uv.lock | 6 +-
8 files changed, 151 insertions(+), 2 deletions(-)
diff --git a/providers/amazon/README.rst b/providers/amazon/README.rst
index fe35a493b0b..d6678558c10 100644
--- a/providers/amazon/README.rst
+++ b/providers/amazon/README.rst
@@ -111,6 +111,7 @@ Extra Dependencies
``aiobotocore`` ``aiobotocore>=3.0.0``
``cncf.kubernetes`` ``apache-airflow-providers-cncf-kubernetes>=7.2.0``
``s3fs`` ``s3fs>=2023.10.0``
+``msk`` ``aws-msk-iam-sasl-signer-python>=1.0.1``
``python3-saml`` ``python3-saml>=1.16.0; python_version < '3.13'``,
``xmlsec>=1.3.14; python_version < '3.13'``, ``lxml>=6.0.0; python_version <
'3.13'``
``apache.hive`` ``apache-airflow-providers-apache-hive``
``exasol`` ``apache-airflow-providers-exasol``
diff --git a/providers/amazon/docs/index.rst b/providers/amazon/docs/index.rst
index 3510b1d3784..f134f522e99 100644
--- a/providers/amazon/docs/index.rst
+++ b/providers/amazon/docs/index.rst
@@ -178,6 +178,7 @@ Extra Dependencies
``aiobotocore`` ``aiobotocore>=3.0.0``
``cncf.kubernetes`` ``apache-airflow-providers-cncf-kubernetes>=7.2.0``
``s3fs`` ``s3fs>=2023.10.0``
+``msk`` ``aws-msk-iam-sasl-signer-python>=1.0.1``
``python3-saml`` ``python3-saml>=1.16.0; python_version < '3.13'``,
``xmlsec>=1.3.14; python_version < '3.13'``, ``lxml>=6.0.0; python_version <
'3.13'``
``apache.hive`` ``apache-airflow-providers-apache-hive``
``exasol`` ``apache-airflow-providers-exasol``
diff --git a/providers/amazon/provider.yaml b/providers/amazon/provider.yaml
index b7731887e17..97c218c2f84 100644
--- a/providers/amazon/provider.yaml
+++ b/providers/amazon/provider.yaml
@@ -418,6 +418,9 @@ integrations:
how-to-guide:
- /docs/apache-airflow-providers-amazon/operators/neptune_analytics.rst
tags: [aws]
+ - integration-name: Amazon MSK
+ external-doc-url: https://aws.amazon.com/msk/
+ tags: [aws]
- integration-name: Amazon S3 Vectors
external-doc-url:
https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-vectors.html
logo:
/docs/integration-logos/[email protected]
@@ -808,7 +811,9 @@ hooks:
python-modules:
- airflow.providers.amazon.aws.hooks.neptune
- airflow.providers.amazon.aws.hooks.neptune_analytics
-
+ - integration-name: Amazon MSK
+ python-modules:
+ - airflow.providers.amazon.aws.hooks.msk
bundles:
- integration-name: Amazon Simple Storage Service (S3)
diff --git a/providers/amazon/pyproject.toml b/providers/amazon/pyproject.toml
index bfb06b198d8..f59258095ee 100644
--- a/providers/amazon/pyproject.toml
+++ b/providers/amazon/pyproject.toml
@@ -97,6 +97,9 @@ dependencies = [
"s3fs" = [
"s3fs>=2023.10.0",
]
+"msk" = [
+ "aws-msk-iam-sasl-signer-python>=1.0.1",
+]
"python3-saml" = [
# Python 3 saml is not compatible with Python 3.13 yet, so we pin it to <
3.13
"python3-saml>=1.16.0; python_version < '3.13'",
diff --git a/providers/amazon/src/airflow/providers/amazon/aws/hooks/msk.py
b/providers/amazon/src/airflow/providers/amazon/aws/hooks/msk.py
new file mode 100644
index 00000000000..e86404b5f06
--- /dev/null
+++ b/providers/amazon/src/airflow/providers/amazon/aws/hooks/msk.py
@@ -0,0 +1,66 @@
+#
+# 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.
+"""This module contains Amazon Managed Streaming for Apache Kafka hook."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from botocore.credentials import CredentialProvider
+
+from airflow.providers.amazon.aws.hooks.base_aws import AwsBaseHook
+
+if TYPE_CHECKING:
+ from botocore.credentials import Credentials
+
+
+class _MskCredentialsProvider(CredentialProvider):
+ METHOD = "airflow"
+
+ def __init__(self, hook: MskHook) -> None:
+ self.hook = hook
+
+ def load(self) -> Credentials | None:
+ return
self.hook.get_session(region_name=self.hook.region_name).get_credentials()
+
+
+class MskHook(AwsBaseHook):
+ """
+ Create an authenticated Amazon Managed Streaming for Apache Kafka client.
+
+ Additional arguments, such as ``aws_conn_id`` or ``region_name``, are
passed
+ to :class:`~airflow.providers.amazon.aws.hooks.base_aws.AwsBaseHook`. Use
+ ``conn`` or ``get_conn()`` to access the underlying
``boto3.client("kafka")``
+ directly.
+ """
+
+ def __init__(self, *args, **kwargs) -> None:
+ kwargs["client_type"] = "kafka"
+ super().__init__(*args, **kwargs)
+
+ def confluent_token(self, config_str: str) -> tuple[str, float]:
+ """Generate an Amazon MSK IAM token for a ``confluent_kafka`` OAuth
callback."""
+ if not self.region_name:
+ raise ValueError("AWS region is required to generate an Amazon MSK
IAM token")
+
+ from aws_msk_iam_sasl_signer import MSKAuthTokenProvider
+
+ token, expiry_ms =
MSKAuthTokenProvider.generate_auth_token_from_credentials_provider(
+ self.region_name, _MskCredentialsProvider(self)
+ )
+ return token, expiry_ms / 1000
diff --git a/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
b/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
index 1038b5d698f..0a093ba5b5c 100644
--- a/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
+++ b/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
@@ -380,6 +380,11 @@ def get_provider_info():
"how-to-guide":
["/docs/apache-airflow-providers-amazon/operators/neptune_analytics.rst"],
"tags": ["aws"],
},
+ {
+ "integration-name": "Amazon MSK",
+ "external-doc-url": "https://aws.amazon.com/msk/",
+ "tags": ["aws"],
+ },
{
"integration-name": "Amazon S3 Vectors",
"external-doc-url":
"https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-vectors.html",
@@ -898,6 +903,7 @@ def get_provider_info():
"airflow.providers.amazon.aws.hooks.neptune_analytics",
],
},
+ {"integration-name": "Amazon MSK", "python-modules":
["airflow.providers.amazon.aws.hooks.msk"]},
],
"bundles": [
{
diff --git a/providers/amazon/tests/unit/amazon/aws/hooks/test_msk.py
b/providers/amazon/tests/unit/amazon/aws/hooks/test_msk.py
new file mode 100644
index 00000000000..846e162587a
--- /dev/null
+++ b/providers/amazon/tests/unit/amazon/aws/hooks/test_msk.py
@@ -0,0 +1,63 @@
+#
+# 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.
+from __future__ import annotations
+
+from unittest import mock
+
+import pytest
+from botocore.credentials import CredentialProvider
+
+from airflow.providers.amazon.aws.hooks.msk import MskHook
+
+MOCK_MSK_SIGNER_MODULE = mock.MagicMock()
+
+
+class TestMskHook:
+ def setup_method(self):
+ self.hook = MskHook(aws_conn_id="aws_msk", region_name="us-east-1")
+
+ def test_init(self):
+ assert self.hook.aws_conn_id == "aws_msk"
+ assert self.hook.client_type == "kafka"
+
+ @mock.patch.dict("sys.modules", {"aws_msk_iam_sasl_signer":
MOCK_MSK_SIGNER_MODULE})
+ @mock.patch.object(MskHook, "get_session")
+ def test_confluent_token(self, mock_get_session):
+ credentials =
mock_get_session.return_value.get_credentials.return_value
+ mock_generate_auth_token = (
+
MOCK_MSK_SIGNER_MODULE.MSKAuthTokenProvider.generate_auth_token_from_credentials_provider
+ )
+ mock_generate_auth_token.reset_mock()
+ mock_generate_auth_token.return_value = ("token", 1_700_000_900_000)
+
+ token, expiry = self.hook.confluent_token("")
+
+ region, credentials_provider = mock_generate_auth_token.call_args.args
+ assert region == "us-east-1"
+ assert isinstance(credentials_provider, CredentialProvider)
+ assert credentials_provider.load() is credentials
+ mock_get_session.assert_called_once_with(region_name="us-east-1")
+ assert token == "token"
+ assert expiry == 1_700_000_900.0
+
+ @mock.patch.object(MskHook, "region_name", new_callable=mock.PropertyMock)
+ def test_confluent_token_requires_region(self, mock_region_name):
+ mock_region_name.return_value = None
+
+ with pytest.raises(ValueError, match="AWS region is required"):
+ self.hook.confluent_token("")
diff --git a/uv.lock b/uv.lock
index 7eea41e3c96..0d0476a4373 100644
--- a/uv.lock
+++ b/uv.lock
@@ -3142,6 +3142,9 @@ microsoft-azure = [
mongo = [
{ name = "apache-airflow-providers-mongo" },
]
+msk = [
+ { name = "aws-msk-iam-sasl-signer-python" },
+]
openlineage = [
{ name = "apache-airflow-providers-openlineage" },
]
@@ -3231,6 +3234,7 @@ requires-dist = [
{ name = "apache-airflow-providers-standard", marker = "extra ==
'standard'", editable = "providers/standard" },
{ name = "asgiref", marker = "python_full_version < '3.14'", specifier =
">=2.3.0" },
{ name = "asgiref", marker = "python_full_version >= '3.14'", specifier =
">=3.11.1" },
+ { name = "aws-msk-iam-sasl-signer-python", marker = "extra == 'msk'",
specifier = ">=1.0.1" },
{ name = "boto3", specifier = ">=1.41.0" },
{ name = "botocore", specifier = ">=1.41.0" },
{ name = "inflection", specifier = ">=0.5.1" },
@@ -3251,7 +3255,7 @@ requires-dist = [
{ name = "watchtower", specifier = ">=3.3.1,<4" },
{ name = "xmlsec", marker = "python_full_version < '3.13' and extra ==
'python3-saml'", specifier = ">=1.3.14" },
]
-provides-extras = ["aiobotocore", "cncf-kubernetes", "s3fs", "python3-saml",
"apache-hive", "exasol", "fab", "ftp", "google", "imap", "microsoft-azure",
"mongo", "pandas", "openlineage", "salesforce", "ssh", "standard",
"common-messaging", "sqlalchemy"]
+provides-extras = ["aiobotocore", "cncf-kubernetes", "s3fs", "msk",
"python3-saml", "apache-hive", "exasol", "fab", "ftp", "google", "imap",
"microsoft-azure", "mongo", "pandas", "openlineage", "salesforce", "ssh",
"standard", "common-messaging", "sqlalchemy"]
[package.metadata.requires-dev]
dev = [