Compare commits

..

No commits in common. "main" and "6.1.0" have entirely different histories.
main ... 6.1.0

139 changed files with 1581 additions and 2008 deletions

6
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1,6 @@
# GitHub code owners
# See https://help.github.com/articles/about-codeowners/
#
# KEEP THIS FILE SORTED. Order is important. Last match takes precedence.
* @aiordache @ulyssessouza

View File

@ -4,50 +4,35 @@ on: [push, pull_request]
env: env:
DOCKER_BUILDKIT: '1' DOCKER_BUILDKIT: '1'
FORCE_COLOR: 1
jobs: jobs:
lint: flake8:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: '3.x' python-version: '3.x'
- run: pip install -U ruff==0.1.8 - run: pip install -U flake8
- name: Run ruff - name: Run flake8
run: ruff docker tests run: flake8 docker/ tests/
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.x'
- run: pip3 install build && python -m build .
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist
unit-tests: unit-tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true
- name: Install dependencies - name: Install dependencies
run: | run: |
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
pip3 install '.[ssh,dev]' pip3 install -r test-requirements.txt -r requirements.txt
- name: Run unit tests - name: Run unit tests
run: | run: |
docker logout docker logout
@ -61,10 +46,7 @@ jobs:
variant: [ "integration-dind", "integration-dind-ssl", "integration-dind-ssh" ] variant: [ "integration-dind", "integration-dind-ssl", "integration-dind-ssh" ]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with:
fetch-depth: 0
fetch-tags: true
- name: make ${{ matrix.variant }} - name: make ${{ matrix.variant }}
run: | run: |
docker logout docker logout

View File

@ -12,28 +12,22 @@ on:
type: boolean type: boolean
default: true default: true
env:
DOCKER_BUILDKIT: '1'
FORCE_COLOR: 1
jobs: jobs:
publish: publish:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: '3.x' python-version: '3.x'
- name: Generate Package - name: Generate Pacakge
run: | run: |
pip3 install build pip3 install wheel
python -m build . python setup.py sdist bdist_wheel
env: env:
# This is also supported by Hatch; see SETUPTOOLS_SCM_PRETEND_VERSION_FOR_DOCKER: ${{ inputs.tag }}
# https://github.com/ofek/hatch-vcs#version-source-environment-variables
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ inputs.tag }}
- name: Publish to PyPI - name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@release/v1

View File

@ -4,14 +4,14 @@ sphinx:
configuration: docs/conf.py configuration: docs/conf.py
build: build:
os: ubuntu-22.04 os: ubuntu-20.04
tools: tools:
python: '3.12' python: '3.10'
python: python:
install: install:
- requirements: docs-requirements.txt
- method: pip - method: pip
path: . path: .
extra_requirements: extra_requirements:
- ssh - ssh
- docs

View File

@ -44,7 +44,7 @@ paragraph in the Docker contribution guidelines.
Before we can review your pull request, please ensure that nothing has been Before we can review your pull request, please ensure that nothing has been
broken by your changes by running the test suite. You can do so simply by broken by your changes by running the test suite. You can do so simply by
running `make test` in the project root. This also includes coding style using running `make test` in the project root. This also includes coding style using
`ruff` `flake8`
### 3. Write clear, self-contained commits ### 3. Write clear, self-contained commits

View File

@ -1,13 +1,17 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG PYTHON_VERSION=3.12 ARG PYTHON_VERSION=3.10
FROM python:${PYTHON_VERSION} FROM python:${PYTHON_VERSION}
WORKDIR /src WORKDIR /src
COPY . .
ARG VERSION=0.0.0.dev0 COPY requirements.txt /src/requirements.txt
RUN --mount=type=cache,target=/cache/pip \ RUN pip install --no-cache-dir -r requirements.txt
PIP_CACHE_DIR=/cache/pip \
SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} \ COPY test-requirements.txt /src/test-requirements.txt
pip install .[ssh] RUN pip install --no-cache-dir -r test-requirements.txt
COPY . .
ARG SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER
RUN pip install --no-cache-dir .

View File

@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG PYTHON_VERSION=3.12 ARG PYTHON_VERSION=3.10
FROM python:${PYTHON_VERSION} FROM python:${PYTHON_VERSION}
@ -11,12 +11,7 @@ RUN addgroup --gid $gid sphinx \
&& useradd --uid $uid --gid $gid -M sphinx && useradd --uid $uid --gid $gid -M sphinx
WORKDIR /src WORKDIR /src
COPY . . COPY requirements.txt docs-requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt -r docs-requirements.txt
ARG VERSION=0.0.0.dev0
RUN --mount=type=cache,target=/cache/pip \
PIP_CACHE_DIR=/cache/pip \
SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} \
pip install .[ssh,docs]
USER sphinx USER sphinx

147
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,147 @@
#!groovy
def imageNameBase = "dockerpinata/docker-py"
def imageNamePy3
def imageDindSSH
def images = [:]
def buildImage = { name, buildargs, pyTag ->
img = docker.image(name)
try {
img.pull()
} catch (Exception exc) {
img = docker.build(name, buildargs)
img.push()
}
if (pyTag?.trim()) images[pyTag] = img.id
}
def buildImages = { ->
wrappedNode(label: "amd64 && ubuntu-2004 && overlay2", cleanWorkspace: true) {
stage("build image") {
checkout(scm)
imageNamePy3 = "${imageNameBase}:py3-${gitCommit()}"
imageDindSSH = "${imageNameBase}:sshdind-${gitCommit()}"
withDockerRegistry(credentialsId:'dockerbuildbot-index.docker.io') {
buildImage(imageDindSSH, "-f tests/Dockerfile-ssh-dind .", "")
buildImage(imageNamePy3, "-f tests/Dockerfile --build-arg PYTHON_VERSION=3.10 .", "py3.10")
}
}
}
}
def getDockerVersions = { ->
def dockerVersions = ["19.03.12"]
wrappedNode(label: "amd64 && ubuntu-2004 && overlay2") {
def result = sh(script: """docker run --rm \\
--entrypoint=python \\
${imageNamePy3} \\
/src/scripts/versions.py
""", returnStdout: true
)
dockerVersions = dockerVersions + result.trim().tokenize(' ')
}
return dockerVersions
}
def getAPIVersion = { engineVersion ->
def versionMap = [
'18.09': '1.39',
'19.03': '1.40'
]
def result = versionMap[engineVersion.substring(0, 5)]
if (!result) {
return '1.40'
}
return result
}
def runTests = { Map settings ->
def dockerVersion = settings.get("dockerVersion", null)
def pythonVersion = settings.get("pythonVersion", null)
def testImage = settings.get("testImage", null)
def apiVersion = getAPIVersion(dockerVersion)
if (!testImage) {
throw new Exception("Need test image object, e.g.: `runTests(testImage: img)`")
}
if (!dockerVersion) {
throw new Exception("Need Docker version to test, e.g.: `runTests(dockerVersion: '19.03.12')`")
}
if (!pythonVersion) {
throw new Exception("Need Python version being tested, e.g.: `runTests(pythonVersion: 'py3.x')`")
}
{ ->
wrappedNode(label: "amd64 && ubuntu-2004 && overlay2", cleanWorkspace: true) {
stage("test python=${pythonVersion} / docker=${dockerVersion}") {
checkout(scm)
def dindContainerName = "dpy-dind-\$BUILD_NUMBER-\$EXECUTOR_NUMBER-${pythonVersion}-${dockerVersion}"
def testContainerName = "dpy-tests-\$BUILD_NUMBER-\$EXECUTOR_NUMBER-${pythonVersion}-${dockerVersion}"
def testNetwork = "dpy-testnet-\$BUILD_NUMBER-\$EXECUTOR_NUMBER-${pythonVersion}-${dockerVersion}"
withDockerRegistry(credentialsId:'dockerbuildbot-index.docker.io') {
try {
// unit tests
sh """docker run --rm \\
-e 'DOCKER_TEST_API_VERSION=${apiVersion}' \\
${testImage} \\
py.test -v -rxs --cov=docker tests/unit
"""
// integration tests
sh """docker network create ${testNetwork}"""
sh """docker run --rm -d --name ${dindContainerName} -v /tmp --privileged --network ${testNetwork} \\
${imageDindSSH} dockerd -H tcp://0.0.0.0:2375
"""
sh """docker run --rm \\
--name ${testContainerName} \\
-e "DOCKER_HOST=tcp://${dindContainerName}:2375" \\
-e 'DOCKER_TEST_API_VERSION=${apiVersion}' \\
--network ${testNetwork} \\
--volumes-from ${dindContainerName} \\
-v $DOCKER_CONFIG/config.json:/root/.docker/config.json \\
${testImage} \\
py.test -v -rxs --cov=docker tests/integration
"""
sh """docker stop ${dindContainerName}"""
// start DIND container with SSH
sh """docker run --rm -d --name ${dindContainerName} -v /tmp --privileged --network ${testNetwork} \\
${imageDindSSH} dockerd --experimental"""
sh """docker exec ${dindContainerName} sh -c /usr/sbin/sshd """
// run SSH tests only
sh """docker run --rm \\
--name ${testContainerName} \\
-e "DOCKER_HOST=ssh://${dindContainerName}:22" \\
-e 'DOCKER_TEST_API_VERSION=${apiVersion}' \\
--network ${testNetwork} \\
--volumes-from ${dindContainerName} \\
-v $DOCKER_CONFIG/config.json:/root/.docker/config.json \\
${testImage} \\
py.test -v -rxs --cov=docker tests/ssh
"""
} finally {
sh """
docker stop ${dindContainerName}
docker network rm ${testNetwork}
"""
}
}
}
}
}
}
buildImages()
def dockerVersions = getDockerVersions()
def testMatrix = [failFast: false]
for (imgKey in new ArrayList(images.keySet())) {
for (version in dockerVersions) {
testMatrix["${imgKey}_${version}"] = runTests([testImage: images[imgKey], dockerVersion: version, pythonVersion: imgKey])
}
}
parallel(testMatrix)

View File

@ -11,19 +11,17 @@
[Org] [Org]
[Org."Core maintainers"] [Org."Core maintainers"]
people = [ people = [
"glours", "aiordache",
"milas", "ulyssessouza",
] ]
[Org.Alumni] [Org.Alumni]
people = [ people = [
"aiordache",
"aanand", "aanand",
"bfirsh", "bfirsh",
"dnephin", "dnephin",
"mnowster", "mnowster",
"mpetazzoni", "mpetazzoni",
"shin-", "shin-",
"ulyssessouza",
] ]
[people] [people]
@ -54,16 +52,6 @@
Email = "dnephin@gmail.com" Email = "dnephin@gmail.com"
GitHub = "dnephin" GitHub = "dnephin"
[people.glours]
Name = "Guillaume Lours"
Email = "705411+glours@users.noreply.github.com"
GitHub = "glours"
[people.milas]
Name = "Milas Bowman"
Email = "devnull@milas.dev"
GitHub = "milas"
[people.mnowster] [people.mnowster]
Name = "Mazz Mosley" Name = "Mazz Mosley"
Email = "mazz@houseofmnowster.com" Email = "mazz@houseofmnowster.com"

9
MANIFEST.in Normal file
View File

@ -0,0 +1,9 @@
include test-requirements.txt
include requirements.txt
include README.md
include README.rst
include LICENSE
recursive-include tests *.py
recursive-include tests/unit/testdata *
recursive-include tests/integration/testdata *
recursive-include tests/gpg-keys *

View File

@ -1,5 +1,5 @@
TEST_API_VERSION ?= 1.45 TEST_API_VERSION ?= 1.41
TEST_ENGINE_VERSION ?= 26.1 TEST_ENGINE_VERSION ?= 20.10
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
PLATFORM := Windows PLATFORM := Windows
@ -11,17 +11,12 @@ ifeq ($(PLATFORM),Linux)
uid_args := "--build-arg uid=$(shell id -u) --build-arg gid=$(shell id -g)" uid_args := "--build-arg uid=$(shell id -u) --build-arg gid=$(shell id -g)"
endif 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/-/+/')
ifeq ($(SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER),)
SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER = "0.0.0.dev0"
endif
.PHONY: all .PHONY: all
all: test all: test
.PHONY: clean .PHONY: clean
clean: clean:
-docker rm -f dpy-dind dpy-dind-certs dpy-dind-ssl -docker rm -f dpy-dind-py3 dpy-dind-certs dpy-dind-ssl
find -name "__pycache__" | xargs rm -rf find -name "__pycache__" | xargs rm -rf
.PHONY: build-dind-ssh .PHONY: build-dind-ssh
@ -30,46 +25,35 @@ build-dind-ssh:
--pull \ --pull \
-t docker-dind-ssh \ -t docker-dind-ssh \
-f tests/Dockerfile-ssh-dind \ -f tests/Dockerfile-ssh-dind \
--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-py3
build: build-py3:
docker build \ docker build \
--pull \ --pull \
-t docker-sdk-python3 \ -t docker-sdk-python3 \
-f tests/Dockerfile \ -f tests/Dockerfile \
--build-arg VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER} \ --build-arg APT_MIRROR .
.
.PHONY: build-docs .PHONY: build-docs
build-docs: build-docs:
docker build \ docker build -t docker-sdk-python-docs -f Dockerfile-docs $(uid_args) .
-t docker-sdk-python-docs \
-f Dockerfile-docs \
--build-arg VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER} \
$(uid_args) \
.
.PHONY: build-dind-certs .PHONY: build-dind-certs
build-dind-certs: build-dind-certs:
docker build \ docker build -t dpy-dind-certs -f tests/Dockerfile-dind-certs .
-t dpy-dind-certs \
-f tests/Dockerfile-dind-certs \
--build-arg VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER} \
.
.PHONY: test .PHONY: test
test: ruff unit-test integration-dind integration-dind-ssl test: flake8 unit-test-py3 integration-dind integration-dind-ssl
.PHONY: unit-test .PHONY: unit-test-py3
unit-test: build unit-test-py3: build-py3
docker run -t --rm docker-sdk-python3 py.test tests/unit docker run -t --rm docker-sdk-python3 py.test tests/unit
.PHONY: integration-test .PHONY: integration-test-py3
integration-test: build integration-test-py3: build-py3
docker run -t --rm -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python3 py.test -v tests/integration/${file} docker run -t --rm -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python3 py.test -v tests/integration/${file}
.PHONY: setup-network .PHONY: setup-network
@ -77,12 +61,15 @@ 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 .PHONY: integration-dind
integration-dind: build setup-network integration-dind: integration-dind-py3
docker rm -vf dpy-dind || :
.PHONY: integration-dind-py3
integration-dind-py3: build-py3 setup-network
docker rm -vf dpy-dind-py3 || :
docker run \ docker run \
--detach \ --detach \
--name dpy-dind \ --name dpy-dind-py3 \
--network dpy-tests \ --network dpy-tests \
--pull=always \ --pull=always \
--privileged \ --privileged \
@ -95,10 +82,10 @@ integration-dind: build setup-network
--rm \ --rm \
--tty \ --tty \
busybox \ busybox \
sh -c 'while ! nc -z dpy-dind 2375; do sleep 1; done' sh -c 'while ! nc -z dpy-dind-py3 2375; do sleep 1; done'
docker run \ docker run \
--env="DOCKER_HOST=tcp://dpy-dind:2375" \ --env="DOCKER_HOST=tcp://dpy-dind-py3:2375" \
--env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}" \ --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}" \
--network dpy-tests \ --network dpy-tests \
--rm \ --rm \
@ -106,11 +93,11 @@ integration-dind: build setup-network
docker-sdk-python3 \ docker-sdk-python3 \
py.test tests/integration/${file} py.test tests/integration/${file}
docker rm -vf dpy-dind docker rm -vf dpy-dind-py3
.PHONY: integration-dind-ssh .PHONY: integration-dind-ssh
integration-dind-ssh: build-dind-ssh build setup-network integration-dind-ssh: build-dind-ssh build-py3 setup-network
docker rm -vf dpy-dind-ssh || : docker rm -vf dpy-dind-ssh || :
docker run -d --network dpy-tests --name dpy-dind-ssh --privileged \ docker run -d --network dpy-tests --name dpy-dind-ssh --privileged \
docker-dind-ssh dockerd --experimental docker-dind-ssh dockerd --experimental
@ -129,7 +116,7 @@ integration-dind-ssh: build-dind-ssh build setup-network
.PHONY: integration-dind-ssl .PHONY: integration-dind-ssl
integration-dind-ssl: build-dind-certs build setup-network integration-dind-ssl: build-dind-certs build-py3 setup-network
docker rm -vf dpy-dind-certs dpy-dind-ssl || : docker rm -vf dpy-dind-certs dpy-dind-ssl || :
docker run -d --name dpy-dind-certs dpy-dind-certs docker run -d --name dpy-dind-certs dpy-dind-certs
@ -176,14 +163,14 @@ integration-dind-ssl: build-dind-certs build setup-network
docker rm -vf dpy-dind-ssl dpy-dind-certs docker rm -vf dpy-dind-ssl dpy-dind-certs
.PHONY: ruff .PHONY: flake8
ruff: build flake8: build-py3
docker run -t --rm docker-sdk-python3 ruff docker tests docker run -t --rm docker-sdk-python3 flake8 docker tests
.PHONY: docs .PHONY: docs
docs: build-docs docs: build-docs
docker run --rm -t -v `pwd`:/src docker-sdk-python-docs sphinx-build docs docs/_build docker run --rm -t -v `pwd`:/src docker-sdk-python-docs sphinx-build docs docs/_build
.PHONY: shell .PHONY: shell
shell: build shell: build-py3
docker run -it -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python3 python docker run -it -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python3 python

View File

@ -1,12 +1,12 @@
# Docker SDK for Python # Docker SDK for Python
[![Build Status](https://github.com/docker/docker-py/actions/workflows/ci.yml/badge.svg)](https://github.com/docker/docker-py/actions/workflows/ci.yml) [![Build Status](https://github.com/docker/docker-py/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/docker/docker-py/actions/workflows/ci.yml/)
A Python library for the Docker Engine API. It lets you do anything the `docker` command does, but from within Python apps run containers, manage containers, manage Swarms, etc. A Python library for the Docker Engine API. It lets you do anything the `docker` command does, but from within Python apps run containers, manage containers, manage Swarms, etc.
## Installation ## Installation
The latest stable version [is available on PyPI](https://pypi.python.org/pypi/docker/). Install with pip: The latest stable version [is available on PyPI](https://pypi.python.org/pypi/docker/). Either add `docker` to your `requirements.txt` file or install with pip:
pip install docker pip install docker

View File

@ -1,6 +1,8 @@
# flake8: noqa
from .api import APIClient from .api import APIClient
from .client import DockerClient, from_env from .client import DockerClient, from_env
from .context import Context, ContextAPI from .context import Context
from .context import ContextAPI
from .tls import TLSConfig from .tls import TLSConfig
from .version import __version__ from .version import __version__

View File

@ -1 +1,2 @@
# flake8: noqa
from .client import APIClient from .client import APIClient

View File

@ -3,7 +3,11 @@ import logging
import os import os
import random import random
from .. import auth, constants, errors, utils from .. import auth
from .. import constants
from .. import errors
from .. import utils
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -125,16 +129,13 @@ class BuildApiMixin:
raise errors.DockerException( raise errors.DockerException(
'Can not use custom encoding if gzip is enabled' 'Can not use custom encoding if gzip is enabled'
) )
if tag is not None:
if not utils.match_tag(tag):
raise errors.DockerException(
f"invalid tag '{tag}': invalid reference format"
)
for key in container_limits.keys(): for key in container_limits.keys():
if key not in constants.CONTAINER_LIMITS_KEYS: if key not in constants.CONTAINER_LIMITS_KEYS:
raise errors.DockerException( raise errors.DockerException(
f"invalid tag '{tag}': invalid reference format" f'Invalid container_limits key {key}'
) )
if custom_context: if custom_context:
if not fileobj: if not fileobj:
raise TypeError("You must specify fileobj with custom_context") raise TypeError("You must specify fileobj with custom_context")
@ -275,24 +276,10 @@ class BuildApiMixin:
return self._stream_helper(response, decode=decode) return self._stream_helper(response, decode=decode)
@utils.minimum_version('1.31') @utils.minimum_version('1.31')
def prune_builds(self, filters=None, keep_storage=None, all=None): def prune_builds(self):
""" """
Delete the builder cache Delete the builder cache
Args:
filters (dict): Filters to process on the prune list.
Needs Docker API v1.39+
Available filters:
- dangling (bool): When set to true (or 1), prune only
unused and untagged images.
- until (str): Can be Unix timestamps, date formatted
timestamps, or Go duration strings (e.g. 10m, 1h30m) computed
relative to the daemon's local time.
keep_storage (int): Amount of disk space in bytes to keep for cache.
Needs Docker API v1.39+
all (bool): Remove all types of build cache.
Needs Docker API v1.39+
Returns: Returns:
(dict): A dictionary containing information about the operation's (dict): A dictionary containing information about the operation's
result. The ``SpaceReclaimed`` key indicates the amount of result. The ``SpaceReclaimed`` key indicates the amount of
@ -303,20 +290,7 @@ class BuildApiMixin:
If the server returns an error. If the server returns an error.
""" """
url = self._url("/build/prune") url = self._url("/build/prune")
if (filters, keep_storage, all) != (None, None, None) \ return self._result(self._post(url), True)
and utils.version_lt(self._version, '1.39'):
raise errors.InvalidVersion(
'`filters`, `keep_storage`, and `all` args are only available '
'for API version > 1.38'
)
params = {}
if filters is not None:
params['filters'] = utils.convert_filters(filters)
if keep_storage is not None:
params['keep-storage'] = keep_storage
if all is not None:
params['all'] = all
return self._result(self._post(url, params=params), True)
def _set_auth_headers(self, headers): def _set_auth_headers(self, headers):
log.debug('Looking for auth config') log.debug('Looking for auth config')
@ -340,8 +314,9 @@ class BuildApiMixin:
auth_data[auth.INDEX_URL] = auth_data.get(auth.INDEX_NAME, {}) auth_data[auth.INDEX_URL] = auth_data.get(auth.INDEX_NAME, {})
log.debug( log.debug(
"Sending auth config (%s)", 'Sending auth config ({})'.format(
', '.join(repr(k) for k in auth_data), ', '.join(repr(k) for k in auth_data.keys())
)
) )
if auth_data: if auth_data:
@ -361,9 +336,12 @@ def process_dockerfile(dockerfile, path):
abs_dockerfile = os.path.join(path, dockerfile) abs_dockerfile = os.path.join(path, dockerfile)
if constants.IS_WINDOWS_PLATFORM and path.startswith( if constants.IS_WINDOWS_PLATFORM and path.startswith(
constants.WINDOWS_LONGPATH_PREFIX): constants.WINDOWS_LONGPATH_PREFIX):
normpath = os.path.normpath( abs_dockerfile = '{}{}'.format(
abs_dockerfile[len(constants.WINDOWS_LONGPATH_PREFIX):]) constants.WINDOWS_LONGPATH_PREFIX,
abs_dockerfile = f'{constants.WINDOWS_LONGPATH_PREFIX}{normpath}' os.path.normpath(
abs_dockerfile[len(constants.WINDOWS_LONGPATH_PREFIX):]
)
)
if (os.path.splitdrive(path)[0] != os.path.splitdrive(abs_dockerfile)[0] or if (os.path.splitdrive(path)[0] != os.path.splitdrive(abs_dockerfile)[0] or
os.path.relpath(abs_dockerfile, path).startswith('..')): os.path.relpath(abs_dockerfile, path).startswith('..')):
# Dockerfile not in context - read data to insert into tar later # Dockerfile not in context - read data to insert into tar later

View File

@ -4,28 +4,18 @@ import urllib
from functools import partial from functools import partial
import requests import requests
import requests.adapters
import requests.exceptions import requests.exceptions
import websocket
from .. import auth from .. import auth
from ..constants import ( from ..constants import (DEFAULT_NUM_POOLS, DEFAULT_NUM_POOLS_SSH,
DEFAULT_MAX_POOL_SIZE, DEFAULT_MAX_POOL_SIZE, DEFAULT_TIMEOUT_SECONDS,
DEFAULT_NUM_POOLS, DEFAULT_USER_AGENT, IS_WINDOWS_PLATFORM,
DEFAULT_NUM_POOLS_SSH, MINIMUM_DOCKER_API_VERSION, STREAM_HEADER_SIZE_BYTES)
DEFAULT_TIMEOUT_SECONDS, from ..errors import (DockerException, InvalidVersion, TLSParameterError,
DEFAULT_USER_AGENT, create_api_error_from_http_exception)
IS_WINDOWS_PLATFORM,
MINIMUM_DOCKER_API_VERSION,
STREAM_HEADER_SIZE_BYTES,
)
from ..errors import (
DockerException,
InvalidVersion,
TLSParameterError,
create_api_error_from_http_exception,
)
from ..tls import TLSConfig from ..tls import TLSConfig
from ..transport import UnixHTTPAdapter from ..transport import SSLHTTPAdapter, UnixHTTPAdapter
from ..utils import check_resource, config, update_headers, utils from ..utils import check_resource, config, update_headers, utils
from ..utils.json_stream import json_stream from ..utils.json_stream import json_stream
from ..utils.proxy import ProxyConfig from ..utils.proxy import ProxyConfig
@ -170,10 +160,10 @@ class APIClient(
base_url, timeout, pool_connections=num_pools, base_url, timeout, pool_connections=num_pools,
max_pool_size=max_pool_size max_pool_size=max_pool_size
) )
except NameError as err: except NameError:
raise DockerException( raise DockerException(
'Install pypiwin32 package to enable npipe:// support' 'Install pypiwin32 package to enable npipe:// support'
) from err )
self.mount('http+docker://', self._custom_adapter) self.mount('http+docker://', self._custom_adapter)
self.base_url = 'http+docker://localnpipe' self.base_url = 'http+docker://localnpipe'
elif base_url.startswith('ssh://'): elif base_url.startswith('ssh://'):
@ -182,10 +172,10 @@ class APIClient(
base_url, timeout, pool_connections=num_pools, base_url, timeout, pool_connections=num_pools,
max_pool_size=max_pool_size, shell_out=use_ssh_client max_pool_size=max_pool_size, shell_out=use_ssh_client
) )
except NameError as err: except NameError:
raise DockerException( raise DockerException(
'Install paramiko package to enable ssh:// support' 'Install paramiko package to enable ssh:// support'
) from err )
self.mount('http+docker://ssh', self._custom_adapter) self.mount('http+docker://ssh', self._custom_adapter)
self._unmount('http://', 'https://') self._unmount('http://', 'https://')
self.base_url = 'http+docker://ssh' self.base_url = 'http+docker://ssh'
@ -194,7 +184,7 @@ class APIClient(
if isinstance(tls, TLSConfig): if isinstance(tls, TLSConfig):
tls.configure_client(self) tls.configure_client(self)
elif tls: elif tls:
self._custom_adapter = requests.adapters.HTTPAdapter( self._custom_adapter = SSLHTTPAdapter(
pool_connections=num_pools) pool_connections=num_pools)
self.mount('https://', self._custom_adapter) self.mount('https://', self._custom_adapter)
self.base_url = base_url self.base_url = base_url
@ -209,27 +199,28 @@ class APIClient(
self._version = version self._version = version
if not isinstance(self._version, str): if not isinstance(self._version, str):
raise DockerException( raise DockerException(
'Version parameter must be a string or None. ' 'Version parameter must be a string or None. Found {}'.format(
f'Found {type(version).__name__}' type(version).__name__
)
) )
if utils.version_lt(self._version, MINIMUM_DOCKER_API_VERSION): if utils.version_lt(self._version, MINIMUM_DOCKER_API_VERSION):
raise InvalidVersion( raise InvalidVersion(
f'API versions below {MINIMUM_DOCKER_API_VERSION} are ' 'API versions below {} are no longer supported by this '
f'no longer supported by this library.' 'library.'.format(MINIMUM_DOCKER_API_VERSION)
) )
def _retrieve_server_version(self): def _retrieve_server_version(self):
try: try:
return self.version(api_version=False)["ApiVersion"] return self.version(api_version=False)["ApiVersion"]
except KeyError as ke: except KeyError:
raise DockerException( raise DockerException(
'Invalid response from docker daemon: key "ApiVersion"' 'Invalid response from docker daemon: key "ApiVersion"'
' is missing.' ' is missing.'
) from ke )
except Exception as e: except Exception as e:
raise DockerException( raise DockerException(
f'Error while fetching server API version: {e}' f'Error while fetching server API version: {e}'
) from e )
def _set_request_timeout(self, kwargs): def _set_request_timeout(self, kwargs):
"""Prepare the kwargs for an HTTP request by inserting the timeout """Prepare the kwargs for an HTTP request by inserting the timeout
@ -257,17 +248,19 @@ class APIClient(
for arg in args: for arg in args:
if not isinstance(arg, str): if not isinstance(arg, str):
raise ValueError( raise ValueError(
f'Expected a string but found {arg} ({type(arg)}) instead' 'Expected a string but found {} ({}) '
'instead'.format(arg, type(arg))
) )
quote_f = partial(urllib.parse.quote, safe="/:") quote_f = partial(urllib.parse.quote, safe="/:")
args = map(quote_f, args) args = map(quote_f, args)
formatted_path = pathfmt.format(*args)
if kwargs.get('versioned_api', True): if kwargs.get('versioned_api', True):
return f'{self.base_url}/v{self._version}{formatted_path}' return '{}/v{}{}'.format(
self.base_url, self._version, pathfmt.format(*args)
)
else: else:
return f'{self.base_url}{formatted_path}' return f'{self.base_url}{pathfmt.format(*args)}'
def _raise_for_status(self, response): def _raise_for_status(self, response):
"""Raises stored :class:`APIError`, if one occurred.""" """Raises stored :class:`APIError`, if one occurred."""
@ -319,16 +312,7 @@ class APIClient(
return self._create_websocket_connection(full_url) return self._create_websocket_connection(full_url)
def _create_websocket_connection(self, url): def _create_websocket_connection(self, url):
try: return websocket.create_connection(url)
import websocket
return websocket.create_connection(url)
except ImportError as ie:
raise DockerException(
'The `websocket-client` library is required '
'for using websocket connections. '
'You can install the `docker` library '
'with the [websocket] extra to install it.'
) from ie
def _get_raw_response_socket(self, response): def _get_raw_response_socket(self, response):
self._raise_for_status(response) self._raise_for_status(response)
@ -495,7 +479,7 @@ class APIClient(
return self._multiplexed_response_stream_helper(res) return self._multiplexed_response_stream_helper(res)
else: else:
return sep.join( return sep.join(
list(self._multiplexed_buffer_helper(res)) [x for x in self._multiplexed_buffer_helper(res)]
) )
def _unmount(self, *args): def _unmount(self, *args):

View File

@ -1,14 +1,13 @@
from datetime import datetime from datetime import datetime
from .. import errors, utils from .. import errors
from .. import utils
from ..constants import DEFAULT_DATA_CHUNK_SIZE from ..constants import DEFAULT_DATA_CHUNK_SIZE
from ..types import ( from ..types import CancellableStream
CancellableStream, from ..types import ContainerConfig
ContainerConfig, from ..types import EndpointConfig
EndpointConfig, from ..types import HostConfig
HostConfig, from ..types import NetworkingConfig
NetworkingConfig,
)
class ContainerApiMixin: class ContainerApiMixin:
@ -113,7 +112,7 @@ class ContainerApiMixin:
@utils.check_resource('container') @utils.check_resource('container')
def commit(self, container, repository=None, tag=None, message=None, def commit(self, container, repository=None, tag=None, message=None,
author=None, pause=True, changes=None, conf=None): author=None, changes=None, conf=None):
""" """
Commit a container to an image. Similar to the ``docker commit`` Commit a container to an image. Similar to the ``docker commit``
command. command.
@ -124,7 +123,6 @@ class ContainerApiMixin:
tag (str): The tag to push tag (str): The tag to push
message (str): A commit message message (str): A commit message
author (str): The name of the author author (str): The name of the author
pause (bool): Whether to pause the container before committing
changes (str): Dockerfile instructions to apply while committing changes (str): Dockerfile instructions to apply while committing
conf (dict): The configuration for the container. See the conf (dict): The configuration for the container. See the
`Engine API documentation `Engine API documentation
@ -141,7 +139,6 @@ class ContainerApiMixin:
'tag': tag, 'tag': tag,
'comment': message, 'comment': message,
'author': author, 'author': author,
'pause': pause,
'changes': changes 'changes': changes
} }
u = self._url("/commit") u = self._url("/commit")
@ -320,11 +317,6 @@ class ContainerApiMixin:
'/var/www': { '/var/www': {
'bind': '/mnt/vol1', 'bind': '/mnt/vol1',
'mode': 'ro', 'mode': 'ro',
},
'/autofs/user1': {
'bind': '/mnt/vol3',
'mode': 'rw',
'propagation': 'shared'
} }
}) })
) )
@ -335,11 +327,10 @@ class ContainerApiMixin:
.. code-block:: python .. code-block:: python
container_id = client.api.create_container( container_id = client.api.create_container(
'busybox', 'ls', volumes=['/mnt/vol1', '/mnt/vol2', '/mnt/vol3'], 'busybox', 'ls', volumes=['/mnt/vol1', '/mnt/vol2'],
host_config=client.api.create_host_config(binds=[ host_config=client.api.create_host_config(binds=[
'/home/user1/:/mnt/vol2', '/home/user1/:/mnt/vol2',
'/var/www:/mnt/vol1:ro', '/var/www:/mnt/vol1:ro',
'/autofs/user1:/mnt/vol3:rw,shared',
]) ])
) )
@ -687,8 +678,7 @@ class ContainerApiMixin:
container (str): The container to diff container (str): The container to diff
Returns: Returns:
(list) A list of dictionaries containing the attributes `Path` (str)
and `Kind`.
Raises: Raises:
:py:class:`docker.errors.APIError` :py:class:`docker.errors.APIError`
@ -844,7 +834,7 @@ class ContainerApiMixin:
float (in fractional seconds) float (in fractional seconds)
Returns: Returns:
(generator of bytes or bytes) (generator or str)
Raises: Raises:
:py:class:`docker.errors.APIError` :py:class:`docker.errors.APIError`
@ -870,8 +860,8 @@ class ContainerApiMixin:
params['since'] = since params['since'] = since
else: else:
raise errors.InvalidArgument( raise errors.InvalidArgument(
'since value should be datetime or positive int/float,' 'since value should be datetime or positive int/float, '
f' not {type(since)}' 'not {}'.format(type(since))
) )
if until is not None: if until is not None:
@ -887,8 +877,8 @@ class ContainerApiMixin:
params['until'] = until params['until'] = until
else: else:
raise errors.InvalidArgument( raise errors.InvalidArgument(
f'until value should be datetime or positive int/float, ' 'until value should be datetime or positive int/float, '
f'not {type(until)}' 'not {}'.format(type(until))
) )
url = self._url("/containers/{0}/logs", container) url = self._url("/containers/{0}/logs", container)
@ -960,7 +950,7 @@ class ContainerApiMixin:
return port_settings.get(private_port) return port_settings.get(private_port)
for protocol in ['tcp', 'udp', 'sctp']: for protocol in ['tcp', 'udp', 'sctp']:
h_ports = port_settings.get(f"{private_port}/{protocol}") h_ports = port_settings.get(private_port + '/' + protocol)
if h_ports: if h_ports:
break break
@ -1173,9 +1163,8 @@ class ContainerApiMixin:
'one_shot is only available in conjunction with ' 'one_shot is only available in conjunction with '
'stream=False' 'stream=False'
) )
return self._stream_helper( return self._stream_helper(self._get(url, params=params),
self._get(url, stream=True, params=params), decode=decode decode=decode)
)
else: else:
if decode: if decode:
raise errors.InvalidArgument( raise errors.InvalidArgument(

View File

@ -1,4 +1,5 @@
from .. import errors, utils from .. import errors
from .. import utils
from ..types import CancellableStream from ..types import CancellableStream

View File

@ -47,7 +47,7 @@ class ImageApiMixin:
image (str): The image to show history for image (str): The image to show history for
Returns: Returns:
(list): The history of the image (str): The history of the image
Raises: Raises:
:py:class:`docker.errors.APIError` :py:class:`docker.errors.APIError`

View File

@ -1,6 +1,7 @@
from .. import utils
from ..errors import InvalidVersion from ..errors import InvalidVersion
from ..utils import check_resource, minimum_version, version_lt from ..utils import check_resource, minimum_version
from ..utils import version_lt
from .. import utils
class NetworkApiMixin: class NetworkApiMixin:

View File

@ -1,6 +1,7 @@
import base64 import base64
from .. import errors, utils from .. import errors
from .. import utils
class SecretApiMixin: class SecretApiMixin:

View File

@ -7,7 +7,9 @@ def _check_api_features(version, task_template, update_config, endpoint_spec,
def raise_version_error(param, min_version): def raise_version_error(param, min_version):
raise errors.InvalidVersion( raise errors.InvalidVersion(
f'{param} is not supported in API version < {min_version}' '{} is not supported in API version < {}'.format(
param, min_version
)
) )
if update_config is not None: if update_config is not None:

View File

@ -1,8 +1,9 @@
import http.client as http_client
import logging import logging
import http.client as http_client
from .. import errors, types, utils
from ..constants import DEFAULT_SWARM_ADDR_POOL, DEFAULT_SWARM_SUBNET_SIZE from ..constants import DEFAULT_SWARM_ADDR_POOL, DEFAULT_SWARM_SUBNET_SIZE
from .. import errors
from .. import types
from .. import utils
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View File

@ -1,4 +1,5 @@
from .. import errors, utils from .. import errors
from .. import utils
class VolumeApiMixin: class VolumeApiMixin:

View File

@ -2,7 +2,8 @@ import base64
import json import json
import logging import logging
from . import credentials, errors from . import credentials
from . import errors
from .utils import config from .utils import config
INDEX_NAME = 'docker.io' INDEX_NAME = 'docker.io'
@ -21,15 +22,15 @@ def resolve_repository_name(repo_name):
index_name, remote_name = split_repo_name(repo_name) index_name, remote_name = split_repo_name(repo_name)
if index_name[0] == '-' or index_name[-1] == '-': if index_name[0] == '-' or index_name[-1] == '-':
raise errors.InvalidRepository( raise errors.InvalidRepository(
f'Invalid index name ({index_name}). ' 'Invalid index name ({}). Cannot begin or end with a'
'Cannot begin or end with a hyphen.' ' hyphen.'.format(index_name)
) )
return resolve_index_name(index_name), remote_name return resolve_index_name(index_name), remote_name
def resolve_index_name(index_name): def resolve_index_name(index_name):
index_name = convert_to_hostname(index_name) index_name = convert_to_hostname(index_name)
if index_name == f"index.{INDEX_NAME}": if index_name == 'index.' + INDEX_NAME:
index_name = INDEX_NAME index_name = INDEX_NAME
return index_name return index_name
@ -98,7 +99,9 @@ class AuthConfig(dict):
for registry, entry in entries.items(): for registry, entry in entries.items():
if not isinstance(entry, dict): if not isinstance(entry, dict):
log.debug( log.debug(
f'Config entry for key {registry} is not auth config' 'Config entry for key {} is not auth config'.format(
registry
)
) )
# We sometimes fall back to parsing the whole config as if it # We sometimes fall back to parsing the whole config as if it
# was the auth config by itself, for legacy purposes. In that # was the auth config by itself, for legacy purposes. In that
@ -106,11 +109,17 @@ class AuthConfig(dict):
# keys is not formatted properly. # keys is not formatted properly.
if raise_on_error: if raise_on_error:
raise errors.InvalidConfigFile( raise errors.InvalidConfigFile(
f'Invalid configuration for registry {registry}' 'Invalid configuration for registry {}'.format(
registry
)
) )
return {} return {}
if 'identitytoken' in entry: if 'identitytoken' in entry:
log.debug(f'Found an IdentityToken entry for registry {registry}') log.debug(
'Found an IdentityToken entry for registry {}'.format(
registry
)
)
conf[registry] = { conf[registry] = {
'IdentityToken': entry['identitytoken'] 'IdentityToken': entry['identitytoken']
} }
@ -121,15 +130,16 @@ class AuthConfig(dict):
# a valid value in the auths config. # a valid value in the auths config.
# https://github.com/docker/compose/issues/3265 # https://github.com/docker/compose/issues/3265
log.debug( log.debug(
f'Auth data for {registry} is absent. ' 'Auth data for {} is absent. Client might be using a '
f'Client might be using a credentials store instead.' 'credentials store instead.'.format(registry)
) )
conf[registry] = {} conf[registry] = {}
continue continue
username, password = decode_auth(entry['auth']) username, password = decode_auth(entry['auth'])
log.debug( log.debug(
f'Found entry (registry={registry!r}, username={username!r})' 'Found entry (registry={}, username={})'
.format(repr(registry), repr(username))
) )
conf[registry] = { conf[registry] = {
@ -267,7 +277,7 @@ class AuthConfig(dict):
except credentials.StoreError as e: except credentials.StoreError as e:
raise errors.DockerException( raise errors.DockerException(
f'Credentials store error: {repr(e)}' f'Credentials store error: {repr(e)}'
) from e )
def _get_store_instance(self, name): def _get_store_instance(self, name):
if name not in self._stores: if name not in self._stores:

View File

@ -1,5 +1,5 @@
from .api.client import APIClient from .api.client import APIClient
from .constants import DEFAULT_MAX_POOL_SIZE, DEFAULT_TIMEOUT_SECONDS from .constants import (DEFAULT_TIMEOUT_SECONDS, DEFAULT_MAX_POOL_SIZE)
from .models.configs import ConfigCollection from .models.configs import ConfigCollection
from .models.containers import ContainerCollection from .models.containers import ContainerCollection
from .models.images import ImageCollection from .models.images import ImageCollection
@ -71,6 +71,8 @@ class DockerClient:
timeout (int): Default timeout for API calls, in seconds. timeout (int): Default timeout for API calls, in seconds.
max_pool_size (int): The maximum number of connections max_pool_size (int): The maximum number of connections
to save in the pool. to save in the pool.
ssl_version (int): A valid `SSL version`_.
assert_hostname (bool): Verify the hostname of the server.
environment (dict): The environment to read environment variables environment (dict): The environment to read environment variables
from. Default: the value of ``os.environ`` from. Default: the value of ``os.environ``
credstore_env (dict): Override environment variables when calling credstore_env (dict): Override environment variables when calling

View File

@ -1,9 +1,8 @@
import sys import sys
from .version import __version__ from .version import __version__
DEFAULT_DOCKER_API_VERSION = '1.45' DEFAULT_DOCKER_API_VERSION = '1.41'
MINIMUM_DOCKER_API_VERSION = '1.24' MINIMUM_DOCKER_API_VERSION = '1.21'
DEFAULT_TIMEOUT_SECONDS = 60 DEFAULT_TIMEOUT_SECONDS = 60
STREAM_HEADER_SIZE_BYTES = 8 STREAM_HEADER_SIZE_BYTES = 8
CONTAINER_LIMITS_KEYS = [ CONTAINER_LIMITS_KEYS = [

View File

@ -1,2 +1,3 @@
from .api import ContextAPI # flake8: noqa
from .context import Context from .context import Context
from .api import ContextAPI

View File

@ -2,14 +2,11 @@ import json
import os import os
from docker import errors from docker import errors
from docker.context.config import get_meta_dir
from .config import ( from docker.context.config import METAFILE
METAFILE, from docker.context.config import get_current_context_name
get_current_context_name, from docker.context.config import write_context_name_to_docker_config
get_meta_dir, from docker.context import Context
write_context_name_to_docker_config,
)
from .context import Context
class ContextAPI: class ContextAPI:
@ -116,8 +113,8 @@ class ContextAPI:
names.append(data["Name"]) names.append(data["Name"])
except Exception as e: except Exception as e:
raise errors.ContextException( raise errors.ContextException(
f"Failed to load metafile {filename}: {e}", "Failed to load metafile {}: {}".format(
) from e filename, e))
contexts = [cls.DEFAULT_CONTEXT] contexts = [cls.DEFAULT_CONTEXT]
for name in names: for name in names:

View File

@ -1,9 +1,10 @@
import hashlib
import json
import os import os
import json
import hashlib
from docker import utils from docker import utils
from docker.constants import DEFAULT_UNIX_SOCKET, IS_WINDOWS_PLATFORM from docker.constants import IS_WINDOWS_PLATFORM
from docker.constants import DEFAULT_UNIX_SOCKET
from docker.utils.config import find_config_file from docker.utils.config import find_config_file
METAFILE = "meta.json" METAFILE = "meta.json"
@ -76,6 +77,5 @@ def get_context_host(path=None, tls=False):
host = utils.parse_host(path, IS_WINDOWS_PLATFORM, tls) host = utils.parse_host(path, IS_WINDOWS_PLATFORM, tls)
if host == DEFAULT_UNIX_SOCKET: if host == DEFAULT_UNIX_SOCKET:
# remove http+ from default docker socket url # remove http+ from default docker socket url
if host.startswith("http+"): return host.strip("http+")
host = host[5:]
return host return host

View File

@ -1,16 +1,12 @@
import json
import os import os
import json
from shutil import copyfile, rmtree from shutil import copyfile, rmtree
from docker.errors import ContextException
from docker.tls import TLSConfig from docker.tls import TLSConfig
from docker.errors import ContextException
from .config import ( from docker.context.config import get_meta_dir
get_context_host, from docker.context.config import get_meta_file
get_meta_dir, from docker.context.config import get_tls_dir
get_meta_file, from docker.context.config import get_context_host
get_tls_dir,
)
class Context: class Context:
@ -46,9 +42,8 @@ class Context:
for k, v in endpoints.items(): for k, v in endpoints.items():
if not isinstance(v, dict): if not isinstance(v, dict):
# unknown format # unknown format
raise ContextException( raise ContextException("""Unknown endpoint format for
f"Unknown endpoint format for context {name}: {v}", context {}: {}""".format(name, v))
)
self.endpoints[k] = v self.endpoints[k] = v
if k != "docker": if k != "docker":
@ -101,9 +96,8 @@ class Context:
metadata = json.load(f) metadata = json.load(f)
except (OSError, KeyError, ValueError) as e: except (OSError, KeyError, ValueError) as e:
# unknown format # unknown format
raise Exception( raise Exception("""Detected corrupted meta file for
f"Detected corrupted meta file for context {name} : {e}" context {} : {}""".format(name, e))
) from e
# for docker endpoints, set defaults for # for docker endpoints, set defaults for
# Host and SkipTLSVerify fields # Host and SkipTLSVerify fields

View File

@ -1,8 +1,4 @@
from .constants import ( # flake8: noqa
DEFAULT_LINUX_STORE,
DEFAULT_OSX_STORE,
DEFAULT_WIN32_STORE,
PROGRAM_PREFIX,
)
from .errors import CredentialsNotFound, StoreError
from .store import Store from .store import Store
from .errors import StoreError, CredentialsNotFound
from .constants import *

View File

@ -13,5 +13,13 @@ class InitializationError(StoreError):
def process_store_error(cpe, program): def process_store_error(cpe, program):
message = cpe.output.decode('utf-8') message = cpe.output.decode('utf-8')
if 'credentials not found in native keychain' in message: if 'credentials not found in native keychain' in message:
return CredentialsNotFound(f'No matching credentials in {program}') return CredentialsNotFound(
return StoreError(f'Credentials store {program} exited with "{message}".') 'No matching credentials in {}'.format(
program
)
)
return StoreError(
'Credentials store {} exited with "{}".'.format(
program, cpe.output.decode('utf-8').strip()
)
)

View File

@ -4,7 +4,8 @@ import shutil
import subprocess import subprocess
import warnings import warnings
from . import constants, errors from . import constants
from . import errors
from .utils import create_environment_dict from .utils import create_environment_dict
@ -19,8 +20,9 @@ class Store:
self.environment = environment self.environment = environment
if self.exe is None: if self.exe is None:
warnings.warn( warnings.warn(
f'{self.program} not installed or not available in PATH', '{} not installed or not available in PATH'.format(
stacklevel=1, self.program
)
) )
def get(self, server): def get(self, server):
@ -71,8 +73,10 @@ class Store:
def _execute(self, subcmd, data_input): def _execute(self, subcmd, data_input):
if self.exe is None: if self.exe is None:
raise errors.StoreError( raise errors.StoreError(
f'{self.program} not installed or not available in PATH' '{} not installed or not available in PATH'.format(
) self.program
)
)
output = None output = None
env = create_environment_dict(self.environment) env = create_environment_dict(self.environment)
try: try:
@ -80,14 +84,18 @@ class Store:
[self.exe, subcmd], input=data_input, env=env, [self.exe, subcmd], input=data_input, env=env,
) )
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
raise errors.process_store_error(e, self.program) from e raise errors.process_store_error(e, self.program)
except OSError as e: except OSError as e:
if e.errno == errno.ENOENT: if e.errno == errno.ENOENT:
raise errors.StoreError( raise errors.StoreError(
f'{self.program} not installed or not available in PATH' '{} not installed or not available in PATH'.format(
) from e self.program
)
)
else: else:
raise errors.StoreError( raise errors.StoreError(
f'Unexpected OS error "{e.strerror}", errno={e.errno}' 'Unexpected OS error "{}", errno={}'.format(
) from e e.strerror, e.errno
)
)
return output return output

View File

@ -27,7 +27,7 @@ def create_api_error_from_http_exception(e):
try: try:
explanation = response.json()['message'] explanation = response.json()['message']
except ValueError: except ValueError:
explanation = (response.text or '').strip() explanation = (response.content or '').strip()
cls = APIError cls = APIError
if response.status_code == 404: if response.status_code == 404:
explanation_msg = (explanation or '').lower() explanation_msg = (explanation or '').lower()
@ -54,16 +54,14 @@ class APIError(requests.exceptions.HTTPError, DockerException):
message = super().__str__() message = super().__str__()
if self.is_client_error(): if self.is_client_error():
message = ( message = '{} Client Error for {}: {}'.format(
f'{self.response.status_code} Client Error for ' self.response.status_code, self.response.url,
f'{self.response.url}: {self.response.reason}' self.response.reason)
)
elif self.is_server_error(): elif self.is_server_error():
message = ( message = '{} Server Error for {}: {}'.format(
f'{self.response.status_code} Server Error for ' self.response.status_code, self.response.url,
f'{self.response.url}: {self.response.reason}' self.response.reason)
)
if self.explanation: if self.explanation:
message = f'{message} ("{self.explanation}")' message = f'{message} ("{self.explanation}")'
@ -144,10 +142,10 @@ class ContainerError(DockerException):
self.stderr = stderr self.stderr = stderr
err = f": {stderr}" if stderr is not None else "" err = f": {stderr}" if stderr is not None else ""
super().__init__( msg = ("Command '{}' in image '{}' returned non-zero exit "
f"Command '{command}' in image '{image}' " "status {}{}").format(command, image, exit_status, err)
f"returned non-zero exit status {exit_status}{err}"
) super().__init__(msg)
class StreamParseError(RuntimeError): class StreamParseError(RuntimeError):

View File

@ -1,5 +1,5 @@
from ..api import APIClient from ..api import APIClient
from .resource import Collection, Model from .resource import Model, Collection
class Config(Model): class Config(Model):
@ -30,7 +30,6 @@ class ConfigCollection(Collection):
def create(self, **kwargs): def create(self, **kwargs):
obj = self.client.api.create_config(**kwargs) obj = self.client.api.create_config(**kwargs)
obj.setdefault("Spec", {})["Name"] = kwargs.get("name")
return self.prepare_model(obj) return self.prepare_model(obj)
create.__doc__ = APIClient.create_config.__doc__ create.__doc__ = APIClient.create_config.__doc__

View File

@ -5,13 +5,10 @@ from collections import namedtuple
from ..api import APIClient from ..api import APIClient
from ..constants import DEFAULT_DATA_CHUNK_SIZE from ..constants import DEFAULT_DATA_CHUNK_SIZE
from ..errors import ( from ..errors import (
ContainerError, ContainerError, DockerException, ImageNotFound,
DockerException, NotFound, create_unexpected_kwargs_error
ImageNotFound,
NotFound,
create_unexpected_kwargs_error,
) )
from ..types import HostConfig, NetworkingConfig from ..types import HostConfig
from ..utils import version_gte from ..utils import version_gte
from .images import Image from .images import Image
from .resource import Collection, Model from .resource import Collection, Model
@ -24,7 +21,6 @@ class Container(Model):
query the Docker daemon for the current properties, causing query the Docker daemon for the current properties, causing
:py:attr:`attrs` to be refreshed. :py:attr:`attrs` to be refreshed.
""" """
@property @property
def name(self): def name(self):
""" """
@ -51,11 +47,11 @@ class Container(Model):
try: try:
result = self.attrs['Config'].get('Labels') result = self.attrs['Config'].get('Labels')
return result or {} return result or {}
except KeyError as ke: except KeyError:
raise DockerException( raise DockerException(
'Label data is not available for sparse objects. Call reload()' 'Label data is not available for sparse objects. Call reload()'
' to retrieve all information' ' to retrieve all information'
) from ke )
@property @property
def status(self): def status(self):
@ -66,15 +62,6 @@ class Container(Model):
return self.attrs['State']['Status'] return self.attrs['State']['Status']
return self.attrs['State'] return self.attrs['State']
@property
def health(self):
"""
The healthcheck status of the container.
For example, ``healthy`, or ``unhealthy`.
"""
return self.attrs.get('State', {}).get('Health', {}).get('Status', 'unknown')
@property @property
def ports(self): def ports(self):
""" """
@ -134,7 +121,6 @@ class Container(Model):
tag (str): The tag to push tag (str): The tag to push
message (str): A commit message message (str): A commit message
author (str): The name of the author author (str): The name of the author
pause (bool): Whether to pause the container before committing
changes (str): Dockerfile instructions to apply while committing changes (str): Dockerfile instructions to apply while committing
conf (dict): The configuration for the container. See the conf (dict): The configuration for the container. See the
`Engine API documentation `Engine API documentation
@ -155,8 +141,7 @@ class Container(Model):
Inspect changes on a container's filesystem. Inspect changes on a container's filesystem.
Returns: Returns:
(list) A list of dictionaries containing the attributes `Path` (str)
and `Kind`.
Raises: Raises:
:py:class:`docker.errors.APIError` :py:class:`docker.errors.APIError`
@ -181,8 +166,7 @@ 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. Ignored if ``detach`` is true. stream (bool): Stream response data. Default: False
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
@ -314,7 +298,7 @@ class Container(Model):
float (in nanoseconds) float (in nanoseconds)
Returns: Returns:
(generator of bytes or bytes): Logs from the container. (generator or str): Logs from the container.
Raises: Raises:
:py:class:`docker.errors.APIError` :py:class:`docker.errors.APIError`
@ -695,14 +679,10 @@ class ContainerCollection(Collection):
This mode is incompatible with ``ports``. This mode is incompatible with ``ports``.
Incompatible with ``network``. Incompatible with ``network``.
networking_config (Dict[str, EndpointConfig]): network_driver_opt (dict): A dictionary of options to provide
Dictionary of EndpointConfig objects for each container network. to the network driver. Defaults to ``None``. Used in
The key is the name of the network. conjuction with ``network``. Incompatible
Defaults to ``None``. with ``network_mode``.
Used in conjuction with ``network``.
Incompatible with ``network_mode``.
oom_kill_disable (bool): Whether to disable OOM killer. oom_kill_disable (bool): Whether to disable OOM killer.
oom_score_adj (int): An integer value containing the score given oom_score_adj (int): An integer value containing the score given
to the container in order to tune OOM killer preferences. to the container in order to tune OOM killer preferences.
@ -867,9 +847,9 @@ class ContainerCollection(Collection):
'together.' 'together.'
) )
if kwargs.get('networking_config') and not kwargs.get('network'): if kwargs.get('network_driver_opt') and not kwargs.get('network'):
raise RuntimeError( raise RuntimeError(
'The option "networking_config" can not be used ' 'The options "network_driver_opt" can not be used '
'without "network".' 'without "network".'
) )
@ -907,9 +887,9 @@ class ContainerCollection(Collection):
container, exit_status, command, image, out container, exit_status, command, image, out
) )
if stream or out is None: return out if stream or out is None else b''.join(
return out [line for line in out]
return b''.join(out) )
def create(self, image, command=None, **kwargs): def create(self, image, command=None, **kwargs):
""" """
@ -1025,7 +1005,6 @@ class ContainerCollection(Collection):
def prune(self, filters=None): def prune(self, filters=None):
return self.client.api.prune_containers(filters=filters) return self.client.api.prune_containers(filters=filters)
prune.__doc__ = APIClient.prune_containers.__doc__ prune.__doc__ = APIClient.prune_containers.__doc__
@ -1144,17 +1123,12 @@ def _create_container_args(kwargs):
host_config_kwargs['binds'] = volumes host_config_kwargs['binds'] = volumes
network = kwargs.pop('network', None) network = kwargs.pop('network', None)
networking_config = kwargs.pop('networking_config', None) network_driver_opt = kwargs.pop('network_driver_opt', None)
if network: if network:
if networking_config: network_configuration = {'driver_opt': network_driver_opt} \
# Sanity check: check if the network is defined in the if network_driver_opt else None
# networking config dict, otherwise switch to None
if network not in networking_config:
networking_config = None
create_kwargs['networking_config'] = NetworkingConfig( create_kwargs['networking_config'] = {network: network_configuration}
networking_config
) if networking_config else {network: None}
host_config_kwargs['network_mode'] = network host_config_kwargs['network_mode'] = network
# All kwargs should have been consumed by this point, so raise # All kwargs should have been consumed by this point, so raise

View File

@ -15,8 +15,10 @@ class Image(Model):
An image on the server. An image on the server.
""" """
def __repr__(self): def __repr__(self):
tag_str = "', '".join(self.tags) return "<{}: '{}'>".format(
return f"<{self.__class__.__name__}: '{tag_str}'>" self.__class__.__name__,
"', '".join(self.tags),
)
@property @property
def labels(self): def labels(self):
@ -51,7 +53,7 @@ class Image(Model):
Show the history of an image. Show the history of an image.
Returns: Returns:
(list): The history of the image. (str): The history of the image.
Raises: Raises:
:py:class:`docker.errors.APIError` :py:class:`docker.errors.APIError`
@ -407,8 +409,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 'errorDetail' in chunk: if 'error' in chunk:
raise ImageLoadError(chunk['errorDetail']['message']) raise ImageLoadError(chunk['error'])
return [self.get(i) for i in images] return [self.get(i) for i in images]
@ -456,8 +458,7 @@ class ImageCollection(Collection):
if 'stream' in kwargs: if 'stream' in kwargs:
warnings.warn( warnings.warn(
'`stream` is not a valid parameter for this method' '`stream` is not a valid parameter for this method'
' and will be overridden', ' and will be overridden'
stacklevel=1,
) )
del kwargs['stream'] del kwargs['stream']
@ -470,8 +471,9 @@ class ImageCollection(Collection):
# to be pulled. # to be pulled.
pass pass
if not all_tags: if not all_tags:
sep = '@' if tag.startswith('sha256:') else ':' return self.get('{0}{2}{1}'.format(
return self.get(f'{repository}{sep}{tag}') repository, tag, '@' if tag.startswith('sha256:') else ':'
))
return self.list(repository) return self.list(repository)
def push(self, repository, tag=None, **kwargs): def push(self, repository, tag=None, **kwargs):

View File

@ -1,7 +1,7 @@
from ..api import APIClient from ..api import APIClient
from ..utils import version_gte from ..utils import version_gte
from .containers import Container from .containers import Container
from .resource import Collection, Model from .resource import Model, Collection
class Network(Model): class Network(Model):

View File

@ -1,4 +1,4 @@
from .resource import Collection, Model from .resource import Model, Collection
class Node(Model): class Node(Model):

View File

@ -187,7 +187,7 @@ class PluginCollection(Collection):
""" """
privileges = self.client.api.plugin_privileges(remote_name) privileges = self.client.api.plugin_privileges(remote_name)
it = self.client.api.pull_plugin(remote_name, privileges, local_name) it = self.client.api.pull_plugin(remote_name, privileges, local_name)
for _data in it: for data in it:
pass pass
return self.get(local_name or remote_name) return self.get(local_name or remote_name)

View File

@ -64,10 +64,9 @@ class Collection:
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
raise TypeError( raise TypeError(
f"'{self.__class__.__name__}' object is not callable. " "'{}' object is not callable. You might be trying to use the old "
"You might be trying to use the old (pre-2.0) API - " "(pre-2.0) API - use docker.APIClient if so."
"use docker.APIClient if so." .format(self.__class__.__name__))
)
def list(self): def list(self):
raise NotImplementedError raise NotImplementedError
@ -89,4 +88,5 @@ class Collection:
elif isinstance(attrs, dict): elif isinstance(attrs, dict):
return self.model(attrs=attrs, client=self.client, collection=self) return self.model(attrs=attrs, client=self.client, collection=self)
else: else:
raise Exception(f"Can't create {self.model.__name__} from {attrs}") raise Exception("Can't create %s from %s" %
(self.model.__name__, attrs))

View File

@ -1,5 +1,5 @@
from ..api import APIClient from ..api import APIClient
from .resource import Collection, Model from .resource import Model, Collection
class Secret(Model): class Secret(Model):

View File

@ -1,9 +1,7 @@
import copy import copy
from docker.errors import create_unexpected_kwargs_error, InvalidArgument
from docker.errors import InvalidArgument, create_unexpected_kwargs_error from docker.types import TaskTemplate, ContainerSpec, Placement, ServiceMode
from docker.types import ContainerSpec, Placement, ServiceMode, TaskTemplate from .resource import Model, Collection
from .resource import Collection, Model
class Service(Model): class Service(Model):

View File

@ -1,6 +1,5 @@
from docker.api import APIClient from docker.api import APIClient
from docker.errors import APIError from docker.errors import APIError
from .resource import Model from .resource import Model

View File

@ -1,5 +1,5 @@
from ..api import APIClient from ..api import APIClient
from .resource import Collection, Model from .resource import Model, Collection
class Volume(Model): class Volume(Model):

View File

@ -1,6 +1,8 @@
import os import os
import ssl
from . import errors from . import errors
from .transport import SSLHTTPAdapter
class TLSConfig: class TLSConfig:
@ -13,18 +15,35 @@ class TLSConfig:
verify (bool or str): This can be a bool or a path to a CA cert verify (bool or str): This can be a bool or a path to a CA cert
file to verify against. If ``True``, verify using ca_cert; file to verify against. If ``True``, verify using ca_cert;
if ``False`` or not specified, do not verify. if ``False`` or not specified, do not verify.
ssl_version (int): A valid `SSL version`_.
assert_hostname (bool): Verify the hostname of the server.
.. _`SSL version`:
https://docs.python.org/3.5/library/ssl.html#ssl.PROTOCOL_TLSv1
""" """
cert = None cert = None
ca_cert = None ca_cert = None
verify = None verify = None
ssl_version = None
def __init__(self, client_cert=None, ca_cert=None, verify=None): def __init__(self, client_cert=None, ca_cert=None, verify=None,
ssl_version=None, assert_hostname=None,
assert_fingerprint=None):
# Argument compatibility/mapping with # Argument compatibility/mapping with
# https://docs.docker.com/engine/articles/https/ # https://docs.docker.com/engine/articles/https/
# This diverges from the Docker CLI in that users can specify 'tls' # This diverges from the Docker CLI in that users can specify 'tls'
# here, but also disable any public/default CA pool verification by # here, but also disable any public/default CA pool verification by
# leaving verify=False # leaving verify=False
self.assert_hostname = assert_hostname
self.assert_fingerprint = assert_fingerprint
# If the user provides an SSL version, we should use their preference
if ssl_version:
self.ssl_version = ssl_version
else:
self.ssl_version = ssl.PROTOCOL_TLS_CLIENT
# "client_cert" must have both or neither cert/key files. In # "client_cert" must have both or neither cert/key files. In
# either case, Alert the user when both are expected, but any are # either case, Alert the user when both are expected, but any are
# missing. # missing.
@ -36,7 +55,7 @@ class TLSConfig:
raise errors.TLSParameterError( raise errors.TLSParameterError(
'client_cert must be a tuple of' 'client_cert must be a tuple of'
' (client certificate, key file)' ' (client certificate, key file)'
) from None )
if not (tls_cert and tls_key) or (not os.path.isfile(tls_cert) or if not (tls_cert and tls_key) or (not os.path.isfile(tls_cert) or
not os.path.isfile(tls_key)): not os.path.isfile(tls_key)):
@ -58,6 +77,8 @@ class TLSConfig:
""" """
Configure a client with these TLS options. Configure a client with these TLS options.
""" """
client.ssl_version = self.ssl_version
if self.verify and self.ca_cert: if self.verify and self.ca_cert:
client.verify = self.ca_cert client.verify = self.ca_cert
else: else:
@ -65,3 +86,9 @@ class TLSConfig:
if self.cert: if self.cert:
client.cert = self.cert client.cert = self.cert
client.mount('https://', SSLHTTPAdapter(
ssl_version=self.ssl_version,
assert_hostname=self.assert_hostname,
assert_fingerprint=self.assert_fingerprint,
))

View File

@ -1,5 +1,6 @@
# flake8: noqa
from .unixconn import UnixHTTPAdapter from .unixconn import UnixHTTPAdapter
from .ssladapter import SSLHTTPAdapter
try: try:
from .npipeconn import NpipeHTTPAdapter from .npipeconn import NpipeHTTPAdapter
from .npipesocket import NpipeSocket from .npipesocket import NpipeSocket

View File

@ -6,8 +6,3 @@ class BaseHTTPAdapter(requests.adapters.HTTPAdapter):
super().close() super().close()
if hasattr(self, 'pools'): if hasattr(self, 'pools'):
self.pools.clear() self.pools.clear()
# Fix for requests 2.32.2+:
# https://github.com/psf/requests/commit/c98e4d133ef29c46a9b68cd783087218a8075e05
def get_connection_with_tls_context(self, request, verify, proxies=None, cert=None):
return self.get_connection(request.url, proxies)

View File

@ -1,13 +1,13 @@
import queue import queue
import requests.adapters import requests.adapters
from docker.transport.basehttpadapter import BaseHTTPAdapter
from .. import constants
from .npipesocket import NpipeSocket
import urllib3 import urllib3
import urllib3.connection import urllib3.connection
from .. import constants
from .basehttpadapter import BaseHTTPAdapter
from .npipesocket import NpipeSocket
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
@ -46,8 +46,9 @@ class NpipeHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
conn = None conn = None
try: try:
conn = self.pool.get(block=self.block, timeout=timeout) conn = self.pool.get(block=self.block, timeout=timeout)
except AttributeError as ae: # self.pool is None
raise urllib3.exceptions.ClosedPoolError(self, "Pool is closed.") from ae except AttributeError: # self.pool is None
raise urllib3.exceptions.ClosedPoolError(self, "Pool is closed.")
except queue.Empty: except queue.Empty:
if self.block: if self.block:
@ -55,7 +56,7 @@ class NpipeHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
self, self,
"Pool reached maximum size and no more " "Pool reached maximum size and no more "
"connections are allowed." "connections are allowed."
) from None )
# Oh well, we'll create a new connection then # Oh well, we'll create a new connection then
return conn or self._new_conn() return conn or self._new_conn()

View File

@ -1,10 +1,7 @@
import functools import functools
import io
import time import time
import io
import pywintypes
import win32api
import win32event
import win32file import win32file
import win32pipe import win32pipe
@ -57,9 +54,7 @@ class NpipeSocket:
0, 0,
None, None,
win32file.OPEN_EXISTING, win32file.OPEN_EXISTING,
(cSECURITY_ANONYMOUS cSECURITY_ANONYMOUS | cSECURITY_SQOS_PRESENT,
| cSECURITY_SQOS_PRESENT
| win32file.FILE_FLAG_OVERLAPPED),
0 0
) )
except win32pipe.error as e: except win32pipe.error as e:
@ -136,37 +131,22 @@ class NpipeSocket:
if not isinstance(buf, memoryview): if not isinstance(buf, memoryview):
readbuf = memoryview(buf) readbuf = memoryview(buf)
event = win32event.CreateEvent(None, True, True, None) err, data = win32file.ReadFile(
try: self._handle,
overlapped = pywintypes.OVERLAPPED() readbuf[:nbytes] if nbytes else readbuf
overlapped.hEvent = event )
err, data = win32file.ReadFile( return len(data)
self._handle,
readbuf[:nbytes] if nbytes else readbuf, def _recv_into_py2(self, buf, nbytes):
overlapped err, data = win32file.ReadFile(self._handle, nbytes or len(buf))
) n = len(data)
wait_result = win32event.WaitForSingleObject(event, self._timeout) buf[:n] = data
if wait_result == win32event.WAIT_TIMEOUT: return n
win32file.CancelIo(self._handle)
raise TimeoutError
return win32file.GetOverlappedResult(self._handle, overlapped, 0)
finally:
win32api.CloseHandle(event)
@check_closed @check_closed
def send(self, string, flags=0): def send(self, string, flags=0):
event = win32event.CreateEvent(None, True, True, None) err, nbytes = win32file.WriteFile(self._handle, string)
try: return nbytes
overlapped = pywintypes.OVERLAPPED()
overlapped.hEvent = event
win32file.WriteFile(self._handle, string, overlapped)
wait_result = win32event.WaitForSingleObject(event, self._timeout)
if wait_result == win32event.WAIT_TIMEOUT:
win32file.CancelIo(self._handle)
raise TimeoutError
return win32file.GetOverlappedResult(self._handle, overlapped, 0)
finally:
win32api.CloseHandle(event)
@check_closed @check_closed
def sendall(self, string, flags=0): def sendall(self, string, flags=0):
@ -185,12 +165,15 @@ class NpipeSocket:
def settimeout(self, value): def settimeout(self, value):
if value is None: if value is None:
# Blocking mode # Blocking mode
self._timeout = win32event.INFINITE self._timeout = win32pipe.NMPWAIT_WAIT_FOREVER
elif not isinstance(value, (float, int)) or value < 0: elif not isinstance(value, (float, int)) or value < 0:
raise ValueError('Timeout value out of range') raise ValueError('Timeout value out of range')
elif value == 0:
# Non-blocking mode
self._timeout = win32pipe.NMPWAIT_NO_WAIT
else: else:
# Timeout mode - Value converted to milliseconds # Timeout mode - Value converted to milliseconds
self._timeout = int(value * 1000) self._timeout = value * 1000
def gettimeout(self): def gettimeout(self):
return self._timeout return self._timeout

View File

@ -1,19 +1,19 @@
import paramiko
import queue
import urllib.parse
import requests.adapters
import logging import logging
import os import os
import queue
import signal import signal
import socket import socket
import subprocess import subprocess
import urllib.parse
import paramiko from docker.transport.basehttpadapter import BaseHTTPAdapter
import requests.adapters from .. import constants
import urllib3 import urllib3
import urllib3.connection import urllib3.connection
from .. import constants
from .basehttpadapter import BaseHTTPAdapter
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
@ -141,8 +141,8 @@ class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
try: try:
conn = self.pool.get(block=self.block, timeout=timeout) conn = self.pool.get(block=self.block, timeout=timeout)
except AttributeError as ae: # self.pool is None except AttributeError: # self.pool is None
raise urllib3.exceptions.ClosedPoolError(self, "Pool is closed.") from ae raise urllib3.exceptions.ClosedPoolError(self, "Pool is closed.")
except queue.Empty: except queue.Empty:
if self.block: if self.block:
@ -150,7 +150,7 @@ class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
self, self,
"Pool reached maximum size and no more " "Pool reached maximum size and no more "
"connections are allowed." "connections are allowed."
) from None )
# Oh well, we'll create a new connection then # Oh well, we'll create a new connection then
return conn or self._new_conn() return conn or self._new_conn()

View File

@ -0,0 +1,62 @@
""" Resolves OpenSSL issues in some servers:
https://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/
https://github.com/kennethreitz/requests/pull/799
"""
from packaging.version import Version
from requests.adapters import HTTPAdapter
from docker.transport.basehttpadapter import BaseHTTPAdapter
import urllib3
PoolManager = urllib3.poolmanager.PoolManager
class SSLHTTPAdapter(BaseHTTPAdapter):
'''An HTTPS Transport Adapter that uses an arbitrary SSL version.'''
__attrs__ = HTTPAdapter.__attrs__ + ['assert_fingerprint',
'assert_hostname',
'ssl_version']
def __init__(self, ssl_version=None, assert_hostname=None,
assert_fingerprint=None, **kwargs):
self.ssl_version = ssl_version
self.assert_hostname = assert_hostname
self.assert_fingerprint = assert_fingerprint
super().__init__(**kwargs)
def init_poolmanager(self, connections, maxsize, block=False):
kwargs = {
'num_pools': connections,
'maxsize': maxsize,
'block': block,
'assert_hostname': self.assert_hostname,
'assert_fingerprint': self.assert_fingerprint,
}
if self.ssl_version and self.can_override_ssl_version():
kwargs['ssl_version'] = self.ssl_version
self.poolmanager = PoolManager(**kwargs)
def get_connection(self, *args, **kwargs):
"""
Ensure assert_hostname is set correctly on our pool
We already take care of a normal poolmanager via init_poolmanager
But we still need to take care of when there is a proxy poolmanager
"""
conn = super().get_connection(*args, **kwargs)
if conn.assert_hostname != self.assert_hostname:
conn.assert_hostname = self.assert_hostname
return conn
def can_override_ssl_version(self):
urllib_ver = urllib3.__version__.split('-')[0]
if urllib_ver is None:
return False
if urllib_ver == 'dev':
return True
return Version(urllib_ver) > Version('1.5')

View File

@ -1,11 +1,12 @@
import requests.adapters
import socket import socket
import requests.adapters from docker.transport.basehttpadapter import BaseHTTPAdapter
from .. import constants
import urllib3 import urllib3
import urllib3.connection import urllib3.connection
from .. import constants
from .basehttpadapter import BaseHTTPAdapter
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
@ -54,7 +55,7 @@ class UnixHTTPAdapter(BaseHTTPAdapter):
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE): max_pool_size=constants.DEFAULT_MAX_POOL_SIZE):
socket_path = socket_url.replace('http+unix://', '') socket_path = socket_url.replace('http+unix://', '')
if not socket_path.startswith('/'): if not socket_path.startswith('/'):
socket_path = f"/{socket_path}" socket_path = '/' + socket_path
self.socket_path = socket_path self.socket_path = socket_path
self.timeout = timeout self.timeout = timeout
self.max_pool_size = max_pool_size self.max_pool_size = max_pool_size

View File

@ -1,24 +1,14 @@
from .containers import ContainerConfig, DeviceRequest, HostConfig, LogConfig, Ulimit # flake8: noqa
from .containers import (
ContainerConfig, HostConfig, LogConfig, Ulimit, DeviceRequest
)
from .daemon import CancellableStream from .daemon import CancellableStream
from .healthcheck import Healthcheck from .healthcheck import Healthcheck
from .networks import EndpointConfig, IPAMConfig, IPAMPool, NetworkingConfig from .networks import EndpointConfig, IPAMConfig, IPAMPool, NetworkingConfig
from .services import ( from .services import (
ConfigReference, ConfigReference, ContainerSpec, DNSConfig, DriverConfig, EndpointSpec,
ContainerSpec, Mount, Placement, PlacementPreference, Privileges, Resources,
DNSConfig, RestartPolicy, RollbackConfig, SecretReference, ServiceMode, TaskTemplate,
DriverConfig, UpdateConfig, NetworkAttachmentConfig
EndpointSpec,
Mount,
NetworkAttachmentConfig,
Placement,
PlacementPreference,
Privileges,
Resources,
RestartPolicy,
RollbackConfig,
SecretReference,
ServiceMode,
TaskTemplate,
UpdateConfig,
) )
from .swarm import SwarmExternalCA, SwarmSpec from .swarm import SwarmSpec, SwarmExternalCA

View File

@ -1,16 +1,8 @@
from .. import errors from .. import errors
from ..utils.utils import ( from ..utils.utils import (
convert_port_bindings, convert_port_bindings, convert_tmpfs_mounts, convert_volume_binds,
convert_tmpfs_mounts, format_environment, format_extra_hosts, normalize_links, parse_bytes,
convert_volume_binds, parse_devices, split_command, version_gte, version_lt,
format_environment,
format_extra_hosts,
normalize_links,
parse_bytes,
parse_devices,
split_command,
version_gte,
version_lt,
) )
from .base import DictType from .base import DictType
from .healthcheck import Healthcheck from .healthcheck import Healthcheck
@ -56,11 +48,8 @@ class LogConfig(DictType):
>>> container = client.create_container('busybox', 'true', >>> container = client.create_container('busybox', 'true',
... host_config=hc) ... host_config=hc)
>>> client.inspect_container(container)['HostConfig']['LogConfig'] >>> client.inspect_container(container)['HostConfig']['LogConfig']
{ {'Type': 'json-file', 'Config': {'labels': 'production_status,geo', 'max-size': '1g'}}
'Type': 'json-file', """ # noqa: E501
'Config': {'labels': 'production_status,geo', 'max-size': '1g'}
}
"""
types = LogConfigTypesEnum types = LogConfigTypesEnum
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -663,25 +652,25 @@ class HostConfig(dict):
def host_config_type_error(param, param_value, expected): def host_config_type_error(param, param_value, expected):
return TypeError( error_msg = 'Invalid type for {0} param: expected {1} but found {2}'
f'Invalid type for {param} param: expected {expected} ' return TypeError(error_msg.format(param, expected, type(param_value)))
f'but found {type(param_value)}'
)
def host_config_version_error(param, version, less_than=True): def host_config_version_error(param, version, less_than=True):
operator = '<' if less_than else '>' operator = '<' if less_than else '>'
return errors.InvalidVersion( error_msg = '{0} param is not supported in API versions {1} {2}'
f'{param} param is not supported in API versions {operator} {version}', return errors.InvalidVersion(error_msg.format(param, operator, version))
)
def host_config_value_error(param, param_value): def host_config_value_error(param, param_value):
return ValueError(f'Invalid value for {param} param: {param_value}') error_msg = 'Invalid value for {0} param: {1}'
return ValueError(error_msg.format(param, param_value))
def host_config_incompatible_error(param, param_value, incompatible_param): def host_config_incompatible_error(param, param_value, incompatible_param):
error_msg = '\"{1}\" {0} is incompatible with {2}'
return errors.InvalidArgument( return errors.InvalidArgument(
f'\"{param_value}\" {param} is incompatible with {incompatible_param}' error_msg.format(param, param_value, incompatible_param)
) )

View File

@ -28,9 +28,9 @@ class CancellableStream:
try: try:
return next(self._stream) return next(self._stream)
except urllib3.exceptions.ProtocolError: except urllib3.exceptions.ProtocolError:
raise StopIteration from None raise StopIteration
except OSError: except OSError:
raise StopIteration from None raise StopIteration
next = __next__ next = __next__

View File

@ -1,12 +1,8 @@
from .. import errors from .. import errors
from ..constants import IS_WINDOWS_PLATFORM from ..constants import IS_WINDOWS_PLATFORM
from ..utils import ( from ..utils import (
check_resource, check_resource, format_environment, format_extra_hosts, parse_bytes,
convert_service_networks, split_command, convert_service_networks,
format_environment,
format_extra_hosts,
parse_bytes,
split_command,
) )
@ -242,7 +238,6 @@ 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.
""" """
@ -250,7 +245,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, subpath=None): tmpfs_mode=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'):
@ -268,7 +263,7 @@ class Mount(dict):
self['BindOptions'] = { self['BindOptions'] = {
'Propagation': propagation 'Propagation': propagation
} }
if any([labels, driver_config, no_copy, tmpfs_size, tmpfs_mode, subpath]): if any([labels, driver_config, no_copy, tmpfs_size, tmpfs_mode]):
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.'
@ -281,8 +276,6 @@ 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]):
@ -377,8 +370,8 @@ def _convert_generic_resources_dict(generic_resources):
return generic_resources return generic_resources
if not isinstance(generic_resources, dict): if not isinstance(generic_resources, dict):
raise errors.InvalidArgument( raise errors.InvalidArgument(
'generic_resources must be a dict or a list ' 'generic_resources must be a dict or a list'
f'(found {type(generic_resources)})' ' (found {})'.format(type(generic_resources))
) )
resources = [] resources = []
for kind, value in generic_resources.items(): for kind, value in generic_resources.items():
@ -388,9 +381,9 @@ def _convert_generic_resources_dict(generic_resources):
elif isinstance(value, str): elif isinstance(value, str):
resource_type = 'NamedResourceSpec' resource_type = 'NamedResourceSpec'
else: else:
kv = {kind: value}
raise errors.InvalidArgument( raise errors.InvalidArgument(
f'Unsupported generic resource reservation type: {kv}' 'Unsupported generic resource reservation '
'type: {}'.format({kind: value})
) )
resources.append({ resources.append({
resource_type: {'Kind': kind, 'Value': value} resource_type: {'Kind': kind, 'Value': value}
@ -771,8 +764,8 @@ class PlacementPreference(dict):
def __init__(self, strategy, descriptor): def __init__(self, strategy, descriptor):
if strategy != 'spread': if strategy != 'spread':
raise errors.InvalidArgument( raise errors.InvalidArgument(
f'PlacementPreference strategy value is invalid ({strategy}): ' 'PlacementPreference strategy value is invalid ({}):'
'must be "spread".' ' must be "spread".'.format(strategy)
) )
self['Spread'] = {'SpreadDescriptor': descriptor} self['Spread'] = {'SpreadDescriptor': descriptor}

View File

@ -1,28 +1,13 @@
# flake8: noqa
from .build import create_archive, exclude_paths, match_tag, mkbuildcontext, tar from .build import create_archive, exclude_paths, mkbuildcontext, tar
from .decorators import check_resource, minimum_version, update_headers from .decorators import check_resource, minimum_version, update_headers
from .utils import ( from .utils import (
compare_version, compare_version, convert_port_bindings, convert_volume_binds,
convert_filters, parse_repository_tag, parse_host,
convert_port_bindings, kwargs_from_env, convert_filters, datetime_to_timestamp,
convert_service_networks, create_host_config, parse_bytes, parse_env_file, version_lt,
convert_volume_binds, version_gte, decode_json_header, split_command, create_ipam_config,
create_host_config, create_ipam_pool, parse_devices, normalize_links, convert_service_networks,
create_ipam_config, format_environment, format_extra_hosts
create_ipam_pool,
datetime_to_timestamp,
decode_json_header,
format_environment,
format_extra_hosts,
kwargs_from_env,
normalize_links,
parse_bytes,
parse_devices,
parse_env_file,
parse_host,
parse_repository_tag,
split_command,
version_gte,
version_lt,
) )

View File

@ -4,19 +4,11 @@ import re
import tarfile import tarfile
import tempfile import tempfile
from ..constants import IS_WINDOWS_PLATFORM
from .fnmatch import fnmatch from .fnmatch import fnmatch
from ..constants import IS_WINDOWS_PLATFORM
_SEP = re.compile('/|\\\\') if IS_WINDOWS_PLATFORM else re.compile('/') _SEP = re.compile('/|\\\\') if IS_WINDOWS_PLATFORM else re.compile('/')
_TAG = re.compile(
r"^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*"
r"(?::[0-9]+)?(/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*"
r"(:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127})?$"
)
def match_tag(tag: str) -> bool:
return bool(_TAG.match(tag))
def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False): def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False):
@ -50,7 +42,7 @@ def exclude_paths(root, patterns, dockerfile=None):
if dockerfile is None: if dockerfile is None:
dockerfile = 'Dockerfile' dockerfile = 'Dockerfile'
patterns.append(f"!{dockerfile}") patterns.append('!' + dockerfile)
pm = PatternMatcher(patterns) pm = PatternMatcher(patterns)
return set(pm.walk(root)) return set(pm.walk(root))
@ -101,10 +93,10 @@ def create_archive(root, files=None, fileobj=None, gzip=False,
try: try:
with open(full_path, 'rb') as f: with open(full_path, 'rb') as f:
t.addfile(i, f) t.addfile(i, f)
except OSError as oe: except OSError:
raise OSError( raise OSError(
f'Can not read file in context: {full_path}' f'Can not read file in context: {full_path}'
) from oe )
else: else:
# Directories, FIFOs, symlinks... don't need to be read. # Directories, FIFOs, symlinks... don't need to be read.
t.addfile(i, None) t.addfile(i, None)
@ -188,7 +180,7 @@ class PatternMatcher:
fpath = os.path.join( fpath = os.path.join(
os.path.relpath(current_dir, root), f os.path.relpath(current_dir, root), f
) )
if fpath.startswith(f".{os.path.sep}"): if fpath.startswith('.' + os.path.sep):
fpath = fpath[2:] fpath = fpath[2:]
match = self.matches(fpath) match = self.matches(fpath)
if not match: if not match:

View File

@ -27,7 +27,9 @@ def minimum_version(version):
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
if utils.version_lt(self._version, version): if utils.version_lt(self._version, version):
raise errors.InvalidVersion( raise errors.InvalidVersion(
f'{f.__name__} is not available for version < {version}', '{} is not available for version < {}'.format(
f.__name__, version
)
) )
return f(self, *args, **kwargs) return f(self, *args, **kwargs)
return wrapper return wrapper

View File

@ -79,18 +79,18 @@ def translate(pat):
i = i + 1 i = i + 1
if i >= n: if i >= n:
# is "**EOF" - to align with .gitignore just accept all # is "**EOF" - to align with .gitignore just accept all
res = f"{res}.*" res = res + '.*'
else: else:
# is "**" # is "**"
# Note that this allows for any # of /'s (even 0) because # Note that this allows for any # of /'s (even 0) because
# the .* will eat everything, even /'s # the .* will eat everything, even /'s
res = f"{res}(.*/)?" res = res + '(.*/)?'
else: else:
# is "*" so map it to anything but "/" # is "*" so map it to anything but "/"
res = f"{res}[^/]*" res = res + '[^/]*'
elif c == '?': elif c == '?':
# "?" is any char except "/" # "?" is any char except "/"
res = f"{res}[^/]" res = res + '[^/]'
elif c == '[': elif c == '[':
j = i j = i
if j < n and pat[j] == '!': if j < n and pat[j] == '!':
@ -100,16 +100,16 @@ def translate(pat):
while j < n and pat[j] != ']': while j < n and pat[j] != ']':
j = j + 1 j = j + 1
if j >= n: if j >= n:
res = f"{res}\\[" res = res + '\\['
else: else:
stuff = pat[i:j].replace('\\', '\\\\') stuff = pat[i:j].replace('\\', '\\\\')
i = j + 1 i = j + 1
if stuff[0] == '!': if stuff[0] == '!':
stuff = f"^{stuff[1:]}" stuff = '^' + stuff[1:]
elif stuff[0] == '^': elif stuff[0] == '^':
stuff = f"\\{stuff}" stuff = '\\' + stuff
res = f'{res}[{stuff}]' res = f'{res}[{stuff}]'
else: else:
res = res + re.escape(c) res = res + re.escape(c)
return f"{res}$" return res + '$'

View File

@ -3,6 +3,7 @@ import json.decoder
from ..errors import StreamParseError from ..errors import StreamParseError
json_decoder = json.JSONDecoder() json_decoder = json.JSONDecoder()
@ -71,4 +72,4 @@ def split_buffer(stream, splitter=None, decoder=lambda a: a):
try: try:
yield decoder(buffered) yield decoder(buffered)
except Exception as e: except Exception as e:
raise StreamParseError(e) from e raise StreamParseError(e)

View File

@ -49,7 +49,7 @@ def port_range(start, end, proto, randomly_available_port=False):
if not end: if not end:
return [start + proto] return [start + proto]
if randomly_available_port: if randomly_available_port:
return [f"{start}-{end}{proto}"] return [f'{start}-{end}' + proto]
return [str(port) + proto for port in range(int(start), int(end) + 1)] return [str(port) + proto for port in range(int(start), int(end) + 1)]

View File

@ -69,9 +69,5 @@ class ProxyConfig(dict):
return proxy_env + environment return proxy_env + environment
def __str__(self): def __str__(self):
return ( return 'ProxyConfig(http={}, https={}, ftp={}, no_proxy={})'.format(
'ProxyConfig(' self.http, self.https, self.ftp, self.no_proxy)
f'http={self.http}, https={self.https}, '
f'ftp={self.ftp}, no_proxy={self.no_proxy}'
')'
)

View File

@ -3,6 +3,7 @@ import os
import select import select
import socket as pysocket import socket as pysocket
import struct import struct
import sys
try: try:
from ..transport import NpipeSocket from ..transport import NpipeSocket
@ -31,18 +32,18 @@ def read(socket, n=4096):
recoverable_errors = (errno.EINTR, errno.EDEADLK, errno.EWOULDBLOCK) recoverable_errors = (errno.EINTR, errno.EDEADLK, errno.EWOULDBLOCK)
if not isinstance(socket, NpipeSocket): if not isinstance(socket, NpipeSocket):
if not hasattr(select, "poll"): if sys.platform == 'win32':
# Limited to 1024 # Limited to 1024
select.select([socket], [], []) select.select([socket], [], [])
else: else:
poll = select.poll() poll = select.poll()
poll.register(socket, select.POLLIN | select.POLLPRI) poll.register(socket)
poll.poll() poll.poll()
try: try:
if hasattr(socket, 'recv'): if hasattr(socket, 'recv'):
return socket.recv(n) return socket.recv(n)
if isinstance(socket, pysocket.SocketIO): if isinstance(socket, getattr(pysocket, 'SocketIO')):
return socket.read(n) return socket.read(n)
return os.read(socket.fileno(), n) return os.read(socket.fileno(), n)
except OSError as e: except OSError as e:
@ -64,7 +65,7 @@ def read_exactly(socket, n):
Reads exactly n bytes from socket Reads exactly n bytes from socket
Raises SocketError if there isn't enough data Raises SocketError if there isn't enough data
""" """
data = b"" data = bytes()
while len(data) < n: while len(data) < n:
next_data = read(socket, n - len(data)) next_data = read(socket, n - len(data))
if not next_data: if not next_data:
@ -152,7 +153,7 @@ def consume_socket_output(frames, demux=False):
if demux is False: if demux is False:
# If the streams are multiplexed, the generator returns strings, that # If the streams are multiplexed, the generator returns strings, that
# we just need to concatenate. # we just need to concatenate.
return b"".join(frames) return bytes().join(frames)
# If the streams are demultiplexed, the generator yields tuples # If the streams are demultiplexed, the generator yields tuples
# (stdout, stderr) # (stdout, stderr)

View File

@ -5,20 +5,19 @@ import os
import os.path import os.path
import shlex import shlex
import string import string
from datetime import datetime, timezone from datetime import datetime
from functools import lru_cache from packaging.version import Version
from itertools import zip_longest
from urllib.parse import urlparse, urlunparse
from .. import errors from .. import errors
from ..constants import ( from ..constants import DEFAULT_HTTP_HOST
BYTE_UNITS, from ..constants import DEFAULT_UNIX_SOCKET
DEFAULT_HTTP_HOST, from ..constants import DEFAULT_NPIPE
DEFAULT_NPIPE, from ..constants import BYTE_UNITS
DEFAULT_UNIX_SOCKET,
)
from ..tls import TLSConfig from ..tls import TLSConfig
from urllib.parse import urlparse, urlunparse
URLComponents = collections.namedtuple( URLComponents = collections.namedtuple(
'URLComponents', 'URLComponents',
'scheme netloc url params query fragment', 'scheme netloc url params query fragment',
@ -45,7 +44,6 @@ def decode_json_header(header):
return json.loads(data) return json.loads(data)
@lru_cache(maxsize=None)
def compare_version(v1, v2): def compare_version(v1, v2):
"""Compare docker versions """Compare docker versions
@ -58,20 +56,14 @@ def compare_version(v1, v2):
>>> compare_version(v2, v2) >>> compare_version(v2, v2)
0 0
""" """
if v1 == v2: s1 = Version(v1)
s2 = Version(v2)
if s1 == s2:
return 0 return 0
# Split into `sys.version_info` like tuples. elif s1 > s2:
s1 = tuple(int(p) for p in v1.split('.')) return -1
s2 = tuple(int(p) for p in v2.split('.')) else:
# Compare each component, padding with 0 if necessary. return 1
for c1, c2 in zip_longest(s1, s2, fillvalue=0):
if c1 == c2:
continue
elif c1 > c2:
return -1
else:
return 1
return 0
def version_lt(v1, v2): def version_lt(v1, v2):
@ -135,7 +127,8 @@ def convert_volume_binds(binds):
if isinstance(v, dict): if isinstance(v, dict):
if 'ro' in v and 'mode' in v: if 'ro' in v and 'mode' in v:
raise ValueError( raise ValueError(
f'Binding cannot contain both "ro" and "mode": {v!r}' 'Binding cannot contain both "ro" and "mode": {}'
.format(repr(v))
) )
bind = v['bind'] bind = v['bind']
@ -149,22 +142,6 @@ def convert_volume_binds(binds):
else: else:
mode = 'rw' mode = 'rw'
# NOTE: this is only relevant for Linux hosts
# (doesn't apply in Docker Desktop)
propagation_modes = [
'rshared',
'shared',
'rslave',
'slave',
'rprivate',
'private',
]
if 'propagation' in v and v['propagation'] in propagation_modes:
if mode:
mode = f"{mode},{v['propagation']}"
else:
mode = v['propagation']
result.append( result.append(
f'{k}:{bind}:{mode}' f'{k}:{bind}:{mode}'
) )
@ -183,8 +160,8 @@ def convert_tmpfs_mounts(tmpfs):
if not isinstance(tmpfs, list): if not isinstance(tmpfs, list):
raise ValueError( raise ValueError(
'Expected tmpfs value to be either a list or a dict, ' 'Expected tmpfs value to be either a list or a dict, found: {}'
f'found: {type(tmpfs).__name__}' .format(type(tmpfs).__name__)
) )
result = {} result = {}
@ -198,8 +175,8 @@ def convert_tmpfs_mounts(tmpfs):
else: else:
raise ValueError( raise ValueError(
"Expected item in tmpfs list to be a string, " "Expected item in tmpfs list to be a string, found: {}"
f"found: {type(mount).__name__}" .format(type(mount).__name__)
) )
result[name] = options result[name] = options
@ -241,9 +218,9 @@ def parse_host(addr, is_win32=False, tls=False):
parsed_url = urlparse(addr) parsed_url = urlparse(addr)
proto = parsed_url.scheme proto = parsed_url.scheme
if not proto or any(x not in f"{string.ascii_letters}+" for x in proto): if not proto or any([x not in string.ascii_letters + '+' for x in proto]):
# https://bugs.python.org/issue754016 # https://bugs.python.org/issue754016
parsed_url = urlparse(f"//{addr}", 'tcp') parsed_url = urlparse('//' + addr, 'tcp')
proto = 'tcp' proto = 'tcp'
if proto == 'fd': if proto == 'fd':
@ -279,14 +256,15 @@ def parse_host(addr, is_win32=False, tls=False):
if parsed_url.path and proto == 'ssh': if parsed_url.path and proto == 'ssh':
raise errors.DockerException( raise errors.DockerException(
f'Invalid bind address format: no path allowed for this protocol: {addr}' 'Invalid bind address format: no path allowed for this protocol:'
' {}'.format(addr)
) )
else: else:
path = parsed_url.path path = parsed_url.path
if proto == 'unix' and parsed_url.hostname is not None: if proto == 'unix' and parsed_url.hostname is not None:
# For legacy reasons, we consider unix://path # For legacy reasons, we consider unix://path
# to be valid and equivalent to unix:///path # to be valid and equivalent to unix:///path
path = f"{parsed_url.hostname}/{path}" path = '/'.join((parsed_url.hostname, path))
netloc = parsed_url.netloc netloc = parsed_url.netloc
if proto in ('tcp', 'ssh'): if proto in ('tcp', 'ssh'):
@ -294,7 +272,8 @@ def parse_host(addr, is_win32=False, tls=False):
if port <= 0: if port <= 0:
if proto != 'ssh': if proto != 'ssh':
raise errors.DockerException( raise errors.DockerException(
f'Invalid bind address format: port is required: {addr}' 'Invalid bind address format: port is required:'
' {}'.format(addr)
) )
port = 22 port = 22
netloc = f'{parsed_url.netloc}:{port}' netloc = f'{parsed_url.netloc}:{port}'
@ -304,7 +283,7 @@ def parse_host(addr, is_win32=False, tls=False):
# Rewrite schemes to fit library internals (requests adapters) # Rewrite schemes to fit library internals (requests adapters)
if proto == 'tcp': if proto == 'tcp':
proto = f"http{'s' if tls else ''}" proto = 'http{}'.format('s' if tls else '')
elif proto == 'unix': elif proto == 'unix':
proto = 'http+unix' proto = 'http+unix'
@ -350,7 +329,7 @@ def parse_devices(devices):
return device_list return device_list
def kwargs_from_env(environment=None): def kwargs_from_env(ssl_version=None, assert_hostname=None, environment=None):
if not environment: if not environment:
environment = os.environ environment = os.environ
host = environment.get('DOCKER_HOST') host = environment.get('DOCKER_HOST')
@ -378,11 +357,18 @@ def kwargs_from_env(environment=None):
if not cert_path: if not cert_path:
cert_path = os.path.join(os.path.expanduser('~'), '.docker') cert_path = os.path.join(os.path.expanduser('~'), '.docker')
if not tls_verify and assert_hostname is None:
# assert_hostname is a subset of TLS verification,
# so if it's not set already then set it to false.
assert_hostname = False
params['tls'] = TLSConfig( params['tls'] = TLSConfig(
client_cert=(os.path.join(cert_path, 'cert.pem'), client_cert=(os.path.join(cert_path, 'cert.pem'),
os.path.join(cert_path, 'key.pem')), os.path.join(cert_path, 'key.pem')),
ca_cert=os.path.join(cert_path, 'ca.pem'), ca_cert=os.path.join(cert_path, 'ca.pem'),
verify=tls_verify, verify=tls_verify,
ssl_version=ssl_version,
assert_hostname=assert_hostname,
) )
return params return params
@ -403,8 +389,8 @@ def convert_filters(filters):
def datetime_to_timestamp(dt): def datetime_to_timestamp(dt):
"""Convert a datetime to a Unix timestamp""" """Convert a UTC datetime to a Unix timestamp"""
delta = dt.astimezone(timezone.utc) - datetime(1970, 1, 1, tzinfo=timezone.utc) delta = dt - datetime.utcfromtimestamp(0)
return delta.seconds + delta.days * 24 * 3600 return delta.seconds + delta.days * 24 * 3600
@ -431,18 +417,19 @@ def parse_bytes(s):
if suffix in units.keys() or suffix.isdigit(): if suffix in units.keys() or suffix.isdigit():
try: try:
digits = float(digits_part) digits = float(digits_part)
except ValueError as ve: except ValueError:
raise errors.DockerException( raise errors.DockerException(
'Failed converting the string value for memory ' 'Failed converting the string value for memory ({}) to'
f'({digits_part}) to an integer.' ' an integer.'.format(digits_part)
) from ve )
# Reconvert to long for the final result # Reconvert to long for the final result
s = int(digits * units[suffix]) s = int(digits * units[suffix])
else: else:
raise errors.DockerException( raise errors.DockerException(
f'The specified value for memory ({s}) should specify the units. ' 'The specified value for memory ({}) should specify the'
'The postfix should be one of the `b` `k` `m` `g` characters' ' units. The postfix should be one of the `b` `k` `m` `g`'
' characters'.format(s)
) )
return s return s
@ -478,7 +465,8 @@ def parse_env_file(env_file):
environment[k] = v environment[k] = v
else: else:
raise errors.DockerException( raise errors.DockerException(
f'Invalid line in environment file {env_file}:\n{line}') 'Invalid line in environment file {}:\n{}'.format(
env_file, line))
return environment return environment

View File

@ -1,8 +1,14 @@
try: try:
from ._version import __version__ from ._version import __version__
except ImportError: except ImportError:
from importlib.metadata import PackageNotFoundError, version
try: try:
__version__ = version('docker') # importlib.metadata available in Python 3.8+, the fallback (0.0.0)
except PackageNotFoundError: # is fine because release builds use _version (above) rather than
# this code path, so it only impacts developing w/ 3.7
from importlib.metadata import version, PackageNotFoundError
try:
__version__ = version('docker')
except PackageNotFoundError:
__version__ = '0.0.0'
except ImportError:
__version__ = '0.0.0' __version__ = '0.0.0'

2
docs-requirements.txt Normal file
View File

@ -0,0 +1,2 @@
myst-parser==0.18.0
Sphinx==5.1.1

View File

@ -1,104 +1,6 @@
Changelog Changelog
========== ==========
7.1.0
-----
### Upgrade Notes
- Bumped minimum engine API version to 1.24
- Bumped default engine API version to 1.44 (Moby 25.0)
### Bugfixes
- Fixed issue with tag parsing when the registry address includes ports that resulted in `invalid tag format` errors
- Fixed issue preventing creating new configs (`ConfigCollection`), which failed with a `KeyError` due to the `name` field
- Fixed an issue due to an update in the [requests](https://github.com/psf/requests) package breaking `docker-py` by applying the [suggested fix](https://github.com/psf/requests/pull/6710)
### Miscellaneous
- Documentation improvements
- Updated Ruff (linter) and fixed minor linting issues
- Packaging/CI updates
- Started using hatch for packaging (https://github.com/pypa/hatch)
- Updated `setup-python` github action
- Updated tests
- Stopped checking for deprecated container and image related fields (`Container` and `ContainerConfig`)
- Updated tests that check `NetworkSettings.Networks.<network>.Aliases` due to engine changes
7.0.0
-----
### Upgrade Notes
- Removed SSL version (`ssl_version`) and explicit hostname check (`assert_hostname`) options
- `assert_hostname` has not been used since Python 3.6 and was removed in 3.12
- Python 3.7+ supports TLSv1.3 by default
- Websocket support is no longer included by default
- Use `pip install docker[websockets]` to include `websocket-client` dependency
- By default, `docker-py` hijacks the TCP connection and does not use Websockets
- Websocket client is only required to use `attach_socket(container, ws=True)`
- Python 3.7 no longer officially supported (reached end-of-life June 2023)
### Features
- Python 3.12 support
- Full `networking_config` support for `containers.create()`
- Replaces `network_driver_opt` (added in 6.1.0)
- Add `health()` property to container that returns status (e.g. `unhealthy`)
- Add `pause` option to `container.commit()`
- Add support for bind mount propagation (e.g. `rshared`, `private`)
- Add `filters`, `keep_storage`, and `all` parameters to `prune_builds()` (requires API v1.39+)
### Bugfixes
- Consistently return `docker.errors.NotFound` on 404 responses
- Validate tag format before image push
### Miscellaneous
- Upgraded urllib3 version in `requirements.txt` (used for development/tests)
- Documentation typo fixes & formatting improvements
- Fixed integration test compatibility for newer Moby engine versions
- Switch to [ruff](https://github.com/astral-sh/ruff) for linting
6.1.3
-----
#### Bugfixes
- Fix compatibility with [`eventlet/eventlet`](https://github.com/eventlet/eventlet)
6.1.2
-----
#### Bugfixes
- Fix for socket timeouts on long `docker exec` calls
6.1.1
-----
#### Bugfixes
- Fix `containers.stats()` hanging with `stream=True`
- Correct return type in docs for `containers.diff()` method
6.1.0
-----
### Upgrade Notes
- Errors are no longer returned during client initialization if the credential helper cannot be found. A warning will be emitted instead, and an error is returned if the credential helper is used.
### Features
- Python 3.11 support
- Use `poll()` instead of `select()` on non-Windows platforms
- New API fields
- `network_driver_opt` on container run / create
- `one-shot` on container stats
- `status` on services list
### Bugfixes
- Support for requests 2.29.0+ and urllib3 2.x
- Do not strip characters from volume names
- Fix connection leak on container.exec_* operations
- Fix errors closing named pipes on Windows
6.0.1
-----
### Bugfixes
- Fix for `The pipe has been ended errors` on Windows
- Support floats for container log filtering by timestamp (`since` / `until`)
6.0.0 6.0.0
----- -----

View File

@ -18,8 +18,6 @@
import datetime import datetime
import os import os
import sys import sys
from importlib.metadata import version
sys.path.insert(0, os.path.abspath('..')) sys.path.insert(0, os.path.abspath('..'))
@ -58,7 +56,7 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = 'Docker SDK for Python' project = 'Docker SDK for Python'
year = datetime.datetime.now().year year = datetime.datetime.now().year
copyright = f'{year} Docker Inc' copyright = '%d Docker Inc' % year
author = 'Docker Inc' author = 'Docker Inc'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
@ -66,6 +64,7 @@ author = 'Docker Inc'
# built documents. # built documents.
# #
# see https://github.com/pypa/setuptools_scm#usage-from-sphinx # see https://github.com/pypa/setuptools_scm#usage-from-sphinx
from importlib.metadata import version
release = version('docker') release = version('docker')
# for example take major/minor # for example take major/minor
version = '.'.join(release.split('.')[:2]) version = '.'.join(release.split('.')[:2])

View File

@ -16,13 +16,10 @@ Prepare the command we are going to use. It prints "hello stdout"
in `stdout`, followed by "hello stderr" in `stderr`: in `stdout`, followed by "hello stderr" in `stderr`:
>>> cmd = '/bin/sh -c "echo hello stdout ; echo hello stderr >&2"' >>> cmd = '/bin/sh -c "echo hello stdout ; echo hello stderr >&2"'
We'll run this command with all four the combinations of ``stream`` We'll run this command with all four the combinations of ``stream``
and ``demux``. and ``demux``.
With ``stream=False`` and ``demux=False``, the output is a string With ``stream=False`` and ``demux=False``, the output is a string
that contains both the `stdout` and the `stderr` output: that contains both the `stdout` and the `stderr` output:
>>> res = container.exec_run(cmd, stream=False, demux=False) >>> res = container.exec_run(cmd, stream=False, demux=False)
>>> res.output >>> res.output
b'hello stderr\nhello stdout\n' b'hello stderr\nhello stdout\n'
@ -55,8 +52,15 @@ Traceback (most recent call last):
File "<stdin>", line 1, in <module> File "<stdin>", line 1, in <module>
StopIteration StopIteration
Finally, with ``stream=False`` and ``demux=True``, the output is a tuple ``(stdout, stderr)``: Finally, with ``stream=False`` and ``demux=True``, the whole output
is returned, but the streams are still separated:
>>> res = container.exec_run(cmd, stream=False, demux=True) >>> res = container.exec_run(cmd, stream=True, demux=True)
>>> res.output >>> next(res.output)
(b'hello stdout\n', b'hello stderr\n') (b'hello stdout\n', None)
>>> next(res.output)
(None, b'hello stderr\n')
>>> next(res.output)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

View File

@ -1,102 +1,5 @@
[build-system] [build-system]
requires = ["hatchling", "hatch-vcs"] requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
build-backend = "hatchling.build"
[project] [tool.setuptools_scm]
name = "docker" write_to = 'docker/_version.py'
dynamic = ["version"]
description = "A Python library for the Docker Engine API."
readme = "README.md"
license = "Apache-2.0"
requires-python = ">=3.8"
maintainers = [
{ name = "Docker Inc.", email = "no-reply@docker.com" },
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Other Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development",
"Topic :: Utilities",
]
dependencies = [
"requests >= 2.26.0",
"urllib3 >= 1.26.0",
"pywin32>=304; sys_platform == \"win32\"",
]
[project.optional-dependencies]
# ssh feature allows DOCKER_HOST=ssh://... style connections
ssh = [
"paramiko>=2.4.3",
]
# tls is always supported, the feature is a no-op for backwards compatibility
tls = []
# websockets can be used as an alternate container attach mechanism but
# by default docker-py hijacks the TCP connection and does not use Websockets
# unless attach_socket(container, ws=True) is called
websockets = [
"websocket-client >= 1.3.0",
]
# docs are dependencies required to build the ReadTheDocs site
# this is only needed for CI / working on the docs!
docs = [
"myst-parser==0.18.0",
"Sphinx==5.1.1",
]
# dev are dependencies required to test & lint this project
# this is only needed if you are making code changes to docker-py!
dev = [
"coverage==7.2.7",
"pytest==7.4.2",
"pytest-cov==4.1.0",
"pytest-timeout==2.1.0",
"ruff==0.1.8",
]
[project.urls]
Changelog = "https://docker-py.readthedocs.io/en/stable/change-log.html"
Documentation = "https://docker-py.readthedocs.io"
Homepage = "https://github.com/docker/docker-py"
Source = "https://github.com/docker/docker-py"
Tracker = "https://github.com/docker/docker-py/issues"
[tool.hatch.version]
source = "vcs"
[tool.hatch.build.hooks.vcs]
version-file = "docker/_version.py"
[tool.hatch.build.targets.sdist]
include = [
"/docker",
]
[tool.ruff]
target-version = "py38"
extend-select = [
"B",
"C",
"F",
"I",
"UP",
"W",
]
ignore = [
"UP012", # unnecessary `UTF-8` argument (we want to be explicit)
"C901", # too complex (there's a whole bunch of these)
]
[tool.ruff.per-file-ignores]
"**/__init__.py" = ["F401"]

6
requirements.txt Normal file
View File

@ -0,0 +1,6 @@
packaging==21.3
paramiko==2.11.0
pywin32==304; sys_platform == 'win32'
requests==2.28.1
urllib3==1.26.11
websocket-client==1.3.3

3
setup.cfg Normal file
View File

@ -0,0 +1,3 @@
[metadata]
description_file = README.rst
license = Apache License 2.0

81
setup.py Normal file
View File

@ -0,0 +1,81 @@
#!/usr/bin/env python
import codecs
import os
from setuptools import find_packages
from setuptools import setup
ROOT_DIR = os.path.dirname(__file__)
SOURCE_DIR = os.path.join(ROOT_DIR)
requirements = [
'packaging >= 14.0',
'requests >= 2.26.0',
'urllib3 >= 1.26.0',
'websocket-client >= 0.32.0',
]
extras_require = {
# win32 APIs if on Windows (required for npipe support)
':sys_platform == "win32"': 'pywin32>=304',
# This is now a no-op, as similarly the requests[security] extra is
# a no-op as of requests 2.26.0, this is always available/by default now
# see https://github.com/psf/requests/pull/5867
'tls': [],
# Only required when connecting using the ssh:// protocol
'ssh': ['paramiko>=2.4.3'],
}
with open('./test-requirements.txt') as test_reqs_txt:
test_requirements = [line for line in test_reqs_txt]
long_description = ''
with codecs.open('./README.md', encoding='utf-8') as readme_md:
long_description = readme_md.read()
setup(
name="docker",
use_scm_version={
'write_to': 'docker/_version.py'
},
description="A Python library for the Docker Engine API.",
long_description=long_description,
long_description_content_type='text/markdown',
url='https://github.com/docker/docker-py',
project_urls={
'Documentation': 'https://docker-py.readthedocs.io',
'Changelog': 'https://docker-py.readthedocs.io/en/stable/change-log.html', # noqa: E501
'Source': 'https://github.com/docker/docker-py',
'Tracker': 'https://github.com/docker/docker-py/issues',
},
packages=find_packages(exclude=["tests.*", "tests"]),
setup_requires=['setuptools_scm'],
install_requires=requirements,
tests_require=test_requirements,
extras_require=extras_require,
python_requires='>=3.7',
zip_safe=False,
test_suite='tests',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Other Environment',
'Intended Audience :: Developers',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Topic :: Software Development',
'Topic :: Utilities',
'License :: OSI Approved :: Apache Software License',
],
maintainer='Ulysses Souza',
maintainer_email='ulysses.souza@docker.com',
)

6
test-requirements.txt Normal file
View File

@ -0,0 +1,6 @@
setuptools==65.5.1
coverage==6.4.2
flake8==4.0.1
pytest==7.1.2
pytest-cov==3.0.0
pytest-timeout==2.1.0

View File

@ -1,8 +1,13 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG PYTHON_VERSION=3.12 ARG PYTHON_VERSION=3.10
FROM python:${PYTHON_VERSION} FROM python:${PYTHON_VERSION}
ARG APT_MIRROR
RUN sed -ri "s/(httpredir|deb).debian.org/${APT_MIRROR:-deb.debian.org}/g" /etc/apt/sources.list \
&& sed -ri "s/(security).debian.org/${APT_MIRROR:-security.debian.org}/g" /etc/apt/sources.list
RUN apt-get update && apt-get -y install --no-install-recommends \ RUN apt-get update && apt-get -y install --no-install-recommends \
gnupg2 \ gnupg2 \
pass pass
@ -26,10 +31,16 @@ RUN curl -sSL -o /opt/docker-credential-pass.tar.gz \
chmod +x /usr/local/bin/docker-credential-pass chmod +x /usr/local/bin/docker-credential-pass
WORKDIR /src WORKDIR /src
COPY . .
ARG VERSION=0.0.0.dev0 COPY requirements.txt /src/requirements.txt
RUN --mount=type=cache,target=/cache/pip \ RUN --mount=type=cache,target=/root/.cache/pip \
PIP_CACHE_DIR=/cache/pip \ pip install -r requirements.txt
SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} \
pip install .[dev,ssh,websockets] COPY test-requirements.txt /src/test-requirements.txt
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r test-requirements.txt
COPY . /src
ARG SETUPTOOLS_SCM_PRETEND_VERSION=99.0.0+docker
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -e .

View File

@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG PYTHON_VERSION=3.12 ARG PYTHON_VERSION=3.10
FROM python:${PYTHON_VERSION} FROM python:${PYTHON_VERSION}
RUN mkdir /tmp/certs RUN mkdir /tmp/certs

View File

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

View File

@ -8,11 +8,10 @@ import tarfile
import tempfile import tempfile
import time import time
import docker
import paramiko import paramiko
import pytest import pytest
import docker
def make_tree(dirs, files): def make_tree(dirs, files):
base = tempfile.mkdtemp() base = tempfile.mkdtemp()
@ -47,19 +46,6 @@ def untar_file(tardata, filename):
return result return result
def skip_if_desktop():
def fn(f):
@functools.wraps(f)
def wrapped(self, *args, **kwargs):
info = self.client.info()
if info['Name'] == 'docker-desktop':
pytest.skip('Test does not support Docker Desktop')
return f(self, *args, **kwargs)
return wrapped
return fn
def requires_api_version(version): def requires_api_version(version):
test_version = os.environ.get( test_version = os.environ.get(
'DOCKER_TEST_API_VERSION', docker.constants.DEFAULT_DOCKER_API_VERSION 'DOCKER_TEST_API_VERSION', docker.constants.DEFAULT_DOCKER_API_VERSION
@ -94,7 +80,7 @@ def wait_on_condition(condition, delay=0.1, timeout=40):
start_time = time.time() start_time = time.time()
while not condition(): while not condition():
if time.time() - start_time > timeout: if time.time() - start_time > timeout:
raise AssertionError(f"Timeout: {condition}") raise AssertionError("Timeout: %s" % condition)
time.sleep(delay) time.sleep(delay)

View File

@ -3,13 +3,13 @@ import os
import shutil import shutil
import tempfile import tempfile
import pytest
from docker import errors from docker import errors
from docker.utils.proxy import ProxyConfig from docker.utils.proxy import ProxyConfig
import pytest
from .base import BaseAPIIntegrationTest, TEST_IMG
from ..helpers import random_name, requires_api_version, requires_experimental from ..helpers import random_name, requires_api_version, requires_experimental
from .base import TEST_IMG, BaseAPIIntegrationTest
class BuildTest(BaseAPIIntegrationTest): class BuildTest(BaseAPIIntegrationTest):
@ -132,7 +132,7 @@ class BuildTest(BaseAPIIntegrationTest):
path=base_dir, path=base_dir,
tag=tag, tag=tag,
) )
for _chunk in stream: for chunk in stream:
pass pass
c = self.client.create_container(tag, ['find', '/test', '-type', 'f']) c = self.client.create_container(tag, ['find', '/test', '-type', 'f'])
@ -142,7 +142,7 @@ class BuildTest(BaseAPIIntegrationTest):
logs = logs.decode('utf-8') logs = logs.decode('utf-8')
assert sorted(filter(None, logs.split('\n'))) == sorted([ assert sorted(list(filter(None, logs.split('\n')))) == sorted([
'/test/#file.txt', '/test/#file.txt',
'/test/ignored/subdir/excepted-with-spaces', '/test/ignored/subdir/excepted-with-spaces',
'/test/ignored/subdir/excepted-file', '/test/ignored/subdir/excepted-file',
@ -160,7 +160,7 @@ class BuildTest(BaseAPIIntegrationTest):
fileobj=script, tag='buildargs', buildargs={'test': 'OK'} fileobj=script, tag='buildargs', buildargs={'test': 'OK'}
) )
self.tmp_imgs.append('buildargs') self.tmp_imgs.append('buildargs')
for _chunk in stream: for chunk in stream:
pass pass
info = self.client.inspect_image('buildargs') info = self.client.inspect_image('buildargs')
@ -180,7 +180,7 @@ class BuildTest(BaseAPIIntegrationTest):
fileobj=script, tag=tag, shmsize=shmsize fileobj=script, tag=tag, shmsize=shmsize
) )
self.tmp_imgs.append(tag) self.tmp_imgs.append(tag)
for _chunk in stream: for chunk in stream:
pass pass
# There is currently no way to get the shmsize # There is currently no way to get the shmsize
@ -198,7 +198,7 @@ class BuildTest(BaseAPIIntegrationTest):
isolation='default' isolation='default'
) )
for _chunk in stream: for chunk in stream:
pass pass
@requires_api_version('1.23') @requires_api_version('1.23')
@ -213,7 +213,7 @@ class BuildTest(BaseAPIIntegrationTest):
fileobj=script, tag='labels', labels=labels fileobj=script, tag='labels', labels=labels
) )
self.tmp_imgs.append('labels') self.tmp_imgs.append('labels')
for _chunk in stream: for chunk in stream:
pass pass
info = self.client.inspect_image('labels') info = self.client.inspect_image('labels')
@ -230,7 +230,7 @@ class BuildTest(BaseAPIIntegrationTest):
stream = self.client.build(fileobj=script, tag='build1') stream = self.client.build(fileobj=script, tag='build1')
self.tmp_imgs.append('build1') self.tmp_imgs.append('build1')
for _chunk in stream: for chunk in stream:
pass pass
stream = self.client.build( stream = self.client.build(
@ -271,11 +271,11 @@ class BuildTest(BaseAPIIntegrationTest):
fileobj=script, target='first', tag='build1' fileobj=script, target='first', tag='build1'
) )
self.tmp_imgs.append('build1') self.tmp_imgs.append('build1')
for _chunk in stream: for chunk in stream:
pass pass
info = self.client.inspect_image('build1') info = self.client.inspect_image('build1')
assert 'OnBuild' not in info['Config'] or not info['Config']['OnBuild'] assert 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):
@ -300,7 +300,7 @@ class BuildTest(BaseAPIIntegrationTest):
) )
self.tmp_imgs.append('dockerpytest_customnetbuild') self.tmp_imgs.append('dockerpytest_customnetbuild')
for _chunk in stream: for chunk in stream:
pass pass
assert self.client.inspect_image('dockerpytest_customnetbuild') assert self.client.inspect_image('dockerpytest_customnetbuild')
@ -312,7 +312,7 @@ class BuildTest(BaseAPIIntegrationTest):
) )
self.tmp_imgs.append('dockerpytest_nonebuild') self.tmp_imgs.append('dockerpytest_nonebuild')
logs = list(stream) logs = [chunk for chunk in stream]
assert 'errorDetail' in logs[-1] assert 'errorDetail' in logs[-1]
assert logs[-1]['errorDetail']['code'] == 1 assert logs[-1]['errorDetail']['code'] == 1
@ -365,7 +365,7 @@ class BuildTest(BaseAPIIntegrationTest):
fileobj=script, tag=tag, squash=squash fileobj=script, tag=tag, squash=squash
) )
self.tmp_imgs.append(tag) self.tmp_imgs.append(tag)
for _chunk in stream: for chunk in stream:
pass pass
return self.client.inspect_image(tag) return self.client.inspect_image(tag)
@ -389,8 +389,10 @@ class BuildTest(BaseAPIIntegrationTest):
lines = [] lines = []
for chunk in stream: for chunk in stream:
lines.append(chunk.get('stream')) lines.append(chunk.get('stream'))
expected = f'{control_chars[0]}{snippet}\n{control_chars[1]}' expected = '{0}{2}\n{1}'.format(
assert any(line == expected for line in lines) control_chars[0], control_chars[1], snippet
)
assert any([line == expected for line in lines])
def test_build_gzip_encoding(self): def test_build_gzip_encoding(self):
base_dir = tempfile.mkdtemp() base_dir = tempfile.mkdtemp()

View File

@ -47,7 +47,7 @@ class ConnectionTimeoutTest(unittest.TestCase):
# This call isn't supposed to complete, and it should fail fast. # This call isn't supposed to complete, and it should fail fast.
try: try:
res = self.client.inspect_container('id') res = self.client.inspect_container('id')
except Exception: except: # noqa: E722
pass pass
end = time.time() end = time.time()
assert res is None assert res is None
@ -72,4 +72,6 @@ class UnixconnTest(unittest.TestCase):
client.close() client.close()
del client del client
assert len(w) == 0, f"No warnings produced: {w[0].message}" assert len(w) == 0, "No warnings produced: {}".format(
w[0].message
)

View File

@ -1,6 +1,5 @@
import pytest
import docker import docker
import pytest
from ..helpers import force_leave_swarm, requires_api_version from ..helpers import force_leave_swarm, requires_api_version
from .base import BaseAPIIntegrationTest from .base import BaseAPIIntegrationTest

View File

@ -9,17 +9,15 @@ import pytest
import requests import requests
import docker import docker
from docker.constants import IS_WINDOWS_PLATFORM
from docker.utils.socket import next_frame_header, read_exactly
from .. import helpers from .. import helpers
from ..helpers import ( from ..helpers import assert_cat_socket_detached_with_keys
assert_cat_socket_detached_with_keys, from ..helpers import ctrl_with
ctrl_with, from ..helpers import requires_api_version
requires_api_version, from .base import BaseAPIIntegrationTest
skip_if_desktop, from .base import TEST_IMG
) from docker.constants import IS_WINDOWS_PLATFORM
from .base import TEST_IMG, BaseAPIIntegrationTest from docker.utils.socket import next_frame_header
from docker.utils.socket import read_exactly
class ListContainersTest(BaseAPIIntegrationTest): class ListContainersTest(BaseAPIIntegrationTest):
@ -124,8 +122,8 @@ class CreateContainerTest(BaseAPIIntegrationTest):
self.client.wait(id) self.client.wait(id)
with pytest.raises(docker.errors.APIError) as exc: with pytest.raises(docker.errors.APIError) as exc:
self.client.remove_container(id) self.client.remove_container(id)
err = exc.value.explanation.lower() err = exc.value.explanation
assert 'stop the container before' in err assert 'You cannot remove ' in err
self.client.remove_container(id, force=True) self.client.remove_container(id, force=True)
def test_create_container_with_volumes_from(self): def test_create_container_with_volumes_from(self):
@ -544,27 +542,6 @@ class VolumeBindTest(BaseAPIIntegrationTest):
inspect_data = self.client.inspect_container(container) inspect_data = self.client.inspect_container(container)
self.check_container_data(inspect_data, False) self.check_container_data(inspect_data, False)
@skip_if_desktop()
def test_create_with_binds_rw_rshared(self):
container = self.run_with_volume_propagation(
False,
'rshared',
TEST_IMG,
['touch', os.path.join(self.mount_dest, self.filename)],
)
inspect_data = self.client.inspect_container(container)
self.check_container_data(inspect_data, True, 'rshared')
container = self.run_with_volume_propagation(
True,
'rshared',
TEST_IMG,
['ls', self.mount_dest],
)
logs = self.client.logs(container).decode('utf-8')
assert self.filename in logs
inspect_data = self.client.inspect_container(container)
self.check_container_data(inspect_data, False, 'rshared')
@requires_api_version('1.30') @requires_api_version('1.30')
def test_create_with_mounts(self): def test_create_with_mounts(self):
mount = docker.types.Mount( mount = docker.types.Mount(
@ -620,57 +597,7 @@ 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 check_container_data(self, inspect_data, rw):
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'):
assert 'Mounts' in inspect_data assert 'Mounts' in inspect_data
filtered = list(filter( filtered = list(filter(
lambda x: x['Destination'] == self.mount_dest, lambda x: x['Destination'] == self.mount_dest,
@ -680,7 +607,6 @@ class VolumeBindTest(BaseAPIIntegrationTest):
mount_data = filtered[0] mount_data = filtered[0]
assert mount_data['Source'] == self.mount_origin assert mount_data['Source'] == self.mount_origin
assert mount_data['RW'] == rw assert mount_data['RW'] == rw
assert mount_data['Propagation'] == propagation
def run_with_volume(self, ro, *args, **kwargs): def run_with_volume(self, ro, *args, **kwargs):
return self.run_container( return self.run_container(
@ -698,23 +624,6 @@ class VolumeBindTest(BaseAPIIntegrationTest):
**kwargs **kwargs
) )
def run_with_volume_propagation(self, ro, propagation, *args, **kwargs):
return self.run_container(
*args,
volumes={self.mount_dest: {}},
host_config=self.client.create_host_config(
binds={
self.mount_origin: {
'bind': self.mount_dest,
'ro': ro,
'propagation': propagation
},
},
network_mode='none'
),
**kwargs
)
class ArchiveTest(BaseAPIIntegrationTest): class ArchiveTest(BaseAPIIntegrationTest):
def test_get_file_archive_from_container(self): def test_get_file_archive_from_container(self):
@ -757,7 +666,9 @@ class ArchiveTest(BaseAPIIntegrationTest):
test_file.seek(0) test_file.seek(0)
ctnr = self.client.create_container( ctnr = self.client.create_container(
TEST_IMG, TEST_IMG,
f"cat {os.path.join('/vol1/', os.path.basename(test_file.name))}", 'cat {}'.format(
os.path.join('/vol1/', os.path.basename(test_file.name))
),
volumes=['/vol1'] volumes=['/vol1']
) )
self.tmp_containers.append(ctnr) self.tmp_containers.append(ctnr)
@ -915,7 +826,7 @@ class LogsTest(BaseAPIIntegrationTest):
exitcode = self.client.wait(id)['StatusCode'] exitcode = self.client.wait(id)['StatusCode']
assert exitcode == 0 assert exitcode == 0
logs = self.client.logs(id) logs = self.client.logs(id)
assert logs == f"{snippet}\n".encode(encoding='ascii') assert logs == (snippet + '\n').encode(encoding='ascii')
def test_logs_tail_option(self): def test_logs_tail_option(self):
snippet = '''Line1 snippet = '''Line1
@ -946,7 +857,7 @@ Line2'''
exitcode = self.client.wait(id)['StatusCode'] exitcode = self.client.wait(id)['StatusCode']
assert exitcode == 0 assert exitcode == 0
assert logs == f"{snippet}\n".encode(encoding='ascii') assert logs == (snippet + '\n').encode(encoding='ascii')
@pytest.mark.timeout(5) @pytest.mark.timeout(5)
@pytest.mark.skipif(os.environ.get('DOCKER_HOST', '').startswith('ssh://'), @pytest.mark.skipif(os.environ.get('DOCKER_HOST', '').startswith('ssh://'),
@ -967,7 +878,7 @@ Line2'''
for chunk in generator: for chunk in generator:
logs += chunk logs += chunk
assert logs == f"{snippet}\n".encode(encoding='ascii') assert logs == (snippet + '\n').encode(encoding='ascii')
def test_logs_with_dict_instead_of_id(self): def test_logs_with_dict_instead_of_id(self):
snippet = 'Flowering Nights (Sakuya Iyazoi)' snippet = 'Flowering Nights (Sakuya Iyazoi)'
@ -980,7 +891,7 @@ Line2'''
exitcode = self.client.wait(id)['StatusCode'] exitcode = self.client.wait(id)['StatusCode']
assert exitcode == 0 assert exitcode == 0
logs = self.client.logs(container) logs = self.client.logs(container)
assert logs == f"{snippet}\n".encode(encoding='ascii') assert logs == (snippet + '\n').encode(encoding='ascii')
def test_logs_with_tail_0(self): def test_logs_with_tail_0(self):
snippet = 'Flowering Nights (Sakuya Iyazoi)' snippet = 'Flowering Nights (Sakuya Iyazoi)'
@ -1009,7 +920,7 @@ Line2'''
logs_until_1 = self.client.logs(container, until=1) logs_until_1 = self.client.logs(container, until=1)
assert logs_until_1 == b'' assert logs_until_1 == b''
logs_until_now = self.client.logs(container, datetime.now()) logs_until_now = self.client.logs(container, datetime.now())
assert logs_until_now == f"{snippet}\n".encode(encoding='ascii') assert logs_until_now == (snippet + '\n').encode(encoding='ascii')
class DiffTest(BaseAPIIntegrationTest): class DiffTest(BaseAPIIntegrationTest):
@ -1175,7 +1086,7 @@ class PortTest(BaseAPIIntegrationTest):
ip, host_port = port_binding['HostIp'], port_binding['HostPort'] ip, host_port = port_binding['HostIp'], port_binding['HostPort']
port_binding = port if not protocol else f"{port}/{protocol}" port_binding = port if not protocol else port + "/" + protocol
assert ip == port_bindings[port_binding][0] assert ip == port_bindings[port_binding][0]
assert host_port == port_bindings[port_binding][1] assert host_port == port_bindings[port_binding][1]
@ -1481,7 +1392,7 @@ class GetContainerStatsTest(BaseAPIIntegrationTest):
response = self.client.stats(container, stream=0) response = self.client.stats(container, stream=0)
self.client.kill(container) self.client.kill(container)
assert isinstance(response, dict) assert type(response) == dict
for key in ['read', 'networks', 'precpu_stats', 'cpu_stats', for key in ['read', 'networks', 'precpu_stats', 'cpu_stats',
'memory_stats', 'blkio_stats']: 'memory_stats', 'blkio_stats']:
assert key in response assert key in response
@ -1494,7 +1405,7 @@ class GetContainerStatsTest(BaseAPIIntegrationTest):
self.client.start(container) self.client.start(container)
stream = self.client.stats(container) stream = self.client.stats(container)
for chunk in stream: for chunk in stream:
assert isinstance(chunk, dict) assert type(chunk) == dict
for key in ['read', 'network', 'precpu_stats', 'cpu_stats', for key in ['read', 'network', 'precpu_stats', 'cpu_stats',
'memory_stats', 'blkio_stats']: 'memory_stats', 'blkio_stats']:
assert key in chunk assert key in chunk

View File

@ -1,12 +1,11 @@
from ..helpers import assert_cat_socket_detached_with_keys
from ..helpers import ctrl_with
from ..helpers import requires_api_version
from .base import BaseAPIIntegrationTest
from .base import TEST_IMG
from docker.utils.proxy import ProxyConfig from docker.utils.proxy import ProxyConfig
from docker.utils.socket import next_frame_header, read_exactly from docker.utils.socket import next_frame_header
from docker.utils.socket import read_exactly
from ..helpers import (
assert_cat_socket_detached_with_keys,
ctrl_with,
requires_api_version,
)
from .base import TEST_IMG, BaseAPIIntegrationTest
class ExecTest(BaseAPIIntegrationTest): class ExecTest(BaseAPIIntegrationTest):

View File

@ -1,5 +1,5 @@
from .base import BaseAPIIntegrationTest, TEST_IMG
from .. import helpers from .. import helpers
from .base import TEST_IMG, BaseAPIIntegrationTest
SECOND = 1000000000 SECOND = 1000000000
@ -16,7 +16,7 @@ class HealthcheckTest(BaseAPIIntegrationTest):
@helpers.requires_api_version('1.24') @helpers.requires_api_version('1.24')
def test_healthcheck_shell_command(self): def test_healthcheck_shell_command(self):
container = self.client.create_container( container = self.client.create_container(
TEST_IMG, 'top', healthcheck={'test': 'echo "hello world"'}) TEST_IMG, 'top', healthcheck=dict(test='echo "hello world"'))
self.tmp_containers.append(container) self.tmp_containers.append(container)
res = self.client.inspect_container(container) res = self.client.inspect_container(container)
@ -27,12 +27,12 @@ class HealthcheckTest(BaseAPIIntegrationTest):
@helpers.requires_api_version('1.24') @helpers.requires_api_version('1.24')
def test_healthcheck_passes(self): def test_healthcheck_passes(self):
container = self.client.create_container( container = self.client.create_container(
TEST_IMG, 'top', healthcheck={ TEST_IMG, 'top', healthcheck=dict(
'test': "true", test="true",
'interval': 1 * SECOND, interval=1 * SECOND,
'timeout': 1 * SECOND, timeout=1 * SECOND,
'retries': 1, retries=1,
}) ))
self.tmp_containers.append(container) self.tmp_containers.append(container)
self.client.start(container) self.client.start(container)
wait_on_health_status(self.client, container, "healthy") wait_on_health_status(self.client, container, "healthy")
@ -40,12 +40,12 @@ class HealthcheckTest(BaseAPIIntegrationTest):
@helpers.requires_api_version('1.24') @helpers.requires_api_version('1.24')
def test_healthcheck_fails(self): def test_healthcheck_fails(self):
container = self.client.create_container( container = self.client.create_container(
TEST_IMG, 'top', healthcheck={ TEST_IMG, 'top', healthcheck=dict(
'test': "false", test="false",
'interval': 1 * SECOND, interval=1 * SECOND,
'timeout': 1 * SECOND, timeout=1 * SECOND,
'retries': 1, retries=1,
}) ))
self.tmp_containers.append(container) self.tmp_containers.append(container)
self.client.start(container) self.client.start(container)
wait_on_health_status(self.client, container, "unhealthy") wait_on_health_status(self.client, container, "unhealthy")
@ -53,14 +53,14 @@ class HealthcheckTest(BaseAPIIntegrationTest):
@helpers.requires_api_version('1.29') @helpers.requires_api_version('1.29')
def test_healthcheck_start_period(self): def test_healthcheck_start_period(self):
container = self.client.create_container( container = self.client.create_container(
TEST_IMG, 'top', healthcheck={ TEST_IMG, 'top', healthcheck=dict(
'test': "echo 'x' >> /counter.txt && " test="echo 'x' >> /counter.txt && "
"test `cat /counter.txt | wc -l` -ge 3", "test `cat /counter.txt | wc -l` -ge 3",
'interval': 1 * SECOND, interval=1 * SECOND,
'timeout': 1 * SECOND, timeout=1 * SECOND,
'retries': 1, retries=1,
'start_period': 3 * SECOND start_period=3 * SECOND
} )
) )
self.tmp_containers.append(container) self.tmp_containers.append(container)

View File

@ -2,18 +2,19 @@ import contextlib
import json import json
import shutil import shutil
import socket import socket
import socketserver
import tarfile import tarfile
import tempfile import tempfile
import threading import threading
from http.server import SimpleHTTPRequestHandler
import pytest import pytest
from http.server import SimpleHTTPRequestHandler
import socketserver
import docker import docker
from ..helpers import requires_api_version, requires_experimental from ..helpers import requires_api_version, requires_experimental
from .base import TEST_IMG, BaseAPIIntegrationTest from .base import BaseAPIIntegrationTest, TEST_IMG
class ListImagesTest(BaseAPIIntegrationTest): class ListImagesTest(BaseAPIIntegrationTest):
@ -31,7 +32,7 @@ class ListImagesTest(BaseAPIIntegrationTest):
def test_images_quiet(self): def test_images_quiet(self):
res1 = self.client.images(quiet=True) res1 = self.client.images(quiet=True)
assert isinstance(res1[0], str) assert type(res1[0]) == str
class PullImageTest(BaseAPIIntegrationTest): class PullImageTest(BaseAPIIntegrationTest):
@ -42,7 +43,7 @@ class PullImageTest(BaseAPIIntegrationTest):
pass pass
res = self.client.pull('hello-world') res = self.client.pull('hello-world')
self.tmp_imgs.append('hello-world') self.tmp_imgs.append('hello-world')
assert isinstance(res, str) assert type(res) == str
assert len(self.client.images('hello-world')) >= 1 assert len(self.client.images('hello-world')) >= 1
img_info = self.client.inspect_image('hello-world') img_info = self.client.inspect_image('hello-world')
assert 'Id' in img_info assert 'Id' in img_info
@ -84,8 +85,13 @@ class CommitTest(BaseAPIIntegrationTest):
img_id = res['Id'] img_id = res['Id']
self.tmp_imgs.append(img_id) self.tmp_imgs.append(img_id)
img = self.client.inspect_image(img_id) img = self.client.inspect_image(img_id)
assert 'Parent' in img assert 'Container' in img
assert img['Container'].startswith(id)
assert 'ContainerConfig' in img
assert 'Image' in img['ContainerConfig']
assert TEST_IMG == img['ContainerConfig']['Image']
busybox_id = self.client.inspect_image(TEST_IMG)['Id'] busybox_id = self.client.inspect_image(TEST_IMG)['Id']
assert 'Parent' in img
assert img['Parent'] == busybox_id assert img['Parent'] == busybox_id
def test_commit_with_changes(self): def test_commit_with_changes(self):
@ -97,6 +103,8 @@ class CommitTest(BaseAPIIntegrationTest):
) )
self.tmp_imgs.append(img_id) self.tmp_imgs.append(img_id)
img = self.client.inspect_image(img_id) img = self.client.inspect_image(img_id)
assert 'Container' in img
assert img['Container'].startswith(cid['Id'])
assert '8000/tcp' in img['Config']['ExposedPorts'] assert '8000/tcp' in img['Config']['ExposedPorts']
assert img['Config']['Cmd'] == ['bash'] assert img['Config']['Cmd'] == ['bash']
@ -255,8 +263,10 @@ class ImportImageTest(BaseAPIIntegrationTest):
data = self.client.get_image(test_img) data = self.client.get_image(test_img)
assert data assert data
output = self.client.load_image(data) output = self.client.load_image(data)
assert any(line for line in output assert any([
if f'Loaded image: {test_img}' in line.get('stream', '')) line for line in output
if f'Loaded image: {test_img}' in line.get('stream', '')
])
@contextlib.contextmanager @contextlib.contextmanager
def temporary_http_file_server(self, stream): def temporary_http_file_server(self, stream):

View File

@ -1,10 +1,9 @@
import pytest
import docker import docker
from docker.types import IPAMConfig, IPAMPool from docker.types import IPAMConfig, IPAMPool
import pytest
from ..helpers import random_name, requires_api_version from ..helpers import random_name, requires_api_version
from .base import TEST_IMG, BaseAPIIntegrationTest from .base import BaseAPIIntegrationTest, TEST_IMG
class TestNetworks(BaseAPIIntegrationTest): class TestNetworks(BaseAPIIntegrationTest):
@ -234,7 +233,7 @@ class TestNetworks(BaseAPIIntegrationTest):
net_name, net_id = self.create_network( net_name, net_id = self.create_network(
ipam=IPAMConfig( ipam=IPAMConfig(
driver='default', driver='default',
pool_configs=[IPAMPool(subnet="2001:389::/64")], pool_configs=[IPAMPool(subnet="2001:389::1/64")],
), ),
) )
container = self.client.create_container( container = self.client.create_container(
@ -328,6 +327,8 @@ class TestNetworks(BaseAPIIntegrationTest):
net_name, net_id = self.create_network() net_name, net_id = self.create_network()
with pytest.raises(docker.errors.APIError): with pytest.raises(docker.errors.APIError):
self.client.create_network(net_name, check_duplicate=True) self.client.create_network(net_name, check_duplicate=True)
net_id = self.client.create_network(net_name, check_duplicate=False)
self.tmp_networks.append(net_id['Id'])
@requires_api_version('1.22') @requires_api_version('1.22')
def test_connect_with_links(self): def test_connect_with_links(self):
@ -388,7 +389,7 @@ class TestNetworks(BaseAPIIntegrationTest):
driver='default', driver='default',
pool_configs=[ pool_configs=[
IPAMPool( IPAMPool(
subnet="2001:389::/64", iprange="2001:389::0/96", subnet="2001:389::1/64", iprange="2001:389::0/96",
gateway="2001:389::ffff" gateway="2001:389::ffff"
) )
] ]
@ -454,7 +455,7 @@ class TestNetworks(BaseAPIIntegrationTest):
driver='default', driver='default',
pool_configs=[ pool_configs=[
IPAMPool( IPAMPool(
subnet="2001:389::/64", iprange="2001:389::0/96", subnet="2001:389::1/64", iprange="2001:389::0/96",
gateway="2001:389::ffff" gateway="2001:389::ffff"
) )
] ]

View File

@ -1,11 +1,10 @@
import os import os
import docker
import pytest import pytest
import docker
from ..helpers import requires_api_version
from .base import BaseAPIIntegrationTest from .base import BaseAPIIntegrationTest
from ..helpers import requires_api_version
SSHFS = 'vieux/sshfs:latest' SSHFS = 'vieux/sshfs:latest'
@ -40,7 +39,7 @@ class PluginTest(BaseAPIIntegrationTest):
return self.client.inspect_plugin(plugin_name) return self.client.inspect_plugin(plugin_name)
except docker.errors.NotFound: except docker.errors.NotFound:
prv = self.client.plugin_privileges(plugin_name) prv = self.client.plugin_privileges(plugin_name)
for _d in self.client.pull_plugin(plugin_name, prv): for d in self.client.pull_plugin(plugin_name, prv):
pass pass
return self.client.inspect_plugin(plugin_name) return self.client.inspect_plugin(plugin_name)
@ -119,7 +118,7 @@ class PluginTest(BaseAPIIntegrationTest):
pass pass
prv = self.client.plugin_privileges(SSHFS) prv = self.client.plugin_privileges(SSHFS)
logs = list(self.client.pull_plugin(SSHFS, prv)) logs = [d for d in self.client.pull_plugin(SSHFS, prv)]
assert filter(lambda x: x['status'] == 'Download complete', logs) assert filter(lambda x: x['status'] == 'Download complete', logs)
assert self.client.inspect_plugin(SSHFS) assert self.client.inspect_plugin(SSHFS)
assert self.client.enable_plugin(SSHFS) assert self.client.enable_plugin(SSHFS)
@ -129,7 +128,7 @@ class PluginTest(BaseAPIIntegrationTest):
pl_data = self.ensure_plugin_installed(SSHFS) pl_data = self.ensure_plugin_installed(SSHFS)
assert pl_data['Enabled'] is False assert pl_data['Enabled'] is False
prv = self.client.plugin_privileges(SSHFS) prv = self.client.plugin_privileges(SSHFS)
logs = list(self.client.upgrade_plugin(SSHFS, SSHFS, prv)) logs = [d for d in self.client.upgrade_plugin(SSHFS, SSHFS, prv)]
assert filter(lambda x: x['status'] == 'Download complete', logs) assert filter(lambda x: x['status'] == 'Download complete', logs)
assert self.client.inspect_plugin(SSHFS) assert self.client.inspect_plugin(SSHFS)
assert self.client.enable_plugin(SSHFS) assert self.client.enable_plugin(SSHFS)

View File

@ -1,6 +1,5 @@
import pytest
import docker import docker
import pytest
from ..helpers import force_leave_swarm, requires_api_version from ..helpers import force_leave_swarm, requires_api_version
from .base import BaseAPIIntegrationTest from .base import BaseAPIIntegrationTest

View File

@ -1,12 +1,13 @@
import random import random
import time import time
import docker
import pytest import pytest
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 BaseAPIIntegrationTest, TEST_IMG
class ServiceTest(BaseAPIIntegrationTest): class ServiceTest(BaseAPIIntegrationTest):
@ -140,7 +141,8 @@ 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.29') @requires_api_version('1.25')
@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

@ -1,8 +1,6 @@
import copy import copy
import pytest
import docker import docker
import pytest
from ..helpers import force_leave_swarm, requires_api_version from ..helpers import force_leave_swarm, requires_api_version
from .base import BaseAPIIntegrationTest from .base import BaseAPIIntegrationTest
@ -129,11 +127,11 @@ class SwarmTest(BaseAPIIntegrationTest):
assert self.init_swarm() assert self.init_swarm()
with pytest.raises(docker.errors.APIError) as exc_info: with pytest.raises(docker.errors.APIError) as exc_info:
self.client.leave_swarm() self.client.leave_swarm()
assert exc_info.value.response.status_code == 503 exc_info.value.response.status_code == 500
assert self.client.leave_swarm(force=True) assert self.client.leave_swarm(force=True)
with pytest.raises(docker.errors.APIError) as exc_info: with pytest.raises(docker.errors.APIError) as exc_info:
self.client.inspect_swarm() self.client.inspect_swarm()
assert exc_info.value.response.status_code == 503 exc_info.value.response.status_code == 406
assert self.client.leave_swarm(force=True) assert self.client.leave_swarm(force=True)
@requires_api_version('1.24') @requires_api_version('1.24')

View File

@ -1,6 +1,5 @@
import pytest
import docker import docker
import pytest
from ..helpers import requires_api_version from ..helpers import requires_api_version
from .base import BaseAPIIntegrationTest from .base import BaseAPIIntegrationTest
@ -17,16 +16,10 @@ 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):
# special name to avoid exponential timeout loop driver_name = 'invalid.driver'
# 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.APIError) as cm: with pytest.raises(docker.errors.NotFound):
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

@ -3,9 +3,8 @@ import shutil
import unittest import unittest
import docker import docker
from docker.utils import kwargs_from_env
from .. import helpers from .. import helpers
from docker.utils import kwargs_from_env
TEST_IMG = 'alpine:3.10' TEST_IMG = 'alpine:3.10'
TEST_API_VERSION = os.environ.get('DOCKER_TEST_API_VERSION') TEST_API_VERSION = os.environ.get('DOCKER_TEST_API_VERSION')
@ -104,7 +103,8 @@ class BaseAPIIntegrationTest(BaseIntegrationTest):
if exitcode != 0: if exitcode != 0:
output = self.client.logs(container) output = self.client.logs(container)
raise Exception( raise Exception(
f"Container exited with code {exitcode}:\n{output}") "Container exited with code {}:\n{}"
.format(exitcode, output))
return container return container

View File

@ -1,9 +1,10 @@
import threading import threading
import unittest import unittest
from datetime import datetime, timedelta
import docker import docker
from datetime import datetime, timedelta
from ..helpers import requires_api_version from ..helpers import requires_api_version
from .base import TEST_API_VERSION from .base import TEST_API_VERSION

View File

@ -1,10 +1,9 @@
import sys import sys
import warnings import warnings
import pytest
import docker.errors import docker.errors
from docker.utils import kwargs_from_env from docker.utils import kwargs_from_env
import pytest
from .base import TEST_IMG from .base import TEST_IMG

View File

@ -1,12 +1,9 @@
import os import os
import tempfile import tempfile
import pytest import pytest
from docker import errors from docker import errors
from docker.context import ContextAPI from docker.context import ContextAPI
from docker.tls import TLSConfig from docker.tls import TLSConfig
from .base import BaseAPIIntegrationTest from .base import BaseAPIIntegrationTest
@ -32,7 +29,7 @@ class ContextLifecycleTest(BaseAPIIntegrationTest):
"test", tls_cfg=docker_tls) "test", tls_cfg=docker_tls)
# check for a context 'test' in the context store # check for a context 'test' in the context store
assert any(ctx.Name == "test" for ctx in ContextAPI.contexts()) assert any([ctx.Name == "test" for ctx in ContextAPI.contexts()])
# retrieve a context object for 'test' # retrieve a context object for 'test'
assert ContextAPI.get_context("test") assert ContextAPI.get_context("test")
# remove context # remove context

View File

@ -6,11 +6,8 @@ import sys
import pytest import pytest
from docker.credentials import ( from docker.credentials import (
DEFAULT_LINUX_STORE, CredentialsNotFound, Store, StoreError, DEFAULT_LINUX_STORE,
DEFAULT_OSX_STORE, DEFAULT_OSX_STORE
CredentialsNotFound,
Store,
StoreError,
) )
@ -25,7 +22,7 @@ class TestStore:
def setup_method(self): def setup_method(self):
self.tmp_keys = [] self.tmp_keys = []
if sys.platform.startswith('linux'): if sys.platform.startswith('linux'):
if shutil.which(f"docker-credential-{DEFAULT_LINUX_STORE}"): if shutil.which('docker-credential-' + DEFAULT_LINUX_STORE):
self.store = Store(DEFAULT_LINUX_STORE) self.store = Store(DEFAULT_LINUX_STORE)
elif shutil.which('docker-credential-pass'): elif shutil.which('docker-credential-pass'):
self.store = Store('pass') self.store = Store('pass')

View File

@ -1,13 +1,13 @@
import os import os
from unittest import mock
from docker.credentials.utils import create_environment_dict from docker.credentials.utils import create_environment_dict
from unittest import mock
@mock.patch.dict(os.environ) @mock.patch.dict(os.environ)
def test_create_environment_dict(): def test_create_environment_dict():
base = {'FOO': 'bar', 'BAZ': 'foobar'} base = {'FOO': 'bar', 'BAZ': 'foobar'}
os.environ = base # noqa: B003 os.environ = base
assert create_environment_dict({'FOO': 'baz'}) == { assert create_environment_dict({'FOO': 'baz'}) == {
'FOO': 'baz', 'BAZ': 'foobar', 'FOO': 'baz', 'BAZ': 'foobar',
} }

Some files were not shown because too many files have changed in this diff Show More