From 7cdf9f0dd2f5f92cf50fa069deb533ce8eaefe75 Mon Sep 17 00:00:00 2001 From: "zainnadeem(RedOpsCell)" Date: Sat, 30 May 2026 23:05:25 +0500 Subject: [PATCH] Add pull option for container create and run Signed-off-by: zainnadeem(RedOpsCell) --- docker/api/container.py | 13 +++++--- docker/models/containers.py | 4 +++ tests/unit/api_container_test.py | 16 ++++++++++ tests/unit/models_containers_test.py | 45 ++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/docker/api/container.py b/docker/api/container.py index d1b870f9c2..f3eab435d2 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -226,7 +226,7 @@ def create_container(self, image, command=None, hostname=None, user=None, mac_address=None, labels=None, stop_signal=None, networking_config=None, healthcheck=None, stop_timeout=None, runtime=None, - use_config_proxy=True, platform=None): + use_config_proxy=True, platform=None, pull=None): """ Creates a container. Parameters are similar to those for the ``docker run`` command except it doesn't support the attach options (``-a``). @@ -408,6 +408,7 @@ def create_container(self, image, command=None, hostname=None, user=None, contains a proxy configuration, the corresponding environment variables will be set in the container being created. platform (str): Platform in the format ``os[/arch[/variant]]``. + pull (str): Pull image before creating the container. Returns: A dictionary with an image 'Id' key and a 'Warnings' key. @@ -437,12 +438,14 @@ def create_container(self, image, command=None, hostname=None, user=None, stop_signal, networking_config, healthcheck, stop_timeout, runtime ) - return self.create_container_from_config(config, name, platform) + return self.create_container_from_config(config, name, platform, pull) def create_container_config(self, *args, **kwargs): return ContainerConfig(self._version, *args, **kwargs) - def create_container_from_config(self, config, name=None, platform=None): + def create_container_from_config( + self, config, name=None, platform=None, pull=None + ): u = self._url("/containers/create") params = { 'name': name @@ -451,8 +454,10 @@ def create_container_from_config(self, config, name=None, platform=None): if utils.version_lt(self._version, '1.41'): raise errors.InvalidVersion( 'platform is not supported for API version < 1.41' - ) + ) params['platform'] = platform + if pull is not None: + params['pull'] = pull res = self._post_json(u, data=config, params=params) return self._result(res, True) diff --git a/docker/models/containers.py b/docker/models/containers.py index 9c9e92c90f..07f466eedb 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -853,6 +853,7 @@ def run(self, image, command=None, stdout=True, stderr=False, stream = kwargs.pop('stream', False) detach = kwargs.pop('detach', False) platform = kwargs.get('platform', None) + pull = kwargs.get('pull', None) if detach and remove: if version_gte(self.client.api._version, '1.25'): @@ -877,6 +878,8 @@ def run(self, image, command=None, stdout=True, stderr=False, container = self.create(image=image, command=command, detach=detach, **kwargs) except ImageNotFound: + if pull == 'never': + raise self.client.images.pull(image, platform=platform) container = self.create(image=image, command=command, detach=detach, **kwargs) @@ -1044,6 +1047,7 @@ def prune(self, filters=None): 'name', 'network_disabled', 'platform', + 'pull', 'stdin_open', 'stop_signal', 'tty', diff --git a/tests/unit/api_container_test.py b/tests/unit/api_container_test.py index b2e5237a2a..1f1646a6ef 100644 --- a/tests/unit/api_container_test.py +++ b/tests/unit/api_container_test.py @@ -369,6 +369,22 @@ def test_create_container_with_platform(self): assert args[1]['headers'] == {'Content-Type': 'application/json'} assert args[1]['params'] == {'name': None, 'platform': 'linux'} + def test_create_container_with_pull(self): + self.client.create_container('busybox', 'true', + pull='always') + + args = fake_request.call_args + assert args[0][1] == url_prefix + 'containers/create' + assert json.loads(args[1]['data']) == json.loads(''' + {"Tty": false, "Image": "busybox", "Cmd": ["true"], + "AttachStdin": false, + "AttachStderr": true, "AttachStdout": true, + "StdinOnce": false, + "OpenStdin": false, "NetworkDisabled": false} + ''') + assert args[1]['headers'] == {'Content-Type': 'application/json'} + assert args[1]['params'] == {'name': None, 'pull': 'always'} + def test_create_container_with_mem_limit_as_int(self): self.client.create_container( 'busybox', 'true', host_config=self.client.create_host_config( diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py index 0e2ae341a9..7ea8bd9b32 100644 --- a/tests/unit/models_containers_test.py +++ b/tests/unit/models_containers_test.py @@ -92,6 +92,7 @@ def test_create_container_args(self): 'platform': 'linux', 'ports': {1111: 4567, 2222: None}, 'privileged': True, + 'pull': 'always', 'publish_all_ports': True, 'read_only': True, 'restart_policy': {'Name': 'always'}, @@ -211,6 +212,7 @@ def test_create_container_args(self): }, 'platform': 'linux', 'ports': [('1111', 'tcp'), ('2222', 'tcp')], + 'pull': 'always', 'stdin_open': True, 'stop_signal': 9, 'tty': True, @@ -244,6 +246,19 @@ def test_run_detach(self): client.api.inspect_container.assert_called_with(FAKE_CONTAINER_ID) client.api.start.assert_called_with(FAKE_CONTAINER_ID) + def test_run_with_pull(self): + client = make_fake_client() + client.containers.run('alpine', pull='always') + client.api.create_container.assert_called_with( + image='alpine', + command=None, + detach=False, + pull='always', + host_config={ + 'NetworkMode': 'default', + } + ) + def test_run_pull(self): client = make_fake_client() @@ -260,6 +275,24 @@ def test_run_pull(self): 'alpine', platform=None, tag='latest', all_tags=False, stream=True ) + def test_run_pull_never_does_not_fallback(self): + client = make_fake_client() + client.api.create_container.side_effect = docker.errors.ImageNotFound("") + + with pytest.raises(docker.errors.ImageNotFound): + client.containers.run('alpine', pull='never') + + client.api.pull.assert_not_called() + client.api.create_container.assert_called_once_with( + image='alpine', + command=None, + detach=False, + pull='never', + host_config={ + 'NetworkMode': 'default', + } + ) + def test_run_with_error(self): client = make_fake_client() client.api.logs.return_value = "some error" @@ -484,6 +517,18 @@ def test_create(self): ) client.api.inspect_container.assert_called_with(FAKE_CONTAINER_ID) + def test_create_with_pull(self): + client = make_fake_client() + container = client.containers.create('alpine', pull='always') + assert isinstance(container, Container) + assert container.id == FAKE_CONTAINER_ID + client.api.create_container.assert_called_with( + image='alpine', + command=None, + pull='always', + host_config={'NetworkMode': 'default'} + ) + def test_create_with_image_object(self): client = make_fake_client() image = client.images.get(FAKE_IMAGE_ID)