This is an automated email from the ASF dual-hosted git repository.

potiuk 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 19e86725d60 Fix DockerOperator init crash on dict mount entries 
(#66553)
19e86725d60 is described below

commit 19e86725d60dc3862ab9b2685bd22016323ca41c
Author: Dov Benyomin Sohacheski <[email protected]>
AuthorDate: Sun May 10 20:35:14 2026 +0300

    Fix DockerOperator init crash on dict mount entries (#66553)
    
    * Fix DockerOperator init crash on dict mount entries
    
    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) <[email protected]>
    
    * Fixed lint
    
    * fix lint
    
    * Annotate self.mounts as list[Mount] to satisfy mypy
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
 .../airflow/providers/docker/operators/docker.py   | 13 +++++++---
 .../tests/unit/docker/operators/test_docker.py     | 30 ++++++++++++++++++++++
 2 files changed, 39 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 345a6c624f1..e81579a4f1c 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 @@ class DockerOperator(BaseOperator):
         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,8 @@ class DockerOperator(BaseOperator):
         self.mount_tmp_dir = mount_tmp_dir
         self.tmp_dir = tmp_dir
         self.user = user
-        self.mounts = mounts or []
+        mounts = [mount if isinstance(mount, Mount) else Mount(**mount) for 
mount in (mounts or [])]
+        self.mounts: list[Mount] = mounts
         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 d375bd577ce..a753894d48f 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 @@ class TestDockerOperator:
         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}"

Reply via email to