Compare commits

...

22 Commits
7.1.0 ... main

Author SHA1 Message Date
Paweł Gronowski 6e6a273573
Merge pull request #3270 from Khushiyant/volume-subpath
Implement Subpath Support for Volumes in Docker-Py (#3243)
2025-06-11 09:21:35 +00:00
Sebastiaan van Stijn 526a9db743
Merge pull request #3336 from thaJeztah/fix_onbuild_assert
integration: adjust tests for omitted "OnBuild"
2025-05-22 10:44:55 +02:00
Sebastiaan van Stijn e5c3eb18b6
integration: adjust tests for omitted "OnBuild"
The Docker API may either return an empty "OnBuild" or omit the
field altogether if it's not set.

Adjust the tests to make either satisfy the test.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-05-22 01:46:53 +02:00
Khushiyant 820769e23c feat(docker/api/container): add support for subpath in volume_opts
TESTED: Yes, added unit tests to verify subpath functionality
Signed-off-by: Khushiyant <khushiyant2002@gmail.com>
2025-03-18 23:16:03 +05:30
Shaun Thompson db7f8b8bb6
Merge pull request #3296 from thaJeztah/fix_test_create_volume_invalid_driver
integration: test_create_volume_invalid_driver allow either 400 or 404
2025-01-17 12:32:14 -05:00
Shaun Thompson 747d23b9d7
Merge pull request #3307 from thaJeztah/deprecated_json_error
image load: don't depend on deprecated JSONMessage.error field
2025-01-17 12:30:54 -05:00
Sebastiaan van Stijn fad84c371a
integration: test_create_volume_invalid_driver allow either 400 or 404
The API currently returns a 404 error when trying to create a volume with
an invalid (non-existing) driver. We are considering changing this status
code to be a 400 (invalid parameter), as even though the _reason_ of the
error may be that the plugin / driver is not found, the _cause_ of the
error is that the user provided a plugin / driver that's invalid for the
engine they're connected to.

This patch updates the test to pass for either case.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-01-14 14:23:24 +01:00
Sebastiaan van Stijn 5a8a42466e
image load: don't depend on deprecated JSONMessage.error field
The error field  was deprecated in favor of the errorDetail struct in
[moby@3043c26], but the API continued to return both. This patch updates
docker-py to not depend on the deprecated field.

[moby@3043c26]: 3043c26419

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-01-14 13:23:38 +01:00
Sebastiaan van Stijn 03e43be6af
Merge pull request #3297 from thaJeztah/fix_makefile_circref
Makefile: fix circular reference for integration-dind
2024-11-18 18:03:11 +01:00
Sebastiaan van Stijn 80a584651b
Merge pull request #2442 from thaJeztah/test_service_logs_un_experimental
test_service_logs: stop testing experimental versions
2024-11-18 18:02:30 +01:00
Sebastiaan van Stijn 8ee28517c7
test_service_logs: stop testing experimental versions
Service logs are no longer experimental, so updating the tests
to only test against "stable"  implementations, and no longer
test the experimental ones.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-11-18 16:09:21 +01:00
Sebastiaan van Stijn d9f9b965b2
Makefile: fix circular reference for integration-dind
Noticed this warning;

    make: Circular integration-dind <- integration-dind dependency dropped.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-11-18 16:07:01 +01:00
Bjorn Neergaard fba6ffe297
Merge pull request #3267 from thaJeztah/add_default_version
Set a dummy-version if none set, and remove unused APT_MIRROR build-arg
2024-11-18 07:48:00 -07:00
Sebastiaan van Stijn 99ce2e6d56
Makefile: remove unused APT_MIRROR build-arg
The APT_MIRROR build-arg was removed from the Dockerfile in commit
ee2310595d, but wasn't removed from the
Makefile.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-11-18 15:41:24 +01:00
Sebastiaan van Stijn 504ce6193c
Set a dummy-version if none set
Make sure the Dockerfiles can be built even if no VERSION build-arg
is passed.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-11-18 15:41:19 +01:00
Sebastiaan van Stijn bb0edd1f66
Merge pull request #3261 from thaJeztah/bump_engine_versions
Bump default API version to 1.45 (Moby 26.0/26.1)
2024-10-27 17:09:14 +01:00
Sebastiaan van Stijn e47e966e94
Bump default API version to 1.45 (Moby 26.0/26.1)
- Update API version to the latest maintained release.
0 Adjust tests for API 1.45

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-27 17:03:02 +01:00
Sebastiaan van Stijn a8bac88221
Merge pull request #3292 from yasonk/fix-exec_run-doc
fixing doc for stream param in exec_run
2024-09-30 23:22:45 +02:00
Sebastiaan van Stijn e031cf0c23
Merge pull request #3290 from laurazard/exec-no-executable-exit-code
tests/exec: expect 127 exit code for missing executable
2024-09-30 15:18:31 +02:00
Laura Brehm b1265470e6
tests/exec: add test for exit code from exec
Execs should return the exit code of the exec'd process, if it started.

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-09-30 14:07:04 +01:00
yasonk 6bbf741c8c
fixing doc for stream param in exec_run
Signed-off-by: yasonk <yason@hey.com>
2024-09-29 18:58:38 -07:00
Laura Brehm 96ef4d3bee
tests/exec: expect 127 exit code for missing executable
Docker Engine has always returned `126` when starting an exec fails due
to a missing binary, but this was due to a bug in the daemon causing the
correct exit code to be overwritten in some cases – see: https://github.com/moby/moby/issues/45795

Change tests to expect correct exit code (`127`).

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-09-27 15:33:11 +01:00
15 changed files with 101 additions and 34 deletions

View File

@ -6,7 +6,7 @@ FROM python:${PYTHON_VERSION}
WORKDIR /src WORKDIR /src
COPY . . COPY . .
ARG VERSION ARG VERSION=0.0.0.dev0
RUN --mount=type=cache,target=/cache/pip \ RUN --mount=type=cache,target=/cache/pip \
PIP_CACHE_DIR=/cache/pip \ PIP_CACHE_DIR=/cache/pip \
SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} \ SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} \

View File

@ -13,7 +13,7 @@ RUN addgroup --gid $gid sphinx \
WORKDIR /src WORKDIR /src
COPY . . COPY . .
ARG VERSION ARG VERSION=0.0.0.dev0
RUN --mount=type=cache,target=/cache/pip \ RUN --mount=type=cache,target=/cache/pip \
PIP_CACHE_DIR=/cache/pip \ PIP_CACHE_DIR=/cache/pip \
SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} \ SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} \

View File

@ -1,5 +1,5 @@
TEST_API_VERSION ?= 1.44 TEST_API_VERSION ?= 1.45
TEST_ENGINE_VERSION ?= 25.0 TEST_ENGINE_VERSION ?= 26.1
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
PLATFORM := Windows PLATFORM := Windows
@ -13,7 +13,7 @@ endif
SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER ?= $(shell git describe --match '[0-9]*' --dirty='.m' --always --tags 2>/dev/null | sed -r 's/-([0-9]+)/.dev\1/' | sed 's/-/+/') SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER ?= $(shell git describe --match '[0-9]*' --dirty='.m' --always --tags 2>/dev/null | sed -r 's/-([0-9]+)/.dev\1/' | sed 's/-/+/')
ifeq ($(SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER),) ifeq ($(SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER),)
SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER = "dev" SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER = "0.0.0.dev0"
endif endif
.PHONY: all .PHONY: all
@ -33,7 +33,7 @@ build-dind-ssh:
--build-arg VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER} \ --build-arg VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER} \
--build-arg ENGINE_VERSION=${TEST_ENGINE_VERSION} \ --build-arg ENGINE_VERSION=${TEST_ENGINE_VERSION} \
--build-arg API_VERSION=${TEST_API_VERSION} \ --build-arg API_VERSION=${TEST_API_VERSION} \
--build-arg APT_MIRROR . .
.PHONY: build .PHONY: build
build: build:
@ -42,7 +42,7 @@ build:
-t docker-sdk-python3 \ -t docker-sdk-python3 \
-f tests/Dockerfile \ -f tests/Dockerfile \
--build-arg VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER} \ --build-arg VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER} \
--build-arg APT_MIRROR . .
.PHONY: build-docs .PHONY: build-docs
build-docs: build-docs:
@ -76,9 +76,6 @@ integration-test: build
setup-network: setup-network:
docker network inspect dpy-tests || docker network create dpy-tests docker network inspect dpy-tests || docker network create dpy-tests
.PHONY: integration-dind
integration-dind: integration-dind
.PHONY: integration-dind .PHONY: integration-dind
integration-dind: build setup-network integration-dind: build setup-network
docker rm -vf dpy-dind || : docker rm -vf dpy-dind || :

View File

@ -2,7 +2,7 @@ import sys
from .version import __version__ from .version import __version__
DEFAULT_DOCKER_API_VERSION = '1.44' DEFAULT_DOCKER_API_VERSION = '1.45'
MINIMUM_DOCKER_API_VERSION = '1.24' MINIMUM_DOCKER_API_VERSION = '1.24'
DEFAULT_TIMEOUT_SECONDS = 60 DEFAULT_TIMEOUT_SECONDS = 60
STREAM_HEADER_SIZE_BYTES = 8 STREAM_HEADER_SIZE_BYTES = 8

View File

@ -181,7 +181,8 @@ class Container(Model):
user (str): User to execute command as. Default: root user (str): User to execute command as. Default: root
detach (bool): If true, detach from the exec command. detach (bool): If true, detach from the exec command.
Default: False Default: False
stream (bool): Stream response data. Default: False stream (bool): Stream response data. Ignored if ``detach`` is true.
Default: False
socket (bool): Return the connection socket to allow custom socket (bool): Return the connection socket to allow custom
read/write operations. Default: False read/write operations. Default: False
environment (dict or list): A dictionary or a list of strings in environment (dict or list): A dictionary or a list of strings in

View File

@ -407,8 +407,8 @@ class ImageCollection(Collection):
if match: if match:
image_id = match.group(2) image_id = match.group(2)
images.append(image_id) images.append(image_id)
if 'error' in chunk: if 'errorDetail' in chunk:
raise ImageLoadError(chunk['error']) raise ImageLoadError(chunk['errorDetail']['message'])
return [self.get(i) for i in images] return [self.get(i) for i in images]

View File

@ -242,6 +242,7 @@ class Mount(dict):
for the ``volume`` type. for the ``volume`` type.
driver_config (DriverConfig): Volume driver configuration. Only valid driver_config (DriverConfig): Volume driver configuration. Only valid
for the ``volume`` type. for the ``volume`` type.
subpath (str): Path inside a volume to mount instead of the volume root.
tmpfs_size (int or string): The size for the tmpfs mount in bytes. tmpfs_size (int or string): The size for the tmpfs mount in bytes.
tmpfs_mode (int): The permission mode for the tmpfs mount. tmpfs_mode (int): The permission mode for the tmpfs mount.
""" """
@ -249,7 +250,7 @@ class Mount(dict):
def __init__(self, target, source, type='volume', read_only=False, def __init__(self, target, source, type='volume', read_only=False,
consistency=None, propagation=None, no_copy=False, consistency=None, propagation=None, no_copy=False,
labels=None, driver_config=None, tmpfs_size=None, labels=None, driver_config=None, tmpfs_size=None,
tmpfs_mode=None): tmpfs_mode=None, subpath=None):
self['Target'] = target self['Target'] = target
self['Source'] = source self['Source'] = source
if type not in ('bind', 'volume', 'tmpfs', 'npipe'): if type not in ('bind', 'volume', 'tmpfs', 'npipe'):
@ -267,7 +268,7 @@ class Mount(dict):
self['BindOptions'] = { self['BindOptions'] = {
'Propagation': propagation 'Propagation': propagation
} }
if any([labels, driver_config, no_copy, tmpfs_size, tmpfs_mode]): if any([labels, driver_config, no_copy, tmpfs_size, tmpfs_mode, subpath]):
raise errors.InvalidArgument( raise errors.InvalidArgument(
'Incompatible options have been provided for the bind ' 'Incompatible options have been provided for the bind '
'type mount.' 'type mount.'
@ -280,6 +281,8 @@ class Mount(dict):
volume_opts['Labels'] = labels volume_opts['Labels'] = labels
if driver_config: if driver_config:
volume_opts['DriverConfig'] = driver_config volume_opts['DriverConfig'] = driver_config
if subpath:
volume_opts['Subpath'] = subpath
if volume_opts: if volume_opts:
self['VolumeOptions'] = volume_opts self['VolumeOptions'] = volume_opts
if any([propagation, tmpfs_size, tmpfs_mode]): if any([propagation, tmpfs_size, tmpfs_mode]):

View File

@ -28,7 +28,7 @@ RUN curl -sSL -o /opt/docker-credential-pass.tar.gz \
WORKDIR /src WORKDIR /src
COPY . . COPY . .
ARG VERSION ARG VERSION=0.0.0.dev0
RUN --mount=type=cache,target=/cache/pip \ RUN --mount=type=cache,target=/cache/pip \
PIP_CACHE_DIR=/cache/pip \ PIP_CACHE_DIR=/cache/pip \
SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} \ SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} \

View File

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG API_VERSION=1.44 ARG API_VERSION=1.45
ARG ENGINE_VERSION=25.0 ARG ENGINE_VERSION=26.1
FROM docker:${ENGINE_VERSION}-dind FROM docker:${ENGINE_VERSION}-dind

View File

@ -275,7 +275,7 @@ class BuildTest(BaseAPIIntegrationTest):
pass pass
info = self.client.inspect_image('build1') info = self.client.inspect_image('build1')
assert not info['Config']['OnBuild'] assert 'OnBuild' not in info['Config'] or not info['Config']['OnBuild']
@requires_api_version('1.25') @requires_api_version('1.25')
def test_build_with_network_mode(self): def test_build_with_network_mode(self):

View File

@ -620,6 +620,56 @@ class VolumeBindTest(BaseAPIIntegrationTest):
assert mount['Source'] == mount_data['Name'] assert mount['Source'] == mount_data['Name']
assert mount_data['RW'] is True assert mount_data['RW'] is True
@requires_api_version('1.45')
def test_create_with_subpath_volume_mount(self):
source_volume = helpers.random_name()
self.client.create_volume(name=source_volume)
setup_container = None
test_container = None
# Create a file structure in the volume to test with
setup_container = self.client.create_container(
TEST_IMG,
[
"sh",
"-c",
'mkdir -p /vol/subdir && echo "test content" > /vol/subdir/testfile.txt',
],
host_config=self.client.create_host_config(
binds=[f"{source_volume}:/vol"]
),
)
self.client.start(setup_container)
self.client.wait(setup_container)
# Now test with subpath
mount = docker.types.Mount(
type="volume",
source=source_volume,
target=self.mount_dest,
read_only=True,
subpath="subdir",
)
host_config = self.client.create_host_config(mounts=[mount])
test_container = self.client.create_container(
TEST_IMG,
["cat", os.path.join(self.mount_dest, "testfile.txt")],
host_config=host_config,
)
self.client.start(test_container)
self.client.wait(test_container) # Wait for container to finish
output = self.client.logs(test_container).decode("utf-8").strip()
# If the subpath feature is working, we should be able to see the content
# of the file in the subdir
assert output == "test content"
def check_container_data(self, inspect_data, rw, propagation='rprivate'): def check_container_data(self, inspect_data, rw, propagation='rprivate'):
assert 'Mounts' in inspect_data assert 'Mounts' in inspect_data
filtered = list(filter( filtered = list(filter(

View File

@ -5,7 +5,7 @@ import pytest
import docker import docker
from ..helpers import force_leave_swarm, requires_api_version, requires_experimental from ..helpers import force_leave_swarm, requires_api_version
from .base import TEST_IMG, BaseAPIIntegrationTest from .base import TEST_IMG, BaseAPIIntegrationTest
@ -140,8 +140,7 @@ class ServiceTest(BaseAPIIntegrationTest):
assert len(services) == 1 assert len(services) == 1
assert services[0]['ID'] == svc_id['ID'] assert services[0]['ID'] == svc_id['ID']
@requires_api_version('1.25') @requires_api_version('1.29')
@requires_experimental(until='1.29')
def test_service_logs(self): def test_service_logs(self):
name, svc_id = self.create_simple_service() name, svc_id = self.create_simple_service()
assert self.get_service_container(name, include_stopped=True) assert self.get_service_container(name, include_stopped=True)

View File

@ -17,10 +17,16 @@ class TestVolumes(BaseAPIIntegrationTest):
assert result['Driver'] == 'local' assert result['Driver'] == 'local'
def test_create_volume_invalid_driver(self): def test_create_volume_invalid_driver(self):
driver_name = 'invalid.driver' # special name to avoid exponential timeout loop
# https://github.com/moby/moby/blob/9e00a63d65434cdedc444e79a2b33a7c202b10d8/pkg/plugins/client.go#L253-L254
driver_name = 'this-plugin-does-not-exist'
with pytest.raises(docker.errors.NotFound): with pytest.raises(docker.errors.APIError) as cm:
self.client.create_volume('perfectcherryblossom', driver_name) self.client.create_volume('perfectcherryblossom', driver_name)
assert (
cm.value.response.status_code == 404 or
cm.value.response.status_code == 400
)
def test_list_volumes(self): def test_list_volumes(self):
name = 'imperishablenight' name = 'imperishablenight'

View File

@ -131,10 +131,9 @@ class ContainerCollectionTest(BaseIntegrationTest):
assert 'NetworkSettings' in attrs assert 'NetworkSettings' in attrs
assert 'Networks' in attrs['NetworkSettings'] assert 'Networks' in attrs['NetworkSettings']
assert list(attrs['NetworkSettings']['Networks'].keys()) == [net_name] assert list(attrs['NetworkSettings']['Networks'].keys()) == [net_name]
# Expect Aliases to list 'test_alias' and the container's short-id. # Aliases no longer include the container's short-id in API v1.45.
# In API version 1.45, the short-id will be removed.
assert attrs['NetworkSettings']['Networks'][net_name]['Aliases'] \ assert attrs['NetworkSettings']['Networks'][net_name]['Aliases'] \
== [test_alias, attrs['Id'][:12]] == [test_alias]
assert attrs['NetworkSettings']['Networks'][net_name]['DriverOpts'] \ assert attrs['NetworkSettings']['Networks'][net_name]['DriverOpts'] \
== test_driver_opt == test_driver_opt
@ -191,9 +190,9 @@ class ContainerCollectionTest(BaseIntegrationTest):
assert 'NetworkSettings' in attrs assert 'NetworkSettings' in attrs
assert 'Networks' in attrs['NetworkSettings'] assert 'Networks' in attrs['NetworkSettings']
assert list(attrs['NetworkSettings']['Networks'].keys()) == [net_name] assert list(attrs['NetworkSettings']['Networks'].keys()) == [net_name]
# Aliases should include the container's short-id (but it will be removed # Aliases no longer include the container's short-id in API v1.45.
# in API v1.45). assert (attrs['NetworkSettings']['Networks'][net_name]['Aliases']
assert attrs['NetworkSettings']['Networks'][net_name]['Aliases'] == [attrs["Id"][:12]] is None)
assert (attrs['NetworkSettings']['Networks'][net_name]['DriverOpts'] assert (attrs['NetworkSettings']['Networks'][net_name]['DriverOpts']
is None) is None)
@ -353,14 +352,26 @@ class ContainerTest(BaseIntegrationTest):
assert exec_output[0] == 0 assert exec_output[0] == 0
assert exec_output[1] == b"hello\n" assert exec_output[1] == b"hello\n"
def test_exec_run_error_code_from_exec(self):
client = docker.from_env(version=TEST_API_VERSION)
container = client.containers.run(
"alpine", "sh -c 'sleep 20'", detach=True
)
self.tmp_containers.append(container.id)
exec_output = container.exec_run("sh -c 'exit 42'")
assert exec_output[0] == 42
def test_exec_run_failed(self): def test_exec_run_failed(self):
client = docker.from_env(version=TEST_API_VERSION) client = docker.from_env(version=TEST_API_VERSION)
container = client.containers.run( container = client.containers.run(
"alpine", "sh -c 'sleep 60'", detach=True "alpine", "sh -c 'sleep 60'", detach=True
) )
self.tmp_containers.append(container.id) self.tmp_containers.append(container.id)
exec_output = container.exec_run("docker ps") exec_output = container.exec_run("non-existent")
assert exec_output[0] == 126 # older versions of docker return `126` in the case that an exec cannot
# be started due to a missing executable. We're fixing this for the
# future, so accept both for now.
assert exec_output[0] == 127 or exec_output[0] == 126
def test_kill(self): def test_kill(self):
client = docker.from_env(version=TEST_API_VERSION) client = docker.from_env(version=TEST_API_VERSION)

View File

@ -266,7 +266,7 @@ class BuildTest(BaseAPIIntegrationTest):
pass pass
info = self.client.inspect_image('build1') info = self.client.inspect_image('build1')
assert not info['Config']['OnBuild'] assert 'OnBuild' not in info['Config'] or not info['Config']['OnBuild']
@requires_api_version('1.25') @requires_api_version('1.25')
def test_build_with_network_mode(self): def test_build_with_network_mode(self):