diff --git a/airflow/providers/docker/hooks/docker.py b/airflow/providers/docker/hooks/docker.py index 763c3a787744b..18ccfd7689ba9 100644 --- a/airflow/providers/docker/hooks/docker.py +++ b/airflow/providers/docker/hooks/docker.py @@ -18,6 +18,8 @@ from __future__ import annotations import json +import sys +import warnings from functools import cached_property from typing import TYPE_CHECKING, Any @@ -103,15 +105,42 @@ def construct_tls_config( :param ssl_version: Version of SSL to use when communicating with docker daemon. """ if ca_cert and client_cert and client_key: - # Ignore type error on SSL version here. - # It is deprecated and type annotation is wrong, and it should be string. - return TLSConfig( - ca_cert=ca_cert, - client_cert=(client_cert, client_key), - verify=verify, - ssl_version=ssl_version, - assert_hostname=assert_hostname, - ) + from packaging.version import Version + + if sys.version_info >= (3, 9): + from importlib.metadata import version + else: + from importlib_metadata import version + + tls_config = { + "ca_cert": ca_cert, + "client_cert": (client_cert, client_key), + "verify": verify, + "assert_hostname": assert_hostname, + "ssl_version": ssl_version, + } + + docker_py_version = Version(version("docker")) + if docker_py_version.major >= 7: + # `ssl_version` and `assert_hostname` removed into the `docker>=7` + # see: https://github.com/docker/docker-py/pull/3185 + if tls_config.pop("ssl_version", None) is not None: + warnings.warn( + f"`ssl_version` removed in `docker.TLSConfig` constructor arguments " + f"since `docker>=7`, but you use {docker_py_version}. " + f"This parameter does not have any affect.", + UserWarning, + stacklevel=2, + ) + if tls_config.pop("assert_hostname", None) is not None: + warnings.warn( + f"`assert_hostname` removed in `docker.TLSConfig` constructor arguments " + f"since `docker>=7`, but you use {docker_py_version}. " + f"This parameter does not have any affect.", + UserWarning, + stacklevel=2, + ) + return TLSConfig(**tls_config) return False @cached_property diff --git a/airflow/providers/docker/provider.yaml b/airflow/providers/docker/provider.yaml index c67d237f30372..5239ea9b7fb49 100644 --- a/airflow/providers/docker/provider.yaml +++ b/airflow/providers/docker/provider.yaml @@ -63,7 +63,7 @@ versions: dependencies: - apache-airflow>=2.6.0 - - docker>=5.0.3 + - docker>=6 - python-dotenv>=0.21.0 integrations: diff --git a/generated/provider_dependencies.json b/generated/provider_dependencies.json index a89c2b918c54b..781fbdd334bca 100644 --- a/generated/provider_dependencies.json +++ b/generated/provider_dependencies.json @@ -428,7 +428,7 @@ "docker": { "deps": [ "apache-airflow>=2.6.0", - "docker>=5.0.3", + "docker>=6", "python-dotenv>=0.21.0" ], "devel-deps": [], diff --git a/pyproject.toml b/pyproject.toml index 9c5739bcab536..f47c5dbfc0052 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -679,7 +679,7 @@ discord = [ # source: airflow/providers/discord/provider.yaml "apache-airflow[http]", ] docker = [ # source: airflow/providers/docker/provider.yaml - "docker>=5.0.3", + "docker>=6", "python-dotenv>=0.21.0", ] elasticsearch = [ # source: airflow/providers/elasticsearch/provider.yaml diff --git a/tests/providers/docker/hooks/test_docker.py b/tests/providers/docker/hooks/test_docker.py index d96c8af275f04..758ab7b808db8 100644 --- a/tests/providers/docker/hooks/test_docker.py +++ b/tests/providers/docker/hooks/test_docker.py @@ -19,15 +19,26 @@ import logging import ssl +import sys +import warnings from unittest import mock import pytest from docker import TLSConfig from docker.errors import APIError +from packaging.version import Version from airflow.exceptions import AirflowException, AirflowNotFoundException from airflow.providers.docker.hooks.docker import DockerHook +if sys.version_info >= (3, 9): + from importlib.metadata import version +else: + from importlib_metadata import version + + +DOCKER_PY_7_PLUS = Version(Version(version("docker")).base_version) >= Version("7") + TEST_CONN_ID = "docker_test_connection" TEST_BASE_URL = "unix://var/run/docker.sock" TEST_TLS_BASE_URL = "tcp://localhost.foo.bar" @@ -251,8 +262,8 @@ def test_construct_tls_config_missing_certs_args(tls_params: dict): @pytest.mark.parametrize( "ssl_version", [ - pytest.param(ssl.PROTOCOL_TLSv1, id="TLSv1"), - pytest.param(ssl.PROTOCOL_TLSv1_2, id="TLSv1_2"), + # Other version-based constraints in `ssl` module marked as deprecated since Python 3.6. + pytest.param(ssl.PROTOCOL_TLS_CLIENT, id="auto-negotiate"), None, ], ) @@ -265,8 +276,23 @@ def test_construct_tls_config(assert_hostname, ssl_version): if ssl_version is not None: tls_params["ssl_version"] = ssl_version - with mock.patch.object(TLSConfig, "__init__", return_value=None) as mock_tls_config: - DockerHook.construct_tls_config(**tls_params) - mock_tls_config.assert_called_once_with( - **expected_call_args, assert_hostname=assert_hostname, ssl_version=ssl_version - ) + if DOCKER_PY_7_PLUS and (assert_hostname is not None or ssl_version is not None): + ctx = pytest.warns(UserWarning, match="removed in `docker\.TLSConfig` constructor arguments") + no_warns = False + else: + ctx = warnings.catch_warnings() + no_warns = True + + # Please note that spec should be set; otherwise we could miss removal into the constructor arguments. + with mock.patch.object(TLSConfig, "__init__", return_value=None, spec=TLSConfig) as mock_tls_config: + with ctx: + if no_warns: + warnings.simplefilter("error") + DockerHook.construct_tls_config(**tls_params) + + if DOCKER_PY_7_PLUS: + mock_tls_config.assert_called_once_with(**expected_call_args) + else: + mock_tls_config.assert_called_once_with( + **expected_call_args, assert_hostname=assert_hostname, ssl_version=ssl_version + )