Compare commits

..

1 Commits
main ... 4.4.3

Author SHA1 Message Date
aiordache 9acb9421ac release notes 4.4.3
Signed-off-by: aiordache <anca.iordache@docker.com>
2021-02-18 15:26:07 +01:00
166 changed files with 2692 additions and 3913 deletions

View File

@ -9,6 +9,3 @@ max_line_length = 80
[*.md]
trim_trailing_whitespace = false
[*.{yaml,yml}]
indent_size = 2

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

@ -2,71 +2,26 @@ name: Python package
on: [push, pull_request]
env:
DOCKER_BUILDKIT: '1'
FORCE_COLOR: 1
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.x'
- run: pip install -U ruff==0.1.8
- name: Run ruff
run: ruff 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:
runs-on: ubuntu-latest
strategy:
max-parallel: 1
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
pip3 install '.[ssh,dev]'
- name: Run unit tests
python -m pip install --upgrade pip
pip install -r test-requirements.txt -r requirements.txt
- name: Test with pytest
run: |
docker logout
rm -rf ~/.docker
py.test -v --cov=docker tests/unit
integration-tests:
runs-on: ubuntu-latest
strategy:
matrix:
variant: [ "integration-dind", "integration-dind-ssl", "integration-dind-ssh" ]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: make ${{ matrix.variant }}
run: |
docker logout
rm -rf ~/.docker
make ${{ matrix.variant }}

View File

@ -1,53 +0,0 @@
name: Release
on:
workflow_dispatch:
inputs:
tag:
description: "Release Tag WITHOUT `v` Prefix (e.g. 6.0.0)"
required: true
dry-run:
description: 'Dry run'
required: false
type: boolean
default: true
env:
DOCKER_BUILDKIT: '1'
FORCE_COLOR: 1
jobs:
publish:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Generate Package
run: |
pip3 install build
python -m build .
env:
# This is also supported by Hatch; see
# https://github.com/ofek/hatch-vcs#version-source-environment-variables
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ inputs.tag }}
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
if: '! inputs.dry-run'
with:
password: ${{ secrets.PYPI_API_TOKEN }}
- name: Create GitHub release
uses: ncipollo/release-action@v1
if: '! inputs.dry-run'
with:
artifacts: "dist/*"
generateReleaseNotes: true
draft: true
commit: ${{ github.sha }}
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ inputs.tag }}

4
.gitignore vendored
View File

@ -13,10 +13,6 @@ html/*
_build/
README.rst
# setuptools_scm
_version.py
env/
venv/
.idea/
*.iml

View File

@ -3,15 +3,8 @@ version: 2
sphinx:
configuration: docs/conf.py
build:
os: ubuntu-22.04
tools:
python: '3.12'
python:
version: 3.5
install:
- method: pip
path: .
extra_requirements:
- ssh
- docs
- requirements: docs-requirements.txt
- requirements: requirements.txt

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
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
`ruff`
`flake8`
### 3. Write clear, self-contained commits

View File

@ -1,13 +1,19 @@
# syntax=docker/dockerfile:1
ARG PYTHON_VERSION=2.7
ARG PYTHON_VERSION=3.12
FROM python:${PYTHON_VERSION}
WORKDIR /src
COPY . .
# Add SSH keys and set permissions
COPY tests/ssh-keys /root/.ssh
RUN chmod -R 600 /root/.ssh
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]
RUN mkdir /src
WORKDIR /src
COPY requirements.txt /src/requirements.txt
RUN pip install -r requirements.txt
COPY test-requirements.txt /src/test-requirements.txt
RUN pip install -r test-requirements.txt
COPY . /src
RUN pip install .

View File

@ -1,6 +1,4 @@
# syntax=docker/dockerfile:1
ARG PYTHON_VERSION=3.12
ARG PYTHON_VERSION=3.7
FROM python:${PYTHON_VERSION}
@ -11,12 +9,7 @@ RUN addgroup --gid $gid sphinx \
&& useradd --uid $uid --gid $gid -M sphinx
WORKDIR /src
COPY . .
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]
COPY requirements.txt docs-requirements.txt ./
RUN pip install -r requirements.txt -r docs-requirements.txt
USER sphinx

15
Dockerfile-py3 Normal file
View File

@ -0,0 +1,15 @@
ARG PYTHON_VERSION=3.7
FROM python:${PYTHON_VERSION}
RUN mkdir /src
WORKDIR /src
COPY requirements.txt /src/requirements.txt
RUN pip install -r requirements.txt
COPY test-requirements.txt /src/test-requirements.txt
RUN pip install -r test-requirements.txt
COPY . /src
RUN pip install .

150
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,150 @@
#!groovy
def imageNameBase = "dockerpinata/docker-py"
def imageNamePy2
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)
imageNamePy2 = "${imageNameBase}:py2-${gitCommit()}"
imageNamePy3 = "${imageNameBase}:py3-${gitCommit()}"
imageDindSSH = "${imageNameBase}:sshdind-${gitCommit()}"
withDockerRegistry(credentialsId:'dockerbuildbot-index.docker.io') {
buildImage(imageDindSSH, "-f tests/Dockerfile-ssh-dind .", "")
buildImage(imageNamePy2, "-f tests/Dockerfile --build-arg PYTHON_VERSION=2.7 .", "py2.7")
buildImage(imageNamePy3, "-f tests/Dockerfile --build-arg PYTHON_VERSION=3.7 .", "py3.7")
}
}
}
}
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: 'py2.7')`")
}
{ ->
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,15 @@
[Org]
[Org."Core maintainers"]
people = [
"glours",
"milas",
"shin-",
]
[Org.Alumni]
people = [
"aiordache",
"aanand",
"bfirsh",
"dnephin",
"mnowster",
"mpetazzoni",
"shin-",
"ulyssessouza",
]
[people]
@ -39,11 +35,6 @@
Email = "aanand@docker.com"
GitHub = "aanand"
[people.aiordache]
Name = "Anca Iordache"
Email = "anca.iordache@docker.com"
GitHub = "aiordache"
[people.bfirsh]
Name = "Ben Firshman"
Email = "b@fir.sh"
@ -54,16 +45,6 @@
Email = "dnephin@gmail.com"
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]
Name = "Mazz Mosley"
Email = "mazz@houseofmnowster.com"
@ -78,8 +59,3 @@
Name = "Joffrey F"
Email = "joffrey@docker.com"
GitHub = "shin-"
[people.ulyssessouza]
Name = "Ulysses Domiciano Souza"
Email = "ulysses.souza@docker.com"
GitHub = "ulyssessouza"

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 *

216
Makefile
View File

@ -1,75 +1,51 @@
TEST_API_VERSION ?= 1.45
TEST_ENGINE_VERSION ?= 26.1
ifeq ($(OS),Windows_NT)
PLATFORM := Windows
else
PLATFORM := $(shell sh -c 'uname -s 2>/dev/null || echo Unknown')
endif
ifeq ($(PLATFORM),Linux)
uid_args := "--build-arg uid=$(shell id -u) --build-arg gid=$(shell id -g)"
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
TEST_API_VERSION ?= 1.39
TEST_ENGINE_VERSION ?= 19.03.13
.PHONY: all
all: test
.PHONY: clean
clean:
-docker rm -f dpy-dind dpy-dind-certs dpy-dind-ssl
-docker rm -f dpy-dind-py2 dpy-dind-py3 dpy-dind-certs dpy-dind-ssl
find -name "__pycache__" | xargs rm -rf
.PHONY: build-dind-ssh
build-dind-ssh:
docker build \
--pull \
-t docker-dind-ssh \
-f tests/Dockerfile-ssh-dind \
--build-arg VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER} \
--build-arg ENGINE_VERSION=${TEST_ENGINE_VERSION} \
--build-arg API_VERSION=${TEST_API_VERSION} \
.
.PHONY: build
build:
docker build \
--pull \
-t docker-sdk-python3 \
-f tests/Dockerfile \
--build-arg VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER} \
.
docker build -t docker-sdk-python -f tests/Dockerfile --build-arg PYTHON_VERSION=2.7 --build-arg APT_MIRROR .
.PHONY: build-dind-ssh
build-dind-ssh:
docker build -t docker-dind-ssh -f tests/Dockerfile-ssh-dind --build-arg ENGINE_VERSION=${TEST_ENGINE_VERSION} --build-arg API_VERSION=${TEST_API_VERSION} --build-arg APT_MIRROR .
.PHONY: build-py3
build-py3:
docker build -t docker-sdk-python3 -f tests/Dockerfile --build-arg APT_MIRROR .
.PHONY: build-docs
build-docs:
docker build \
-t docker-sdk-python-docs \
-f Dockerfile-docs \
--build-arg VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER} \
$(uid_args) \
.
docker build -t docker-sdk-python-docs -f Dockerfile-docs --build-arg uid=$(shell id -u) --build-arg gid=$(shell id -g) .
.PHONY: build-dind-certs
build-dind-certs:
docker build \
-t dpy-dind-certs \
-f tests/Dockerfile-dind-certs \
--build-arg VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER} \
.
docker build -t dpy-dind-certs -f tests/Dockerfile-dind-certs .
.PHONY: test
test: ruff unit-test integration-dind integration-dind-ssl
test: flake8 unit-test unit-test-py3 integration-dind integration-dind-ssl
.PHONY: unit-test
unit-test: build
docker run -t --rm docker-sdk-python py.test tests/unit
.PHONY: unit-test-py3
unit-test-py3: build-py3
docker run -t --rm docker-sdk-python3 py.test tests/unit
.PHONY: integration-test
integration-test: build
docker run -t --rm -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python py.test -v tests/integration/${file}
.PHONY: integration-test-py3
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}
.PHONY: setup-network
@ -77,108 +53,70 @@ setup-network:
docker network inspect dpy-tests || docker network create dpy-tests
.PHONY: integration-dind
integration-dind: build setup-network
docker rm -vf dpy-dind || :
integration-dind: integration-dind-py2 integration-dind-py3
docker run \
--detach \
--name dpy-dind \
--network dpy-tests \
--pull=always \
--privileged \
docker:${TEST_ENGINE_VERSION}-dind \
dockerd -H tcp://0.0.0.0:2375 --experimental
.PHONY: integration-dind-py2
integration-dind-py2: build setup-network
docker rm -vf dpy-dind-py2 || :
docker run -d --network dpy-tests --name dpy-dind-py2 --privileged\
docker:${TEST_ENGINE_VERSION}-dind dockerd -H tcp://0.0.0.0:2375 --experimental
docker run -t --rm --env="DOCKER_HOST=tcp://dpy-dind-py2:2375" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\
--network dpy-tests docker-sdk-python py.test tests/integration/${file}
docker rm -vf dpy-dind-py2
# Wait for Docker-in-Docker to come to life
docker run \
--network dpy-tests \
--rm \
--tty \
busybox \
sh -c 'while ! nc -z dpy-dind 2375; do sleep 1; done'
.PHONY: integration-dind-py3
integration-dind-py3: build-py3 setup-network
docker rm -vf dpy-dind-py3 || :
docker run -d --network dpy-tests --name dpy-dind-py3 --privileged\
docker:${TEST_ENGINE_VERSION}-dind dockerd -H tcp://0.0.0.0:2375 --experimental
docker run -t --rm --env="DOCKER_HOST=tcp://dpy-dind-py3:2375" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\
--network dpy-tests docker-sdk-python3 py.test tests/integration/${file}
docker rm -vf dpy-dind-py3
docker run \
--env="DOCKER_HOST=tcp://dpy-dind:2375" \
--env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}" \
--network dpy-tests \
--rm \
--tty \
docker-sdk-python3 \
py.test tests/integration/${file}
docker rm -vf dpy-dind
.PHONY: integration-dind-ssh
integration-dind-ssh: build-dind-ssh build setup-network
docker rm -vf dpy-dind-ssh || :
docker run -d --network dpy-tests --name dpy-dind-ssh --privileged \
.PHONY: integration-ssh-py2
integration-ssh-py2: build-dind-ssh build setup-network
docker rm -vf dpy-dind-py2 || :
docker run -d --network dpy-tests --name dpy-dind-py2 --privileged\
docker-dind-ssh dockerd --experimental
# start SSH daemon for known key
docker exec dpy-dind-ssh sh -c "/usr/sbin/sshd -h /etc/ssh/known_ed25519 -p 22"
docker exec dpy-dind-ssh sh -c "/usr/sbin/sshd -h /etc/ssh/unknown_ed25519 -p 2222"
docker run \
--tty \
--rm \
--env="DOCKER_HOST=ssh://dpy-dind-ssh" \
--env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}" \
--env="UNKNOWN_DOCKER_SSH_HOST=ssh://dpy-dind-ssh:2222" \
--network dpy-tests \
docker-sdk-python3 py.test tests/ssh/${file}
docker rm -vf dpy-dind-ssh
# start SSH daemon
docker exec dpy-dind-py2 sh -c "/usr/sbin/sshd"
docker run -t --rm --env="DOCKER_HOST=ssh://dpy-dind-py2" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\
--network dpy-tests docker-sdk-python py.test tests/ssh/${file}
docker rm -vf dpy-dind-py2
.PHONY: integration-ssh-py3
integration-ssh-py3: build-dind-ssh build-py3 setup-network
docker rm -vf dpy-dind-py3 || :
docker run -d --network dpy-tests --name dpy-dind-py3 --privileged\
docker-dind-ssh dockerd --experimental
# start SSH daemon
docker exec dpy-dind-py3 sh -c "/usr/sbin/sshd"
docker run -t --rm --env="DOCKER_HOST=ssh://dpy-dind-py3" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\
--network dpy-tests docker-sdk-python3 py.test tests/ssh/${file}
docker rm -vf dpy-dind-py3
.PHONY: integration-dind-ssl
integration-dind-ssl: build-dind-certs build setup-network
integration-dind-ssl: build-dind-certs build build-py3
docker rm -vf dpy-dind-certs dpy-dind-ssl || :
docker run -d --name dpy-dind-certs dpy-dind-certs
docker run \
--detach \
--env="DOCKER_CERT_PATH=/certs" \
--env="DOCKER_HOST=tcp://localhost:2375" \
--env="DOCKER_TLS_VERIFY=1" \
--name dpy-dind-ssl \
--network dpy-tests \
--network-alias docker \
--pull=always \
--privileged \
--volume /tmp \
--volumes-from dpy-dind-certs \
docker:${TEST_ENGINE_VERSION}-dind \
dockerd \
--tlsverify \
--tlscacert=/certs/ca.pem \
--tlscert=/certs/server-cert.pem \
--tlskey=/certs/server-key.pem \
-H tcp://0.0.0.0:2375 \
--experimental
# Wait for Docker-in-Docker to come to life
docker run \
--network dpy-tests \
--rm \
--tty \
busybox \
sh -c 'while ! nc -z dpy-dind-ssl 2375; do sleep 1; done'
docker run \
--env="DOCKER_CERT_PATH=/certs" \
--env="DOCKER_HOST=tcp://docker:2375" \
--env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}" \
--env="DOCKER_TLS_VERIFY=1" \
--network dpy-tests \
--rm \
--volumes-from dpy-dind-ssl \
--tty \
docker-sdk-python3 \
py.test tests/integration/${file}
docker run -d --env="DOCKER_HOST=tcp://localhost:2375" --env="DOCKER_TLS_VERIFY=1"\
--env="DOCKER_CERT_PATH=/certs" --volumes-from dpy-dind-certs --name dpy-dind-ssl\
--network dpy-tests --network-alias docker -v /tmp --privileged\
docker:${TEST_ENGINE_VERSION}-dind\
dockerd --tlsverify --tlscacert=/certs/ca.pem --tlscert=/certs/server-cert.pem\
--tlskey=/certs/server-key.pem -H tcp://0.0.0.0:2375 --experimental
docker run -t --rm --volumes-from dpy-dind-ssl --env="DOCKER_HOST=tcp://docker:2375"\
--env="DOCKER_TLS_VERIFY=1" --env="DOCKER_CERT_PATH=/certs" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\
--network dpy-tests docker-sdk-python py.test tests/integration/${file}
docker run -t --rm --volumes-from dpy-dind-ssl --env="DOCKER_HOST=tcp://docker:2375"\
--env="DOCKER_TLS_VERIFY=1" --env="DOCKER_CERT_PATH=/certs" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\
--network dpy-tests docker-sdk-python3 py.test tests/integration/${file}
docker rm -vf dpy-dind-ssl dpy-dind-certs
.PHONY: ruff
ruff: build
docker run -t --rm docker-sdk-python3 ruff docker tests
.PHONY: flake8
flake8: build
docker run -t --rm docker-sdk-python flake8 docker tests
.PHONY: docs
docs: build-docs
@ -186,4 +124,4 @@ docs: build-docs
.PHONY: shell
shell: build
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-python python

View File

@ -1,17 +1,18 @@
# 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://travis-ci.org/docker/docker-py.svg?branch=master)](https://travis-ci.org/docker/docker-py)
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
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
> Older versions (< 6.0) required installing `docker[tls]` for SSL/TLS support.
> This is no longer necessary and is a no-op, but is supported for backwards compatibility.
If you are intending to connect to a docker host via TLS, add `docker[tls]` to your requirements instead, or install with pip:
pip install docker[tls]
## Usage

13
appveyor.yml Normal file
View File

@ -0,0 +1,13 @@
version: '{branch}-{build}'
install:
- "SET PATH=C:\\Python37-x64;C:\\Python37-x64\\Scripts;%PATH%"
- "python --version"
- "python -m pip install --upgrade pip"
- "pip install tox==2.9.1"
# Build the binary after tests
build: false
test_script:
- "tox"

View File

@ -1,7 +1,10 @@
# flake8: noqa
from .api import APIClient
from .client import DockerClient, from_env
from .context import Context, ContextAPI
from .context import Context
from .context import ContextAPI
from .tls import TLSConfig
from .version import __version__
from .version import version, version_info
__version__ = version
__title__ = 'docker'

View File

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

View File

@ -3,12 +3,16 @@ import logging
import os
import random
from .. import auth, constants, errors, utils
from .. import auth
from .. import constants
from .. import errors
from .. import utils
log = logging.getLogger(__name__)
class BuildApiMixin:
class BuildApiMixin(object):
def build(self, path=None, tag=None, quiet=False, fileobj=None,
nocache=False, rm=False, timeout=None,
custom_context=False, encoding=None, pull=False,
@ -72,7 +76,6 @@ class BuildApiMixin:
forcerm (bool): Always remove intermediate containers, even after
unsuccessful builds
dockerfile (str): path within the build context to the Dockerfile
gzip (bool): If set to ``True``, gzip compression/encoding is used
buildargs (dict): A dictionary of build arguments
container_limits (dict): A dictionary of limits applied to each
container created by the build process. Valid keys:
@ -125,16 +128,13 @@ class BuildApiMixin:
raise errors.DockerException(
'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():
if key not in constants.CONTAINER_LIMITS_KEYS:
raise errors.DockerException(
f"invalid tag '{tag}': invalid reference format"
'Invalid container_limits key {0}'.format(key)
)
if custom_context:
if not fileobj:
raise TypeError("You must specify fileobj with custom_context")
@ -150,10 +150,10 @@ class BuildApiMixin:
dockerignore = os.path.join(path, '.dockerignore')
exclude = None
if os.path.exists(dockerignore):
with open(dockerignore) as f:
with open(dockerignore, 'r') as f:
exclude = list(filter(
lambda x: x != '' and x[0] != '#',
[line.strip() for line in f.read().splitlines()]
[l.strip() for l in f.read().splitlines()]
))
dockerfile = process_dockerfile(dockerfile, path)
context = utils.tar(
@ -275,24 +275,10 @@ class BuildApiMixin:
return self._stream_helper(response, decode=decode)
@utils.minimum_version('1.31')
def prune_builds(self, filters=None, keep_storage=None, all=None):
def prune_builds(self):
"""
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:
(dict): A dictionary containing information about the operation's
result. The ``SpaceReclaimed`` key indicates the amount of
@ -303,20 +289,7 @@ class BuildApiMixin:
If the server returns an error.
"""
url = self._url("/build/prune")
if (filters, keep_storage, all) != (None, None, None) \
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)
return self._result(self._post(url), True)
def _set_auth_headers(self, headers):
log.debug('Looking for auth config')
@ -340,8 +313,9 @@ class BuildApiMixin:
auth_data[auth.INDEX_URL] = auth_data.get(auth.INDEX_NAME, {})
log.debug(
"Sending auth config (%s)",
', '.join(repr(k) for k in auth_data),
'Sending auth config ({0})'.format(
', '.join(repr(k) for k in auth_data.keys())
)
)
if auth_data:
@ -361,15 +335,18 @@ def process_dockerfile(dockerfile, path):
abs_dockerfile = os.path.join(path, dockerfile)
if constants.IS_WINDOWS_PLATFORM and path.startswith(
constants.WINDOWS_LONGPATH_PREFIX):
normpath = os.path.normpath(
abs_dockerfile[len(constants.WINDOWS_LONGPATH_PREFIX):])
abs_dockerfile = f'{constants.WINDOWS_LONGPATH_PREFIX}{normpath}'
abs_dockerfile = '{}{}'.format(
constants.WINDOWS_LONGPATH_PREFIX,
os.path.normpath(
abs_dockerfile[len(constants.WINDOWS_LONGPATH_PREFIX):]
)
)
if (os.path.splitdrive(path)[0] != os.path.splitdrive(abs_dockerfile)[0] or
os.path.relpath(abs_dockerfile, path).startswith('..')):
# Dockerfile not in context - read data to insert into tar later
with open(abs_dockerfile) as df:
with open(abs_dockerfile, 'r') as df:
return (
f'.dockerfile.{random.getrandbits(160):x}',
'.dockerfile.{0:x}'.format(random.getrandbits(160)),
df.read()
)

View File

@ -1,31 +1,21 @@
import json
import struct
import urllib
from functools import partial
import requests
import requests.adapters
import requests.exceptions
import six
import websocket
from .. import auth
from ..constants import (
DEFAULT_MAX_POOL_SIZE,
DEFAULT_NUM_POOLS,
DEFAULT_NUM_POOLS_SSH,
DEFAULT_TIMEOUT_SECONDS,
DEFAULT_USER_AGENT,
IS_WINDOWS_PLATFORM,
MINIMUM_DOCKER_API_VERSION,
STREAM_HEADER_SIZE_BYTES,
)
from ..errors import (
DockerException,
InvalidVersion,
TLSParameterError,
create_api_error_from_http_exception,
)
from ..constants import (DEFAULT_NUM_POOLS, DEFAULT_NUM_POOLS_SSH,
DEFAULT_MAX_POOL_SIZE, DEFAULT_TIMEOUT_SECONDS,
DEFAULT_USER_AGENT, 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 ..transport import UnixHTTPAdapter
from ..transport import SSLHTTPAdapter, UnixHTTPAdapter
from ..utils import check_resource, config, update_headers, utils
from ..utils.json_stream import json_stream
from ..utils.proxy import ProxyConfig
@ -117,7 +107,7 @@ class APIClient(
user_agent=DEFAULT_USER_AGENT, num_pools=None,
credstore_env=None, use_ssh_client=False,
max_pool_size=DEFAULT_MAX_POOL_SIZE):
super().__init__()
super(APIClient, self).__init__()
if tls and not base_url:
raise TLSParameterError(
@ -170,10 +160,10 @@ class APIClient(
base_url, timeout, pool_connections=num_pools,
max_pool_size=max_pool_size
)
except NameError as err:
except NameError:
raise DockerException(
'Install pypiwin32 package to enable npipe:// support'
) from err
)
self.mount('http+docker://', self._custom_adapter)
self.base_url = 'http+docker://localnpipe'
elif base_url.startswith('ssh://'):
@ -182,10 +172,10 @@ class APIClient(
base_url, timeout, pool_connections=num_pools,
max_pool_size=max_pool_size, shell_out=use_ssh_client
)
except NameError as err:
except NameError:
raise DockerException(
'Install paramiko package to enable ssh:// support'
) from err
)
self.mount('http+docker://ssh', self._custom_adapter)
self._unmount('http://', 'https://')
self.base_url = 'http+docker://ssh'
@ -194,7 +184,7 @@ class APIClient(
if isinstance(tls, TLSConfig):
tls.configure_client(self)
elif tls:
self._custom_adapter = requests.adapters.HTTPAdapter(
self._custom_adapter = SSLHTTPAdapter(
pool_connections=num_pools)
self.mount('https://', self._custom_adapter)
self.base_url = base_url
@ -202,34 +192,35 @@ class APIClient(
# version detection needs to be after unix adapter mounting
if version is None or (isinstance(
version,
str
six.string_types
) and version.lower() == 'auto'):
self._version = self._retrieve_server_version()
else:
self._version = version
if not isinstance(self._version, str):
if not isinstance(self._version, six.string_types):
raise DockerException(
'Version parameter must be a string or None. '
f'Found {type(version).__name__}'
'Version parameter must be a string or None. Found {0}'.format(
type(version).__name__
)
)
if utils.version_lt(self._version, MINIMUM_DOCKER_API_VERSION):
raise InvalidVersion(
f'API versions below {MINIMUM_DOCKER_API_VERSION} are '
f'no longer supported by this library.'
'API versions below {} are no longer supported by this '
'library.'.format(MINIMUM_DOCKER_API_VERSION)
)
def _retrieve_server_version(self):
try:
return self.version(api_version=False)["ApiVersion"]
except KeyError as ke:
except KeyError:
raise DockerException(
'Invalid response from docker daemon: key "ApiVersion"'
' is missing.'
) from ke
)
except Exception as e:
raise DockerException(
f'Error while fetching server API version: {e}'
) from e
'Error while fetching server API version: {0}'.format(e)
)
def _set_request_timeout(self, kwargs):
"""Prepare the kwargs for an HTTP request by inserting the timeout
@ -255,26 +246,28 @@ class APIClient(
def _url(self, pathfmt, *args, **kwargs):
for arg in args:
if not isinstance(arg, str):
if not isinstance(arg, six.string_types):
raise ValueError(
f'Expected a string but found {arg} ({type(arg)}) instead'
'Expected a string but found {0} ({1}) '
'instead'.format(arg, type(arg))
)
quote_f = partial(urllib.parse.quote, safe="/:")
quote_f = partial(six.moves.urllib.parse.quote, safe="/:")
args = map(quote_f, args)
formatted_path = pathfmt.format(*args)
if kwargs.get('versioned_api', True):
return f'{self.base_url}/v{self._version}{formatted_path}'
return '{0}/v{1}{2}'.format(
self.base_url, self._version, pathfmt.format(*args)
)
else:
return f'{self.base_url}{formatted_path}'
return '{0}{1}'.format(self.base_url, pathfmt.format(*args))
def _raise_for_status(self, response):
"""Raises stored :class:`APIError`, if one occurred."""
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
raise create_api_error_from_http_exception(e) from e
raise create_api_error_from_http_exception(e)
def _result(self, response, json=False, binary=False):
assert not (json and binary)
@ -291,7 +284,7 @@ class APIClient(
# so we do this disgusting thing here.
data2 = {}
if data is not None and isinstance(data, dict):
for k, v in iter(data.items()):
for k, v in six.iteritems(data):
if v is not None:
data2[k] = v
elif data is not None:
@ -319,16 +312,7 @@ class APIClient(
return self._create_websocket_connection(full_url)
def _create_websocket_connection(self, url):
try:
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
return websocket.create_connection(url)
def _get_raw_response_socket(self, response):
self._raise_for_status(response)
@ -336,10 +320,12 @@ class APIClient(
sock = response.raw._fp.fp.raw.sock
elif self.base_url.startswith('http+docker://ssh'):
sock = response.raw._fp.fp.channel
else:
elif six.PY3:
sock = response.raw._fp.fp.raw
if self.base_url.startswith("https://"):
sock = sock._sock
else:
sock = response.raw._fp.fp._sock
try:
# Keep a reference to the response to stop it being garbage
# collected. If the response is garbage collected, it will
@ -357,7 +343,8 @@ class APIClient(
if response.raw._fp.chunked:
if decode:
yield from json_stream(self._stream_helper(response, False))
for chunk in json_stream(self._stream_helper(response, False)):
yield chunk
else:
reader = response.raw
while not reader.closed:
@ -413,19 +400,10 @@ class APIClient(
def _stream_raw_result(self, response, chunk_size=1, decode=True):
''' Stream result for TTY-enabled container and raw binary data'''
self._raise_for_status(response)
# Disable timeout on the underlying socket to prevent
# Read timed out(s) for long running processes
socket = self._get_raw_response_socket(response)
self._disable_socket_timeout(socket)
yield from response.iter_content(chunk_size, decode)
for out in response.iter_content(chunk_size, decode):
yield out
def _read_from_socket(self, response, stream, tty=True, demux=False):
"""Consume all data from the socket, close the response and return the
data. If stream=True, then a generator is returned instead and the
caller is responsible for closing the response.
"""
socket = self._get_raw_response_socket(response)
gen = frames_iter(socket, tty)
@ -440,11 +418,8 @@ class APIClient(
if stream:
return gen
else:
try:
# Wait for all frames, concatenate them, and return the result
return consume_socket_output(gen, demux=demux)
finally:
response.close()
# Wait for all the frames, concatenate them, and return the result
return consume_socket_output(gen, demux=demux)
def _disable_socket_timeout(self, socket):
""" Depending on the combination of python version and whether we're
@ -490,12 +465,12 @@ class APIClient(
self._result(res, binary=True)
self._raise_for_status(res)
sep = b''
sep = six.binary_type()
if stream:
return self._multiplexed_response_stream_helper(res)
else:
return sep.join(
list(self._multiplexed_buffer_helper(res))
[x for x in self._multiplexed_buffer_helper(res)]
)
def _unmount(self, *args):
@ -504,7 +479,7 @@ class APIClient(
def get_adapter(self, url):
try:
return super().get_adapter(url)
return super(APIClient, self).get_adapter(url)
except requests.exceptions.InvalidSchema as e:
if self._custom_adapter:
return self._custom_adapter

View File

@ -1,11 +1,13 @@
import base64
import six
from .. import utils
class ConfigApiMixin:
class ConfigApiMixin(object):
@utils.minimum_version('1.30')
def create_config(self, name, data, labels=None, templating=None):
def create_config(self, name, data, labels=None):
"""
Create a config
@ -13,9 +15,6 @@ class ConfigApiMixin:
name (string): Name of the config
data (bytes): Config data to be stored
labels (dict): A mapping of labels to assign to the config
templating (dict): dictionary containing the name of the
templating driver to be used expressed as
{ name: <templating_driver_name>}
Returns (dict): ID of the newly created config
"""
@ -23,12 +22,12 @@ class ConfigApiMixin:
data = data.encode('utf-8')
data = base64.b64encode(data)
data = data.decode('ascii')
if six.PY3:
data = data.decode('ascii')
body = {
'Data': data,
'Name': name,
'Labels': labels,
'Templating': templating
'Labels': labels
}
url = self._url('/configs/create')

View File

@ -1,17 +1,18 @@
from datetime import datetime
from .. import errors, utils
import six
from .. import errors
from .. import utils
from ..constants import DEFAULT_DATA_CHUNK_SIZE
from ..types import (
CancellableStream,
ContainerConfig,
EndpointConfig,
HostConfig,
NetworkingConfig,
)
from ..types import CancellableStream
from ..types import ContainerConfig
from ..types import EndpointConfig
from ..types import HostConfig
from ..types import NetworkingConfig
class ContainerApiMixin:
class ContainerApiMixin(object):
@utils.check_resource('container')
def attach(self, container, stdout=True, stderr=True,
stream=False, logs=False, demux=False):
@ -113,7 +114,7 @@ class ContainerApiMixin:
@utils.check_resource('container')
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``
command.
@ -124,7 +125,6 @@ class ContainerApiMixin:
tag (str): The tag to push
message (str): A commit message
author (str): The name of the author
pause (bool): Whether to pause the container before committing
changes (str): Dockerfile instructions to apply while committing
conf (dict): The configuration for the container. See the
`Engine API documentation
@ -141,7 +141,6 @@ class ContainerApiMixin:
'tag': tag,
'comment': message,
'author': author,
'pause': pause,
'changes': changes
}
u = self._url("/commit")
@ -226,7 +225,7 @@ class ContainerApiMixin:
mac_address=None, labels=None, stop_signal=None,
networking_config=None, healthcheck=None,
stop_timeout=None, runtime=None,
use_config_proxy=True, platform=None):
use_config_proxy=True):
"""
Creates a container. Parameters are similar to those for the ``docker
run`` command except it doesn't support the attach options (``-a``).
@ -245,9 +244,9 @@ class ContainerApiMixin:
.. code-block:: python
container_id = client.api.create_container(
container_id = cli.create_container(
'busybox', 'ls', ports=[1111, 2222],
host_config=client.api.create_host_config(port_bindings={
host_config=cli.create_host_config(port_bindings={
1111: 4567,
2222: None
})
@ -259,24 +258,22 @@ class ContainerApiMixin:
.. code-block:: python
client.api.create_host_config(
port_bindings={1111: ('127.0.0.1', 4567)}
)
cli.create_host_config(port_bindings={1111: ('127.0.0.1', 4567)})
Or without host port assignment:
.. code-block:: python
client.api.create_host_config(port_bindings={1111: ('127.0.0.1',)})
cli.create_host_config(port_bindings={1111: ('127.0.0.1',)})
If you wish to use UDP instead of TCP (default), you need to declare
ports as such in both the config and host config:
.. code-block:: python
container_id = client.api.create_container(
container_id = cli.create_container(
'busybox', 'ls', ports=[(1111, 'udp'), 2222],
host_config=client.api.create_host_config(port_bindings={
host_config=cli.create_host_config(port_bindings={
'1111/udp': 4567, 2222: None
})
)
@ -286,7 +283,7 @@ class ContainerApiMixin:
.. code-block:: python
client.api.create_host_config(port_bindings={
cli.create_host_config(port_bindings={
1111: [1234, 4567]
})
@ -294,7 +291,7 @@ class ContainerApiMixin:
.. code-block:: python
client.api.create_host_config(port_bindings={
cli.create_host_config(port_bindings={
1111: [
('192.168.0.100', 1234),
('192.168.0.101', 1234)
@ -310,9 +307,9 @@ class ContainerApiMixin:
.. code-block:: python
container_id = client.api.create_container(
container_id = cli.create_container(
'busybox', 'ls', volumes=['/mnt/vol1', '/mnt/vol2'],
host_config=client.api.create_host_config(binds={
host_config=cli.create_host_config(binds={
'/home/user1/': {
'bind': '/mnt/vol2',
'mode': 'rw',
@ -320,11 +317,6 @@ class ContainerApiMixin:
'/var/www': {
'bind': '/mnt/vol1',
'mode': 'ro',
},
'/autofs/user1': {
'bind': '/mnt/vol3',
'mode': 'rw',
'propagation': 'shared'
}
})
)
@ -334,12 +326,11 @@ class ContainerApiMixin:
.. code-block:: python
container_id = client.api.create_container(
'busybox', 'ls', volumes=['/mnt/vol1', '/mnt/vol2', '/mnt/vol3'],
host_config=client.api.create_host_config(binds=[
container_id = cli.create_container(
'busybox', 'ls', volumes=['/mnt/vol1', '/mnt/vol2'],
host_config=cli.create_host_config(binds=[
'/home/user1/:/mnt/vol2',
'/var/www:/mnt/vol1:ro',
'/autofs/user1:/mnt/vol3:rw,shared',
])
)
@ -355,15 +346,15 @@ class ContainerApiMixin:
.. code-block:: python
networking_config = client.api.create_networking_config({
'network1': client.api.create_endpoint_config(
networking_config = docker_client.create_networking_config({
'network1': docker_client.create_endpoint_config(
ipv4_address='172.28.0.124',
aliases=['foo', 'bar'],
links=['container2']
)
})
ctnr = client.api.create_container(
ctnr = docker_client.create_container(
img, command, networking_config=networking_config
)
@ -407,7 +398,6 @@ class ContainerApiMixin:
configuration file (``~/.docker/config.json`` by default)
contains a proxy configuration, the corresponding environment
variables will be set in the container being created.
platform (str): Platform in the format ``os[/arch[/variant]]``.
Returns:
A dictionary with an image 'Id' key and a 'Warnings' key.
@ -418,7 +408,7 @@ class ContainerApiMixin:
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
if isinstance(volumes, str):
if isinstance(volumes, six.string_types):
volumes = [volumes, ]
if isinstance(environment, dict):
@ -437,22 +427,16 @@ class ContainerApiMixin:
stop_signal, networking_config, healthcheck,
stop_timeout, runtime
)
return self.create_container_from_config(config, name, platform)
return self.create_container_from_config(config, name)
def create_container_config(self, *args, **kwargs):
return ContainerConfig(self._version, *args, **kwargs)
def create_container_from_config(self, config, name=None, platform=None):
def create_container_from_config(self, config, name=None):
u = self._url("/containers/create")
params = {
'name': name
}
if platform:
if utils.version_lt(self._version, '1.41'):
raise errors.InvalidVersion(
'platform is not supported for API version < 1.41'
)
params['platform'] = platform
res = self._post_json(u, data=config, params=params)
return self._result(res, True)
@ -597,13 +581,10 @@ class ContainerApiMixin:
Example:
>>> client.api.create_host_config(
... privileged=True,
... cap_drop=['MKNOD'],
... volumes_from=['nostalgic_newton'],
... )
>>> cli.create_host_config(privileged=True, cap_drop=['MKNOD'],
volumes_from=['nostalgic_newton'])
{'CapDrop': ['MKNOD'], 'LxcConf': None, 'Privileged': True,
'VolumesFrom': ['nostalgic_newton'], 'PublishAllPorts': False}
'VolumesFrom': ['nostalgic_newton'], 'PublishAllPorts': False}
"""
if not kwargs:
@ -631,11 +612,11 @@ class ContainerApiMixin:
Example:
>>> client.api.create_network('network1')
>>> networking_config = client.api.create_networking_config({
'network1': client.api.create_endpoint_config()
>>> docker_client.create_network('network1')
>>> networking_config = docker_client.create_networking_config({
'network1': docker_client.create_endpoint_config()
})
>>> container = client.api.create_container(
>>> container = docker_client.create_container(
img, command, networking_config=networking_config
)
@ -669,7 +650,7 @@ class ContainerApiMixin:
Example:
>>> endpoint_config = client.api.create_endpoint_config(
>>> endpoint_config = client.create_endpoint_config(
aliases=['web', 'app'],
links={'app_db': 'db', 'another': None},
ipv4_address='132.65.0.123'
@ -687,8 +668,7 @@ class ContainerApiMixin:
container (str): The container to diff
Returns:
(list) A list of dictionaries containing the attributes `Path`
and `Kind`.
(str)
Raises:
:py:class:`docker.errors.APIError`
@ -749,7 +729,7 @@ class ContainerApiMixin:
>>> c = docker.APIClient()
>>> f = open('./sh_bin.tar', 'wb')
>>> bits, stat = c.api.get_archive(container, '/bin/sh')
>>> bits, stat = c.get_archive(container, '/bin/sh')
>>> print(stat)
{'name': 'sh', 'size': 1075464, 'mode': 493,
'mtime': '2018-10-01T15:37:48-07:00', 'linkTarget': ''}
@ -810,7 +790,7 @@ class ContainerApiMixin:
url = self._url("/containers/{0}/kill", container)
params = {}
if signal is not None:
if not isinstance(signal, str):
if not isinstance(signal, six.string_types):
signal = int(signal)
params['signal'] = signal
res = self._post(url, params=params)
@ -836,15 +816,14 @@ class ContainerApiMixin:
tail (str or int): Output specified number of lines at the end of
logs. Either an integer of number of lines or the string
``all``. Default ``all``
since (datetime, int, or float): Show logs since a given datetime,
integer epoch (in seconds) or float (in fractional seconds)
since (datetime or int): Show logs since a given datetime or
integer epoch (in seconds)
follow (bool): Follow log output. Default ``False``
until (datetime, int, or float): Show logs that occurred before
the given datetime, integer epoch (in seconds), or
float (in fractional seconds)
until (datetime or int): Show logs that occurred before the given
datetime or integer epoch (in seconds)
Returns:
(generator of bytes or bytes)
(generator or str)
Raises:
:py:class:`docker.errors.APIError`
@ -866,12 +845,10 @@ class ContainerApiMixin:
params['since'] = utils.datetime_to_timestamp(since)
elif (isinstance(since, int) and since > 0):
params['since'] = since
elif (isinstance(since, float) and since > 0.0):
params['since'] = since
else:
raise errors.InvalidArgument(
'since value should be datetime or positive int/float,'
f' not {type(since)}'
'since value should be datetime or positive int, '
'not {}'.format(type(since))
)
if until is not None:
@ -883,12 +860,10 @@ class ContainerApiMixin:
params['until'] = utils.datetime_to_timestamp(until)
elif (isinstance(until, int) and until > 0):
params['until'] = until
elif (isinstance(until, float) and until > 0.0):
params['until'] = until
else:
raise errors.InvalidArgument(
f'until value should be datetime or positive int/float, '
f'not {type(until)}'
'until value should be datetime or positive int, '
'not {}'.format(type(until))
)
url = self._url("/containers/{0}/logs", container)
@ -941,7 +916,7 @@ class ContainerApiMixin:
.. code-block:: python
>>> client.api.port('7174d6347063', 80)
>>> cli.port('7174d6347063', 80)
[{'HostIp': '0.0.0.0', 'HostPort': '80'}]
"""
res = self._get(self._url("/containers/{0}/json", container))
@ -960,7 +935,7 @@ class ContainerApiMixin:
return port_settings.get(private_port)
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:
break
@ -976,7 +951,7 @@ class ContainerApiMixin:
container (str): The container where the file(s) will be extracted
path (str): Path inside the container where the file(s) will be
extracted. Must exist.
data (bytes or stream): tar data to be extracted
data (bytes): tar data to be extracted
Returns:
(bool): True if the call succeeds.
@ -1120,10 +1095,10 @@ class ContainerApiMixin:
Example:
>>> container = client.api.create_container(
>>> container = cli.create_container(
... image='busybox:latest',
... command='/bin/sleep 30')
>>> client.api.start(container=container.get('Id'))
>>> cli.start(container=container.get('Id'))
"""
if args or kwargs:
raise errors.DeprecatedMethod(
@ -1136,7 +1111,7 @@ class ContainerApiMixin:
self._raise_for_status(res)
@utils.check_resource('container')
def stats(self, container, decode=None, stream=True, one_shot=None):
def stats(self, container, decode=None, stream=True):
"""
Stream statistics for a specific container. Similar to the
``docker stats`` command.
@ -1148,9 +1123,6 @@ class ContainerApiMixin:
False by default.
stream (bool): If set to false, only the current stats will be
returned instead of a stream. True by default.
one_shot (bool): If set to true, Only get a single stat instead of
waiting for 2 cycles. Must be used with stream=false. False by
default.
Raises:
:py:class:`docker.errors.APIError`
@ -1158,30 +1130,16 @@ class ContainerApiMixin:
"""
url = self._url("/containers/{0}/stats", container)
params = {
'stream': stream
}
if one_shot is not None:
if utils.version_lt(self._version, '1.41'):
raise errors.InvalidVersion(
'one_shot is not supported for API version < 1.41'
)
params['one-shot'] = one_shot
if stream:
if one_shot:
raise errors.InvalidArgument(
'one_shot is only available in conjunction with '
'stream=False'
)
return self._stream_helper(
self._get(url, stream=True, params=params), decode=decode
)
return self._stream_helper(self._get(url, stream=True),
decode=decode)
else:
if decode:
raise errors.InvalidArgument(
"decode is only available in conjunction with stream=True"
)
return self._result(self._get(url, params=params), json=True)
return self._result(self._get(url, params={'stream': False}),
json=True)
@utils.check_resource('container')
def stop(self, container, timeout=None):

View File

@ -4,7 +4,7 @@ from datetime import datetime
from .. import auth, types, utils
class DaemonApiMixin:
class DaemonApiMixin(object):
@utils.minimum_version('1.25')
def df(self):
"""

View File

@ -1,8 +1,10 @@
from .. import errors, utils
from ..types import CancellableStream
import six
from .. import errors
from .. import utils
class ExecApiMixin:
class ExecApiMixin(object):
@utils.check_resource('container')
def exec_create(self, container, cmd, stdout=True, stderr=True,
stdin=False, tty=False, privileged=False, user='',
@ -43,7 +45,7 @@ class ExecApiMixin:
'Setting environment for exec is not supported in API < 1.25'
)
if isinstance(cmd, str):
if isinstance(cmd, six.string_types):
cmd = utils.split_command(cmd)
if isinstance(environment, dict):
@ -125,10 +127,9 @@ class ExecApiMixin:
detach (bool): If true, detach from the exec command.
Default: False
tty (bool): Allocate a pseudo-TTY. Default: False
stream (bool): Return response data progressively as an iterator
of strings, rather than a single string.
stream (bool): Stream response data. Default: False
socket (bool): Return the connection socket to allow custom
read/write operations. Must be closed by the caller when done.
read/write operations.
demux (bool): Return stdout and stderr separately
Returns:
@ -162,15 +163,7 @@ class ExecApiMixin:
stream=True
)
if detach:
try:
return self._result(res)
finally:
res.close()
return self._result(res)
if socket:
return self._get_raw_response_socket(res)
output = self._read_from_socket(res, stream, tty=tty, demux=demux)
if stream:
return CancellableStream(output, res)
else:
return output
return self._read_from_socket(res, stream, tty=tty, demux=demux)

View File

@ -1,13 +1,15 @@
import logging
import os
import six
from .. import auth, errors, utils
from ..constants import DEFAULT_DATA_CHUNK_SIZE
log = logging.getLogger(__name__)
class ImageApiMixin:
class ImageApiMixin(object):
@utils.check_resource('image')
def get_image(self, image, chunk_size=DEFAULT_DATA_CHUNK_SIZE):
@ -29,7 +31,7 @@ class ImageApiMixin:
Example:
>>> image = client.api.get_image("busybox:latest")
>>> image = cli.get_image("busybox:latest")
>>> f = open('/tmp/busybox-latest.tar', 'wb')
>>> for chunk in image:
>>> f.write(chunk)
@ -47,7 +49,7 @@ class ImageApiMixin:
image (str): The image to show history for
Returns:
(list): The history of the image
(str): The history of the image
Raises:
:py:class:`docker.errors.APIError`
@ -128,7 +130,7 @@ class ImageApiMixin:
params = _import_image_params(
repository, tag, image,
src=(src if isinstance(src, str) else None),
src=(src if isinstance(src, six.string_types) else None),
changes=changes
)
headers = {'Content-Type': 'application/tar'}
@ -137,7 +139,7 @@ class ImageApiMixin:
return self._result(
self._post(u, data=None, params=params)
)
elif isinstance(src, str): # from file path
elif isinstance(src, six.string_types): # from file path
with open(src, 'rb') as f:
return self._result(
self._post(
@ -377,8 +379,7 @@ class ImageApiMixin:
Example:
>>> resp = client.api.pull('busybox', stream=True, decode=True)
... for line in resp:
>>> for line in cli.pull('busybox', stream=True, decode=True):
... print(json.dumps(line, indent=4))
{
"status": "Pulling image (latest) from busybox",
@ -457,12 +458,7 @@ class ImageApiMixin:
If the server returns an error.
Example:
>>> resp = client.api.push(
... 'yourname/app',
... stream=True,
... decode=True,
... )
... for line in resp:
>>> for line in cli.push('yourname/app', stream=True, decode=True):
... print(line)
{'status': 'Pushing repository yourname/app (1 tags)'}
{'status': 'Pushing','progressDetail': {}, 'id': '511136ea3c5a'}
@ -513,14 +509,13 @@ class ImageApiMixin:
res = self._delete(self._url("/images/{0}", image), params=params)
return self._result(res, True)
def search(self, term, limit=None):
def search(self, term):
"""
Search for images on Docker Hub. Similar to the ``docker search``
command.
Args:
term (str): A term to search for.
limit (int): The maximum number of results to return.
Returns:
(list of dicts): The response of the search.
@ -529,12 +524,8 @@ class ImageApiMixin:
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
params = {'term': term}
if limit is not None:
params['limit'] = limit
return self._result(
self._get(self._url("/images/search"), params=params),
self._get(self._url("/images/search"), params={'term': term}),
True
)
@ -558,7 +549,7 @@ class ImageApiMixin:
Example:
>>> client.api.tag('ubuntu', 'localhost:5000/ubuntu', 'latest',
>>> client.tag('ubuntu', 'localhost:5000/ubuntu', 'latest',
force=True)
"""
params = {
@ -575,7 +566,7 @@ class ImageApiMixin:
def is_file(src):
try:
return (
isinstance(src, str) and
isinstance(src, six.string_types) and
os.path.isfile(src)
)
except TypeError: # a data string will make isfile() raise a TypeError

View File

@ -1,9 +1,10 @@
from .. import utils
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(object):
def networks(self, names=None, ids=None, filters=None):
"""
List networks. Similar to the ``docker network ls`` command.
@ -74,7 +75,7 @@ class NetworkApiMixin:
Example:
A network using the bridge driver:
>>> client.api.create_network("network1", driver="bridge")
>>> client.create_network("network1", driver="bridge")
You can also create more advanced networks with custom IPAM
configurations. For example, setting the subnet to
@ -89,7 +90,7 @@ class NetworkApiMixin:
>>> ipam_config = docker.types.IPAMConfig(
pool_configs=[ipam_pool]
)
>>> client.api.create_network("network1", driver="bridge",
>>> docker_client.create_network("network1", driver="bridge",
ipam=ipam_config)
"""
if options is not None and not isinstance(options, dict):
@ -215,8 +216,7 @@ class NetworkApiMixin:
def connect_container_to_network(self, container, net_id,
ipv4_address=None, ipv6_address=None,
aliases=None, links=None,
link_local_ips=None, driver_opt=None,
mac_address=None):
link_local_ips=None, driver_opt=None):
"""
Connect a container to a network.
@ -235,16 +235,13 @@ class NetworkApiMixin:
network, using the IPv6 protocol. Defaults to ``None``.
link_local_ips (:py:class:`list`): A list of link-local
(IPv4/IPv6) addresses.
mac_address (str): The MAC address of this container on the
network. Defaults to ``None``.
"""
data = {
"Container": container,
"EndpointConfig": self.create_endpoint_config(
aliases=aliases, links=links, ipv4_address=ipv4_address,
ipv6_address=ipv6_address, link_local_ips=link_local_ips,
driver_opt=driver_opt,
mac_address=mac_address
driver_opt=driver_opt
),
}

View File

@ -1,7 +1,9 @@
import six
from .. import auth, utils
class PluginApiMixin:
class PluginApiMixin(object):
@utils.minimum_version('1.25')
@utils.check_resource('name')
def configure_plugin(self, name, options):
@ -19,7 +21,7 @@ class PluginApiMixin:
url = self._url('/plugins/{0}/set', name)
data = options
if isinstance(data, dict):
data = [f'{k}={v}' for k, v in data.items()]
data = ['{0}={1}'.format(k, v) for k, v in six.iteritems(data)]
res = self._post_json(url, data=data)
self._raise_for_status(res)
return True
@ -51,20 +53,19 @@ class PluginApiMixin:
return True
@utils.minimum_version('1.25')
def disable_plugin(self, name, force=False):
def disable_plugin(self, name):
"""
Disable an installed plugin.
Args:
name (string): The name of the plugin. The ``:latest`` tag is
optional, and is the default if omitted.
force (bool): To enable the force query parameter.
Returns:
``True`` if successful
"""
url = self._url('/plugins/{0}/disable', name)
res = self._post(url, params={'force': force})
res = self._post(url)
self._raise_for_status(res)
return True

View File

@ -1,9 +1,12 @@
import base64
from .. import errors, utils
import six
from .. import errors
from .. import utils
class SecretApiMixin:
class SecretApiMixin(object):
@utils.minimum_version('1.25')
def create_secret(self, name, data, labels=None, driver=None):
"""
@ -22,7 +25,8 @@ class SecretApiMixin:
data = data.encode('utf-8')
data = base64.b64encode(data)
data = data.decode('ascii')
if six.PY3:
data = data.decode('ascii')
body = {
'Data': data,
'Name': name,

View File

@ -7,7 +7,9 @@ def _check_api_features(version, task_template, update_config, endpoint_spec,
def raise_version_error(param, min_version):
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:
@ -43,7 +45,7 @@ def _check_api_features(version, task_template, update_config, endpoint_spec,
if task_template is not None:
if 'ForceUpdate' in task_template and utils.version_lt(
version, '1.25'):
raise_version_error('force_update', '1.25')
raise_version_error('force_update', '1.25')
if task_template.get('Placement'):
if utils.version_lt(version, '1.30'):
@ -111,7 +113,7 @@ def _merge_task_template(current, override):
return merged
class ServiceApiMixin:
class ServiceApiMixin(object):
@utils.minimum_version('1.24')
def create_service(
self, task_template, name=None, labels=None, mode=None,
@ -260,7 +262,7 @@ class ServiceApiMixin:
return True
@utils.minimum_version('1.24')
def services(self, filters=None, status=None):
def services(self, filters=None):
"""
List services.
@ -268,8 +270,6 @@ class ServiceApiMixin:
filters (dict): Filters to process on the nodes list. Valid
filters: ``id``, ``name`` , ``label`` and ``mode``.
Default: ``None``.
status (bool): Include the service task count of running and
desired tasks. Default: ``None``.
Returns:
A list of dictionaries containing data about each service.
@ -281,12 +281,6 @@ class ServiceApiMixin:
params = {
'filters': utils.convert_filters(filters) if filters else None
}
if status is not None:
if utils.version_lt(self._version, '1.41'):
raise errors.InvalidVersion(
'status is not supported in API version < 1.41'
)
params['status'] = status
url = self._url('/services')
return self._result(self._get(url, params=params), True)

View File

@ -1,13 +1,14 @@
import http.client as http_client
import logging
from .. import errors, types, utils
from six.moves import http_client
from ..constants import DEFAULT_SWARM_ADDR_POOL, DEFAULT_SWARM_SUBNET_SIZE
from .. import errors
from .. import types
from .. import utils
log = logging.getLogger(__name__)
class SwarmApiMixin:
class SwarmApiMixin(object):
def create_swarm_spec(self, *args, **kwargs):
"""
@ -57,10 +58,10 @@ class SwarmApiMixin:
Example:
>>> spec = client.api.create_swarm_spec(
>>> spec = client.create_swarm_spec(
snapshot_interval=5000, log_entries_for_slow_followers=1200
)
>>> client.api.init_swarm(
>>> client.init_swarm(
advertise_addr='eth0', listen_addr='0.0.0.0:5000',
force_new_cluster=False, swarm_spec=spec
)
@ -84,7 +85,7 @@ class SwarmApiMixin:
def init_swarm(self, advertise_addr=None, listen_addr='0.0.0.0:2377',
force_new_cluster=False, swarm_spec=None,
default_addr_pool=None, subnet_size=None,
data_path_addr=None, data_path_port=None):
data_path_addr=None):
"""
Initialize a new Swarm using the current connected engine as the first
node.
@ -117,9 +118,6 @@ class SwarmApiMixin:
networks created from the default subnet pool. Default: None
data_path_addr (string): Address or interface to use for data path
traffic. For example, 192.168.1.1, or an interface, like eth0.
data_path_port (int): Port number to use for data path traffic.
Acceptable port range is 1024 to 49151. If set to ``None`` or
0, the default port 4789 will be used. Default: None
Returns:
(str): The ID of the created node.
@ -168,14 +166,6 @@ class SwarmApiMixin:
)
data['DataPathAddr'] = data_path_addr
if data_path_port is not None:
if utils.version_lt(self._version, '1.40'):
raise errors.InvalidVersion(
'Data path port is only available for '
'API version >= 1.40'
)
data['DataPathPort'] = data_path_port
response = self._post_json(url, data=data)
return self._result(response, json=True)
@ -364,8 +354,8 @@ class SwarmApiMixin:
Example:
>>> key = client.api.get_unlock_key()
>>> client.unlock_swarm(key)
>>> key = client.get_unlock_key()
>>> client.unlock_node(key)
"""
if isinstance(key, dict):
@ -406,7 +396,7 @@ class SwarmApiMixin:
'Role': 'manager',
'Labels': {'foo': 'bar'}
}
>>> client.api.update_node(node_id='24ifsmvkjbyhk', version=8,
>>> client.update_node(node_id='24ifsmvkjbyhk', version=8,
node_spec=node_spec)
"""

View File

@ -1,7 +1,8 @@
from .. import errors, utils
from .. import errors
from .. import utils
class VolumeApiMixin:
class VolumeApiMixin(object):
def volumes(self, filters=None):
"""
List volumes currently registered by the docker daemon. Similar to the
@ -20,7 +21,7 @@ class VolumeApiMixin:
Example:
>>> client.api.volumes()
>>> cli.volumes()
{u'Volumes': [{u'Driver': u'local',
u'Mountpoint': u'/var/lib/docker/volumes/foobar/_data',
u'Name': u'foobar'},
@ -55,18 +56,15 @@ class VolumeApiMixin:
Example:
>>> volume = client.api.create_volume(
... name='foobar',
... driver='local',
... driver_opts={'foo': 'bar', 'baz': 'false'},
... labels={"key": "value"},
... )
... print(volume)
>>> volume = cli.create_volume(name='foobar', driver='local',
driver_opts={'foo': 'bar', 'baz': 'false'},
labels={"key": "value"})
>>> print(volume)
{u'Driver': u'local',
u'Labels': {u'key': u'value'},
u'Mountpoint': u'/var/lib/docker/volumes/foobar/_data',
u'Name': u'foobar',
u'Scope': u'local'}
u'Labels': {u'key': u'value'},
u'Mountpoint': u'/var/lib/docker/volumes/foobar/_data',
u'Name': u'foobar',
u'Scope': u'local'}
"""
url = self._url('/volumes/create')
@ -106,7 +104,7 @@ class VolumeApiMixin:
Example:
>>> client.api.inspect_volume('foobar')
>>> cli.inspect_volume('foobar')
{u'Driver': u'local',
u'Mountpoint': u'/var/lib/docker/volumes/foobar/_data',
u'Name': u'foobar'}

View File

@ -2,11 +2,14 @@ import base64
import json
import logging
from . import credentials, errors
import six
from . import credentials
from . import errors
from .utils import config
INDEX_NAME = 'docker.io'
INDEX_URL = f'https://index.{INDEX_NAME}/v1/'
INDEX_URL = 'https://index.{0}/v1/'.format(INDEX_NAME)
TOKEN_USERNAME = '<token>'
log = logging.getLogger(__name__)
@ -15,21 +18,21 @@ log = logging.getLogger(__name__)
def resolve_repository_name(repo_name):
if '://' in repo_name:
raise errors.InvalidRepository(
f'Repository name cannot contain a scheme ({repo_name})'
'Repository name cannot contain a scheme ({0})'.format(repo_name)
)
index_name, remote_name = split_repo_name(repo_name)
if index_name[0] == '-' or index_name[-1] == '-':
raise errors.InvalidRepository(
f'Invalid index name ({index_name}). '
'Cannot begin or end with a hyphen.'
'Invalid index name ({0}). Cannot begin or end with a'
' hyphen.'.format(index_name)
)
return resolve_index_name(index_name), remote_name
def resolve_index_name(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
return index_name
@ -95,10 +98,12 @@ class AuthConfig(dict):
"""
conf = {}
for registry, entry in entries.items():
for registry, entry in six.iteritems(entries):
if not isinstance(entry, dict):
log.debug(
f'Config entry for key {registry} is not auth config'
'Config entry for key {0} is not auth config'.format(
registry
)
)
# We sometimes fall back to parsing the whole config as if it
# was the auth config by itself, for legacy purposes. In that
@ -106,11 +111,17 @@ class AuthConfig(dict):
# keys is not formatted properly.
if raise_on_error:
raise errors.InvalidConfigFile(
f'Invalid configuration for registry {registry}'
'Invalid configuration for registry {0}'.format(
registry
)
)
return {}
if 'identitytoken' in entry:
log.debug(f'Found an IdentityToken entry for registry {registry}')
log.debug(
'Found an IdentityToken entry for registry {0}'.format(
registry
)
)
conf[registry] = {
'IdentityToken': entry['identitytoken']
}
@ -121,15 +132,16 @@ class AuthConfig(dict):
# a valid value in the auths config.
# https://github.com/docker/compose/issues/3265
log.debug(
f'Auth data for {registry} is absent. '
f'Client might be using a credentials store instead.'
'Auth data for {0} is absent. Client might be using a '
'credentials store instead.'.format(registry)
)
conf[registry] = {}
continue
username, password = decode_auth(entry['auth'])
log.debug(
f'Found entry (registry={registry!r}, username={username!r})'
'Found entry (registry={0}, username={1})'
.format(repr(registry), repr(username))
)
conf[registry] = {
@ -158,7 +170,7 @@ class AuthConfig(dict):
try:
with open(config_file) as f:
config_dict = json.load(f)
except (OSError, KeyError, ValueError) as e:
except (IOError, KeyError, ValueError) as e:
# Likely missing new Docker config file or it's in an
# unknown format, continue to attempt to read old location
# and format.
@ -218,7 +230,7 @@ class AuthConfig(dict):
store_name = self.get_credential_store(registry)
if store_name is not None:
log.debug(
f'Using credentials store "{store_name}"'
'Using credentials store "{0}"'.format(store_name)
)
cfg = self._resolve_authconfig_credstore(registry, store_name)
if cfg is not None:
@ -227,15 +239,15 @@ class AuthConfig(dict):
# Default to the public index server
registry = resolve_index_name(registry) if registry else INDEX_NAME
log.debug(f"Looking for auth entry for {repr(registry)}")
log.debug("Looking for auth entry for {0}".format(repr(registry)))
if registry in self.auths:
log.debug(f"Found {repr(registry)}")
log.debug("Found {0}".format(repr(registry)))
return self.auths[registry]
for key, conf in self.auths.items():
for key, conf in six.iteritems(self.auths):
if resolve_index_name(key) == registry:
log.debug(f"Found {repr(key)}")
log.debug("Found {0}".format(repr(key)))
return conf
log.debug("No entry found")
@ -246,7 +258,7 @@ class AuthConfig(dict):
# The ecosystem is a little schizophrenic with index.docker.io VS
# docker.io - in that case, it seems the full URL is necessary.
registry = INDEX_URL
log.debug(f"Looking for auth entry for {repr(registry)}")
log.debug("Looking for auth entry for {0}".format(repr(registry)))
store = self._get_store_instance(credstore_name)
try:
data = store.get(registry)
@ -266,8 +278,8 @@ class AuthConfig(dict):
return None
except credentials.StoreError as e:
raise errors.DockerException(
f'Credentials store error: {repr(e)}'
) from e
'Credentials store error: {0}'.format(repr(e))
)
def _get_store_instance(self, name):
if name not in self._stores:
@ -317,7 +329,7 @@ def convert_to_hostname(url):
def decode_auth(auth):
if isinstance(auth, str):
if isinstance(auth, six.string_types):
auth = auth.encode('ascii')
s = base64.b64decode(auth)
login, pwd = s.split(b':', 1)
@ -373,6 +385,7 @@ def _load_legacy_config(config_file):
}}
except Exception as e:
log.debug(e)
pass
log.debug("All parsing attempts failed - returning empty config")
return {}

View File

@ -1,5 +1,5 @@
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.containers import ContainerCollection
from .models.images import ImageCollection
@ -13,7 +13,7 @@ from .models.volumes import VolumeCollection
from .utils import kwargs_from_env
class DockerClient:
class DockerClient(object):
"""
A client for communicating with a Docker server.
@ -71,6 +71,8 @@ class DockerClient:
timeout (int): Default timeout for API calls, in seconds.
max_pool_size (int): The maximum number of connections
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
from. Default: the value of ``os.environ``
credstore_env (dict): Override environment variables when calling
@ -210,7 +212,7 @@ class DockerClient:
close.__doc__ = APIClient.close.__doc__
def __getattr__(self, name):
s = [f"'DockerClient' object has no attribute '{name}'"]
s = ["'DockerClient' object has no attribute '{}'".format(name)]
# If a user calls a method on APIClient, they
if hasattr(APIClient, name):
s.append("In Docker SDK for Python 2.0, this method is now on the "

View File

@ -1,9 +1,8 @@
import sys
from .version import version
from .version import __version__
DEFAULT_DOCKER_API_VERSION = '1.45'
MINIMUM_DOCKER_API_VERSION = '1.24'
DEFAULT_DOCKER_API_VERSION = '1.39'
MINIMUM_DOCKER_API_VERSION = '1.21'
DEFAULT_TIMEOUT_SECONDS = 60
STREAM_HEADER_SIZE_BYTES = 8
CONTAINER_LIMITS_KEYS = [
@ -29,7 +28,7 @@ INSECURE_REGISTRY_DEPRECATION_WARNING = \
IS_WINDOWS_PLATFORM = (sys.platform == 'win32')
WINDOWS_LONGPATH_PREFIX = '\\\\?\\'
DEFAULT_USER_AGENT = f"docker-sdk-python/{__version__}"
DEFAULT_USER_AGENT = "docker-sdk-python/{0}".format(version)
DEFAULT_NUM_POOLS = 25
# The OpenSSH server default value for MaxSessions is 10 which means we can

View File

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

View File

@ -2,17 +2,14 @@ import json
import os
from docker import errors
from .config import (
METAFILE,
get_current_context_name,
get_meta_dir,
write_context_name_to_docker_config,
)
from .context import Context
from docker.context.config import get_meta_dir
from docker.context.config import METAFILE
from docker.context.config import get_current_context_name
from docker.context.config import write_context_name_to_docker_config
from docker.context import Context
class ContextAPI:
class ContextAPI(object):
"""Context API.
Contains methods for context management:
create, list, remove, get, inspect.
@ -112,12 +109,12 @@ class ContextAPI:
if filename == METAFILE:
try:
data = json.load(
open(os.path.join(dirname, filename)))
open(os.path.join(dirname, filename), "r"))
names.append(data["Name"])
except Exception as e:
raise errors.ContextException(
f"Failed to load metafile {filename}: {e}",
) from e
"Failed to load metafile {}: {}".format(
filename, e))
contexts = [cls.DEFAULT_CONTEXT]
for name in names:
@ -141,7 +138,7 @@ class ContextAPI:
err = write_context_name_to_docker_config(name)
if err:
raise errors.ContextException(
f'Failed to set current context: {err}')
'Failed to set current context: {}'.format(err))
@classmethod
def remove_context(cls, name):

View File

@ -1,9 +1,10 @@
import hashlib
import json
import os
import json
import hashlib
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
METAFILE = "meta.json"
@ -14,7 +15,7 @@ def get_current_context_name():
docker_cfg_path = find_config_file()
if docker_cfg_path:
try:
with open(docker_cfg_path) as f:
with open(docker_cfg_path, "r") as f:
name = json.load(f).get("currentContext", "default")
except Exception:
return "default"
@ -28,7 +29,7 @@ def write_context_name_to_docker_config(name=None):
config = {}
if docker_cfg_path:
try:
with open(docker_cfg_path) as f:
with open(docker_cfg_path, "r") as f:
config = json.load(f)
except Exception as e:
return e
@ -76,6 +77,5 @@ def get_context_host(path=None, tls=False):
host = utils.parse_host(path, IS_WINDOWS_PLATFORM, tls)
if host == DEFAULT_UNIX_SOCKET:
# remove http+ from default docker socket url
if host.startswith("http+"):
host = host[5:]
return host.strip("http+")
return host

View File

@ -1,21 +1,16 @@
import json
import os
import json
from shutil import copyfile, rmtree
from docker.errors import ContextException
from docker.tls import TLSConfig
from .config import (
get_context_host,
get_meta_dir,
get_meta_file,
get_tls_dir,
)
from docker.errors import ContextException
from docker.context.config import get_meta_dir
from docker.context.config import get_meta_file
from docker.context.config import get_tls_dir
from docker.context.config import get_context_host
class Context:
"""A context."""
def __init__(self, name, orchestrator=None, host=None, endpoints=None,
tls=False):
if not name:
@ -46,9 +41,8 @@ class Context:
for k, v in endpoints.items():
if not isinstance(v, dict):
# unknown format
raise ContextException(
f"Unknown endpoint format for context {name}: {v}",
)
raise ContextException("""Unknown endpoint format for
context {}: {}""".format(name, v))
self.endpoints[k] = v
if k != "docker":
@ -99,11 +93,10 @@ class Context:
try:
with open(meta_file) as f:
metadata = json.load(f)
except (OSError, KeyError, ValueError) as e:
except (IOError, KeyError, ValueError) as e:
# unknown format
raise Exception(
f"Detected corrupted meta file for context {name} : {e}"
) from e
raise Exception("""Detected corrupted meta file for
context {} : {}""".format(name, e))
# for docker endpoints, set defaults for
# Host and SkipTLSVerify fields
@ -134,12 +127,8 @@ class Context:
elif filename.startswith("key"):
key = os.path.join(tls_dir, endpoint, filename)
if all([ca_cert, cert, key]):
verify = None
if endpoint == "docker" and not self.endpoints["docker"].get(
"SkipTLSVerify", False):
verify = True
certs[endpoint] = TLSConfig(
client_cert=(cert, key), ca_cert=ca_cert, verify=verify)
client_cert=(cert, key), ca_cert=ca_cert)
self.tls_cfg = certs
self.tls_path = tls_dir
@ -177,7 +166,7 @@ class Context:
rmtree(self.tls_path)
def __repr__(self):
return f"<{self.__class__.__name__}: '{self.name}'>"
return "<%s: '%s'>" % (self.__class__.__name__, self.name)
def __str__(self):
return json.dumps(self.__call__(), indent=2)

View File

@ -1,8 +1,4 @@
from .constants import (
DEFAULT_LINUX_STORE,
DEFAULT_OSX_STORE,
DEFAULT_WIN32_STORE,
PROGRAM_PREFIX,
)
from .errors import CredentialsNotFound, StoreError
# flake8: noqa
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):
message = cpe.output.decode('utf-8')
if 'credentials not found in native keychain' in message:
return CredentialsNotFound(f'No matching credentials in {program}')
return StoreError(f'Credentials store {program} exited with "{message}".')
return CredentialsNotFound(
'No matching credentials in {}'.format(
program
)
)
return StoreError(
'Credentials store {} exited with "{}".'.format(
program, cpe.output.decode('utf-8').strip()
)
)

View File

@ -1,33 +1,36 @@
import errno
import json
import shutil
import subprocess
import warnings
from . import constants, errors
import six
from . import constants
from . import errors
from .utils import create_environment_dict
from .utils import find_executable
class Store:
class Store(object):
def __init__(self, program, environment=None):
""" Create a store object that acts as an interface to
perform the basic operations for storing, retrieving
and erasing credentials using `program`.
"""
self.program = constants.PROGRAM_PREFIX + program
self.exe = shutil.which(self.program)
self.exe = find_executable(self.program)
self.environment = environment
if self.exe is None:
warnings.warn(
f'{self.program} not installed or not available in PATH',
stacklevel=1,
raise errors.InitializationError(
'{} not installed or not available in PATH'.format(
self.program
)
)
def get(self, server):
""" Retrieve credentials for `server`. If no credentials are found,
a `StoreError` will be raised.
"""
if not isinstance(server, bytes):
if not isinstance(server, six.binary_type):
server = server.encode('utf-8')
data = self._execute('get', server)
result = json.loads(data.decode('utf-8'))
@ -38,7 +41,7 @@ class Store:
# raise CredentialsNotFound
if result['Username'] == '' and result['Secret'] == '':
raise errors.CredentialsNotFound(
f'No matching credentials in {self.program}'
'No matching credentials in {}'.format(self.program)
)
return result
@ -58,7 +61,7 @@ class Store:
""" Erase credentials for `server`. Raises a `StoreError` if an error
occurs.
"""
if not isinstance(server, bytes):
if not isinstance(server, six.binary_type):
server = server.encode('utf-8')
self._execute('erase', server)
@ -69,25 +72,36 @@ class Store:
return json.loads(data.decode('utf-8'))
def _execute(self, subcmd, data_input):
if self.exe is None:
raise errors.StoreError(
f'{self.program} not installed or not available in PATH'
)
output = None
env = create_environment_dict(self.environment)
try:
output = subprocess.check_output(
[self.exe, subcmd], input=data_input, env=env,
)
if six.PY3:
output = subprocess.check_output(
[self.exe, subcmd], input=data_input, env=env,
)
else:
process = subprocess.Popen(
[self.exe, subcmd], stdin=subprocess.PIPE,
stdout=subprocess.PIPE, env=env,
)
output, _ = process.communicate(data_input)
if process.returncode != 0:
raise subprocess.CalledProcessError(
returncode=process.returncode, cmd='', output=output
)
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:
if e.errno == errno.ENOENT:
raise errors.StoreError(
f'{self.program} not installed or not available in PATH'
) from e
'{} not installed or not available in PATH'.format(
self.program
)
)
else:
raise errors.StoreError(
f'Unexpected OS error "{e.strerror}", errno={e.errno}'
) from e
'Unexpected OS error "{}", errno={}'.format(
e.strerror, e.errno
)
)
return output

View File

@ -1,4 +1,32 @@
import distutils.spawn
import os
import sys
def find_executable(executable, path=None):
"""
As distutils.spawn.find_executable, but on Windows, look up
every extension declared in PATHEXT instead of just `.exe`
"""
if sys.platform != 'win32':
return distutils.spawn.find_executable(executable, path)
if path is None:
path = os.environ['PATH']
paths = path.split(os.pathsep)
extensions = os.environ.get('PATHEXT', '.exe').split(os.pathsep)
base, ext = os.path.splitext(executable)
if not os.path.isfile(executable):
for p in paths:
for ext in extensions:
f = os.path.join(p, base + ext)
if os.path.isfile(f):
return f
return None
else:
return executable
def create_environment_dict(overrides):

View File

@ -1,14 +1,5 @@
import requests
_image_not_found_explanation_fragments = frozenset(
fragment.lower() for fragment in [
'no such image',
'not found: does not exist or no pull access',
'repository does not exist',
'was found but does not match the specified platform',
]
)
class DockerException(Exception):
"""
@ -27,16 +18,17 @@ def create_api_error_from_http_exception(e):
try:
explanation = response.json()['message']
except ValueError:
explanation = (response.text or '').strip()
explanation = (response.content or '').strip()
cls = APIError
if response.status_code == 404:
explanation_msg = (explanation or '').lower()
if any(fragment in explanation_msg
for fragment in _image_not_found_explanation_fragments):
if explanation and ('No such image' in str(explanation) or
'not found: does not exist or no pull access'
in str(explanation) or
'repository does not exist' in str(explanation)):
cls = ImageNotFound
else:
cls = NotFound
raise cls(e, response=response, explanation=explanation) from e
raise cls(e, response=response, explanation=explanation)
class APIError(requests.exceptions.HTTPError, DockerException):
@ -46,27 +38,25 @@ class APIError(requests.exceptions.HTTPError, DockerException):
def __init__(self, message, response=None, explanation=None):
# requests 1.2 supports response as a keyword argument, but
# requests 1.1 doesn't
super().__init__(message)
super(APIError, self).__init__(message)
self.response = response
self.explanation = explanation
def __str__(self):
message = super().__str__()
message = super(APIError, self).__str__()
if self.is_client_error():
message = (
f'{self.response.status_code} Client Error for '
f'{self.response.url}: {self.response.reason}'
)
message = '{0} Client Error for {1}: {2}'.format(
self.response.status_code, self.response.url,
self.response.reason)
elif self.is_server_error():
message = (
f'{self.response.status_code} Server Error for '
f'{self.response.url}: {self.response.reason}'
)
message = '{0} Server Error for {1}: {2}'.format(
self.response.status_code, self.response.url,
self.response.reason)
if self.explanation:
message = f'{message} ("{self.explanation}")'
message = '{0} ("{1}")'.format(message, self.explanation)
return message
@ -143,11 +133,11 @@ class ContainerError(DockerException):
self.image = image
self.stderr = stderr
err = f": {stderr}" if stderr is not None else ""
super().__init__(
f"Command '{command}' in image '{image}' "
f"returned non-zero exit status {exit_status}{err}"
)
err = ": {}".format(stderr) if stderr is not None else ""
msg = ("Command '{}' in image '{}' returned non-zero exit "
"status {}{}").format(command, image, exit_status, err)
super(ContainerError, self).__init__(msg)
class StreamParseError(RuntimeError):
@ -157,7 +147,7 @@ class StreamParseError(RuntimeError):
class BuildError(DockerException):
def __init__(self, reason, build_log):
super().__init__(reason)
super(BuildError, self).__init__(reason)
self.msg = reason
self.build_log = build_log
@ -167,8 +157,8 @@ class ImageLoadError(DockerException):
def create_unexpected_kwargs_error(name, kwargs):
quoted_kwargs = [f"'{k}'" for k in sorted(kwargs)]
text = [f"{name}() "]
quoted_kwargs = ["'{}'".format(k) for k in sorted(kwargs)]
text = ["{}() ".format(name)]
if len(quoted_kwargs) == 1:
text.append("got an unexpected keyword argument ")
else:
@ -182,7 +172,7 @@ class MissingContextParameter(DockerException):
self.param = param
def __str__(self):
return (f"missing parameter: {self.param}")
return ("missing parameter: {}".format(self.param))
class ContextAlreadyExists(DockerException):
@ -190,7 +180,7 @@ class ContextAlreadyExists(DockerException):
self.name = name
def __str__(self):
return (f"context {self.name} already exists")
return ("context {} already exists".format(self.name))
class ContextException(DockerException):
@ -206,4 +196,4 @@ class ContextNotFound(DockerException):
self.name = name
def __str__(self):
return (f"context '{self.name}' not found")
return ("context '{}' not found".format(self.name))

View File

@ -1,5 +1,5 @@
from ..api import APIClient
from .resource import Collection, Model
from .resource import Model, Collection
class Config(Model):
@ -7,7 +7,7 @@ class Config(Model):
id_attribute = 'ID'
def __repr__(self):
return f"<{self.__class__.__name__}: '{self.name}'>"
return "<%s: '%s'>" % (self.__class__.__name__, self.name)
@property
def name(self):
@ -30,7 +30,6 @@ class ConfigCollection(Collection):
def create(self, **kwargs):
obj = self.client.api.create_config(**kwargs)
obj.setdefault("Spec", {})["Name"] = kwargs.get("name")
return self.prepare_model(obj)
create.__doc__ = APIClient.create_config.__doc__

View File

@ -5,13 +5,10 @@ from collections import namedtuple
from ..api import APIClient
from ..constants import DEFAULT_DATA_CHUNK_SIZE
from ..errors import (
ContainerError,
DockerException,
ImageNotFound,
NotFound,
create_unexpected_kwargs_error,
ContainerError, DockerException, ImageNotFound,
NotFound, create_unexpected_kwargs_error
)
from ..types import HostConfig, NetworkingConfig
from ..types import HostConfig
from ..utils import version_gte
from .images import Image
from .resource import Collection, Model
@ -24,7 +21,6 @@ class Container(Model):
query the Docker daemon for the current properties, causing
:py:attr:`attrs` to be refreshed.
"""
@property
def name(self):
"""
@ -51,11 +47,11 @@ class Container(Model):
try:
result = self.attrs['Config'].get('Labels')
return result or {}
except KeyError as ke:
except KeyError:
raise DockerException(
'Label data is not available for sparse objects. Call reload()'
' to retrieve all information'
) from ke
)
@property
def status(self):
@ -66,15 +62,6 @@ class Container(Model):
return self.attrs['State']['Status']
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
def ports(self):
"""
@ -134,7 +121,6 @@ class Container(Model):
tag (str): The tag to push
message (str): A commit message
author (str): The name of the author
pause (bool): Whether to pause the container before committing
changes (str): Dockerfile instructions to apply while committing
conf (dict): The configuration for the container. See the
`Engine API documentation
@ -155,8 +141,7 @@ class Container(Model):
Inspect changes on a container's filesystem.
Returns:
(list) A list of dictionaries containing the attributes `Path`
and `Kind`.
(str)
Raises:
:py:class:`docker.errors.APIError`
@ -181,8 +166,7 @@ class Container(Model):
user (str): User to execute command as. Default: root
detach (bool): If true, detach from the exec command.
Default: False
stream (bool): Stream response data. Ignored if ``detach`` is true.
Default: False
stream (bool): Stream response data. Default: False
socket (bool): Return the connection socket to allow custom
read/write operations. Default: False
environment (dict or list): A dictionary or a list of strings in
@ -306,15 +290,14 @@ class Container(Model):
tail (str or int): Output specified number of lines at the end of
logs. Either an integer of number of lines or the string
``all``. Default ``all``
since (datetime, int, or float): Show logs since a given datetime,
integer epoch (in seconds) or float (in nanoseconds)
since (datetime or int): Show logs since a given datetime or
integer epoch (in seconds)
follow (bool): Follow log output. Default ``False``
until (datetime, int, or float): Show logs that occurred before
the given datetime, integer epoch (in seconds), or
float (in nanoseconds)
until (datetime or int): Show logs that occurred before the given
datetime or integer epoch (in seconds)
Returns:
(generator of bytes or bytes): Logs from the container.
(generator or str): Logs from the container.
Raises:
:py:class:`docker.errors.APIError`
@ -340,7 +323,7 @@ class Container(Model):
Args:
path (str): Path inside the container where the file(s) will be
extracted. Must exist.
data (bytes or stream): tar data to be extracted
data (bytes): tar data to be extracted
Returns:
(bool): True if the call succeeds.
@ -570,11 +553,6 @@ class ContainerCollection(Collection):
``["SYS_ADMIN", "MKNOD"]``.
cap_drop (list of str): Drop kernel capabilities.
cgroup_parent (str): Override the default parent cgroup.
cgroupns (str): Override the default cgroup namespace mode for the
container. One of:
- ``private`` the container runs in its own private cgroup
namespace.
- ``host`` use the host system's cgroup namespace.
cpu_count (int): Number of usable CPUs (Windows only).
cpu_percent (int): Usable percentage of the available CPUs
(Windows only).
@ -622,28 +600,7 @@ class ContainerCollection(Collection):
group_add (:py:class:`list`): List of additional group names and/or
IDs that the container process will run as.
healthcheck (dict): Specify a test to perform to check that the
container is healthy. The dict takes the following keys:
- test (:py:class:`list` or str): Test to perform to determine
container health. Possible values:
- Empty list: Inherit healthcheck from parent image
- ``["NONE"]``: Disable healthcheck
- ``["CMD", args...]``: exec arguments directly.
- ``["CMD-SHELL", command]``: Run command in the system's
default shell.
If a string is provided, it will be used as a ``CMD-SHELL``
command.
- interval (int): The time to wait between checks in
nanoseconds. It should be 0 or at least 1000000 (1 ms).
- timeout (int): The time to wait before considering the check
to have hung. It should be 0 or at least 1000000 (1 ms).
- retries (int): The number of consecutive failures needed to
consider a container as unhealthy.
- start_period (int): Start period for the container to
initialize before starting health-retries countdown in
nanoseconds. It should be 0 or at least 1000000 (1 ms).
container is healthy.
hostname (str): Optional hostname for the container.
init (bool): Run an init inside the container that forwards
signals and reaps processes
@ -687,7 +644,7 @@ class ContainerCollection(Collection):
network_mode (str): One of:
- ``bridge`` Create a new network stack for the container on
the bridge network.
on the bridge network.
- ``none`` No networking for this container.
- ``container:<name|id>`` Reuse another container's network
stack.
@ -695,14 +652,6 @@ class ContainerCollection(Collection):
This mode is incompatible with ``ports``.
Incompatible with ``network``.
networking_config (Dict[str, EndpointConfig]):
Dictionary of EndpointConfig objects for each container network.
The key is the name of the network.
Defaults to ``None``.
Used in conjuction with ``network``.
Incompatible with ``network_mode``.
oom_kill_disable (bool): Whether to disable OOM killer.
oom_score_adj (int): An integer value containing the score given
to the container in order to tune OOM killer preferences.
@ -812,15 +761,6 @@ class ContainerCollection(Collection):
{'/home/user1/': {'bind': '/mnt/vol2', 'mode': 'rw'},
'/var/www': {'bind': '/mnt/vol1', 'mode': 'ro'}}
Or a list of strings which each one of its elements specifies a
mount volume.
For example:
.. code-block:: python
['/home/user1/:/mnt/vol2','/var/www:/mnt/vol1']
volumes_from (:py:class:`list`): List of container names or IDs to
get volumes from.
working_dir (str): Path to the working directory.
@ -852,7 +792,7 @@ class ContainerCollection(Collection):
image = image.id
stream = kwargs.pop('stream', False)
detach = kwargs.pop('detach', False)
platform = kwargs.get('platform', None)
platform = kwargs.pop('platform', None)
if detach and remove:
if version_gte(self.client.api._version, '1.25'):
@ -867,12 +807,6 @@ class ContainerCollection(Collection):
'together.'
)
if kwargs.get('networking_config') and not kwargs.get('network'):
raise RuntimeError(
'The option "networking_config" can not be used '
'without "network".'
)
try:
container = self.create(image=image, command=command,
detach=detach, **kwargs)
@ -907,9 +841,9 @@ class ContainerCollection(Collection):
container, exit_status, command, image, out
)
if stream or out is None:
return out
return b''.join(out)
return out if stream or out is None else b''.join(
[line for line in out]
)
def create(self, image, command=None, **kwargs):
"""
@ -1025,7 +959,6 @@ class ContainerCollection(Collection):
def prune(self, filters=None):
return self.client.api.prune_containers(filters=filters)
prune.__doc__ = APIClient.prune_containers.__doc__
@ -1043,7 +976,6 @@ RUN_CREATE_KWARGS = [
'mac_address',
'name',
'network_disabled',
'platform',
'stdin_open',
'stop_signal',
'tty',
@ -1060,7 +992,6 @@ RUN_HOST_CONFIG_KWARGS = [
'cap_add',
'cap_drop',
'cgroup_parent',
'cgroupns',
'cpu_count',
'cpu_percent',
'cpu_period',
@ -1144,17 +1075,8 @@ def _create_container_args(kwargs):
host_config_kwargs['binds'] = volumes
network = kwargs.pop('network', None)
networking_config = kwargs.pop('networking_config', None)
if network:
if networking_config:
# Sanity check: check if the network is defined in the
# networking config dict, otherwise switch to None
if network not in networking_config:
networking_config = None
create_kwargs['networking_config'] = NetworkingConfig(
networking_config
) if networking_config else {network: None}
create_kwargs['networking_config'] = {network: None}
host_config_kwargs['network_mode'] = network
# All kwargs should have been consumed by this point, so raise
@ -1187,10 +1109,8 @@ def _host_volume_from_bind(bind):
bits = rest.split(':', 1)
if len(bits) == 1 or bits[1] in ('ro', 'rw'):
return drive + bits[0]
elif bits[1].endswith(':ro') or bits[1].endswith(':rw'):
return bits[1][:-3]
else:
return bits[1]
return bits[1].rstrip(':ro').rstrip(':rw')
ExecResult = namedtuple('ExecResult', 'exit_code,output')

View File

@ -2,6 +2,8 @@ import itertools
import re
import warnings
import six
from ..api import APIClient
from ..constants import DEFAULT_DATA_CHUNK_SIZE
from ..errors import BuildError, ImageLoadError, InvalidArgument
@ -15,8 +17,7 @@ class Image(Model):
An image on the server.
"""
def __repr__(self):
tag_str = "', '".join(self.tags)
return f"<{self.__class__.__name__}: '{tag_str}'>"
return "<%s: '%s'>" % (self.__class__.__name__, "', '".join(self.tags))
@property
def labels(self):
@ -29,12 +30,12 @@ class Image(Model):
@property
def short_id(self):
"""
The ID of the image truncated to 12 characters, plus the ``sha256:``
The ID of the image truncated to 10 characters, plus the ``sha256:``
prefix.
"""
if self.id.startswith('sha256:'):
return self.id[:19]
return self.id[:12]
return self.id[:17]
return self.id[:10]
@property
def tags(self):
@ -51,7 +52,7 @@ class Image(Model):
Show the history of an image.
Returns:
(list): The history of the image.
(str): The history of the image.
Raises:
:py:class:`docker.errors.APIError`
@ -59,24 +60,6 @@ class Image(Model):
"""
return self.client.api.history(self.id)
def remove(self, force=False, noprune=False):
"""
Remove this image.
Args:
force (bool): Force removal of the image
noprune (bool): Do not delete untagged parents
Raises:
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
return self.client.api.remove_image(
self.id,
force=force,
noprune=noprune,
)
def save(self, chunk_size=DEFAULT_DATA_CHUNK_SIZE, named=False):
"""
Get a tarball of an image. Similar to the ``docker save`` command.
@ -101,19 +84,19 @@ class Image(Model):
Example:
>>> image = cli.images.get("busybox:latest")
>>> image = cli.get_image("busybox:latest")
>>> f = open('/tmp/busybox-latest.tar', 'wb')
>>> for chunk in image.save():
>>> for chunk in image:
>>> f.write(chunk)
>>> f.close()
"""
img = self.id
if named:
img = self.tags[0] if self.tags else img
if isinstance(named, str):
if isinstance(named, six.string_types):
if named not in self.tags:
raise InvalidArgument(
f"{named} is not a valid tag for this image"
"{} is not a valid tag for this image".format(named)
)
img = named
@ -144,7 +127,7 @@ class RegistryData(Model):
Image metadata stored on the registry, including available platforms.
"""
def __init__(self, image_name, *args, **kwargs):
super().__init__(*args, **kwargs)
super(RegistryData, self).__init__(*args, **kwargs)
self.image_name = image_name
@property
@ -157,10 +140,10 @@ class RegistryData(Model):
@property
def short_id(self):
"""
The ID of the image truncated to 12 characters, plus the ``sha256:``
The ID of the image truncated to 10 characters, plus the ``sha256:``
prefix.
"""
return self.id[:19]
return self.id[:17]
def pull(self, platform=None):
"""
@ -197,7 +180,7 @@ class RegistryData(Model):
parts = platform.split('/')
if len(parts) > 3 or len(parts) < 1:
raise InvalidArgument(
f'"{platform}" is not a valid platform descriptor'
'"{0}" is not a valid platform descriptor'.format(platform)
)
platform = {'os': parts[0]}
if len(parts) > 2:
@ -222,10 +205,10 @@ class ImageCollection(Collection):
Build an image and return it. Similar to the ``docker build``
command. Either ``path`` or ``fileobj`` must be set.
If you already have a tar file for the Docker build context (including
a Dockerfile), pass a readable file-like object to ``fileobj``
and also pass ``custom_context=True``. If the stream is also
compressed, set ``encoding`` to the correct value (e.g ``gzip``).
If you have a tar file for the Docker build context (including a
Dockerfile) already, pass a readable file-like object to ``fileobj``
and also pass ``custom_context=True``. If the stream is compressed
also, set ``encoding`` to the correct value (e.g ``gzip``).
If you want to get the raw output of the build, use the
:py:meth:`~docker.api.build.BuildApiMixin.build` method in the
@ -282,7 +265,7 @@ class ImageCollection(Collection):
Returns:
(tuple): The first item is the :py:class:`Image` object for the
image that was built. The second item is a generator of the
image that was build. The second item is a generator of the
build logs as JSON-decoded objects.
Raises:
@ -294,7 +277,7 @@ class ImageCollection(Collection):
If neither ``path`` nor ``fileobj`` is specified.
"""
resp = self.client.api.build(**kwargs)
if isinstance(resp, str):
if isinstance(resp, six.string_types):
return self.get(resp)
last_event = None
image_id = None
@ -407,8 +390,8 @@ class ImageCollection(Collection):
if match:
image_id = match.group(2)
images.append(image_id)
if 'errorDetail' in chunk:
raise ImageLoadError(chunk['errorDetail']['message'])
if 'error' in chunk:
raise ImageLoadError(chunk['error'])
return [self.get(i) for i in images]
@ -456,8 +439,7 @@ class ImageCollection(Collection):
if 'stream' in kwargs:
warnings.warn(
'`stream` is not a valid parameter for this method'
' and will be overridden',
stacklevel=1,
' and will be overridden'
)
del kwargs['stream']
@ -470,8 +452,9 @@ class ImageCollection(Collection):
# to be pulled.
pass
if not all_tags:
sep = '@' if tag.startswith('sha256:') else ':'
return self.get(f'{repository}{sep}{tag}')
return self.get('{0}{2}{1}'.format(
repository, tag, '@' if tag.startswith('sha256:') else ':'
))
return self.list(repository)
def push(self, repository, tag=None, **kwargs):

View File

@ -1,7 +1,7 @@
from ..api import APIClient
from ..utils import version_gte
from .containers import Container
from .resource import Collection, Model
from .resource import Model, Collection
class Network(Model):
@ -184,7 +184,7 @@ class NetworkCollection(Collection):
def list(self, *args, **kwargs):
"""
List networks. Similar to the ``docker network ls`` command.
List networks. Similar to the ``docker networks ls`` command.
Args:
names (:py:class:`list`): List of names to filter by.

View File

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

View File

@ -7,7 +7,7 @@ class Plugin(Model):
A plugin on the server.
"""
def __repr__(self):
return f"<{self.__class__.__name__}: '{self.name}'>"
return "<%s: '%s'>" % (self.__class__.__name__, self.name)
@property
def name(self):
@ -44,19 +44,16 @@ class Plugin(Model):
self.client.api.configure_plugin(self.name, options)
self.reload()
def disable(self, force=False):
def disable(self):
"""
Disable the plugin.
Args:
force (bool): Force disable. Default: False
Raises:
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
self.client.api.disable_plugin(self.name, force)
self.client.api.disable_plugin(self.name)
self.reload()
def enable(self, timeout=0):
@ -120,11 +117,8 @@ class Plugin(Model):
if remote is None:
remote = self.name
privileges = self.client.api.plugin_privileges(remote)
yield from self.client.api.upgrade_plugin(
self.name,
remote,
privileges,
)
for d in self.client.api.upgrade_plugin(self.name, remote, privileges):
yield d
self.reload()
@ -187,7 +181,7 @@ class PluginCollection(Collection):
"""
privileges = self.client.api.plugin_privileges(remote_name)
it = self.client.api.pull_plugin(remote_name, privileges, local_name)
for _data in it:
for data in it:
pass
return self.get(local_name or remote_name)

View File

@ -1,4 +1,5 @@
class Model:
class Model(object):
"""
A base class for representing a single object on the server.
"""
@ -17,13 +18,13 @@ class Model:
self.attrs = {}
def __repr__(self):
return f"<{self.__class__.__name__}: {self.short_id}>"
return "<%s: %s>" % (self.__class__.__name__, self.short_id)
def __eq__(self, other):
return isinstance(other, self.__class__) and self.id == other.id
def __hash__(self):
return hash(f"{self.__class__.__name__}:{self.id}")
return hash("%s:%s" % (self.__class__.__name__, self.id))
@property
def id(self):
@ -35,9 +36,9 @@ class Model:
@property
def short_id(self):
"""
The ID of the object, truncated to 12 characters.
The ID of the object, truncated to 10 characters.
"""
return self.id[:12]
return self.id[:10]
def reload(self):
"""
@ -48,7 +49,7 @@ class Model:
self.attrs = new_model.attrs
class Collection:
class Collection(object):
"""
A base class for representing all objects of a particular type on the
server.
@ -64,10 +65,9 @@ class Collection:
def __call__(self, *args, **kwargs):
raise TypeError(
f"'{self.__class__.__name__}' object is not callable. "
"You might be trying to use the old (pre-2.0) API - "
"use docker.APIClient if so."
)
"'{}' object is not callable. You might be trying to use the old "
"(pre-2.0) API - use docker.APIClient if so."
.format(self.__class__.__name__))
def list(self):
raise NotImplementedError
@ -89,4 +89,5 @@ class Collection:
elif isinstance(attrs, dict):
return self.model(attrs=attrs, client=self.client, collection=self)
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 .resource import Collection, Model
from .resource import Model, Collection
class Secret(Model):
@ -7,7 +7,7 @@ class Secret(Model):
id_attribute = 'ID'
def __repr__(self):
return f"<{self.__class__.__name__}: '{self.name}'>"
return "<%s: '%s'>" % (self.__class__.__name__, self.name)
@property
def name(self):
@ -30,7 +30,6 @@ class SecretCollection(Collection):
def create(self, **kwargs):
obj = self.client.api.create_secret(**kwargs)
obj.setdefault("Spec", {})["Name"] = kwargs.get("name")
return self.prepare_model(obj)
create.__doc__ = APIClient.create_secret.__doc__

View File

@ -1,9 +1,7 @@
import copy
from docker.errors import InvalidArgument, create_unexpected_kwargs_error
from docker.types import ContainerSpec, Placement, ServiceMode, TaskTemplate
from .resource import Collection, Model
from docker.errors import create_unexpected_kwargs_error, InvalidArgument
from docker.types import TaskTemplate, ContainerSpec, Placement, ServiceMode
from .resource import Model, Collection
class Service(Model):
@ -215,12 +213,6 @@ class ServiceCollection(Collection):
to the service.
privileges (Privileges): Security options for the service's
containers.
cap_add (:py:class:`list`): A list of kernel capabilities to add to
the default set for the container.
cap_drop (:py:class:`list`): A list of kernel capabilities to drop
from the default set for the container.
sysctls (:py:class:`dict`): A dict of sysctl values to add to the
container
Returns:
:py:class:`Service`: The created service.
@ -268,8 +260,6 @@ class ServiceCollection(Collection):
filters (dict): Filters to process on the nodes list. Valid
filters: ``id``, ``name`` , ``label`` and ``mode``.
Default: ``None``.
status (bool): Include the service task count of running and
desired tasks. Default: ``None``.
Returns:
list of :py:class:`Service`: The services.
@ -287,8 +277,6 @@ class ServiceCollection(Collection):
# kwargs to copy straight over to ContainerSpec
CONTAINER_SPEC_KWARGS = [
'args',
'cap_add',
'cap_drop',
'command',
'configs',
'dns_config',
@ -311,7 +299,6 @@ CONTAINER_SPEC_KWARGS = [
'tty',
'user',
'workdir',
'sysctls',
]
# kwargs to copy straight over to TaskTemplate
@ -327,7 +314,6 @@ CREATE_SERVICE_KWARGS = [
'labels',
'mode',
'update_config',
'rollback_config',
'endpoint_spec',
]

View File

@ -1,6 +1,5 @@
from docker.api import APIClient
from docker.errors import APIError
from .resource import Model
@ -12,7 +11,7 @@ class Swarm(Model):
id_attribute = 'ID'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
super(Swarm, self).__init__(*args, **kwargs)
if self.client:
try:
self.reload()
@ -36,8 +35,7 @@ class Swarm(Model):
def init(self, advertise_addr=None, listen_addr='0.0.0.0:2377',
force_new_cluster=False, default_addr_pool=None,
subnet_size=None, data_path_addr=None, data_path_port=None,
**kwargs):
subnet_size=None, data_path_addr=None, **kwargs):
"""
Initialize a new swarm on this Engine.
@ -67,9 +65,6 @@ class Swarm(Model):
networks created from the default subnet pool. Default: None
data_path_addr (string): Address or interface to use for data path
traffic. For example, 192.168.1.1, or an interface, like eth0.
data_path_port (int): Port number to use for data path traffic.
Acceptable port range is 1024 to 49151. If set to ``None`` or
0, the default port 4789 will be used. Default: None
task_history_retention_limit (int): Maximum number of tasks
history stored.
snapshot_interval (int): Number of logs entries between snapshot.
@ -126,7 +121,6 @@ class Swarm(Model):
'default_addr_pool': default_addr_pool,
'subnet_size': subnet_size,
'data_path_addr': data_path_addr,
'data_path_port': data_path_port,
}
init_kwargs['swarm_spec'] = self.client.api.create_swarm_spec(**kwargs)
node_id = self.client.api.init_swarm(**init_kwargs)

View File

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

View File

@ -1,30 +1,67 @@
import os
import ssl
from . import errors
from .transport import SSLHTTPAdapter
class TLSConfig:
class TLSConfig(object):
"""
TLS configuration.
Args:
client_cert (tuple of str): Path to client cert, path to client key.
ca_cert (str): Path to CA cert file.
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;
if ``False`` or not specified, do not verify.
verify (bool or str): This can be ``False`` or a path to a CA cert
file.
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
ca_cert = 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
# https://docs.docker.com/engine/articles/https/
# This diverges from the Docker CLI in that users can specify 'tls'
# here, but also disable any public/default CA pool verification by
# leaving verify=False
self.assert_hostname = assert_hostname
self.assert_fingerprint = assert_fingerprint
# TODO(dperny): according to the python docs, PROTOCOL_TLSvWhatever is
# depcreated, and it's recommended to use OPT_NO_TLSvWhatever instead
# to exclude versions. But I think that might require a bigger
# architectural change, so I've opted not to pursue it at this time
# If the user provides an SSL version, we should use their preference
if ssl_version:
self.ssl_version = ssl_version
else:
# If the user provides no ssl version, we should default to
# TLSv1_2. This option is the most secure, and will work for the
# majority of users with reasonably up-to-date software. However,
# before doing so, detect openssl version to ensure we can support
# it.
if ssl.OPENSSL_VERSION_INFO[:3] >= (1, 0, 1) and hasattr(
ssl, 'PROTOCOL_TLSv1_2'):
# If the OpenSSL version is high enough to support TLSv1_2,
# then we should use it.
self.ssl_version = getattr(ssl, 'PROTOCOL_TLSv1_2')
else:
# Otherwise, TLS v1.0 seems to be the safest default;
# SSLv23 fails in mysterious ways:
# https://github.com/docker/docker-py/issues/963
self.ssl_version = ssl.PROTOCOL_TLSv1
# "client_cert" must have both or neither cert/key files. In
# either case, Alert the user when both are expected, but any are
# missing.
@ -36,7 +73,7 @@ class TLSConfig:
raise errors.TLSParameterError(
'client_cert must be a tuple of'
' (client certificate, key file)'
) from None
)
if not (tls_cert and tls_key) or (not os.path.isfile(tls_cert) or
not os.path.isfile(tls_key)):
@ -58,6 +95,8 @@ class TLSConfig:
"""
Configure a client with these TLS options.
"""
client.ssl_version = self.ssl_version
if self.verify and self.ca_cert:
client.verify = self.ca_cert
else:
@ -65,3 +104,9 @@ class TLSConfig:
if 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 .ssladapter import SSLHTTPAdapter
try:
from .npipeconn import NpipeHTTPAdapter
from .npipesocket import NpipeSocket

View File

@ -3,11 +3,6 @@ import requests.adapters
class BaseHTTPAdapter(requests.adapters.HTTPAdapter):
def close(self):
super().close()
super(BaseHTTPAdapter, self).close()
if hasattr(self, 'pools'):
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,19 +1,26 @@
import queue
import six
import requests.adapters
import urllib3
import urllib3.connection
from docker.transport.basehttpadapter import BaseHTTPAdapter
from .. import constants
from .basehttpadapter import BaseHTTPAdapter
from .npipesocket import NpipeSocket
if six.PY3:
import http.client as httplib
else:
import httplib
try:
import requests.packages.urllib3 as urllib3
except ImportError:
import urllib3
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
class NpipeHTTPConnection(urllib3.connection.HTTPConnection):
class NpipeHTTPConnection(httplib.HTTPConnection, object):
def __init__(self, npipe_path, timeout=60):
super().__init__(
super(NpipeHTTPConnection, self).__init__(
'localhost', timeout=timeout
)
self.npipe_path = npipe_path
@ -28,7 +35,7 @@ class NpipeHTTPConnection(urllib3.connection.HTTPConnection):
class NpipeHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
def __init__(self, npipe_path, timeout=60, maxsize=10):
super().__init__(
super(NpipeHTTPConnectionPool, self).__init__(
'localhost', timeout=timeout, maxsize=maxsize
)
self.npipe_path = npipe_path
@ -46,17 +53,18 @@ class NpipeHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
conn = None
try:
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 queue.Empty:
except AttributeError: # self.pool is None
raise urllib3.exceptions.ClosedPoolError(self, "Pool is closed.")
except six.moves.queue.Empty:
if self.block:
raise urllib3.exceptions.EmptyPoolError(
self,
"Pool reached maximum size and no more "
"connections are allowed."
) from None
# Oh well, we'll create a new connection then
)
pass # Oh well, we'll create a new connection then
return conn or self._new_conn()
@ -77,7 +85,7 @@ class NpipeHTTPAdapter(BaseHTTPAdapter):
self.pools = RecentlyUsedContainer(
pool_connections, dispose_func=lambda p: p.close()
)
super().__init__()
super(NpipeHTTPAdapter, self).__init__()
def get_connection(self, url, proxies=None):
with self.pools.lock:

View File

@ -1,10 +1,8 @@
import functools
import io
import time
import io
import pywintypes
import win32api
import win32event
import six
import win32file
import win32pipe
@ -26,7 +24,7 @@ def check_closed(f):
return wrapped
class NpipeSocket:
class NpipeSocket(object):
""" Partial implementation of the socket API over windows named pipes.
This implementation is only designed to be used as a client socket,
and server-specific methods (bind, listen, accept...) are not
@ -57,9 +55,7 @@ class NpipeSocket:
0,
None,
win32file.OPEN_EXISTING,
(cSECURITY_ANONYMOUS
| cSECURITY_SQOS_PRESENT
| win32file.FILE_FLAG_OVERLAPPED),
cSECURITY_ANONYMOUS | cSECURITY_SQOS_PRESENT,
0
)
except win32pipe.error as e:
@ -132,41 +128,29 @@ class NpipeSocket:
@check_closed
def recv_into(self, buf, nbytes=0):
if six.PY2:
return self._recv_into_py2(buf, nbytes)
readbuf = buf
if not isinstance(buf, memoryview):
readbuf = memoryview(buf)
event = win32event.CreateEvent(None, True, True, None)
try:
overlapped = pywintypes.OVERLAPPED()
overlapped.hEvent = event
err, data = win32file.ReadFile(
self._handle,
readbuf[:nbytes] if nbytes else readbuf,
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)
err, data = win32file.ReadFile(
self._handle,
readbuf[:nbytes] if nbytes else readbuf
)
return len(data)
def _recv_into_py2(self, buf, nbytes):
err, data = win32file.ReadFile(self._handle, nbytes or len(buf))
n = len(data)
buf[:n] = data
return n
@check_closed
def send(self, string, flags=0):
event = win32event.CreateEvent(None, True, True, None)
try:
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)
err, nbytes = win32file.WriteFile(self._handle, string)
return nbytes
@check_closed
def sendall(self, string, flags=0):
@ -185,12 +169,15 @@ class NpipeSocket:
def settimeout(self, value):
if value is None:
# Blocking mode
self._timeout = win32event.INFINITE
self._timeout = win32pipe.NMPWAIT_WAIT_FOREVER
elif not isinstance(value, (float, int)) or value < 0:
raise ValueError('Timeout value out of range')
elif value == 0:
# Non-blocking mode
self._timeout = win32pipe.NMPWAIT_NO_WAIT
else:
# Timeout mode - Value converted to milliseconds
self._timeout = int(value * 1000)
self._timeout = value * 1000
def gettimeout(self):
return self._timeout
@ -208,7 +195,7 @@ class NpipeFileIOBase(io.RawIOBase):
self.sock = npipe_socket
def close(self):
super().close()
super(NpipeFileIOBase, self).close()
self.sock = None
def fileno(self):

View File

@ -1,25 +1,31 @@
import paramiko
import requests.adapters
import six
import logging
import os
import queue
import signal
import socket
import subprocess
import urllib.parse
import paramiko
import requests.adapters
import urllib3
import urllib3.connection
from docker.transport.basehttpadapter import BaseHTTPAdapter
from .. import constants
from .basehttpadapter import BaseHTTPAdapter
if six.PY3:
import http.client as httplib
else:
import httplib
try:
import requests.packages.urllib3 as urllib3
except ImportError:
import urllib3
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
class SSHSocket(socket.socket):
def __init__(self, host):
super().__init__(
super(SSHSocket, self).__init__(
socket.AF_INET, socket.SOCK_STREAM)
self.host = host
self.port = None
@ -47,15 +53,10 @@ class SSHSocket(socket.socket):
signal.signal(signal.SIGINT, signal.SIG_IGN)
preexec_func = f
env = dict(os.environ)
# drop LD_LIBRARY_PATH and SSL_CERT_FILE
env.pop('LD_LIBRARY_PATH', None)
env.pop('SSL_CERT_FILE', None)
self.proc = subprocess.Popen(
args,
env=env,
' '.join(args),
env=os.environ,
shell=True,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
preexec_fn=preexec_func)
@ -83,7 +84,8 @@ class SSHSocket(socket.socket):
def makefile(self, mode):
if not self.proc:
self.connect()
self.proc.stdout.channel = self
if six.PY3:
self.proc.stdout.channel = self
return self.proc.stdout
@ -95,9 +97,9 @@ class SSHSocket(socket.socket):
self.proc.terminate()
class SSHConnection(urllib3.connection.HTTPConnection):
class SSHConnection(httplib.HTTPConnection, object):
def __init__(self, ssh_transport=None, timeout=60, host=None):
super().__init__(
super(SSHConnection, self).__init__(
'localhost', timeout=timeout
)
self.ssh_transport = ssh_transport
@ -121,7 +123,7 @@ class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
scheme = 'ssh'
def __init__(self, ssh_client=None, timeout=60, maxsize=10, host=None):
super().__init__(
super(SSHConnectionPool, self).__init__(
'localhost', timeout=timeout, maxsize=maxsize
)
self.ssh_transport = None
@ -141,17 +143,17 @@ class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
try:
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 six.moves.queue.Empty:
if self.block:
raise urllib3.exceptions.EmptyPoolError(
self,
"Pool reached maximum size and no more "
"connections are allowed."
) from None
# Oh well, we'll create a new connection then
)
pass # Oh well, we'll create a new connection then
return conn or self._new_conn()
@ -180,12 +182,12 @@ class SSHHTTPAdapter(BaseHTTPAdapter):
self.pools = RecentlyUsedContainer(
pool_connections, dispose_func=lambda p: p.close()
)
super().__init__()
super(SSHHTTPAdapter, self).__init__()
def _create_paramiko_client(self, base_url):
logging.getLogger("paramiko").setLevel(logging.WARNING)
self.ssh_client = paramiko.SSHClient()
base_url = urllib.parse.urlparse(base_url)
base_url = six.moves.urllib_parse.urlparse(base_url)
self.ssh_params = {
"hostname": base_url.hostname,
"port": base_url.port,
@ -197,21 +199,20 @@ class SSHHTTPAdapter(BaseHTTPAdapter):
with open(ssh_config_file) as f:
conf.parse(f)
host_config = conf.lookup(base_url.hostname)
self.ssh_conf = host_config
if 'proxycommand' in host_config:
self.ssh_params["sock"] = paramiko.ProxyCommand(
host_config['proxycommand']
self.ssh_conf['proxycommand']
)
if 'hostname' in host_config:
self.ssh_params['hostname'] = host_config['hostname']
if base_url.port is None and 'port' in host_config:
self.ssh_params['port'] = host_config['port']
self.ssh_params['port'] = self.ssh_conf['port']
if base_url.username is None and 'user' in host_config:
self.ssh_params['username'] = host_config['user']
if 'identityfile' in host_config:
self.ssh_params['key_filename'] = host_config['identityfile']
self.ssh_params['username'] = self.ssh_conf['user']
self.ssh_client.load_system_host_keys()
self.ssh_client.set_missing_host_key_policy(paramiko.RejectPolicy())
self.ssh_client.set_missing_host_key_policy(paramiko.WarningPolicy())
def _connect(self):
if self.ssh_client:
@ -245,6 +246,6 @@ class SSHHTTPAdapter(BaseHTTPAdapter):
return pool
def close(self):
super().close()
super(SSHHTTPAdapter, self).close()
if self.ssh_client:
self.ssh_client.close()

View File

@ -0,0 +1,73 @@
""" Resolves OpenSSL issues in some servers:
https://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/
https://github.com/kennethreitz/requests/pull/799
"""
import sys
from distutils.version import StrictVersion
from requests.adapters import HTTPAdapter
from docker.transport.basehttpadapter import BaseHTTPAdapter
try:
import requests.packages.urllib3 as urllib3
except ImportError:
import urllib3
PoolManager = urllib3.poolmanager.PoolManager
# Monkey-patching match_hostname with a version that supports
# IP-address checking. Not necessary for Python 3.5 and above
if sys.version_info[0] < 3 or sys.version_info[1] < 5:
from backports.ssl_match_hostname import match_hostname
urllib3.connection.match_hostname = match_hostname
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(SSLHTTPAdapter, self).__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(SSLHTTPAdapter, self).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 StrictVersion(urllib_ver) > StrictVersion('1.5')

View File

@ -1,24 +1,41 @@
import socket
import six
import requests.adapters
import urllib3
import urllib3.connection
import socket
from six.moves import http_client as httplib
from docker.transport.basehttpadapter import BaseHTTPAdapter
from .. import constants
from .basehttpadapter import BaseHTTPAdapter
try:
import requests.packages.urllib3 as urllib3
except ImportError:
import urllib3
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
class UnixHTTPConnection(urllib3.connection.HTTPConnection):
class UnixHTTPResponse(httplib.HTTPResponse, object):
def __init__(self, sock, *args, **kwargs):
disable_buffering = kwargs.pop('disable_buffering', False)
if six.PY2:
# FIXME: We may need to disable buffering on Py3 as well,
# but there's no clear way to do it at the moment. See:
# https://github.com/docker/docker-py/issues/1799
kwargs['buffering'] = not disable_buffering
super(UnixHTTPResponse, self).__init__(sock, *args, **kwargs)
class UnixHTTPConnection(httplib.HTTPConnection, object):
def __init__(self, base_url, unix_socket, timeout=60):
super().__init__(
super(UnixHTTPConnection, self).__init__(
'localhost', timeout=timeout
)
self.base_url = base_url
self.unix_socket = unix_socket
self.timeout = timeout
self.disable_buffering = False
def connect(self):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
@ -26,10 +43,21 @@ class UnixHTTPConnection(urllib3.connection.HTTPConnection):
sock.connect(self.unix_socket)
self.sock = sock
def putheader(self, header, *values):
super(UnixHTTPConnection, self).putheader(header, *values)
if header == 'Connection' and 'Upgrade' in values:
self.disable_buffering = True
def response_class(self, sock, *args, **kwargs):
if self.disable_buffering:
kwargs['disable_buffering'] = True
return UnixHTTPResponse(sock, *args, **kwargs)
class UnixHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
def __init__(self, base_url, socket_path, timeout=60, maxsize=10):
super().__init__(
super(UnixHTTPConnectionPool, self).__init__(
'localhost', timeout=timeout, maxsize=maxsize
)
self.base_url = base_url
@ -54,14 +82,14 @@ class UnixHTTPAdapter(BaseHTTPAdapter):
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE):
socket_path = socket_url.replace('http+unix://', '')
if not socket_path.startswith('/'):
socket_path = f"/{socket_path}"
socket_path = '/' + socket_path
self.socket_path = socket_path
self.timeout = timeout
self.max_pool_size = max_pool_size
self.pools = RecentlyUsedContainer(
pool_connections, dispose_func=lambda p: p.close()
)
super().__init__()
super(UnixHTTPAdapter, self).__init__()
def get_connection(self, url, proxies=None):
with self.pools.lock:

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 .healthcheck import Healthcheck
from .networks import EndpointConfig, IPAMConfig, IPAMPool, NetworkingConfig
from .services import (
ConfigReference,
ContainerSpec,
DNSConfig,
DriverConfig,
EndpointSpec,
Mount,
NetworkAttachmentConfig,
Placement,
PlacementPreference,
Privileges,
Resources,
RestartPolicy,
RollbackConfig,
SecretReference,
ServiceMode,
TaskTemplate,
UpdateConfig,
ConfigReference, ContainerSpec, DNSConfig, DriverConfig, EndpointSpec,
Mount, Placement, PlacementPreference, Privileges, Resources,
RestartPolicy, RollbackConfig, SecretReference, ServiceMode, TaskTemplate,
UpdateConfig, NetworkAttachmentConfig
)
from .swarm import SwarmExternalCA, SwarmSpec
from .swarm import SwarmSpec, SwarmExternalCA

View File

@ -1,4 +1,7 @@
import six
class DictType(dict):
def __init__(self, init):
for k, v in init.items():
for k, v in six.iteritems(init):
self[k] = v

View File

@ -1,22 +1,16 @@
import six
from .. import errors
from ..utils.utils import (
convert_port_bindings,
convert_tmpfs_mounts,
convert_volume_binds,
format_environment,
format_extra_hosts,
normalize_links,
parse_bytes,
parse_devices,
split_command,
version_gte,
version_lt,
convert_port_bindings, convert_tmpfs_mounts, convert_volume_binds,
format_environment, format_extra_hosts, normalize_links, parse_bytes,
parse_devices, split_command, version_gte, version_lt,
)
from .base import DictType
from .healthcheck import Healthcheck
class LogConfigTypesEnum:
class LogConfigTypesEnum(object):
_values = (
'json-file',
'syslog',
@ -56,11 +50,8 @@ class LogConfig(DictType):
>>> container = client.create_container('busybox', 'true',
... host_config=hc)
>>> client.inspect_container(container)['HostConfig']['LogConfig']
{
'Type': 'json-file',
'Config': {'labels': 'production_status,geo', 'max-size': '1g'}
}
"""
{'Type': 'json-file', 'Config': {'labels': 'production_status,geo', 'max-size': '1g'}}
""" # noqa: E501
types = LogConfigTypesEnum
def __init__(self, **kwargs):
@ -70,7 +61,7 @@ class LogConfig(DictType):
if config and not isinstance(config, dict):
raise ValueError("LogConfig.config must be a dictionary")
super().__init__({
super(LogConfig, self).__init__({
'Type': log_driver_type,
'Config': config
})
@ -126,13 +117,13 @@ class Ulimit(DictType):
name = kwargs.get('name', kwargs.get('Name'))
soft = kwargs.get('soft', kwargs.get('Soft'))
hard = kwargs.get('hard', kwargs.get('Hard'))
if not isinstance(name, str):
if not isinstance(name, six.string_types):
raise ValueError("Ulimit.name must be a string")
if soft and not isinstance(soft, int):
raise ValueError("Ulimit.soft must be an integer")
if hard and not isinstance(hard, int):
raise ValueError("Ulimit.hard must be an integer")
super().__init__({
super(Ulimit, self).__init__({
'Name': name,
'Soft': soft,
'Hard': hard
@ -193,7 +184,7 @@ class DeviceRequest(DictType):
if driver is None:
driver = ''
elif not isinstance(driver, str):
elif not isinstance(driver, six.string_types):
raise ValueError('DeviceRequest.driver must be a string')
if count is None:
count = 0
@ -212,7 +203,7 @@ class DeviceRequest(DictType):
elif not isinstance(options, dict):
raise ValueError('DeviceRequest.options must be a dict')
super().__init__({
super(DeviceRequest, self).__init__({
'Driver': driver,
'Count': count,
'DeviceIDs': device_ids,
@ -283,8 +274,7 @@ class HostConfig(dict):
volume_driver=None, cpu_count=None, cpu_percent=None,
nano_cpus=None, cpuset_mems=None, runtime=None, mounts=None,
cpu_rt_period=None, cpu_rt_runtime=None,
device_cgroup_rules=None, device_requests=None,
cgroupns=None):
device_cgroup_rules=None, device_requests=None):
if mem_limit is not None:
self['Memory'] = parse_bytes(mem_limit)
@ -307,7 +297,7 @@ class HostConfig(dict):
self['MemorySwappiness'] = mem_swappiness
if shm_size is not None:
if isinstance(shm_size, str):
if isinstance(shm_size, six.string_types):
shm_size = parse_bytes(shm_size)
self['ShmSize'] = shm_size
@ -368,7 +358,7 @@ class HostConfig(dict):
self['Devices'] = parse_devices(devices)
if group_add:
self['GroupAdd'] = [str(grp) for grp in group_add]
self['GroupAdd'] = [six.text_type(grp) for grp in group_add]
if dns is not None:
self['Dns'] = dns
@ -388,11 +378,11 @@ class HostConfig(dict):
if not isinstance(sysctls, dict):
raise host_config_type_error('sysctls', sysctls, 'dict')
self['Sysctls'] = {}
for k, v in sysctls.items():
self['Sysctls'][k] = str(v)
for k, v in six.iteritems(sysctls):
self['Sysctls'][k] = six.text_type(v)
if volumes_from is not None:
if isinstance(volumes_from, str):
if isinstance(volumes_from, six.string_types):
volumes_from = volumes_from.split(',')
self['VolumesFrom'] = volumes_from
@ -414,7 +404,7 @@ class HostConfig(dict):
if isinstance(lxc_conf, dict):
formatted = []
for k, v in lxc_conf.items():
for k, v in six.iteritems(lxc_conf):
formatted.append({'Key': k, 'Value': str(v)})
lxc_conf = formatted
@ -569,7 +559,7 @@ class HostConfig(dict):
self["PidsLimit"] = pids_limit
if isolation:
if not isinstance(isolation, str):
if not isinstance(isolation, six.string_types):
raise host_config_type_error('isolation', isolation, 'string')
if version_lt(version, '1.24'):
raise host_config_version_error('isolation', '1.24')
@ -619,7 +609,7 @@ class HostConfig(dict):
self['CpuPercent'] = cpu_percent
if nano_cpus:
if not isinstance(nano_cpus, int):
if not isinstance(nano_cpus, six.integer_types):
raise host_config_type_error('nano_cpus', nano_cpus, 'int')
if version_lt(version, '1.25'):
raise host_config_version_error('nano_cpus', '1.25')
@ -658,30 +648,27 @@ class HostConfig(dict):
req = DeviceRequest(**req)
self['DeviceRequests'].append(req)
if cgroupns:
self['CgroupnsMode'] = cgroupns
def host_config_type_error(param, param_value, expected):
return TypeError(
f'Invalid type for {param} param: expected {expected} '
f'but found {type(param_value)}'
)
error_msg = 'Invalid type for {0} param: expected {1} but found {2}'
return TypeError(error_msg.format(param, expected, type(param_value)))
def host_config_version_error(param, version, less_than=True):
operator = '<' if less_than else '>'
return errors.InvalidVersion(
f'{param} param is not supported in API versions {operator} {version}',
)
error_msg = '{0} param is not supported in API versions {1} {2}'
return errors.InvalidVersion(error_msg.format(param, operator, version))
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):
error_msg = '\"{1}\" {0} is incompatible with {2}'
return errors.InvalidArgument(
f'\"{param_value}\" {param} is incompatible with {incompatible_param}'
error_msg.format(param, param_value, incompatible_param)
)
@ -712,17 +699,17 @@ class ContainerConfig(dict):
'version 1.29'
)
if isinstance(command, str):
if isinstance(command, six.string_types):
command = split_command(command)
if isinstance(entrypoint, str):
if isinstance(entrypoint, six.string_types):
entrypoint = split_command(entrypoint)
if isinstance(environment, dict):
environment = format_environment(environment)
if isinstance(labels, list):
labels = {lbl: '' for lbl in labels}
labels = dict((lbl, six.text_type('')) for lbl in labels)
if isinstance(ports, list):
exposed_ports = {}
@ -733,10 +720,10 @@ class ContainerConfig(dict):
if len(port_definition) == 2:
proto = port_definition[1]
port = port_definition[0]
exposed_ports[f'{port}/{proto}'] = {}
exposed_ports['{0}/{1}'.format(port, proto)] = {}
ports = exposed_ports
if isinstance(volumes, str):
if isinstance(volumes, six.string_types):
volumes = [volumes, ]
if isinstance(volumes, list):
@ -765,7 +752,7 @@ class ContainerConfig(dict):
'Hostname': hostname,
'Domainname': domainname,
'ExposedPorts': ports,
'User': str(user) if user is not None else None,
'User': six.text_type(user) if user is not None else None,
'Tty': tty,
'OpenStdin': stdin_open,
'StdinOnce': stdin_once,

View File

@ -1,11 +1,14 @@
import socket
import urllib3
try:
import requests.packages.urllib3 as urllib3
except ImportError:
import urllib3
from ..errors import DockerException
class CancellableStream:
class CancellableStream(object):
"""
Stream wrapper for real-time events, logs, etc. from the server.
@ -28,9 +31,9 @@ class CancellableStream:
try:
return next(self._stream)
except urllib3.exceptions.ProtocolError:
raise StopIteration from None
except OSError:
raise StopIteration from None
raise StopIteration
except socket.error:
raise StopIteration
next = __next__

View File

@ -1,5 +1,7 @@
from .base import DictType
import six
class Healthcheck(DictType):
"""
@ -29,7 +31,7 @@ class Healthcheck(DictType):
"""
def __init__(self, **kwargs):
test = kwargs.get('test', kwargs.get('Test'))
if isinstance(test, str):
if isinstance(test, six.string_types):
test = ["CMD-SHELL", test]
interval = kwargs.get('interval', kwargs.get('Interval'))
@ -37,7 +39,7 @@ class Healthcheck(DictType):
retries = kwargs.get('retries', kwargs.get('Retries'))
start_period = kwargs.get('start_period', kwargs.get('StartPeriod'))
super().__init__({
super(Healthcheck, self).__init__({
'Test': test,
'Interval': interval,
'Timeout': timeout,
@ -51,7 +53,7 @@ class Healthcheck(DictType):
@test.setter
def test(self, value):
if isinstance(value, str):
if isinstance(value, six.string_types):
value = ["CMD-SHELL", value]
self['Test'] = value

View File

@ -4,8 +4,7 @@ from ..utils import normalize_links, version_lt
class EndpointConfig(dict):
def __init__(self, version, aliases=None, links=None, ipv4_address=None,
ipv6_address=None, link_local_ips=None, driver_opt=None,
mac_address=None):
ipv6_address=None, link_local_ips=None, driver_opt=None):
if version_lt(version, '1.22'):
raise errors.InvalidVersion(
'Endpoint config is not supported for API version < 1.22'
@ -24,13 +23,6 @@ class EndpointConfig(dict):
if ipv6_address:
ipam_config['IPv6Address'] = ipv6_address
if mac_address:
if version_lt(version, '1.25'):
raise errors.InvalidVersion(
'mac_address is not supported for API version < 1.25'
)
self['MacAddress'] = mac_address
if link_local_ips is not None:
if version_lt(version, '1.24'):
raise errors.InvalidVersion(

View File

@ -1,12 +1,10 @@
import six
from .. import errors
from ..constants import IS_WINDOWS_PLATFORM
from ..utils import (
check_resource,
convert_service_networks,
format_environment,
format_extra_hosts,
parse_bytes,
split_command,
check_resource, format_environment, format_extra_hosts, parse_bytes,
split_command, convert_service_networks,
)
@ -33,7 +31,6 @@ class TaskTemplate(dict):
force_update (int): A counter that triggers an update even if no
relevant parameters have been changed.
"""
def __init__(self, container_spec, resources=None, restart_policy=None,
placement=None, log_driver=None, networks=None,
force_update=None):
@ -115,24 +112,16 @@ class ContainerSpec(dict):
containers. Only used for Windows containers.
init (boolean): Run an init inside the container that forwards signals
and reaps processes.
cap_add (:py:class:`list`): A list of kernel capabilities to add to the
default set for the container.
cap_drop (:py:class:`list`): A list of kernel capabilities to drop from
the default set for the container.
sysctls (:py:class:`dict`): A dict of sysctl values to add to
the container
"""
def __init__(self, image, command=None, args=None, hostname=None, env=None,
workdir=None, user=None, labels=None, mounts=None,
stop_grace_period=None, secrets=None, tty=None, groups=None,
open_stdin=None, read_only=None, stop_signal=None,
healthcheck=None, hosts=None, dns_config=None, configs=None,
privileges=None, isolation=None, init=None, cap_add=None,
cap_drop=None, sysctls=None):
privileges=None, isolation=None, init=None):
self['Image'] = image
if isinstance(command, str):
if isinstance(command, six.string_types):
command = split_command(command)
self['Command'] = command
self['Args'] = args
@ -162,7 +151,7 @@ class ContainerSpec(dict):
if mounts is not None:
parsed_mounts = []
for mount in mounts:
if isinstance(mount, str):
if isinstance(mount, six.string_types):
parsed_mounts.append(Mount.parse_mount_string(mount))
else:
# If mount already parsed
@ -199,24 +188,6 @@ class ContainerSpec(dict):
if init is not None:
self['Init'] = init
if cap_add is not None:
if not isinstance(cap_add, list):
raise TypeError('cap_add must be a list')
self['CapabilityAdd'] = cap_add
if cap_drop is not None:
if not isinstance(cap_drop, list):
raise TypeError('cap_drop must be a list')
self['CapabilityDrop'] = cap_drop
if sysctls is not None:
if not isinstance(sysctls, dict):
raise TypeError('sysctls must be a dict')
self['Sysctls'] = sysctls
class Mount(dict):
"""
@ -242,20 +213,18 @@ class Mount(dict):
for the ``volume`` type.
driver_config (DriverConfig): Volume driver configuration. Only valid
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_mode (int): The permission mode for the tmpfs mount.
"""
def __init__(self, target, source, type='volume', read_only=False,
consistency=None, propagation=None, no_copy=False,
labels=None, driver_config=None, tmpfs_size=None,
tmpfs_mode=None, subpath=None):
tmpfs_mode=None):
self['Target'] = target
self['Source'] = source
if type not in ('bind', 'volume', 'tmpfs', 'npipe'):
raise errors.InvalidArgument(
f'Unsupported mount type: "{type}"'
'Unsupported mount type: "{}"'.format(type)
)
self['Type'] = type
self['ReadOnly'] = read_only
@ -268,7 +237,7 @@ class Mount(dict):
self['BindOptions'] = {
'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(
'Incompatible options have been provided for the bind '
'type mount.'
@ -281,8 +250,6 @@ class Mount(dict):
volume_opts['Labels'] = labels
if driver_config:
volume_opts['DriverConfig'] = driver_config
if subpath:
volume_opts['Subpath'] = subpath
if volume_opts:
self['VolumeOptions'] = volume_opts
if any([propagation, tmpfs_size, tmpfs_mode]):
@ -293,7 +260,7 @@ class Mount(dict):
elif type == 'tmpfs':
tmpfs_opts = {}
if tmpfs_mode:
if not isinstance(tmpfs_mode, int):
if not isinstance(tmpfs_mode, six.integer_types):
raise errors.InvalidArgument(
'tmpfs_mode must be an integer'
)
@ -313,7 +280,7 @@ class Mount(dict):
parts = string.split(':')
if len(parts) > 3:
raise errors.InvalidArgument(
f'Invalid mount format "{string}"'
'Invalid mount format "{0}"'.format(string)
)
if len(parts) == 1:
return cls(target=parts[0], source=None)
@ -349,7 +316,6 @@ class Resources(dict):
``{ resource_name: resource_value }``. Alternatively, a list of
of resource specifications as defined by the Engine API.
"""
def __init__(self, cpu_limit=None, mem_limit=None, cpu_reservation=None,
mem_reservation=None, generic_resources=None):
limits = {}
@ -377,20 +343,20 @@ def _convert_generic_resources_dict(generic_resources):
return generic_resources
if not isinstance(generic_resources, dict):
raise errors.InvalidArgument(
'generic_resources must be a dict or a list '
f'(found {type(generic_resources)})'
'generic_resources must be a dict or a list'
' (found {})'.format(type(generic_resources))
)
resources = []
for kind, value in generic_resources.items():
for kind, value in six.iteritems(generic_resources):
resource_type = None
if isinstance(value, int):
resource_type = 'DiscreteResourceSpec'
elif isinstance(value, str):
resource_type = 'NamedResourceSpec'
else:
kv = {kind: value}
raise errors.InvalidArgument(
f'Unsupported generic resource reservation type: {kv}'
'Unsupported generic resource reservation '
'type: {}'.format({kind: value})
)
resources.append({
resource_type: {'Kind': kind, 'Value': value}
@ -418,9 +384,8 @@ class UpdateConfig(dict):
an update before the failure action is invoked, specified as a
floating point number between 0 and 1. Default: 0
order (string): Specifies the order of operations when rolling out an
updated task. Either ``start-first`` or ``stop-first`` are accepted.
updated task. Either ``start_first`` or ``stop_first`` are accepted.
"""
def __init__(self, parallelism=0, delay=None, failure_action='continue',
monitor=None, max_failure_ratio=None, order=None):
self['Parallelism'] = parallelism
@ -456,8 +421,7 @@ class UpdateConfig(dict):
class RollbackConfig(UpdateConfig):
"""
Used to specify the way container rollbacks should be performed by a
service
Used to specify the way containe rollbacks should be performed by a service
Args:
parallelism (int): Maximum number of tasks to be rolled back in one
@ -473,13 +437,13 @@ class RollbackConfig(UpdateConfig):
a rollback before the failure action is invoked, specified as a
floating point number between 0 and 1. Default: 0
order (string): Specifies the order of operations when rolling out a
rolled back task. Either ``start-first`` or ``stop-first`` are
rolled back task. Either ``start_first`` or ``stop_first`` are
accepted.
"""
pass
class RestartConditionTypesEnum:
class RestartConditionTypesEnum(object):
_values = (
'none',
'on-failure',
@ -510,7 +474,7 @@ class RestartPolicy(dict):
max_attempts=0, window=0):
if condition not in self.condition_types._values:
raise TypeError(
f'Invalid RestartPolicy condition {condition}'
'Invalid RestartPolicy condition {0}'.format(condition)
)
self['Condition'] = condition
@ -532,7 +496,6 @@ class DriverConfig(dict):
name (string): Name of the driver to use.
options (dict): Driver-specific options. Default: ``None``.
"""
def __init__(self, name, options=None):
self['Name'] = name
if options:
@ -554,7 +517,6 @@ class EndpointSpec(dict):
is ``(target_port [, protocol [, publish_mode]])``.
Ports can only be provided if the ``vip`` resolution mode is used.
"""
def __init__(self, mode=None, ports=None):
if ports:
self['Ports'] = convert_service_ports(ports)
@ -571,7 +533,7 @@ def convert_service_ports(ports):
)
result = []
for k, v in ports.items():
for k, v in six.iteritems(ports):
port_spec = {
'Protocol': 'tcp',
'PublishedPort': k
@ -597,70 +559,37 @@ def convert_service_ports(ports):
class ServiceMode(dict):
"""
Indicate whether a service or a job should be deployed as a replicated
or global service, and associated parameters
Indicate whether a service should be deployed as a replicated or global
service, and associated parameters
Args:
mode (string): Can be either ``replicated``, ``global``,
``replicated-job`` or ``global-job``
mode (string): Can be either ``replicated`` or ``global``
replicas (int): Number of replicas. For replicated services only.
concurrency (int): Number of concurrent jobs. For replicated job
services only.
"""
def __init__(self, mode, replicas=None, concurrency=None):
replicated_modes = ('replicated', 'replicated-job')
supported_modes = replicated_modes + ('global', 'global-job')
if mode not in supported_modes:
def __init__(self, mode, replicas=None):
if mode not in ('replicated', 'global'):
raise errors.InvalidArgument(
'mode must be either "replicated", "global", "replicated-job"'
' or "global-job"'
'mode must be either "replicated" or "global"'
)
if mode not in replicated_modes:
if replicas is not None:
raise errors.InvalidArgument(
'replicas can only be used for "replicated" or'
' "replicated-job" mode'
)
if concurrency is not None:
raise errors.InvalidArgument(
'concurrency can only be used for "replicated-job" mode'
)
service_mode = self._convert_mode(mode)
self.mode = service_mode
self[service_mode] = {}
if mode != 'replicated' and replicas is not None:
raise errors.InvalidArgument(
'replicas can only be used for replicated mode'
)
self[mode] = {}
if replicas is not None:
if mode == 'replicated':
self[service_mode]['Replicas'] = replicas
self[mode]['Replicas'] = replicas
if mode == 'replicated-job':
self[service_mode]['MaxConcurrent'] = concurrency or 1
self[service_mode]['TotalCompletions'] = replicas
@staticmethod
def _convert_mode(original_mode):
if original_mode == 'global-job':
return 'GlobalJob'
if original_mode == 'replicated-job':
return 'ReplicatedJob'
return original_mode
@property
def mode(self):
if 'global' in self:
return 'global'
return 'replicated'
@property
def replicas(self):
if 'replicated' in self:
return self['replicated'].get('Replicas')
if 'ReplicatedJob' in self:
return self['ReplicatedJob'].get('TotalCompletions')
return None
if self.mode != 'replicated':
return None
return self['replicated'].get('Replicas')
class SecretReference(dict):
@ -734,7 +663,6 @@ class Placement(dict):
platforms (:py:class:`list` of tuple): A list of platforms
expressed as ``(arch, os)`` tuples
"""
def __init__(self, constraints=None, preferences=None, platforms=None,
maxreplicas=None):
if constraints is not None:
@ -767,12 +695,11 @@ class PlacementPreference(dict):
the scheduler will try to spread tasks evenly over groups of
nodes identified by this label.
"""
def __init__(self, strategy, descriptor):
if strategy != 'spread':
raise errors.InvalidArgument(
f'PlacementPreference strategy value is invalid ({strategy}): '
'must be "spread".'
'PlacementPreference strategy value is invalid ({}):'
' must be "spread".'.format(strategy)
)
self['Spread'] = {'SpreadDescriptor': descriptor}
@ -789,7 +716,6 @@ class DNSConfig(dict):
options (:py:class:`list`): A list of internal resolver variables
to be modified (e.g., ``debug``, ``ndots:3``, etc.).
"""
def __init__(self, nameservers=None, search=None, options=None):
self['Nameservers'] = nameservers
self['Search'] = search
@ -820,7 +746,6 @@ class Privileges(dict):
selinux_type (string): SELinux type label
selinux_level (string): SELinux level label
"""
def __init__(self, credentialspec_file=None, credentialspec_registry=None,
selinux_disable=None, selinux_user=None, selinux_role=None,
selinux_type=None, selinux_level=None):
@ -863,7 +788,6 @@ class NetworkAttachmentConfig(dict):
options (:py:class:`dict`): Driver attachment options for the
network target.
"""
def __init__(self, target, aliases=None, options=None):
self['Target'] = target
self['Aliases'] = aliases

View File

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

View File

@ -4,19 +4,13 @@ import re
import tarfile
import tempfile
from ..constants import IS_WINDOWS_PLATFORM
import six
from .fnmatch import fnmatch
from ..constants import IS_WINDOWS_PLATFORM
_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):
@ -50,7 +44,7 @@ def exclude_paths(root, patterns, dockerfile=None):
if dockerfile is None:
dockerfile = 'Dockerfile'
patterns.append(f"!{dockerfile}")
patterns.append('!' + dockerfile)
pm = PatternMatcher(patterns)
return set(pm.walk(root))
@ -75,7 +69,7 @@ def create_archive(root, files=None, fileobj=None, gzip=False,
t = tarfile.open(mode='w:gz' if gzip else 'w', fileobj=fileobj)
if files is None:
files = build_file_list(root)
extra_names = {e[0] for e in extra_files}
extra_names = set(e[0] for e in extra_files)
for path in files:
if path in extra_names:
# Extra files override context files with the same name
@ -101,10 +95,10 @@ def create_archive(root, files=None, fileobj=None, gzip=False,
try:
with open(full_path, 'rb') as f:
t.addfile(i, f)
except OSError as oe:
raise OSError(
f'Can not read file in context: {full_path}'
) from oe
except IOError:
raise IOError(
'Can not read file in context: {}'.format(full_path)
)
else:
# Directories, FIFOs, symlinks... don't need to be read.
t.addfile(i, None)
@ -125,8 +119,12 @@ def mkbuildcontext(dockerfile):
t = tarfile.open(mode='w', fileobj=f)
if isinstance(dockerfile, io.StringIO):
dfinfo = tarfile.TarInfo('Dockerfile')
raise TypeError('Please use io.BytesIO to create in-memory '
'Dockerfiles with Python 3')
if six.PY3:
raise TypeError('Please use io.BytesIO to create in-memory '
'Dockerfiles with Python 3')
else:
dfinfo.size = len(dockerfile.getvalue())
dockerfile.seek(0)
elif isinstance(dockerfile, io.BytesIO):
dfinfo = tarfile.TarInfo('Dockerfile')
dfinfo.size = len(dockerfile.getvalue())
@ -156,7 +154,7 @@ def walk(root, patterns, default=True):
# Heavily based on
# https://github.com/moby/moby/blob/master/pkg/fileutils/fileutils.go
class PatternMatcher:
class PatternMatcher(object):
def __init__(self, patterns):
self.patterns = list(filter(
lambda p: p.dirs, [Pattern(p) for p in patterns]
@ -188,7 +186,7 @@ class PatternMatcher:
fpath = os.path.join(
os.path.relpath(current_dir, root), f
)
if fpath.startswith(f".{os.path.sep}"):
if fpath.startswith('.' + os.path.sep):
fpath = fpath[2:]
match = self.matches(fpath)
if not match:
@ -214,12 +212,13 @@ class PatternMatcher:
break
if skip:
continue
yield from rec_walk(cur)
for sub in rec_walk(cur):
yield sub
return rec_walk(root)
class Pattern:
class Pattern(object):
def __init__(self, pattern_str):
self.exclusion = False
if pattern_str.startswith('!'):
@ -232,9 +231,6 @@ class Pattern:
@classmethod
def normalize(cls, p):
# Remove trailing spaces
p = p.strip()
# Leading and trailing slashes are not relevant. Yes,
# "foo.py/" must exclude the "foo.py" regular file. "."
# components are not relevant either, even if the whole

View File

@ -18,11 +18,11 @@ def find_config_file(config_path=None):
os.path.join(home_dir(), LEGACY_DOCKER_CONFIG_FILENAME), # 4
]))
log.debug(f"Trying paths: {repr(paths)}")
log.debug("Trying paths: {0}".format(repr(paths)))
for path in paths:
if os.path.exists(path):
log.debug(f"Found file at path: {path}")
log.debug("Found file at path: {0}".format(path))
return path
log.debug("No config file found")
@ -57,7 +57,7 @@ def load_general_config(config_path=None):
try:
with open(config_file) as f:
return json.load(f)
except (OSError, ValueError) as e:
except (IOError, ValueError) as e:
# In the case of a legacy `.dockercfg` file, we won't
# be able to load any JSON data.
log.debug(e)

View File

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

View File

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

View File

@ -1,8 +1,14 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import json
import json.decoder
import six
from ..errors import StreamParseError
json_decoder = json.JSONDecoder()
@ -14,7 +20,7 @@ def stream_as_text(stream):
instead of byte streams.
"""
for data in stream:
if not isinstance(data, str):
if not isinstance(data, six.text_type):
data = data.decode('utf-8', 'replace')
yield data
@ -40,8 +46,8 @@ def json_stream(stream):
return split_buffer(stream, json_splitter, json_decoder.decode)
def line_splitter(buffer, separator='\n'):
index = buffer.find(str(separator))
def line_splitter(buffer, separator=u'\n'):
index = buffer.find(six.text_type(separator))
if index == -1:
return None
return buffer[:index + 1], buffer[index + 1:]
@ -55,7 +61,7 @@ def split_buffer(stream, splitter=None, decoder=lambda a: a):
of the input.
"""
splitter = splitter or line_splitter
buffered = ''
buffered = six.text_type('')
for data in stream_as_text(stream):
buffered += data
@ -71,4 +77,4 @@ def split_buffer(stream, splitter=None, decoder=lambda a: a):
try:
yield decoder(buffered)
except Exception as e:
raise StreamParseError(e) from e
raise StreamParseError(e)

View File

@ -3,7 +3,7 @@ import re
PORT_SPEC = re.compile(
"^" # Match full string
"(" # External part
r"(\[?(?P<host>[a-fA-F\d.:]+)\]?:)?" # Address
r"((?P<host>[a-fA-F\d.:]+):)?" # Address
r"(?P<ext>[\d]*)(-(?P<ext_end>[\d]+))?:" # External range
")?"
r"(?P<int>[\d]+)(-(?P<int_end>[\d]+))?" # Internal range
@ -49,7 +49,7 @@ def port_range(start, end, proto, randomly_available_port=False):
if not end:
return [start + proto]
if randomly_available_port:
return [f"{start}-{end}{proto}"]
return ['{}-{}'.format(start, end) + proto]
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
def __str__(self):
return (
'ProxyConfig('
f'http={self.http}, https={self.https}, '
f'ftp={self.ftp}, no_proxy={self.no_proxy}'
')'
)
return 'ProxyConfig(http={}, https={}, ftp={}, no_proxy={})'.format(
self.http, self.https, self.ftp, self.no_proxy)

View File

@ -4,6 +4,8 @@ import select
import socket as pysocket
import struct
import six
try:
from ..transport import NpipeSocket
except ImportError:
@ -18,11 +20,6 @@ class SocketError(Exception):
pass
# NpipeSockets have their own error types
# pywintypes.error: (109, 'ReadFile', 'The pipe has been ended.')
NPIPE_ENDED = 109
def read(socket, n=4096):
"""
Reads at most n bytes from socket
@ -30,33 +27,18 @@ def read(socket, n=4096):
recoverable_errors = (errno.EINTR, errno.EDEADLK, errno.EWOULDBLOCK)
if not isinstance(socket, NpipeSocket):
if not hasattr(select, "poll"):
# Limited to 1024
select.select([socket], [], [])
else:
poll = select.poll()
poll.register(socket, select.POLLIN | select.POLLPRI)
poll.poll()
if six.PY3 and not isinstance(socket, NpipeSocket):
select.select([socket], [], [])
try:
if hasattr(socket, 'recv'):
return socket.recv(n)
if isinstance(socket, pysocket.SocketIO):
if six.PY3 and isinstance(socket, getattr(pysocket, 'SocketIO')):
return socket.read(n)
return os.read(socket.fileno(), n)
except OSError as e:
except EnvironmentError as e:
if e.errno not in recoverable_errors:
raise
except Exception as e:
is_pipe_ended = (isinstance(socket, NpipeSocket) and
len(e.args) > 0 and
e.args[0] == NPIPE_ENDED)
if is_pipe_ended:
# npipes don't support duplex sockets, so we interpret
# a PIPE_ENDED error as a close operation (0-length read).
return ''
raise
def read_exactly(socket, n):
@ -64,7 +46,7 @@ def read_exactly(socket, n):
Reads exactly n bytes from socket
Raises SocketError if there isn't enough data
"""
data = b""
data = six.binary_type()
while len(data) < n:
next_data = read(socket, n - len(data))
if not next_data:
@ -152,7 +134,7 @@ def consume_socket_output(frames, demux=False):
if demux is False:
# If the streams are multiplexed, the generator returns strings, that
# we just need to concatenate.
return b"".join(frames)
return six.binary_type().join(frames)
# If the streams are demultiplexed, the generator yields tuples
# (stdout, stderr)
@ -184,4 +166,4 @@ def demux_adaptor(stream_id, data):
elif stream_id == STDERR:
return (None, data)
else:
raise ValueError(f'{stream_id} is not a valid stream')
raise ValueError('{0} is not a valid stream'.format(stream_id))

View File

@ -1,28 +1,26 @@
import base64
import collections
import json
import os
import os.path
import shlex
import string
from datetime import datetime, timezone
from functools import lru_cache
from itertools import zip_longest
from urllib.parse import urlparse, urlunparse
from datetime import datetime
from distutils.version import StrictVersion
import six
from .. import errors
from ..constants import (
BYTE_UNITS,
DEFAULT_HTTP_HOST,
DEFAULT_NPIPE,
DEFAULT_UNIX_SOCKET,
)
from ..tls import TLSConfig
from .. import tls
from ..constants import DEFAULT_HTTP_HOST
from ..constants import DEFAULT_UNIX_SOCKET
from ..constants import DEFAULT_NPIPE
from ..constants import BYTE_UNITS
URLComponents = collections.namedtuple(
'URLComponents',
'scheme netloc url params query fragment',
)
if six.PY2:
from urllib import splitnport
from urlparse import urlparse
else:
from urllib.parse import splitnport, urlparse
def create_ipam_pool(*args, **kwargs):
@ -41,11 +39,11 @@ def create_ipam_config(*args, **kwargs):
def decode_json_header(header):
data = base64.b64decode(header)
data = data.decode('utf-8')
if six.PY3:
data = data.decode('utf-8')
return json.loads(data)
@lru_cache(maxsize=None)
def compare_version(v1, v2):
"""Compare docker versions
@ -58,20 +56,14 @@ def compare_version(v1, v2):
>>> compare_version(v2, v2)
0
"""
if v1 == v2:
s1 = StrictVersion(v1)
s2 = StrictVersion(v2)
if s1 == s2:
return 0
# Split into `sys.version_info` like tuples.
s1 = tuple(int(p) for p in v1.split('.'))
s2 = tuple(int(p) for p in v2.split('.'))
# Compare each component, padding with 0 if necessary.
for c1, c2 in zip_longest(s1, s2, fillvalue=0):
if c1 == c2:
continue
elif c1 > c2:
return -1
else:
return 1
return 0
elif s1 > s2:
return -1
else:
return 1
def version_lt(v1, v2):
@ -88,7 +80,7 @@ def _convert_port_binding(binding):
if len(binding) == 2:
result['HostPort'] = binding[1]
result['HostIp'] = binding[0]
elif isinstance(binding[0], str):
elif isinstance(binding[0], six.string_types):
result['HostIp'] = binding[0]
else:
result['HostPort'] = binding[0]
@ -112,7 +104,7 @@ def _convert_port_binding(binding):
def convert_port_bindings(port_bindings):
result = {}
for k, v in iter(port_bindings.items()):
for k, v in six.iteritems(port_bindings):
key = str(k)
if '/' not in key:
key += '/tcp'
@ -129,17 +121,18 @@ def convert_volume_binds(binds):
result = []
for k, v in binds.items():
if isinstance(k, bytes):
if isinstance(k, six.binary_type):
k = k.decode('utf-8')
if isinstance(v, dict):
if 'ro' in v and 'mode' in v:
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']
if isinstance(bind, bytes):
if isinstance(bind, six.binary_type):
bind = bind.decode('utf-8')
if 'ro' in v:
@ -149,30 +142,14 @@ def convert_volume_binds(binds):
else:
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(
f'{k}:{bind}:{mode}'
six.text_type('{0}:{1}:{2}').format(k, bind, mode)
)
else:
if isinstance(v, bytes):
if isinstance(v, six.binary_type):
v = v.decode('utf-8')
result.append(
f'{k}:{v}:rw'
six.text_type('{0}:{1}:rw').format(k, v)
)
return result
@ -183,13 +160,13 @@ def convert_tmpfs_mounts(tmpfs):
if not isinstance(tmpfs, list):
raise ValueError(
'Expected tmpfs value to be either a list or a dict, '
f'found: {type(tmpfs).__name__}'
'Expected tmpfs value to be either a list or a dict, found: {}'
.format(type(tmpfs).__name__)
)
result = {}
for mount in tmpfs:
if isinstance(mount, str):
if isinstance(mount, six.string_types):
if ":" in mount:
name, options = mount.split(":", 1)
else:
@ -198,8 +175,8 @@ def convert_tmpfs_mounts(tmpfs):
else:
raise ValueError(
"Expected item in tmpfs list to be a string, "
f"found: {type(mount).__name__}"
"Expected item in tmpfs list to be a string, found: {}"
.format(type(mount).__name__)
)
result[name] = options
@ -214,7 +191,7 @@ def convert_service_networks(networks):
result = []
for n in networks:
if isinstance(n, str):
if isinstance(n, six.string_types):
n = {'Target': n}
result.append(n)
return result
@ -231,6 +208,10 @@ def parse_repository_tag(repo_name):
def parse_host(addr, is_win32=False, tls=False):
path = ''
port = None
host = None
# Sensible defaults
if not addr and is_win32:
return DEFAULT_NPIPE
@ -241,9 +222,9 @@ def parse_host(addr, is_win32=False, tls=False):
parsed_url = urlparse(addr)
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
parsed_url = urlparse(f"//{addr}", 'tcp')
parsed_url = urlparse('//' + addr, 'tcp')
proto = 'tcp'
if proto == 'fd':
@ -259,14 +240,14 @@ def parse_host(addr, is_win32=False, tls=False):
if proto not in ('tcp', 'unix', 'npipe', 'ssh'):
raise errors.DockerException(
f"Invalid bind address protocol: {addr}"
"Invalid bind address protocol: {}".format(addr)
)
if proto == 'tcp' and not parsed_url.netloc:
# "tcp://" is exceptionally disallowed by convention;
# omitting a hostname for other protocols is fine
raise errors.DockerException(
f'Invalid bind address format: {addr}'
'Invalid bind address format: {}'.format(addr)
)
if any([
@ -274,51 +255,45 @@ def parse_host(addr, is_win32=False, tls=False):
parsed_url.password
]):
raise errors.DockerException(
f'Invalid bind address format: {addr}'
'Invalid bind address format: {}'.format(addr)
)
if parsed_url.path and proto == 'ssh':
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:
path = parsed_url.path
if proto == 'unix' and parsed_url.hostname is not None:
# For legacy reasons, we consider 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
if proto in ('tcp', 'ssh'):
port = parsed_url.port or 0
if port <= 0:
# parsed_url.hostname strips brackets from IPv6 addresses,
# which can be problematic hence our use of splitnport() instead.
host, port = splitnport(parsed_url.netloc)
if port is None or port < 0:
if proto != 'ssh':
raise errors.DockerException(
f'Invalid bind address format: port is required: {addr}'
'Invalid bind address format: port is required:'
' {}'.format(addr)
)
port = 22
netloc = f'{parsed_url.netloc}:{port}'
if not parsed_url.hostname:
netloc = f'{DEFAULT_HTTP_HOST}:{port}'
if not host:
host = DEFAULT_HTTP_HOST
# Rewrite schemes to fit library internals (requests adapters)
if proto == 'tcp':
proto = f"http{'s' if tls else ''}"
proto = 'http{}'.format('s' if tls else '')
elif proto == 'unix':
proto = 'http+unix'
if proto in ('http+unix', 'npipe'):
return f"{proto}://{path}".rstrip('/')
return urlunparse(URLComponents(
scheme=proto,
netloc=netloc,
url=path,
params='',
query='',
fragment='',
)).rstrip('/')
return "{}://{}".format(proto, path).rstrip('/')
return '{0}://{1}:{2}{3}'.format(proto, host, port, path).rstrip('/')
def parse_devices(devices):
@ -327,9 +302,9 @@ def parse_devices(devices):
if isinstance(device, dict):
device_list.append(device)
continue
if not isinstance(device, str):
if not isinstance(device, six.string_types):
raise errors.DockerException(
f'Invalid device type {type(device)}'
'Invalid device type {0}'.format(type(device))
)
device_mapping = device.split(':')
if device_mapping:
@ -350,7 +325,7 @@ def parse_devices(devices):
return device_list
def kwargs_from_env(environment=None):
def kwargs_from_env(ssl_version=None, assert_hostname=None, environment=None):
if not environment:
environment = os.environ
host = environment.get('DOCKER_HOST')
@ -378,11 +353,18 @@ def kwargs_from_env(environment=None):
if not cert_path:
cert_path = os.path.join(os.path.expanduser('~'), '.docker')
params['tls'] = TLSConfig(
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'] = tls.TLSConfig(
client_cert=(os.path.join(cert_path, 'cert.pem'),
os.path.join(cert_path, 'key.pem')),
ca_cert=os.path.join(cert_path, 'ca.pem'),
verify=tls_verify,
ssl_version=ssl_version,
assert_hostname=assert_hostname,
)
return params
@ -390,26 +372,26 @@ def kwargs_from_env(environment=None):
def convert_filters(filters):
result = {}
for k, v in iter(filters.items()):
for k, v in six.iteritems(filters):
if isinstance(v, bool):
v = 'true' if v else 'false'
if not isinstance(v, list):
v = [v, ]
result[k] = [
str(item) if not isinstance(item, str) else item
str(item) if not isinstance(item, six.string_types) else item
for item in v
]
return json.dumps(result)
def datetime_to_timestamp(dt):
"""Convert a datetime to a Unix timestamp"""
delta = dt.astimezone(timezone.utc) - datetime(1970, 1, 1, tzinfo=timezone.utc)
"""Convert a UTC datetime to a Unix timestamp"""
delta = dt - datetime.utcfromtimestamp(0)
return delta.seconds + delta.days * 24 * 3600
def parse_bytes(s):
if isinstance(s, (int, float,)):
if isinstance(s, six.integer_types + (float,)):
return s
if len(s) == 0:
return 0
@ -431,18 +413,19 @@ def parse_bytes(s):
if suffix in units.keys() or suffix.isdigit():
try:
digits = float(digits_part)
except ValueError as ve:
except ValueError:
raise errors.DockerException(
'Failed converting the string value for memory '
f'({digits_part}) to an integer.'
) from ve
'Failed converting the string value for memory ({0}) to'
' an integer.'.format(digits_part)
)
# Reconvert to long for the final result
s = int(digits * units[suffix])
else:
raise errors.DockerException(
f'The specified value for memory ({s}) should specify the units. '
'The postfix should be one of the `b` `k` `m` `g` characters'
'The specified value for memory ({0}) should specify the'
' units. The postfix should be one of the `b` `k` `m` `g`'
' characters'.format(s)
)
return s
@ -450,9 +433,9 @@ def parse_bytes(s):
def normalize_links(links):
if isinstance(links, dict):
links = iter(links.items())
links = six.iteritems(links)
return [f'{k}:{v}' if v else k for k, v in sorted(links)]
return ['{0}:{1}'.format(k, v) if v else k for k, v in sorted(links)]
def parse_env_file(env_file):
@ -462,7 +445,7 @@ def parse_env_file(env_file):
"""
environment = {}
with open(env_file) as f:
with open(env_file, 'r') as f:
for line in f:
if line[0] == '#':
@ -478,12 +461,15 @@ def parse_env_file(env_file):
environment[k] = v
else:
raise errors.DockerException(
f'Invalid line in environment file {env_file}:\n{line}')
'Invalid line in environment file {0}:\n{1}'.format(
env_file, line))
return environment
def split_command(command):
if six.PY2 and not isinstance(command, six.binary_type):
command = command.encode('utf-8')
return shlex.split(command)
@ -491,22 +477,22 @@ def format_environment(environment):
def format_env(key, value):
if value is None:
return key
if isinstance(value, bytes):
if isinstance(value, six.binary_type):
value = value.decode('utf-8')
return f'{key}={value}'
return [format_env(*var) for var in iter(environment.items())]
return u'{key}={value}'.format(key=key, value=value)
return [format_env(*var) for var in six.iteritems(environment)]
def format_extra_hosts(extra_hosts, task=False):
# Use format dictated by Swarm API if container is part of a task
if task:
return [
f'{v} {k}' for k, v in sorted(iter(extra_hosts.items()))
'{} {}'.format(v, k) for k, v in sorted(six.iteritems(extra_hosts))
]
return [
f'{k}:{v}' for k, v in sorted(iter(extra_hosts.items()))
'{}:{}'.format(k, v) for k, v in sorted(six.iteritems(extra_hosts))
]

View File

@ -1,8 +1,2 @@
try:
from ._version import __version__
except ImportError:
from importlib.metadata import PackageNotFoundError, version
try:
__version__ = version('docker')
except PackageNotFoundError:
__version__ = '0.0.0'
version = "4.4.3"
version_info = tuple([int(d) for d in version.split("-")[0].split(".")])

2
docs-requirements.txt Normal file
View File

@ -0,0 +1,2 @@
recommonmark==0.4.0
Sphinx==1.4.6

View File

@ -1,12 +1,3 @@
dl.hide-signature > dt {
display: none;
}
dl.field-list > dt {
/* prevent code blocks from forcing wrapping on the "Parameters" header */
word-break: initial;
}
code.literal{
hyphens: none;
}

View File

@ -1,213 +1,11 @@
Changelog
Change log
==========
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
-----
### Upgrade Notes
- Minimum supported Python version is 3.7+
- When installing with pip, the `docker[tls]` extra is deprecated and a no-op,
use `docker` for same functionality (TLS support is always available now)
- Native Python SSH client (used by default / `use_ssh_client=False`) will now
reject unknown host keys with `paramiko.ssh_exception.SSHException`
- Short IDs are now 12 characters instead of 10 characters (same as Docker CLI)
### Features
- Python 3.10 support
- Automatically negotiate most secure TLS version
- Add `platform` (e.g. `linux/amd64`, `darwin/arm64`) to container create & run
- Add support for `GlobalJob` and `ReplicatedJobs` for Swarm
- Add `remove()` method on `Image`
- Add `force` param to `disable()` on `Plugin`
### Bugfixes
- Fix install issues on Windows related to `pywin32`
- Do not accept unknown SSH host keys in native Python SSH mode
- Use 12 character short IDs for consistency with Docker CLI
- Ignore trailing whitespace in `.dockerignore` files
- Fix IPv6 host parsing when explicit port specified
- Fix `ProxyCommand` option for SSH connections
- Do not spawn extra subshell when launching external SSH client
- Improve exception semantics to preserve context
- Documentation improvements (formatting, examples, typos, missing params)
### Miscellaneous
- Upgrade dependencies in `requirements.txt` to latest versions
- Remove extraneous transitive dependencies
- Eliminate usages of deprecated functions/methods
- Test suite reliability improvements
- GitHub Actions workflows for linting, unit tests, integration tests, and
publishing releases
5.0.3
-----
[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/76?closed=1)
### Features
- Add `cap_add` and `cap_drop` parameters to service create and ContainerSpec
- Add `templating` parameter to config create
### Bugfixes
- Fix getting a read timeout for logs/attach with a tty and slow output
### Miscellaneous
- Fix documentation examples
5.0.2
-----
[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/75?closed=1)
### Bugfixes
- Fix `disable_buffering` regression
5.0.1
-----
[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/74?closed=1)
### Bugfixes
- Bring back support for ssh identity file
- Cleanup remaining python-2 dependencies
- Fix image save example in docs
### Miscellaneous
- Bump urllib3 to 1.26.5
- Bump requests to 2.26.0
5.0.0
-----
[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/70?closed=1)
### Breaking changes
- Remove support for Python 2.7
- Make Python 3.6 the minimum version supported
### Features
- Add `limit` parameter to image search endpoint
### Bugfixes
- Fix `KeyError` exception on secret create
- Verify TLS keys loaded from docker contexts
- Update PORT_SPEC regex to allow square brackets for IPv6 addresses
- Fix containers and images documentation examples
4.4.4
-----
[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/73?closed=1)
### Bugfixes
- Remove `LD_LIBRARY_PATH` and `SSL_CERT_FILE` environment variables when shelling out to the ssh client
4.4.3
-----
[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/72?closed=1)
### Features
- Add support for docker.types.Placement.MaxReplicas
### Bugfixes
- Fix SSH port parsing when shelling out to the ssh client

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# docker-sdk-python documentation build configuration file, created by
# sphinx-quickstart on Wed Sep 14 15:48:58 2016.
@ -18,8 +19,6 @@
import datetime
import os
import sys
from importlib.metadata import version
sys.path.insert(0, os.path.abspath('..'))
@ -35,19 +34,24 @@ sys.path.insert(0, os.path.abspath('..'))
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
'myst_parser'
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
source_suffix = {
'.rst': 'restructuredtext',
'.txt': 'markdown',
'.md': 'markdown',
from recommonmark.parser import CommonMarkParser
source_parsers = {
'.md': CommonMarkParser,
}
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
source_suffix = ['.rst', '.md']
# source_suffix = '.md'
# The encoding of source files.
#
# source_encoding = 'utf-8-sig'
@ -56,26 +60,28 @@ source_suffix = {
master_doc = 'index'
# General information about the project.
project = 'Docker SDK for Python'
project = u'Docker SDK for Python'
year = datetime.datetime.now().year
copyright = f'{year} Docker Inc'
author = 'Docker Inc'
copyright = u'%d Docker Inc' % year
author = u'Docker Inc'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# see https://github.com/pypa/setuptools_scm#usage-from-sphinx
release = version('docker')
# for example take major/minor
version = '.'.join(release.split('.')[:2])
with open('../docker/version.py', 'r') as vfile:
exec(vfile.read())
# The full version, including alpha/beta/rc tags.
release = version
# The short X.Y version.
version = '{}.{}'.format(version_info[0], version_info[1])
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'en'
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
@ -277,8 +283,8 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'docker-sdk-python.tex', 'docker-sdk-python Documentation',
'Docker Inc.', 'manual'),
(master_doc, 'docker-sdk-python.tex', u'docker-sdk-python Documentation',
u'Docker Inc.', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@ -319,7 +325,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'docker-sdk-python', 'docker-sdk-python Documentation',
(master_doc, 'docker-sdk-python', u'docker-sdk-python Documentation',
[author], 1)
]
@ -334,7 +340,7 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'docker-sdk-python', 'docker-sdk-python Documentation',
(master_doc, 'docker-sdk-python', u'docker-sdk-python Documentation',
author, 'docker-sdk-python', 'One line description of project.',
'Miscellaneous'),
]

View File

@ -15,7 +15,7 @@ For example, to check the server against a specific CA certificate:
.. code-block:: python
tls_config = docker.tls.TLSConfig(ca_cert='/path/to/ca.pem', verify=True)
tls_config = docker.tls.TLSConfig(ca_cert='/path/to/ca.pem')
client = docker.DockerClient(base_url='<https_url>', tls=tls_config)
This is the equivalent of ``docker --tlsverify --tlscacert /path/to/ca.pem ...``.

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`:
>>> cmd = '/bin/sh -c "echo hello stdout ; echo hello stderr >&2"'
We'll run this command with all four the combinations of ``stream``
and ``demux``.
With ``stream=False`` and ``demux=False``, the output is a string
that contains both the `stdout` and the `stderr` output:
>>> res = container.exec_run(cmd, stream=False, demux=False)
>>> res.output
b'hello stderr\nhello stdout\n'
@ -55,8 +52,15 @@ Traceback (most recent call last):
File "<stdin>", line 1, in <module>
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.output
(b'hello stdout\n', b'hello stderr\n')
>>> res = container.exec_run(cmd, stream=True, demux=True)
>>> next(res.output)
(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 +0,0 @@
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[project]
name = "docker"
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"]

18
requirements.txt Normal file
View File

@ -0,0 +1,18 @@
appdirs==1.4.3
asn1crypto==0.22.0
backports.ssl-match-hostname==3.5.0.1
cffi==1.14.4
cryptography==3.2
enum34==1.1.6
idna==2.5
ipaddress==1.0.18
packaging==16.8
paramiko==2.4.2
pycparser==2.17
pyOpenSSL==18.0.0
pyparsing==2.2.0
pywin32==227; sys_platform == 'win32'
requests==2.20.0
six==1.10.0
urllib3==1.24.3
websocket-client==0.56.0

View File

@ -52,8 +52,8 @@ class Version(namedtuple('_Version', 'major minor patch stage edition')):
return (int(self.major), int(self.minor), int(self.patch)) + stage
def __str__(self):
stage = f'-{self.stage}' if self.stage else ''
edition = f'-{self.edition}' if self.edition else ''
stage = '-{}'.format(self.stage) if self.stage else ''
edition = '-{}'.format(self.edition) if self.edition else ''
return '.'.join(map(str, self[:3])) + edition + stage

6
setup.cfg Normal file
View File

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

95
setup.py Normal file
View File

@ -0,0 +1,95 @@
#!/usr/bin/env python
from __future__ import print_function
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 = [
'six >= 1.4.0',
'websocket-client >= 0.32.0',
'requests >= 2.14.2, != 2.18.0',
]
extras_require = {
':python_version < "3.5"': 'backports.ssl_match_hostname >= 3.5',
# While not imported explicitly, the ipaddress module is required for
# ssl_match_hostname to verify hosts match with certificates via
# ServerAltname: https://pypi.python.org/pypi/backports.ssl_match_hostname
':python_version < "3.3"': 'ipaddress >= 1.0.16',
# win32 APIs if on Windows (required for npipe support)
':sys_platform == "win32"': 'pywin32==227',
# If using docker-py over TLS, highly recommend this option is
# pip-installed or pinned.
# TODO: if pip installing both "requests" and "requests[security]", the
# extra package from the "security" option are not installed (see
# https://github.com/pypa/pip/issues/4391). Once that's fixed, instead of
# installing the extra dependencies, install the following instead:
# 'requests[security] >= 2.5.2, != 2.11.0, != 2.12.2'
'tls': ['pyOpenSSL>=17.5.0', 'cryptography>=1.3.4', 'idna>=2.0.0'],
# Only required when connecting using the ssh:// protocol
'ssh': ['paramiko>=2.4.2'],
}
version = None
exec(open('docker/version.py').read())
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",
version=version,
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"]),
install_requires=requirements,
tests_require=test_requirements,
extras_require=extras_require,
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*',
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 :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Topic :: Software Development',
'Topic :: Utilities',
'License :: OSI Approved :: Apache Software License',
],
maintainer='Joffrey F',
maintainer_email='joffrey@docker.com',
)

7
test-requirements.txt Normal file
View File

@ -0,0 +1,7 @@
setuptools==44.0.0 # last version with python 2.7 support
coverage==4.5.2
flake8==3.6.0
mock==1.0.1
pytest==4.3.1
pytest-cov==2.6.1
pytest-timeout==1.3.3

View File

@ -1,16 +1,17 @@
# syntax=docker/dockerfile:1
ARG PYTHON_VERSION=3.7
ARG PYTHON_VERSION=3.12
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 \
gnupg2 \
pass
# Add SSH keys and set permissions
COPY tests/ssh/config/client /root/.ssh
COPY tests/ssh/config/server/known_ed25519.pub /root/.ssh/known_hosts
RUN sed -i '1s;^;dpy-dind-ssh ;' /root/.ssh/known_hosts
COPY tests/ssh-keys /root/.ssh
RUN chmod -R 600 /root/.ssh
COPY ./tests/gpg-keys /gpg-keys
@ -26,10 +27,11 @@ RUN curl -sSL -o /opt/docker-credential-pass.tar.gz \
chmod +x /usr/local/bin/docker-credential-pass
WORKDIR /src
COPY . .
COPY requirements.txt /src/requirements.txt
RUN pip install -r 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 .[dev,ssh,websockets]
COPY test-requirements.txt /src/test-requirements.txt
RUN pip install -r test-requirements.txt
COPY . /src
RUN pip install .

View File

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

View File

@ -1,20 +1,23 @@
# syntax=docker/dockerfile:1
ARG API_VERSION=1.45
ARG ENGINE_VERSION=26.1
ARG API_VERSION=1.39
ARG ENGINE_VERSION=19.03.12
FROM docker:${ENGINE_VERSION}-dind
RUN apk add --no-cache --upgrade \
openssh
RUN apk add --no-cache \
openssh
COPY tests/ssh/config/server /etc/ssh/
# Add the keys and set permissions
RUN ssh-keygen -A
# copy the test SSH config
RUN echo "IgnoreUserKnownHosts yes" > /etc/ssh/sshd_config && \
echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config && \
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
# set authorized keys for client paswordless connection
COPY tests/ssh/config/client/id_rsa.pub /root/.ssh/authorized_keys
COPY tests/ssh-keys/authorized_keys /root/.ssh/authorized_keys
RUN chmod 600 /root/.ssh/authorized_keys
# RUN echo "root:root" | chpasswd
RUN chmod -R 600 /etc/ssh \
&& chmod -R 600 /root/.ssh \
&& ln -s /usr/local/bin/docker /usr/bin/docker
RUN echo "root:root" | chpasswd
RUN ln -s /usr/local/bin/docker /usr/bin/docker
EXPOSE 22

View File

@ -8,10 +8,10 @@ import tarfile
import tempfile
import time
import docker
import paramiko
import pytest
import docker
import six
def make_tree(dirs, files):
@ -47,19 +47,6 @@ def untar_file(tardata, filename):
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):
test_version = os.environ.get(
'DOCKER_TEST_API_VERSION', docker.constants.DEFAULT_DOCKER_API_VERSION
@ -67,7 +54,7 @@ def requires_api_version(version):
return pytest.mark.skipif(
docker.utils.version_lt(test_version, version),
reason=f"API version is too low (< {version})"
reason="API version is too low (< {0})".format(version)
)
@ -94,12 +81,12 @@ def wait_on_condition(condition, delay=0.1, timeout=40):
start_time = time.time()
while not condition():
if time.time() - start_time > timeout:
raise AssertionError(f"Timeout: {condition}")
raise AssertionError("Timeout: %s" % condition)
time.sleep(delay)
def random_name():
return f'dockerpytest_{random.getrandbits(64):x}'
return u'dockerpytest_{0:x}'.format(random.getrandbits(64))
def force_leave_swarm(client):
@ -118,11 +105,11 @@ def force_leave_swarm(client):
def swarm_listen_addr():
return f'0.0.0.0:{random.randrange(10000, 25000)}'
return '0.0.0.0:{0}'.format(random.randrange(10000, 25000))
def assert_cat_socket_detached_with_keys(sock, inputs):
if hasattr(sock, '_sock'):
if six.PY3 and hasattr(sock, '_sock'):
sock = sock._sock
for i in inputs:
@ -141,7 +128,7 @@ def assert_cat_socket_detached_with_keys(sock, inputs):
# of the daemon no longer cause this to raise an error.
try:
sock.sendall(b'make sure the socket is closed\n')
except OSError:
except socket.error:
return
sock.sendall(b"make sure the socket is closed\n")
@ -157,4 +144,4 @@ def ctrl_with(char):
if re.match('[a-z]', char):
return chr(ord(char) - ord('a') + 1).encode('ascii')
else:
raise Exception('char must be [a-z]')
raise(Exception('char must be [a-z]'))

View File

@ -3,13 +3,14 @@ import os
import shutil
import tempfile
import pytest
from docker import errors
from docker.utils.proxy import ProxyConfig
import pytest
import six
from .base import BaseAPIIntegrationTest, TEST_IMG
from ..helpers import random_name, requires_api_version, requires_experimental
from .base import TEST_IMG, BaseAPIIntegrationTest
class BuildTest(BaseAPIIntegrationTest):
@ -70,8 +71,9 @@ class BuildTest(BaseAPIIntegrationTest):
assert len(logs) > 0
def test_build_from_stringio(self):
return
script = io.StringIO('\n'.join([
if six.PY3:
return
script = io.StringIO(six.text_type('\n').join([
'FROM busybox',
'RUN mkdir -p /tmp/test',
'EXPOSE 8080',
@ -81,7 +83,8 @@ class BuildTest(BaseAPIIntegrationTest):
stream = self.client.build(fileobj=script)
logs = ''
for chunk in stream:
chunk = chunk.decode('utf-8')
if six.PY3:
chunk = chunk.decode('utf-8')
logs += chunk
assert logs != ''
@ -100,9 +103,7 @@ class BuildTest(BaseAPIIntegrationTest):
'ignored',
'Dockerfile',
'.dockerignore',
' ignored-with-spaces ', # check that spaces are trimmed
'!ignored/subdir/excepted-file',
'! ignored/subdir/excepted-with-spaces '
'', # empty line,
'#*', # comment line
]))
@ -113,9 +114,6 @@ class BuildTest(BaseAPIIntegrationTest):
with open(os.path.join(base_dir, '#file.txt'), 'w') as f:
f.write('this file should not be ignored')
with open(os.path.join(base_dir, 'ignored-with-spaces'), 'w') as f:
f.write("this file should be ignored")
subdir = os.path.join(base_dir, 'ignored', 'subdir')
os.makedirs(subdir)
with open(os.path.join(subdir, 'file'), 'w') as f:
@ -124,15 +122,12 @@ class BuildTest(BaseAPIIntegrationTest):
with open(os.path.join(subdir, 'excepted-file'), 'w') as f:
f.write("this file should not be ignored")
with open(os.path.join(subdir, 'excepted-with-spaces'), 'w') as f:
f.write("this file should not be ignored")
tag = 'docker-py-test-build-with-dockerignore'
stream = self.client.build(
path=base_dir,
tag=tag,
)
for _chunk in stream:
for chunk in stream:
pass
c = self.client.create_container(tag, ['find', '/test', '-type', 'f'])
@ -140,11 +135,11 @@ class BuildTest(BaseAPIIntegrationTest):
self.client.wait(c)
logs = self.client.logs(c)
logs = logs.decode('utf-8')
if six.PY3:
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/ignored/subdir/excepted-with-spaces',
'/test/ignored/subdir/excepted-file',
'/test/not-ignored'
])
@ -160,7 +155,7 @@ class BuildTest(BaseAPIIntegrationTest):
fileobj=script, tag='buildargs', buildargs={'test': 'OK'}
)
self.tmp_imgs.append('buildargs')
for _chunk in stream:
for chunk in stream:
pass
info = self.client.inspect_image('buildargs')
@ -180,7 +175,7 @@ class BuildTest(BaseAPIIntegrationTest):
fileobj=script, tag=tag, shmsize=shmsize
)
self.tmp_imgs.append(tag)
for _chunk in stream:
for chunk in stream:
pass
# There is currently no way to get the shmsize
@ -198,7 +193,7 @@ class BuildTest(BaseAPIIntegrationTest):
isolation='default'
)
for _chunk in stream:
for chunk in stream:
pass
@requires_api_version('1.23')
@ -213,7 +208,7 @@ class BuildTest(BaseAPIIntegrationTest):
fileobj=script, tag='labels', labels=labels
)
self.tmp_imgs.append('labels')
for _chunk in stream:
for chunk in stream:
pass
info = self.client.inspect_image('labels')
@ -230,7 +225,7 @@ class BuildTest(BaseAPIIntegrationTest):
stream = self.client.build(fileobj=script, tag='build1')
self.tmp_imgs.append('build1')
for _chunk in stream:
for chunk in stream:
pass
stream = self.client.build(
@ -271,11 +266,11 @@ class BuildTest(BaseAPIIntegrationTest):
fileobj=script, target='first', tag='build1'
)
self.tmp_imgs.append('build1')
for _chunk in stream:
for chunk in stream:
pass
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')
def test_build_with_network_mode(self):
@ -300,7 +295,7 @@ class BuildTest(BaseAPIIntegrationTest):
)
self.tmp_imgs.append('dockerpytest_customnetbuild')
for _chunk in stream:
for chunk in stream:
pass
assert self.client.inspect_image('dockerpytest_customnetbuild')
@ -312,7 +307,7 @@ class BuildTest(BaseAPIIntegrationTest):
)
self.tmp_imgs.append('dockerpytest_nonebuild')
logs = list(stream)
logs = [chunk for chunk in stream]
assert 'errorDetail' in logs[-1]
assert logs[-1]['errorDetail']['code'] == 1
@ -345,7 +340,8 @@ class BuildTest(BaseAPIIntegrationTest):
assert self.client.inspect_image(img_name)
ctnr = self.run_container(img_name, 'cat /hosts-file')
logs = self.client.logs(ctnr)
logs = logs.decode('utf-8')
if six.PY3:
logs = logs.decode('utf-8')
assert '127.0.0.1\textrahost.local.test' in logs
assert '127.0.0.1\thello.world.test' in logs
@ -365,7 +361,7 @@ class BuildTest(BaseAPIIntegrationTest):
fileobj=script, tag=tag, squash=squash
)
self.tmp_imgs.append(tag)
for _chunk in stream:
for chunk in stream:
pass
return self.client.inspect_image(tag)
@ -380,7 +376,7 @@ class BuildTest(BaseAPIIntegrationTest):
snippet = 'Ancient Temple (Mystic Oriental Dream ~ Ancient Temple)'
script = io.BytesIO(b'\n'.join([
b'FROM busybox',
f'RUN sh -c ">&2 echo \'{snippet}\'"'.encode('utf-8')
'RUN sh -c ">&2 echo \'{0}\'"'.format(snippet).encode('utf-8')
]))
stream = self.client.build(
@ -389,8 +385,10 @@ class BuildTest(BaseAPIIntegrationTest):
lines = []
for chunk in stream:
lines.append(chunk.get('stream'))
expected = f'{control_chars[0]}{snippet}\n{control_chars[1]}'
assert any(line == expected for line in lines)
expected = '{0}{2}\n{1}'.format(
control_chars[0], control_chars[1], snippet
)
assert any([line == expected for line in lines])
def test_build_gzip_encoding(self):
base_dir = tempfile.mkdtemp()
@ -442,7 +440,7 @@ class BuildTest(BaseAPIIntegrationTest):
@requires_api_version('1.32')
@requires_experimental(until=None)
def test_build_invalid_platform(self):
script = io.BytesIO(b'FROM busybox\n')
script = io.BytesIO('FROM busybox\n'.encode('ascii'))
with pytest.raises(errors.APIError) as excinfo:
stream = self.client.build(fileobj=script, platform='foobar')

View File

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

View File

@ -1,6 +1,7 @@
import pytest
# -*- coding: utf-8 -*-
import docker
import pytest
from ..helpers import force_leave_swarm, requires_api_version
from .base import BaseAPIIntegrationTest
@ -30,7 +31,7 @@ class ConfigAPITest(BaseAPIIntegrationTest):
def test_create_config_unicode_data(self):
config_id = self.client.create_config(
'favorite_character', 'いざよいさくや'
'favorite_character', u'いざよいさくや'
)
self.tmp_configs.append(config_id)
assert 'ID' in config_id
@ -69,16 +70,3 @@ class ConfigAPITest(BaseAPIIntegrationTest):
data = self.client.configs(filters={'name': ['favorite_character']})
assert len(data) == 1
assert data[0]['ID'] == config_id['ID']
@requires_api_version('1.37')
def test_create_config_with_templating(self):
config_id = self.client.create_config(
'favorite_character', 'sakuya izayoi',
templating={'name': 'golang'}
)
self.tmp_configs.append(config_id)
assert 'ID' in config_id
data = self.client.inspect_config(config_id)
assert data['Spec']['Name'] == 'favorite_character'
assert 'Templating' in data['Spec']
assert data['Spec']['Templating']['Name'] == 'golang'

View File

@ -7,19 +7,18 @@ from datetime import datetime
import pytest
import requests
import six
import docker
from docker.constants import IS_WINDOWS_PLATFORM
from docker.utils.socket import next_frame_header, read_exactly
from .. import helpers
from ..helpers import (
assert_cat_socket_detached_with_keys,
ctrl_with,
requires_api_version,
skip_if_desktop,
)
from .base import TEST_IMG, BaseAPIIntegrationTest
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.constants import IS_WINDOWS_PLATFORM
from docker.utils.socket import next_frame_header
from docker.utils.socket import read_exactly
class ListContainersTest(BaseAPIIntegrationTest):
@ -36,7 +35,7 @@ class ListContainersTest(BaseAPIIntegrationTest):
assert len(retrieved) == 1
retrieved = retrieved[0]
assert 'Command' in retrieved
assert retrieved['Command'] == 'true'
assert retrieved['Command'] == six.text_type('true')
assert 'Image' in retrieved
assert re.search(r'alpine:.*', retrieved['Image'])
assert 'Status' in retrieved
@ -105,11 +104,13 @@ class CreateContainerTest(BaseAPIIntegrationTest):
self.client.start(container3_id)
assert self.client.wait(container3_id)['StatusCode'] == 0
logs = self.client.logs(container3_id).decode('utf-8')
assert f'{link_env_prefix1}_NAME=' in logs
assert f'{link_env_prefix1}_ENV_FOO=1' in logs
assert f'{link_env_prefix2}_NAME=' in logs
assert f'{link_env_prefix2}_ENV_FOO=1' in logs
logs = self.client.logs(container3_id)
if six.PY3:
logs = logs.decode('utf-8')
assert '{0}_NAME='.format(link_env_prefix1) in logs
assert '{0}_ENV_FOO=1'.format(link_env_prefix1) in logs
assert '{0}_NAME='.format(link_env_prefix2) in logs
assert '{0}_ENV_FOO=1'.format(link_env_prefix2) in logs
def test_create_with_restart_policy(self):
container = self.client.create_container(
@ -124,8 +125,8 @@ class CreateContainerTest(BaseAPIIntegrationTest):
self.client.wait(id)
with pytest.raises(docker.errors.APIError) as exc:
self.client.remove_container(id)
err = exc.value.explanation.lower()
assert 'stop the container before' in err
err = exc.value.explanation
assert 'You cannot remove ' in err
self.client.remove_container(id, force=True)
def test_create_container_with_volumes_from(self):
@ -217,20 +218,6 @@ class CreateContainerTest(BaseAPIIntegrationTest):
self.client.kill(id)
@requires_api_version('1.41')
def test_create_with_cgroupns(self):
host_config = self.client.create_host_config(cgroupns='private')
container = self.client.create_container(
image=TEST_IMG,
command=['sleep', '60'],
host_config=host_config,
)
self.tmp_containers.append(container)
res = self.client.inspect_container(container)
assert 'private' == res['HostConfig']['CgroupnsMode']
def test_group_id_ints(self):
container = self.client.create_container(
TEST_IMG, 'id -G',
@ -240,7 +227,9 @@ class CreateContainerTest(BaseAPIIntegrationTest):
self.client.start(container)
self.client.wait(container)
logs = self.client.logs(container).decode('utf-8')
logs = self.client.logs(container)
if six.PY3:
logs = logs.decode('utf-8')
groups = logs.strip().split(' ')
assert '1000' in groups
assert '1001' in groups
@ -255,7 +244,9 @@ class CreateContainerTest(BaseAPIIntegrationTest):
self.client.start(container)
self.client.wait(container)
logs = self.client.logs(container).decode('utf-8')
logs = self.client.logs(container)
if six.PY3:
logs = logs.decode('utf-8')
groups = logs.strip().split(' ')
assert '1000' in groups
@ -476,13 +467,16 @@ class CreateContainerTest(BaseAPIIntegrationTest):
def test_create_with_device_cgroup_rules(self):
rule = 'c 7:128 rwm'
ctnr = self.client.create_container(
TEST_IMG, 'true', host_config=self.client.create_host_config(
TEST_IMG, 'cat /sys/fs/cgroup/devices/devices.list',
host_config=self.client.create_host_config(
device_cgroup_rules=[rule]
)
)
self.tmp_containers.append(ctnr)
config = self.client.inspect_container(ctnr)
assert config['HostConfig']['DeviceCgroupRules'] == [rule]
self.client.start(ctnr)
assert rule in self.client.logs(ctnr).decode('utf-8')
def test_create_with_uts_mode(self):
container = self.client.create_container(
@ -500,7 +494,7 @@ class CreateContainerTest(BaseAPIIntegrationTest):
)
class VolumeBindTest(BaseAPIIntegrationTest):
def setUp(self):
super().setUp()
super(VolumeBindTest, self).setUp()
self.mount_dest = '/mnt'
@ -521,7 +515,10 @@ class VolumeBindTest(BaseAPIIntegrationTest):
TEST_IMG,
['ls', self.mount_dest],
)
logs = self.client.logs(container).decode('utf-8')
logs = self.client.logs(container)
if six.PY3:
logs = logs.decode('utf-8')
assert self.filename in logs
inspect_data = self.client.inspect_container(container)
self.check_container_data(inspect_data, True)
@ -537,34 +534,15 @@ class VolumeBindTest(BaseAPIIntegrationTest):
TEST_IMG,
['ls', self.mount_dest],
)
logs = self.client.logs(container).decode('utf-8')
logs = self.client.logs(container)
if six.PY3:
logs = logs.decode('utf-8')
assert self.filename in logs
inspect_data = self.client.inspect_container(container)
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')
def test_create_with_mounts(self):
mount = docker.types.Mount(
@ -576,7 +554,9 @@ class VolumeBindTest(BaseAPIIntegrationTest):
host_config=host_config
)
assert container
logs = self.client.logs(container).decode('utf-8')
logs = self.client.logs(container)
if six.PY3:
logs = logs.decode('utf-8')
assert self.filename in logs
inspect_data = self.client.inspect_container(container)
self.check_container_data(inspect_data, True)
@ -593,7 +573,9 @@ class VolumeBindTest(BaseAPIIntegrationTest):
host_config=host_config
)
assert container
logs = self.client.logs(container).decode('utf-8')
logs = self.client.logs(container)
if six.PY3:
logs = logs.decode('utf-8')
assert self.filename in logs
inspect_data = self.client.inspect_container(container)
self.check_container_data(inspect_data, False)
@ -620,57 +602,7 @@ class VolumeBindTest(BaseAPIIntegrationTest):
assert mount['Source'] == mount_data['Name']
assert mount_data['RW'] is True
@requires_api_version('1.45')
def test_create_with_subpath_volume_mount(self):
source_volume = helpers.random_name()
self.client.create_volume(name=source_volume)
setup_container = None
test_container = None
# Create a file structure in the volume to test with
setup_container = self.client.create_container(
TEST_IMG,
[
"sh",
"-c",
'mkdir -p /vol/subdir && echo "test content" > /vol/subdir/testfile.txt',
],
host_config=self.client.create_host_config(
binds=[f"{source_volume}:/vol"]
),
)
self.client.start(setup_container)
self.client.wait(setup_container)
# Now test with subpath
mount = docker.types.Mount(
type="volume",
source=source_volume,
target=self.mount_dest,
read_only=True,
subpath="subdir",
)
host_config = self.client.create_host_config(mounts=[mount])
test_container = self.client.create_container(
TEST_IMG,
["cat", os.path.join(self.mount_dest, "testfile.txt")],
host_config=host_config,
)
self.client.start(test_container)
self.client.wait(test_container) # Wait for container to finish
output = self.client.logs(test_container).decode("utf-8").strip()
# If the subpath feature is working, we should be able to see the content
# of the file in the subdir
assert output == "test content"
def check_container_data(self, inspect_data, rw, propagation='rprivate'):
def check_container_data(self, inspect_data, rw):
assert 'Mounts' in inspect_data
filtered = list(filter(
lambda x: x['Destination'] == self.mount_dest,
@ -680,7 +612,6 @@ class VolumeBindTest(BaseAPIIntegrationTest):
mount_data = filtered[0]
assert mount_data['Source'] == self.mount_origin
assert mount_data['RW'] == rw
assert mount_data['Propagation'] == propagation
def run_with_volume(self, ro, *args, **kwargs):
return self.run_container(
@ -698,29 +629,12 @@ class VolumeBindTest(BaseAPIIntegrationTest):
**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):
def test_get_file_archive_from_container(self):
data = 'The Maid and the Pocket Watch of Blood'
ctnr = self.client.create_container(
TEST_IMG, f'sh -c "echo {data} > /vol1/data.txt"',
TEST_IMG, 'sh -c "echo {0} > /vol1/data.txt"'.format(data),
volumes=['/vol1']
)
self.tmp_containers.append(ctnr)
@ -731,14 +645,15 @@ class ArchiveTest(BaseAPIIntegrationTest):
for d in strm:
destination.write(d)
destination.seek(0)
retrieved_data = helpers.untar_file(destination, 'data.txt')\
.decode('utf-8')
retrieved_data = helpers.untar_file(destination, 'data.txt')
if six.PY3:
retrieved_data = retrieved_data.decode('utf-8')
assert data == retrieved_data.strip()
def test_get_file_stat_from_container(self):
data = 'The Maid and the Pocket Watch of Blood'
ctnr = self.client.create_container(
TEST_IMG, f'sh -c "echo -n {data} > /vol1/data.txt"',
TEST_IMG, 'sh -c "echo -n {0} > /vol1/data.txt"'.format(data),
volumes=['/vol1']
)
self.tmp_containers.append(ctnr)
@ -757,7 +672,9 @@ class ArchiveTest(BaseAPIIntegrationTest):
test_file.seek(0)
ctnr = self.client.create_container(
TEST_IMG,
f"cat {os.path.join('/vol1/', os.path.basename(test_file.name))}",
'cat {0}'.format(
os.path.join('/vol1/', os.path.basename(test_file.name))
),
volumes=['/vol1']
)
self.tmp_containers.append(ctnr)
@ -766,6 +683,9 @@ class ArchiveTest(BaseAPIIntegrationTest):
self.client.start(ctnr)
self.client.wait(ctnr)
logs = self.client.logs(ctnr)
if six.PY3:
logs = logs.decode('utf-8')
data = data.decode('utf-8')
assert logs.strip() == data
def test_copy_directory_to_container(self):
@ -780,7 +700,9 @@ class ArchiveTest(BaseAPIIntegrationTest):
self.client.put_archive(ctnr, '/vol1', test_tar)
self.client.start(ctnr)
self.client.wait(ctnr)
logs = self.client.logs(ctnr).decode('utf-8')
logs = self.client.logs(ctnr)
if six.PY3:
logs = logs.decode('utf-8')
results = logs.strip().split()
assert 'a.py' in results
assert 'b.py' in results
@ -801,7 +723,7 @@ class RenameContainerTest(BaseAPIIntegrationTest):
if version == '1.5.0':
assert name == inspect['Name']
else:
assert f'/{name}' == inspect['Name']
assert '/{0}'.format(name) == inspect['Name']
class StartContainerTest(BaseAPIIntegrationTest):
@ -907,7 +829,7 @@ class LogsTest(BaseAPIIntegrationTest):
def test_logs(self):
snippet = 'Flowering Nights (Sakuya Iyazoi)'
container = self.client.create_container(
TEST_IMG, f'echo {snippet}'
TEST_IMG, 'echo {0}'.format(snippet)
)
id = container['Id']
self.tmp_containers.append(id)
@ -915,13 +837,13 @@ class LogsTest(BaseAPIIntegrationTest):
exitcode = self.client.wait(id)['StatusCode']
assert exitcode == 0
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):
snippet = '''Line1
Line2'''
container = self.client.create_container(
TEST_IMG, f'echo "{snippet}"'
TEST_IMG, 'echo "{0}"'.format(snippet)
)
id = container['Id']
self.tmp_containers.append(id)
@ -934,19 +856,19 @@ Line2'''
def test_logs_streaming_and_follow(self):
snippet = 'Flowering Nights (Sakuya Iyazoi)'
container = self.client.create_container(
TEST_IMG, f'echo {snippet}'
TEST_IMG, 'echo {0}'.format(snippet)
)
id = container['Id']
self.tmp_containers.append(id)
self.client.start(id)
logs = b''
logs = six.binary_type()
for chunk in self.client.logs(id, stream=True, follow=True):
logs += chunk
exitcode = self.client.wait(id)['StatusCode']
assert exitcode == 0
assert logs == f"{snippet}\n".encode(encoding='ascii')
assert logs == (snippet + '\n').encode(encoding='ascii')
@pytest.mark.timeout(5)
@pytest.mark.skipif(os.environ.get('DOCKER_HOST', '').startswith('ssh://'),
@ -954,12 +876,12 @@ Line2'''
def test_logs_streaming_and_follow_and_cancel(self):
snippet = 'Flowering Nights (Sakuya Iyazoi)'
container = self.client.create_container(
TEST_IMG, f'sh -c "echo \\"{snippet}\\" && sleep 3"'
TEST_IMG, 'sh -c "echo \\"{0}\\" && sleep 3"'.format(snippet)
)
id = container['Id']
self.tmp_containers.append(id)
self.client.start(id)
logs = b''
logs = six.binary_type()
generator = self.client.logs(id, stream=True, follow=True)
threading.Timer(1, generator.close).start()
@ -967,12 +889,12 @@ Line2'''
for chunk in generator:
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):
snippet = 'Flowering Nights (Sakuya Iyazoi)'
container = self.client.create_container(
TEST_IMG, f'echo {snippet}'
TEST_IMG, 'echo {0}'.format(snippet)
)
id = container['Id']
self.tmp_containers.append(id)
@ -980,12 +902,12 @@ Line2'''
exitcode = self.client.wait(id)['StatusCode']
assert exitcode == 0
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):
snippet = 'Flowering Nights (Sakuya Iyazoi)'
container = self.client.create_container(
TEST_IMG, f'echo "{snippet}"'
TEST_IMG, 'echo "{0}"'.format(snippet)
)
id = container['Id']
self.tmp_containers.append(id)
@ -999,7 +921,7 @@ Line2'''
def test_logs_with_until(self):
snippet = 'Shanghai Teahouse (Hong Meiling)'
container = self.client.create_container(
TEST_IMG, f'echo "{snippet}"'
TEST_IMG, 'echo "{0}"'.format(snippet)
)
self.tmp_containers.append(container)
@ -1009,7 +931,7 @@ Line2'''
logs_until_1 = self.client.logs(container, until=1)
assert logs_until_1 == b''
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):
@ -1175,7 +1097,7 @@ class PortTest(BaseAPIIntegrationTest):
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 host_port == port_bindings[port_binding][1]
@ -1195,7 +1117,7 @@ class ContainerTopTest(BaseAPIIntegrationTest):
self.client.start(container)
res = self.client.top(container)
if not IS_WINDOWS_PLATFORM:
assert res['Titles'] == ['PID', 'USER', 'TIME', 'COMMAND']
assert res['Titles'] == [u'PID', u'USER', u'TIME', u'COMMAND']
assert len(res['Processes']) == 1
assert res['Processes'][0][-1] == 'sleep 60'
self.client.kill(container)
@ -1213,7 +1135,7 @@ class ContainerTopTest(BaseAPIIntegrationTest):
self.client.start(container)
res = self.client.top(container, '-eopid,user')
assert res['Titles'] == ['PID', 'USER']
assert res['Titles'] == [u'PID', u'USER']
assert len(res['Processes']) == 1
assert res['Processes'][0][10] == 'sleep 60'
@ -1300,10 +1222,10 @@ class AttachContainerTest(BaseAPIIntegrationTest):
sock = self.client.attach_socket(container, ws=False)
assert sock.fileno() > -1
def test_run_container_reading_socket_http(self):
def test_run_container_reading_socket(self):
line = 'hi there and stuff and things, words!'
# `echo` appends CRLF, `printf` doesn't
command = f"printf '{line}'"
command = "printf '{0}'".format(line)
container = self.client.create_container(TEST_IMG, command,
detach=True, tty=False)
self.tmp_containers.append(container)
@ -1320,33 +1242,12 @@ class AttachContainerTest(BaseAPIIntegrationTest):
data = read_exactly(pty_stdout, next_size)
assert data.decode('utf-8') == line
@pytest.mark.xfail(condition=bool(os.environ.get('DOCKER_CERT_PATH', '')),
reason='DOCKER_CERT_PATH not respected for websockets')
def test_run_container_reading_socket_ws(self):
line = 'hi there and stuff and things, words!'
# `echo` appends CRLF, `printf` doesn't
command = f"printf '{line}'"
container = self.client.create_container(TEST_IMG, command,
detach=True, tty=False)
self.tmp_containers.append(container)
opts = {"stdout": 1, "stream": 1, "logs": 1}
pty_stdout = self.client.attach_socket(container, opts, ws=True)
self.addCleanup(pty_stdout.close)
self.client.start(container)
data = pty_stdout.recv()
assert data.decode('utf-8') == line
@pytest.mark.timeout(10)
def test_attach_no_stream(self):
container = self.client.create_container(
TEST_IMG, 'echo hello'
)
self.tmp_containers.append(container)
self.client.start(container)
self.client.wait(container, condition='not-running')
output = self.client.attach(container, stream=False, logs=True)
assert output == 'hello\n'.encode(encoding='ascii')
@ -1481,7 +1382,7 @@ class GetContainerStatsTest(BaseAPIIntegrationTest):
response = self.client.stats(container, stream=0)
self.client.kill(container)
assert isinstance(response, dict)
assert type(response) == dict
for key in ['read', 'networks', 'precpu_stats', 'cpu_stats',
'memory_stats', 'blkio_stats']:
assert key in response
@ -1494,7 +1395,7 @@ class GetContainerStatsTest(BaseAPIIntegrationTest):
self.client.start(container)
stream = self.client.stats(container)
for chunk in stream:
assert isinstance(chunk, dict)
assert type(chunk) == dict
for key in ['read', 'network', 'precpu_stats', 'cpu_stats',
'memory_stats', 'blkio_stats']:
assert key in chunk
@ -1608,7 +1509,7 @@ class LinkTest(BaseAPIIntegrationTest):
# Remove link
linked_name = self.client.inspect_container(container2_id)['Name'][1:]
link_name = f'{linked_name}/{link_alias}'
link_name = '%s/%s' % (linked_name, link_alias)
self.client.remove_container(link_name, link=True)
# Link is gone

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