From e9f0095c3ec50e2adf19baaf56f2f5a0c74268a2 Mon Sep 17 00:00:00 2001 From: Dov Benyomin Sohacheski Date: Thu, 7 May 2026 17:48:17 +0300 Subject: [PATCH 1/4] Fix DockerOperator init crash on dict mount entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #52451 added a loop in ``DockerOperator.__init__`` that assigns ``template_fields`` on every entry of ``mounts``. Plain ``dict`` entries have no ``__dict__`` slot, so the assignment raises:: AttributeError: 'dict' object has no attribute 'template_fields' and no __dict__ for setting new attributes at construction time, breaking any DAG that historically passed mount entries as ``dict`` (a documented and previously supported form). Fixes the regression by normalising each entry to ``docker.types.Mount`` on input — ``Mount`` instances are passed through, and ``dict`` entries are unpacked into ``Mount(**entry)``. The subsequent ``template_fields`` assignment then operates on a uniform ``Mount`` (a ``dict`` subclass that does carry an instance ``__dict__``). Closes: #66345 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../providers/docker/operators/docker.py | 14 ++++++--- .../unit/docker/operators/test_docker.py | 30 +++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/providers/docker/src/airflow/providers/docker/operators/docker.py b/providers/docker/src/airflow/providers/docker/operators/docker.py index 345a6c624f1a7..a2174acb01790 100644 --- a/providers/docker/src/airflow/providers/docker/operators/docker.py +++ b/providers/docker/src/airflow/providers/docker/operators/docker.py @@ -151,8 +151,12 @@ class DockerOperator(BaseOperator): The path is also made available via the environment variable ``AIRFLOW_TMP_DIR`` inside the container. :param user: Default user inside the docker container. - :param mounts: List of volumes to mount into the container. Each item should - be a :py:class:`docker.types.Mount` instance. (templated) + :param mounts: List of volumes to mount into the container. Each item may + be a :py:class:`docker.types.Mount` instance, or a ``dict`` of + :py:class:`~docker.types.Mount` keyword arguments (e.g. + ``{"target": "/data", "source": "vol", "type": "volume"}``); ``dict`` + entries are converted to ``Mount`` instances at construction time. + (templated) :param entrypoint: Overwrite the default ENTRYPOINT of the image :param working_dir: Working directory to set on the container (equivalent to the -w switch the docker client) @@ -245,7 +249,7 @@ def __init__( mount_tmp_dir: bool = True, tmp_dir: str = "/tmp/airflow", user: str | int | None = None, - mounts: list[Mount] | None = None, + mounts: list[Mount | dict] | None = None, entrypoint: str | list[str] | None = None, working_dir: str | None = None, xcom_all: bool = False, @@ -304,7 +308,9 @@ def __init__( self.mount_tmp_dir = mount_tmp_dir self.tmp_dir = tmp_dir self.user = user - self.mounts = mounts or [] + self.mounts = [ + mount if isinstance(mount, Mount) else Mount(**mount) for mount in (mounts or []) + ] for mount in self.mounts: mount.template_fields = ("Source", "Target", "Type") self.entrypoint = entrypoint diff --git a/providers/docker/tests/unit/docker/operators/test_docker.py b/providers/docker/tests/unit/docker/operators/test_docker.py index d375bd577ce2d..a753894d48f8c 100644 --- a/providers/docker/tests/unit/docker/operators/test_docker.py +++ b/providers/docker/tests/unit/docker/operators/test_docker.py @@ -818,3 +818,33 @@ def test_basic_docker_operator_with_template_fields(self, create_task_instance_o rendered = ti.render_templates() assert rendered.container_name == f"python_{ti.dag_id}" assert rendered.mounts[0]["Target"] == f"/{ti.run_id}" + + def test_dict_mounts_are_normalized_to_mount_objects(self): + op = DockerOperator( + task_id="test", + image="test", + mounts=[ + {"target": "/data", "source": "workspace", "type": "volume", "read_only": False}, + Mount(target="/logs", source="logs", type="volume"), + ], + ) + assert all(isinstance(m, Mount) for m in op.mounts) + assert op.mounts[0]["Target"] == "/data" + assert op.mounts[0]["Source"] == "workspace" + assert op.mounts[0]["Type"] == "volume" + assert op.mounts[0]["ReadOnly"] is False + assert op.mounts[1]["Target"] == "/logs" + + @pytest.mark.db_test + def test_dict_mounts_are_templated(self, create_task_instance_of_operator): + ti = create_task_instance_of_operator( + operator_class=DockerOperator, + dag_id="test", + task_id="test", + image="test", + mounts=[ + {"target": "/{{task_instance.run_id}}", "source": "workspace", "type": "volume"}, + ], + ) + rendered = ti.render_templates() + assert rendered.mounts[0]["Target"] == f"/{ti.run_id}" From 2d364bcacc03de4e1a3236f0631cf8911506c459 Mon Sep 17 00:00:00 2001 From: Dov Benyomin Sohacheski Date: Thu, 7 May 2026 21:33:15 +0300 Subject: [PATCH 2/4] Fixed lint --- .../docker/src/airflow/providers/docker/operators/docker.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/providers/docker/src/airflow/providers/docker/operators/docker.py b/providers/docker/src/airflow/providers/docker/operators/docker.py index a2174acb01790..f93d9e7f71228 100644 --- a/providers/docker/src/airflow/providers/docker/operators/docker.py +++ b/providers/docker/src/airflow/providers/docker/operators/docker.py @@ -308,9 +308,7 @@ def __init__( self.mount_tmp_dir = mount_tmp_dir self.tmp_dir = tmp_dir self.user = user - self.mounts = [ - mount if isinstance(mount, Mount) else Mount(**mount) for mount in (mounts or []) - ] + self.mounts = [mount if isinstance(mount, Mount) else Mount(**mount) for mount in (mounts or [])] for mount in self.mounts: mount.template_fields = ("Source", "Target", "Type") self.entrypoint = entrypoint From f20d0499c13761118f8b3579054bab13ebe3cb62 Mon Sep 17 00:00:00 2001 From: Dov Benyomin Sohacheski Date: Sun, 10 May 2026 07:36:56 +0300 Subject: [PATCH 3/4] fix lint --- .../docker/src/airflow/providers/docker/operators/docker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/providers/docker/src/airflow/providers/docker/operators/docker.py b/providers/docker/src/airflow/providers/docker/operators/docker.py index f93d9e7f71228..740e07cc3b611 100644 --- a/providers/docker/src/airflow/providers/docker/operators/docker.py +++ b/providers/docker/src/airflow/providers/docker/operators/docker.py @@ -308,7 +308,8 @@ def __init__( self.mount_tmp_dir = mount_tmp_dir self.tmp_dir = tmp_dir self.user = user - self.mounts = [mount if isinstance(mount, Mount) else Mount(**mount) for mount in (mounts or [])] + mounts = [mount if isinstance(mount, Mount) else Mount(**mount) for mount in (mounts or [])] + self.mounts = mounts for mount in self.mounts: mount.template_fields = ("Source", "Target", "Type") self.entrypoint = entrypoint From 6c998a74c7b6d1bb28fb4317b19fda1caf6e41c6 Mon Sep 17 00:00:00 2001 From: Dov Benyomin Sohacheski Date: Sun, 10 May 2026 08:46:36 +0300 Subject: [PATCH 4/4] Annotate self.mounts as list[Mount] to satisfy mypy --- .../docker/src/airflow/providers/docker/operators/docker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/docker/src/airflow/providers/docker/operators/docker.py b/providers/docker/src/airflow/providers/docker/operators/docker.py index 740e07cc3b611..e81579a4f1c23 100644 --- a/providers/docker/src/airflow/providers/docker/operators/docker.py +++ b/providers/docker/src/airflow/providers/docker/operators/docker.py @@ -309,7 +309,7 @@ def __init__( self.tmp_dir = tmp_dir self.user = user mounts = [mount if isinstance(mount, Mount) else Mount(**mount) for mount in (mounts or [])] - self.mounts = mounts + self.mounts: list[Mount] = mounts for mount in self.mounts: mount.template_fields = ("Source", "Target", "Type") self.entrypoint = entrypoint